dsh-agy 0.3.2 → 0.4.1

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.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as isAgyDisabled } from "./accounts-CKWybAfc.mjs";
2
- import { t as createAgyRuntime } from "./plugin-common-B5dyfnMH.mjs";
1
+ import { i as isAgyDisabled } from "./accounts-BjdDqUq1.mjs";
2
+ import { t as createAgyRuntime } from "./plugin-common-B7OSfnyl.mjs";
3
3
  //#region src/index.ts
4
4
  const name = "dsh-agy";
5
5
  const inject = ["llm"];
@@ -1,14 +1,14 @@
1
1
  import { m as fetchAgyFirstOk, v as AgyAuthError, y as AgyPoolBlockedError } from "./constants-BpyRirvk.mjs";
2
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-CKWybAfc.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-BjdDqUq1.mjs";
4
4
  import { i as generateAntigravityRequestId, n as currentSessionGeneration, r as deriveAntigravitySessionId, t as bumpSessionGeneration } from "./identity-Ci8EGc1F.mjs";
5
- import { i as setThoughtSignature, r as resolveMultimodalFiles, t as toAgyRequestBody } from "./translate-BQu8HLvB.mjs";
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
7
  import { i as resolveAgyModel, n as listAgyModels, t as catalogModelList } from "./models-Qn0-sh6X.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
  /**
@@ -69,6 +69,8 @@ function buildRequestHeaders(session) {
69
69
  }
70
70
  var AgyAdapter = class extends LlmAdapter {
71
71
  options;
72
+ cachedModels = null;
73
+ inFlightModels = null;
72
74
  constructor(options) {
73
75
  super();
74
76
  this.options = options;
@@ -93,20 +95,54 @@ var AgyAdapter = class extends LlmAdapter {
93
95
  /**
94
96
  * The complete catalog, ignoring the user's hidden set.
95
97
  *
98
+ * Cached in memory for `MODEL_LIST_CACHE_TTL_MS` with in-flight deduplication.
99
+ * DSH 0.2.0-rc.1 calls `listModels` on every model and effort selection via
100
+ * `requireModel`; without caching, each click paid a multi-second round-trip to
101
+ * Google's `fetchAvailableModels` through the proxy, showing a loading spinner
102
+ * on every interaction.
103
+ *
96
104
  * The settings page lists models through this rather than `listModels`: if it
97
105
  * read the filtered list, a hidden model would vanish from the page along
98
106
  * with the switch that hides it, leaving no way to turn it back on without
99
107
  * hand-editing the file.
100
108
  */
101
- async listAllModels() {
102
- try {
103
- const session = await this.options.getSession();
104
- const routing = { proxyUrl: session?.account.proxy };
105
- return await listAgyModels(session?.auth.access, session?.account.projectId, accountFetch(routing));
106
- } catch (error) {
107
- if (error instanceof AgyPoolBlockedError || error instanceof AgyAuthError) return catalogModelList();
108
- throw error;
109
- }
109
+ async listAllModels(force = false) {
110
+ const now = Date.now();
111
+ if (!force && this.cachedModels && now - this.cachedModels.at < 6e5) return this.cachedModels.models;
112
+ if (this.inFlightModels) return this.inFlightModels;
113
+ const task = (async () => {
114
+ try {
115
+ const session = await this.options.getSession();
116
+ const routing = { proxyUrl: session?.account.proxy };
117
+ const models = await listAgyModels(session?.auth.access, session?.account.projectId, accountFetch(routing));
118
+ this.cachedModels = {
119
+ models,
120
+ at: Date.now()
121
+ };
122
+ return models;
123
+ } catch (error) {
124
+ if (error instanceof AgyPoolBlockedError || error instanceof AgyAuthError) return catalogModelList();
125
+ if (this.cachedModels) return this.cachedModels.models;
126
+ throw error;
127
+ } finally {
128
+ this.inFlightModels = null;
129
+ }
130
+ })();
131
+ this.inFlightModels = task;
132
+ return task;
133
+ }
134
+ /**
135
+ * Drop the cached model list.
136
+ *
137
+ * Model discovery is account-scoped (what `fetchAvailableModels` reports and
138
+ * which proxy it rides depend on the account), but the cache is deliberately
139
+ * keyed to NO account: a keyed read would have to call `getSession()` on
140
+ * every hit, and that call can probe quota or refresh a token — the network
141
+ * cost this cache exists to remove. Every event that can switch the account
142
+ * discovery rides (activation, rotation) therefore drops the cache instead.
143
+ */
144
+ invalidateModelCache() {
145
+ this.cachedModels = null;
110
146
  }
111
147
  async resolveModel(provider, model) {
112
148
  return resolveAgyModel(provider, model);
@@ -225,7 +261,10 @@ var AgyAdapter = class extends LlmAdapter {
225
261
  } catch (error) {
226
262
  const classified = classifyFetchError(error, { proxyUrl: session.account.proxy });
227
263
  await this.options.reportFailure(classified.kind, session);
228
- this.recordUsage(session, options.model, { ok: false }, startedAt);
264
+ this.recordUsage(session, options.model, {
265
+ ok: false,
266
+ reason: "network-error"
267
+ }, startedAt);
229
268
  throw new LlmError(classified.message ?? "agy fetch failed", "TRANSPORT", { cause: error });
230
269
  }
231
270
  if (response.ok) return { response };
@@ -253,7 +292,8 @@ var AgyAdapter = class extends LlmAdapter {
253
292
  });
254
293
  this.recordUsage(session, options.model, {
255
294
  ok: false,
256
- rateLimited: classified.kind === "rate-limit"
295
+ rateLimited: classified.kind === "rate-limit",
296
+ reason: classified.kind
257
297
  }, startedAt);
258
298
  if (classified.kind === "rate-limit") {
259
299
  if (classified.rateLimitCategory === "quota_exhausted") throw new LlmError(`agy daily quota exhausted (${response.status}): ${classified.message ?? ""}`, QUOTA_EXCEEDED_CODE);
@@ -301,7 +341,10 @@ var AgyAdapter = class extends LlmAdapter {
301
341
  } catch (error) {
302
342
  if (error instanceof DOMException && error.name === "AbortError") throw new LlmError("agy stream aborted", "ABORTED", { cause: error });
303
343
  await this.options.reportFailure("network-error", session);
304
- this.recordUsage(session, options.model, { ok: false }, startedAt);
344
+ this.recordUsage(session, options.model, {
345
+ ok: false,
346
+ reason: "network-error"
347
+ }, startedAt);
305
348
  throw new LlmError(error instanceof Error ? describeFetchError(error) : "agy stream parse failed", UPSTREAM_ERROR_CODE, { cause: error });
306
349
  }
307
350
  }
@@ -320,6 +363,7 @@ var AgyAdapter = class extends LlmAdapter {
320
363
  ...model === void 0 ? {} : { model },
321
364
  ok: result.ok,
322
365
  ...result.rateLimited === true ? { rateLimited: true } : {},
366
+ ...result.reason === void 0 ? {} : { reason: result.reason },
323
367
  ...result.usage === void 0 ? {} : { usage: result.usage },
324
368
  ...result.ttftMs === void 0 ? {} : { ttftMs: result.ttftMs },
325
369
  latencyMs: Math.max(0, Date.now() - startedAt)
@@ -381,7 +425,7 @@ var ModelVisibility = class ModelVisibility {
381
425
  /** Reusable frozen empty set, so the common "nothing disabled" read allocates nothing. */
382
426
  sets = /* @__PURE__ */ new Map();
383
427
  constructor(options = {}) {
384
- this.file = options.file ?? join(resolveDshHome(), "agy-models.json");
428
+ this.file = options.file ?? migrateToAgyDir("agy-models.json").file;
385
429
  const initial = this.readWithRaw();
386
430
  this.disabled = initial.disabled;
387
431
  this.raw = initial.raw;
@@ -615,7 +659,7 @@ var ThinkingBudgetStore = class ThinkingBudgetStore {
615
659
  /** When `reloadNow` last ran, for the hot-path throttle. */
616
660
  checkedAt;
617
661
  constructor(options = {}) {
618
- this.file = options.file ?? join(resolveDshHome(), "agy-thinking.json");
662
+ this.file = options.file ?? migrateToAgyDir("agy-thinking.json").file;
619
663
  this.revalidateIntervalMs = options.revalidateIntervalMs ?? 1e3;
620
664
  const initial = this.readWithRaw();
621
665
  this.doc = initial.doc;
@@ -1050,6 +1094,7 @@ var UsageStats = class {
1050
1094
  flushIntervalMs;
1051
1095
  maxPending;
1052
1096
  onFlushError;
1097
+ persistRecent;
1053
1098
  /** This process's un-flushed records. */
1054
1099
  pending = [];
1055
1100
  timer;
@@ -1060,13 +1105,14 @@ var UsageStats = class {
1060
1105
  */
1061
1106
  flushFailedSince;
1062
1107
  constructor(options = {}) {
1063
- this.file = options.file ?? join(resolveDshHome(), "agy-stats.json");
1108
+ this.file = options.file ?? migrateToAgyDir("agy-stats.json").file;
1064
1109
  this.lock = options.lock ?? properStatsLock;
1065
1110
  this.now = options.now ?? (() => Date.now());
1066
1111
  this.flushEvery = options.flushEvery ?? 50;
1067
1112
  this.flushIntervalMs = options.flushIntervalMs ?? 5e3;
1068
1113
  this.maxPending = options.maxPending ?? 500;
1069
1114
  this.onFlushError = options.onFlushError;
1115
+ this.persistRecent = options.persistRecent;
1070
1116
  }
1071
1117
  /** Path of the backing file. */
1072
1118
  get path() {
@@ -1101,6 +1147,7 @@ var UsageStats = class {
1101
1147
  }
1102
1148
  /** Record one request. Hot path: no I/O. */
1103
1149
  record(record) {
1150
+ this.noteRecent(record);
1104
1151
  this.pending.push(record);
1105
1152
  if (this.maxPending > 0 && this.pending.length > this.maxPending) this.pending.splice(0, this.pending.length - this.maxPending);
1106
1153
  if (this.flushFailedSince === void 0 && this.flushEvery > 0 && this.pending.length >= this.flushEvery) {
@@ -1109,6 +1156,51 @@ var UsageStats = class {
1109
1156
  }
1110
1157
  this.armTimer();
1111
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
+ }
1112
1204
  armTimer() {
1113
1205
  if (this.timer !== void 0 || this.flushIntervalMs <= 0) return;
1114
1206
  this.timer = setTimeout(() => {
@@ -1193,6 +1285,204 @@ var UsageStats = class {
1193
1285
  return this.flushFailedSince !== void 0;
1194
1286
  }
1195
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
+ };
1196
1486
  //#endregion
1197
1487
  //#region src/plugin-common.ts
1198
1488
  /**
@@ -1255,24 +1545,58 @@ async function warmVersionCache(store) {
1255
1545
  } catch {}
1256
1546
  await resolveAntigravityVersion(fetchImpl).catch(() => {});
1257
1547
  }
1258
- /** 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. */
1259
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) {
1260
1570
  const { codec } = await resolveCodec(ctx);
1261
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");
1262
1574
  const store = new JsonAccountStore({
1263
- file: `${dshHome}/agy-accounts.json`,
1575
+ file: accountsMigrated.file,
1264
1576
  codec
1265
1577
  });
1266
1578
  warmVersionCache(store);
1267
- const stats = new UsageStats({ onFlushError: (error) => {
1268
- 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`);
1269
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
+ });
1270
1590
  const modelVisibility = new ModelVisibility();
1271
1591
  const thinkingBudget = new ThinkingBudgetStore();
1592
+ let invalidateModelCache = () => {};
1272
1593
  const sessions = new AgySessionManager({
1273
1594
  store,
1274
1595
  recordUsage: (record) => {
1275
1596
  stats.record(record);
1597
+ },
1598
+ onRotate: () => {
1599
+ invalidateModelCache();
1276
1600
  }
1277
1601
  });
1278
1602
  const healthIntervalMs = Number(process.env.DSH_AGY_HEALTH_INTERVAL_MS ?? 0);
@@ -1295,19 +1619,25 @@ async function createAgyRuntime(ctx) {
1295
1619
  });
1296
1620
  }
1297
1621
  });
1622
+ invalidateModelCache = () => {
1623
+ adapter.invalidateModelCache();
1624
+ };
1625
+ adapter.listAllModels().catch(() => {});
1298
1626
  process.once("exit", () => {
1299
1627
  stats.flushSync();
1628
+ recentStore.flushSync();
1300
1629
  });
1301
1630
  return {
1302
1631
  store,
1303
1632
  sessions,
1304
1633
  adapter,
1305
1634
  stats,
1635
+ recentStore,
1306
1636
  modelVisibility,
1307
1637
  thinkingBudget
1308
1638
  };
1309
1639
  }
1310
1640
  //#endregion
1311
- 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 };
1312
1642
 
1313
- //# sourceMappingURL=plugin-common-B5dyfnMH.mjs.map
1643
+ //# sourceMappingURL=plugin-common-B7OSfnyl.mjs.map