instar 1.3.744 → 1.3.746

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 (25) hide show
  1. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  2. package/dist/core/PostUpdateMigrator.js +13 -3
  3. package/dist/core/PostUpdateMigrator.js.map +1 -1
  4. package/dist/monitoring/delivery-failure-sentinel/recovery-policy.d.ts +7 -0
  5. package/dist/monitoring/delivery-failure-sentinel/recovery-policy.d.ts.map +1 -1
  6. package/dist/monitoring/delivery-failure-sentinel/recovery-policy.js +47 -0
  7. package/dist/monitoring/delivery-failure-sentinel/recovery-policy.js.map +1 -1
  8. package/dist/server/routes.d.ts +23 -0
  9. package/dist/server/routes.d.ts.map +1 -1
  10. package/dist/server/routes.js +112 -16
  11. package/dist/server/routes.js.map +1 -1
  12. package/package.json +4 -2
  13. package/scripts/lint-model-registry-freshness.mjs +192 -0
  14. package/scripts/model-registry-freshness.manifest.json +104 -0
  15. package/src/data/builtin-manifest.json +65 -65
  16. package/src/templates/scripts/slack-reply.sh +30 -0
  17. package/src/templates/scripts/telegram-reply.sh +50 -5
  18. package/upgrades/1.3.745.md +19 -0
  19. package/upgrades/1.3.746.md +43 -0
  20. package/upgrades/side-effects/model-registry-freshness-guard.md +53 -0
  21. package/upgrades/side-effects/slack-outbound-robustness-r8m1-arm-b-adapter-timeout.md +76 -0
  22. package/upgrades/side-effects/slack-outbound-robustness-r8m1-recovery-policy.md +110 -0
  23. package/upgrades/side-effects/slack-outbound-robustness-slack-delivery-id.md +83 -0
  24. package/upgrades/side-effects/slack-outbound-robustness-slack-forward-refusal.md +74 -0
  25. package/upgrades/side-effects/slack-outbound-robustness-telegram-prepost-mint.md +81 -0
@@ -341,6 +341,20 @@ if [ -z "$JSON_BODY" ]; then
341
341
  JSON_BODY="{\"text\":\"${ESCAPED}\"${META_FIELD}}"
342
342
  fi
343
343
 
344
+ # ── delivery-id minted BEFORE the first POST (spec slack-outbound-robustness
345
+ # §2.6, round-3 C1) ──
346
+ # The id is sent as X-Instar-DeliveryId on the INITIAL send so the server
347
+ # records THIS id the moment the send lands. Every later redrive of a
348
+ # recoverable failure then reuses this exact id and is answered
349
+ # `idempotent:true` — closing the latent double-post window that minting at
350
+ # ENQUEUE time left open (the first send was permanently outside the id-ledger
351
+ # guarantee, so a redrive past the content-dedup window re-posted the message
352
+ # under an id the server had never seen). The enqueue below reuses this same
353
+ # DELIVERY_ID and ATTEMPTED_AT. A mint failure (python3 gone) degrades to
354
+ # today's headerless send — fail toward delivery, never a refused send.
355
+ DELIVERY_ID=$(python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null)
356
+ ATTEMPTED_AT=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
357
+
344
358
  # Assemble curl args. Always include X-Instar-AgentId when we can resolve it
345
359
  # from config — the server uses it to reject wrong-port requests before
346
360
  # evaluating the token.
@@ -353,6 +367,9 @@ fi
353
367
  if [ -n "$AGENT_ID" ]; then
354
368
  CURL_ARGS+=(-H "X-Instar-AgentId: ${AGENT_ID}")
355
369
  fi
370
+ if [ -n "$DELIVERY_ID" ]; then
371
+ CURL_ARGS+=(-H "X-Instar-DeliveryId: ${DELIVERY_ID}")
372
+ fi
356
373
 
357
374
  RESPONSE=$(curl "${CURL_ARGS[@]}")
358
375
 
@@ -398,13 +415,33 @@ else
398
415
  # - 5xx, conn-refused (HTTP_CODE=000), DNS failure (also 000)
399
416
  # - 403 with structured `agent_id_mismatch`
400
417
  # - 403 with structured `rate_limited` (sentinel honors Retry-After)
418
+ # - 409 with structured `delivery-in-flight` (R8-M1 Arm C — the reservation
419
+ # race; NON-LOSING, redriven under the same pre-minted id)
401
420
  # NOT recoverable here (already handled above or terminal):
402
421
  # - 200 (success), 408 (ambiguous), 422 (tone gate)
403
- # - 400, 403/revoked, 403 unstructured
422
+ # - 400, 403/revoked, 403 unstructured, 409 unstructured
404
423
  RECOVERABLE=0
405
424
  if [ "$HTTP_CODE" = "000" ] || \
406
425
  ( [ "$HTTP_CODE" -ge 500 ] 2>/dev/null && [ "$HTTP_CODE" -le 599 ] 2>/dev/null ); then
407
426
  RECOVERABLE=1
427
+ elif [ "$HTTP_CODE" = "409" ]; then
428
+ # 409 delivery-in-flight (spec R8-M1 Arm C): the server's §2.4 single-flight
429
+ # reservation saw a concurrent POST for THIS delivery-id still in flight.
430
+ # This is NON-LOSING, never terminal — enqueue under the SAME pre-minted id
431
+ # so the sentinel redrives; by then the first call has resolved (recorded →
432
+ # idempotent, or failed → retryable). recovery-policy classifies structured
433
+ # 409 delivery-in-flight as retry (Arm A). An UNSTRUCTURED 409 is terminal
434
+ # (default-deny) exactly like an unstructured 4xx.
435
+ IN_FLIGHT_CODE=$(echo "$BODY" | python3 -c 'import sys,json
436
+ try:
437
+ print(json.load(sys.stdin).get("error",""))
438
+ except Exception:
439
+ print("")' 2>/dev/null)
440
+ if [ "$IN_FLIGHT_CODE" = "delivery-in-flight" ]; then
441
+ RECOVERABLE=1
442
+ else
443
+ RECOVERABLE=0
444
+ fi
408
445
  elif [ "$HTTP_CODE" = "403" ]; then
409
446
  # Inspect the structured error code in the body. Unstructured 403 is
410
447
  # default-deny per spec § 2b.
@@ -433,11 +470,16 @@ except Exception:
433
470
  SAFE_AGENT_ID=$(printf '%s' "${AGENT_ID:-unknown}" | tr -c 'A-Za-z0-9._-' '_')
434
471
  QUEUE_DB="${QUEUE_DIR}/pending-relay.${SAFE_AGENT_ID}.sqlite"
435
472
 
436
- # delivery_id — UUIDv4 via python3 (already a hard dep above).
437
- DELIVERY_ID=$(python3 -c 'import uuid; print(uuid.uuid4())' 2>/dev/null)
473
+ # delivery_id — the id was minted BEFORE the initial POST and sent on it
474
+ # (spec §2.6 round-3 C1); the enqueue reuses that exact id so a redrive of
475
+ # THIS row is answered idempotent:true by the server that already recorded
476
+ # it. If the pre-POST mint failed (python3 unavailable), the initial send
477
+ # went out headerless — the server never recorded an id, so there is
478
+ # nothing to make the redrive idempotent; skip the enqueue with the loud
479
+ # note (fail toward loudness, exactly today's degraded behavior).
438
480
  if [ -z "$DELIVERY_ID" ]; then
439
481
  echo "Failed (HTTP $HTTP_CODE): $BODY" >&2
440
- echo " (also: failed to generate delivery_id; queue write skipped)" >&2
482
+ echo " (also: no delivery_id was minted pre-POST; queue write skipped)" >&2
441
483
  exit 1
442
484
  fi
443
485
 
@@ -457,7 +499,10 @@ except Exception:
457
499
  TRUNCATED=1
458
500
  fi
459
501
 
460
- ATTEMPTED_AT=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
502
+ # ATTEMPTED_AT was stamped at the PRE-POST mint (spec §2.6 round-5 m3): the
503
+ # 25h-ledger > 24h-row-TTL margin holds only if both clocks anchor at the
504
+ # send, so the enqueue reuses the mint-time stamp rather than re-stamping
505
+ # `now` (which a wedged/slept script would push arbitrarily late).
461
506
  NOW_EPOCH=$(date -u +%s)
462
507
 
463
508
  # Run the queue write through python3's stdlib sqlite3 module — it
@@ -0,0 +1,19 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Repo-internal CI tooling only — a deterministic lint + manifest + unit test that
9
+ flags when per-provider `capable/latest/frontier` model pins go stale. Ships in
10
+ report mode (non-gating); changes no model id and no runtime path. No user-facing
11
+ or agent-facing behavior change.
12
+
13
+ ## What to Tell Your User
14
+
15
+ None — internal change (no user-facing surface).
16
+
17
+ ## Summary of New Capabilities
18
+
19
+ None — internal change (no user-facing surface).
@@ -0,0 +1,43 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Slack outbound delivery robustness (roadmap Phase 2.1) — the first build increments of the converged `docs/specs/slack-outbound-robustness.md`, hardening the Slack (and, where it shares code, Telegram) outbound reply path.
9
+
10
+ - **R8-M1 status composition (the accepted build residual), all three arms:**
11
+ - **Arm A** (already landed): the pure `recovery-policy` classifies a structured `409 delivery-in-flight` as retry-at-backoff, not the generic `4xx → escalate` (which would terminalize a deliverable message and fire a spurious escalation).
12
+ - **Arm B**: `/slack/reply` now bounds the outbound adapter send with a timeout strictly below the reservation TTL and maps a send TIMEOUT to an ambiguous `408`, never the `500` catch-all. A `500` was classified as retry → the sentinel redrove → the message double-posted.
13
+ - **Arm C**: both reply scripts (`slack-reply.sh`, `telegram-reply.sh`) classify a structured `409 delivery-in-flight` as NON-LOSING — never a blind re-send (double-post), never a silent drop.
14
+ - **Latent double-post fix (both channels):** the reply scripts minted the delivery-id at ENQUEUE time — AFTER the first send already failed — so the very first send attempt was outside the idempotency guarantee. An accepted-but-response-lost first send re-posted the same message under an id the server had never seen. The scripts now mint the delivery-id BEFORE the first POST and send it as `X-Instar-DeliveryId` on the initial send; `/slack/reply` reads and records it (mirroring `/telegram/reply`), so a redrive of that send is answered idempotent instead of double-posting.
15
+ - **`/internal/slack-forward` typed refusal:** the route's only deployed semantic was an echo bug (posting inbound user text back out) with zero live callers. It now refuses with `409 misdirected-route` and raises one deduped breadcrumb per boot; the real inbound re-point is Phase 2.2.
16
+
17
+ Migration Parity: the reply-script template changes ship to already-deployed agents via the `PostUpdateMigrator` SHA-history entry (Telegram) and a new feature marker (Slack). Ships dark/internal — no fleet behavior flips on.
18
+
19
+ ## What to Tell Your User
20
+
21
+ - **Slack replies are getting the same delivery hardening Telegram already has**: "I'm closing a gap where, if a Slack reply's network hiccuped at the wrong moment, the same message could go out twice — now it lands exactly once." This is internal plumbing rolling out quietly; nothing for you to turn on.
22
+
23
+ ## Summary of New Capabilities
24
+
25
+ | Capability | How to Use |
26
+ |-----------|-----------|
27
+ | Slack reply delivery-id idempotency | automatic (a repeat send under the same id is answered idempotent, never re-posted) |
28
+ | Adapter-send timeout classified ambiguous, not a server error | automatic |
29
+ | Reply-script 409 delivery-in-flight handled as non-losing | automatic (both Slack and Telegram reply scripts) |
30
+ | `/internal/slack-forward` fenced off until Phase 2.2 | automatic (typed refusal) |
31
+
32
+ ## Evidence
33
+
34
+ Not reproducible in dev without a live Slack workspace plus a mid-send network partition — the end-to-end live proof is the roadmap clause run on the dev agent (spec §7 "Live proof"): kill the network mid-reply, confirm the message arrives exactly once with a recovery audit row, and a manual re-POST of the same delivery-id returns idempotent.
35
+
36
+ The specific failure modes are closed by deterministic semantic tests that a future reader can re-run:
37
+
38
+ - Double-post via mint-timing: `tests/unit/telegram-reply-prepost-mint.test.ts` (`enqueues a recoverable 5xx under the SAME pre-minted id`) and `tests/unit/slack-reply-delivery-id-script.test.ts` (`sends X-Instar-DeliveryId on the POST`) assert the id is minted before the first send and reused — the id the deployed scripts left un-covered on the first attempt.
39
+ - Double-post via 500-on-timeout: `tests/integration/slack-reply-adapter-timeout.test.ts` asserts a send timeout maps to `408` (finalize-ambiguous, never re-posted), not `500` (retry → double-post).
40
+ - 409 non-losing: `tests/unit/recovery-policy.test.ts` (Arm A table cases) + the two script tests assert structured `delivery-in-flight` is retried/non-losing while an unstructured 409 stays terminal.
41
+ - Route idempotency: `tests/integration/slack-reply-delivery-id.test.ts` asserts a repeat same-id POST does not re-send and a FAILED first send does not poison the id (its retry still delivers).
42
+
43
+ All named tests pass (`vitest run`), and `tsc --noEmit` is clean.
@@ -0,0 +1,53 @@
1
+ # Side-effects — Model-registry freshness guard
2
+
3
+ **Change:** add a deterministic, model-id-AGNOSTIC lint that flags when Instar's
4
+ per-provider "capable/latest/frontier" model pins have gone stale, plus its
5
+ manifest (the human-edit frontier allowlist + `lastReviewedAt`) and a unit test.
6
+ Ships **non-gating** (`enforcement: "report"`), dark/reversible.
7
+
8
+ ## Files touched
9
+ - `scripts/lint-model-registry-freshness.mjs` (new) — the checker. Two teeth:
10
+ staleness window + per-door allowlist-membership drift. Exports a pure
11
+ `checkModelRegistryFreshness()` for the test; CLI honors `enforcement`.
12
+ - `scripts/model-registry-freshness.manifest.json` (new) — the single edit
13
+ surface: `frontierAllowlist`, `pins` (file+regex), `lastReviewedAt`,
14
+ `stalenessWindowDays`, `enforcement`, `doors` (door-status map), `flaggedStale`.
15
+ - `tests/unit/model-registry-freshness.test.ts` (new) — 11 tests, both sides of
16
+ both teeth + gating + the shipped manifest's self-consistency.
17
+ - `package.json` — appended the lint to the `lint` chain (runs in report mode,
18
+ exits 0, VISIBLE in CI logs) + `lint:model-freshness[:strict]` aliases.
19
+ - `docs/LLM-ROUTING-REGISTRY.md` — a freshness-guard pointer note.
20
+
21
+ ## Blast radius
22
+ - **CI:** the lint is added to `npm run lint`. In `report` mode it ALWAYS exits
23
+ 0, so it cannot break the build while the current list is known-stale. It only
24
+ prints findings/warnings. Flipping `enforcement: "strict"` (a future,
25
+ operator-gated one-line manifest edit) is what makes it gate.
26
+ - **Runtime:** NONE. This is build/CI tooling only — it never runs in the server,
27
+ never touches a session, never changes model routing. It reads source files
28
+ and a JSON manifest; it writes nothing.
29
+ - **Model IDs:** UNCHANGED. This change deliberately does NOT swap any pinned
30
+ model id — those swaps wait on operator confirmation of exact frontier ids.
31
+ The manifest's allowlist is seeded with the CURRENTLY-pinned ids so drift is
32
+ green today; the known-stale pins are carried in `flaggedStale` as warnings.
33
+
34
+ ## Reversibility
35
+ - Delete the two new scripts + test + revert the `package.json`/doc edits.
36
+ - Or set `enforcement` stays `report` forever (inert-but-visible).
37
+ - No migration, no config default, no state file, no template change.
38
+
39
+ ## Rollback lever
40
+ - Remove `&& node scripts/lint-model-registry-freshness.mjs` from the `lint`
41
+ script, or set the manifest `enforcement` to any value other than `strict`.
42
+
43
+ ## Migration parity
44
+ - N/A — this is repo-internal CI tooling, not an agent-installed file. No hook,
45
+ config default, CLAUDE.md template section, or built-in skill is changed, so
46
+ `PostUpdateMigrator` needs no entry.
47
+
48
+ ## Follow-ups (operator-gated, NOT in this change)
49
+ 1. Confirm exact frontier ids (`gemini-3-pro-preview`?, `gpt-5.6 Sol`?) and swap
50
+ the pins + update `frontierAllowlist` + bump `lastReviewedAt` in one change.
51
+ 2. Then flip `enforcement: "strict"` so the guard gates.
52
+ 3. Door-liveness gaps surfaced by the audit: codex CLI not installed but routed
53
+ to; Homebrew gemini-cli formula deprecated (disabled 2026-12-18).
@@ -0,0 +1,76 @@
1
+ # Side-Effects Review — R8-M1 Arm B: adapter-send timeout → 408 (not 500)
2
+
3
+ **Version / slug:** `slack-outbound-robustness-r8m1-arm-b-adapter-timeout`
4
+ **Date:** `2026-07-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ The second arm of the accepted build residual R8-M1 (spec §2.4 / `accepted-build-residual` frontmatter). The deployed `/slack/reply` handler wraps the outbound adapter call (`ctx.slack.sendToChannel`) in a bare `try/catch` that answers `500` on ANY error (`routes.ts` slack-reply catch). A SLOW send (Slack API accepted the post but the call hasn't returned) that trips the route budget therefore surfaces as `500` → `recovery-policy` classifies `5xx` as RETRY → the sentinel redrives → the message double-posts. The fix bounds the adapter call with an explicit timeout STRICTLY BELOW the §2.4 single-flight reservation TTL (`SLACK_ADAPTER_SEND_TIMEOUT_MS = 30_000` < 60s) via `sendWithAdapterTimeout`; a timeout throws the typed `AdapterSendTimeoutError`, which the catch maps to `408 { error: 'adapter-send-timeout', ambiguous: true }` — the AMBIGUOUS class both the reply scripts (`slack-reply.sh:108-117`) and `recovery-policy` (`finalize-ambiguous`) already treat as NEVER-re-posted. A genuine adapter error still answers `500`.
11
+
12
+ Files touched: `src/server/routes.ts` (the `sendWithAdapterTimeout` helper + `AdapterSendTimeoutError` + `SLACK_ADAPTER_SEND_TIMEOUT_MS` constant, and the `/slack/reply` send-and-catch wiring), `tests/unit/adapter-send-timeout.test.ts` (both sides of the boundary), `tests/integration/slack-reply-adapter-timeout.test.ts` (route mapping: timeout→408, real-error→500, success→200).
13
+
14
+ ## Decision-point inventory
15
+
16
+ - `sendWithAdapterTimeout(send, timeoutMs)` — add — races the send against a bounded timer; on timeout rejects `AdapterSendTimeoutError` and abandons the still-running send. Clears the timer in `finally` (no dangling handle).
17
+ - `AdapterSendTimeoutError` — add — a typed marker so the catch can distinguish "ambiguous timeout" from a real server error without string-sniffing.
18
+ - `SLACK_ADAPTER_SEND_TIMEOUT_MS = 30_000` — add — pinned strictly below the §2.4 reservation TTL (60s) so a still-in-flight handler can never outlive its reservation and race a retry.
19
+ - `/slack/reply` catch — modify — `AdapterSendTimeoutError` → `408 ambiguous`; every other error keeps today's `500`.
20
+
21
+ ## 1. Over-block
22
+
23
+ None. The change only RE-CLASSIFIES a timeout that used to be a `500`. A `408` is strictly SAFER than the `500` it replaces (408 → finalize-ambiguous vs 500 → retry → double-post). No legitimate delivery is newly rejected; a successful send still answers `200`, a genuine error still `500`.
24
+
25
+ ## 2. Under-block
26
+
27
+ The 30s budget is a heuristic ceiling: a send that returns at 29.9s is delivered normally; one at 30.1s is abandoned and reported ambiguous even though it may still land. That is the CORRECT ambiguous direction (the message MAY have posted → never blindly re-post). The full §2.4 single-flight reservation (a later increment) is what makes the ambiguous outcome converge to exactly-once via the durable id-ledger + content dedup; this arm only stops the 500→retry→double-post terminal. The Telegram `sendToTopic` path is out of scope for Arm B (its own error handling is unchanged; the spec cites the slack 500 catch-all as the grounded defect).
28
+
29
+ ## 3. Level-of-abstraction fit
30
+
31
+ Yes. The timeout wrapper sits at the route — the single place that owns the HTTP response class for a send — and the classification (`AdapterSendTimeoutError` → 408) is one visible branch in the existing catch. The wrapper is a pure, injectable-timeout helper unit-testable in isolation; the route test proves the mapping.
32
+
33
+ ## 4. Signal vs authority compliance
34
+
35
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
36
+
37
+ - [x] No — this is deterministic transport classification, not a brittle string-matcher gaining blocking authority. It relaxes a double-post-causing 500 toward the loss-free ambiguous 408; it never withholds a message (the tone gate remains the sole withholding authority, unchanged).
38
+
39
+ ## 5. Interactions
40
+
41
+ Composes with R8-M1 Arm A (recovery-policy already maps 408 → finalize-ambiguous) and with the deployed `slack-reply.sh` 408 branch (verify-before-resend guidance). The §2.4 single-flight reservation (a later increment) reads the same 30s budget as its `adapter-call timeout < reservation TTL` invariant. No change to the success or genuine-error paths.
42
+
43
+ ## 6. External surfaces
44
+
45
+ New exported symbols (internal): `sendWithAdapterTimeout`, `AdapterSendTimeoutError`, `SLACK_ADAPTER_SEND_TIMEOUT_MS`. New response shape on `/slack/reply`: `408 { error: 'adapter-send-timeout', ambiguous: true }` on a send timeout (previously a `500`). No new route, config key, env var, or CLI.
46
+
47
+ ## 6b. Operator-surface quality
48
+
49
+ No operator-facing surface changes — the 408 is consumed by the reply script + sentinel, which already render ambiguous guidance to the agent.
50
+
51
+ ## 7. Multi-machine posture
52
+
53
+ The timeout wrapper is machine-local and stateless (a per-request timer). No cross-machine state.
54
+
55
+ ## 8. Rollback cost
56
+
57
+ Trivial: revert the `/slack/reply` catch branch + the three symbols + the two test files. A rolled-back binary answers `500` on a slow send again (the deployed behavior). No schema, config, or persisted state.
58
+
59
+ ## Conclusion
60
+
61
+ A minimal, deterministic route-level classification that closes the R8-M1 Arm B regression (a slow adapter send terminalizing as 500 → retry → double-post) by mapping an adapter-send timeout to the ambiguous 408 both the script and recovery-policy already handle as never-re-posted.
62
+
63
+ ## Second-pass review (if required)
64
+
65
+ Not required — pure additive transport classification, fail-toward-not-double-posting direction, trivially reversible, both boundary sides tested.
66
+
67
+ ## Evidence pointers
68
+
69
+ - `src/server/routes.ts` — `sendWithAdapterTimeout`, `AdapterSendTimeoutError`, `SLACK_ADAPTER_SEND_TIMEOUT_MS`, the `/slack/reply` send wrap + catch mapping.
70
+ - `tests/unit/adapter-send-timeout.test.ts` — resolves-fast / hangs-past-budget / real-error-propagates / TTL-invariant.
71
+ - `tests/integration/slack-reply-adapter-timeout.test.ts` — 408-on-timeout / 500-on-real-error / 200-on-success.
72
+ - `docs/specs/slack-outbound-robustness.md` §2.4, `accepted-build-residual` frontmatter (arm b).
73
+
74
+ ## Class-Closure Declaration (display-only mirror)
75
+
76
+ Class: the adapter-send-timeout member of the R8-M1 status-composition class. Arm A (recovery-policy 409) and this Arm B (route timeout→408) both close status members that would otherwise drive a double-post; Arm C (script 409 classification) closes the last member in its own increment.
@@ -0,0 +1,110 @@
1
+ # Side-Effects Review — R8-M1 Arm A: recovery-policy `409 delivery-in-flight → retry`
2
+
3
+ **Version / slug:** `slack-outbound-robustness-r8m1-recovery-policy`
4
+ **Date:** `2026-07-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ The accepted build-phase residual R8-M1 (Arm A) from the slack-outbound-robustness review ceremony, settled in code with a test — the first build increment of `docs/specs/slack-outbound-robustness.md`. The §2.4 single-flight reservation (a later increment) makes `/slack/reply` and `/telegram/reply` answer a concurrent same-`delivery-id` POST with a structured `409 { error: 'delivery-in-flight' }`. The DEPLOYED pure `recovery-policy.ts` classifies every unlisted 4xx via `if (httpCode >= 400 && httpCode < 500) → escalate` (`:189`), so on the raw-HTTP Telegram redrive lane (which has no funnel typed-result mapping table) a routine reservation race would terminalize a deliverable message and fire a spurious operator escalation. The fix adds ONE named, tested exception: a structured `409 delivery-in-flight` retries at the existing §3c backoff (bounded by the same MAX_ATTEMPTS + 24h TTL caps as every transport retry); an unstructured/unknown 409 keeps the deployed default-deny (escalate). This reconciles the spec-wide "recovery-policy stays byte-untouched" invariant HONESTLY — as a single visible exception, not a silent one. Files touched: `src/monitoring/delivery-failure-sentinel/recovery-policy.ts` (the 409 branch + a `parseErrorCode` helper + an exported `DELIVERY_IN_FLIGHT_ERROR` wire-string constant) and `tests/unit/recovery-policy.test.ts` (six new table cases).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `recovery-policy.evaluatePolicy 409 branch` — add — a structured `409 delivery-in-flight` retries at backoff; every other 409 escalates (default-deny). Placed before the generic `4xx → escalate` so only the exact structured code is rescued.
15
+ - `DELIVERY_IN_FLIGHT_ERROR` constant — add — the single source of truth for the wire string; the route (later increment) references it so policy and route can never drift.
16
+ - `parseErrorCode` helper — add — parses `{ error: string }`; distinct from the untouched `parse403` so the deployed 403 path stays byte-identical.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ **What legitimate inputs does this change reject that it shouldn't?**
23
+
24
+ None new. The change only RESCUES a status the deployed code escalated (409): structured `delivery-in-flight` now retries instead of terminalizing. An unstructured 409 still escalates exactly as the deployed generic-4xx branch did — no legitimate delivery is newly rejected. The retry is bounded by the identical MAX_ATTEMPTS + TTL caps, so a pathological forever-racing 409 still escalates loudly rather than looping (P19).
25
+
26
+ ---
27
+
28
+ ## 2. Under-block
29
+
30
+ **What failure modes does this still miss?**
31
+
32
+ If a non-instar server ever returned `409 { error: 'delivery-in-flight' }` for a genuinely terminal conflict, this would retry it 9 times before escalating — bounded, loud, never silent, and not a real deployment shape (only the instar reservation route emits this exact structured body, and the `/whoami` gate already protects against redriving through a foreign server config). Arms B (adapter-timeout → 408) and C (script 409 classification) are settled in their own increments (the §2.4 route work and the §2.6 script refresh); this increment is Arm A only.
33
+
34
+ ---
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ **Is this at the right layer?**
39
+
40
+ Yes. The pure `recovery-policy` module is exactly where HTTP-status → action classification belongs — it is the single deterministic authority for the raw-HTTP redrive lanes, exhaustively table-tested. Putting the 409 rule here (rather than a translation shim in the Telegram redrive caller) makes the exception ONE visible branch in the enumerable decision table, which is where the round-8 reviewers said it belongs.
41
+
42
+ ---
43
+
44
+ ## 4. Signal vs authority compliance
45
+
46
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
47
+
48
+ **Does this change hold blocking authority with brittle logic?**
49
+
50
+ - [x] No — this is deterministic policy classification in a pure module, not a brittle string-matcher gaining new blocking authority.
51
+
52
+ The branch keys on an EXACT structured error code (`delivery-in-flight`) parsed from JSON, not a substring/heuristic. It relaxes an escalate toward retry (the never-lose-a-message direction); it never withholds a message. Authority over what withholds still rests with the tone gate (unchanged).
53
+
54
+ ---
55
+
56
+ ## 5. Interactions
57
+
58
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
59
+
60
+ It is consumed by `DeliveryFailureSentinel`'s redrive loop for the Telegram lane (raw HTTP → `evaluatePolicy`) and is the safety net if any Slack row ever reaches `evaluatePolicy` with a raw 409. The Slack lane primarily maps `delivery-in-flight` → retry through the §2.3 funnel typed-result table (a later increment); both paths converge on the same "retry, no breaker arm, no attention noise" outcome. `reasonToCategory` is unaffected on the retry path (a `delivery_in_flight_*` reason only reaches escalate on exhaustion, where it falls to the existing `unstructured_403` category — a rare, correct catch-all).
61
+
62
+ ---
63
+
64
+ ## 6. External surfaces
65
+
66
+ No new routes, config keys, env vars, or CLI in this increment. `DELIVERY_IN_FLIGHT_ERROR` is an internal exported constant.
67
+
68
+ ---
69
+
70
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
71
+
72
+ No operator-facing surface changes in this increment.
73
+
74
+ ---
75
+
76
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
77
+
78
+ `recovery-policy` is a pure, machine-local, stateless function — no cross-machine state. Classification is identical on every machine by construction (no clock, no I/O; `now` is injected).
79
+
80
+ ---
81
+
82
+ ## 8. Rollback cost
83
+
84
+ Trivial and isolated: revert the single 409 branch + helper + constant + the six test cases. No schema, no config, no migration, no persisted state. A rolled-back binary simply escalates a structured 409 again (the deployed behavior) — loud, never a misdelivery.
85
+
86
+ ---
87
+
88
+ ## Conclusion
89
+
90
+ A minimal, deterministic, bounded, test-backed exception that closes the R8-M1 Arm A regression (a routine reservation race terminalizing a deliverable Telegram message) while keeping the "recovery-policy byte-untouched" invariant honest as a single visible branch.
91
+
92
+ ---
93
+
94
+ ## Second-pass review (if required)
95
+
96
+ Not required — pure additive classification branch in a fully table-tested module, fail-toward-delivery direction, trivially reversible.
97
+
98
+ ---
99
+
100
+ ## Evidence pointers
101
+
102
+ - `src/monitoring/delivery-failure-sentinel/recovery-policy.ts` — the 409 branch, `parseErrorCode`, `DELIVERY_IN_FLIGHT_ERROR`.
103
+ - `tests/unit/recovery-policy.test.ts` — describe block "evaluatePolicy — 409 delivery-in-flight (spec R8-M1 Arm A)": structured→retry@backoff, MAX_ATTEMPTS→escalate, TTL→escalate, unstructured→escalate, other-structured→escalate, no-body→escalate.
104
+ - `docs/specs/slack-outbound-robustness.md` §2.3/§2.4, `accepted-build-residual` frontmatter; `docs/specs/reports/slack-outbound-robustness-round8-findings.md` §"The blocking finding".
105
+
106
+ ---
107
+
108
+ ## Class-Closure Declaration (display-only mirror)
109
+
110
+ Class: raw-HTTP status composition against the deployed `recovery-policy` classifier. This increment closes the 409 member (Arm A). Arms B/C close in their own increments; the exhaustive recovery-policy table test is the standing guard that any future status keeps composing.
@@ -0,0 +1,83 @@
1
+ # Side-Effects Review — Slack delivery-id idempotency + pre-POST mint + 409 Arm C
2
+
3
+ **Version / slug:** `slack-outbound-robustness-slack-delivery-id`
4
+ **Date:** `2026-07-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ The Slack lane of the double-post fix + R8-M1 Arm C (spec §2.4 / §2.6):
11
+
12
+ 1. **`/slack/reply` delivery-id idempotency (§2.4).** The route now reads `X-Instar-DeliveryId`; a repeat POST with a seen id returns `200 { idempotent: true }` WITHOUT re-sending. The id is recorded ONLY after a successful send (a failed send — including the ambiguous 408 timeout — never poisons the id, so its legitimate retry still delivers). Mirrors the deployed `/telegram/reply` behavior, reusing the same route-scoped LRU helpers.
13
+ 2. **`slack-reply.sh` pre-POST mint (task 2).** The script mints the UUID BEFORE the first POST and sends it as `X-Instar-DeliveryId` on the initial send — so the server records THAT id the moment the send lands, closing the first-attempt double-post window the deployed headerless send left open. A mint failure degrades to today's headerless send (fail toward delivery).
14
+ 3. **`slack-reply.sh` 409 → NON-LOSING (Arm C).** A structured `409 { error: 'delivery-in-flight' }` (the §2.4 reservation race) is non-losing: the script does NOT re-send (the in-flight call under the same id owns delivery) and does NOT drop — exit 0. An UNSTRUCTURED 409 is a genuine conflict → terminal exit 1.
15
+
16
+ **Migration Parity:** a new `slack-reply-feature: delivery-id` marker is added to the script and set as the `migrateReplyScriptTo408` `featureMarker`, so a deployed thread-ts-arg-only slack-reply.sh is refreshed on update (the new template contains BOTH markers).
17
+
18
+ Files touched: `src/server/routes.ts` (`/slack/reply` delivery-id read + record), `src/templates/scripts/slack-reply.sh` (marker, pre-POST mint, 409 branch), `src/core/PostUpdateMigrator.ts` (featureMarker bump), `tests/integration/slack-reply-delivery-id.test.ts`, `tests/unit/slack-reply-delivery-id-script.test.ts`.
19
+
20
+ ## Decision-point inventory
21
+
22
+ - `/slack/reply` delivery-id read/record — add — idempotent-200 on a seen id; record only after success. Reuses the existing `deliveryLruHas`/`deliveryLruRecord` (route-scoped, TTL-aware).
23
+ - `slack-reply.sh` pre-POST mint + header — add — id born before the send; mint-failure degrades to headerless.
24
+ - `slack-reply.sh` 409 branch — add — structured delivery-in-flight → exit 0 non-losing; unstructured → exit 1.
25
+ - `featureMarker: 'slack-reply-feature: delivery-id'` — change — migration parity for the template change.
26
+
27
+ ## 1. Over-block
28
+
29
+ None. Idempotency only suppresses a re-send of an ALREADY-seen id (the point). A send with no delivery-id header is never gated. The unstructured-409 terminal path matches the deployed `else` fall-through (the script had no 409 branch before, so any 409 was terminal — the structured case is a strict rescue toward non-losing).
30
+
31
+ ## 2. Under-block
32
+
33
+ The route id-ledger is IN-MEMORY (deployed-Telegram parity), so a restart clears it — a redrive after a restart at a ≥15-min backoff step past the content-dedup window can still double-post. That is the exact §2.4 durable-ledger gap, tracked as a follow-up increment (OQ-4). This increment closes the WITHIN-process first-attempt window (the mint-timing double-post) on Slack; the durable ledger closes the cross-restart window. slack-reply.sh has no queue tail yet (§2.6 full port is a later increment), so a recoverable Slack send failure still fails loudly rather than enqueueing — the honest, named residual (today's behavior for everything but 409/408).
34
+
35
+ ## 3. Level-of-abstraction fit
36
+
37
+ Yes. Route-level idempotency belongs at the route (it owns the HTTP response); the delivery-id lifecycle belongs in the script that owns the send. Both mirror the Telegram precedent exactly.
38
+
39
+ ## 4. Signal vs authority compliance
40
+
41
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
42
+
43
+ - [x] No — deterministic id-equality idempotency + a shell classifier keying on an exact structured code. Neither withholds a message on judgment; the tone gate remains the sole withholding authority.
44
+
45
+ ## 5. Interactions
46
+
47
+ The route idempotency + the script pre-POST mint compose: the initial send records the id, a redrive of the same id is answered idempotent. The 408 (Arm B) and 409 (Arm C) branches converge on never-double-posting. No change to the success or genuine-error paths; existing slack reply-route + thread-route tests pass unchanged.
48
+
49
+ ## 6. External surfaces
50
+
51
+ `/slack/reply` now honors `X-Instar-DeliveryId` (new idempotent-200 shape). New script marker line. No new route/config/env/CLI.
52
+
53
+ ## 6b. Operator-surface quality
54
+
55
+ No operator-facing surface changes.
56
+
57
+ ## 7. Multi-machine posture
58
+
59
+ Delivery-id state is machine-local (the in-memory LRU lives in the process that owns the socket). No cross-machine state.
60
+
61
+ ## 8. Rollback cost
62
+
63
+ Low: revert the `/slack/reply` read/record block, the script edits, the featureMarker, and the two tests. A rolled-back binary ignores the header (deployed behavior).
64
+
65
+ ## Conclusion
66
+
67
+ Closes the Slack first-attempt double-post window (pre-POST mint + route idempotency) and classifies the reservation-race 409 as non-losing on the Slack script, mirroring the Telegram lane, under the Testing Integrity Standard with real-route + real-script tests and Migration Parity for the template change.
68
+
69
+ ## Second-pass review (if required)
70
+
71
+ Not required — route idempotency + script classifier mirroring an established deployed pattern, fail-toward-delivery, reversible, both surfaces tested.
72
+
73
+ ## Evidence pointers
74
+
75
+ - `src/server/routes.ts` — `/slack/reply` X-Instar-DeliveryId read + record-after-success.
76
+ - `src/templates/scripts/slack-reply.sh` — pre-POST mint, header, 409 branch, feature marker.
77
+ - `src/core/PostUpdateMigrator.ts` — `slack-reply-feature: delivery-id` featureMarker.
78
+ - `tests/integration/slack-reply-delivery-id.test.ts`, `tests/unit/slack-reply-delivery-id-script.test.ts`.
79
+ - `docs/specs/slack-outbound-robustness.md` §2.4, §2.6, R8-M1 Arm C.
80
+
81
+ ## Class-Closure Declaration (display-only mirror)
82
+
83
+ Class: delivery-id-covers-first-attempt (mint-timing double-post) on the Slack lane + the 409 member of the R8-M1 status class on the Slack script. The cross-restart window (durable id-ledger, §2.4) and the full slack-reply.sh queue tail (§2.6) remain as tracked follow-up increments.
@@ -0,0 +1,74 @@
1
+ # Side-Effects Review — /internal/slack-forward typed refusal (§2.7)
2
+
3
+ **Version / slug:** `slack-outbound-robustness-slack-forward-refusal`
4
+ **Date:** `2026-07-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Spec §2.7 (round-1 M6). As deployed, `POST /internal/slack-forward` took `{channelId, text}` and called `ctx.slack.sendToChannel(channelId, text)` — but its only caller, `SlackLifeline.forwardToServer`, forwards INBOUND user messages, so the route's sole live semantic is an ECHO BUG (posting the user's own message back at them). `SlackLifeline` is written but never instantiated, so this echo path has never run live. Both round-2 externals rejected gate-only: gating an echo defect still ships an echo defect the day `SlackLifeline` is wired. The change makes the route a typed refusal: it returns `409 { error: 'misdirected-route', detail: '…re-point owned by Phase 2.2…' }` and raises ONE deduped attention breadcrumb per boot. Bearer auth is preserved (the route stays inside the authed router). The real inbound path (session injection, mirroring `/internal/telegram-forward`) is Phase 2.2.
11
+
12
+ Files touched: `src/server/routes.ts` (the route body + a boot-once breadcrumb latch), `tests/integration/slack-forward-refusal.test.ts` (new), `tests/integration/slack-mrkdwn-reply-route.test.ts` (updated the old echo-behavior assertion to the refusal contract).
13
+
14
+ ## Decision-point inventory
15
+
16
+ - `/internal/slack-forward` body — replace — the `sendToChannel` echo with a `409 misdirected-route` typed refusal.
17
+ - `slackForwardBreadcrumbRaised` latch — add — a createRoutes-scoped boolean so the attention breadcrumb fires at most once per boot (a caller loop can't flood the attention queue).
18
+
19
+ ## 1. Over-block
20
+
21
+ The route no longer "delivers" anything — but there was no legitimate delivery to block: its only traffic was an inbound message being echoed back out, which is a defect, not a feature. Fail-toward-delivery deliberately does NOT apply here (argued, not assumed): delivering this route's traffic means posting the user's own inbound text at them. The refusal is the loss-free direction.
22
+
23
+ ## 2. Under-block
24
+
25
+ The route still accepts and 409s any payload (it doesn't validate `channelId`/`text` first) — intentional: the point is that NO shape is legitimate on this route until Phase 2.2 re-points it. An unauthed request is still rejected by the router's auth middleware before reaching the handler (unchanged). The full session-injection re-point (the inbound exactly-once ledger + sentinel intercept) stays Phase 2.2 by design — this increment only closes the echo hazard window.
26
+
27
+ ## 3. Level-of-abstraction fit
28
+
29
+ Yes. The refusal lives at the route that owns the response; the one-time breadcrumb latch is route-scoped state alongside the other per-boot route state.
30
+
31
+ ## 4. Signal vs authority compliance
32
+
33
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
34
+
35
+ - [x] No — a static typed refusal, no heuristic, no blocking authority over content. It removes an outbound send entirely (strictly less exposure).
36
+
37
+ ## 5. Interactions
38
+
39
+ Composes with the rest of the Slack outbound hardening: the legitimate outbound path is `/slack/reply` (now idempotent + timeout-bounded); this route is fenced off until Phase 2.2 gives it a real inbound semantic. The attention breadcrumb rides the existing `ctx.telegram.createAttentionItem` best-effort surface.
40
+
41
+ ## 6. External surfaces
42
+
43
+ `/internal/slack-forward` response changes from `200 { ok }` / `500` to a fixed `409 { error: 'misdirected-route', detail }`. One deduped attention item id `slack-forward-misdirected-route`. No new route/config/env/CLI.
44
+
45
+ ## 6b. Operator-surface quality
46
+
47
+ The one attention breadcrumb is plain-English and names the owner (Phase 2.2) — an operator seeing it understands the route was hit and that a re-point is pending, not a live failure.
48
+
49
+ ## 7. Multi-machine posture
50
+
51
+ The breadcrumb latch is per-process (per boot, per machine) — a benign duplicate across machines is bounded to one-per-boot-per-machine and deduped by stable attention id. No shared state.
52
+
53
+ ## 8. Rollback cost
54
+
55
+ Trivial: restore the `sendToChannel` body + latch + tests. A rolled-back binary echoes again (the deployed defect) — which is exactly what this closes, so rollback is a conscious regression, not a silent one.
56
+
57
+ ## Conclusion
58
+
59
+ Closes the `/internal/slack-forward` echo-bug hazard with a typed 409 refusal + a one-time breadcrumb, deferring the real inbound re-point to Phase 2.2, under the Testing Integrity Standard with route tests (refusal + no-post + once-per-boot breadcrumb).
60
+
61
+ ## Second-pass review (if required)
62
+
63
+ Not required — a static route refusal removing an outbound send, reversible, tested both directions.
64
+
65
+ ## Evidence pointers
66
+
67
+ - `src/server/routes.ts` — `/internal/slack-forward` 409 refusal + `slackForwardBreadcrumbRaised`.
68
+ - `tests/integration/slack-forward-refusal.test.ts` — 409 refusal + once-per-boot breadcrumb.
69
+ - `tests/integration/slack-mrkdwn-reply-route.test.ts` — updated to the refusal contract (was the echo assertion).
70
+ - `docs/specs/slack-outbound-robustness.md` §2.7.
71
+
72
+ ## Class-Closure Declaration (display-only mirror)
73
+
74
+ Class: the ungated internal Slack outbound route (the audit's "one internal route bypasses even that"). This increment fences it off entirely (409 refusal) until Phase 2.2 gives it a real inbound semantic.