deepline 0.3.431 → 0.3.433
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/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/billing/credit-recovery.ts +97 -6
- package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +33 -0
- package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +94 -10
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-dead-letter.ts +46 -2
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-message.ts +4 -0
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +4 -4
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +36 -0
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +95 -8
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +5 -1
- package/dist/cli/{auth-C4O2LOMP.mjs → auth-5LF5G3Z2.mjs} +4 -4
- package/dist/cli/auth-FGHYE2JG.js +40 -0
- package/dist/cli/{billing-KJWTSINT.mjs → billing-I7ZADV7D.mjs} +4 -4
- package/dist/cli/{billing-RSZCFJLQ.js → billing-M34SJBAB.js} +73 -73
- package/dist/cli/{chunk-PMEMHRBA.mjs → chunk-2TNGO4DZ.mjs} +5 -4
- package/dist/cli/{chunk-DCEMKJAM.mjs → chunk-3C25DYSE.mjs} +3 -3
- package/dist/cli/{chunk-ZFZB3J6X.mjs → chunk-3CZEDR4K.mjs} +5 -5
- package/dist/cli/{chunk-II2W3O4Z.mjs → chunk-3ET2ZKZS.mjs} +1 -1
- package/dist/cli/{chunk-727ZKS55.mjs → chunk-42JTV4IM.mjs} +1 -1
- package/dist/cli/{chunk-E36ZD6WX.js → chunk-6R6SQQGQ.js} +4 -4
- package/dist/cli/{chunk-7H2VOSMU.mjs → chunk-B3XEZMYS.mjs} +8 -8
- package/dist/cli/{chunk-IZIKOENG.mjs → chunk-BSQH4V3Y.mjs} +1 -1
- package/dist/cli/{chunk-Y4YTLJOY.js → chunk-CXGNMAFV.js} +16 -16
- package/dist/cli/{chunk-VFEB52MK.js → chunk-DAEODAWB.js} +35 -35
- package/dist/cli/{chunk-YMVDYAH6.js → chunk-FYDPU4QV.js} +26 -26
- package/dist/cli/{chunk-XQ6GLQBP.mjs → chunk-G4P74R2F.mjs} +2 -2
- package/dist/cli/{chunk-QZZDQRZR.mjs → chunk-GZS6ZB55.mjs} +1 -1
- package/dist/cli/{chunk-VCV523UN.mjs → chunk-HAC6SRO5.mjs} +1 -1
- package/dist/cli/{chunk-OU7HHXQF.js → chunk-I5CB3UUX.js} +50 -50
- package/dist/cli/{chunk-QVCA5NFP.js → chunk-JNX4Y37G.js} +1 -1
- package/dist/cli/{chunk-EMXOHRK3.js → chunk-JUZW32YP.js} +8 -8
- package/dist/cli/{chunk-X6YQF3UF.js → chunk-KAWPJNUO.js} +20 -20
- package/dist/cli/{chunk-FNYEER22.js → chunk-KQ5VO34X.js} +10 -10
- package/dist/cli/{chunk-H37US5PT.js → chunk-N5VOE6KZ.js} +6 -6
- package/dist/cli/{chunk-C5ATRVQJ.js → chunk-O7X44YGX.js} +10 -9
- package/dist/cli/{chunk-VIA6YQI7.js → chunk-PKOCM6JB.js} +52 -52
- package/dist/cli/{chunk-5FE6L6K7.mjs → chunk-PM3DL6YP.mjs} +3 -3
- package/dist/cli/{chunk-LVO7M2MG.mjs → chunk-PRA2CG4B.mjs} +1 -1
- package/dist/cli/{chunk-MIGX46JW.js → chunk-T77HG3R7.js} +2 -2
- package/dist/cli/{chunk-4MUENWMZ.mjs → chunk-UAUHERWW.mjs} +2 -2
- package/dist/cli/{chunk-CAJT43U2.js → chunk-UDJXL4SM.js} +79 -79
- package/dist/cli/{chunk-CZ4JZP2F.mjs → chunk-VMCJGLJL.mjs} +2 -2
- package/dist/cli/{chunk-UE34XUXS.js → chunk-WVCAELST.js} +6 -6
- package/dist/cli/{chunk-K5M4OOJJ.mjs → chunk-XSEQLAHE.mjs} +2 -2
- package/dist/cli/{chunk-FNHIJPQ6.mjs → chunk-YBSOJ6D4.mjs} +4 -4
- package/dist/cli/{chunk-24FMUK3P.js → chunk-ZMRP5UXN.js} +2 -2
- package/dist/cli/{csv-AFWU7THX.mjs → csv-3TCORDI3.mjs} +3 -3
- package/dist/cli/{csv-R7BRVDCD.js → csv-R5I5ZWSC.js} +12 -12
- package/dist/cli/{db-VWE6GS7Z.js → db-BXEK3WX2.js} +25 -25
- package/dist/cli/{db-4DDGZWIG.mjs → db-D7NC4XAM.mjs} +4 -4
- package/dist/cli/{describe-VPSIS7MQ.js → describe-LSOFG74H.js} +16 -16
- package/dist/cli/{describe-O4H6N4EO.mjs → describe-SXYZTJQ5.mjs} +12 -12
- package/dist/cli/describe-core.d.mts +1 -1
- package/dist/cli/describe-core.d.ts +1 -1
- package/dist/cli/{executions-LHNWIQDR.js → executions-BF5YRFHH.js} +6 -6
- package/dist/cli/{executions-BLF6JAAZ.mjs → executions-ULXJPGV4.mjs} +4 -4
- package/dist/cli/{feedback-6BNBRU3O.mjs → feedback-6FJOHQJB.mjs} +3 -3
- package/dist/cli/{feedback-YJ6EB3JH.js → feedback-VJM56OFG.js} +7 -7
- package/dist/cli/git-TSBBSAK3.js +24 -0
- package/dist/cli/{git-TCLUOJU7.mjs → git-WGZMHPW3.mjs} +4 -4
- package/dist/cli/index.js +131 -131
- package/dist/cli/index.mjs +36 -36
- package/dist/cli/{legacy-noops-6ZKNRJBD.mjs → legacy-noops-EVFQBQ6H.mjs} +4 -4
- package/dist/cli/{legacy-noops-4KXLXXFC.js → legacy-noops-ZRDESRRJ.js} +7 -7
- package/dist/cli/{monitors-5XJIT3GU.mjs → monitors-4PMVB7RD.mjs} +4 -4
- package/dist/cli/{monitors-6U3WH74P.js → monitors-FR4OSX77.js} +84 -84
- package/dist/cli/{org-6LAFFTIE.js → org-HSY6TVRI.js} +64 -64
- package/dist/cli/{org-QZZL2WO2.mjs → org-QBENPGXU.mjs} +4 -4
- package/dist/cli/{play-7KDM3QTU.mjs → play-7O7GXOWT.mjs} +10 -10
- package/dist/cli/{play-CCDHG3GT.js → play-JSY4OSLN.js} +283 -283
- package/dist/cli/{providers-WEYDXXND.mjs → providers-76OM5TQ7.mjs} +4 -4
- package/dist/cli/{providers-VUNDMKOF.js → providers-RLLSVNR6.js} +7 -7
- package/dist/cli/{quickstart-ABTQUOYC.js → quickstart-MZZDW6VC.js} +5 -5
- package/dist/cli/{quickstart-UAWZJ52P.mjs → quickstart-UHB5GNJV.mjs} +3 -3
- package/dist/cli/search-GCMZS6IK.js +30 -0
- package/dist/cli/{search-VTEFQWX4.mjs → search-X5P34RAU.mjs} +5 -5
- package/dist/cli/{secrets-ZSMBJJAU.js → secrets-EKVWSDQT.js} +13 -13
- package/dist/cli/{secrets-FKGXGVTS.mjs → secrets-L43HK4A6.mjs} +4 -4
- package/dist/cli/{sessions-RXJZVW7N.js → sessions-IPV23HXJ.js} +5 -5
- package/dist/cli/{sessions-ZWTSTLOZ.mjs → sessions-QPK5NPXO.mjs} +4 -4
- package/dist/cli/{settings-ULIRON4N.js → settings-5CJ6SMAN.js} +75 -75
- package/dist/cli/{settings-2TYYFRK2.mjs → settings-NF4VMLGN.mjs} +8 -8
- package/dist/cli/{setup-PRTFVHWH.mjs → setup-3JVUB4V6.mjs} +7 -7
- package/dist/cli/{setup-D6PIAKTH.js → setup-FHK6S4JC.js} +42 -42
- package/dist/cli/{skills-S6LAHWZZ.mjs → skills-HZLL2C7G.mjs} +5 -5
- package/dist/cli/skills-U2R75N5D.js +29 -0
- package/dist/cli/tools-N57LENEU.js +44 -0
- package/dist/cli/{tools-3QL4GWQI.mjs → tools-TG5SDGNS.mjs} +12 -12
- package/dist/cli/{update-MH6Z5UL7.mjs → update-4UZ6PUZK.mjs} +7 -7
- package/dist/cli/update-WYEO75YZ.js +31 -0
- package/dist/cli/{workflow-YSIK4DXK.mjs → workflow-FAJY6TR4.mjs} +5 -5
- package/dist/cli/{workflow-ECWHO7I5.js → workflow-UTPL4KIF.js} +15 -15
- package/dist/{client-C0VoQB5b.d.ts → client-CgmfMj_N.d.ts} +73 -4
- package/dist/{client-BuCrTjW6.d.mts → client-DIY1WpSW.d.mts} +73 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -6
- package/dist/index.mjs +3 -3
- package/dist/install-integrity.json +80 -80
- package/dist/plays/play-description.d.mts +1 -1
- package/dist/plays/play-description.d.ts +1 -1
- package/dist/plays/run-request.d.mts +1 -1
- package/dist/plays/run-request.d.ts +1 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/dist/viewer/viewer.css +4 -0
- package/dist/viewer/viewer.js +19 -3
- package/package.json +1 -1
- package/dist/cli/auth-IJQWGGCT.js +0 -40
- package/dist/cli/git-OVSSGF57.js +0 -24
- package/dist/cli/search-44L5VX4Q.js +0 -30
- package/dist/cli/skills-5YWEURZ2.js +0 -29
- package/dist/cli/tools-UE45QLRH.js +0 -44
- package/dist/cli/update-J5DUS5I6.js +0 -31
|
@@ -205,7 +205,7 @@ export const SDK_RELEASE = {
|
|
|
205
205
|
// getters keep their established compatibility behavior.
|
|
206
206
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
207
207
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
208
|
-
version: '0.3.
|
|
208
|
+
version: '0.3.433',
|
|
209
209
|
updateSummary: {
|
|
210
210
|
introducedIn: '0.3.90',
|
|
211
211
|
summary:
|
|
@@ -2,8 +2,9 @@ import { parseStoredMonitorBillingState } from './state';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Only these event-time billing denials are healed by a restored Deepline
|
|
5
|
-
* balance. A monthly spending cap
|
|
6
|
-
*
|
|
5
|
+
* balance. A monthly spending cap is healed by the cap admitting again
|
|
6
|
+
* (`isMonitorCapSuspensionReason`), and an opaque billing failure deliberately
|
|
7
|
+
* remains an explicit-recovery state.
|
|
7
8
|
*/
|
|
8
9
|
export function isCreditExhaustionSuspensionReason(
|
|
9
10
|
reason: string | undefined,
|
|
@@ -14,6 +15,60 @@ export function isCreditExhaustionSuspensionReason(
|
|
|
14
15
|
);
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The billing policy sweep's credit pause. When the sweep finds an exhausted
|
|
20
|
+
* wallet it deactivates a direct monitor with this reason, and the repair that
|
|
21
|
+
* converges it records the same reason as the billing suspension's. The sweep
|
|
22
|
+
* then defers a `billing_suspended` row to credit recovery and never revives
|
|
23
|
+
* it itself, so unless credit recovery accepts it, a top-up has no automatic
|
|
24
|
+
* path back (the reason's own resolution promises one).
|
|
25
|
+
*
|
|
26
|
+
* Accepted only with the policy's provenance on the row: the same suspension
|
|
27
|
+
* reason under any other deactivation reason is not proof the wallet caused
|
|
28
|
+
* it.
|
|
29
|
+
*
|
|
30
|
+
* KAR-43: an exhausted wallet switches every Monitor off through this policy
|
|
31
|
+
* pause, Fleet members included, so a Fleet the policy suspended is credit
|
|
32
|
+
* restorable too (`isCreditRestorableFleetSuspensionReason`).
|
|
33
|
+
*/
|
|
34
|
+
export const POLICY_CREDIT_SUSPENSION_REASON = 'insufficient_balance';
|
|
35
|
+
|
|
36
|
+
/** The policy's spending-cap pause (KAR-43). */
|
|
37
|
+
export const POLICY_CAP_SUSPENSION_REASON = 'monthly_cap_exceeded';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A Fleet's billing suspension that a restored balance heals: an event-time
|
|
41
|
+
* credit denial or the policy's own credit pause. The Fleet's suspension
|
|
42
|
+
* record is written only by Fleet billing lifecycle, so its reason is its
|
|
43
|
+
* provenance.
|
|
44
|
+
*/
|
|
45
|
+
export function isCreditRestorableFleetSuspensionReason(
|
|
46
|
+
reason: string | undefined,
|
|
47
|
+
): boolean {
|
|
48
|
+
return (
|
|
49
|
+
isCreditExhaustionSuspensionReason(reason) ||
|
|
50
|
+
reason === POLICY_CREDIT_SUSPENSION_REASON
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A spending-cap suspension, healed once this period's spend is under the cap. */
|
|
55
|
+
export function isMonitorCapSuspensionReason(
|
|
56
|
+
reason: string | undefined,
|
|
57
|
+
): boolean {
|
|
58
|
+
return reason === POLICY_CAP_SUSPENSION_REASON;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isDirectCreditSuspension(input: {
|
|
62
|
+
suspensionReason: string | undefined;
|
|
63
|
+
deactivationReason: string | undefined;
|
|
64
|
+
}): boolean {
|
|
65
|
+
return (
|
|
66
|
+
isCreditExhaustionSuspensionReason(input.suspensionReason) ||
|
|
67
|
+
(input.suspensionReason === POLICY_CREDIT_SUSPENSION_REASON &&
|
|
68
|
+
input.deactivationReason === POLICY_CREDIT_SUSPENSION_REASON)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
17
72
|
/**
|
|
18
73
|
* A billing suspension is not itself proof that TamRadar accepted the delete.
|
|
19
74
|
* Keep this test beside the recovery selector so every automatic path rejects
|
|
@@ -44,9 +99,10 @@ export function hasConfirmedProviderMonitorUpstreamCleanup(
|
|
|
44
99
|
}
|
|
45
100
|
|
|
46
101
|
/**
|
|
47
|
-
* Narrow selector for direct
|
|
48
|
-
* may re-provision
|
|
49
|
-
*
|
|
102
|
+
* Narrow selector for direct credit-exhaustion suspensions that credit
|
|
103
|
+
* restoration may re-provision: an event-time suspension, or the policy
|
|
104
|
+
* sweep's own credit pause. It intentionally does not include manual intent,
|
|
105
|
+
* spending-cap denials, opaque billing failures, or cleanup still in flight.
|
|
50
106
|
*/
|
|
51
107
|
export function isCreditRestorableDirectMonitorSuspension(input: {
|
|
52
108
|
fleetId?: string;
|
|
@@ -61,7 +117,10 @@ export function isCreditRestorableDirectMonitorSuspension(input: {
|
|
|
61
117
|
if (
|
|
62
118
|
billingState.state !== 'billing_suspended' ||
|
|
63
119
|
!billingState.reactivation_required ||
|
|
64
|
-
!
|
|
120
|
+
!isDirectCreditSuspension({
|
|
121
|
+
suspensionReason: billingState.suspension_reason,
|
|
122
|
+
deactivationReason: input.deactivationReason,
|
|
123
|
+
}) ||
|
|
65
124
|
!hasConfirmedProviderMonitorUpstreamCleanup(input.providerBinding)
|
|
66
125
|
) {
|
|
67
126
|
return false;
|
|
@@ -75,3 +134,35 @@ export function isCreditRestorableDirectMonitorSuspension(input: {
|
|
|
75
134
|
input.deactivationReason === 'insufficient_balance'
|
|
76
135
|
);
|
|
77
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Narrow selector for direct spending-cap suspensions that may be re-provisioned
|
|
140
|
+
* once the cap admits again (KAR-43): the cap is raised, or the next billing
|
|
141
|
+
* period begins. The caller proves the cap admits; this proves the row was
|
|
142
|
+
* switched off for the cap, by the platform, and that upstream cleanup is
|
|
143
|
+
* confirmed. A customer stop, an opaque billing failure and an unfinished
|
|
144
|
+
* cleanup are never selected.
|
|
145
|
+
*/
|
|
146
|
+
export function isCapRestorableDirectMonitorSuspension(input: {
|
|
147
|
+
fleetId?: string;
|
|
148
|
+
status: string;
|
|
149
|
+
desired?: 'active' | 'inactive';
|
|
150
|
+
deactivationReason?: string;
|
|
151
|
+
billingState: unknown;
|
|
152
|
+
providerBinding: unknown;
|
|
153
|
+
}): boolean {
|
|
154
|
+
if (input.fleetId !== undefined || input.status !== 'disabled') return false;
|
|
155
|
+
const billingState = parseStoredMonitorBillingState(input.billingState);
|
|
156
|
+
if (
|
|
157
|
+
billingState.state !== 'billing_suspended' ||
|
|
158
|
+
!billingState.reactivation_required ||
|
|
159
|
+
!isMonitorCapSuspensionReason(billingState.suspension_reason) ||
|
|
160
|
+
!hasConfirmedProviderMonitorUpstreamCleanup(input.providerBinding)
|
|
161
|
+
) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return (
|
|
165
|
+
input.desired !== 'inactive' ||
|
|
166
|
+
input.deactivationReason === POLICY_CAP_SUSPENSION_REASON
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -187,6 +187,39 @@ export function monitorBillingRequiresExplicitReactivation(
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Whether a monthly-billed monitor is still inside the period it has already
|
|
192
|
+
* paid for (KAR-43 decision 8).
|
|
193
|
+
*
|
|
194
|
+
* A subscription monitor (TamRadar `contact_job_changes`) is billed per month,
|
|
195
|
+
* not per finding: the first month at create, then renewed by the
|
|
196
|
+
* `charge-reconcile` cron 24 hours before the provider's `next_charge_at`.
|
|
197
|
+
* That renewal is its funding gate. Until the paid period ends, a negative
|
|
198
|
+
* balance or an exceeded cap must not switch it off: its findings add no
|
|
199
|
+
* charges, the customer keeps the month they paid for, and a recreate would
|
|
200
|
+
* charge Deepline again (TamRadar has no reactivate endpoint).
|
|
201
|
+
*
|
|
202
|
+
* `provider_next_charge_at` is the provider-owned period end; `next_renewal_at`
|
|
203
|
+
* is Deepline's own periodic renewal. Either one in the future proves a paid
|
|
204
|
+
* period. An unparseable anchor proves nothing.
|
|
205
|
+
*/
|
|
206
|
+
export function monitorWithinPaidSubscriptionPeriod(
|
|
207
|
+
value: unknown,
|
|
208
|
+
now: number,
|
|
209
|
+
): boolean {
|
|
210
|
+
const state = parseStoredMonitorBillingState(value);
|
|
211
|
+
if (state.state !== 'active' && state.state !== 'billing_pending') {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return [state.provider_next_charge_at, state.next_renewal_at].some(
|
|
215
|
+
(anchor) => {
|
|
216
|
+
if (!anchor) return false;
|
|
217
|
+
const paidThrough = Date.parse(anchor);
|
|
218
|
+
return Number.isFinite(paidThrough) && now < paidThrough;
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
export function publicMonitorBillingSummary(
|
|
191
224
|
state: StoredMonitorBillingState,
|
|
192
225
|
customerState: ProviderMonitorCustomerState,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
monitorBillingRequiresExplicitReactivation,
|
|
3
|
+
monitorWithinPaidSubscriptionPeriod,
|
|
4
|
+
} from './billing/state';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Why a monitor is desired-inactive.
|
|
@@ -62,9 +65,9 @@ export const MONITOR_DEACTIVATION_REASONS: readonly MonitorDeactivationReasonDef
|
|
|
62
65
|
id: 'monthly_cap_exceeded',
|
|
63
66
|
owner: 'policy',
|
|
64
67
|
summary:
|
|
65
|
-
'Deactivated because the organization reached its monthly
|
|
68
|
+
'Deactivated because the organization reached its monthly spending cap.',
|
|
66
69
|
resolution:
|
|
67
|
-
|
|
70
|
+
"Raise the cap, or wait for the next billing period. Monitors are reactivated automatically once this period's spend is under the cap again.",
|
|
68
71
|
},
|
|
69
72
|
{
|
|
70
73
|
id: 'billing_failure',
|
|
@@ -159,8 +162,15 @@ export type MonitorPolicyRow = {
|
|
|
159
162
|
desired?: 'active' | 'inactive' | undefined;
|
|
160
163
|
deactivationReason?: string | undefined;
|
|
161
164
|
billingState?: unknown;
|
|
165
|
+
fleetId?: string | undefined;
|
|
162
166
|
};
|
|
163
167
|
|
|
168
|
+
/** The two spend reasons policy writes and reverts (KAR-43). */
|
|
169
|
+
export const SPEND_POLICY_REASONS: ReadonlySet<string> = new Set([
|
|
170
|
+
'insufficient_balance',
|
|
171
|
+
'monthly_cap_exceeded',
|
|
172
|
+
]);
|
|
173
|
+
|
|
164
174
|
export type MonitorPolicyDecision =
|
|
165
175
|
| { kind: 'deactivate'; reason: string }
|
|
166
176
|
| { kind: 'reactivate' }
|
|
@@ -182,22 +192,59 @@ export type MonitorPolicyDecision =
|
|
|
182
192
|
export function decideMonitorBalancePolicy(input: {
|
|
183
193
|
row: MonitorPolicyRow;
|
|
184
194
|
balance: number | null;
|
|
195
|
+
/**
|
|
196
|
+
* This billing period's recorded spend is at or over the workspace's
|
|
197
|
+
* monthly cap (KAR-43). Absent means no cap applies.
|
|
198
|
+
*/
|
|
199
|
+
capExceeded?: boolean;
|
|
200
|
+
/** Decision time, for the paid-subscription-period exclusion. */
|
|
201
|
+
now?: number;
|
|
185
202
|
}): MonitorPolicyDecision {
|
|
186
203
|
const admits = balanceAdmitsMonitors(input.balance);
|
|
204
|
+
const capExceeded = input.capExceeded === true;
|
|
187
205
|
const reason = input.row.deactivationReason;
|
|
188
|
-
|
|
206
|
+
// An exhausted wallet outranks an exceeded cap: a top-up alone must not
|
|
207
|
+
// leave a monitor that the cap still holds off labelled as a credit pause.
|
|
208
|
+
const offReason = !admits
|
|
209
|
+
? 'insufficient_balance'
|
|
210
|
+
: capExceeded
|
|
211
|
+
? 'monthly_cap_exceeded'
|
|
212
|
+
: null;
|
|
213
|
+
if (offReason !== null) {
|
|
189
214
|
if (input.row.desired === 'inactive') {
|
|
190
215
|
// Already off. Only re-label when policy already owned the reason: a
|
|
191
216
|
// customer's own deactivation must not be re-attributed to billing.
|
|
192
|
-
|
|
217
|
+
// A row whose event-time suspension owns recovery keeps its reason.
|
|
218
|
+
return reason === offReason
|
|
193
219
|
? { kind: 'none' }
|
|
194
|
-
: isPolicyDeactivation(reason)
|
|
195
|
-
|
|
220
|
+
: isPolicyDeactivation(reason) &&
|
|
221
|
+
!monitorBillingRequiresExplicitReactivation(input.row.billingState)
|
|
222
|
+
? { kind: 'deactivate', reason: offReason }
|
|
196
223
|
: { kind: 'none' };
|
|
197
224
|
}
|
|
198
|
-
|
|
225
|
+
// KAR-43 decision 8: a monthly-billed monitor inside the period it has
|
|
226
|
+
// already paid for keeps running. Its renewal charge is its funding gate.
|
|
227
|
+
//
|
|
228
|
+
// DIRECT MONITORS ONLY. A Fleet's billing suspension is one fleet-wide
|
|
229
|
+
// lifecycle: the Fleet becomes `billing_suspended`, and only then is it
|
|
230
|
+
// eligible for automatic reactivation, once EVERY member is disabled. A
|
|
231
|
+
// member left running would hold its Fleet in `billing_suspending`, which
|
|
232
|
+
// nothing recovers, so a Fleet member is switched off with its Fleet. The
|
|
233
|
+
// slot-month rule (`monitorPaidThroughAdmission`) still makes its
|
|
234
|
+
// recreate inside the paid period free to the customer.
|
|
235
|
+
if (
|
|
236
|
+
input.row.fleetId === undefined &&
|
|
237
|
+
monitorWithinPaidSubscriptionPeriod(
|
|
238
|
+
input.row.billingState,
|
|
239
|
+
input.now ?? Date.now(),
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
return { kind: 'none' };
|
|
243
|
+
}
|
|
244
|
+
return { kind: 'deactivate', reason: offReason };
|
|
199
245
|
}
|
|
200
|
-
// The balance
|
|
246
|
+
// The balance and the cap admit again. Revive only what THIS policy turned
|
|
247
|
+
// off.
|
|
201
248
|
//
|
|
202
249
|
// ASYMMETRIC ON `null`, DELIBERATELY. An unreadable wallet is permissive for
|
|
203
250
|
// deactivation - "we do not know" must not turn a customer's monitors off -
|
|
@@ -209,10 +256,47 @@ export function decideMonitorBalancePolicy(input: {
|
|
|
209
256
|
if (
|
|
210
257
|
input.balance !== null &&
|
|
211
258
|
input.row.desired === 'inactive' &&
|
|
212
|
-
reason
|
|
259
|
+
reason !== undefined &&
|
|
260
|
+
SPEND_POLICY_REASONS.has(reason) &&
|
|
213
261
|
!monitorBillingRequiresExplicitReactivation(input.row.billingState)
|
|
214
262
|
) {
|
|
215
263
|
return { kind: 'reactivate' };
|
|
216
264
|
}
|
|
217
265
|
return { kind: 'none' };
|
|
218
266
|
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// The shutdown notification (KAR-43)
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
/** The two reasons a workspace is told its Monitors were switched off. */
|
|
273
|
+
export type MonitorShutdownNotificationReason =
|
|
274
|
+
| 'insufficient_credits'
|
|
275
|
+
| 'monthly_cap_exceeded';
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The notification subject for "this workspace's Monitors". The shutdown
|
|
279
|
+
* notification is one per shutdown, not one per monitor, so its ordering
|
|
280
|
+
* subject is the workspace's Monitors as a whole.
|
|
281
|
+
*/
|
|
282
|
+
export const WORKSPACE_MONITORS_NOTIFICATION_SUBJECT = 'workspace-monitors';
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Collapse every billing reason that switches a Monitor off into the two the
|
|
286
|
+
* customer acts on: add credits, or raise the cap. Event-time denials, the
|
|
287
|
+
* policy's credit pause and the cap pause all name the same remedy. Anything
|
|
288
|
+
* else (an opaque billing failure, a customer stop) is not this notification.
|
|
289
|
+
*/
|
|
290
|
+
export function monitorShutdownNotificationReason(
|
|
291
|
+
reason: string | null | undefined,
|
|
292
|
+
): MonitorShutdownNotificationReason | null {
|
|
293
|
+
if (
|
|
294
|
+
reason === 'insufficient_credits' ||
|
|
295
|
+
reason === 'insufficient_credits_after_provider_event' ||
|
|
296
|
+
reason === 'insufficient_balance'
|
|
297
|
+
) {
|
|
298
|
+
return 'insufficient_credits';
|
|
299
|
+
}
|
|
300
|
+
if (reason === 'monthly_cap_exceeded') return 'monthly_cap_exceeded';
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
@@ -100,6 +100,9 @@ type DeadLetterMessageInput = Pick<
|
|
|
100
100
|
radarKey?: string | null;
|
|
101
101
|
eventIdempotencyKey?: string | null;
|
|
102
102
|
rawBody?: string;
|
|
103
|
+
postgresSchedulerSchema?: string | null;
|
|
104
|
+
runtimeApiBaseUrl?: string | null;
|
|
105
|
+
deadLetterId?: string | null;
|
|
103
106
|
};
|
|
104
107
|
|
|
105
108
|
async function markCapturedRawWriteFailed(
|
|
@@ -132,6 +135,12 @@ async function markCapturedRawWriteFailed(
|
|
|
132
135
|
// null-keyed poison events stay distinct (the partial index only enforces
|
|
133
136
|
// uniqueness WHERE event_idempotency_key IS NOT NULL). Also marks the captured
|
|
134
137
|
// raw row write_failed when one exists for the key.
|
|
138
|
+
//
|
|
139
|
+
// A redriven event that fails again returns its row to `open` with the
|
|
140
|
+
// attempts added up, so a row never claims `replaying` or `replayed` for an
|
|
141
|
+
// event that was not processed. A redrive message names its row
|
|
142
|
+
// (`deadLetterId`), which also covers a null-keyed event the index cannot
|
|
143
|
+
// match. A `discarded` row stays discarded.
|
|
135
144
|
export async function recordSignalRadarDeadLetter(input: {
|
|
136
145
|
client: InboundPgClient;
|
|
137
146
|
message: DeadLetterMessageInput;
|
|
@@ -151,18 +160,53 @@ export async function recordSignalRadarDeadLetter(input: {
|
|
|
151
160
|
headers: message.headers ?? {},
|
|
152
161
|
receivedAt: message.receivedAt,
|
|
153
162
|
admission: message.admission ?? null,
|
|
163
|
+
// A Preview callback runs on its own scheduler lane and calls back to its
|
|
164
|
+
// own origin; a redrive must keep both.
|
|
165
|
+
postgresSchedulerSchema: message.postgresSchedulerSchema ?? null,
|
|
166
|
+
runtimeApiBaseUrl: message.runtimeApiBaseUrl ?? null,
|
|
154
167
|
};
|
|
155
168
|
|
|
169
|
+
if (message.deadLetterId) {
|
|
170
|
+
const reopened = await client.query(
|
|
171
|
+
`UPDATE dl_meta.signal_radar_inbound_dead_letter
|
|
172
|
+
SET status = CASE WHEN status = 'discarded' THEN status ELSE 'open' END,
|
|
173
|
+
attempts = attempts + $2,
|
|
174
|
+
last_error = $3,
|
|
175
|
+
updated_at = now()
|
|
176
|
+
WHERE dead_letter_id = $1
|
|
177
|
+
RETURNING dead_letter_id`,
|
|
178
|
+
[message.deadLetterId, input.attempts, lastError.slice(0, 8000)],
|
|
179
|
+
);
|
|
180
|
+
if ((reopened.rows ?? []).length > 0) {
|
|
181
|
+
if (eventIdempotencyKey) {
|
|
182
|
+
await markCapturedRawWriteFailed(
|
|
183
|
+
client,
|
|
184
|
+
eventIdempotencyKey,
|
|
185
|
+
lastError,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
156
192
|
if (eventIdempotencyKey) {
|
|
157
193
|
await client.query(
|
|
158
|
-
`INSERT INTO dl_meta.signal_radar_inbound_dead_letter
|
|
194
|
+
`INSERT INTO dl_meta.signal_radar_inbound_dead_letter AS dead_letter
|
|
159
195
|
(dead_letter_id, provider, radar_key, org_id, event_idempotency_key,
|
|
160
196
|
payload, headers, attempts, last_error, status)
|
|
161
197
|
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8, $9, 'open')
|
|
162
198
|
ON CONFLICT (event_idempotency_key)
|
|
163
199
|
WHERE event_idempotency_key IS NOT NULL
|
|
164
200
|
DO UPDATE SET
|
|
165
|
-
|
|
201
|
+
status = CASE
|
|
202
|
+
WHEN dead_letter.status IN ('replaying', 'replayed') THEN 'open'
|
|
203
|
+
ELSE dead_letter.status
|
|
204
|
+
END,
|
|
205
|
+
attempts = CASE
|
|
206
|
+
WHEN dead_letter.status IN ('replaying', 'replayed')
|
|
207
|
+
THEN dead_letter.attempts + EXCLUDED.attempts
|
|
208
|
+
ELSE EXCLUDED.attempts
|
|
209
|
+
END,
|
|
166
210
|
last_error = EXCLUDED.last_error,
|
|
167
211
|
payload = EXCLUDED.payload,
|
|
168
212
|
headers = EXCLUDED.headers,
|
|
@@ -35,4 +35,8 @@ export type SignalRadarInboundMessage = {
|
|
|
35
35
|
// Optional radar hint the worker resolves durably from Neon; present only on
|
|
36
36
|
// dead-letter/replay envelopes where it was already known.
|
|
37
37
|
radarKey?: string | null;
|
|
38
|
+
// The tenant dead-letter row this message redrives. The worker settles that
|
|
39
|
+
// row from the outcome: `replayed` once processed, back to `open` when it
|
|
40
|
+
// dead-letters again. Absent on every first delivery.
|
|
41
|
+
deadLetterId?: string | null;
|
|
38
42
|
};
|
|
@@ -219,19 +219,19 @@ export const OPERATIONS_QUEUE_CATALOG = [
|
|
|
219
219
|
),
|
|
220
220
|
queue(
|
|
221
221
|
'signal-radar-inbound-legacy-dlq',
|
|
222
|
-
'Signal-radar
|
|
222
|
+
'Signal-radar inbound dead letters',
|
|
223
223
|
'provider_queue',
|
|
224
224
|
'monitors',
|
|
225
|
-
'Tenant-scoped
|
|
225
|
+
'Tenant-scoped inbound callbacks that exhausted their deliveries; redrive (retry) or retire one item; counts can overlap raw receipts',
|
|
226
226
|
300_000,
|
|
227
227
|
900_000,
|
|
228
228
|
),
|
|
229
229
|
queue(
|
|
230
230
|
'signal-radar-inbound-typed-write-failures',
|
|
231
|
-
'Signal-radar
|
|
231
|
+
'Signal-radar raw receipts not written',
|
|
232
232
|
'provider_queue',
|
|
233
233
|
'monitors',
|
|
234
|
-
'
|
|
234
|
+
'Read-only evidence: tenant raw callback receipts not written as rows, including deliberate raw-only captures; act on the dead-letter item',
|
|
235
235
|
300_000,
|
|
236
236
|
900_000,
|
|
237
237
|
),
|
|
@@ -909,6 +909,42 @@
|
|
|
909
909
|
"failureAlert": "convex_action_wrapper",
|
|
910
910
|
"domainReportRequired": true
|
|
911
911
|
},
|
|
912
|
+
{
|
|
913
|
+
"id": "convex.monitor-cap-change",
|
|
914
|
+
"name": "Monitor spending-cap change",
|
|
915
|
+
"scheduler": "convex_scheduled_action",
|
|
916
|
+
"cadence": "runAfter(0) when a workspace that has Monitors changes or removes its spending cap",
|
|
917
|
+
"timezone": "UTC",
|
|
918
|
+
"source": "packages/convex/scheduledJobsNode.ts",
|
|
919
|
+
"locator": "reactToCreditBalanceChange",
|
|
920
|
+
"owner": "billing",
|
|
921
|
+
"criticality": "critical",
|
|
922
|
+
"purpose": "Re-decide a workspace's Monitors against its changed spending cap: revive cap-paused Monitors through the fenced billing-recovery lifecycle once the cap admits, or switch them off when spend already exceeds a lowered cap.",
|
|
923
|
+
"successDefinition": "The bounded policy pass completes and submits any durable operation it needs, or records a valid no-op.",
|
|
924
|
+
"failureDefinition": "The policy pass cannot run. The hourly provider-monitor policy sweep remains the backstop.",
|
|
925
|
+
"startGraceMs": 120000,
|
|
926
|
+
"runTimeoutMs": 300000,
|
|
927
|
+
"failureAlert": "convex_action_wrapper",
|
|
928
|
+
"domainReportRequired": true
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
"id": "convex.monitor-spend-shutdown",
|
|
932
|
+
"name": "Monitor spend shutdown",
|
|
933
|
+
"scheduler": "convex_scheduled_action",
|
|
934
|
+
"cadence": "runAfter(0) from the per-workspace notification worker when a queued balance change goes below 0 or a spending-cap denial is recorded while a Monitor runs, and from a failed automatic recharge",
|
|
935
|
+
"timezone": "UTC",
|
|
936
|
+
"source": "packages/convex/scheduledJobsNode.ts",
|
|
937
|
+
"locator": "reactToCreditBalanceChange",
|
|
938
|
+
"owner": "billing",
|
|
939
|
+
"criticality": "critical",
|
|
940
|
+
"purpose": "Switch every Monitor in a workspace off for credit or the spending cap as soon as spend makes it necessary, instead of waiting for the hourly policy sweep. Direct monthly-billed Monitors inside their paid period are left running.",
|
|
941
|
+
"successDefinition": "The balance or cap is re-checked and the bounded policy pass submits its durable operations, or a valid no-op is recorded (balance back above 0, cap admits, automatic recharge in progress, no running Monitor).",
|
|
942
|
+
"failureDefinition": "The re-check or a policy page cannot run. The hourly provider-monitor policy sweep remains the backstop.",
|
|
943
|
+
"startGraceMs": 120000,
|
|
944
|
+
"runTimeoutMs": 300000,
|
|
945
|
+
"failureAlert": "convex_action_wrapper",
|
|
946
|
+
"domainReportRequired": true
|
|
947
|
+
},
|
|
912
948
|
{
|
|
913
949
|
"id": "inngest.workflow-run-watchdog",
|
|
914
950
|
"name": "Workflow run watchdog",
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import { PRODUCT_NOTIFICATION_EVENT_CATALOG } from './contract';
|
|
8
8
|
import {
|
|
9
9
|
ProductNotificationEvent,
|
|
10
|
+
ProductNotificationTargetPath,
|
|
10
11
|
getProductNotificationEventDefinition,
|
|
11
12
|
productNotificationTriggerIsAllowed,
|
|
12
13
|
type ProductNotificationEventName,
|
|
@@ -136,6 +137,30 @@ function validateInsufficientCreditDisable(
|
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
function validateMonthlyCapMonitorDisable(trigger: ProductNotificationTrigger) {
|
|
141
|
+
if (
|
|
142
|
+
trigger.kind !== 'state_transition' ||
|
|
143
|
+
trigger.targetState !== 'disabled' ||
|
|
144
|
+
trigger.reasonCode !== 'monthly_cap_exceeded'
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'Spending-cap Monitor rules must enter disabled for monthly_cap_exceeded.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* KAR-43 decision 7: a shutdown notification goes only to a workspace that
|
|
154
|
+
* has at least one Monitor. A workspace-wide shutdown event names how many
|
|
155
|
+
* Monitors it switched off; a per-Monitor event is itself a Monitor.
|
|
156
|
+
*/
|
|
157
|
+
function switchedOffAMonitor(payload: ProductNotificationFacts): boolean {
|
|
158
|
+
return (
|
|
159
|
+
payload.affectedMonitorCount === undefined ||
|
|
160
|
+
Number(payload.affectedMonitorCount) >= 1
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
function validateFailedOutcome(trigger: ProductNotificationTrigger) {
|
|
140
165
|
if (trigger.kind === 'consecutive_outcomes' && trigger.outcome !== 'failed') {
|
|
141
166
|
throw new Error('Failure rules require failed consecutive outcomes.');
|
|
@@ -321,14 +346,19 @@ const PRODUCT_NOTIFICATION_TYPE_REGISTRY = [
|
|
|
321
346
|
requiredFilter: {},
|
|
322
347
|
allowedTriggers: ['state_transition'],
|
|
323
348
|
catalog: {
|
|
324
|
-
name: '
|
|
349
|
+
name: 'Monitors turned off due to insufficient credits',
|
|
325
350
|
category: 'credits',
|
|
326
351
|
description:
|
|
327
|
-
|
|
352
|
+
"Notify when Deepline turns off the workspace's Monitors because it ran out of credits. Adding credits turns them back on.",
|
|
328
353
|
allowMultiple: false,
|
|
329
354
|
editor: { kind: 'none' },
|
|
330
355
|
},
|
|
331
356
|
rule: {
|
|
357
|
+
// KAR-43: on by default for workspace owners, once per shutdown. The
|
|
358
|
+
// workspace condition fires on the first Monitor switched off and
|
|
359
|
+
// rearms when the balance rises.
|
|
360
|
+
default: { ruleKey: 'default.monitor_disabled', groupKey: 'default' },
|
|
361
|
+
eligible: switchedOffAMonitor,
|
|
332
362
|
conditionSubject: 'workspace',
|
|
333
363
|
rearm: {
|
|
334
364
|
eventName: ProductNotificationEvent.CreditsBalanceChanged,
|
|
@@ -345,15 +375,72 @@ const PRODUCT_NOTIFICATION_TYPE_REGISTRY = [
|
|
|
345
375
|
}),
|
|
346
376
|
validate: validateInsufficientCreditDisable,
|
|
347
377
|
present: ({ payload }) => ({
|
|
348
|
-
title: '
|
|
378
|
+
title: 'Monitors turned off: out of credits',
|
|
349
379
|
summary: [
|
|
350
|
-
|
|
380
|
+
'Deepline turned off the Monitors in your ',
|
|
351
381
|
{ workspace: true },
|
|
352
|
-
' ran out of credits.',
|
|
382
|
+
' because it ran out of credits.',
|
|
353
383
|
],
|
|
354
|
-
details: [
|
|
355
|
-
|
|
356
|
-
|
|
384
|
+
details: [
|
|
385
|
+
'Add credits to turn them back on. Monitors resume automatically once the balance is above zero.',
|
|
386
|
+
'Events that arrive while Monitors are off are not delivered later.',
|
|
387
|
+
],
|
|
388
|
+
targetUrl: ProductNotificationTargetPath.Billing,
|
|
389
|
+
subjectKind: 'workspace_monitors',
|
|
390
|
+
subjectId: String(payload.monitorId),
|
|
391
|
+
severity: 'error',
|
|
392
|
+
}),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: 'monitor.disabled.monthly_cap_exceeded',
|
|
397
|
+
eventName: ProductNotificationEvent.MonitorLifecycleChanged,
|
|
398
|
+
requiredFilter: {},
|
|
399
|
+
allowedTriggers: ['state_transition'],
|
|
400
|
+
catalog: {
|
|
401
|
+
name: 'Monitors turned off at the spending cap',
|
|
402
|
+
category: 'credits',
|
|
403
|
+
description:
|
|
404
|
+
"Notify when Deepline turns off the workspace's Monitors because it reached its monthly spending cap. Raising the cap, or the next billing period, turns them back on.",
|
|
405
|
+
allowMultiple: false,
|
|
406
|
+
editor: { kind: 'none' },
|
|
407
|
+
},
|
|
408
|
+
rule: {
|
|
409
|
+
default: {
|
|
410
|
+
ruleKey: 'default.monitor_disabled_monthly_cap',
|
|
411
|
+
groupKey: 'default',
|
|
412
|
+
},
|
|
413
|
+
eligible: switchedOffAMonitor,
|
|
414
|
+
conditionSubject: 'workspace',
|
|
415
|
+
// Monitors coming back under the cap rearm it for the next shutdown.
|
|
416
|
+
rearm: {
|
|
417
|
+
eventName: ProductNotificationEvent.MonitorLifecycleChanged,
|
|
418
|
+
matches: (payload) =>
|
|
419
|
+
payload.currentState === 'active' &&
|
|
420
|
+
payload.reasonCode === 'monthly_cap_exceeded',
|
|
421
|
+
},
|
|
422
|
+
create: (_input, base) => ({
|
|
423
|
+
...base,
|
|
424
|
+
trigger: {
|
|
425
|
+
kind: 'state_transition',
|
|
426
|
+
targetState: 'disabled',
|
|
427
|
+
reasonCode: 'monthly_cap_exceeded',
|
|
428
|
+
},
|
|
429
|
+
}),
|
|
430
|
+
validate: validateMonthlyCapMonitorDisable,
|
|
431
|
+
present: ({ payload }) => ({
|
|
432
|
+
title: 'Monitors turned off: spending cap reached',
|
|
433
|
+
summary: [
|
|
434
|
+
'Deepline turned off the Monitors in your ',
|
|
435
|
+
{ workspace: true },
|
|
436
|
+
' because it reached its monthly spending cap.',
|
|
437
|
+
],
|
|
438
|
+
details: [
|
|
439
|
+
'Raise the cap to turn them back on. Otherwise they resume automatically when the next billing period begins.',
|
|
440
|
+
'Events that arrive while Monitors are off are not delivered later.',
|
|
441
|
+
],
|
|
442
|
+
targetUrl: ProductNotificationTargetPath.Billing,
|
|
443
|
+
subjectKind: 'workspace_monitors',
|
|
357
444
|
subjectId: String(payload.monitorId),
|
|
358
445
|
severity: 'error',
|
|
359
446
|
}),
|
|
@@ -128,7 +128,11 @@ export const PRODUCT_NOTIFICATION_EVENT_DEFINITIONS: readonly ProductNotificatio
|
|
|
128
128
|
targetUrl: 'string',
|
|
129
129
|
},
|
|
130
130
|
orderingSubjectFact: 'monitorId',
|
|
131
|
-
optionalFacts: {
|
|
131
|
+
optionalFacts: {
|
|
132
|
+
previousReasonCode: 'string',
|
|
133
|
+
/** A workspace-wide shutdown names how many Monitors it switched off. */
|
|
134
|
+
affectedMonitorCount: 'number',
|
|
135
|
+
},
|
|
132
136
|
allowedTriggers: ['state_transition'],
|
|
133
137
|
},
|
|
134
138
|
{
|