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/README.md +21 -10
- package/lib/{accounts-CKWybAfc.mjs → accounts-BjdDqUq1.mjs} +425 -76
- package/lib/accounts-BjdDqUq1.mjs.map +1 -0
- package/lib/cli/index.d.mts.map +1 -1
- package/lib/cli/index.mjs +4 -2
- package/lib/cli/index.mjs.map +1 -1
- package/lib/client.js +406 -36
- package/lib/index.mjs +2 -2
- package/lib/{plugin-common-B5dyfnMH.mjs → plugin-common-B7OSfnyl.mjs} +354 -24
- package/lib/plugin-common-B7OSfnyl.mjs.map +1 -0
- package/lib/{translate-BQu8HLvB.mjs → translate-YhGDUuVa.mjs} +157 -23
- package/lib/translate-YhGDUuVa.mjs.map +1 -0
- package/lib/web/plugin.d.mts +28 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +63 -25
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +4 -3
- package/lib/accounts-CKWybAfc.mjs.map +0 -1
- package/lib/plugin-common-B5dyfnMH.mjs.map +0 -1
- package/lib/translate-BQu8HLvB.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as isAgyDisabled } from "./accounts-
|
|
2
|
-
import { t as createAgyRuntime } from "./plugin-common-
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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, {
|
|
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, {
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
-
/**
|
|
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:
|
|
1575
|
+
file: accountsMigrated.file,
|
|
1264
1576
|
codec
|
|
1265
1577
|
});
|
|
1266
1578
|
warmVersionCache(store);
|
|
1267
|
-
const
|
|
1268
|
-
ctx.logger.warn(`[dsh-agy]
|
|
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 {
|
|
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-
|
|
1643
|
+
//# sourceMappingURL=plugin-common-B7OSfnyl.mjs.map
|