instar 1.3.555 → 1.3.557

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.
Files changed (51) hide show
  1. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  2. package/dist/core/PostUpdateMigrator.js +15 -0
  3. package/dist/core/PostUpdateMigrator.js.map +1 -1
  4. package/dist/core/SessionManager.d.ts +27 -0
  5. package/dist/core/SessionManager.d.ts.map +1 -1
  6. package/dist/core/SessionManager.js +75 -2
  7. package/dist/core/SessionManager.js.map +1 -1
  8. package/dist/core/types.d.ts +23 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/core/types.js.map +1 -1
  11. package/dist/feedback-factory/inbox/BlobInboxClient.d.ts +75 -0
  12. package/dist/feedback-factory/inbox/BlobInboxClient.d.ts.map +1 -0
  13. package/dist/feedback-factory/inbox/BlobInboxClient.js +121 -0
  14. package/dist/feedback-factory/inbox/BlobInboxClient.js.map +1 -0
  15. package/dist/feedback-factory/inbox/InboxDrainer.d.ts +91 -0
  16. package/dist/feedback-factory/inbox/InboxDrainer.d.ts.map +1 -0
  17. package/dist/feedback-factory/inbox/InboxDrainer.js +188 -0
  18. package/dist/feedback-factory/inbox/InboxDrainer.js.map +1 -0
  19. package/dist/feedback-factory/receiver/BlobInboxStore.d.ts +30 -0
  20. package/dist/feedback-factory/receiver/BlobInboxStore.d.ts.map +1 -0
  21. package/dist/feedback-factory/receiver/BlobInboxStore.js +36 -0
  22. package/dist/feedback-factory/receiver/BlobInboxStore.js.map +1 -0
  23. package/dist/feedback-factory/receiver/handlers.d.ts +18 -3
  24. package/dist/feedback-factory/receiver/handlers.d.ts.map +1 -1
  25. package/dist/feedback-factory/receiver/handlers.js +8 -4
  26. package/dist/feedback-factory/receiver/handlers.js.map +1 -1
  27. package/dist/feedback-factory/store/JsonlFeedbackStore.d.ts +65 -0
  28. package/dist/feedback-factory/store/JsonlFeedbackStore.d.ts.map +1 -0
  29. package/dist/feedback-factory/store/JsonlFeedbackStore.js +194 -0
  30. package/dist/feedback-factory/store/JsonlFeedbackStore.js.map +1 -0
  31. package/dist/scaffold/templates.d.ts.map +1 -1
  32. package/dist/scaffold/templates.js +4 -0
  33. package/dist/scaffold/templates.js.map +1 -1
  34. package/dist/server/AgentServer.d.ts +4 -0
  35. package/dist/server/AgentServer.d.ts.map +1 -1
  36. package/dist/server/AgentServer.js +53 -0
  37. package/dist/server/AgentServer.js.map +1 -1
  38. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  39. package/dist/server/CapabilityIndex.js +11 -0
  40. package/dist/server/CapabilityIndex.js.map +1 -1
  41. package/dist/server/routes.d.ts +4 -0
  42. package/dist/server/routes.d.ts.map +1 -1
  43. package/dist/server/routes.js +10 -0
  44. package/dist/server/routes.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/data/builtin-manifest.json +65 -65
  47. package/src/scaffold/templates.ts +4 -0
  48. package/upgrades/1.3.556.md +34 -0
  49. package/upgrades/1.3.557.md +61 -0
  50. package/upgrades/side-effects/age-kill-transcript-aware-resume.md +115 -0
  51. package/upgrades/side-effects/feedback-receiver-persistence.md +81 -0
@@ -0,0 +1,61 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The session age-limit reaper (`SessionManager`'s wall-clock timeout in `monitorTick`) now
9
+ consults the session's live transcript before judging it idle enough to kill. Previously the
10
+ age-gate's "truly idle?" check looked only at the tmux pane (idle prompt) and the process tree
11
+ (no non-baseline child process) — both BLIND to MCP/tool work, which runs out of the pane's
12
+ process tree (the Playwright MCP server) with only short-lived bash calls in between. A session
13
+ actively driving a browser therefore looked idle between tool calls and was terminal-killed at
14
+ the ~5h age cap with `midWork:false` (no resume).
15
+
16
+ The other reap path — the `SessionReaper` (pressure/idle) — already cross-checks transcript
17
+ growth and would not have killed it. This change brings the age-kill path to parity:
18
+ `SessionManager.isTranscriptRecentlyActive(session, withinMs)` resolves the per-framework
19
+ transcript via the core `resolveFrameworkTranscriptPath` and treats a session whose transcript
20
+ was written within `AGE_GATE_TRANSCRIPT_ACTIVE_MS` (2 min) as actively working — the kill is held
21
+ off, exactly as it already is for a session with a live child process. The idle decision is
22
+ extracted into a pure, exported `isAgeGateTrulyIdle(idleAtPrompt, hasActiveProcs, transcriptActive)`.
23
+ An unresolvable transcript (no session id, missing file, or a framework with no resolver case like
24
+ pi-cli) returns false and the gate falls back to exactly today's pane/procs behavior — safe
25
+ degradation, no regression. Machine-local; no config key, route, or migration.
26
+
27
+ ## What to Tell Your User
28
+
29
+ - **Long-running sessions don't get killed while they're busy**: "If one of my longer sessions was
30
+ ever shut down while I was actually in the middle of real work — especially driving a browser or
31
+ an automation tool — that shouldn't happen anymore. I now tell whether I'm still working from my
32
+ live activity, not just whether the terminal looks quiet, so the watchdog leaves me alone until I
33
+ genuinely stop."
34
+
35
+ ## Summary of New Capabilities
36
+
37
+ | Capability | How to Use |
38
+ |-----------|-----------|
39
+ | Age-reaper recognizes browser/tool work as "active" | automatic |
40
+
41
+ ## Evidence
42
+
43
+ This fixes a real production incident, verified two ways:
44
+
45
+ **Before (the actual failure, from `logs/reap-log.jsonl`):** the session
46
+ `Self-Coherence — Knowing My Own Parallel Work` (topic 25660), which was actively driving the
47
+ Playwright MCP server to provision a GitHub identity, was reaped at `2026-06-14T03:34:55Z` with
48
+ `reason:"age-limit"`, `disposition:"terminal"`, `midWork:false` — killed mid-work, no resume, at
49
+ the ~5h cap, because between tool calls the pane looked idle and no child process existed.
50
+
51
+ **After (decision-boundary reproduction):** the extracted `isAgeGateTrulyIdle` is unit-tested over
52
+ all 8 input combinations. The exact incident inputs — idle pane + no child process + a transcript
53
+ written in the last 2 minutes — now resolve to `false` (held off, not killed), while the
54
+ genuine-zombie inputs (idle pane + no process + a quiet transcript) still resolve to `true` (still
55
+ reaped — no regression to the real zombie-cleanup). Behavioral tests also cover
56
+ `isTranscriptRecentlyActive` for recent/stale/no-id/missing-file/cross-framework cases.
57
+
58
+ **Why not a full live before/after:** reproducing the original end-to-end requires a >5h wall-clock
59
+ wait plus a live MCP session, so it is not practical to exercise in dev; the decision-boundary
60
+ repro over the captured incident inputs is the faithful, deterministic substitute, and the real
61
+ reap-log entry above is the production "before."
@@ -0,0 +1,115 @@
1
+ # Side-Effects Review — Age-Kill Transcript-Awareness
2
+
3
+ **Version / slug:** `age-kill-transcript-aware-resume`
4
+ **Date:** `2026-06-14`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** spec-convergence panel (6 internal reviewers + Standards-Conformance gate; external non-Claude pass degraded — see convergence report)
7
+
8
+ ## Summary of the change
9
+
10
+ The session age-limit reaper (`SessionManager.monitorTick`, the `elapsed > limit` block) decides
11
+ whether an over-age session is "truly idle" and therefore age-kill-eligible. Before this change
12
+ that decision used only two signals — the tmux pane shows an idle prompt, and there is no
13
+ non-baseline child process — both BLIND to MCP/tool work (the Playwright MCP server runs out of
14
+ the pane's process tree; bash tool calls are short-lived). On 2026-06-13 an actively-working
15
+ session was terminal-killed because, between tool calls, it looked idle. This change adds a third
16
+ signal: `isTranscriptRecentlyActive(session, 120_000)` — true when the session's framework
17
+ transcript (JSONL) was modified within the last 2 minutes. The idle decision is extracted into a
18
+ pure, exported `isAgeGateTrulyIdle(idleAtPrompt, hasActiveProcs, transcriptActive)` so the exact
19
+ incident is reproducible at the decision boundary in a unit test. Files: `src/core/SessionManager.ts`
20
+ (constant `AGE_GATE_TRANSCRIPT_ACTIVE_MS`, the `isAgeGateTrulyIdle` function, the
21
+ `isTranscriptRecentlyActive` method, the import of `resolveFrameworkTranscriptPath`, and the
22
+ age-gate wiring); tests in `tests/unit/session-timeout-activity-aware.test.ts` and
23
+ `tests/unit/session-manager-behavioral.test.ts`.
24
+
25
+ ## Decision-point inventory
26
+
27
+ - `SessionManager.monitorTick age-gate (ageGateTrulyIdle)` — **modify** — adds a transcript-activity
28
+ defer condition; the kill now requires idle-pane AND no-child-proc AND transcript-not-recently-active.
29
+
30
+ ---
31
+
32
+ ## 1. Over-block
33
+
34
+ **What legitimate inputs does this change reject that it shouldn't?**
35
+
36
+ In reap terms, "over-block" = wrongly DEFERRING a kill. A genuinely-finished session whose
37
+ transcript was touched <2 min before the check is held off for up to one 2-minute window. This is
38
+ harmless: the idle-detection block immediately below the age gate still reaps it once its
39
+ transcript goes quiet. No session is kept alive indefinitely on a stale signal — only real,
40
+ ongoing transcript writes (which require real model activity) sustain the defer.
41
+
42
+ ## 2. Under-block
43
+
44
+ **What failure modes does this still miss?**
45
+
46
+ The change ONLY adds a defer condition; it removes no kill. A session with NO resolvable transcript
47
+ (an unknown framework, or pi-cli which has no resolver case) gets `isTranscriptRecentlyActive ===
48
+ false` and the age gate falls back to EXACTLY today's pane/procs verdict — so such a session is no
49
+ better protected than before (safe degradation, no NEW under-block). The deliberate non-coverage:
50
+ the age gate does not try to be a hard resource ceiling for runaway-but-active sessions — that is
51
+ owned by SessionWatchdog (stuck-process), the context-wedge sentinel, and the CPU/memory-pressure
52
+ reaper, all unchanged.
53
+
54
+ ## 3. Level-of-abstraction fit
55
+
56
+ **Is this at the right layer?**
57
+
58
+ Yes. The SessionReaper (the pressure/idle reap path) ALREADY layers transcript-growth awareness
59
+ (`probeTranscript`/`transcriptDelta`). This change brings the parallel age-kill path to parity at
60
+ the SAME layer using the SAME core resolver (`resolveFrameworkTranscriptPath` from
61
+ `core/FrameworkSessionStore`). It is a cheap detector feeding an existing defer, not a new
62
+ authority and not a re-implementation of an existing primitive. It deliberately does NOT depend on
63
+ `monitoring/transcriptProber` (that would invert core→monitoring layering); it uses the core
64
+ resolver directly.
65
+
66
+ ## 4. Signal vs authority compliance
67
+
68
+ **Does this hold blocking authority with brittle logic, or produce a signal that feeds a smart gate?**
69
+
70
+ It produces a SIGNAL that ADDS a defer (keep) condition to an existing authority. It grants no new
71
+ blocking authority and moves strictly in the safe direction (fewer kills of active sessions). The
72
+ probe is brittle (a single statSync) but its failure mode is bounded by the fail-to-`false`
73
+ contract: a probe error never keeps a session alive — it only declines to add the new defer
74
+ reason, falling through to the existing pane/procs verdict. (Ref: `docs/signal-vs-authority.md`.)
75
+
76
+ ## 5. Interactions
77
+
78
+ **Does it shadow another check, get shadowed, double-fire, or race with adjacent cleanup?**
79
+
80
+ Consistent with the SessionReaper's existing transcript gate (same resolver, same intent). The
81
+ single-point mtime check vs the SessionReaper's cross-tick delta is an intentional, benign
82
+ asymmetry (the SessionReaper requires repeated positive-idle confirmations before it reaps, so the
83
+ two never produce a contradictory kill on the same tick). The idle-detection block below the age
84
+ gate still owns genuinely-idle reaping (the held off path falls through, no `continue`). No new
85
+ kill/revive loop: the change does NOT touch `WorkEvidence`/`ReapGuard.evaluate()`/`workEvidence()`,
86
+ so the documented evaluate-vs-workEvidence disagreement class (2026-06-13 13-session loop) is
87
+ untouched. Adversarial review confirmed loop-safety. The `overAgeButActiveLogged` set already
88
+ prevents log spam on the held off path.
89
+
90
+ ## 6. External surfaces
91
+
92
+ **Does it change anything visible to other agents/users/systems? Timing/state dependence?**
93
+
94
+ No API route, no config key, no hook, no template, no dashboard surface. The only externally
95
+ observable effect: a long-running, actively-working session is no longer terminal-killed at ~5h —
96
+ which is the intended, operator-requested behavior. The new constant is code-level (not a config
97
+ surface). The change reads only a local file's mtime; it depends on the framework runtime writing
98
+ its transcript on activity (true for claude-code, codex-cli, gemini-cli; pi-cli safely degrades).
99
+
100
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
101
+
102
+ **Machine-local BY DESIGN.** Each machine's `SessionManager` reaps only its OWN sessions and reads
103
+ only its OWN local transcripts. There is no shared/replicated/proxied state: the decision is a
104
+ per-session, per-machine read of a local file. No cross-machine surface is introduced or needed —
105
+ a reap decision is inherently local to the machine running the session. No user-facing notice is
106
+ added (so no one-voice gating concern); no durable cross-machine state (so no topic-transfer
107
+ strand); no generated URL.
108
+
109
+ ## 8. Rollback cost
110
+
111
+ Revert the PR — a single-file behavior change plus its tests; no data migration, no agent-state
112
+ repair, no config rollback. As an interim dial without a revert, setting
113
+ `AGE_GATE_TRANSCRIPT_ACTIVE_MS` to 0 makes the new defer condition inert (reverts to the prior
114
+ two-signal behavior). Ships with the dist on update (no PostUpdateMigrator entry needed — the
115
+ change is internal runtime behavior, not an agent-installed file).
@@ -0,0 +1,81 @@
1
+ # Side-Effects Review — Feedback-factory receiver persistence (Option-B receiving end)
2
+
3
+ **Version / slug:** `feedback-receiver-persistence`
4
+ **Date:** `2026-06-11`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `required (receiver/intake decision surface) — see appended response`
7
+
8
+ ## Summary of the change
9
+
10
+ Implements the receiving end of the migration spec's Option-B write seam (docs/specs/feedback-factory-migration.md, Amendment A1 / Q2b, Dawn-confirmed 2026-06-11): the canonical front (feedback-front/ on Vercel) durably persists each ACCEPTED fleet report into a cloud Vercel-Blob inbox, and a new `InboxDrainer` on the operated machine ingests them into a new durable `JsonlFeedbackStore` (format-compatible with the proven AS-IS import artifacts). Files: `src/feedback-factory/inbox/BlobInboxClient.ts` (new — pinned Blob REST, zero deps), `src/feedback-factory/inbox/InboxDrainer.ts` (new), `src/feedback-factory/store/JsonlFeedbackStore.ts` (new), `src/feedback-factory/receiver/BlobInboxStore.ts` (new), `src/feedback-factory/receiver/handlers.ts` (handler made async over a narrow `ReceiverStore` seam — decision logic untouched), `feedback-front/src/feedback.ts` + rebundled `api/feedback.js` (persistence mode behind a deploy-time env token; byte-equivalent Phase-0 behavior without it), server wiring (`AgentServer.ts`, `routes.ts` GET /feedback-inbox/status, `CapabilityIndex.ts`), config type (`types.ts feedbackFactory.receiverPersistence`, dark default), Agent Awareness (`templates.ts`, `PostUpdateMigrator.migrateClaudeMd` + shadow marker). All three test tiers added.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `handleFeedbackSubmit` intake defense chain (rate limit / fingerprint / honeypot / HMAC / validation / dedup) — **pass-through** — made async-capable; decision ORDER, status codes, and messages unchanged (existing unit tests re-asserted as async; the front's HMAC round-trip harness re-run green against the rebuilt bundle).
15
+ - Front mode selection (`inboxToken()` present ⇒ persistence mode, absent ⇒ Phase-0 verify-only) — **add** — a deploy-time structural gate, not a per-request heuristic; no content-based decision.
16
+ - `parseInboxItem` quarantine boundary in the drainer — **add** — classifies an inbox object as ingestable vs malformed. Malformed is PRESERVED (quarantine/), never dropped; no user-visible block.
17
+ - `feedbackFactory.receiverPersistence.enabled` config gate — **add** — dark default; enabled-without-token degrades to dark with a loud boot log line.
18
+
19
+ ---
20
+
21
+ ## 1. Over-block
22
+
23
+ The intake defense chain is untouched, so no new rejection of legitimate reports. The new quarantine boundary could over-classify: a future receiver writing a richer shape would still pass (extra fields flow through `[k: string]: unknown`); only a missing/empty `feedbackId`, `title`, or `description` quarantines — and the receiver structurally always writes those (it validated them two layers earlier). Residual: an operator hand-seeding the inbox with a malformed object gets a quarantined (preserved) object, which is the intended behavior. Precision note (second-pass finding): the validator only empty-checks `feedbackId`; an empty-STRING title/description passes ingest — the safe, less-quarantine direction, and the receiver validated both two layers earlier. No issue identified beyond that.
24
+
25
+ ## 2. Under-block
26
+
27
+ - The per-IP rate limiter on Vercel is per-warm-instance, so a multi-instance burst admits more than 10/hr/IP (the reference's limiter has the same scope on its single deployment; fingerprint + honeypot + HMAC layers are instance-independent). Known accepted gap, flagged in the Phase-0 deploy runbook; durable backing is a deploy-time follow-up. <!-- tracked: topic-12476 -->
28
+ - Front-side dedup (`hasFeedback` = inbox prefix list) cannot see already-DRAINED ids — a retransmit after drain re-enters the inbox. By design: the drainer's canonical-store dedup drops it at ingest (integration test pins this), so end-to-end idempotency holds.
29
+
30
+ ## 3. Level-of-abstraction fit
31
+
32
+ The front is stateless HTTP at the edge (Vercel — correct per spec §2.2); durability is a storage primitive at the platform layer (Blob — correct: a stateless function cannot host a durable buffer, which is exactly why the "forward + buffer in the function" alternative was rejected); the drainer is lightweight always-on at the operated-machine layer (Mini — matches §2.2's own placement taxonomy); the canonical store is data at the disk layer, format-aligned with the import machinery it inherits from. The drainer FEEDS the existing FeedbackStore seam rather than re-implementing any receiver logic. No layer inversion identified.
33
+
34
+ ## 4. Signal vs authority compliance
35
+
36
+ No new blocking authority anywhere. Every accept/reject decision stays in the already-shipped, already-reviewed ported defense chain; the new components are pure transport/persistence; the status route is read-only observability; the quarantine path preserves rather than discards. Compliant with docs/signal-vs-authority.md — this change adds zero brittle checks with authority.
37
+
38
+ ## 5. Interactions
39
+
40
+ - `cutoverReadiness` and the import machinery are untouched; the JsonlFeedbackStore deliberately READS the same JSONL shape `PersistedShadowImportTarget` writes, so the cutover import artifact seeds the canonical store without a translation step (unit test pins alias-id adoption).
41
+ - The handler's async change touches its one existing consumer (its unit test, updated). The front imported only defense fns before; it now also imports the handler — bundled at dev time, no runtime dep added.
42
+ - No double-fire risk: the drainer is single-instance (one server), `drainOnce` is reentrancy-guarded (`ticking`), and delete-after-commit + dedup makes overlap with a retransmit benign.
43
+ - Server boot: the init block is try/caught and null-on-failure (deny-safe 503), so it cannot cascade into adjacent component init.
44
+
45
+ ## 6. External surfaces
46
+
47
+ - New Bearer-gated read-only route `GET /feedback-inbox/status` (+ CapabilityIndex + CLAUDE.md template/migration). No unauthenticated surface added.
48
+ - The front's PERSISTENCE mode changes what a fleet sender sees ONLY in that accepted reports now return the reference receiver's exact response shapes (it runs the faithful handler) — and the mode is unreachable until a Blob token reaches the function's env. Note (second-pass finding): the fallback to the store-injected `BLOB_READ_WRITE_TOKEN` means CONNECTING a Blob store to the Vercel project is itself the enabling act — deliberate, dashboard-gated, on a dedicated project, but it is the precise activation boundary, not "only the cutover deploy". Until a token exists the deployed front's no-token behavior is byte-identical Phase-0 (the 405/503 pre-gate bodies were restored to the original shape after the second-pass caught a dropped cosmetic `phase: 0` field; verify script re-run green against the rebuilt bundle).
49
+ - Blob objects are public-but-unguessable URLs (random suffix ON, pinned by unit test). Report bodies are fleet bug reports — same sensitivity class as today's POST-to-Portal path; pathnames are never predictable, and the drainer reads URLs from list results only. Token lives in env (front: Vercel env; operated machine: `FEEDBACK_INBOX_BLOB_TOKEN`), never in config.json or source.
50
+ - Timing dependence: the drainer is poll-based; a down machine only delays ingestion — the durable inbox is the buffer. That asymmetry is the design's point.
51
+
52
+ ## 7. Rollback cost
53
+
54
+ - Dark by default: fleet installs see only an inert 503 route; no behavior change anywhere until BOTH the config flag and the token exist.
55
+ - Operated instance back-out: set `enabled: false` (or drop the token) and restart — the inbox simply accumulates durably until re-enabled; nothing is lost, nothing dangles.
56
+ - Front back-out: remove the token env from the Vercel project → next invocation is Phase-0 verify-only again. No deploy needed beyond an env change.
57
+ - Full revert of the PR is clean: no migrations of durable data, no schema, no external state created at merge time. The PostUpdateMigrator section-append is idempotent and harmless if orphaned (documentation text only).
58
+
59
+ ---
60
+
61
+ ## Second-pass review response (independent reviewer subagent, 2026-06-11)
62
+
63
+ Concern raised: the artifact's "byte-equivalent Phase-0 behavior without the token" claim was overstated — two responses in the front's no-token path changed. `feedback-front/src/feedback.ts` (405 method-not-allowed) and (503 receiver-not-configured) both dropped the `phase: 0` field that origin/main includes, and these two checks run BEFORE the token gate, so the change was live in today's no-token deployment. Materiality is low: the fleet sender never hits either path (POST-only, secret configured), the verify harness asserts neither, and dropping `phase: 0` there is arguably correct since those responses are now mode-independent — but the artifact asserted byte-equivalence twice and it was checkably false.
64
+
65
+ Everything else audited independently checked out:
66
+ - **Intake chain identical**: the `handlers.ts` diff vs origin/main touches only the new `ReceiverStore` interface, the async signature, and two awaited store calls — decision order, status codes, and messages unchanged; the handlers.test.ts diff reduces to pure async-call-shape edits.
67
+ - **Delete-after-commit + reentrancy real**: `drainOnce` guards on `ticking` (cleared in `finally`); `drainBlob` commits via a synchronous `appendFileSync` before `await client.del`; the quarantine path preserves-then-clears; pagination re-lists the first page with zero-progress and maxBatches guards; the integration test pins retransmit-after-drain dedup.
68
+ - **Dark default holds at all three gates**: server requires `rp?.enabled === true` AND the token env (warns + stays null otherwise); the route 503s on null; the front requires `inboxToken()`. Nuance: the `BLOB_READ_WRITE_TOKEN` fallback means connecting a Blob store to the Vercel project auto-enables persistence mode — deliberate, dashboard-gated, acceptable, but it weakens "unreachable until the cutover deploy".
69
+ - **Signal-vs-authority compliant**: no new blocking authority — `parseInboxItem` is a boundary structural validator whose failure path preserves rather than drops; all accept/reject judgment stays in the pre-existing ported defense chain. Minor imprecision (no action): §1's "empty title/description quarantines" — the code only empty-checks `feedbackId`; empty-string title/description pass (the safe direction).
70
+
71
+ **Resolution (author, same session):** the concern was fixed in the design's favor of byte-identity — `phase: 0` restored on both pre-gate responses (the no-token deployment is now literally byte-identical to origin/main), the front rebundled, and the verify harness re-run green. The two nuances were folded into §1 and §6 above. Iterated and closed.
72
+
73
+ ---
74
+
75
+ ## Post-merge live-API verification (2026-06-13, activation build)
76
+
77
+ Bringing the operated receiver live against the **real** Vercel Blob API surfaced one external-dependency defect not catchable by the stubbed unit tier (§6 follow-up):
78
+
79
+ - **`BlobInboxClient` x-api-version 9 → 7.** The hand-rolled client declared `x-api-version: 9` (carried over from the @vercel/blob source it was pinned against). The production Blob API REJECTS this client's v7-era request shape (pathname-in-URL, simple PUT body) with `400 bad_request "Invalid pathname"` once the declared version is 9+; the newer protocol versions expect a different request format the hand-rolled client does not build. Verified live against PUT/LIST/DELETE: all succeed at `7`, all fail at `9`/`11`. Pinned to `7` (the version whose wire contract matches the request this client builds). One-line fix in the shared client → corrects BOTH the front bundle and the operated-machine drainer. Adopting the @vercel/blob 2.x (`BLOB_API_VERSION=12`) protocol is a larger rewrite, tracked as receiver hardening. **Side-effect class:** external-API contract only — no data-shape, auth, dedup-key, or rollback impact (inbox object layout + drain semantics unchanged). This is the wire-version a request declares, not what it stores.
80
+ - **Activation evidence:** a signed HMAC POST to `https://feedback.dawn-tunnel.dev/api/feedback` returns `200 {received:true}` and the report lands durably in the Blob inbox as `inbox/<feedbackId>.json` with `verified:true, status:unprocessed` — the full §6 external surface exercised end-to-end against the live store.
81
+ - **Operator-env note (§6):** the operated receiver's webhook secret must be set with no trailing bytes — a prod env value of `instar-rising-tide-v1` + a literal backslash-n (NOT a real newline) was found and corrected. `normalizeWebhookSecret`/`trim()` strips real whitespace but NOT a literal `\n`, so a corrupt env value silently fails the HMAC. Set via `printf` (no trailing newline).