diff --git a/src/apps/history-writer.mjs b/src/apps/history-writer.mjs index 1d3729c..1863c9d 100644 --- a/src/apps/history-writer.mjs +++ b/src/apps/history-writer.mjs @@ -123,6 +123,7 @@ const rawQuoteTopics = [ config.kafkaTopicRawNearIntentsQuote, ]; const quoteLifecycleRetentionMaintenanceIntervalMs = 60 * 1000; +const historyWriterStartedAt = new Date().toISOString(); const liveEvidenceTopics = [ config.kafkaTopicNormSwapDemand, config.kafkaTopicDecisionTradeDecision, @@ -219,10 +220,14 @@ const state = { await refreshPortfolioMetrics().catch((error) => { state.metrics_error = serializeError(error); }); -await refreshQuoteOutcomeAttributions().catch((error) => { +await refreshQuoteOutcomeAttributions().then((records) => publishQuoteOutcomeNotifications(records, { + minObservedAt: historyWriterStartedAt, +})).catch((error) => { state.quote_outcomes_error = serializeError(error); }); -await refreshIntentRequestOutcomeAttributions().catch((error) => { +await refreshIntentRequestOutcomeAttributions().then((records) => publishIntentRequestOutcomeNotifications(records, { + minObservedAt: historyWriterStartedAt, +})).catch((error) => { state.intent_request_outcomes_error = serializeError(error); }); await refreshQuoteLifecycleRetentionState().catch((error) => { diff --git a/test/history-writer-static.test.mjs b/test/history-writer-static.test.mjs index c81fab1..490d248 100644 --- a/test/history-writer-static.test.mjs +++ b/test/history-writer-static.test.mjs @@ -43,3 +43,10 @@ test('history writer passes tracked assets into portfolio valuation', () => { assert.match(source, /valuationAssets:\s*inputs\.valuationAssets \|\| \[\]/); assert.match(source, /inputs\.valuationAssets[\s\S]+asset\.priceId/); }); + +test('history writer startup outcome refresh publishes newly computed ntfy notifications', () => { + assert.match(source, /historyWriterStartedAt\s*=\s*new Date\(\)\.toISOString\(\)/); + assert.match(source, /refreshQuoteOutcomeAttributions\(\)\.then\(\(records\) => publishQuoteOutcomeNotifications\(records, \{\s*minObservedAt:\s*historyWriterStartedAt,\s*\}\)\)/); + assert.match(source, /refreshIntentRequestOutcomeAttributions\(\)\.then\(\(records\) => publishIntentRequestOutcomeNotifications\(records, \{\s*minObservedAt:\s*historyWriterStartedAt,\s*\}\)\)/); + assert.match(source, /outcomeNotificationObservedAtOrAfter/); +}); diff --git a/test/notification-layer.test.mjs b/test/notification-layer.test.mjs index 570c87c..afe2f41 100644 --- a/test/notification-layer.test.mjs +++ b/test/notification-layer.test.mjs @@ -200,7 +200,7 @@ test('notification dispatcher publishes each durable notification key once', asy assert.equal(finished.length, 1); }); -test('outcome recency filter keeps startup refresh from sending historical notifications', () => { +test('observed timestamp recency filter rejects stale outcome observations', () => { assert.equal(observedAtOrAfter('2026-04-16T10:00:00.000Z', '2026-04-16T09:59:59.000Z'), true); assert.equal(observedAtOrAfter('2026-04-16T09:00:00.000Z', '2026-04-16T09:59:59.000Z'), false); });