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
package/dist/server/routes.js
CHANGED
|
@@ -128,6 +128,8 @@ import { ScopeCoherenceTracker } from '../core/ScopeCoherenceTracker.js';
|
|
|
128
128
|
import { isJunkPayload } from '../core/junk-payload.js';
|
|
129
129
|
import { detectLocalhostLink } from '../core/localhost-link.js';
|
|
130
130
|
import { detectJargon } from '../core/JargonDetector.js';
|
|
131
|
+
import { detectRawFilePath } from '../core/raw-file-path.js';
|
|
132
|
+
import { OutboundAdvisoryAudit, composeAdvisories, PREFLIGHT_TEXT_CAP, } from '../messaging/OutboundAdvisory.js';
|
|
131
133
|
import { TruncationDetector } from '../paste/TruncationDetector.js';
|
|
132
134
|
import { SecretDrop } from './SecretDrop.js';
|
|
133
135
|
import { computeFingerprint } from '../threadline/client/MessageEncryptor.js';
|
|
@@ -767,7 +769,18 @@ export function createRoutes(ctx) {
|
|
|
767
769
|
reason: junkResult.reason,
|
|
768
770
|
};
|
|
769
771
|
}
|
|
770
|
-
|
|
772
|
+
// ── Jargon signal — single-sourced here for ALL channels, scoped to
|
|
773
|
+
// non-'reply' kinds (health-alert | automated). The gate's jargon rule
|
|
774
|
+
// (B12) is health-alert-scoped and conversational replies are explicitly
|
|
775
|
+
// jargon-legal ("prose discussion of internals is fine"), so feeding the
|
|
776
|
+
// signal on a 'reply' is dead weight that only adds over-block tail.
|
|
777
|
+
// The old `options.jargon` caller opt-in is dropped — the kind decides.
|
|
778
|
+
// Live-config kill switch: messaging.outboundFloor.jargonAlways
|
|
779
|
+
// (absence = on; read per request so a rollback needs no restart).
|
|
780
|
+
// Spec: outbound-jargon-filepath-gap §2.2.
|
|
781
|
+
const kindForSignals = options.messageKind ?? 'reply';
|
|
782
|
+
const jargonAlwaysOn = ctx.liveConfig?.get('messaging.outboundFloor.jargonAlways', true) ?? true;
|
|
783
|
+
if (jargonAlwaysOn && (kindForSignals === 'health-alert' || kindForSignals === 'automated')) {
|
|
771
784
|
try {
|
|
772
785
|
const j = detectJargon(text);
|
|
773
786
|
signals.jargon = { detected: j.detected, terms: j.terms, score: j.score };
|
|
@@ -776,6 +789,20 @@ export function createRoutes(ctx) {
|
|
|
776
789
|
// Detector errors never override the authority — skip the signal.
|
|
777
790
|
}
|
|
778
791
|
}
|
|
792
|
+
// ── Raw-file-path signal — ALL kinds. Pure SIGNAL anchoring the
|
|
793
|
+
// existing B2_FILE_PATH judgment with the exact deterministic match;
|
|
794
|
+
// a legitimate "I edited src/foo.ts" stays the authority's call.
|
|
795
|
+
// Fail-OPEN: a detector throw skips the signal, never withholds.
|
|
796
|
+
// Spec: outbound-jargon-filepath-gap §2.3.
|
|
797
|
+
try {
|
|
798
|
+
const fp = detectRawFilePath(text);
|
|
799
|
+
if (fp.detected) {
|
|
800
|
+
signals.filePath = { detected: true, match: fp.match };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// @silent-fallback-ok — detector errors never override the authority; skip the signal (outbound-jargon-filepath-gap §2.3).
|
|
805
|
+
}
|
|
779
806
|
// Recent conversation — used by both the authority and the dedup detector.
|
|
780
807
|
let recentMessages;
|
|
781
808
|
let recentOutbound = [];
|
|
@@ -956,6 +983,50 @@ export function createRoutes(ctx) {
|
|
|
956
983
|
// AgentServer; this is the §3.3 single-funnel hookup, so the publisher can never
|
|
957
984
|
// reach sendToTopic without passing through the identical evaluateOutbound.
|
|
958
985
|
ctx.growthDigestPublisher?.attachSender((text) => postToUpdatesTopic(text));
|
|
986
|
+
// ── Outbound advisory (inform-only preflight for automated senders) ──
|
|
987
|
+
// Spec: docs/specs/outbound-jargon-filepath-gap.md §2.4. The SERVER is the
|
|
988
|
+
// single audit writer; escalation informs the OPERATOR via one deduped
|
|
989
|
+
// Attention item (fixed sourceContext so the per-source topic budget binds).
|
|
990
|
+
// Thresholds are live-config reads — tuning needs no restart.
|
|
991
|
+
const outboundAdvisoryAudit = new OutboundAdvisoryAudit({
|
|
992
|
+
logPath: path.join(ctx.config.stateDir, '..', 'logs', 'outbound-advisory.jsonl'),
|
|
993
|
+
getIgnoreThreshold: () => ctx.liveConfig?.get('messaging.outboundAdvisory.ignoreEscalationThreshold', 3) ?? 3,
|
|
994
|
+
getSlugThreshold: () => ctx.liveConfig?.get('messaging.outboundAdvisory.ignoreEscalationSlugThreshold', 5) ?? 5,
|
|
995
|
+
raiseAttention: (item) => ctx.telegram
|
|
996
|
+
? ctx.telegram.createAttentionItem({
|
|
997
|
+
id: item.id,
|
|
998
|
+
title: item.title,
|
|
999
|
+
summary: item.summary,
|
|
1000
|
+
category: item.category,
|
|
1001
|
+
priority: item.priority,
|
|
1002
|
+
description: item.description,
|
|
1003
|
+
sourceContext: item.sourceContext,
|
|
1004
|
+
})
|
|
1005
|
+
: undefined,
|
|
1006
|
+
});
|
|
1007
|
+
/** Runtime enum coercion (TypeScript unions don't validate at runtime —
|
|
1008
|
+
* the value reaches the gate prompt, the audit, and the relay hop, so an
|
|
1009
|
+
* unrecognized kind is coerced to 'unknown' before threading anywhere). */
|
|
1010
|
+
function coerceMessageKind(value) {
|
|
1011
|
+
if (value === undefined || value === null)
|
|
1012
|
+
return undefined;
|
|
1013
|
+
return value === 'reply' || value === 'health-alert' || value === 'automated' || value === 'unknown'
|
|
1014
|
+
? value
|
|
1015
|
+
: 'unknown';
|
|
1016
|
+
}
|
|
1017
|
+
function coerceSenderClass(value) {
|
|
1018
|
+
if (value === undefined || value === null)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
return value === 'script' || value === 'llm-session' ? value : 'unknown';
|
|
1021
|
+
}
|
|
1022
|
+
function coerceAdvisoryCodes(value) {
|
|
1023
|
+
if (!Array.isArray(value))
|
|
1024
|
+
return [];
|
|
1025
|
+
return value
|
|
1026
|
+
.filter((v) => typeof v === 'string')
|
|
1027
|
+
.map((v) => v.slice(0, 32))
|
|
1028
|
+
.slice(0, 8);
|
|
1029
|
+
}
|
|
959
1030
|
/**
|
|
960
1031
|
* Self-Violation Signal — OBSERVE-ONLY (Correction & Preference Learning
|
|
961
1032
|
* Sentinel extension).
|
|
@@ -7004,6 +7075,54 @@ export function createRoutes(ctx) {
|
|
|
7004
7075
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
7005
7076
|
}
|
|
7006
7077
|
});
|
|
7078
|
+
// ── POST /messaging/preflight — the inform-only advisory preflight ──
|
|
7079
|
+
// Deterministic detectors ONLY (no LLM call); 200 with advisories, never a
|
|
7080
|
+
// content-4xx; fail-open contract: the calling script treats any non-200 /
|
|
7081
|
+
// timeout / malformed response as an empty advisory list and proceeds to
|
|
7082
|
+
// the send. Spec: outbound-jargon-filepath-gap §2.4(1).
|
|
7083
|
+
router.post('/messaging/preflight', (req, res) => {
|
|
7084
|
+
const { text, topicId, jobSlug } = (req.body ?? {});
|
|
7085
|
+
if (!text || typeof text !== 'string') {
|
|
7086
|
+
res.status(400).json({ error: '"text" field required' });
|
|
7087
|
+
return;
|
|
7088
|
+
}
|
|
7089
|
+
const kind = coerceMessageKind((req.body ?? {}).messageKind) ?? 'reply';
|
|
7090
|
+
const enabled = ctx.liveConfig?.get('messaging.outboundAdvisory.enabled', true) ?? true;
|
|
7091
|
+
if (!enabled) {
|
|
7092
|
+
// Rollback lever (live config, no restart): behave exactly like a clean
|
|
7093
|
+
// preflight so the script proceeds straight to the send.
|
|
7094
|
+
res.json({ advisories: [], disabled: true });
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
const topic = Number(topicId);
|
|
7098
|
+
if (kind === 'automated' && (!Number.isFinite(topic) || typeof jobSlug !== 'string' || jobSlug.length === 0)) {
|
|
7099
|
+
// Required for audit keying, escalation, and spoof correlation — a
|
|
7100
|
+
// localhost POST carries no server-visible session identity to derive
|
|
7101
|
+
// them from. (The shipped script always sends both; a 400 here fails
|
|
7102
|
+
// open script-side anyway.)
|
|
7103
|
+
res.status(400).json({ error: 'automated preflight requires numeric "topicId" and string "jobSlug"' });
|
|
7104
|
+
return;
|
|
7105
|
+
}
|
|
7106
|
+
const capped = text.length > PREFLIGHT_TEXT_CAP ? text.slice(0, PREFLIGHT_TEXT_CAP) : text;
|
|
7107
|
+
// Advisories are composed for automated sends only — the conversational
|
|
7108
|
+
// path already has the full authority pipeline, and the operator's
|
|
7109
|
+
// over-block concern argues against any new friction there (§4 Q2).
|
|
7110
|
+
const advisories = kind === 'automated' ? composeAdvisories(capped) : [];
|
|
7111
|
+
outboundAdvisoryAudit.recordPreflight({
|
|
7112
|
+
topicId: Number.isFinite(topic) ? topic : 0,
|
|
7113
|
+
jobSlug: typeof jobSlug === 'string' ? jobSlug : '',
|
|
7114
|
+
kind,
|
|
7115
|
+
text: capped,
|
|
7116
|
+
advisories: advisories.map((a) => a.code),
|
|
7117
|
+
});
|
|
7118
|
+
res.json({ advisories });
|
|
7119
|
+
});
|
|
7120
|
+
// ── GET /messaging/advisory-log — bounded tail of the advisory audit ──
|
|
7121
|
+
router.get('/messaging/advisory-log', (req, res) => {
|
|
7122
|
+
const rawLimit = parseInt(String(req.query.limit ?? '50'), 10);
|
|
7123
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(rawLimit, 1), 500) : 50;
|
|
7124
|
+
res.json({ entries: outboundAdvisoryAudit.readTail(limit) });
|
|
7125
|
+
});
|
|
7007
7126
|
router.post('/telegram/reply/:topicId', async (req, res) => {
|
|
7008
7127
|
if (!ctx.telegram) {
|
|
7009
7128
|
res.status(503).json({ error: 'Telegram not configured' });
|
|
@@ -7056,6 +7175,59 @@ export function createRoutes(ctx) {
|
|
|
7056
7175
|
const allowDebugText = metadata?.allowDebugText === true;
|
|
7057
7176
|
const allowDuplicate = metadata?.allowDuplicate === true;
|
|
7058
7177
|
const allowLocalhostLink = metadata?.allowLocalhostLink === true;
|
|
7178
|
+
// ── Message kind (spec outbound-jargon-filepath-gap §2.1) ──
|
|
7179
|
+
// Stamped structurally by the scheduler env → forwarded by
|
|
7180
|
+
// telegram-reply.sh as metadata.messageKind. Absent → 'reply' (default
|
|
7181
|
+
// behavior, unchanged). Unrecognized values coerce to 'unknown' before
|
|
7182
|
+
// threading anywhere (runtime enum validation).
|
|
7183
|
+
const messageKind = coerceMessageKind(metadata?.messageKind);
|
|
7184
|
+
const senderClass = coerceSenderClass(metadata?.senderClass);
|
|
7185
|
+
const advisoryAck = metadata?.advisoryAck === true;
|
|
7186
|
+
const advisoryCodes = coerceAdvisoryCodes(metadata?.advisoryCodes);
|
|
7187
|
+
const metadataJobSlug = typeof metadata?.jobSlug === 'string' ? metadata.jobSlug.slice(0, 128) : '';
|
|
7188
|
+
// ── Observability breadcrumbs (§2.1 — visibility on the named dodge
|
|
7189
|
+
// classes; sovereignty over the send is accepted, nothing is gated). ──
|
|
7190
|
+
try {
|
|
7191
|
+
const sessionName = resolveTopicSession(String(topicId));
|
|
7192
|
+
const topicSession = sessionName
|
|
7193
|
+
? ctx.state.listSessions().find((s) => s.tmuxSession === sessionName)
|
|
7194
|
+
: undefined;
|
|
7195
|
+
const topicJobSlug = topicSession?.jobSlug;
|
|
7196
|
+
if (topicJobSlug && messageKind === undefined) {
|
|
7197
|
+
// A kindless send whose topic maps to a job session — possible
|
|
7198
|
+
// hand-curl / env-unset bypass. EXEMPT: a sentinel redrive carrying
|
|
7199
|
+
// X-Instar-DeliveryId that matches an ACTUAL queue row (legacy queued
|
|
7200
|
+
// rows may lack a kind; a redrive is not a bypass — §2.5). A
|
|
7201
|
+
// fabricated header must not buy the exemption.
|
|
7202
|
+
const isValidRedrive = typeof deliveryId === 'string' &&
|
|
7203
|
+
/^[0-9a-f-]{16,64}$/i.test(deliveryId) &&
|
|
7204
|
+
ctx.pendingRelayLookup?.(deliveryId) === true;
|
|
7205
|
+
if (!isValidRedrive) {
|
|
7206
|
+
console.log(`[telegram/reply] breadcrumb: kindless send to topic ${topicId} mapping to job session (slug ${topicJobSlug}) — possible preflight bypass (observability only)`);
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
if (messageKind === 'automated' && topicJobSlug && !advisoryAck) {
|
|
7210
|
+
// Automated send with no recent PREFLIGHT-written clean/advised row —
|
|
7211
|
+
// possible class-spoof or modified script. Counts only preflight rows
|
|
7212
|
+
// (a send's own acked row must not self-license it) and only on the
|
|
7213
|
+
// ORIGINATING machine (the index is process-local by construction).
|
|
7214
|
+
if (!outboundAdvisoryAudit.hasRecentPreflight(metadataJobSlug || topicJobSlug, topicId)) {
|
|
7215
|
+
console.log(`[telegram/reply] breadcrumb: automated send to topic ${topicId} (slug ${metadataJobSlug || topicJobSlug}) with no recent preflight row — possible class-spoof (observability only)`);
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
if (senderClass === 'script' && metadataJobSlug && ctx.scheduler) {
|
|
7219
|
+
// A declared script class is validated against the job definition —
|
|
7220
|
+
// the scheduler KNOWS whether this slug is script-mode. A spoofed
|
|
7221
|
+
// 'script' on an LLM-session job is breadcrumbed, never trusted.
|
|
7222
|
+
const jobDef = ctx.scheduler.getJobs().find((j) => j.slug === metadataJobSlug);
|
|
7223
|
+
if (jobDef && jobDef.execute?.type !== 'script') {
|
|
7224
|
+
console.log(`[telegram/reply] breadcrumb: senderClass 'script' declared for non-script job "${metadataJobSlug}" — possible class spoof (observability only)`);
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
catch {
|
|
7229
|
+
/* @silent-fallback-ok — §2.1 breadcrumbs are observe-only; never affect delivery */
|
|
7230
|
+
}
|
|
7059
7231
|
// ── Content-dedup (2026-06-06): suppress the agent re-sending the SAME
|
|
7060
7232
|
// text to the same topic within the window (an agent re-announcing its last
|
|
7061
7233
|
// status after a restart/recovery, or a relay re-emitting identical content
|
|
@@ -7086,6 +7258,7 @@ export function createRoutes(ctx) {
|
|
|
7086
7258
|
allowDebugText,
|
|
7087
7259
|
allowDuplicate,
|
|
7088
7260
|
allowLocalhostLink,
|
|
7261
|
+
messageKind,
|
|
7089
7262
|
})))
|
|
7090
7263
|
return;
|
|
7091
7264
|
try {
|
|
@@ -7094,7 +7267,20 @@ export function createRoutes(ctx) {
|
|
|
7094
7267
|
// whether the reply actually landed — without it the relay could only
|
|
7095
7268
|
// ever return a placeholder 0 and so reported "ok" even when nothing was
|
|
7096
7269
|
// delivered (the false-success-under-load class).
|
|
7097
|
-
const sendResult = await ctx.telegram.sendToTopic(topicId, text, {
|
|
7270
|
+
const sendResult = await ctx.telegram.sendToTopic(topicId, text, {
|
|
7271
|
+
skipStallClear: isProxy,
|
|
7272
|
+
// Relay-hop forwarding (§2.5): when this standby relays through the
|
|
7273
|
+
// lease holder, the kind metadata must survive the hop so the
|
|
7274
|
+
// HOLDER's gate/audit see accurate context. Direct sends ignore it.
|
|
7275
|
+
kindMetadata: messageKind || senderClass || advisoryAck
|
|
7276
|
+
? {
|
|
7277
|
+
...(messageKind ? { messageKind } : {}),
|
|
7278
|
+
...(senderClass ? { senderClass } : {}),
|
|
7279
|
+
...(metadataJobSlug ? { jobSlug: metadataJobSlug } : {}),
|
|
7280
|
+
...(advisoryAck ? { advisoryAck: true, advisoryCodes } : {}),
|
|
7281
|
+
}
|
|
7282
|
+
: undefined,
|
|
7283
|
+
});
|
|
7098
7284
|
// Record the content fingerprint AFTER a successful send so an identical
|
|
7099
7285
|
// re-send within the window is suppressed — but a FAILED send (which
|
|
7100
7286
|
// throws before here) is never recorded, so its legitimate retry isn't lost.
|
|
@@ -7152,9 +7338,31 @@ export function createRoutes(ctx) {
|
|
|
7152
7338
|
if (deliveryId && typeof deliveryId === 'string' && /^[0-9a-f-]{16,64}$/i.test(deliveryId)) {
|
|
7153
7339
|
deliveryLruRecord(deliveryId);
|
|
7154
7340
|
}
|
|
7341
|
+
// ── Acked advisory audit (§2.4(5) — the server is the single writer
|
|
7342
|
+
// of 'acked'). Written on SUCCESSFUL delivery so an acked-then-queued
|
|
7343
|
+
// send still lands its row after the sentinel redrive (the queue
|
|
7344
|
+
// carries the metadata whole) instead of false-firing the escalation.
|
|
7345
|
+
if (advisoryAck) {
|
|
7346
|
+
try {
|
|
7347
|
+
outboundAdvisoryAudit.recordAck({
|
|
7348
|
+
topicId,
|
|
7349
|
+
jobSlug: metadataJobSlug,
|
|
7350
|
+
kind: messageKind ?? 'reply',
|
|
7351
|
+
text,
|
|
7352
|
+
advisories: advisoryCodes,
|
|
7353
|
+
});
|
|
7354
|
+
}
|
|
7355
|
+
catch {
|
|
7356
|
+
/* @silent-fallback-ok — §2.4(5) acked-audit is observe-only; never affects delivery */
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
7155
7359
|
res.json({ ok: true, topicId, messageId: sendResult?.messageId });
|
|
7156
7360
|
}
|
|
7157
7361
|
catch (err) {
|
|
7362
|
+
// @silent-fallback-ok — NOT a silent fallback: this catch surfaces a 500 to the
|
|
7363
|
+
// caller. The tag exists because the no-silent-fallbacks scanner's fixed 20-line
|
|
7364
|
+
// window reaches past this route's end into the next route's `db = null`
|
|
7365
|
+
// initializer (pattern-3 false positive after nearby line shifts).
|
|
7158
7366
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
7159
7367
|
}
|
|
7160
7368
|
});
|
|
@@ -7534,6 +7742,10 @@ export function createRoutes(ctx) {
|
|
|
7534
7742
|
if (await checkOutboundMessage(text, 'slack', res, {
|
|
7535
7743
|
allowDebugText: metadata?.allowDebugText === true,
|
|
7536
7744
|
allowDuplicate: metadata?.allowDuplicate === true,
|
|
7745
|
+
// Kind threading mirrors /telegram/reply — the jargon/filePath signal
|
|
7746
|
+
// computation is single-sourced inside evaluateOutbound, so every
|
|
7747
|
+
// channel gets it uniformly (spec outbound-jargon-filepath-gap §2.2).
|
|
7748
|
+
messageKind: coerceMessageKind(metadata?.messageKind),
|
|
7537
7749
|
}))
|
|
7538
7750
|
return;
|
|
7539
7751
|
try {
|
|
@@ -7699,7 +7911,8 @@ export function createRoutes(ctx) {
|
|
|
7699
7911
|
if (!lane) {
|
|
7700
7912
|
const blocked = await checkOutboundMessage(candidate, 'telegram', res, {
|
|
7701
7913
|
messageKind: isHealthAlert ? 'health-alert' : 'reply',
|
|
7702
|
-
jargon:
|
|
7914
|
+
// jargon arg dropped — the kind decides: evaluateOutbound now computes
|
|
7915
|
+
// the jargon signal itself for health-alert/automated kinds (§2.2).
|
|
7703
7916
|
// No topicId — attention items create new topics; no prior thread context applies.
|
|
7704
7917
|
});
|
|
7705
7918
|
if (blocked) {
|