pan-wizard 3.28.0 → 3.30.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/README.md +39 -39
- package/bin/install-lib.cjs +65 -58
- package/bin/install.js +199 -188
- package/commands/pan/army.md +2 -2
- package/commands/pan/audit-deployment.md +2 -2
- package/commands/pan/cost.md +19 -7
- package/commands/pan/exec-phase.md +2 -0
- package/commands/pan/focus-auto.md +5 -5
- package/hooks/dist/pan-check-update.js +4 -0
- package/hooks/dist/pan-cost-logger.js +322 -43
- package/hooks/dist/pan-stop-guard.js +81 -2
- package/hooks/dist/pan-trace-logger.js +275 -32
- package/package.json +4 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
- package/pan-wizard-core/bin/lib/commands.cjs +3 -1
- package/pan-wizard-core/bin/lib/constants.cjs +17 -0
- package/pan-wizard-core/bin/lib/context-budget.cjs +10 -0
- package/pan-wizard-core/bin/lib/core.cjs +17 -4
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +511 -0
- package/pan-wizard-core/bin/lib/cost.cjs +165 -55
- package/pan-wizard-core/bin/lib/git.cjs +5 -1
- package/pan-wizard-core/bin/lib/hud.cjs +5 -3
- package/pan-wizard-core/bin/lib/hygiene.cjs +22 -25
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +3 -3
- package/pan-wizard-core/bin/lib/memory.cjs +14 -8
- package/pan-wizard-core/bin/lib/optimize.cjs +78 -2
- package/pan-wizard-core/bin/lib/utils.cjs +22 -0
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +1 -1
- package/pan-wizard-core/bin/lib/verify.cjs +24 -10
- package/pan-wizard-core/bin/pan-tools.cjs +8 -1
- package/pan-wizard-core/references/model-profiles.md +4 -4
- package/pan-wizard-core/references/planning-config.md +19 -23
- package/pan-wizard-core/workflows/health.md +1 -0
- package/pan-wizard-core/workflows/settings.md +2 -4
- package/scripts/coverage-gate.cjs +257 -0
- package/scripts/install-git-hooks.js +5 -0
- package/scripts/mutation-probe.cjs +272 -0
- package/scripts/release-check.js +33 -12
- package/scripts/test-quality-lint.cjs +240 -0
- package/scripts/test-surface.cjs +336 -0
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
* - hit rate: cache_read / (cache_read + input - cache_write) if any cache activity
|
|
31
31
|
*
|
|
32
32
|
* Rate table is approximate — real pricing comes from the provider's API.
|
|
33
|
-
* Rates are US dollars per million tokens,
|
|
34
|
-
*
|
|
33
|
+
* Rates are US dollars per million tokens, verified against each provider's
|
|
34
|
+
* pricing page on the date in RATES_VERIFIED_AT (below). Users can override with
|
|
35
|
+
* `.planning/config.json` → `cost.rates`.
|
|
35
36
|
*/
|
|
36
37
|
|
|
37
38
|
const fs = require('fs');
|
|
@@ -47,63 +48,109 @@ const TOKENS_FILE = 'tokens.jsonl';
|
|
|
47
48
|
* Override per-model in config.json → cost.rates.
|
|
48
49
|
*/
|
|
49
50
|
const DEFAULT_RATES = {
|
|
50
|
-
// Anthropic —
|
|
51
|
-
// $5/$25 (the old $15/$75 Opus pricing ended with
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
51
|
+
// Anthropic — platform.claude.com/docs/en/about-claude/pricing, every row read
|
|
52
|
+
// 2026-09-23. Opus 4.6+/Opus 5 are $5/$25 (the old $15/$75 Opus pricing ended with
|
|
53
|
+
// the 4.5 generation). cache_write is the 5-minute write rate (1.25× input; the
|
|
54
|
+
// 1-hour write bills 2×, which the ledger does not split yet). Cache reads follow
|
|
55
|
+
// the 0.1× convention EXCEPT on three models the page footnotes: Fable 5.1 and
|
|
56
|
+
// Mythos 5.1 at 0.025× input, Opus 5.5 at 0.05×. Each needs its own row, because
|
|
57
|
+
// the family-prefix fallback would otherwise price it at its predecessor's reads.
|
|
58
|
+
// Fable 5.1 is what the `fable`/`best` aliases resolve to (model-config: neither
|
|
59
|
+
// Fable model is any plan's default); cached re-reads are the bulk of PAN's
|
|
60
|
+
// traffic (ADR-0044).
|
|
58
61
|
'claude-fable-5-1': { input: 10.0, output: 50.0, cache_read: 0.25, cache_write: 12.5 },
|
|
59
62
|
'claude-fable-5': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
|
|
60
|
-
// Mythos 5.1 / Mythos 5 (limited availability)
|
|
61
|
-
//
|
|
62
|
-
// the two models whose cache reads bill at 0.025× input; Mythos 5 follows the 0.1× rule.
|
|
63
|
-
// Added for reality check R7: resolveRate returned null for both ids.
|
|
63
|
+
// Mythos 5.1 / Mythos 5 (limited availability): $10/$50; Mythos 5.1 reads at
|
|
64
|
+
// 0.025× input like Fable 5.1, Mythos 5 follows the 0.1× rule (R7).
|
|
64
65
|
'claude-mythos-5-1': { input: 10.0, output: 50.0, cache_read: 0.25, cache_write: 12.5 },
|
|
65
66
|
'claude-mythos-5': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
|
|
67
|
+
// Opus 5.5 (claude-opus-5-5, released 2026-09-22): $4 input / $20 output, 5-minute
|
|
68
|
+
// writes $5, cache hits $0.20 — the page prices its hits at 0.05× input. Claude
|
|
69
|
+
// Code 2.1.280 made it the default model on every plan (model-config, read
|
|
70
|
+
// 2026-09-23), so PAN's inherit tier now runs on it. Without this row the family
|
|
71
|
+
// prefix priced it as Opus 5: cache reads 2.5× high, the rest a quarter high
|
|
72
|
+
// (reality check 2026-09-22, R25). The models overview lists no dated snapshot —
|
|
73
|
+
// from the 4.6 generation on, the dateless id is canonical.
|
|
74
|
+
'claude-opus-5-5': { input: 4.0, output: 20.0, cache_read: 0.20, cache_write: 5.0 },
|
|
66
75
|
'claude-opus-5': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
|
|
67
76
|
'claude-opus-4-8': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
|
|
68
77
|
'claude-opus-4-7': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
|
|
69
78
|
'claude-opus-4-6': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
|
|
70
|
-
// Opus 4.5 (dated id claude-opus-4-5-20251101)
|
|
71
|
-
//
|
|
72
|
-
// and priced as null (R7).
|
|
79
|
+
// Opus 4.5 (dated id claude-opus-4-5-20251101): without this row the dated id had
|
|
80
|
+
// no family prefix to land on and priced as null (R7).
|
|
73
81
|
'claude-opus-4-5': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
|
|
74
82
|
// Sonnet 5 is $2/$10: the launch price announced as introductory through
|
|
75
|
-
// 2026-08-31 was made permanent and the scheduled rise to $3/$15 cancelled
|
|
76
|
-
//
|
|
77
|
-
//
|
|
83
|
+
// 2026-08-31 was made permanent and the scheduled rise to $3/$15 cancelled.
|
|
84
|
+
// Lesson: never write down a pre-announced price — this row carried the future
|
|
85
|
+
// rate for a month and over-billed by half.
|
|
78
86
|
'claude-sonnet-5': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50 },
|
|
79
87
|
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
80
|
-
// Sonnet 4.5 (dated id claude-sonnet-4-5-20250929)
|
|
81
|
-
// $3/$15/$0.30/$3.75 (the pre-Sonnet-5 rate; Sonnet 5 is $2/$10). R7.
|
|
88
|
+
// Sonnet 4.5 (dated id claude-sonnet-4-5-20250929): $3/$15/$0.30/$3.75 (R7).
|
|
82
89
|
'claude-sonnet-4-5': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
90
|
+
// Haiku 4.5: API id claude-haiku-4-5-20251001, alias claude-haiku-4-5.
|
|
83
91
|
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
|
|
84
92
|
|
|
85
|
-
// OpenAI —
|
|
86
|
-
// input
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
// OpenAI — developers.openai.com/api/docs/pricing, Standard tier, short context
|
|
94
|
+
// (≤272K input tokens), read 2026-09-23. On the GPT-5.6 and GPT-6 families a cache
|
|
95
|
+
// write bills at 1.25× input IN PLACE of the input rate ("writes are not an
|
|
96
|
+
// additive fee"); GPT-5.5 charges no cache write ("-"), so its write tokens bill
|
|
97
|
+
// as plain input. Until 2026-09-22 this block said "no separate write charge" for
|
|
98
|
+
// every row, which had stopped being true — Terra and Luna writes were a fifth
|
|
99
|
+
// low (R27). Longest-prefix match lets each tier key win over a shorter one.
|
|
100
|
+
// GPT-6: Astra is the flagship; Sol and Luna were released 2026-09-22. Codex's
|
|
101
|
+
// config docs use gpt-6-sol as the default model and gpt-6-luna for lower-cost
|
|
102
|
+
// subagents (learn.chatgpt.com/docs/config-file/config-basic, /agent-configuration/subagents).
|
|
103
|
+
'gpt-6-astra': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
|
|
104
|
+
'gpt-6-sol': { input: 2.0, output: 10.0, cache_read: 0.2, cache_write: 2.5 },
|
|
105
|
+
'gpt-6-luna': { input: 0.10, output: 0.50, cache_read: 0.01, cache_write: 0.125 },
|
|
106
|
+
// GPT-5.6. The bare `gpt-5.6` id is an alias that "routes requests to GPT-5.6 Sol"
|
|
107
|
+
// (its model page, read 2026-09-23), so it carries Sol's row — not the $5/$30 it
|
|
108
|
+
// carried before, which no page listed. Sol's price is promotional "at least
|
|
109
|
+
// through November 21, 2026": re-read it after that date.
|
|
110
|
+
'gpt-5.6': { input: 4.0, output: 20.0, cache_read: 0.4, cache_write: 5.0 },
|
|
111
|
+
'gpt-5.6-sol': { input: 4.0, output: 20.0, cache_read: 0.4, cache_write: 5.0 },
|
|
112
|
+
'gpt-5.6-terra': { input: 2.0, output: 12.0, cache_read: 0.2, cache_write: 2.5 },
|
|
113
|
+
'gpt-5.6-luna': { input: 0.20, output: 1.20, cache_read: 0.02, cache_write: 0.25 },
|
|
114
|
+
// Priced only in the page's separate "Cyber models" table, not in the Standard tiers.
|
|
115
|
+
'gpt-5.6-cyber': { input: 12.5, output: 75.0, cache_read: 1.25, cache_write: 15.625 },
|
|
93
116
|
'gpt-5.5': { input: 5.0, output: 30.0, cache_read: 0.5, cache_write: 5.0 },
|
|
94
117
|
|
|
95
|
-
// Google Gemini —
|
|
96
|
-
//
|
|
118
|
+
// Google Gemini — ai.google.dev/gemini-api/docs/pricing, paid tier, text input,
|
|
119
|
+
// the ≤200K-token prompt tier where the page splits by length, read 2026-09-23.
|
|
120
|
+
// cache_read is the page's "context caching" price: 0.1× input on every row, not
|
|
121
|
+
// the 0.25× this block carried until 2026-09-22 (cache reads 2.5× high, R26). The
|
|
122
|
+
// tokens that CREATE a cache bill at the standard input price (Google Cloud's
|
|
123
|
+
// context-cache overview; the Gemini API pages list only cached-token and storage
|
|
124
|
+
// charges), so cache_write = input. Cache STORAGE bills per million tokens per
|
|
125
|
+
// HOUR, which a per-token table cannot carry: PAN's Gemini totals exclude it.
|
|
126
|
+
// The 3.6/3.7/3.8 Flash prices run "through December 31, 2026" and double from
|
|
127
|
+
// January 1, 2027 (the page states both) — re-read them after that date.
|
|
128
|
+
'gemini-3.8-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
129
|
+
'gemini-3.7-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
130
|
+
'gemini-3.6-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
131
|
+
'gemini-3.5-flash': { input: 1.50, output: 9.00, cache_read: 0.15, cache_write: 1.50 },
|
|
132
|
+
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50, cache_read: 0.03, cache_write: 0.30 },
|
|
133
|
+
// Shutdown announced for 2027-05-07 (deprecations page); replacement gemini-3.5-flash-lite.
|
|
134
|
+
'gemini-3.1-flash-lite': { input: 0.25, output: 1.50, cache_read: 0.025, cache_write: 0.25 },
|
|
135
|
+
'gemini-3-flash-preview': { input: 0.50, output: 3.00, cache_read: 0.05, cache_write: 0.50 },
|
|
136
|
+
// The page lists 3.1 Pro only as gemini-3.1-pro-preview; the bare id keeps the
|
|
137
|
+
// preview's row so records written under it stay priced.
|
|
138
|
+
'gemini-3.1-pro': { input: 2.00, output: 12.0, cache_read: 0.20, cache_write: 2.00 },
|
|
139
|
+
'gemini-3.1-pro-preview': { input: 2.00, output: 12.0, cache_read: 0.20, cache_write: 2.00 },
|
|
140
|
+
// The 2.5 family is "not deprecated", but the models page limits access to users
|
|
141
|
+
// who have used it before — new projects cannot count on it (PAN's Google routing
|
|
142
|
+
// tiers moved off it, R28). Rows kept for the ledgers that recorded it.
|
|
143
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.0, cache_read: 0.125, cache_write: 1.25 },
|
|
144
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50, cache_read: 0.03, cache_write: 0.30 },
|
|
145
|
+
'gemini-2.5-flash-lite': { input: 0.10, output: 0.40, cache_read: 0.01, cache_write: 0.10 },
|
|
97
146
|
// (gemini-1.5-pro removed 2026-06: retired model; records for it fall back to tier rates.)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'reasoning': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
|
|
106
|
-
'mid': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
147
|
+
|
|
148
|
+
// Tier fallbacks when the model id is unknown: reasoning tracks the default Opus
|
|
149
|
+
// (Opus 5.5 since 2026-09-22 — the model the inherit tier runs on for most users),
|
|
150
|
+
// mid the Sonnet the `sonnet` alias resolves to on the Anthropic API (Sonnet 5),
|
|
151
|
+
// fast Haiku 4.5.
|
|
152
|
+
'reasoning': { input: 4.0, output: 20.0, cache_read: 0.20, cache_write: 5.0 },
|
|
153
|
+
'mid': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50 },
|
|
107
154
|
'fast': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
|
|
108
155
|
};
|
|
109
156
|
|
|
@@ -330,23 +377,67 @@ function readRecords(cwd) {
|
|
|
330
377
|
*/
|
|
331
378
|
/**
|
|
332
379
|
* A record is "suspect" when its token counts are physically implausible for a
|
|
333
|
-
* single subagent — the signature
|
|
334
|
-
* (billions of cache-read, cache-read dwarfing input, 100%
|
|
335
|
-
*
|
|
336
|
-
*
|
|
380
|
+
* single subagent — the oversum signature: a session's cumulative usage booked
|
|
381
|
+
* to one subagent row (billions of cache-read, cache-read dwarfing input, 100%
|
|
382
|
+
* cache-hit). Written by pre-v3.12.4 hooks, and by the parent-transcript slice
|
|
383
|
+
* path of every later hook up to v3.28 whenever a slice started at cursor 0 on a
|
|
384
|
+
* long-lived session. Such records are quarantined from aggregates so a
|
|
385
|
+
* poisoned ledger can't report millions of dollars.
|
|
386
|
+
* See docs/FIELD-REPORT-army-2026-06.md.
|
|
337
387
|
* @param {Object} r - a cost record
|
|
338
388
|
* @returns {boolean}
|
|
339
389
|
*/
|
|
390
|
+
// No single subagent runs for six hours — the longest native-workflow phase runs
|
|
391
|
+
// measured in the harness finish inside an hour — while a parent-transcript slice
|
|
392
|
+
// that spans a working day, or the idle night between two stops, does. Mirrored by
|
|
393
|
+
// the hooks' SLICE_MAX_DURATION_MS, which nulls the span on write. Calibrated on
|
|
394
|
+
// eleven field ledgers (2026-09): of the timed rows the old ratio rule flagged,
|
|
395
|
+
// 55 spanned under three hours and 12 spanned six to twenty-four — the latter all
|
|
396
|
+
// parent slices booked to a `general-purpose` or workflow subagent.
|
|
397
|
+
const SUSPECT_MAX_DURATION_MS = 6 * 60 * 60 * 1000;
|
|
398
|
+
|
|
340
399
|
function isSuspectRecord(r) {
|
|
341
400
|
if (!r || typeof r !== 'object') return false;
|
|
401
|
+
// Rows measured from a transcript that belongs to exactly one actor — the
|
|
402
|
+
// subagent's own file (v3.29 hooks, `cost rebuild`) or the main thread's
|
|
403
|
+
// session file (`cost rebuild`) — cannot carry another actor's usage, so the
|
|
404
|
+
// oversum signature does not apply to them: a 24-day main thread with
|
|
405
|
+
// billions of cached reads is simply a long session, measured exactly.
|
|
406
|
+
if (r.token_source === 'agent-transcript' || r.token_source === 'session-transcript') return false;
|
|
342
407
|
const cr = r.cache_read_tokens || 0;
|
|
343
408
|
const io = (r.input_tokens || 0) + (r.output_tokens || 0);
|
|
344
409
|
if (cr > 5e8) return true; // no scoped subagent re-reads >500M cached tokens
|
|
345
|
-
if (cr > 1e7 && cr > 100 * (io + 1)) return true; // cache-read dwarfs input+output
|
|
346
410
|
if ((r.output_tokens || 0) > 1e7) return true; // ~10M output = cumulative oversum
|
|
411
|
+
const dur = typeof r.duration_ms === 'number' ? r.duration_ms : null;
|
|
412
|
+
if (dur != null && dur > SUSPECT_MAX_DURATION_MS) return true; // a six-hour-plus "subagent" is a session's history
|
|
413
|
+
// Cache-read dwarfing input+output is the oversum signature ONLY for a row the
|
|
414
|
+
// hook could not time (pre-v3.20 rows, unreadable transcripts). A timed row
|
|
415
|
+
// with a plausible span is a real agent: under prompt caching every turn
|
|
416
|
+
// re-reads the cached context, so a hundred-turn agent legitimately reads
|
|
417
|
+
// 300× more cached tokens than it writes. Applied to timed rows, this rule
|
|
418
|
+
// had excluded fifty sub-hour agents (~3 billion real cache-read tokens)
|
|
419
|
+
// from eleven field ledgers (2026-09).
|
|
420
|
+
if (dur == null && cr > 1e7 && cr > 100 * (io + 1)) return true;
|
|
347
421
|
return false;
|
|
348
422
|
}
|
|
349
423
|
|
|
424
|
+
/**
|
|
425
|
+
* A record is "empty" when it carries no tokens on any axis and no model: a
|
|
426
|
+
* spawn the hook could not measure — a sibling stop that arrived before the
|
|
427
|
+
* shared parent transcript had grown (the pre-v3.29 slice path), or a payload
|
|
428
|
+
* with neither usage nor a readable transcript. It has no cost and no tokens;
|
|
429
|
+
* counting it as a call inflated call counts by up to 2x in the field (480 of
|
|
430
|
+
* 976 rows across eleven ledgers, 2026-09). A zero-token row that names a model
|
|
431
|
+
* is NOT empty — that is a measured run that happened to use nothing.
|
|
432
|
+
* @param {Object} r - a cost record
|
|
433
|
+
* @returns {boolean}
|
|
434
|
+
*/
|
|
435
|
+
function isEmptyRecord(r) {
|
|
436
|
+
if (!r || typeof r !== 'object') return false;
|
|
437
|
+
if (r.model) return false;
|
|
438
|
+
return !(r.input_tokens || r.output_tokens || r.cache_read_tokens || r.cache_write_tokens);
|
|
439
|
+
}
|
|
440
|
+
|
|
350
441
|
function aggregate(cwd, opts) {
|
|
351
442
|
const records = readRecords(cwd);
|
|
352
443
|
const malformedSkipped = _lastReadMalformed; // captured before any later read
|
|
@@ -372,6 +463,7 @@ function aggregate(cwd, opts) {
|
|
|
372
463
|
cost_usd: 0,
|
|
373
464
|
cost_unknown: 0,
|
|
374
465
|
suspect_excluded: 0,
|
|
466
|
+
empty_excluded: 0,
|
|
375
467
|
malformed_skipped: malformedSkipped,
|
|
376
468
|
};
|
|
377
469
|
|
|
@@ -393,9 +485,11 @@ function aggregate(cwd, opts) {
|
|
|
393
485
|
}
|
|
394
486
|
|
|
395
487
|
for (const r of filtered) {
|
|
396
|
-
// Quarantine physically-impossible records (
|
|
397
|
-
//
|
|
488
|
+
// Quarantine physically-impossible records (the transcript-oversum
|
|
489
|
+
// signature) so a poisoned ledger doesn't poison the totals / HUD / /pan:cost.
|
|
398
490
|
if (isSuspectRecord(r)) { totals.suspect_excluded += 1; continue; }
|
|
491
|
+
// Skip unmeasured spawns: no tokens, no model, nothing to price or count.
|
|
492
|
+
if (isEmptyRecord(r)) { totals.empty_excluded += 1; continue; }
|
|
399
493
|
totals.calls += 1;
|
|
400
494
|
totals.input_tokens += r.input_tokens || 0;
|
|
401
495
|
totals.output_tokens += r.output_tokens || 0;
|
|
@@ -452,7 +546,12 @@ function renderTable(agg) {
|
|
|
452
546
|
lines.push(window);
|
|
453
547
|
lines.push('');
|
|
454
548
|
lines.push('Totals');
|
|
455
|
-
|
|
549
|
+
const skipped = [
|
|
550
|
+
agg.totals.suspect_excluded > 0 ? `${agg.totals.suspect_excluded} suspect` : null,
|
|
551
|
+
agg.totals.empty_excluded > 0 ? `${agg.totals.empty_excluded} empty` : null,
|
|
552
|
+
agg.totals.malformed_skipped > 0 ? `${agg.totals.malformed_skipped} malformed` : null,
|
|
553
|
+
].filter(Boolean);
|
|
554
|
+
lines.push(` Calls : ${agg.totals.calls}${skipped.length ? ` (excluded: ${skipped.join(', ')})` : ''}`);
|
|
456
555
|
lines.push(` Input tokens : ${agg.totals.input_tokens.toLocaleString()}`);
|
|
457
556
|
lines.push(` Output tokens : ${agg.totals.output_tokens.toLocaleString()}`);
|
|
458
557
|
lines.push(` Cache read : ${agg.totals.cache_read_tokens.toLocaleString()}`);
|
|
@@ -535,12 +634,22 @@ function cmdCostClear(cwd, raw) {
|
|
|
535
634
|
|
|
536
635
|
// ─── Rate-table staleness ───────────────────────────────────────────────────
|
|
537
636
|
|
|
538
|
-
// Date DEFAULT_RATES was last verified against published provider pricing
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
637
|
+
// Date DEFAULT_RATES was last verified against published provider pricing — ALL
|
|
638
|
+
// providers' pricing pages, not only Anthropic's (the 2026-09-10 bump covered the
|
|
639
|
+
// Anthropic rows alone, and the Gemini and OpenAI rows it vouched for had never
|
|
640
|
+
// matched their pages: reality check 2026-09-22, RC29). Bump it only when every
|
|
641
|
+
// row has been read against its page that day.
|
|
642
|
+
//
|
|
643
|
+
// `models check` flags the table once it is older than RATES_STALE_AFTER_DAYS.
|
|
644
|
+
// That window was half a year until 2026-09-22 (R38), which is longer than the
|
|
645
|
+
// interval at which the lineup now moves: the default Claude model changed on
|
|
646
|
+
// every plan inside twelve days of the last verification. Sixty days is the
|
|
647
|
+
// review cadence the ecosystem reviews settled on. The calendar still cannot see
|
|
648
|
+
// a default-model change inside the window — that is the job of the
|
|
649
|
+
// documented-default-ids fixture test (tests/fixtures/documented-default-models.json,
|
|
650
|
+
// R31), which fails the suite the day a documented default id has no exact row.
|
|
651
|
+
const RATES_VERIFIED_AT = '2026-09-23';
|
|
652
|
+
const RATES_STALE_AFTER_DAYS = 60;
|
|
544
653
|
const RATE_TIERS = ['reasoning', 'mid', 'fast'];
|
|
545
654
|
|
|
546
655
|
function checkRatesStaleness(now = new Date()) {
|
|
@@ -572,6 +681,7 @@ module.exports = {
|
|
|
572
681
|
readRecords,
|
|
573
682
|
aggregate,
|
|
574
683
|
isSuspectRecord,
|
|
684
|
+
isEmptyRecord,
|
|
575
685
|
renderTable,
|
|
576
686
|
renderChart,
|
|
577
687
|
resolveRate,
|
|
@@ -82,7 +82,11 @@ function cmdGitCommit(cwd, opts, raw) {
|
|
|
82
82
|
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', finalMessage];
|
|
83
83
|
const r = execGit(cwd, commitArgs);
|
|
84
84
|
if (r.exitCode !== 0) {
|
|
85
|
-
|
|
85
|
+
// Git says "nothing to commit" for a clean tree and "nothing added to commit but
|
|
86
|
+
// untracked files present" when the only changes are untracked. Both mean no change
|
|
87
|
+
// was NEEDED; only the first was recognised, so the second was reported as a failed
|
|
88
|
+
// commit with "unknown git error" (measured 2026-09-17).
|
|
89
|
+
if ((r.stdout + r.stderr).includes('nothing to commit') || (r.stdout + r.stderr).includes('nothing added to commit')) {
|
|
86
90
|
// No error key, exit 0: nothing to commit means no change was NEEDED, not that
|
|
87
91
|
// a change failed. Pinned as a success in CLI-REFERENCE ("Error Shape").
|
|
88
92
|
output({ committed: false, reason: 'nothing_to_commit' }, raw, 'nothing to commit');
|
|
@@ -424,8 +424,10 @@ function fmtTokens(n) {
|
|
|
424
424
|
/**
|
|
425
425
|
* Assess whether a cost ledger is trustworthy enough to show dollar figures.
|
|
426
426
|
* Two failure modes are treated as "don't quote a number":
|
|
427
|
-
* - legacy: more records were quarantined as implausible than
|
|
428
|
-
*
|
|
427
|
+
* - legacy: more records were quarantined as implausible than were measured
|
|
428
|
+
* (the transcript-oversum signature: a session's cumulative usage booked to
|
|
429
|
+
* one subagent, written by hooks before v3.29) — quarantine advised.
|
|
430
|
+
* Unmeasured spawns (`empty_excluded`) count on neither side.
|
|
429
431
|
* - unresolved: every surviving record lacks a resolvable model→rate, so the
|
|
430
432
|
* computed spend is a misleading $0 even though real tokens were spent.
|
|
431
433
|
* Returns { ok:true } when figures are safe to display.
|
|
@@ -439,7 +441,7 @@ function ledgerReliability(totals) {
|
|
|
439
441
|
const total = suspect + calls;
|
|
440
442
|
return {
|
|
441
443
|
ok: false, kind: 'legacy',
|
|
442
|
-
message: `${suspect} of ${total} cost records are implausible (
|
|
444
|
+
message: `${suspect} of ${total} measured cost records are implausible (a session's usage booked to one subagent — rows written by hooks before v3.29). Rebuild the ledger from the transcripts with <b>pan-tools cost rebuild --apply</b> (dry-run first without the flag); if the transcripts are gone, quarantine it with <b>pan-tools hygiene clean --apply</b>. Rows captured by v3.29+ hooks are attributed per agent.`,
|
|
443
445
|
};
|
|
444
446
|
}
|
|
445
447
|
if (calls > 0 && unknown >= calls) {
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* campaigns run: runtime installs fall behind the latest version, legacy
|
|
6
6
|
* uppercase planning filenames linger from pre-v2.2 layouts, atomic-write
|
|
7
7
|
* .tmp orphans survive crashes, per-agent memory logs grow past the cap,
|
|
8
|
-
* cost ledgers
|
|
8
|
+
* cost ledgers carry oversum rows (a session's cumulative usage booked to one
|
|
9
|
+
* subagent — pre-v3.12.4 hooks wrote nothing else, and the parent-transcript
|
|
10
|
+
* slice path kept producing them up to v3.28), telemetry
|
|
9
11
|
* trace sessions pile up unboundedly, and stray fragment `.planning/`
|
|
10
12
|
* directories appear where a mapping step once ran.
|
|
11
13
|
*
|
|
@@ -39,7 +41,7 @@ const {
|
|
|
39
41
|
CHARS_PER_TOKEN,
|
|
40
42
|
STATE_FILE,
|
|
41
43
|
} = require('./constants.cjs');
|
|
42
|
-
const { planningPath, planningRel } = require('./utils.cjs');
|
|
44
|
+
const { planningPath, planningRel, detectPlanningModel } = require('./utils.cjs');
|
|
43
45
|
const { detectForeignPlanningTree } = require('./foreign-planning.cjs');
|
|
44
46
|
const { listMemoryAgents, readMemory, compactMemory } = require('./memory.cjs');
|
|
45
47
|
const { readRecords, isSuspectRecord, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
|
|
@@ -253,7 +255,8 @@ function recordMass(r) {
|
|
|
253
255
|
}
|
|
254
256
|
|
|
255
257
|
/**
|
|
256
|
-
* H-5: cost ledger dominated by physically implausible
|
|
258
|
+
* H-5: cost ledger dominated by physically implausible records — the oversum
|
|
259
|
+
* signature, a session's cumulative usage booked to one subagent row.
|
|
257
260
|
*
|
|
258
261
|
* Gated on token MASS as well as record count. A count-only gate passes a ledger
|
|
259
262
|
* whose few bad rows carry most of the tokens — field case: 24% of rows suspect
|
|
@@ -286,7 +289,7 @@ function checkCostLedger(cwd) {
|
|
|
286
289
|
: 'token mass';
|
|
287
290
|
findings.push(mkFinding('poisoned-ledger', 'critical',
|
|
288
291
|
planningRel(METRICS_DIR, TOKENS_FILE),
|
|
289
|
-
`${suspect}/${records.length} records suspect (${Math.round(ratio * 100)}% of rows, ${Math.round(massRatio * 100)}% of token mass) —
|
|
292
|
+
`${suspect}/${records.length} records suspect (${Math.round(ratio * 100)}% of rows, ${Math.round(massRatio * 100)}% of token mass) — oversum signature (a session's cumulative usage booked to one subagent row), tripped on ${basis}; aggregates quarantine them but the file is dead weight. Run \`cost rebuild\` first while the session transcripts still exist — quarantining moves the whole ledger aside, and a rebuild afterwards has no rows left to keep`,
|
|
290
293
|
{ action: 'quarantine-ledger' }));
|
|
291
294
|
return { findings };
|
|
292
295
|
}
|
|
@@ -466,7 +469,7 @@ function checkCachedContext(cwd) {
|
|
|
466
469
|
try {
|
|
467
470
|
const ttl = assessCacheTtl(readRecords(cwd).filter(r => !isSuspectRecord(r)));
|
|
468
471
|
if (ttl.recommend) {
|
|
469
|
-
findings.push(mkFinding('cache-context',
|
|
472
|
+
findings.push(mkFinding('cache-context', ttl.severity, planningRel(path.join(METRICS_DIR, TOKENS_FILE)), ttl.advice, null));
|
|
470
473
|
}
|
|
471
474
|
} catch { /* no ledger, or unreadable — nothing to say */ }
|
|
472
475
|
return { findings };
|
|
@@ -494,15 +497,11 @@ function checkPlanningFragment(cwd) {
|
|
|
494
497
|
const dir = planningPath(cwd);
|
|
495
498
|
let entries = [];
|
|
496
499
|
try { entries = fs.readdirSync(dir); } catch { return { findings, planning_exists: false }; }
|
|
497
|
-
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
const SPINE = ['project.md', 'state.md', 'phases', 'roadmap.md', 'requirements.md',
|
|
503
|
-
'milestones', 'focus', 'quick', 'orchestration'];
|
|
504
|
-
const hasSpine = SPINE.some(s => lower.includes(s));
|
|
505
|
-
if (!hasSpine && entries.length > 0) {
|
|
500
|
+
// A spine is whatever marks a deliberate PAN workflow — the phase model, the focus
|
|
501
|
+
// model, or an orchestration campaign (PLANNING_MODEL_MARKERS, shared with
|
|
502
|
+
// `validate health` so the two verbs cannot disagree about a tree). A dir holding
|
|
503
|
+
// only generated artifacts (codebase maps, metrics, traces) is a stray fragment.
|
|
504
|
+
if (detectPlanningModel(dir).model === 'fragment') {
|
|
506
505
|
findings.push(mkFinding('planning-fragment', 'info', planningRootRel(),
|
|
507
506
|
`.planning exists with ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} (${entries.slice(0, 5).join(', ')}) but no workflow spine (project/state/phases/focus/…) — likely a stray partial run; review and delete manually`,
|
|
508
507
|
null));
|
|
@@ -661,16 +660,14 @@ function applyFix(cwd, finding) {
|
|
|
661
660
|
const dest = `${abs}.quarantined-${stamp}`;
|
|
662
661
|
fs.renameSync(abs, dest);
|
|
663
662
|
|
|
664
|
-
// The cursor is a per-
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
cursorNote = ', cursor reset';
|
|
673
|
-
} catch { /* no cursor to reset */ }
|
|
663
|
+
// The cost cursor (COST_CURSOR_FILE) STAYS. It is a per-TRANSCRIPT
|
|
664
|
+
// high-water mark — how many records of each session transcript the
|
|
665
|
+
// hooks have already attributed — not a position in the ledger, so it
|
|
666
|
+
// has nothing to do with the file being moved aside. This step used to
|
|
667
|
+
// delete it as "a fresh ledger must not inherit the old read position",
|
|
668
|
+
// and the next SubagentStop then re-summed every session transcript from
|
|
669
|
+
// line 0: a fresh oversum row the day after each quarantine (field,
|
|
670
|
+
// 2026-08-25 → 08-26). Quarantine and poison had become a loop.
|
|
674
671
|
|
|
675
672
|
// Quarantine leaves a dated copy behind, and nothing else ever removes
|
|
676
673
|
// one. Run hygiene a few times over a year and the metrics dir fills
|
|
@@ -680,7 +677,7 @@ function applyFix(cwd, finding) {
|
|
|
680
677
|
|
|
681
678
|
return {
|
|
682
679
|
applied: true,
|
|
683
|
-
detail: `renamed to ${path.basename(dest)}${
|
|
680
|
+
detail: `renamed to ${path.basename(dest)}${prunedNote} — fresh ledger starts clean; transcript cursor kept`,
|
|
684
681
|
};
|
|
685
682
|
}
|
|
686
683
|
default:
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Three derived targets:
|
|
12
12
|
* 1. AGENTS.md — the universal, cross-runtime tools memory. PAN owns exactly
|
|
13
|
-
* the marker-fenced `<!-- BEGIN/END PAN WIZARD -->` section (
|
|
14
|
-
*
|
|
13
|
+
* the marker-fenced `<!-- BEGIN/END PAN WIZARD -->` section (Codex, Copilot and
|
|
14
|
+
* OpenCode read AGENTS.md natively; Claude via the CLAUDE.md bridge; Gemini reads GEMINI.md by default). User content
|
|
15
15
|
* outside the markers is preserved byte-for-byte.
|
|
16
16
|
* 2. CLAUDE.md — the Claude bridge (`@AGENTS.md` import), regenerated only
|
|
17
17
|
* when the Claude runtime is installed here.
|
|
@@ -126,7 +126,7 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
|
|
|
126
126
|
const targets = [];
|
|
127
127
|
const warnings = [];
|
|
128
128
|
|
|
129
|
-
// 1. AGENTS.md — universal PAN section (
|
|
129
|
+
// 1. AGENTS.md — universal PAN section (Claude Code reads it through the bridge in step 2).
|
|
130
130
|
{
|
|
131
131
|
const p = path.join(cwd, 'AGENTS.md');
|
|
132
132
|
const existing = safeReadFile(p);
|
|
@@ -301,11 +301,17 @@ function selectMemory(cwd, agent, opts = {}) {
|
|
|
301
301
|
/**
|
|
302
302
|
* Memory-load telemetry gate (ADR-0036 acceptance signal). Estimates the tokens
|
|
303
303
|
* of memory that would be injected whole (every agent log) and compares to the
|
|
304
|
-
* median per-agent
|
|
304
|
+
* median per-agent PROMPT from the trustworthy cost ledger (suspect records
|
|
305
305
|
* quarantined). Read-only, non-blocking; degrades to an absolute-token check
|
|
306
306
|
* when the ledger is thin.
|
|
307
307
|
*
|
|
308
|
-
*
|
|
308
|
+
* The prompt is `input + cache_read + cache_write`, not `input` alone: under prompt
|
|
309
|
+
* caching the uncached remainder is tens of tokens, so dividing by it reported 1.8k
|
|
310
|
+
* of memory as 8,940% of a "median agent input" and called it critical (field sweep
|
|
311
|
+
* 2026-09-17). Memory is injected into the whole prompt, so the whole prompt is what
|
|
312
|
+
* it must be measured against.
|
|
313
|
+
*
|
|
314
|
+
* @returns {{memory_tokens, agents, median_prompt_tokens, fraction, status, advisory}}
|
|
309
315
|
*/
|
|
310
316
|
function memoryLoadBudget(cwd, opts = {}) {
|
|
311
317
|
const { agents } = listMemoryAgents(cwd);
|
|
@@ -317,12 +323,12 @@ function memoryLoadBudget(cwd, opts = {}) {
|
|
|
317
323
|
let median = null;
|
|
318
324
|
try {
|
|
319
325
|
const cost = require('./cost.cjs');
|
|
320
|
-
const
|
|
321
|
-
.filter(r => !cost.isSuspectRecord(r))
|
|
322
|
-
.map(r => Number(r.input_tokens) || 0)
|
|
326
|
+
const prompts = (cost.readRecords(cwd) || [])
|
|
327
|
+
.filter(r => !cost.isSuspectRecord(r) && !cost.isEmptyRecord(r))
|
|
328
|
+
.map(r => (Number(r.input_tokens) || 0) + (Number(r.cache_read_tokens) || 0) + (Number(r.cache_write_tokens) || 0))
|
|
323
329
|
.filter(n => n > 0)
|
|
324
330
|
.sort((a, b) => a - b);
|
|
325
|
-
if (
|
|
331
|
+
if (prompts.length) median = prompts[Math.floor(prompts.length / 2)];
|
|
326
332
|
} catch { /* thin/absent ledger — absolute-token check only */ }
|
|
327
333
|
|
|
328
334
|
const fraction = median ? memoryTokens / median : null;
|
|
@@ -335,9 +341,9 @@ function memoryLoadBudget(cwd, opts = {}) {
|
|
|
335
341
|
const advisory = status === 'ok'
|
|
336
342
|
? 'Memory-load within budget.'
|
|
337
343
|
: `Memory injection is ~${memoryTokens} tokens across ${agents.length} agent log(s)` +
|
|
338
|
-
(fraction != null ? ` (~${Math.round(fraction * 100)}% of median agent
|
|
344
|
+
(fraction != null ? ` (~${Math.round(fraction * 100)}% of a median agent prompt)` : '') +
|
|
339
345
|
`. Bound it with cue-scoped 'memory select' or trim with 'memory compact <agent>'.`;
|
|
340
|
-
return { memory_tokens: memoryTokens, agents: agents.length,
|
|
346
|
+
return { memory_tokens: memoryTokens, agents: agents.length, median_prompt_tokens: median, fraction, status, advisory };
|
|
341
347
|
}
|
|
342
348
|
|
|
343
349
|
// ─── CLI command wrappers ────────────────────────────────────────────────────
|
|
@@ -134,9 +134,82 @@ function getCurrentSessionId(cwd) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Day-scoped auto-session id, the same shape the trace hook mints
|
|
139
|
+
* (`sess_auto_YYYYMMDD`) so a day's hook-written and agent-reported events share one
|
|
140
|
+
* session instead of splitting into two.
|
|
141
|
+
*/
|
|
142
|
+
function autoSessionId(now = new Date()) {
|
|
143
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
144
|
+
return `sess_auto_${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* How long a `current-session` pointer is evidence of a live session. An explicit
|
|
149
|
+
* (non-auto) session used to stay "current" indefinitely — a July session was still
|
|
150
|
+
* current in September in a field project, so `readActiveSessionMeta` in the cost hook
|
|
151
|
+
* backfilled two-month-old command/phase onto today's ledger rows, and agent-reported
|
|
152
|
+
* events would have landed in a long-dead session's directory (field sweep 2026-09-17).
|
|
153
|
+
*/
|
|
154
|
+
const SESSION_STALE_MS = 24 * 60 * 60 * 1000;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Last time anything was written to a session: its event log if it has one, else the
|
|
158
|
+
* moment it started. Null when the session cannot be read at all.
|
|
159
|
+
*/
|
|
160
|
+
function sessionLastActivityMs(cwd, sid) {
|
|
161
|
+
const dir = path.join(getTracesDir(cwd), sid);
|
|
162
|
+
try {
|
|
163
|
+
return fs.statSync(path.join(dir, TRACE_EVENT_FILE)).mtimeMs;
|
|
164
|
+
} catch { /* no events yet */ }
|
|
165
|
+
try {
|
|
166
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dir, OPT_SESSION_FILE), 'utf-8'));
|
|
167
|
+
const t = new Date(meta.started_at).getTime();
|
|
168
|
+
return Number.isFinite(t) ? t : null;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Is this session finished, or too old to still be the one running? Read-only —
|
|
176
|
+
* finalizing a stale session is the writing path's job (the trace hook's rollover).
|
|
177
|
+
*/
|
|
178
|
+
function isSessionStale(cwd, sid, now = Date.now()) {
|
|
179
|
+
const dir = path.join(getTracesDir(cwd), sid);
|
|
180
|
+
try {
|
|
181
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dir, OPT_SESSION_FILE), 'utf-8'));
|
|
182
|
+
if (meta && meta.ended_at) return true;
|
|
183
|
+
} catch { /* unreadable meta — fall through to the age test */ }
|
|
184
|
+
const last = sessionLastActivityMs(cwd, sid);
|
|
185
|
+
if (last === null) return true;
|
|
186
|
+
return now - last > SESSION_STALE_MS;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Append one trace event.
|
|
191
|
+
*
|
|
192
|
+
* Creates the day's auto-session when none is active. It used to return false instead,
|
|
193
|
+
* and the 16 `optimize trace log` call sites in the workflows are fire-and-forget
|
|
194
|
+
* (`2>/dev/null || true`), so on the phase pipeline — which never starts a trace
|
|
195
|
+
* session — every agent-reported event was silently discarded. Across fourteen field
|
|
196
|
+
* projects the instrument held 3,737 events, 99.7% of them the completion rows the hook
|
|
197
|
+
* writes, and not one error, gap or correction in its whole history (sweep 2026-09-17).
|
|
198
|
+
* An explicit `--session` is still honoured verbatim.
|
|
199
|
+
*/
|
|
137
200
|
function logTraceEvent(cwd, event, sessionId) {
|
|
138
|
-
|
|
139
|
-
|
|
201
|
+
// An explicit id is honoured verbatim. A POINTER, by contrast, is only evidence while
|
|
202
|
+
// the session it names is alive; a dead one is no session at all.
|
|
203
|
+
let sid = sessionId || null;
|
|
204
|
+
if (!sid) {
|
|
205
|
+
const current = getCurrentSessionId(cwd);
|
|
206
|
+
if (current && !isSessionStale(cwd, current)) sid = current;
|
|
207
|
+
}
|
|
208
|
+
if (!sid) {
|
|
209
|
+
const created = initTraceSession(cwd, { sessionId: autoSessionId(), description: 'auto-session (day-scoped)' });
|
|
210
|
+
if (!created || created.error) return false;
|
|
211
|
+
sid = created.session_id;
|
|
212
|
+
}
|
|
140
213
|
|
|
141
214
|
try {
|
|
142
215
|
const sessionDir = path.join(getTracesDir(cwd), sid);
|
|
@@ -1290,6 +1363,9 @@ module.exports = {
|
|
|
1290
1363
|
TRACE_EVENT_FILE,
|
|
1291
1364
|
OPT_SESSION_FILE,
|
|
1292
1365
|
CURRENT_SESSION_FILE,
|
|
1366
|
+
autoSessionId,
|
|
1367
|
+
isSessionStale,
|
|
1368
|
+
SESSION_STALE_MS,
|
|
1293
1369
|
EVENT_TYPES,
|
|
1294
1370
|
IMPACT_LEVELS,
|
|
1295
1371
|
VALID_SCOPES,
|