switchroom 0.14.84 → 0.14.85
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/cli/switchroom.js
CHANGED
|
@@ -49815,8 +49815,8 @@ var {
|
|
|
49815
49815
|
} = import__.default;
|
|
49816
49816
|
|
|
49817
49817
|
// src/build-info.ts
|
|
49818
|
-
var VERSION = "0.14.
|
|
49819
|
-
var COMMIT_SHA = "
|
|
49818
|
+
var VERSION = "0.14.85";
|
|
49819
|
+
var COMMIT_SHA = "292c683f";
|
|
49820
49820
|
|
|
49821
49821
|
// src/cli/agent.ts
|
|
49822
49822
|
init_source();
|
package/package.json
CHANGED
|
@@ -27,20 +27,53 @@ import { join } from "path";
|
|
|
27
27
|
const STATE_FILE = "credits-watch.json";
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* that warrant a user-facing notification. Other values (null,
|
|
32
|
-
* undefined, transient unknowns) are treated as "no notification
|
|
33
|
-
* needed".
|
|
30
|
+
* Which `cachedExtraUsageDisabledReason` values warrant a user-facing alarm.
|
|
34
31
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* IMPORTANT — empty by default (subscription-only model). The flag name says it
|
|
33
|
+
* all: `cachedExtraUsageDisabledReason` is *always* "why **extra usage** (the
|
|
34
|
+
* optional pay-as-you-go layer beyond the Pro/Max plan) is disabled." switchroom
|
|
35
|
+
* is subscription-only by design (compliance pillar 3 — "the plan is the
|
|
36
|
+
* ceiling, no API/pay-as-you-go"), so extra usage being OFF is the **expected,
|
|
37
|
+
* desired** state. Every value (`out_of_credits`, `extra_usage_disabled`,
|
|
38
|
+
* `credits_exhausted`, even `org_level_disabled` = "org admin disabled extra
|
|
39
|
+
* usage") describes that benign state — none indicates a real switchroom
|
|
40
|
+
* failure. Firing a "cron + replies will fail, buy credits" card on it is a
|
|
41
|
+
* false alarm AND advises something that contradicts the subscription-honest
|
|
42
|
+
* model.
|
|
43
|
+
*
|
|
44
|
+
* The genuine failure modes are covered elsewhere:
|
|
45
|
+
* - an account's subscription window exhausts → stderr → fleet auto-fallback
|
|
46
|
+
* (model-unavailable.ts → fireFleetAutoFallback) rolls to a healthy account;
|
|
47
|
+
* - the WHOLE fleet exhausts → the quota-watch all-exhausted operator alert.
|
|
48
|
+
*
|
|
49
|
+
* So credits-watch defaults to silent. An operator who actually runs on
|
|
50
|
+
* pay-as-you-go and wants the alarm can opt specific reasons back in via
|
|
51
|
+
* `SWITCHROOM_CREDITS_WATCH_FATAL_REASONS` (comma-separated). See
|
|
52
|
+
* `resolveCreditWatchFatalReasons`.
|
|
37
53
|
*/
|
|
38
|
-
const
|
|
54
|
+
export const DEFAULT_CREDIT_FATAL_REASONS = new Set<string>();
|
|
55
|
+
|
|
56
|
+
/** All reason strings recognized for opt-in via the env var. */
|
|
57
|
+
const KNOWN_CREDIT_REASONS = [
|
|
39
58
|
"out_of_credits",
|
|
40
59
|
"org_level_disabled",
|
|
41
60
|
"credits_exhausted",
|
|
42
61
|
"extra_usage_disabled",
|
|
43
|
-
]
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the active fatal-reason set. Empty by default (subscription-only);
|
|
66
|
+
* if `SWITCHROOM_CREDITS_WATCH_FATAL_REASONS` is set, parse it as a
|
|
67
|
+
* comma-separated list (tokens kept verbatim — an unknown token is harmless,
|
|
68
|
+
* it simply never matches a real reason value). `*` opts in all known reasons.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveCreditWatchFatalReasons(env: NodeJS.ProcessEnv): Set<string> {
|
|
71
|
+
const raw = env.SWITCHROOM_CREDITS_WATCH_FATAL_REASONS;
|
|
72
|
+
if (!raw || raw.trim().length === 0) return new Set(DEFAULT_CREDIT_FATAL_REASONS);
|
|
73
|
+
if (raw.trim() === "*") return new Set(KNOWN_CREDIT_REASONS);
|
|
74
|
+
const wanted = new Set(raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0));
|
|
75
|
+
return wanted;
|
|
76
|
+
}
|
|
44
77
|
|
|
45
78
|
export interface CreditState {
|
|
46
79
|
/** Last reason we notified the user about. null when healthy / never notified. */
|
|
@@ -103,15 +136,22 @@ export function evaluateCreditState(args: {
|
|
|
103
136
|
currentReason: string | null;
|
|
104
137
|
prev: CreditState;
|
|
105
138
|
now: number;
|
|
139
|
+
/**
|
|
140
|
+
* Which reasons are treated as fatal (worth alarming). Defaults to
|
|
141
|
+
* `DEFAULT_CREDIT_FATAL_REASONS` (empty — subscription-only: extra-usage-off
|
|
142
|
+
* is never a real failure). The gateway passes the env-resolved set.
|
|
143
|
+
*/
|
|
144
|
+
fatalReasons?: Set<string>;
|
|
106
145
|
}): CreditDecision {
|
|
107
146
|
const { agentName, currentReason, prev, now } = args;
|
|
147
|
+
const fatalReasons = args.fatalReasons ?? DEFAULT_CREDIT_FATAL_REASONS;
|
|
108
148
|
|
|
109
149
|
// Non-fatal current state (null, or some unknown reason) — no
|
|
110
150
|
// notification regardless of prev (we already notified on entry to
|
|
111
151
|
// fatal; recovery from a known-fatal state below is the only path
|
|
112
152
|
// that fires when current is null).
|
|
113
|
-
const currentIsFatal = currentReason != null &&
|
|
114
|
-
const prevIsFatal = prev.lastNotifiedReason != null &&
|
|
153
|
+
const currentIsFatal = currentReason != null && fatalReasons.has(currentReason);
|
|
154
|
+
const prevIsFatal = prev.lastNotifiedReason != null && fatalReasons.has(prev.lastNotifiedReason);
|
|
115
155
|
|
|
116
156
|
// Recovery path: last-notified was fatal, current is null/non-fatal.
|
|
117
157
|
if (!currentIsFatal && prevIsFatal) {
|
|
@@ -52507,12 +52507,22 @@ function extractFlowItems(line) {
|
|
|
52507
52507
|
import { readFileSync as readFileSync32, writeFileSync as writeFileSync21, existsSync as existsSync33, mkdirSync as mkdirSync21 } from "fs";
|
|
52508
52508
|
import { join as join30 } from "path";
|
|
52509
52509
|
var STATE_FILE = "credits-watch.json";
|
|
52510
|
-
var
|
|
52510
|
+
var DEFAULT_CREDIT_FATAL_REASONS = new Set;
|
|
52511
|
+
var KNOWN_CREDIT_REASONS = [
|
|
52511
52512
|
"out_of_credits",
|
|
52512
52513
|
"org_level_disabled",
|
|
52513
52514
|
"credits_exhausted",
|
|
52514
52515
|
"extra_usage_disabled"
|
|
52515
|
-
]
|
|
52516
|
+
];
|
|
52517
|
+
function resolveCreditWatchFatalReasons(env) {
|
|
52518
|
+
const raw = env.SWITCHROOM_CREDITS_WATCH_FATAL_REASONS;
|
|
52519
|
+
if (!raw || raw.trim().length === 0)
|
|
52520
|
+
return new Set(DEFAULT_CREDIT_FATAL_REASONS);
|
|
52521
|
+
if (raw.trim() === "*")
|
|
52522
|
+
return new Set(KNOWN_CREDIT_REASONS);
|
|
52523
|
+
const wanted = new Set(raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0));
|
|
52524
|
+
return wanted;
|
|
52525
|
+
}
|
|
52516
52526
|
function emptyCreditState() {
|
|
52517
52527
|
return { lastNotifiedReason: null, lastNotifiedAt: 0 };
|
|
52518
52528
|
}
|
|
@@ -52541,8 +52551,9 @@ function readClaudeJsonOverage(claudeConfigDir) {
|
|
|
52541
52551
|
}
|
|
52542
52552
|
function evaluateCreditState(args) {
|
|
52543
52553
|
const { agentName: agentName3, currentReason, prev, now } = args;
|
|
52544
|
-
const
|
|
52545
|
-
const
|
|
52554
|
+
const fatalReasons = args.fatalReasons ?? DEFAULT_CREDIT_FATAL_REASONS;
|
|
52555
|
+
const currentIsFatal = currentReason != null && fatalReasons.has(currentReason);
|
|
52556
|
+
const prevIsFatal = prev.lastNotifiedReason != null && fatalReasons.has(prev.lastNotifiedReason);
|
|
52546
52557
|
if (!currentIsFatal && prevIsFatal) {
|
|
52547
52558
|
return {
|
|
52548
52559
|
kind: "notify",
|
|
@@ -52869,9 +52880,9 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52869
52880
|
}
|
|
52870
52881
|
|
|
52871
52882
|
// ../src/build-info.ts
|
|
52872
|
-
var VERSION = "0.14.
|
|
52873
|
-
var COMMIT_SHA = "
|
|
52874
|
-
var COMMIT_DATE = "2026-06-
|
|
52883
|
+
var VERSION = "0.14.85";
|
|
52884
|
+
var COMMIT_SHA = "292c683f";
|
|
52885
|
+
var COMMIT_DATE = "2026-06-07T14:50:17+10:00";
|
|
52875
52886
|
var LATEST_PR = null;
|
|
52876
52887
|
var COMMITS_AHEAD_OF_TAG = 2;
|
|
52877
52888
|
|
|
@@ -61040,7 +61051,8 @@ async function runCreditWatch() {
|
|
|
61040
61051
|
agentName: agentName3,
|
|
61041
61052
|
currentReason: reason,
|
|
61042
61053
|
prev,
|
|
61043
|
-
now: Date.now()
|
|
61054
|
+
now: Date.now(),
|
|
61055
|
+
fatalReasons: resolveCreditWatchFatalReasons(process.env)
|
|
61044
61056
|
});
|
|
61045
61057
|
if (decision.kind === "skip") {
|
|
61046
61058
|
return;
|
|
@@ -408,6 +408,7 @@ import { synthesizeAllowRuleDiff, extractAddedAllowRule } from '../permission-di
|
|
|
408
408
|
import {
|
|
409
409
|
readClaudeJsonOverage,
|
|
410
410
|
evaluateCreditState,
|
|
411
|
+
resolveCreditWatchFatalReasons,
|
|
411
412
|
loadCreditState,
|
|
412
413
|
saveCreditState,
|
|
413
414
|
} from '../credits-watch.js'
|
|
@@ -14742,6 +14743,9 @@ async function runCreditWatch(): Promise<void> {
|
|
|
14742
14743
|
currentReason: reason,
|
|
14743
14744
|
prev,
|
|
14744
14745
|
now: Date.now(),
|
|
14746
|
+
// Subscription-only: empty by default → extra-usage-off never alarms.
|
|
14747
|
+
// Opt back in via SWITCHROOM_CREDITS_WATCH_FATAL_REASONS. See credits-watch.ts.
|
|
14748
|
+
fatalReasons: resolveCreditWatchFatalReasons(process.env),
|
|
14745
14749
|
})
|
|
14746
14750
|
if (decision.kind === 'skip') {
|
|
14747
14751
|
return
|
|
@@ -13,6 +13,7 @@ import { join } from "path";
|
|
|
13
13
|
import {
|
|
14
14
|
readClaudeJsonOverage,
|
|
15
15
|
evaluateCreditState,
|
|
16
|
+
resolveCreditWatchFatalReasons,
|
|
16
17
|
loadCreditState,
|
|
17
18
|
saveCreditState,
|
|
18
19
|
emptyCreditState,
|
|
@@ -81,10 +82,14 @@ describe("readClaudeJsonOverage", () => {
|
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
|
|
84
|
-
describe("evaluateCreditState — transition decisions", () => {
|
|
85
|
+
describe("evaluateCreditState — transition decisions (machinery, explicit fatal set)", () => {
|
|
85
86
|
const NOW = 1_780_000_000_000;
|
|
86
87
|
const HEALTHY = emptyCreditState();
|
|
87
88
|
const FATAL_OUT = { lastNotifiedReason: "out_of_credits", lastNotifiedAt: NOW - 1000 };
|
|
89
|
+
// The transition machinery is policy-agnostic — pass an explicit fatal set so
|
|
90
|
+
// these tests pin entered/changed/exited behaviour independent of the (now
|
|
91
|
+
// empty) subscription-only default.
|
|
92
|
+
const FATAL = new Set(["out_of_credits", "org_level_disabled", "credits_exhausted", "extra_usage_disabled"]);
|
|
88
93
|
|
|
89
94
|
it("entry: healthy → fatal triggers a notify", () => {
|
|
90
95
|
const d = evaluateCreditState({
|
|
@@ -92,6 +97,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
92
97
|
currentReason: "out_of_credits",
|
|
93
98
|
prev: HEALTHY,
|
|
94
99
|
now: NOW,
|
|
100
|
+
fatalReasons: FATAL,
|
|
95
101
|
});
|
|
96
102
|
expect(d.kind).toBe("notify");
|
|
97
103
|
if (d.kind !== "notify") return;
|
|
@@ -108,6 +114,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
108
114
|
currentReason: "out_of_credits",
|
|
109
115
|
prev: FATAL_OUT,
|
|
110
116
|
now: NOW,
|
|
117
|
+
fatalReasons: FATAL,
|
|
111
118
|
});
|
|
112
119
|
expect(d.kind).toBe("skip");
|
|
113
120
|
if (d.kind !== "skip") return;
|
|
@@ -120,6 +127,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
120
127
|
currentReason: "org_level_disabled",
|
|
121
128
|
prev: FATAL_OUT,
|
|
122
129
|
now: NOW,
|
|
130
|
+
fatalReasons: FATAL,
|
|
123
131
|
});
|
|
124
132
|
expect(d.kind).toBe("notify");
|
|
125
133
|
if (d.kind !== "notify") return;
|
|
@@ -134,6 +142,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
134
142
|
currentReason: null,
|
|
135
143
|
prev: FATAL_OUT,
|
|
136
144
|
now: NOW,
|
|
145
|
+
fatalReasons: FATAL,
|
|
137
146
|
});
|
|
138
147
|
expect(d.kind).toBe("notify");
|
|
139
148
|
if (d.kind !== "notify") return;
|
|
@@ -148,6 +157,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
148
157
|
currentReason: "some_unknown_transient_reason",
|
|
149
158
|
prev: HEALTHY,
|
|
150
159
|
now: NOW,
|
|
160
|
+
fatalReasons: FATAL,
|
|
151
161
|
});
|
|
152
162
|
expect(d.kind).toBe("skip");
|
|
153
163
|
if (d.kind !== "skip") return;
|
|
@@ -160,6 +170,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
160
170
|
currentReason: null,
|
|
161
171
|
prev: HEALTHY,
|
|
162
172
|
now: NOW,
|
|
173
|
+
fatalReasons: FATAL,
|
|
163
174
|
});
|
|
164
175
|
expect(d.kind).toBe("skip");
|
|
165
176
|
});
|
|
@@ -170,6 +181,7 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
170
181
|
currentReason: "out_of_credits",
|
|
171
182
|
prev: HEALTHY,
|
|
172
183
|
now: NOW,
|
|
184
|
+
fatalReasons: FATAL,
|
|
173
185
|
});
|
|
174
186
|
expect(d.kind).toBe("notify");
|
|
175
187
|
if (d.kind !== "notify") return;
|
|
@@ -178,6 +190,58 @@ describe("evaluateCreditState — transition decisions", () => {
|
|
|
178
190
|
});
|
|
179
191
|
});
|
|
180
192
|
|
|
193
|
+
describe("evaluateCreditState — subscription-only default (the fix)", () => {
|
|
194
|
+
const NOW = 1_780_000_000_000;
|
|
195
|
+
const HEALTHY = emptyCreditState();
|
|
196
|
+
|
|
197
|
+
// With the default (empty) fatal set, NONE of the extra-usage reasons alarm —
|
|
198
|
+
// because for subscription-only switchroom, extra-usage-off is the expected
|
|
199
|
+
// state and real exhaustion is handled by failover. This is the bug fix.
|
|
200
|
+
for (const reason of ["out_of_credits", "extra_usage_disabled", "credits_exhausted", "org_level_disabled"]) {
|
|
201
|
+
it(`default: '${reason}' does NOT alarm (no false 'out of credits' card)`, () => {
|
|
202
|
+
const d = evaluateCreditState({
|
|
203
|
+
agentName: "clerk",
|
|
204
|
+
currentReason: reason,
|
|
205
|
+
prev: HEALTHY,
|
|
206
|
+
now: NOW,
|
|
207
|
+
// fatalReasons omitted → DEFAULT_CREDIT_FATAL_REASONS (empty)
|
|
208
|
+
});
|
|
209
|
+
expect(d.kind).toBe("skip");
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
it("opt-in via explicit set restores the alarm (operator on pay-as-you-go)", () => {
|
|
214
|
+
const d = evaluateCreditState({
|
|
215
|
+
agentName: "clerk",
|
|
216
|
+
currentReason: "out_of_credits",
|
|
217
|
+
prev: HEALTHY,
|
|
218
|
+
now: NOW,
|
|
219
|
+
fatalReasons: new Set(["out_of_credits"]),
|
|
220
|
+
});
|
|
221
|
+
expect(d.kind).toBe("notify");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("resolveCreditWatchFatalReasons", () => {
|
|
226
|
+
it("defaults to EMPTY (subscription-only)", () => {
|
|
227
|
+
expect(resolveCreditWatchFatalReasons({}).size).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
it("parses a comma-separated opt-in list", () => {
|
|
230
|
+
const s = resolveCreditWatchFatalReasons({ SWITCHROOM_CREDITS_WATCH_FATAL_REASONS: "out_of_credits, org_level_disabled" });
|
|
231
|
+
expect(s.has("out_of_credits")).toBe(true);
|
|
232
|
+
expect(s.has("org_level_disabled")).toBe(true);
|
|
233
|
+
expect(s.size).toBe(2);
|
|
234
|
+
});
|
|
235
|
+
it("'*' opts in all known reasons", () => {
|
|
236
|
+
const s = resolveCreditWatchFatalReasons({ SWITCHROOM_CREDITS_WATCH_FATAL_REASONS: "*" });
|
|
237
|
+
expect(s.has("out_of_credits")).toBe(true);
|
|
238
|
+
expect(s.size).toBeGreaterThanOrEqual(4);
|
|
239
|
+
});
|
|
240
|
+
it("blank/whitespace → empty", () => {
|
|
241
|
+
expect(resolveCreditWatchFatalReasons({ SWITCHROOM_CREDITS_WATCH_FATAL_REASONS: " " }).size).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
181
245
|
describe("loadCreditState / saveCreditState — round-trip", () => {
|
|
182
246
|
let tmp: string;
|
|
183
247
|
|