instar 1.3.468 → 1.3.469
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/init.js +5 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +12 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/EscalationGovernor.d.ts +138 -0
- package/dist/core/EscalationGovernor.d.ts.map +1 -0
- package/dist/core/EscalationGovernor.js +308 -0
- package/dist/core/EscalationGovernor.js.map +1 -0
- package/dist/core/ModelSwapService.d.ts +116 -0
- package/dist/core/ModelSwapService.d.ts.map +1 -0
- package/dist/core/ModelSwapService.js +266 -0
- package/dist/core/ModelSwapService.js.map +1 -0
- package/dist/core/ModelTierEscalation.d.ts +128 -0
- package/dist/core/ModelTierEscalation.d.ts.map +1 -0
- package/dist/core/ModelTierEscalation.js +232 -0
- package/dist/core/ModelTierEscalation.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +3 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +313 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +5 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +9 -4
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +14 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/BurnDetector.d.ts +10 -0
- package/dist/monitoring/BurnDetector.d.ts.map +1 -1
- package/dist/monitoring/BurnDetector.js +6 -0
- package/dist/monitoring/BurnDetector.js.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.d.ts +3 -0
- package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
- package/dist/monitoring/GuardPostureTripwire.js +44 -0
- package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
- package/dist/monitoring/TokenLedger.d.ts +7 -0
- package/dist/monitoring/TokenLedger.d.ts.map +1 -1
- package/dist/monitoring/TokenLedger.js +17 -0
- package/dist/monitoring/TokenLedger.js.map +1 -1
- package/dist/monitoring/UltraSessionCapMonitor.d.ts +61 -0
- package/dist/monitoring/UltraSessionCapMonitor.d.ts.map +1 -0
- package/dist/monitoring/UltraSessionCapMonitor.js +103 -0
- package/dist/monitoring/UltraSessionCapMonitor.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +120 -1
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +55 -6
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +83 -67
- package/src/scaffold/templates.ts +4 -0
- package/src/templates/hooks/model-tier-reconciler.js +137 -0
- package/src/templates/hooks/model-tier-skill-entry.sh +72 -0
- package/src/templates/hooks/settings-template.json +15 -0
- package/upgrades/1.3.469.md +71 -0
- package/upgrades/session-mgmt-buildctx-race.eli16.md +9 -0
- package/upgrades/side-effects/model-tier-escalation.md +267 -0
- package/upgrades/side-effects/session-mgmt-buildctx-race.md +48 -0
- package/upgrades/side-effects/watchdog-launchctl-test-guard.md +84 -0
- package/upgrades/watchdog-launchctl-test-guard.eli16.md +9 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelSwapService — §5.3 of docs/specs/FABLE-MODEL-ESCALATION-SPEC.md.
|
|
3
|
+
*
|
|
4
|
+
* The narrow, server-side mid-session model swap (Trigger #1 only). The
|
|
5
|
+
* reconciler hook only SIGNALS — this service is the single authority that
|
|
6
|
+
* performs a swap, and it verifies every swap with an independent oracle.
|
|
7
|
+
*
|
|
8
|
+
* Contract (each clause spec-cited):
|
|
9
|
+
* - `:name` resolves by EXACT match against the live session registry —
|
|
10
|
+
* never globbed, substring-matched, or concatenated into the tmux target
|
|
11
|
+
* (round-2 Security-N1). The only string ever sent to the pane is the
|
|
12
|
+
* server-derived `/model <validated-id>`.
|
|
13
|
+
* - The model id is derived server-side via the §5.1 resolver — NEVER
|
|
14
|
+
* accepted from the caller (Sec-F5).
|
|
15
|
+
* - Refuses protected sessions (authorization boundary) and sessions that
|
|
16
|
+
* are not idle-with-empty-input (round-2 Security-F6 live-input
|
|
17
|
+
* collision window).
|
|
18
|
+
* - Honors `enabled:false` / `dryRun:true`; dryRun evaluates every gate
|
|
19
|
+
* but acquires nothing and injects nothing.
|
|
20
|
+
* - Injects via tmux `send-keys -l --` + a SEPARATE Enter (SessionManager
|
|
21
|
+
* .sendInput — the same hardened primitive the input route uses).
|
|
22
|
+
* - **Canary read-back via an oracle independent of the Session.model
|
|
23
|
+
* field this swap would write** (round-3 Integration-NEW-1): the pane's
|
|
24
|
+
* own acknowledgment of the /model command, parsed to exclude the echo
|
|
25
|
+
* of our injected input. Only on a confirmed match is Session.model
|
|
26
|
+
* updated. Unconfirmed ⇒ Session.model untouched, behaviourally default,
|
|
27
|
+
* ONE Attention item (round-1 Adversarial-H5 / Lessons-C3).
|
|
28
|
+
* - **Accounting fails toward counting** (round-2 Adversarial-NEW-2): the
|
|
29
|
+
* escalation is counted at injection, before the canary, once per
|
|
30
|
+
* (instance, transition) episode.
|
|
31
|
+
* - Server-side dwell guard: never swaps the same session twice within
|
|
32
|
+
* `minTierDwellMs` (§5.5 hysteresis backstop; suppressed flaps audited).
|
|
33
|
+
*/
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import { IDLE_PROMPT_PATTERNS } from './SessionManager.js';
|
|
37
|
+
import { SWAP_CAPABILITY, escalatedModelIds, resolveTierModel, } from './ModelTierEscalation.js';
|
|
38
|
+
/**
|
|
39
|
+
* Does the pane tail show a prompt-ready idle state WITH an empty input
|
|
40
|
+
* line? Fail-closed: anything ambiguous returns false and the swap is
|
|
41
|
+
* refused as retryable (round-2 Security-F6).
|
|
42
|
+
*/
|
|
43
|
+
export function paneIdleWithEmptyInput(tail) {
|
|
44
|
+
if (!tail)
|
|
45
|
+
return false;
|
|
46
|
+
const idle = IDLE_PROMPT_PATTERNS.some(p => tail.includes(p));
|
|
47
|
+
if (!idle)
|
|
48
|
+
return false;
|
|
49
|
+
// Empty input line: a prompt row with nothing typed after the prompt char —
|
|
50
|
+
// bare, or showing only the CLI's own placeholder hint (`Try "…`). The REAL
|
|
51
|
+
// CLI renders the prompt as `❯` (U+276F); synthetic/test panes and older
|
|
52
|
+
// renders use ASCII `>` — accept both (live-canary finding, 2026-06-09:
|
|
53
|
+
// the ASCII-only match made idle detection fail against every real
|
|
54
|
+
// session, refusing all swaps as not-idle).
|
|
55
|
+
const lines = tail.split('\n');
|
|
56
|
+
return lines.some(line => {
|
|
57
|
+
const stripped = line.replace(/[│|]/g, ' ').replace(/❯/g, '>').trim();
|
|
58
|
+
if (stripped === '>')
|
|
59
|
+
return true;
|
|
60
|
+
if (/^>\s*Try "/.test(stripped))
|
|
61
|
+
return true;
|
|
62
|
+
return false;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Independent-oracle parse (§5.3 canary): does the pane acknowledge that
|
|
67
|
+
* the model is now `modelId`, on a line that is NOT the echo of our own
|
|
68
|
+
* injected `/model …` input? Conservative by design: an unrecognized format
|
|
69
|
+
* reads as NOT confirmed (the spec's honest-degrade direction).
|
|
70
|
+
*
|
|
71
|
+
* The REAL CLI acks with the model's DISPLAY NAME, not the id (live-canary
|
|
72
|
+
* finding, 2026-06-09): `/model claude-fable-5` → "Set model to Fable 5 and
|
|
73
|
+
* saved as your default for new sessions". We accept the exact id OR the
|
|
74
|
+
* display form derived from the id (closed-enum ids only ever reach here,
|
|
75
|
+
* so the derivation is over a known, validated vocabulary): family token
|
|
76
|
+
* capitalized + up to two leading version components dot-joined —
|
|
77
|
+
* claude-fable-5 → "Fable 5", claude-opus-4-8 → "Opus 4.8".
|
|
78
|
+
*/
|
|
79
|
+
export function paneConfirmsModel(tail, modelId) {
|
|
80
|
+
if (!tail)
|
|
81
|
+
return false;
|
|
82
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
83
|
+
const alternatives = [escapeRe(modelId)];
|
|
84
|
+
const m = modelId.match(/^(?:claude-)?([a-z]+)((?:-[0-9]+)*)/i);
|
|
85
|
+
if (m) {
|
|
86
|
+
const family = m[1][0].toUpperCase() + m[1].slice(1).toLowerCase();
|
|
87
|
+
const version = m[2].split('-').filter(Boolean).slice(0, 2).join('.');
|
|
88
|
+
alternatives.push(escapeRe(version ? `${family} ${version}` : family));
|
|
89
|
+
}
|
|
90
|
+
const ack = new RegExp(`set model to[^\\n]*\\b(?:${alternatives.join('|')})\\b`, 'i');
|
|
91
|
+
return tail.split('\n').some(line => {
|
|
92
|
+
if (line.includes('/model'))
|
|
93
|
+
return false; // echo of our injected input
|
|
94
|
+
return ack.test(line);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export class ModelSwapService {
|
|
98
|
+
deps;
|
|
99
|
+
auditPath;
|
|
100
|
+
now;
|
|
101
|
+
wait;
|
|
102
|
+
/** In-memory per-session last-swap timestamps (dwell backstop). Reset on
|
|
103
|
+
* restart — the safe direction (a restart may swap immediately). */
|
|
104
|
+
lastSwapAt = new Map();
|
|
105
|
+
constructor(deps) {
|
|
106
|
+
this.deps = deps;
|
|
107
|
+
this.auditPath = path.join(deps.stateDir, 'state', 'model-tier-escalation', 'audit.jsonl');
|
|
108
|
+
this.now = deps.now ?? (() => Date.now());
|
|
109
|
+
this.wait = deps.wait ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Swap `name`'s session to `tier`. Body-validated upstream; this method
|
|
113
|
+
* re-validates everything it relies on (defense in depth).
|
|
114
|
+
*/
|
|
115
|
+
async swap(name, tier) {
|
|
116
|
+
const cfg = this.deps.getConfig();
|
|
117
|
+
// Exact-match registry lookup — by Session.name first, then by the
|
|
118
|
+
// registry's own tmuxSession value. Never partial, never globbed.
|
|
119
|
+
const running = this.deps.sessions.listRunningSessions();
|
|
120
|
+
const session = running.find(s => s.name === name) ?? running.find(s => s.tmuxSession === name);
|
|
121
|
+
if (!session) {
|
|
122
|
+
return this.refuse(name, tier, 'unknown-session');
|
|
123
|
+
}
|
|
124
|
+
const framework = (session.framework ?? 'claude-code');
|
|
125
|
+
if (SWAP_CAPABILITY[framework] !== 'mid-session') {
|
|
126
|
+
// §5.6 — declared capability honored by code, not prose.
|
|
127
|
+
return this.refuse(session.name, tier, 'launch-time-only-framework');
|
|
128
|
+
}
|
|
129
|
+
if (this.deps.protectedSessions().includes(session.tmuxSession)) {
|
|
130
|
+
return this.refuse(session.name, tier, 'protected-session');
|
|
131
|
+
}
|
|
132
|
+
// Server-derived id — the caller can only ever name a TIER.
|
|
133
|
+
const targetId = resolveTierModel(framework, tier, cfg, e => this.audit({ type: 'resolve-rejected', ...e }));
|
|
134
|
+
if (targetId == null) {
|
|
135
|
+
// Back-compat contract: no model configured ⇒ zero swaps, not an error.
|
|
136
|
+
this.audit({ type: 'noop', session: session.name, tier, reason: 'no-model-configured' });
|
|
137
|
+
return { status: 'noop', reason: 'no-model-configured' };
|
|
138
|
+
}
|
|
139
|
+
if (session.model === targetId) {
|
|
140
|
+
return { status: 'noop', reason: 'already-on-tier', model: targetId, confirmed: true };
|
|
141
|
+
}
|
|
142
|
+
// RESCUE DE-ESCALATION (Phase-5 review finding): a `tier:'default'` swap
|
|
143
|
+
// for a session CURRENTLY on an escalated id bypasses the enabled/dryRun
|
|
144
|
+
// gates. Those flags are the rollback levers — if they also refused the
|
|
145
|
+
// swap-back, an escalated session would be stranded on the ultra model in
|
|
146
|
+
// exactly the state the operator is trying to leave (the one refusal
|
|
147
|
+
// whose failure direction is MORE spend, inverting §3.5). Strictly
|
|
148
|
+
// cost-reducing, mirrors the governor exemption below; idle/protected/
|
|
149
|
+
// dwell/canary gates all still apply. Fleet installs stay inert: their
|
|
150
|
+
// sessions are never on an escalated id, so disabled still refuses.
|
|
151
|
+
const isRescueDeescalation = tier === 'default' && session.model != null && escalatedModelIds(cfg).has(session.model);
|
|
152
|
+
if (!cfg.enabled && !isRescueDeescalation) {
|
|
153
|
+
return this.refuse(session.name, tier, 'disabled');
|
|
154
|
+
}
|
|
155
|
+
// Dwell backstop (§5.5): never swap the same session twice within
|
|
156
|
+
// minTierDwellMs. Suppressed flaps are audited.
|
|
157
|
+
const last = this.lastSwapAt.get(session.id);
|
|
158
|
+
if (last != null && this.now() - last < cfg.costGuards.minTierDwellMs) {
|
|
159
|
+
this.audit({ type: 'flap-suppressed', session: session.name, tier, sinceLastSwapMs: this.now() - last });
|
|
160
|
+
return { status: 'refused', reason: 'dwell' };
|
|
161
|
+
}
|
|
162
|
+
// Idle + empty input line (round-2 Security-F6) — fail closed.
|
|
163
|
+
const tail = this.deps.sessions.captureMeaningfulTail(session.tmuxSession, 8);
|
|
164
|
+
if (!paneIdleWithEmptyInput(tail)) {
|
|
165
|
+
return this.refuse(session.name, tier, 'not-idle');
|
|
166
|
+
}
|
|
167
|
+
// §7/§8 cost gates — only escalations consume budget/lease; a swap back
|
|
168
|
+
// to default is always cost-free (it REDUCES spend).
|
|
169
|
+
const transition = `${session.model ?? 'default'}→${tier}`;
|
|
170
|
+
let admit = { allow: true };
|
|
171
|
+
if (tier === 'escalated') {
|
|
172
|
+
admit = this.deps.governor.admitEscalation({
|
|
173
|
+
instanceId: session.id,
|
|
174
|
+
accountId: session.subscriptionAccountId,
|
|
175
|
+
modelId: targetId,
|
|
176
|
+
transition,
|
|
177
|
+
dry: cfg.dryRun,
|
|
178
|
+
});
|
|
179
|
+
if (!admit.allow) {
|
|
180
|
+
return this.refuse(session.name, tier, `cost-guard:${admit.reason}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (cfg.dryRun && !isRescueDeescalation) {
|
|
184
|
+
this.audit({
|
|
185
|
+
type: 'dry-run-would-swap',
|
|
186
|
+
session: session.name,
|
|
187
|
+
tier,
|
|
188
|
+
model: targetId,
|
|
189
|
+
freeWindow: admit.freeWindow,
|
|
190
|
+
});
|
|
191
|
+
return { status: 'dry-run', model: targetId };
|
|
192
|
+
}
|
|
193
|
+
if (isRescueDeescalation && (!cfg.enabled || cfg.dryRun)) {
|
|
194
|
+
// Loud trail: a real swap performed while the feature is off/dry is
|
|
195
|
+
// exactly the event an operator auditing a rollback wants to see.
|
|
196
|
+
this.audit({ type: 'rescue-deescalation', session: session.name, model: targetId, enabled: cfg.enabled, dryRun: cfg.dryRun });
|
|
197
|
+
}
|
|
198
|
+
// Inject: literal text + separate Enter via the hardened primitive. The
|
|
199
|
+
// ONLY pane-bound string is the server-derived, enum-validated id.
|
|
200
|
+
const sent = this.deps.sessions.sendInput(session.tmuxSession, `/model ${targetId}`);
|
|
201
|
+
if (!sent) {
|
|
202
|
+
return this.refuse(session.name, tier, 'inject-failed');
|
|
203
|
+
}
|
|
204
|
+
if (tier === 'escalated') {
|
|
205
|
+
// Fails toward counting — recorded at injection, before the canary.
|
|
206
|
+
this.deps.governor.recordInjection(session.id, transition);
|
|
207
|
+
}
|
|
208
|
+
this.audit({ type: 'injected', session: session.name, tier, model: targetId });
|
|
209
|
+
// Canary read-back — independent oracle, N attempts.
|
|
210
|
+
const attempts = this.deps.canaryAttempts ?? 5;
|
|
211
|
+
const interval = this.deps.canaryIntervalMs ?? 600;
|
|
212
|
+
for (let i = 0; i < attempts; i++) {
|
|
213
|
+
await this.wait(interval);
|
|
214
|
+
const readback = this.deps.sessions.captureMeaningfulTail(session.tmuxSession, 30);
|
|
215
|
+
if (paneConfirmsModel(readback, targetId)) {
|
|
216
|
+
session.model = targetId;
|
|
217
|
+
try {
|
|
218
|
+
this.deps.saveSession(session);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
this.audit({ type: 'save-session-failed', session: session.name, error: err.message });
|
|
222
|
+
}
|
|
223
|
+
this.lastSwapAt.set(session.id, this.now());
|
|
224
|
+
this.audit({ type: 'swap-confirmed', session: session.name, tier, model: targetId, attempts: i + 1 });
|
|
225
|
+
return { status: 'swapped', model: targetId, confirmed: true };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// NOT confirmed: do NOT mark reconciled, do NOT touch Session.model;
|
|
229
|
+
// behaviourally the session is treated as default. One Attention item.
|
|
230
|
+
this.lastSwapAt.set(session.id, this.now()); // still dwell — don't re-inject in a tight loop
|
|
231
|
+
this.audit({ type: 'swap-unconfirmed', session: session.name, tier, model: targetId, attempts });
|
|
232
|
+
try {
|
|
233
|
+
this.deps.attention?.({
|
|
234
|
+
id: `model-swap-unconfirmed-${session.id}-${tier}`,
|
|
235
|
+
title: `Model swap unconfirmed: ${session.name}`,
|
|
236
|
+
summary: `Injected /model ${targetId} into "${session.name}" but the independent ` +
|
|
237
|
+
`read-back did not confirm within ${attempts} attempts. The session is ` +
|
|
238
|
+
`treated as DEFAULT tier; the swap was still counted against the ` +
|
|
239
|
+
`escalation budget (fails toward counting).`,
|
|
240
|
+
category: 'model-tier-escalation',
|
|
241
|
+
priority: 'NORMAL',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// attention surface unavailable — the audit record stands
|
|
246
|
+
}
|
|
247
|
+
return { status: 'unconfirmed', model: targetId, confirmed: false };
|
|
248
|
+
}
|
|
249
|
+
// ── internals ──────────────────────────────────────────────────────────
|
|
250
|
+
refuse(sessionName, tier, reason) {
|
|
251
|
+
this.audit({ type: 'refused', session: sessionName, tier, reason });
|
|
252
|
+
return { status: 'refused', reason };
|
|
253
|
+
}
|
|
254
|
+
/** Structured-fields-only audit appender (Sec-F7) — same trail as the
|
|
255
|
+
* governor so one file tells the whole escalation story. */
|
|
256
|
+
audit(event) {
|
|
257
|
+
try {
|
|
258
|
+
fs.mkdirSync(path.dirname(this.auditPath), { recursive: true });
|
|
259
|
+
fs.appendFileSync(this.auditPath, JSON.stringify({ ts: new Date(this.now()).toISOString(), source: 'model-swap', ...event }) + '\n');
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// best-effort — never throws into the swap path
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=ModelSwapService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelSwapService.js","sourceRoot":"","sources":["../../src/core/ModelSwapService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelTierEscalation — §5.1 of docs/specs/FABLE-MODEL-ESCALATION-SPEC.md.
|
|
3
|
+
*
|
|
4
|
+
* The per-framework (framework, tier) → concrete-model-id resolver plus the
|
|
5
|
+
* closed model-id enumerations and per-adapter swap-capability declarations.
|
|
6
|
+
*
|
|
7
|
+
* Core contracts (all spec-cited, all fail-closed):
|
|
8
|
+
* - Reads ONLY trusted config (`models.tierEscalation`) — never the
|
|
9
|
+
* mode-state file. The mode-state can request a TIER; it can never
|
|
10
|
+
* supply a model ID (spec Sec-F3).
|
|
11
|
+
* - `escalated: null` (or an absent framework entry) resolves to the
|
|
12
|
+
* default ⇒ no swap ever — the backwards-compat contract for
|
|
13
|
+
* codex/gemini/pi until they ship an ultra model (spec §5.1).
|
|
14
|
+
* - Every id is validated by regex AND membership in the framework's
|
|
15
|
+
* closed `knownModelIds` enumeration before it can reach a launch arg
|
|
16
|
+
* or tmux send-keys (spec Sec-F1/F2 — keystroke-injection guard).
|
|
17
|
+
* - Worst-case failure of every path = `null` = the session stays on its
|
|
18
|
+
* default model (spec §3.5: a routing decision, never a block).
|
|
19
|
+
*
|
|
20
|
+
* Pure logic, no I/O. The governor (§7/§8) and swap service (§5.3) live in
|
|
21
|
+
* EscalationGovernor.ts / ModelSwapService.ts.
|
|
22
|
+
*/
|
|
23
|
+
/** The frameworks the escalation policy knows. Closed enum — anything else
|
|
24
|
+
* resolves to null (spec §5.1 line 1). Kept as a literal union to avoid an
|
|
25
|
+
* import cycle with core/types.ts (matches the inline pattern used there). */
|
|
26
|
+
export type EscalationFramework = 'claude-code' | 'codex-cli' | 'gemini-cli' | 'pi-cli';
|
|
27
|
+
export declare const ESCALATION_FRAMEWORKS: readonly EscalationFramework[];
|
|
28
|
+
export type EscalationTier = 'default' | 'escalated';
|
|
29
|
+
/** §5.6 — machine-checkable per-adapter swap capability, not prose. */
|
|
30
|
+
export type SwapCapability = 'mid-session' | 'launch-time-only';
|
|
31
|
+
export interface TierEscalationFrameworkEntry {
|
|
32
|
+
/** `null` ⇒ use the account default (today's behavior). */
|
|
33
|
+
default: string | null;
|
|
34
|
+
/** `null` ⇒ no ultra model ⇒ escalation is a strict no-op (back-compat). */
|
|
35
|
+
escalated: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface TierEscalationTriggers {
|
|
38
|
+
/** Skill names whose entry signals an escalation work-mode (§4). */
|
|
39
|
+
skills: string[];
|
|
40
|
+
/** Trigger #1 also fires on project-initiative design state (§4). */
|
|
41
|
+
projectDesign: boolean;
|
|
42
|
+
/** Optional LLM intent check for ambiguous phrasing (§5.4). Default OFF. */
|
|
43
|
+
llmIntentCheck: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface TierEscalationCostGuards {
|
|
46
|
+
/** Map of model id → last free-window UTC date (inclusive). §8. */
|
|
47
|
+
respectFreeWindows: Record<string, string>;
|
|
48
|
+
/** Refuse escalation without a cached quota snapshot showing headroom (§7).
|
|
49
|
+
* Quota unavailable/errored ⇒ fail CLOSED to default. */
|
|
50
|
+
requireQuotaHeadroom: boolean;
|
|
51
|
+
/** §7 lease cap — admission control for concurrent escalated sessions. */
|
|
52
|
+
maxConcurrentEscalatedPerAccount: number;
|
|
53
|
+
/** §8 — load-bearing for Trigger #1 (short in-conversation swaps). */
|
|
54
|
+
maxEscalationsPerHour: number;
|
|
55
|
+
/** §8 — load-bearing for Trigger #2 (long spawned runs). null = disabled. */
|
|
56
|
+
dailyUltraTokenCap: number | null;
|
|
57
|
+
/** §5.5 — mode-state self-expiry. Expiry INVALIDATES (fresh trigger needed). */
|
|
58
|
+
maxEscalationTtlMs: number;
|
|
59
|
+
/** §5.5 asymmetric hysteresis — never swap twice within this window. */
|
|
60
|
+
minTierDwellMs: number;
|
|
61
|
+
/** §5.5 — de-escalate only after the condition is clear this many turns. */
|
|
62
|
+
minTierDwellTurns: number;
|
|
63
|
+
}
|
|
64
|
+
export interface TierEscalationConfig {
|
|
65
|
+
/** Fleet default OFF; dev agents (Echo/Codey) ship ENABLED (§10). */
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
/** Log intended swaps without performing them. `enabled:false` wins. */
|
|
68
|
+
dryRun: boolean;
|
|
69
|
+
triggers: TierEscalationTriggers;
|
|
70
|
+
frameworks: Partial<Record<EscalationFramework, TierEscalationFrameworkEntry>>;
|
|
71
|
+
costGuards: TierEscalationCostGuards;
|
|
72
|
+
}
|
|
73
|
+
/** §9 schema defaults — exactly the converged spec's config block. */
|
|
74
|
+
export declare const DEFAULT_TIER_ESCALATION_CONFIG: TierEscalationConfig;
|
|
75
|
+
export declare const KNOWN_CLAUDE_MODEL_IDS: readonly ["claude-fable-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5", "claude-haiku-4-5-20251001", "opus", "sonnet", "haiku"];
|
|
76
|
+
export declare const KNOWN_CODEX_MODEL_IDS: readonly ["gpt-5.2", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"];
|
|
77
|
+
export declare const KNOWN_MODEL_IDS: Record<EscalationFramework, readonly string[]>;
|
|
78
|
+
/**
|
|
79
|
+
* §5.6 — per-adapter swap capability. `claude-code` declares 'mid-session'
|
|
80
|
+
* but the §5.3 contract still verifies EVERY swap with the independent-oracle
|
|
81
|
+
* canary at runtime; the pre-enable proof is the gated live E2E
|
|
82
|
+
* (tests/e2e/model-swap-live-canary.test.ts). The other three are
|
|
83
|
+
* launch-time-only today (and strict no-ops until an escalated model is
|
|
84
|
+
* populated), so "documented, not silently dropped" is enforced by code.
|
|
85
|
+
*/
|
|
86
|
+
export declare const SWAP_CAPABILITY: Record<EscalationFramework, SwapCapability>;
|
|
87
|
+
/** §5.1 injection-safe id shape. Anything else is audit+reject. */
|
|
88
|
+
export declare const MODEL_ID_RE: RegExp;
|
|
89
|
+
/** Reason codes for fail-closed resolutions — structured, audit-friendly. */
|
|
90
|
+
export type ResolveRejectReason = 'unknown-framework' | 'no-framework-entry' | 'null-id' | 'invalid-id-shape' | 'id-not-in-closed-enum';
|
|
91
|
+
export interface ResolveAuditEvent {
|
|
92
|
+
framework: string;
|
|
93
|
+
tier: EscalationTier;
|
|
94
|
+
reason: ResolveRejectReason;
|
|
95
|
+
/** Escaped + truncated — never the raw value verbatim past 80 chars (Sec-F7). */
|
|
96
|
+
rejectedId?: string;
|
|
97
|
+
}
|
|
98
|
+
/** Escape + truncate an untrusted id for audit records (spec Sec-F7). */
|
|
99
|
+
export declare function escapeIdForAudit(id: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* §5.1 resolver — maps (framework, tier) → concrete model id | null,
|
|
102
|
+
* reading ONLY the trusted config. Every rejection fails closed to null
|
|
103
|
+
* (session stays on default) and reports a structured audit event.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveTierModel(framework: string, tier: EscalationTier, config: TierEscalationConfig | undefined, audit?: (event: ResolveAuditEvent) => void): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* True when escalation could ever change behavior for this framework:
|
|
108
|
+
* an escalated id is configured AND resolves through the closed enum.
|
|
109
|
+
* Used by callers to keep the no-escalated-model case a true no-op.
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasEscalatedModel(framework: string, config: TierEscalationConfig | undefined): boolean;
|
|
112
|
+
/** The set of configured escalated ids across all frameworks — used by the
|
|
113
|
+
* §11 router-exclusion guard and the §8 ultra-cap monitor. */
|
|
114
|
+
export declare function escalatedModelIds(config: TierEscalationConfig | undefined): Set<string>;
|
|
115
|
+
/**
|
|
116
|
+
* §8 free-window semantics: compared as a UTC date, inclusive through the
|
|
117
|
+
* named day. Returns true while the window is open. After expiry the
|
|
118
|
+
* quota/budget guards apply unchanged — the window relaxes nothing
|
|
119
|
+
* structural (informational + drives the dev-agent dogfood window).
|
|
120
|
+
*/
|
|
121
|
+
export declare function isWithinFreeWindow(modelId: string, guards: TierEscalationCostGuards, nowMs: number): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Merge an operator's (possibly partial / absent) config block over the §9
|
|
124
|
+
* defaults. Add-missing-only semantics live in PostUpdateMigrator; this is
|
|
125
|
+
* the READ-side normalizer so every consumer sees a complete shape.
|
|
126
|
+
*/
|
|
127
|
+
export declare function normalizeTierEscalationConfig(raw: unknown): TierEscalationConfig;
|
|
128
|
+
//# sourceMappingURL=ModelTierEscalation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelTierEscalation.d.ts","sourceRoot":"","sources":["../../src/core/ModelTierEscalation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelTierEscalation — §5.1 of docs/specs/FABLE-MODEL-ESCALATION-SPEC.md.
|
|
3
|
+
*
|
|
4
|
+
* The per-framework (framework, tier) → concrete-model-id resolver plus the
|
|
5
|
+
* closed model-id enumerations and per-adapter swap-capability declarations.
|
|
6
|
+
*
|
|
7
|
+
* Core contracts (all spec-cited, all fail-closed):
|
|
8
|
+
* - Reads ONLY trusted config (`models.tierEscalation`) — never the
|
|
9
|
+
* mode-state file. The mode-state can request a TIER; it can never
|
|
10
|
+
* supply a model ID (spec Sec-F3).
|
|
11
|
+
* - `escalated: null` (or an absent framework entry) resolves to the
|
|
12
|
+
* default ⇒ no swap ever — the backwards-compat contract for
|
|
13
|
+
* codex/gemini/pi until they ship an ultra model (spec §5.1).
|
|
14
|
+
* - Every id is validated by regex AND membership in the framework's
|
|
15
|
+
* closed `knownModelIds` enumeration before it can reach a launch arg
|
|
16
|
+
* or tmux send-keys (spec Sec-F1/F2 — keystroke-injection guard).
|
|
17
|
+
* - Worst-case failure of every path = `null` = the session stays on its
|
|
18
|
+
* default model (spec §3.5: a routing decision, never a block).
|
|
19
|
+
*
|
|
20
|
+
* Pure logic, no I/O. The governor (§7/§8) and swap service (§5.3) live in
|
|
21
|
+
* EscalationGovernor.ts / ModelSwapService.ts.
|
|
22
|
+
*/
|
|
23
|
+
export const ESCALATION_FRAMEWORKS = [
|
|
24
|
+
'claude-code',
|
|
25
|
+
'codex-cli',
|
|
26
|
+
'gemini-cli',
|
|
27
|
+
'pi-cli',
|
|
28
|
+
];
|
|
29
|
+
/** §9 schema defaults — exactly the converged spec's config block. */
|
|
30
|
+
export const DEFAULT_TIER_ESCALATION_CONFIG = {
|
|
31
|
+
enabled: false,
|
|
32
|
+
dryRun: true,
|
|
33
|
+
triggers: {
|
|
34
|
+
skills: ['build', 'autonomous', 'instar-dev', 'spec-converge'],
|
|
35
|
+
projectDesign: true,
|
|
36
|
+
llmIntentCheck: false,
|
|
37
|
+
},
|
|
38
|
+
frameworks: {
|
|
39
|
+
'claude-code': { default: 'claude-opus-4-8', escalated: 'claude-fable-5' },
|
|
40
|
+
'codex-cli': { default: null, escalated: null },
|
|
41
|
+
'gemini-cli': { default: null, escalated: null },
|
|
42
|
+
'pi-cli': { default: null, escalated: null },
|
|
43
|
+
},
|
|
44
|
+
costGuards: {
|
|
45
|
+
respectFreeWindows: { 'claude-fable-5': '2026-06-22' },
|
|
46
|
+
requireQuotaHeadroom: true,
|
|
47
|
+
maxConcurrentEscalatedPerAccount: 2,
|
|
48
|
+
maxEscalationsPerHour: 8,
|
|
49
|
+
dailyUltraTokenCap: null,
|
|
50
|
+
maxEscalationTtlMs: 21_600_000, // 6h
|
|
51
|
+
minTierDwellMs: 300_000, // 5min
|
|
52
|
+
minTierDwellTurns: 1,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Closed model-id enumerations per launch adapter (§5.2(c), net-new).
|
|
57
|
+
* A config value outside its framework's list NEVER reaches a launch arg or
|
|
58
|
+
* send-keys — resolveTierModel fails closed to null instead.
|
|
59
|
+
*
|
|
60
|
+
* claude-code: concrete ids the Claude CLI accepts via `--model`, plus the
|
|
61
|
+
* CLI tier aliases it documents (haiku/sonnet/opus). Includes the ultra
|
|
62
|
+
* model `claude-fable-5` — the first populated escalation target.
|
|
63
|
+
* codex-cli: mirror of CODEX_MODELS_SUBSCRIPTION (routes.ts spawn allowlist).
|
|
64
|
+
* gemini-cli: re-exported from the adapter's own closed list.
|
|
65
|
+
* pi-cli: CLOSED-EMPTY by design — pi model ids are `provider/id` patterns
|
|
66
|
+
* whose provider half is per-agent config; there is no universal closed
|
|
67
|
+
* enumeration, so the escalation policy refuses ALL pi ids (strict no-op,
|
|
68
|
+
* §5.6) until a real enumeration is populated. The `/` also fails the id
|
|
69
|
+
* regex — two independent fail-closed layers.
|
|
70
|
+
*/
|
|
71
|
+
import { KNOWN_GEMINI_MODELS } from '../providers/adapters/gemini-cli/models.js';
|
|
72
|
+
export const KNOWN_CLAUDE_MODEL_IDS = [
|
|
73
|
+
'claude-fable-5',
|
|
74
|
+
'claude-opus-4-8',
|
|
75
|
+
'claude-opus-4-7',
|
|
76
|
+
'claude-opus-4-6',
|
|
77
|
+
'claude-sonnet-4-6',
|
|
78
|
+
'claude-haiku-4-5',
|
|
79
|
+
'claude-haiku-4-5-20251001',
|
|
80
|
+
'opus',
|
|
81
|
+
'sonnet',
|
|
82
|
+
'haiku',
|
|
83
|
+
];
|
|
84
|
+
export const KNOWN_CODEX_MODEL_IDS = [
|
|
85
|
+
'gpt-5.2',
|
|
86
|
+
'gpt-5.3-codex',
|
|
87
|
+
'gpt-5.4',
|
|
88
|
+
'gpt-5.4-mini',
|
|
89
|
+
'gpt-5.5',
|
|
90
|
+
];
|
|
91
|
+
export const KNOWN_MODEL_IDS = {
|
|
92
|
+
'claude-code': KNOWN_CLAUDE_MODEL_IDS,
|
|
93
|
+
'codex-cli': KNOWN_CODEX_MODEL_IDS,
|
|
94
|
+
'gemini-cli': KNOWN_GEMINI_MODELS,
|
|
95
|
+
'pi-cli': [],
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* §5.6 — per-adapter swap capability. `claude-code` declares 'mid-session'
|
|
99
|
+
* but the §5.3 contract still verifies EVERY swap with the independent-oracle
|
|
100
|
+
* canary at runtime; the pre-enable proof is the gated live E2E
|
|
101
|
+
* (tests/e2e/model-swap-live-canary.test.ts). The other three are
|
|
102
|
+
* launch-time-only today (and strict no-ops until an escalated model is
|
|
103
|
+
* populated), so "documented, not silently dropped" is enforced by code.
|
|
104
|
+
*/
|
|
105
|
+
export const SWAP_CAPABILITY = {
|
|
106
|
+
'claude-code': 'mid-session',
|
|
107
|
+
'codex-cli': 'launch-time-only',
|
|
108
|
+
'gemini-cli': 'launch-time-only',
|
|
109
|
+
'pi-cli': 'launch-time-only',
|
|
110
|
+
};
|
|
111
|
+
/** §5.1 injection-safe id shape. Anything else is audit+reject. */
|
|
112
|
+
export const MODEL_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
113
|
+
/** Escape + truncate an untrusted id for audit records (spec Sec-F7). */
|
|
114
|
+
export function escapeIdForAudit(id) {
|
|
115
|
+
const escaped = JSON.stringify(id).slice(1, -1); // escape control chars/quotes
|
|
116
|
+
return escaped.length > 80 ? `${escaped.slice(0, 80)}…(truncated)` : escaped;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* §5.1 resolver — maps (framework, tier) → concrete model id | null,
|
|
120
|
+
* reading ONLY the trusted config. Every rejection fails closed to null
|
|
121
|
+
* (session stays on default) and reports a structured audit event.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveTierModel(framework, tier, config, audit) {
|
|
124
|
+
if (!ESCALATION_FRAMEWORKS.includes(framework)) {
|
|
125
|
+
audit?.({ framework, tier, reason: 'unknown-framework' });
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const fw = framework;
|
|
129
|
+
const frameworks = config?.frameworks ?? {};
|
|
130
|
+
// Own-property lookup only — a polluted Object.prototype key can never
|
|
131
|
+
// smuggle an entry in (§5.1 "Object.create(null) / hasOwnProperty lookup").
|
|
132
|
+
const entry = Object.prototype.hasOwnProperty.call(frameworks, fw)
|
|
133
|
+
? frameworks[fw]
|
|
134
|
+
: undefined;
|
|
135
|
+
if (!entry) {
|
|
136
|
+
audit?.({ framework, tier, reason: 'no-framework-entry' });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const id = tier === 'escalated' ? (entry.escalated ?? entry.default) : entry.default;
|
|
140
|
+
if (id == null) {
|
|
141
|
+
// Not an anomaly: `default: null` means "use the account default"
|
|
142
|
+
// (today's behavior) and `escalated: null` means strict no-op.
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (typeof id !== 'string' || !MODEL_ID_RE.test(id)) {
|
|
146
|
+
audit?.({ framework, tier, reason: 'invalid-id-shape', rejectedId: escapeIdForAudit(String(id)) });
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (!KNOWN_MODEL_IDS[fw].includes(id)) {
|
|
150
|
+
audit?.({ framework, tier, reason: 'id-not-in-closed-enum', rejectedId: escapeIdForAudit(id) });
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* True when escalation could ever change behavior for this framework:
|
|
157
|
+
* an escalated id is configured AND resolves through the closed enum.
|
|
158
|
+
* Used by callers to keep the no-escalated-model case a true no-op.
|
|
159
|
+
*/
|
|
160
|
+
export function hasEscalatedModel(framework, config) {
|
|
161
|
+
const escalated = resolveTierModel(framework, 'escalated', config);
|
|
162
|
+
const dflt = resolveTierModel(framework, 'default', config);
|
|
163
|
+
return escalated != null && escalated !== dflt;
|
|
164
|
+
}
|
|
165
|
+
/** The set of configured escalated ids across all frameworks — used by the
|
|
166
|
+
* §11 router-exclusion guard and the §8 ultra-cap monitor. */
|
|
167
|
+
export function escalatedModelIds(config) {
|
|
168
|
+
const ids = new Set();
|
|
169
|
+
for (const fw of ESCALATION_FRAMEWORKS) {
|
|
170
|
+
const id = resolveTierModel(fw, 'escalated', config);
|
|
171
|
+
const dflt = resolveTierModel(fw, 'default', config);
|
|
172
|
+
if (id != null && id !== dflt)
|
|
173
|
+
ids.add(id);
|
|
174
|
+
}
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* §8 free-window semantics: compared as a UTC date, inclusive through the
|
|
179
|
+
* named day. Returns true while the window is open. After expiry the
|
|
180
|
+
* quota/budget guards apply unchanged — the window relaxes nothing
|
|
181
|
+
* structural (informational + drives the dev-agent dogfood window).
|
|
182
|
+
*/
|
|
183
|
+
export function isWithinFreeWindow(modelId, guards, nowMs) {
|
|
184
|
+
const windows = guards.respectFreeWindows ?? {};
|
|
185
|
+
if (!Object.prototype.hasOwnProperty.call(windows, modelId))
|
|
186
|
+
return false;
|
|
187
|
+
const dateStr = windows[modelId];
|
|
188
|
+
if (typeof dateStr !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr))
|
|
189
|
+
return false;
|
|
190
|
+
// Inclusive through the named UTC day: open until the start of the next day.
|
|
191
|
+
const endExclusive = Date.parse(`${dateStr}T00:00:00.000Z`) + 24 * 60 * 60 * 1000;
|
|
192
|
+
return Number.isFinite(endExclusive) && nowMs < endExclusive;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Merge an operator's (possibly partial / absent) config block over the §9
|
|
196
|
+
* defaults. Add-missing-only semantics live in PostUpdateMigrator; this is
|
|
197
|
+
* the READ-side normalizer so every consumer sees a complete shape.
|
|
198
|
+
*/
|
|
199
|
+
export function normalizeTierEscalationConfig(raw) {
|
|
200
|
+
const d = DEFAULT_TIER_ESCALATION_CONFIG;
|
|
201
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
202
|
+
return {
|
|
203
|
+
...d,
|
|
204
|
+
triggers: { ...d.triggers, skills: [...d.triggers.skills] },
|
|
205
|
+
frameworks: { ...d.frameworks },
|
|
206
|
+
costGuards: { ...d.costGuards, respectFreeWindows: { ...d.costGuards.respectFreeWindows } },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const r = raw;
|
|
210
|
+
return {
|
|
211
|
+
enabled: typeof r.enabled === 'boolean' ? r.enabled : d.enabled,
|
|
212
|
+
dryRun: typeof r.dryRun === 'boolean' ? r.dryRun : d.dryRun,
|
|
213
|
+
triggers: {
|
|
214
|
+
skills: Array.isArray(r.triggers?.skills)
|
|
215
|
+
? r.triggers.skills.filter((s) => typeof s === 'string')
|
|
216
|
+
: [...d.triggers.skills],
|
|
217
|
+
projectDesign: typeof r.triggers?.projectDesign === 'boolean' ? r.triggers.projectDesign : d.triggers.projectDesign,
|
|
218
|
+
llmIntentCheck: typeof r.triggers?.llmIntentCheck === 'boolean' ? r.triggers.llmIntentCheck : d.triggers.llmIntentCheck,
|
|
219
|
+
},
|
|
220
|
+
frameworks: { ...d.frameworks, ...(r.frameworks && typeof r.frameworks === 'object' ? r.frameworks : {}) },
|
|
221
|
+
costGuards: {
|
|
222
|
+
...d.costGuards,
|
|
223
|
+
...(r.costGuards && typeof r.costGuards === 'object' ? r.costGuards : {}),
|
|
224
|
+
respectFreeWindows: {
|
|
225
|
+
...(r.costGuards?.respectFreeWindows && typeof r.costGuards.respectFreeWindows === 'object'
|
|
226
|
+
? r.costGuards.respectFreeWindows
|
|
227
|
+
: d.costGuards.respectFreeWindows),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=ModelTierEscalation.js.map
|