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.
Files changed (38) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +13 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  5. package/dist/core/PostUpdateMigrator.js +20 -0
  6. package/dist/core/PostUpdateMigrator.js.map +1 -1
  7. package/dist/core/UsherActedCorrelator.d.ts +96 -0
  8. package/dist/core/UsherActedCorrelator.d.ts.map +1 -0
  9. package/dist/core/UsherActedCorrelator.js +175 -0
  10. package/dist/core/UsherActedCorrelator.js.map +1 -0
  11. package/dist/core/UsherSignalStore.d.ts +26 -2
  12. package/dist/core/UsherSignalStore.d.ts.map +1 -1
  13. package/dist/core/UsherSignalStore.js +17 -2
  14. package/dist/core/UsherSignalStore.js.map +1 -1
  15. package/dist/messaging/AttentionTopicGuard.d.ts +88 -0
  16. package/dist/messaging/AttentionTopicGuard.d.ts.map +1 -0
  17. package/dist/messaging/AttentionTopicGuard.js +154 -0
  18. package/dist/messaging/AttentionTopicGuard.js.map +1 -0
  19. package/dist/messaging/TelegramAdapter.d.ts +39 -0
  20. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  21. package/dist/messaging/TelegramAdapter.js +142 -1
  22. package/dist/messaging/TelegramAdapter.js.map +1 -1
  23. package/dist/server/AgentServer.d.ts.map +1 -1
  24. package/dist/server/AgentServer.js +1 -0
  25. package/dist/server/AgentServer.js.map +1 -1
  26. package/dist/server/routes.d.ts +4 -0
  27. package/dist/server/routes.d.ts.map +1 -1
  28. package/dist/server/routes.js +12 -0
  29. package/dist/server/routes.js.map +1 -1
  30. package/dist/server/usherRoutes.d.ts.map +1 -1
  31. package/dist/server/usherRoutes.js +7 -1
  32. package/dist/server/usherRoutes.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/data/builtin-manifest.json +63 -63
  35. package/upgrades/1.3.72.md +65 -0
  36. package/upgrades/1.3.73.md +73 -0
  37. package/upgrades/side-effects/attention-topic-flood-guard.md +74 -0
  38. 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
- /** Mark a signal as acted-on (precision numerator). Best-effort. */
53
- markActed(topicId: number, signalId: string): boolean;
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;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;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,oEAAoE;IACpE,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAcrD,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,WAAW,EAAE;IAKtD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY;CAG1C"}
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
- /** Mark a signal as acted-on (precision numerator). Best-effort. */
90
- markActed(topicId, signalId) {
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":"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"}
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