instar 1.3.745 → 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.
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +13 -3
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/monitoring/delivery-failure-sentinel/recovery-policy.d.ts +7 -0
- package/dist/monitoring/delivery-failure-sentinel/recovery-policy.d.ts.map +1 -1
- package/dist/monitoring/delivery-failure-sentinel/recovery-policy.js +47 -0
- package/dist/monitoring/delivery-failure-sentinel/recovery-policy.js.map +1 -1
- package/dist/server/routes.d.ts +23 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +112 -16
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/templates/scripts/slack-reply.sh +30 -0
- package/src/templates/scripts/telegram-reply.sh +50 -5
- package/upgrades/1.3.746.md +43 -0
- package/upgrades/side-effects/slack-outbound-robustness-r8m1-arm-b-adapter-timeout.md +76 -0
- package/upgrades/side-effects/slack-outbound-robustness-r8m1-recovery-policy.md +110 -0
- package/upgrades/side-effects/slack-outbound-robustness-slack-delivery-id.md +83 -0
- package/upgrades/side-effects/slack-outbound-robustness-slack-forward-refusal.md +74 -0
- 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 —
|
|
437
|
-
|
|
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:
|
|
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
|
|
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,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,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.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Side-Effects Review — Telegram pre-POST delivery-id mint + 409 Arm C
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-outbound-robustness-telegram-prepost-mint`
|
|
4
|
+
**Date:** `2026-07-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Two coupled fixes to the DEPLOYED Telegram reply script (spec §2.6 round-3 C1 + R8-M1 Arm C):
|
|
11
|
+
|
|
12
|
+
1. **Pre-POST delivery-id mint (the latent double-post fix, task 2).** The deployed `telegram-reply.sh` mints the `delivery_id` at ENQUEUE time — i.e. only AFTER the first POST already failed — so the FIRST send is permanently outside the id-ledger guarantee. If the initial POST is accepted server-side but the response is lost to the script, the script enqueues under a FRESH id and a redrive past the content-dedup window re-POSTs the same message under an id the server never recorded → double-post. The fix mints the UUID BEFORE the initial POST and sends it as `X-Instar-DeliveryId` on the initial send; the server records THAT id the moment the send lands (the route already reads + records the header), and the enqueue reuses the exact same id + a mint-time `attempted_at`, so every redrive of that row is answered `idempotent:true`.
|
|
13
|
+
2. **409 delivery-in-flight → NON-LOSING (Arm C).** The classifier gains a 409 branch: a structured `{ "error": "delivery-in-flight" }` (the §2.4 single-flight reservation race) is RECOVERABLE — enqueued under the same pre-minted id so the sentinel redrives and converges to idempotent. An UNSTRUCTURED 409 stays terminal (default-deny), matching `recovery-policy`'s deployed direction.
|
|
14
|
+
|
|
15
|
+
**Migration Parity:** the current shipped `telegram-reply.sh` SHA (`63ca933e…`) is added to `PostUpdateMigrator.TELEGRAM_REPLY_PRIOR_SHIPPED_SHAS` so deployed agents cleanly upgrade to the pre-POST-mint template on update (rather than getting a `.new` candidate).
|
|
16
|
+
|
|
17
|
+
Files touched: `src/templates/scripts/telegram-reply.sh`, `src/core/PostUpdateMigrator.ts` (SHA-history entry), `tests/unit/telegram-reply-prepost-mint.test.ts`.
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
- `pre-POST DELIVERY_ID mint + ATTEMPTED_AT` — move — from inside the enqueue block to before the initial `curl`. A mint failure (python3 gone) degrades to a headerless send (fail toward delivery) and, on a later recoverable failure, skips the enqueue with the loud note (today's degraded behavior).
|
|
22
|
+
- `X-Instar-DeliveryId` on the initial curl — add — only when a mint succeeded.
|
|
23
|
+
- `409` classifier branch — add — structured `delivery-in-flight` → recoverable; unstructured → terminal.
|
|
24
|
+
- `TELEGRAM_REPLY_PRIOR_SHIPPED_SHAS` — add one entry — migration parity for the template change.
|
|
25
|
+
|
|
26
|
+
## 1. Over-block
|
|
27
|
+
|
|
28
|
+
None. The initial send is unchanged except for one extra header; a mint failure keeps today's headerless send. No legitimate send is newly refused. An unstructured 409 stays terminal exactly as an unstructured 4xx does today (the script had no 409 branch, so a 409 previously fell to the terminal `else` — behavior preserved for the unstructured case).
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
`--max-time` on the initial curl (the §2.6 wedged-script gap + the round-7 phase-aware exit-28 classification) is NOT added in this increment — the deployed initial curl has no `--max-time` today, so this change keeps that exact behavior (a hang is a hang, as today) and only strictly improves the first-send id coverage. The exit-28 phase-aware split is tracked as remaining §2.6 machinery. A pre-POST mint failure still leaves the first send outside the ledger (headerless) — the honest, named degradation, identical to today's queue-write-skip.
|
|
33
|
+
|
|
34
|
+
## 3. Level-of-abstraction fit
|
|
35
|
+
|
|
36
|
+
Yes. The delivery-id lifecycle belongs in the reply script (Layer 1) that owns the send; the route already reads + records the header. Moving the mint earlier in the SAME script is the minimal, correct place.
|
|
37
|
+
|
|
38
|
+
## 4. Signal vs authority compliance
|
|
39
|
+
|
|
40
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
41
|
+
|
|
42
|
+
- [x] No — a shell classifier that keys on an exact structured error code (`delivery-in-flight`) and a header add. It never withholds a message; it relaxes the never-seen-a-409 default toward the loss-free recoverable direction only for the exact structured code.
|
|
43
|
+
|
|
44
|
+
## 5. Interactions
|
|
45
|
+
|
|
46
|
+
The pre-POST id composes with the deployed `/telegram/reply` X-Instar-DeliveryId LRU (records the id on the first successful send → a redrive is idempotent). The 409 branch composes with R8-M1 Arm A (`recovery-policy` retries structured 409). The enqueue's reuse of the pre-minted id is what makes the sentinel's redrive idempotent rather than a fresh double-post. Existing telegram-reply tests (advisory preflight, end-to-end enqueue, --max-time clamp) all still pass.
|
|
47
|
+
|
|
48
|
+
## 6. External surfaces
|
|
49
|
+
|
|
50
|
+
No new route/config/env/CLI. One new outbound header on the initial `/telegram/reply` POST (`X-Instar-DeliveryId`), which the route already handles. One migration SHA entry.
|
|
51
|
+
|
|
52
|
+
## 6b. Operator-surface quality
|
|
53
|
+
|
|
54
|
+
No operator-facing surface changes.
|
|
55
|
+
|
|
56
|
+
## 7. Multi-machine posture
|
|
57
|
+
|
|
58
|
+
The delivery-id + queue are machine-local by design (the failure and its retry belong to the machine that owns the socket). No cross-machine state introduced.
|
|
59
|
+
|
|
60
|
+
## 8. Rollback cost
|
|
61
|
+
|
|
62
|
+
Low: revert the script edits + the SHA entry + the test. A rolled-back binary mints at enqueue again (the deployed latent-double-post behavior). The migration SHA entry is additive and harmless if left.
|
|
63
|
+
|
|
64
|
+
## Conclusion
|
|
65
|
+
|
|
66
|
+
Closes the latent Telegram double-post window (delivery-id minted too late to cover the first attempt) by minting pre-POST and reusing the id at enqueue, and classifies the reservation-race 409 as non-losing — both under the Testing Integrity Standard with a real-script test, and with Migration Parity for the template change.
|
|
67
|
+
|
|
68
|
+
## Second-pass review (if required)
|
|
69
|
+
|
|
70
|
+
Not required — script-side classifier + header add + migration SHA, fail-toward-delivery, reversible, tested against the real shipped script.
|
|
71
|
+
|
|
72
|
+
## Evidence pointers
|
|
73
|
+
|
|
74
|
+
- `src/templates/scripts/telegram-reply.sh` — pre-POST mint, `X-Instar-DeliveryId` header, 409 branch, mint-time `attempted_at`.
|
|
75
|
+
- `src/core/PostUpdateMigrator.ts` — `TELEGRAM_REPLY_PRIOR_SHIPPED_SHAS` entry `63ca933e…`.
|
|
76
|
+
- `tests/unit/telegram-reply-prepost-mint.test.ts` — header-on-initial-send / same-id-enqueue / 409-in-flight-recoverable / 409-unstructured-terminal.
|
|
77
|
+
- `docs/specs/slack-outbound-robustness.md` §2.6, R8-M1 Arm C.
|
|
78
|
+
|
|
79
|
+
## Class-Closure Declaration (display-only mirror)
|
|
80
|
+
|
|
81
|
+
Class: delivery-id-covers-first-attempt (the mint-timing double-post window) on the Telegram lane, plus the 409 member of the R8-M1 status class on the Telegram script. The Slack lane's equivalent (route idempotency + slack-reply.sh pre-POST mint + 409) closes in its own increment.
|