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.
Files changed (77) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +41 -15
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/MachineIdentity.d.ts +13 -0
  5. package/dist/core/MachineIdentity.d.ts.map +1 -1
  6. package/dist/core/MachineIdentity.js +31 -0
  7. package/dist/core/MachineIdentity.js.map +1 -1
  8. package/dist/core/MeteredSpendGate.d.ts +127 -0
  9. package/dist/core/MeteredSpendGate.d.ts.map +1 -0
  10. package/dist/core/MeteredSpendGate.js +248 -0
  11. package/dist/core/MeteredSpendGate.js.map +1 -0
  12. package/dist/core/MeteredSpendLedger.d.ts +154 -0
  13. package/dist/core/MeteredSpendLedger.d.ts.map +1 -0
  14. package/dist/core/MeteredSpendLedger.js +434 -0
  15. package/dist/core/MeteredSpendLedger.js.map +1 -0
  16. package/dist/core/PinAttemptStore.d.ts +35 -0
  17. package/dist/core/PinAttemptStore.d.ts.map +1 -0
  18. package/dist/core/PinAttemptStore.js +85 -0
  19. package/dist/core/PinAttemptStore.js.map +1 -0
  20. package/dist/core/PostUpdateMigrator.d.ts +7 -0
  21. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  22. package/dist/core/PostUpdateMigrator.js +24 -0
  23. package/dist/core/PostUpdateMigrator.js.map +1 -1
  24. package/dist/core/RenderedPlanStore.d.ts +61 -0
  25. package/dist/core/RenderedPlanStore.d.ts.map +1 -0
  26. package/dist/core/RenderedPlanStore.js +92 -0
  27. package/dist/core/RenderedPlanStore.js.map +1 -0
  28. package/dist/core/RoutingSpendCapsStore.d.ts +113 -0
  29. package/dist/core/RoutingSpendCapsStore.d.ts.map +1 -0
  30. package/dist/core/RoutingSpendCapsStore.js +241 -0
  31. package/dist/core/RoutingSpendCapsStore.js.map +1 -0
  32. package/dist/core/SpendAlertDispatcher.d.ts +94 -0
  33. package/dist/core/SpendAlertDispatcher.d.ts.map +1 -0
  34. package/dist/core/SpendAlertDispatcher.js +168 -0
  35. package/dist/core/SpendAlertDispatcher.js.map +1 -0
  36. package/dist/core/SpendAlertEmitters.d.ts +118 -0
  37. package/dist/core/SpendAlertEmitters.d.ts.map +1 -0
  38. package/dist/core/SpendAlertEmitters.js +220 -0
  39. package/dist/core/SpendAlertEmitters.js.map +1 -0
  40. package/dist/core/SpendAlertResolver.d.ts +89 -0
  41. package/dist/core/SpendAlertResolver.d.ts.map +1 -0
  42. package/dist/core/SpendAlertResolver.js +175 -0
  43. package/dist/core/SpendAlertResolver.js.map +1 -0
  44. package/dist/core/TelegramSpendTopicChannel.d.ts +50 -0
  45. package/dist/core/TelegramSpendTopicChannel.d.ts.map +1 -0
  46. package/dist/core/TelegramSpendTopicChannel.js +106 -0
  47. package/dist/core/TelegramSpendTopicChannel.js.map +1 -0
  48. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  49. package/dist/core/WriteDomainRegistry.js +17 -0
  50. package/dist/core/WriteDomainRegistry.js.map +1 -1
  51. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  52. package/dist/core/devGatedFeatures.js +9 -0
  53. package/dist/core/devGatedFeatures.js.map +1 -1
  54. package/dist/core/routingSpendView.d.ts +10 -2
  55. package/dist/core/routingSpendView.d.ts.map +1 -1
  56. package/dist/core/routingSpendView.js +18 -10
  57. package/dist/core/routingSpendView.js.map +1 -1
  58. package/dist/core/types.d.ts +45 -3
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/core/types.js.map +1 -1
  61. package/dist/server/AgentServer.d.ts +25 -0
  62. package/dist/server/AgentServer.d.ts.map +1 -1
  63. package/dist/server/AgentServer.js +304 -0
  64. package/dist/server/AgentServer.js.map +1 -1
  65. package/dist/server/routes.d.ts +10 -0
  66. package/dist/server/routes.d.ts.map +1 -1
  67. package/dist/server/routes.js +196 -4
  68. package/dist/server/routes.js.map +1 -1
  69. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  70. package/dist/testing/selfActionRegistry.js +206 -0
  71. package/dist/testing/selfActionRegistry.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/data/builtin-manifest.json +64 -64
  74. package/upgrades/1.3.783.md +66 -0
  75. package/upgrades/1.3.784.md +59 -0
  76. package/upgrades/side-effects/routing-spend-increment-b.md +44 -0
  77. 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,mEAAmE;AACnE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,GAAG,CAAU,CAAC;AAE3D,sFAAsF;AACtF,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAElD,wFAAwF;AACxF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAC1C,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACvD,yEAAyE;AACzE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AACvD,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAqC9D,SAAS,MAAM,CAAC,EAAU;IACxB,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,OAAO,kBAAkB;IACZ,UAAU,CAAuB;IACjC,SAAS,CAAS;IAClB,GAAG,CAAe;IAEnC,oGAAoG;IAC5F,QAAQ,GAAG,IAAI,GAAG,EAAgG,CAAC;IAC3H,wFAAwF;IAChF,cAAc,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAEhD,YAAY,IAA+B;QACzC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACK,IAAI,CAAC,KAAsD;QACjE,IAAI,CAAC;YACH,KAAK,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,iDAAiD;QACnD,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,WAAW,CAAC,EAAa;QACvB,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACxB,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;YAC5B,CAAC;iBAAM,IAAI,EAAE,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;gBACxC,IAAI,CAAC,IAAI,CAAC;oBACR,IAAI,EAAE,SAAS;oBACf,SAAS,EAAE,iBAAiB,EAAE,CAAC,MAAM,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE;oBAC1E,IAAI,EACF,wDAAwD,EAAE,CAAC,MAAM,IAAI,SAAS,YAAY,EAAE,CAAC,MAAM,IAAI;wBACvG,iFAAiF;wBACjF,4FAA4F;iBAC/F,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,4EAA4E;QAC9E,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,EAAkB;QACzC,MAAM,MAAM,GAA6F;YACvG,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE;YACpG,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE;SACxG,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;gBAAE,SAAS;YAC3B,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC;YACjC,KAAK,MAAM,SAAS,IAAI,uBAAuB,EAAE,CAAC;gBAChD,IAAI,IAAI,IAAI,SAAS,EAAE,CAAC;oBACtB,0EAA0E;oBAC1E,IAAI,CAAC,IAAI,CAAC;wBACR,IAAI,EAAE,cAAc;wBACpB,SAAS,EAAE,kBAAkB,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,IAAI,SAAS,IAAI,CAAC,CAAC,MAAM,EAAE;wBAC9E,IAAI,EACF,WAAW,EAAE,CAAC,MAAM,WAAW,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,OAAO,OAAO;4BACjF,KAAK,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc;qBACpE,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,IAA0B;QAC1C,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACpB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,IAAI,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACvJ,OAAO;YACT,CAAC;YACD,IAAI,IAAI,CAAC,cAAc;gBAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,2EAA2E;YAC3E,+DAA+D;QACjE,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,KAAa,EAAE,WAAmB;QACzD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,2BAA2B,CAAC,CAAC;QAC/D,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAChC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,IAAI,EAAE,EAAE,CAAC;YACrF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/B,CAAC;QACD,8EAA8E;QAC9E,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,EAAE,CAAC,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,wBAAwB,CAAC,CAAC;QACrF,MAAM,QAAQ,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,IAAI,wBAAwB,CAAC;QAEnE,iFAAiF;QACjF,IAAI,EAAE,CAAC,QAAQ,IAAI,WAAW;YAAE,OAAO,CAAC,oDAAoD;QAC5F,IAAI,KAAK,GAAG,EAAE,CAAC,eAAe;YAAE,OAAO,CAAC,qCAAqC;QAC7E,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC;QACjB,EAAE,CAAC,eAAe,GAAG,KAAK,GAAG,yBAAyB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;QAEtF,IAAI,CAAC,IAAI,CAAC;YACR,IAAI,EAAE,WAAW;YACjB,SAAS,EAAE,mBAAmB,IAAI,CAAC,SAAS,IAAI,KAAK,IAAI,MAAM,IAAI,EAAE,CAAC,QAAQ,EAAE;YAChF,IAAI,EAAE,QAAQ;gBACZ,CAAC,CAAC,qBAAqB,KAAK,uBAAuB,EAAE,CAAC,WAAW,CAAC,MAAM,wCAAwC;oBAC9G,oGAAoG;gBACtG,CAAC,CAAC,qBAAqB,KAAK,oEAAoE;oBAC9F,YAAY,EAAE,CAAC,QAAQ,IAAI,WAAW,0EAA0E;YACpH,+EAA+E;YAC/E,qDAAqD;YACrD,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjD,CAAC,CAAC;IACL,CAAC;IAEO,gBAAgB;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,KAAK,IAAI;YAAE,IAAI,CAAC,cAAc,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAChF,IAAI,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,KAAK,+BAA+B,EAAE,CAAC;YAClE,+EAA+E;YAC/E,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,gBAAgB;gBACtB,SAAS,EAAE,wBAAwB,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE;gBAC3D,IAAI,EACF,oCAAoC,IAAI,CAAC,cAAc,CAAC,KAAK,mCAAmC;oBAChG,YAAY,+BAA+B,4DAA4D;oBACvG,0DAA0D;aAC7D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,wBAAwB,CAAC,cAAsB,EAAE,QAAgB;QAC/D,IAAI,CAAC;YACH,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,qBAAqB,QAAQ,EAAE;gBAC1C,IAAI,EACF,0DAA0D,cAAc,wBAAwB;oBAChG,oGAAoG;oBACpG,iFAAiF;aACpF,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,oEAAoE;QACtE,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,qBAAqB,CAAC,MAAc,EAAE,IAAY,EAAE,QAAgB;QAClE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAC;YACpF,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,qBAAqB,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE;gBAC1D,IAAI,EACF,MAAM,IAAI,aAAa,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,KAAK,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,kBAAkB;oBAC1G,YAAY,MAAM,oFAAoF;aACzG,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,mFAAmF;QACrF,CAAC;IACH,CAAC;CACF"}
@@ -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"}