instar 1.3.457 → 1.3.459
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 +42 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +16 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ProactiveSwapMonitor.d.ts +149 -0
- package/dist/core/ProactiveSwapMonitor.d.ts.map +1 -0
- package/dist/core/ProactiveSwapMonitor.js +208 -0
- package/dist/core/ProactiveSwapMonitor.js.map +1 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/GrowthMilestoneAnalyst.d.ts +21 -3
- package/dist/monitoring/GrowthMilestoneAnalyst.d.ts.map +1 -1
- package/dist/monitoring/GrowthMilestoneAnalyst.js +50 -1
- package/dist/monitoring/GrowthMilestoneAnalyst.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +4 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +4 -1
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +27 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.3.458.md +62 -0
- package/upgrades/1.3.459.md +42 -0
- package/upgrades/side-effects/dev-gate-conformance-r6.md +73 -0
- package/upgrades/side-effects/proactive-prelimit-swap.md +64 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProactiveSwapMonitor — the PRE-LIMIT half of the Subscription & Auth Standard's
|
|
3
|
+
* continuity guarantee (P1.3).
|
|
4
|
+
*
|
|
5
|
+
* The reactive path (`autoSwapOnRateLimit`) only fires AFTER a session hits its
|
|
6
|
+
* account's wall: a RateLimitSentinel escalation on an ACTUAL throttle signal.
|
|
7
|
+
* So a session blips at the wall before recovering — and an UNTAGGED session
|
|
8
|
+
* (one with no `subscriptionAccountId`, e.g. the primary interactive session
|
|
9
|
+
* running on the default config) has nothing for the swap engine to grab, so it
|
|
10
|
+
* just wedges until an operator manually swaps the default login. That is the
|
|
11
|
+
* exact 2026-06-09 failure that motivated this monitor.
|
|
12
|
+
*
|
|
13
|
+
* This monitor moves a session OFF an account BEFORE it walls, at a lag-aware
|
|
14
|
+
* measured threshold below the real limit.
|
|
15
|
+
*
|
|
16
|
+
* ── Why a separate, lower threshold (lag-aware) ──
|
|
17
|
+
* The QuotaPoller reads utilization periodically, so our reading TRAILS the real
|
|
18
|
+
* usage (measured live: our ~90% == Anthropic's ~95% on the same account). A
|
|
19
|
+
* pre-emptive swap therefore triggers at a LOWER measured threshold (default 80)
|
|
20
|
+
* to leave margin for the lag — by the time we read 80%, real is higher, and the
|
|
21
|
+
* swap completes before the wall.
|
|
22
|
+
*
|
|
23
|
+
* ── Effective-account resolution (covers the session you actually use) ──
|
|
24
|
+
* A session carries `subscriptionAccountId` only if it was pinned at spawn. The
|
|
25
|
+
* primary interactive session usually runs on the DEFAULT config (untagged), so
|
|
26
|
+
* the monitor resolves an untagged session's effective account from the default
|
|
27
|
+
* config's live login (InUseAccountResolver). Without this, the session a user
|
|
28
|
+
* is actively in would be invisible to the swap engine and wedge at the wall.
|
|
29
|
+
*
|
|
30
|
+
* ── Bounded, non-storming ──
|
|
31
|
+
* Per evaluation: only accounts AT pressure that have a sub-threshold ALTERNATE
|
|
32
|
+
* are sources; candidates are sorted newest-first (the just-(re)started
|
|
33
|
+
* interactive session ranks first) and capped per cycle; each swapped session
|
|
34
|
+
* enters a cooldown so a slow restart isn't double-swapped. Near the wall the
|
|
35
|
+
* monitor triggers a fresh poll so a fast burn isn't missed between the
|
|
36
|
+
* low-frequency baseline polls.
|
|
37
|
+
*
|
|
38
|
+
* Gated OFF by default (moving live sessions is real authority — same authority
|
|
39
|
+
* as `autoSwapOnRateLimit`, just an earlier trigger). The decision core is pure
|
|
40
|
+
* (injected deps) so it tests with zero sessions and zero network.
|
|
41
|
+
*/
|
|
42
|
+
import type { SubscriptionAccount } from './SubscriptionPool.js';
|
|
43
|
+
/** A running, swap-eligible session as the monitor sees it. */
|
|
44
|
+
export interface ProactiveSwapSession {
|
|
45
|
+
/** tmux session name (what the swap path keys on). */
|
|
46
|
+
sessionName: string;
|
|
47
|
+
/** The pool account this session is tagged with, or null if untagged
|
|
48
|
+
* (untagged ⇒ running on the default config ⇒ resolved via the default login). */
|
|
49
|
+
accountId: string | null;
|
|
50
|
+
/** ISO start time — newest-first ordering proxy for "most recently active". */
|
|
51
|
+
startedAt?: string;
|
|
52
|
+
}
|
|
53
|
+
/** The shape returned by the injected swap (a subset of QuotaAwareScheduler.SwapResult). */
|
|
54
|
+
export interface ProactiveSwapOutcome {
|
|
55
|
+
swapped: boolean;
|
|
56
|
+
toAccountId: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface ProactiveSwapMonitorConfig {
|
|
59
|
+
/** Current pool accounts (e.g. () => pool.list()). */
|
|
60
|
+
listAccounts: () => SubscriptionAccount[];
|
|
61
|
+
/** Currently-running, swap-eligible (claude-code) sessions. */
|
|
62
|
+
listRunningSessions: () => ProactiveSwapSession[];
|
|
63
|
+
/** The pool account the DEFAULT config is logged into right now (or null).
|
|
64
|
+
* Untagged sessions run here; from InUseAccountResolver in production. */
|
|
65
|
+
resolveDefaultAccountId: () => Promise<string | null>;
|
|
66
|
+
/** Performs the actual swap (wraps QuotaAwareScheduler.onQuotaPressure). */
|
|
67
|
+
swap: (args: {
|
|
68
|
+
sessionName: string;
|
|
69
|
+
exhaustedAccountId: string;
|
|
70
|
+
nowMs: number;
|
|
71
|
+
}) => Promise<ProactiveSwapOutcome>;
|
|
72
|
+
/** Optional fresh-poll trigger, awaited when an account is in the watch zone. */
|
|
73
|
+
triggerPoll?: () => Promise<unknown>;
|
|
74
|
+
/** Measured binding-window utilization % that triggers a pre-emptive swap. Default 80. */
|
|
75
|
+
thresholdPct?: number;
|
|
76
|
+
/** When an at-risk account is within this many points of the threshold, the
|
|
77
|
+
* monitor refreshes the poll before deciding (so a fast burn isn't missed
|
|
78
|
+
* between baseline polls). Default 15 (i.e. watch zone starts at 65%). */
|
|
79
|
+
watchMarginPct?: number;
|
|
80
|
+
/** Max sessions swapped per evaluation cycle (storm guard). Default 3. */
|
|
81
|
+
maxSwapsPerCycle?: number;
|
|
82
|
+
/** Per-session cooldown after a successful swap before it's eligible again.
|
|
83
|
+
* Default 600000 (10m) — must exceed the swap+restart time. */
|
|
84
|
+
cooldownMs?: number;
|
|
85
|
+
/** Monitor tick cadence. Default 180000 (3m). */
|
|
86
|
+
tickMs?: number;
|
|
87
|
+
/** Injected for tests. */
|
|
88
|
+
now?: () => number;
|
|
89
|
+
logger?: {
|
|
90
|
+
log: (m: string) => void;
|
|
91
|
+
warn: (m: string) => void;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export interface ProactiveSwapTickResult {
|
|
95
|
+
/** Session names that were actually swapped this pass. */
|
|
96
|
+
swapped: string[];
|
|
97
|
+
/** How many sessions were eligible (at-pressure, has-alternate, off-cooldown). */
|
|
98
|
+
considered: number;
|
|
99
|
+
/** Whether a fresh poll was triggered before the decision (watch zone). */
|
|
100
|
+
refreshed: boolean;
|
|
101
|
+
}
|
|
102
|
+
export declare class ProactiveSwapMonitor {
|
|
103
|
+
private readonly cfg;
|
|
104
|
+
private readonly thresholdPct;
|
|
105
|
+
private readonly watchMarginPct;
|
|
106
|
+
private readonly maxSwapsPerCycle;
|
|
107
|
+
private readonly cooldownMs;
|
|
108
|
+
private readonly tickMs;
|
|
109
|
+
private readonly now;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
/** Last successful-swap timestamp per session (cooldown bookkeeping). */
|
|
112
|
+
private readonly lastSwapAt;
|
|
113
|
+
private timer;
|
|
114
|
+
private ticking;
|
|
115
|
+
private lastResult;
|
|
116
|
+
constructor(cfg: ProactiveSwapMonitorConfig);
|
|
117
|
+
start(): void;
|
|
118
|
+
stop(): void;
|
|
119
|
+
/** Status for the read route (never throws). */
|
|
120
|
+
status(): {
|
|
121
|
+
thresholdPct: number;
|
|
122
|
+
watchPct: number;
|
|
123
|
+
maxSwapsPerCycle: number;
|
|
124
|
+
cooldownMs: number;
|
|
125
|
+
tickMs: number;
|
|
126
|
+
running: boolean;
|
|
127
|
+
lastResult: ProactiveSwapTickResult | null;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* One monitor pass: if any at-risk account is in the watch zone, refresh the
|
|
131
|
+
* quota poll first (fresh data near the wall), then evaluate + swap.
|
|
132
|
+
*/
|
|
133
|
+
tick(): Promise<ProactiveSwapTickResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Pure-ish decision + swap on the CURRENT snapshots (no poll refresh). Exposed
|
|
136
|
+
* for tests and the on-demand check route.
|
|
137
|
+
*/
|
|
138
|
+
evaluate(): Promise<{
|
|
139
|
+
swapped: string[];
|
|
140
|
+
considered: number;
|
|
141
|
+
}>;
|
|
142
|
+
/**
|
|
143
|
+
* Map running sessions to candidates whose EFFECTIVE account is at/over minPct.
|
|
144
|
+
* Effective account = the session's tag, else the default-config login (so the
|
|
145
|
+
* untagged interactive session is visible). Resolves the default login once.
|
|
146
|
+
*/
|
|
147
|
+
private mapCandidates;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=ProactiveSwapMonitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProactiveSwapMonitor.d.ts","sourceRoot":"","sources":["../../src/core/ProactiveSwapMonitor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProactiveSwapMonitor — the PRE-LIMIT half of the Subscription & Auth Standard's
|
|
3
|
+
* continuity guarantee (P1.3).
|
|
4
|
+
*
|
|
5
|
+
* The reactive path (`autoSwapOnRateLimit`) only fires AFTER a session hits its
|
|
6
|
+
* account's wall: a RateLimitSentinel escalation on an ACTUAL throttle signal.
|
|
7
|
+
* So a session blips at the wall before recovering — and an UNTAGGED session
|
|
8
|
+
* (one with no `subscriptionAccountId`, e.g. the primary interactive session
|
|
9
|
+
* running on the default config) has nothing for the swap engine to grab, so it
|
|
10
|
+
* just wedges until an operator manually swaps the default login. That is the
|
|
11
|
+
* exact 2026-06-09 failure that motivated this monitor.
|
|
12
|
+
*
|
|
13
|
+
* This monitor moves a session OFF an account BEFORE it walls, at a lag-aware
|
|
14
|
+
* measured threshold below the real limit.
|
|
15
|
+
*
|
|
16
|
+
* ── Why a separate, lower threshold (lag-aware) ──
|
|
17
|
+
* The QuotaPoller reads utilization periodically, so our reading TRAILS the real
|
|
18
|
+
* usage (measured live: our ~90% == Anthropic's ~95% on the same account). A
|
|
19
|
+
* pre-emptive swap therefore triggers at a LOWER measured threshold (default 80)
|
|
20
|
+
* to leave margin for the lag — by the time we read 80%, real is higher, and the
|
|
21
|
+
* swap completes before the wall.
|
|
22
|
+
*
|
|
23
|
+
* ── Effective-account resolution (covers the session you actually use) ──
|
|
24
|
+
* A session carries `subscriptionAccountId` only if it was pinned at spawn. The
|
|
25
|
+
* primary interactive session usually runs on the DEFAULT config (untagged), so
|
|
26
|
+
* the monitor resolves an untagged session's effective account from the default
|
|
27
|
+
* config's live login (InUseAccountResolver). Without this, the session a user
|
|
28
|
+
* is actively in would be invisible to the swap engine and wedge at the wall.
|
|
29
|
+
*
|
|
30
|
+
* ── Bounded, non-storming ──
|
|
31
|
+
* Per evaluation: only accounts AT pressure that have a sub-threshold ALTERNATE
|
|
32
|
+
* are sources; candidates are sorted newest-first (the just-(re)started
|
|
33
|
+
* interactive session ranks first) and capped per cycle; each swapped session
|
|
34
|
+
* enters a cooldown so a slow restart isn't double-swapped. Near the wall the
|
|
35
|
+
* monitor triggers a fresh poll so a fast burn isn't missed between the
|
|
36
|
+
* low-frequency baseline polls.
|
|
37
|
+
*
|
|
38
|
+
* Gated OFF by default (moving live sessions is real authority — same authority
|
|
39
|
+
* as `autoSwapOnRateLimit`, just an earlier trigger). The decision core is pure
|
|
40
|
+
* (injected deps) so it tests with zero sessions and zero network.
|
|
41
|
+
*/
|
|
42
|
+
import { selectAccount, accountAtPressure, } from './QuotaAwareScheduler.js';
|
|
43
|
+
export class ProactiveSwapMonitor {
|
|
44
|
+
cfg;
|
|
45
|
+
thresholdPct;
|
|
46
|
+
watchMarginPct;
|
|
47
|
+
maxSwapsPerCycle;
|
|
48
|
+
cooldownMs;
|
|
49
|
+
tickMs;
|
|
50
|
+
now;
|
|
51
|
+
logger;
|
|
52
|
+
/** Last successful-swap timestamp per session (cooldown bookkeeping). */
|
|
53
|
+
lastSwapAt = new Map();
|
|
54
|
+
timer = null;
|
|
55
|
+
ticking = false;
|
|
56
|
+
lastResult = null;
|
|
57
|
+
constructor(cfg) {
|
|
58
|
+
this.cfg = cfg;
|
|
59
|
+
this.thresholdPct = cfg.thresholdPct ?? 80;
|
|
60
|
+
this.watchMarginPct = cfg.watchMarginPct ?? 15;
|
|
61
|
+
this.maxSwapsPerCycle = cfg.maxSwapsPerCycle ?? 3;
|
|
62
|
+
this.cooldownMs = cfg.cooldownMs ?? 600_000;
|
|
63
|
+
this.tickMs = cfg.tickMs ?? 180_000;
|
|
64
|
+
this.now = cfg.now ?? (() => Date.now());
|
|
65
|
+
this.logger = cfg.logger ?? { log: () => { }, warn: () => { } };
|
|
66
|
+
}
|
|
67
|
+
start() {
|
|
68
|
+
if (this.timer)
|
|
69
|
+
return;
|
|
70
|
+
this.timer = setInterval(() => {
|
|
71
|
+
if (this.ticking)
|
|
72
|
+
return; // never overlap ticks
|
|
73
|
+
void this.tick();
|
|
74
|
+
}, this.tickMs);
|
|
75
|
+
this.timer.unref?.();
|
|
76
|
+
}
|
|
77
|
+
stop() {
|
|
78
|
+
if (this.timer) {
|
|
79
|
+
clearInterval(this.timer);
|
|
80
|
+
this.timer = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Status for the read route (never throws). */
|
|
84
|
+
status() {
|
|
85
|
+
return {
|
|
86
|
+
thresholdPct: this.thresholdPct,
|
|
87
|
+
watchPct: Math.max(0, this.thresholdPct - this.watchMarginPct),
|
|
88
|
+
maxSwapsPerCycle: this.maxSwapsPerCycle,
|
|
89
|
+
cooldownMs: this.cooldownMs,
|
|
90
|
+
tickMs: this.tickMs,
|
|
91
|
+
running: this.timer !== null,
|
|
92
|
+
lastResult: this.lastResult,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One monitor pass: if any at-risk account is in the watch zone, refresh the
|
|
97
|
+
* quota poll first (fresh data near the wall), then evaluate + swap.
|
|
98
|
+
*/
|
|
99
|
+
async tick() {
|
|
100
|
+
if (this.ticking) {
|
|
101
|
+
return this.lastResult ?? { swapped: [], considered: 0, refreshed: false };
|
|
102
|
+
}
|
|
103
|
+
this.ticking = true;
|
|
104
|
+
try {
|
|
105
|
+
let refreshed = false;
|
|
106
|
+
if (this.cfg.triggerPoll) {
|
|
107
|
+
const watchPct = Math.max(0, this.thresholdPct - this.watchMarginPct);
|
|
108
|
+
const near = (await this.mapCandidates(watchPct)).length > 0;
|
|
109
|
+
if (near) {
|
|
110
|
+
try {
|
|
111
|
+
await this.cfg.triggerPoll();
|
|
112
|
+
refreshed = true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// @silent-fallback-ok: a poll blip just means we decide on prior data
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const evaluated = await this.evaluate();
|
|
120
|
+
const result = { ...evaluated, refreshed };
|
|
121
|
+
this.lastResult = result;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
this.ticking = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Pure-ish decision + swap on the CURRENT snapshots (no poll refresh). Exposed
|
|
130
|
+
* for tests and the on-demand check route.
|
|
131
|
+
*/
|
|
132
|
+
async evaluate() {
|
|
133
|
+
const nowMs = this.now();
|
|
134
|
+
const accounts = this.cfg.listAccounts();
|
|
135
|
+
const atPressure = await this.mapCandidates(this.thresholdPct);
|
|
136
|
+
const eligible = atPressure.filter((c) => {
|
|
137
|
+
const last = this.lastSwapAt.get(c.sessionName);
|
|
138
|
+
if (last !== undefined && nowMs - last < this.cooldownMs)
|
|
139
|
+
return false;
|
|
140
|
+
// Only swap when there's an alternate BELOW the proactive threshold — never
|
|
141
|
+
// move a session onto an account that is itself nearly full (anti-thrash).
|
|
142
|
+
const alt = selectAccount(accounts, { softThresholdPct: this.thresholdPct, nowMs }, c.accountId);
|
|
143
|
+
return alt !== null;
|
|
144
|
+
});
|
|
145
|
+
// Newest-(re)started first: the interactive session a user is actively in
|
|
146
|
+
// (it just restarted on compaction/recovery) ranks ahead of idle background
|
|
147
|
+
// sessions, so under the per-cycle cap it is rescued first.
|
|
148
|
+
eligible.sort((a, b) => b.startedMs - a.startedMs);
|
|
149
|
+
const toSwap = eligible.slice(0, this.maxSwapsPerCycle);
|
|
150
|
+
const swapped = [];
|
|
151
|
+
for (const c of toSwap) {
|
|
152
|
+
let outcome;
|
|
153
|
+
try {
|
|
154
|
+
outcome = await this.cfg.swap({
|
|
155
|
+
sessionName: c.sessionName,
|
|
156
|
+
exhaustedAccountId: c.accountId,
|
|
157
|
+
nowMs,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// @silent-fallback-ok: a swap failure is retried next cycle (no cooldown set)
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (outcome.swapped) {
|
|
165
|
+
this.lastSwapAt.set(c.sessionName, nowMs);
|
|
166
|
+
swapped.push(c.sessionName);
|
|
167
|
+
this.logger.log(`[ProactiveSwap] ${c.sessionName}: pre-emptively swapped off ${c.accountId} → ${outcome.toAccountId} ` +
|
|
168
|
+
`(account ≥${this.thresholdPct}% measured — moved before the wall, conversation preserved)`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { swapped, considered: eligible.length };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Map running sessions to candidates whose EFFECTIVE account is at/over minPct.
|
|
175
|
+
* Effective account = the session's tag, else the default-config login (so the
|
|
176
|
+
* untagged interactive session is visible). Resolves the default login once.
|
|
177
|
+
*/
|
|
178
|
+
async mapCandidates(minPct) {
|
|
179
|
+
const accounts = this.cfg.listAccounts();
|
|
180
|
+
const byId = new Map(accounts.map((a) => [a.id, a]));
|
|
181
|
+
let defaultAcctId = null;
|
|
182
|
+
try {
|
|
183
|
+
defaultAcctId = await this.cfg.resolveDefaultAccountId();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
defaultAcctId = null; // @silent-fallback-ok: unknown default login → tagged-only
|
|
187
|
+
}
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const s of this.cfg.listRunningSessions()) {
|
|
190
|
+
const eff = s.accountId ?? defaultAcctId;
|
|
191
|
+
if (!eff)
|
|
192
|
+
continue;
|
|
193
|
+
const acct = byId.get(eff);
|
|
194
|
+
if (!acct)
|
|
195
|
+
continue;
|
|
196
|
+
if (!accountAtPressure(acct, minPct))
|
|
197
|
+
continue;
|
|
198
|
+
const startedMs = s.startedAt ? Date.parse(s.startedAt) : NaN;
|
|
199
|
+
out.push({
|
|
200
|
+
sessionName: s.sessionName,
|
|
201
|
+
accountId: eff,
|
|
202
|
+
startedMs: Number.isFinite(startedMs) ? startedMs : 0,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=ProactiveSwapMonitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProactiveSwapMonitor.js","sourceRoot":"","sources":["../../src/core/ProactiveSwapMonitor.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -2409,6 +2409,33 @@ export interface InstarConfig {
|
|
|
2409
2409
|
* auto-swap functional (a session must carry which account it's on for the
|
|
2410
2410
|
* swap engine to move it). Unset → spawns use the default config (no-op). */
|
|
2411
2411
|
pinSessionsToPool?: boolean;
|
|
2412
|
+
/**
|
|
2413
|
+
* DARK by default: the PRE-LIMIT swap. When enabled, the ProactiveSwapMonitor
|
|
2414
|
+
* moves a session OFF an account that crosses `thresholdPct` on its binding
|
|
2415
|
+
* window BEFORE it walls — vs. the reactive `autoSwapOnRateLimit`, which only
|
|
2416
|
+
* fires AFTER the wall. Same authority as the reactive swap (it moves live
|
|
2417
|
+
* sessions), earlier trigger → opt-in. Covers untagged sessions too by
|
|
2418
|
+
* resolving the default-config login (so the primary interactive session is
|
|
2419
|
+
* swap-visible instead of wedging at the wall).
|
|
2420
|
+
*/
|
|
2421
|
+
proactiveSwap?: {
|
|
2422
|
+
/** Master switch. Default false (dark). */
|
|
2423
|
+
enabled?: boolean;
|
|
2424
|
+
/** Measured binding-window utilization % that triggers a pre-emptive swap.
|
|
2425
|
+
* Default 80 — below the real wall to absorb poll-lag (measured trails
|
|
2426
|
+
* real ~5%), so the swap completes with margin. */
|
|
2427
|
+
thresholdPct?: number;
|
|
2428
|
+
/** Refresh the poll before deciding when an at-risk account is within this
|
|
2429
|
+
* many points of the threshold (catch a fast burn between baseline polls).
|
|
2430
|
+
* Default 15. */
|
|
2431
|
+
watchMarginPct?: number;
|
|
2432
|
+
/** Max sessions swapped per evaluation cycle (storm guard). Default 3. */
|
|
2433
|
+
maxSwapsPerCycle?: number;
|
|
2434
|
+
/** Per-session cooldown (ms) after a successful swap. Default 600000 (10m). */
|
|
2435
|
+
cooldownMs?: number;
|
|
2436
|
+
/** Monitor tick cadence (ms). Default 180000 (3m). */
|
|
2437
|
+
tickMs?: number;
|
|
2438
|
+
};
|
|
2412
2439
|
/** P2.1 enrollment wizard knobs (all optional). */
|
|
2413
2440
|
enrollment?: {
|
|
2414
2441
|
/** Per-framework login command override (defaults: claude-code →
|