instar 1.3.783 → 1.3.785
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/routingSpendView.d.ts +16 -0
- package/dist/core/routingSpendView.d.ts.map +1 -1
- package/dist/core/routingSpendView.js +23 -0
- package/dist/core/routingSpendView.js.map +1 -1
- 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/monitoring/FeatureMetricsLedger.d.ts +6 -0
- package/dist/monitoring/FeatureMetricsLedger.d.ts.map +1 -1
- package/dist/monitoring/FeatureMetricsLedger.js +7 -2
- package/dist/monitoring/FeatureMetricsLedger.js.map +1 -1
- package/dist/monitoring/ProviderCostReportStore.d.ts +138 -0
- package/dist/monitoring/ProviderCostReportStore.d.ts.map +1 -0
- package/dist/monitoring/ProviderCostReportStore.js +0 -0
- package/dist/monitoring/ProviderCostReportStore.js.map +1 -0
- package/dist/monitoring/ProviderReconciliationSweep.d.ts +72 -0
- package/dist/monitoring/ProviderReconciliationSweep.d.ts.map +1 -0
- package/dist/monitoring/ProviderReconciliationSweep.js +97 -0
- package/dist/monitoring/ProviderReconciliationSweep.js.map +1 -0
- package/dist/server/AgentServer.d.ts +19 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +251 -46
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +40 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +166 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/data/state-coherence-registry.json +1241 -1223
- package/upgrades/1.3.784.md +59 -0
- package/upgrades/1.3.785.md +56 -0
- package/upgrades/side-effects/routing-spend-increment-1c.md +44 -0
- package/upgrades/side-effects/routing-spend-increment-c.md +44 -0
|
@@ -91,6 +91,14 @@ import { MeteredSpendGate } from '../core/MeteredSpendGate.js';
|
|
|
91
91
|
import { RenderedPlanStore } from '../core/RenderedPlanStore.js';
|
|
92
92
|
import { PinAttemptStore } from '../core/PinAttemptStore.js';
|
|
93
93
|
import { SpendAlertResolver, buildStalePriceAlert } from '../core/SpendAlertResolver.js';
|
|
94
|
+
import { SpendAlertDispatcher } from '../core/SpendAlertDispatcher.js';
|
|
95
|
+
import { TelegramSpendTopicChannel, spendAlertDeliveryId } from '../core/TelegramSpendTopicChannel.js';
|
|
96
|
+
import { SpendAlertEmitters } from '../core/SpendAlertEmitters.js';
|
|
97
|
+
import { MachineIdentityManager } from '../core/MachineIdentity.js';
|
|
98
|
+
import { PendingRelayStore as SpendAlertRelayStore } from '../messaging/pending-relay-store.js';
|
|
99
|
+
import { ProviderCostReportStore } from '../monitoring/ProviderCostReportStore.js';
|
|
100
|
+
import { ProviderReconciliationSweep } from '../monitoring/ProviderReconciliationSweep.js';
|
|
101
|
+
import { meteredKeysFromChains } from '../core/routingSpendView.js';
|
|
94
102
|
import { DEFAULT_FRESHNESS_SLA_DAYS } from '../core/routingPriceAuthority.js';
|
|
95
103
|
import { METERED_ROUTING_DOORS } from '../data/llmBenchCoverage.js';
|
|
96
104
|
import { DEFAULT_METERED_CAPS } from '../core/routingSpendView.js';
|
|
@@ -188,6 +196,14 @@ export class AgentServer {
|
|
|
188
196
|
meteredSweepTimer = null;
|
|
189
197
|
spendAlertResolver = null;
|
|
190
198
|
spendAlertTimer = null;
|
|
199
|
+
/** Increment C alert layer (dryRun-first live-on-dev — FD-16). */
|
|
200
|
+
spendAlertDispatcher = null;
|
|
201
|
+
spendAlertEmitters = null;
|
|
202
|
+
/** Lazily-opened durable relay handle for money-critical alert delivery. */
|
|
203
|
+
spendAlertRelayStore = null;
|
|
204
|
+
/** Layer 1c provider-report store + reconciliation sweep (reporting-only; FD-21). */
|
|
205
|
+
providerCostReportStore = null;
|
|
206
|
+
reconSweepTimer = null;
|
|
191
207
|
featureMetricsPruneTimer = null;
|
|
192
208
|
a2aDeliveryTracker = null;
|
|
193
209
|
tokenLedgerPoller = null;
|
|
@@ -654,6 +670,72 @@ export class AgentServer {
|
|
|
654
670
|
console.warn('[instar] pin-attempt store init failed (non-fatal — in-memory lockout only):', err);
|
|
655
671
|
this.pinAttemptStore = null;
|
|
656
672
|
}
|
|
673
|
+
// ── Layer 1c provider-report store + reconciliation sweep (FD-21 —
|
|
674
|
+
// REPORTING-only; rides the same dev-gated view flag as the summary;
|
|
675
|
+
// structurally excluded from the money gate, which imports nothing here).
|
|
676
|
+
if (routingSpendOn) {
|
|
677
|
+
try {
|
|
678
|
+
const rsCfg = options.config.routingSpend;
|
|
679
|
+
this.providerCostReportStore = new ProviderCostReportStore({
|
|
680
|
+
dbPath: path.join(options.config.stateDir, 'server-data', 'provider-cost-reports.db'),
|
|
681
|
+
retentionDays: rsCfg?.providerReportRetentionDays ?? 400,
|
|
682
|
+
});
|
|
683
|
+
this.providerCostReportStore.prune(); // boot prune (batched — scal-F4)
|
|
684
|
+
const doorToKey = new Map(meteredKeysFromChains().map((m) => [m.door, m.keyRef]));
|
|
685
|
+
const sweep = new ProviderReconciliationSweep({
|
|
686
|
+
store: this.providerCostReportStore,
|
|
687
|
+
// Internal-derived per (keyRef, door): the metered doors' daily token
|
|
688
|
+
// buckets × the as-of reviewed price (the same reporting math the
|
|
689
|
+
// summary uses) — reporting-side reads only, never the money mutex.
|
|
690
|
+
internalDerivedUsd: (sinceMs, untilMs) => {
|
|
691
|
+
const ledger = this.featureMetricsLedger;
|
|
692
|
+
const prices = this.routingPriceAuthority;
|
|
693
|
+
if (!ledger || !prices)
|
|
694
|
+
return [];
|
|
695
|
+
prices.reloadIfChanged();
|
|
696
|
+
const sinceDays = Math.max(1, Math.ceil((untilMs - sinceMs) / 86_400_000) + 1);
|
|
697
|
+
const buckets = ledger.spendTokenRollupDaily({ sinceDays });
|
|
698
|
+
const agg = new Map();
|
|
699
|
+
for (const b of buckets) {
|
|
700
|
+
const keyRef = doorToKey.get(b.door);
|
|
701
|
+
if (!keyRef)
|
|
702
|
+
continue; // metered doors only
|
|
703
|
+
if (b.bucketStartMs < sinceMs - 86_400_000 || b.bucketStartMs > untilMs)
|
|
704
|
+
continue;
|
|
705
|
+
const res = prices.resolve(b.door, b.modelId, b.bucketStartMs);
|
|
706
|
+
const cost = prices.reportingCost(res, b.tokensIn, b.tokensOut, b.tokensCached);
|
|
707
|
+
const e = agg.get(`${keyRef} ${b.door}`) ?? { keyRef, door: b.door, internalUsd: 0 };
|
|
708
|
+
e.internalUsd += cost.grossUsd;
|
|
709
|
+
agg.set(`${keyRef} ${b.door}`, e);
|
|
710
|
+
}
|
|
711
|
+
return [...agg.values()];
|
|
712
|
+
},
|
|
713
|
+
committedUsd: (keyRef) => {
|
|
714
|
+
try {
|
|
715
|
+
return this.meteredSpendLedger?.committed(keyRef).committedLifetimeUsd ?? null;
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
// @silent-fallback-ok: committed is holder-known enrichment; a
|
|
719
|
+
// read failure records the comparison provider-vs-internal only.
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
// The Increment-C emit surface (late-bound; dispatcher dryRun-soaked).
|
|
724
|
+
onDrift: (keyRef, door, driftPct) => this.spendAlertEmitters?.onReconciliationDrift(keyRef, door, driftPct),
|
|
725
|
+
driftAlertPct: rsCfg?.reconciliation?.driftAlertPct ?? 10,
|
|
726
|
+
});
|
|
727
|
+
const sweepMs = Math.max(1, rsCfg?.reconciliation?.sweepIntervalHours ?? 6) * 60 * 60 * 1000;
|
|
728
|
+
this.reconSweepTimer = setInterval(() => void sweep.run(), sweepMs);
|
|
729
|
+
this.reconSweepTimer.unref?.();
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
// @silent-fallback-ok: loud warn — a broken provider-report layer
|
|
733
|
+
// degrades every row to internal-derived (labeled); it never touches
|
|
734
|
+
// money admission or the router path.
|
|
735
|
+
console.warn('[instar] provider-cost report layer init failed (non-fatal — internal-derived reporting remains):', err);
|
|
736
|
+
this.providerCostReportStore = null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
657
739
|
// ── Increment B MONEY layer (routing-control-room-spend §Layer 3 / Surface 2).
|
|
658
740
|
// EXPLICIT enable only (`routingSpend.money.enabled === true`) — a documented
|
|
659
741
|
// DARK_GATE_EXCLUSIONS action-bearing case (FD-16): NEVER resolveDevAgentGate.
|
|
@@ -679,6 +761,9 @@ export class AgentServer {
|
|
|
679
761
|
prices: this.routingPriceAuthority,
|
|
680
762
|
capsStore: this.routingSpendCapsStore,
|
|
681
763
|
machineId,
|
|
764
|
+
// Increment C observer (signal-only, late-bound — the alert layer
|
|
765
|
+
// constructs after this block; null while dark).
|
|
766
|
+
onGateEvent: (ev) => this.spendAlertEmitters?.onGateEvent(ev),
|
|
682
767
|
// Single-machine trivially self-confirms (it IS the pool). On a
|
|
683
768
|
// multi-machine pool the REAL designation re-confirmation plumbing
|
|
684
769
|
// lands with the metered dispatch seam — until then the gate
|
|
@@ -700,51 +785,7 @@ export class AgentServer {
|
|
|
700
785
|
const alertsCfg = options.config.routingSpend;
|
|
701
786
|
const tg = this.telegramAdapter;
|
|
702
787
|
if (tg) {
|
|
703
|
-
|
|
704
|
-
this.spendAlertResolver = new SpendAlertResolver({
|
|
705
|
-
configuredTopicId: () => {
|
|
706
|
-
const id = alertsCfg?.alerts?.telegramTopicId;
|
|
707
|
-
return typeof id === 'number' && Number.isFinite(id) ? id : undefined;
|
|
708
|
-
},
|
|
709
|
-
readPersistedTopicId: () => {
|
|
710
|
-
try {
|
|
711
|
-
const raw = JSON.parse(fs.readFileSync(persistPath, 'utf-8'));
|
|
712
|
-
return typeof raw.topicId === 'number' ? raw.topicId : undefined;
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
// @silent-fallback-ok: no persisted record yet — rung 2 simply misses.
|
|
716
|
-
return undefined;
|
|
717
|
-
}
|
|
718
|
-
},
|
|
719
|
-
persistTopicId: (topicId) => {
|
|
720
|
-
try {
|
|
721
|
-
const tmp = persistPath + '.tmp';
|
|
722
|
-
fs.writeFileSync(tmp, JSON.stringify({ topicId }), { mode: 0o600 });
|
|
723
|
-
fs.renameSync(tmp, persistPath);
|
|
724
|
-
}
|
|
725
|
-
catch {
|
|
726
|
-
// @silent-fallback-ok: a failed persist means the next resolve re-runs
|
|
727
|
-
// the ladder; creation stays fenced, so no duplicate can result.
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
|
-
// Single machine trivially self-confirms (it IS the serving lease); on a
|
|
731
|
-
// multi-machine pool the confirmed-lease plumbing rides the C increment —
|
|
732
|
-
// until then this machine does NOT create (fail toward the lifeline).
|
|
733
|
-
servingLeaseConfirmedAgoMs: () => (poolOn ? null : 0),
|
|
734
|
-
createTopic: async () => {
|
|
735
|
-
const t = await tg.createForumTopic('💰 Routing & Spend Alerts', undefined, {
|
|
736
|
-
origin: 'system',
|
|
737
|
-
bounded: true,
|
|
738
|
-
label: 'routing-spend-alerts',
|
|
739
|
-
});
|
|
740
|
-
return t.topicId;
|
|
741
|
-
},
|
|
742
|
-
sendToTopic: async (topicId, text) => {
|
|
743
|
-
await tg.sendToTopic(topicId, text);
|
|
744
|
-
return true;
|
|
745
|
-
},
|
|
746
|
-
lifelineTopicId: () => tg.getLifelineTopicId(),
|
|
747
|
-
});
|
|
788
|
+
this.spendAlertResolver ??= this.buildSpendAlertResolver(tg, options.config, poolOn);
|
|
748
789
|
const staleCheck = () => {
|
|
749
790
|
try {
|
|
750
791
|
const prices = this.routingPriceAuthority;
|
|
@@ -760,7 +801,15 @@ export class AgentServer {
|
|
|
760
801
|
const res = prices.resolve(door, '__staleness-probe__', Date.now());
|
|
761
802
|
if (res.priceStale && res.newestPointAgeDays !== null) {
|
|
762
803
|
const meta = prices.doorMetaFor(door);
|
|
763
|
-
|
|
804
|
+
const alert = buildStalePriceAlert(door, res.newestPointAgeDays, meta?.freshnessSlaDays ?? DEFAULT_FRESHNESS_SLA_DAYS);
|
|
805
|
+
// Increment C: prefer the dispatcher (lane dedup + coalescing +
|
|
806
|
+
// dryRun soak); the B resolver-direct path remains the fallback
|
|
807
|
+
// when the alert layer is dark (byte-identical B behavior).
|
|
808
|
+
const dispatcher = this.spendAlertDispatcher;
|
|
809
|
+
if (dispatcher)
|
|
810
|
+
void dispatcher.dispatch(alert);
|
|
811
|
+
else
|
|
812
|
+
void resolver.emit(alert);
|
|
764
813
|
}
|
|
765
814
|
}
|
|
766
815
|
}
|
|
@@ -786,6 +835,84 @@ export class AgentServer {
|
|
|
786
835
|
this.spendPlanStore = null;
|
|
787
836
|
}
|
|
788
837
|
}
|
|
838
|
+
// ── Increment C alert layer (routing-control-room-spend §Surface 2 Alerts).
|
|
839
|
+
// dryRun-FIRST live-on-dev (FD-16): `routingSpend.alerts.enabled` rides the
|
|
840
|
+
// dev-agent gate; `alerts.dryRun` defaults TRUE so a dev agent soaks the
|
|
841
|
+
// dispatch decisions in logs/routing-spend-alerts.jsonl before anything is
|
|
842
|
+
// delivered. Independent of the money layer — door-dark/fallback alerts can
|
|
843
|
+
// flow with no door ever armed (that is WHY the SERVING lease owns creation).
|
|
844
|
+
try {
|
|
845
|
+
const alertsCfgC = options.config.routingSpend?.alerts;
|
|
846
|
+
const alertsOn = resolveDevAgentGate(alertsCfgC?.enabled, options.config);
|
|
847
|
+
const tgC = this.telegramAdapter;
|
|
848
|
+
if (alertsOn && tgC) {
|
|
849
|
+
const poolOnC = options.config.multiMachine?.sessionPool?.enabled === true;
|
|
850
|
+
this.spendAlertResolver ??= this.buildSpendAlertResolver(tgC, options.config, poolOnC);
|
|
851
|
+
const auditPath = path.join(options.config.projectDir, 'logs', 'routing-spend-alerts.jsonl');
|
|
852
|
+
const auditSink = (entry) => {
|
|
853
|
+
try {
|
|
854
|
+
fs.mkdirSync(path.dirname(auditPath), { recursive: true });
|
|
855
|
+
fs.appendFileSync(auditPath, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n', { mode: 0o600 });
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
// @silent-fallback-ok: the audit line is observability — never blocks dispatch.
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
const channel = new TelegramSpendTopicChannel({
|
|
862
|
+
resolver: this.spendAlertResolver,
|
|
863
|
+
sendToTopic: async (topicId, text) => {
|
|
864
|
+
await tgC.sendToTopic(topicId, text);
|
|
865
|
+
return true;
|
|
866
|
+
},
|
|
867
|
+
lifelineTopicId: () => tgC.getLifelineTopicId(),
|
|
868
|
+
// Money-critical kinds ride the durable relay when the store opens
|
|
869
|
+
// (retry-until-delivered — delivery-robustness Layers 2/3; the
|
|
870
|
+
// DeliveryFailureSentinel drains this store). Opened lazily,
|
|
871
|
+
// per-agent-id isolated, degrade-to-direct-send on ANY failure.
|
|
872
|
+
enqueueDurable: (topicId, text) => {
|
|
873
|
+
try {
|
|
874
|
+
this.spendAlertRelayStore ??= SpendAlertRelayStore.open(options.config.projectName, options.config.stateDir);
|
|
875
|
+
const store = this.spendAlertRelayStore;
|
|
876
|
+
const id = spendAlertDeliveryId(topicId, text);
|
|
877
|
+
return store.enqueue({
|
|
878
|
+
delivery_id: id,
|
|
879
|
+
topic_id: topicId,
|
|
880
|
+
text_hash: id,
|
|
881
|
+
text,
|
|
882
|
+
http_code: 0,
|
|
883
|
+
attempted_port: options.config.port ?? 0,
|
|
884
|
+
next_attempt_at: new Date().toISOString(),
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
// @silent-fallback-ok: no sqlite / store unavailable — the channel
|
|
889
|
+
// degrades to the direct send + lifeline ladder (delivery degrades,
|
|
890
|
+
// never dies; the dispatcher's latch still requires a confirmed leg).
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
audit: auditSink,
|
|
895
|
+
});
|
|
896
|
+
this.spendAlertDispatcher = new SpendAlertDispatcher({
|
|
897
|
+
channels: [channel],
|
|
898
|
+
dryRun: alertsCfgC?.dryRun !== false, // FD-16 dryRun-first
|
|
899
|
+
auditPath,
|
|
900
|
+
});
|
|
901
|
+
const machineIdC = options.config.machineId ?? 'single-machine';
|
|
902
|
+
this.spendAlertEmitters = new SpendAlertEmitters({
|
|
903
|
+
dispatcher: this.spendAlertDispatcher,
|
|
904
|
+
machineId: machineIdC,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
catch (err) {
|
|
909
|
+
// @silent-fallback-ok: loud warn — a broken alert layer degrades to the
|
|
910
|
+
// Increment-B behavior (resolver-direct stale-price emission); it never
|
|
911
|
+
// touches money admission or the router path.
|
|
912
|
+
console.warn('[instar] routing-spend alert layer init failed (non-fatal — B-level alerts remain):', err);
|
|
913
|
+
this.spendAlertDispatcher = null;
|
|
914
|
+
this.spendAlertEmitters = null;
|
|
915
|
+
}
|
|
789
916
|
// Observable Intelligence × Responsible Resource: the audit trail is kept
|
|
790
917
|
// long enough to see behaviour/performance trends, then aged out — never
|
|
791
918
|
// hoarded forever. Default 30d; tune via monitoring.featureMetrics.retentionDays
|
|
@@ -2126,6 +2253,7 @@ export class AgentServer {
|
|
|
2126
2253
|
meteredSpendGate: this.meteredSpendGate,
|
|
2127
2254
|
spendPlanStore: this.spendPlanStore,
|
|
2128
2255
|
pinAttemptStore: this.pinAttemptStore,
|
|
2256
|
+
providerCostReportStore: this.providerCostReportStore,
|
|
2129
2257
|
resourceLedger: this.resourceLedger,
|
|
2130
2258
|
processFootprintMonitor: this.processFootprintMonitor,
|
|
2131
2259
|
approvalLedger: this.approvalLedger,
|
|
@@ -3917,6 +4045,15 @@ export class AgentServer {
|
|
|
3917
4045
|
clearInterval(this.spendAlertTimer);
|
|
3918
4046
|
}
|
|
3919
4047
|
catch { /* @silent-fallback-ok: timer teardown is best-effort cleanup at shutdown */ }
|
|
4048
|
+
try {
|
|
4049
|
+
await this.spendAlertDispatcher?.flushDigest();
|
|
4050
|
+
}
|
|
4051
|
+
catch { /* @silent-fallback-ok: a failed final digest flush loses only a coalesced NOTICE at shutdown */ }
|
|
4052
|
+
try {
|
|
4053
|
+
if (this.reconSweepTimer)
|
|
4054
|
+
clearInterval(this.reconSweepTimer);
|
|
4055
|
+
}
|
|
4056
|
+
catch { /* @silent-fallback-ok: timer teardown is best-effort cleanup at shutdown */ }
|
|
3920
4057
|
this.featureMetricsPruneTimer = null;
|
|
3921
4058
|
}
|
|
3922
4059
|
if (this.tokenLedger) {
|
|
@@ -4017,6 +4154,74 @@ export class AgentServer {
|
|
|
4017
4154
|
* its map in memory, so constructing a second instance on the same file would
|
|
4018
4155
|
* lose updates between the two caches.
|
|
4019
4156
|
*/
|
|
4157
|
+
/** Increment C — the router fan-out + gate observer reach the emitters here (late-bound; null while dark). */
|
|
4158
|
+
getSpendAlertEmitters() {
|
|
4159
|
+
return this.spendAlertEmitters;
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* Shared builder for the "💰 Routing & Spend Alerts" topic resolver — used by
|
|
4163
|
+
* BOTH the Increment-B money block (stale-price cadence) and the Increment-C
|
|
4164
|
+
* alert layer, whichever constructs first. Rung 2 carries both halves: the
|
|
4165
|
+
* machine-local persisted record AND the pool-published registry field
|
|
4166
|
+
* (FD-6 — a future serving-lease holder inherits the id, never re-creates).
|
|
4167
|
+
*/
|
|
4168
|
+
buildSpendAlertResolver(tg, config, poolOn) {
|
|
4169
|
+
const persistPath = path.join(config.stateDir, 'state', 'routing-spend-alert-topic.json');
|
|
4170
|
+
const machineId = config.machineId;
|
|
4171
|
+
const registry = new MachineIdentityManager(config.stateDir);
|
|
4172
|
+
return new SpendAlertResolver({
|
|
4173
|
+
configuredTopicId: () => {
|
|
4174
|
+
const id = config.routingSpend?.alerts?.telegramTopicId;
|
|
4175
|
+
return typeof id === 'number' && Number.isFinite(id) ? id : undefined;
|
|
4176
|
+
},
|
|
4177
|
+
readPersistedTopicId: () => {
|
|
4178
|
+
try {
|
|
4179
|
+
const raw = JSON.parse(fs.readFileSync(persistPath, 'utf-8'));
|
|
4180
|
+
return typeof raw.topicId === 'number' ? raw.topicId : undefined;
|
|
4181
|
+
}
|
|
4182
|
+
catch {
|
|
4183
|
+
// @silent-fallback-ok: no persisted record yet — rung 2 simply misses.
|
|
4184
|
+
return undefined;
|
|
4185
|
+
}
|
|
4186
|
+
},
|
|
4187
|
+
persistTopicId: (topicId) => {
|
|
4188
|
+
try {
|
|
4189
|
+
const tmp = persistPath + '.tmp';
|
|
4190
|
+
fs.writeFileSync(tmp, JSON.stringify({ topicId }), { mode: 0o600 });
|
|
4191
|
+
fs.renameSync(tmp, persistPath);
|
|
4192
|
+
}
|
|
4193
|
+
catch {
|
|
4194
|
+
// @silent-fallback-ok: a failed persist means the next resolve re-runs
|
|
4195
|
+
// the ladder; creation stays fenced, so no duplicate can result.
|
|
4196
|
+
}
|
|
4197
|
+
},
|
|
4198
|
+
// FD-6 rung 2, pool half: the replicated machine-registry field.
|
|
4199
|
+
readPoolPublishedTopicId: () => registry.readAnyRoutingSpendAlertTopic(),
|
|
4200
|
+
publishTopicId: (topicId) => {
|
|
4201
|
+
if (!machineId)
|
|
4202
|
+
return; // single-machine installs: the local record suffices
|
|
4203
|
+
registry.updateRoutingSpendAlertTopic(machineId, topicId);
|
|
4204
|
+
},
|
|
4205
|
+
// Single machine trivially self-confirms (it IS the serving lease); on a
|
|
4206
|
+
// multi-machine pool the confirmed-lease plumbing is a tracked follow-up —
|
|
4207
|
+
// until then this machine does NOT create (fail toward the lifeline)
|
|
4208
|
+
// <!-- tracked: CMT-1929 -->.
|
|
4209
|
+
servingLeaseConfirmedAgoMs: () => (poolOn ? null : 0),
|
|
4210
|
+
createTopic: async () => {
|
|
4211
|
+
const t = await tg.createForumTopic('💰 Routing & Spend Alerts', undefined, {
|
|
4212
|
+
origin: 'system',
|
|
4213
|
+
bounded: true,
|
|
4214
|
+
label: 'routing-spend-alerts',
|
|
4215
|
+
});
|
|
4216
|
+
return t.topicId;
|
|
4217
|
+
},
|
|
4218
|
+
sendToTopic: async (topicId, text) => {
|
|
4219
|
+
await tg.sendToTopic(topicId, text);
|
|
4220
|
+
return true;
|
|
4221
|
+
},
|
|
4222
|
+
lifelineTopicId: () => tg.getLifelineTopicId(),
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4020
4225
|
getTopicOperatorStore() {
|
|
4021
4226
|
return this.topicOperatorStore;
|
|
4022
4227
|
}
|