instar 1.3.485 → 1.3.486
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/MessagingToneGate.d.ts +23 -2
- package/dist/core/MessagingToneGate.d.ts.map +1 -1
- package/dist/core/MessagingToneGate.js +15 -1
- package/dist/core/MessagingToneGate.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +21 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +22 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/TelegramRelay.d.ts +1 -0
- package/dist/core/TelegramRelay.d.ts.map +1 -1
- package/dist/core/TelegramRelay.js +9 -1
- package/dist/core/TelegramRelay.js.map +1 -1
- package/dist/core/raw-file-path.d.ts +33 -0
- package/dist/core/raw-file-path.d.ts.map +1 -0
- package/dist/core/raw-file-path.js +105 -0
- package/dist/core/raw-file-path.js.map +1 -0
- package/dist/messaging/OutboundAdvisory.d.ts +152 -0
- package/dist/messaging/OutboundAdvisory.d.ts.map +1 -0
- package/dist/messaging/OutboundAdvisory.js +453 -0
- package/dist/messaging/OutboundAdvisory.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +6 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +4 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/pending-relay-store.d.ts +7 -0
- package/dist/messaging/pending-relay-store.d.ts.map +1 -1
- package/dist/messaging/pending-relay-store.js +9 -3
- package/dist/messaging/pending-relay-store.js.map +1 -1
- package/dist/monitoring/delivery-failure-sentinel.d.ts +6 -1
- package/dist/monitoring/delivery-failure-sentinel.d.ts.map +1 -1
- package/dist/monitoring/delivery-failure-sentinel.js +15 -3
- package/dist/monitoring/delivery-failure-sentinel.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +8 -0
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +22 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +12 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +216 -3
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +67 -67
- package/src/scaffold/templates.ts +7 -1
- package/src/templates/scripts/telegram-reply.sh +202 -10
- package/upgrades/1.3.486.md +38 -0
- package/upgrades/side-effects/outbound-advisory-inform-only.md +252 -0
|
@@ -18,6 +18,28 @@
|
|
|
18
18
|
# --stdin-base64 Decode stdin/argument text from base64 before sending.
|
|
19
19
|
# Use this for content that may contain shell syntax or
|
|
20
20
|
# heredoc delimiters such as a literal EOF line.
|
|
21
|
+
# --ack-advisory Acknowledge a preflight advisory and send the message
|
|
22
|
+
# unchanged. The preflight still runs (so the override is
|
|
23
|
+
# audited) but never withholds. FLAG ONLY — there is
|
|
24
|
+
# deliberately no env form (a standing env export would be
|
|
25
|
+
# a blanket pre-ack that silently disables the inform
|
|
26
|
+
# layer; spec outbound-jargon-filepath-gap §2.4(4)).
|
|
27
|
+
#
|
|
28
|
+
# Outbound advisory preflight (inform-only — spec outbound-jargon-filepath-gap §2.4):
|
|
29
|
+
# When this send comes from an automated LLM job session (the scheduler
|
|
30
|
+
# stamps INSTAR_MESSAGE_KIND=automated + INSTAR_SENDER_CLASS=llm-session
|
|
31
|
+
# into the session env — the model types nothing), the script first asks
|
|
32
|
+
# the server's deterministic detectors about the text. If they flag
|
|
33
|
+
# something (raw file path, dev jargon, localhost link), the message is
|
|
34
|
+
# NOT sent yet: the advisory prints to stdout (so the agent reads it in
|
|
35
|
+
# its transcript) and the script exits 0. The agent then either fixes the
|
|
36
|
+
# text and re-runs, or re-runs with --ack-advisory to send unchanged.
|
|
37
|
+
# The advisory layer NEVER blocks: the ack path always delivers past it,
|
|
38
|
+
# and every error path (server down, timeout, bad JSON) proceeds straight
|
|
39
|
+
# to the send as if the preflight returned nothing. Script-class senders
|
|
40
|
+
# (INSTAR_SENDER_CLASS=script) skip the preflight — there is no agent to
|
|
41
|
+
# inform. Conversational sessions have none of these env vars and are
|
|
42
|
+
# completely unaffected.
|
|
21
43
|
#
|
|
22
44
|
# Port resolution (in order):
|
|
23
45
|
# 1. INSTAR_PORT environment variable (explicit operator override).
|
|
@@ -33,6 +55,7 @@
|
|
|
33
55
|
|
|
34
56
|
FORMAT=""
|
|
35
57
|
STDIN_BASE64=0
|
|
58
|
+
ACK_ADVISORY=0
|
|
36
59
|
|
|
37
60
|
# Parse leading flags before positional args.
|
|
38
61
|
while [ $# -gt 0 ]; do
|
|
@@ -49,6 +72,10 @@ while [ $# -gt 0 ]; do
|
|
|
49
72
|
STDIN_BASE64=1
|
|
50
73
|
shift
|
|
51
74
|
;;
|
|
75
|
+
--ack-advisory)
|
|
76
|
+
ACK_ADVISORY=1
|
|
77
|
+
shift
|
|
78
|
+
;;
|
|
52
79
|
--)
|
|
53
80
|
shift
|
|
54
81
|
break
|
|
@@ -117,11 +144,14 @@ v = c.get('authToken', '')
|
|
|
117
144
|
print(v if isinstance(v, str) else '')
|
|
118
145
|
print(c.get('projectName', ''))
|
|
119
146
|
print(c.get('port', ''))
|
|
147
|
+
t = (((c.get('messaging') or {}).get('outboundAdvisory') or {}).get('timeoutMs', ''))
|
|
148
|
+
print(t if isinstance(t, (int, float)) else '')
|
|
120
149
|
" 2>/dev/null)
|
|
121
150
|
CONFIG_AUTH=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '1p')
|
|
122
151
|
[ -z "$AUTH_TOKEN" ] && AUTH_TOKEN="$CONFIG_AUTH"
|
|
123
152
|
AGENT_ID=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '2p')
|
|
124
153
|
CONFIG_PORT=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '3p')
|
|
154
|
+
CONFIG_ADVISORY_TIMEOUT_MS=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '4p')
|
|
125
155
|
fi
|
|
126
156
|
|
|
127
157
|
if [ -n "$INSTAR_PORT" ]; then
|
|
@@ -133,21 +163,162 @@ else
|
|
|
133
163
|
echo "WARN: telegram-reply.sh — no INSTAR_PORT env and no port in .instar/config.json; falling back to 4040" >&2
|
|
134
164
|
fi
|
|
135
165
|
|
|
136
|
-
#
|
|
137
|
-
|
|
166
|
+
# ── Outbound advisory preflight (inform-only; spec outbound-jargon-filepath-gap §2.4) ──
|
|
167
|
+
# Validate the scheduler-stamped env values against the literal enums BEFORE
|
|
168
|
+
# use (an unexpected value forwards nothing — the server additionally coerces
|
|
169
|
+
# unknowns server-side).
|
|
170
|
+
MESSAGE_KIND=""
|
|
171
|
+
case "${INSTAR_MESSAGE_KIND:-}" in
|
|
172
|
+
reply|health-alert|unknown|automated) MESSAGE_KIND="$INSTAR_MESSAGE_KIND" ;;
|
|
173
|
+
esac
|
|
174
|
+
SENDER_CLASS=""
|
|
175
|
+
case "${INSTAR_SENDER_CLASS:-}" in
|
|
176
|
+
script|llm-session) SENDER_CLASS="$INSTAR_SENDER_CLASS" ;;
|
|
177
|
+
esac
|
|
178
|
+
# Job slug rides the metadata for server-side audit keying; charset-clamped
|
|
179
|
+
# here so it can never carry quotes/injection into a JSON or SQL context.
|
|
180
|
+
JOB_SLUG=$(printf '%s' "${INSTAR_JOB_SLUG:-}" | tr -c 'A-Za-z0-9._-' '_' | head -c 128)
|
|
181
|
+
|
|
182
|
+
ADVISORY_CODES_CSV=""
|
|
183
|
+
if [ "$MESSAGE_KIND" = "automated" ] && [ "$SENDER_CLASS" = "llm-session" ]; then
|
|
184
|
+
# Timeout: config messaging.outboundAdvisory.timeoutMs (default 2000ms),
|
|
185
|
+
# converted ms→SECONDS for curl --max-time with ceil division, clamped to
|
|
186
|
+
# [1, 10]. A raw `--max-time 2000` would be a ~33-minute fail-HANG and
|
|
187
|
+
# `$((MS/1000))` on values <1000 would yield 0 = no timeout — both invert
|
|
188
|
+
# the fail-open contract.
|
|
189
|
+
ADV_MS="${CONFIG_ADVISORY_TIMEOUT_MS:-2000}"
|
|
190
|
+
case "$ADV_MS" in (*[!0-9]*|'') ADV_MS=2000 ;; esac
|
|
191
|
+
ADV_SECS=$(( (ADV_MS + 999) / 1000 ))
|
|
192
|
+
[ "$ADV_SECS" -lt 1 ] && ADV_SECS=1
|
|
193
|
+
[ "$ADV_SECS" -gt 10 ] && ADV_SECS=10
|
|
194
|
+
|
|
195
|
+
# Preflight body via python3 (already a hard dependency of this script). If
|
|
196
|
+
# python3 is unavailable the preflight is skipped entirely — fail-open.
|
|
197
|
+
PREFLIGHT_BODY=$(PF_TOPIC="$TOPIC_ID" PF_SLUG="$JOB_SLUG" PF_KIND="$MESSAGE_KIND" python3 -c '
|
|
198
|
+
import sys, json, os
|
|
199
|
+
print(json.dumps({
|
|
200
|
+
"text": sys.stdin.read(),
|
|
201
|
+
"messageKind": os.environ.get("PF_KIND", "automated"),
|
|
202
|
+
"topicId": int(os.environ.get("PF_TOPIC") or 0),
|
|
203
|
+
"jobSlug": os.environ.get("PF_SLUG", ""),
|
|
204
|
+
}))
|
|
205
|
+
' <<<"$MSG" 2>/dev/null)
|
|
206
|
+
|
|
207
|
+
if [ -n "$PREFLIGHT_BODY" ]; then
|
|
208
|
+
PREFLIGHT_CURL=(-s -X POST "http://localhost:${PORT}/messaging/preflight"
|
|
209
|
+
-H 'Content-Type: application/json'
|
|
210
|
+
--max-time "$ADV_SECS"
|
|
211
|
+
-d "$PREFLIGHT_BODY")
|
|
212
|
+
if [ -n "$AUTH_TOKEN" ]; then
|
|
213
|
+
PREFLIGHT_CURL+=(-H "Authorization: Bearer ${AUTH_TOKEN}")
|
|
214
|
+
fi
|
|
215
|
+
if [ -n "$AGENT_ID" ]; then
|
|
216
|
+
PREFLIGHT_CURL+=(-H "X-Instar-AgentId: ${AGENT_ID}")
|
|
217
|
+
fi
|
|
218
|
+
PREFLIGHT_RESP=$(curl "${PREFLIGHT_CURL[@]}" 2>/dev/null)
|
|
219
|
+
|
|
220
|
+
# Parse advisories — every failure mode (empty/timeout/malformed/disabled)
|
|
221
|
+
# yields an empty result and the send proceeds (fail-OPEN end-to-end).
|
|
222
|
+
ADVISORY_RENDERED=$(printf '%s' "$PREFLIGHT_RESP" | python3 -c '
|
|
138
223
|
import sys, json
|
|
224
|
+
try:
|
|
225
|
+
resp = json.load(sys.stdin)
|
|
226
|
+
advisories = resp.get("advisories") or []
|
|
227
|
+
except Exception:
|
|
228
|
+
advisories = []
|
|
229
|
+
codes = ",".join(a.get("code", "") for a in advisories if isinstance(a, dict) and a.get("code"))
|
|
230
|
+
print(codes)
|
|
231
|
+
for a in advisories:
|
|
232
|
+
if not isinstance(a, dict):
|
|
233
|
+
continue
|
|
234
|
+
print("- " + str(a.get("code", "")))
|
|
235
|
+
m = a.get("match")
|
|
236
|
+
if m:
|
|
237
|
+
# Inert, delimited, quoted token under a fixed label — never spliced
|
|
238
|
+
# into instruction-shaped prose (injection-pinned rendering).
|
|
239
|
+
print(" detected: " + json.dumps(str(m)[:120]))
|
|
240
|
+
g = a.get("guidance")
|
|
241
|
+
if g:
|
|
242
|
+
print(" guidance: " + str(g))
|
|
243
|
+
' 2>/dev/null)
|
|
244
|
+
ADVISORY_CODES_CSV=$(printf '%s\n' "$ADVISORY_RENDERED" | sed -n '1p')
|
|
245
|
+
ADVISORY_DETAIL=$(printf '%s\n' "$ADVISORY_RENDERED" | sed '1d')
|
|
246
|
+
|
|
247
|
+
if [ -n "$ADVISORY_CODES_CSV" ] && [ "$ACK_ADVISORY" != "1" ]; then
|
|
248
|
+
# Inform the sender BEFORE the user sees anything. The FIRST line is
|
|
249
|
+
# machine-unmissable and literal (the E2E asserts this exact string).
|
|
250
|
+
# Exit 0 is deliberate: non-zero means delivery failure and triggers
|
|
251
|
+
# queue/retry semantics in callers; an advisory is neither — the message
|
|
252
|
+
# was deliberately not yet sent and the next move belongs to the agent.
|
|
253
|
+
echo "NOT SENT — advisory (fix and re-run, or re-run with --ack-advisory to send unchanged)"
|
|
254
|
+
echo ""
|
|
255
|
+
echo "The outbound advisory flagged this automated message BEFORE delivery:"
|
|
256
|
+
printf '%s\n' "$ADVISORY_DETAIL"
|
|
257
|
+
echo ""
|
|
258
|
+
echo "Next move (yours — the advisory layer never blocks):"
|
|
259
|
+
echo " 1. FIX: revise the message and re-run this script (preferred)."
|
|
260
|
+
echo " 2. SEND AS-IS: re-run with --ack-advisory to deliver unchanged (the override is audited)."
|
|
261
|
+
exit 0
|
|
262
|
+
fi
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# ── Serialized kind metadata — computed ONCE, shared by both body builders
|
|
267
|
+
# AND both queue writers (a queued send must carry the metadata whole so the
|
|
268
|
+
# sentinel redrive doesn't mis-kind it / drop an ack — spec §2.5). Every
|
|
269
|
+
# component is enum-validated or charset-clamped above, so this fragment is
|
|
270
|
+
# safe to interpolate into JSON and (parameterized) SQL contexts.
|
|
271
|
+
METADATA_JSON=""
|
|
272
|
+
if [ -n "$MESSAGE_KIND" ] || [ -n "$SENDER_CLASS" ] || [ -n "$JOB_SLUG" ]; then
|
|
273
|
+
META_PARTS=""
|
|
274
|
+
[ -n "$MESSAGE_KIND" ] && META_PARTS="\"messageKind\":\"${MESSAGE_KIND}\""
|
|
275
|
+
if [ -n "$SENDER_CLASS" ]; then
|
|
276
|
+
[ -n "$META_PARTS" ] && META_PARTS="${META_PARTS},"
|
|
277
|
+
META_PARTS="${META_PARTS}\"senderClass\":\"${SENDER_CLASS}\""
|
|
278
|
+
fi
|
|
279
|
+
if [ -n "$JOB_SLUG" ]; then
|
|
280
|
+
[ -n "$META_PARTS" ] && META_PARTS="${META_PARTS},"
|
|
281
|
+
META_PARTS="${META_PARTS}\"jobSlug\":\"${JOB_SLUG}\""
|
|
282
|
+
fi
|
|
283
|
+
if [ "$ACK_ADVISORY" = "1" ] && [ "$MESSAGE_KIND" = "automated" ] && [ "$SENDER_CLASS" = "llm-session" ]; then
|
|
284
|
+
# REQUIRED annotation (§2.4(4)) — how the server audits "acked" as the
|
|
285
|
+
# single writer. Carries the overridden codes, including [] for a
|
|
286
|
+
# preemptive ack on a clean message (itself a signal).
|
|
287
|
+
ACK_CODES_JSON=$(printf '%s' "$ADVISORY_CODES_CSV" | tr -cd 'A-Z_,' | awk -F',' '{out=""; for(i=1;i<=NF;i++){if($i!=""){out=out (out==""?"":",") "\""$i"\""}} print "["out"]"}')
|
|
288
|
+
[ -z "$ACK_CODES_JSON" ] && ACK_CODES_JSON="[]"
|
|
289
|
+
[ -n "$META_PARTS" ] && META_PARTS="${META_PARTS},"
|
|
290
|
+
META_PARTS="${META_PARTS}\"advisoryAck\":true,\"advisoryCodes\":${ACK_CODES_JSON}"
|
|
291
|
+
fi
|
|
292
|
+
METADATA_JSON="{${META_PARTS}}"
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# Build JSON body (text + optional format + optional kind metadata). A
|
|
296
|
+
# conversational session (no kind env) produces the identical body it
|
|
297
|
+
# always did.
|
|
298
|
+
JSON_BODY=$(B_META="$METADATA_JSON" python3 -c '
|
|
299
|
+
import sys, json, os
|
|
139
300
|
msg = sys.argv[1]
|
|
140
301
|
fmt = sys.argv[2]
|
|
141
302
|
body = {"text": msg}
|
|
142
303
|
if fmt:
|
|
143
304
|
body["format"] = fmt
|
|
305
|
+
meta_raw = os.environ.get("B_META", "")
|
|
306
|
+
if meta_raw:
|
|
307
|
+
try:
|
|
308
|
+
body["metadata"] = json.loads(meta_raw)
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
144
311
|
print(json.dumps(body))
|
|
145
312
|
' "$MSG" "$FORMAT" 2>/dev/null)
|
|
146
313
|
|
|
147
314
|
if [ -z "$JSON_BODY" ]; then
|
|
148
315
|
# Fallback if python3 not available: basic escape, no format override.
|
|
316
|
+
# BOTH builders must carry the kind fields, or a python-degraded agent
|
|
317
|
+
# silently drops them and the incident class recurs (spec §2.1).
|
|
149
318
|
ESCAPED=$(printf '%s' "$MSG" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g')
|
|
150
|
-
|
|
319
|
+
META_FIELD=""
|
|
320
|
+
[ -n "$METADATA_JSON" ] && META_FIELD=",\"metadata\":${METADATA_JSON}"
|
|
321
|
+
JSON_BODY="{\"text\":\"${ESCAPED}\"${META_FIELD}}"
|
|
151
322
|
fi
|
|
152
323
|
|
|
153
324
|
# Assemble curl args. Always include X-Instar-AgentId when we can resolve it
|
|
@@ -293,6 +464,7 @@ except Exception:
|
|
|
293
464
|
Q_PORT="$PORT" \
|
|
294
465
|
Q_ATTEMPTED_AT="$ATTEMPTED_AT" \
|
|
295
466
|
Q_TRUNCATED="$TRUNCATED" \
|
|
467
|
+
Q_METADATA="$METADATA_JSON" \
|
|
296
468
|
Q_DB_PATH="$QUEUE_DB" \
|
|
297
469
|
python3 -c '
|
|
298
470
|
import os, sqlite3, sys, json, datetime
|
|
@@ -324,7 +496,8 @@ try:
|
|
|
324
496
|
state TEXT NOT NULL,
|
|
325
497
|
claimed_by TEXT,
|
|
326
498
|
status_history TEXT NOT NULL DEFAULT "[]",
|
|
327
|
-
truncated INTEGER NOT NULL DEFAULT 0
|
|
499
|
+
truncated INTEGER NOT NULL DEFAULT 0,
|
|
500
|
+
message_metadata TEXT
|
|
328
501
|
)""")
|
|
329
502
|
# Idempotent column add for older schemas.
|
|
330
503
|
try:
|
|
@@ -332,6 +505,13 @@ try:
|
|
|
332
505
|
except sqlite3.OperationalError as e:
|
|
333
506
|
if "duplicate column name" not in str(e):
|
|
334
507
|
raise
|
|
508
|
+
# message_metadata (kind/senderClass/advisoryAck) — the redrive must carry
|
|
509
|
+
# the metadata whole (spec outbound-jargon-filepath-gap §2.5). Idempotent.
|
|
510
|
+
try:
|
|
511
|
+
conn.execute("ALTER TABLE entries ADD COLUMN message_metadata TEXT")
|
|
512
|
+
except sqlite3.OperationalError as e:
|
|
513
|
+
if "duplicate column name" not in str(e):
|
|
514
|
+
raise
|
|
335
515
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_state_next ON entries(state, next_attempt_at)")
|
|
336
516
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_text_hash_topic ON entries(text_hash, topic_id)")
|
|
337
517
|
# 5s dedup window (spec § 2b step 2).
|
|
@@ -355,8 +535,8 @@ try:
|
|
|
355
535
|
delivery_id, topic_id, text_hash, text, format,
|
|
356
536
|
http_code, error_body, attempted_port,
|
|
357
537
|
attempted_at, attempts, next_attempt_at,
|
|
358
|
-
state, claimed_by, status_history, truncated
|
|
359
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NULL, "queued", NULL, ?, ?)""",
|
|
538
|
+
state, claimed_by, status_history, truncated, message_metadata
|
|
539
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NULL, "queued", NULL, ?, ?, ?)""",
|
|
360
540
|
(
|
|
361
541
|
os.environ["Q_DELIVERY_ID"],
|
|
362
542
|
int(os.environ["Q_TOPIC_ID"]),
|
|
@@ -369,6 +549,7 @@ try:
|
|
|
369
549
|
os.environ["Q_ATTEMPTED_AT"],
|
|
370
550
|
initial_history,
|
|
371
551
|
int(os.environ.get("Q_TRUNCATED", "0")),
|
|
552
|
+
os.environ.get("Q_METADATA") or None,
|
|
372
553
|
),
|
|
373
554
|
)
|
|
374
555
|
conn.commit()
|
|
@@ -386,6 +567,15 @@ except Exception as exc:
|
|
|
386
567
|
# with IF NOT EXISTS so this is safe even if the Python path
|
|
387
568
|
# partially ran.
|
|
388
569
|
printf '%s' "$QUEUE_TEXT" > "${QUEUE_DB}.tmp.text"
|
|
570
|
+
# message_metadata rides via a temp file + readfile (same pattern as the
|
|
571
|
+
# text BLOB) so no shell value is ever interpolated into SQL — the
|
|
572
|
+
# metadata is enum-built, but the no-raw-interpolation rule holds anyway
|
|
573
|
+
# (spec outbound-jargon-filepath-gap §2.5).
|
|
574
|
+
META_SQL_VALUE="NULL"
|
|
575
|
+
if [ -n "$METADATA_JSON" ]; then
|
|
576
|
+
printf '%s' "$METADATA_JSON" > "${QUEUE_DB}.tmp.meta"
|
|
577
|
+
META_SQL_VALUE="CAST(readfile('${QUEUE_DB}.tmp.meta') AS TEXT)"
|
|
578
|
+
fi
|
|
389
579
|
sqlite3 "$QUEUE_DB" >/dev/null 2>&1 <<SQL
|
|
390
580
|
PRAGMA journal_mode=WAL;
|
|
391
581
|
PRAGMA synchronous=NORMAL;
|
|
@@ -405,22 +595,24 @@ CREATE TABLE IF NOT EXISTS entries (
|
|
|
405
595
|
state TEXT NOT NULL,
|
|
406
596
|
claimed_by TEXT,
|
|
407
597
|
status_history TEXT NOT NULL DEFAULT '[]',
|
|
408
|
-
truncated INTEGER NOT NULL DEFAULT 0
|
|
598
|
+
truncated INTEGER NOT NULL DEFAULT 0,
|
|
599
|
+
message_metadata TEXT
|
|
409
600
|
);
|
|
601
|
+
ALTER TABLE entries ADD COLUMN message_metadata TEXT;
|
|
410
602
|
CREATE INDEX IF NOT EXISTS idx_state_next ON entries(state, next_attempt_at);
|
|
411
603
|
CREATE INDEX IF NOT EXISTS idx_text_hash_topic ON entries(text_hash, topic_id);
|
|
412
604
|
INSERT OR IGNORE INTO entries (
|
|
413
605
|
delivery_id, topic_id, text_hash, text, format,
|
|
414
606
|
http_code, error_body, attempted_port, attempted_at,
|
|
415
|
-
attempts, state, status_history, truncated
|
|
607
|
+
attempts, state, status_history, truncated, message_metadata
|
|
416
608
|
) VALUES (
|
|
417
609
|
'$DELIVERY_ID', $TOPIC_ID, '$TEXT_HASH',
|
|
418
610
|
CAST(readfile('${QUEUE_DB}.tmp.text') AS BLOB), $( [ -n "$FORMAT" ] && printf "'%s'" "$FORMAT" || echo "NULL"),
|
|
419
611
|
$HTTP_CODE, NULL, $PORT, '$ATTEMPTED_AT',
|
|
420
|
-
1, 'queued', '[]', $TRUNCATED
|
|
612
|
+
1, 'queued', '[]', $TRUNCATED, $META_SQL_VALUE
|
|
421
613
|
);
|
|
422
614
|
SQL
|
|
423
|
-
rm -f "${QUEUE_DB}.tmp.text" 2>/dev/null
|
|
615
|
+
rm -f "${QUEUE_DB}.tmp.text" "${QUEUE_DB}.tmp.meta" 2>/dev/null
|
|
424
616
|
chmod 600 "$QUEUE_DB" 2>/dev/null
|
|
425
617
|
fi
|
|
426
618
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Implements `docs/specs/outbound-jargon-filepath-gap.md` (r2, converged + approved) — the durable Structure-over-Willpower fix for the 2026-06-10 incident where a background job sent the operator a reminder containing dev jargon and a raw repo path, bypassing both standing standards because nothing told the infrastructure the message was an automated alert. One PR, no deferrals:
|
|
9
|
+
|
|
10
|
+
- **Structural message kind (§2.1)** — the scheduler stamps `INSTAR_MESSAGE_KIND=automated` + `INSTAR_JOB_SLUG` + `INSTAR_SENDER_CLASS` into job session environments (BOTH `SessionManager.spawnSession` spawn lanes — headless AND rerouted-interactive — plus `JobScheduler.runScriptJob` with `senderClass=script`). The model types nothing; interactive sessions get none of these and stay `'reply'`. The `messageKind` union widens with `'automated'` (single-sourced as `MessageKind` in MessagingToneGate; all five prior sites now use it) + a `renderMessageKind` branch describing the kind to the authority. NO new block rule and NO B12 re-scope — the operator constraint is zero new blocking power.
|
|
11
|
+
- **Jargon signal single-sourced (§2.2)** — `evaluateOutbound` computes `detectJargon` itself for non-`reply` kinds (`health-alert | automated`), replacing the `options.jargon` caller opt-in (the one vestigial caller is cleaned up). Conversational replies deliberately get NO jargon signal (over-block scope). Live-config kill switch `messaging.outboundFloor.jargonAlways` (absence = on).
|
|
12
|
+
- **Raw-file-path SIGNAL (§2.3)** — new `detectRawFilePath` (`src/core/raw-file-path.ts`), a linear-regex sibling of `localhost-link.ts`: indexOf prescreen, bounded segments, never matches inside an `http(s)://` URL, match echo bounded to 120 chars and stopping before `?` (an adjacent secret can never ride along), fail-OPEN at every call site. Feeds `signals.filePath` to the existing authority on all kinds — anchoring the B2_FILE_PATH judgment it already has.
|
|
13
|
+
- **The preflight advisory (§2.4)** — `telegram-reply.sh` (template) gains an inform-only preflight, active ONLY for `automated` + `llm-session` senders: it calls the new `POST /messaging/preflight` (deterministic detectors only, no LLM; 64KB cap; runtime enum coercion at both boundaries) and on findings prints an advisory whose FIRST line is the literal `NOT SENT — advisory (fix and re-run, or re-run with --ack-advisory to send unchanged)` and exits 0 — the message is withheld until the SENDER resolves it (fix, or `--ack-advisory`, both always deliver past the advisory layer). Fail-OPEN end-to-end: route down / timeout / malformed JSON / disabled → the send proceeds untouched. The curl timeout converts config ms→seconds with ceil + a [1,10]s clamp (a raw `--max-time 2000` would be a ~33-minute fail-HANG). Flag-only ack (no env form — a standing pre-ack would silently disable the inform layer).
|
|
14
|
+
- **Server-side single-writer audit + escalation (§2.4(5–6))** — every preflight outcome is one JSONL line in `logs/outbound-advisory.jsonl` (`clean|advised|acked`; `acked` written only by `/telegram/reply` when a send carries the REQUIRED `metadata.advisoryAck` + overridden codes); size-rotated; read via the new `GET /messaging/advisory-log` (bounded tail, never the whole file). An in-memory write-time index drives the repeated-ignore escalation: N unresolved advised per signature (default 3) or per-slug aggregate across topics (default 5) raises ONE deduped HIGH Attention item with the FIXED `sourceContext: outbound-advisory-escalation` (P17 — the per-source topic budget genuinely binds); an interleaved clean heartbeat does NOT reset the count (reset-gaming closed); a near-identical clean re-send (the fix landing) resolves it; habitual `--ack-advisory` overriding raises the preemptive-ack consumer (NORMAL).
|
|
15
|
+
- **The kind survives every hop (§2.5)** — BOTH `telegram-reply.sh` body builders (python3 + sed fallback) forward `metadata.{messageKind,senderClass,jobSlug}`; BOTH script queue writers AND `PendingRelayStore` gain a nullable `message_metadata` column via the existing idempotent-ALTER conventions (a pre-column legacy DB upgrades on open — tested); the `DeliveryFailureSentinel` redrive forwards the metadata whole (an acked-then-queued send still lands its `acked` row — no escalation false-fire on transient delivery failure); the cross-machine relay hop (`sendToTopic → outboundRelay → relayOutbound`) carries it to the holder. Observability breadcrumbs (never gates): kindless send to a job-session topic (exempt for a redrive whose `X-Instar-DeliveryId` matches a REAL queue row), automated send with no recent preflight row (class-spoof visibility), declared `script` class on a non-script job.
|
|
16
|
+
- **Migration parity (§5)** — the current live `telegram-reply.sh` SHA (`3e30b2cd…`) joins `TELEGRAM_REPLY_PRIOR_SHIPPED_SHAS` so deployed stock scripts actually receive the new template; CLAUDE.md template + `migrateClaudeMd` gain the "Outbound advisory for automated messages" awareness section; the hand-curl `/telegram/reply` example is REPLACED by the relay-script form (no printed map to the bypass); the section is registered in `migrateFrameworkShadowCapabilities` markers[] (Codex/Gemini mirrors) and the feature-delivery-completeness guard.
|
|
17
|
+
|
|
18
|
+
## What to Tell Your User
|
|
19
|
+
|
|
20
|
+
- "My background jobs' messages now get a quality check before they reach you: if an automated reminder contains a raw file path, internal dev jargon, or a link that only works on my machine, the job is told to fix it BEFORE you ever see it — and the fix guidance is built in. Nothing can get stuck: the check informs the sender, it never blocks, and if a job keeps ignoring its own feedback you get one consolidated heads-up instead of silence."
|
|
21
|
+
- "Your normal conversations with me are completely unaffected — this only touches messages composed by scheduled background jobs."
|
|
22
|
+
|
|
23
|
+
## Summary of New Capabilities
|
|
24
|
+
|
|
25
|
+
| Capability | How to Use |
|
|
26
|
+
|-----------|-----------|
|
|
27
|
+
| Advisory preflight on automated sends | Automatic — scheduler-stamped; job models type nothing |
|
|
28
|
+
| `--ack-advisory` send-as-is override | Re-run `telegram-reply.sh --ack-advisory …` after an advisory (audited) |
|
|
29
|
+
| Advisory audit trail | `GET /messaging/advisory-log?limit=50` (Bearer-authed) |
|
|
30
|
+
| Repeated-ignore escalation | Automatic — one deduped Attention item per misbehaving job |
|
|
31
|
+
| Rollback levers (live, no restart) | `messaging.outboundAdvisory.enabled:false` · `messaging.outboundFloor.jargonAlways:false` |
|
|
32
|
+
|
|
33
|
+
## Evidence
|
|
34
|
+
|
|
35
|
+
- Founding incident reproduction shape covered end-to-end: the REAL shipped `telegram-reply.sh` run under bash with a recording curl shim proves (1) a raw-path automated send does NOT deliver on the first call, prints the LITERAL `NOT SENT — advisory…` first line, exits 0; (2) corrected text delivers; (3) `--ack-advisory` delivers the original and the send body carries `advisoryAck` + codes; (4) preflight network-failure AND malformed-JSON both DELIVER (fail-open proof); (5) a conversational session makes NO preflight call and produces the byte-identical legacy body (`tests/unit/telegram-reply-advisory-script.test.ts`, 12 tests).
|
|
36
|
+
- 26 detector tests incl. a 4KB-pathological-input bounded-time ReDoS test and the adjacent-secret echo bound (`raw-file-path.test.ts`); 17 audit/escalation tests incl. the reset-gaming case (interleaved clean does NOT reset) and the per-slug topic-varying aggregate (`outbound-advisory.test.ts`); 15 route tests incl. "the server DELIVERS a raw-path automated message exactly as today — never blocks" (`outbound-advisory-routes.test.ts`); queue pre-column-DB ALTER + sentinel metadata-forward tests (`pending-relay-metadata.test.ts`); relay-hop body test (`relay-kind-forward.test.ts`); spawn-lane env assertions (`session-manager-behavioral.test.ts`).
|
|
37
|
+
- Tier-3 alive: the REAL AgentServer on the production init path answers `POST /messaging/preflight` 200-with-advisories and `GET /messaging/advisory-log` 200, enforces Bearer auth (401 without), writes the JSONL where documented, and the live-config kill switch flips the preflight off WITHOUT a restart (`tests/e2e/outbound-advisory-alive.test.ts`, 5 tests).
|
|
38
|
+
- `tsc --noEmit` exit 0; full unit suite green before push.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<!-- DRAFT — completed in Phase 4 of /instar-dev. Phase 1 principle check + Phase 2 plan recorded below first. -->
|
|
2
|
+
|
|
3
|
+
# Side-Effects Review — Outbound advisory: jargon + raw-file-path gaps for automated senders (inform-only)
|
|
4
|
+
|
|
5
|
+
**Version / slug:** `outbound-advisory-inform-only`
|
|
6
|
+
**Date:** `2026-06-11`
|
|
7
|
+
**Author:** `echo (instar-dev agent)`
|
|
8
|
+
**Second-pass reviewer:** `required — outbound-messaging decision surface (Phase 5)`
|
|
9
|
+
|
|
10
|
+
## Phase 1 — Principle check (recorded before any code)
|
|
11
|
+
|
|
12
|
+
**Does this change involve a decision point?** YES — it touches the outbound-messaging
|
|
13
|
+
pipeline, the single most sensitive decision surface in instar. The signal-vs-authority
|
|
14
|
+
principle applies directly, and the design (per the converged + approved spec
|
|
15
|
+
`docs/specs/outbound-jargon-filepath-gap.md`, r2) was shaped around it:
|
|
16
|
+
|
|
17
|
+
- `detectRawFilePath` (new) and `detectJargon` (re-scoped) are pure DETECTORS — regex,
|
|
18
|
+
cheap, fail-open, no blocking power. They feed the EXISTING authority
|
|
19
|
+
(`MessagingToneGate.review`) as signals. No new rule, no re-scoped block rule.
|
|
20
|
+
- The preflight advisory is honestly named in the spec as a "default-withhold the sender
|
|
21
|
+
always resolves" — the sending agent keeps final authority (fix or `--ack-advisory`,
|
|
22
|
+
both always deliver past the advisory layer). It never consults a judge, never
|
|
23
|
+
escalates against the sender, fails open on every error path.
|
|
24
|
+
- The repeated-ignore escalation informs the OPERATOR (one deduped Attention item),
|
|
25
|
+
gates nothing.
|
|
26
|
+
- Governing operator constraint (Justin, 2026-06-10): zero new blocking authority.
|
|
27
|
+
r1's deterministic 422 floor was DELETED in r2 for exactly this reason.
|
|
28
|
+
|
|
29
|
+
## Phase 2 — Plan (recorded before any code)
|
|
30
|
+
|
|
31
|
+
**Build location:** fresh worktree `.worktrees/echo-outbound-advisory-build`, branch
|
|
32
|
+
`echo/outbound-advisory-build`, based off `JKHeadley/main` @ v1.3.484 (verified:
|
|
33
|
+
`git remote -v` = JKHeadley https, `package.json` 1.3.484, git identity
|
|
34
|
+
`Instar Agent (echo) <echo@instar.local>`).
|
|
35
|
+
|
|
36
|
+
**Implementation order:**
|
|
37
|
+
1. `detectRawFilePath` detector (sibling of `localhost-link.ts`, same linear-regex discipline) + unit tests.
|
|
38
|
+
2. `messageKind` union widening (`'automated'`) in all five cited sites + `renderMessageKind` branch.
|
|
39
|
+
3. `checkOutboundMessage`: always-compute jargon for non-`reply` kinds (drop the `options.jargon` opt-in; fix the one vestigial caller); feed `signals.filePath`.
|
|
40
|
+
4. `POST /messaging/preflight` + `GET /messaging/advisory-log` + server-side single-writer JSONL audit + in-memory escalation index + Attention escalation (fixed sourceContext).
|
|
41
|
+
5. Scheduler env injection: BOTH SessionManager spawn env blocks (headless + rerouted-interactive) + JobScheduler `runScriptJob` env (`INSTAR_MESSAGE_KIND`, `INSTAR_JOB_SLUG`, `INSTAR_SENDER_CLASS`).
|
|
42
|
+
6. `telegram-reply.sh` template: metadata forwarding in BOTH body builders, preflight loop + `--ack-advisory`, curl timeout ms→s clamp, queue metadata column in BOTH script queue writers (quote-safe).
|
|
43
|
+
7. `PendingRelayStore` COLUMN_ADDS metadata column + `DeliveryFailureSentinel.postReply` threading + redrive exemption via `X-Instar-DeliveryId` validated against a real queue row.
|
|
44
|
+
8. Relay-hop forwarding (3-layer widening) so kind/senderClass/ack survive the cross-machine hop.
|
|
45
|
+
9. `/telegram/reply`: read `metadata.messageKind` → `checkOutboundMessage`; `acked` audit row; kindless-job-send + class-spoof breadcrumbs; senderClass validation against the job definition.
|
|
46
|
+
10. Config plumbing via LiveConfig (live re-read; `?? true` defaults — rollback without restart).
|
|
47
|
+
11. PostUpdateMigrator: current live telegram-reply SHA → allowlist; CLAUDE.md template section (add advisory awareness, remove hand-curl example); `migrateFrameworkShadowCapabilities` marker.
|
|
48
|
+
12. All three test tiers per spec §6; release fragment; this artifact completed; trace; commit; PR; CI; merge (Phase 7 auto-merge on green).
|
|
49
|
+
|
|
50
|
+
**Decision points touched:** outbound `/telegram/reply` pipeline (signals only), new preflight route (advisory only), Attention queue (operator-inform only).
|
|
51
|
+
**Existing detectors/authorities interacted with:** `detectJargon`, `detectLocalhostLink`, `MessagingToneGate.review` (B2/B12 untouched in scope), grounding-before-messaging hook (independent layer, unchanged).
|
|
52
|
+
**Rollback path:** `messaging.outboundAdvisory.enabled:false` + `messaging.outboundFloor.jargonAlways:false` (live config, no restart); scheduler env injection inert without script/route consumers; queue column is nullable + idempotent-ALTER (legacy rows fine).
|
|
53
|
+
|
|
54
|
+
## Summary of the change
|
|
55
|
+
|
|
56
|
+
Implements the converged + approved spec `docs/specs/outbound-jargon-filepath-gap.md` (r2): structural
|
|
57
|
+
`automated` message-kind stamping at job spawn (SessionManager both lanes + JobScheduler script env),
|
|
58
|
+
the jargon signal single-sourced for non-reply kinds, a new `detectRawFilePath` signal detector, an
|
|
59
|
+
inform-only preflight advisory in `telegram-reply.sh` backed by `POST /messaging/preflight` +
|
|
60
|
+
`GET /messaging/advisory-log`, a server-side single-writer audit with repeated-ignore escalation, kind
|
|
61
|
+
metadata threading through the durable relay queue / sentinel redrive / cross-machine relay hop, and
|
|
62
|
+
full migration parity (SHA allowlist + CLAUDE.md + shadow mirrors). Files: `src/core/raw-file-path.ts`
|
|
63
|
+
(new), `src/messaging/OutboundAdvisory.ts` (new), `src/core/MessagingToneGate.ts`,
|
|
64
|
+
`src/server/routes.ts`, `src/server/AgentServer.ts`, `src/core/SessionManager.ts`,
|
|
65
|
+
`src/scheduler/JobScheduler.ts`, `src/templates/scripts/telegram-reply.sh`,
|
|
66
|
+
`src/messaging/pending-relay-store.ts`, `src/monitoring/delivery-failure-sentinel.ts`,
|
|
67
|
+
`src/messaging/TelegramAdapter.ts`, `src/core/TelegramRelay.ts`, `src/core/PostUpdateMigrator.ts`,
|
|
68
|
+
`src/scaffold/templates.ts` + three test tiers.
|
|
69
|
+
|
|
70
|
+
## Decision-point inventory
|
|
71
|
+
|
|
72
|
+
- `evaluateOutbound` / `checkOutboundMessage` (routes.ts) — **modify (signals only)** — two new SIGNALS (jargon for non-reply kinds, filePath for all kinds) feed the EXISTING authority; no decision logic changed; block/allow remains solely the authority's.
|
|
73
|
+
- `MessagingToneGate.review` — **pass-through** — receives the new kind + signals as context; rules untouched (no B12 re-scope, no new rule).
|
|
74
|
+
- `POST /messaging/preflight` (new) — **add (advisory only)** — deterministic detectors, returns advisories; CANNOT refuse a send (the server delivers regardless — proven by test).
|
|
75
|
+
- `telegram-reply.sh` preflight loop — **add (default-withhold the sender always resolves)** — honestly named per spec §2.4: the first flagged attempt does not deliver; resolution (fix or ack) belongs exclusively to the sender; fail-OPEN on every error path.
|
|
76
|
+
- Repeated-ignore escalation — **add (operator-inform only)** — one deduped Attention item; gates nothing.
|
|
77
|
+
- Observability breadcrumbs in `/telegram/reply` — **add (log lines only)** — never affect delivery; wrapped in a try/catch.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 1. Over-block
|
|
82
|
+
|
|
83
|
+
The server holds NO new block surface — `/telegram/reply` delivers a raw-path automated message
|
|
84
|
+
exactly as before (test-proven). The script-side withhold can over-fire two ways:
|
|
85
|
+
|
|
86
|
+
- `detectRawFilePath` false positive (e.g. a legitimate user-actionable path in a power-user setup):
|
|
87
|
+
costs the sender one `--ack-advisory` re-run; the message still delivers unchanged. Bounded cost,
|
|
88
|
+
stated in the spec's risk table.
|
|
89
|
+
- `detectJargon` false positive on an automated send: same single-re-run cost. Jargon is deliberately
|
|
90
|
+
NOT computed for conversational replies, so the over-block tail the operator has repeatedly warned
|
|
91
|
+
about cannot grow on the main path.
|
|
92
|
+
|
|
93
|
+
A genuinely-flagged localhost link is the one case `--ack-advisory` cannot deliver — but that block
|
|
94
|
+
belongs to the PRE-EXISTING localhost guard (untouched); the advisory's static guidance states this
|
|
95
|
+
honestly rather than promising ack-delivery.
|
|
96
|
+
|
|
97
|
+
## 2. Under-block
|
|
98
|
+
|
|
99
|
+
- A job that hand-curls `/telegram/reply` (bypassing the script) sends kindless and skips the
|
|
100
|
+
preflight entirely — named residual (§2.1/§2.5), bounded by the kindless-job-send breadcrumb and the
|
|
101
|
+
removal of the hand-curl example from the CLAUDE.md template. Accepted by design (sender sovereignty).
|
|
102
|
+
- `isProxy`/`isSystemTemplate`/`willRelay` system-composed sends skip `checkOutboundMessage` today and
|
|
103
|
+
are untouched — there is no sender to inform. Named residual.
|
|
104
|
+
- A one-shot sender that ignores its advisory drops that one message with no re-fire behind it; the
|
|
105
|
+
per-slug aggregate escalation is the bound. Accepted residual cost of inform-only, per the operator's
|
|
106
|
+
explicit constraint.
|
|
107
|
+
- `detectRawFilePath` misses path shapes outside its three alternatives (e.g. bare relative
|
|
108
|
+
`foo/bar.txt` with an unknown top dir, Windows `C:\` paths). Deliberate precision-first scope —
|
|
109
|
+
the LLM authority's B2 judgment still covers them on gated kinds.
|
|
110
|
+
|
|
111
|
+
## 3. Level-of-abstraction fit
|
|
112
|
+
|
|
113
|
+
Correct layers throughout: the detector is a cheap deterministic sibling of `localhost-link.ts` in
|
|
114
|
+
`src/core/`; the signals feed the EXISTING authority instead of running parallel to it (the spec's
|
|
115
|
+
grounding correction explicitly rejected a new gate); the advisory loop lives in the mandated relay
|
|
116
|
+
script (the only place that can inform the sender BEFORE delivery without giving the server veto
|
|
117
|
+
power); the audit/escalation live server-side where the single-writer guarantee is enforceable. The
|
|
118
|
+
kind is stamped at the SPAWNER (the only layer that structurally knows a session is a job).
|
|
119
|
+
|
|
120
|
+
## 4. Signal vs authority compliance
|
|
121
|
+
|
|
122
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
123
|
+
|
|
124
|
+
- [x] No — this change produces signals consumed by an existing smart gate.
|
|
125
|
+
|
|
126
|
+
`detectRawFilePath` and the re-scoped `detectJargon` are pure detectors feeding
|
|
127
|
+
`MessagingToneGate.review`. The preflight advisory holds no blocking authority: its withhold is always
|
|
128
|
+
and only resolved by the sender, never escalates, never consults a judge, and fails open on every
|
|
129
|
+
error path — the spec names it honestly as a "default-withhold the sender always resolves" rather than
|
|
130
|
+
claiming "no authority at all". r1's deterministic 422 floor was DELETED per the operator's
|
|
131
|
+
inform-only constraint; no block rule was added or re-scoped anywhere in this change.
|
|
132
|
+
|
|
133
|
+
## 5. Interactions
|
|
134
|
+
|
|
135
|
+
- **Shadowing:** the preflight runs BEFORE the send, script-side; the existing pipeline (localhost
|
|
136
|
+
guard, tone gate, dedup, length cap) runs unchanged server-side afterward. One send can legitimately
|
|
137
|
+
receive two differently-shaped interventions (grounding hook + advisory) — independent layers; the
|
|
138
|
+
advisory text never claims to be the only check.
|
|
139
|
+
- **Double-fire:** the `acked` audit row is written ONLY on successful delivery, so an
|
|
140
|
+
acked-then-queued send audits once, after the redrive — verified that the queue carries
|
|
141
|
+
`advisoryAck` whole, closing the escalation false-fire on transient delivery failures.
|
|
142
|
+
- **Races:** the escalation index is in-memory single-process (write-time, no poller); restart resets
|
|
143
|
+
are accepted, stated best-effort observability. JSONL appends are single `appendFileSync` calls
|
|
144
|
+
(O_APPEND).
|
|
145
|
+
- **Feedback loops:** the escalation raises Attention items through `createAttentionItem` with a
|
|
146
|
+
FIXED sourceContext and stable per-signature ids (id-dedup prevents re-raise loops). **Corrected
|
|
147
|
+
after the second-pass review:** HIGH/URGENT items are EXEMPT from the per-source topic budget, so
|
|
148
|
+
the original all-HIGH design would have been an un-budgeted topic-per-signature flood for a
|
|
149
|
+
topic-varying sender. Iterated: per-signature items are NORMAL (the budget genuinely binds — a
|
|
150
|
+
burst-invariant test feeds escalations through the REAL AttentionTopicGuard and asserts allowed
|
|
151
|
+
topics ≤ the budget); the per-slug aggregate is the single loud HIGH bound (one deduped item per
|
|
152
|
+
slug); once a slug's aggregate has fired, further per-signature items for that slug are suppressed.
|
|
153
|
+
- The sentinel's `postReply` signature widened with optional trailing params — all existing callers
|
|
154
|
+
and test doubles remain compatible (verified by the full suite).
|
|
155
|
+
|
|
156
|
+
## 6. External surfaces
|
|
157
|
+
|
|
158
|
+
- **Other agents on the machine:** job sessions of every framework (claude/codex/gemini) get the new
|
|
159
|
+
env vars — inert unless the relay script consumes them. The CLAUDE.md awareness section reaches
|
|
160
|
+
Codex/Gemini via the shadow-capability mirror.
|
|
161
|
+
- **Install base:** the `telegram-reply.sh` re-deploy rides the SHA-gated migrator; user-modified
|
|
162
|
+
scripts are never stomped (`.new` + degradation event, existing behavior). The queue column arrives
|
|
163
|
+
via idempotent ALTER on every writer — legacy rows read null metadata and ride the delivery-id
|
|
164
|
+
breadcrumb exemption.
|
|
165
|
+
- **External systems:** no new egress. The preflight is a localhost call; detectors are local regex.
|
|
166
|
+
- **Persistent state:** new `logs/outbound-advisory.jsonl` (size-rotated, single `.1` rollover); new
|
|
167
|
+
nullable `message_metadata` column in the pending-relay SQLite (additive, no migration needed for
|
|
168
|
+
rollback).
|
|
169
|
+
- **Timing:** the preflight adds one localhost round-trip (~ms) + ~100ms of process spawns per
|
|
170
|
+
automated send; bounded by the clamped curl timeout; the conversational path is untouched.
|
|
171
|
+
|
|
172
|
+
## 7. Rollback cost
|
|
173
|
+
|
|
174
|
+
- **Hot levers (live config, NO restart):** `messaging.outboundAdvisory.enabled:false` disables the
|
|
175
|
+
preflight + escalation; `messaging.outboundFloor.jargonAlways:false` reverts the jargon signal.
|
|
176
|
+
Both are read per-request through LiveConfig.
|
|
177
|
+
- **Code revert:** the scheduler env injection is inert without the script/route consumers, so a
|
|
178
|
+
partial rollback degrades safely in any order.
|
|
179
|
+
- **Data:** the JSONL audit and the nullable queue column need no cleanup on rollback (additive,
|
|
180
|
+
null-tolerant readers).
|
|
181
|
+
- **User visibility during rollback:** none — worst case, automated sends simply stop being advised.
|
|
182
|
+
|
|
183
|
+
## Conclusion
|
|
184
|
+
|
|
185
|
+
The review confirms the design holds the operator's governing constraint structurally: zero new
|
|
186
|
+
blocking authority anywhere in the server; the only withhold lives script-side, is always resolvable
|
|
187
|
+
by the sender alone, and fails open on every error path (each proven by a test, not asserted). The
|
|
188
|
+
named residuals (hand-curl bypass, system-composed sends, one-shot drops) are bounded by breadcrumbs
|
|
189
|
+
and the escalation, and are the accepted cost of inform-only — the operator chose this trade
|
|
190
|
+
explicitly. One design change came out of testing: the fix-landing resolution threshold was tuned to
|
|
191
|
+
0.4 Jaccard after measuring the founding incident shape (~0.47) vs the gaming heartbeat (~0.06).
|
|
192
|
+
Clear to ship pending the required second-pass review (outbound-messaging decision surface).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Second-pass review (required — outbound messaging surface)
|
|
197
|
+
|
|
198
|
+
**Reviewer:** subagent (claude, independent second-pass)
|
|
199
|
+
**Independent read of the artifact: concern → resolved → concur (re-confirmed below)**
|
|
200
|
+
|
|
201
|
+
First-pass verdict (verbatim): the core inform-only claims all verified adversarially against the
|
|
202
|
+
code (no new server-side blocking authority; no silent drop — the NOT-SENT line precedes every
|
|
203
|
+
withhold; fail-open complete across curl failure/timeout/non-200/malformed-JSON/missing-python3/
|
|
204
|
+
disabled; secret-safety bounds hold at all four echo sites; queue/redrive/relay threading and
|
|
205
|
+
migration parity verified). Three concerns were raised:
|
|
206
|
+
|
|
207
|
+
1. **(Load-bearing) The §5 flood-bound claim was FALSE as written:** per-signature escalations were
|
|
208
|
+
HIGH, and HIGH bypasses BOTH the per-source budget and the universal topic budget — a
|
|
209
|
+
topic-varying sender would have produced K un-budgeted HIGH topics (the 2026-06-05 flood shape);
|
|
210
|
+
the spec-required burst proof test was missing, which is exactly why the false claim survived.
|
|
211
|
+
**Resolution (implemented):** per-signature items demoted to NORMAL (budget-eligible); per-slug
|
|
212
|
+
aggregate kept as the single deduped HIGH; per-signature suppressed once the slug aggregate has
|
|
213
|
+
fired; burst-invariant test added driving 10 distinct misbehaving signatures through the REAL
|
|
214
|
+
`AttentionTopicGuard` and asserting allowed topics ≤ the budget, plus a suppression test.
|
|
215
|
+
2. **Two §6 assertions were absent.** **Resolution (implemented):** non-telegram (slack) automated
|
|
216
|
+
send asserted to receive the jargon + filePath signals (with the slack route now threading
|
|
217
|
+
`metadata.messageKind`, mirroring telegram); the Tier-2-location nit (route tests under
|
|
218
|
+
`tests/unit/` driving a real express app) stands with this note.
|
|
219
|
+
3. **(Disclosed, no action)** the 0.4 Jaccard fix-landing threshold can under-resolve a heavy
|
|
220
|
+
rewrite; bounded to NORMAL/budgeted items by resolution 1.
|
|
221
|
+
|
|
222
|
+
**Re-confirmation (independent, after the iteration):** concur — "All three resolutions verified in
|
|
223
|
+
code and tests against the real `AttentionTopicGuard` (35/35 green)"; the burst test was verified
|
|
224
|
+
non-vacuous (a revert to HIGH fails both assertions). The one cosmetic nit (a stale test-header
|
|
225
|
+
comment) was fixed in the same pass.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Evidence pointers
|
|
230
|
+
|
|
231
|
+
- `tests/unit/telegram-reply-advisory-script.test.ts` — the REAL shipped script under bash + recording
|
|
232
|
+
curl shim: NOT-SENT literal/exit-0, ack annotation, fail-open ×2, legacy-body byte-shape, ms→s clamp,
|
|
233
|
+
queue metadata on HTTP-000.
|
|
234
|
+
- `tests/unit/raw-file-path.test.ts` (26) · `tests/unit/outbound-advisory.test.ts` (17) ·
|
|
235
|
+
`tests/unit/outbound-advisory-routes.test.ts` (15) · `tests/unit/pending-relay-metadata.test.ts` (4) ·
|
|
236
|
+
`tests/unit/relay-kind-forward.test.ts` (2) · spawn-env assertions in
|
|
237
|
+
`tests/unit/session-manager-behavioral.test.ts`.
|
|
238
|
+
- `tests/e2e/outbound-advisory-alive.test.ts` (5) — production-init alive + auth + on-disk audit +
|
|
239
|
+
live kill switch.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Post-CI follow-up (same PR)
|
|
244
|
+
|
|
245
|
+
The no-silent-fallbacks ratchet (CI shard 3) correctly demanded that every spec-mandated fail-open
|
|
246
|
+
catch be a TRACKED decision: all are now annotated `@silent-fallback-ok` with the spec-section
|
|
247
|
+
justification (fail-open is the §0 operator constraint, not an accident), plus one documented
|
|
248
|
+
scanner-window false positive on a pre-existing 500-responder catch. No behavior change.
|
|
249
|
+
|
|
250
|
+
CI round 2: one source-inspection test (`slack-thread-session-wiring`) used a fixed 1600-char
|
|
251
|
+
extraction window that the slack route's new messageKind threading overflowed — window widened to
|
|
252
|
+
2400 with a comment naming the cause. No behavior change.
|