instar 1.3.269 → 1.3.271
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 +73 -11
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +24 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/coordination/MandateAudit.d.ts +48 -0
- package/dist/coordination/MandateAudit.d.ts.map +1 -0
- package/dist/coordination/MandateAudit.js +95 -0
- package/dist/coordination/MandateAudit.js.map +1 -0
- package/dist/coordination/MandateGate.d.ts +48 -0
- package/dist/coordination/MandateGate.d.ts.map +1 -0
- package/dist/coordination/MandateGate.js +104 -0
- package/dist/coordination/MandateGate.js.map +1 -0
- package/dist/coordination/MandateStore.d.ts +60 -0
- package/dist/coordination/MandateStore.d.ts.map +1 -0
- package/dist/coordination/MandateStore.js +106 -0
- package/dist/coordination/MandateStore.js.map +1 -0
- package/dist/coordination/ReviewExchange.d.ts +135 -0
- package/dist/coordination/ReviewExchange.d.ts.map +1 -0
- package/dist/coordination/ReviewExchange.js +192 -0
- package/dist/coordination/ReviewExchange.js.map +1 -0
- package/dist/coordination/conditions.d.ts +32 -0
- package/dist/coordination/conditions.d.ts.map +1 -0
- package/dist/coordination/conditions.js +54 -0
- package/dist/coordination/conditions.js.map +1 -0
- package/dist/coordination/types.d.ts +74 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +17 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/core/ApprovalLedger.d.ts +132 -0
- package/dist/core/ApprovalLedger.d.ts.map +1 -0
- package/dist/core/ApprovalLedger.js +191 -0
- package/dist/core/ApprovalLedger.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +33 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SecretStore.d.ts.map +1 -1
- package/dist/core/SecretStore.js +10 -1
- package/dist/core/SecretStore.js.map +1 -1
- package/dist/core/SessionBuildContextStore.d.ts +33 -0
- package/dist/core/SessionBuildContextStore.d.ts.map +1 -0
- package/dist/core/SessionBuildContextStore.js +107 -0
- package/dist/core/SessionBuildContextStore.js.map +1 -0
- package/dist/core/SessionManager.d.ts +4 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +44 -3
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/TransferByNickname.d.ts +6 -0
- package/dist/core/TransferByNickname.d.ts.map +1 -1
- package/dist/core/TransferByNickname.js +18 -6
- package/dist/core/TransferByNickname.js.map +1 -1
- package/dist/core/seamlessnessConfig.d.ts.map +1 -1
- package/dist/core/seamlessnessConfig.js +12 -1
- package/dist/core/seamlessnessConfig.js.map +1 -1
- package/dist/core/types.d.ts +34 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/feedback-factory/migration/immutableGuard.d.ts +87 -0
- package/dist/feedback-factory/migration/immutableGuard.d.ts.map +1 -0
- package/dist/feedback-factory/migration/immutableGuard.js +163 -0
- package/dist/feedback-factory/migration/immutableGuard.js.map +1 -0
- package/dist/feedback-factory/migration/importIntegrity.d.ts +112 -0
- package/dist/feedback-factory/migration/importIntegrity.d.ts.map +1 -0
- package/dist/feedback-factory/migration/importIntegrity.js +202 -0
- package/dist/feedback-factory/migration/importIntegrity.js.map +1 -0
- package/dist/feedback-factory/monitor/parityMonitor.d.ts +75 -0
- package/dist/feedback-factory/monitor/parityMonitor.d.ts.map +1 -0
- package/dist/feedback-factory/monitor/parityMonitor.js +98 -0
- package/dist/feedback-factory/monitor/parityMonitor.js.map +1 -0
- package/dist/feedback-factory/monitor/parityMonitorStore.d.ts +44 -0
- package/dist/feedback-factory/monitor/parityMonitorStore.d.ts.map +1 -0
- package/dist/feedback-factory/monitor/parityMonitorStore.js +80 -0
- package/dist/feedback-factory/monitor/parityMonitorStore.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +19 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/MentorAutonomousGuardian.d.ts.map +1 -1
- package/dist/scheduler/MentorAutonomousGuardian.js +1 -0
- package/dist/scheduler/MentorAutonomousGuardian.js.map +1 -1
- package/dist/server/AgentServer.d.ts +11 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +102 -1
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +45 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +19 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +482 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates.ts +19 -1
- package/upgrades/1.3.270.md +106 -0
- package/upgrades/1.3.271.md +154 -0
- package/upgrades/approval-ledger-phase2.eli16.md +56 -0
- package/upgrades/autoloop-gate-compliance.eli16.md +11 -0
- package/upgrades/feedback-phase3-monitor.eli16.md +51 -0
- package/upgrades/feedback-phase4-integrity.eli16.md +54 -0
- package/upgrades/respawn-build-context-reestablishment.eli16.md +5 -0
- package/upgrades/side-effects/approval-ledger-phase2.md +82 -0
- package/upgrades/side-effects/autoloop-gate-compliance.md +43 -0
- package/upgrades/side-effects/coordination-mandate-enforcement.md +86 -0
- package/upgrades/side-effects/dashboard-pool-sessions.md +103 -0
- package/upgrades/side-effects/feedback-phase3-monitor.md +37 -0
- package/upgrades/side-effects/feedback-phase4-integrity.md +35 -0
- package/upgrades/side-effects/respawn-build-context-reestablishment.md +81 -0
- package/upgrades/side-effects/review-exchange-protocol.md +60 -0
- package/upgrades/side-effects/secret-drop-store-first.md +117 -0
- package/upgrades/side-effects/transfer-idempotency-exactly-once.md +105 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Side-effects review — Coordination Mandate enforcement (spec §4, G2.2)
|
|
2
|
+
|
|
3
|
+
Spec: `docs/specs/coordination-mandate.md` (approved by Justin, A/A/B, 2026-06-05).
|
|
4
|
+
Change: new `src/coordination/` module (types, MandateStore, MandateGate, MandateAudit,
|
|
5
|
+
ConditionsRegistry) + six `/mandate*` routes wired through `AgentServer` → `RouteContext`.
|
|
6
|
+
|
|
7
|
+
## 1. Blast radius
|
|
8
|
+
Additive only. No existing route, gate, sentinel, or store is modified. The engine is
|
|
9
|
+
**deny-by-default and inert**: with no mandate issued (the universal state at deploy),
|
|
10
|
+
every `/mandate/evaluate` call denies — so shipping this changes NO behavior anywhere
|
|
11
|
+
until the operator issues the first mandate through the PIN-gated route. It never
|
|
12
|
+
weakens an existing safety check; it adds a controlled delegation surface beside them.
|
|
13
|
+
|
|
14
|
+
## 2. State / data
|
|
15
|
+
Two new files under the `.instar/state/` convention, created on first use:
|
|
16
|
+
`coordination-mandates.json` (the signed mandates) and `mandate-audit.jsonl`
|
|
17
|
+
(append-only, hash-chained). No schema migration; absent files mean an empty store and
|
|
18
|
+
an empty audit. Torn-trailing-line tolerant reads.
|
|
19
|
+
|
|
20
|
+
## 3. Security model (the load-bearing dimension)
|
|
21
|
+
- **requester ≠ authorizer preserved:** the human-authored mandate is the authorizer.
|
|
22
|
+
The agent's Bearer token CANNOT issue or revoke — `/mandate/issue` and
|
|
23
|
+
`/mandate/:id/revoke` require the operator's dashboard PIN (sha256 + timingSafeEqual +
|
|
24
|
+
per-IP attempt limiting, mirroring `/dashboard/unlock`). Tested at all three tiers.
|
|
25
|
+
- **Authorship proof (T1/T2):** HMAC over the canonical authored bytes (key-order-stable
|
|
26
|
+
serialization), produced only at issuance. A forged or widened mandate fails
|
|
27
|
+
`verifyAuthorship` → the gate denies. `revoked` is excluded from the proof so the
|
|
28
|
+
kill switch works post-issuance; revocation is checked on every evaluation (T5).
|
|
29
|
+
- **Conditions (T7/T10):** objective resolvers from real state; an unregistered or
|
|
30
|
+
throwing resolver evaluates FALSE (deny-safe). The first mandate (per decision 3B)
|
|
31
|
+
carries no conditioned authority; `integrity-gate-pass` and `parity-zero-divergence`
|
|
32
|
+
are registered deny-safe stubs until the real state is wired for a future cutover
|
|
33
|
+
authority — which per decision 1A is not delegated anyway.
|
|
34
|
+
- **Audit (T3/T8):** every decision (allow AND deny) appends a hash-chained entry;
|
|
35
|
+
`verifyChain()` detects any edit/deletion and is surfaced on `GET /mandate/audit`.
|
|
36
|
+
- **Stated trust boundary (T12):** an attacker with local disk write access could edit
|
|
37
|
+
server-managed state — the same trust root as today; the proof stops forged AUTHORED
|
|
38
|
+
content, and the audit chain makes tamper detectable. Out of scope per the spec.
|
|
39
|
+
|
|
40
|
+
## 4. Failure modes
|
|
41
|
+
- No `stateDir` → engine is null → all `/mandate*` routes 503 (tested).
|
|
42
|
+
- Engine init failure → own try/catch, never cascades to other inits.
|
|
43
|
+
- Malformed issuance body → 400 with a specific reason (agents pair, non-empty
|
|
44
|
+
authorities, future expiry — each tested both sides).
|
|
45
|
+
- No `dashboardPin` configured → issuance/revocation 503 (PIN auth unavailable) — the
|
|
46
|
+
gate cannot be bypassed by the PIN simply being absent.
|
|
47
|
+
|
|
48
|
+
## 5. Interactions
|
|
49
|
+
- `RouteContext` gains a required `coordination` field; the only production assembler
|
|
50
|
+
(`AgentServer`) is updated. Route tests use partial `ctx: any` → unaffected (tsc green).
|
|
51
|
+
- Express route-order hazard handled: `/mandate/audit` and `/mandate/evaluate` are
|
|
52
|
+
registered BEFORE `/mandate/:id` (tested explicitly).
|
|
53
|
+
- No locks, queues, LLM calls, or budgets shared with other components.
|
|
54
|
+
|
|
55
|
+
## 6. Performance
|
|
56
|
+
O(mandates) reads of a small JSON file per evaluation + one appendFileSync per decision.
|
|
57
|
+
Evaluation volume is a handful of A2A actions per migration step — negligible.
|
|
58
|
+
|
|
59
|
+
## 7. Migration parity
|
|
60
|
+
No `.claude/settings.json` hook, config default, or hook-script change → no
|
|
61
|
+
`PostUpdateMigrator` entry needed. Server-side code activates on the next server start
|
|
62
|
+
after update. Agent-Awareness satisfied: CLAUDE.md template gains a Coordination
|
|
63
|
+
Mandate blurb (`generateClaudeMd`). The feature is inert-by-default everywhere (deny
|
|
64
|
+
without a mandate), so fleet exposure is zero until an operator issues one.
|
|
65
|
+
|
|
66
|
+
## Tests
|
|
67
|
+
- Unit (`tests/unit/coordination-mandate.test.ts`, 17): authorship valid/forged/widened,
|
|
68
|
+
revoke-preserves-proof, canonical key-order stability, audit chain intact/tampered,
|
|
69
|
+
conditions (true/false/unknown/compound/throwing), the gate's FULL deny ladder
|
|
70
|
+
(missing, forged, expired, revoked, non-party, unlisted action, out-of-bounds params,
|
|
71
|
+
unmet condition) + both allow paths, bounds helper both sides.
|
|
72
|
+
- Integration (`tests/integration/mandate-routes.test.ts`, 9): PIN-less issue refused,
|
|
73
|
+
wrong PIN refused, issue/list/evaluate/revoke round-trip, deny-by-default audited,
|
|
74
|
+
route order, validation 400s, 503s.
|
|
75
|
+
- E2E (`tests/e2e/coordination-mandate-lifecycle.test.ts`, 5): real AgentServer boot,
|
|
76
|
+
alive + Bearer-gated, deny-by-default on fresh boot, full PIN-issue→allow→deny→
|
|
77
|
+
revoke→deny lifecycle with chain-verified audit, and the production-HMAC
|
|
78
|
+
wiring-integrity proof (persisted authProof verifies; a widened mandate fails).
|
|
79
|
+
|
|
80
|
+
## Post-review amendment (same PR, pre-merge)
|
|
81
|
+
|
|
82
|
+
- **Discoverability:** the `/mandate` prefix is classified in `CapabilityIndex.ts`
|
|
83
|
+
(CAPABILITY_INDEX → surfaced in `/capabilities` with all six endpoints + the
|
|
84
|
+
evaluate-before-acting + PIN-gating semantics), satisfying the
|
|
85
|
+
capabilities-discoverability gate. Agents discover the gate; they still cannot
|
|
86
|
+
issue through it.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Side-Effects Review — Dashboard pool-wide sessions (every session, every machine)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dashboard-pool-sessions`
|
|
4
|
+
**Date:** `2026-06-05`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (additive read-only aggregation + UI; no authority, no mutation; every boundary test-pinned across three tiers)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`GET /sessions` self-tags each session with `machineId`/`machineNickname` when
|
|
11
|
+
the pool is wired, and `GET /sessions?scope=pool` aggregates every reachable
|
|
12
|
+
peer's plain `/sessions` (new `resolvePeerUrls` route hook off the machine
|
|
13
|
+
identity registry), tagging remote sessions with their machine + `remote:true`.
|
|
14
|
+
The dashboard polls the aggregate every 15s and renders a machine badge on
|
|
15
|
+
every session row; remote rows are informational (no subscribe/close). CLAUDE.md
|
|
16
|
+
template + idempotent migration carry the agent awareness.
|
|
17
|
+
|
|
18
|
+
## Decision-point inventory
|
|
19
|
+
|
|
20
|
+
1. Pool wired vs not → self-tag vs omit fields. Both sides pinned (integration).
|
|
21
|
+
2. `scope=pool` vs plain → envelope vs back-compatible array. Both pinned
|
|
22
|
+
(integration + e2e).
|
|
23
|
+
3. Peer reachable vs dead → merged+tagged vs `pool.failed` entry. Both pinned
|
|
24
|
+
against a REAL second HTTP server / a dead port.
|
|
25
|
+
4. Dashboard row local vs remote → interactive vs informational (click, close,
|
|
26
|
+
active highlight, row key namespace). All pinned (unit).
|
|
27
|
+
5. Migration: section-present-without-marker / marker-present / no-section.
|
|
28
|
+
All three pinned.
|
|
29
|
+
|
|
30
|
+
## 1. Over-block
|
|
31
|
+
|
|
32
|
+
Nothing rejected. The plain route's shape and fields are unchanged for
|
|
33
|
+
existing callers (additive fields only). `scope=pool` is opt-in.
|
|
34
|
+
|
|
35
|
+
## 2. Under-block
|
|
36
|
+
|
|
37
|
+
- Remote sessions are view-only — no remote kill or terminal streaming (named
|
|
38
|
+
non-goals; the tooltip routes the user to the owning machine's dashboard).
|
|
39
|
+
- The aggregate trusts each peer's self-report; a peer that lies about its
|
|
40
|
+
sessions is already inside the trust boundary (same authToken).
|
|
41
|
+
- The 15s poll means a remote session can be up to ~15s stale — acceptable for
|
|
42
|
+
a visibility surface; local sessions remain real-time via the WebSocket.
|
|
43
|
+
|
|
44
|
+
## 3. Level-of-abstraction fit
|
|
45
|
+
|
|
46
|
+
Aggregation lives in the one route that owns session listing, reusing each
|
|
47
|
+
peer's EXISTING plain route (no new peer surface, no recursion — peers are
|
|
48
|
+
fetched without `scope`). Peer discovery (`resolvePeerUrls`) is wired in
|
|
49
|
+
`server.ts` from the same identity-registry source the lease/marker transports
|
|
50
|
+
already use. The dashboard keeps the WebSocket as the live local feed and
|
|
51
|
+
layers the poll on top — remote state is kept in a separate array precisely
|
|
52
|
+
because the WS replaces the local one each broadcast.
|
|
53
|
+
|
|
54
|
+
## 4. Signal vs authority compliance
|
|
55
|
+
|
|
56
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
57
|
+
|
|
58
|
+
No authority added anywhere: read-only aggregation, fail-soft per peer
|
|
59
|
+
(`failed` entries, never a 500), best-effort dashboard poll (a failed tick
|
|
60
|
+
retries next interval).
|
|
61
|
+
|
|
62
|
+
## 5. Interactions
|
|
63
|
+
|
|
64
|
+
- Cross-machine fetch uses the established Bearer-authToken pattern
|
|
65
|
+
(placement-proxy precedent) with a 5s `AbortSignal.timeout` so a hung peer
|
|
66
|
+
can't pin the route.
|
|
67
|
+
- No recursion: the aggregate always fetches peers' PLAIN `/sessions`.
|
|
68
|
+
- The WebSocket session feed is untouched; local rows merely inherit the self
|
|
69
|
+
nickname at render time from the poll response.
|
|
70
|
+
- tmux-name collisions across machines are handled by namespacing remote row
|
|
71
|
+
keys (`remote:<machineId>:<tmux>`).
|
|
72
|
+
|
|
73
|
+
## 6. External surfaces
|
|
74
|
+
|
|
75
|
+
One query param on an existing authed route; one optional RouteContext hook;
|
|
76
|
+
dashboard HTML/JS; CLAUDE.md template bullet + idempotent migration. No config,
|
|
77
|
+
no notifications, no new files on disk.
|
|
78
|
+
|
|
79
|
+
## 7. Rollback cost
|
|
80
|
+
|
|
81
|
+
Trivial — revert the PR. The plain route shape is unchanged, so nothing else
|
|
82
|
+
depends on the new fields; no state, no schema, no migration side effects
|
|
83
|
+
beyond an extra CLAUDE.md bullet (inert text).
|
|
84
|
+
|
|
85
|
+
## Conclusion
|
|
86
|
+
|
|
87
|
+
Additive, read-only, fail-soft visibility feature at the minimal scope that
|
|
88
|
+
satisfies the operator requirement verbatim: every session shows on the
|
|
89
|
+
dashboard, and every row states its machine.
|
|
90
|
+
|
|
91
|
+
## Second-pass review (if required)
|
|
92
|
+
|
|
93
|
+
Not required — see header.
|
|
94
|
+
|
|
95
|
+
## Evidence pointers
|
|
96
|
+
|
|
97
|
+
- `tests/unit/dashboard-sessionMachineBadge.test.ts` (8) — UI contract.
|
|
98
|
+
- `tests/unit/PostUpdateMigrator-poolSessionsVisibility.test.ts` (3) — parity.
|
|
99
|
+
- `tests/integration/sessions-pool-scope.test.ts` (5) — route contract incl. a
|
|
100
|
+
REAL peer server + dead-peer degradation.
|
|
101
|
+
- `tests/e2e/sessions-pool-scope-lifecycle.test.ts` (3) — feature-alive on the
|
|
102
|
+
production init path.
|
|
103
|
+
- `docs/specs/dashboard-pool-sessions.md` + `.eli16.md`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Side effects — Phase-3 parity monitor + cutover gate
|
|
2
|
+
|
|
3
|
+
## New files (additive only)
|
|
4
|
+
- `src/feedback-factory/monitor/parityMonitor.ts` — `ParityMonitor` (records passes, tracks
|
|
5
|
+
the zero-divergence streak, exposes the `gate()` cutover signal) + `DEFAULT_GATE_POLICY`.
|
|
6
|
+
- `tests/unit/feedback-factory/parity-monitor.test.ts` (9 tests).
|
|
7
|
+
|
|
8
|
+
## Runtime impact
|
|
9
|
+
- **None at boot / on the live server.** Not imported by `server.ts`, any route, job, or
|
|
10
|
+
hook. No new endpoint, no config key, no `PostUpdateMigrator` entry. No new dependency
|
|
11
|
+
(pure logic over the existing `ParityResult` type).
|
|
12
|
+
|
|
13
|
+
## Behavioral guarantees
|
|
14
|
+
- `gate()` clears ONLY when all four conditions hold: ≥ requiredCleanPasses consecutive clean
|
|
15
|
+
passes, clean window ≥ minWindowMs of real time, ≥ minClustersObserved clusters across the
|
|
16
|
+
streak, and zero divergence in the streak. Any divergent pass resets the streak.
|
|
17
|
+
- The monitor never mutates anything and never triggers cutover itself — it only computes a
|
|
18
|
+
cleared/blocked verdict that a downstream executor reads.
|
|
19
|
+
- Conservative failure mode: an over-strict policy keeps the gate BLOCKED longer (cutover
|
|
20
|
+
waits); it can never clear early.
|
|
21
|
+
|
|
22
|
+
## Reversibility
|
|
23
|
+
- Fully reversible: delete `monitor/parityMonitor.ts` + its test. Nothing references it yet.
|
|
24
|
+
|
|
25
|
+
## Follow-on wiring (not in this PR)
|
|
26
|
+
- The Coordination Mandate condition registry (G2.2) maps `parity-zero-divergence` →
|
|
27
|
+
`monitor.gate(now).cleared`. The Phase-4 cutover executor (G2.4) consults it before the flip.
|
|
28
|
+
- A durable wrapper (persist passes to JSONL so the window survives a restart) + a read-only
|
|
29
|
+
`/parity/status` route land with the cutover-executor wiring.
|
|
30
|
+
|
|
31
|
+
## Durability (added)
|
|
32
|
+
- `parityMonitorStore.ts` — `DurableParityMonitor` + `JsonlPassPersistence`. The zero-divergence
|
|
33
|
+
window spans HOURS; without persistence a restart silently resets the streak. Passes are
|
|
34
|
+
appended to an append-only JSONL and reloaded on construction, so the window survives a
|
|
35
|
+
restart. Torn final line (crash mid-append) is skipped on reload, never corrupting the prior
|
|
36
|
+
window. Injectable `PassPersistence` keeps the logic unit-testable without disk. Still not
|
|
37
|
+
wired into the running server (the cutover executor constructs it with the live path).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Side effects — Phase-4 feedback-migration integrity tooling
|
|
2
|
+
|
|
3
|
+
## New files (additive only)
|
|
4
|
+
- `src/feedback-factory/migration/immutableGuard.ts` — `GuardedFeedbackStore` decorator +
|
|
5
|
+
`isClusterImmutable` / `hasGovernanceNotes` invariants.
|
|
6
|
+
- `src/feedback-factory/migration/importIntegrity.ts` — pure import-gate core (checksums,
|
|
7
|
+
fingerprint-uniqueness scan, schema-equivalence, referential integrity, sequence reset,
|
|
8
|
+
`runIntegrityGate`).
|
|
9
|
+
- `tests/unit/feedback-factory/immutable-guard.test.ts` (15 tests).
|
|
10
|
+
- `tests/unit/feedback-factory/import-integrity.test.ts` (21 tests).
|
|
11
|
+
|
|
12
|
+
## Runtime impact
|
|
13
|
+
- **None at boot / on the live server.** These modules are NOT imported by `server.ts`, any
|
|
14
|
+
route, job, or hook. They are library building blocks the Phase-4 cutover execution will
|
|
15
|
+
call. No new endpoint, no new config key, no migration to `PostUpdateMigrator`.
|
|
16
|
+
- No new dependencies (uses `node:crypto` only).
|
|
17
|
+
|
|
18
|
+
## Behavioral guarantees
|
|
19
|
+
- `GuardedFeedbackStore` only ever REFUSES a mutation of an immutable cluster (pre-cutover
|
|
20
|
+
createdAt or non-null governance note) and records a `GuardViolation`; it never alters
|
|
21
|
+
data and passes all reads + non-curated writes straight through to the inner store.
|
|
22
|
+
- A failing `onViolation` audit sink can never break the store or surface as a mutation
|
|
23
|
+
(try/caught, annotated `@silent-fallback-ok`).
|
|
24
|
+
- Checksums collapse null/undefined/"" so the import gate does not flap on the
|
|
25
|
+
null-vs-empty governance-note distinction (that distinction is asserted separately by
|
|
26
|
+
schema-equivalence).
|
|
27
|
+
|
|
28
|
+
## Reversibility
|
|
29
|
+
- Fully reversible: delete the two `migration/` files + their tests. Nothing else
|
|
30
|
+
references them yet.
|
|
31
|
+
|
|
32
|
+
## Follow-on wiring (not in this PR)
|
|
33
|
+
- Phase-4 cutover executor (G2.4) constructs the `GuardedFeedbackStore` around the real
|
|
34
|
+
Prisma-backed store with the cutover timestamp from config, and runs `runIntegrityGate`
|
|
35
|
+
over the export/import pair as the Phase-2 gate.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Side-Effects Review — Respawn build-context re-establishment
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `respawn-build-context-reestablishment`
|
|
4
|
+
**Date:** `2026-06-04`
|
|
5
|
+
**Author:** `instar-codey`
|
|
6
|
+
**Second-pass reviewer:** `instar-codey second-pass checklist`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
This change hardens the interactive SessionManager respawn path for developer build sessions. A new sidecar store records each running session's live tmux pane working directory, using the generic state layer's temp-file-plus-rename atomic write. When a resumed interactive session has a fresh saved cwd under an agent worktree, SessionManager prepends a `[BUILD-CONTEXT RESTORE]` continuation note to the resumed message. Normal home-only sessions, stale records, missing worktrees, and non-enabled agents receive no note.
|
|
11
|
+
|
|
12
|
+
The feature is dark by default for normal agents. Server boot resolves the session-level feature flag as an explicit override if present, otherwise the standard development-agent gate.
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
- `SessionBuildContextStore` — add — owns sidecar state validation, crash-safe persistence through StateManager, cwd eligibility, staleness, missing-path checks, branch enrichment, and restore-note formatting.
|
|
17
|
+
- `SessionManager.monitorTick` — modified — records pane cwd after liveness is confirmed, without changing session lifecycle decisions.
|
|
18
|
+
- `SessionManager.spawnInteractiveSession` — modified — prepends a restore note only for resume spawns with an eligible saved worktree context.
|
|
19
|
+
- `server` startup wiring — modified — resolves the feature dark by default and live for development agents unless explicitly overridden.
|
|
20
|
+
- Session-management e2e — modified — proves a real tmux pane under a fixture worktree gets the restore note while a home-only control stays unchanged.
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
The restore can skip a legitimate build context if the tmux cwd is not under a `.worktrees` path. That is intentional. The spec's blast-radius guard is more important than restoring every possible hand-navigated directory. Agents can still recover manually from conversation context; the automated note is limited to the known build-worktree convention.
|
|
25
|
+
|
|
26
|
+
The restore can also skip when the sidecar is stale or the worktree directory was removed. That prevents a respawn from sending the agent back into a deleted or long-finished checkout.
|
|
27
|
+
|
|
28
|
+
## 2. Under-block
|
|
29
|
+
|
|
30
|
+
The sidecar records cwd for running sessions when the feature is enabled, including home-only sessions. Home-only records are not eligible for restore, so this is additional local state but not additional behavior. The record contains local paths and an optional branch name; it does not include prompt content or credentials.
|
|
31
|
+
|
|
32
|
+
The branch name is best-effort enrichment. If Git cannot read the branch, the restore note still works with the worktree path alone. Failure to read branch never blocks monitoring or respawn.
|
|
33
|
+
|
|
34
|
+
## 3. Level-of-abstraction fit
|
|
35
|
+
|
|
36
|
+
SessionManager owns tmux lifecycle, liveness checks, interactive respawn, and initial-message injection, so it is the correct place to sample pane cwd and prepend the resume note. The sidecar store keeps persistence and eligibility logic out of the large SessionManager class and makes the boundary unit-testable.
|
|
37
|
+
|
|
38
|
+
The change does not force tmux to spawn inside the worktree. That matches the design spec: the session home remains stable, and the resumed agent receives deterministic instruction to return to the build checkout only when appropriate.
|
|
39
|
+
|
|
40
|
+
## 4. Signal vs authority compliance
|
|
41
|
+
|
|
42
|
+
Required reference: [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
43
|
+
|
|
44
|
+
- [x] Not applicable to conversational/product judgment — this is structural session lifecycle restoration.
|
|
45
|
+
|
|
46
|
+
The cwd sample is a mechanical tmux fact. The restore decision is a deterministic filesystem/scope guard: fresh, existing, different from spawn cwd, and under a worktree path. It does not judge user intent or message meaning.
|
|
47
|
+
|
|
48
|
+
## 5. Interactions
|
|
49
|
+
|
|
50
|
+
- **Shadowing:** The restore note is prepended before the normal initial-message injection. Existing InputGuard and injection mechanics still handle the resulting message through the same path.
|
|
51
|
+
- **Double-fire:** The note is computed once per resumed spawn. Reusing an already-live tmux session does not inject a restore note.
|
|
52
|
+
- **Races:** A worktree can be removed between sidecar read and agent acting on the note. The respawn path checks existence immediately before injecting; after that, the note is advisory and the agent can report if the path disappears.
|
|
53
|
+
- **Feedback loops:** Monitoring persists only when cwd changes, avoiding a write every tick for steady sessions.
|
|
54
|
+
|
|
55
|
+
## 6. External surfaces
|
|
56
|
+
|
|
57
|
+
The visible behavior is a new continuation preamble for eligible resumed developer sessions. The preamble is intentionally plain text and starts with `[BUILD-CONTEXT RESTORE]` so the agent can act on it deterministically. Normal topic sessions that remain in agent home see no new text.
|
|
58
|
+
|
|
59
|
+
The persistent surface is a local sidecar state record keyed by tmux session name. It stores spawn cwd, current cwd, optional branch, and update time.
|
|
60
|
+
|
|
61
|
+
## 7. Rollback cost
|
|
62
|
+
|
|
63
|
+
Rollback is a code revert of the sidecar store, SessionManager wiring, server gate wiring, tests, and artifacts. The sidecar state file can remain harmlessly on disk; without the code path it is ignored. No migration or state repair is required.
|
|
64
|
+
|
|
65
|
+
## Conclusion
|
|
66
|
+
|
|
67
|
+
The change addresses the respawn amnesia failure without changing normal spawn cwd, without broadening behavior for home-only sessions, and without relying on conversation memory to rediscover the checkout. The test set covers pure eligibility/staleness behavior, mocked tracker-to-respawn wiring, and a real tmux e2e with both worktree and home-only controls.
|
|
68
|
+
|
|
69
|
+
## Second-pass review
|
|
70
|
+
|
|
71
|
+
**Reviewer:** `instar-codey separate pass`
|
|
72
|
+
**Independent read of the artifact:** concur
|
|
73
|
+
|
|
74
|
+
The risky surface is SessionManager. The implementation keeps the new behavior behind a feature gate, samples only after liveness is confirmed, writes through crash-safe state persistence, and injects only a continuation note for eligible resumed spawns. The scope guard is the core safety property and is covered in all three test tiers.
|
|
75
|
+
|
|
76
|
+
## Evidence pointers
|
|
77
|
+
|
|
78
|
+
- `npm test -- --run tests/unit/session-build-context-store.test.ts tests/integration/session-build-context-respawn.test.ts` passed.
|
|
79
|
+
- `npm test -- --run tests/e2e/session-management-e2e.test.ts -t "restores a worktree build context"` passed.
|
|
80
|
+
- `npm test -- --run tests/unit/no-silent-fallbacks.test.ts tests/unit/session-build-context-store.test.ts` passed after marking best-effort branch enrichment as an intentional fail-open.
|
|
81
|
+
- `npm run lint` passed.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Side-effects review — ReviewExchange protocol (coordination-mandate spec §7, G2.3)
|
|
2
|
+
|
|
3
|
+
Spec: `docs/specs/coordination-mandate.md` (approved by Justin, A/A/B, 2026-06-05) — §7
|
|
4
|
+
names G2.3: "autonomous code-review protocol over Threadline, gated by the
|
|
5
|
+
`sign-code-review` authority". Change: new `src/coordination/ReviewExchange.ts` engine +
|
|
6
|
+
six `/review-exchange*` routes wired through the existing coordination block in
|
|
7
|
+
`AgentServer` → `RouteContext`.
|
|
8
|
+
|
|
9
|
+
## 1. Blast radius
|
|
10
|
+
|
|
11
|
+
Additive only. No existing route, gate, store, or sentinel is modified. The engine rides
|
|
12
|
+
the EXISTING MandateGate — it adds no new authority kind, no new signing key, and no new
|
|
13
|
+
trust root. Inert by inheritance: with no mandate issued (the universal state at deploy),
|
|
14
|
+
every sign-off path refuses 403, so shipping this changes NO behavior until the operator
|
|
15
|
+
issues a mandate AND an agent drives an exchange.
|
|
16
|
+
|
|
17
|
+
## 2. State / data
|
|
18
|
+
|
|
19
|
+
One new file under the `.instar/state/` convention, created on first use:
|
|
20
|
+
`review-exchanges.json` (the exchange records). No schema migration; an absent file is an
|
|
21
|
+
empty list. Audit entries land in the EXISTING `mandate-audit.jsonl` hash chain (the gate
|
|
22
|
+
records them; this engine only stores the returned entry hashes).
|
|
23
|
+
|
|
24
|
+
## 3. Security model
|
|
25
|
+
|
|
26
|
+
- **No new authorization surface.** Both sign-offs (peer approve + owner countersign) are
|
|
27
|
+
evaluated through the same MandateGate (`sign-code-review` authority): named party,
|
|
28
|
+
bounds, expiry, revocation — every decision audited. A deny refuses the state
|
|
29
|
+
transition; the route surfaces it as 403 with the gate's reason (gate-deny precedence
|
|
30
|
+
over not-found, so deny-by-default cannot masquerade as a 404).
|
|
31
|
+
- **requester ≠ authorizer preserved.** The exchange routes are Bearer-gated (no PIN by
|
|
32
|
+
design): creating/recording an exchange delegates nothing — the GATE is what authorizes
|
|
33
|
+
sign-offs, and the gate's authority comes only from the PIN-issued mandate.
|
|
34
|
+
- **Peer asymmetry stated, not hidden.** The peer's instance may not run this engine, so
|
|
35
|
+
the peer's signature is their authenticated Threadline verdict message recorded BY
|
|
36
|
+
REFERENCE (`kind: 'authenticated-peer-verdict'`, evidence = the message ref). The gate
|
|
37
|
+
still evaluates the peer's fingerprint identically. The caller is responsible for only
|
|
38
|
+
recording verdicts that actually arrived over the authenticated relay — the same trust
|
|
39
|
+
the existing A2A surface already places in the relay's sender authentication.
|
|
40
|
+
- **Content addressing.** `packageSha256` is fixed at creation and has no update path; a
|
|
41
|
+
changed package is a different exchange. `request-changes` is terminal — rework
|
|
42
|
+
restarts as a new exchange, so a stale approval can never be replayed onto new code.
|
|
43
|
+
- **Kill switch reaches the protocol.** Revoking the mandate mid-exchange blocks every
|
|
44
|
+
subsequent sign-off (tested at unit + e2e tiers).
|
|
45
|
+
|
|
46
|
+
## 4. Failure modes
|
|
47
|
+
|
|
48
|
+
- Engine init failure → `coordination` stays null → all routes 503 (deny-safe, no boot
|
|
49
|
+
block; same try/catch as the mandate engine).
|
|
50
|
+
- Torn/absent state file → empty list (`@silent-fallback-ok` annotated).
|
|
51
|
+
- A gate deny mid-protocol leaves the exchange in its prior state (no partial advance) —
|
|
52
|
+
asserted by tests on both deny paths.
|
|
53
|
+
|
|
54
|
+
## 5. Test coverage
|
|
55
|
+
|
|
56
|
+
11 unit + 7 integration + 4 e2e (22 total): both sides of every decision boundary —
|
|
57
|
+
creation validation, linear-order enforcement, gate-deny on both sign-offs,
|
|
58
|
+
deny-by-default with no mandate, named-party refusals (stranger AND wrong-party), bounds
|
|
59
|
+
mismatch, request-changes terminality, revocation mid-exchange, audit-hash wiring
|
|
60
|
+
integrity on the production boot path, 503 when unavailable.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Side-Effects Review — Secret Drop store-first durable persistence
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `secret-drop-store-first`
|
|
4
|
+
**Date:** `2026-06-04`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (single decision boundary, both sides test-pinned; operator-mandated incident fix)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
A submitted Secret Drop is persisted store-first to the durable AES-256-GCM
|
|
11
|
+
encrypted SecretStore (`secretDrops.<token>`) the instant it is received, the
|
|
12
|
+
retrieve route transparently falls back to that copy when the in-memory one is
|
|
13
|
+
gone, and a successful consume deletes both. Opt out with
|
|
14
|
+
`secrets.persistDrops: false` (absence = enabled). Plus a structural
|
|
15
|
+
test-environment guard in `MasterKeyManager`: any VITEST/NODE_ENV=test run is
|
|
16
|
+
forced to file-key mode so no test can overwrite the machine-global keychain
|
|
17
|
+
entry (the failure the first run of this PR's own test caused live on
|
|
18
|
+
2026-06-05, remediated same hour).
|
|
19
|
+
|
|
20
|
+
## Decision-point inventory
|
|
21
|
+
|
|
22
|
+
1. `persistDrops !== false` — persist on submit / fall back on retrieve / clean
|
|
23
|
+
on consume vs pre-existing in-memory-only behavior. Both sides covered in
|
|
24
|
+
`secret-drop-store-first.test.ts`.
|
|
25
|
+
2. `consume` true/false against the durable copy — delete vs preserve. Both
|
|
26
|
+
sides covered.
|
|
27
|
+
3. `inTestRun` in `MasterKeyManager` — file-key forced vs production keychain
|
|
28
|
+
resolution. Test side pinned in `secret-store.test.ts`; production side is
|
|
29
|
+
the unchanged existing behavior.
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
Nothing is rejected. Submission, retrieval, and consume flows accept exactly
|
|
34
|
+
what they accepted before. A durable-persist FAILURE never blocks the
|
|
35
|
+
submission (best-effort + loud ERROR log) — the user's drop always succeeds if
|
|
36
|
+
the in-memory submit succeeded.
|
|
37
|
+
|
|
38
|
+
## 2. Under-block
|
|
39
|
+
|
|
40
|
+
- A secret submitted while `persistDrops:false` still has the original churn
|
|
41
|
+
exposure — explicit operator opt-out, documented.
|
|
42
|
+
- The durable copy does not yet sync cross-machine (Phase 4) — a drop made on
|
|
43
|
+
the laptop is durable on the laptop only.
|
|
44
|
+
- The broader key-coherence disease (machine-global keychain account, no
|
|
45
|
+
key-id header, silent-empty on decrypt failure) is NOT fixed here — filed as
|
|
46
|
+
a follow-up finding; this PR only makes tests structurally unable to trigger
|
|
47
|
+
it.
|
|
48
|
+
|
|
49
|
+
## 3. Level-of-abstraction fit
|
|
50
|
+
|
|
51
|
+
The persist lives in the submit route directly after the in-memory `submit()`
|
|
52
|
+
succeeds — the single place a submission is born. The fallback lives in the
|
|
53
|
+
one retrieve route the hardened helper uses. No new routes, no SecretDrop
|
|
54
|
+
class changes, no helper changes. The test guard lives in the
|
|
55
|
+
`MasterKeyManager` constructor — the single chokepoint every key resolution
|
|
56
|
+
passes through (Structure > Willpower: per-test `forceFileKey` conventions
|
|
57
|
+
would rot).
|
|
58
|
+
|
|
59
|
+
## 4. Signal vs authority compliance
|
|
60
|
+
|
|
61
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
62
|
+
|
|
63
|
+
No blocking authority added. Persist is best-effort (loud log, never 500s);
|
|
64
|
+
fallback only widens what retrieval can find; the test guard constrains TESTS
|
|
65
|
+
only, never production resolution.
|
|
66
|
+
|
|
67
|
+
## 5. Interactions
|
|
68
|
+
|
|
69
|
+
- The 15-min in-memory TTL/idle cleanup still runs; after cleanup the durable
|
|
70
|
+
copy now serves the retrieve instead of a 404 — that is the fix working.
|
|
71
|
+
- `onStuckConsumer` notify-only hardening unchanged.
|
|
72
|
+
- The Telegram confirm + agent nudge fire AFTER the persist, so by the time
|
|
73
|
+
the agent is told "retrieve me," the durable copy already exists.
|
|
74
|
+
- `secret-drop-retrieve.mjs` needs no changes — it talks to the same route.
|
|
75
|
+
- SecretStore `set()` is read-modify-write; a decrypt failure throws BEFORE
|
|
76
|
+
any write, so the persist path cannot clobber an existing store it cannot
|
|
77
|
+
read (verified against the live 2026-06-05 key-split incident).
|
|
78
|
+
|
|
79
|
+
## 6. External surfaces
|
|
80
|
+
|
|
81
|
+
No new HTTP routes. One new config block (`secrets.persistDrops`,
|
|
82
|
+
`secrets.forceFileKey` — both optional, absence = current defaults). CLAUDE.md
|
|
83
|
+
template Security bullet rewritten + idempotent PostUpdateMigrator migration
|
|
84
|
+
for existing agents. One new encrypted-at-rest artifact class:
|
|
85
|
+
`secretDrops.<token>` entries inside the existing vault file.
|
|
86
|
+
|
|
87
|
+
## 7. Rollback cost
|
|
88
|
+
|
|
89
|
+
Low. `secrets.persistDrops: false` is an instant behavioral rollback without a
|
|
90
|
+
deploy. Full revert restores in-memory-only semantics; any `secretDrops.*`
|
|
91
|
+
entries already in vaults are inert data deletable via `SecretStore.delete`.
|
|
92
|
+
No schema, no irreversible op.
|
|
93
|
+
|
|
94
|
+
## Conclusion
|
|
95
|
+
|
|
96
|
+
Operator-mandated incident fix at the minimal coherent scope: one persist
|
|
97
|
+
call, one fallback branch, one cleanup branch, one structural test guard.
|
|
98
|
+
Every decision boundary has both sides pinned by tests; the failure it fixes
|
|
99
|
+
was reproduced live twice during the build (auto-update restart ate a 1h
|
|
100
|
+
token; test run split the vault keys).
|
|
101
|
+
|
|
102
|
+
## Second-pass review (if required)
|
|
103
|
+
|
|
104
|
+
Not required — see header. The riskiest element (writing secrets to disk) is
|
|
105
|
+
the operator's explicit instruction, encrypted with the pre-existing vault
|
|
106
|
+
machinery, and consume-cleaned.
|
|
107
|
+
|
|
108
|
+
## Evidence pointers
|
|
109
|
+
|
|
110
|
+
- `tests/integration/secret-drop-store-first.test.ts` — 5 route-level tests.
|
|
111
|
+
- `tests/unit/secret-store.test.ts` — keychain pollution guard.
|
|
112
|
+
- `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` — 3
|
|
113
|
+
migration tests (rewrite, idempotency, fresh-inject wording).
|
|
114
|
+
- `docs/specs/secret-drop-store-first.md` + `.eli16.md` — spec + overview.
|
|
115
|
+
- Live remediation evidence: vault backup at
|
|
116
|
+
`/tmp/config.secrets.enc.backup-1780635105`; keychain entry cdat
|
|
117
|
+
`2026-06-05T04:46:39Z` matched the test run start `04:46:38`.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Side-Effects Review — Transfer idempotency + exactly-once-when-live
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `transfer-idempotency-exactly-once`
|
|
4
|
+
**Date:** `2026-06-05`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required` (pure-planner reorder + config-resolution default; every boundary side test-pinned)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
1. `planTransferByNickname`: the already-satisfied checks (`owner === target`,
|
|
11
|
+
NEW `pin === target` via optional `currentPinOf`) now run BEFORE the rate
|
|
12
|
+
limit, so a duplicate of an already-satisfied move plans `noop` ("already
|
|
13
|
+
there") instead of `reject/rate-limited`. The consumer wires `currentPinOf`
|
|
14
|
+
from `TopicPlacementPinStore` and words the `already-on-target` noop as
|
|
15
|
+
"already running on X — nothing to move."
|
|
16
|
+
2. `resolveSeamlessnessConfig`: `exactlyOnceIngress` defaults ON when the
|
|
17
|
+
session pool stage is `live-transfer`/`rebalance` (a live pool without the
|
|
18
|
+
ingress dedupe ledger re-executes retried/replayed user commands — the
|
|
19
|
+
2026-06-05 4×-execution incident). Explicit values always win.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
1. `owner === target` → noop vs continue. Both sides pinned (incident test +
|
|
24
|
+
transfer test).
|
|
25
|
+
2. `pin === target` → noop vs continue; absent `currentPinOf` → old behavior.
|
|
26
|
+
All three pinned.
|
|
27
|
+
3. Rate-limit window for a real move (different target) → still rejected.
|
|
28
|
+
Pinned.
|
|
29
|
+
4. Stage-derived `exactlyOnceIngress` default: off (none/dark/shadow), on
|
|
30
|
+
(live-transfer/rebalance), explicit override both directions. All pinned.
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
Nothing new is rejected. The planner now rejects strictly LESS (duplicates
|
|
35
|
+
that previously rate-limited now noop). The exactly-once default only
|
|
36
|
+
activates a gate that itself fails open (ledger errors fall through to normal
|
|
37
|
+
routing, per the existing route code).
|
|
38
|
+
|
|
39
|
+
## 2. Under-block
|
|
40
|
+
|
|
41
|
+
- The rate limit no longer applies to same-target duplicates — deliberate:
|
|
42
|
+
re-pinning the same value is idempotent. Rapid same-target spam costs one
|
|
43
|
+
pin-store write + one "already there" reply per message; the per-topic
|
|
44
|
+
Telegram traffic itself is bounded upstream.
|
|
45
|
+
- The exactly-once default does NOT retroactively enable the ledger on pools
|
|
46
|
+
still in dark/shadow — pre-live stages keep the old dark default by design.
|
|
47
|
+
|
|
48
|
+
## 3. Level-of-abstraction fit
|
|
49
|
+
|
|
50
|
+
The idempotency fix lives in the pure planner (the single decision function
|
|
51
|
+
both the natural-language path and `POST /pool/transfer` flow through), not in
|
|
52
|
+
the Telegram consumer. The exactly-once coupling lives in config RESOLUTION
|
|
53
|
+
(`resolveSeamlessnessConfig`) — the one place defaults are derived — not in
|
|
54
|
+
the wiring site.
|
|
55
|
+
|
|
56
|
+
## 4. Signal vs authority compliance
|
|
57
|
+
|
|
58
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
59
|
+
|
|
60
|
+
No new blocking authority. The planner reorder converts rejections into
|
|
61
|
+
no-ops. The exactly-once gate it activates was already authority-bearing by
|
|
62
|
+
design (spec §8 G3a) and fails open on any ledger error.
|
|
63
|
+
|
|
64
|
+
## 5. Interactions
|
|
65
|
+
|
|
66
|
+
- `noop` consumer behavior unchanged structurally (pin set + release-if-self
|
|
67
|
+
+ message): re-setting an identical pin refreshes `updatedAt`, which can
|
|
68
|
+
extend the rate window for a SUBSEQUENT different-target move by up to 10s
|
|
69
|
+
— bounded, harmless.
|
|
70
|
+
- Activating `exactlyOnceIngress` on live pools also activates the
|
|
71
|
+
reply-marker transport (same flag, existing wiring) — that is the intended
|
|
72
|
+
pairing per spec §8 G3a.
|
|
73
|
+
- Sentinel intercepts (emergency stop / pause) run BEFORE the ingress gate in
|
|
74
|
+
the route — unchanged; an emergency stop is never deduped away.
|
|
75
|
+
|
|
76
|
+
## 6. External surfaces
|
|
77
|
+
|
|
78
|
+
No new routes, no new config keys (an existing key's DEFAULT becomes
|
|
79
|
+
stage-aware). One user-visible message wording improvement. No migration:
|
|
80
|
+
config absence means "follow the default," which is the point.
|
|
81
|
+
|
|
82
|
+
## 7. Rollback cost
|
|
83
|
+
|
|
84
|
+
Trivial. `exactlyOnceIngress: false` is an instant per-agent opt-out without
|
|
85
|
+
deploy; reverting the PR restores both old behaviors. No state, no schema.
|
|
86
|
+
|
|
87
|
+
## Conclusion
|
|
88
|
+
|
|
89
|
+
Two small, surgical changes at the correct layers, each killing one half of a
|
|
90
|
+
user-facing incident: duplicates narrated as failures, and duplicates
|
|
91
|
+
executing at all. Strictly less rejection, strictly more dedupe, both
|
|
92
|
+
test-pinned on every side.
|
|
93
|
+
|
|
94
|
+
## Second-pass review (if required)
|
|
95
|
+
|
|
96
|
+
Not required — see header.
|
|
97
|
+
|
|
98
|
+
## Evidence pointers
|
|
99
|
+
|
|
100
|
+
- `tests/unit/TransferByNickname.test.ts` — 5 new idempotency tests (incl. the
|
|
101
|
+
verbatim incident case).
|
|
102
|
+
- `tests/unit/seamlessnessConfig.test.ts` — 4 new coupling tests.
|
|
103
|
+
- Production evidence: `logs/server.log` 2026-06-05T04:20:43/04:20:55 double
|
|
104
|
+
pin + zero `exactly-once` lines; topic 13481 screenshot (Moving →
|
|
105
|
+
rate-limited → Moving → rate-limited).
|