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.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +152 -3
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ThreadlineNicknames.d.ts +88 -0
- package/dist/threadline/ThreadlineNicknames.d.ts.map +1 -0
- package/dist/threadline/ThreadlineNicknames.js +195 -0
- package/dist/threadline/ThreadlineNicknames.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/0.28.82.md +35 -0
- package/upgrades/0.28.83.md +45 -0
- package/upgrades/side-effects/attention-tone-gate-and-stable-id.md +162 -0
- package/upgrades/side-effects/threadline-nickname-resolver-authority.md +81 -0
package/dist/server/routes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
9948
|
-
|
|
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,
|