session-orchestrator 4.1.0 → 5.0.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/.agents/skills/session-plan/SKILL.md +1 -1
- package/.agents/skills/session-start/SKILL.md +1 -1
- package/.agents/skills/ux-grill/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.codex-plugin/skills/session-start/SKILL.md +1 -1
- package/.codex-plugin/skills/ux-grill/SKILL.md +21 -0
- package/.codex-plugin/skills/ux-grill/agents/openai.yaml +5 -0
- package/.cursor/commands/ux-grill.md +14 -0
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-start/SKILL.md +1 -1
- package/.cursor/skills/ux-grill/SKILL.md +13 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +128 -1
- package/README.md +98 -86
- package/agents/dialectic-deriver.md +11 -0
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/session.md +3 -2
- package/commands/ux-grill.md +51 -0
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +117 -50
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +9 -0
- package/docs/components.md +9 -9
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +13 -6
- package/docs/github-mirror-protection.md +61 -20
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/migration-v5.md +62 -0
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +16 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +70 -3
- package/hooks/_lib/lock-bootstrap.mjs +84 -1
- package/hooks/_lib/vcs-create-matcher.mjs +401 -16
- package/hooks/enforce-scope.mjs +201 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks.json +7 -2
- package/hooks/on-session-start.mjs +171 -49
- package/hooks/post-bash-issue-budget-refund.mjs +375 -0
- package/hooks/pre-auq-clarity.mjs +70 -18
- package/hooks/pre-bash-issue-budget.mjs +170 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +5 -4
- package/pi/prompts/ux-grill.md +12 -0
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-vitest-green.mjs +4 -2
- package/scripts/dialectic-deriver.mjs +32 -8
- package/scripts/emit-session.mjs +72 -1
- package/scripts/lib/agent-status.mjs +441 -9
- package/scripts/lib/auq/schema.mjs +10 -3
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/ci-status-banner.mjs +29 -6
- package/scripts/lib/claude-md-budget-lint.mjs +52 -2
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/config.mjs +12 -1
- package/scripts/lib/eval/engine.mjs +7 -1
- package/scripts/lib/file-lock.mjs +151 -8
- package/scripts/lib/git-porcelain.mjs +113 -0
- package/scripts/lib/instruction-budget-guard.mjs +415 -47
- package/scripts/lib/io.mjs +29 -4
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +412 -9
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/learnings/sizing-subject.mjs +44 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +19 -38
- package/scripts/lib/locks/state-md-lock.mjs +19 -41
- package/scripts/lib/maintenance-due-banner.mjs +450 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/peer-cards/merger.mjs +143 -0
- package/scripts/lib/pre-dispatch-check.mjs +20 -14
- package/scripts/lib/project-hygiene.mjs +81 -30
- package/scripts/lib/quality-gate.mjs +27 -71
- package/scripts/lib/reconcile/engine.mjs +19 -1
- package/scripts/lib/reconcile/writer.mjs +278 -11
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/scope-echo.mjs +346 -0
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-lock.mjs +62 -2
- package/scripts/lib/session-record-repair.mjs +91 -0
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/filters.mjs +26 -1
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +429 -56
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/test-runner/artifact-paths.mjs +30 -5
- package/scripts/lib/test-runner/issue-reconcile.mjs +45 -8
- package/scripts/lib/tmux-layout/layouts.mjs +62 -4
- package/scripts/lib/ux-grill/collect.mjs +1163 -0
- package/scripts/lib/ux-grill/compare.mjs +285 -0
- package/scripts/lib/ux-grill/manifest.mjs +618 -0
- package/scripts/lib/ux-grill/measures.mjs +431 -0
- package/scripts/lib/ux-grill/paths.mjs +224 -0
- package/scripts/lib/ux-grill/pencil-coverage.mjs +284 -0
- package/scripts/lib/ux-grill/reconcile.mjs +344 -0
- package/scripts/lib/ux-grill/run-record.mjs +316 -0
- package/scripts/lib/ux-grill/schema.mjs +321 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-untracked-test-deps.mjs +33 -19
- package/scripts/lib/validate/check-unwired-features.mjs +56 -27
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-status/board-lock.mjs +18 -0
- package/scripts/lib/vault-status/board-writer.mjs +8 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/mcp-server.sh +16 -1
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +7 -2
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +60 -209
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/references/bootstrap-ecosystem-health-flow.md +48 -0
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +37 -0
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +108 -0
- package/skills/bootstrap/references/bootstrap-rules-fetch-bridge.md +64 -0
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/claude-md-drift-check/SKILL.md +9 -2
- package/skills/claude-md-drift-check/checker.mjs +213 -21
- package/skills/discovery/SKILL.md +6 -173
- package/skills/discovery/probes/vault-staleness.mjs +35 -5
- package/skills/discovery/probes-docs.md +8 -4
- package/skills/discovery/probes-supply-chain.md +4 -2
- package/skills/discovery/probes-ui.md +8 -4
- package/skills/discovery/probes-vault.md +12 -4
- package/skills/discovery/references/discovery-interactive-triage.md +139 -0
- package/skills/discovery/references/discovery-triage-state.md +54 -0
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/eval/rubric-v1.md +13 -0
- package/skills/evolve/SKILL.md +2 -458
- package/skills/evolve/references/evolve-analyze-mode.md +360 -0
- package/skills/evolve/references/evolve-dialectic-mode.md +139 -0
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/plan/mode-retro.md +4 -3
- package/skills/reconcile/SKILL.md +11 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/drift-operations.md +20 -5
- package/skills/session-end/metrics-collection.md +1 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +27 -67
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-2-quality-gate.md +3 -3
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +32 -1
- package/skills/session-end/session-metrics-write.md +33 -12
- package/skills/session-plan/SKILL.md +46 -180
- package/skills/session-plan/references/session-plan-task-classification.md +152 -0
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +41 -7
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/operations-contract.md +114 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +42 -24
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/session-start/soul.md +2 -2
- package/skills/test-runner/SKILL.md +1 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/tmux-layout/SKILL.md +3 -1
- package/skills/ux-grill/SKILL.md +211 -0
- package/skills/ux-grill/rubric-v2.md +201 -0
- package/skills/ux-grill/soul.md +76 -0
- package/skills/wave-executor/SKILL.md +32 -127
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-executor-quality-gate.md +61 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +86 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +12 -2
- package/skills/wave-executor/references/wave-loop-review.md +19 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/_shared/ux-manifest.template.md +149 -0
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
- package/templates/_shared/journey-manifest.md +0 -114
|
@@ -53,11 +53,28 @@
|
|
|
53
53
|
* be reconstruction, not correction. Consumers comparing token totals across the
|
|
54
54
|
* 2026-08-11 boundary must treat it as a series break.
|
|
55
55
|
*
|
|
56
|
+
* ## Schema-version boundary — v1 and v2 token_input are different quantities (#1244)
|
|
57
|
+
*
|
|
58
|
+
* Since 2026-09-09 (`schema_version: 2`) a stop record's `token_input` is
|
|
59
|
+
* BILLABLE PROMPT VOLUME — uncached + cache_read + cache_creation — where v1
|
|
60
|
+
* held raw `usage.input_tokens` only. Under prompt caching those differ by up
|
|
61
|
+
* to five orders of magnitude (measured: 56 vs 3,676,179 on one agent), so
|
|
62
|
+
* summing them together produces a number describing neither. This module
|
|
63
|
+
* therefore sums ONLY `schema_version >= 2` token-bearing records into
|
|
64
|
+
* `total_token_*` and reports the excluded ones as `legacy_v1_records`: the
|
|
65
|
+
* boundary is DECLARED, never silent.
|
|
66
|
+
*
|
|
67
|
+
* `total_cost_usd` is computed per record via `costUsd()` and is null when ANY
|
|
68
|
+
* priced record carries a model the price table does not know — a partial cost
|
|
69
|
+
* is worse than no cost, because it reads as a complete one. `cost_records_priced`
|
|
70
|
+
* / `cost_records_total` say how much of the session the estimate covers.
|
|
71
|
+
*
|
|
56
72
|
* @module session-token-rollup
|
|
57
73
|
*/
|
|
58
74
|
|
|
59
75
|
import { readFileSync } from 'node:fs';
|
|
60
76
|
import { resolve } from 'node:path';
|
|
77
|
+
import { costUsd } from './telemetry/pricing.mjs';
|
|
61
78
|
|
|
62
79
|
// ---------------------------------------------------------------------------
|
|
63
80
|
// Default subagents.jsonl path (relative to cwd, mirroring the rest of the
|
|
@@ -85,12 +102,29 @@ function isTokenBearing(record) {
|
|
|
85
102
|
return record?.subagent_transcript_found === true;
|
|
86
103
|
}
|
|
87
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Is this record inside the schema_version 2 token contract? (#1244)
|
|
107
|
+
* @param {object} record
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
function isV2(record) {
|
|
111
|
+
return typeof record?.schema_version === 'number' && record.schema_version >= 2;
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
/**
|
|
89
115
|
* @typedef {Object} TokenRollupResult
|
|
90
116
|
* @property {number|null} total_token_input - Sum of token_input across TOKEN-BEARING matched records; null when none had a non-null value.
|
|
91
117
|
* @property {number|null} total_token_output - Sum of token_output across TOKEN-BEARING matched records; null when none had a non-null value.
|
|
92
118
|
* @property {number} subagents_with_tokens - Count of distinct agent_ids with at least one token-bearing record. This is the numerator of the honest coverage ratio.
|
|
93
119
|
* @property {number} matched_records - Total count of JSONL records matched by parentSessionId. Counts start records, phantom stops and pre-#949 records alike, so it is NOT the denominator for a token-coverage ratio — dividing by it is what made healthy sessions read as 12% covered.
|
|
120
|
+
* @property {number|null} total_token_input_uncached - Sum of token_input_uncached across v2 token-bearing records.
|
|
121
|
+
* @property {number|null} total_token_cache_read - Sum of token_cache_read across v2 token-bearing records.
|
|
122
|
+
* @property {number|null} total_token_cache_creation - Sum of token_cache_creation across v2 token-bearing records.
|
|
123
|
+
* @property {number|null} total_cost_usd - Σ costUsd() over v2 token-bearing records; null when ANY of them carries an unknown model (never 0 — see telemetry/pricing.mjs).
|
|
124
|
+
* @property {number} cost_records_priced - How many token-bearing records the price table could price.
|
|
125
|
+
* @property {number} cost_records_total - How many token-bearing records were candidates for pricing.
|
|
126
|
+
* @property {number} legacy_v1_records - Token-bearing records EXCLUDED from every total above because their schema_version < 2 (their token_input is a different quantity).
|
|
127
|
+
* @property {2} _token_schema - The token contract these totals were computed under.
|
|
94
128
|
*/
|
|
95
129
|
|
|
96
130
|
/**
|
|
@@ -112,6 +146,14 @@ export function rollupSessionTokens({
|
|
|
112
146
|
total_token_output: null,
|
|
113
147
|
subagents_with_tokens: 0,
|
|
114
148
|
matched_records: 0,
|
|
149
|
+
total_token_input_uncached: null,
|
|
150
|
+
total_token_cache_read: null,
|
|
151
|
+
total_token_cache_creation: null,
|
|
152
|
+
total_cost_usd: null,
|
|
153
|
+
cost_records_priced: 0,
|
|
154
|
+
cost_records_total: 0,
|
|
155
|
+
legacy_v1_records: 0,
|
|
156
|
+
_token_schema: 2,
|
|
115
157
|
};
|
|
116
158
|
|
|
117
159
|
if (typeof parentSessionId !== 'string' || parentSessionId.length === 0) {
|
|
@@ -157,10 +199,21 @@ export function rollupSessionTokens({
|
|
|
157
199
|
// Aggregate — skip null/undefined token values.
|
|
158
200
|
let sumInput = null;
|
|
159
201
|
let sumOutput = null;
|
|
202
|
+
let sumUncached = null;
|
|
203
|
+
let sumCacheRead = null;
|
|
204
|
+
let sumCacheCreation = null;
|
|
205
|
+
let sumCost = null;
|
|
206
|
+
let costPriced = 0;
|
|
207
|
+
let costTotal = 0;
|
|
208
|
+
let costUnknownModel = false;
|
|
209
|
+
let legacyV1 = 0;
|
|
160
210
|
|
|
161
211
|
// Track distinct agent_ids that contributed at least one non-null token.
|
|
162
212
|
const agentsWithTokens = new Set();
|
|
163
213
|
|
|
214
|
+
const addNonNegative = (acc, value) =>
|
|
215
|
+
typeof value === 'number' && value >= 0 ? (acc ?? 0) + value : acc;
|
|
216
|
+
|
|
164
217
|
for (const record of matched) {
|
|
165
218
|
// Provenance gate (#949) — a record whose tokens describe the PARENT
|
|
166
219
|
// transcript, or no transcript at all, contributes nothing. Skipping it
|
|
@@ -169,24 +222,48 @@ export function rollupSessionTokens({
|
|
|
169
222
|
// which is true, instead of a fabricated 0.
|
|
170
223
|
if (!isTokenBearing(record)) continue;
|
|
171
224
|
|
|
225
|
+
// Schema gate (#1244) — a v1 record's token_input is raw uncached input,
|
|
226
|
+
// a different quantity from a v2 record's billable prompt volume. Count it
|
|
227
|
+
// so the boundary is visible, never sum it.
|
|
228
|
+
if (!isV2(record)) {
|
|
229
|
+
legacyV1 += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
172
233
|
const inp = record.token_input;
|
|
173
234
|
const out = record.token_output;
|
|
174
235
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
236
|
+
sumInput = addNonNegative(sumInput, inp);
|
|
237
|
+
sumOutput = addNonNegative(sumOutput, out);
|
|
238
|
+
sumUncached = addNonNegative(sumUncached, record.token_input_uncached);
|
|
239
|
+
sumCacheRead = addNonNegative(sumCacheRead, record.token_cache_read);
|
|
240
|
+
sumCacheCreation = addNonNegative(sumCacheCreation, record.token_cache_creation);
|
|
181
241
|
|
|
182
242
|
// Count this agent as having tokens if either field is a non-null number.
|
|
183
|
-
|
|
184
|
-
(typeof inp === 'number' && inp >= 0) ||
|
|
185
|
-
|
|
186
|
-
) {
|
|
243
|
+
const hasTokens =
|
|
244
|
+
(typeof inp === 'number' && inp >= 0) || (typeof out === 'number' && out >= 0);
|
|
245
|
+
if (hasTokens) {
|
|
187
246
|
if (record.agent_id !== undefined && record.agent_id !== null) {
|
|
188
247
|
agentsWithTokens.add(record.agent_id);
|
|
189
248
|
}
|
|
249
|
+
|
|
250
|
+
// Cost: every token-bearing v2 record is a pricing candidate. One unknown
|
|
251
|
+
// model poisons the SESSION total — a cost covering some of the agents
|
|
252
|
+
// reads as covering all of them.
|
|
253
|
+
costTotal += 1;
|
|
254
|
+
const cost = costUsd({
|
|
255
|
+
model: record.model,
|
|
256
|
+
tokenInputUncached: record.token_input_uncached,
|
|
257
|
+
tokenCacheRead: record.token_cache_read,
|
|
258
|
+
tokenCacheCreation: record.token_cache_creation,
|
|
259
|
+
tokenOutput: record.token_output,
|
|
260
|
+
});
|
|
261
|
+
if (cost === null) {
|
|
262
|
+
costUnknownModel = true;
|
|
263
|
+
} else {
|
|
264
|
+
costPriced += 1;
|
|
265
|
+
sumCost = (sumCost ?? 0) + cost;
|
|
266
|
+
}
|
|
190
267
|
}
|
|
191
268
|
}
|
|
192
269
|
|
|
@@ -195,5 +272,13 @@ export function rollupSessionTokens({
|
|
|
195
272
|
total_token_output: sumOutput,
|
|
196
273
|
subagents_with_tokens: agentsWithTokens.size,
|
|
197
274
|
matched_records: matched.length,
|
|
275
|
+
total_token_input_uncached: sumUncached,
|
|
276
|
+
total_token_cache_read: sumCacheRead,
|
|
277
|
+
total_token_cache_creation: sumCacheCreation,
|
|
278
|
+
total_cost_usd: costUnknownModel ? null : sumCost,
|
|
279
|
+
cost_records_priced: costPriced,
|
|
280
|
+
cost_records_total: costTotal,
|
|
281
|
+
legacy_v1_records: legacyV1,
|
|
282
|
+
_token_schema: 2,
|
|
198
283
|
};
|
|
199
284
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
14
|
-
import { dirname,
|
|
14
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
15
15
|
import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
|
|
16
16
|
import { withStateMdLock } from '../session-lock.mjs';
|
|
17
|
+
import { resolveStateDir } from '../platform.mjs';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Sets frontmatter.updated to the given ISO 8601 timestamp and returns the
|
|
@@ -70,33 +71,26 @@ export function updateFrontmatterFields(contents, fields) {
|
|
|
70
71
|
// writeFileSync(STATE)` migrate to `await writeStateMd(repoRoot, contents => …)`,
|
|
71
72
|
// which mechanically serialises concurrent writers via `withStateMdLock`.
|
|
72
73
|
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
'.claude/STATE.md',
|
|
76
|
-
'.codex/STATE.md',
|
|
77
|
-
'.cursor/STATE.md',
|
|
78
|
-
'.pi/STATE.md',
|
|
79
|
-
];
|
|
74
|
+
// Legacy fallback order; the requested artifact's active harness always wins.
|
|
75
|
+
const STATE_DIR_CANDIDATES = ['.claude', '.codex', '.cursor', '.pi'];
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
...STATE_MD_CANDIDATES.filter((candidate) => candidate !== preferred),
|
|
99
|
-
];
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a state artifact independently of other files in the state directory.
|
|
79
|
+
* Check SO_STATE_DIR, the detected active harness, then legacy harness directories.
|
|
80
|
+
* If none exist, return the preferred path for create-on-first-write callers.
|
|
81
|
+
* In particular, an existing legacy STATE.md must not redirect a native scope.
|
|
82
|
+
*
|
|
83
|
+
* @param {string|undefined} repoRoot
|
|
84
|
+
* @param {string} filename - Artifact filename, such as STATE.md or wave-scope.json.
|
|
85
|
+
* @returns {string} Absolute artifact path.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveStateArtifactPath(repoRoot, filename) {
|
|
88
|
+
const root = repoRoot ?? process.cwd();
|
|
89
|
+
const override = (process.env.SO_STATE_DIR ?? '').trim();
|
|
90
|
+
const active = resolveStateDir();
|
|
91
|
+
const directories = [...new Set([override || active, active, ...STATE_DIR_CANDIDATES])];
|
|
92
|
+
const candidates = directories.map((directory) => resolvePath(root, directory, filename));
|
|
93
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
/**
|
|
@@ -110,13 +104,7 @@ function orderedStateMdCandidates() {
|
|
|
110
104
|
* @returns {string} Absolute path to STATE.md.
|
|
111
105
|
*/
|
|
112
106
|
export function resolveStateMdPath(repoRoot) {
|
|
113
|
-
|
|
114
|
-
const candidates = orderedStateMdCandidates();
|
|
115
|
-
for (const candidate of candidates) {
|
|
116
|
-
const abs = resolvePath(join(root, candidate));
|
|
117
|
-
if (existsSync(abs)) return abs;
|
|
118
|
-
}
|
|
119
|
-
return resolvePath(join(root, candidates[0]));
|
|
107
|
+
return resolveStateArtifactPath(repoRoot, 'STATE.md');
|
|
120
108
|
}
|
|
121
109
|
|
|
122
110
|
// ---------------------------------------------------------------------------
|
package/scripts/lib/state-md.mjs
CHANGED
|
@@ -32,14 +32,28 @@
|
|
|
32
32
|
* Optional:
|
|
33
33
|
* agent_type string | null — e.g. 'explore', 'writer', 'test-writer'
|
|
34
34
|
* parent_session_id string | null — session that spawned this subagent
|
|
35
|
-
* token_input integer | null —
|
|
35
|
+
* token_input integer | null — BILLABLE PROMPT VOLUME for this subagent.
|
|
36
|
+
* schema_version 1: raw `usage.input_tokens` only.
|
|
37
|
+
* schema_version 2 (#1244, 2026-09-09): uncached + cache_read +
|
|
38
|
+
* cache_creation. This is a REDEFINITION — a v1 and a v2 value
|
|
39
|
+
* are not comparable and must never be summed together.
|
|
40
|
+
* token_input_uncached integer | null — raw `usage.input_tokens` (v2+)
|
|
41
|
+
* token_cache_read integer | null — `usage.cache_read_input_tokens` (v2+)
|
|
42
|
+
* token_cache_creation integer | null — `usage.cache_creation_input_tokens` (v2+)
|
|
36
43
|
* token_output integer | null — completion token count for this subagent
|
|
44
|
+
* model string | null — model id from the transcript (v2+); null when
|
|
45
|
+
* the transcript exposes none. Required to price
|
|
46
|
+
* the record — see scripts/lib/telemetry/pricing.mjs.
|
|
37
47
|
* total_cost_usd number | null — native total cost in USD (#624, fractional,
|
|
38
48
|
* best-effort: null when the harness does not
|
|
39
49
|
* expose it; no rate table is applied)
|
|
40
50
|
*
|
|
41
51
|
* OTel aliases (optional, stop-only, additive — #411, schema_version=1 backwards-compat):
|
|
42
|
-
* gen_ai.usage.input_tokens integer | null —
|
|
52
|
+
* gen_ai.usage.input_tokens integer | null — RAW UNCACHED prompt tokens (OTel
|
|
53
|
+
* semantic). Under schema_version 2
|
|
54
|
+
* this is deliberately ≠ token_input.
|
|
55
|
+
* gen_ai.usage.cache_read_input_tokens integer | null (v2+)
|
|
56
|
+
* gen_ai.usage.cache_creation_input_tokens integer | null (v2+)
|
|
43
57
|
* gen_ai.usage.output_tokens integer | null — alias of token_output
|
|
44
58
|
* gen_ai.system 'anthropic' — AI provider identifier
|
|
45
59
|
*/
|
|
@@ -52,8 +66,25 @@ import path from 'node:path';
|
|
|
52
66
|
// Constants
|
|
53
67
|
// ---------------------------------------------------------------------------
|
|
54
68
|
|
|
55
|
-
/** Current subagent-record schema version. */
|
|
56
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
69
|
+
/** Current subagent-record schema version (2 since #1244, 2026-09-09). */
|
|
70
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Schema versions this module reads. v1 records stay valid forever — the ledger
|
|
74
|
+
* is append-only — but their `token_input` means something different (see the
|
|
75
|
+
* module header), which is why consumers gate on the version rather than trust
|
|
76
|
+
* the field name.
|
|
77
|
+
*/
|
|
78
|
+
export const SUPPORTED_SCHEMA_VERSIONS = Object.freeze([1, 2]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Version assumed for a record read from disk that carries none. It is 1, NOT
|
|
82
|
+
* `CURRENT_SCHEMA_VERSION`: a versionless record predates the field, so
|
|
83
|
+
* stamping it with the current version would relabel legacy token semantics as
|
|
84
|
+
* v2 and let the rollup sum a raw-input-only figure into a billable-volume
|
|
85
|
+
* total — exactly the silent mixing #1244 exists to prevent.
|
|
86
|
+
*/
|
|
87
|
+
export const LEGACY_SCHEMA_VERSION = 1;
|
|
57
88
|
|
|
58
89
|
/** Allowed event values. */
|
|
59
90
|
export const VALID_EVENTS = Object.freeze(['start', 'stop']);
|
|
@@ -102,10 +133,10 @@ export function validateSubagent(entry, options = {}) {
|
|
|
102
133
|
throw new ValidationError('subagent record must be a non-null object');
|
|
103
134
|
}
|
|
104
135
|
|
|
105
|
-
// schema_version
|
|
106
|
-
if (entry.schema_version
|
|
136
|
+
// schema_version — v1 and v2 both validate (append-only ledger, #1244).
|
|
137
|
+
if (!SUPPORTED_SCHEMA_VERSIONS.includes(entry.schema_version)) {
|
|
107
138
|
throw new ValidationError(
|
|
108
|
-
`schema_version must be ${
|
|
139
|
+
`schema_version must be one of ${SUPPORTED_SCHEMA_VERSIONS.join('|')}, got: ${entry.schema_version}`,
|
|
109
140
|
'schema_version',
|
|
110
141
|
);
|
|
111
142
|
}
|
|
@@ -185,6 +216,23 @@ export function validateSubagent(entry, options = {}) {
|
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
|
|
219
|
+
// Cache-bucket fields (optional, #1244 / schema_version 2). Same
|
|
220
|
+
// non-negative-integer-or-null contract as token_input above.
|
|
221
|
+
for (const field of ['token_input_uncached', 'token_cache_read', 'token_cache_creation']) {
|
|
222
|
+
const value = entry[field];
|
|
223
|
+
if (value !== undefined && value !== null) {
|
|
224
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
225
|
+
throw new ValidationError(`${field} must be a non-negative integer or null`, field);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// model (optional, #1244) — the transcript's model id; null is an honest
|
|
231
|
+
// absence and makes the record unpriceable, never free.
|
|
232
|
+
if (entry.model !== undefined && entry.model !== null && typeof entry.model !== 'string') {
|
|
233
|
+
throw new ValidationError('model must be a string or null', 'model');
|
|
234
|
+
}
|
|
235
|
+
|
|
188
236
|
// total_cost_usd (optional, #624) — fractional number (not integer), best-effort.
|
|
189
237
|
if (entry.total_cost_usd !== undefined && entry.total_cost_usd !== null) {
|
|
190
238
|
if (typeof entry.total_cost_usd !== 'number' || !Number.isFinite(entry.total_cost_usd) || entry.total_cost_usd < 0) {
|
|
@@ -208,6 +256,19 @@ export function validateSubagent(entry, options = {}) {
|
|
|
208
256
|
}
|
|
209
257
|
}
|
|
210
258
|
|
|
259
|
+
// OTel cache aliases — #1244 additive (schema_version 2).
|
|
260
|
+
for (const field of [
|
|
261
|
+
'gen_ai.usage.cache_read_input_tokens',
|
|
262
|
+
'gen_ai.usage.cache_creation_input_tokens',
|
|
263
|
+
]) {
|
|
264
|
+
const value = entry[field];
|
|
265
|
+
if (value !== undefined && value !== null) {
|
|
266
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
267
|
+
throw new ValidationError(`${field} must be a non-negative integer or null`, field);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
211
272
|
// OTel alias — #411 additive, schema_version=1 backwards-compat
|
|
212
273
|
if (entry['gen_ai.system'] !== undefined && entry['gen_ai.system'] !== null) {
|
|
213
274
|
if (typeof entry['gen_ai.system'] !== 'string') {
|
|
@@ -234,15 +295,22 @@ export function normalizeSubagent(entry) {
|
|
|
234
295
|
if (!entry || typeof entry !== 'object') return entry;
|
|
235
296
|
return {
|
|
236
297
|
...entry,
|
|
237
|
-
schema_version: entry.schema_version ??
|
|
298
|
+
schema_version: entry.schema_version ?? LEGACY_SCHEMA_VERSION,
|
|
238
299
|
agent_type: entry.agent_type ?? null,
|
|
239
300
|
parent_session_id: entry.parent_session_id ?? null,
|
|
240
301
|
token_input: entry.token_input ?? null,
|
|
241
302
|
token_output: entry.token_output ?? null,
|
|
303
|
+
// #1244 additive (schema_version 2) — cache buckets + model id.
|
|
304
|
+
token_input_uncached: entry.token_input_uncached ?? null,
|
|
305
|
+
token_cache_read: entry.token_cache_read ?? null,
|
|
306
|
+
token_cache_creation: entry.token_cache_creation ?? null,
|
|
307
|
+
model: entry.model ?? null,
|
|
242
308
|
// total_cost_usd — #624 additive, best-effort native cost (null when absent)
|
|
243
309
|
total_cost_usd: entry.total_cost_usd ?? null,
|
|
244
310
|
// OTel alias — #411 additive, schema_version=1 backwards-compat
|
|
245
311
|
'gen_ai.usage.input_tokens': entry['gen_ai.usage.input_tokens'] ?? null,
|
|
312
|
+
'gen_ai.usage.cache_read_input_tokens': entry['gen_ai.usage.cache_read_input_tokens'] ?? null,
|
|
313
|
+
'gen_ai.usage.cache_creation_input_tokens': entry['gen_ai.usage.cache_creation_input_tokens'] ?? null,
|
|
246
314
|
'gen_ai.usage.output_tokens': entry['gen_ai.usage.output_tokens'] ?? null,
|
|
247
315
|
'gen_ai.system': entry['gen_ai.system'] ?? null,
|
|
248
316
|
};
|
|
@@ -266,7 +334,7 @@ export function migrateLegacySubagent(entry) {
|
|
|
266
334
|
if (!entry || typeof entry !== 'object') return entry;
|
|
267
335
|
const out = { ...entry };
|
|
268
336
|
if (out.schema_version === undefined || out.schema_version === null) {
|
|
269
|
-
out.schema_version =
|
|
337
|
+
out.schema_version = LEGACY_SCHEMA_VERSION;
|
|
270
338
|
}
|
|
271
339
|
return out;
|
|
272
340
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pricing.mjs — USD price table for token-bucket cost estimation (#1244).
|
|
3
|
+
*
|
|
4
|
+
* The subagent ledger records four token buckets per stop record
|
|
5
|
+
* (`token_input_uncached`, `token_cache_read`, `token_cache_creation`,
|
|
6
|
+
* `token_output`). Each bucket is billed at its OWN rate, so a cost estimate
|
|
7
|
+
* that multiplies one blended rate by a single token total is wrong by up to
|
|
8
|
+
* an order of magnitude on a cache-heavy run — which is the whole reason this
|
|
9
|
+
* table exists rather than a single `$/token` constant.
|
|
10
|
+
*
|
|
11
|
+
* ## What is measured and what is derived
|
|
12
|
+
*
|
|
13
|
+
* `input` and `output` are the first-party Anthropic API list prices as
|
|
14
|
+
* published in the bundled model table, sourced on the date `PRICING_TABLE_DATE`
|
|
15
|
+
* carries (2026-09-09) — that constant is the SSOT, never a second date repeated
|
|
16
|
+
* in this prose. `cache_read` and `cache_creation` are DERIVED from the
|
|
17
|
+
* documented ephemeral-cache multipliers (read ≈ 0.1×, 5-minute write ≈ 1.25×
|
|
18
|
+
* the input rate) except where a row carries a documented figure — Claude Fable
|
|
19
|
+
* 5.1 publishes $0.25/MTok cache reads directly, which is NOT 0.1× its $10
|
|
20
|
+
* input rate. Every row therefore declares its own `cache_source` so a reader
|
|
21
|
+
* never has to guess which numbers were read off a price list and which were
|
|
22
|
+
* multiplied out here.
|
|
23
|
+
*
|
|
24
|
+
* A row with `verified: false` carries a PLACEHOLDER: its numbers are a
|
|
25
|
+
* best-effort stand-in, never a quoted price. `costUsd()` still prices such a
|
|
26
|
+
* row (an estimate is more useful than a null when it is labelled), but any
|
|
27
|
+
* consumer publishing a dollar figure must surface the flag.
|
|
28
|
+
*
|
|
29
|
+
* ## Null means UNKNOWN MODEL, never zero
|
|
30
|
+
*
|
|
31
|
+
* `priceFor()` and `costUsd()` return `null` for a model this table does not
|
|
32
|
+
* know. `null` is an honest absence — the same discipline the token rollup
|
|
33
|
+
* applies to `total_token_input`. Coercing it to `0` fabricates a free run and
|
|
34
|
+
* is the single most damaging misreading of this module.
|
|
35
|
+
*
|
|
36
|
+
* All rates are USD per MILLION tokens.
|
|
37
|
+
*
|
|
38
|
+
* @module telemetry/pricing
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The date the `input`/`output` rates in PRICING_TABLE were sourced.
|
|
43
|
+
* Any consumer quoting a dollar figure should quote this date beside it — a
|
|
44
|
+
* price without its measurement date ages silently.
|
|
45
|
+
* @type {string}
|
|
46
|
+
*/
|
|
47
|
+
export const PRICING_TABLE_DATE = '2026-09-09';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} PricingRow
|
|
51
|
+
* @property {number} input - USD per 1M uncached prompt tokens.
|
|
52
|
+
* @property {number} cache_read - USD per 1M tokens served from the prompt cache.
|
|
53
|
+
* @property {number} cache_creation - USD per 1M tokens written to the prompt cache.
|
|
54
|
+
* @property {number} output - USD per 1M completion tokens.
|
|
55
|
+
* @property {boolean} verified - false ⇒ the row is a labelled placeholder, not a quoted price.
|
|
56
|
+
* @property {'documented'|'derived-multiplier'} cache_source - provenance of the two cache rates.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Per-model USD-per-million-token rates.
|
|
61
|
+
* @type {Readonly<Record<string, PricingRow>>}
|
|
62
|
+
*/
|
|
63
|
+
export const PRICING_TABLE = Object.freeze({
|
|
64
|
+
// Claude Fable 5.1 — $10 / $50 list; cache reads documented at $0.25/MTok
|
|
65
|
+
// (NOT the usual 0.1× multiplier), cache writes derived at 1.25× input.
|
|
66
|
+
'claude-fable-5-1': Object.freeze({
|
|
67
|
+
input: 10.0,
|
|
68
|
+
cache_read: 0.25,
|
|
69
|
+
cache_creation: 12.5,
|
|
70
|
+
output: 50.0,
|
|
71
|
+
verified: true,
|
|
72
|
+
cache_source: 'documented',
|
|
73
|
+
}),
|
|
74
|
+
'claude-opus-5': Object.freeze({
|
|
75
|
+
input: 5.0,
|
|
76
|
+
cache_read: 0.5,
|
|
77
|
+
cache_creation: 6.25,
|
|
78
|
+
output: 25.0,
|
|
79
|
+
verified: true,
|
|
80
|
+
cache_source: 'derived-multiplier',
|
|
81
|
+
}),
|
|
82
|
+
'claude-sonnet-5': Object.freeze({
|
|
83
|
+
input: 2.0,
|
|
84
|
+
cache_read: 0.2,
|
|
85
|
+
cache_creation: 2.5,
|
|
86
|
+
output: 10.0,
|
|
87
|
+
verified: true,
|
|
88
|
+
cache_source: 'derived-multiplier',
|
|
89
|
+
}),
|
|
90
|
+
'claude-haiku-4-5': Object.freeze({
|
|
91
|
+
input: 1.0,
|
|
92
|
+
cache_read: 0.1,
|
|
93
|
+
cache_creation: 1.25,
|
|
94
|
+
output: 5.0,
|
|
95
|
+
verified: true,
|
|
96
|
+
cache_source: 'derived-multiplier',
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Explicit aliases for model ids that appear in transcripts but are not the
|
|
102
|
+
* canonical table key — dated snapshots and the `[1m]` context-window suffix
|
|
103
|
+
* the harness stamps on long-context sessions.
|
|
104
|
+
* @type {Readonly<Record<string, string>>}
|
|
105
|
+
*/
|
|
106
|
+
export const MODEL_ALIASES = Object.freeze({
|
|
107
|
+
'claude-haiku-4-5-20251001': 'claude-haiku-4-5',
|
|
108
|
+
'claude-opus-5[1m]': 'claude-opus-5',
|
|
109
|
+
'claude-fable-5-1[1m]': 'claude-fable-5-1',
|
|
110
|
+
'claude-sonnet-5[1m]': 'claude-sonnet-5',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Look up the rate row for a model id.
|
|
115
|
+
*
|
|
116
|
+
* Resolution order: exact key → explicit alias → longest matching table key
|
|
117
|
+
* that the id starts with (so `claude-opus-5-20260401` and `claude-opus-5[1m]`
|
|
118
|
+
* both resolve to `claude-opus-5`). Longest-prefix wins so a future
|
|
119
|
+
* `claude-opus-5-1` row is never shadowed by `claude-opus-5`.
|
|
120
|
+
*
|
|
121
|
+
* @param {string|null|undefined} modelId
|
|
122
|
+
* @returns {PricingRow|null} the row, or null when the model is UNKNOWN
|
|
123
|
+
* (never a zero-rate row — see the module header).
|
|
124
|
+
*/
|
|
125
|
+
export function priceFor(modelId) {
|
|
126
|
+
if (typeof modelId !== 'string') return null;
|
|
127
|
+
const id = modelId.trim();
|
|
128
|
+
if (!id) return null;
|
|
129
|
+
|
|
130
|
+
if (Object.hasOwn(PRICING_TABLE, id)) return PRICING_TABLE[id];
|
|
131
|
+
|
|
132
|
+
const aliased = MODEL_ALIASES[id];
|
|
133
|
+
if (aliased && Object.hasOwn(PRICING_TABLE, aliased)) return PRICING_TABLE[aliased];
|
|
134
|
+
|
|
135
|
+
let best = null;
|
|
136
|
+
for (const key of Object.keys(PRICING_TABLE)) {
|
|
137
|
+
if (id.startsWith(key) && (best === null || key.length > best.length)) best = key;
|
|
138
|
+
}
|
|
139
|
+
return best === null ? null : PRICING_TABLE[best];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Non-negative finite number within the exactly-representable integer range,
|
|
144
|
+
* else 0. Absent is 0 — an absent bucket costs nothing; an unknown MODEL is what
|
|
145
|
+
* yields null (in costUsd, not here).
|
|
146
|
+
*
|
|
147
|
+
* The `MAX_SAFE_INTEGER` ceiling is not pedantry: a corrupt bucket of `1e308`
|
|
148
|
+
* is finite, so it passed the old guard and multiplied out to `Infinity`, which
|
|
149
|
+
* `JSON.stringify` writes as `null` — the exact encoding this module reserves
|
|
150
|
+
* for "unknown model". A token count above 2^53 is not a measurement.
|
|
151
|
+
*
|
|
152
|
+
* @param {unknown} n
|
|
153
|
+
* @returns {number}
|
|
154
|
+
*/
|
|
155
|
+
function num(n) {
|
|
156
|
+
return typeof n === 'number' && Number.isFinite(n) && n >= 0 && n <= Number.MAX_SAFE_INTEGER
|
|
157
|
+
? n
|
|
158
|
+
: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Estimate the USD cost of one record's four token buckets.
|
|
163
|
+
*
|
|
164
|
+
* Each bucket is multiplied by ITS OWN rate — a cache read is ~10× cheaper
|
|
165
|
+
* than an uncached prompt token and a cache write ~1.25× more expensive, so a
|
|
166
|
+
* blended rate is not an approximation of this, it is a different number.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} opts
|
|
169
|
+
* @param {string|null|undefined} opts.model - model id from the transcript
|
|
170
|
+
* @param {number|null|undefined} opts.tokenInputUncached
|
|
171
|
+
* @param {number|null|undefined} opts.tokenCacheRead
|
|
172
|
+
* @param {number|null|undefined} opts.tokenCacheCreation
|
|
173
|
+
* @param {number|null|undefined} opts.tokenOutput
|
|
174
|
+
* @returns {number|null} USD cost, or `null` when the model is UNKNOWN.
|
|
175
|
+
* **null means "unknown model", never 0.** A caller that coerces it to 0
|
|
176
|
+
* reports a free run that was not free.
|
|
177
|
+
*/
|
|
178
|
+
export function costUsd({
|
|
179
|
+
model,
|
|
180
|
+
tokenInputUncached,
|
|
181
|
+
tokenCacheRead,
|
|
182
|
+
tokenCacheCreation,
|
|
183
|
+
tokenOutput,
|
|
184
|
+
} = {}) {
|
|
185
|
+
const row = priceFor(model);
|
|
186
|
+
if (row === null) return null;
|
|
187
|
+
const perToken = 1e-6;
|
|
188
|
+
const cost =
|
|
189
|
+
num(tokenInputUncached) * row.input * perToken +
|
|
190
|
+
num(tokenCacheRead) * row.cache_read * perToken +
|
|
191
|
+
num(tokenCacheCreation) * row.cache_creation * perToken +
|
|
192
|
+
num(tokenOutput) * row.output * perToken;
|
|
193
|
+
// Belt and braces beside `num()`'s ceiling: a non-finite total would serialise
|
|
194
|
+
// as JSON `null` and be indistinguishable from "unknown model". Returning null
|
|
195
|
+
// deliberately makes that reading TRUE rather than accidental.
|
|
196
|
+
return Number.isFinite(cost) ? cost : null;
|
|
197
|
+
}
|
|
@@ -348,11 +348,46 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
348
348
|
let startedAt = null;
|
|
349
349
|
let sessionType = null;
|
|
350
350
|
let lastTs = null;
|
|
351
|
+
// `orchestrator.session.shape_resolved` (scripts/lib/session-shape.mjs) is
|
|
352
|
+
// the plan-time measurement: it fires AFTER the operator picked a mode, and
|
|
353
|
+
// it is the only event carrying `session_profile`. `session.started`'s mode
|
|
354
|
+
// predates the choice, and the STATE.md profile read below evaporates the
|
|
355
|
+
// moment STATE.md is rewritten — so when a shape record exists it wins for
|
|
356
|
+
// BOTH fields. Latest wins; absent ⇒ the pre-existing behaviour, unchanged.
|
|
357
|
+
//
|
|
358
|
+
// "Latest" is decidable only for a record carrying a parseable timestamp. An
|
|
359
|
+
// UNDATED record has no place in that order and must never displace a dated
|
|
360
|
+
// one (the earlier `ts === null` disjunct inverted exactly that). It is kept
|
|
361
|
+
// separately and used only when no dated shape record exists AND the
|
|
362
|
+
// pre-existing sources below yielded nothing.
|
|
363
|
+
let shapeType = null;
|
|
364
|
+
let shapeProfile = null;
|
|
365
|
+
let shapeTs = null;
|
|
366
|
+
let undatedShapeType = null;
|
|
367
|
+
let undatedShapeProfile = null;
|
|
351
368
|
|
|
352
369
|
for (const ev of events) {
|
|
353
370
|
if (!ev || typeof ev !== 'object') continue;
|
|
354
371
|
const ts = typeof ev.timestamp === 'string' && !Number.isNaN(Date.parse(ev.timestamp)) ? ev.timestamp : null;
|
|
355
372
|
if (ts && (lastTs === null || ts > lastTs)) lastTs = ts;
|
|
373
|
+
if (ev.event === 'orchestrator.session.shape_resolved') {
|
|
374
|
+
const type = typeof ev.session_type === 'string' && ev.session_type.trim() !== '' ? ev.session_type.trim() : null;
|
|
375
|
+
// OMITTED, never null, when the session has no profile — only a
|
|
376
|
+
// present string may overwrite an earlier reading.
|
|
377
|
+
const profile =
|
|
378
|
+
typeof ev.session_profile === 'string' && ev.session_profile.trim() !== '' ? ev.session_profile.trim() : null;
|
|
379
|
+
if (ts !== null) {
|
|
380
|
+
if (shapeTs === null || ts >= shapeTs) {
|
|
381
|
+
shapeTs = ts;
|
|
382
|
+
if (type !== null) shapeType = type;
|
|
383
|
+
if (profile !== null) shapeProfile = profile;
|
|
384
|
+
}
|
|
385
|
+
} else if (undatedShapeType === null && undatedShapeProfile === null) {
|
|
386
|
+
undatedShapeType = type;
|
|
387
|
+
undatedShapeProfile = profile;
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
356
391
|
if (ev.event !== 'orchestrator.session.started') continue;
|
|
357
392
|
// `mode` is what session-start writes; `session_type` is the ledger's own
|
|
358
393
|
// name for the same fact. Read both — neither is guaranteed present.
|
|
@@ -364,10 +399,24 @@ export function deriveSessionFromEvents(metricsDir) {
|
|
|
364
399
|
}
|
|
365
400
|
}
|
|
366
401
|
|
|
367
|
-
if (
|
|
402
|
+
if (shapeType !== null) sessionType = shapeType;
|
|
403
|
+
// An undated shape record is the weakest reading there is: it wins over
|
|
404
|
+
// nothing at all, and over nothing else. A dated shape record (shapeTs) or
|
|
405
|
+
// a `session.started` mode both outrank it.
|
|
406
|
+
// Type and profile are read as a PAIR from the same record, so the undated
|
|
407
|
+
// fallback is taken as a pair too — never spliced onto a type another source
|
|
408
|
+
// supplied.
|
|
409
|
+
let profile = shapeProfile;
|
|
410
|
+
if (shapeTs === null && sessionType === null) {
|
|
411
|
+
sessionType = undatedShapeType;
|
|
412
|
+
profile = undatedShapeProfile;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (startedAt === null && sessionType === null && profile === null) return { session: {}, source: 'absent' };
|
|
368
416
|
|
|
369
417
|
const session = {};
|
|
370
418
|
if (sessionType !== null) session.session_type = sessionType;
|
|
419
|
+
if (profile !== null) session.session_profile = profile;
|
|
371
420
|
if (startedAt !== null) session.started_at = startedAt;
|
|
372
421
|
// completed_at is the last life-sign, never the wall clock — the same
|
|
373
422
|
// omit-never-fabricate contract session-close-backfill.mjs uses (#914 R1).
|