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.
- package/dist/feedback-factory/processor/parity.d.ts +5 -1
- package/dist/feedback-factory/processor/parity.d.ts.map +1 -1
- package/dist/feedback-factory/processor/parity.js +8 -1
- package/dist/feedback-factory/processor/parity.js.map +1 -1
- package/dist/feedback-factory/processor/transitions.d.ts +33 -0
- package/dist/feedback-factory/processor/transitions.d.ts.map +1 -1
- package/dist/feedback-factory/processor/transitions.js +58 -0
- package/dist/feedback-factory/processor/transitions.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +1 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/templates/scripts/convergence-check.sh +25 -2
- package/upgrades/1.3.184.md +177 -0
- package/upgrades/side-effects/feedback-factory-status-normalization.md +91 -0
- package/upgrades/side-effects/updates-status-restart-immediately.md +55 -0
- package/upgrades/side-effects/url-provenance-sibling-node-trust.md +46 -0
- package/upgrades/1.3.182.md +0 -59
|
@@ -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.
|
package/upgrades/1.3.182.md
DELETED
|
@@ -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.
|