instar 1.3.182 → 1.3.184

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.
@@ -0,0 +1,91 @@
1
+ # Side-Effects Review — feedback-factory canonical status normalization (Phase 1)
2
+
3
+ **Slug:** `feedback-factory-status-normalization`
4
+ **Date:** `2026-05-31`
5
+ **Author:** Echo
6
+ **Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26, topic 12476)
7
+ **Second-pass reviewer:** not required (pure additive primitives + one comparator predicate; both-sides-of-boundary unit coverage)
8
+ **Scope:** The status-vocabulary normalization primitives in `src/feedback-factory/processor/transitions.ts`, and their application in the Phase-3 parity comparator `src/feedback-factory/processor/parity.ts` (`compareClusterOutcomes`). Driven by Dawn's pinned canonical contract (thread-978f016b): the authoritative `V1_TO_V2_STATUS` (feedback-processor.py:1035) + terminal set (:379) that Portal owns.
9
+
10
+ ## Summary of the change
11
+
12
+ During the migration, three status vocabularies coexist: the v1 legacy literals Portal still writes (`open`/`fixed`/`resolved`/…), the mid-migration write-gate superset both sides accept, and the canonical v2 lifecycle (`V2_STATES`). The Phase-3 live-mirror parity comparator compared cluster outcome status with a raw `i.status !== p.status`, so an equivalent cluster — Portal `resolved` vs the ported Instar processor's `closed` — read as a **divergence**, which structurally **blocks Phase-4 cutover** (`ParityResult.divergent`). That is benign vocabulary skew being treated as a real history fork.
13
+
14
+ This change adds three pure primitives to `transitions.ts` — `V1_TO_V2_STATUS` (the v1→v2 projection map), `TERMINAL_STATUSES` + `isTerminalStatus()` (terminal check on the normalized status, incl. the terminal-only `legacy_closed` literal), and `normalizeStatus()` (idempotent v1→v2 projection; already-v2 and unknown values pass through) — and applies `normalizeStatus` to BOTH sides of the status comparison in `compareClusterOutcomes` before the `!==`. Reported divergence values stay raw so the operator still sees each side's actual stored status when a genuine mismatch survives. **No route/job behavior changes** — the parity comparator is exercised by the dry-run runner, and `transitions.ts` is a pure library.
15
+
16
+ ## Decision-point inventory
17
+
18
+ - `compareClusterOutcomes` status equality (`parity.ts:~155`) — **modify** — now compares in normalized v2 space; recurrence + missing-on-one-side checks unchanged.
19
+ - `normalizeStatus` / `isTerminalStatus` / `V1_TO_V2_STATUS` / `TERMINAL_STATUSES` (`transitions.ts`) — **add** — canonical vocabulary primitives mirrored from Portal's reference.
20
+
21
+ ---
22
+
23
+ ## 1. Over-block
24
+
25
+ The only "block/allow"-like surface here is `ParityResult.divergent`, which gates Phase-4 cutover. **Over-block before this change:** a window where Portal stored `resolved`/`open`/`fixed` and Instar recomputed `closed`/`new`/`fix_applied` for the SAME fingerprint produced a `status` divergence and a red verdict — blocking cutover on noise. This change removes exactly that false block: each v1↔v2 pair now reconciles. It does not newly reject any input — it only stops rejecting vocabulary-equivalent ones.
26
+
27
+ ---
28
+
29
+ ## 2. Under-block
30
+
31
+ **What real divergence could normalization now mask?** Only a difference that vanishes under the pinned v1→v2 map. The map is a small, fixed bijection on the legacy literals (`open→new`, `fixed→fix_applied`, `resolved→closed`; investigating/wontfix/duplicate identity). A genuine lifecycle mismatch — e.g. Instar `investigating` vs Portal `resolved` (→`closed`) — still differs after normalization and is still flagged (asserted in `parity.test.ts`). Recurrence divergence on a vocabulary-equivalent status is also still flagged (asserted) — normalization touches status only. Risk that the map itself is wrong is contained: it is byte-mirrored from Dawn's owned reference and asserted against drift in `transitions.test.ts`.
32
+
33
+ ---
34
+
35
+ ## 3. Level-of-abstraction fit
36
+
37
+ Correct layer. `normalizeStatus`/`isTerminalStatus` are pure processor-logic primitives living beside the existing ported lifecycle decision functions (`canTransition`, `detectCycling`) in `transitions.ts` — the same module that already owns `V2_STATES`. The parity comparator is the right and only consumer for the projection at cutover time; it now USES the shared primitive rather than re-encoding status equality. No higher-level gate is bypassed and no lower-level primitive is re-implemented.
38
+
39
+ ---
40
+
41
+ ## 4. Signal vs authority compliance
42
+
43
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
44
+
45
+ - [x] No — this change has no block/allow surface over user/agent messages. `ParityResult.divergent` is a structural equivalence signal consumed by the dry-run/cutover decision (a human-gated process), not an autonomous block on traffic.
46
+
47
+ `compareClusterOutcomes` is a deterministic comparator over two pre-computed outcome lists; it owns no runtime authority over messages or operations. The normalization makes its signal *more* accurate (fewer false divergences) without granting it new authority. The terminal authority over a cluster's lifecycle remains with the curated cluster history (per the spec's Signal-vs-Authority lesson), untouched here.
48
+
49
+ ---
50
+
51
+ ## 5. Interactions
52
+
53
+ - **Shadowing:** none. The status branch is independent of the fingerprint invariant (invariant 1) and the recurrence branch; normalization is scoped to the status `!==` only. `missing-instar`/`missing-portal` paths are unchanged.
54
+ - **Double-fire:** none. Single comparator, single call site (the dry-run runner).
55
+ - **Races:** none. Pure functions over injected snapshots; no shared mutable state.
56
+ - **Feedback loops:** none. The comparator reads Portal's read-only snapshot + supplied Instar outcomes and emits a verdict; it writes nothing back into either system.
57
+ - **Import graph:** `parity.ts` now imports `normalizeStatus` from `transitions.ts`. `transitions.ts` imports nothing — no cycle introduced (confirmed: `tsc --noEmit` clean).
58
+
59
+ ---
60
+
61
+ ## 6. External surfaces
62
+
63
+ - **Other agents / install base:** none. Internal feedback-factory library code; not wired to a route or job. No CLAUDE.md-template/hook/config surface touched → no Migration-Parity obligation.
64
+ - **External systems:** none. Portal is read-only here; no write path.
65
+ - **Persistent state:** none. No schema, ledger, or memory file change. The JSONL audit-trail RECORD shape (`dryRunCompare.toRecords`) is unchanged — the `status` divergence still reports raw values, so existing audit consumers see the same field semantics.
66
+ - **Reported-value semantics:** an emitted `status` divergence continues to carry the RAW stored status of each side (not the normalized form), deliberately — so a human reading the audit trail sees what each side actually stored. Documented in code + tests.
67
+
68
+ ---
69
+
70
+ ## 7. Rollback cost
71
+
72
+ Pure code change — revert the two source edits and ship as a patch. No persistent state, no data migration, no agent-state repair, no user-visible surface. Reverting restores the prior raw `!==` comparison; the only effect of a revert is the re-appearance of vocabulary-skew false divergences in a dry-run (a stricter, not looser, gate — fail-safe direction for a cutover block).
73
+
74
+ ---
75
+
76
+ ## Equivalence verification
77
+
78
+ - `V1_TO_V2_STATUS` and `TERMINAL_STATUSES` are byte-mirrored from Dawn's pinned authoritative definitions (feedback-processor.py:1035 / :379) and asserted exactly in `transitions.test.ts` (no-drift tests).
79
+ - `normalizeStatus` proven idempotent (no v2 output value is itself a re-mapping v1 key) — asserted via double-apply.
80
+ - `isTerminalStatus` proven to normalize-before-check: raw v1 `resolved` reads terminal (the load-bearing case); `legacy_closed` is terminal but not a `V2_STATES` member.
81
+ - `transitions.ts` change is **purely additive** — `canTransition`/`detectCycling` (the Python byte-parity surface covered by `scripts/feedback-factory/transitions-parity.mjs`) are untouched, so byte-exact parity with the reference is preserved by construction.
82
+
83
+ ## Tests
84
+
85
+ - **Tier-1 unit (CI):** `tests/unit/feedback-factory/transitions.test.ts` — `normalizeStatus` (each mapping, identity entries, idempotence, already-v2 passthrough, unknown passthrough, exact-map no-drift) and `isTerminalStatus` (every terminal state, the normalize-before-check `resolved` case, non-terminal v1 literals incl. `fixed→fix_applied`, exact-set no-drift, `legacy_closed`). `tests/unit/feedback-factory/parity.test.ts` — each v1↔v2 pair reconciles, direction-agnostic, genuine lifecycle divergence still flagged with RAW reported values, recurrence not masked, and a green `compareInvariants` verdict across a vocabulary-skewed window. **156 feedback-factory unit tests green, `tsc` clean.**
86
+ - **Tier-2 integration:** `tests/integration/feedback-factory-process.test.ts` — 4/4 green (no regression in the processing composition).
87
+ - **No new E2E tier:** the comparator is not wired to a live route/job — the live dual-forward parity run (the Phase-4 cutover gate) is the operational verification and runs against Portal's `/api/instar/read` via `HttpParitySource`. Reasoned decision, documented.
88
+
89
+ ## Conclusion
90
+
91
+ This review produced no design change — the projection is the minimal, contract-faithful fix for the false-divergence seam Dawn pinned, scoped precisely to the parity comparator + the shared primitives. The two consumer-side sites that ALSO read raw status literals — `FeedbackStore.getActiveClusters` (`!= 'resolved'`) and `reportPartition` (raw `'open'`/`'fixed'`) — were deliberately left out of this change because the correct fix hinges on the canonical active/merge-candidate query semantics (`!= closed`, faithful to the reference `status != 'resolved'`, vs full `!isTerminal`), which is a bilateral question for Dawn (the domain owner) rather than a guess that could itself introduce a divergence. That open question is being raised to Dawn directly on thread-978f016b. Clear to ship.
@@ -0,0 +1,55 @@
1
+ # Side-Effects Review — Surface restartImmediately in GET /updates/status
2
+
3
+ **Slug:** `updates-status-restart-immediately`
4
+ **Date:** 2026-06-01
5
+ **Author:** echo
6
+ **Spec:** `docs/specs/updates-status-restart-immediately-spec.md` (approved by Justin, Telegram 13435)
7
+
8
+ ## Summary of the change
9
+
10
+ Closes the #641 observability gap (#59): #641 added `restartImmediately` to
11
+ `AutoUpdaterStatus` + `getStatus()` and claimed it was "surfaced in GET
12
+ /updates/status", but the route's hand-picked response object omitted it. One
13
+ line added so the route echoes `auto.restartImmediately`.
14
+
15
+ **Files changed (source):**
16
+ - `src/server/routes.ts`: inside `router.get('/updates/status')`, the
17
+ `if (ctx.autoUpdater)` `Object.assign(status, {...})` now includes
18
+ `restartImmediately: auto.restartImmediately`. The value already exists on the
19
+ status object (from #641); no other field touched.
20
+
21
+ **Files changed (tests):**
22
+ - `tests/integration/updates-status-restart-immediately-route.test.ts` — +2:
23
+ builds the router with a stub `autoUpdater` and asserts `GET /updates/status`
24
+ returns `restartImmediately` for both `true` and `false` (both sides of the
25
+ boundary; regression pin so it can't be dropped from the pick-list again).
26
+
27
+ ## Blast radius
28
+
29
+ Pure additive observability. The route gains one boolean field sourced from an
30
+ already-computed `getStatus()`. No behavior changes; no other field altered; the
31
+ no-`autoUpdater` path is unchanged (field simply absent). tsc + linters clean.
32
+
33
+ ## Behavior delta
34
+
35
+ | Scenario | Before | After |
36
+ |---|---|---|
37
+ | `GET /updates/status`, autoUpdater wired, flag on | no field | `restartImmediately: true` |
38
+ | `GET /updates/status`, autoUpdater wired, flag off (fleet) | no field | `restartImmediately: false` |
39
+ | `GET /updates/status`, no autoUpdater | field absent | field absent (unchanged) |
40
+
41
+ ## Risks considered
42
+
43
+ - **Leaking sensitive data?** No — it's a boolean config flag, same exposure
44
+ class as the `autoApply` field already returned.
45
+ - **Breaking existing consumers?** No — additive field; existing fields unchanged.
46
+
47
+ ## Migration parity
48
+
49
+ None — no agent-installed file changes (no hook/config/skill/CLAUDE.md template).
50
+ Read-only API field; existing agents gain it automatically on update.
51
+
52
+ ## Tests / lint
53
+
54
+ 2 new integration tests pass; `npm run lint` (tsc + destructive/LLM/URL-log/
55
+ codex-drift) clean.
@@ -0,0 +1,46 @@
1
+ # Side effects — URL-provenance sibling-node trust
2
+
3
+ ## What changes at runtime
4
+
5
+ The pre-messaging convergence check's URL-provenance guard now additionally
6
+ trusts URL hosts that share the agent's tunnel **parent** domain (sibling nodes
7
+ of the same multi-machine agent), in addition to the agent's exact own tunnel
8
+ host, Cloudflare quick tunnels, and the static allowlist it already trusted.
9
+
10
+ ## Who is affected
11
+
12
+ - **Single-machine agents:** NO behavior change. They address no sibling nodes;
13
+ the parent-domain trust simply never matches a host they'd ever message. A
14
+ fabricated unfamiliar URL is still flagged exactly as before.
15
+ - **Multi-machine agents:** a message that legitimately references a sibling
16
+ node (e.g. the laptop addressing the mini's tunnel host to drive the
17
+ multi-machine reply relay) is no longer falsely flagged as an "unfamiliar
18
+ domain."
19
+
20
+ ## Blast radius
21
+
22
+ - Single file: `src/templates/scripts/convergence-check.sh`, the URL-provenance
23
+ block only. Re-deployed to `.instar/scripts/convergence-check.sh` via the
24
+ existing `migrateScripts()` template-load path (no PostUpdateMigrator code
25
+ change; the template content IS the migration).
26
+ - No config keys added/changed. No new dependency. The check still runs as the
27
+ same heuristic gate behind the `grounding-before-messaging` hook.
28
+
29
+ ## Failure modes considered
30
+
31
+ - **Over-trust of a public-suffix apex:** prevented — the parent is derived only
32
+ when the own tunnel host has ≥3 DNS labels, so a 2-label apex config yields no
33
+ parent (verified by test).
34
+ - **Look-alike suffix attack** (`echo.dawn-tunnel.dev.evil.com`): blocked — the
35
+ match is a true DNS-suffix test (`${URL_HOST%.$PARENT}`), so a host that merely
36
+ contains the parent mid-string but ends elsewhere is not trusted (verified by
37
+ test).
38
+ - **Missing/empty tunnel config:** no parent derived → unchanged behavior.
39
+
40
+ ## Not touched
41
+
42
+ - The pre-existing inline fallback `PostUpdateMigrator.getConvergenceCheckInline()`
43
+ is intentionally NOT modified here. In practice `loadTemplate()` always returns
44
+ the template file, so the inline path is dead code; it additionally has a
45
+ separate, pre-existing bash syntax error on `main` (independent of this change)
46
+ and entangling a fix would muddy this diff. Flagged as a separate latent issue.
@@ -1,59 +0,0 @@
1
- # Upgrade Guide — vNEXT
2
-
3
- <!-- assembled-by: assemble-next-md -->
4
- <!-- bump: patch -->
5
-
6
- ## What Changed
7
-
8
- **A moved conversation's reply no longer hangs silently when the other machine
9
- is briefly unreachable.** When you move a conversation to another machine, that
10
- machine relays its replies through the machine that holds the Telegram
11
- connection. That relay had two operational defects, both found by driving the
12
- multi-machine feature live: it had no time limit (so when the receiving machine's
13
- connection was momentarily restarting, the reply hung for over a minute with no
14
- result), and it failed completely silently (a dropped reply wrote nothing to the
15
- log, so there was no way to tell why it didn't arrive).
16
-
17
- The relay is now bounded and observable: it gives the receiving machine a fixed
18
- window (15 seconds by default, adjustable) and then fails fast, and every failure
19
- writes one clear line saying exactly what went wrong (no reachable machine, a
20
- rejection with its status code, or a timeout). The relay logic was also extracted
21
- into its own well-tested unit.
22
-
23
- It also no longer reports a false success: previously the relay could report
24
- "delivered" even when the message never actually reached the chat (it accepted a
25
- response that carried no real message id). Now the receiving machine returns the
26
- real message id and the relay only counts a reply as delivered when that id is
27
- present — otherwise it's treated as undelivered and surfaced, so a busy or flaky
28
- moment becomes a real, visible failure (and a retry candidate) instead of a
29
- silent loss dressed up as success.
30
-
31
- ## What to Tell Your User
32
-
33
- Nothing to configure. If you run across more than one machine and move a
34
- conversation between them, a reply that can't be delivered now fails quickly with
35
- a clear reason in the log, instead of hanging for over a minute and vanishing
36
- without explanation. Single-machine setups are unaffected.
37
-
38
- ## Summary of New Capabilities
39
-
40
- - `relayOutbound` (`src/core/TelegramRelay.ts`) — the tokenless-standby reply
41
- relay, extracted as a pure injectable unit with a bounded `AbortController`
42
- timeout and a log line on every failure path. `server.ts` wires
43
- `telegram.outboundRelay` to it. New optional `multiMachine.relayTimeoutMs`
44
- (default 15000).
45
-
46
- ## Evidence
47
-
48
- - Reproduction (live, 2026-06-01): driving the multi-machine reply proof, a
49
- relayed reply hung 25s then 70s with no result and no log line; root cause was
50
- the holder's tunnel being mid-restart, and the relay `fetch` having no timeout.
51
- Once the tunnel was back, the relay completed (a tone-gate response and an `ok`
52
- came back through the full standby→holder chain), confirming the path itself
53
- works — but the hang + silence were real operational defects on the way there.
54
- - Tests: `tests/unit/telegram-relay-timeout-observability.test.ts` (7) drive the
55
- real `relayOutbound` with an injected fetch + logger: 2xx returns messageId and
56
- posts the right URL + `Bearer` header; self-hold no-ops; no-peer-URL logs;
57
- non-2xx logs the status; **a hanging holder aborts within the timeout (elapsed
58
- under 2s) and logs `timeout after Nms`**; a network error logs its message; the
59
- `silent` flag passes through. 7/7 green; `tsc` clean.