instar 1.3.782 → 1.3.784
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 +41 -15
- package/dist/commands/server.js.map +1 -1
- package/dist/core/MachineIdentity.d.ts +13 -0
- package/dist/core/MachineIdentity.d.ts.map +1 -1
- package/dist/core/MachineIdentity.js +31 -0
- package/dist/core/MachineIdentity.js.map +1 -1
- package/dist/core/MeteredSpendGate.d.ts +127 -0
- package/dist/core/MeteredSpendGate.d.ts.map +1 -0
- package/dist/core/MeteredSpendGate.js +248 -0
- package/dist/core/MeteredSpendGate.js.map +1 -0
- package/dist/core/MeteredSpendLedger.d.ts +154 -0
- package/dist/core/MeteredSpendLedger.d.ts.map +1 -0
- package/dist/core/MeteredSpendLedger.js +434 -0
- package/dist/core/MeteredSpendLedger.js.map +1 -0
- package/dist/core/PinAttemptStore.d.ts +35 -0
- package/dist/core/PinAttemptStore.d.ts.map +1 -0
- package/dist/core/PinAttemptStore.js +85 -0
- package/dist/core/PinAttemptStore.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +7 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +24 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/RenderedPlanStore.d.ts +61 -0
- package/dist/core/RenderedPlanStore.d.ts.map +1 -0
- package/dist/core/RenderedPlanStore.js +92 -0
- package/dist/core/RenderedPlanStore.js.map +1 -0
- package/dist/core/RoutingSpendCapsStore.d.ts +113 -0
- package/dist/core/RoutingSpendCapsStore.d.ts.map +1 -0
- package/dist/core/RoutingSpendCapsStore.js +241 -0
- package/dist/core/RoutingSpendCapsStore.js.map +1 -0
- package/dist/core/SpendAlertDispatcher.d.ts +94 -0
- package/dist/core/SpendAlertDispatcher.d.ts.map +1 -0
- package/dist/core/SpendAlertDispatcher.js +168 -0
- package/dist/core/SpendAlertDispatcher.js.map +1 -0
- package/dist/core/SpendAlertEmitters.d.ts +118 -0
- package/dist/core/SpendAlertEmitters.d.ts.map +1 -0
- package/dist/core/SpendAlertEmitters.js +220 -0
- package/dist/core/SpendAlertEmitters.js.map +1 -0
- package/dist/core/SpendAlertResolver.d.ts +89 -0
- package/dist/core/SpendAlertResolver.d.ts.map +1 -0
- package/dist/core/SpendAlertResolver.js +175 -0
- package/dist/core/SpendAlertResolver.js.map +1 -0
- package/dist/core/TelegramSpendTopicChannel.d.ts +50 -0
- package/dist/core/TelegramSpendTopicChannel.d.ts.map +1 -0
- package/dist/core/TelegramSpendTopicChannel.js +106 -0
- package/dist/core/TelegramSpendTopicChannel.js.map +1 -0
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +17 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +9 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/routingSpendView.d.ts +10 -2
- package/dist/core/routingSpendView.d.ts.map +1 -1
- package/dist/core/routingSpendView.js +18 -10
- package/dist/core/routingSpendView.js.map +1 -1
- package/dist/core/types.d.ts +45 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/server/AgentServer.d.ts +25 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +304 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +10 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +196 -4
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +206 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/upgrades/1.3.783.md +66 -0
- package/upgrades/1.3.784.md +59 -0
- package/upgrades/side-effects/routing-spend-increment-b.md +44 -0
- package/upgrades/side-effects/routing-spend-increment-c.md +44 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendAlertDispatcher — the Increment-C channel-abstracted alert layer of the
|
|
3
|
+
* Routing Control Room (docs/specs/routing-control-room-spend-alerts.md,
|
|
4
|
+
* §Surface 2 Alerts / FD-6 / Amendment 2).
|
|
5
|
+
*
|
|
6
|
+
* ONE dedicated topic, message-INTO not topic-PER: every spend alert lands in
|
|
7
|
+
* the single "💰 Routing & Spend Alerts" topic (the `burnDetection.alertTopicId`
|
|
8
|
+
* sendToTopic precedent) — deliberately NEVER `POST /attention` (topic-per-item
|
|
9
|
+
* is the flood the operator directive forbids). The lifeline is the single
|
|
10
|
+
* NAMED emergency exception (unresolvable topic / failed money-critical
|
|
11
|
+
* delivery), never a second routine destination.
|
|
12
|
+
*
|
|
13
|
+
* Discipline (all BEFORE any channel send):
|
|
14
|
+
* - LANE-SCOPED dedup (S-F8): money-critical kinds (cap-hit, holder-dead)
|
|
15
|
+
* ride a DISTINCT dedupe lane from informational kinds (door-dark,
|
|
16
|
+
* fallback-spike, price/recon drift), so a flapping door's volume can never
|
|
17
|
+
* coalesce a money-critical cap alert into a digest line. Lanes govern
|
|
18
|
+
* COALESCING, never the destination.
|
|
19
|
+
* - EDGE latch per dedupeKey: latches ONLY on CONFIRMED delivery, so a
|
|
20
|
+
* transient failure stays eligible for re-send instead of permanently
|
|
21
|
+
* suppressed.
|
|
22
|
+
* - COALESCING: informational alerts within the digest window aggregate into
|
|
23
|
+
* ONE message per episode; money-critical alerts are never digested.
|
|
24
|
+
* - dryRun-FIRST (FD-16: Increment C ships dryRun-first live-on-dev): while
|
|
25
|
+
* dryRun holds, every decision is recorded to the scrubbed jsonl and
|
|
26
|
+
* NOTHING is delivered.
|
|
27
|
+
* - Scrubbed jsonl audit (S-F7): kind / lane / dedupeKey / decision / door /
|
|
28
|
+
* counts — NEVER a provider response/error body, never a key-shaped
|
|
29
|
+
* substring, never a token.
|
|
30
|
+
*/
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
/** Money-critical kinds ride their own dedupe lane + the durable relay (S-F8/G5). */
|
|
34
|
+
export const MONEY_CRITICAL_KINDS = new Set(['cap-hit', 'holder-dead']);
|
|
35
|
+
const DEFAULT_REARM_INFORMATIONAL_MS = 24 * 60 * 60 * 1000;
|
|
36
|
+
const DEFAULT_REARM_MONEY_MS = 6 * 60 * 60 * 1000;
|
|
37
|
+
const DEFAULT_DIGEST_WINDOW_MS = 15 * 60 * 1000;
|
|
38
|
+
export class SpendAlertDispatcher {
|
|
39
|
+
channels;
|
|
40
|
+
dryRun;
|
|
41
|
+
digestWindowMs;
|
|
42
|
+
auditPath;
|
|
43
|
+
now;
|
|
44
|
+
/** Edge latches per lane (S-F8: distinct lanes so volumes can't cross-suppress). */
|
|
45
|
+
latched = new Map(); // `${lane}|${dedupeKey}` → confirmed-at ms
|
|
46
|
+
/** Pending informational digest: texts accumulated within the window. */
|
|
47
|
+
digestPending = [];
|
|
48
|
+
digestTimer = null;
|
|
49
|
+
digestFlushInFlight = false;
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
this.channels = opts.channels;
|
|
52
|
+
this.dryRun = opts.dryRun !== false; // default TRUE (FD-16 dryRun-first)
|
|
53
|
+
this.digestWindowMs = opts.digestWindowMs ?? DEFAULT_DIGEST_WINDOW_MS;
|
|
54
|
+
this.auditPath = opts.auditPath ?? null;
|
|
55
|
+
this.now = opts.now ?? (() => Date.now());
|
|
56
|
+
}
|
|
57
|
+
laneOf(kind) {
|
|
58
|
+
return MONEY_CRITICAL_KINDS.has(kind) ? 'money-critical' : 'informational';
|
|
59
|
+
}
|
|
60
|
+
/** Dispatch one alert through dedup → coalescing → channels. Never throws. */
|
|
61
|
+
async dispatch(alert) {
|
|
62
|
+
const lane = this.laneOf(alert.kind);
|
|
63
|
+
try {
|
|
64
|
+
const latchKey = `${lane}|${alert.dedupeKey}`;
|
|
65
|
+
const reArm = alert.reArmMs ?? (lane === 'money-critical' ? DEFAULT_REARM_MONEY_MS : DEFAULT_REARM_INFORMATIONAL_MS);
|
|
66
|
+
const last = this.latched.get(latchKey);
|
|
67
|
+
if (last !== undefined && this.now() - last < reArm) {
|
|
68
|
+
this.audit({ decision: 'suppressed', kind: alert.kind, lane, dedupeKey: alert.dedupeKey });
|
|
69
|
+
return { decision: 'suppressed', lane };
|
|
70
|
+
}
|
|
71
|
+
if (this.dryRun) {
|
|
72
|
+
// FD-16 soak: record the WOULD-send (with the latch set, so the soak
|
|
73
|
+
// telemetry measures the real post-dedup volume) — deliver nothing.
|
|
74
|
+
this.latched.set(latchKey, this.now());
|
|
75
|
+
this.audit({ decision: 'dry-run', kind: alert.kind, lane, dedupeKey: alert.dedupeKey });
|
|
76
|
+
return { decision: 'dry-run', lane };
|
|
77
|
+
}
|
|
78
|
+
if (lane === 'informational') {
|
|
79
|
+
// Coalesce into ONE digest message per window (never for money-critical).
|
|
80
|
+
this.digestPending.push({ key: alert.dedupeKey, text: alert.text });
|
|
81
|
+
this.latched.set(latchKey, this.now()); // digested = handled; window re-arms it
|
|
82
|
+
this.scheduleDigestFlush();
|
|
83
|
+
this.audit({ decision: 'coalesced', kind: alert.kind, lane, dedupeKey: alert.dedupeKey, pending: this.digestPending.length });
|
|
84
|
+
return { decision: 'coalesced', lane };
|
|
85
|
+
}
|
|
86
|
+
// Money-critical: immediate, never digested, durable path preferred.
|
|
87
|
+
const outcome = await this.deliverThroughChannels(alert.text, true);
|
|
88
|
+
if (outcome === 'sent' || outcome === 'sent-lifeline' || outcome === 'queued-durable') {
|
|
89
|
+
this.latched.set(latchKey, this.now());
|
|
90
|
+
}
|
|
91
|
+
this.audit({ decision: outcome, kind: alert.kind, lane, dedupeKey: alert.dedupeKey });
|
|
92
|
+
return { decision: outcome, lane };
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// The dispatcher is a notifier — it must never throw into an emitter's
|
|
96
|
+
// (or the gate's) call path. Audit and move on.
|
|
97
|
+
this.audit({ decision: 'failed', kind: alert.kind, lane, dedupeKey: alert.dedupeKey, error: String(err).slice(0, 200) });
|
|
98
|
+
return { decision: 'failed', lane };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Flush any pending digest immediately (test hook + shutdown path). */
|
|
102
|
+
async flushDigest() {
|
|
103
|
+
if (this.digestTimer) {
|
|
104
|
+
clearTimeout(this.digestTimer);
|
|
105
|
+
this.digestTimer = null;
|
|
106
|
+
}
|
|
107
|
+
if (this.digestFlushInFlight || this.digestPending.length === 0)
|
|
108
|
+
return;
|
|
109
|
+
this.digestFlushInFlight = true;
|
|
110
|
+
try {
|
|
111
|
+
const items = this.digestPending.splice(0);
|
|
112
|
+
const text = items.length === 1
|
|
113
|
+
? items[0].text
|
|
114
|
+
: `📋 Routing spend digest (${items.length} notices):\n` + items.map((i) => `• ${i.text}`).join('\n');
|
|
115
|
+
const outcome = await this.deliverThroughChannels(text, false);
|
|
116
|
+
if (outcome === 'failed') {
|
|
117
|
+
// Stay eligible: un-latch the digested keys so the next occurrence retries.
|
|
118
|
+
for (const i of items)
|
|
119
|
+
this.latched.delete(`informational|${i.key}`);
|
|
120
|
+
}
|
|
121
|
+
this.audit({ decision: `digest-${outcome}`, count: items.length });
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.digestFlushInFlight = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
scheduleDigestFlush() {
|
|
128
|
+
if (this.digestTimer)
|
|
129
|
+
return;
|
|
130
|
+
this.digestTimer = setTimeout(() => {
|
|
131
|
+
this.digestTimer = null;
|
|
132
|
+
void this.flushDigest();
|
|
133
|
+
}, this.digestWindowMs);
|
|
134
|
+
this.digestTimer.unref?.();
|
|
135
|
+
}
|
|
136
|
+
async deliverThroughChannels(text, moneyCritical) {
|
|
137
|
+
let best = 'failed';
|
|
138
|
+
for (const ch of this.channels) {
|
|
139
|
+
try {
|
|
140
|
+
const r = await ch.deliver(text, moneyCritical);
|
|
141
|
+
if (r === 'sent')
|
|
142
|
+
return 'sent';
|
|
143
|
+
if (r === 'queued-durable' && best === 'failed')
|
|
144
|
+
best = 'queued-durable';
|
|
145
|
+
if (r === 'sent-lifeline' && best === 'failed')
|
|
146
|
+
best = 'sent-lifeline';
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// @silent-fallback-ok: one channel throwing must not break the others;
|
|
150
|
+
// the terminal outcome (incl. all-failed) is audited by the caller.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return best;
|
|
154
|
+
}
|
|
155
|
+
audit(entry) {
|
|
156
|
+
if (!this.auditPath)
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
fs.mkdirSync(path.dirname(this.auditPath), { recursive: true });
|
|
160
|
+
fs.appendFileSync(this.auditPath, JSON.stringify({ ts: new Date(this.now()).toISOString(), ...entry }) + '\n', { mode: 0o600 });
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// @silent-fallback-ok: the audit line is observability riding a
|
|
164
|
+
// notification decision that already happened — never blocks dispatch.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=SpendAlertDispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpendAlertDispatcher.js","sourceRoot":"","sources":["../../src/core/SpendAlertDispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendAlertEmitters — the Increment-C trigger set of the Routing Control Room
|
|
3
|
+
* (docs/specs/routing-control-room-spend-alerts.md, §Surface 2 Alerts —
|
|
4
|
+
* Triggers, "Self-Heal Before Notify").
|
|
5
|
+
*
|
|
6
|
+
* Every emitter is a SIGNAL consumer feeding the SpendAlertDispatcher: it
|
|
7
|
+
* blocks nothing, gates nothing, and throw-swallows so a notifier failure can
|
|
8
|
+
* never disturb the gate/router path it observes.
|
|
9
|
+
*
|
|
10
|
+
* Kinds produced here:
|
|
11
|
+
* - cap-approach — 50%/80% on BOTH daily AND lifetime (G4), edge-triggered per
|
|
12
|
+
* (capKind, threshold, window); coalesced into the digest.
|
|
13
|
+
* - cap-hit — ONE edge-triggered money-critical alert, worded honestly
|
|
14
|
+
* ("a reservation would exceed key X's daily cap", actual vs reserved —
|
|
15
|
+
* A-Min13). Protective; the adjust action is the operator's.
|
|
16
|
+
* - door-dark — downstream of swap-tail self-heal; escalates only on
|
|
17
|
+
* whole-chain exhaustion (RouterFailClosedError plans). P19 brakes:
|
|
18
|
+
* max-attempts = chain length per episode bucket, widening backoff, a
|
|
19
|
+
* flapping breaker (N exhaustions/window → critical wording, bypasses
|
|
20
|
+
* coalescing via the money-critical lane), scrubbed jsonl (the dispatcher's).
|
|
21
|
+
* - fallback-spike — routine self-healed fallback churn is NOT an operator
|
|
22
|
+
* event (Near-Silent): every fallback is counted; a digest line fires ONLY
|
|
23
|
+
* when the hourly rate crosses an absolute ceiling (code constant).
|
|
24
|
+
* - holder-dead — the ONE named exception to holder-single-voice (A2-2): a
|
|
25
|
+
* SURVIVING machine emits when the pool observes the metered-lease holder
|
|
26
|
+
* offline past the mesh-death threshold while any door is live. Stable
|
|
27
|
+
* pool-wide dedupe key `spend-holder-dead:<keyEpoch>`. Single-machine:
|
|
28
|
+
* strict no-op (there is no surviving other voice).
|
|
29
|
+
* - recon-drift — the emit surface for the Layer-1c provider-reconciliation
|
|
30
|
+
* sweep (lands next PR); wording per Amendment 1.
|
|
31
|
+
*/
|
|
32
|
+
import type { SpendAlertDispatcher } from './SpendAlertDispatcher.js';
|
|
33
|
+
/** Cap-approach thresholds (G4) — code-defined, both cap kinds. */
|
|
34
|
+
export declare const CAP_APPROACH_THRESHOLDS: readonly [0.5, 0.8];
|
|
35
|
+
/** Fallback-spike absolute per-hour ceiling (code constant — Near-Silent default). */
|
|
36
|
+
export declare const FALLBACK_SPIKE_PER_HOUR_CEILING = 60;
|
|
37
|
+
/** Door-dark flapping breaker: N exhaustions inside the window escalate the wording. */
|
|
38
|
+
export declare const DOOR_DARK_FLAP_THRESHOLD = 5;
|
|
39
|
+
export declare const DOOR_DARK_FLAP_WINDOW_MS: number;
|
|
40
|
+
/** Door-dark per-episode widening backoff base + episode bucket size. */
|
|
41
|
+
export declare const DOOR_DARK_BACKOFF_BASE_MS: number;
|
|
42
|
+
export declare const DOOR_DARK_EPISODE_BUCKET_MS: number;
|
|
43
|
+
export interface GateAdmitEvent {
|
|
44
|
+
type: 'admit';
|
|
45
|
+
keyRef: string;
|
|
46
|
+
door: string;
|
|
47
|
+
committedLifetimeUsd: number;
|
|
48
|
+
committedDayUsd: number;
|
|
49
|
+
lifetimeCapUsd: number;
|
|
50
|
+
dailyCapUsd: number;
|
|
51
|
+
}
|
|
52
|
+
export interface GateRefusalEvent {
|
|
53
|
+
type: 'refusal';
|
|
54
|
+
reason: string;
|
|
55
|
+
keyRef?: string;
|
|
56
|
+
door?: string;
|
|
57
|
+
detail: string;
|
|
58
|
+
}
|
|
59
|
+
export type GateEvent = GateAdmitEvent | GateRefusalEvent;
|
|
60
|
+
export interface NatureRoutePlanEvent {
|
|
61
|
+
component?: string;
|
|
62
|
+
dryRun: boolean;
|
|
63
|
+
failClosed?: boolean;
|
|
64
|
+
resolution?: {
|
|
65
|
+
outcome?: string;
|
|
66
|
+
resolvedChain?: string;
|
|
67
|
+
swapTail?: unknown[];
|
|
68
|
+
primary?: {
|
|
69
|
+
door?: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** True when the served position was NOT the chain primary (a fallback served). */
|
|
73
|
+
servedFallback?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface SpendAlertEmittersOptions {
|
|
76
|
+
dispatcher: SpendAlertDispatcher;
|
|
77
|
+
machineId: string;
|
|
78
|
+
now?: () => number;
|
|
79
|
+
}
|
|
80
|
+
export declare class SpendAlertEmitters {
|
|
81
|
+
private readonly dispatcher;
|
|
82
|
+
private readonly machineId;
|
|
83
|
+
private readonly now;
|
|
84
|
+
/** door-dark per-episode state: chain → { bucket, attempts, nextAllowedAtMs, exhaustionsWindow } */
|
|
85
|
+
private doorDark;
|
|
86
|
+
/** fallback counter: hour-bucket → count (bounded: only the current bucket is kept). */
|
|
87
|
+
private fallbackBucket;
|
|
88
|
+
constructor(opts: SpendAlertEmittersOptions);
|
|
89
|
+
/**
|
|
90
|
+
* Observer-isolated dispatch: the real dispatcher never throws/rejects, but
|
|
91
|
+
* the observer CONTRACT must hold against any implementation — a sync throw
|
|
92
|
+
* OR an async rejection here can never reach the gate/router path, and can
|
|
93
|
+
* never surface as an unhandled rejection.
|
|
94
|
+
*/
|
|
95
|
+
private fire;
|
|
96
|
+
/** The MeteredSpendGate observer (onGateEvent). NEVER throws into the gate path. */
|
|
97
|
+
onGateEvent(ev: GateEvent): void;
|
|
98
|
+
private checkCapApproach;
|
|
99
|
+
/**
|
|
100
|
+
* The router-signal consumer (fed by the onNatureRoutePlan fan-out, I-9).
|
|
101
|
+
* Counts every served fallback (Near-Silent: jsonl only via the dispatcher's
|
|
102
|
+
* audit); emits a digest line ONLY on a rate spike; escalates door-dark on
|
|
103
|
+
* whole-chain exhaustion with P19 brakes.
|
|
104
|
+
*/
|
|
105
|
+
onNatureRoutePlan(plan: NatureRoutePlanEvent): void;
|
|
106
|
+
private onChainExhausted;
|
|
107
|
+
private onFallbackServed;
|
|
108
|
+
/**
|
|
109
|
+
* holder-dead (A2-2): call from pool observation when the metered-lease
|
|
110
|
+
* holder is offline past the mesh-death threshold while any door is live.
|
|
111
|
+
* The caller (a SURVIVING machine) supplies the keyEpoch for the stable
|
|
112
|
+
* pool-wide dedupe key. Single-machine installs never call this.
|
|
113
|
+
*/
|
|
114
|
+
onMeteredLeaseHolderDead(holderNickname: string, keyEpoch: number): void;
|
|
115
|
+
/** recon-drift emit surface (Layer 1c feeds this next PR — Amendment 1 wording). */
|
|
116
|
+
onReconciliationDrift(keyRef: string, door: string, driftPct: number): void;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=SpendAlertEmitters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpendAlertEmitters.d.ts","sourceRoot":"","sources":["../../src/core/SpendAlertEmitters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,qBAAsB,CAAC;AAE3D,sFAAsF;AACtF,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD,wFAAwF;AACxF,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,QAAiB,CAAC;AACvD,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,QAAgB,CAAC;AACvD,eAAO,MAAM,2BAA2B,QAAqB,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE1D,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC7G,mFAAmF;IACnF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,oBAAoB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAMD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,oGAAoG;IACpG,OAAO,CAAC,QAAQ,CAA2G;IAC3H,wFAAwF;IACxF,OAAO,CAAC,cAAc,CAA0B;gBAEpC,IAAI,EAAE,yBAAyB;IAM3C;;;;;OAKG;IACH,OAAO,CAAC,IAAI;IASZ,oFAAoF;IACpF,WAAW,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IAmBhC,OAAO,CAAC,gBAAgB;IAuBxB;;;;;OAKG;IACH,iBAAiB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI;IAanD,OAAO,CAAC,gBAAgB;IAiCxB,OAAO,CAAC,gBAAgB;IAiBxB;;;;;OAKG;IACH,wBAAwB,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAgBxE,oFAAoF;IACpF,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;CAc5E"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendAlertEmitters — the Increment-C trigger set of the Routing Control Room
|
|
3
|
+
* (docs/specs/routing-control-room-spend-alerts.md, §Surface 2 Alerts —
|
|
4
|
+
* Triggers, "Self-Heal Before Notify").
|
|
5
|
+
*
|
|
6
|
+
* Every emitter is a SIGNAL consumer feeding the SpendAlertDispatcher: it
|
|
7
|
+
* blocks nothing, gates nothing, and throw-swallows so a notifier failure can
|
|
8
|
+
* never disturb the gate/router path it observes.
|
|
9
|
+
*
|
|
10
|
+
* Kinds produced here:
|
|
11
|
+
* - cap-approach — 50%/80% on BOTH daily AND lifetime (G4), edge-triggered per
|
|
12
|
+
* (capKind, threshold, window); coalesced into the digest.
|
|
13
|
+
* - cap-hit — ONE edge-triggered money-critical alert, worded honestly
|
|
14
|
+
* ("a reservation would exceed key X's daily cap", actual vs reserved —
|
|
15
|
+
* A-Min13). Protective; the adjust action is the operator's.
|
|
16
|
+
* - door-dark — downstream of swap-tail self-heal; escalates only on
|
|
17
|
+
* whole-chain exhaustion (RouterFailClosedError plans). P19 brakes:
|
|
18
|
+
* max-attempts = chain length per episode bucket, widening backoff, a
|
|
19
|
+
* flapping breaker (N exhaustions/window → critical wording, bypasses
|
|
20
|
+
* coalescing via the money-critical lane), scrubbed jsonl (the dispatcher's).
|
|
21
|
+
* - fallback-spike — routine self-healed fallback churn is NOT an operator
|
|
22
|
+
* event (Near-Silent): every fallback is counted; a digest line fires ONLY
|
|
23
|
+
* when the hourly rate crosses an absolute ceiling (code constant).
|
|
24
|
+
* - holder-dead — the ONE named exception to holder-single-voice (A2-2): a
|
|
25
|
+
* SURVIVING machine emits when the pool observes the metered-lease holder
|
|
26
|
+
* offline past the mesh-death threshold while any door is live. Stable
|
|
27
|
+
* pool-wide dedupe key `spend-holder-dead:<keyEpoch>`. Single-machine:
|
|
28
|
+
* strict no-op (there is no surviving other voice).
|
|
29
|
+
* - recon-drift — the emit surface for the Layer-1c provider-reconciliation
|
|
30
|
+
* sweep (lands next PR); wording per Amendment 1.
|
|
31
|
+
*/
|
|
32
|
+
/** Cap-approach thresholds (G4) — code-defined, both cap kinds. */
|
|
33
|
+
export const CAP_APPROACH_THRESHOLDS = [0.5, 0.8];
|
|
34
|
+
/** Fallback-spike absolute per-hour ceiling (code constant — Near-Silent default). */
|
|
35
|
+
export const FALLBACK_SPIKE_PER_HOUR_CEILING = 60;
|
|
36
|
+
/** Door-dark flapping breaker: N exhaustions inside the window escalate the wording. */
|
|
37
|
+
export const DOOR_DARK_FLAP_THRESHOLD = 5;
|
|
38
|
+
export const DOOR_DARK_FLAP_WINDOW_MS = 60 * 60 * 1000;
|
|
39
|
+
/** Door-dark per-episode widening backoff base + episode bucket size. */
|
|
40
|
+
export const DOOR_DARK_BACKOFF_BASE_MS = 5 * 60 * 1000;
|
|
41
|
+
export const DOOR_DARK_EPISODE_BUCKET_MS = 6 * 60 * 60 * 1000;
|
|
42
|
+
function utcDay(ms) {
|
|
43
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
44
|
+
}
|
|
45
|
+
export class SpendAlertEmitters {
|
|
46
|
+
dispatcher;
|
|
47
|
+
machineId;
|
|
48
|
+
now;
|
|
49
|
+
/** door-dark per-episode state: chain → { bucket, attempts, nextAllowedAtMs, exhaustionsWindow } */
|
|
50
|
+
doorDark = new Map();
|
|
51
|
+
/** fallback counter: hour-bucket → count (bounded: only the current bucket is kept). */
|
|
52
|
+
fallbackBucket = { hour: -1, count: 0 };
|
|
53
|
+
constructor(opts) {
|
|
54
|
+
this.dispatcher = opts.dispatcher;
|
|
55
|
+
this.machineId = opts.machineId;
|
|
56
|
+
this.now = opts.now ?? (() => Date.now());
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Observer-isolated dispatch: the real dispatcher never throws/rejects, but
|
|
60
|
+
* the observer CONTRACT must hold against any implementation — a sync throw
|
|
61
|
+
* OR an async rejection here can never reach the gate/router path, and can
|
|
62
|
+
* never surface as an unhandled rejection.
|
|
63
|
+
*/
|
|
64
|
+
fire(alert) {
|
|
65
|
+
try {
|
|
66
|
+
void Promise.resolve(this.dispatcher.dispatch(alert)).catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// @silent-fallback-ok: observer isolation — a throwing dispatcher is the
|
|
70
|
+
// notifier's failure, never the observed path's.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** The MeteredSpendGate observer (onGateEvent). NEVER throws into the gate path. */
|
|
74
|
+
onGateEvent(ev) {
|
|
75
|
+
try {
|
|
76
|
+
if (ev.type === 'admit') {
|
|
77
|
+
this.checkCapApproach(ev);
|
|
78
|
+
}
|
|
79
|
+
else if (ev.reason === 'cap-exceeded') {
|
|
80
|
+
this.fire({
|
|
81
|
+
kind: 'cap-hit',
|
|
82
|
+
dedupeKey: `spend-cap-hit:${ev.keyRef ?? 'unknown'}:${utcDay(this.now())}`,
|
|
83
|
+
text: `🛑 Spending cap hit: a reservation would exceed key '${ev.keyRef ?? 'unknown'}'s cap — ${ev.detail}. ` +
|
|
84
|
+
`The call fell to a free door (paid routing on this key is refusing new spend). ` +
|
|
85
|
+
`Raising the cap is your PIN action on the Spend tab; the freeze/disarm levers are one tap.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// @silent-fallback-ok: a notifier failure must never disturb the gate path.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
checkCapApproach(ev) {
|
|
94
|
+
const checks = [
|
|
95
|
+
{ capKind: 'daily', committed: ev.committedDayUsd, cap: ev.dailyCapUsd, window: utcDay(this.now()) },
|
|
96
|
+
{ capKind: 'lifetime', committed: ev.committedLifetimeUsd, cap: ev.lifetimeCapUsd, window: 'lifetime' },
|
|
97
|
+
];
|
|
98
|
+
for (const c of checks) {
|
|
99
|
+
if (!(c.cap > 0))
|
|
100
|
+
continue;
|
|
101
|
+
const frac = c.committed / c.cap;
|
|
102
|
+
for (const threshold of CAP_APPROACH_THRESHOLDS) {
|
|
103
|
+
if (frac >= threshold) {
|
|
104
|
+
// Edge per (capKind, threshold, window) — the dispatcher's latch dedupes.
|
|
105
|
+
this.fire({
|
|
106
|
+
kind: 'cap-approach',
|
|
107
|
+
dedupeKey: `spend-approach:${ev.keyRef}:${c.capKind}:${threshold}:${c.window}`,
|
|
108
|
+
text: `📊 Key '${ev.keyRef}' is at ${Math.round(frac * 100)}% of its ${c.capKind} cap ` +
|
|
109
|
+
`($${c.committed.toFixed(2)} of $${c.cap.toFixed(2)} committed).`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The router-signal consumer (fed by the onNatureRoutePlan fan-out, I-9).
|
|
117
|
+
* Counts every served fallback (Near-Silent: jsonl only via the dispatcher's
|
|
118
|
+
* audit); emits a digest line ONLY on a rate spike; escalates door-dark on
|
|
119
|
+
* whole-chain exhaustion with P19 brakes.
|
|
120
|
+
*/
|
|
121
|
+
onNatureRoutePlan(plan) {
|
|
122
|
+
try {
|
|
123
|
+
if (plan.failClosed) {
|
|
124
|
+
this.onChainExhausted(plan.resolution?.resolvedChain ?? 'unknown', Array.isArray(plan.resolution?.swapTail) ? plan.resolution.swapTail.length + 1 : 3);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (plan.servedFallback)
|
|
128
|
+
this.onFallbackServed();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// @silent-fallback-ok: observer isolation (I-9) — a notifier failure never
|
|
132
|
+
// breaks the LLM call path or the sibling fan-out subscribers.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
onChainExhausted(chain, chainLength) {
|
|
136
|
+
const nowMs = this.now();
|
|
137
|
+
const bucket = Math.floor(nowMs / DOOR_DARK_EPISODE_BUCKET_MS);
|
|
138
|
+
let st = this.doorDark.get(chain);
|
|
139
|
+
if (!st || st.bucket !== bucket) {
|
|
140
|
+
st = { bucket, attempts: 0, nextAllowedAtMs: 0, exhaustions: st?.exhaustions ?? [] };
|
|
141
|
+
this.doorDark.set(chain, st);
|
|
142
|
+
}
|
|
143
|
+
// Flapping breaker window bookkeeping (kept small: prune outside the window).
|
|
144
|
+
st.exhaustions.push(nowMs);
|
|
145
|
+
st.exhaustions = st.exhaustions.filter((t) => nowMs - t <= DOOR_DARK_FLAP_WINDOW_MS);
|
|
146
|
+
const flapping = st.exhaustions.length >= DOOR_DARK_FLAP_THRESHOLD;
|
|
147
|
+
// P19 brakes: max-attempts = chain length per episode bucket + widening backoff.
|
|
148
|
+
if (st.attempts >= chainLength)
|
|
149
|
+
return; // episode budget spent — the bucket rolling re-arms
|
|
150
|
+
if (nowMs < st.nextAllowedAtMs)
|
|
151
|
+
return; // inside the widening backoff window
|
|
152
|
+
st.attempts += 1;
|
|
153
|
+
st.nextAllowedAtMs = nowMs + DOOR_DARK_BACKOFF_BASE_MS * Math.pow(2, st.attempts - 1);
|
|
154
|
+
this.fire({
|
|
155
|
+
kind: 'door-dark',
|
|
156
|
+
dedupeKey: `spend-door-dark:${this.machineId}:${chain}:${bucket}:${st.attempts}`,
|
|
157
|
+
text: flapping
|
|
158
|
+
? `🚨 Routing chain '${chain}' is FLAPPING dark (${st.exhaustions.length} full exhaustions in the last hour) — ` +
|
|
159
|
+
`every door including the free tails failed repeatedly. Gated work on this chain is failing closed.`
|
|
160
|
+
: `⚠️ Routing chain '${chain}' went dark — every door including the free tails was unavailable ` +
|
|
161
|
+
`(attempt ${st.attempts}/${chainLength} this episode). Self-heal continues; this is the whole-chain escalation.`,
|
|
162
|
+
// The flapping escalation must not sit in a digest: shorten its re-arm instead
|
|
163
|
+
// of switching lanes (kinds keep their lane — S-F8).
|
|
164
|
+
...(flapping ? { reArmMs: 30 * 60 * 1000 } : {}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
onFallbackServed() {
|
|
168
|
+
const hour = Math.floor(this.now() / 3_600_000);
|
|
169
|
+
if (this.fallbackBucket.hour !== hour)
|
|
170
|
+
this.fallbackBucket = { hour, count: 0 };
|
|
171
|
+
this.fallbackBucket.count += 1;
|
|
172
|
+
if (this.fallbackBucket.count === FALLBACK_SPIKE_PER_HOUR_CEILING) {
|
|
173
|
+
// Edge exactly at the ceiling crossing — once per hour bucket by construction.
|
|
174
|
+
this.fire({
|
|
175
|
+
kind: 'fallback-spike',
|
|
176
|
+
dedupeKey: `spend-fallback-spike:${this.machineId}:${hour}`,
|
|
177
|
+
text: `📈 Routing fallback rate spiked: ${this.fallbackBucket.count} fallback-served calls this hour ` +
|
|
178
|
+
`(ceiling ${FALLBACK_SPIKE_PER_HOUR_CEILING}). Primaries are degrading — the chains are self-healing, ` +
|
|
179
|
+
`but persistent churn is worth a look at the Routing Map.`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* holder-dead (A2-2): call from pool observation when the metered-lease
|
|
185
|
+
* holder is offline past the mesh-death threshold while any door is live.
|
|
186
|
+
* The caller (a SURVIVING machine) supplies the keyEpoch for the stable
|
|
187
|
+
* pool-wide dedupe key. Single-machine installs never call this.
|
|
188
|
+
*/
|
|
189
|
+
onMeteredLeaseHolderDead(holderNickname, keyEpoch) {
|
|
190
|
+
try {
|
|
191
|
+
this.fire({
|
|
192
|
+
kind: 'holder-dead',
|
|
193
|
+
dedupeKey: `spend-holder-dead:${keyEpoch}`,
|
|
194
|
+
text: `🚨 Paid routing is frozen — the metered-lease machine '${holderNickname}' is offline past the ` +
|
|
195
|
+
`mesh-death threshold while paid doors are live. Free doors still serve. Reclaim/re-designate from ` +
|
|
196
|
+
`the dashboard Spend tab (PIN) when you've confirmed the machine is really gone.`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// @silent-fallback-ok: the surviving-voice alert is best-effort — pool
|
|
201
|
+
// observation re-fires it on its next tick if this dispatch failed.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** recon-drift emit surface (Layer 1c feeds this next PR — Amendment 1 wording). */
|
|
205
|
+
onReconciliationDrift(keyRef, door, driftPct) {
|
|
206
|
+
try {
|
|
207
|
+
const bucket = driftPct >= 0 ? Math.floor(driftPct / 10) : Math.ceil(driftPct / 10);
|
|
208
|
+
this.fire({
|
|
209
|
+
kind: 'recon-drift',
|
|
210
|
+
dedupeKey: `spend-recon-drift:${keyRef}:${door}:${bucket}`,
|
|
211
|
+
text: `🧾 ${door} reports ~${Math.abs(Math.round(driftPct))}% ${driftPct > 0 ? 'more' : 'less'} than we booked ` +
|
|
212
|
+
`for key '${keyRef}' — your reviewed price may be stale; consider promoting the observed price (PIN).`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// @silent-fallback-ok: reporting-side observability; the sweep re-fires next pass.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=SpendAlertEmitters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpendAlertEmitters.js","sourceRoot":"","sources":["../../src/core/SpendAlertEmitters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendAlertResolver — the MINIMAL topic-resolver foundation + the two
|
|
3
|
+
* Increment-B alert kinds (stale-price / observed-drift) of the Routing Control
|
|
4
|
+
* Room (docs/specs/routing-control-room-spend-alerts.md, §Surface 2 Alerts,
|
|
5
|
+
* C5-5 sequencing: stale pricing changes money ADMISSION behavior, so its alarm
|
|
6
|
+
* ships WITH the money increment; the full channel abstraction + the remaining
|
|
7
|
+
* emitter set are Increment C).
|
|
8
|
+
*
|
|
9
|
+
* Resolution LADDER (first hit wins; only the last rung creates):
|
|
10
|
+
* 1. Operator-configured `routingSpend.alerts.telegramTopicId` — that id IS the
|
|
11
|
+
* topic; nothing is ever created.
|
|
12
|
+
* 2. The machine-local persisted record (the `persistLifelineTopicId` pattern).
|
|
13
|
+
* (The pool-published half of rung 2 rides the machine-registry heartbeat
|
|
14
|
+
* surface and lands with Increment C — tracked: CMT-1929.)
|
|
15
|
+
* 3. Create ONCE — ONLY when this machine is the CONFIRMED serving-lease holder
|
|
16
|
+
* (fenced; lease re-confirmed within the same bounded staleness window the
|
|
17
|
+
* money gate uses) — as a bounded create-once SYSTEM topic. In-process
|
|
18
|
+
* single-flight guards a burst of first-alerts.
|
|
19
|
+
* Anyone who cannot resolve/create falls back to the LIFELINE — fail toward
|
|
20
|
+
* the lifeline, never toward a possible duplicate.
|
|
21
|
+
*
|
|
22
|
+
* Emission discipline: edge-triggered per dedupe key (an alert latches only on
|
|
23
|
+
* a CONFIRMED send, so a transient failure stays eligible for re-send);
|
|
24
|
+
* metadata-only text (door / age-days / threshold — NEVER a provider body or a
|
|
25
|
+
* key-shaped substring).
|
|
26
|
+
*/
|
|
27
|
+
export interface SpendAlert {
|
|
28
|
+
/** Informational lane in B (stale-price / observed-drift). Money-critical lanes land in C. */
|
|
29
|
+
kind: 'stale-price' | 'observed-drift';
|
|
30
|
+
dedupeKey: string;
|
|
31
|
+
text: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SpendAlertResolverDeps {
|
|
34
|
+
/** Rung 1: the operator-configured topic id (config-read, live). */
|
|
35
|
+
configuredTopicId: () => number | undefined;
|
|
36
|
+
/** Rung 2 (machine-local half): read/persist the auto-created id. */
|
|
37
|
+
readPersistedTopicId: () => number | undefined;
|
|
38
|
+
persistTopicId: (topicId: number) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Rung 2 (POOL-PUBLISHED half, Increment C — FD-6): read the id any pool
|
|
41
|
+
* machine published on the replicated machine registry, and publish this
|
|
42
|
+
* machine's created id there — so a FUTURE serving-lease holder INHERITS the
|
|
43
|
+
* id instead of re-creating (Telegram mints a fresh id per createForumTopic,
|
|
44
|
+
* so last-writer-wins can never merge two creations). Optional: absent on
|
|
45
|
+
* single-machine installs (the machine-local record suffices there).
|
|
46
|
+
*/
|
|
47
|
+
readPoolPublishedTopicId?: () => number | undefined;
|
|
48
|
+
publishTopicId?: (topicId: number) => void;
|
|
49
|
+
/** Rung 3 gate: ms since the serving lease was POSITIVELY re-confirmed (null = not holder / unconfirmed). */
|
|
50
|
+
servingLeaseConfirmedAgoMs: () => number | null;
|
|
51
|
+
/** Create the bounded create-once system topic; returns its id. */
|
|
52
|
+
createTopic: () => Promise<number>;
|
|
53
|
+
/** Deliver one message into a topic; resolves true on CONFIRMED send. */
|
|
54
|
+
sendToTopic: (topicId: number, text: string) => Promise<boolean>;
|
|
55
|
+
/** The lifeline fallback topic id (always-existing system topic), if resolvable. */
|
|
56
|
+
lifelineTopicId: () => number | undefined;
|
|
57
|
+
/** Scrubbed observability sink (jsonl line per emission/decision). */
|
|
58
|
+
audit?: (entry: Record<string, unknown>) => void;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/** The same bounded staleness window the money gate uses (N-2). */
|
|
62
|
+
export declare const SERVING_LEASE_WINDOW_MS = 60000;
|
|
63
|
+
export declare class SpendAlertResolver {
|
|
64
|
+
private readonly d;
|
|
65
|
+
private readonly now;
|
|
66
|
+
/** Edge-trigger latch: dedupeKey → last CONFIRMED emission ms. */
|
|
67
|
+
private latched;
|
|
68
|
+
/** In-process single-flight for topic creation. */
|
|
69
|
+
private creating;
|
|
70
|
+
constructor(deps: SpendAlertResolverDeps);
|
|
71
|
+
/** Rung-1 accessor for the channel's repoint detection (G5) — never resolves/creates. */
|
|
72
|
+
configuredTopicIdForRepointCheck(): number | undefined;
|
|
73
|
+
/** Resolve the dedicated topic id via the ladder — or undefined (caller falls back to lifeline). */
|
|
74
|
+
resolveTopicId(): Promise<number | undefined>;
|
|
75
|
+
/**
|
|
76
|
+
* Emit one alert: dedicated topic first, lifeline on ANY failure (unset,
|
|
77
|
+
* create-refused, send-failed). Edge-triggered — a dedupe key that already
|
|
78
|
+
* emitted (confirmed) within `reArmMs` is suppressed; the latch only sets on
|
|
79
|
+
* CONFIRMED delivery, so a transient failure stays eligible.
|
|
80
|
+
*/
|
|
81
|
+
emit(alert: SpendAlert, reArmMs?: number): Promise<'sent' | 'sent-lifeline' | 'suppressed' | 'failed'>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build the two Increment-B alert kinds from the price authority's staleness /
|
|
85
|
+
* drift view for the metered doors. Metadata-only wording (S-F7).
|
|
86
|
+
*/
|
|
87
|
+
export declare function buildStalePriceAlert(door: string, ageDays: number, slaDays: number): SpendAlert;
|
|
88
|
+
export declare function buildObservedDriftAlert(door: string, modelId: string, driftPct: number): SpendAlert;
|
|
89
|
+
//# sourceMappingURL=SpendAlertResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpendAlertResolver.d.ts","sourceRoot":"","sources":["../../src/core/SpendAlertResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,UAAU;IACzB,8FAA8F;IAC9F,IAAI,EAAE,aAAa,GAAG,gBAAgB,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,iBAAiB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC5C,qEAAqE;IACrE,oBAAoB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC/C,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;;;;;;OAOG;IACH,wBAAwB,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACpD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,6GAA6G;IAC7G,0BAA0B,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAChD,mEAAmE;IACnE,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,yEAAyE;IACzE,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,oFAAoF;IACpF,eAAe,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC1C,sEAAsE;IACtE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACjD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAyB;IAC3C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,kEAAkE;IAClE,OAAO,CAAC,OAAO,CAA6B;IAC5C,mDAAmD;IACnD,OAAO,CAAC,QAAQ,CAA4C;gBAEhD,IAAI,EAAE,sBAAsB;IAKxC,yFAAyF;IACzF,gCAAgC,IAAI,MAAM,GAAG,SAAS;IAKtD,oGAAoG;IAC9F,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAyDnD;;;;;OAKG;IACG,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,SAAsB,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,GAAG,YAAY,GAAG,QAAQ,CAAC;CAmC1H;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAS/F;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CASnG"}
|