instar 1.3.783 → 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 +8 -0
- package/dist/core/MeteredSpendGate.d.ts.map +1 -1
- package/dist/core/MeteredSpendGate.js +20 -0
- package/dist/core/MeteredSpendGate.js.map +1 -1
- 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 +12 -0
- package/dist/core/SpendAlertResolver.d.ts.map +1 -1
- package/dist/core/SpendAlertResolver.js +34 -0
- package/dist/core/SpendAlertResolver.js.map +1 -1
- 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/types.d.ts +22 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/server/AgentServer.d.ts +16 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +173 -46
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +128 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.784.md +59 -0
- package/upgrades/side-effects/routing-spend-increment-c.md +44 -0
|
@@ -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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"}
|
|
@@ -36,6 +36,16 @@ export interface SpendAlertResolverDeps {
|
|
|
36
36
|
/** Rung 2 (machine-local half): read/persist the auto-created id. */
|
|
37
37
|
readPersistedTopicId: () => number | undefined;
|
|
38
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;
|
|
39
49
|
/** Rung 3 gate: ms since the serving lease was POSITIVELY re-confirmed (null = not holder / unconfirmed). */
|
|
40
50
|
servingLeaseConfirmedAgoMs: () => number | null;
|
|
41
51
|
/** Create the bounded create-once system topic; returns its id. */
|
|
@@ -58,6 +68,8 @@ export declare class SpendAlertResolver {
|
|
|
58
68
|
/** In-process single-flight for topic creation. */
|
|
59
69
|
private creating;
|
|
60
70
|
constructor(deps: SpendAlertResolverDeps);
|
|
71
|
+
/** Rung-1 accessor for the channel's repoint detection (G5) — never resolves/creates. */
|
|
72
|
+
configuredTopicIdForRepointCheck(): number | undefined;
|
|
61
73
|
/** Resolve the dedicated topic id via the ladder — or undefined (caller falls back to lifeline). */
|
|
62
74
|
resolveTopicId(): Promise<number | undefined>;
|
|
63
75
|
/**
|
|
@@ -1 +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,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,oGAAoG;IAC9F,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;
|
|
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"}
|
|
@@ -37,6 +37,11 @@ export class SpendAlertResolver {
|
|
|
37
37
|
this.d = deps;
|
|
38
38
|
this.now = deps.now ?? (() => Date.now());
|
|
39
39
|
}
|
|
40
|
+
/** Rung-1 accessor for the channel's repoint detection (G5) — never resolves/creates. */
|
|
41
|
+
configuredTopicIdForRepointCheck() {
|
|
42
|
+
const configured = this.d.configuredTopicId();
|
|
43
|
+
return typeof configured === 'number' && Number.isFinite(configured) ? configured : undefined;
|
|
44
|
+
}
|
|
40
45
|
/** Resolve the dedicated topic id via the ladder — or undefined (caller falls back to lifeline). */
|
|
41
46
|
async resolveTopicId() {
|
|
42
47
|
const configured = this.d.configuredTopicId();
|
|
@@ -45,6 +50,27 @@ export class SpendAlertResolver {
|
|
|
45
50
|
const persisted = this.d.readPersistedTopicId();
|
|
46
51
|
if (typeof persisted === 'number' && Number.isFinite(persisted))
|
|
47
52
|
return persisted;
|
|
53
|
+
// Rung 2 pool half (FD-6): an id ANY pool machine published — a new serving
|
|
54
|
+
// holder inherits instead of re-creating. Adopted locally so the next
|
|
55
|
+
// resolve is disk-fast even if the registry read degrades later.
|
|
56
|
+
try {
|
|
57
|
+
const pooled = this.d.readPoolPublishedTopicId?.();
|
|
58
|
+
if (typeof pooled === 'number' && Number.isFinite(pooled)) {
|
|
59
|
+
try {
|
|
60
|
+
this.d.persistTopicId(pooled);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// @silent-fallback-ok: local adoption is an optimization; the pool
|
|
64
|
+
// read stays the source next resolve.
|
|
65
|
+
}
|
|
66
|
+
this.d.audit?.({ decision: 'adopted-pool-published', topicId: pooled });
|
|
67
|
+
return pooled;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// @silent-fallback-ok: a degraded registry read falls through the ladder —
|
|
72
|
+
// creation stays fenced below, so a miss can never mint a duplicate.
|
|
73
|
+
}
|
|
48
74
|
// Rung 3: create once — serving-lease-holder-only, fenced, single-flight.
|
|
49
75
|
const confirmedAgo = this.d.servingLeaseConfirmedAgoMs();
|
|
50
76
|
if (confirmedAgo === null || confirmedAgo > SERVING_LEASE_WINDOW_MS) {
|
|
@@ -56,6 +82,14 @@ export class SpendAlertResolver {
|
|
|
56
82
|
try {
|
|
57
83
|
const id = await this.d.createTopic();
|
|
58
84
|
this.d.persistTopicId(id);
|
|
85
|
+
try {
|
|
86
|
+
this.d.publishTopicId?.(id); // rung-2 pool half: peers + future holders inherit
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// @silent-fallback-ok: a failed publish leaves the machine-local
|
|
90
|
+
// record authoritative; the next holder falls back to the lifeline
|
|
91
|
+
// rather than duplicating (creation is lease-fenced).
|
|
92
|
+
}
|
|
59
93
|
this.d.audit?.({ decision: 'created', topicId: id });
|
|
60
94
|
return id;
|
|
61
95
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpendAlertResolver.js","sourceRoot":"","sources":["../../src/core/SpendAlertResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;
|
|
1
|
+
{"version":3,"file":"SpendAlertResolver.js","sourceRoot":"","sources":["../../src/core/SpendAlertResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelegramSpendTopicChannel — the Increment-C concrete AlertChannel (FD-6):
|
|
3
|
+
* delivers each already-deduped/coalesced spend alert as a MESSAGE into the
|
|
4
|
+
* ONE dedicated "💰 Routing & Spend Alerts" topic, resolved through the
|
|
5
|
+
* SpendAlertResolver ladder (operator-configured id → pool-published/persisted
|
|
6
|
+
* record → fenced serving-lease-holder-only create-once), with the LIFELINE as
|
|
7
|
+
* the single named emergency fallback.
|
|
8
|
+
*
|
|
9
|
+
* Money-critical deliveries (G5 hardened) prefer the DURABLE relay
|
|
10
|
+
* (PendingRelayStore + DeliveryFailureSentinel — delivery-robustness Layers
|
|
11
|
+
* 2/3) via the injected enqueue: retry-until-delivered instead of
|
|
12
|
+
* fire-and-forget, and the dispatcher's edge latch only sets on a CONFIRMED /
|
|
13
|
+
* durably-queued outcome. When no durable enqueue is wired (single-process
|
|
14
|
+
* tests, lean installs) it degrades to direct send + lifeline fallback.
|
|
15
|
+
*
|
|
16
|
+
* Repoint audibility (G5): when the operator-configured topic id CHANGES, the
|
|
17
|
+
* channel posts a one-line "spend alerts now route to <topic>" confirmation
|
|
18
|
+
* into BOTH the old topic (if still resolvable) and the new one, and audits the
|
|
19
|
+
* repoint — a Bearer-level actor cannot SILENTLY redirect the operator's money
|
|
20
|
+
* alerts (the knob still cannot touch money admission).
|
|
21
|
+
*/
|
|
22
|
+
import type { AlertChannel } from './SpendAlertDispatcher.js';
|
|
23
|
+
import type { SpendAlertResolver } from './SpendAlertResolver.js';
|
|
24
|
+
export interface TelegramSpendTopicChannelDeps {
|
|
25
|
+
resolver: SpendAlertResolver;
|
|
26
|
+
/** Direct Telegram send; resolves true on confirmed delivery. */
|
|
27
|
+
sendToTopic: (topicId: number, text: string) => Promise<boolean>;
|
|
28
|
+
/** The always-existing system topic (emergency fallback). */
|
|
29
|
+
lifelineTopicId: () => number | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* OPTIONAL durable-relay enqueue (PendingRelayStore). Returns true when the
|
|
32
|
+
* message is durably queued (retry-until-delivered — counts as handled).
|
|
33
|
+
*/
|
|
34
|
+
enqueueDurable?: (topicId: number, text: string) => boolean;
|
|
35
|
+
/** Scrubbed observability sink (shares the dispatcher's jsonl). */
|
|
36
|
+
audit?: (entry: Record<string, unknown>) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare class TelegramSpendTopicChannel implements AlertChannel {
|
|
39
|
+
readonly id = "telegram";
|
|
40
|
+
private readonly d;
|
|
41
|
+
/** The configured id seen last delivery — repoint detection (G5). */
|
|
42
|
+
private lastConfiguredId;
|
|
43
|
+
constructor(deps: TelegramSpendTopicChannelDeps);
|
|
44
|
+
deliver(text: string, moneyCritical: boolean): Promise<'sent' | 'sent-lifeline' | 'queued-durable' | 'failed'>;
|
|
45
|
+
/** G5: a changed operator-configured id is made AUDIBLE, never silent. */
|
|
46
|
+
private announceRepointIfChanged;
|
|
47
|
+
}
|
|
48
|
+
/** Deterministic delivery id for the durable relay (idempotent enqueue). */
|
|
49
|
+
export declare function spendAlertDeliveryId(topicId: number, text: string): string;
|
|
50
|
+
//# sourceMappingURL=TelegramSpendTopicChannel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelegramSpendTopicChannel.d.ts","sourceRoot":"","sources":["../../src/core/TelegramSpendTopicChannel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,iEAAiE;IACjE,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,6DAA6D;IAC7D,eAAe,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5D,mEAAmE;IACnE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAClD;AAED,qBAAa,yBAA0B,YAAW,YAAY;IAC5D,QAAQ,CAAC,EAAE,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAgC;IAClD,qEAAqE;IACrE,OAAO,CAAC,gBAAgB,CAAqB;gBAEjC,IAAI,EAAE,6BAA6B;IAIzC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,GAAG,gBAAgB,GAAG,QAAQ,CAAC;IA4CpH,0EAA0E;YAC5D,wBAAwB;CAgBvC;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1E"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelegramSpendTopicChannel — the Increment-C concrete AlertChannel (FD-6):
|
|
3
|
+
* delivers each already-deduped/coalesced spend alert as a MESSAGE into the
|
|
4
|
+
* ONE dedicated "💰 Routing & Spend Alerts" topic, resolved through the
|
|
5
|
+
* SpendAlertResolver ladder (operator-configured id → pool-published/persisted
|
|
6
|
+
* record → fenced serving-lease-holder-only create-once), with the LIFELINE as
|
|
7
|
+
* the single named emergency fallback.
|
|
8
|
+
*
|
|
9
|
+
* Money-critical deliveries (G5 hardened) prefer the DURABLE relay
|
|
10
|
+
* (PendingRelayStore + DeliveryFailureSentinel — delivery-robustness Layers
|
|
11
|
+
* 2/3) via the injected enqueue: retry-until-delivered instead of
|
|
12
|
+
* fire-and-forget, and the dispatcher's edge latch only sets on a CONFIRMED /
|
|
13
|
+
* durably-queued outcome. When no durable enqueue is wired (single-process
|
|
14
|
+
* tests, lean installs) it degrades to direct send + lifeline fallback.
|
|
15
|
+
*
|
|
16
|
+
* Repoint audibility (G5): when the operator-configured topic id CHANGES, the
|
|
17
|
+
* channel posts a one-line "spend alerts now route to <topic>" confirmation
|
|
18
|
+
* into BOTH the old topic (if still resolvable) and the new one, and audits the
|
|
19
|
+
* repoint — a Bearer-level actor cannot SILENTLY redirect the operator's money
|
|
20
|
+
* alerts (the knob still cannot touch money admission).
|
|
21
|
+
*/
|
|
22
|
+
import crypto from 'node:crypto';
|
|
23
|
+
export class TelegramSpendTopicChannel {
|
|
24
|
+
id = 'telegram';
|
|
25
|
+
d;
|
|
26
|
+
/** The configured id seen last delivery — repoint detection (G5). */
|
|
27
|
+
lastConfiguredId;
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
this.d = deps;
|
|
30
|
+
}
|
|
31
|
+
async deliver(text, moneyCritical) {
|
|
32
|
+
await this.announceRepointIfChanged();
|
|
33
|
+
const topicId = await this.d.resolver.resolveTopicId();
|
|
34
|
+
if (topicId !== undefined) {
|
|
35
|
+
// Money-critical: durable relay FIRST (retry-until-delivered).
|
|
36
|
+
if (moneyCritical && this.d.enqueueDurable) {
|
|
37
|
+
try {
|
|
38
|
+
if (this.d.enqueueDurable(topicId, text)) {
|
|
39
|
+
this.d.audit?.({ channel: this.id, outcome: 'queued-durable', topicId });
|
|
40
|
+
return 'queued-durable';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// @silent-fallback-ok: a relay enqueue failure falls through to the
|
|
45
|
+
// direct send + lifeline ladder below — delivery degrades, never dies.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
if (await this.d.sendToTopic(topicId, text))
|
|
50
|
+
return 'sent';
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// @silent-fallback-ok: a set-but-wrong/deleted topic id is a FALLBACK
|
|
54
|
+
// case, not a black hole — the lifeline path below is the designed
|
|
55
|
+
// continuation (G5), and the terminal outcome is audited by the caller.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const lifeline = this.d.lifelineTopicId();
|
|
59
|
+
if (lifeline !== undefined) {
|
|
60
|
+
// Money-critical: even the lifeline leg prefers the durable relay.
|
|
61
|
+
if (moneyCritical && this.d.enqueueDurable) {
|
|
62
|
+
try {
|
|
63
|
+
if (this.d.enqueueDurable(lifeline, text))
|
|
64
|
+
return 'queued-durable';
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// @silent-fallback-ok: same degradation ladder as above.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (await this.d.sendToTopic(lifeline, text))
|
|
72
|
+
return 'sent-lifeline';
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// @silent-fallback-ok: terminal 'failed' is returned below and the
|
|
76
|
+
// dispatcher leaves the alert UN-latched (eligible for re-send).
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return 'failed';
|
|
80
|
+
}
|
|
81
|
+
/** G5: a changed operator-configured id is made AUDIBLE, never silent. */
|
|
82
|
+
async announceRepointIfChanged() {
|
|
83
|
+
let configured;
|
|
84
|
+
try {
|
|
85
|
+
configured = this.d.resolver.configuredTopicIdForRepointCheck();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return; // @silent-fallback-ok: repoint detection is best-effort observability
|
|
89
|
+
}
|
|
90
|
+
if (configured === this.lastConfiguredId)
|
|
91
|
+
return;
|
|
92
|
+
const old = this.lastConfiguredId;
|
|
93
|
+
this.lastConfiguredId = configured;
|
|
94
|
+
if (old === undefined || configured === undefined)
|
|
95
|
+
return; // first observation / unset — nothing to announce
|
|
96
|
+
const note = `🔀 Spend alerts now route to topic ${configured} (was ${old}). If you didn't change routingSpend.alerts.telegramTopicId, check your config.`;
|
|
97
|
+
this.d.audit?.({ channel: this.id, outcome: 'repoint', from: old, to: configured });
|
|
98
|
+
// Both topics, best-effort — the announcement must never block a delivery.
|
|
99
|
+
await Promise.allSettled([this.d.sendToTopic(old, note), this.d.sendToTopic(configured, note)]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Deterministic delivery id for the durable relay (idempotent enqueue). */
|
|
103
|
+
export function spendAlertDeliveryId(topicId, text) {
|
|
104
|
+
return 'spend-alert:' + crypto.createHash('sha256').update(`${topicId}|${text}`).digest('hex').slice(0, 24);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=TelegramSpendTopicChannel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelegramSpendTopicChannel.js","sourceRoot":"","sources":["../../src/core/TelegramSpendTopicChannel.ts"],"names":[],"mappings":"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"}
|