agent-orchestrator-kit 0.7.0 → 0.9.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 +24 -0
- package/README.md +18 -7
- package/bin/agent-orchestrator.js +255 -65
- package/bin/amp-usage.js +63 -0
- package/bin/cursor-cost-estimate.js +50 -0
- package/bin/metrics-time.js +99 -0
- package/bin/session-client.js +187 -0
- package/bin/spend-collect.js +253 -48
- package/package.json +1 -1
- package/templates/.agents/commands/opsx-archive.md +1 -1
- package/templates/.agents/rules/session-handoff.mdc +2 -2
- package/templates/.agents/subagents/spec-archiver.md +1 -1
|
@@ -6,6 +6,15 @@ import { join, dirname, basename, resolve } from 'path';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
8
|
import { collectSpend } from './spend-collect.js';
|
|
9
|
+
import { resolveRestoreClient, ampThreadIdFromEnv } from './session-client.js';
|
|
10
|
+
import {
|
|
11
|
+
earlierTimestamp,
|
|
12
|
+
formatKyivDisplay,
|
|
13
|
+
isoOrNull,
|
|
14
|
+
laterTimestamp,
|
|
15
|
+
nowUtcIso,
|
|
16
|
+
parseFlexibleIso,
|
|
17
|
+
} from './metrics-time.js';
|
|
9
18
|
|
|
10
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
20
|
const KIT_ROOT = join(__dirname, '..');
|
|
@@ -484,7 +493,7 @@ function printSpendHealth(projectDir) {
|
|
|
484
493
|
? String(process.env.AMP_DATA_DIR).trim()
|
|
485
494
|
: join(home, '.local', 'share', 'amp');
|
|
486
495
|
const ampOk = existsSync(join(ampDir, 'threads'));
|
|
487
|
-
console.log(` amp ${ampOk ? 'ok (threads found)' : 'no local Amp data'}`);
|
|
496
|
+
console.log(` amp ${ampOk ? 'ok (threads found)' : 'no local Amp data'}; locked client + amp threads export`);
|
|
488
497
|
console.log('');
|
|
489
498
|
}
|
|
490
499
|
|
|
@@ -1122,12 +1131,22 @@ function firstNonNull(...values) {
|
|
|
1122
1131
|
return null;
|
|
1123
1132
|
}
|
|
1124
1133
|
|
|
1134
|
+
function isPlaceholderModel(value) {
|
|
1135
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
1136
|
+
return !normalized || ['unknown', 'none', 'n/a', '-', '—', 'null', 'amp-default'].includes(normalized);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1125
1139
|
function resolveModel(opts, env, reported) {
|
|
1126
|
-
const
|
|
1140
|
+
const pick = (value) => {
|
|
1141
|
+
const text = value == null ? '' : String(value).trim();
|
|
1142
|
+
if (!text || isPlaceholderModel(text)) return '';
|
|
1143
|
+
return text;
|
|
1144
|
+
};
|
|
1145
|
+
const flag = pick(opts && opts.model);
|
|
1127
1146
|
if (flag) return flag;
|
|
1128
|
-
const fromReport = reported && reported.model
|
|
1147
|
+
const fromReport = pick(reported && reported.model);
|
|
1129
1148
|
if (fromReport) return fromReport;
|
|
1130
|
-
const fromEnv = env && env.AOK_MODEL
|
|
1149
|
+
const fromEnv = pick(env && env.AOK_MODEL);
|
|
1131
1150
|
if (fromEnv) return fromEnv;
|
|
1132
1151
|
return null;
|
|
1133
1152
|
}
|
|
@@ -1151,7 +1170,7 @@ function inferPlatformFromHost(env) {
|
|
|
1151
1170
|
return null;
|
|
1152
1171
|
}
|
|
1153
1172
|
|
|
1154
|
-
function resolvePlatform(opts, env, reported) {
|
|
1173
|
+
function resolvePlatform(opts, env, reported, pending) {
|
|
1155
1174
|
const flag = opts && opts.platform != null ? String(opts.platform).trim() : '';
|
|
1156
1175
|
if (flag) {
|
|
1157
1176
|
const lower = flag.toLowerCase();
|
|
@@ -1172,6 +1191,10 @@ function resolvePlatform(opts, env, reported) {
|
|
|
1172
1191
|
if (VALID_PLATFORMS.has(lower)) return { value: lower };
|
|
1173
1192
|
return { value: null, warn: 'invalid AOK_PLATFORM (use cursor, claude, or amp)' };
|
|
1174
1193
|
}
|
|
1194
|
+
const pendingPlatform = pending && pending.platform != null ? String(pending.platform).trim() : '';
|
|
1195
|
+
if (pendingPlatform && VALID_PLATFORMS.has(pendingPlatform)) {
|
|
1196
|
+
return { value: pendingPlatform };
|
|
1197
|
+
}
|
|
1175
1198
|
return { value: inferPlatformFromHost(env) };
|
|
1176
1199
|
}
|
|
1177
1200
|
|
|
@@ -1589,14 +1612,14 @@ function readHandoffFields(projectDir, changeName) {
|
|
|
1589
1612
|
}
|
|
1590
1613
|
|
|
1591
1614
|
const METRICS_VERSION = 1;
|
|
1592
|
-
const METRICS_SPEND_KEYS = ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd'];
|
|
1615
|
+
const METRICS_SPEND_KEYS = ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'costUsdEstimated'];
|
|
1593
1616
|
|
|
1594
1617
|
function metricsFilePath(projectDir, changeName) {
|
|
1595
1618
|
return join(projectDir, 'openspec', 'changes', changeName, 'metrics.json');
|
|
1596
1619
|
}
|
|
1597
1620
|
|
|
1598
1621
|
function emptySpendTotals() {
|
|
1599
|
-
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null };
|
|
1622
|
+
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
1600
1623
|
}
|
|
1601
1624
|
|
|
1602
1625
|
function emptyPlatformSpend(source = 'none') {
|
|
@@ -1606,6 +1629,7 @@ function emptyPlatformSpend(source = 'none') {
|
|
|
1606
1629
|
totalTokens: null,
|
|
1607
1630
|
costUsd: null,
|
|
1608
1631
|
ampCredits: null,
|
|
1632
|
+
costUsdEstimated: null,
|
|
1609
1633
|
source,
|
|
1610
1634
|
};
|
|
1611
1635
|
}
|
|
@@ -1673,7 +1697,9 @@ function loadMetricsFile(filePath, changeName, nowIso) {
|
|
|
1673
1697
|
|
|
1674
1698
|
function saveMetricsFile(filePath, metrics) {
|
|
1675
1699
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
1676
|
-
|
|
1700
|
+
const out = { ...metrics };
|
|
1701
|
+
delete out.timezone;
|
|
1702
|
+
writeFileSync(filePath, `${JSON.stringify(out, null, 2)}\n`);
|
|
1677
1703
|
}
|
|
1678
1704
|
|
|
1679
1705
|
function numOrNull(value) {
|
|
@@ -1698,12 +1724,6 @@ function phaseForRole(role) {
|
|
|
1698
1724
|
return 'other';
|
|
1699
1725
|
}
|
|
1700
1726
|
|
|
1701
|
-
function isoOrNull(value) {
|
|
1702
|
-
if (!value) return null;
|
|
1703
|
-
const ms = Date.parse(String(value));
|
|
1704
|
-
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
1727
|
function sessionFieldOrSources(session, key) {
|
|
1708
1728
|
if (session[key] != null && session[key] !== '') return numOrNull(session[key]);
|
|
1709
1729
|
let sum = null;
|
|
@@ -1716,7 +1736,7 @@ function sessionFieldOrSources(session, key) {
|
|
|
1716
1736
|
function lastSessionEndedAt(metrics) {
|
|
1717
1737
|
let last = null;
|
|
1718
1738
|
for (const session of metrics.sessions || []) {
|
|
1719
|
-
if (session.endedAt
|
|
1739
|
+
if (session.endedAt) last = last == null ? session.endedAt : laterTimestamp(last, session.endedAt);
|
|
1720
1740
|
}
|
|
1721
1741
|
return last;
|
|
1722
1742
|
}
|
|
@@ -1735,6 +1755,33 @@ function existingSourceIdSet(metrics) {
|
|
|
1735
1755
|
return ids;
|
|
1736
1756
|
}
|
|
1737
1757
|
|
|
1758
|
+
function lastNonArchiverSession(sessions) {
|
|
1759
|
+
const list = Array.isArray(sessions) ? sessions : [];
|
|
1760
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
1761
|
+
if (list[i] && list[i].role !== 'Archiver') return list[i];
|
|
1762
|
+
}
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function dropStaleArchiveSelfReport(reported, sessions) {
|
|
1767
|
+
const base = reported && typeof reported === 'object' ? { ...reported } : emptyMetricsFields();
|
|
1768
|
+
const last = lastNonArchiverSession(sessions);
|
|
1769
|
+
if (!last) return base;
|
|
1770
|
+
const sameInput = numOrNull(base.inputTokens) != null && numOrNull(base.inputTokens) === numOrNull(last.inputTokens);
|
|
1771
|
+
const sameOutput = numOrNull(base.outputTokens) === numOrNull(last.outputTokens);
|
|
1772
|
+
if (sameInput && sameOutput) {
|
|
1773
|
+
base.inputTokens = null;
|
|
1774
|
+
base.outputTokens = null;
|
|
1775
|
+
base.totalTokens = null;
|
|
1776
|
+
base.costUsd = null;
|
|
1777
|
+
base.ampCredits = null;
|
|
1778
|
+
if (!base.spendSource || base.spendSource === 'self-report') base.spendSource = null;
|
|
1779
|
+
}
|
|
1780
|
+
if (base.model && last.model && base.model === last.model) base.model = null;
|
|
1781
|
+
if (base.platform && last.platform && base.platform === last.platform) base.platform = null;
|
|
1782
|
+
return base;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1738
1785
|
function uniqueSourceModels(sources) {
|
|
1739
1786
|
const seen = [];
|
|
1740
1787
|
for (const src of sources || []) {
|
|
@@ -1793,11 +1840,35 @@ function sourceAmpCredits(sources) {
|
|
|
1793
1840
|
return sum;
|
|
1794
1841
|
}
|
|
1795
1842
|
|
|
1796
|
-
function
|
|
1843
|
+
function sourceEstimatedUsd(sources) {
|
|
1844
|
+
let sum = null;
|
|
1845
|
+
for (const src of sources || []) {
|
|
1846
|
+
sum = addNullable(sum, numOrNull(src.costUsdEstimated));
|
|
1847
|
+
}
|
|
1848
|
+
return sum;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function ampThreadTotals(threads) {
|
|
1852
|
+
const list = Array.isArray(threads) ? threads : [];
|
|
1853
|
+
let costUsd = null;
|
|
1854
|
+
let agentMode = null;
|
|
1855
|
+
const models = [];
|
|
1856
|
+
for (const thread of list) {
|
|
1857
|
+
costUsd = addNullable(costUsd, numOrNull(thread && thread.costUsd));
|
|
1858
|
+
if (!agentMode && thread && thread.agentMode) agentMode = String(thread.agentMode);
|
|
1859
|
+
for (const row of (thread && thread.models) || []) {
|
|
1860
|
+
if (row && row.model && !models.includes(row.model)) models.push(row.model);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return { costUsd, agentMode, models };
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function resolveSessionSpend(opts, reported, sources, extra = {}) {
|
|
1797
1867
|
const flags = opts || {};
|
|
1798
1868
|
const self = reported || emptyMetricsFields();
|
|
1799
1869
|
const fromSources = sessionTotalsFromSources(sources || []);
|
|
1800
1870
|
const sourceCredits = sourceAmpCredits(sources || []);
|
|
1871
|
+
const fromAmp = ampThreadTotals(extra.ampThreads);
|
|
1801
1872
|
const flagInput = numOrNull(flags.inputTokens);
|
|
1802
1873
|
const flagOutput = numOrNull(flags.outputTokens);
|
|
1803
1874
|
const flagTotal = numOrNull(flags.totalTokens);
|
|
@@ -1809,12 +1880,14 @@ function resolveSessionSpend(opts, reported, sources) {
|
|
|
1809
1880
|
if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
|
|
1810
1881
|
totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
1811
1882
|
}
|
|
1812
|
-
const costUsd = firstNonNull(flagCost, self.costUsd, fromSources.costUsd);
|
|
1883
|
+
const costUsd = firstNonNull(flagCost, self.costUsd, fromAmp.costUsd, fromSources.costUsd);
|
|
1813
1884
|
const ampCredits = firstNonNull(flagCredits, self.ampCredits, sourceCredits);
|
|
1885
|
+
const costUsdEstimated = sourceEstimatedUsd(sources || []);
|
|
1814
1886
|
let spendSource = 'unreported';
|
|
1815
1887
|
if (self.spendSource) spendSource = String(self.spendSource);
|
|
1816
1888
|
else if (hasSpendOverride(flags)) spendSource = 'flag';
|
|
1817
1889
|
else if (reportedHasSpendNumbers(self)) spendSource = 'self-report';
|
|
1890
|
+
else if (fromAmp.costUsd != null) spendSource = 'amp-usage';
|
|
1818
1891
|
else if (
|
|
1819
1892
|
fromSources.inputTokens != null
|
|
1820
1893
|
|| fromSources.outputTokens != null
|
|
@@ -1824,7 +1897,7 @@ function resolveSessionSpend(opts, reported, sources) {
|
|
|
1824
1897
|
) {
|
|
1825
1898
|
spendSource = 'adapter';
|
|
1826
1899
|
}
|
|
1827
|
-
return { inputTokens, outputTokens, totalTokens, costUsd, ampCredits, spendSource };
|
|
1900
|
+
return { inputTokens, outputTokens, totalTokens, costUsd, ampCredits, costUsdEstimated, spendSource, agentMode: fromAmp.agentMode };
|
|
1828
1901
|
}
|
|
1829
1902
|
|
|
1830
1903
|
function sessionTotalsFromFlags(opts) {
|
|
@@ -1856,7 +1929,7 @@ function sessionTotalsFromSources(sources) {
|
|
|
1856
1929
|
return { inputTokens, outputTokens, totalTokens, costUsd };
|
|
1857
1930
|
}
|
|
1858
1931
|
|
|
1859
|
-
function runCollectSpend(metrics, windowStart, windowEnd) {
|
|
1932
|
+
function runCollectSpend(metrics, windowStart, windowEnd, extra = {}) {
|
|
1860
1933
|
try {
|
|
1861
1934
|
return collectSpend({
|
|
1862
1935
|
cwd: process.cwd(),
|
|
@@ -1865,25 +1938,48 @@ function runCollectSpend(metrics, windowStart, windowEnd) {
|
|
|
1865
1938
|
existingSourceIds: existingSourceIdSet(metrics),
|
|
1866
1939
|
env: process.env,
|
|
1867
1940
|
homedir: process.env.HOME,
|
|
1941
|
+
platforms: extra.platforms,
|
|
1942
|
+
ampThreadId: extra.ampThreadId,
|
|
1943
|
+
ampCli: extra.ampCli === true,
|
|
1944
|
+
exportAmpThread: extra.exportAmpThread,
|
|
1945
|
+
usageAmpThread: extra.usageAmpThread,
|
|
1868
1946
|
});
|
|
1869
1947
|
} catch {
|
|
1870
1948
|
return { sources: [], byPlatform: defaultSpendByPlatform(), byModel: [], notes: [] };
|
|
1871
1949
|
}
|
|
1872
1950
|
}
|
|
1873
1951
|
|
|
1874
|
-
function applyCollectedSessionFields(session, sources, resolvedModel, opts, reported) {
|
|
1952
|
+
function applyCollectedSessionFields(session, sources, resolvedModel, opts, reported, extra = {}) {
|
|
1875
1953
|
session.sources = sources || [];
|
|
1876
1954
|
const uniqueModels = uniqueSourceModels(session.sources);
|
|
1877
|
-
|
|
1955
|
+
const fromAmp = ampThreadTotals(extra.ampThreads);
|
|
1956
|
+
for (const model of fromAmp.models) {
|
|
1957
|
+
if (model && !uniqueModels.includes(model)) uniqueModels.push(model);
|
|
1958
|
+
}
|
|
1959
|
+
if (!session.model || isPlaceholderModel(session.model)) {
|
|
1960
|
+
session.model = (resolvedModel && !isPlaceholderModel(resolvedModel) ? resolvedModel : null)
|
|
1961
|
+
|| primaryModelFromSources(session.sources)
|
|
1962
|
+
|| fromAmp.models[0]
|
|
1963
|
+
|| null;
|
|
1964
|
+
}
|
|
1878
1965
|
if (!session.platform) session.platform = primaryPlatformFromSources(session.sources) || null;
|
|
1879
1966
|
if (uniqueModels.length > 1) session.models = uniqueModels;
|
|
1880
|
-
const spend = resolveSessionSpend(opts, reported, session.sources);
|
|
1967
|
+
const spend = resolveSessionSpend(opts, reported, session.sources, extra);
|
|
1881
1968
|
session.inputTokens = spend.inputTokens;
|
|
1882
1969
|
session.outputTokens = spend.outputTokens;
|
|
1883
1970
|
session.totalTokens = spend.totalTokens;
|
|
1884
1971
|
session.costUsd = spend.costUsd;
|
|
1885
1972
|
session.ampCredits = spend.ampCredits;
|
|
1973
|
+
session.costUsdEstimated = spend.costUsdEstimated;
|
|
1886
1974
|
session.spendSource = spend.spendSource;
|
|
1975
|
+
if (spend.agentMode) session.agentMode = spend.agentMode;
|
|
1976
|
+
const usageModels = [];
|
|
1977
|
+
for (const thread of extra.ampThreads || []) {
|
|
1978
|
+
for (const row of thread.models || []) {
|
|
1979
|
+
if (row && row.model) usageModels.push(row);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (usageModels.length) session.usageModels = usageModels;
|
|
1887
1983
|
}
|
|
1888
1984
|
|
|
1889
1985
|
function sessionTotalsLookOverridden(session) {
|
|
@@ -1904,13 +2000,17 @@ function metricsBackfillLastSession(projectDir, changeName) {
|
|
|
1904
2000
|
}
|
|
1905
2001
|
|
|
1906
2002
|
function metricsBackfillFile(filePath, changeName) {
|
|
1907
|
-
const nowIso =
|
|
2003
|
+
const nowIso = nowUtcIso();
|
|
1908
2004
|
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
1909
2005
|
const sessions = metrics.sessions || [];
|
|
1910
2006
|
if (!sessions.length) return { filePath, added: 0, empty: true };
|
|
1911
2007
|
const last = sessions[sessions.length - 1];
|
|
1912
2008
|
const windowStart = last.startedAt || collectWindowStart(metrics);
|
|
1913
|
-
const
|
|
2009
|
+
const lastPlatform = last && last.platform;
|
|
2010
|
+
const collected = runCollectSpend(metrics, windowStart, nowIso, {
|
|
2011
|
+
ampThreadId: last && last.threadId || ampThreadIdFromEnv(process.env) || null,
|
|
2012
|
+
ampCli: lastPlatform === 'amp',
|
|
2013
|
+
});
|
|
1914
2014
|
const incoming = collected.sources || [];
|
|
1915
2015
|
if (!incoming.length) return { filePath, added: 0 };
|
|
1916
2016
|
const overridden = sessionTotalsLookOverridden(last);
|
|
@@ -1927,7 +2027,7 @@ function metricsBackfillFile(filePath, changeName) {
|
|
|
1927
2027
|
applyCollectedSessionFields(last, merged, last.model, {}, {
|
|
1928
2028
|
model: last.model,
|
|
1929
2029
|
platform: last.platform,
|
|
1930
|
-
});
|
|
2030
|
+
}, { ampThreads: collected.ampThreads });
|
|
1931
2031
|
}
|
|
1932
2032
|
metrics.updatedAt = nowIso;
|
|
1933
2033
|
recomputeMetricsAggregates(metrics);
|
|
@@ -1935,9 +2035,9 @@ function metricsBackfillFile(filePath, changeName) {
|
|
|
1935
2035
|
return { filePath, added: incoming.length };
|
|
1936
2036
|
}
|
|
1937
2037
|
|
|
1938
|
-
function adapterSourceName(platform) {
|
|
2038
|
+
function adapterSourceName(platform, via) {
|
|
1939
2039
|
if (platform === 'claude') return 'claude-jsonl';
|
|
1940
|
-
if (platform === 'amp') return 'amp-thread';
|
|
2040
|
+
if (platform === 'amp') return via === 'amp-cli' ? 'amp-cli' : 'amp-thread';
|
|
1941
2041
|
if (platform === 'cursor') return 'cursor-hook';
|
|
1942
2042
|
return null;
|
|
1943
2043
|
}
|
|
@@ -1949,11 +2049,12 @@ function spendTuple(obj) {
|
|
|
1949
2049
|
totalTokens: numOrNull(obj && obj.totalTokens),
|
|
1950
2050
|
costUsd: numOrNull(obj && obj.costUsd),
|
|
1951
2051
|
ampCredits: numOrNull(obj && obj.ampCredits),
|
|
2052
|
+
costUsdEstimated: numOrNull(obj && obj.costUsdEstimated),
|
|
1952
2053
|
};
|
|
1953
2054
|
}
|
|
1954
2055
|
|
|
1955
2056
|
function spendTuplesMatch(a, b) {
|
|
1956
|
-
return ['inputTokens', 'outputTokens', 'totalTokens'
|
|
2057
|
+
return ['inputTokens', 'outputTokens', 'totalTokens'].every((key) => {
|
|
1957
2058
|
const left = a[key];
|
|
1958
2059
|
const right = b[key];
|
|
1959
2060
|
if (left == null && right == null) return true;
|
|
@@ -1967,6 +2068,7 @@ function addSpendNums(target, nums) {
|
|
|
1967
2068
|
target.totalTokens = addNullable(target.totalTokens, nums.totalTokens);
|
|
1968
2069
|
target.costUsd = addNullable(target.costUsd, nums.costUsd);
|
|
1969
2070
|
target.ampCredits = addNullable(target.ampCredits, nums.ampCredits);
|
|
2071
|
+
target.costUsdEstimated = addNullable(target.costUsdEstimated, nums.costUsdEstimated);
|
|
1970
2072
|
}
|
|
1971
2073
|
|
|
1972
2074
|
function recomputeSpendMaps(metrics) {
|
|
@@ -1984,6 +2086,7 @@ function recomputeSpendMaps(metrics) {
|
|
|
1984
2086
|
totalTokens: null,
|
|
1985
2087
|
costUsd: null,
|
|
1986
2088
|
ampCredits: null,
|
|
2089
|
+
costUsdEstimated: null,
|
|
1987
2090
|
};
|
|
1988
2091
|
addSpendNums(row, nums);
|
|
1989
2092
|
byModel.set(key, row);
|
|
@@ -1993,7 +2096,7 @@ function recomputeSpendMaps(metrics) {
|
|
|
1993
2096
|
const nums = spendTuple(src);
|
|
1994
2097
|
if (src.platform && byPlatform[src.platform]) {
|
|
1995
2098
|
addSpendNums(byPlatform[src.platform], nums);
|
|
1996
|
-
const label = adapterSourceName(src.platform);
|
|
2099
|
+
const label = adapterSourceName(src.platform, src.via);
|
|
1997
2100
|
if (label) byPlatform[src.platform].source = label;
|
|
1998
2101
|
}
|
|
1999
2102
|
addModelRow(src.model, src.platform, nums);
|
|
@@ -2008,6 +2111,19 @@ function recomputeSpendMaps(metrics) {
|
|
|
2008
2111
|
|
|
2009
2112
|
if (sourcesMatchSession) {
|
|
2010
2113
|
for (const src of sources) addSourceRow(src);
|
|
2114
|
+
if (session.costUsd != null && sourceTotals.costUsd == null && session.platform && byPlatform[session.platform]) {
|
|
2115
|
+
byPlatform[session.platform].costUsd = addNullable(byPlatform[session.platform].costUsd, session.costUsd);
|
|
2116
|
+
}
|
|
2117
|
+
for (const row of session.usageModels || []) {
|
|
2118
|
+
addModelRow(row.model, session.platform || 'amp', {
|
|
2119
|
+
inputTokens: null,
|
|
2120
|
+
outputTokens: null,
|
|
2121
|
+
totalTokens: null,
|
|
2122
|
+
costUsd: numOrNull(row.costUsd),
|
|
2123
|
+
ampCredits: null,
|
|
2124
|
+
costUsdEstimated: null,
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2011
2127
|
continue;
|
|
2012
2128
|
}
|
|
2013
2129
|
|
|
@@ -2031,8 +2147,8 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2031
2147
|
totals.sessions += 1;
|
|
2032
2148
|
if (session.runtime === 'cloud') totals.cloudSessions += 1;
|
|
2033
2149
|
totals.durationMs = addNullable(totals.durationMs, numOrNull(session.durationMs));
|
|
2034
|
-
if (session.startedAt
|
|
2035
|
-
if (session.endedAt
|
|
2150
|
+
if (session.startedAt) firstStart = firstStart == null ? session.startedAt : earlierTimestamp(firstStart, session.startedAt);
|
|
2151
|
+
if (session.endedAt) lastEnd = lastEnd == null ? session.endedAt : laterTimestamp(lastEnd, session.endedAt);
|
|
2036
2152
|
const key = session.phase || 'other';
|
|
2037
2153
|
const phase = phases[key] || { sessions: 0, durationMs: null, ...emptySpendTotals(), agents: [], models: [] };
|
|
2038
2154
|
phase.sessions += 1;
|
|
@@ -2052,7 +2168,7 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2052
2168
|
phases[key] = phase;
|
|
2053
2169
|
}
|
|
2054
2170
|
if (firstStart && lastEnd) {
|
|
2055
|
-
totals.leadTimeMs = Math.max(0,
|
|
2171
|
+
totals.leadTimeMs = Math.max(0, parseFlexibleIso(lastEnd) - parseFlexibleIso(firstStart));
|
|
2056
2172
|
}
|
|
2057
2173
|
metrics.phases = phases;
|
|
2058
2174
|
metrics.totals = totals;
|
|
@@ -2060,11 +2176,17 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2060
2176
|
recomputeSpendMaps(metrics);
|
|
2061
2177
|
}
|
|
2062
2178
|
|
|
2063
|
-
function metricsRecordSessionStart(projectDir, changeName, role) {
|
|
2179
|
+
function metricsRecordSessionStart(projectDir, changeName, role, client = {}) {
|
|
2064
2180
|
const filePath = metricsFilePath(projectDir, changeName);
|
|
2065
|
-
const nowIso =
|
|
2181
|
+
const nowIso = nowUtcIso();
|
|
2066
2182
|
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
2067
|
-
metrics.pending = {
|
|
2183
|
+
metrics.pending = {
|
|
2184
|
+
startedAt: nowIso,
|
|
2185
|
+
role: role || '',
|
|
2186
|
+
platform: client.platform || null,
|
|
2187
|
+
threadId: client.threadId || null,
|
|
2188
|
+
clientSource: client.source || null,
|
|
2189
|
+
};
|
|
2068
2190
|
metrics.updatedAt = nowIso;
|
|
2069
2191
|
saveMetricsFile(filePath, metrics);
|
|
2070
2192
|
return filePath;
|
|
@@ -2072,15 +2194,28 @@ function metricsRecordSessionStart(projectDir, changeName, role) {
|
|
|
2072
2194
|
|
|
2073
2195
|
function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
2074
2196
|
const filePath = metricsFilePath(projectDir, fields.changeName);
|
|
2075
|
-
const nowIso =
|
|
2197
|
+
const nowIso = nowUtcIso();
|
|
2076
2198
|
const metrics = loadMetricsFile(filePath, fields.changeName, nowIso);
|
|
2077
|
-
const startedAt = isoOrNull(opts.startedAt) || (metrics.pending && metrics.pending.startedAt) || null;
|
|
2078
|
-
const
|
|
2199
|
+
const startedAt = isoOrNull(opts.startedAt) || isoOrNull(metrics.pending && metrics.pending.startedAt) || null;
|
|
2200
|
+
const startedMs = parseFlexibleIso(startedAt);
|
|
2201
|
+
const endedMs = parseFlexibleIso(nowIso);
|
|
2202
|
+
const durationMs = Number.isFinite(startedMs) && Number.isFinite(endedMs)
|
|
2203
|
+
? Math.max(0, endedMs - startedMs)
|
|
2204
|
+
: null;
|
|
2079
2205
|
const reported = opts.reported || fields.metrics || emptyMetricsFields();
|
|
2080
2206
|
const resolvedModel = opts.model === undefined ? resolveModel(opts, process.env, reported) : opts.model;
|
|
2081
2207
|
const windowStart = collectWindowStart(metrics);
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2208
|
+
const pending = metrics.pending || {};
|
|
2209
|
+
const platform = opts.platform || pending.platform || null;
|
|
2210
|
+
const collectAll = opts.collect === true;
|
|
2211
|
+
const platforms = collectAll ? undefined : (platform ? [platform] : []);
|
|
2212
|
+
const shouldCollect = collectAll || (Array.isArray(platforms) && platforms.length > 0);
|
|
2213
|
+
const collected = shouldCollect
|
|
2214
|
+
? runCollectSpend(metrics, windowStart, nowIso, {
|
|
2215
|
+
platforms,
|
|
2216
|
+
ampThreadId: opts.ampThreadId || pending.threadId || ampThreadIdFromEnv(process.env) || null,
|
|
2217
|
+
ampCli: collectAll || platform === 'amp',
|
|
2218
|
+
})
|
|
2084
2219
|
: { sources: [] };
|
|
2085
2220
|
const session = {
|
|
2086
2221
|
startedAt,
|
|
@@ -2091,7 +2226,8 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2091
2226
|
runtime: fields.runtime || 'local',
|
|
2092
2227
|
agentId: fields.agentId || 'none',
|
|
2093
2228
|
model: resolvedModel || null,
|
|
2094
|
-
platform: opts.platform || null,
|
|
2229
|
+
platform: opts.platform || pending.platform || null,
|
|
2230
|
+
threadId: opts.ampThreadId || pending.threadId || null,
|
|
2095
2231
|
tasks: fields.tasks || null,
|
|
2096
2232
|
sources: [],
|
|
2097
2233
|
inputTokens: null,
|
|
@@ -2099,9 +2235,10 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2099
2235
|
totalTokens: null,
|
|
2100
2236
|
costUsd: null,
|
|
2101
2237
|
ampCredits: null,
|
|
2238
|
+
costUsdEstimated: null,
|
|
2102
2239
|
spendSource: 'unreported',
|
|
2103
2240
|
};
|
|
2104
|
-
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported);
|
|
2241
|
+
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported, collected);
|
|
2105
2242
|
metrics.sessions.push(session);
|
|
2106
2243
|
metrics.pending = null;
|
|
2107
2244
|
metrics.updatedAt = nowIso;
|
|
@@ -2117,13 +2254,22 @@ function metricsRecordSessionEnd(projectDir, fields, opts = {}) {
|
|
|
2117
2254
|
|
|
2118
2255
|
function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
2119
2256
|
const filePath = join(targetDir, 'metrics.json');
|
|
2120
|
-
const nowIso =
|
|
2257
|
+
const nowIso = nowUtcIso();
|
|
2121
2258
|
const metrics = loadMetricsFile(filePath, changeName, nowIso);
|
|
2122
2259
|
const windowStart = lastSessionEndedAt(metrics) || metrics.createdAt;
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2260
|
+
const collectAll = opts.collect === true;
|
|
2261
|
+
const platform = opts.platform || null;
|
|
2262
|
+
const platforms = collectAll ? undefined : (platform ? [platform] : []);
|
|
2263
|
+
const shouldCollect = collectAll || (Array.isArray(platforms) && platforms.length > 0);
|
|
2264
|
+
const ampThreadId = opts.ampThreadId || ampThreadIdFromEnv(process.env) || null;
|
|
2265
|
+
const collected = shouldCollect
|
|
2266
|
+
? runCollectSpend(metrics, windowStart, nowIso, {
|
|
2267
|
+
platforms,
|
|
2268
|
+
ampThreadId,
|
|
2269
|
+
ampCli: collectAll || platform === 'amp',
|
|
2270
|
+
})
|
|
2125
2271
|
: { sources: [] };
|
|
2126
|
-
const reported = opts.reported
|
|
2272
|
+
const reported = dropStaleArchiveSelfReport(opts.reported, metrics.sessions);
|
|
2127
2273
|
const resolvedModel = opts.model === undefined ? resolveModel(opts, process.env, reported) : opts.model;
|
|
2128
2274
|
const session = {
|
|
2129
2275
|
startedAt: nowIso,
|
|
@@ -2134,7 +2280,8 @@ function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
|
2134
2280
|
runtime: opts.runtime || 'local',
|
|
2135
2281
|
agentId: opts.agentId || 'none',
|
|
2136
2282
|
model: resolvedModel || null,
|
|
2137
|
-
platform
|
|
2283
|
+
platform,
|
|
2284
|
+
threadId: ampThreadId || null,
|
|
2138
2285
|
tasks: opts.tasks || null,
|
|
2139
2286
|
sources: [],
|
|
2140
2287
|
inputTokens: null,
|
|
@@ -2142,9 +2289,10 @@ function metricsFinalizeArchive(targetDir, changeName, opts = {}) {
|
|
|
2142
2289
|
totalTokens: null,
|
|
2143
2290
|
costUsd: null,
|
|
2144
2291
|
ampCredits: null,
|
|
2292
|
+
costUsdEstimated: null,
|
|
2145
2293
|
spendSource: 'unreported',
|
|
2146
2294
|
};
|
|
2147
|
-
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported);
|
|
2295
|
+
applyCollectedSessionFields(session, collected.sources || [], resolvedModel, opts, reported, collected);
|
|
2148
2296
|
metrics.sessions.push(session);
|
|
2149
2297
|
metrics.archivedAt = nowIso;
|
|
2150
2298
|
metrics.pending = null;
|
|
@@ -2179,6 +2327,17 @@ function formatMetricsCost(value) {
|
|
|
2179
2327
|
return value == null ? '—' : `$${Number(value).toFixed(2)}`;
|
|
2180
2328
|
}
|
|
2181
2329
|
|
|
2330
|
+
function formatMetricsCostLine(spend) {
|
|
2331
|
+
const billed = spend && spend.costUsd;
|
|
2332
|
+
const estimated = spend && spend.costUsdEstimated;
|
|
2333
|
+
if (billed == null && estimated == null) return '—';
|
|
2334
|
+
if (billed != null && estimated != null) {
|
|
2335
|
+
return `${formatMetricsCost(billed)} billed + ~${formatMetricsCost(estimated)} est.`;
|
|
2336
|
+
}
|
|
2337
|
+
if (billed != null) return formatMetricsCost(billed);
|
|
2338
|
+
return `~${formatMetricsCost(estimated)} est.`;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2182
2341
|
function sessionSpendSourceLabel(session) {
|
|
2183
2342
|
const raw = session && session.spendSource;
|
|
2184
2343
|
if (raw == null || String(raw).trim() === '') return 'unreported';
|
|
@@ -2193,11 +2352,14 @@ function renderMetricsSummary(metrics) {
|
|
|
2193
2352
|
lines.push(`work time: ${formatMetricsDuration(metrics.totals.durationMs)}`);
|
|
2194
2353
|
lines.push(`lead time: ${formatMetricsDuration(metrics.totals.leadTimeMs)}`);
|
|
2195
2354
|
lines.push(`tokens: ${formatMetricsNumber(metrics.spend.totalTokens)} (in: ${formatMetricsNumber(metrics.spend.inputTokens)}, out: ${formatMetricsNumber(metrics.spend.outputTokens)})`);
|
|
2196
|
-
lines.push(`cost: ${
|
|
2355
|
+
lines.push(`cost: ${formatMetricsCostLine(metrics.spend)}`);
|
|
2197
2356
|
lines.push(`unreported: ${unreported}`);
|
|
2198
|
-
if (metrics.archivedAt) lines.push(`archived: ${metrics.archivedAt}`);
|
|
2357
|
+
if (metrics.archivedAt) lines.push(`archived: ${formatKyivDisplay(metrics.archivedAt)}`);
|
|
2199
2358
|
if (metrics.pending) {
|
|
2200
|
-
|
|
2359
|
+
const pendingClient = metrics.pending.platform
|
|
2360
|
+
? ` ${metrics.pending.platform}${metrics.pending.threadId ? ` ${metrics.pending.threadId}` : ''}`
|
|
2361
|
+
: '';
|
|
2362
|
+
lines.push(`open session since ${metrics.pending.startedAt} (${metrics.pending.role || 'unknown role'}${pendingClient})`);
|
|
2201
2363
|
}
|
|
2202
2364
|
|
|
2203
2365
|
const phaseOrder = ['explore', 'design', 'spec', 'review', 'apply', 'archive', 'other'];
|
|
@@ -2212,7 +2374,7 @@ function renderMetricsSummary(metrics) {
|
|
|
2212
2374
|
String(phase.sessions).padEnd(9),
|
|
2213
2375
|
formatMetricsDuration(phase.durationMs).padEnd(9),
|
|
2214
2376
|
formatMetricsNumber(phase.totalTokens).padEnd(9),
|
|
2215
|
-
|
|
2377
|
+
formatMetricsCostLine(phase).padEnd(9),
|
|
2216
2378
|
(phase.agents.join(', ') || '—').padEnd(20),
|
|
2217
2379
|
phase.models.join(', ') || '—',
|
|
2218
2380
|
].join(' '));
|
|
@@ -2228,7 +2390,7 @@ function renderMetricsSummary(metrics) {
|
|
|
2228
2390
|
lines.push([
|
|
2229
2391
|
key.padEnd(10),
|
|
2230
2392
|
formatMetricsNumber(row.totalTokens).padEnd(9),
|
|
2231
|
-
|
|
2393
|
+
formatMetricsCostLine(row).padEnd(9),
|
|
2232
2394
|
formatMetricsNumber(row.ampCredits).padEnd(9),
|
|
2233
2395
|
row.source || 'none',
|
|
2234
2396
|
].join(' '));
|
|
@@ -2246,7 +2408,7 @@ function renderMetricsSummary(metrics) {
|
|
|
2246
2408
|
String(row.model || '—').padEnd(20),
|
|
2247
2409
|
String(row.platform || '—').padEnd(10),
|
|
2248
2410
|
formatMetricsNumber(row.totalTokens).padEnd(9),
|
|
2249
|
-
|
|
2411
|
+
formatMetricsCostLine(row).padEnd(9),
|
|
2250
2412
|
formatMetricsNumber(row.ampCredits),
|
|
2251
2413
|
].join(' '));
|
|
2252
2414
|
}
|
|
@@ -2256,10 +2418,11 @@ function renderMetricsSummary(metrics) {
|
|
|
2256
2418
|
lines.push('');
|
|
2257
2419
|
lines.push('recent sessions:');
|
|
2258
2420
|
for (const session of sessions.slice(-5)) {
|
|
2259
|
-
const spendLabel = session.totalTokens != null || session.costUsd != null
|
|
2260
|
-
? ` — ${formatMetricsNumber(session.totalTokens)} tok, ${
|
|
2421
|
+
const spendLabel = session.totalTokens != null || session.costUsd != null || session.costUsdEstimated != null
|
|
2422
|
+
? ` — ${formatMetricsNumber(session.totalTokens)} tok, ${formatMetricsCostLine(session)}`
|
|
2261
2423
|
: '';
|
|
2262
|
-
|
|
2424
|
+
const modeLabel = session.agentMode ? ` mode:${session.agentMode}` : '';
|
|
2425
|
+
lines.push(`- ${formatKyivDisplay(session.endedAt)} ${String(session.phase || '').padEnd(7)} ${formatMetricsDuration(session.durationMs).padEnd(9)} ${session.role || '(no role)'}${session.model ? ` [${session.model}]` : ''}${modeLabel} (${sessionSpendSourceLabel(session)})${spendLabel}`);
|
|
2263
2426
|
}
|
|
2264
2427
|
}
|
|
2265
2428
|
return lines;
|
|
@@ -3438,7 +3601,7 @@ program
|
|
|
3438
3601
|
.option('--output-tokens <n>', 'Output tokens spent in the Archiver session')
|
|
3439
3602
|
.option('--total-tokens <n>', 'Total tokens spent in the Archiver session (default: input + output)')
|
|
3440
3603
|
.option('--cost-usd <usd>', 'Cost of the Archiver session in USD')
|
|
3441
|
-
.option('--collect', '
|
|
3604
|
+
.option('--collect', 'Collect all spend adapters (default: locked Cursor/Amp/Claude client only)', false)
|
|
3442
3605
|
.action((name, opts) => {
|
|
3443
3606
|
const projectDir = process.cwd();
|
|
3444
3607
|
const fail = (msg) => {
|
|
@@ -3558,11 +3721,25 @@ program
|
|
|
3558
3721
|
if (existsSync(archivedHandoffPath)) {
|
|
3559
3722
|
priorFields = fieldsFromSections(name, parseHandoffMarkdown(readFileSync(archivedHandoffPath, 'utf-8')));
|
|
3560
3723
|
}
|
|
3561
|
-
const
|
|
3562
|
-
const
|
|
3724
|
+
const metricsPreview = loadMetricsFile(join(targetDir, 'metrics.json'), name, nowUtcIso());
|
|
3725
|
+
const reported = dropStaleArchiveSelfReport(priorFields.metrics, metricsPreview.sessions);
|
|
3726
|
+
const client = resolveRestoreClient({
|
|
3727
|
+
env: process.env,
|
|
3728
|
+
cwd: projectDir,
|
|
3729
|
+
homedir: process.env.HOME,
|
|
3730
|
+
platform: opts.platform,
|
|
3731
|
+
});
|
|
3732
|
+
const archivePlatform = resolvePlatform(opts, process.env, reported, {
|
|
3733
|
+
platform: client.platform,
|
|
3734
|
+
threadId: client.threadId,
|
|
3735
|
+
});
|
|
3563
3736
|
const archiveModel = resolveModel(opts, process.env, reported);
|
|
3564
3737
|
if (archivePlatform.warn) console.error(archivePlatform.warn);
|
|
3565
3738
|
printMetricsSectionWarnings(reported, Boolean(archivePlatform.warn));
|
|
3739
|
+
const clientLabel = archivePlatform.value || client.platform
|
|
3740
|
+
? `${archivePlatform.value || client.platform}${client.threadId ? ` ${client.threadId}` : ''}${client.source ? ` (${client.source})` : ''}`
|
|
3741
|
+
: 'unknown — pass --platform or run archive from Cursor/Amp/Claude';
|
|
3742
|
+
console.error(`metrics: archive client ${clientLabel}`);
|
|
3566
3743
|
const runtimeResult = resolveRuntime({}, process.env, priorFields);
|
|
3567
3744
|
const progress = parseTasksProgress(targetDir);
|
|
3568
3745
|
const fields = {
|
|
@@ -3589,7 +3766,8 @@ program
|
|
|
3589
3766
|
const memoryPath = persistMemoryFromHandoff(projectDir, fields);
|
|
3590
3767
|
const metricsPath = metricsFinalizeArchive(targetDir, name, {
|
|
3591
3768
|
model: archiveModel,
|
|
3592
|
-
platform: archivePlatform.value || null,
|
|
3769
|
+
platform: archivePlatform.value || client.platform || null,
|
|
3770
|
+
ampThreadId: client.threadId || ampThreadIdFromEnv(process.env) || null,
|
|
3593
3771
|
runtime: fields.runtime,
|
|
3594
3772
|
agentId: fields.agentId,
|
|
3595
3773
|
tasks: fields.tasks,
|
|
@@ -3609,7 +3787,7 @@ program
|
|
|
3609
3787
|
console.log(`memory: ${memoryPath.replace(`${projectDir}/`, '')}`);
|
|
3610
3788
|
console.log(`metrics: ${metricsPath.replace(`${projectDir}/`, '')} (archived_at set)`);
|
|
3611
3789
|
try {
|
|
3612
|
-
const archivedMetrics = loadMetricsFile(metricsPath, name,
|
|
3790
|
+
const archivedMetrics = loadMetricsFile(metricsPath, name, nowUtcIso());
|
|
3613
3791
|
for (const line of renderMetricsSummary(archivedMetrics)) console.log(line);
|
|
3614
3792
|
} catch {}
|
|
3615
3793
|
log.ok(`archived ${name}`);
|
|
@@ -3877,8 +4055,18 @@ program
|
|
|
3877
4055
|
log.warn(`Memory JSON empty or missing at ${memoryPath}`);
|
|
3878
4056
|
}
|
|
3879
4057
|
if (opts.metrics !== false && existsSync(changeDir)) {
|
|
3880
|
-
const
|
|
4058
|
+
const client = resolveRestoreClient({
|
|
4059
|
+
env: process.env,
|
|
4060
|
+
cwd: projectDir,
|
|
4061
|
+
homedir: process.env.HOME,
|
|
4062
|
+
platform: opts.platform,
|
|
4063
|
+
});
|
|
4064
|
+
const metricsPath = metricsRecordSessionStart(projectDir, name, fields ? fields.nextRole : '', client);
|
|
4065
|
+
const clientLabel = client.platform
|
|
4066
|
+
? `${client.platform}${client.threadId ? ` ${client.threadId}` : ''} (${client.source})`
|
|
4067
|
+
: 'unknown — pass --platform or fill ## Metrics';
|
|
3881
4068
|
log.ok(`metrics: session start recorded (${metricsPath.replace(`${projectDir}/`, '')})`);
|
|
4069
|
+
log.info(`metrics: client ${clientLabel}`);
|
|
3882
4070
|
}
|
|
3883
4071
|
return;
|
|
3884
4072
|
}
|
|
@@ -3948,7 +4136,8 @@ program
|
|
|
3948
4136
|
}
|
|
3949
4137
|
|
|
3950
4138
|
const reported = fields.metrics || emptyMetricsFields();
|
|
3951
|
-
const
|
|
4139
|
+
const metricsPreview = loadMetricsFile(metricsFilePath(projectDir, name), name, nowUtcIso());
|
|
4140
|
+
const platformResult = resolvePlatform(opts, process.env, reported, metricsPreview.pending);
|
|
3952
4141
|
if (platformResult.error) {
|
|
3953
4142
|
log.err(platformResult.error);
|
|
3954
4143
|
process.exitCode = 1;
|
|
@@ -3977,6 +4166,7 @@ program
|
|
|
3977
4166
|
totalTokens: opts.totalTokens,
|
|
3978
4167
|
costUsd: opts.costUsd,
|
|
3979
4168
|
collect: opts.collect === true,
|
|
4169
|
+
ampThreadId: (metricsPreview.pending && metricsPreview.pending.threadId) || ampThreadIdFromEnv(process.env) || null,
|
|
3980
4170
|
reported,
|
|
3981
4171
|
});
|
|
3982
4172
|
console.error(pc.green(' ✓'), `metrics.json updated: ${metricsPath.replace(`${projectDir}/`, '')}`);
|
|
@@ -4047,7 +4237,7 @@ program
|
|
|
4047
4237
|
return;
|
|
4048
4238
|
}
|
|
4049
4239
|
|
|
4050
|
-
const metrics = loadMetricsFile(filePath, name,
|
|
4240
|
+
const metrics = loadMetricsFile(filePath, name, nowUtcIso());
|
|
4051
4241
|
if (opts.json) {
|
|
4052
4242
|
process.stdout.write(`${JSON.stringify(metrics, null, 2)}\n`);
|
|
4053
4243
|
return;
|