dsh-agy 0.4.0 → 0.4.2

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.
Files changed (28) hide show
  1. package/README.md +21 -10
  2. package/lib/{accounts-BN4h0AU4.mjs → accounts-D605JE2i.mjs} +186 -19
  3. package/lib/accounts-D605JE2i.mjs.map +1 -0
  4. package/lib/cli/index.d.mts.map +1 -1
  5. package/lib/cli/index.mjs +9 -7
  6. package/lib/cli/index.mjs.map +1 -1
  7. package/lib/client.js +402 -51
  8. package/lib/{constants-BpyRirvk.mjs → constants-CmA9wXNe.mjs} +3 -3
  9. package/lib/{constants-BpyRirvk.mjs.map → constants-CmA9wXNe.mjs.map} +1 -1
  10. package/lib/{exchange-LzEyxve1.mjs → exchange-C-tndv7j.mjs} +3 -3
  11. package/lib/{exchange-LzEyxve1.mjs.map → exchange-C-tndv7j.mjs.map} +1 -1
  12. package/lib/{import-CotRHNDk.mjs → import-SxPDjH3u.mjs} +4 -4
  13. package/lib/{import-CotRHNDk.mjs.map → import-SxPDjH3u.mjs.map} +1 -1
  14. package/lib/index.mjs +2 -2
  15. package/lib/{models-Qn0-sh6X.mjs → models-B6bD7zPi.mjs} +3 -3
  16. package/lib/{models-Qn0-sh6X.mjs.map → models-B6bD7zPi.mjs.map} +1 -1
  17. package/lib/{plugin-common-N7p9Cm-3.mjs → plugin-common-kSBwIaFi.mjs} +303 -17
  18. package/lib/plugin-common-kSBwIaFi.mjs.map +1 -0
  19. package/lib/{proxy-DfaL73mL.mjs → proxy-BbuQX5U9.mjs} +28 -8
  20. package/lib/proxy-BbuQX5U9.mjs.map +1 -0
  21. package/lib/{quota-summary-adID82I2.mjs → quota-summary-DEA3P3Zh.mjs} +3 -3
  22. package/lib/{quota-summary-adID82I2.mjs.map → quota-summary-DEA3P3Zh.mjs.map} +1 -1
  23. package/lib/web/plugin.mjs +40 -18
  24. package/lib/web/plugin.mjs.map +1 -1
  25. package/package.json +1 -4
  26. package/lib/accounts-BN4h0AU4.mjs.map +0 -1
  27. package/lib/plugin-common-N7p9Cm-3.mjs.map +0 -1
  28. package/lib/proxy-DfaL73mL.mjs.map +0 -1
@@ -1,14 +1,14 @@
1
- import { m as fetchAgyFirstOk, v as AgyAuthError, y as AgyPoolBlockedError } from "./constants-BpyRirvk.mjs";
2
- import { a as probeFetch, o as proxiedFetch, t as accountFetch } from "./proxy-DfaL73mL.mjs";
3
- import { _ as resolveMasterKeyCodec, b as describeFetchError, d as MASTER_KEY_REF, f as createAesGcmCodec, g as resolveDshHome, h as persistMasterKey, m as loadMasterKey, p as deriveKey, r as AgySessionManager, s as resolveAntigravityVersion, t as JsonAccountStore, u as pickProbeProxyUrl, v as classifyFetchError, x as isSessionAccumulationOverflow, y as classifyHttpError } from "./accounts-BN4h0AU4.mjs";
1
+ import { m as fetchAgyFirstOk, v as AgyAuthError, y as AgyPoolBlockedError } from "./constants-CmA9wXNe.mjs";
2
+ import { a as probeFetch, o as proxiedFetch, t as accountFetch } from "./proxy-BbuQX5U9.mjs";
3
+ import { S as isSessionAccumulationOverflow, _ as resolveDshHome, b as classifyHttpError, d as migrateToAgyDir, f as MASTER_KEY_REF, g as persistMasterKey, h as loadMasterKey, m as deriveKey, p as createAesGcmCodec, r as AgySessionManager, s as resolveAntigravityVersion, t as JsonAccountStore, u as pickProbeProxyUrl, v as resolveMasterKeyCodec, x as describeFetchError, y as classifyFetchError } from "./accounts-D605JE2i.mjs";
4
4
  import { i as generateAntigravityRequestId, n as currentSessionGeneration, r as deriveAntigravitySessionId, t as bumpSessionGeneration } from "./identity-Ci8EGc1F.mjs";
5
5
  import { i as setThoughtSignature, r as resolveMultimodalFiles, t as toAgyRequestBody } from "./translate-YhGDUuVa.mjs";
6
6
  import { t as parseAgySse } from "./parse-Bs4rPkHA.mjs";
7
- import { i as resolveAgyModel, n as listAgyModels, t as catalogModelList } from "./models-Qn0-sh6X.mjs";
7
+ import { i as resolveAgyModel, n as listAgyModels, t as catalogModelList } from "./models-B6bD7zPi.mjs";
8
8
  import { randomBytes } from "node:crypto";
9
9
  import { LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
10
10
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
11
- import { dirname, join } from "node:path";
11
+ import { dirname } from "node:path";
12
12
  import lockfile from "proper-lockfile";
13
13
  //#region src/adapter/adapter.ts
14
14
  /**
@@ -261,7 +261,10 @@ var AgyAdapter = class extends LlmAdapter {
261
261
  } catch (error) {
262
262
  const classified = classifyFetchError(error, { proxyUrl: session.account.proxy });
263
263
  await this.options.reportFailure(classified.kind, session);
264
- this.recordUsage(session, options.model, { ok: false }, startedAt);
264
+ this.recordUsage(session, options.model, {
265
+ ok: false,
266
+ reason: "network-error"
267
+ }, startedAt);
265
268
  throw new LlmError(classified.message ?? "agy fetch failed", "TRANSPORT", { cause: error });
266
269
  }
267
270
  if (response.ok) return { response };
@@ -289,7 +292,8 @@ var AgyAdapter = class extends LlmAdapter {
289
292
  });
290
293
  this.recordUsage(session, options.model, {
291
294
  ok: false,
292
- rateLimited: classified.kind === "rate-limit"
295
+ rateLimited: classified.kind === "rate-limit",
296
+ reason: classified.kind
293
297
  }, startedAt);
294
298
  if (classified.kind === "rate-limit") {
295
299
  if (classified.rateLimitCategory === "quota_exhausted") throw new LlmError(`agy daily quota exhausted (${response.status}): ${classified.message ?? ""}`, QUOTA_EXCEEDED_CODE);
@@ -337,7 +341,10 @@ var AgyAdapter = class extends LlmAdapter {
337
341
  } catch (error) {
338
342
  if (error instanceof DOMException && error.name === "AbortError") throw new LlmError("agy stream aborted", "ABORTED", { cause: error });
339
343
  await this.options.reportFailure("network-error", session);
340
- this.recordUsage(session, options.model, { ok: false }, startedAt);
344
+ this.recordUsage(session, options.model, {
345
+ ok: false,
346
+ reason: "network-error"
347
+ }, startedAt);
341
348
  throw new LlmError(error instanceof Error ? describeFetchError(error) : "agy stream parse failed", UPSTREAM_ERROR_CODE, { cause: error });
342
349
  }
343
350
  }
@@ -356,6 +363,7 @@ var AgyAdapter = class extends LlmAdapter {
356
363
  ...model === void 0 ? {} : { model },
357
364
  ok: result.ok,
358
365
  ...result.rateLimited === true ? { rateLimited: true } : {},
366
+ ...result.reason === void 0 ? {} : { reason: result.reason },
359
367
  ...result.usage === void 0 ? {} : { usage: result.usage },
360
368
  ...result.ttftMs === void 0 ? {} : { ttftMs: result.ttftMs },
361
369
  latencyMs: Math.max(0, Date.now() - startedAt)
@@ -417,7 +425,7 @@ var ModelVisibility = class ModelVisibility {
417
425
  /** Reusable frozen empty set, so the common "nothing disabled" read allocates nothing. */
418
426
  sets = /* @__PURE__ */ new Map();
419
427
  constructor(options = {}) {
420
- this.file = options.file ?? join(resolveDshHome(), "agy-models.json");
428
+ this.file = options.file ?? migrateToAgyDir("agy-models.json").file;
421
429
  const initial = this.readWithRaw();
422
430
  this.disabled = initial.disabled;
423
431
  this.raw = initial.raw;
@@ -651,7 +659,7 @@ var ThinkingBudgetStore = class ThinkingBudgetStore {
651
659
  /** When `reloadNow` last ran, for the hot-path throttle. */
652
660
  checkedAt;
653
661
  constructor(options = {}) {
654
- this.file = options.file ?? join(resolveDshHome(), "agy-thinking.json");
662
+ this.file = options.file ?? migrateToAgyDir("agy-thinking.json").file;
655
663
  this.revalidateIntervalMs = options.revalidateIntervalMs ?? 1e3;
656
664
  const initial = this.readWithRaw();
657
665
  this.doc = initial.doc;
@@ -1086,6 +1094,7 @@ var UsageStats = class {
1086
1094
  flushIntervalMs;
1087
1095
  maxPending;
1088
1096
  onFlushError;
1097
+ persistRecent;
1089
1098
  /** This process's un-flushed records. */
1090
1099
  pending = [];
1091
1100
  timer;
@@ -1096,13 +1105,14 @@ var UsageStats = class {
1096
1105
  */
1097
1106
  flushFailedSince;
1098
1107
  constructor(options = {}) {
1099
- this.file = options.file ?? join(resolveDshHome(), "agy-stats.json");
1108
+ this.file = options.file ?? migrateToAgyDir("agy-stats.json").file;
1100
1109
  this.lock = options.lock ?? properStatsLock;
1101
1110
  this.now = options.now ?? (() => Date.now());
1102
1111
  this.flushEvery = options.flushEvery ?? 50;
1103
1112
  this.flushIntervalMs = options.flushIntervalMs ?? 5e3;
1104
1113
  this.maxPending = options.maxPending ?? 500;
1105
1114
  this.onFlushError = options.onFlushError;
1115
+ this.persistRecent = options.persistRecent;
1106
1116
  }
1107
1117
  /** Path of the backing file. */
1108
1118
  get path() {
@@ -1137,6 +1147,7 @@ var UsageStats = class {
1137
1147
  }
1138
1148
  /** Record one request. Hot path: no I/O. */
1139
1149
  record(record) {
1150
+ this.noteRecent(record);
1140
1151
  this.pending.push(record);
1141
1152
  if (this.maxPending > 0 && this.pending.length > this.maxPending) this.pending.splice(0, this.pending.length - this.maxPending);
1142
1153
  if (this.flushFailedSince === void 0 && this.flushEvery > 0 && this.pending.length >= this.flushEvery) {
@@ -1145,6 +1156,51 @@ var UsageStats = class {
1145
1156
  }
1146
1157
  this.armTimer();
1147
1158
  }
1159
+ /**
1160
+ * The in-memory recent-activity ring, persisted by an injected
1161
+ * `RecentActivityStore` when one is supplied.
1162
+ *
1163
+ * The ring itself stays in-memory and I/O-free — `noteRecent` only flattens —
1164
+ * while the OPTIONAL `persistRecent` seam hands each flattened record to
1165
+ * `recent-store.ts`, which owns the file, the triggers, the lock, and the
1166
+ * failure story. When no store is injected (tests, the CLI) the ring behaves
1167
+ * exactly as before: per-process, never persisted.
1168
+ *
1169
+ * An earlier revision recorded why persistence was rejected outright (file
1170
+ * growth, a versioned migration, a wider stored surface); `recent-store.ts`
1171
+ * answers each — a bounded list in its own file, no version field, emails
1172
+ * and model ids only — which is what turned this from a bad idea into a
1173
+ * spec.
1174
+ */
1175
+ recent = [];
1176
+ noteRecent(record) {
1177
+ const entry = {
1178
+ at: this.now(),
1179
+ account: record.account ?? null,
1180
+ model: record.model ?? null,
1181
+ kind: record.poolEvent === true ? "rotation" : record.source,
1182
+ ok: record.ok === true,
1183
+ rateLimited: record.rateLimited === true,
1184
+ latencyMs: record.latencyMs ?? null,
1185
+ ttftMs: record.ttftMs ?? null,
1186
+ output: record.usage?.output ?? null,
1187
+ reason: record.ok === true ? null : record.reason ?? null
1188
+ };
1189
+ this.recent.push(entry);
1190
+ if (this.recent.length > 200) this.recent.splice(0, this.recent.length - 200);
1191
+ try {
1192
+ this.persistRecent?.(entry);
1193
+ } catch {}
1194
+ }
1195
+ /**
1196
+ * Newest-first copy of the ring, for the `pool.recent` RPC.
1197
+ *
1198
+ * A copy, not the live array: the RPC layer may hold it across awaits while
1199
+ * `record()` keeps pushing.
1200
+ */
1201
+ recentRequests() {
1202
+ return [...this.recent].reverse();
1203
+ }
1148
1204
  armTimer() {
1149
1205
  if (this.timer !== void 0 || this.flushIntervalMs <= 0) return;
1150
1206
  this.timer = setTimeout(() => {
@@ -1229,6 +1285,204 @@ var UsageStats = class {
1229
1285
  return this.flushFailedSince !== void 0;
1230
1286
  }
1231
1287
  };
1288
+ /** Capture-key dedupe: the fields two processes writing the same capture share. */
1289
+ function captureKey(entry) {
1290
+ return `${entry.at}|${entry.account ?? ""}|${entry.model ?? ""}|${entry.kind}|${entry.output ?? ""}`;
1291
+ }
1292
+ /**
1293
+ * Coerce one parsed JSON value into a `RecentActivity`, or null when it is not
1294
+ * one. No version field: a record this build cannot parse is dropped, which
1295
+ * for a bounded diagnostics list is the whole migration story.
1296
+ */
1297
+ function parseRecentActivity(raw) {
1298
+ if (typeof raw !== "object" || raw === null) return null;
1299
+ const r = raw;
1300
+ const kinds = [
1301
+ "chat",
1302
+ "cli",
1303
+ "verify",
1304
+ "test",
1305
+ "rotation"
1306
+ ];
1307
+ if (typeof r.at !== "number" || !Number.isFinite(r.at)) return null;
1308
+ if (!kinds.includes(r.kind)) return null;
1309
+ if (typeof r.ok !== "boolean") return null;
1310
+ const nullableString = (v) => typeof v === "string" && v.length > 0 ? v : null;
1311
+ const nullableNumber = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
1312
+ return {
1313
+ at: r.at,
1314
+ account: nullableString(r.account),
1315
+ model: nullableString(r.model),
1316
+ kind: r.kind,
1317
+ ok: r.ok,
1318
+ rateLimited: r.rateLimited === true,
1319
+ latencyMs: nullableNumber(r.latencyMs),
1320
+ ttftMs: nullableNumber(r.ttftMs),
1321
+ output: nullableNumber(r.output),
1322
+ reason: nullableString(r.reason)
1323
+ };
1324
+ }
1325
+ /** Merge one batch into an on-disk list (pure; the caller owns the lock and the write). */
1326
+ function mergeRecentEntries(onDisk, batch, cap = 200) {
1327
+ const byKey = /* @__PURE__ */ new Map();
1328
+ for (const entry of [...onDisk, ...batch]) byKey.set(captureKey(entry), entry);
1329
+ return [...byKey.values()].sort((a, b) => a.at - b.at).slice(-cap);
1330
+ }
1331
+ const properRecentLock = { withLock(file, fn) {
1332
+ const release = lockfile.lockSync(file, {
1333
+ stale: 8e3,
1334
+ update: 4e3,
1335
+ realpath: false
1336
+ });
1337
+ try {
1338
+ return fn();
1339
+ } finally {
1340
+ release();
1341
+ }
1342
+ } };
1343
+ /**
1344
+ * Persists the recent-activity ring across processes and restarts.
1345
+ *
1346
+ * The plugin constructs it BEFORE any record can arrive, seeds the in-memory
1347
+ * ring from the file, and hands it to `UsageStats` as the persistence half;
1348
+ * `record()` in `UsageStats` stays I/O-free — it appends to this store's
1349
+ * pending list, and flushing happens on the triggers above.
1350
+ */
1351
+ var RecentActivityStore = class {
1352
+ file;
1353
+ lock;
1354
+ now;
1355
+ flushEvery;
1356
+ flushIntervalMs;
1357
+ onFlushError;
1358
+ /** The seeded ring plus this process's un-flushed captures. */
1359
+ entries = [];
1360
+ pending = [];
1361
+ timer;
1362
+ flushFailedSince;
1363
+ lastFlushError;
1364
+ constructor(options = {}) {
1365
+ this.file = options.file ?? migrateToAgyDir("agy-recent.json").file;
1366
+ this.lock = options.lock ?? properRecentLock;
1367
+ this.now = options.now ?? (() => Date.now());
1368
+ this.flushEvery = options.flushEvery ?? 25;
1369
+ this.flushIntervalMs = options.flushIntervalMs ?? 3e4;
1370
+ this.onFlushError = options.onFlushError;
1371
+ try {
1372
+ const parsed = JSON.parse(readFileSync(this.file, "utf8"));
1373
+ if (Array.isArray(parsed)) {
1374
+ for (const raw of parsed) {
1375
+ const entry = parseRecentActivity(raw);
1376
+ if (entry) this.entries.push(entry);
1377
+ }
1378
+ if (this.entries.length > 200) this.entries.splice(0, this.entries.length - 200);
1379
+ }
1380
+ } catch {}
1381
+ }
1382
+ /** Path of the backing file. */
1383
+ get path() {
1384
+ return this.file;
1385
+ }
1386
+ /** Whether the ring is currently failing to persist (a run of failures). */
1387
+ get flushFailing() {
1388
+ return this.flushFailedSince !== void 0;
1389
+ }
1390
+ /** Number of records awaiting flush. */
1391
+ get pendingCount() {
1392
+ return this.pending.length;
1393
+ }
1394
+ /**
1395
+ * Capture one flattened record and the I/O-free hot-path append.
1396
+ *
1397
+ * `UsageStats.record()` calls this after folding into its own ring; keeping
1398
+ * the write-behind state HERE (not in UsageStats) is what lets the file have
1399
+ * its own triggers, its own lock, and its own failure story without widening
1400
+ * the ledger's.
1401
+ */
1402
+ capture(entry) {
1403
+ this.entries.push(entry);
1404
+ if (this.entries.length > 200) this.entries.splice(0, this.entries.length - 200);
1405
+ this.pending.push(entry);
1406
+ if (this.flushEvery > 0 && this.pending.length >= this.flushEvery) {
1407
+ this.flush();
1408
+ return;
1409
+ }
1410
+ this.armTimer();
1411
+ }
1412
+ /**
1413
+ * Newest-first copy of the ring, for the `pool.recent` RPC: the seeded
1414
+ * history plus everything this process captured since boot.
1415
+ */
1416
+ recentRequests() {
1417
+ return [...this.entries].reverse();
1418
+ }
1419
+ armTimer() {
1420
+ if (this.timer !== void 0 || this.flushIntervalMs <= 0) return;
1421
+ this.timer = setTimeout(() => {
1422
+ this.timer = void 0;
1423
+ this.flush();
1424
+ }, this.flushIntervalMs);
1425
+ this.timer.unref?.();
1426
+ }
1427
+ /**
1428
+ * Merge pending captures into the persisted file.
1429
+ *
1430
+ * Same merge discipline as the ledger: re-read under the lock, merge by
1431
+ * capture key (two processes replaying the same window must not duplicate
1432
+ * rows), sort by time, cap. A failed flush puts the batch back — a
1433
+ * diagnostics ring never breaks a generation — and reports once per run.
1434
+ */
1435
+ flush() {
1436
+ if (this.timer !== void 0) {
1437
+ clearTimeout(this.timer);
1438
+ this.timer = void 0;
1439
+ }
1440
+ const batch = this.pending;
1441
+ if (batch.length === 0) return;
1442
+ this.pending = [];
1443
+ const now = this.now();
1444
+ try {
1445
+ if (!existsSync(this.file)) {
1446
+ mkdirSync(dirname(this.file), { recursive: true });
1447
+ writeFileSync(this.file, "[]\n", { mode: 384 });
1448
+ }
1449
+ this.lock.withLock(this.file, () => {
1450
+ const onDisk = [];
1451
+ try {
1452
+ const parsed = JSON.parse(readFileSync(this.file, "utf8"));
1453
+ if (Array.isArray(parsed)) for (const raw of parsed) {
1454
+ const entry = parseRecentActivity(raw);
1455
+ if (entry) onDisk.push(entry);
1456
+ }
1457
+ } catch {}
1458
+ const merged = mergeRecentEntries(onDisk, batch);
1459
+ const tmp = `${this.file}.tmp`;
1460
+ writeFileSync(tmp, JSON.stringify(merged, null, 2) + "\n", { mode: 384 });
1461
+ renameSync(tmp, this.file);
1462
+ });
1463
+ this.lastFlushError = void 0;
1464
+ this.flushFailedSince = void 0;
1465
+ } catch (error) {
1466
+ this.pending = [...batch, ...this.pending];
1467
+ this.lastFlushError = error;
1468
+ if (this.flushFailedSince === void 0) {
1469
+ this.flushFailedSince = now;
1470
+ try {
1471
+ this.onFlushError?.(error);
1472
+ } catch {}
1473
+ }
1474
+ this.armTimer();
1475
+ }
1476
+ }
1477
+ /** Flush if anything is pending; called on process exit. */
1478
+ flushSync() {
1479
+ this.flush();
1480
+ }
1481
+ /** The most recent flush failure, if any (diagnostics). */
1482
+ get flushError() {
1483
+ return this.lastFlushError;
1484
+ }
1485
+ };
1232
1486
  //#endregion
1233
1487
  //#region src/plugin-common.ts
1234
1488
  /**
@@ -1291,18 +1545,48 @@ async function warmVersionCache(store) {
1291
1545
  } catch {}
1292
1546
  await resolveAntigravityVersion(fetchImpl).catch(() => {});
1293
1547
  }
1294
- /** Build the store, session manager, and adapter for one plugin entry. */
1548
+ /**
1549
+ * The process-wide runtime promise, shared by both entry points.
1550
+ *
1551
+ * Splitting it was a real defect, not a stylistic one: the ledger FILE merges
1552
+ * across instances, so counters looked fine, but every in-memory surface is
1553
+ * per instance — the web entry's ring and in-flight map could never see the
1554
+ * main plugin's chat traffic (same process, two rings), and `account.activate`
1555
+ * cleared affinity pins on the WRONG session manager while the serving one
1556
+ * kept its stale pin. Both entries call this in one process; one memo here is
1557
+ * what makes that "shared runtime" claim true. Separate profiles are separate
1558
+ * processes, so the memo never bridges compositions.
1559
+ */
1560
+ let sharedRuntime;
1561
+ /** Build (once per process) and return the runtime both entries share. */
1295
1562
  async function createAgyRuntime(ctx) {
1563
+ if (sharedRuntime === void 0) sharedRuntime = buildAgyRuntime(ctx).catch((error) => {
1564
+ sharedRuntime = void 0;
1565
+ throw error;
1566
+ });
1567
+ return sharedRuntime;
1568
+ }
1569
+ async function buildAgyRuntime(ctx) {
1296
1570
  const { codec } = await resolveCodec(ctx);
1297
1571
  const dshHome = resolveDshHome();
1572
+ const accountsMigrated = migrateToAgyDir("agy-accounts.json", dshHome);
1573
+ if (accountsMigrated.skew) ctx.logger.warn("[dsh-agy] legacy data file detected next to the agy folder — an older dsh-agy process may still be writing the pre-0.5 layout; its writes will not be seen until every dsh surface is restarted");
1298
1574
  const store = new JsonAccountStore({
1299
- file: `${dshHome}/agy-accounts.json`,
1575
+ file: accountsMigrated.file,
1300
1576
  codec
1301
1577
  });
1302
1578
  warmVersionCache(store);
1303
- const stats = new UsageStats({ onFlushError: (error) => {
1304
- ctx.logger.warn(`[dsh-agy] usage ledger could not be written (${stats.path}): ${error instanceof Error ? error.message : String(error)} — counts are buffered and will retry`);
1579
+ const recentStore = new RecentActivityStore({ onFlushError: (error) => {
1580
+ ctx.logger.warn(`[dsh-agy] recent-activity file could not be written (${recentStore.path}): ${error instanceof Error ? error.message : String(error)} — entries are buffered and will retry`);
1305
1581
  } });
1582
+ const stats = new UsageStats({
1583
+ persistRecent: (entry) => {
1584
+ recentStore.capture(entry);
1585
+ },
1586
+ onFlushError: (error) => {
1587
+ ctx.logger.warn(`[dsh-agy] usage ledger could not be written (${stats.path}): ${error instanceof Error ? error.message : String(error)} — counts are buffered and will retry`);
1588
+ }
1589
+ });
1306
1590
  const modelVisibility = new ModelVisibility();
1307
1591
  const thinkingBudget = new ThinkingBudgetStore();
1308
1592
  let invalidateModelCache = () => {};
@@ -1341,17 +1625,19 @@ async function createAgyRuntime(ctx) {
1341
1625
  adapter.listAllModels().catch(() => {});
1342
1626
  process.once("exit", () => {
1343
1627
  stats.flushSync();
1628
+ recentStore.flushSync();
1344
1629
  });
1345
1630
  return {
1346
1631
  store,
1347
1632
  sessions,
1348
1633
  adapter,
1349
1634
  stats,
1635
+ recentStore,
1350
1636
  modelVisibility,
1351
1637
  thinkingBudget
1352
1638
  };
1353
1639
  }
1354
1640
  //#endregion
1355
- export { THINKING_BUDGET_MAX as a, CLAUDE_BUDGET_MIN as i, foldWindowBreakdown as n, CLAUDE_BUDGET_MAX as r, createAgyRuntime as t };
1641
+ export { CLAUDE_BUDGET_MIN as a, CLAUDE_BUDGET_MAX as i, dayKey as n, THINKING_BUDGET_MAX as o, foldWindowBreakdown as r, createAgyRuntime as t };
1356
1642
 
1357
- //# sourceMappingURL=plugin-common-N7p9Cm-3.mjs.map
1643
+ //# sourceMappingURL=plugin-common-kSBwIaFi.mjs.map