instar 1.3.544 → 1.3.545
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/core/CredentialRebalancer.d.ts +116 -0
- package/dist/core/CredentialRebalancer.d.ts.map +1 -0
- package/dist/core/CredentialRebalancer.js +159 -0
- package/dist/core/CredentialRebalancer.js.map +1 -0
- package/dist/core/CredentialRebalancerPolicy.d.ts +137 -0
- package/dist/core/CredentialRebalancerPolicy.d.ts.map +1 -0
- package/dist/core/CredentialRebalancerPolicy.js +284 -0
- package/dist/core/CredentialRebalancerPolicy.js.map +1 -0
- package/dist/core/CredentialRebalancerSnapshot.d.ts +66 -0
- package/dist/core/CredentialRebalancerSnapshot.d.ts.map +1 -0
- package/dist/core/CredentialRebalancerSnapshot.js +111 -0
- package/dist/core/CredentialRebalancerSnapshot.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.545.md +68 -0
- package/upgrades/side-effects/ws52-incb-b1-rebalancer-policy.md +44 -0
- package/upgrades/side-effects/ws52-incb-b2-default-eviction.md +42 -0
- package/upgrades/side-effects/ws52-incb-b3a-rebalancer-orchestrator.md +44 -0
- package/upgrades/side-effects/ws52-incb-b3b-snapshot-mappers.md +37 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRebalancer — the §2.4 balancer ORCHESTRATOR (Increment B, step B3a).
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4.
|
|
5
|
+
*
|
|
6
|
+
* ── What this is ──
|
|
7
|
+
* Wraps the pure `CredentialRebalancerPolicy.decidePass()` decision core in a stateful
|
|
8
|
+
* pass loop: on each `tick()` it builds the read-only pass snapshot from injected
|
|
9
|
+
* providers (ledger/verify → slots, quota poller → accounts, config resolver), asks the
|
|
10
|
+
* policy for the zero-or-more swaps, and ACTUATES each accepted swap through the injected
|
|
11
|
+
* executor — but ONLY under the feature's dark/dry-run gate. It carries the hysteresis
|
|
12
|
+
* state the pure policy cannot (cooldown timestamps across passes) and the §2.4 P19
|
|
13
|
+
* breaker (N consecutive FAILED swaps opens it; it retries on the next quota-fresh pass).
|
|
14
|
+
*
|
|
15
|
+
* ── Safety contract (the autonomous-write surface — this is the risky layer) ──
|
|
16
|
+
* 1. DARK = STRICT NO-OP. When `isEnabled()` is false the tick returns immediately
|
|
17
|
+
* having built NOTHING and called the executor ZERO times (the §2.4 "a pass with no
|
|
18
|
+
* actuation performs zero keychain/CLI operations" invariant, extended to the whole
|
|
19
|
+
* dark state).
|
|
20
|
+
* 2. DRY-RUN actuates the DECISION but not the WRITE. The executor itself enforces
|
|
21
|
+
* dryRun (Step 5), so a dry-run pass audits what it WOULD swap and advances cooldown
|
|
22
|
+
* state so the simulated cadence is realistic, but moves no credential.
|
|
23
|
+
* 3. The executor is the ONLY write path. This class never touches a keychain directly;
|
|
24
|
+
* it calls `deps.swap()` (a thin wrapper over the gated, oracle-verified, staged
|
|
25
|
+
* CredentialSwapExecutor.swap). A swap rejection/`ok:false` increments the breaker.
|
|
26
|
+
* 4. EVERY pass is auditable. The decision, the actuation outcome, the breaker state,
|
|
27
|
+
* and the surfaced degraded/attention entries are all recorded in `status()`.
|
|
28
|
+
*
|
|
29
|
+
* Pure orchestration over injected deps → unit-testable without a keychain. The server
|
|
30
|
+
* wiring (the setInterval pass) + the live `GET /credentials/rebalancer` status are B3b.
|
|
31
|
+
*/
|
|
32
|
+
import { type RebalancerPolicyConfig, type AccountState, type SlotState, type SwapDecision } from './CredentialRebalancerPolicy.js';
|
|
33
|
+
export interface RebalancerResolvedConfig {
|
|
34
|
+
policy: RebalancerPolicyConfig;
|
|
35
|
+
/** A slot's verify counts as recent within this window. */
|
|
36
|
+
auditCadenceMs: number;
|
|
37
|
+
/** The account that should serve `~/.claude` (objective-0), or null. */
|
|
38
|
+
desiredDefaultAccountId: string | null;
|
|
39
|
+
/** Ceiling for forced wall-overrides per rolling window. */
|
|
40
|
+
maxForcedOverridesPerWindow: number;
|
|
41
|
+
/** N consecutive FAILED swaps that open the P19 breaker. */
|
|
42
|
+
breakerThreshold: number;
|
|
43
|
+
}
|
|
44
|
+
export interface RebalancerActuationResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
detail?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface CredentialRebalancerDeps {
|
|
49
|
+
/** Live feature gate — read per tick so a restartless flip is honored. */
|
|
50
|
+
isEnabled: () => boolean;
|
|
51
|
+
/** Live dry-run gate. When true the executor audits but writes nothing. */
|
|
52
|
+
isDryRun: () => boolean;
|
|
53
|
+
/** Build the per-slot snapshot (ledger tenancy + verify/quarantine/activity). */
|
|
54
|
+
listSlots: () => SlotState[];
|
|
55
|
+
/** Build the per-account snapshot (quota + reset proximity from the poller). */
|
|
56
|
+
listAccounts: () => AccountState[];
|
|
57
|
+
/** Resolve the clamped config for this pass. */
|
|
58
|
+
resolveConfig: () => RebalancerResolvedConfig;
|
|
59
|
+
/** The ONLY write path: the gated, oracle-verified CredentialSwapExecutor.swap wrapper. */
|
|
60
|
+
swap: (slotA: string, slotB: string) => Promise<RebalancerActuationResult>;
|
|
61
|
+
/** Optional sinks. */
|
|
62
|
+
emitAudit?: (record: PassAudit) => void;
|
|
63
|
+
emitDegraded?: (message: string) => void;
|
|
64
|
+
emitAttention?: (message: string) => void;
|
|
65
|
+
now?: () => number;
|
|
66
|
+
}
|
|
67
|
+
export interface PassAudit {
|
|
68
|
+
at: number;
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
dryRun: boolean;
|
|
71
|
+
decisions: SwapDecision[];
|
|
72
|
+
actuated: Array<{
|
|
73
|
+
decision: SwapDecision;
|
|
74
|
+
result: RebalancerActuationResult;
|
|
75
|
+
}>;
|
|
76
|
+
degraded: string[];
|
|
77
|
+
attention: string[];
|
|
78
|
+
noActuationReason?: string;
|
|
79
|
+
breakerOpen: boolean;
|
|
80
|
+
}
|
|
81
|
+
export interface RebalancerStatus {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
breaker: {
|
|
84
|
+
open: boolean;
|
|
85
|
+
consecutiveFailures: number;
|
|
86
|
+
threshold: number;
|
|
87
|
+
};
|
|
88
|
+
lastPass: PassAudit | null;
|
|
89
|
+
/** Count of pairs/tenants currently under cooldown (for the status surface). */
|
|
90
|
+
cooldownPairs: number;
|
|
91
|
+
cooldownTenants: number;
|
|
92
|
+
}
|
|
93
|
+
export declare class CredentialRebalancer {
|
|
94
|
+
private readonly deps;
|
|
95
|
+
private readonly now;
|
|
96
|
+
private lastActuationByPair;
|
|
97
|
+
private lastActuationByTenant;
|
|
98
|
+
private forcedOverridesInWindow;
|
|
99
|
+
private forcedWindowStart;
|
|
100
|
+
private consecutiveFailures;
|
|
101
|
+
private breakerOpen;
|
|
102
|
+
private lastPass;
|
|
103
|
+
constructor(deps: CredentialRebalancerDeps);
|
|
104
|
+
/**
|
|
105
|
+
* Run one balancer pass. DARK ⇒ strict no-op. Returns the pass audit.
|
|
106
|
+
* The breaker, when open, still runs the pass (to re-probe) but the spec retries on a
|
|
107
|
+
* quota-fresh pass — here we simply attempt and let a continued failure keep it open;
|
|
108
|
+
* a success resets it.
|
|
109
|
+
*/
|
|
110
|
+
tick(): Promise<PassAudit>;
|
|
111
|
+
/** Record cooldown timestamps for the tenants the decision exchanged. */
|
|
112
|
+
private recordActuation;
|
|
113
|
+
status(): RebalancerStatus;
|
|
114
|
+
private safeBreakerThreshold;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=CredentialRebalancer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRebalancer.d.ts","sourceRoot":"","sources":["../../src/core/CredentialRebalancer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRebalancer — the §2.4 balancer ORCHESTRATOR (Increment B, step B3a).
|
|
3
|
+
*
|
|
4
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4.
|
|
5
|
+
*
|
|
6
|
+
* ── What this is ──
|
|
7
|
+
* Wraps the pure `CredentialRebalancerPolicy.decidePass()` decision core in a stateful
|
|
8
|
+
* pass loop: on each `tick()` it builds the read-only pass snapshot from injected
|
|
9
|
+
* providers (ledger/verify → slots, quota poller → accounts, config resolver), asks the
|
|
10
|
+
* policy for the zero-or-more swaps, and ACTUATES each accepted swap through the injected
|
|
11
|
+
* executor — but ONLY under the feature's dark/dry-run gate. It carries the hysteresis
|
|
12
|
+
* state the pure policy cannot (cooldown timestamps across passes) and the §2.4 P19
|
|
13
|
+
* breaker (N consecutive FAILED swaps opens it; it retries on the next quota-fresh pass).
|
|
14
|
+
*
|
|
15
|
+
* ── Safety contract (the autonomous-write surface — this is the risky layer) ──
|
|
16
|
+
* 1. DARK = STRICT NO-OP. When `isEnabled()` is false the tick returns immediately
|
|
17
|
+
* having built NOTHING and called the executor ZERO times (the §2.4 "a pass with no
|
|
18
|
+
* actuation performs zero keychain/CLI operations" invariant, extended to the whole
|
|
19
|
+
* dark state).
|
|
20
|
+
* 2. DRY-RUN actuates the DECISION but not the WRITE. The executor itself enforces
|
|
21
|
+
* dryRun (Step 5), so a dry-run pass audits what it WOULD swap and advances cooldown
|
|
22
|
+
* state so the simulated cadence is realistic, but moves no credential.
|
|
23
|
+
* 3. The executor is the ONLY write path. This class never touches a keychain directly;
|
|
24
|
+
* it calls `deps.swap()` (a thin wrapper over the gated, oracle-verified, staged
|
|
25
|
+
* CredentialSwapExecutor.swap). A swap rejection/`ok:false` increments the breaker.
|
|
26
|
+
* 4. EVERY pass is auditable. The decision, the actuation outcome, the breaker state,
|
|
27
|
+
* and the surfaced degraded/attention entries are all recorded in `status()`.
|
|
28
|
+
*
|
|
29
|
+
* Pure orchestration over injected deps → unit-testable without a keychain. The server
|
|
30
|
+
* wiring (the setInterval pass) + the live `GET /credentials/rebalancer` status are B3b.
|
|
31
|
+
*/
|
|
32
|
+
import { decidePass, } from './CredentialRebalancerPolicy.js';
|
|
33
|
+
function sortedPair(a, b) {
|
|
34
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
35
|
+
}
|
|
36
|
+
export class CredentialRebalancer {
|
|
37
|
+
deps;
|
|
38
|
+
now;
|
|
39
|
+
// Hysteresis state the pure policy cannot hold (carried across passes).
|
|
40
|
+
lastActuationByPair = {};
|
|
41
|
+
lastActuationByTenant = {};
|
|
42
|
+
forcedOverridesInWindow = 0;
|
|
43
|
+
forcedWindowStart = 0;
|
|
44
|
+
// P19 breaker.
|
|
45
|
+
consecutiveFailures = 0;
|
|
46
|
+
breakerOpen = false;
|
|
47
|
+
lastPass = null;
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
this.now = deps.now ?? (() => Date.now());
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run one balancer pass. DARK ⇒ strict no-op. Returns the pass audit.
|
|
54
|
+
* The breaker, when open, still runs the pass (to re-probe) but the spec retries on a
|
|
55
|
+
* quota-fresh pass — here we simply attempt and let a continued failure keep it open;
|
|
56
|
+
* a success resets it.
|
|
57
|
+
*/
|
|
58
|
+
async tick() {
|
|
59
|
+
const at = this.now();
|
|
60
|
+
if (!this.deps.isEnabled()) {
|
|
61
|
+
// STRICT no-op while dark: build nothing, call the executor zero times.
|
|
62
|
+
const audit = { at, enabled: false, dryRun: this.deps.isDryRun(), decisions: [], actuated: [], degraded: [], attention: [], noActuationReason: 'feature dark', breakerOpen: this.breakerOpen };
|
|
63
|
+
this.lastPass = audit;
|
|
64
|
+
return audit;
|
|
65
|
+
}
|
|
66
|
+
const cfg = this.deps.resolveConfig();
|
|
67
|
+
// Roll the forced-override window if a window has elapsed (the per-window budget resets).
|
|
68
|
+
if (at - this.forcedWindowStart >= cfg.policy.perTenantCooldownMs * 2) {
|
|
69
|
+
this.forcedWindowStart = at;
|
|
70
|
+
this.forcedOverridesInWindow = 0;
|
|
71
|
+
}
|
|
72
|
+
const input = {
|
|
73
|
+
now: at,
|
|
74
|
+
slots: this.deps.listSlots(),
|
|
75
|
+
accounts: this.deps.listAccounts(),
|
|
76
|
+
cooldowns: {
|
|
77
|
+
lastActuationByPair: this.lastActuationByPair,
|
|
78
|
+
lastActuationByTenant: this.lastActuationByTenant,
|
|
79
|
+
forcedOverridesInWindow: this.forcedOverridesInWindow,
|
|
80
|
+
maxForcedOverridesPerWindow: cfg.maxForcedOverridesPerWindow,
|
|
81
|
+
},
|
|
82
|
+
config: cfg.policy,
|
|
83
|
+
auditCadenceMs: cfg.auditCadenceMs,
|
|
84
|
+
desiredDefaultAccountId: cfg.desiredDefaultAccountId,
|
|
85
|
+
};
|
|
86
|
+
const result = decidePass(input);
|
|
87
|
+
for (const m of result.degraded)
|
|
88
|
+
this.deps.emitDegraded?.(m);
|
|
89
|
+
for (const m of result.attention)
|
|
90
|
+
this.deps.emitAttention?.(m);
|
|
91
|
+
const dryRun = this.deps.isDryRun();
|
|
92
|
+
const actuated = [];
|
|
93
|
+
for (const decision of result.decisions) {
|
|
94
|
+
const r = await this.deps.swap(decision.targetSlot, decision.sourceSlot).catch((e) => ({ ok: false, detail: e instanceof Error ? e.message : String(e) }));
|
|
95
|
+
actuated.push({ decision, result: r });
|
|
96
|
+
if (r.ok) {
|
|
97
|
+
// Advance the hysteresis state (in BOTH dry-run and live — dry-run simulates the
|
|
98
|
+
// real cadence so a dry-run soak shows realistic anti-churn behavior). Recorded by
|
|
99
|
+
// the TENANT pair the decision exchanged (NOT the fixed slot seats).
|
|
100
|
+
this.recordActuation(decision, input, at);
|
|
101
|
+
this.consecutiveFailures = 0;
|
|
102
|
+
this.breakerOpen = false;
|
|
103
|
+
if (decision.forced === 'wall-override')
|
|
104
|
+
this.forcedOverridesInWindow += 1;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Only a LIVE failure counts toward the breaker (a dry-run never writes, so it
|
|
108
|
+
// cannot "fail" a write — the executor returns ok under dry-run).
|
|
109
|
+
if (!dryRun) {
|
|
110
|
+
this.consecutiveFailures += 1;
|
|
111
|
+
if (this.consecutiveFailures >= cfg.breakerThreshold && !this.breakerOpen) {
|
|
112
|
+
this.breakerOpen = true;
|
|
113
|
+
this.deps.emitDegraded?.(`credential rebalancer P19 breaker opened after ${this.consecutiveFailures} consecutive failed swaps`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const audit = {
|
|
119
|
+
at, enabled: true, dryRun,
|
|
120
|
+
decisions: result.decisions, actuated,
|
|
121
|
+
degraded: result.degraded, attention: result.attention,
|
|
122
|
+
noActuationReason: result.noActuationReason,
|
|
123
|
+
breakerOpen: this.breakerOpen,
|
|
124
|
+
};
|
|
125
|
+
this.lastPass = audit;
|
|
126
|
+
return audit;
|
|
127
|
+
}
|
|
128
|
+
/** Record cooldown timestamps for the tenants the decision exchanged. */
|
|
129
|
+
recordActuation(decision, input, at) {
|
|
130
|
+
const slotById = new Map(input.slots.map((s) => [s.slot, s]));
|
|
131
|
+
const tA = slotById.get(decision.targetSlot)?.tenantAccountId ?? null;
|
|
132
|
+
const tB = slotById.get(decision.sourceSlot)?.tenantAccountId ?? null;
|
|
133
|
+
if (tA)
|
|
134
|
+
this.lastActuationByTenant[tA] = at;
|
|
135
|
+
if (tB)
|
|
136
|
+
this.lastActuationByTenant[tB] = at;
|
|
137
|
+
if (tA && tB)
|
|
138
|
+
this.lastActuationByPair[sortedPair(tA, tB)] = at;
|
|
139
|
+
}
|
|
140
|
+
status() {
|
|
141
|
+
const cfg = this.safeBreakerThreshold();
|
|
142
|
+
return {
|
|
143
|
+
enabled: this.deps.isEnabled(),
|
|
144
|
+
breaker: { open: this.breakerOpen, consecutiveFailures: this.consecutiveFailures, threshold: cfg },
|
|
145
|
+
lastPass: this.lastPass,
|
|
146
|
+
cooldownPairs: Object.keys(this.lastActuationByPair).length,
|
|
147
|
+
cooldownTenants: Object.keys(this.lastActuationByTenant).length,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
safeBreakerThreshold() {
|
|
151
|
+
try {
|
|
152
|
+
return this.deps.resolveConfig().breakerThreshold;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return 3;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=CredentialRebalancer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRebalancer.js","sourceRoot":"","sources":["../../src/core/CredentialRebalancer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRebalancerPolicy — the §2.4 "stock-trader loop" decision core, as a PURE
|
|
3
|
+
* function (Increment B, step B1 of live credential re-pointing).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4 (the balancer).
|
|
6
|
+
*
|
|
7
|
+
* ── What this is ──
|
|
8
|
+
* Given a read-only snapshot of a pass (per-account quota + reset proximity, per-slot
|
|
9
|
+
* tenancy/verify/activity, cooldown state, resolved+clamped config), this computes the
|
|
10
|
+
* ZERO-OR-MORE swap decisions for ONE pass. It performs NO IO and holds NO authority:
|
|
11
|
+
* it DECIDES; a separate actuator (step B3) routes an accepted decision through the
|
|
12
|
+
* Step-5 CredentialSwapExecutor under the dark/dry-run gate. Splitting the decision out
|
|
13
|
+
* makes the entire policy — every threshold, cap, cooldown, and eligibility rule —
|
|
14
|
+
* exhaustively unit-testable without a keychain (Tier-0 supervision, §2.4: a
|
|
15
|
+
* deterministic policy over enumerable numeric thresholds).
|
|
16
|
+
*
|
|
17
|
+
* ── Priority of objectives (§2.4) ──
|
|
18
|
+
* 1. Wall avoidance — a tenant over the high-water mark gets the highest-headroom
|
|
19
|
+
* eligible account. A tenant over the CRITICAL mark triggers a wall-OVERRIDE that
|
|
20
|
+
* bypasses the cooldowns + the 1-swap/pass cap, bounded by its own caps
|
|
21
|
+
* (fresh-data gate, maxForcedSwapsPerPass, maxForcedOverridesPerWindow).
|
|
22
|
+
* 2. Use-it-or-lose-it drain — an account whose WEEKLY window resets soon with unused
|
|
23
|
+
* headroom is dealt to the busiest slot (weekly only; 5h windows regenerate).
|
|
24
|
+
* 3. Default-slot preference — keep the designated default account in `~/.claude`.
|
|
25
|
+
*
|
|
26
|
+
* Non-forced objectives are bounded to ONE swap per pass (acting twice on one sensor
|
|
27
|
+
* reading is noise). Only the wall-override may emit up to `maxForcedSwapsPerPass`.
|
|
28
|
+
*
|
|
29
|
+
* Dead-default eviction, the correlated-oracle-outage floor, quarantine-exit, the P19
|
|
30
|
+
* breaker, and the scheduled identity audit are step B2 (this module decides swaps; B2
|
|
31
|
+
* adds the degraded-state machinery). This module surfaces the terminal "nothing to do"
|
|
32
|
+
* / "no eligible target" states so B2/B3 can act on them.
|
|
33
|
+
*/
|
|
34
|
+
/** Resolved + clamped knobs the pure policy operates on (the resolver lives in B3). */
|
|
35
|
+
export interface RebalancerPolicyConfig {
|
|
36
|
+
/** Wall-avoidance high-water utilization % (either window). Clamp [50,99]. */
|
|
37
|
+
highWaterPct: number;
|
|
38
|
+
/** Critical mark % for the wall-override. Clamp [85,99]. */
|
|
39
|
+
criticalPct: number;
|
|
40
|
+
/** Drain horizon — a weekly window resetting within this many hours is drainable. */
|
|
41
|
+
drainHorizonHours: number;
|
|
42
|
+
/** Minimum unused weekly headroom % for a drain to be worth it. */
|
|
43
|
+
drainHeadroomMinPct: number;
|
|
44
|
+
/** Minimum score delta to justify a NON-forced move (urgency-clamped). */
|
|
45
|
+
minScoreDelta: number;
|
|
46
|
+
/** Max forced (wall-override) swaps a single pass may emit. Clamp [1, N-slots]. */
|
|
47
|
+
maxForcedSwapsPerPass: number;
|
|
48
|
+
/** Per-pair cooldown ms (≥1× poll interval). */
|
|
49
|
+
perPairCooldownMs: number;
|
|
50
|
+
/** Per-tenant cooldown ms (≥2× poll interval) — defeats the 3-way rotation attack. */
|
|
51
|
+
perTenantCooldownMs: number;
|
|
52
|
+
/** Quota older than this is SOURCE-only (never a swap-in target). */
|
|
53
|
+
staleQuotaMs: number;
|
|
54
|
+
/** Urgency `1/hoursUntilReset` is clamped at this many hours so the delta floor stays meaningful. */
|
|
55
|
+
urgencyClampHours: number;
|
|
56
|
+
}
|
|
57
|
+
export interface AccountState {
|
|
58
|
+
accountId: string;
|
|
59
|
+
status: 'ok' | 'needs-reauth' | 'disabled';
|
|
60
|
+
/** Utilization % on the 5h window (0..100), or null when unknown. */
|
|
61
|
+
fiveHrPct: number | null;
|
|
62
|
+
/** Utilization % on the weekly window (0..100), or null when unknown. */
|
|
63
|
+
weeklyPct: number | null;
|
|
64
|
+
/** Hours until the weekly window resets, or null when unknown. */
|
|
65
|
+
weeklyResetsInHours: number | null;
|
|
66
|
+
/** When this account's quota was last measured (for the staleness gate). */
|
|
67
|
+
measuredAt: number;
|
|
68
|
+
}
|
|
69
|
+
export interface SlotState {
|
|
70
|
+
slot: string;
|
|
71
|
+
tenantAccountId: string | null;
|
|
72
|
+
isDefault: boolean;
|
|
73
|
+
quarantined: boolean;
|
|
74
|
+
/** Last oracle identity-verify time, or null if never. */
|
|
75
|
+
lastVerifiedAt: number | null;
|
|
76
|
+
/** True if the last scheduled audit flagged this slot's identity as divergent. */
|
|
77
|
+
lastAuditDivergent: boolean;
|
|
78
|
+
/** Per-slot "drain in progress" hold — exempt from being a drain DESTINATION (§2.4 obj 2). */
|
|
79
|
+
drainInProgress: boolean;
|
|
80
|
+
/** Recent activity score (higher = busier); drain targets the busiest slot. */
|
|
81
|
+
busyness: number;
|
|
82
|
+
/** The most-recently-previously-verified account for this slot (the correlated-outage
|
|
83
|
+
* floor's "last known good"); used only to keep the DEFAULT slot non-empty when the
|
|
84
|
+
* oracle is down for every slot. Optional — absent on a never-verified slot. */
|
|
85
|
+
lastKnownGoodAccountId?: string | null;
|
|
86
|
+
}
|
|
87
|
+
export interface CooldownState {
|
|
88
|
+
/** key = sortedPair(a,b) → last actuation ms. */
|
|
89
|
+
lastActuationByPair: Record<string, number>;
|
|
90
|
+
/** accountId → last actuation ms (the tenant cooldown). */
|
|
91
|
+
lastActuationByTenant: Record<string, number>;
|
|
92
|
+
/** Forced wall-overrides already spent in the current rolling window. */
|
|
93
|
+
forcedOverridesInWindow: number;
|
|
94
|
+
/** The ceiling for forced overrides in a window; at/over it, the override STOPS (surfaced). */
|
|
95
|
+
maxForcedOverridesPerWindow: number;
|
|
96
|
+
}
|
|
97
|
+
export interface RebalancePassInput {
|
|
98
|
+
now: number;
|
|
99
|
+
slots: SlotState[];
|
|
100
|
+
accounts: AccountState[];
|
|
101
|
+
cooldowns: CooldownState;
|
|
102
|
+
config: RebalancerPolicyConfig;
|
|
103
|
+
/** A slot's identity verify counts as "recent" if within this window (the audit cadence). */
|
|
104
|
+
auditCadenceMs: number;
|
|
105
|
+
/** The account that SHOULD serve `~/.claude` (so manual `claude` is predictable). When the
|
|
106
|
+
* default slot's tenant dies/quarantines, a healthy verified tenant is dealt in; when the
|
|
107
|
+
* desired default is itself dead, any healthy verified tenant keeps the slot alive. Null =
|
|
108
|
+
* no default preference configured (the default-eviction objective is then inert). */
|
|
109
|
+
desiredDefaultAccountId?: string | null;
|
|
110
|
+
}
|
|
111
|
+
export interface SwapDecision {
|
|
112
|
+
/** The slot being acted on (rescued, or the drain destination, or the default slot). */
|
|
113
|
+
targetSlot: string;
|
|
114
|
+
/** The slot whose tenant is exchanged in. */
|
|
115
|
+
sourceSlot: string;
|
|
116
|
+
objective: 'wall' | 'drain' | 'default' | 'default-eviction';
|
|
117
|
+
/** Set when this is a cooldown/cap-bypassing wall-override or a default-slot rescue. */
|
|
118
|
+
forced: 'wall-override' | 'default-eviction' | null;
|
|
119
|
+
reason: string;
|
|
120
|
+
}
|
|
121
|
+
export interface PassResult {
|
|
122
|
+
/** 0..maxForcedSwapsPerPass decisions (normal objectives emit ≤1; wall-override may emit more). */
|
|
123
|
+
decisions: SwapDecision[];
|
|
124
|
+
/** DegradationReporter-bound entries (terminal "stuck" states). */
|
|
125
|
+
degraded: string[];
|
|
126
|
+
/** Attention-queue-bound entries (operator-surfaced terminal states). */
|
|
127
|
+
attention: string[];
|
|
128
|
+
/** Human reason when zero decisions were made (for the audited no-op pass). */
|
|
129
|
+
noActuationReason?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Decide the swap(s) for one pass. Pure: same input → same output, no IO.
|
|
133
|
+
* A pass with no actuation returns `decisions: []` + a `noActuationReason` (the caller
|
|
134
|
+
* audits the no-op pass and performs ZERO keychain/CLI operations — the §2.4 invariant).
|
|
135
|
+
*/
|
|
136
|
+
export declare function decidePass(input: RebalancePassInput): PassResult;
|
|
137
|
+
//# sourceMappingURL=CredentialRebalancerPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRebalancerPolicy.d.ts","sourceRoot":"","sources":["../../src/core/CredentialRebalancerPolicy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRebalancerPolicy — the §2.4 "stock-trader loop" decision core, as a PURE
|
|
3
|
+
* function (Increment B, step B1 of live credential re-pointing).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4 (the balancer).
|
|
6
|
+
*
|
|
7
|
+
* ── What this is ──
|
|
8
|
+
* Given a read-only snapshot of a pass (per-account quota + reset proximity, per-slot
|
|
9
|
+
* tenancy/verify/activity, cooldown state, resolved+clamped config), this computes the
|
|
10
|
+
* ZERO-OR-MORE swap decisions for ONE pass. It performs NO IO and holds NO authority:
|
|
11
|
+
* it DECIDES; a separate actuator (step B3) routes an accepted decision through the
|
|
12
|
+
* Step-5 CredentialSwapExecutor under the dark/dry-run gate. Splitting the decision out
|
|
13
|
+
* makes the entire policy — every threshold, cap, cooldown, and eligibility rule —
|
|
14
|
+
* exhaustively unit-testable without a keychain (Tier-0 supervision, §2.4: a
|
|
15
|
+
* deterministic policy over enumerable numeric thresholds).
|
|
16
|
+
*
|
|
17
|
+
* ── Priority of objectives (§2.4) ──
|
|
18
|
+
* 1. Wall avoidance — a tenant over the high-water mark gets the highest-headroom
|
|
19
|
+
* eligible account. A tenant over the CRITICAL mark triggers a wall-OVERRIDE that
|
|
20
|
+
* bypasses the cooldowns + the 1-swap/pass cap, bounded by its own caps
|
|
21
|
+
* (fresh-data gate, maxForcedSwapsPerPass, maxForcedOverridesPerWindow).
|
|
22
|
+
* 2. Use-it-or-lose-it drain — an account whose WEEKLY window resets soon with unused
|
|
23
|
+
* headroom is dealt to the busiest slot (weekly only; 5h windows regenerate).
|
|
24
|
+
* 3. Default-slot preference — keep the designated default account in `~/.claude`.
|
|
25
|
+
*
|
|
26
|
+
* Non-forced objectives are bounded to ONE swap per pass (acting twice on one sensor
|
|
27
|
+
* reading is noise). Only the wall-override may emit up to `maxForcedSwapsPerPass`.
|
|
28
|
+
*
|
|
29
|
+
* Dead-default eviction, the correlated-oracle-outage floor, quarantine-exit, the P19
|
|
30
|
+
* breaker, and the scheduled identity audit are step B2 (this module decides swaps; B2
|
|
31
|
+
* adds the degraded-state machinery). This module surfaces the terminal "nothing to do"
|
|
32
|
+
* / "no eligible target" states so B2/B3 can act on them.
|
|
33
|
+
*/
|
|
34
|
+
function sortedPair(a, b) {
|
|
35
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
36
|
+
}
|
|
37
|
+
/** Max utilization across the two windows (the wall is whichever is closer). */
|
|
38
|
+
function maxUtil(acc) {
|
|
39
|
+
return Math.max(acc.fiveHrPct ?? 0, acc.weeklyPct ?? 0);
|
|
40
|
+
}
|
|
41
|
+
/** A slot whose verify is recent AND not divergent is a valid swap-in target (§2.4 recency gate). */
|
|
42
|
+
function targetVerifiedRecent(slot, now, auditCadenceMs) {
|
|
43
|
+
return (!slot.quarantined &&
|
|
44
|
+
!slot.lastAuditDivergent &&
|
|
45
|
+
slot.lastVerifiedAt !== null &&
|
|
46
|
+
now - slot.lastVerifiedAt <= auditCadenceMs);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Decide the swap(s) for one pass. Pure: same input → same output, no IO.
|
|
50
|
+
* A pass with no actuation returns `decisions: []` + a `noActuationReason` (the caller
|
|
51
|
+
* audits the no-op pass and performs ZERO keychain/CLI operations — the §2.4 invariant).
|
|
52
|
+
*/
|
|
53
|
+
export function decidePass(input) {
|
|
54
|
+
const { now, slots, accounts, cooldowns, config, auditCadenceMs } = input;
|
|
55
|
+
const degraded = [];
|
|
56
|
+
const attention = [];
|
|
57
|
+
const accById = new Map(accounts.map((a) => [a.accountId, a]));
|
|
58
|
+
// Eligibility (§2.4): a tenant that is needs-reauth/disabled never participates; a
|
|
59
|
+
// quarantined/unverified slot never participates; a slot with no tenant can't move.
|
|
60
|
+
const participatingSlots = slots.filter((s) => {
|
|
61
|
+
if (s.tenantAccountId === null)
|
|
62
|
+
return false;
|
|
63
|
+
const acc = accById.get(s.tenantAccountId);
|
|
64
|
+
if (!acc)
|
|
65
|
+
return false;
|
|
66
|
+
return acc.status === 'ok';
|
|
67
|
+
});
|
|
68
|
+
// A slot is a valid swap-in TARGET only if its quota is fresh (stale headroom may mask a
|
|
69
|
+
// wall — the anti-conservative direction → SOURCE-only) AND its identity verify is recent.
|
|
70
|
+
const isFreshQuota = (acc) => now - acc.measuredAt <= config.staleQuotaMs;
|
|
71
|
+
// ── Objective 0: dead/quarantined-default eviction (the slot that must never be empty) ──
|
|
72
|
+
// Keep `~/.claude` serving a HEALTHY VERIFIED tenant so manual `claude` keeps working
|
|
73
|
+
// (goal 3 beats slot symmetry + the quarantine-exclusion rule, but ONLY for the default
|
|
74
|
+
// slot). Runs FIRST: a frozen default freezes the operator's manual invocations.
|
|
75
|
+
if (input.desiredDefaultAccountId) {
|
|
76
|
+
const defaultSlot = slots.find((s) => s.isDefault);
|
|
77
|
+
if (defaultSlot) {
|
|
78
|
+
const defTenant = defaultSlot.tenantAccountId ? accById.get(defaultSlot.tenantAccountId) : undefined;
|
|
79
|
+
const defaultDead = defaultSlot.quarantined ||
|
|
80
|
+
!defTenant ||
|
|
81
|
+
defTenant.status === 'needs-reauth' ||
|
|
82
|
+
defTenant.status === 'disabled';
|
|
83
|
+
if (defaultDead) {
|
|
84
|
+
// A healthy VERIFIED tenant to deal in (identity-verified THIS pass before the move —
|
|
85
|
+
// a "healthy per stale ledger" target that is actually dead would re-quarantine the
|
|
86
|
+
// default; the recency gate is that pre-check in the pure policy).
|
|
87
|
+
const healthy = slots
|
|
88
|
+
.filter((s) => s.slot !== defaultSlot.slot && !s.quarantined && s.tenantAccountId !== null)
|
|
89
|
+
.filter((s) => {
|
|
90
|
+
const a = accById.get(s.tenantAccountId);
|
|
91
|
+
return a?.status === 'ok' && targetVerifiedRecent(s, now, auditCadenceMs);
|
|
92
|
+
})
|
|
93
|
+
.sort((a, b) => maxUtil(accById.get(a.tenantAccountId)) - maxUtil(accById.get(b.tenantAccountId)))[0];
|
|
94
|
+
if (healthy) {
|
|
95
|
+
return {
|
|
96
|
+
decisions: [{
|
|
97
|
+
targetSlot: defaultSlot.slot,
|
|
98
|
+
sourceSlot: healthy.slot,
|
|
99
|
+
objective: 'default-eviction',
|
|
100
|
+
forced: 'default-eviction',
|
|
101
|
+
reason: `default slot ${defaultSlot.slot} is ${defaultSlot.quarantined ? 'quarantined' : 'dead (' + (defTenant?.status ?? 'no-tenant') + ')'}; dealing healthy verified ${healthy.tenantAccountId} in to keep manual claude working`,
|
|
102
|
+
}],
|
|
103
|
+
degraded: [],
|
|
104
|
+
attention: [`default slot ${defaultSlot.slot} was ${defaultSlot.quarantined ? 'quarantined' : 'dead'} — rescued with ${healthy.tenantAccountId}; the displaced credential is parked and needs re-auth/re-probe`],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Correlated-oracle-outage floor: NO slot is currently oracle-verifiable (an
|
|
108
|
+
// identity-oracle endpoint storm quarantines every probe at once). Do NOT empty/churn the
|
|
109
|
+
// default — preserve its last-known-good assignment + surface; honest bound: this is
|
|
110
|
+
// "preserve last KNOWN-GOOD + flag", NOT "manual claude is certified working".
|
|
111
|
+
const anyVerifiable = slots.some((s) => targetVerifiedRecent(s, now, auditCadenceMs));
|
|
112
|
+
if (!anyVerifiable) {
|
|
113
|
+
return {
|
|
114
|
+
decisions: [],
|
|
115
|
+
degraded: ['no slot is oracle-verifiable — default-slot eviction suspended (correlated-outage floor)'],
|
|
116
|
+
attention: [`oracle unavailable for every slot; ${defaultSlot.slot} preserved at its last-known-good${defaultSlot.lastKnownGoodAccountId ? ' (' + defaultSlot.lastKnownGoodAccountId + ')' : ''} — NOT certified live; no eviction until the oracle returns`],
|
|
117
|
+
noActuationReason: 'correlated oracle outage — default preserved at last-known-good, no eviction',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Verifiable slots exist but none is an eligible healthy tenant: surface, do not act.
|
|
121
|
+
return {
|
|
122
|
+
decisions: [],
|
|
123
|
+
degraded: [],
|
|
124
|
+
attention: [`default slot ${defaultSlot.slot} is dead/quarantined and no healthy verified tenant is available to rescue it`],
|
|
125
|
+
noActuationReason: 'default slot dead but no eligible healthy tenant to deal in',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Objective 1: wall avoidance ───────────────────────────────────────────────
|
|
131
|
+
// Slots whose tenant exceeds the high-water mark, worst-first.
|
|
132
|
+
const walling = participatingSlots
|
|
133
|
+
.map((s) => ({ slot: s, acc: accById.get(s.tenantAccountId), util: maxUtil(accById.get(s.tenantAccountId)) }))
|
|
134
|
+
.filter((x) => x.util >= config.highWaterPct)
|
|
135
|
+
.sort((a, b) => b.util - a.util);
|
|
136
|
+
// Eligible rescue targets: an account with the most headroom whose CURRENT slot is a
|
|
137
|
+
// verified-recent, fresh-quota, non-walling slot we can exchange with.
|
|
138
|
+
const rescueTargets = participatingSlots
|
|
139
|
+
.map((s) => ({ slot: s, acc: accById.get(s.tenantAccountId) }))
|
|
140
|
+
.filter((x) => maxUtil(x.acc) < config.highWaterPct && isFreshQuota(x.acc) && targetVerifiedRecent(x.slot, now, auditCadenceMs))
|
|
141
|
+
.sort((a, b) => maxUtil(a.acc) - maxUtil(b.acc)); // lowest utilization (most headroom) first
|
|
142
|
+
const decisions = [];
|
|
143
|
+
const actedSlots = new Set();
|
|
144
|
+
const actedTenants = new Set();
|
|
145
|
+
// Wall-override: critical-mark slots bypass cooldowns + the 1-swap cap, bounded by
|
|
146
|
+
// maxForcedSwapsPerPass + the fresh-data gate + maxForcedOverridesPerWindow.
|
|
147
|
+
const critical = walling.filter((x) => x.util >= config.criticalPct);
|
|
148
|
+
let overridesLeft = Math.max(0, cooldowns.maxForcedOverridesPerWindow - cooldowns.forcedOverridesInWindow);
|
|
149
|
+
let forcedThisPass = 0;
|
|
150
|
+
for (const w of critical) {
|
|
151
|
+
if (forcedThisPass >= config.maxForcedSwapsPerPass)
|
|
152
|
+
break;
|
|
153
|
+
if (overridesLeft <= 0) {
|
|
154
|
+
degraded.push('wall-override budget exhausted — no durable rescue available');
|
|
155
|
+
attention.push(`wall-override budget exhausted for slot ${w.slot.slot} — a thrashing tenant the rescue can't durably help`);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
// Fresh-data gate: act on a NEW critical reading only (the tenant's quota must be newer
|
|
159
|
+
// than its last actuation), so the override never re-fires on the same snapshot.
|
|
160
|
+
const lastAct = cooldowns.lastActuationByTenant[w.acc.accountId] ?? -Infinity;
|
|
161
|
+
if (w.acc.measuredAt <= lastAct)
|
|
162
|
+
continue;
|
|
163
|
+
const target = rescueTargets.find((t) => !actedSlots.has(t.slot.slot) && t.slot.slot !== w.slot.slot && !actedTenants.has(t.acc.accountId));
|
|
164
|
+
if (!target) {
|
|
165
|
+
attention.push(`no eligible non-walling rescue target for critical slot ${w.slot.slot}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
decisions.push({
|
|
169
|
+
targetSlot: w.slot.slot,
|
|
170
|
+
sourceSlot: target.slot.slot,
|
|
171
|
+
objective: 'wall',
|
|
172
|
+
forced: 'wall-override',
|
|
173
|
+
reason: `critical wall ${w.util}%≥${config.criticalPct}% on ${w.acc.accountId}; rescued with ${target.acc.accountId} (${maxUtil(target.acc)}%) — cooldowns bypassed`,
|
|
174
|
+
});
|
|
175
|
+
actedSlots.add(w.slot.slot);
|
|
176
|
+
actedSlots.add(target.slot.slot);
|
|
177
|
+
actedTenants.add(w.acc.accountId);
|
|
178
|
+
actedTenants.add(target.acc.accountId);
|
|
179
|
+
forcedThisPass += 1;
|
|
180
|
+
overridesLeft -= 1;
|
|
181
|
+
}
|
|
182
|
+
// If a forced override already acted this pass, we are done (the override is the highest
|
|
183
|
+
// priority; non-forced objectives wait for the next pass to respect 1-swap hygiene).
|
|
184
|
+
if (decisions.length > 0) {
|
|
185
|
+
return { decisions, degraded, attention };
|
|
186
|
+
}
|
|
187
|
+
// Non-forced wall avoidance (85–95%): respects per-pair + per-tenant cooldowns, 1 swap/pass.
|
|
188
|
+
const cooldownOk = (_slotA, _slotB, accA, accB) => {
|
|
189
|
+
// The per-pair cooldown keys on the TENANT pair (the two accounts being exchanged),
|
|
190
|
+
// consistent with the per-tenant cooldown — "don't re-exchange these two tenants too
|
|
191
|
+
// soon" — NOT on the fixed slot seats.
|
|
192
|
+
const pairLast = cooldowns.lastActuationByPair[sortedPair(accA, accB)] ?? -Infinity;
|
|
193
|
+
if (now - pairLast < config.perPairCooldownMs)
|
|
194
|
+
return false;
|
|
195
|
+
const tA = cooldowns.lastActuationByTenant[accA] ?? -Infinity;
|
|
196
|
+
const tB = cooldowns.lastActuationByTenant[accB] ?? -Infinity;
|
|
197
|
+
if (now - tA < config.perTenantCooldownMs || now - tB < config.perTenantCooldownMs)
|
|
198
|
+
return false;
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
// Fresh-data gate for non-forced moves: both tenants' quota newer than their last actuation.
|
|
202
|
+
const freshDataOk = (accA, accB) => {
|
|
203
|
+
const lA = cooldowns.lastActuationByTenant[accA.accountId] ?? -Infinity;
|
|
204
|
+
const lB = cooldowns.lastActuationByTenant[accB.accountId] ?? -Infinity;
|
|
205
|
+
return accA.measuredAt > lA && accB.measuredAt > lB;
|
|
206
|
+
};
|
|
207
|
+
// The non-forced path handles ONLY the [highWater, critical) band. A critical (≥95%)
|
|
208
|
+
// slot is the override path's responsibility; if the override couldn't act (budget /
|
|
209
|
+
// fresh-data / no target) it was surfaced above and WAITS — it is never silently
|
|
210
|
+
// downgraded to a cooldown-respecting rescue in the same pass (that would re-churn the
|
|
211
|
+
// exact thrashing slot the override budget gave up on).
|
|
212
|
+
for (const w of walling.filter((x) => x.util < config.criticalPct)) {
|
|
213
|
+
const target = rescueTargets.find((t) => t.slot.slot !== w.slot.slot);
|
|
214
|
+
if (!target) {
|
|
215
|
+
attention.push(`no eligible non-walling rescue target for slot ${w.slot.slot}`);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (!cooldownOk(w.slot.slot, target.slot.slot, w.acc.accountId, target.acc.accountId))
|
|
219
|
+
continue;
|
|
220
|
+
if (!freshDataOk(w.acc, target.acc))
|
|
221
|
+
continue;
|
|
222
|
+
decisions.push({
|
|
223
|
+
targetSlot: w.slot.slot,
|
|
224
|
+
sourceSlot: target.slot.slot,
|
|
225
|
+
objective: 'wall',
|
|
226
|
+
forced: null,
|
|
227
|
+
reason: `wall ${w.util}%≥${config.highWaterPct}% on ${w.acc.accountId}; rescued with ${target.acc.accountId} (${maxUtil(target.acc)}%)`,
|
|
228
|
+
});
|
|
229
|
+
return { decisions, degraded, attention };
|
|
230
|
+
}
|
|
231
|
+
// ── Objective 2: use-it-or-lose-it drain ──────────────────────────────────────
|
|
232
|
+
// An account whose WEEKLY window resets within the horizon with ≥ headroom-min unused,
|
|
233
|
+
// dealt to the busiest eligible slot that is NOT itself a drain-in-progress destination.
|
|
234
|
+
const drainable = accounts
|
|
235
|
+
.filter((a) => a.status === 'ok' && a.weeklyResetsInHours !== null && a.weeklyResetsInHours <= config.drainHorizonHours)
|
|
236
|
+
.filter((a) => a.weeklyPct !== null && 100 - a.weeklyPct >= config.drainHeadroomMinPct)
|
|
237
|
+
.filter((a) => isFreshQuota(a))
|
|
238
|
+
.sort((a, b) => (a.weeklyResetsInHours - b.weeklyResetsInHours)); // most-reset-proximate first
|
|
239
|
+
if (drainable.length > 0) {
|
|
240
|
+
const drainAcc = drainable[0];
|
|
241
|
+
// The drain SOURCE slot currently holding that account (the one whose tenant we deal out).
|
|
242
|
+
const drainSourceSlot = participatingSlots.find((s) => s.tenantAccountId === drainAcc.accountId);
|
|
243
|
+
// Busiest eligible destination: not the source, not drain-in-progress-held, verified-recent.
|
|
244
|
+
const dest = participatingSlots
|
|
245
|
+
.filter((s) => s.tenantAccountId !== drainAcc.accountId && !s.drainInProgress && targetVerifiedRecent(s, now, auditCadenceMs))
|
|
246
|
+
.sort((a, b) => b.busyness - a.busyness)[0];
|
|
247
|
+
if (drainSourceSlot && dest) {
|
|
248
|
+
const accDest = accById.get(dest.tenantAccountId);
|
|
249
|
+
// Drain respects cooldowns + fresh-data + the min improvement floor (urgency-clamped).
|
|
250
|
+
if (cooldownOk(drainSourceSlot.slot, dest.slot, drainAcc.accountId, accDest.accountId) && freshDataOk(drainAcc, accDest)) {
|
|
251
|
+
const hoursUntil = Math.max(config.urgencyClampHours, drainAcc.weeklyResetsInHours);
|
|
252
|
+
const urgency = 1 / hoursUntil;
|
|
253
|
+
const score = (100 - drainAcc.weeklyPct) * urgency * 100; // headroom × clamped urgency
|
|
254
|
+
if (score >= config.minScoreDelta) {
|
|
255
|
+
decisions.push({
|
|
256
|
+
targetSlot: dest.slot,
|
|
257
|
+
sourceSlot: drainSourceSlot.slot,
|
|
258
|
+
objective: 'drain',
|
|
259
|
+
forced: null,
|
|
260
|
+
reason: `drain ${drainAcc.accountId} (weekly resets in ${drainAcc.weeklyResetsInHours}h, ${100 - drainAcc.weeklyPct}% unused) → busiest slot ${dest.slot}`,
|
|
261
|
+
});
|
|
262
|
+
return { decisions, degraded, attention };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ── Objective 3: default-slot preference ──────────────────────────────────────
|
|
268
|
+
// Keep the designated default account in `~/.claude` when neither (1) nor (2) overrode.
|
|
269
|
+
// (The pure policy only signals the preference when the default slot does NOT already
|
|
270
|
+
// hold the default account AND the move clears the floor; B3 supplies which account is
|
|
271
|
+
// "the default" via the slot.isDefault marker + a desiredDefaultAccountId in config-land.
|
|
272
|
+
// In B1 we surface the preference as a no-op reason when nothing else acted.)
|
|
273
|
+
return {
|
|
274
|
+
decisions: [],
|
|
275
|
+
degraded,
|
|
276
|
+
attention,
|
|
277
|
+
noActuationReason: walling.length > 0
|
|
278
|
+
? 'walling slots present but every candidate move was held by a cooldown / fresh-data gate / missing eligible target'
|
|
279
|
+
: drainable.length > 0
|
|
280
|
+
? 'drainable account present but the move did not clear cooldown / fresh-data / min-improvement'
|
|
281
|
+
: 'no wall, no drainable window, slots balanced — zero actuation',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=CredentialRebalancerPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRebalancerPolicy.js","sourceRoot":"","sources":["../../src/core/CredentialRebalancerPolicy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRebalancerSnapshot — pure mappers from the live system's state into the
|
|
3
|
+
* balancer's read-only pass snapshot (Increment B, step B3b wiring helper).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4.
|
|
6
|
+
*
|
|
7
|
+
* The CredentialRebalancer orchestrator (B3a) consumes injected `listSlots` / `listAccounts`
|
|
8
|
+
* / `resolveConfig` providers. These pure functions ARE those providers — they translate
|
|
9
|
+
* the CredentialLocationLedger assignments and the SubscriptionPool accounts into the
|
|
10
|
+
* policy's `SlotState` / `AccountState`, and clamp the configured balancer knobs into the
|
|
11
|
+
* resolved config. Kept pure + out of server.ts so the mapping (the place a units/sign
|
|
12
|
+
* bug would silently mis-steer the balancer) is unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
import type { SlotState, AccountState } from './CredentialRebalancerPolicy.js';
|
|
15
|
+
import type { RebalancerResolvedConfig } from './CredentialRebalancer.js';
|
|
16
|
+
import type { SubscriptionAccount, SubscriptionAccountStatus } from './SubscriptionPool.js';
|
|
17
|
+
import type { CredentialAssignment } from './CredentialLocationLedger.js';
|
|
18
|
+
/**
|
|
19
|
+
* Map the pool account lifecycle status into the policy's eligibility status.
|
|
20
|
+
* Only needs-reauth/disabled are INELIGIBLE (dead credentials). rate-limited is `ok`:
|
|
21
|
+
* a walled account must stay eligible so wall-avoidance can RESCUE its slot (its high
|
|
22
|
+
* utilization % drives the wall objective; excluding it would strand the very slot that
|
|
23
|
+
* needs help).
|
|
24
|
+
*/
|
|
25
|
+
export declare function mapAccountStatus(s: SubscriptionAccountStatus): AccountState['status'];
|
|
26
|
+
/** One pool account → the policy's AccountState. */
|
|
27
|
+
export declare function mapAccount(account: SubscriptionAccount, now: number): AccountState;
|
|
28
|
+
export declare function mapAccounts(accounts: readonly SubscriptionAccount[], now: number): AccountState[];
|
|
29
|
+
export interface SlotMapOptions {
|
|
30
|
+
/** The config-home slot that is the default (`~/.claude`), so isDefault is set. */
|
|
31
|
+
defaultSlot?: string | null;
|
|
32
|
+
/** Optional per-slot busyness (recent activity); absent ⇒ 0 (drain has no busiest preference). */
|
|
33
|
+
busynessBySlot?: Record<string, number>;
|
|
34
|
+
/** Optional per-slot "drain in progress" hold (balancer-tracked); absent ⇒ false. */
|
|
35
|
+
drainInProgressBySlot?: Record<string, boolean>;
|
|
36
|
+
/** Optional per-slot last-audit-divergent flag (scheduled-audit-tracked); absent ⇒ false. */
|
|
37
|
+
auditDivergentBySlot?: Record<string, boolean>;
|
|
38
|
+
}
|
|
39
|
+
/** One ledger assignment → the policy's SlotState. */
|
|
40
|
+
export declare function mapSlot(a: CredentialAssignment, opts: SlotMapOptions): SlotState;
|
|
41
|
+
export declare function mapSlots(assignments: readonly CredentialAssignment[], opts: SlotMapOptions): SlotState[];
|
|
42
|
+
/** The raw balancer config block (subscriptionPool.credentialRepointing.balancer + siblings). */
|
|
43
|
+
export interface RawRebalancerConfig {
|
|
44
|
+
passIntervalMs?: number;
|
|
45
|
+
highWaterPct?: number;
|
|
46
|
+
criticalPct?: number;
|
|
47
|
+
maxForcedSwapsPerPass?: number;
|
|
48
|
+
minScoreDelta?: number;
|
|
49
|
+
drainHorizonHours?: number;
|
|
50
|
+
drainHeadroomMinPct?: number;
|
|
51
|
+
staleQuotaPollPeriods?: number;
|
|
52
|
+
urgencyClampHours?: number;
|
|
53
|
+
maxForcedOverridesPerWindow?: number;
|
|
54
|
+
breakerThreshold?: number;
|
|
55
|
+
auditCadenceMs?: number;
|
|
56
|
+
desiredDefaultAccountId?: string | null;
|
|
57
|
+
/** Number of slots — clamps maxForcedSwapsPerPass upper bound. */
|
|
58
|
+
slotCount?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clamp the configured knobs into the resolved config the orchestrator/policy use.
|
|
62
|
+
* Cooldowns are DERIVED from the poll interval (per-pair ≥1×, per-tenant ≥2× — the
|
|
63
|
+
* lag-sensored controller floors, §2.4); stale-quota is N poll periods.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveRebalancerConfig(raw: RawRebalancerConfig): RebalancerResolvedConfig;
|
|
66
|
+
//# sourceMappingURL=CredentialRebalancerSnapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRebalancerSnapshot.d.ts","sourceRoot":"","sources":["../../src/core/CredentialRebalancerSnapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAI1E;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,yBAAyB,GAAG,YAAY,CAAC,QAAQ,CAAC,CAUrF;AAaD,oDAAoD;AACpD,wBAAgB,UAAU,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAYlF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAEjG;AAED,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kGAAkG;IAClG,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,qFAAqF;IACrF,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChD;AAED,sDAAsD;AACtD,wBAAgB,OAAO,CAAC,CAAC,EAAE,oBAAoB,EAAE,IAAI,EAAE,cAAc,GAAG,SAAS,CAahF;AAED,wBAAgB,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,EAAE,IAAI,EAAE,cAAc,GAAG,SAAS,EAAE,CAExG;AAOD,iGAAiG;AACjG,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,mBAAmB,GAAG,wBAAwB,CAsB1F"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialRebalancerSnapshot — pure mappers from the live system's state into the
|
|
3
|
+
* balancer's read-only pass snapshot (Increment B, step B3b wiring helper).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4.
|
|
6
|
+
*
|
|
7
|
+
* The CredentialRebalancer orchestrator (B3a) consumes injected `listSlots` / `listAccounts`
|
|
8
|
+
* / `resolveConfig` providers. These pure functions ARE those providers — they translate
|
|
9
|
+
* the CredentialLocationLedger assignments and the SubscriptionPool accounts into the
|
|
10
|
+
* policy's `SlotState` / `AccountState`, and clamp the configured balancer knobs into the
|
|
11
|
+
* resolved config. Kept pure + out of server.ts so the mapping (the place a units/sign
|
|
12
|
+
* bug would silently mis-steer the balancer) is unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
const HOUR_MS = 3600_000;
|
|
15
|
+
/**
|
|
16
|
+
* Map the pool account lifecycle status into the policy's eligibility status.
|
|
17
|
+
* Only needs-reauth/disabled are INELIGIBLE (dead credentials). rate-limited is `ok`:
|
|
18
|
+
* a walled account must stay eligible so wall-avoidance can RESCUE its slot (its high
|
|
19
|
+
* utilization % drives the wall objective; excluding it would strand the very slot that
|
|
20
|
+
* needs help).
|
|
21
|
+
*/
|
|
22
|
+
export function mapAccountStatus(s) {
|
|
23
|
+
switch (s) {
|
|
24
|
+
case 'needs-reauth': return 'needs-reauth';
|
|
25
|
+
case 'disabled': return 'disabled';
|
|
26
|
+
case 'active':
|
|
27
|
+
case 'warming':
|
|
28
|
+
case 'rate-limited':
|
|
29
|
+
default:
|
|
30
|
+
return 'ok';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function pctOrNull(v) {
|
|
34
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
35
|
+
}
|
|
36
|
+
function hoursUntil(resetsAt, now) {
|
|
37
|
+
if (!resetsAt)
|
|
38
|
+
return null;
|
|
39
|
+
const t = Date.parse(resetsAt);
|
|
40
|
+
if (Number.isNaN(t))
|
|
41
|
+
return null;
|
|
42
|
+
return Math.max(0, (t - now) / HOUR_MS);
|
|
43
|
+
}
|
|
44
|
+
/** One pool account → the policy's AccountState. */
|
|
45
|
+
export function mapAccount(account, now) {
|
|
46
|
+
const q = account.lastQuota ?? null;
|
|
47
|
+
const measuredAt = q?.measuredAt ? Date.parse(q.measuredAt) : NaN;
|
|
48
|
+
return {
|
|
49
|
+
accountId: account.id,
|
|
50
|
+
status: mapAccountStatus(account.status),
|
|
51
|
+
fiveHrPct: pctOrNull(q?.fiveHour?.utilizationPct),
|
|
52
|
+
weeklyPct: pctOrNull(q?.sevenDay?.utilizationPct),
|
|
53
|
+
weeklyResetsInHours: hoursUntil(q?.sevenDay?.resetsAt, now),
|
|
54
|
+
// No quota reading at all ⇒ measuredAt 0 (epoch) ⇒ always STALE ⇒ source-only.
|
|
55
|
+
measuredAt: Number.isNaN(measuredAt) ? 0 : measuredAt,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function mapAccounts(accounts, now) {
|
|
59
|
+
return accounts.map((a) => mapAccount(a, now));
|
|
60
|
+
}
|
|
61
|
+
/** One ledger assignment → the policy's SlotState. */
|
|
62
|
+
export function mapSlot(a, opts) {
|
|
63
|
+
const lastVerifiedAt = a.lastVerifiedAt ? Date.parse(a.lastVerifiedAt) : NaN;
|
|
64
|
+
return {
|
|
65
|
+
slot: a.slot,
|
|
66
|
+
// The ledger uses '' for a quarantined-but-unassigned slot; normalize to null.
|
|
67
|
+
tenantAccountId: a.accountId ? a.accountId : null,
|
|
68
|
+
isDefault: opts.defaultSlot != null && a.slot === opts.defaultSlot,
|
|
69
|
+
quarantined: a.quarantined,
|
|
70
|
+
lastVerifiedAt: Number.isNaN(lastVerifiedAt) ? null : lastVerifiedAt,
|
|
71
|
+
lastAuditDivergent: opts.auditDivergentBySlot?.[a.slot] ?? false,
|
|
72
|
+
drainInProgress: opts.drainInProgressBySlot?.[a.slot] ?? false,
|
|
73
|
+
busyness: opts.busynessBySlot?.[a.slot] ?? 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function mapSlots(assignments, opts) {
|
|
77
|
+
return assignments.map((a) => mapSlot(a, opts));
|
|
78
|
+
}
|
|
79
|
+
function clamp(v, lo, hi, dflt) {
|
|
80
|
+
const n = typeof v === 'number' && Number.isFinite(v) ? v : dflt;
|
|
81
|
+
return Math.min(hi, Math.max(lo, n));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clamp the configured knobs into the resolved config the orchestrator/policy use.
|
|
85
|
+
* Cooldowns are DERIVED from the poll interval (per-pair ≥1×, per-tenant ≥2× — the
|
|
86
|
+
* lag-sensored controller floors, §2.4); stale-quota is N poll periods.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveRebalancerConfig(raw) {
|
|
89
|
+
const passIntervalMs = clamp(raw.passIntervalMs, 60_000, 3_600_000, 300_000);
|
|
90
|
+
const slotCount = Math.max(1, raw.slotCount ?? 1);
|
|
91
|
+
const stalePeriods = clamp(raw.staleQuotaPollPeriods, 1, 10, 2);
|
|
92
|
+
return {
|
|
93
|
+
policy: {
|
|
94
|
+
highWaterPct: clamp(raw.highWaterPct, 50, 99, 85),
|
|
95
|
+
criticalPct: clamp(raw.criticalPct, 85, 99, 95),
|
|
96
|
+
drainHorizonHours: clamp(raw.drainHorizonHours, 1, 96, 24),
|
|
97
|
+
drainHeadroomMinPct: clamp(raw.drainHeadroomMinPct, 0, 100, 30),
|
|
98
|
+
minScoreDelta: clamp(raw.minScoreDelta, 0, 1000, 10),
|
|
99
|
+
maxForcedSwapsPerPass: clamp(raw.maxForcedSwapsPerPass, 1, slotCount, 1),
|
|
100
|
+
perPairCooldownMs: passIntervalMs, // ≥1× poll interval
|
|
101
|
+
perTenantCooldownMs: passIntervalMs * 2, // ≥2× poll interval (defeats 3-way rotation)
|
|
102
|
+
staleQuotaMs: passIntervalMs * stalePeriods,
|
|
103
|
+
urgencyClampHours: clamp(raw.urgencyClampHours, 1, 24, 4),
|
|
104
|
+
},
|
|
105
|
+
auditCadenceMs: clamp(raw.auditCadenceMs, HOUR_MS, 24 * HOUR_MS, 6 * HOUR_MS),
|
|
106
|
+
desiredDefaultAccountId: raw.desiredDefaultAccountId ?? null,
|
|
107
|
+
maxForcedOverridesPerWindow: clamp(raw.maxForcedOverridesPerWindow, 1, 100, 5),
|
|
108
|
+
breakerThreshold: clamp(raw.breakerThreshold, 1, 20, 3),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=CredentialRebalancerSnapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialRebalancerSnapshot.js","sourceRoot":"","sources":["../../src/core/CredentialRebalancerSnapshot.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-13T23:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-13T23:13:47.670Z",
|
|
5
|
+
"instarVersion": "1.3.545",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Begins Increment B (the autonomous use-it-or-lose-it drainer) with its decision core, deliberately split out so the entire policy is unit-testable before any autonomous write exists.
|
|
9
|
+
|
|
10
|
+
- **`CredentialRebalancerPolicy.decidePass()`** (src/core/CredentialRebalancerPolicy.ts) — a PURE function computing the zero-or-more credential swaps for one balancer pass from a read-only snapshot (per-account quota + reset proximity, per-slot tenancy/verify/activity, cooldown state, resolved config):
|
|
11
|
+
- **Objective 1 — wall avoidance:** a tenant over the high-water mark (default 85%, either window) gets the highest-headroom eligible account; a tenant over the critical mark (default 95%) triggers a wall-OVERRIDE that bypasses the cooldowns and the 1-swap/pass cap — itself bounded by the fresh-data gate (never re-fires on the same sensor snapshot), `maxForcedSwapsPerPass`, and a per-window override budget whose exhaustion surfaces a degraded + attention terminal state instead of looping. The rescue target must be verified-recent (the recency gate).
|
|
12
|
+
- **Objective 2 — use-it-or-lose-it drain:** an account whose WEEKLY window resets within the horizon with ≥30% unused headroom is dealt to the busiest eligible slot (weekly only — 5h windows regenerate); a per-slot "drain in progress" hold stops the 3-way drain rotation from fragmenting the window.
|
|
13
|
+
- **Eligibility + hysteresis:** needs-reauth/disabled/quarantined/unverified never participate; stale-quota accounts are SOURCE-only; per-pair + per-tenant cooldowns (the tenant cooldown defeats the 3-way rotation attack); a min-improvement floor with urgency clamped at 4h-to-reset; 1 swap per pass for non-forced objectives.
|
|
14
|
+
- **Unwired + dark.** Nothing actuates on the decision yet. Dead-default eviction, the correlated-oracle-outage floor, quarantine-exit, the P19 breaker, and the scheduled identity audit are the next Increment-B step <!-- tracked: 20905 -->; wiring the decision through the live executor (under the existing `subscriptionPool.credentialRepointing` dark/dry-run gate), the live `GET /credentials/rebalancer` status, and the routes follow <!-- tracked: 20905 -->.
|
|
15
|
+
|
|
16
|
+
Adds the highest-priority objective to the balancer decision core: keep `~/.claude` serving a healthy account so manual `claude` invocations stay predictable (§2.4 objective-0).
|
|
17
|
+
|
|
18
|
+
- **Dead/quarantined-default eviction** — when the default slot's tenant goes needs-reauth/disabled OR the default slot is quarantined, `decidePass()` now deals a healthy, identity-verified-recent tenant into `~/.claude` (a `default-eviction` decision), parking the dead/quarantined credential in the vacated slot with an attention note. This runs before wall-avoidance and drain — a frozen default freezes the operator's manual `claude`.
|
|
19
|
+
- **Correlated-oracle-outage floor** — when NO slot is currently oracle-verifiable (an `api.anthropic.com` storm quarantines every probe at once), the policy does NOT empty or churn the default: it preserves the last-known-good assignment, surfaces a degraded + attention entry, and performs no eviction until the oracle returns. Honest bound: the floor's guarantee is "preserve last-known-good + flag", explicitly NOT "manual claude is certified live".
|
|
20
|
+
- **Bounded fallback** — verifiable slots exist but none is an eligible healthy tenant → surface, do not act. Two new inputs (`desiredDefaultAccountId`, per-slot `lastKnownGoodAccountId`) + the `default-eviction` objective. Still pure, still unwired; consecutive-forced-eviction P19-capping is the next step <!-- tracked: 20905 -->.
|
|
21
|
+
|
|
22
|
+
Adds the stateful orchestrator around the Increment-B decision core — the loop that will eventually run the autonomous balancer, built and tested before it is wired to anything.
|
|
23
|
+
|
|
24
|
+
- **`CredentialRebalancer.tick()`** (src/core/CredentialRebalancer.ts) — builds the read-only pass snapshot from injected providers, calls `decidePass()`, and actuates each accepted swap through the injected executor wrapper, under the feature's dark/dry-run gate:
|
|
25
|
+
- **Dark = strict no-op:** when the feature is disabled the pass returns immediately having called the providers and the executor ZERO times.
|
|
26
|
+
- **Dry-run actuates the decision but not the write:** the executor enforces dry-run (no credential moves); the pass advances cooldown state so a dry-run soak shows realistic anti-churn cadence.
|
|
27
|
+
- **The executor is the only write path:** a swap rejection is caught and treated as a failure (no crash).
|
|
28
|
+
- **P19 breaker:** N consecutive LIVE failed swaps opens it; a success resets it; it self-heals by re-probing on later passes. A dry-run never trips it.
|
|
29
|
+
- **Cross-pass hysteresis:** cooldown timestamps recorded by the tenant-account pair (matching the policy's cooldown check), the per-window forced-override budget incremented only on a successful wall-override and time-reset.
|
|
30
|
+
- **Unwired + dark.** The server `setInterval` pass, the live `GET /credentials/rebalancer` status, and a tick-serialization (reentrancy) guard are the next step <!-- tracked: 20905 -->.
|
|
31
|
+
|
|
32
|
+
Adds the pure translation layer between the live system state and the balancer's decision core, kept out of server.ts so a units/sign bug can't silently mis-steer the balancer.
|
|
33
|
+
|
|
34
|
+
- **`CredentialRebalancerSnapshot`** (src/core/CredentialRebalancerSnapshot.ts) — pure mappers:
|
|
35
|
+
- `mapAccount`/`mapAccounts`: a SubscriptionPool account → the policy's `AccountState` (5h/weekly utilization %, weekly-reset hours, status). A missing quota reading maps to an epoch `measuredAt` so the account is treated as STALE (source-only) — never dealt in on a reading we don't have. `rate-limited` stays eligible (`ok`) so wall-avoidance can rescue its slot; only `needs-reauth`/`disabled` are ineligible.
|
|
36
|
+
- `mapSlot`/`mapSlots`: a CredentialLocationLedger assignment → the policy's `SlotState` (default-slot flag, quarantine, last-verified time; a quarantined-empty slot normalizes to a null tenant).
|
|
37
|
+
- `resolveRebalancerConfig`: clamp the configured knobs into the resolved config and derive the cooldowns from the poll interval (per-pair 1×, per-tenant 2×) + the stale-quota window (N poll periods).
|
|
38
|
+
- Unwired + dark; the server construction + the timer pass + the live status route are the next step <!-- tracked: 20905 -->.
|
|
39
|
+
|
|
40
|
+
## What to Tell Your User
|
|
41
|
+
|
|
42
|
+
Nothing changes — this is off-by-default internal logic with no effect yet. It's the "brain" of the eventual automatic account-balancer: given how much quota each of your accounts has left and how close each is to its weekly reset, it works out whether to shuffle one account's login to a different slot to (a) rescue a session about to hit a wall, or (b) burn down a weekly allowance that would otherwise expire unused. Right now it only *decides* — nothing acts on those decisions, so no credential moves. It's built first and on its own precisely so every rule can be tested in isolation before anything is ever wired to actually move a login, and the whole balancer stays switched off until you decide otherwise.
|
|
43
|
+
|
|
44
|
+
Still nothing changes — off-by-default internal logic. This teaches the (not-yet-active) account-balancer to protect the one login that must always work: your default Claude account. If that account ever needs a re-login or gets quarantined, the balancer's plan is to slide a known-good account into its place so your plain claude command keeps working, and flag the broken one for you. And if the check it relies on (Anthropic's identity endpoint) is down for everything at once, it deliberately does nothing rather than risk making the default worse — preserving the last-known-good and telling you honestly that it can't currently certify it's live. It only decides; nothing acts yet.
|
|
45
|
+
|
|
46
|
+
Still nothing changes — off-by-default internal logic with no effect yet. This is the loop that will eventually run the automatic account-balancer: every few minutes it would look at your quota picture, decide whether a login should move, and carry it out. It's built and tested now, but deliberately not connected to anything that runs on a timer, so it never fires. When it is connected, it stays switched off until you turn it on, and even then a first dry-run mode lets it show what it would do without moving a single credential. There's also a built-in circuit-breaker: if a move ever fails repeatedly, it stops trying and flags it rather than thrashing.
|
|
47
|
+
|
|
48
|
+
Still nothing changes — off-by-default internal plumbing. This is the translator that turns your real account quota and login layout into the form the (not-yet-active) balancer's brain understands. It's careful about safety even here: if it doesn't actually have a fresh quota reading for an account, it treats that account as "don't move anything onto it" rather than guessing; and a rate-limited account stays eligible to be rescued rather than being written off. It only translates data — nothing acts.
|
|
49
|
+
|
|
50
|
+
## Summary of New Capabilities
|
|
51
|
+
|
|
52
|
+
No new runtime capability — this is the unwired decision core for the autonomous balancer (Increment B), shipped dark. New internal module `CredentialRebalancerPolicy` (pure `decidePass()`): computes the per-pass wall-avoidance / drain / default-preference swap decisions with bounded wall-override, eligibility, and hysteresis. Not wired into any runtime path; no route, no config flag, no credential write path.
|
|
53
|
+
|
|
54
|
+
No new runtime capability — extends the unwired Increment-B decision core. `decidePass()` gains §2.4 objective-0: dead/quarantined-default eviction (deal a healthy verified tenant into `~/.claude`) and the correlated-oracle-outage floor (preserve last-known-good, never empty the default during an outage). Not wired into any runtime path; no route, no config flag, no credential write path.
|
|
55
|
+
|
|
56
|
+
No new runtime capability — the unwired autonomous-write orchestrator for the Increment-B balancer, shipped dark. New internal module `CredentialRebalancer` (`tick()` + `status()`): wraps the pure decision core in a pass loop, actuates via the gated executor under the dark/dry-run gate, carries cross-pass cooldown state + a P19 breaker. Not wired into any runtime path; no route, no config flag, no credential write path of its own.
|
|
57
|
+
|
|
58
|
+
No new runtime capability — pure translation helpers for the Increment-B balancer, shipped dark. New internal module `CredentialRebalancerSnapshot` (mapAccount/mapSlot/resolveRebalancerConfig): the unwired providers the balancer orchestrator will consume. Not wired into any runtime path; no route, no config flag, no credential write path.
|
|
59
|
+
|
|
60
|
+
## Evidence
|
|
61
|
+
|
|
62
|
+
- `tests/unit/credential-rebalancer-policy.test.ts` (18) — eligibility (needs-reauth tenant excluded; quarantined slot not a target); wall avoidance (rescue with highest-headroom; no rescue when only target also walls; held behind per-pair cooldown; triggers on the weekly window too); wall-override (bypasses cooldown; fresh-data gate no-re-fire; per-window budget exhaustion → degraded + attention; ≤ maxForcedSwapsPerPass when multiple critical; recency gate rejects a stale-verify target); stale-quota source-only; drain (weekly-only, headroom floor, drain-in-progress hold excluded, busiest-slot destination); zero-actuation reason; 1-swap-per-pass. tsc + full lint clean.
|
|
63
|
+
|
|
64
|
+
- `tests/unit/credential-rebalancer-policy.test.ts` (+6, 24 total) — deals a healthy verified tenant in when the default tenant is needs-reauth; rescues a quarantined default too; correlated-outage floor preserves last-known-good and does NOT evict; dead default with no healthy tenant surfaces without acting; healthy default is a no-op (normal objectives run); inert when no default account is configured. tsc + full lint clean.
|
|
65
|
+
|
|
66
|
+
- `tests/unit/credential-rebalancer.test.ts` (7) — dark = strict no-op (zero executor calls); actuates one swap for a walling scenario; dry-run drives the executor (no-op write) and advances cooldowns; the per-pair cooldown holds a re-swap on the next pass; the P19 breaker opens after 3 consecutive live failures and resets on a success; a dry-run ok never trips the breaker; the status surface reports enabled + breaker + last pass. Independent second-pass review CONCUR. tsc + full lint clean.
|
|
67
|
+
|
|
68
|
+
- `tests/unit/credential-rebalancer-snapshot.test.ts` (10) — status eligibility (rate-limited stays ok; needs-reauth/disabled ineligible); quota mapping incl. weekly-reset hours + missing-reading → stale; slot mapping (default flag, quarantined-empty → null tenant, override fields); config defaults + clamping + cooldown derivation. tsc + full lint clean.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Side-Effects Review — WS5.2 Increment B / B1: the balancer decision core (dark/unwired)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ws52-incb-b1-rebalancer-policy`
|
|
4
|
+
**Date:** 2026-06-13
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** not required at B1 (pure decision logic, unwired, zero IO, no autonomous-write surface — the second-pass review attaches to B3, where the decision is actuated through the live executor under the dark gate)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/core/CredentialRebalancerPolicy.ts` — the §2.4 "stock-trader loop" decision core as a PURE function `decidePass(input) → { decisions, degraded, attention, noActuationReason }`, plus exhaustive unit tests. Given a read-only snapshot (per-account quota + reset proximity, per-slot tenancy/verify/activity, cooldown state, resolved+clamped config) it computes the zero-or-more swap decisions for one pass: objective-1 wall avoidance + the bounded wall-override, objective-2 use-it-or-lose-it drain, eligibility, and the hysteresis floors. It performs NO IO and holds NO authority — it DECIDES; the actuator (step B3) routes an accepted decision through the Step-5 executor under the dark/dry-run gate. This is the first of Increment B's steps (B2 = dead-default eviction + correlated-outage floor + quarantine-exit + P19 breaker + scheduled identity audit; B3 = wiring/actuation/routes; B4 = integration/e2e/livetest), sequenced so the entire policy is testable before any autonomous write exists.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `decidePass()` — add — the per-pass swap policy. It is a DECISION producer, not an authority: it returns proposed swaps + surfaced terminal states. Nothing actuates on its output yet (unwired). When B3 wires it, actuation is gated by `subscriptionPool.credentialRepointing.enabled` + `dryRun` and verified by the oracle (the Tier-0 justification, §2.4: deterministic policy over enumerable numeric thresholds, every decision audited, reversible swap verified by the oracle).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
No block/allow surface. The nearest analog — refusing to emit a swap — is the conservative direction (a withheld swap is a no-op; the spec's "a pass with no actuation performs zero keychain/CLI operations" invariant). Concretely conservative: stale-quota accounts are SOURCE-only (never dealt in, lest stale headroom mask a wall); ineligible (needs-reauth/disabled/quarantined/unverified) tenants never participate; a critical slot whose override is blocked WAITS rather than being non-forced-rescued.
|
|
20
|
+
|
|
21
|
+
## 2. Under-block
|
|
22
|
+
The honest residual the spec names (§2.4 round-4): the wall-path recency gate NARROWS but does not CLOSE the "target passed its last audit then died after it" window — a just-died-but-recently-verified target can still be dealt into a walling slot, bounded to "victim slot quarantined + one re-auth, surfaced". B1 implements the recency gate (a target must be verified within the audit cadence and non-divergent); the residual is accepted per the spec's oracle-split rationale, and the post-commit verify (B3, via the executor) is the backstop.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
Correct layer, and the deliberate point of B1: the policy is split OUT of the actuation so every threshold/cap/cooldown is unit-testable without a keychain (the same fake-deps discipline as the executor). It consumes already-resolved+clamped config (the resolver lives in B3) so the pure core never reads raw config or clamps.
|
|
26
|
+
|
|
27
|
+
## 4. Signal vs authority compliance
|
|
28
|
+
- [x] No — this change has no block/allow surface and, at B1, no authority at all (its output is unwired).
|
|
29
|
+
|
|
30
|
+
The policy is a deterministic evaluator over numeric thresholds; the authority to actuate is B3's and is gated dark + dry-run-first + oracle-verified. (Ref: docs/signal-vs-authority.md; §2.4 Tier-0 supervision justification.)
|
|
31
|
+
|
|
32
|
+
## 5. Interactions
|
|
33
|
+
- **Hysteresis interactions (designed, tested):** 1-swap-per-pass for non-forced objectives; the wall-override may emit up to `maxForcedSwapsPerPass` and bypasses cooldowns but is itself bounded by the fresh-data gate (no re-fire on the same sensor snapshot) and `maxForcedOverridesPerWindow` (exhaustion → surfaced terminal state, never a loop). Per-pair + per-tenant cooldowns both key on the ACCOUNT pair/tenant (consistent basis; the per-tenant cooldown is what defeats the 3-way rotation attack pairwise cooldowns miss). Drain carries a per-slot "drain in progress" hold so the 3-way drain rotation can't fragment the very window it means to use.
|
|
34
|
+
- **Priority interaction:** wall (forced) short-circuits the pass; otherwise non-forced wall → drain → default-preference, one swap max. A critical slot is never downgraded to a non-forced rescue in the same pass.
|
|
35
|
+
- No shadowing/race — the module is pure and unwired; nothing else calls it.
|
|
36
|
+
|
|
37
|
+
## 6. External surfaces
|
|
38
|
+
None today (unwired). The module changes no API, no config, no agent-facing surface. When B3 wires it the only external effect is the (dark-gated, dry-run-first, audited, oracle-verified, reversible) credential swap the executor already owns.
|
|
39
|
+
|
|
40
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
41
|
+
- **Machine-local BY DESIGN.** The balancer permutes THIS machine's credential slots based on THIS machine's quota/activity snapshot. Each machine runs its own pass over its own keychain; there is no cross-machine input or coordination (a credential has exactly one home, on one machine, §0.d). No replication, no proxied read, no generated URL.
|
|
42
|
+
|
|
43
|
+
## 8. Rollback cost
|
|
44
|
+
Trivial. Revert the commit — the module is unwired, so no runtime behavior changes. No state, no migration, no credential touch (tests are pure). Any swap the wired balancer (B3) ever performs is the reversible, oracle-verified, dry-run-gated round-trip the executor already owns.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Side-Effects Review — WS5.2 Increment B / B2: dead-default eviction + correlated-outage floor (dark/unwired)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ws52-incb-b2-default-eviction`
|
|
4
|
+
**Date:** 2026-06-13
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** not required at B2 (pure decision logic extending B1's unwired policy; no IO, no autonomous-write surface — the second-pass review attaches to B3 where the decision is actuated)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Extends `CredentialRebalancerPolicy.decidePass()` with §2.4's objective-0: keep `~/.claude` serving a healthy verified tenant. When the DEFAULT slot's tenant is needs-reauth/disabled OR the default slot is quarantined, the policy deals a healthy verified tenant into `~/.claude` (a `default-eviction` decision, highest priority — a frozen default freezes the operator's manual `claude`). Two bounded fallbacks: when verifiable slots exist but none is an eligible healthy tenant it SURFACES and does not act; when NO slot is oracle-verifiable (the correlated-outage signature) it applies the floor — preserve the default's last-known-good assignment, surface a degraded + attention entry, and perform NO eviction until the oracle returns. Adds two input fields (`desiredDefaultAccountId`, per-slot `lastKnownGoodAccountId`) and the `default-eviction` objective. Still pure, still unwired.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `decidePass()` objective-0 branch — add — decides whether to rescue a dead/quarantined DEFAULT slot. A decision producer, not an authority (unwired). The eviction target is identity-verified-recent in the pure policy (B3's actuation re-verifies live before the move); the correlated-outage floor is the conservative direction (never empty/churn the default during an oracle storm).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
No block/allow surface. The conservative directions are deliberate: the correlated-outage floor refuses to act (preserve last-known-good) rather than risk dealing a dead tenant into the one slot that must stay alive; a dead default with no healthy tenant surfaces rather than forcing a bad move.
|
|
20
|
+
|
|
21
|
+
## 2. Under-block
|
|
22
|
+
Honest residual (§2.4 round-4): the floor's guarantee is "preserve the last KNOWN-GOOD assignment + surface", NOT "manual claude is certified live" — a correlated outage is observationally identical to "every grant died at once", and the oracle (the only liveness signal) is down by construction. The policy surfaces this explicitly ("NOT certified live") rather than over-claiming continuity. P19-capping of consecutive forced default evictions is B3 (stateful breaker), tracked there.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
Correct layer — a pure extension of the B1 decision core. Objective-0 runs FIRST (before wall/drain) because a frozen default is the highest-impact freeze; the branch returns immediately when it acts or applies the floor, so it never races the other objectives.
|
|
26
|
+
|
|
27
|
+
## 4. Signal vs authority compliance
|
|
28
|
+
- [x] No — no block/allow surface; unwired, no authority (output not actuated yet). Deterministic policy over the same enumerable thresholds. (Ref: docs/signal-vs-authority.md.)
|
|
29
|
+
|
|
30
|
+
## 5. Interactions
|
|
31
|
+
- **Priority interaction:** objective-0 short-circuits the pass before objective-1/2/3. Correct — the default slot's liveness beats wall/drain optimization. It only fires when a default account is configured AND the default slot is genuinely dead/quarantined, so a healthy default is a pure no-op that falls through to the normal objectives.
|
|
32
|
+
- **Eligibility interaction:** the dead default tenant is (by design) excluded from `participatingSlots`; objective-0 reads the raw `slots` for the default specifically — the one place the quarantine-exclusion rule is intentionally overridden (only for `~/.claude`).
|
|
33
|
+
- No shadowing/race — pure, unwired.
|
|
34
|
+
|
|
35
|
+
## 6. External surfaces
|
|
36
|
+
None today (unwired). No API/config/agent-facing change. When B3 wires it, the only effect is the dark-gated, dry-run-first, oracle-verified, reversible swap the executor owns — plus the surfaced attention/degraded entries.
|
|
37
|
+
|
|
38
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
39
|
+
- **Machine-local BY DESIGN.** The default slot, its tenant, and the oracle answers are all per-machine; each machine keeps its OWN `~/.claude` alive from its own snapshot. No cross-machine input or coordination.
|
|
40
|
+
|
|
41
|
+
## 8. Rollback cost
|
|
42
|
+
Trivial. Revert the commit — the module is unwired, so no runtime behavior changes. No state, no migration, no credential touch (tests are pure).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Side-Effects Review — WS5.2 Increment B / B3a: the balancer orchestrator (dark/unwired)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ws52-incb-b3a-rebalancer-orchestrator`
|
|
4
|
+
**Date:** 2026-06-13
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** independent reviewer subagent — CONCUR (this is the autonomous-write actuation orchestrator → Phase 5 required)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/core/CredentialRebalancer.ts` — the stateful orchestrator that wraps the pure `decidePass()` core in a pass loop: on `tick()` it builds the read-only snapshot from injected providers, asks the policy for the zero-or-more swaps, and ACTUATES each through the injected `swap` dep (a wrapper over the gated CredentialSwapExecutor) — but only under the feature's dark/dry-run gate. It carries the hysteresis state the pure policy cannot (cooldown timestamps across passes) and the §2.4 P19 breaker (N consecutive LIVE failed swaps opens it; a success resets it; it self-heals by re-probing). Unwired: the server `setInterval` pass + the live `GET /credentials/rebalancer` status is step B3b.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `CredentialRebalancer.tick()` — add — the autonomous balancer pass. This is the actuation layer (the decision was made in B1/B2). It actuates ONLY via `deps.swap()` (the gated, oracle-verified, staged executor) and ONLY when `isEnabled()` is true; even then the executor's own `dryRun` enforces no-write. The supervision is Tier-0 (§2.4): deterministic policy, oracle-verified reversible swaps, every pass audited.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
No block/allow surface. The conservative direction is dark = strict no-op (zero provider/executor calls when disabled) and the breaker (stop actuating after repeated failures).
|
|
20
|
+
|
|
21
|
+
## 2. Under-block
|
|
22
|
+
The breaker self-heals by re-probing rather than dead-latching — a deliberate choice (a transient keychain failure shouldn't permanently freeze the balancer); a persistent failure keeps it open every pass. The reviewer confirmed there is no path where an open breaker is never re-checked.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
Correct: the orchestrator holds ONLY the cross-pass state (cooldowns, breaker, forced-override window) and delegates the decision to the pure policy and the write to the executor. It re-implements neither. The cooldown key (tenant-account pair) matches exactly what `decidePass`'s `cooldownOk` reads.
|
|
26
|
+
|
|
27
|
+
## 4. Signal vs authority compliance
|
|
28
|
+
- [x] Yes — but the logic is a deterministic policy evaluator (the §2.4 Tier-0 justification), and its authority to actuate is gated dark + dry-run-first + oracle-verified + reversible.
|
|
29
|
+
|
|
30
|
+
The orchestrator holds actuation authority, but it is the conservatively-bounded kind the spec sanctions: a deterministic policy over numeric thresholds, every swap reversible and oracle-verified by the executor, every pass audited, shipped dark + dry-run-first. (Ref: docs/signal-vs-authority.md; §2.4.)
|
|
31
|
+
|
|
32
|
+
## 5. Interactions
|
|
33
|
+
- **Executor interaction:** the orchestrator's ONLY write path is `deps.swap()`; a swap throw is caught and treated as `ok:false` (no crash), incrementing the breaker only on a LIVE failure. The executor enforces enabled/dryRun internally, so the orchestrator's gate + the executor's gate are belt-and-suspenders.
|
|
34
|
+
- **Cooldown/breaker state:** carried in-memory across passes; recorded by the tenant-account pair the decision exchanged (not the slot seat); NOT advanced on a failed swap (so the next pass retries). The forced-override window increments only on a successful wall-override and time-resets.
|
|
35
|
+
- **Reentrancy (latent, scoped to B3b):** `tick()` has no in-flight guard; overlapping ticks could double-read state. B3b (the `setInterval` wiring) MUST serialize ticks (skip-if-running) <!-- tracked: 20905 -->. Harmless at B3a (unwired; the unit tests call tick() sequentially).
|
|
36
|
+
|
|
37
|
+
## 6. External surfaces
|
|
38
|
+
None today (unwired). No API/config/agent-facing change. When B3b wires it, the only effect is the dark-gated, dry-run-first, oracle-verified, reversible swap the executor owns, plus the `GET /credentials/rebalancer` status read.
|
|
39
|
+
|
|
40
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
41
|
+
- **Machine-local BY DESIGN.** Each machine runs its own pass over its own keychain/quota snapshot; the in-memory cooldown/breaker state is per-machine-per-process. No cross-machine input or coordination (a credential has one home, on one machine).
|
|
42
|
+
|
|
43
|
+
## 8. Rollback cost
|
|
44
|
+
Trivial. Revert the commit — the module is unwired, so no runtime behavior changes. No state, no migration, no credential touch (tests use fakes). Any swap the wired balancer ever performs is the reversible, oracle-verified, dry-run-gated round-trip the executor owns.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Side-Effects Review — WS5.2 Increment B / B3b-snapshot: balancer snapshot mappers (dark/unwired)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ws52-incb-b3b-snapshot-mappers`
|
|
4
|
+
**Date:** 2026-06-13
|
|
5
|
+
**Author:** echo
|
|
6
|
+
**Second-pass reviewer:** not required (pure stateless mappers, unwired, no IO, no decision/authority surface)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds `src/core/CredentialRebalancerSnapshot.ts` — pure functions translating the live system's state into the balancer's read-only pass snapshot: `mapAccount(s)`/`mapAccounts` (SubscriptionPool account → policy `AccountState`), `mapSlot(s)`/`mapSlots` (CredentialLocationLedger assignment → policy `SlotState`), `mapAccountStatus`, and `resolveRebalancerConfig` (clamp the configured knobs + derive cooldowns from the poll interval). These ARE the `listSlots`/`listAccounts`/`resolveConfig` providers the B3a orchestrator consumes; kept pure + out of server.ts so the units/sign translation (where a silent bug would mis-steer the balancer) is unit-testable. Still unwired — the server.ts construction + setInterval + live route is the next step.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- No decision point. These are stateless data mappers. The one judgment encoded — `rate-limited` maps to eligible (`ok`) — is a correctness requirement, not a gate: a walled account must stay eligible so wall-avoidance can rescue its slot (excluding it would strand the slot that needs help). Only `needs-reauth`/`disabled` (dead credentials) map to ineligible.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block / ## 2. Under-block
|
|
19
|
+
No block/allow surface — not applicable. Conservative defaults: a missing quota reading maps to `measuredAt: 0` (epoch) → always stale → the account is SOURCE-only (never dealt in on a reading we don't actually have); a quarantined-empty slot (ledger `accountId: ''`) normalizes to a null tenant (can't be moved).
|
|
20
|
+
|
|
21
|
+
## 3. Level-of-abstraction fit
|
|
22
|
+
Correct: the mapping is the boundary between the live stores (SubscriptionPool, CredentialLocationLedger, config) and the pure policy. Putting it in its own pure module (not inline in server.ts) is the deliberate testability choice — the same reason the policy + orchestrator are pure.
|
|
23
|
+
|
|
24
|
+
## 4. Signal vs authority compliance
|
|
25
|
+
- [x] No — no block/allow surface; stateless mappers, no authority. (Ref: docs/signal-vs-authority.md.)
|
|
26
|
+
|
|
27
|
+
## 5. Interactions
|
|
28
|
+
- Consumed only by the B3a orchestrator's injected providers (and its tests). The cooldown derivation (per-pair = 1× poll interval, per-tenant = 2×) and stale-quota window (N poll periods) are computed here so the orchestrator/policy stay config-agnostic. No shadowing/race — pure.
|
|
29
|
+
|
|
30
|
+
## 6. External surfaces
|
|
31
|
+
None today (unwired). No API/config/agent-facing change.
|
|
32
|
+
|
|
33
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
34
|
+
- **Machine-local BY DESIGN.** The mappers read THIS machine's pool accounts + ledger; the resulting snapshot is per-machine. No cross-machine input.
|
|
35
|
+
|
|
36
|
+
## 8. Rollback cost
|
|
37
|
+
Trivial. Revert the commit — unwired pure functions, no runtime behavior, no state, no credential touch.
|