instar 1.3.953 → 1.3.955

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 (44) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +30 -10
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/DecisionQualityRecorderImpl.d.ts +22 -0
  5. package/dist/core/DecisionQualityRecorderImpl.d.ts.map +1 -1
  6. package/dist/core/DecisionQualityRecorderImpl.js +26 -0
  7. package/dist/core/DecisionQualityRecorderImpl.js.map +1 -1
  8. package/dist/core/MessagingToneGate.d.ts +67 -1
  9. package/dist/core/MessagingToneGate.d.ts.map +1 -1
  10. package/dist/core/MessagingToneGate.js +127 -19
  11. package/dist/core/MessagingToneGate.js.map +1 -1
  12. package/dist/core/PostUpdateMigrator.d.ts +10 -0
  13. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  14. package/dist/core/PostUpdateMigrator.js +44 -1
  15. package/dist/core/PostUpdateMigrator.js.map +1 -1
  16. package/dist/core/TelegramRelay.d.ts +27 -2
  17. package/dist/core/TelegramRelay.d.ts.map +1 -1
  18. package/dist/core/TelegramRelay.js +40 -1
  19. package/dist/core/TelegramRelay.js.map +1 -1
  20. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  21. package/dist/core/devGatedFeatures.js +6 -0
  22. package/dist/core/devGatedFeatures.js.map +1 -1
  23. package/dist/data/provenanceCoverage.d.ts.map +1 -1
  24. package/dist/data/provenanceCoverage.js +22 -0
  25. package/dist/data/provenanceCoverage.js.map +1 -1
  26. package/dist/messaging/outbound-credential-guard.d.ts +116 -0
  27. package/dist/messaging/outbound-credential-guard.d.ts.map +1 -0
  28. package/dist/messaging/outbound-credential-guard.js +157 -0
  29. package/dist/messaging/outbound-credential-guard.js.map +1 -0
  30. package/dist/scaffold/templates.d.ts.map +1 -1
  31. package/dist/scaffold/templates.js +4 -2
  32. package/dist/scaffold/templates.js.map +1 -1
  33. package/dist/server/routes.d.ts.map +1 -1
  34. package/dist/server/routes.js +410 -11
  35. package/dist/server/routes.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/data/builtin-manifest.json +64 -64
  38. package/src/data/provenanceCoverage.ts +22 -0
  39. package/src/scaffold/templates.ts +4 -2
  40. package/src/templates/scripts/telegram-reply.sh +93 -2
  41. package/upgrades/1.3.954.md +22 -0
  42. package/upgrades/1.3.955.md +73 -0
  43. package/upgrades/side-effects/cross-machine-door-capability-registry.md +6 -0
  44. package/upgrades/side-effects/tone-gate-advisory-migration.md +336 -0
@@ -55,7 +55,7 @@ import { writeConfigAtomic, readSelfKnowledgeFlags } from '../core/BootSelfKnowl
55
55
  import { rateLimiter, signViewPath, OUTBOUND_GATE_REVIEW_BUDGET_MS } from './middleware.js';
56
56
  import { reviewWithinBudget } from './outboundGateBudget.js';
57
57
  import { resolveToneRecipientClass } from './toneRecipientClass.js';
58
- import { buildDegradedToneResult, resolveToneGateOperatorConfig, fingerprintAutomatedTemplate } from '../core/MessagingToneGate.js';
58
+ import { RULE_DISPOSITIONS, buildDegradedToneResult, resolveToneGateOperatorConfig, fingerprintAutomatedTemplate } from '../core/MessagingToneGate.js';
59
59
  import { writeLifelineRestartSignal } from '../core/version-skew.js';
60
60
  import { readSessionClocks } from '../core/SessionClockReader.js';
61
61
  import { P13_PROTOCOL_VERSION } from '../core/CompletionEvaluator.js';
@@ -74,9 +74,9 @@ import { classifyMachineEmptyState } from './poolEmptyState.js';
74
74
  import { decorateWithRopeCondition } from './poolRopeCondition.js';
75
75
  // LLM-Decision Quality Meter (llm-decision-quality-meter §5.5) — P9/P10 routes.
76
76
  import { runDecisionGradingPass } from '../core/decisionGradingPass.js';
77
- import { annotateDecisionOutcome, getDecisionAnnotationRejectionCounters } from '../core/DecisionQualityRecorderImpl.js';
77
+ import { annotateDecisionOutcome, decisionQualityRecordingLive, getDecisionAnnotationRejectionCounters } from '../core/DecisionQualityRecorderImpl.js';
78
78
  import { annotateCompletionRealcheck } from '../core/AutonomousRealCheckAnnotator.js';
79
- import { DP_COMPLETION_EVALUATE, PROVENANCE_COVERAGE, findWiredWithoutGraders } from '../data/provenanceCoverage.js';
79
+ import { DP_COMPLETION_EVALUATE, DP_MESSAGING_TONE_GATE, PROVENANCE_COVERAGE, findWiredWithoutGraders } from '../data/provenanceCoverage.js';
80
80
  import { RemoteCloseAudit } from '../core/RemoteCloseAudit.js';
81
81
  import { RemoteAckStore } from '../core/RemoteAckStore.js';
82
82
  import { FailureLedger } from '../monitoring/FailureLedger.js';
@@ -194,6 +194,9 @@ import { resolvePeerFingerprint, resolvePeerFingerprintByName } from '../threadl
194
194
  import { ScopeCoherenceTracker } from '../core/ScopeCoherenceTracker.js';
195
195
  import { isJunkPayload } from '../core/junk-payload.js';
196
196
  import { detectLocalhostLink } from '../core/localhost-link.js';
197
+ import { RelayRefusedError } from '../core/TelegramRelay.js';
198
+ import { scrubForStore } from '../core/durableSecretScrub.js';
199
+ import { credentialGuardMessage, detectOutboundCredential } from '../messaging/outbound-credential-guard.js';
197
200
  import { detectJargon } from '../core/JargonDetector.js';
198
201
  import { detectRawFilePath } from '../core/raw-file-path.js';
199
202
  import { detectParkedOnUser } from '../core/parked-on-user.js';
@@ -999,6 +1002,157 @@ export function createRoutes(ctx) {
999
1002
  // reply isn't re-sent across a restart (finding_cross_restart_duplicate_replies,
1000
1003
  // topic 21816). Fail-open: a backing hiccup degrades to in-memory-only.
1001
1004
  new SqliteOutboundDedupStore(SqliteOutboundDedupStore.defaultPath(ctx.config.stateDir)));
1005
+ /**
1006
+ * Minimum override-reason length. Low ON PURPOSE — this is a "did you type
1007
+ * anything" floor, not a quality bar. A length check cannot judge whether a
1008
+ * reason is good; that is the bulk judge's job, later, reading the recorded
1009
+ * text. Setting it high would only teach the agent to pad.
1010
+ */
1011
+ const TONE_ADVISORY_REASON_MIN = 8;
1012
+ /**
1013
+ * Record how the agent REACTED to an advisory nudge — the tone gate's first
1014
+ * real evidence source (`tone-agent-override-v1` / `tone-agent-complied-v1`).
1015
+ *
1016
+ * override → the verdict is graded `wrong`; complied → `right`. Both land at
1017
+ * the `self-report` rung, so an interested party's account can never outrank
1018
+ * an independent grader.
1019
+ *
1020
+ * Silent no-op without a `decisionRef`: an outcome with nothing to join to is
1021
+ * an orphan row that inflates volume and grades nothing. Total and
1022
+ * fire-and-forget — observability must never break the message path.
1023
+ */
1024
+ /**
1025
+ * A router-minted correlation id: `d-<uuid>` / `b-<uuid>`, optionally with a
1026
+ * machine segment. The agent hands this back through request metadata, so it
1027
+ * is UNTRUSTED input to a durable write — shape-check before it reaches the
1028
+ * annotate chokepoint. (The chokepoint validates grade/rung/owner but has
1029
+ * never had to validate a correlation id, because until this feature no
1030
+ * agent-supplied string could reach it.)
1031
+ */
1032
+ /**
1033
+ * Parse the four tone-advisory reaction fields off a request `metadata`
1034
+ * object, clamped. One helper rather than four inline ternaries per callsite:
1035
+ * the review found the fields plumbed into 2 of 7 outbound routes, and
1036
+ * per-callsite duplication is exactly how that happens again.
1037
+ */
1038
+ function toneAdvisoryMetadata(metadata) {
1039
+ const str = (v, max) => typeof v === 'string' ? v.slice(0, max) : undefined;
1040
+ return {
1041
+ toneAdvisoryAck: str(metadata?.toneAdvisoryAck, 64),
1042
+ toneAdvisoryAckReason: str(metadata?.toneAdvisoryAckReason, 500),
1043
+ toneAdvisoryDecisionRef: str(metadata?.toneAdvisoryDecisionRef, 128),
1044
+ toneAdvisoryComplied: str(metadata?.toneAdvisoryComplied, 64),
1045
+ };
1046
+ }
1047
+ /**
1048
+ * ── Server-side compliance correlation ──────────────────────────────────
1049
+ *
1050
+ * The problem this removes: an override is STRUCTURALLY enforced (the message
1051
+ * will not send without its reason, so the `wrong` grade always lands), while
1052
+ * compliance was an opt-in field the agent had to remember on a revised
1053
+ * re-send. That asymmetry biases the sample toward "the gate was wrong" — the
1054
+ * exact bias the compliance path exists to prevent — and "remember to declare
1055
+ * it" is precisely the willpower this codebase refuses to build on.
1056
+ *
1057
+ * So the server remembers instead. When an advisory 422 goes out we note
1058
+ * (topic → rule, decisionRef, text fingerprint). If the next PASSING send to
1059
+ * that topic within the window carries DIFFERENT text, the agent revised: the
1060
+ * original verdict is graded `right` automatically, with no agent metadata.
1061
+ *
1062
+ * Deliberately conservative — it grades `right` only on positive evidence of a
1063
+ * revision (a passing send with changed text inside the window). A silent
1064
+ * abandonment records nothing rather than being counted either way.
1065
+ */
1066
+ // 10 minutes, not 30: the correlation cannot distinguish "revised the nudged
1067
+ // message" from "gave up on it and sent something unrelated next". A shorter
1068
+ // window is the cheap discriminator — a revision follows a nudge promptly,
1069
+ // while an unrelated message is more likely to arrive later. A similarity
1070
+ // threshold was considered and REJECTED: a genuine rewrite in response to a
1071
+ // nudge often shares almost no wording with the original ("I put it in
1072
+ // docs/x.md" → "want a link or the summary here?"), so similarity would
1073
+ // silently discard exactly the compliances worth recording.
1074
+ const ADVISORY_PENDING_TTL_MS = 10 * 60 * 1000;
1075
+ const ADVISORY_PENDING_MAX = 500;
1076
+ const pendingAdvisories = new Map();
1077
+ function sha256Short(s) {
1078
+ return createHash('sha256').update(s).digest('hex').slice(0, 32);
1079
+ }
1080
+ function notePendingAdvisory(topicId, rule, decisionRef, text) {
1081
+ if (typeof topicId !== 'number')
1082
+ return;
1083
+ // Bounded: drop the oldest rather than grow without limit.
1084
+ if (pendingAdvisories.size >= ADVISORY_PENDING_MAX) {
1085
+ const oldest = [...pendingAdvisories.entries()].sort((a, b) => a[1].at - b[1].at)[0];
1086
+ if (oldest)
1087
+ pendingAdvisories.delete(oldest[0]);
1088
+ }
1089
+ pendingAdvisories.set(topicId, { rule, decisionRef, textSha: sha256Short(text), at: Date.now() });
1090
+ }
1091
+ /** Returns the reaction to record, if this passing send revises a pending advisory. */
1092
+ function takeRevisionOf(topicId, text) {
1093
+ if (typeof topicId !== 'number')
1094
+ return null;
1095
+ const p = pendingAdvisories.get(topicId);
1096
+ if (!p)
1097
+ return null;
1098
+ if (Date.now() - p.at > ADVISORY_PENDING_TTL_MS) {
1099
+ pendingAdvisories.delete(topicId);
1100
+ return null;
1101
+ }
1102
+ // Identical text is not a revision — it is a resend, and a resend that
1103
+ // happens to pass is NOT evidence the gate was right.
1104
+ if (sha256Short(text) === p.textSha)
1105
+ return null;
1106
+ pendingAdvisories.delete(topicId);
1107
+ return { rule: p.rule, decisionRef: p.decisionRef };
1108
+ }
1109
+ function isPlausibleDecisionRef(ref) {
1110
+ return /^[db]-[0-9a-f]{8}(-[0-9a-f-]{8,})?[0-9a-f-]*$/i.test(ref) && ref.length <= 128;
1111
+ }
1112
+ function recordToneAdvisoryReaction(input) {
1113
+ try {
1114
+ if (!input.decisionRef || !isPlausibleDecisionRef(input.decisionRef))
1115
+ return;
1116
+ const ref = input.decisionRef;
1117
+ const reaction = input.reaction;
1118
+ const rule = input.rule;
1119
+ const derivation = input.derivation;
1120
+ // The reason IS the evidence, and it is agent-authored free text landing
1121
+ // in two durable stores. The chokepoint's own scrub is weaker than the
1122
+ // pattern set this very change treats as authoritative (it misses a
1123
+ // 20-char AWS key), so scrub with the strong list HERE before handing it
1124
+ // over. A guard that walls a credential in the message body while storing
1125
+ // one from the reason field would be theatre.
1126
+ const note = input.reason ? scrubForStore(input.reason).text.slice(0, 500) : undefined;
1127
+ // ── Off the send path ────────────────────────────────────────────────
1128
+ // `upsertOutcome` is a synchronous better-sqlite3 transaction that
1129
+ // recomputes the whole (decision_point, day) rollup twice over a window-
1130
+ // function view SQLite cannot push a predicate into. Measured: ~3ms at
1131
+ // today's 1.4k rows, ~80ms at 60k, ~190ms at 120k — and this change puts
1132
+ // the FIRST high-frequency conversational path in front of it. The result
1133
+ // is discarded either way, so there is no reason for the operator's reply
1134
+ // latency to carry a growing rollup recompute.
1135
+ setImmediate(() => {
1136
+ try {
1137
+ annotateDecisionOutcome({
1138
+ correlationId: ref,
1139
+ ruleId: reaction === 'override' ? 'tone-agent-override-v1' : 'tone-agent-complied-v1',
1140
+ gradedBy: { component: 'ToneGateAdvisory' },
1141
+ grade: reaction === 'override' ? 'wrong' : 'right',
1142
+ decisionPoint: DP_MESSAGING_TONE_GATE,
1143
+ ...(note ? { evidenceNote: note } : {}),
1144
+ evidence: { reaction, rule, ...(derivation ? { derivation } : {}) },
1145
+ });
1146
+ }
1147
+ catch {
1148
+ /* @silent-fallback-ok — recording a reaction must never affect delivery */
1149
+ }
1150
+ });
1151
+ }
1152
+ catch {
1153
+ /* @silent-fallback-ok — recording a reaction must never affect delivery */
1154
+ }
1155
+ }
1002
1156
  async function evaluateOutbound(text, channel, options) {
1003
1157
  // ── Localhost-link guard (operator-mandated HARD rule, 2026-06-05) ──
1004
1158
  // A clickable localhost/loopback link must never reach a user — the user
@@ -1025,6 +1179,30 @@ export function createRoutes(ctx) {
1025
1179
  };
1026
1180
  }
1027
1181
  }
1182
+ // ── Live-credential hard wall (operator directive 2026-07-19) ──────────
1183
+ // The ONE non-overridable outbound check, and the precondition that makes
1184
+ // the advisory migration safe: every LLM judgment became an overridable
1185
+ // nudge, so the thing that must never be overridable moved to a
1186
+ // deterministic guard. Runs BEFORE the authority — like the localhost-link
1187
+ // guard — so it holds on installs with no gate configured, during a
1188
+ // provider outage, and under spawn-cap saturation. No `metadata` escape
1189
+ // hatch exists by design; the error names the credential CLASS only.
1190
+ {
1191
+ const cred = detectOutboundCredential(text);
1192
+ if (cred.detected && cred.kind) {
1193
+ return {
1194
+ ok: false,
1195
+ status: 422,
1196
+ reason: 'credential-exposure-guard',
1197
+ body: {
1198
+ error: credentialGuardMessage(cred.kind),
1199
+ blockedBy: 'credential-exposure-guard',
1200
+ credentialKind: cred.kind,
1201
+ overridable: false,
1202
+ },
1203
+ };
1204
+ }
1205
+ }
1028
1206
  if (!ctx.messagingToneGate)
1029
1207
  return { ok: true }; // No authority configured — pass through
1030
1208
  try {
@@ -1298,7 +1476,11 @@ export function createRoutes(ctx) {
1298
1476
  // Operator-tier delivery suppresses both fail-closed and degrade (we want OPEN).
1299
1477
  const budgetFailClosed = !operatorTierDeliver && perCallAllowsClosed && globalFailClosed !== false;
1300
1478
  const budgetDegrade = budgetFailClosed && globalFailClosed !== true
1301
- ? (latencyMs) => buildDegradedToneResult(text, latencyMs, 'budget-timeout')
1479
+ ? (latencyMs) =>
1480
+ // Same disposition as review()'s fast-throw degrade: the SLOW and
1481
+ // FAST manifestations of one outage must not disagree about
1482
+ // whether a caught artifact is a nudge or a wall.
1483
+ buildDegradedToneResult(text, latencyMs, 'budget-timeout', _toneCfg?.advisoryMigration === true)
1302
1484
  : undefined;
1303
1485
  const templateFingerprint = options.messageKind === 'automated' ? fingerprintAutomatedTemplate(text) : undefined;
1304
1486
  const result = await reviewWithinBudget(ctx.messagingToneGate.review(text, {
@@ -1332,6 +1514,36 @@ export function createRoutes(ctx) {
1332
1514
  signals,
1333
1515
  result,
1334
1516
  });
1517
+ // ── Compliance credit ────────────────────────────────────────────────
1518
+ // The revised re-send passed → the ORIGINAL verdict is graded `right`.
1519
+ // Recording only overrides would measure the gate exclusively through the
1520
+ // cases the agent disputed: a sample guaranteed to look worse than the
1521
+ // gate is.
1522
+ //
1523
+ // The SERVER correlates this (see notePendingAdvisory) so it does not
1524
+ // depend on the agent remembering two metadata fields across a revise
1525
+ // cycle — the override path is structurally enforced, and an opt-in
1526
+ // counterpart would leave the sample permanently skewed. The explicit
1527
+ // `toneAdvisoryComplied` remains honored as a direct declaration.
1528
+ if (result.pass) {
1529
+ const revised = takeRevisionOf(options.topicId, text);
1530
+ if (options.toneAdvisoryComplied) {
1531
+ recordToneAdvisoryReaction({
1532
+ decisionRef: options.toneAdvisoryDecisionRef ?? revised?.decisionRef,
1533
+ rule: options.toneAdvisoryComplied,
1534
+ reaction: 'complied',
1535
+ derivation: 'declared',
1536
+ });
1537
+ }
1538
+ else if (revised) {
1539
+ recordToneAdvisoryReaction({
1540
+ decisionRef: revised.decisionRef,
1541
+ rule: revised.rule,
1542
+ reaction: 'complied',
1543
+ derivation: 'inferred',
1544
+ });
1545
+ }
1546
+ }
1335
1547
  if (!result.pass) {
1336
1548
  // ── Advisory disposition (operator directive 2026-07-18, topic 29723) ──
1337
1549
  // An advisory-rule citation is a NUDGE, never a terminal block: the
@@ -1340,9 +1552,93 @@ export function createRoutes(ctx) {
1340
1552
  // resend unchanged with metadata.toneAdvisoryAck = "<rule>" to
1341
1553
  // acknowledge; the override is RECORDED (a decision-quality signal,
1342
1554
  // never authority). A blocking rule can NEVER be overridden this way.
1555
+ // ── The evidence-capturability invariant ─────────────────────────────
1556
+ // NEVER trade authority for evidence we are not collecting.
1557
+ //
1558
+ // The migration's entire justification is that an overridable nudge
1559
+ // produces the disagreement data a hard block cannot. That bargain is
1560
+ // void whenever the reaction could not actually be recorded, and there
1561
+ // are two independent ways for that to be true — both reachable in the
1562
+ // DEFAULT configuration, which is what makes this a real hazard rather
1563
+ // than a theoretical one:
1564
+ //
1565
+ // 1. The quality seam is dark or dry-run. `provenance.uniformSeam` is
1566
+ // a SEPARATE gate from `toneGate.advisoryMigration`, and its dryRun
1567
+ // DEFAULTS TRUE — so flipping the migration on without it would
1568
+ // hand out every loosening and record nothing.
1569
+ // 2. No `decisionRef` was minted. The budget-timeout degrade builds
1570
+ // its verdict in the route, outside the gate, where the router's
1571
+ // correlation id is unreachable — precisely the path that fires
1572
+ // under load.
1573
+ //
1574
+ // In either case the verdict falls back to a BLOCK. That is the safe
1575
+ // direction (it preserves today's behaviour rather than relaxing it),
1576
+ // and it makes the migration self-limiting: it can only ever be as live
1577
+ // as its own evidence collection.
1578
+ //
1579
+ // SCOPE: only a MIGRATION-derived advisory is subject to this. A rule
1580
+ // whose BASELINE disposition is already advisory (B21, which shipped
1581
+ // that way in the correction-derived-hardening work) never made the
1582
+ // evidence bargain — it was always simply the agent's call — so
1583
+ // demoting it here would silently harden a shipped, working behaviour
1584
+ // on every install where the quality seam is dark. That would be a
1585
+ // regression introduced by a safety check, which is its own bug class.
1586
+ if (result.advisory === true &&
1587
+ RULE_DISPOSITIONS[result.rule] !== 'advisory' &&
1588
+ (!result.decisionRef || !decisionQualityRecordingLive())) {
1589
+ logToneGateDecision({
1590
+ text,
1591
+ channel,
1592
+ topicId: options.topicId,
1593
+ signals,
1594
+ result: { ...result, advisory: false, advisoryUnrecordable: true },
1595
+ });
1596
+ return {
1597
+ ok: false,
1598
+ status: 422,
1599
+ reason: 'tone-gate-blocked',
1600
+ body: {
1601
+ error: 'tone-gate-blocked',
1602
+ rule: result.rule,
1603
+ issue: result.issue,
1604
+ suggestion: result.suggestion,
1605
+ // Named, never silent: the operator can see WHY a nudge came back
1606
+ // as a wall (No Silent Degradation).
1607
+ advisoryUnavailable: !result.decisionRef
1608
+ ? 'no-decision-ref'
1609
+ : 'quality-recording-not-live',
1610
+ latencyMs: result.latencyMs,
1611
+ },
1612
+ };
1613
+ }
1343
1614
  if (result.advisory === true) {
1344
1615
  if (options.toneAdvisoryAck === result.rule) {
1345
- const overridden = { ...result, advisoryOverridden: true };
1616
+ // The override REASON is structurally required, not requested. The
1617
+ // whole point of the migration is the evidence: an override with no
1618
+ // recorded reason is an ungradeable event, and "remember to explain
1619
+ // yourself" is exactly the willpower this codebase refuses to rely
1620
+ // on. So a reasonless ack is refused and the message stays unsent.
1621
+ const reason = (options.toneAdvisoryAckReason ?? '').trim();
1622
+ if (reason.length < TONE_ADVISORY_REASON_MIN) {
1623
+ return {
1624
+ ok: false,
1625
+ status: 422,
1626
+ reason: 'tone-gate-advisory-reason-required',
1627
+ body: {
1628
+ error: 'tone-gate-advisory-reason-required',
1629
+ notSent: true,
1630
+ rule: result.rule,
1631
+ howToProceed: `The override needs a reason: re-send with metadata.toneAdvisoryAck = "${result.rule}" AND ` +
1632
+ `metadata.toneAdvisoryAckReason = a short sentence on why the nudge is wrong here. ` +
1633
+ `It is recorded as the evidence that grades this check.`,
1634
+ },
1635
+ };
1636
+ }
1637
+ const overridden = {
1638
+ ...result,
1639
+ advisoryOverridden: true,
1640
+ overrideReasonHead: scrubForStore(reason).text.slice(0, 120),
1641
+ };
1346
1642
  logToneGateDecision({
1347
1643
  text,
1348
1644
  channel,
@@ -1350,8 +1646,17 @@ export function createRoutes(ctx) {
1350
1646
  signals,
1351
1647
  result: overridden,
1352
1648
  });
1649
+ recordToneAdvisoryReaction({
1650
+ decisionRef: options.toneAdvisoryDecisionRef ?? result.decisionRef,
1651
+ rule: result.rule,
1652
+ reaction: 'override',
1653
+ reason,
1654
+ });
1353
1655
  return { ok: true };
1354
1656
  }
1657
+ // Remember it so a later revised send is credited WITHOUT the agent
1658
+ // having to carry metadata back (the asymmetry fix).
1659
+ notePendingAdvisory(options.topicId, result.rule, result.decisionRef, text);
1355
1660
  return {
1356
1661
  ok: false,
1357
1662
  status: 422,
@@ -1362,7 +1667,16 @@ export function createRoutes(ctx) {
1362
1667
  rule: result.rule,
1363
1668
  issue: result.issue,
1364
1669
  suggestion: result.suggestion,
1365
- howToProceed: `ADVISORY the message was NOT sent, and the decision is yours: revise and re-send, or re-send unchanged with metadata.toneAdvisoryAck set to "${result.rule}" to acknowledge and deliver (the override is recorded).`,
1670
+ // Handed back so the resend can JOIN its reaction to this exact
1671
+ // decision. Without it an override is an orphan the grader can
1672
+ // never attach to the verdict it disputes.
1673
+ ...(result.decisionRef ? { decisionRef: result.decisionRef } : {}),
1674
+ howToProceed: `ADVISORY — the message was NOT sent, and the decision is yours. Either (a) revise and re-send, adding ` +
1675
+ `metadata.toneAdvisoryComplied = "${result.rule}"` +
1676
+ (result.decisionRef ? ` and metadata.toneAdvisoryDecisionRef = "${result.decisionRef}"` : '') +
1677
+ ` so the check gets credit for a catch; or (b) re-send unchanged with metadata.toneAdvisoryAck = ` +
1678
+ `"${result.rule}" plus metadata.toneAdvisoryAckReason explaining why the nudge is wrong here. ` +
1679
+ `Both are recorded — they are how this check learns whether it is any good.`,
1366
1680
  latencyMs: result.latencyMs,
1367
1681
  },
1368
1682
  };
@@ -1724,6 +2038,22 @@ export function createRoutes(ctx) {
1724
2038
  // failure so it is AUDITED, never silent (the No-Silent-Degradation
1725
2039
  // reconciliation rests on this being observable).
1726
2040
  failedOpenOperatorChannel: entry.result.failedOpenOperatorChannel || false,
2041
+ // ── Advisory migration audit (ALWAYS ON) ──────────────────────────
2042
+ // The override is the one event this whole feature exists to observe,
2043
+ // and until this line existed an override logged BYTE-IDENTICALLY to
2044
+ // the verdict it overrode. The structured evidence row is the rich
2045
+ // record, but it lives behind a separate feature gate that defaults to
2046
+ // recording nothing — so the audit that must never be absent belongs
2047
+ // HERE, on the unconditional stderr line, not there.
2048
+ advisory: entry.result.advisory || false,
2049
+ advisoryOverridden: entry.result.advisoryOverridden || false,
2050
+ // Present only when the advisory disposition was withdrawn because its
2051
+ // evidence could not be captured — names the reason rather than
2052
+ // silently reverting to a wall.
2053
+ advisoryUnrecordable: entry.result.advisoryUnrecordable || false,
2054
+ // A bounded, scrubbed head of the override reason. Enough to audit that
2055
+ // a reason existed and roughly what it said; never the whole field.
2056
+ overrideReasonHead: entry.result.overrideReasonHead ?? null,
1727
2057
  latencyMs: entry.result.latencyMs,
1728
2058
  signals: {
1729
2059
  junk: entry.signals.junk?.detected ?? null,
@@ -11932,6 +12262,15 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
11932
12262
  // Advisory-rule override (operator directive 2026-07-18): the FULL rule id
11933
12263
  // the sender explicitly acknowledges. Length-clamped; validated against the
11934
12264
  // cited rule at the seam (a mismatch or a blocking rule is never overridden).
12265
+ const toneAdvisoryAckReason = typeof metadata?.toneAdvisoryAckReason === 'string'
12266
+ ? metadata.toneAdvisoryAckReason.slice(0, 500)
12267
+ : undefined;
12268
+ const toneAdvisoryDecisionRef = typeof metadata?.toneAdvisoryDecisionRef === 'string'
12269
+ ? metadata.toneAdvisoryDecisionRef.slice(0, 128)
12270
+ : undefined;
12271
+ const toneAdvisoryComplied = typeof metadata?.toneAdvisoryComplied === 'string'
12272
+ ? metadata.toneAdvisoryComplied.slice(0, 64)
12273
+ : undefined;
11935
12274
  const toneAdvisoryAck = typeof metadata?.toneAdvisoryAck === 'string'
11936
12275
  ? metadata.toneAdvisoryAck.slice(0, 64)
11937
12276
  : undefined;
@@ -12011,6 +12350,9 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
12011
12350
  allowLocalhostLink,
12012
12351
  messageKind,
12013
12352
  toneAdvisoryAck,
12353
+ toneAdvisoryAckReason,
12354
+ toneAdvisoryDecisionRef,
12355
+ toneAdvisoryComplied,
12014
12356
  })))
12015
12357
  return;
12016
12358
  // In-flight reservation (2026-07-03): the isDuplicate pre-check above and the
@@ -12038,12 +12380,23 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
12038
12380
  // Relay-hop forwarding (§2.5): when this standby relays through the
12039
12381
  // lease holder, the kind metadata must survive the hop so the
12040
12382
  // HOLDER's gate/audit see accurate context. Direct sends ignore it.
12041
- kindMetadata: messageKind || senderClass || advisoryAck
12383
+ kindMetadata: messageKind || senderClass || advisoryAck || toneAdvisoryAck || toneAdvisoryComplied
12042
12384
  ? {
12043
12385
  ...(messageKind ? { messageKind } : {}),
12044
12386
  ...(senderClass ? { senderClass } : {}),
12045
12387
  ...(metadataJobSlug ? { jobSlug: metadataJobSlug } : {}),
12046
12388
  ...(advisoryAck ? { advisoryAck: true, advisoryCodes } : {}),
12389
+ // ── Tone-advisory reaction across the relay hop ─────────────
12390
+ // A tokenless standby SKIPS its own gate and relays to the
12391
+ // lease holder, which gates on receipt. Without these four the
12392
+ // holder re-cites the advisory on every attempt and the ack can
12393
+ // never reach it — a relayed topic would hold a permanently
12394
+ // unsendable message with no recourse, which is the exact
12395
+ // failure the migration exists to remove.
12396
+ ...(toneAdvisoryAck ? { toneAdvisoryAck } : {}),
12397
+ ...(toneAdvisoryAckReason ? { toneAdvisoryAckReason } : {}),
12398
+ ...(toneAdvisoryDecisionRef ? { toneAdvisoryDecisionRef } : {}),
12399
+ ...(toneAdvisoryComplied ? { toneAdvisoryComplied } : {}),
12047
12400
  }
12048
12401
  : undefined,
12049
12402
  });
@@ -12138,6 +12491,16 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
12138
12491
  // retry of this exact text is not wrongly suppressed as a duplicate. Safe
12139
12492
  // no-op when nothing was reserved (allowDuplicate / below-floor text).
12140
12493
  outboundContentDedup.releaseReservation(topicId, text);
12494
+ // ── Holder refusal, relayed verbatim ────────────────────────────────
12495
+ // On a tokenless standby the tone gate runs on the HOLDER, so its 422
12496
+ // arrives here as a thrown RelayRefusedError. Re-emit the holder's own
12497
+ // status and body: reporting an actionable refusal (a nudge with a rule,
12498
+ // a decisionRef and how to proceed) as a generic 500 is what left a
12499
+ // relayed topic holding a permanently unanswerable message.
12500
+ if (err instanceof RelayRefusedError) {
12501
+ res.status(err.status).json({ ...err.body, relayedFromHolder: true });
12502
+ return;
12503
+ }
12141
12504
  // @silent-fallback-ok — NOT a silent fallback: this catch surfaces a 500 to the
12142
12505
  // caller. The tag exists because the no-silent-fallbacks scanner's fixed 20-line
12143
12506
  // window reaches past this route's end into the next route's `db = null`
@@ -12356,6 +12719,11 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
12356
12719
  // verdicts (a fast block) still hold; only the no-verdict-in-time path
12357
12720
  // delivers. (Block/allow on a returned verdict is unchanged.)
12358
12721
  failClosedOnBudgetTimeout: false,
12722
+ // Channel parity for the advisory migration. This route is MANDATED by
12723
+ // the agent template for every ship/restart narration and has no
12724
+ // fallback channel — so an advisory 422 here that promised an override
12725
+ // the route could not accept would loop the agent with nowhere to go.
12726
+ ...toneAdvisoryMetadata(metadata),
12359
12727
  }))
12360
12728
  return;
12361
12729
  try {
@@ -12898,9 +13266,10 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
12898
13266
  if (await checkOutboundMessage(text, 'slack', res, {
12899
13267
  allowDebugText: metadata?.allowDebugText === true,
12900
13268
  allowDuplicate: metadata?.allowDuplicate === true,
12901
- toneAdvisoryAck: typeof metadata?.toneAdvisoryAck === 'string'
12902
- ? metadata.toneAdvisoryAck.slice(0, 64)
12903
- : undefined,
13269
+ // Channel parity: the override/compliance evidence path is not a
13270
+ // Telegram feature. A Slack override that recorded nothing would make
13271
+ // the meter's sample silently channel-dependent.
13272
+ ...toneAdvisoryMetadata(metadata),
12904
13273
  // Kind threading mirrors /telegram/reply — the jargon/filePath signal
12905
13274
  // computation is single-sourced inside evaluateOutbound, so every
12906
13275
  // channel gets it uniformly (spec outbound-jargon-filepath-gap §2.2).
@@ -14538,6 +14907,9 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
14538
14907
  let right = 0;
14539
14908
  let wrong = 0;
14540
14909
  let unknown = 0;
14910
+ // right+wrong outcomes whose evidence is the interested party's own
14911
+ // account (see the honesty marker on gradeDistribution below).
14912
+ let selfReportGraded = 0;
14541
14913
  const byRule = {};
14542
14914
  const byRung = {};
14543
14915
  const byStrength = {};
@@ -14548,6 +14920,9 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
14548
14920
  wrong += r.n;
14549
14921
  else if (r.grade === 'unknown')
14550
14922
  unknown += r.n;
14923
+ if ((r.grade === 'right' || r.grade === 'wrong') && r.evidenceStrength === 'self-report') {
14924
+ selfReportGraded += r.n;
14925
+ }
14551
14926
  bump(byRule, r.ruleId, r.grade, r.n);
14552
14927
  bump(byRung, r.rung, r.grade, r.n);
14553
14928
  bump(byStrength, r.evidenceStrength, r.grade, r.n);
@@ -14564,7 +14939,26 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
14564
14939
  outcomesKnownRatio: decisions > 0 ? outcomesKnown / decisions : 0,
14565
14940
  // Below the minimum sample an aggregate rate is not actionable (§5.5 codex r5).
14566
14941
  insufficientEvidence: outcomesKnown < minSample,
14567
- gradeDistribution: { right, wrong, unknown, expired: expiredByPoint.get(entry.decisionPoint) ?? 0 },
14942
+ gradeDistribution: {
14943
+ right,
14944
+ wrong,
14945
+ unknown,
14946
+ expired: expiredByPoint.get(entry.decisionPoint) ?? 0,
14947
+ // ── Self-report honesty marker ────────────────────────────────────
14948
+ // `byStrength` segregates evidence classes, but this FLAT sibling
14949
+ // blends them — and it is the field a casual reader (or a future
14950
+ // automated consumer) quotes. With the tone gate's agent-reaction
14951
+ // rules registered, a decision point can now have right/wrong grades
14952
+ // that are ENTIRELY the agent's own account of a judgment about its
14953
+ // own message. Unmarked, that reads as measured error rate.
14954
+ //
14955
+ // These two fields let any consumer of the flat distribution see how
14956
+ // much of it is self-report without having to know `byStrength`
14957
+ // exists. `selfReportOnly` is the loud case: every graded outcome
14958
+ // here is the interested party's word.
14959
+ selfReportShare: right + wrong > 0 ? selfReportGraded / (right + wrong) : 0,
14960
+ selfReportOnly: right + wrong > 0 && selfReportGraded === right + wrong,
14961
+ },
14568
14962
  // Strength FIRST (default aggregate) — proof-like and heuristic grades are never conflated.
14569
14963
  byStrength,
14570
14964
  byRule,
@@ -16470,6 +16864,10 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
16470
16864
  if (await checkOutboundMessage(text, 'whatsapp', res, {
16471
16865
  allowDebugText: metadata?.allowDebugText === true,
16472
16866
  allowDuplicate: metadata?.allowDuplicate === true,
16867
+ // Channel parity for the advisory migration — an override path that
16868
+ // works on Telegram and silently does not on WhatsApp would make the
16869
+ // meter's sample channel-dependent, and strand this channel's agent.
16870
+ ...toneAdvisoryMetadata(metadata),
16473
16871
  }))
16474
16872
  return;
16475
16873
  try {
@@ -16522,6 +16920,7 @@ document.getElementById('mcpForm').addEventListener('submit', async function (e)
16522
16920
  if (await checkOutboundMessage(text, 'imessage', res, {
16523
16921
  allowDebugText: imessageMetadata?.allowDebugText === true,
16524
16922
  allowDuplicate: imessageMetadata?.allowDuplicate === true,
16923
+ ...toneAdvisoryMetadata(imessageMetadata),
16525
16924
  }))
16526
16925
  return;
16527
16926
  }