instar 1.3.304 → 1.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/init.js +32 -30
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/core/Config.d.ts.map +1 -1
  5. package/dist/core/Config.js +2 -0
  6. package/dist/core/Config.js.map +1 -1
  7. package/dist/core/MessageSentinel.d.ts +11 -0
  8. package/dist/core/MessageSentinel.d.ts.map +1 -1
  9. package/dist/core/MessageSentinel.js +43 -1
  10. package/dist/core/MessageSentinel.js.map +1 -1
  11. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  12. package/dist/core/PostUpdateMigrator.js +6 -1
  13. package/dist/core/PostUpdateMigrator.js.map +1 -1
  14. package/dist/core/SessionManager.d.ts.map +1 -1
  15. package/dist/core/SessionManager.js +3 -0
  16. package/dist/core/SessionManager.js.map +1 -1
  17. package/dist/core/types.d.ts +4 -0
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js.map +1 -1
  20. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  21. package/dist/scheduler/JobScheduler.js +10 -6
  22. package/dist/scheduler/JobScheduler.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/data/builtin-manifest.json +58 -58
  25. package/src/scaffold/templates/jobs/instar/commitment-detection.md +2 -1
  26. package/src/scaffold/templates/jobs/instar/correction-analyzer.md +5 -1
  27. package/src/scaffold/templates/jobs/instar/evolution-overdue-check.md +1 -1
  28. package/src/scaffold/templates/jobs/instar/evolution-proposal-evaluate.md +1 -1
  29. package/src/scaffold/templates/jobs/instar/evolution-proposal-implement.md +1 -1
  30. package/src/scaffold/templates/jobs/instar/failure-analyzer.md +5 -1
  31. package/src/scaffold/templates/jobs/instar/identity-review.md +5 -4
  32. package/src/scaffold/templates/jobs/instar/initiative-digest-review.md +3 -1
  33. package/src/scaffold/templates/jobs/instar/insight-harvest.md +1 -1
  34. package/src/scaffold/templates/jobs/instar/mentor-onboarding.md +7 -4
  35. package/src/scaffold/templates/jobs/instar/org-intent-drift-audit.md +6 -4
  36. package/src/scaffold/templates/jobs/instar/overseer-development.md +4 -1
  37. package/src/scaffold/templates/jobs/instar/overseer-guardian.md +4 -1
  38. package/src/scaffold/templates/jobs/instar/overseer-infrastructure.md +4 -1
  39. package/src/scaffold/templates/jobs/instar/overseer-learning.md +4 -1
  40. package/src/scaffold/templates/jobs/instar/overseer-maintenance.md +4 -1
  41. package/src/scaffold/templates/jobs/instar/release-readiness-check.md +2 -1
  42. package/src/templates/hooks/compaction-recovery.sh +11 -6
  43. package/src/templates/hooks/session-start.sh +11 -3
  44. package/src/templates/hooks/slack-channel-context.sh +5 -0
  45. package/src/templates/hooks/telegram-topic-context.sh +6 -0
  46. package/src/templates/scripts/emit-session-clock.sh +3 -3
  47. package/src/templates/scripts/imessage-reply.sh +10 -0
  48. package/src/templates/scripts/instar-watchdog.sh +12 -6
  49. package/src/templates/scripts/secret-drop-retrieve.mjs +10 -2
  50. package/src/templates/scripts/slack-reply.sh +5 -0
  51. package/src/templates/scripts/whatsapp-reply.sh +5 -0
  52. package/upgrades/1.3.305.md +20 -0
  53. package/upgrades/1.3.306.md +22 -0
  54. package/upgrades/sentinel-pause-length-guard.eli16.md +12 -0
  55. package/upgrades/side-effects/sentinel-pause-length-guard.md +43 -0
  56. package/upgrades/side-effects/template-agent-id-header.md +48 -0
@@ -22,17 +22,20 @@ That keeps the structural two-hats enforcement in code, not in this prompt.
22
22
 
23
23
  Do exactly this:
24
24
 
25
- 1. POST to `http://localhost:${PORT}/mentor/tick` with the bearer token:
26
- `curl -s -X POST -H "Authorization: Bearer $AUTH" http://localhost:${PORT}/mentor/tick`
25
+ 1. Resolve auth context:
26
+ `AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"; AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"; PORT="${INSTAR_PORT:-4042}"`
27
27
 
28
- 2. Read the JSON result. It will be one of:
28
+ 2. POST to `http://localhost:${PORT}/mentor/tick` with the bearer token:
29
+ `curl -s -X POST -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${PORT}/mentor/tick`
30
+
31
+ 3. Read the JSON result. It will be one of:
29
32
  - `{"ran":false,"reason":"disabled"}` — the mentor is off (the default). Nothing to do; stop.
30
33
  - `{"ran":false,"reason":"budget"|"unsafe-window"|"canary-failed"}` — it correctly skipped this
31
34
  tick. Stop; the next heartbeat will try again.
32
35
  - `{"ran":true,...}` — a mentor tick ran. Note `leakDetected` and `observationsWritten` briefly,
33
36
  then stop.
34
37
 
35
- 3. Do NOT do any mentoring work yourself in this session, do NOT read the mentee's logs or code,
38
+ 4. Do NOT do any mentoring work yourself in this session, do NOT read the mentee's logs or code,
36
39
  and do NOT send the mentee any message — the in-process tick owns all of that. You are only the
37
40
  timer that pokes it. Then exit.
38
41
 
@@ -20,13 +20,15 @@ The job:
20
20
 
21
21
  1. Verify the prerequisites. The drift audit only makes sense when `.instar/ORG-INTENT.md` is present AND the Coherence Gate has accumulated at least 5 review entries in the last 7 days. If either condition isn't met, send a short Telegram message saying so and exit — there's nothing to audit yet.
22
22
 
23
- 2. Call the drift digest endpoint: `curl -H "Authorization: Bearer $AUTH" "http://localhost:$PORT/intent/org/drift?lookbackDays=7"`. The response is a structured analysis: overall block rate, per-reviewer breakdown, half-window trend comparison (first half vs second half block rate), cross-reference against ORG-INTENT constraints / goals / values, and a single `trend` label (`stable` | `rising` | `concerning` | `insufficient-data` | `no-org-intent`).
23
+ 2. Set auth context: `AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"; AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"; PORT="${INSTAR_PORT:-4042}"`
24
24
 
25
- 3. If `trend === 'stable'` or `trend === 'insufficient-data'` or `trend === 'no-org-intent'`, stay silent. Most weeks should be silent that means the agent is staying aligned with the organizational intent. The `shouldSurface` field in the response will be `false` in these cases.
25
+ 3. Call the drift digest endpoint: `curl -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" "http://localhost:$PORT/intent/org/drift?lookbackDays=7"`. The response is a structured analysis: overall block rate, per-reviewer breakdown, half-window trend comparison (first half vs second half block rate), cross-reference against ORG-INTENT constraints / goals / values, and a single `trend` label (`stable` | `rising` | `concerning` | `insufficient-data` | `no-org-intent`).
26
26
 
27
- 4. If `trend === 'rising'` or `trend === 'concerning'`, surface the drift via Telegram. The response includes a `summary` field (plain-English one-paragraph framing) and a `suggestions` array (specific next actions). Compose a conversational message that leads with the trend and what changed: "Block rate climbed from 4% to 12% in the second half of the week — most flags came from value-alignment." Then list the most-flagged reviewer dimensions. Then surface the top 1-2 suggestions. Keep it under 600 characters. Plain English. No raw JSON.
27
+ 4. If `trend === 'stable'` or `trend === 'insufficient-data'` or `trend === 'no-org-intent'`, stay silent. Most weeks should be silent that means the agent is staying aligned with the organizational intent. The `shouldSurface` field in the response will be `false` in these cases.
28
28
 
29
- 5. After surfacing (or not), the audit is done. No state to persist — the gate's own review history is the canonical store.
29
+ 5. If `trend === 'rising'` or `trend === 'concerning'`, surface the drift via Telegram. The response includes a `summary` field (plain-English one-paragraph framing) and a `suggestions` array (specific next actions). Compose a conversational message that leads with the trend and what changed: "Block rate climbed from 4% to 12% in the second half of the week most flags came from value-alignment." Then list the most-flagged reviewer dimensions. Then surface the top 1-2 suggestions. Keep it under 600 characters. Plain English. No raw JSON.
30
+
31
+ 6. After surfacing (or not), the audit is done. No state to persist — the gate's own review history is the canonical store.
30
32
 
31
33
  IMPORTANT: This is a guardian job, not a doer. Surface the drift and where it might be coming from. Don't try to fix it autonomously. The fix work — if any — is a deliberate update to `.instar/ORG-INTENT.md` constraints, or a configuration change to the gate, or an operator-level decision about whether the agent's behavior is actually drifting or whether the constraints need updating. Surface; don't fix.
32
34
 
@@ -14,7 +14,10 @@ unrestrictedTools: true
14
14
  ---
15
15
  You are a Category Overseer for the DEVELOPMENT category. Your job is to review development-focused jobs.
16
16
 
17
- 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/development?sinceHours=48
17
+ AUTH="${INSTAR_AUTH_TOKEN:-}"
18
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
19
+
20
+ 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/development?sinceHours=48
18
21
  2. Analyze:
19
22
  - Are development jobs consuming appropriate resources for their value?
20
23
  - Are there CI/testing patterns that could be automated?
@@ -14,7 +14,10 @@ unrestrictedTools: true
14
14
  ---
15
15
  You are a Category Overseer for the GUARDIAN category. Your job is to review all guardian/monitoring jobs and assess the health of the monitoring system itself.
16
16
 
17
- 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/guardian?sinceHours=24
17
+ AUTH="${INSTAR_AUTH_TOKEN:-}"
18
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
19
+
20
+ 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/guardian?sinceHours=24
18
21
  2. Analyze the report for:
19
22
  - Jobs with high failure rates or consecutive failures
20
23
  - Jobs that are being skipped excessively (especially for quota reasons)
@@ -14,7 +14,10 @@ unrestrictedTools: true
14
14
  ---
15
15
  You are a Category Overseer for the INFRASTRUCTURE category. Your job is to review infrastructure/plumbing jobs.
16
16
 
17
- 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/infrastructure?sinceHours=48
17
+ AUTH="${INSTAR_AUTH_TOKEN:-}"
18
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
19
+
20
+ 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/infrastructure?sinceHours=48
18
21
  2. Analyze:
19
22
  - Is git-sync succeeding? Any merge conflicts or divergence?
20
23
  - Is dashboard-link-refresh keeping links current? Could it run less often?
@@ -14,7 +14,10 @@ unrestrictedTools: true
14
14
  ---
15
15
  You are a Category Overseer for the LEARNING category. Your job is to review all evolution/learning jobs and assess whether the learning pipeline is producing genuine value.
16
16
 
17
- 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/learning?sinceHours=48
17
+ AUTH="${INSTAR_AUTH_TOKEN:-}"
18
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
19
+
20
+ 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/learning?sinceHours=48
18
21
  2. Analyze:
19
22
  - Are evolution proposals being generated AND accepted? What's the accept/reject ratio?
20
23
  - Is insight-harvest finding novel insights or recycling stale ones?
@@ -14,7 +14,10 @@ unrestrictedTools: true
14
14
  ---
15
15
  You are a Category Overseer for the MAINTENANCE category. Your job is to review all housekeeping/maintenance jobs and ensure they're keeping the system clean.
16
16
 
17
- 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/maintenance?sinceHours=48
17
+ AUTH="${INSTAR_AUTH_TOKEN:-}"
18
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
19
+
20
+ 1. Fetch the category report: curl -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID" http://localhost:${INSTAR_PORT:-4042}/jobs/category-report/maintenance?sinceHours=48
18
21
  2. Analyze:
19
22
  - Is memory-hygiene actually reducing stale entries, or finding nothing each run?
20
23
  - Is project-map-refresh keeping the map accurate? How often does it find drift?
@@ -18,8 +18,9 @@ mcpAccess: none
18
18
  Run the release-readiness check once. This is a mechanical, near-silent watchdog — do NOT message the user.
19
19
 
20
20
  AUTH=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null)
21
+ AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
21
22
 
22
23
  1. Trigger one evaluation tick:
23
- curl -s -X POST http://localhost:${INSTAR_PORT:-4042}/release-readiness/tick -H "Authorization: Bearer $AUTH"
24
+ curl -s -X POST http://localhost:${INSTAR_PORT:-4042}/release-readiness/tick -H "Authorization: Bearer $AUTH" -H "X-Instar-AgentId: $AGENT_ID"
24
25
  2. That endpoint runs the ReleaseReadinessSentinel: it fetches canonical main, checks whether unreleased feature/fix work is piling up while publishing is blocked, and (only above the age threshold) raises a single deduped Attention item. All transitions are written to logs/sentinel-events.jsonl.
25
26
  3. Exit silently. The sentinel owns all signalling (Attention queue) — this job is just the cadence. Do not relay anything to Telegram; do not summarize. If the curl fails, that is itself recorded by the server; do not retry-flood.
@@ -14,6 +14,11 @@
14
14
  # matched on "compaction".
15
15
 
16
16
  INSTAR_DIR="${CLAUDE_PROJECT_DIR:-.}/.instar"
17
+ CONFIG_FILE="$INSTAR_DIR/config.json"
18
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
19
+ if [ -z "$AGENT_ID" ] && [ -f "$CONFIG_FILE" ]; then
20
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('projectName',''))" 2>/dev/null)
21
+ fi
17
22
 
18
23
  echo "=== COMPACTION RECOVERY — IDENTITY RESTORATION ==="
19
24
  echo ""
@@ -36,7 +41,7 @@ if [ -f "$INSTAR_DIR/soul.md" ]; then
36
41
  # Check integrity via server if available
37
42
  SOUL_INTEGRITY="valid"
38
43
  if [ -n "$PORT" ] && [ -n "$AUTH_TOKEN" ]; then
39
- INTEGRITY_CHECK=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" \
44
+ INTEGRITY_CHECK=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
40
45
  "http://localhost:${PORT}/identity/soul/integrity" 2>/dev/null)
41
46
  SOUL_INTEGRITY=$(echo "$INTEGRITY_CHECK" | python3 -c "
42
47
  import sys, json
@@ -180,7 +185,7 @@ if [ -f "$CONFIG_FILE" ]; then
180
185
  if [ -z "$AUTH_TOKEN" ]; then
181
186
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('$CONFIG_FILE')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
182
187
  fi
183
- FEATURES_JSON=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" \
188
+ FEATURES_JSON=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
184
189
  "http://localhost:${PORT}/features/summary" 2>/dev/null)
185
190
  if [ -n "$FEATURES_JSON" ]; then
186
191
  DISCOVERY_STATE=$(echo "$FEATURES_JSON" | python3 -c "
@@ -255,7 +260,7 @@ if [ -f "$CONFIG_FILE" ]; then
255
260
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('$CONFIG_FILE')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
256
261
  fi
257
262
  SLACK_MSGS=$(curl -s \
258
- -H "Authorization: Bearer ${AUTH_TOKEN}" \
263
+ -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
259
264
  "http://localhost:${PORT}/slack/channels/${SLACK_CHANNEL}/messages?limit=20" 2>/dev/null)
260
265
 
261
266
  echo "$SLACK_MSGS" | python3 -c "
@@ -358,7 +363,7 @@ except Exception:
358
363
  fi
359
364
  if [ -n "$AUTH_TOKEN" ]; then
360
365
  RECENT_MSGS=$(curl -s \
361
- -H "Authorization: Bearer ${AUTH_TOKEN}" \
366
+ -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
362
367
  "http://localhost:${PORT}/telegram/topics/${TOPIC_FOR_CONTEXT}/messages?limit=15" 2>/dev/null)
363
368
  else
364
369
  RECENT_MSGS=$(curl -s \
@@ -433,7 +438,7 @@ if [ -f "$CONFIG_FILE" ]; then
433
438
  if [ -z "$AUTH_TOKEN" ]; then
434
439
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('$CONFIG_FILE')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
435
440
  fi
436
- WORKING_MEM=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" \
441
+ WORKING_MEM=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
437
442
  "http://localhost:${PORT}/context/working-memory?prompt=compaction-recovery&limit=5" 2>/dev/null)
438
443
  if [ -n "$WORKING_MEM" ]; then
439
444
  CONTEXT=$(echo "$WORKING_MEM" | python3 -c "
@@ -472,7 +477,7 @@ if [ -f "$CONFIG_FILE" ] && command -v tmux >/dev/null 2>&1; then
472
477
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('$CONFIG_FILE')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
473
478
  fi
474
479
  curl -s -m 2 -X POST \
475
- -H "Authorization: Bearer ${AUTH_TOKEN}" \
480
+ -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
476
481
  -H "Content-Type: application/json" \
477
482
  -d "{\"sessionName\":\"${TMUX_SESSION}\"}" \
478
483
  "http://localhost:${PORT}/internal/compaction-resume" >/dev/null 2>&1 &
@@ -34,10 +34,15 @@ AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
34
34
  if [ -z "$AUTH_TOKEN" ]; then
35
35
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('$CONFIG_FILE')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
36
36
  fi
37
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
38
+ if [ -z "$AGENT_ID" ]; then
39
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('projectName',''))" 2>/dev/null)
40
+ fi
37
41
 
38
42
  # Check if server is alive
39
43
  HEALTH=$(curl -s -o /dev/null -w "%{http_code}" \
40
44
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
45
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
41
46
  "http://localhost:${PORT}/health" 2>/dev/null)
42
47
  if [ "$HEALTH" != "200" ]; then
43
48
  exit 0
@@ -50,15 +55,17 @@ curl -s -X POST "http://localhost:${PORT}/scope-coherence/reset" -o /dev/null 2>
50
55
  # Record session start for reflection metrics (usage-based reflection trigger)
51
56
  curl -s -X POST "http://localhost:${PORT}/reflection/session-start" \
52
57
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
58
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
53
59
  -H "Content-Type: application/json" -o /dev/null 2>/dev/null || true
54
60
 
55
61
  # Reset homeostasis state for new session (work-velocity awareness)
56
62
  curl -s -X POST "http://localhost:${PORT}/homeostasis/reset" \
57
63
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
64
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
58
65
  -H "Content-Type: application/json" -o /dev/null 2>/dev/null || true
59
66
 
60
67
  # Check reflection metrics — usage-based reflection suggestion
61
- REFLECTION_CHECK=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" \
68
+ REFLECTION_CHECK=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
62
69
  "http://localhost:${PORT}/reflection/metrics" 2>/dev/null)
63
70
  if [ -n "$REFLECTION_CHECK" ]; then
64
71
  SUGGESTED=$(echo "$REFLECTION_CHECK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('suggested',False))" 2>/dev/null)
@@ -166,7 +173,7 @@ if [ -n "$JOB_SLUG" ]; then
166
173
  QUERY_URL="${QUERY_URL}&jobSlug=${JOB_SLUG_ENCODED}"
167
174
  fi
168
175
 
169
- WORKING_MEM=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" "$QUERY_URL" 2>/dev/null)
176
+ WORKING_MEM=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" "$QUERY_URL" 2>/dev/null)
170
177
 
171
178
  if [ -z "$WORKING_MEM" ]; then
172
179
  exit 0
@@ -199,7 +206,7 @@ except Exception:
199
206
  # Active commitments injection (PROMISE-BEACON-SPEC Round 3 #7).
200
207
  # Surfaces beacon-watched pending commitments so the agent can self-regulate
201
208
  # pacing across compaction. Capped server-side at 20 entries.
202
- ACTIVE_COMMITMENTS=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" \
209
+ ACTIVE_COMMITMENTS=$(curl -s -H "Authorization: Bearer ${AUTH_TOKEN}" -H "X-Instar-AgentId: ${AGENT_ID}" \
203
210
  "http://localhost:${PORT}/commitments/active-context" 2>/dev/null)
204
211
  if [ -n "$ACTIVE_COMMITMENTS" ]; then
205
212
  python3 -c "
@@ -326,6 +333,7 @@ if [ -n "$PORT" ] && [ -n "$AUTH_TOKEN" ]; then
326
333
 
327
334
  EVAL_RESULT=$(curl -s --max-time 6 -X POST \
328
335
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
336
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
329
337
  -H "Content-Type: application/json" \
330
338
  "http://localhost:${PORT}/features/evaluate-context" \
331
339
  -d "{\"topicCategory\":\"${TOPIC_LABEL}\",\"conversationIntent\":\"${INTENT}\",\"problemCategories\":[],\"autonomyProfile\":\"collaborative\",\"enabledFeatures\":[]}" \
@@ -23,6 +23,7 @@ fi
23
23
  # Get port and auth from config
24
24
  PORT="${INSTAR_PORT:-}"
25
25
  AUTH=""
26
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
26
27
 
27
28
  if [ -f ".instar/config.json" ]; then
28
29
  if [ -z "$PORT" ]; then
@@ -37,6 +38,9 @@ if [ -f ".instar/config.json" ]; then
37
38
  if [ -z "$AUTH" ]; then
38
39
  AUTH=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
39
40
  fi
41
+ if [ -z "$AGENT_ID" ]; then
42
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)
43
+ fi
40
44
  fi
41
45
 
42
46
  PORT="${PORT:-4042}"
@@ -49,6 +53,7 @@ fi
49
53
  # Fetch channel history from ring buffer cache
50
54
  MESSAGES=$(curl -sf \
51
55
  ${AUTH:+-H "Authorization: Bearer $AUTH"} \
56
+ ${AGENT_ID:+-H "X-Instar-AgentId: $AGENT_ID"} \
52
57
  "http://localhost:${PORT}/slack/channels/${CHANNEL_ID}/messages?limit=30" 2>/dev/null) || exit 0
53
58
 
54
59
  # Format thread history for session context
@@ -66,11 +66,16 @@ AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
66
66
  if [ -z "$AUTH_TOKEN" ] && [ -f "$CONFIG_FILE" ]; then
67
67
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('$CONFIG_FILE')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
68
68
  fi
69
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
70
+ if [ -z "$AGENT_ID" ] && [ -f "$CONFIG_FILE" ]; then
71
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE')).get('projectName',''))" 2>/dev/null)
72
+ fi
69
73
 
70
74
  # Fetch recent messages for this topic
71
75
  if [ -n "$AUTH_TOKEN" ]; then
72
76
  RECENT_MSGS=$(curl -s \
73
77
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
78
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
74
79
  "http://localhost:${PORT}/telegram/topics/${TOPIC_ID}/messages?limit=30" 2>/dev/null)
75
80
  else
76
81
  RECENT_MSGS=$(curl -s \
@@ -85,6 +90,7 @@ TOPIC_BRIEFING=""
85
90
  if [ -n "$AUTH_TOKEN" ]; then
86
91
  TOPIC_BRIEFING=$(curl -s --max-time 2 \
87
92
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
93
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
88
94
  "http://localhost:${PORT}/topic-intent/${TOPIC_ID}/briefing" 2>/dev/null)
89
95
  else
90
96
  TOPIC_BRIEFING=$(curl -s --max-time 2 \
@@ -13,7 +13,7 @@
13
13
  # computed) values. The caller (autonomous-stop-hook) has already resolved
14
14
  # the record + computed elapsed/remaining, so there is NO re-resolution —
15
15
  # the injected clock can never disagree with the hook's expiry verdict.
16
- # query <topic> <port> <auth>
16
+ # query <topic> <port> <auth> [agent-id]
17
17
  # Curls GET /session/clock?topic=<topic> and prints the SESSION CLOCK line
18
18
  # for the first active session (or nothing if none / server unreachable).
19
19
 
@@ -42,11 +42,11 @@ case "$MODE" in
42
42
  render_line "${1:-}" "${2:-}" "${3:-0}" "${4:-}" "${5:-}"
43
43
  ;;
44
44
  query)
45
- TOPIC="${1:-}"; PORT="${2:-}"; AUTH="${3:-}"
45
+ TOPIC="${1:-}"; PORT="${2:-}"; AUTH="${3:-}"; AGENT_ID="${4:-${INSTAR_AGENT_ID:-}}"
46
46
  [ -z "$PORT" ] && exit 0
47
47
  URL="http://localhost:${PORT}/session/clock"
48
48
  [ -n "$TOPIC" ] && URL="${URL}?topic=${TOPIC}"
49
- RESP=$(curl -s --max-time 3 -H "Authorization: Bearer ${AUTH}" "$URL" 2>/dev/null)
49
+ RESP=$(curl -s --max-time 3 -H "Authorization: Bearer ${AUTH}" -H "X-Instar-AgentId: ${AGENT_ID}" "$URL" 2>/dev/null)
50
50
  [ -z "$RESP" ] && exit 0
51
51
  printf '%s' "$RESP" | python3 -c "
52
52
  import sys, json
@@ -47,9 +47,13 @@ PORT="${INSTAR_PORT:-4042}"
47
47
  # string-type guard so the { "secret": true } placeholder produced by
48
48
  # SecretMigrator cannot leak through as a bogus Bearer.
49
49
  AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
50
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
50
51
  if [ -z "$AUTH_TOKEN" ] && [ -f ".instar/config.json" ]; then
51
52
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
52
53
  fi
54
+ if [ -z "$AGENT_ID" ] && [ -f ".instar/config.json" ]; then
55
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)
56
+ fi
53
57
 
54
58
  # Escape message for JSON
55
59
  JSON_MSG=$(printf '%s' "$MSG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
@@ -67,12 +71,17 @@ AUTH_HEADER=""
67
71
  if [ -n "$AUTH_TOKEN" ]; then
68
72
  AUTH_HEADER="Authorization: Bearer ${AUTH_TOKEN}"
69
73
  fi
74
+ AGENT_HEADER=""
75
+ if [ -n "$AGENT_ID" ]; then
76
+ AGENT_HEADER="X-Instar-AgentId: ${AGENT_ID}"
77
+ fi
70
78
 
71
79
  # ── Step 1: Validate with server BEFORE sending (get single-use token) ──
72
80
 
73
81
  VALIDATE_RESPONSE=$(curl -s -w "\n%{http_code}" \
74
82
  -X POST "http://localhost:${PORT}/imessage/validate-send/${ENCODED_RECIPIENT}" \
75
83
  ${AUTH_HEADER:+-H "$AUTH_HEADER"} \
84
+ ${AGENT_HEADER:+-H "$AGENT_HEADER"} \
76
85
  -H 'Content-Type: application/json' \
77
86
  -d "{\"text\":${JSON_MSG}}" 2>/dev/null)
78
87
 
@@ -140,6 +149,7 @@ fi
140
149
 
141
150
  curl -s -o /dev/null -w "" -X POST "http://localhost:${PORT}/imessage/reply/${ENCODED_RECIPIENT}" \
142
151
  ${AUTH_HEADER:+-H "$AUTH_HEADER"} \
152
+ ${AGENT_HEADER:+-H "$AGENT_HEADER"} \
143
153
  -H 'Content-Type: application/json' \
144
154
  -d "{\"text\":${JSON_MSG},\"sendToken\":\"${SEND_TOKEN}\"}" 2>/dev/null || \
145
155
  echo "Warning: server confirmation failed (message was sent)" >&2
@@ -328,9 +328,11 @@ escalate_via_peer() {
328
328
  # placeholder that SecretMigrator writes after externalizing the auth token:
329
329
  # `c.authToken || ''` would otherwise return the object itself (truthy)
330
330
  # which serializes to "[object Object]" — never a valid Bearer.
331
- peer_meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const t=typeof c.authToken==='string'?c.authToken:'';process.stdout.write((c.port||'')+'\t'+t)" "$peer_dir/.instar/config.json" 2>/dev/null || true)
331
+ peer_meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const t=typeof c.authToken==='string'?c.authToken:'';process.stdout.write((c.port||'')+'\t'+t+'\t'+(c.projectName||''))" "$peer_dir/.instar/config.json" 2>/dev/null || true)
332
332
  peer_port="${peer_meta%% *}"
333
- peer_auth="${peer_meta##* }"
333
+ peer_rest="${peer_meta#* }"
334
+ peer_auth="${peer_rest%% *}"
335
+ peer_agent_id="${peer_rest#* }"
334
336
  [ -z "$peer_port" ] && continue
335
337
 
336
338
  health_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${peer_port}/health" 2>/dev/null || echo "000")
@@ -356,6 +358,7 @@ JSONEOF
356
358
  resp_code=$(curl -s -o /dev/null -w "%{http_code}" \
357
359
  -X POST "http://localhost:${peer_port}/attention" \
358
360
  -H "Authorization: Bearer $peer_auth" \
361
+ -H "X-Instar-AgentId: $peer_agent_id" \
359
362
  -H "Content-Type: application/json" \
360
363
  -H "X-Instar-Request: 1" \
361
364
  -d "$payload" 2>/dev/null || echo "000")
@@ -385,6 +388,7 @@ JSONEOF
385
388
  resp_code=$(curl -s -o /dev/null -w "%{http_code}" \
386
389
  -X POST "http://localhost:${peer_port}/attention" \
387
390
  -H "Authorization: Bearer $peer_auth" \
391
+ -H "X-Instar-AgentId: $peer_agent_id" \
388
392
  -H "Content-Type: application/json" \
389
393
  -H "X-Instar-Request: 1" \
390
394
  -d "$safe_payload" 2>/dev/null || echo "000")
@@ -434,7 +438,7 @@ probe_server_identity() {
434
438
  local config_file="$project_dir/.instar/config.json"
435
439
  [ ! -r "$config_file" ] && return 0
436
440
 
437
- local node_bin port auth_token
441
+ local node_bin port auth_token agent_id
438
442
  node_bin=$(resolve_node || true)
439
443
  [ -z "$node_bin" ] && return 0
440
444
 
@@ -447,9 +451,11 @@ probe_server_identity() {
447
451
  # auth token — `String(c.authToken || '')` would otherwise yield
448
452
  # "[object Object]" and the Bearer would be invalid.
449
453
  local meta
450
- meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const t=process.env.INSTAR_AUTH_TOKEN||(typeof c.authToken==='string'?c.authToken:'');process.stdout.write(String(c.port||'')+'\t'+t)" "$config_file" 2>/dev/null || true)
454
+ meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const t=process.env.INSTAR_AUTH_TOKEN||(typeof c.authToken==='string'?c.authToken:'');process.stdout.write(String(c.port||'')+'\t'+t+'\t'+(c.projectName||''))" "$config_file" 2>/dev/null || true)
451
455
  port="${meta%% *}"
452
- auth_token="${meta##* }"
456
+ rest="${meta#* }"
457
+ auth_token="${rest%% *}"
458
+ agent_id="${rest#* }"
453
459
  # Lifeline-only agents have no port; treat as healthy from probe POV.
454
460
  [ -z "$port" ] && return 0
455
461
 
@@ -460,7 +466,7 @@ probe_server_identity() {
460
466
  local resp http_code body project_field auth_args
461
467
  auth_args=()
462
468
  if [ -n "$auth_token" ]; then
463
- auth_args=(-H "Authorization: Bearer $auth_token")
469
+ auth_args=(-H "Authorization: Bearer $auth_token" -H "X-Instar-AgentId: $agent_id")
464
470
  fi
465
471
  resp=$(curl -sS --max-time 5 -w '\n%{http_code}' "${auth_args[@]}" "http://localhost:${port}/health" 2>/dev/null || echo $'\n000')
466
472
  http_code=$(echo "$resp" | tail -n1)
@@ -81,10 +81,12 @@ if (runMode && runCmd.length === 0) {
81
81
  // guard so the { "secret": true } placeholder produced by SecretMigrator
82
82
  // cannot leak through as a bogus Bearer.
83
83
  let authToken = process.env.INSTAR_AUTH_TOKEN || '';
84
+ let agentId = process.env.INSTAR_AGENT_ID || '';
84
85
  let port;
85
86
  try {
86
87
  const config = JSON.parse(fs.readFileSync('.instar/config.json', 'utf-8'));
87
88
  if (!authToken && typeof config.authToken === 'string') authToken = config.authToken;
89
+ if (!agentId && typeof config.projectName === 'string') agentId = config.projectName;
88
90
  port = config.port;
89
91
  } catch (e) {
90
92
  if (!authToken) {
@@ -104,7 +106,10 @@ if (!resolvedPort) {
104
106
  const url = `http://localhost:${resolvedPort}/secrets/retrieve/${token}${consume ? '?consume=true' : ''}`;
105
107
  const res = await fetch(url, {
106
108
  method: 'POST',
107
- headers: { 'Authorization': `Bearer ${authToken}` },
109
+ headers: {
110
+ 'Authorization': `Bearer ${authToken}`,
111
+ ...(agentId ? { 'X-Instar-AgentId': agentId } : {}),
112
+ },
108
113
  });
109
114
 
110
115
  if (!res.ok) {
@@ -160,7 +165,10 @@ async function consumeSubmission() {
160
165
  try {
161
166
  await fetch(`http://localhost:${resolvedPort}/secrets/retrieve/${token}?consume=true`, {
162
167
  method: 'POST',
163
- headers: { 'Authorization': `Bearer ${authToken}` },
168
+ headers: {
169
+ 'Authorization': `Bearer ${authToken}`,
170
+ ...(agentId ? { 'X-Instar-AgentId': agentId } : {}),
171
+ },
164
172
  });
165
173
  } catch {
166
174
  // Non-fatal — the sliding/absolute-cap cleanup will reclaim it regardless.
@@ -38,6 +38,7 @@ fi
38
38
  # Get port from env or config
39
39
  PORT="${INSTAR_PORT:-}"
40
40
  AUTH=""
41
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
41
42
 
42
43
  if [ -f ".instar/config.json" ]; then
43
44
  if [ -z "$PORT" ]; then
@@ -52,6 +53,9 @@ if [ -f ".instar/config.json" ]; then
52
53
  if [ -z "$AUTH" ]; then
53
54
  AUTH=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
54
55
  fi
56
+ if [ -z "$AGENT_ID" ]; then
57
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)
58
+ fi
55
59
  fi
56
60
 
57
61
  PORT="${PORT:-4042}"
@@ -68,6 +72,7 @@ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
68
72
  "http://localhost:${PORT}/slack/reply/${CHANNEL_ID}" \
69
73
  -H "Content-Type: application/json" \
70
74
  ${AUTH:+-H "Authorization: Bearer $AUTH"} \
75
+ ${AGENT_ID:+-H "X-Instar-AgentId: $AGENT_ID"} \
71
76
  -d "{\"text\": ${ESCAPED}}")
72
77
 
73
78
  HTTP_CODE=$(echo "$RESPONSE" | tail -1)
@@ -38,9 +38,13 @@ PORT="${INSTAR_PORT:-4040}"
38
38
  # string-type guard so the { "secret": true } placeholder produced by
39
39
  # SecretMigrator cannot leak through as a bogus Bearer.
40
40
  AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
41
+ AGENT_ID="${INSTAR_AGENT_ID:-}"
41
42
  if [ -z "$AUTH_TOKEN" ] && [ -f ".instar/config.json" ]; then
42
43
  AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
43
44
  fi
45
+ if [ -z "$AGENT_ID" ] && [ -f ".instar/config.json" ]; then
46
+ AGENT_ID=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)
47
+ fi
44
48
 
45
49
  # Escape for JSON
46
50
  JSON_MSG=$(printf '%s' "$MSG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
@@ -53,6 +57,7 @@ if [ -n "$AUTH_TOKEN" ]; then
53
57
  RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/whatsapp/send/${JID}" \
54
58
  -H 'Content-Type: application/json' \
55
59
  -H "Authorization: Bearer ${AUTH_TOKEN}" \
60
+ -H "X-Instar-AgentId: ${AGENT_ID}" \
56
61
  -d "{\"text\":${JSON_MSG}}")
57
62
  else
58
63
  RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/whatsapp/send/${JID}" \
@@ -0,0 +1,20 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The MessageSentinel's LLM layer now downgrades a 'pause' classification to normal pass-through when the message exceeds 25 words (MAX_PAUSE_DIRECTIVE_WORDS). Emergency-stop is deliberately not length-gated (safety asymmetry). The downgrade is logged and carried in the classification reason.
9
+
10
+ ## What to Tell Your User
11
+
12
+ Long working messages that happen to end with phrases like "stand by" or "hold off for now" can no longer be swallowed as pause commands. Previously such a message could pause your agent's session and vanish entirely — you'd see only "Session paused" where your instructions should have landed. Short, genuine pause directives still pause as before.
13
+
14
+ ## Summary of New Capabilities
15
+
16
+ - Pause classifications on messages over 25 words pass through as normal conversation; the message is delivered instead of consumed.
17
+
18
+ ## Evidence
19
+
20
+ Live failures (2026-06-05, topic 1052): two ~200-word mentor coaching messages ending "…and stand by" were LLM-classified 'pause' and CONSUMED by the telegram-forward sentinel intercept — "Session paused" posted at 14:22:58Z and 14:41:24Z, the messages never routed, present in no queue or drop ledger (the third mechanism of the day's delivery-loss trilogy, after #839 inbound replay and #840 outbound purge). The classifier prompt already instructs "substantive content ⇒ NORMAL" but the closing imperative intermittently won — prompt-as-willpower; this guard is the structure. Pinned by 4 tests: long-pause downgrade, short-pause preserved, long emergency-stop preserved, exact 25/26-word boundary.
@@ -0,0 +1,22 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Built-in job, hook, and helper-script templates now include the agent identity header on authenticated local API calls. The scheduler and session launcher expose `INSTAR_AGENT_ID` alongside the existing local auth token so installed templates do not need to guess their identity.
9
+
10
+ ## What to Tell Your User
11
+
12
+ Nothing user-visible. This quiets internal auth deprecation warnings from built-in jobs and hooks and keeps installed templates aligned with the stricter local auth contract.
13
+
14
+ ## Summary of New Capabilities
15
+
16
+ - Job gates, script jobs, and spawned sessions receive `INSTAR_AGENT_ID`.
17
+ - Built-in local API templates send `X-Instar-AgentId` with bearer auth.
18
+ - A regression test scans shipped templates for bearer-only local API calls.
19
+
20
+ ## Evidence
21
+
22
+ Grounded against canonical `authMiddleware`: bearer-only local API calls are still accepted during the deprecation window but log a warning; `/whoami` already requires the header. Focused validation: `npx vitest run tests/unit/template-agent-id-header.test.ts tests/unit/JobScheduler.test.ts tests/unit/JobScheduler-script-job.test.ts tests/unit/telegram-topic-context-session-clock.test.ts` passed, 41/41 tests.
@@ -0,0 +1,12 @@
1
+ # A long working message ending in "stand by" can no longer be eaten as a pause command
2
+
3
+ The message sentinel reads every inbound message and classifies it: normal conversation, a redirect, a pause command, or an emergency stop. Pause and emergency-stop classifications are intercepted — the session is paused or killed, a confirmation is posted, and **the message itself is consumed**, never delivered. That's correct for "stop everything": the message IS the command.
4
+
5
+ The failure: a ~200-word mentor coaching message — task status, instructions, and a closing "…report the numbers back here and stand by" — was classified as a **pause command**. The session paused, "Session paused. Send a message to resume." was posted, and the entire message vanished: not in any queue, not in any drop ledger, just gone. It happened twice in one afternoon on the same topic (14:22 and 14:41), both times on messages ending with "stand by". The classifier's prompt already asks for exactly the right judgment ("substantive content means NORMAL") — but a closing imperative intermittently wins anyway, and a prompt instruction is willpower, not structure.
6
+
7
+ The structural guard: a **pause** classification on a message longer than 25 words downgrades to normal pass-through, with the downgrade logged and recorded in the classification's reason. The rationale is an asymmetry the prompt itself already encodes:
8
+
9
+ - **Emergency-stop is untouched.** Stopping unnecessarily on a long panicked message is a tolerable false positive — safety first, always.
10
+ - **Pause is politeness, not safety.** Consuming a long message to be polite destroys real content for zero protective payoff. A genuine natural-language pause directive ("hold on, wait for me to finish reading before you continue") is well under 25 words; nobody writes a 200-word pause command.
11
+
12
+ The fast-path layer needed no change — its existing 4-word gate already keeps long messages away from pattern matches. Slash commands (/pause, /stop) are exact matches and unaffected. Four tests pin the boundary: long pause downgrades, short pause still pauses, long emergency-stop still stops, and the exact 25/26-word edge.