instar 0.28.81 → 0.28.83

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.
@@ -78,8 +78,10 @@ import { ProcessIntegrity } from '../core/ProcessIntegrity.js';
78
78
  import { getOutboundQueueStatus, cleanupDeliveredOutbound, buildAgentList } from '../messaging/GitSyncTransport.js';
79
79
  import { verifyAgentToken, getAgentToken } from '../messaging/AgentTokenManager.js';
80
80
  import { createThreadlineRoutes } from '../threadline/ThreadlineEndpoints.js';
81
+ import { ThreadlineNicknames } from '../threadline/ThreadlineNicknames.js';
81
82
  import { ScopeCoherenceTracker } from '../core/ScopeCoherenceTracker.js';
82
83
  import { isJunkPayload } from '../core/junk-payload.js';
84
+ import { detectJargon } from '../core/JargonDetector.js';
83
85
  import { TruncationDetector } from '../paste/TruncationDetector.js';
84
86
  import { SecretDrop } from './SecretDrop.js';
85
87
  import { matchesSystemTemplate } from '../messaging/system-templates.js';
@@ -484,6 +486,15 @@ export function createRoutes(ctx) {
484
486
  reason: junkResult.reason,
485
487
  };
486
488
  }
489
+ if (options.jargon) {
490
+ try {
491
+ const j = detectJargon(text);
492
+ signals.jargon = { detected: j.detected, terms: j.terms, score: j.score };
493
+ }
494
+ catch {
495
+ // Detector errors never override the authority — skip the signal.
496
+ }
497
+ }
487
498
  // Recent conversation — used by both the authority and the dedup detector.
488
499
  let recentMessages;
489
500
  let recentOutbound = [];
@@ -521,6 +532,7 @@ export function createRoutes(ctx) {
521
532
  recentMessages,
522
533
  signals,
523
534
  targetStyle: ctx.config.messagingStyle,
535
+ messageKind: options.messageKind,
524
536
  });
525
537
  // Structured observability: log every decision the authority made. This is
526
538
  // the "why I blocked" log — over-block audits read this. Invalid-rule
@@ -4579,6 +4591,23 @@ export function createRoutes(ctx) {
4579
4591
  res.status(400).json({ error: '"priority" must be one of: URGENT, HIGH, NORMAL, LOW' });
4580
4592
  return;
4581
4593
  }
4594
+ // Attention items reach the user as a new Telegram topic with the title,
4595
+ // summary, and (optional) description as the body. Run that user-facing
4596
+ // candidate through the outbound-message authority before creating the
4597
+ // topic. For health-class categories ("degradation", "health"), invoke
4598
+ // the health-alert ruleset (B12/B13/B14) so jargon-laden, no-CTA, and
4599
+ // self-healed-event messages get suppressed instead of spawning topics.
4600
+ const isHealthAlert = typeof category === 'string' && /^(degradation|health|health-alert|alert)$/i.test(category);
4601
+ const candidate = [title, summary, description].filter((s) => typeof s === 'string' && s.length > 0).join('\n\n');
4602
+ const blocked = await checkOutboundMessage(candidate, 'telegram', res, {
4603
+ messageKind: isHealthAlert ? 'health-alert' : 'reply',
4604
+ jargon: isHealthAlert,
4605
+ // No topicId — attention items create new topics; no prior thread context applies.
4606
+ });
4607
+ if (blocked) {
4608
+ // checkOutboundMessage already wrote the 422 response.
4609
+ return;
4610
+ }
4582
4611
  try {
4583
4612
  const item = await ctx.telegram.createAttentionItem({
4584
4613
  id,
@@ -9722,6 +9751,96 @@ export function createRoutes(ctx) {
9722
9751
  res.status(413).json({ success: false, error: 'Message too large (64KB limit)' });
9723
9752
  return;
9724
9753
  }
9754
+ // Nickname-first resolution. User-curated names in nicknames.json are the
9755
+ // highest-authority mapping (signal-vs-authority: relay discovery is signal,
9756
+ // user nickname is authority). If "Dawn" maps to fingerprint X here, we
9757
+ // route to X regardless of what the relay's discovery cache says about
9758
+ // some other agent that also calls itself "Dawn". Skip when caller used
9759
+ // a "name:fpPrefix" qualifier (they're explicitly disambiguating) or
9760
+ // when the input already looks like a fingerprint.
9761
+ let nicknameResolvedFp = null;
9762
+ const looksLikeFingerprint = /^[0-9a-f]{16,64}$/i.test(targetAgent);
9763
+ // Parse "name:fpPrefix" once so both branches below can reuse it.
9764
+ const fpPrefixParse = (() => {
9765
+ const ci = targetAgent.lastIndexOf(':');
9766
+ if (ci <= 0 || ci >= targetAgent.length - 1)
9767
+ return null;
9768
+ const suffix = targetAgent.substring(ci + 1);
9769
+ if (!/^[0-9a-f]{4,32}$/i.test(suffix))
9770
+ return null;
9771
+ return { name: targetAgent.substring(0, ci), fpPrefix: suffix.toLowerCase() };
9772
+ })();
9773
+ const usesFpPrefixSyntax = fpPrefixParse !== null;
9774
+ if (!looksLikeFingerprint) {
9775
+ try {
9776
+ const nicknameStore = new ThreadlineNicknames({ stateDir: ctx.config.stateDir });
9777
+ // For "name:fpPrefix" inputs, look up the bare name in the nickname
9778
+ // store and filter the candidates by the prefix. This lets the user
9779
+ // disambiguate ambiguous nickname entries with the same syntax that
9780
+ // works for known-agents.json — the spec promises this remedy.
9781
+ // Without this, "Dawn:abcd" would skip nickname lookup entirely and
9782
+ // the documented disambiguation path would be a dead end.
9783
+ const lookupName = fpPrefixParse ? fpPrefixParse.name : targetAgent;
9784
+ const lookup = nicknameStore.resolveByName(lookupName);
9785
+ if (lookup && 'ambiguous' in lookup) {
9786
+ if (fpPrefixParse) {
9787
+ const matched = lookup.candidates.filter(c => c.fingerprint.toLowerCase().startsWith(fpPrefixParse.fpPrefix));
9788
+ if (matched.length === 1) {
9789
+ nicknameResolvedFp = matched[0].fingerprint;
9790
+ }
9791
+ else if (matched.length === 0) {
9792
+ const hints = lookup.candidates
9793
+ .map(c => `${c.entry.nickname}:${c.fingerprint.substring(0, 8)}`)
9794
+ .join(', ');
9795
+ res.status(409).json({
9796
+ success: false,
9797
+ error: `Nickname "${fpPrefixParse.name}" has no candidate matching prefix "${fpPrefixParse.fpPrefix}". Candidates: ${hints}.`,
9798
+ });
9799
+ return;
9800
+ }
9801
+ else {
9802
+ const hints = matched
9803
+ .map(c => `${c.entry.nickname}:${c.fingerprint.substring(0, 8)}`)
9804
+ .join(', ');
9805
+ res.status(409).json({
9806
+ success: false,
9807
+ error: `Prefix "${fpPrefixParse.fpPrefix}" still ambiguous among nickname "${fpPrefixParse.name}" candidates: ${hints}. Use a longer prefix.`,
9808
+ });
9809
+ return;
9810
+ }
9811
+ }
9812
+ else {
9813
+ const hints = lookup.candidates
9814
+ .map(c => `${c.entry.nickname}:${c.fingerprint.substring(0, 8)} (${c.fingerprint})`)
9815
+ .join(', ');
9816
+ res.status(409).json({
9817
+ success: false,
9818
+ error: `Ambiguous nickname "${targetAgent}" — multiple fingerprints share this name in nicknames.json: ${hints}. Send by fingerprint or use "name:fpPrefix" syntax.`,
9819
+ });
9820
+ return;
9821
+ }
9822
+ }
9823
+ else if (lookup) {
9824
+ // Single-candidate match. If caller also passed a fpPrefix, sanity-
9825
+ // check that it agrees with the curated mapping; on disagreement,
9826
+ // honor the caller's explicit prefix (they may be telling us the
9827
+ // nickname is stale) and skip nickname authority for this send.
9828
+ if (fpPrefixParse && !lookup.fingerprint.toLowerCase().startsWith(fpPrefixParse.fpPrefix)) {
9829
+ console.warn(`[relay-send] Nickname/prefix disagreement for "${fpPrefixParse.name}": ` +
9830
+ `nickname maps to ${lookup.fingerprint.substring(0, 8)}…, ` +
9831
+ `caller prefix is "${fpPrefixParse.fpPrefix}". Honoring caller prefix.`);
9832
+ }
9833
+ else {
9834
+ nicknameResolvedFp = lookup.fingerprint;
9835
+ }
9836
+ }
9837
+ }
9838
+ catch (err) {
9839
+ // Nickname store read failed — fall through to existing resolution.
9840
+ // Don't block sends on a corrupt nicknames file.
9841
+ console.warn(`[relay-send] Nickname store read failed (non-fatal): ${err instanceof Error ? err.message : err}`);
9842
+ }
9843
+ }
9725
9844
  const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
9726
9845
  // Mint a stable UUID threadId when caller didn't provide one so
9727
9846
  // first-contact messages aren't dropped on the recipient side. Both
@@ -9747,7 +9866,14 @@ export function createRoutes(ctx) {
9747
9866
  targetFpPrefix = suffix.toLowerCase();
9748
9867
  }
9749
9868
  }
9750
- const nameMatches = agents.filter(a => a.name === targetName || a.name?.toLowerCase() === targetName?.toLowerCase());
9869
+ // If a nickname resolved upstream, prefer fingerprint match over
9870
+ // name match — the user-curated mapping is authoritative.
9871
+ const nameMatches = nicknameResolvedFp
9872
+ ? agents.filter(a => {
9873
+ const fp = (a.fingerprint || a.publicKey?.substring(0, 32) || '').toLowerCase();
9874
+ return fp === nicknameResolvedFp.toLowerCase();
9875
+ })
9876
+ : agents.filter(a => a.name === targetName || a.name?.toLowerCase() === targetName?.toLowerCase());
9751
9877
  // PR-3: Fingerprint-based disambiguation. If multiple known agents
9752
9878
  // share a name, require a `name:fpPrefix` qualifier to pick one.
9753
9879
  // Previously this silently fell through to the relay, which then
@@ -9944,8 +10070,31 @@ export function createRoutes(ctx) {
9944
10070
  return;
9945
10071
  }
9946
10072
  try {
9947
- // Resolve name → fingerprint if targetAgent isn't already a fingerprint
9948
- const resolvedId = await relayClient.resolveAgent(targetAgent);
10073
+ // Resolve name → fingerprint. Nickname store wins over relay discovery
10074
+ // when the user has explicitly curated a mapping (set above). Falling
10075
+ // back to relay discovery only when no nickname matched. If the relay
10076
+ // happens to also know this name but maps it to a different fingerprint,
10077
+ // we silently override with the nickname mapping — the user's choice is
10078
+ // authority. Logged so the conflict is visible in operator logs.
10079
+ let resolvedId;
10080
+ if (nicknameResolvedFp) {
10081
+ resolvedId = nicknameResolvedFp;
10082
+ try {
10083
+ const discoveryFp = await relayClient.resolveAgent(targetAgent);
10084
+ if (discoveryFp && discoveryFp.toLowerCase() !== nicknameResolvedFp.toLowerCase()) {
10085
+ console.warn(`[relay-send] Nickname/discovery mismatch for "${targetAgent}": ` +
10086
+ `nickname maps to ${nicknameResolvedFp.substring(0, 8)}…, ` +
10087
+ `relay discovery returned ${discoveryFp.substring(0, 8)}…. ` +
10088
+ `Honoring user-curated nickname.`);
10089
+ }
10090
+ }
10091
+ catch {
10092
+ // Discovery probe is best-effort for the conflict warning only.
10093
+ }
10094
+ }
10095
+ else {
10096
+ resolvedId = await relayClient.resolveAgent(targetAgent);
10097
+ }
9949
10098
  if (!resolvedId) {
9950
10099
  res.status(404).json({
9951
10100
  success: false,