instar 1.3.71 → 1.3.73
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/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +20 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/UsherActedCorrelator.d.ts +96 -0
- package/dist/core/UsherActedCorrelator.d.ts.map +1 -0
- package/dist/core/UsherActedCorrelator.js +175 -0
- package/dist/core/UsherActedCorrelator.js.map +1 -0
- package/dist/core/UsherSignalStore.d.ts +26 -2
- package/dist/core/UsherSignalStore.d.ts.map +1 -1
- package/dist/core/UsherSignalStore.js +17 -2
- package/dist/core/UsherSignalStore.js.map +1 -1
- package/dist/messaging/AttentionTopicGuard.d.ts +88 -0
- package/dist/messaging/AttentionTopicGuard.d.ts.map +1 -0
- package/dist/messaging/AttentionTopicGuard.js +154 -0
- package/dist/messaging/AttentionTopicGuard.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +39 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +142 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +4 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +12 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/server/usherRoutes.d.ts.map +1 -1
- package/dist/server/usherRoutes.js +7 -1
- package/dist/server/usherRoutes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/upgrades/1.3.72.md +65 -0
- package/upgrades/1.3.73.md +73 -0
- package/upgrades/side-effects/attention-topic-flood-guard.md +74 -0
- package/upgrades/side-effects/usher-precision-loop.md +131 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsherActedCorrelator — the precision-numerator wiring for the Usher (rung 4 of
|
|
3
|
+
* continuous-working-awareness).
|
|
4
|
+
*
|
|
5
|
+
* The Usher FIRES re-surface signals (UsherSignalStore.recordSignal). Until this
|
|
6
|
+
* module, nothing ever called markActed — so `precision = acted/fired` was pinned
|
|
7
|
+
* at 0 and the rung-5 (mid-task injection) gate could never be satisfied by data.
|
|
8
|
+
* This closes that loop via TWO correlation paths, both reducing to "does some
|
|
9
|
+
* probe text COVER a fired signal's contextText?":
|
|
10
|
+
*
|
|
11
|
+
* (a) auto-use — the agent's next reply on the topic uses the re-surfaced
|
|
12
|
+
* context. creditUsherOnOutbound(). The nudge was useful: the
|
|
13
|
+
* agent acted on it.
|
|
14
|
+
* (b) miss-map — the user later had to CORRECT the agent (a HumanAsDetector
|
|
15
|
+
* signal) on that same context. creditUsherOnMiss(). The nudge
|
|
16
|
+
* was a genuine catch the agent ignored — still a true positive
|
|
17
|
+
* for the Usher's precision.
|
|
18
|
+
*
|
|
19
|
+
* Matching is deliberately PRECISION-OVER-RECALL: precision gates whether the
|
|
20
|
+
* Usher earns the right to interrupt (rung 5), so a falsely-HIGH precision is the
|
|
21
|
+
* dangerous direction. We require a meaningful salient-term overlap (≥ MIN_SHARED
|
|
22
|
+
* shared content words AND ≥ MIN_COVERAGE of the context's terms) and only credit
|
|
23
|
+
* recently-fired signals. Under-crediting a fast reply is acceptable; inflating
|
|
24
|
+
* the gate is not.
|
|
25
|
+
*
|
|
26
|
+
* All exported integration fns are best-effort and NEVER throw into the
|
|
27
|
+
* message/delivery path — mirroring the Usher's own degrade-safety invariants.
|
|
28
|
+
*/
|
|
29
|
+
import type { UsherSignal, UsherSignalStore } from './UsherSignalStore.js';
|
|
30
|
+
/** Minimal inbound-entry shape the miss path reads (matches InboundMessageEntry). */
|
|
31
|
+
export interface MissEntry {
|
|
32
|
+
topicId?: number | null;
|
|
33
|
+
text?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare const MIN_SHARED = 2;
|
|
36
|
+
export declare const MIN_COVERAGE = 0.5;
|
|
37
|
+
/** Window in which the agent's reply still counts as "using" a just-fired nudge. */
|
|
38
|
+
export declare const USE_WINDOW_MS: number;
|
|
39
|
+
/** Window in which a later user correction still credits a prior nudge. */
|
|
40
|
+
export declare const MISS_WINDOW_MS: number;
|
|
41
|
+
/**
|
|
42
|
+
* Salient content tokens of a text: lowercased, alphanumeric-split, ≥3 chars,
|
|
43
|
+
* stopwords removed. Pure + deterministic.
|
|
44
|
+
*/
|
|
45
|
+
export declare function salientTerms(text: string): Set<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Does `probeText` cover enough of `contextText`'s salient terms to count as a
|
|
48
|
+
* genuine reference to it? Requires ≥ minShared shared terms AND ≥ minCoverage
|
|
49
|
+
* fraction of the context's terms (so single-coincidental-word matches fail, and
|
|
50
|
+
* a long context needs proportionally more overlap).
|
|
51
|
+
*/
|
|
52
|
+
export declare function contextCoveredBy(contextText: string, probeText: string, opts?: {
|
|
53
|
+
minShared?: number;
|
|
54
|
+
minCoverage?: number;
|
|
55
|
+
}): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Pure: among NOT-yet-acted signals, which ones does `probeText` cover? Returns
|
|
58
|
+
* their ids. `maxAgeMs`+`nowMs` optionally drop signals older than the window.
|
|
59
|
+
*/
|
|
60
|
+
export declare function findCoveredSignalIds(signals: UsherSignal[], probeText: string, opts?: {
|
|
61
|
+
minShared?: number;
|
|
62
|
+
minCoverage?: number;
|
|
63
|
+
maxAgeMs?: number;
|
|
64
|
+
nowMs?: number;
|
|
65
|
+
}): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Core integration: correlate `probeText` against a topic's fired signals and
|
|
68
|
+
* mark the matches acted (stamped with `via`). Best-effort, never throws.
|
|
69
|
+
* Returns the ids actually marked.
|
|
70
|
+
*/
|
|
71
|
+
export declare function markActedByCoverage(store: UsherSignalStore, topicId: number, probeText: string, via: 'use' | 'miss', opts?: {
|
|
72
|
+
now?: () => number;
|
|
73
|
+
maxAgeMs?: number;
|
|
74
|
+
minShared?: number;
|
|
75
|
+
minCoverage?: number;
|
|
76
|
+
onMarked?: (id: string) => void;
|
|
77
|
+
}): string[];
|
|
78
|
+
/**
|
|
79
|
+
* Path (a): the agent's reply just went out on `topicId`. Credit any recent
|
|
80
|
+
* faded-context nudge the reply actually used. Wire on the outbound-reply path.
|
|
81
|
+
*/
|
|
82
|
+
export declare function creditUsherOnOutbound(store: UsherSignalStore | null | undefined, topicId: number, replyText: string, opts?: {
|
|
83
|
+
now?: () => number;
|
|
84
|
+
maxAgeMs?: number;
|
|
85
|
+
}): string[];
|
|
86
|
+
/**
|
|
87
|
+
* Path (b): the user just had to correct the agent (`missSignal` non-null). If a
|
|
88
|
+
* recent nudge on this topic covers what they're correcting, that nudge was a
|
|
89
|
+
* real catch the agent ignored → credit it. Wire on the inbound human-detector
|
|
90
|
+
* seam. No-op when there was no miss, no topic, or no text.
|
|
91
|
+
*/
|
|
92
|
+
export declare function creditUsherOnMiss(store: UsherSignalStore | null | undefined, missSignal: unknown, entry: MissEntry, opts?: {
|
|
93
|
+
now?: () => number;
|
|
94
|
+
maxAgeMs?: number;
|
|
95
|
+
}): string[];
|
|
96
|
+
//# sourceMappingURL=UsherActedCorrelator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UsherActedCorrelator.d.ts","sourceRoot":"","sources":["../../src/core/UsherActedCorrelator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE3E,qFAAqF;AACrF,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,eAAO,MAAM,YAAY,MAAM,CAAC;AAChC,oFAAoF;AACpF,eAAO,MAAM,aAAa,QAAqB,CAAC;AAChD,2EAA2E;AAC3E,eAAO,MAAM,cAAc,QAAsB,CAAC;AAgBlD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAStD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAClD,OAAO,CAST;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACrF,MAAM,EAAE,CAaV;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,KAAK,GAAG,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAC1H,MAAM,EAAE,CAyBV;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC1C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,MAAM,EAAE,CAMV;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAC1C,UAAU,EAAE,OAAO,EACnB,KAAK,EAAE,SAAS,EAChB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,MAAM,EAAE,CASV"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsherActedCorrelator — the precision-numerator wiring for the Usher (rung 4 of
|
|
3
|
+
* continuous-working-awareness).
|
|
4
|
+
*
|
|
5
|
+
* The Usher FIRES re-surface signals (UsherSignalStore.recordSignal). Until this
|
|
6
|
+
* module, nothing ever called markActed — so `precision = acted/fired` was pinned
|
|
7
|
+
* at 0 and the rung-5 (mid-task injection) gate could never be satisfied by data.
|
|
8
|
+
* This closes that loop via TWO correlation paths, both reducing to "does some
|
|
9
|
+
* probe text COVER a fired signal's contextText?":
|
|
10
|
+
*
|
|
11
|
+
* (a) auto-use — the agent's next reply on the topic uses the re-surfaced
|
|
12
|
+
* context. creditUsherOnOutbound(). The nudge was useful: the
|
|
13
|
+
* agent acted on it.
|
|
14
|
+
* (b) miss-map — the user later had to CORRECT the agent (a HumanAsDetector
|
|
15
|
+
* signal) on that same context. creditUsherOnMiss(). The nudge
|
|
16
|
+
* was a genuine catch the agent ignored — still a true positive
|
|
17
|
+
* for the Usher's precision.
|
|
18
|
+
*
|
|
19
|
+
* Matching is deliberately PRECISION-OVER-RECALL: precision gates whether the
|
|
20
|
+
* Usher earns the right to interrupt (rung 5), so a falsely-HIGH precision is the
|
|
21
|
+
* dangerous direction. We require a meaningful salient-term overlap (≥ MIN_SHARED
|
|
22
|
+
* shared content words AND ≥ MIN_COVERAGE of the context's terms) and only credit
|
|
23
|
+
* recently-fired signals. Under-crediting a fast reply is acceptable; inflating
|
|
24
|
+
* the gate is not.
|
|
25
|
+
*
|
|
26
|
+
* All exported integration fns are best-effort and NEVER throw into the
|
|
27
|
+
* message/delivery path — mirroring the Usher's own degrade-safety invariants.
|
|
28
|
+
*/
|
|
29
|
+
// ── Matching knobs (precision-over-recall) ───────────────────────────────────
|
|
30
|
+
export const MIN_SHARED = 2; // a single coincidental word can't credit a signal
|
|
31
|
+
export const MIN_COVERAGE = 0.5; // must cover at least half the context's salient terms
|
|
32
|
+
/** Window in which the agent's reply still counts as "using" a just-fired nudge. */
|
|
33
|
+
export const USE_WINDOW_MS = 6 * 60 * 60 * 1000; // 6h
|
|
34
|
+
/** Window in which a later user correction still credits a prior nudge. */
|
|
35
|
+
export const MISS_WINDOW_MS = 24 * 60 * 60 * 1000; // 24h
|
|
36
|
+
// English stopwords + a few instar-conversation fillers. Kept small and stable:
|
|
37
|
+
// the goal is to drop function words so overlap reflects real subject matter.
|
|
38
|
+
const STOPWORDS = new Set([
|
|
39
|
+
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'your', 'with', 'that', 'this',
|
|
40
|
+
'have', 'has', 'had', 'was', 'were', 'will', 'would', 'should', 'could', 'can',
|
|
41
|
+
'its', 'it', 'is', 'be', 'been', 'being', 'from', 'into', 'over', 'under', 'about',
|
|
42
|
+
'they', 'them', 'their', 'there', 'here', 'what', 'when', 'where', 'which', 'who',
|
|
43
|
+
'how', 'why', 'all', 'any', 'some', 'one', 'two', 'now', 'then', 'than', 'just',
|
|
44
|
+
'out', 'off', 'too', 'very', 'our', 'his', 'her', 'him', 'she', 'his', 'we',
|
|
45
|
+
'get', 'got', 'let', 'see', 'use', 'used', 'make', 'made', 'want', 'need',
|
|
46
|
+
'yes', 'yeah', 'okay', 'sure', 'thing', 'things', 'stuff', 'like', 'also',
|
|
47
|
+
'going', 'gonna', 'really', 'much', 'more', 'most', 'still', 'back', 'done',
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Salient content tokens of a text: lowercased, alphanumeric-split, ≥3 chars,
|
|
51
|
+
* stopwords removed. Pure + deterministic.
|
|
52
|
+
*/
|
|
53
|
+
export function salientTerms(text) {
|
|
54
|
+
const out = new Set();
|
|
55
|
+
if (!text || typeof text !== 'string')
|
|
56
|
+
return out;
|
|
57
|
+
for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
58
|
+
if (raw.length < 3)
|
|
59
|
+
continue;
|
|
60
|
+
if (STOPWORDS.has(raw))
|
|
61
|
+
continue;
|
|
62
|
+
out.add(raw);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Does `probeText` cover enough of `contextText`'s salient terms to count as a
|
|
68
|
+
* genuine reference to it? Requires ≥ minShared shared terms AND ≥ minCoverage
|
|
69
|
+
* fraction of the context's terms (so single-coincidental-word matches fail, and
|
|
70
|
+
* a long context needs proportionally more overlap).
|
|
71
|
+
*/
|
|
72
|
+
export function contextCoveredBy(contextText, probeText, opts) {
|
|
73
|
+
const ctx = salientTerms(contextText);
|
|
74
|
+
if (ctx.size === 0)
|
|
75
|
+
return false;
|
|
76
|
+
const probe = salientTerms(probeText);
|
|
77
|
+
if (probe.size === 0)
|
|
78
|
+
return false;
|
|
79
|
+
let shared = 0;
|
|
80
|
+
for (const t of ctx)
|
|
81
|
+
if (probe.has(t))
|
|
82
|
+
shared++;
|
|
83
|
+
const coverage = shared / ctx.size;
|
|
84
|
+
return shared >= (opts?.minShared ?? MIN_SHARED) && coverage >= (opts?.minCoverage ?? MIN_COVERAGE);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pure: among NOT-yet-acted signals, which ones does `probeText` cover? Returns
|
|
88
|
+
* their ids. `maxAgeMs`+`nowMs` optionally drop signals older than the window.
|
|
89
|
+
*/
|
|
90
|
+
export function findCoveredSignalIds(signals, probeText, opts) {
|
|
91
|
+
const ids = [];
|
|
92
|
+
const maxAgeMs = opts?.maxAgeMs;
|
|
93
|
+
const nowMs = opts?.nowMs ?? Date.now();
|
|
94
|
+
for (const s of signals) {
|
|
95
|
+
if (!s || s.acted)
|
|
96
|
+
continue;
|
|
97
|
+
if (maxAgeMs !== undefined) {
|
|
98
|
+
const firedMs = Date.parse(s.at);
|
|
99
|
+
if (!Number.isNaN(firedMs) && nowMs - firedMs > maxAgeMs)
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (contextCoveredBy(s.contextText, probeText, opts))
|
|
103
|
+
ids.push(s.id);
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Core integration: correlate `probeText` against a topic's fired signals and
|
|
109
|
+
* mark the matches acted (stamped with `via`). Best-effort, never throws.
|
|
110
|
+
* Returns the ids actually marked.
|
|
111
|
+
*/
|
|
112
|
+
export function markActedByCoverage(store, topicId, probeText, via, opts) {
|
|
113
|
+
try {
|
|
114
|
+
if (!probeText || typeof probeText !== 'string')
|
|
115
|
+
return [];
|
|
116
|
+
if (typeof topicId !== 'number' || !Number.isFinite(topicId))
|
|
117
|
+
return [];
|
|
118
|
+
const nowMs = (opts?.now ?? (() => Date.now()))();
|
|
119
|
+
const signals = store.getSignals(topicId, 50);
|
|
120
|
+
const ids = findCoveredSignalIds(signals, probeText, {
|
|
121
|
+
minShared: opts?.minShared,
|
|
122
|
+
minCoverage: opts?.minCoverage,
|
|
123
|
+
maxAgeMs: opts?.maxAgeMs,
|
|
124
|
+
nowMs,
|
|
125
|
+
});
|
|
126
|
+
if (ids.length === 0)
|
|
127
|
+
return [];
|
|
128
|
+
const at = new Date(nowMs).toISOString();
|
|
129
|
+
const marked = [];
|
|
130
|
+
for (const id of ids) {
|
|
131
|
+
if (store.markActed(topicId, id, { via, at })) {
|
|
132
|
+
marked.push(id);
|
|
133
|
+
try {
|
|
134
|
+
opts?.onMarked?.(id);
|
|
135
|
+
}
|
|
136
|
+
catch { /* observability hook must not break */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return marked;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Path (a): the agent's reply just went out on `topicId`. Credit any recent
|
|
147
|
+
* faded-context nudge the reply actually used. Wire on the outbound-reply path.
|
|
148
|
+
*/
|
|
149
|
+
export function creditUsherOnOutbound(store, topicId, replyText, opts) {
|
|
150
|
+
if (!store)
|
|
151
|
+
return [];
|
|
152
|
+
return markActedByCoverage(store, topicId, replyText, 'use', {
|
|
153
|
+
now: opts?.now,
|
|
154
|
+
maxAgeMs: opts?.maxAgeMs ?? USE_WINDOW_MS,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Path (b): the user just had to correct the agent (`missSignal` non-null). If a
|
|
159
|
+
* recent nudge on this topic covers what they're correcting, that nudge was a
|
|
160
|
+
* real catch the agent ignored → credit it. Wire on the inbound human-detector
|
|
161
|
+
* seam. No-op when there was no miss, no topic, or no text.
|
|
162
|
+
*/
|
|
163
|
+
export function creditUsherOnMiss(store, missSignal, entry, opts) {
|
|
164
|
+
if (!store || !missSignal)
|
|
165
|
+
return [];
|
|
166
|
+
const topicId = entry?.topicId;
|
|
167
|
+
const text = entry?.text;
|
|
168
|
+
if (typeof topicId !== 'number' || !text)
|
|
169
|
+
return [];
|
|
170
|
+
return markActedByCoverage(store, topicId, text, 'miss', {
|
|
171
|
+
now: opts?.now,
|
|
172
|
+
maxAgeMs: opts?.maxAgeMs ?? MISS_WINDOW_MS,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=UsherActedCorrelator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UsherActedCorrelator.js","sourceRoot":"","sources":["../../src/core/UsherActedCorrelator.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,11 +23,24 @@ export interface UsherSignal {
|
|
|
23
23
|
at: string;
|
|
24
24
|
/** True once the re-surfaced context was actually used (precision numerator). */
|
|
25
25
|
acted: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Which path confirmed the signal was useful (set when `acted` flips true):
|
|
28
|
+
* 'use' — the agent's next reply actually used the re-surfaced context.
|
|
29
|
+
* 'miss' — the user later had to correct the agent on that same context, so
|
|
30
|
+
* the nudge was a genuine catch the agent ignored.
|
|
31
|
+
* Optional for backward-compat with signals/callers that pre-date the split.
|
|
32
|
+
*/
|
|
33
|
+
actedVia?: 'use' | 'miss';
|
|
34
|
+
/** ISO timestamp when the signal was marked acted. */
|
|
35
|
+
actedAt?: string;
|
|
26
36
|
}
|
|
27
37
|
export interface UsherMetrics {
|
|
28
38
|
fired: number;
|
|
29
39
|
acted: number;
|
|
30
40
|
last_fired_at: string | null;
|
|
41
|
+
/** Precision numerator, split by which path confirmed usefulness (observability). */
|
|
42
|
+
acted_by_use?: number;
|
|
43
|
+
acted_by_miss?: number;
|
|
31
44
|
}
|
|
32
45
|
interface UsherTopicFile {
|
|
33
46
|
topicId: number;
|
|
@@ -49,8 +62,19 @@ export declare class UsherSignalStore {
|
|
|
49
62
|
turn: number;
|
|
50
63
|
at?: string;
|
|
51
64
|
}): string | null;
|
|
52
|
-
/**
|
|
53
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Mark a signal as acted-on (precision numerator). Best-effort, idempotent
|
|
67
|
+
* (a second call on the same signal is a no-op returning false).
|
|
68
|
+
*
|
|
69
|
+
* `opts.via` records WHICH correlation path confirmed usefulness ('use' =
|
|
70
|
+
* the agent used it in a reply; 'miss' = the user had to correct on it) and
|
|
71
|
+
* is reflected in the split metrics. `opts.at` lets callers stamp a
|
|
72
|
+
* deterministic timestamp (tests); defaults to now.
|
|
73
|
+
*/
|
|
74
|
+
markActed(topicId: number, signalId: string, opts?: {
|
|
75
|
+
via?: 'use' | 'miss';
|
|
76
|
+
at?: string;
|
|
77
|
+
}): boolean;
|
|
54
78
|
getSignals(topicId: number, limit?: number): UsherSignal[];
|
|
55
79
|
getMetrics(topicId: number): UsherMetrics;
|
|
56
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsherSignalStore.d.ts","sourceRoot":"","sources":["../../src/core/UsherSignalStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,iFAAiF;IACjF,KAAK,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"UsherSignalStore.d.ts","sourceRoot":"","sources":["../../src/core/UsherSignalStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,iFAAiF;IACjF,KAAK,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,EAAE,CAAC,CAAC;CAClB;AAQD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAAS;gBAER,QAAQ,EAAE,MAAM;IAO5B,OAAO,CAAC,QAAQ;IAIhB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc;IAerC,OAAO,CAAC,IAAI;IAWZ,yFAAyF;IACzF,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,GAAG,IAAI;IA0BvI;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;IAkBnG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,WAAW,EAAE;IAKtD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY;CAG1C"}
|
|
@@ -86,15 +86,30 @@ export class UsherSignalStore {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Mark a signal as acted-on (precision numerator). Best-effort, idempotent
|
|
91
|
+
* (a second call on the same signal is a no-op returning false).
|
|
92
|
+
*
|
|
93
|
+
* `opts.via` records WHICH correlation path confirmed usefulness ('use' =
|
|
94
|
+
* the agent used it in a reply; 'miss' = the user had to correct on it) and
|
|
95
|
+
* is reflected in the split metrics. `opts.at` lets callers stamp a
|
|
96
|
+
* deterministic timestamp (tests); defaults to now.
|
|
97
|
+
*/
|
|
98
|
+
markActed(topicId, signalId, opts) {
|
|
91
99
|
try {
|
|
92
100
|
const file = this.load(topicId);
|
|
93
101
|
const sig = file.signals.find(x => x.id === signalId);
|
|
94
102
|
if (!sig || sig.acted)
|
|
95
103
|
return false;
|
|
96
104
|
sig.acted = true;
|
|
105
|
+
if (opts?.via)
|
|
106
|
+
sig.actedVia = opts.via;
|
|
107
|
+
sig.actedAt = opts?.at ?? new Date().toISOString();
|
|
97
108
|
file.metrics.acted += 1;
|
|
109
|
+
if (opts?.via === 'use')
|
|
110
|
+
file.metrics.acted_by_use = (file.metrics.acted_by_use ?? 0) + 1;
|
|
111
|
+
else if (opts?.via === 'miss')
|
|
112
|
+
file.metrics.acted_by_miss = (file.metrics.acted_by_miss ?? 0) + 1;
|
|
98
113
|
this.save(file);
|
|
99
114
|
return true;
|
|
100
115
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsherSignalStore.js","sourceRoot":"","sources":["../../src/core/UsherSignalStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"UsherSignalStore.js","sourceRoot":"","sources":["../../src/core/UsherSignalStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttentionTopicGuard — a per-source + global circuit breaker for forum-topic
|
|
3
|
+
* creation. NOT a blocker: it is a DELIVERY SHAPER (same class as
|
|
4
|
+
* SentinelNotifier), never a gate. It holds no authority over agent behavior or
|
|
5
|
+
* information flow — it only changes the FORM of delivery (one coalesced topic +
|
|
6
|
+
* a log line) for non-critical, high-volume notices, and never withholds a
|
|
7
|
+
* critical notice or drops an item. See docs/signal-vs-authority.md (the rate-
|
|
8
|
+
* counter / transport-dedup carve-outs).
|
|
9
|
+
*
|
|
10
|
+
* Built in response to the 2026-05-28 topic-spam flood (the SECOND such flood;
|
|
11
|
+
* the first, 2026-05-22, produced the SentinelNotifier fix). Root cause both
|
|
12
|
+
* times: `TelegramAdapter.createAttentionItem` spawns a BRAND-NEW forum topic
|
|
13
|
+
* per attention item, and there was no structural gate stopping a housekeeping
|
|
14
|
+
* feature from raising attention items at volume.
|
|
15
|
+
*
|
|
16
|
+
* It sits at the one chokepoint (createAttentionItem) and caps how many NEW
|
|
17
|
+
* topics may be spawned in a rolling window — BOTH per source AND globally. Past
|
|
18
|
+
* the budget, items are COALESCED into one running notice topic + logged. The
|
|
19
|
+
* GLOBAL cap is load-bearing: a mis-wired feature that varies its `sourceContext`
|
|
20
|
+
* per item would dodge a per-source-only budget, so once total topic creation in
|
|
21
|
+
* the window exceeds the global ceiling, every further non-critical item — of any
|
|
22
|
+
* source — coalesces into ONE shared global bucket. That makes the "a brand-new
|
|
23
|
+
* mis-wired feature gets auto-throttled" guarantee hold regardless of source
|
|
24
|
+
* cardinality.
|
|
25
|
+
*
|
|
26
|
+
* Invariants:
|
|
27
|
+
* - HIGH / URGENT (case-insensitive; CRITICAL treated as URGENT) ALWAYS get
|
|
28
|
+
* their own topic and are never counted. Critical messages are never coalesced.
|
|
29
|
+
* - No item is ever DROPPED. A coalesced item is still recorded in the attention
|
|
30
|
+
* store and written to the suppression audit log; only its per-item TOPIC is
|
|
31
|
+
* withheld.
|
|
32
|
+
*
|
|
33
|
+
* Pure logic, injectable clock, bounded memory — unit-testable in isolation.
|
|
34
|
+
*/
|
|
35
|
+
export interface AttentionTopicGuardConfig {
|
|
36
|
+
/** When false, every item is allowed its own topic (pre-guard behavior). */
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/** Rolling window over which topic creations are counted. */
|
|
39
|
+
windowMs: number;
|
|
40
|
+
/** Max NEW topics a single source may spawn within `windowMs`. */
|
|
41
|
+
maxTopicsPerSource: number;
|
|
42
|
+
/**
|
|
43
|
+
* Max NEW topics across ALL sources within `windowMs`. Backstop against a
|
|
44
|
+
* source that varies its key per item to dodge the per-source budget. Past
|
|
45
|
+
* this, non-critical items coalesce into one shared global bucket regardless
|
|
46
|
+
* of source. Must be >= maxTopicsPerSource to be meaningful.
|
|
47
|
+
*/
|
|
48
|
+
maxTopicsGlobal: number;
|
|
49
|
+
/** Hard cap on distinct source keys tracked (memory bound). */
|
|
50
|
+
maxTrackedSources: number;
|
|
51
|
+
}
|
|
52
|
+
export declare const DEFAULT_ATTENTION_TOPIC_GUARD: AttentionTopicGuardConfig;
|
|
53
|
+
/** The shared key used when the GLOBAL cap (not a per-source cap) trips. */
|
|
54
|
+
export declare const GLOBAL_BUCKET = "*";
|
|
55
|
+
export type GuardDecision = {
|
|
56
|
+
action: 'allow';
|
|
57
|
+
} | {
|
|
58
|
+
action: 'coalesce';
|
|
59
|
+
firstInEpisode: boolean;
|
|
60
|
+
suppressedCount: number;
|
|
61
|
+
bucket: string;
|
|
62
|
+
};
|
|
63
|
+
export declare class AttentionTopicGuard {
|
|
64
|
+
private readonly cfg;
|
|
65
|
+
private readonly now;
|
|
66
|
+
/** source key -> timestamps of recent topic-relevant events within the window. */
|
|
67
|
+
private readonly events;
|
|
68
|
+
/** bucket key -> count of items coalesced in the CURRENT (uninterrupted) episode. */
|
|
69
|
+
private readonly episode;
|
|
70
|
+
/** Global rolling timeline of topic-relevant events (all sources). */
|
|
71
|
+
private globalEvents;
|
|
72
|
+
constructor(cfg?: Partial<AttentionTopicGuardConfig>, now?: () => number);
|
|
73
|
+
get config(): Readonly<AttentionTopicGuardConfig>;
|
|
74
|
+
/**
|
|
75
|
+
* Decide whether an item from `source` at `priority` may spawn its own topic.
|
|
76
|
+
* Returns the coalesce `bucket` (the source key, or GLOBAL_BUCKET when only the
|
|
77
|
+
* global cap tripped) so the adapter routes all global-flood notices into ONE
|
|
78
|
+
* shared topic rather than one-per-varying-source.
|
|
79
|
+
*/
|
|
80
|
+
decide(source: string | undefined, priority: string | undefined): GuardDecision;
|
|
81
|
+
/** Drop source keys whose events have all aged out, and hard-cap map size. */
|
|
82
|
+
private evictStaleSources;
|
|
83
|
+
/** Test/inspection seam. */
|
|
84
|
+
episodeCount(bucket: string): number;
|
|
85
|
+
/** Test/inspection seam — number of distinct source keys currently tracked. */
|
|
86
|
+
get trackedSourceCount(): number;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=AttentionTopicGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttentionTopicGuard.d.ts","sourceRoot":"","sources":["../../src/messaging/AttentionTopicGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,MAAM,WAAW,yBAAyB;IACxC,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,6BAA6B,EAAE,yBAM3C,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAW7F,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA4B;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,qFAAqF;IACrF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,sEAAsE;IACtE,OAAO,CAAC,YAAY,CAAgB;gBAExB,GAAG,GAAE,OAAO,CAAC,yBAAyB,CAAM,EAAE,GAAG,GAAE,MAAM,MAAyB;IAmB9F,IAAI,MAAM,IAAI,QAAQ,CAAC,yBAAyB,CAAC,CAEhD;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa;IAqC/E,8EAA8E;IAC9E,OAAO,CAAC,iBAAiB;IAuBzB,4BAA4B;IAC5B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAIpC,+EAA+E;IAC/E,IAAI,kBAAkB,IAAI,MAAM,CAE/B;CACF"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AttentionTopicGuard — a per-source + global circuit breaker for forum-topic
|
|
3
|
+
* creation. NOT a blocker: it is a DELIVERY SHAPER (same class as
|
|
4
|
+
* SentinelNotifier), never a gate. It holds no authority over agent behavior or
|
|
5
|
+
* information flow — it only changes the FORM of delivery (one coalesced topic +
|
|
6
|
+
* a log line) for non-critical, high-volume notices, and never withholds a
|
|
7
|
+
* critical notice or drops an item. See docs/signal-vs-authority.md (the rate-
|
|
8
|
+
* counter / transport-dedup carve-outs).
|
|
9
|
+
*
|
|
10
|
+
* Built in response to the 2026-05-28 topic-spam flood (the SECOND such flood;
|
|
11
|
+
* the first, 2026-05-22, produced the SentinelNotifier fix). Root cause both
|
|
12
|
+
* times: `TelegramAdapter.createAttentionItem` spawns a BRAND-NEW forum topic
|
|
13
|
+
* per attention item, and there was no structural gate stopping a housekeeping
|
|
14
|
+
* feature from raising attention items at volume.
|
|
15
|
+
*
|
|
16
|
+
* It sits at the one chokepoint (createAttentionItem) and caps how many NEW
|
|
17
|
+
* topics may be spawned in a rolling window — BOTH per source AND globally. Past
|
|
18
|
+
* the budget, items are COALESCED into one running notice topic + logged. The
|
|
19
|
+
* GLOBAL cap is load-bearing: a mis-wired feature that varies its `sourceContext`
|
|
20
|
+
* per item would dodge a per-source-only budget, so once total topic creation in
|
|
21
|
+
* the window exceeds the global ceiling, every further non-critical item — of any
|
|
22
|
+
* source — coalesces into ONE shared global bucket. That makes the "a brand-new
|
|
23
|
+
* mis-wired feature gets auto-throttled" guarantee hold regardless of source
|
|
24
|
+
* cardinality.
|
|
25
|
+
*
|
|
26
|
+
* Invariants:
|
|
27
|
+
* - HIGH / URGENT (case-insensitive; CRITICAL treated as URGENT) ALWAYS get
|
|
28
|
+
* their own topic and are never counted. Critical messages are never coalesced.
|
|
29
|
+
* - No item is ever DROPPED. A coalesced item is still recorded in the attention
|
|
30
|
+
* store and written to the suppression audit log; only its per-item TOPIC is
|
|
31
|
+
* withheld.
|
|
32
|
+
*
|
|
33
|
+
* Pure logic, injectable clock, bounded memory — unit-testable in isolation.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_ATTENTION_TOPIC_GUARD = {
|
|
36
|
+
enabled: true,
|
|
37
|
+
windowMs: 10 * 60 * 1000,
|
|
38
|
+
maxTopicsPerSource: 3,
|
|
39
|
+
maxTopicsGlobal: 8,
|
|
40
|
+
maxTrackedSources: 512,
|
|
41
|
+
};
|
|
42
|
+
/** The shared key used when the GLOBAL cap (not a per-source cap) trips. */
|
|
43
|
+
export const GLOBAL_BUCKET = '*';
|
|
44
|
+
/** Priorities that always get their own topic and are never counted. */
|
|
45
|
+
const CRITICAL_PRIORITIES = new Set(['HIGH', 'URGENT', 'CRITICAL']);
|
|
46
|
+
function coerceNum(v, fallback, { int = false, min = 0 } = {}) {
|
|
47
|
+
const n = typeof v === 'number' ? v : NaN;
|
|
48
|
+
if (!Number.isFinite(n) || n < min)
|
|
49
|
+
return fallback;
|
|
50
|
+
return int ? Math.floor(n) : n;
|
|
51
|
+
}
|
|
52
|
+
export class AttentionTopicGuard {
|
|
53
|
+
cfg;
|
|
54
|
+
now;
|
|
55
|
+
/** source key -> timestamps of recent topic-relevant events within the window. */
|
|
56
|
+
events = new Map();
|
|
57
|
+
/** bucket key -> count of items coalesced in the CURRENT (uninterrupted) episode. */
|
|
58
|
+
episode = new Map();
|
|
59
|
+
/** Global rolling timeline of topic-relevant events (all sources). */
|
|
60
|
+
globalEvents = [];
|
|
61
|
+
constructor(cfg = {}, now = () => Date.now()) {
|
|
62
|
+
// Validate/coerce: a fat-fingered NaN/negative must NOT silently disable the
|
|
63
|
+
// guard (NaN comparisons are always false → it would never trip).
|
|
64
|
+
const d = DEFAULT_ATTENTION_TOPIC_GUARD;
|
|
65
|
+
const windowMs = coerceNum(cfg.windowMs, d.windowMs, { min: 1 });
|
|
66
|
+
const maxTopicsPerSource = coerceNum(cfg.maxTopicsPerSource, d.maxTopicsPerSource, { int: true, min: 0 });
|
|
67
|
+
this.cfg = {
|
|
68
|
+
enabled: cfg.enabled !== false,
|
|
69
|
+
windowMs,
|
|
70
|
+
maxTopicsPerSource,
|
|
71
|
+
// Independent global ceiling. A LOW global with a HIGH per-source budget is
|
|
72
|
+
// the intended high-cardinality config (cap total topics regardless of how
|
|
73
|
+
// many distinct source keys appear), so we do NOT clamp it up to per-source.
|
|
74
|
+
maxTopicsGlobal: coerceNum(cfg.maxTopicsGlobal, d.maxTopicsGlobal, { int: true, min: 1 }),
|
|
75
|
+
maxTrackedSources: coerceNum(cfg.maxTrackedSources, d.maxTrackedSources, { int: true, min: 1 }),
|
|
76
|
+
};
|
|
77
|
+
this.now = now;
|
|
78
|
+
}
|
|
79
|
+
get config() {
|
|
80
|
+
return this.cfg;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Decide whether an item from `source` at `priority` may spawn its own topic.
|
|
84
|
+
* Returns the coalesce `bucket` (the source key, or GLOBAL_BUCKET when only the
|
|
85
|
+
* global cap tripped) so the adapter routes all global-flood notices into ONE
|
|
86
|
+
* shared topic rather than one-per-varying-source.
|
|
87
|
+
*/
|
|
88
|
+
decide(source, priority) {
|
|
89
|
+
if (!this.cfg.enabled)
|
|
90
|
+
return { action: 'allow' };
|
|
91
|
+
const p = (priority ?? '').toUpperCase();
|
|
92
|
+
if (CRITICAL_PRIORITIES.has(p))
|
|
93
|
+
return { action: 'allow' };
|
|
94
|
+
const key = source && source.trim() ? source : 'unknown';
|
|
95
|
+
const now = this.now();
|
|
96
|
+
const cutoff = now - this.cfg.windowMs;
|
|
97
|
+
const perRecent = (this.events.get(key) ?? []).filter((t) => t >= cutoff);
|
|
98
|
+
this.globalEvents = this.globalEvents.filter((t) => t >= cutoff);
|
|
99
|
+
const perTripped = perRecent.length >= this.cfg.maxTopicsPerSource;
|
|
100
|
+
const globalTripped = this.globalEvents.length >= this.cfg.maxTopicsGlobal;
|
|
101
|
+
// Record this event in both timelines so a sustained flood keeps the window
|
|
102
|
+
// full and stays in a single coalesce episode.
|
|
103
|
+
perRecent.push(now);
|
|
104
|
+
this.events.set(key, perRecent);
|
|
105
|
+
this.globalEvents.push(now);
|
|
106
|
+
this.evictStaleSources(cutoff);
|
|
107
|
+
if (perTripped || globalTripped) {
|
|
108
|
+
// Per-source flood → coalesce under the source (one topic per flooding
|
|
109
|
+
// source). Global-only flood (many low-volume sources) → coalesce under the
|
|
110
|
+
// shared global bucket (one topic total), defeating key-variation dodges.
|
|
111
|
+
const bucket = perTripped ? key : GLOBAL_BUCKET;
|
|
112
|
+
const count = (this.episode.get(bucket) ?? 0) + 1;
|
|
113
|
+
this.episode.set(bucket, count);
|
|
114
|
+
return { action: 'coalesce', firstInEpisode: count === 1, suppressedCount: count, bucket };
|
|
115
|
+
}
|
|
116
|
+
// Under both budgets: own topic, and any prior episode for this source is over.
|
|
117
|
+
this.episode.set(key, 0);
|
|
118
|
+
return { action: 'allow' };
|
|
119
|
+
}
|
|
120
|
+
/** Drop source keys whose events have all aged out, and hard-cap map size. */
|
|
121
|
+
evictStaleSources(cutoff) {
|
|
122
|
+
if (this.events.size <= this.cfg.maxTrackedSources) {
|
|
123
|
+
// cheap path: nothing to do until we approach the cap.
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const [k, ts] of this.events) {
|
|
127
|
+
const live = ts.filter((t) => t >= cutoff);
|
|
128
|
+
if (live.length === 0) {
|
|
129
|
+
this.events.delete(k);
|
|
130
|
+
this.episode.delete(k);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
this.events.set(k, live);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// If still over the cap (many genuinely-active sources), drop the oldest.
|
|
137
|
+
while (this.events.size > this.cfg.maxTrackedSources) {
|
|
138
|
+
const oldest = this.events.keys().next().value;
|
|
139
|
+
if (oldest === undefined)
|
|
140
|
+
break;
|
|
141
|
+
this.events.delete(oldest);
|
|
142
|
+
this.episode.delete(oldest);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Test/inspection seam. */
|
|
146
|
+
episodeCount(bucket) {
|
|
147
|
+
return this.episode.get(bucket && bucket.trim() ? bucket : 'unknown') ?? 0;
|
|
148
|
+
}
|
|
149
|
+
/** Test/inspection seam — number of distinct source keys currently tracked. */
|
|
150
|
+
get trackedSourceCount() {
|
|
151
|
+
return this.events.size;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=AttentionTopicGuard.js.map
|