@falling-ts/dsh-force-compact 0.2.0
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/LICENSE +21 -0
- package/README.cn.md +170 -0
- package/README.md +479 -0
- package/cordis.patch.yml +13 -0
- package/index.js +550 -0
- package/package.json +54 -0
- package/src/core/crashnet.js +215 -0
- package/src/core/log.js +346 -0
- package/src/core/pairing.js +188 -0
- package/src/core/policy.js +35 -0
- package/src/core/projected.js +139 -0
- package/src/core/settings.js +475 -0
- package/src/core/ui-signal.js +271 -0
- package/src/engine/backend.js +143 -0
- package/src/engine/builtin.js +1336 -0
- package/src/engine/checkpoint.js +206 -0
- package/src/engine/region.js +579 -0
- package/src/engine/summarizer.js +818 -0
- package/src/hooks/command.js +164 -0
- package/src/hooks/guard.js +706 -0
- package/src/hooks/idle.js +136 -0
- package/src/hooks/wire-rewrite.js +151 -0
- package/web/client.js +807 -0
- package/web/swish.css +188 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-pairing balance over a session surface — ported verbatim (plain JS, no
|
|
3
|
+
* type layer) from the official `@deepseek-ai/dsh-compaction` tool-pairing
|
|
4
|
+
* ledger (`deepseek-harness/packages/compaction/compaction/src/tool-pairing.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Compaction changes surface positions, so safe cuts are derived from
|
|
7
|
+
* tool-call/result content in current surface order rather than step markers.
|
|
8
|
+
*
|
|
9
|
+
* Balance definition (official semantics):
|
|
10
|
+
* • an `assistant/message` event contributes +N where N is the number of
|
|
11
|
+
* `tool-call` content blocks in `data.message.content`;
|
|
12
|
+
* • a `tool/result` event contributes -1;
|
|
13
|
+
* • every other event contributes 0.
|
|
14
|
+
* A surface CUT (between two consecutive surface positions, or before the
|
|
15
|
+
* first / after the last) is BALANCED when the running in-progress tool-call
|
|
16
|
+
* count at that cut is 0 — i.e. no unanswered tool call straddles the cut.
|
|
17
|
+
* A surface of N nodes therefore has N+1 cuts; the leading cut (before the
|
|
18
|
+
* first node) is trivially balanced.
|
|
19
|
+
*
|
|
20
|
+
* The balance table is computed incrementally and cached PER SESSION behind a
|
|
21
|
+
* `WeakMap` keyed on the session object (mirrors the official
|
|
22
|
+
* `balanceCacheBySession`). The cache advances only over the NEW suffix of the
|
|
23
|
+
* surface (appends), so repeated reads over a growing surface stay near-O(new
|
|
24
|
+
* nodes). A surface REPLACEMENT (compactation checkpoint landing) bumps the
|
|
25
|
+
* session's `replaceGeneration`, which forces a full rebuild from scratch —
|
|
26
|
+
* exactly the official rebuild path ("the same fold started from the
|
|
27
|
+
* empty-surface state").
|
|
28
|
+
*
|
|
29
|
+
* Corrupt-surface handling matches the official module: a surface sequence
|
|
30
|
+
* whose log slot holds a different event (`eventForSeq` mismatch) or a
|
|
31
|
+
* `tool/result` with no preceding open call THROWS, so a corrupted log can
|
|
32
|
+
* never leave a partially-advanced cache state behind (the tail is validated
|
|
33
|
+
* BEFORE the live cache mutates). Callers in this plugin wrap the public
|
|
34
|
+
* predicates in safe envelopes that degrade to "assume balanced" rather than
|
|
35
|
+
* throwing into the compaction path — see `safe*` variants below.
|
|
36
|
+
*
|
|
37
|
+
* @module @falling-ts/dsh-force-compact/pairing
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Incremental balance state for one session surface generation.
|
|
42
|
+
* @typedef {Object} BalanceCache
|
|
43
|
+
* @property {number} generation Surface rewrite generation this state
|
|
44
|
+
* describes (`session.surface.replaceGeneration`).
|
|
45
|
+
* @property {ReadonlyArray<boolean>} cutBalanced Balance of every surface cut
|
|
46
|
+
* in current order: a surface of N sequences has N + 1 cuts, entry `i`
|
|
47
|
+
* being the cut before sequence `i` and the final entry the cut after the
|
|
48
|
+
* surface tail.
|
|
49
|
+
* @property {Map<number, number>} indexBySeq Current surface position of each
|
|
50
|
+
* event seq, indexing {@link BalanceCache#cutBalanced}.
|
|
51
|
+
* @property {number} inProgressToolCalls In-progress tool-call count after
|
|
52
|
+
* the processed surface tail.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const balanceCacheBySession = new WeakMap()
|
|
56
|
+
|
|
57
|
+
/** How one surface event changes the in-progress tool-call count (official `eventDelta`). */
|
|
58
|
+
export function eventDelta(event) {
|
|
59
|
+
const type = event && event.type
|
|
60
|
+
if (type === 'assistant/message') {
|
|
61
|
+
const message = event.data && event.data.message
|
|
62
|
+
const content = (message && Array.isArray(message.content)) ? message.content : []
|
|
63
|
+
return content.filter(block => block && block.type === 'tool-call').length
|
|
64
|
+
}
|
|
65
|
+
if (type === 'tool/result') return -1
|
|
66
|
+
return 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Read and validate the event named by a surface sequence (official `eventForSeq`). */
|
|
70
|
+
function eventForSeq(events, seq) {
|
|
71
|
+
const event = events[seq]
|
|
72
|
+
if (event === undefined || event.seq !== seq) {
|
|
73
|
+
throw new Error(`tool-pairing balance: surface seq ${seq} has no matching session event (corrupt surface)`)
|
|
74
|
+
}
|
|
75
|
+
return event
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Fold surface sequences not yet in the cache into its balance state (official `extendCache`). */
|
|
79
|
+
function extendCache(session, cache, seqs) {
|
|
80
|
+
const processed = cache.cutBalanced.length - 1
|
|
81
|
+
const tail = seqs.slice(processed)
|
|
82
|
+
// Validate the unseen tail BEFORE mutating the live cache, so a corrupt
|
|
83
|
+
// append cannot leave a partially advanced state behind.
|
|
84
|
+
const events = session.events
|
|
85
|
+
const pendingCuts = []
|
|
86
|
+
let inProgressToolCalls = cache.inProgressToolCalls
|
|
87
|
+
for (const seq of tail) {
|
|
88
|
+
inProgressToolCalls += eventDelta(eventForSeq(events, seq))
|
|
89
|
+
if (inProgressToolCalls < 0) {
|
|
90
|
+
throw new Error(`tool-pairing balance: tool/result at surface seq ${seq} has no matching tool-call (corrupt surface)`)
|
|
91
|
+
}
|
|
92
|
+
pendingCuts.push(inProgressToolCalls === 0)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
tail.forEach((seq, offset) => cache.indexBySeq.set(seq, processed + offset))
|
|
96
|
+
cache.cutBalanced = cache.cutBalanced.concat(pendingCuts)
|
|
97
|
+
cache.inProgressToolCalls = inProgressToolCalls
|
|
98
|
+
return cache
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Balance state synchronized with the current session surface (official `balanceCache`). */
|
|
102
|
+
function balanceCache(session) {
|
|
103
|
+
const surface = session.surface
|
|
104
|
+
const seqs = surface.nodes
|
|
105
|
+
const generation = (surface.replaceGeneration === undefined) ? 0 : surface.replaceGeneration
|
|
106
|
+
const cached = balanceCacheBySession.get(session)
|
|
107
|
+
|
|
108
|
+
if (cached === undefined || cached.generation !== generation || cached.cutBalanced.length - 1 > seqs.length) {
|
|
109
|
+
// A rebuild is the same fold started from the empty-surface state, whose
|
|
110
|
+
// single leading cut is trivially balanced.
|
|
111
|
+
const rebuilt = extendCache(session, {
|
|
112
|
+
generation,
|
|
113
|
+
cutBalanced: [true],
|
|
114
|
+
indexBySeq: new Map(),
|
|
115
|
+
inProgressToolCalls: 0,
|
|
116
|
+
}, seqs)
|
|
117
|
+
balanceCacheBySession.set(session, rebuilt)
|
|
118
|
+
return rebuilt
|
|
119
|
+
}
|
|
120
|
+
if (cached.cutBalanced.length - 1 < seqs.length) return extendCache(session, cached, seqs)
|
|
121
|
+
return cached
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Balance of the cut at a sequence's position plus offset; rejects foreign seqs (official `cutBalance`). */
|
|
125
|
+
function cutBalance(cache, seq, offset) {
|
|
126
|
+
const index = cache.indexBySeq.get(seq)
|
|
127
|
+
const balanced = index === undefined ? undefined : cache.cutBalanced[index + offset]
|
|
128
|
+
if (balanced === undefined) {
|
|
129
|
+
throw new Error(`tool-pairing balance: surface seq ${seq} not found`)
|
|
130
|
+
}
|
|
131
|
+
return balanced
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether the cut immediately BEFORE a current surface sequence is tool-pairing
|
|
136
|
+
* balanced (official `toolPairingBalancedBefore`). THROWS on a corrupt surface
|
|
137
|
+
* or a seq absent from the current surface — use {@link toolPairingBalancedBeforeSafe}
|
|
138
|
+
* on plugin hot paths.
|
|
139
|
+
* @param {import('@deepseek-ai/dsh-session').Session} session
|
|
140
|
+
* @param {number} seq
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
export function toolPairingBalancedBefore(session, seq) {
|
|
144
|
+
return cutBalance(balanceCache(session), seq, 0)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether the cut immediately AFTER a current surface sequence is tool-pairing
|
|
149
|
+
* balanced (official `toolPairingBalancedAfter`). THROWING variant — see the
|
|
150
|
+
* `Before` twin.
|
|
151
|
+
* @param {import('@deepseek-ai/dsh-session').Session} session
|
|
152
|
+
* @param {number} seq
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
export function toolPairingBalancedAfter(session, seq) {
|
|
156
|
+
return cutBalance(balanceCache(session), seq, 1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Safe variant for plugin hot paths: identical math, but a corrupt-surface
|
|
161
|
+
* throw DETERMINES "this cut is balanced" (returns `true`) so a damaged log
|
|
162
|
+
* degrades to "attempt the compaction" (where the session core's own replace
|
|
163
|
+
* validation is the last line of defense) instead of wedging the whole
|
|
164
|
+
* compaction path in a perpetual selection-failure loop.
|
|
165
|
+
* @param {import('@deepseek-ai/dsh-session').Session} session
|
|
166
|
+
* @param {number} seq
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
export function toolPairingBalancedBeforeSafe(session, seq) {
|
|
170
|
+
try {
|
|
171
|
+
return toolPairingBalancedBefore(session, seq)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const message = (error instanceof Error) ? error.message : String(error)
|
|
174
|
+
try { console.warn(`[force-compact] pairing-ledger degraded (assuming balanced before seq ${seq}): ${message}`) } catch { /* never throw out */ }
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Safe trailing-cut twin of {@link toolPairingBalancedBeforeSafe}. */
|
|
180
|
+
export function toolPairingBalancedAfterSafe(session, seq) {
|
|
181
|
+
try {
|
|
182
|
+
return toolPairingBalancedAfter(session, seq)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const message = (error instanceof Error) ? error.message : String(error)
|
|
185
|
+
try { console.warn(`[force-compact] pairing-ledger degraded (assuming balanced after seq ${seq}): ${message}`) } catch { /* never throw out */ }
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-force-compact tunables.
|
|
3
|
+
*
|
|
4
|
+
* These are the plugin's own compaction policy knobs. They are deliberately
|
|
5
|
+
* fixed constants (not cordis `Config` fields): a standalone Host listener has
|
|
6
|
+
* no `Config` schema, and a deployment that wants a different policy points its
|
|
7
|
+
* composition at the `compaction` service's own `retainRatio` / `summarizationModel`
|
|
8
|
+
* instead — see AGENTS.md for why.
|
|
9
|
+
* @module @falling-ts/dsh-force-compact/config
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Minimum surface nodes before any compaction is considered. */
|
|
13
|
+
export const MIN_NODES = 6
|
|
14
|
+
|
|
15
|
+
/** Fraction of the recent tail (by surface-node count) retained verbatim and never compacted. */
|
|
16
|
+
export const RETAIN_RATIO = 0.25
|
|
17
|
+
|
|
18
|
+
/** Compact only when the compactable prefix (all but the retained tail) spans at least this many surface nodes. */
|
|
19
|
+
export const MIN_COMPACTABLE_NODES = 4
|
|
20
|
+
|
|
21
|
+
/** Generation cap for the summarization call. */
|
|
22
|
+
export const MAX_SUMMARY_TOKENS = 16384
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the full config object for a compaction round.
|
|
26
|
+
* @returns {Readonly<{minNodes: number, retainRatio: number, minCompactableNodes: number, maxSummaryTokens: number}>}
|
|
27
|
+
*/
|
|
28
|
+
export function resolveConfig() {
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
minNodes: MIN_NODES,
|
|
31
|
+
retainRatio: RETAIN_RATIO,
|
|
32
|
+
minCompactableNodes: MIN_COMPACTABLE_NODES,
|
|
33
|
+
maxSummaryTokens: MAX_SUMMARY_TOKENS,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-force-compact shared reader for the official `projectedTokens` reading.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE
|
|
5
|
+
* ---------------
|
|
6
|
+
* Every threshold gate, region-selection budget basis, and diagnostic line in
|
|
7
|
+
* the plugin must key off the SAME number the harness renders in the bottom-right
|
|
8
|
+
* corner (`上下文已用 xx% / ~xxK / xxK`). That rendered figure IS the
|
|
9
|
+
* `projectedTokens` member of the host `contextPressure` projection — the
|
|
10
|
+
* provider-anchored sample plus the surface movement since the sample. Reading
|
|
11
|
+
* it through the official registry keeps a single definition of the pressure
|
|
12
|
+
* basis alive inside the plugin instead of a parallel heuristic drifting from
|
|
13
|
+
* upstream revisions.
|
|
14
|
+
*
|
|
15
|
+
* WHERE IT COMES FROM
|
|
16
|
+
* --------------------
|
|
17
|
+
* `ctx.get('sessionProjections')` is the host registry that DRIVES every
|
|
18
|
+
* registered projection unit forward eagerly over committed session events
|
|
19
|
+
* (framework-owned watermark cache). `snapshot(session).values.contextPressure`
|
|
20
|
+
* hands back the SAME wire object the host broadcasts to the browser as
|
|
21
|
+
* `session/projection` frames — already validated against the unit's own
|
|
22
|
+
* `viewSchema`, so the plugin never re-implements the anchor math. Fields are
|
|
23
|
+
* individually optional (`pressureTokens` / `projectedTokens` /
|
|
24
|
+
* `contextWindow`): absent until a provider reports usage.
|
|
25
|
+
*
|
|
26
|
+
* SEMANTICS (mirrors the official JSDoc — read before building gates on this)
|
|
27
|
+
* ----------------------------------------------------------------------------
|
|
28
|
+
* The three members are INDEPENDENT last-wins records, not one atomic request
|
|
29
|
+
* observation: switching models can pair a fresh capacity with the previous
|
|
30
|
+
* route's pressure until the next request reports usage. Official stance —
|
|
31
|
+
* this is a USER-FACING REFERENCE, not a billing or gating input. The plugin
|
|
32
|
+
* therefore treats `projectedTokens` as the authoritative pressure basis but
|
|
33
|
+
* FAILS OPEN (returns `undefined`) when it is absent, so a session that has
|
|
34
|
+
* not reported usage yet still degrades gracefully to the char-based
|
|
35
|
+
* estimator at each call site instead of blocking.
|
|
36
|
+
*
|
|
37
|
+
* CALIBER NOTES FOR CALLERS
|
|
38
|
+
* -------------------------
|
|
39
|
+
* `projectedTokens` = `max(0, pressureTokens + surfaceTokens −
|
|
40
|
+
* sampledSurfaceTokens)`. The delta term is estimated at the meter's fixed
|
|
41
|
+
* density (CHARS_PER_TOKEN=4), so the figure UNDERCOUNTS heavy-CJK / tool-JSON
|
|
42
|
+
* content relative to the pure `surfaceTokens` sum — a deliberate provider
|
|
43
|
+
* anchor the meter prefers over the unanchored sum. Gate thresholds calibrated
|
|
44
|
+
* against the old `surfaceTokens` basis sit slightly higher (fewer triggers)
|
|
45
|
+
* after this swap; that is intended behaviour, not a bug.
|
|
46
|
+
*
|
|
47
|
+
* COST CONTRACT
|
|
48
|
+
* -------------
|
|
49
|
+
* Sync, pure read. First touch of a freshly-restarted long session replays the
|
|
50
|
+
* whole log once (lazy fold seeding) — O(events); every later call in the same
|
|
51
|
+
* process hits the eager-fold watermark cache — effectively O(1). Call sites
|
|
52
|
+
* are the pre-step gate (once per model step) and the retained-tail path (rare
|
|
53
|
+
* compaction rounds), so the hot-path cost is negligible.
|
|
54
|
+
*
|
|
55
|
+
* FAILURE MODES — ALL RESOLVED TO `undefined` (never throw)
|
|
56
|
+
* ---------------------------------------------------------
|
|
57
|
+
* • `ctx` / `ctx.get` absent or not a function → `undefined`
|
|
58
|
+
* • `sessionProjections` registry not mounted (trimmed compositions) → `undefined`
|
|
59
|
+
* • `snapshot` throws on a transient backend fault → caught, `undefined`
|
|
60
|
+
* • `session` unusable → `undefined`
|
|
61
|
+
* • `contextPressure` unit not folded for this session → `undefined`
|
|
62
|
+
* • unit present but `projectedTokens` still absent (no usage sample yet) → `undefined`
|
|
63
|
+
*
|
|
64
|
+
* Callers pair a `=== undefined` result with their existing char-estimator
|
|
65
|
+
* fallback (see `estimateSessionTokens` / `estimateSurfaceTokensLocal`), so a
|
|
66
|
+
* degraded registry never blocks a model request or a compaction commit.
|
|
67
|
+
*
|
|
68
|
+
* @module @falling-ts/dsh-force-compact/projected
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read the official `projectedTokens` for one session — the SAME number the
|
|
73
|
+
* harness renders in the bottom-right corner.
|
|
74
|
+
*
|
|
75
|
+
* Fail-open by design (see module doc): every absence/degradation resolves to
|
|
76
|
+
* `undefined` rather than propagating. Pair the result with a char-based
|
|
77
|
+
* estimator fallback at each call site.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} ctx cordis context (Host listener `this` / apply ctx).
|
|
80
|
+
* @param {object|undefined} session live session handle.
|
|
81
|
+
* @returns {number|undefined} the official `projectedTokens` reading, or
|
|
82
|
+
* `undefined` when the registry is unavailable, the snapshot failed, the
|
|
83
|
+
* unit is not folded for this session, or no usage sample has been captured
|
|
84
|
+
* yet (all fail-open — caller decides the fallback).
|
|
85
|
+
*/
|
|
86
|
+
export function getProjectedTokens(ctx, session) {
|
|
87
|
+
try {
|
|
88
|
+
if (typeof ctx?.get !== 'function') return undefined
|
|
89
|
+
const registry = ctx.get('sessionProjections')
|
|
90
|
+
if (registry === undefined || registry === null) return undefined
|
|
91
|
+
if (typeof registry.snapshot !== 'function') return undefined
|
|
92
|
+
if (session === undefined || session === null) return undefined
|
|
93
|
+
const snap = registry.snapshot(session)
|
|
94
|
+
const cp = snap && snap.values && snap.values.contextPressure
|
|
95
|
+
if (cp === undefined || cp === null) return undefined
|
|
96
|
+
const projected = cp.projectedTokens
|
|
97
|
+
return (typeof projected === 'number' && Number.isFinite(projected)) ? projected : undefined
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Classify WHY {@link getProjectedTokens} resolved to `undefined` for one
|
|
105
|
+
* session — a DIAGNOSTIC aid that reproduces the same read but labels the exact
|
|
106
|
+
* failure tier, so an operator can tell apart "registry/service not reachable
|
|
107
|
+
* from this context" from "reachable but the session simply has no usage sample
|
|
108
|
+
* yet". Pure, sync, never throws, returns a short stable reason string (or
|
|
109
|
+
* `'available'` with the numeric reading attached when the read succeeds).
|
|
110
|
+
*
|
|
111
|
+
* @param {object} ctx cordis context.
|
|
112
|
+
* @param {object|undefined} session live session handle.
|
|
113
|
+
* @returns {string} one of: `available:<n>` / `no-ctx.get` / `registry-absent` /
|
|
114
|
+
* `registry-no-snapshot` / `session-unusable` / `snap-threw` /
|
|
115
|
+
* `unit-not-folded` / `no-usage-sample-yet`.
|
|
116
|
+
*/
|
|
117
|
+
export function diagnoseProjectedTokensAbsence(ctx, session) {
|
|
118
|
+
try {
|
|
119
|
+
if (typeof ctx?.get !== 'function') return 'no-ctx.get'
|
|
120
|
+
const registry = ctx.get('sessionProjections')
|
|
121
|
+
if (registry === undefined || registry === null) return 'registry-absent'
|
|
122
|
+
if (typeof registry.snapshot !== 'function') return 'registry-no-snapshot'
|
|
123
|
+
if (session === undefined || session === null) return 'session-unusable'
|
|
124
|
+
let snap
|
|
125
|
+
try {
|
|
126
|
+
snap = registry.snapshot(session)
|
|
127
|
+
} catch {
|
|
128
|
+
return 'snap-threw'
|
|
129
|
+
}
|
|
130
|
+
const cp = snap && snap.values && snap.values.contextPressure
|
|
131
|
+
if (cp === undefined || cp === null) return 'unit-not-folded'
|
|
132
|
+
const projected = cp.projectedTokens
|
|
133
|
+
return (typeof projected === 'number' && Number.isFinite(projected))
|
|
134
|
+
? `available:${projected}`
|
|
135
|
+
: 'no-usage-sample-yet'
|
|
136
|
+
} catch {
|
|
137
|
+
return 'unexpected-throw'
|
|
138
|
+
}
|
|
139
|
+
}
|