@rayadesu/dsh-llm-billing 0.3.11 → 0.3.13

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/lib/index.js CHANGED
@@ -136,6 +136,7 @@ let DeepSeekBalanceGateway = (() => {
136
136
  let _getSessionSpend_decorators;
137
137
  let _getTodaySpend_decorators;
138
138
  let _getTodaySessionsSpend_decorators;
139
+ let _getDelegatedSpend_decorators;
139
140
  let _getTurnSpend_decorators;
140
141
  let _getSessionTurnSpends_decorators;
141
142
  return class DeepSeekBalanceGateway extends _classSuper {
@@ -145,6 +146,7 @@ let DeepSeekBalanceGateway = (() => {
145
146
  _getSessionSpend_decorators = [Remote("getSessionSpend")];
146
147
  _getTodaySpend_decorators = [Remote("getTodaySpend")];
147
148
  _getTodaySessionsSpend_decorators = [Remote("getTodaySessionsSpend")];
149
+ _getDelegatedSpend_decorators = [Remote("getDelegatedSpend")];
148
150
  _getTurnSpend_decorators = [Remote("getTurnSpend")];
149
151
  _getSessionTurnSpends_decorators = [Remote("getSessionTurnSpends")];
150
152
  __esDecorate(this, null, _getBalance_decorators, {
@@ -191,6 +193,17 @@ let DeepSeekBalanceGateway = (() => {
191
193
  },
192
194
  metadata: _metadata
193
195
  }, null, _instanceExtraInitializers);
196
+ __esDecorate(this, null, _getDelegatedSpend_decorators, {
197
+ kind: "method",
198
+ name: "getDelegatedSpend",
199
+ static: false,
200
+ private: false,
201
+ access: {
202
+ has: (obj) => "getDelegatedSpend" in obj,
203
+ get: (obj) => obj.getDelegatedSpend
204
+ },
205
+ metadata: _metadata
206
+ }, null, _instanceExtraInitializers);
194
207
  __esDecorate(this, null, _getTurnSpend_decorators, {
195
208
  kind: "method",
196
209
  name: "getTurnSpend",
@@ -263,7 +276,9 @@ let DeepSeekBalanceGateway = (() => {
263
276
  /**
264
277
  * Read today's billed spend per session, restricted to the queried Beijing
265
278
  * day. Rows carry the session's durable title and sort by cost descending;
266
- * sessions with no priced usage on the day are omitted.
279
+ * sessions with no priced usage on the day are omitted, and every subagent
280
+ * session is merged into the row of the top-level session that delegated it,
281
+ * so the ranking lists conversations rather than delegations.
267
282
  * @param force - bypass the host-side 60s cache (manual refresh); omitted
268
283
  * means a cached read.
269
284
  * @returns today's per-session rows, highest first.
@@ -272,6 +287,19 @@ let DeepSeekBalanceGateway = (() => {
272
287
  return this.options.fetchTodaySessionsSpend(force ?? false);
273
288
  }
274
289
  /**
290
+ * Read the subagent part of one conversation's billed spend: every subagent
291
+ * session the given session delegated, transitively, across every day (a
292
+ * session's own log cannot price its delegation children). `isSubagent`
293
+ * reports whether the queried session is itself a delegated child.
294
+ * @param sessionId - the session whose delegated subtree to sum.
295
+ * @param force - bypass the host-side 60s cache (manual refresh); omitted
296
+ * means a cached read.
297
+ * @returns the delegated subtotal plus its per-model rows.
298
+ */
299
+ getDelegatedSpend(sessionId, force) {
300
+ return this.options.fetchDelegatedSpend(sessionId, force ?? false);
301
+ }
302
+ /**
275
303
  * Read one completed Turn's billed spend (same per-event pricing as
276
304
  * {@link priceEvent}).
277
305
  * @param sessionId - the session owning the Turn.
@@ -1298,13 +1326,23 @@ function foldOwnBilling(unit, events, seedLength = 0) {
1298
1326
  * every scan.
1299
1327
  * - events path (plans A2/A3): collect and price only today's events in one
1300
1328
  * pass (per-event Beijing-day filter during collection) with a hard cap,
1301
- * skipping sessions whose persisted revision is unchanged since the last
1302
- * scan.
1329
+ * adopting the fold it already priced for a session whose persisted revision
1330
+ * is unchanged since the last pass.
1303
1331
  *
1304
1332
  * Both strategies run behind the same {@link TodaySpendCache}, so a miss
1305
1333
  * happens at most once per 60 seconds per process, and a manual refresh
1306
1334
  * (`force`) bypasses the time window but keeps the revision caches — an
1307
- * unchanged log provably cannot change the aggregate.
1335
+ * unchanged log provably cannot change the aggregate. That proof is what makes
1336
+ * a revision gate a CACHE: both strategies therefore adopt the resolution they
1337
+ * remember for an unchanged revision instead of skipping the session, so an
1338
+ * unchanged log costs no I/O and still contributes its full spend (and title)
1339
+ * to the aggregate and the ranking on every scan.
1340
+ *
1341
+ * The per-session ranking is a per-CONVERSATION ranking: a subagent child is
1342
+ * work the delegating conversation paid for, not a session the user opened, so
1343
+ * every subagent row is folded into the row of the top-level session at the
1344
+ * root of its `parentSession` chain (see {@link rollUpSubagentSpend}). The
1345
+ * aggregate is unaffected — it sums the same sessions either way.
1308
1346
  *
1309
1347
  * Forked sessions never double-count: a fork child's log opens with a
1310
1348
  * verbatim copy of its source session's events (its inherited boundary), so
@@ -1344,6 +1382,79 @@ function foldSessionTitle(events) {
1344
1382
  return null;
1345
1383
  }
1346
1384
  /**
1385
+ * Whether one session's durable header marks it as a subagent child. Either
1386
+ * marker is enough: `origin` is DSH's navigation classification and
1387
+ * `delegationDepth` is its persisted recursion budget, so a header carrying
1388
+ * only the depth (or only the origin) is still a delegation child. A session
1389
+ * created without either — an ordinary session, a user fork, or a cold resume
1390
+ * — is top-level.
1391
+ * @param header - the session's lineage slice; `undefined` reads as top-level.
1392
+ * @returns true when the session was created as a subagent child.
1393
+ */
1394
+ function isSubagentSession(header) {
1395
+ if (header === void 0) return false;
1396
+ return header.origin === "subagent" || (header.delegationDepth ?? 0) > 0;
1397
+ }
1398
+ /**
1399
+ * The top-level session one session's ranking row belongs to: the session
1400
+ * itself for a top-level session, and for a subagent child the first ancestor
1401
+ * up the `parentSession` chain that is not itself a subagent child. A
1402
+ * multi-generation delegation (a subagent that spawned subagents) therefore
1403
+ * lands on the same root row as its parent, and a child whose parent header is
1404
+ * unknown is attributed to the parent id its own header names — the parent is
1405
+ * authoritative even when its log is not part of this scan.
1406
+ * @param id - the session whose row is being attributed.
1407
+ * @param lineage - lineage of every session this scan saw, by id.
1408
+ * @returns the session id whose ranking row the input belongs to.
1409
+ */
1410
+ function topLevelSessionOf(id, lineage) {
1411
+ let current = id;
1412
+ const seen = /* @__PURE__ */ new Set([current]);
1413
+ for (;;) {
1414
+ const header = lineage.get(current);
1415
+ if (!isSubagentSession(header)) return current;
1416
+ const parent = header?.parentSession;
1417
+ if (parent === void 0 || seen.has(parent)) return current;
1418
+ seen.add(parent);
1419
+ current = parent;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Fold every subagent child's row into the top-level row it belongs to
1424
+ * ({@link topLevelSessionOf}), so the ranking lists conversations rather than
1425
+ * every delegation a conversation started. A child's spend is added to its
1426
+ * ancestor's `total`; the ancestor's `ownTotal` keeps its own spend only. A
1427
+ * top-level session whose own day was empty but whose subagents priced
1428
+ * something still gets a row (with `ownTotal` 0), carrying the title the scan
1429
+ * resolved for it in `titles`.
1430
+ * @param rows - one row per session that priced something today (own spends).
1431
+ * @param lineage - lineage of every session this scan saw, by id.
1432
+ * @param titles - resolved display titles by session id; a session absent from
1433
+ * the map has no resolved title and its created row reports `null`.
1434
+ * @returns the merged rows, sorted by `total` descending.
1435
+ */
1436
+ function rollUpSubagentSpend(rows, lineage, titles = /* @__PURE__ */ new Map()) {
1437
+ const merged = /* @__PURE__ */ new Map();
1438
+ for (const row of rows) {
1439
+ const target = topLevelSessionOf(row.sessionId, lineage);
1440
+ const carried = merged.get(target);
1441
+ if (carried === void 0) {
1442
+ merged.set(target, target === row.sessionId ? row : {
1443
+ sessionId: target,
1444
+ title: titles.get(target) ?? null,
1445
+ total: row.total,
1446
+ ownTotal: 0
1447
+ });
1448
+ continue;
1449
+ }
1450
+ merged.set(target, {
1451
+ ...carried,
1452
+ total: carried.total + row.total
1453
+ });
1454
+ }
1455
+ return [...merged.values()].sort((left, right) => right.total - left.total);
1456
+ }
1457
+ /**
1347
1458
  * Read one live session's complete event log across both runtime families.
1348
1459
  * @throws when the session exposes neither the legacy `events` snapshot nor
1349
1460
  * the newer `snapshotEvents()` reader — an unknown runtime surface must
@@ -1484,23 +1595,78 @@ function evictOldest$1(map, limit) {
1484
1595
  if (oldest !== void 0) map.delete(oldest);
1485
1596
  }
1486
1597
  /**
1598
+ * The merged whole-session spend of every subagent session delegated FROM one
1599
+ * session, transitively: the subagent subtotal a conversation's own log cannot
1600
+ * price. Only sessions DSH marked as delegation children count, so a user fork
1601
+ * (which names a `parentSession` too) is never billed into its source. A
1602
+ * malformed lineage that points back at the queried session is ignored rather
1603
+ * than counted twice.
1604
+ * @param id - the session whose delegated subtree to sum.
1605
+ * @param ownSpend - whole-session own spend per session, from one scan pass.
1606
+ * @param lineage - lineage per session, from the same pass.
1607
+ * @returns the merged subtree spend; empty when the session delegated nothing priced.
1608
+ */
1609
+ function delegatedSpendOf(id, ownSpend, lineage) {
1610
+ const children = /* @__PURE__ */ new Map();
1611
+ for (const [childId, header] of lineage) {
1612
+ const parent = header.parentSession;
1613
+ if (parent === void 0 || !isSubagentSession(header)) continue;
1614
+ const siblings = children.get(parent);
1615
+ if (siblings === void 0) children.set(parent, [childId]);
1616
+ else siblings.push(childId);
1617
+ }
1618
+ const seen = /* @__PURE__ */ new Set([id]);
1619
+ const pending = [...children.get(id) ?? []];
1620
+ for (const child of pending) seen.add(child);
1621
+ let total = emptyTodaySpend();
1622
+ while (pending.length > 0) {
1623
+ const current = pending.pop();
1624
+ total = mergeTodaySpend(total, ownSpend.get(current) ?? emptyTodaySpend());
1625
+ for (const child of children.get(current) ?? []) {
1626
+ if (seen.has(child)) continue;
1627
+ seen.add(child);
1628
+ pending.push(child);
1629
+ }
1630
+ }
1631
+ return total;
1632
+ }
1633
+ /**
1487
1634
  * The aggregate computation behind a cache miss. Chooses the projection path
1488
1635
  * when the projection registry is composed, the events path otherwise; both
1489
- * gate cold reads on persisted revisions so steady-state scans touch only
1490
- * sessions whose logs actually changed.
1636
+ * gate cold reads on persisted revisions AND adopt the fold they already hold
1637
+ * for an unchanged log, so steady-state scans re-read only sessions whose logs
1638
+ * actually changed while every unchanged session keeps contributing.
1491
1639
  */
1492
1640
  var TodaySpendScanner = class {
1493
1641
  deps;
1494
- /** Cold sessions resolved on the projection path: id → revision + unit state + title. */
1642
+ /**
1643
+ * Cold sessions resolved by either strategy: id → the persisted revision the
1644
+ * resolution saw, the session's OWN-events fold, and its folded title.
1645
+ *
1646
+ * The two strategies differ in how they PRICE a cold log (an eager projection
1647
+ * cell plus the cache ladder, or a local fold over the read log), never in
1648
+ * what an unchanged log contributes to the day — so one memory serves both.
1649
+ * The projection path reuses the resolved unit; the events path reuses the
1650
+ * fold it priced on the previous pass. A strategy that skipped an unchanged
1651
+ * log WITHOUT adopting its remembered fold would silently drop that session
1652
+ * from the aggregate and the ranking on every scan after the first.
1653
+ */
1495
1654
  coldResolved = /* @__PURE__ */ new Map();
1496
1655
  /** Cold sessions whose resolution failed: id → revision (retried only when the log changes). */
1497
1656
  coldFailed = /* @__PURE__ */ new Map();
1498
- /** Cold sessions resolved on the events path: id → revision (events were collected). */
1499
- lastEventsScan;
1500
1657
  constructor(deps) {
1501
1658
  this.deps = deps;
1502
1659
  }
1503
1660
  /**
1661
+ * Remember one cold session's resolution, bounded by
1662
+ * {@link COLD_RESOLVE_CACHE_LIMIT}: evicting the oldest entry (instead of
1663
+ * clearing) keeps the other sessions' resolved state warm across scans.
1664
+ */
1665
+ rememberCold(id, resolution) {
1666
+ evictOldest$1(this.coldResolved, COLD_RESOLVE_CACHE_LIMIT);
1667
+ this.coldResolved.set(id, resolution);
1668
+ }
1669
+ /**
1504
1670
  * Compute today's aggregate for one Beijing day.
1505
1671
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
1506
1672
  * @returns today's spend across every session.
@@ -1510,8 +1676,11 @@ var TodaySpendScanner = class {
1510
1676
  }
1511
1677
  /**
1512
1678
  * Compute today's per-session spend for one Beijing day, sorted by cost
1513
- * descending. Sessions with no priced usage on the day are omitted; each
1514
- * row carries the session's durable title folded from its log.
1679
+ * descending. One row per top-level session: sessions with no priced usage
1680
+ * on the day are omitted (unless their subagents priced something, which the
1681
+ * roll-up merges into their row), every subagent session is folded into the
1682
+ * top-level session that delegated it, and each row carries the session's
1683
+ * durable title folded from its log.
1515
1684
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
1516
1685
  * @returns today's per-session rows, highest first.
1517
1686
  */
@@ -1524,6 +1693,9 @@ var TodaySpendScanner = class {
1524
1693
  * read, unit fold, and title fold instead of scanning twice. Chooses the
1525
1694
  * projection path when the projection registry is composed, the events path
1526
1695
  * otherwise.
1696
+ *
1697
+ * The aggregate sums every priced session, subagents included — the ranking's
1698
+ * subagent roll-up only regroups rows, so neither total moves.
1527
1699
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
1528
1700
  * @returns the aggregate plus per-session rows sorted by cost descending.
1529
1701
  */
@@ -1533,7 +1705,7 @@ var TodaySpendScanner = class {
1533
1705
  return this.scanDetailProjections(dayKey);
1534
1706
  }
1535
1707
  /**
1536
- * Resolve one cold session's billing unit state and display title.
1708
+ * Resolve one cold session's billing fold state and display title.
1537
1709
  *
1538
1710
  * The zero-I/O projection-cache row answers the query directly whenever its
1539
1711
  * own latest priced day is NOT the queried day: the row then proves the
@@ -1551,7 +1723,7 @@ var TodaySpendScanner = class {
1551
1723
  * @param header - the listed session header (the cache identity witness).
1552
1724
  * @param seeded - whether the session carries a fork-inherited prefix.
1553
1725
  * @param dayKey - the Beijing-time day being aggregated.
1554
- * @returns the resolved state and title, or `undefined` when unreadable.
1726
+ * @returns the resolved fold state and title, or `undefined` when unreadable.
1555
1727
  */
1556
1728
  async resolveCold(header, seeded, dayKey) {
1557
1729
  const { persistence, projectionCache, logger } = this.deps;
@@ -1560,7 +1732,7 @@ var TodaySpendScanner = class {
1560
1732
  if (cache !== void 0) try {
1561
1733
  const value = cache.cachedSnapshot(header, 0, [BILLING_UNIT_KEY])?.values[BILLING_UNIT_KEY];
1562
1734
  if (value !== void 0 && value.dayKey !== dayKey) return {
1563
- value,
1735
+ fold: value,
1564
1736
  title: null
1565
1737
  };
1566
1738
  } catch (error) {
@@ -1572,7 +1744,7 @@ var TodaySpendScanner = class {
1572
1744
  try {
1573
1745
  const read = await persistenceInspect(persistenceService, header.id);
1574
1746
  return {
1575
- value: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
1747
+ fold: foldOwnBilling(this.deps.unit, read.events, read.seedLength),
1576
1748
  title: foldSessionTitle(read.events)
1577
1749
  };
1578
1750
  } catch (error) {
@@ -1595,11 +1767,11 @@ var TodaySpendScanner = class {
1595
1767
  /**
1596
1768
  * Cold-ladder adopt: for every stored session not live, either the
1597
1769
  * revision-gated resolution already in {@link coldResolved} is adopted
1598
- * (unchanged log costs nothing) or the session is queued behind a bounded
1599
- * parallel fan-out, resolved, remembered, and then adopted. A session whose
1600
- * resolution failed is remembered too (by revision), so an unreadable log
1601
- * is not re-read on every scan; a changed revision retries it. One
1602
- * unreadable session never blanks the whole-day aggregate.
1770
+ * (unchanged log costs nothing and still counts) or the session is queued
1771
+ * behind a bounded parallel fan-out, resolved, remembered, and then adopted.
1772
+ * A session whose resolution failed is remembered too (by revision), so an
1773
+ * unreadable log is not re-read on every scan; a changed revision retries it.
1774
+ * One unreadable session never blanks the whole-day aggregate.
1603
1775
  * @param liveIds - ids of sessions already folded from the live store.
1604
1776
  * @param snapshots - stored snapshot list (either runtime family).
1605
1777
  * @param dayKey - the Beijing-time day being aggregated.
@@ -1627,8 +1799,7 @@ var TodaySpendScanner = class {
1627
1799
  const resolved = await this.resolveCold(header, seeded, dayKey);
1628
1800
  if (resolved !== void 0) {
1629
1801
  this.coldFailed.delete(header.id);
1630
- evictOldest$1(this.coldResolved, COLD_RESOLVE_CACHE_LIMIT);
1631
- this.coldResolved.set(header.id, {
1802
+ this.rememberCold(header.id, {
1632
1803
  revision,
1633
1804
  ...resolved
1634
1805
  });
@@ -1645,14 +1816,17 @@ var TodaySpendScanner = class {
1645
1816
  /**
1646
1817
  * Events-path collection shared by both aggregate and per-session scans:
1647
1818
  * fold each session's log with the shared pricing fold (attempt samples with
1648
- * same-step replacement) and announce the session's latest-day spend, gated
1649
- * by revisions — a persisted session whose log did not change since the last
1650
- * scan is skipped. A fork child's inherited prefix (`seq < seedLength`) is
1651
- * skipped, so each model output is priced only in its source session. The
1652
- * hard cap counts the queried day's events; the revision watermark only
1653
- * advances on a complete pass.
1819
+ * same-step replacement) and announce the session's latest-day spend. A
1820
+ * persisted session whose log did not change since it was last resolved is
1821
+ * answered from {@link coldResolved} instead of being re-read: it keeps
1822
+ * counting toward the aggregate and the ranking at zero cost, which is what
1823
+ * makes the revision gate a cache rather than a way to lose sessions. A fork
1824
+ * child's inherited prefix (`seq < seedLength`) is skipped, so each model
1825
+ * output is priced only in its source session. The hard cap counts the
1826
+ * queried day's events; a truncated pass remembers nothing it read, so the
1827
+ * next one re-reads whatever this one cut short.
1654
1828
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
1655
- * @param onSession - fold one session's state plus its complete log.
1829
+ * @param onSession - adopt one session's fold, title, and lineage.
1656
1830
  * @returns whether the hard cap truncated the scan.
1657
1831
  */
1658
1832
  async collectTodayEvents(dayKey, onSession) {
@@ -1660,7 +1834,8 @@ var TodaySpendScanner = class {
1660
1834
  const liveIds = /* @__PURE__ */ new Set();
1661
1835
  let collected = 0;
1662
1836
  let truncated = false;
1663
- const collect = (id, events, seedLength) => {
1837
+ /** Price one complete log, announce it, and return what to remember. */
1838
+ const collect = (id, events, seedLength, lineage) => {
1664
1839
  const folder = new BillingFolder(billing, catalog, seedLength);
1665
1840
  for (const event of events) {
1666
1841
  if (beijingPartsOf(event.time).dayKey === dayKey) {
@@ -1672,13 +1847,19 @@ var TodaySpendScanner = class {
1672
1847
  }
1673
1848
  folder.add(event);
1674
1849
  }
1675
- onSession(id, folder.fold, events);
1850
+ const fold = folder.fold;
1851
+ const title = foldSessionTitle(events);
1852
+ onSession(id, fold, title, lineage);
1853
+ return {
1854
+ fold,
1855
+ title
1856
+ };
1676
1857
  };
1677
1858
  if (sessions !== void 0) {
1678
1859
  const store = sessions();
1679
1860
  if (store !== void 0) for (const session of store.list()) {
1680
1861
  liveIds.add(session.id);
1681
- collect(session.id, liveSessionEvents(session), forkBoundaryOf(session));
1862
+ collect(session.id, liveSessionEvents(session), forkBoundaryOf(session), session.header ?? {});
1682
1863
  if (truncated) break;
1683
1864
  }
1684
1865
  }
@@ -1687,16 +1868,23 @@ var TodaySpendScanner = class {
1687
1868
  const snapshots = await persistenceListSnapshots(persistenceService);
1688
1869
  for (const { header, revision } of snapshots) {
1689
1870
  if (liveIds.has(header.id)) continue;
1690
- if (this.lastEventsScan?.get(header.id) === revision) continue;
1871
+ const resolved = this.coldResolved.get(header.id);
1872
+ if (resolved !== void 0 && resolved.revision === revision) {
1873
+ onSession(header.id, resolved.fold, resolved.title, header);
1874
+ continue;
1875
+ }
1691
1876
  try {
1692
1877
  const read = await persistenceInspect(persistenceService, header.id);
1693
- collect(header.id, read.events, read.seedLength);
1878
+ const priced = collect(header.id, read.events, read.seedLength, header);
1879
+ if (!truncated) this.rememberCold(header.id, {
1880
+ revision,
1881
+ ...priced
1882
+ });
1694
1883
  } catch (error) {
1695
1884
  logger.warn(`llm-billing: skipping unreadable session ${header.id}: ${String(error)}`);
1696
1885
  }
1697
1886
  if (truncated) break;
1698
1887
  }
1699
- if (!truncated) this.lastEventsScan = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot.revision]));
1700
1888
  }
1701
1889
  if (truncated) logger.warn(`llm-billing: today's events exceeded ${maxEvents}; result truncated`);
1702
1890
  return truncated;
@@ -1706,7 +1894,9 @@ var TodaySpendScanner = class {
1706
1894
  * (title folded from the live log, so a rename is reflected immediately),
1707
1895
  * revision-gated cold ladder for the rest (title resolved on inspect, `null`
1708
1896
  * when answered from the projection cache). A fork child's cell covers its
1709
- * inherited prefix, so its own-events fold supplies both outputs.
1897
+ * inherited prefix, so its own-events fold supplies both outputs. Lineage
1898
+ * (which session delegated which) comes from the same headers the boundary
1899
+ * does, so the ranking's subagent roll-up costs no extra read.
1710
1900
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
1711
1901
  * @returns the aggregate plus per-session rows, sorted by cost descending.
1712
1902
  */
@@ -1715,71 +1905,107 @@ var TodaySpendScanner = class {
1715
1905
  const projectionsService = projections?.();
1716
1906
  let aggregate = emptyTodaySpend();
1717
1907
  const rows = /* @__PURE__ */ new Map();
1908
+ const lineage = /* @__PURE__ */ new Map();
1909
+ const titles = /* @__PURE__ */ new Map();
1910
+ const ownSpend = /* @__PURE__ */ new Map();
1911
+ const createdAt = /* @__PURE__ */ new Map();
1718
1912
  const liveIds = /* @__PURE__ */ new Set();
1719
1913
  if (sessions !== void 0) {
1720
1914
  const store = sessions();
1721
1915
  if (store !== void 0) for (const { session, state } of this.liveBillingEntries(store, projectionsService)) {
1722
1916
  liveIds.add(session.id);
1723
- if (state === void 0 || state.dayKey !== dayKey) continue;
1917
+ lineage.set(session.id, session.header ?? {});
1918
+ const born = session.header?.createdAt;
1919
+ if (born !== void 0) createdAt.set(session.id, born);
1920
+ const title = foldSessionTitle(liveSessionEvents(session));
1921
+ titles.set(session.id, title);
1922
+ if (state === void 0) continue;
1923
+ ownSpend.set(session.id, state.session);
1924
+ if (state.dayKey !== dayKey) continue;
1724
1925
  aggregate = mergeTodaySpend(aggregate, state.spend);
1725
1926
  rows.set(session.id, {
1726
1927
  sessionId: session.id,
1727
- title: foldSessionTitle(liveSessionEvents(session)),
1728
- total: state.spend.total
1928
+ title,
1929
+ total: state.spend.total,
1930
+ ownTotal: state.spend.total
1729
1931
  });
1730
1932
  }
1731
1933
  }
1732
1934
  const persistenceService = persistence?.();
1733
1935
  if (persistenceService !== void 0) {
1734
1936
  const snapshots = await persistenceListSnapshots(persistenceService);
1937
+ for (const { header } of snapshots) {
1938
+ if (liveIds.has(header.id)) continue;
1939
+ lineage.set(header.id, header);
1940
+ if (header.createdAt !== void 0) createdAt.set(header.id, header.createdAt);
1941
+ }
1735
1942
  await this.coldAdopt(liveIds, snapshots, dayKey, (id, resolved) => {
1736
- if (resolved.value.dayKey !== dayKey) return;
1737
- aggregate = mergeTodaySpend(aggregate, resolved.value.spend);
1943
+ if (resolved.title !== null && !titles.has(id)) titles.set(id, resolved.title);
1944
+ ownSpend.set(id, resolved.fold.session);
1945
+ if (resolved.fold.dayKey !== dayKey) return;
1946
+ aggregate = mergeTodaySpend(aggregate, resolved.fold.spend);
1738
1947
  rows.set(id, {
1739
1948
  sessionId: id,
1740
1949
  title: resolved.title,
1741
- total: resolved.value.spend.total
1950
+ total: resolved.fold.spend.total,
1951
+ ownTotal: resolved.fold.spend.total
1742
1952
  });
1743
1953
  });
1744
1954
  }
1745
1955
  return {
1746
1956
  aggregate,
1747
- sessions: sortRows(rows)
1957
+ sessions: rollUpSubagentSpend([...rows.values()], lineage, titles),
1958
+ dayKey,
1959
+ ownSpend,
1960
+ lineage,
1961
+ createdAt
1748
1962
  };
1749
1963
  }
1750
1964
  /**
1751
1965
  * Events path, one pass for both outputs: price today's events (per-event
1752
- * Beijing-day filter during collection, hard cap), gated by revisions. A
1966
+ * Beijing-day filter during collection, hard cap), revision-gated so an
1967
+ * unchanged log is adopted from {@link coldResolved} instead of re-read. A
1753
1968
  * fork child's inherited prefix (`seq < seedLength`) is skipped, so each
1754
1969
  * model output is priced only in its source session. Titles fold from each
1755
1970
  * session's complete log — a `session/title` event can predate today — so a
1756
- * rename is reflected as soon as the session's log is re-read.
1971
+ * rename is reflected as soon as the session's log is re-read, and an
1972
+ * unchanged session keeps the title its earlier read folded. Lineage comes
1973
+ * from the same headers, which the ranking roll-up needs.
1757
1974
  * @param dayKey - the Beijing-time calendar-day key to aggregate.
1758
1975
  * @returns the aggregate plus per-session rows, sorted by cost descending.
1759
1976
  */
1760
1977
  async scanDetailEvents(dayKey) {
1761
1978
  let aggregate = emptyTodaySpend();
1762
- const sessions = [];
1763
- await this.collectTodayEvents(dayKey, (id, fold, events) => {
1979
+ const rows = [];
1980
+ const lineage = /* @__PURE__ */ new Map();
1981
+ const titles = /* @__PURE__ */ new Map();
1982
+ const ownSpend = /* @__PURE__ */ new Map();
1983
+ const createdAt = /* @__PURE__ */ new Map();
1984
+ await this.collectTodayEvents(dayKey, (id, fold, title, sessionLineage) => {
1985
+ lineage.set(id, sessionLineage);
1986
+ titles.set(id, title);
1987
+ const born = sessionLineage.createdAt;
1988
+ if (born !== void 0) createdAt.set(id, born);
1989
+ ownSpend.set(id, fold.session);
1764
1990
  if (fold.dayKey !== dayKey) return;
1765
1991
  aggregate = mergeTodaySpend(aggregate, fold.spend);
1766
- sessions.push({
1992
+ rows.push({
1767
1993
  sessionId: id,
1768
- title: foldSessionTitle(events),
1769
- total: fold.spend.total
1994
+ title,
1995
+ total: fold.spend.total,
1996
+ ownTotal: fold.spend.total
1770
1997
  });
1771
1998
  });
1772
- sessions.sort((left, right) => right.total - left.total);
1773
1999
  return {
1774
2000
  aggregate,
1775
- sessions
2001
+ sessions: rollUpSubagentSpend(rows, lineage, titles),
2002
+ dayKey,
2003
+ ownSpend,
2004
+ lineage,
2005
+ createdAt
1776
2006
  };
1777
2007
  }
1778
2008
  };
1779
- /** Per-session rows from the map, highest total first. */
1780
- function sortRows(rows) {
1781
- return [...rows.values()].sort((left, right) => right.total - left.total);
1782
- }
1783
2009
  //#endregion
1784
2010
  //#region lib/types/index.js
1785
2011
  /**
@@ -2021,11 +2247,15 @@ function createUnitRegistrar(ctx, unit) {
2021
2247
  * events path serves today's spend.
2022
2248
  * - plans A1–A3: the scanner chooses the projection path when the registry
2023
2249
  * is composed, the events path otherwise.
2250
+ * All three reads come from ONE cached pass: the day's aggregate, its
2251
+ * per-session ranking, and the delegated-subagent subtree of a conversation
2252
+ * (the same pass folds each session's whole-session total, so the subtree read
2253
+ * costs no second scan).
2024
2254
  * @param ctx - plugin context.
2025
2255
  * @param facts - resolved endpoint, credential, pricing, and catalog facts.
2026
2256
  * @param unit - the shared projection unit definition.
2027
2257
  * @param ensureUnit - idempotent unit registrar (last-resort registration).
2028
- * @returns the two today-spend loaders.
2258
+ * @returns the three today-spend loaders.
2029
2259
  */
2030
2260
  function createTodaySpendLoaders(ctx, facts, unit, ensureUnit) {
2031
2261
  const scanner = new TodaySpendScanner({
@@ -2043,7 +2273,18 @@ function createTodaySpendLoaders(ctx, facts, unit, ensureUnit) {
2043
2273
  const todayCache = new TodaySpendCache((dayKey) => scanner.scanDetail(dayKey), TODAY_SPEND_CACHE_MS);
2044
2274
  return {
2045
2275
  fetchTodaySpend: async (force = false) => (await todayCache.get(force)).aggregate,
2046
- fetchTodaySessionsSpend: async (force = false) => ({ sessions: (await todayCache.get(force)).sessions })
2276
+ fetchTodaySessionsSpend: async (force = false) => ({ sessions: (await todayCache.get(force)).sessions }),
2277
+ fetchDelegatedSpend: async (sessionId, force = false) => {
2278
+ const detail = await todayCache.get(force);
2279
+ const spend = delegatedSpendOf(sessionId, detail.ownSpend, detail.lineage);
2280
+ const createdAt = detail.createdAt.get(sessionId);
2281
+ return {
2282
+ total: spend.total,
2283
+ models: spend.models,
2284
+ isSubagent: isSubagentSession(detail.lineage.get(sessionId)),
2285
+ crossedDay: createdAt !== void 0 && beijingDayKey(new Date(createdAt)) !== detail.dayKey
2286
+ };
2287
+ }
2047
2288
  };
2048
2289
  }
2049
2290
  /** One completed Turn's spend loader, located by its closing message id. */
@@ -2128,7 +2369,7 @@ function apply(ctx, config) {
2128
2369
  ctx.on("session/created", ensureUnit);
2129
2370
  const fetchBalance = createBalanceFetcher(ctx, facts);
2130
2371
  const fetchSessionSpend = createSessionSpendFetcher(ctx, facts);
2131
- const { fetchTodaySpend, fetchTodaySessionsSpend } = createTodaySpendLoaders(ctx, facts, unit, ensureUnit);
2372
+ const { fetchTodaySpend, fetchTodaySessionsSpend, fetchDelegatedSpend } = createTodaySpendLoaders(ctx, facts, unit, ensureUnit);
2132
2373
  const fetchTurnSpend = createTurnSpendFetcher(ctx, facts);
2133
2374
  const fetchTurnSpends = createTurnSpendsFetcher(ctx, facts);
2134
2375
  new DeepSeekBalanceGateway(ctx, {
@@ -2136,9 +2377,10 @@ function apply(ctx, config) {
2136
2377
  fetchSessionSpend,
2137
2378
  fetchTodaySpend,
2138
2379
  fetchTodaySessionsSpend,
2380
+ fetchDelegatedSpend,
2139
2381
  fetchTurnSpend,
2140
2382
  fetchTurnSpends
2141
2383
  });
2142
2384
  }
2143
2385
  //#endregion
2144
- export { BALANCE_CACHE_MS, BALANCE_TIMEOUT_MS, BILLING_UNIT_KEY, BillingFolder, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, FLASH_SERIES_RATE_CHANGE_AT, PUBLIC_BASE_URL, SESSION_SPEND_CACHE_LIMIT, SESSION_TURN_SPEND_CACHE_LIMIT, SessionTurnSpendFolder, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, V4_PRO_ROUTE_SWITCH_AT, addEventContribution, apply, applyBillingEvent, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, emptyBillingFoldState, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, liveSessionEvents, mergeTodaySpend, name, negateSpend, parseDeepSeekBalance, persistenceInspect, persistenceListSnapshots, priceEvent, priceUsage, resolveBilling, subtractSpend };
2386
+ export { BALANCE_CACHE_MS, BALANCE_TIMEOUT_MS, BILLING_UNIT_KEY, BillingFolder, Config, DEFAULT_MODEL_PRICING, DEFAULT_PEAK_HOURS, DeepSeekBalanceGateway, FLASH_SERIES_RATE_CHANGE_AT, PUBLIC_BASE_URL, SESSION_SPEND_CACHE_LIMIT, SESSION_TURN_SPEND_CACHE_LIMIT, SessionTurnSpendFolder, SpendAccumulator, TODAY_SPEND_CACHE_MS, TODAY_SPEND_MAX_EVENTS, TodaySpendCache, TodaySpendScanner, V4_PRO_ROUTE_SWITCH_AT, addEventContribution, apply, applyBillingEvent, beijingDayKey, billingTodaySpendDefinition, computeSessionSpend, computeSessionTurnSpends, computeTodaySpend, computeTurnSpend, delegatedSpendOf, emptyBillingFoldState, emptyTodaySpend, fetchDeepSeekBalance, foldBillingUnit, foldOwnBilling, foldSessionTitle, forkBoundaryOf, isPeak, isSeededSession, isSubagentSession, liveSessionEvents, mergeTodaySpend, name, negateSpend, parseDeepSeekBalance, persistenceInspect, persistenceListSnapshots, priceEvent, priceUsage, resolveBilling, rollUpSubagentSpend, subtractSpend, topLevelSessionOf };