agent-orchestrator-kit 0.13.0 → 0.14.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/CHANGELOG.md +8 -0
- package/README.md +16 -3
- package/bin/agent-orchestrator.js +335 -115
- package/bin/claude-cost-estimate.js +51 -0
- package/bin/spend-collect.js +76 -13
- package/package.json +1 -1
- package/templates/.agents/commands/opsx-propose.md +6 -0
- package/templates/.agents/commands/opsx-review.md +35 -11
- package/templates/.agents/rules/session-handoff.mdc +2 -2
- package/templates/.agents/skills/agent-orchestration/SKILL.md +5 -4
- package/templates/.agents/skills/openspec-propose/SKILL.md +6 -0
- package/templates/.agents/subagents/openspec-guide.md +3 -2
- package/templates/.agents/subagents/session-handoff.md +2 -1
- package/templates/.agents/subagents/spec-architect.md +6 -1
- package/templates/.agents/subagents/spec-archiver.md +2 -1
- package/templates/.agents/subagents/spec-reviewer.md +5 -1
- package/templates/AGENTS.md +1 -1
- package/templates/scripts/cursor-spend-collect.cjs +129 -67
|
@@ -5,7 +5,8 @@ import { readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSyn
|
|
|
5
5
|
import { join, dirname, basename, resolve } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import { collectSpend
|
|
8
|
+
import { collectSpend } from './spend-collect.js';
|
|
9
|
+
import { describeClaudeCostEstimate } from './claude-cost-estimate.js';
|
|
9
10
|
import { resolveRestoreClient, ampThreadIdFromEnv } from './session-client.js';
|
|
10
11
|
import {
|
|
11
12
|
earlierTimestamp,
|
|
@@ -968,8 +969,14 @@ function firstLineCommand(value) {
|
|
|
968
969
|
function firstSpawnName(value) {
|
|
969
970
|
const tick = String(value || '').match(/`([a-z0-9-]+)`/i);
|
|
970
971
|
if (tick) return tick[1];
|
|
971
|
-
const
|
|
972
|
-
return
|
|
972
|
+
const role = canonicalRole(value);
|
|
973
|
+
return {
|
|
974
|
+
Explorer: 'explorer',
|
|
975
|
+
Architect: 'spec-architect',
|
|
976
|
+
'Spec Reviewer': 'spec-reviewer',
|
|
977
|
+
Implementer: '',
|
|
978
|
+
Archiver: '',
|
|
979
|
+
}[role] || '';
|
|
973
980
|
}
|
|
974
981
|
|
|
975
982
|
function sectionOr(sections, title, fallback = '') {
|
|
@@ -1611,7 +1618,7 @@ function readHandoffFields(projectDir, changeName) {
|
|
|
1611
1618
|
return { filePath, fields: fieldsFromSections(changeName, sections) };
|
|
1612
1619
|
}
|
|
1613
1620
|
|
|
1614
|
-
const METRICS_VERSION =
|
|
1621
|
+
const METRICS_VERSION = 2;
|
|
1615
1622
|
const METRICS_SPEND_KEYS = ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated'];
|
|
1616
1623
|
const LEFTOVER_GRACE_MS = 120000;
|
|
1617
1624
|
|
|
@@ -1675,6 +1682,92 @@ function defaultMetrics(changeName, nowIso) {
|
|
|
1675
1682
|
};
|
|
1676
1683
|
}
|
|
1677
1684
|
|
|
1685
|
+
const VALID_COST_SOURCES = new Set(['api-estimate', 'api-estimate-fallback', 'amp-usage']);
|
|
1686
|
+
|
|
1687
|
+
function compactModelRows(sources) {
|
|
1688
|
+
const byModel = new Map();
|
|
1689
|
+
for (const source of sources || []) {
|
|
1690
|
+
if (!source) continue;
|
|
1691
|
+
const platform = VALID_PLATFORMS.has(source.platform) ? source.platform : null;
|
|
1692
|
+
const model = source.model == null || source.model === '' ? null : String(source.model);
|
|
1693
|
+
const key = `${platform || ''}::${model || ''}`;
|
|
1694
|
+
const row = byModel.get(key) || {
|
|
1695
|
+
model,
|
|
1696
|
+
platform,
|
|
1697
|
+
inputTokens: null,
|
|
1698
|
+
outputTokens: null,
|
|
1699
|
+
totalTokens: null,
|
|
1700
|
+
costUsd: null,
|
|
1701
|
+
costUsdEstimated: null,
|
|
1702
|
+
};
|
|
1703
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd']) {
|
|
1704
|
+
row[field] = addNullable(row[field], numOrNull(source[field]));
|
|
1705
|
+
}
|
|
1706
|
+
row.costUsdEstimated = roundUsd4(addNullable(row.costUsdEstimated, numOrNull(source.costUsdEstimated)));
|
|
1707
|
+
if (VALID_COST_SOURCES.has(source.costSource)) {
|
|
1708
|
+
if (!row.costSource || row.costSource === source.costSource) row.costSource = source.costSource;
|
|
1709
|
+
else delete row.costSource;
|
|
1710
|
+
}
|
|
1711
|
+
byModel.set(key, row);
|
|
1712
|
+
}
|
|
1713
|
+
return [...byModel.values()];
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function compactSourceTotals(sources) {
|
|
1717
|
+
const sourceIds = [];
|
|
1718
|
+
const sourceTotals = {};
|
|
1719
|
+
for (const source of sources || []) {
|
|
1720
|
+
if (!source || source.id == null || source.id === '') continue;
|
|
1721
|
+
const id = String(source.id);
|
|
1722
|
+
if (!sourceIds.includes(id)) sourceIds.push(id);
|
|
1723
|
+
const total = numOrNull(source.totalTokens);
|
|
1724
|
+
if (total != null) sourceTotals[id] = Math.max(numOrNull(sourceTotals[id]) ?? 0, total);
|
|
1725
|
+
}
|
|
1726
|
+
return { sourceIds, sourceTotals };
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function normalizeSessionV2(session) {
|
|
1730
|
+
const normalized = session && typeof session === 'object' && !Array.isArray(session) ? { ...session } : {};
|
|
1731
|
+
const legacySources = Array.isArray(normalized.sources) ? normalized.sources : null;
|
|
1732
|
+
if (legacySources) {
|
|
1733
|
+
const bestById = new Map();
|
|
1734
|
+
for (const source of legacySources) {
|
|
1735
|
+
if (!source || source.id == null || source.id === '') continue;
|
|
1736
|
+
const id = String(source.id);
|
|
1737
|
+
const previous = bestById.get(id);
|
|
1738
|
+
if (!previous || (numOrNull(source.totalTokens) ?? 0) >= (numOrNull(previous.totalTokens) ?? 0)) bestById.set(id, source);
|
|
1739
|
+
}
|
|
1740
|
+
const compactSources = [...bestById.values()].map((source) => ({
|
|
1741
|
+
...source,
|
|
1742
|
+
model: source.model || normalized.model || null,
|
|
1743
|
+
platform: source.platform || normalized.platform || null,
|
|
1744
|
+
}));
|
|
1745
|
+
const compact = compactSourceTotals(compactSources);
|
|
1746
|
+
normalized.sourceIds = compact.sourceIds;
|
|
1747
|
+
normalized.sourceTotals = compact.sourceTotals;
|
|
1748
|
+
normalized.byModel = compactModelRows(compactSources);
|
|
1749
|
+
if (!normalized.platform && compactSources[0] && compactSources[0].platform) normalized.platform = compactSources[0].platform;
|
|
1750
|
+
if (!normalized.model && compactSources[0] && compactSources[0].model) normalized.model = compactSources[0].model;
|
|
1751
|
+
delete normalized.sources;
|
|
1752
|
+
} else {
|
|
1753
|
+
normalized.sourceIds = Array.isArray(normalized.sourceIds)
|
|
1754
|
+
? [...new Set(normalized.sourceIds.filter((id) => id != null && id !== '').map(String))]
|
|
1755
|
+
: [];
|
|
1756
|
+
normalized.sourceTotals = normalized.sourceTotals && typeof normalized.sourceTotals === 'object' && !Array.isArray(normalized.sourceTotals)
|
|
1757
|
+
? { ...normalized.sourceTotals }
|
|
1758
|
+
: {};
|
|
1759
|
+
normalized.byModel = Array.isArray(normalized.byModel) ? normalized.byModel.map((row) => ({ ...row })) : [];
|
|
1760
|
+
}
|
|
1761
|
+
return normalized;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function normalizeMetricsV2(metrics) {
|
|
1765
|
+
if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) return metrics;
|
|
1766
|
+
metrics.version = METRICS_VERSION;
|
|
1767
|
+
metrics.sessions = Array.isArray(metrics.sessions) ? metrics.sessions.map(normalizeSessionV2) : [];
|
|
1768
|
+
return metrics;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1678
1771
|
function loadMetricsFile(filePath, changeName, nowIso) {
|
|
1679
1772
|
if (!existsSync(filePath)) return defaultMetrics(changeName, nowIso);
|
|
1680
1773
|
let parsed;
|
|
@@ -1687,7 +1780,7 @@ function loadMetricsFile(filePath, changeName, nowIso) {
|
|
|
1687
1780
|
return defaultMetrics(changeName, nowIso);
|
|
1688
1781
|
}
|
|
1689
1782
|
const base = defaultMetrics(changeName, parsed.createdAt || nowIso);
|
|
1690
|
-
return {
|
|
1783
|
+
return normalizeMetricsV2({
|
|
1691
1784
|
...base,
|
|
1692
1785
|
...parsed,
|
|
1693
1786
|
version: METRICS_VERSION,
|
|
@@ -1698,12 +1791,12 @@ function loadMetricsFile(filePath, changeName, nowIso) {
|
|
|
1698
1791
|
totals: { ...base.totals, ...(parsed.totals && typeof parsed.totals === 'object' ? parsed.totals : {}) },
|
|
1699
1792
|
phases: parsed.phases && typeof parsed.phases === 'object' && !Array.isArray(parsed.phases) ? parsed.phases : {},
|
|
1700
1793
|
sessions: Array.isArray(parsed.sessions) ? parsed.sessions : [],
|
|
1701
|
-
};
|
|
1794
|
+
});
|
|
1702
1795
|
}
|
|
1703
1796
|
|
|
1704
1797
|
function saveMetricsFile(filePath, metrics) {
|
|
1705
1798
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
1706
|
-
const out = { ...metrics };
|
|
1799
|
+
const out = normalizeMetricsV2({ ...metrics });
|
|
1707
1800
|
delete out.timezone;
|
|
1708
1801
|
writeFileSync(filePath, `${JSON.stringify(out, null, 2)}\n`);
|
|
1709
1802
|
}
|
|
@@ -1750,7 +1843,7 @@ function phaseForRole(role) {
|
|
|
1750
1843
|
function sessionFieldOrSources(session, key) {
|
|
1751
1844
|
if (session[key] != null && session[key] !== '') return numOrNull(session[key]);
|
|
1752
1845
|
let sum = null;
|
|
1753
|
-
for (const src of session.
|
|
1846
|
+
for (const src of session.byModel || []) {
|
|
1754
1847
|
sum = addNullable(sum, numOrNull(src[key]));
|
|
1755
1848
|
}
|
|
1756
1849
|
return sum;
|
|
@@ -1764,8 +1857,14 @@ function lastSessionEndedAt(metrics) {
|
|
|
1764
1857
|
return last;
|
|
1765
1858
|
}
|
|
1766
1859
|
|
|
1767
|
-
function collectWindowStart(metrics, extra) {
|
|
1768
|
-
|
|
1860
|
+
function collectWindowStart(metrics, extra = {}) {
|
|
1861
|
+
const explicit = isoOrNull(extra.explicitStartedAt);
|
|
1862
|
+
const pending = isoOrNull(metrics.pending && metrics.pending.startedAt);
|
|
1863
|
+
const selected = explicit || pending || isoOrNull(extra.lastEndedAt) || isoOrNull(metrics.createdAt);
|
|
1864
|
+
if (!selected) return nowUtcIso();
|
|
1865
|
+
if (!explicit && !pending) return selected;
|
|
1866
|
+
const selectedMs = parseFlexibleIso(selected);
|
|
1867
|
+
return Number.isFinite(selectedMs) ? new Date(selectedMs - LEFTOVER_GRACE_MS).toISOString() : selected;
|
|
1769
1868
|
}
|
|
1770
1869
|
|
|
1771
1870
|
function leftoverGraceEnd(endedAt) {
|
|
@@ -1774,6 +1873,15 @@ function leftoverGraceEnd(endedAt) {
|
|
|
1774
1873
|
return new Date(ms + LEFTOVER_GRACE_MS).toISOString();
|
|
1775
1874
|
}
|
|
1776
1875
|
|
|
1876
|
+
function sessionLeftoverEnd(session, pendingStartedAt) {
|
|
1877
|
+
const pending = isoOrNull(pendingStartedAt);
|
|
1878
|
+
const grace = leftoverGraceEnd(session && session.endedAt);
|
|
1879
|
+
if (session && session.threadId) return pending || grace;
|
|
1880
|
+
if (!pending) return grace;
|
|
1881
|
+
if (!grace) return pending;
|
|
1882
|
+
return earlierTimestamp(pending, grace);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1777
1885
|
function leftoverTimestampInWindow(at, windowStart, leftoverEnd, exclusiveEnd) {
|
|
1778
1886
|
const t = parseFlexibleIso(at);
|
|
1779
1887
|
if (!Number.isFinite(t)) return false;
|
|
@@ -1788,10 +1896,10 @@ function leftoverTimestampInWindow(at, windowStart, leftoverEnd, exclusiveEnd) {
|
|
|
1788
1896
|
return true;
|
|
1789
1897
|
}
|
|
1790
1898
|
|
|
1791
|
-
function uniqueAmpSourceThreadPrefix(
|
|
1899
|
+
function uniqueAmpSourceThreadPrefix(sourceIds) {
|
|
1792
1900
|
const prefixes = [];
|
|
1793
|
-
for (const
|
|
1794
|
-
const id =
|
|
1901
|
+
for (const sourceId of sourceIds || []) {
|
|
1902
|
+
const id = sourceId == null ? '' : String(sourceId);
|
|
1795
1903
|
if (!id.startsWith('T-')) continue;
|
|
1796
1904
|
const prefix = id.split(':')[0];
|
|
1797
1905
|
if (!prefix) continue;
|
|
@@ -1803,14 +1911,14 @@ function uniqueAmpSourceThreadPrefix(sources) {
|
|
|
1803
1911
|
function leftoverAmpThreadId(session) {
|
|
1804
1912
|
if (!session) return null;
|
|
1805
1913
|
if (session.threadId) return String(session.threadId);
|
|
1806
|
-
return uniqueAmpSourceThreadPrefix(session.
|
|
1914
|
+
return uniqueAmpSourceThreadPrefix(session.sourceIds);
|
|
1807
1915
|
}
|
|
1808
1916
|
|
|
1809
1917
|
function sessionAmpThreadKeys(session) {
|
|
1810
1918
|
const keys = new Set();
|
|
1811
1919
|
if (session && session.threadId) keys.add(String(session.threadId));
|
|
1812
|
-
for (const
|
|
1813
|
-
const id =
|
|
1920
|
+
for (const sourceId of (session && session.sourceIds) || []) {
|
|
1921
|
+
const id = sourceId == null ? '' : String(sourceId);
|
|
1814
1922
|
if (!id.startsWith('T-')) continue;
|
|
1815
1923
|
const prefix = id.split(':')[0];
|
|
1816
1924
|
if (prefix) keys.add(prefix);
|
|
@@ -1848,11 +1956,11 @@ function sessionSpendIsFrozen(session) {
|
|
|
1848
1956
|
return session.spendSource === 'self-report' && reportedHasSpendNumbers(session);
|
|
1849
1957
|
}
|
|
1850
1958
|
|
|
1851
|
-
function
|
|
1852
|
-
const out =
|
|
1959
|
+
function existingSourceTotals(metrics) {
|
|
1960
|
+
const out = {};
|
|
1853
1961
|
for (const session of metrics.sessions || []) {
|
|
1854
|
-
for (const
|
|
1855
|
-
|
|
1962
|
+
for (const [id, total] of Object.entries(session.sourceTotals || {})) {
|
|
1963
|
+
out[id] = Math.max(numOrNull(out[id]) ?? 0, numOrNull(total) ?? 0);
|
|
1856
1964
|
}
|
|
1857
1965
|
}
|
|
1858
1966
|
return out;
|
|
@@ -1861,13 +1969,15 @@ function existingSourceRecords(metrics) {
|
|
|
1861
1969
|
function existingSourceIdSet(metrics) {
|
|
1862
1970
|
const ids = new Set();
|
|
1863
1971
|
for (const session of metrics.sessions || []) {
|
|
1864
|
-
for (const
|
|
1865
|
-
if (src && src.id != null && src.id !== '') ids.add(String(src.id));
|
|
1866
|
-
}
|
|
1972
|
+
for (const id of session.sourceIds || []) ids.add(String(id));
|
|
1867
1973
|
}
|
|
1868
1974
|
return ids;
|
|
1869
1975
|
}
|
|
1870
1976
|
|
|
1977
|
+
function existingSessionThreadIds(metrics) {
|
|
1978
|
+
return [...new Set((metrics.sessions || []).map((session) => session && session.threadId).filter(Boolean).map(String))];
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1871
1981
|
function lastNonArchiverSession(sessions) {
|
|
1872
1982
|
const list = Array.isArray(sessions) ? sessions : [];
|
|
1873
1983
|
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
@@ -1903,6 +2013,63 @@ function uniqueSourceModels(sources) {
|
|
|
1903
2013
|
return seen;
|
|
1904
2014
|
}
|
|
1905
2015
|
|
|
2016
|
+
function sessionTotalsFromModels(byModel) {
|
|
2017
|
+
const totals = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
2018
|
+
for (const row of byModel || []) {
|
|
2019
|
+
for (const key of Object.keys(totals)) totals[key] = addNullable(totals[key], numOrNull(row[key]));
|
|
2020
|
+
}
|
|
2021
|
+
totals.costUsdEstimated = roundUsd4(totals.costUsdEstimated);
|
|
2022
|
+
return totals;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function mergeModelRows(current, incoming) {
|
|
2026
|
+
const merged = new Map();
|
|
2027
|
+
for (const raw of [...(current || []), ...(incoming || [])]) {
|
|
2028
|
+
if (!raw) continue;
|
|
2029
|
+
const model = raw.model == null || raw.model === '' ? null : String(raw.model);
|
|
2030
|
+
const key = `${raw.platform || ''}::${model || ''}`;
|
|
2031
|
+
const row = merged.get(key) || {
|
|
2032
|
+
model,
|
|
2033
|
+
platform: raw.platform || null,
|
|
2034
|
+
inputTokens: null,
|
|
2035
|
+
outputTokens: null,
|
|
2036
|
+
totalTokens: null,
|
|
2037
|
+
costUsd: null,
|
|
2038
|
+
costUsdEstimated: null,
|
|
2039
|
+
};
|
|
2040
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd']) {
|
|
2041
|
+
row[field] = addNullable(row[field], numOrNull(raw[field]));
|
|
2042
|
+
}
|
|
2043
|
+
row.costUsdEstimated = roundUsd4(addNullable(row.costUsdEstimated, numOrNull(raw.costUsdEstimated)));
|
|
2044
|
+
if (raw.costSource) row.costSource = raw.costSource;
|
|
2045
|
+
merged.set(key, row);
|
|
2046
|
+
}
|
|
2047
|
+
return [...merged.values()];
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function compactCollectedDelta(session, sources) {
|
|
2051
|
+
const adjusted = [];
|
|
2052
|
+
for (const source of sources || []) {
|
|
2053
|
+
if (!source || source.id == null) continue;
|
|
2054
|
+
const previous = numOrNull(session.sourceTotals && session.sourceTotals[source.id]);
|
|
2055
|
+
const next = numOrNull(source.totalTokens);
|
|
2056
|
+
if (previous == null || next == null) {
|
|
2057
|
+
adjusted.push(source);
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
const delta = Math.max(0, next - previous);
|
|
2061
|
+
if (delta === 0) continue;
|
|
2062
|
+
const ratio = next > 0 ? delta / next : 0;
|
|
2063
|
+
const copy = { ...source };
|
|
2064
|
+
for (const field of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated']) {
|
|
2065
|
+
const value = numOrNull(copy[field]);
|
|
2066
|
+
if (value != null) copy[field] = field.includes('Usd') ? roundUsd4(value * ratio) : Math.round(value * ratio);
|
|
2067
|
+
}
|
|
2068
|
+
adjusted.push(copy);
|
|
2069
|
+
}
|
|
2070
|
+
return adjusted;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1906
2073
|
function rankSources(sources) {
|
|
1907
2074
|
return [...sources].sort((a, b) => {
|
|
1908
2075
|
const ta = a.totalTokens ?? 0;
|
|
@@ -1974,7 +2141,15 @@ function ampThreadTotals(threads) {
|
|
|
1974
2141
|
if (row && row.model && !models.includes(row.model)) models.push(row.model);
|
|
1975
2142
|
}
|
|
1976
2143
|
}
|
|
1977
|
-
|
|
2144
|
+
let inputTokens = null;
|
|
2145
|
+
let outputTokens = null;
|
|
2146
|
+
let totalTokens = null;
|
|
2147
|
+
for (const thread of list) {
|
|
2148
|
+
inputTokens = addNullable(inputTokens, numOrNull(thread && thread.inputTokens));
|
|
2149
|
+
outputTokens = addNullable(outputTokens, numOrNull(thread && thread.outputTokens));
|
|
2150
|
+
totalTokens = addNullable(totalTokens, numOrNull(thread && thread.totalTokens));
|
|
2151
|
+
}
|
|
2152
|
+
return { costUsd, agentMode, models, inputTokens, outputTokens, totalTokens };
|
|
1978
2153
|
}
|
|
1979
2154
|
|
|
1980
2155
|
function resolveSessionSpend(opts, reported, sources, extra = {}) {
|
|
@@ -1988,9 +2163,9 @@ function resolveSessionSpend(opts, reported, sources, extra = {}) {
|
|
|
1988
2163
|
const flagTotal = numOrNull(flags.totalTokens);
|
|
1989
2164
|
const flagCost = numOrNull(flags.costUsd);
|
|
1990
2165
|
const flagCredits = numOrNull(flags.ampCredits);
|
|
1991
|
-
const inputTokens = firstNonNull(flagInput, self.inputTokens, fromSources.inputTokens);
|
|
1992
|
-
const outputTokens = firstNonNull(flagOutput, self.outputTokens, fromSources.outputTokens);
|
|
1993
|
-
let totalTokens = firstNonNull(flagTotal, self.totalTokens, fromSources.totalTokens);
|
|
2166
|
+
const inputTokens = firstNonNull(flagInput, self.inputTokens, fromAmp.inputTokens, fromSources.inputTokens);
|
|
2167
|
+
const outputTokens = firstNonNull(flagOutput, self.outputTokens, fromAmp.outputTokens, fromSources.outputTokens);
|
|
2168
|
+
let totalTokens = firstNonNull(flagTotal, self.totalTokens, fromAmp.totalTokens, fromSources.totalTokens);
|
|
1994
2169
|
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
1995
2170
|
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
1996
2171
|
}
|
|
@@ -2050,7 +2225,7 @@ function runCollectSpend(metrics, windowStart, windowEnd, extra = {}) {
|
|
|
2050
2225
|
windowStart,
|
|
2051
2226
|
windowEnd,
|
|
2052
2227
|
existingSourceIds: existingSourceIdSet(metrics),
|
|
2053
|
-
|
|
2228
|
+
existingSourceTotals: existingSourceTotals(metrics),
|
|
2054
2229
|
env: extra.env || process.env,
|
|
2055
2230
|
homedir: extra.homedir || process.env.HOME,
|
|
2056
2231
|
platforms: extra.platforms,
|
|
@@ -2061,9 +2236,12 @@ function runCollectSpend(metrics, windowStart, windowEnd, extra = {}) {
|
|
|
2061
2236
|
usageAmpThread: extra.usageAmpThread,
|
|
2062
2237
|
listRecentAmpThreads: extra.listRecentAmpThreads,
|
|
2063
2238
|
listAmpThreads: extra.listAmpThreads,
|
|
2239
|
+
collectAll: extra.collectAll === true,
|
|
2240
|
+
existingThreadIds: existingSessionThreadIds(metrics),
|
|
2241
|
+
rebillThreadId: extra.rebillThreadId,
|
|
2064
2242
|
});
|
|
2065
2243
|
} catch {
|
|
2066
|
-
return { sources: [], byPlatform: defaultSpendByPlatform(), byModel: [], notes: [] };
|
|
2244
|
+
return { sources: [], ids: [], totals: {}, byPlatform: defaultSpendByPlatform(), byModel: [], notes: [] };
|
|
2067
2245
|
}
|
|
2068
2246
|
}
|
|
2069
2247
|
|
|
@@ -2078,7 +2256,7 @@ function leftoverCollectPlatforms(session, collectAll, ampThreadId) {
|
|
|
2078
2256
|
}
|
|
2079
2257
|
|
|
2080
2258
|
function resyncLeftoverSessionSpend(session, priorCost, priorSource) {
|
|
2081
|
-
const fromSources =
|
|
2259
|
+
const fromSources = sessionTotalsFromModels(session.byModel || []);
|
|
2082
2260
|
session.inputTokens = fromSources.inputTokens;
|
|
2083
2261
|
session.outputTokens = fromSources.outputTokens;
|
|
2084
2262
|
let totalTokens = fromSources.totalTokens;
|
|
@@ -2116,6 +2294,7 @@ function attachLeftoverSources(metrics, session, leftoverEnd, extra = {}) {
|
|
|
2116
2294
|
env: extra.env,
|
|
2117
2295
|
cwd: extra.cwd,
|
|
2118
2296
|
homedir: extra.homedir,
|
|
2297
|
+
rebillThreadId: ampThreadId,
|
|
2119
2298
|
});
|
|
2120
2299
|
let incoming = (collected.sources || []).filter((src) => leftoverTimestampInWindow(
|
|
2121
2300
|
src.at,
|
|
@@ -2131,32 +2310,43 @@ function attachLeftoverSources(metrics, session, leftoverEnd, extra = {}) {
|
|
|
2131
2310
|
});
|
|
2132
2311
|
}
|
|
2133
2312
|
if (!incoming.length) return 0;
|
|
2134
|
-
const
|
|
2313
|
+
const beforeIds = new Set(session.sourceIds || []);
|
|
2135
2314
|
if (sessionSpendIsFrozen(session)) {
|
|
2136
|
-
|
|
2137
|
-
|
|
2315
|
+
const delta = compactCollectedDelta(session, incoming);
|
|
2316
|
+
session.sourceIds = [...new Set([...(session.sourceIds || []), ...incoming.map((source) => String(source.id))])];
|
|
2317
|
+
session.sourceTotals = { ...(session.sourceTotals || {}) };
|
|
2318
|
+
for (const source of incoming) session.sourceTotals[source.id] = Math.max(numOrNull(session.sourceTotals[source.id]) ?? 0, numOrNull(source.totalTokens) ?? 0);
|
|
2319
|
+
session.byModel = mergeModelRows(session.byModel, compactModelRows(delta));
|
|
2320
|
+
const uniqueModels = uniqueSourceModels(session.byModel);
|
|
2138
2321
|
if (uniqueModels.length > 1) session.models = uniqueModels;
|
|
2139
2322
|
} else {
|
|
2140
2323
|
const priorCost = numOrNull(session.costUsd);
|
|
2141
2324
|
const priorSource = session.spendSource;
|
|
2142
|
-
applyCollectedSessionFields(session,
|
|
2325
|
+
applyCollectedSessionFields(session, incoming, session.model, {}, {
|
|
2143
2326
|
model: session.model,
|
|
2144
2327
|
platform: session.platform,
|
|
2145
2328
|
}, { ampThreads: Array.isArray(extra.ampThreads) ? extra.ampThreads : collected.ampThreads });
|
|
2146
2329
|
resyncLeftoverSessionSpend(session, priorCost, priorSource);
|
|
2147
2330
|
}
|
|
2148
|
-
return incoming.length;
|
|
2331
|
+
return incoming.filter((source) => !beforeIds.has(String(source.id))).length;
|
|
2149
2332
|
}
|
|
2150
2333
|
|
|
2151
2334
|
function applyCollectedSessionFields(session, sources, resolvedModel, opts, reported, extra = {}) {
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2335
|
+
const incoming = sources || [];
|
|
2336
|
+
const delta = compactCollectedDelta(session, incoming);
|
|
2337
|
+
session.sourceIds = [...new Set([...(session.sourceIds || []), ...incoming.map((source) => String(source.id))])];
|
|
2338
|
+
session.sourceTotals = { ...(session.sourceTotals || {}) };
|
|
2339
|
+
for (const source of incoming) {
|
|
2340
|
+
session.sourceTotals[source.id] = Math.max(numOrNull(session.sourceTotals[source.id]) ?? 0, numOrNull(source.totalTokens) ?? 0);
|
|
2341
|
+
}
|
|
2342
|
+
session.byModel = mergeModelRows(session.byModel, compactModelRows(delta));
|
|
2343
|
+
const uniqueModels = uniqueSourceModels(session.byModel);
|
|
2154
2344
|
const fromAmp = ampThreadTotals(extra.ampThreads);
|
|
2155
2345
|
for (const model of fromAmp.models) {
|
|
2156
2346
|
if (model && !uniqueModels.includes(model)) uniqueModels.push(model);
|
|
2157
2347
|
}
|
|
2158
2348
|
if (uniqueModels.length > 1) session.models = uniqueModels;
|
|
2159
|
-
const sourceModel = primaryModelFromSources(
|
|
2349
|
+
const sourceModel = primaryModelFromSources(incoming);
|
|
2160
2350
|
if (sourceModel) {
|
|
2161
2351
|
session.model = sourceModel;
|
|
2162
2352
|
} else {
|
|
@@ -2164,8 +2354,8 @@ function applyCollectedSessionFields(session, sources, resolvedModel, opts, repo
|
|
|
2164
2354
|
|| fromAmp.models[0]
|
|
2165
2355
|
|| null;
|
|
2166
2356
|
}
|
|
2167
|
-
if (!session.platform) session.platform = primaryPlatformFromSources(
|
|
2168
|
-
const spend = resolveSessionSpend(opts, reported,
|
|
2357
|
+
if (!session.platform) session.platform = primaryPlatformFromSources(incoming) || null;
|
|
2358
|
+
const spend = resolveSessionSpend(opts, reported, incoming, extra);
|
|
2169
2359
|
session.inputTokens = spend.inputTokens;
|
|
2170
2360
|
session.outputTokens = spend.outputTokens;
|
|
2171
2361
|
session.totalTokens = spend.totalTokens;
|
|
@@ -2178,10 +2368,39 @@ function applyCollectedSessionFields(session, sources, resolvedModel, opts, repo
|
|
|
2178
2368
|
if (usageModels.length || Array.isArray(session.usageModels)) {
|
|
2179
2369
|
session.usageModels = usageModels;
|
|
2180
2370
|
}
|
|
2371
|
+
if (session.platform === 'amp' && usageModels.length) {
|
|
2372
|
+
session.byModel = usageModels.map((row) => ({
|
|
2373
|
+
model: row.model,
|
|
2374
|
+
platform: 'amp',
|
|
2375
|
+
inputTokens: numOrNull(row.inputTokens),
|
|
2376
|
+
outputTokens: numOrNull(row.outputTokens),
|
|
2377
|
+
totalTokens: numOrNull(row.totalTokens) ?? ((numOrNull(row.inputTokens) ?? 0) + (numOrNull(row.outputTokens) ?? 0)),
|
|
2378
|
+
costUsd: numOrNull(row.costUsd),
|
|
2379
|
+
costUsdEstimated: null,
|
|
2380
|
+
...(numOrNull(row.costUsd) != null ? { costSource: 'amp-usage' } : {}),
|
|
2381
|
+
}));
|
|
2382
|
+
}
|
|
2383
|
+
if (session.costUsd == null) {
|
|
2384
|
+
for (const row of session.byModel) {
|
|
2385
|
+
if (row.costUsd != null) continue;
|
|
2386
|
+
const described = describeClaudeCostEstimate({
|
|
2387
|
+
model: String(row.model || '').replace(/^Claude\s+/i, 'claude-').replaceAll(' ', '-').toLowerCase(),
|
|
2388
|
+
inputTokens: row.inputTokens,
|
|
2389
|
+
outputTokens: row.outputTokens,
|
|
2390
|
+
});
|
|
2391
|
+
if (described) {
|
|
2392
|
+
row.costUsdEstimated = described.usd;
|
|
2393
|
+
row.costSource = described.costSource;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
session.costUsdEstimated = sessionTotalsFromModels(session.byModel).costUsdEstimated;
|
|
2397
|
+
} else {
|
|
2398
|
+
session.costUsdEstimated = null;
|
|
2399
|
+
}
|
|
2181
2400
|
}
|
|
2182
2401
|
|
|
2183
2402
|
function sessionTotalsLookOverridden(session) {
|
|
2184
|
-
const fromSources =
|
|
2403
|
+
const fromSources = sessionTotalsFromModels(session.byModel || []);
|
|
2185
2404
|
return ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'].some((key) => {
|
|
2186
2405
|
const sessionVal = numOrNull(session[key]);
|
|
2187
2406
|
const sourceVal = numOrNull(fromSources[key]);
|
|
@@ -2212,16 +2431,16 @@ function metricsBackfillFile(filePath, changeName) {
|
|
|
2212
2431
|
});
|
|
2213
2432
|
const incoming = collected.sources || [];
|
|
2214
2433
|
if (!incoming.length) return { filePath, added: 0 };
|
|
2215
|
-
const merged = [...(last.sources || []), ...incoming];
|
|
2216
2434
|
if (sessionSpendIsFrozen(last)) {
|
|
2217
|
-
last.
|
|
2218
|
-
const uniqueModels = uniqueSourceModels(merged);
|
|
2219
|
-
if (uniqueModels.length > 1) last.models = uniqueModels;
|
|
2435
|
+
applyCollectedSessionFields(last, incoming, last.model, {}, last, { ampThreads: collected.ampThreads });
|
|
2220
2436
|
} else {
|
|
2221
|
-
|
|
2437
|
+
const priorCost = numOrNull(last.costUsd);
|
|
2438
|
+
const priorSource = last.spendSource;
|
|
2439
|
+
applyCollectedSessionFields(last, incoming, last.model, {}, {
|
|
2222
2440
|
model: last.model,
|
|
2223
2441
|
platform: last.platform,
|
|
2224
2442
|
}, { ampThreads: collected.ampThreads });
|
|
2443
|
+
resyncLeftoverSessionSpend(last, priorCost, priorSource);
|
|
2225
2444
|
}
|
|
2226
2445
|
metrics.updatedAt = nowIso;
|
|
2227
2446
|
recomputeMetricsAggregates(metrics);
|
|
@@ -2286,35 +2505,19 @@ function recomputeSpendMaps(metrics) {
|
|
|
2286
2505
|
byModel.set(key, row);
|
|
2287
2506
|
}
|
|
2288
2507
|
|
|
2289
|
-
function addSourceRow(src) {
|
|
2290
|
-
const nums = spendTuple(src);
|
|
2291
|
-
if (src.platform && byPlatform[src.platform]) {
|
|
2292
|
-
addSpendNums(byPlatform[src.platform], nums);
|
|
2293
|
-
const label = adapterSourceName(src.platform, src.via);
|
|
2294
|
-
if (label) byPlatform[src.platform].source = label;
|
|
2295
|
-
}
|
|
2296
|
-
addModelRow(src.model, src.platform, nums);
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
2508
|
for (const session of metrics.sessions || []) {
|
|
2300
|
-
const sources = session.sources || [];
|
|
2301
|
-
if (sources.length > 0) {
|
|
2302
|
-
for (const src of sources) addSourceRow(src);
|
|
2303
|
-
let sourceCost = null;
|
|
2304
|
-
for (const src of sources) sourceCost = addNullable(sourceCost, numOrNull(src.costUsd));
|
|
2305
|
-
if (sourceCost == null && session.platform && byPlatform[session.platform]) {
|
|
2306
|
-
byPlatform[session.platform].costUsd = addNullable(
|
|
2307
|
-
byPlatform[session.platform].costUsd,
|
|
2308
|
-
numOrNull(session.costUsd),
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
2311
|
-
continue;
|
|
2312
|
-
}
|
|
2313
2509
|
const sessionNums = spendTuple(session);
|
|
2314
2510
|
if (session.platform && byPlatform[session.platform]) {
|
|
2315
2511
|
addSpendNums(byPlatform[session.platform], sessionNums);
|
|
2512
|
+
if ((session.sourceIds || []).length) {
|
|
2513
|
+
byPlatform[session.platform].source = adapterSourceName(session.platform, session.platform === 'amp' ? 'amp-cli' : null);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
if ((session.byModel || []).length) {
|
|
2517
|
+
for (const row of session.byModel) addModelRow(row.model, row.platform || session.platform, spendTuple(row));
|
|
2518
|
+
} else {
|
|
2519
|
+
addModelRow(session.model, session.platform, sessionNums);
|
|
2316
2520
|
}
|
|
2317
|
-
addModelRow(session.model, session.platform, sessionNums);
|
|
2318
2521
|
}
|
|
2319
2522
|
for (const key of Object.keys(byPlatform)) {
|
|
2320
2523
|
byPlatform[key].costUsdEstimated = roundUsd4(byPlatform[key].costUsdEstimated);
|
|
@@ -2327,7 +2530,7 @@ function recomputeSpendMaps(metrics) {
|
|
|
2327
2530
|
}
|
|
2328
2531
|
|
|
2329
2532
|
function recomputeMetricsAggregates(metrics) {
|
|
2330
|
-
|
|
2533
|
+
normalizeMetricsV2(metrics);
|
|
2331
2534
|
const phases = {};
|
|
2332
2535
|
const totals = { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 };
|
|
2333
2536
|
const spend = emptySpendTotals();
|
|
@@ -2367,22 +2570,8 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2367
2570
|
? session.endedAt
|
|
2368
2571
|
: laterTimestamp(phase.endedAt, session.endedAt);
|
|
2369
2572
|
}
|
|
2370
|
-
const sources = session.sources || [];
|
|
2371
|
-
let sourceCost = null;
|
|
2372
|
-
for (const src of sources) sourceCost = addNullable(sourceCost, numOrNull(src.costUsd));
|
|
2373
2573
|
for (const spendKey of METRICS_SPEND_KEYS) {
|
|
2374
|
-
|
|
2375
|
-
if (sources.length > 0) {
|
|
2376
|
-
if (spendKey === 'costUsd') {
|
|
2377
|
-
value = sourceCost == null ? numOrNull(session.costUsd) : sourceCost;
|
|
2378
|
-
} else {
|
|
2379
|
-
for (const src of sources) {
|
|
2380
|
-
value = addNullable(value, numOrNull(src[spendKey]));
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
} else {
|
|
2384
|
-
value = sessionFieldOrSources(session, spendKey);
|
|
2385
|
-
}
|
|
2574
|
+
const value = sessionFieldOrSources(session, spendKey);
|
|
2386
2575
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
2387
2576
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
2388
2577
|
}
|
|
@@ -2435,19 +2624,29 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2435
2624
|
const nowIso = nowUtcIso();
|
|
2436
2625
|
const metrics = loadMetricsFile(filePath, fields.changeName, nowIso);
|
|
2437
2626
|
const lastClosed = (metrics.sessions || [])[(metrics.sessions || []).length - 1];
|
|
2438
|
-
const leftoverEnd = (metrics.pending && metrics.pending.startedAt)
|
|
2439
|
-
? metrics.pending.startedAt
|
|
2440
|
-
: leftoverGraceEnd(lastClosed && lastClosed.endedAt);
|
|
2627
|
+
const leftoverEnd = sessionLeftoverEnd(lastClosed, metrics.pending && metrics.pending.startedAt);
|
|
2441
2628
|
attachLeftoverSources(metrics, lastClosed, leftoverEnd, {
|
|
2442
2629
|
exclusiveEnd: Boolean(metrics.pending && metrics.pending.startedAt),
|
|
2443
2630
|
collect: opts.collect === true,
|
|
2444
2631
|
});
|
|
2445
|
-
|
|
2632
|
+
const explicitStartedAt = isoOrNull(opts.startedAt);
|
|
2633
|
+
const pendingStartedAt = isoOrNull(metrics.pending && metrics.pending.startedAt);
|
|
2634
|
+
const startedAt = explicitStartedAt || pendingStartedAt || isoOrNull(lastClosed && lastClosed.endedAt) || isoOrNull(metrics.createdAt) || nowIso;
|
|
2635
|
+
if (!explicitStartedAt && !pendingStartedAt) console.error('Warning: persist without restore; collecting from the last closed session boundary.');
|
|
2446
2636
|
const reported = opts.reported || fields.metrics || emptyMetricsFields();
|
|
2447
2637
|
const resolvedModel = opts.model === undefined ? resolveModel(opts, process.env, reported) : opts.model;
|
|
2448
|
-
const windowStart = collectWindowStart(metrics, {
|
|
2638
|
+
const windowStart = collectWindowStart(metrics, {
|
|
2639
|
+
explicitStartedAt,
|
|
2640
|
+
lastEndedAt: lastClosed && lastClosed.endedAt,
|
|
2641
|
+
});
|
|
2449
2642
|
const pending = metrics.pending || {};
|
|
2450
2643
|
const platform = opts.platform || pending.platform || null;
|
|
2644
|
+
const cursorThreadId = platform === 'cursor'
|
|
2645
|
+
? (pending.threadId || String(process.env.CURSOR_CONVERSATION_ID || '').trim() || null)
|
|
2646
|
+
: null;
|
|
2647
|
+
const ampThreadId = platform === 'amp'
|
|
2648
|
+
? (opts.ampThreadId || pending.threadId || ampThreadIdFromEnv(process.env) || null)
|
|
2649
|
+
: null;
|
|
2451
2650
|
const collectAll = opts.collect === true;
|
|
2452
2651
|
const platforms = collectAll ? undefined : (platform ? [platform] : []);
|
|
2453
2652
|
const shouldCollect = collectAll || (Array.isArray(platforms) && platforms.length > 0);
|
|
@@ -2455,9 +2654,11 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2455
2654
|
const collected = shouldCollect
|
|
2456
2655
|
? runCollectSpend(metrics, windowStart, endedAt, {
|
|
2457
2656
|
platforms,
|
|
2458
|
-
ampThreadId
|
|
2657
|
+
ampThreadId,
|
|
2459
2658
|
ampCli: collectAll || platform === 'amp',
|
|
2460
|
-
cursorConversationId:
|
|
2659
|
+
cursorConversationId: cursorThreadId || undefined,
|
|
2660
|
+
listRecentAmpThreads: false,
|
|
2661
|
+
collectAll,
|
|
2461
2662
|
})
|
|
2462
2663
|
: { sources: [] };
|
|
2463
2664
|
const startedMs = parseFlexibleIso(startedAt);
|
|
@@ -2476,9 +2677,11 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2476
2677
|
agentId: fields.agentId || 'none',
|
|
2477
2678
|
model: resolvedModel || null,
|
|
2478
2679
|
platform: opts.platform || pending.platform || null,
|
|
2479
|
-
threadId:
|
|
2680
|
+
threadId: cursorThreadId || ampThreadId || pending.threadId || null,
|
|
2480
2681
|
tasks: fields.tasks || null,
|
|
2481
|
-
|
|
2682
|
+
sourceIds: [],
|
|
2683
|
+
sourceTotals: {},
|
|
2684
|
+
byModel: [],
|
|
2482
2685
|
inputTokens: null,
|
|
2483
2686
|
outputTokens: null,
|
|
2484
2687
|
totalTokens: null,
|
|
@@ -2488,22 +2691,6 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2488
2691
|
spendSource: 'unreported',
|
|
2489
2692
|
};
|
|
2490
2693
|
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported, collected);
|
|
2491
|
-
if (!session.startedAt) {
|
|
2492
|
-
let earliest = null;
|
|
2493
|
-
for (const src of session.sources || []) {
|
|
2494
|
-
const at = isoOrNull(src.at);
|
|
2495
|
-
if (!at) continue;
|
|
2496
|
-
earliest = earliest == null ? at : earlierTimestamp(earliest, at);
|
|
2497
|
-
}
|
|
2498
|
-
if (earliest) {
|
|
2499
|
-
session.startedAt = earliest;
|
|
2500
|
-
const fromMs = parseFlexibleIso(session.startedAt);
|
|
2501
|
-
const toMs = parseFlexibleIso(session.endedAt);
|
|
2502
|
-
session.durationMs = Number.isFinite(fromMs) && Number.isFinite(toMs)
|
|
2503
|
-
? Math.max(0, toMs - fromMs)
|
|
2504
|
-
: null;
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
2694
|
metrics.sessions.push(session);
|
|
2508
2695
|
metrics.pending = null;
|
|
2509
2696
|
metrics.updatedAt = nowIso;
|
|
@@ -2534,12 +2721,15 @@ function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
|
2534
2721
|
const platforms = collectAll ? undefined : (platform ? [platform] : []);
|
|
2535
2722
|
const shouldCollect = collectAll || (Array.isArray(platforms) && platforms.length > 0);
|
|
2536
2723
|
const ampThreadId = opts.ampThreadId || pending.threadId || ampThreadIdFromEnv(process.env) || null;
|
|
2724
|
+
const archiveWindowStart = collectWindowStart(metrics, {});
|
|
2537
2725
|
const collected = shouldCollect
|
|
2538
|
-
? runCollectSpend(metrics,
|
|
2726
|
+
? runCollectSpend(metrics, archiveWindowStart, endedAt, {
|
|
2539
2727
|
platforms,
|
|
2540
2728
|
ampThreadId,
|
|
2541
2729
|
ampCli: collectAll || platform === 'amp',
|
|
2542
2730
|
cursorConversationId: platform === 'cursor' ? (pending.threadId || ampThreadId) : undefined,
|
|
2731
|
+
listRecentAmpThreads: false,
|
|
2732
|
+
collectAll,
|
|
2543
2733
|
})
|
|
2544
2734
|
: { sources: [] };
|
|
2545
2735
|
const reported = dropStaleArchiveSelfReport(opts.reported, metrics.sessions);
|
|
@@ -2556,7 +2746,9 @@ function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
|
2556
2746
|
platform,
|
|
2557
2747
|
threadId: ampThreadId || null,
|
|
2558
2748
|
tasks: opts.tasks || null,
|
|
2559
|
-
|
|
2749
|
+
sourceIds: [],
|
|
2750
|
+
sourceTotals: {},
|
|
2751
|
+
byModel: [],
|
|
2560
2752
|
inputTokens: null,
|
|
2561
2753
|
outputTokens: null,
|
|
2562
2754
|
totalTokens: null,
|
|
@@ -2595,8 +2787,9 @@ function metricsPrepareArchiveStart(changeRoot, changeName, client = {}, extra =
|
|
|
2595
2787
|
clientSource: client.source || null,
|
|
2596
2788
|
};
|
|
2597
2789
|
}
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2790
|
+
const previous = lastNonArchiverSession(metrics.sessions);
|
|
2791
|
+
const leftoverEnd = sessionLeftoverEnd(previous, metrics.pending && metrics.pending.startedAt);
|
|
2792
|
+
attachLeftoverSources(metrics, previous, leftoverEnd, {
|
|
2600
2793
|
exclusiveEnd: true,
|
|
2601
2794
|
collect: extra.collect === true,
|
|
2602
2795
|
});
|
|
@@ -4485,6 +4678,8 @@ program
|
|
|
4485
4678
|
.command('metrics [change-name]')
|
|
4486
4679
|
.description('Show recorded session metrics for a change: time per phase, tokens, cost, roles, and models')
|
|
4487
4680
|
.option('--json', 'Print raw metrics.json', false)
|
|
4681
|
+
.option('--summary-json', 'Print compact aggregate JSON for dashboards', false)
|
|
4682
|
+
.option('--migrate', 'Rewrite metrics.json using the current schema without recomputing numbers', false)
|
|
4488
4683
|
.option('--collect', 'Backfill the last session from local spend adapters without adding a new session', false)
|
|
4489
4684
|
.action((changeName, opts) => {
|
|
4490
4685
|
const projectDir = process.cwd();
|
|
@@ -4541,6 +4736,29 @@ program
|
|
|
4541
4736
|
}
|
|
4542
4737
|
|
|
4543
4738
|
const metrics = loadMetricsFile(filePath, name, nowUtcIso());
|
|
4739
|
+
if (opts.migrate) {
|
|
4740
|
+
saveMetricsFile(filePath, metrics);
|
|
4741
|
+
if (!opts.json && !opts.summaryJson) {
|
|
4742
|
+
log.ok(`migrated: ${filePath.replace(`${projectDir}/`, '')}`);
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
if (opts.summaryJson) {
|
|
4747
|
+
recomputeMetricsAggregates(metrics);
|
|
4748
|
+
const summary = {
|
|
4749
|
+
version: metrics.version,
|
|
4750
|
+
change: metrics.change,
|
|
4751
|
+
createdAt: metrics.createdAt,
|
|
4752
|
+
archivedAt: metrics.archivedAt,
|
|
4753
|
+
totals: metrics.totals,
|
|
4754
|
+
phases: metrics.phases,
|
|
4755
|
+
spend: metrics.spend,
|
|
4756
|
+
spendByPlatform: metrics.spendByPlatform,
|
|
4757
|
+
spendByModel: metrics.spendByModel,
|
|
4758
|
+
};
|
|
4759
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4544
4762
|
if (opts.json) {
|
|
4545
4763
|
process.stdout.write(`${JSON.stringify(metrics, null, 2)}\n`);
|
|
4546
4764
|
return;
|
|
@@ -4574,4 +4792,6 @@ export {
|
|
|
4574
4792
|
sessionSpendIsFrozen,
|
|
4575
4793
|
attachLeftoverSources,
|
|
4576
4794
|
runCollectSpend,
|
|
4795
|
+
normalizeMetricsV2,
|
|
4796
|
+
firstSpawnName,
|
|
4577
4797
|
};
|