token-goat 2.9.19 → 2.9.21
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 +3 -1
- package/dist/{token-goat-chunk-JNIFSVWM.mjs → token-goat-chunk-2ZHD3XCD.mjs} +5 -1
- package/dist/{token-goat-chunk-P46RTOAQ.mjs → token-goat-chunk-4IIU3GNS.mjs} +10 -7
- package/dist/{token-goat-chunk-2GTB5RBZ.mjs → token-goat-chunk-4V4OZHHV.mjs} +11 -271
- package/dist/token-goat-chunk-64OEF3UN.mjs +128 -0
- package/dist/{token-goat-chunk-AVRD7SBO.mjs → token-goat-chunk-6AQLZMCO.mjs} +270 -29
- package/dist/{token-goat-chunk-LACH3WKS.mjs → token-goat-chunk-BIEDTF6V.mjs} +3 -3
- package/dist/{token-goat-chunk-PIAESKVU.mjs → token-goat-chunk-BKHJRFDE.mjs} +568 -429
- package/dist/token-goat-chunk-CAHN4QU7.mjs +26 -0
- package/dist/{token-goat-chunk-MU6I4IDI.mjs → token-goat-chunk-CMAJGP3E.mjs} +11 -7
- package/dist/token-goat-chunk-CMHBTXMY.mjs +41 -0
- package/dist/{token-goat-chunk-HPMMLV37.mjs → token-goat-chunk-CZ5ZFJSN.mjs} +256 -341
- package/dist/{token-goat-chunk-7DGTVBWP.mjs → token-goat-chunk-DDP64XR7.mjs} +159 -1863
- package/dist/{token-goat-chunk-FX5DHKGF.mjs → token-goat-chunk-DISVHOW6.mjs} +4 -4
- package/dist/{token-goat-chunk-L2UOJBV4.mjs → token-goat-chunk-FZN77SZD.mjs} +27 -19
- package/dist/{token-goat-chunk-DA6TIDLI.mjs → token-goat-chunk-GO7OWONJ.mjs} +18 -13
- package/dist/{token-goat-chunk-EP7TCWHM.mjs → token-goat-chunk-IAMIQLCV.mjs} +83 -66
- package/dist/{token-goat-chunk-NJKNAC37.mjs → token-goat-chunk-IJUHQX2M.mjs} +1 -1
- package/dist/token-goat-chunk-KQI24O4B.mjs +292 -0
- package/dist/{token-goat-chunk-GYC34EGE.mjs → token-goat-chunk-KRPJIXCT.mjs} +2 -2
- package/dist/token-goat-chunk-KVAXTA2W.mjs +198 -0
- package/dist/{token-goat-chunk-FJZ5MZK7.mjs → token-goat-chunk-LK3YOKDF.mjs} +2 -2
- package/dist/token-goat-chunk-M43UJVYQ.mjs +1816 -0
- package/dist/{token-goat-chunk-SOKAPOOC.mjs → token-goat-chunk-M6CDVASA.mjs} +46 -47
- package/dist/token-goat-chunk-MECDAPXB.mjs +1989 -0
- package/dist/token-goat-chunk-MEKVJZUN.mjs +324 -0
- package/dist/token-goat-chunk-MQACN7VG.mjs +1379 -0
- package/dist/{token-goat-chunk-3W5P673V.mjs → token-goat-chunk-MZQORUX5.mjs} +1 -1
- package/dist/{token-goat-chunk-DHIXE6GV.mjs → token-goat-chunk-ORRDWKLA.mjs} +1 -1
- package/dist/{token-goat-chunk-F24NE3FO.mjs → token-goat-chunk-PCR43MCM.mjs} +2 -2
- package/dist/token-goat-chunk-PDYQA3AP.mjs +248 -0
- package/dist/{token-goat-chunk-S7RFIX4Y.mjs → token-goat-chunk-PHDNWISV.mjs} +45 -14
- package/dist/token-goat-chunk-PPYZS7TI.mjs +304 -0
- package/dist/{token-goat-chunk-BTKDZW7F.mjs → token-goat-chunk-QTIRVK3M.mjs} +1 -1
- package/dist/{token-goat-chunk-62VJ47WN.mjs → token-goat-chunk-QUM3UNWS.mjs} +5 -5
- package/dist/{token-goat-chunk-KWM6KBPA.mjs → token-goat-chunk-SHV6QQAS.mjs} +1 -1
- package/dist/{token-goat-chunk-FQYMZASI.mjs → token-goat-chunk-SMUYPBF5.mjs} +796 -4464
- package/dist/{token-goat-chunk-ZGQXRYKE.mjs → token-goat-chunk-TGBSMHPM.mjs} +1 -1
- package/dist/{token-goat-chunk-EXLLLIC3.mjs → token-goat-chunk-VXKT3W4C.mjs} +3 -3
- package/dist/{token-goat-chunk-WKO5TVNA.mjs → token-goat-chunk-XJUPKJF5.mjs} +544 -576
- package/dist/{token-goat-chunk-IBTFGZBX.mjs → token-goat-chunk-XQSSPX3X.mjs} +2 -2
- package/dist/{token-goat-chunk-VJQBYSXG.mjs → token-goat-chunk-ZJWPJL7O.mjs} +3 -3
- package/dist/token-goat-hook.mjs +17 -11
- package/dist/token-goat.core.mjs +27 -19
- package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
- package/docs/cli.md +13 -12
- package/docs/install.md +1 -1
- package/docs/security.md +2 -2
- package/package.json +3 -2
- package/dist/token-goat-chunk-NYFYIRG3.mjs +0 -35
|
@@ -0,0 +1,1989 @@
|
|
|
1
|
+
import { createRequire as __cjsRequire } from 'node:module';
|
|
2
|
+
const require = __cjsRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
claudeConfigDir,
|
|
5
|
+
readTranscriptTail
|
|
6
|
+
} from "./token-goat-chunk-MEKVJZUN.mjs";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MAX_AGE_MS,
|
|
9
|
+
DEFAULT_MAX_COUNT,
|
|
10
|
+
recordStat,
|
|
11
|
+
redactSecrets
|
|
12
|
+
} from "./token-goat-chunk-PHDNWISV.mjs";
|
|
13
|
+
import {
|
|
14
|
+
detectHarness,
|
|
15
|
+
isAutoTriggerMultiplierExplicit,
|
|
16
|
+
loadConfig,
|
|
17
|
+
shortFingerprint
|
|
18
|
+
} from "./token-goat-chunk-M6CDVASA.mjs";
|
|
19
|
+
import {
|
|
20
|
+
registerReset
|
|
21
|
+
} from "./token-goat-chunk-EEIDFMEM.mjs";
|
|
22
|
+
import {
|
|
23
|
+
LOCK_WAIT_MS_HARDENED,
|
|
24
|
+
atomicWriteText,
|
|
25
|
+
dataDir,
|
|
26
|
+
ensureDirSync,
|
|
27
|
+
foldPath,
|
|
28
|
+
isCodeFenceDelimiter,
|
|
29
|
+
normalizePathForwardSlash,
|
|
30
|
+
sanitizeIdForFilename,
|
|
31
|
+
stripLower,
|
|
32
|
+
tokenGoatHome,
|
|
33
|
+
withFileLock
|
|
34
|
+
} from "./token-goat-chunk-TGBSMHPM.mjs";
|
|
35
|
+
import {
|
|
36
|
+
normalizePath
|
|
37
|
+
} from "./token-goat-chunk-NMTKNYGF.mjs";
|
|
38
|
+
import {
|
|
39
|
+
init_define_import_meta_env
|
|
40
|
+
} from "./token-goat-chunk-A37V4PBF.mjs";
|
|
41
|
+
|
|
42
|
+
// src/session.ts
|
|
43
|
+
init_define_import_meta_env();
|
|
44
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
45
|
+
import * as fs from "node:fs";
|
|
46
|
+
var _files = /* @__PURE__ */ new Map();
|
|
47
|
+
var _filesAtLoad = /* @__PURE__ */ new Map();
|
|
48
|
+
var _filesFullReadCountAtLoad = /* @__PURE__ */ new Map();
|
|
49
|
+
var _hintsShown = /* @__PURE__ */ new Set();
|
|
50
|
+
var _scheduledPromptCounts = /* @__PURE__ */ new Map();
|
|
51
|
+
var _webFetches = /* @__PURE__ */ new Map();
|
|
52
|
+
var _bashOutputs = /* @__PURE__ */ new Map();
|
|
53
|
+
var _grepQueries = /* @__PURE__ */ new Map();
|
|
54
|
+
var _globQueries = /* @__PURE__ */ new Map();
|
|
55
|
+
var _lastTabContext = null;
|
|
56
|
+
var _seenImageHashes = [];
|
|
57
|
+
var _outstandingAgentSpawns = [];
|
|
58
|
+
var _outstandingAgentSpawnsAtLoad = [];
|
|
59
|
+
var _bashReruns = /* @__PURE__ */ new Set();
|
|
60
|
+
var _curlDownloads = /* @__PURE__ */ new Map();
|
|
61
|
+
var _curlDownloadsAtLoad = /* @__PURE__ */ new Map();
|
|
62
|
+
var _fileLineRanges = /* @__PURE__ */ new Map();
|
|
63
|
+
var _fileServedOutputs = /* @__PURE__ */ new Map();
|
|
64
|
+
var _fileLineRangesAtLoad = /* @__PURE__ */ new Map();
|
|
65
|
+
var _fileServedOutputsAtLoad = /* @__PURE__ */ new Map();
|
|
66
|
+
var _compactedAt = 0;
|
|
67
|
+
var _cliReads = /* @__PURE__ */ new Set();
|
|
68
|
+
var _pendingLargeFileHints = /* @__PURE__ */ new Map();
|
|
69
|
+
var _pendingLargeFileHintsAtLoad = /* @__PURE__ */ new Map();
|
|
70
|
+
var _sessionId = null;
|
|
71
|
+
var _transcriptPath = null;
|
|
72
|
+
function fileSize(absPath) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.statSync(absPath).size;
|
|
75
|
+
} catch {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolveFilesKey(normalized) {
|
|
80
|
+
if (_files.has(normalized)) return normalized;
|
|
81
|
+
const folded = foldPath(normalized);
|
|
82
|
+
if (folded === normalized) return normalized;
|
|
83
|
+
for (const existingKey of _files.keys()) {
|
|
84
|
+
if (foldPath(existingKey) === folded) return existingKey;
|
|
85
|
+
}
|
|
86
|
+
return normalized;
|
|
87
|
+
}
|
|
88
|
+
function recordFileRead(filePath, isFullRead = true) {
|
|
89
|
+
const normalized = normalizePath(filePath);
|
|
90
|
+
const key = resolveFilesKey(normalized);
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const size = fileSize(normalized);
|
|
93
|
+
const prev = _files.get(key);
|
|
94
|
+
if (prev === void 0) {
|
|
95
|
+
_files.set(key, {
|
|
96
|
+
path: key,
|
|
97
|
+
readCount: 1,
|
|
98
|
+
lastReadAt: now,
|
|
99
|
+
wasEdited: false,
|
|
100
|
+
sizeBytes: size,
|
|
101
|
+
...isFullRead ? { lastFullReadAt: now, fullReadCount: 1 } : {}
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
_files.set(key, {
|
|
106
|
+
...prev,
|
|
107
|
+
readCount: prev.readCount + 1,
|
|
108
|
+
lastReadAt: now,
|
|
109
|
+
sizeBytes: size,
|
|
110
|
+
...isFullRead ? { lastFullReadAt: now, fullReadCount: (prev.fullReadCount ?? 0) + 1 } : {}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
var MAX_SYMBOLS_PER_FILE = 25;
|
|
114
|
+
function recordSymbolRead(filePath, symbol) {
|
|
115
|
+
const normalized = normalizePath(filePath);
|
|
116
|
+
const key = resolveFilesKey(normalized);
|
|
117
|
+
const prev = _files.get(key);
|
|
118
|
+
if (prev === void 0) {
|
|
119
|
+
_files.set(key, {
|
|
120
|
+
path: key,
|
|
121
|
+
readCount: 0,
|
|
122
|
+
lastReadAt: Date.now(),
|
|
123
|
+
wasEdited: false,
|
|
124
|
+
sizeBytes: fileSize(normalized),
|
|
125
|
+
symbols_read: [symbol]
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const existing = prev.symbols_read ?? [];
|
|
130
|
+
if (existing.includes(symbol)) return;
|
|
131
|
+
const symbols_read = [...existing, symbol].slice(-MAX_SYMBOLS_PER_FILE);
|
|
132
|
+
_files.set(key, { ...prev, symbols_read });
|
|
133
|
+
}
|
|
134
|
+
function filesReadCountAtLoad() {
|
|
135
|
+
return _filesAtLoad;
|
|
136
|
+
}
|
|
137
|
+
function filesFullReadCountAtLoad() {
|
|
138
|
+
return _filesFullReadCountAtLoad;
|
|
139
|
+
}
|
|
140
|
+
function recordFileEdit(filePath) {
|
|
141
|
+
const normalized = normalizePath(filePath);
|
|
142
|
+
const key = resolveFilesKey(normalized);
|
|
143
|
+
_fileLineRanges.delete(foldPath(normalized));
|
|
144
|
+
const prev = _files.get(key);
|
|
145
|
+
if (prev === void 0) {
|
|
146
|
+
_files.set(key, {
|
|
147
|
+
path: key,
|
|
148
|
+
readCount: 0,
|
|
149
|
+
lastReadAt: 0,
|
|
150
|
+
wasEdited: true,
|
|
151
|
+
sizeBytes: fileSize(normalized)
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
_files.set(key, { ...prev, wasEdited: true });
|
|
156
|
+
}
|
|
157
|
+
function getSessionFiles() {
|
|
158
|
+
return _files;
|
|
159
|
+
}
|
|
160
|
+
function wasFileReadThisSession(filePath) {
|
|
161
|
+
const entry = _files.get(resolveFilesKey(normalizePath(filePath)));
|
|
162
|
+
return entry !== void 0 && entry.readCount > 0 && entry.lastReadAt >= _compactedAt;
|
|
163
|
+
}
|
|
164
|
+
function wasFileFullyReadThisSession(filePath) {
|
|
165
|
+
const entry = _files.get(resolveFilesKey(normalizePath(filePath)));
|
|
166
|
+
return entry !== void 0 && entry.lastFullReadAt !== void 0 && entry.lastFullReadAt >= _compactedAt;
|
|
167
|
+
}
|
|
168
|
+
function getCompactedAt() {
|
|
169
|
+
return _compactedAt;
|
|
170
|
+
}
|
|
171
|
+
function markCompacted(now = Date.now()) {
|
|
172
|
+
if (now <= _compactedAt) return;
|
|
173
|
+
_compactedAt = now;
|
|
174
|
+
_fileLineRanges = /* @__PURE__ */ new Map();
|
|
175
|
+
_fileServedOutputs = /* @__PURE__ */ new Map();
|
|
176
|
+
}
|
|
177
|
+
function getSessionFileEntry(filePath) {
|
|
178
|
+
return _files.get(resolveFilesKey(normalizePath(filePath)));
|
|
179
|
+
}
|
|
180
|
+
function wasHintShown(hintKey) {
|
|
181
|
+
return _hintsShown.has(hintKey);
|
|
182
|
+
}
|
|
183
|
+
function markHintShown(hintKey) {
|
|
184
|
+
_hintsShown.add(hintKey);
|
|
185
|
+
}
|
|
186
|
+
function recordScheduledPrompt(sessionId) {
|
|
187
|
+
const count = (_scheduledPromptCounts.get(sessionId) ?? 0) + 1;
|
|
188
|
+
_scheduledPromptCounts.set(sessionId, count);
|
|
189
|
+
return count;
|
|
190
|
+
}
|
|
191
|
+
function wasCliReadThisSession(cliReadKey) {
|
|
192
|
+
return _cliReads.has(cliReadKey);
|
|
193
|
+
}
|
|
194
|
+
function recordCliRead(cliReadKey) {
|
|
195
|
+
_cliReads.add(cliReadKey);
|
|
196
|
+
}
|
|
197
|
+
function recordLargeFileHintPending(filePath, sizeBytes) {
|
|
198
|
+
_pendingLargeFileHints.set(foldPath(normalizePath(filePath)), sizeBytes);
|
|
199
|
+
}
|
|
200
|
+
function takePendingLargeFileHint(filePath) {
|
|
201
|
+
const key = foldPath(normalizePath(filePath));
|
|
202
|
+
const size = _pendingLargeFileHints.get(key);
|
|
203
|
+
if (size === void 0) return null;
|
|
204
|
+
_pendingLargeFileHints.delete(key);
|
|
205
|
+
return size;
|
|
206
|
+
}
|
|
207
|
+
function consumedPendingLargeFileHintKeys() {
|
|
208
|
+
const consumed = [];
|
|
209
|
+
for (const key of _pendingLargeFileHintsAtLoad.keys()) {
|
|
210
|
+
if (!_pendingLargeFileHints.has(key)) consumed.push(key);
|
|
211
|
+
}
|
|
212
|
+
return consumed;
|
|
213
|
+
}
|
|
214
|
+
function pendingLargeFileHintsAtLoad() {
|
|
215
|
+
return _pendingLargeFileHintsAtLoad;
|
|
216
|
+
}
|
|
217
|
+
var WEB_FETCH_KEY_SEP = "\0";
|
|
218
|
+
function webFetchKey(url, prompt) {
|
|
219
|
+
const digest = shortFingerprint(`${url}${WEB_FETCH_KEY_SEP}${prompt}`);
|
|
220
|
+
return [redactSecrets(url).text, redactSecrets(prompt).text, digest].join(WEB_FETCH_KEY_SEP);
|
|
221
|
+
}
|
|
222
|
+
function recordWebFetch(url, prompt, cacheId) {
|
|
223
|
+
_webFetches.set(webFetchKey(url, prompt), cacheId);
|
|
224
|
+
}
|
|
225
|
+
function migrateCurlDownloadKey(key) {
|
|
226
|
+
if (/^[0-9a-f]{16}$/.test(key)) return key;
|
|
227
|
+
return curlDownloadKey(key);
|
|
228
|
+
}
|
|
229
|
+
function migrateWebFetchKey(key) {
|
|
230
|
+
const parts = key.split(WEB_FETCH_KEY_SEP);
|
|
231
|
+
if (parts.length !== 2) return key;
|
|
232
|
+
return webFetchKey(parts[0] ?? "", parts[1] ?? "");
|
|
233
|
+
}
|
|
234
|
+
function getSessionWebFetches() {
|
|
235
|
+
return _webFetches;
|
|
236
|
+
}
|
|
237
|
+
function recordBashOutput(commandHash, outputId, _sizeBytes) {
|
|
238
|
+
_bashOutputs.set(commandHash, outputId);
|
|
239
|
+
}
|
|
240
|
+
function getSessionBashOutputs() {
|
|
241
|
+
return Array.from(_bashOutputs.entries());
|
|
242
|
+
}
|
|
243
|
+
function recordBashRerun(commandHash) {
|
|
244
|
+
_bashReruns.add(commandHash);
|
|
245
|
+
}
|
|
246
|
+
function getSessionBashReruns() {
|
|
247
|
+
return Array.from(_bashReruns);
|
|
248
|
+
}
|
|
249
|
+
function getBashOutputId(commandHash) {
|
|
250
|
+
return _bashOutputs.get(commandHash) ?? null;
|
|
251
|
+
}
|
|
252
|
+
function recordGrepQuery(signature, matchCount) {
|
|
253
|
+
_grepQueries.set(signature, matchCount);
|
|
254
|
+
}
|
|
255
|
+
function getGrepMatchCount(signature) {
|
|
256
|
+
return _grepQueries.get(signature) ?? null;
|
|
257
|
+
}
|
|
258
|
+
function recordGlobQuery(signature, matchCount) {
|
|
259
|
+
_globQueries.set(signature, matchCount);
|
|
260
|
+
}
|
|
261
|
+
function getGlobMatchCount(signature) {
|
|
262
|
+
return _globQueries.get(signature) ?? null;
|
|
263
|
+
}
|
|
264
|
+
function setLastTabContext(text) {
|
|
265
|
+
_lastTabContext = shortFingerprint(text);
|
|
266
|
+
}
|
|
267
|
+
function lastTabContextMatches(text) {
|
|
268
|
+
return _lastTabContext !== null && _lastTabContext === shortFingerprint(text);
|
|
269
|
+
}
|
|
270
|
+
var MAX_SEEN_IMAGE_HASHES = 16;
|
|
271
|
+
function hasSeenImage(hash) {
|
|
272
|
+
return _seenImageHashes.includes(hash);
|
|
273
|
+
}
|
|
274
|
+
function recordSeenImage(hash) {
|
|
275
|
+
const at = _seenImageHashes.indexOf(hash);
|
|
276
|
+
if (at !== -1) _seenImageHashes.splice(at, 1);
|
|
277
|
+
_seenImageHashes.push(hash);
|
|
278
|
+
if (_seenImageHashes.length > MAX_SEEN_IMAGE_HASHES) {
|
|
279
|
+
_seenImageHashes.splice(0, _seenImageHashes.length - MAX_SEEN_IMAGE_HASHES);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
var MAX_OUTSTANDING_AGENT_SPAWNS = 30;
|
|
283
|
+
function recordOutstandingAgentSpawn(prompt) {
|
|
284
|
+
_outstandingAgentSpawns.push({ prompt: redactSecrets(prompt).text, ts: Date.now() });
|
|
285
|
+
if (_outstandingAgentSpawns.length > MAX_OUTSTANDING_AGENT_SPAWNS) {
|
|
286
|
+
_outstandingAgentSpawns.splice(0, _outstandingAgentSpawns.length - MAX_OUTSTANDING_AGENT_SPAWNS);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function getOutstandingAgentSpawns() {
|
|
290
|
+
return _outstandingAgentSpawns;
|
|
291
|
+
}
|
|
292
|
+
function removeOutstandingAgentSpawn(finishedPrompt) {
|
|
293
|
+
const idx = _outstandingAgentSpawns.findIndex((e) => finishedPrompt.startsWith(e.prompt));
|
|
294
|
+
if (idx !== -1) _outstandingAgentSpawns.splice(idx, 1);
|
|
295
|
+
}
|
|
296
|
+
function outstandingAgentSpawnKey(prompt, ts) {
|
|
297
|
+
return `${prompt} ${ts}`;
|
|
298
|
+
}
|
|
299
|
+
function outstandingAgentSpawnsAtLoad() {
|
|
300
|
+
return _outstandingAgentSpawnsAtLoad;
|
|
301
|
+
}
|
|
302
|
+
function consumedOutstandingAgentSpawnKeys() {
|
|
303
|
+
const currentKeys = new Set(_outstandingAgentSpawns.map((e) => outstandingAgentSpawnKey(e.prompt, e.ts)));
|
|
304
|
+
const consumed = [];
|
|
305
|
+
for (const e of _outstandingAgentSpawnsAtLoad) {
|
|
306
|
+
const key = outstandingAgentSpawnKey(e.prompt, e.ts);
|
|
307
|
+
if (!currentKeys.has(key)) consumed.push(key);
|
|
308
|
+
}
|
|
309
|
+
return consumed;
|
|
310
|
+
}
|
|
311
|
+
function curlDownloadKey(url) {
|
|
312
|
+
return shortFingerprint(url);
|
|
313
|
+
}
|
|
314
|
+
function recordCurlDownload(url, savedPath) {
|
|
315
|
+
_curlDownloads.set(curlDownloadKey(url), savedPath);
|
|
316
|
+
}
|
|
317
|
+
function getCurlDownloadPath(url) {
|
|
318
|
+
return _curlDownloads.get(curlDownloadKey(url)) ?? null;
|
|
319
|
+
}
|
|
320
|
+
function clearCurlDownload(url) {
|
|
321
|
+
_curlDownloads.delete(curlDownloadKey(url));
|
|
322
|
+
}
|
|
323
|
+
function curlDownloadsAtLoad() {
|
|
324
|
+
return _curlDownloadsAtLoad;
|
|
325
|
+
}
|
|
326
|
+
function consumedCurlDownloadKeys() {
|
|
327
|
+
const consumed = [];
|
|
328
|
+
for (const url of _curlDownloadsAtLoad.keys()) {
|
|
329
|
+
if (!_curlDownloads.has(url)) consumed.push(url);
|
|
330
|
+
}
|
|
331
|
+
return consumed;
|
|
332
|
+
}
|
|
333
|
+
function consumedFileLineRangeKeys() {
|
|
334
|
+
const consumed = [];
|
|
335
|
+
for (const key of _fileLineRangesAtLoad.keys()) {
|
|
336
|
+
if (!_fileLineRanges.has(key)) consumed.push(key);
|
|
337
|
+
}
|
|
338
|
+
return consumed;
|
|
339
|
+
}
|
|
340
|
+
function consumedFileServedOutputKeys() {
|
|
341
|
+
const consumed = [];
|
|
342
|
+
for (const key of _fileServedOutputsAtLoad.keys()) {
|
|
343
|
+
if (!_fileServedOutputs.has(key)) consumed.push(key);
|
|
344
|
+
}
|
|
345
|
+
return consumed;
|
|
346
|
+
}
|
|
347
|
+
var MAX_RANGES_PER_FILE = 64;
|
|
348
|
+
function statIdentity(absPath) {
|
|
349
|
+
try {
|
|
350
|
+
const st = fs.statSync(absPath);
|
|
351
|
+
return { size: st.size, mtimeMs: st.mtimeMs };
|
|
352
|
+
} catch {
|
|
353
|
+
return void 0;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function recordFileLineRange(filePath, start, end) {
|
|
357
|
+
const normalized = normalizePath(filePath);
|
|
358
|
+
const key = foldPath(normalized);
|
|
359
|
+
const ranges = _fileLineRanges.get(key) ?? [];
|
|
360
|
+
if (!ranges.some(([s, e]) => s === start && e === end)) {
|
|
361
|
+
ranges.push([start, end]);
|
|
362
|
+
if (ranges.length > MAX_RANGES_PER_FILE) ranges.splice(0, ranges.length - MAX_RANGES_PER_FILE);
|
|
363
|
+
_fileLineRanges.set(key, ranges);
|
|
364
|
+
}
|
|
365
|
+
const entryKey = resolveFilesKey(normalized);
|
|
366
|
+
const entry = _files.get(entryKey);
|
|
367
|
+
const identity = statIdentity(normalized);
|
|
368
|
+
if (entry && identity) _files.set(entryKey, { ...entry, rangeFileIdentity: identity });
|
|
369
|
+
}
|
|
370
|
+
function getFileLineRanges(filePath) {
|
|
371
|
+
return _fileLineRanges.get(foldPath(normalizePath(filePath))) ?? [];
|
|
372
|
+
}
|
|
373
|
+
function resetFileLineRanges(filePath) {
|
|
374
|
+
_fileLineRanges.delete(foldPath(normalizePath(filePath)));
|
|
375
|
+
}
|
|
376
|
+
var MAX_SERVED_OUTPUTS_PER_FILE = 8;
|
|
377
|
+
var GENERIC_SERVED_OUTPUT_KEY = "\0<bash-generic-served>";
|
|
378
|
+
var GENERIC_SERVED_OUTPUT_FOLDED_KEY = foldPath(normalizePath(GENERIC_SERVED_OUTPUT_KEY));
|
|
379
|
+
var MAX_GENERIC_SERVED_OUTPUTS = 32;
|
|
380
|
+
function recordFileServedOutput(filePath, outputId) {
|
|
381
|
+
const key = foldPath(normalizePath(filePath));
|
|
382
|
+
const cap = key === GENERIC_SERVED_OUTPUT_FOLDED_KEY ? MAX_GENERIC_SERVED_OUTPUTS : MAX_SERVED_OUTPUTS_PER_FILE;
|
|
383
|
+
const ids = (_fileServedOutputs.get(key) ?? []).filter((id) => id !== outputId);
|
|
384
|
+
ids.push(outputId);
|
|
385
|
+
if (ids.length > cap) ids.splice(0, ids.length - cap);
|
|
386
|
+
_fileServedOutputs.set(key, ids);
|
|
387
|
+
}
|
|
388
|
+
function getFileServedOutputs(filePath) {
|
|
389
|
+
return _fileServedOutputs.get(foldPath(normalizePath(filePath))) ?? [];
|
|
390
|
+
}
|
|
391
|
+
function markFileTruncated(filePath, isFullRead = true) {
|
|
392
|
+
const normalized = normalizePath(filePath);
|
|
393
|
+
const key = resolveFilesKey(normalized);
|
|
394
|
+
const prev = _files.get(key);
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
if (prev === void 0) {
|
|
397
|
+
_files.set(key, {
|
|
398
|
+
path: key,
|
|
399
|
+
readCount: 1,
|
|
400
|
+
lastReadAt: now,
|
|
401
|
+
wasEdited: false,
|
|
402
|
+
sizeBytes: fileSize(normalized),
|
|
403
|
+
wasTruncated: true,
|
|
404
|
+
...isFullRead ? { lastFullReadAt: now, fullReadCount: 1 } : {}
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
_files.set(key, { ...prev, wasTruncated: true });
|
|
409
|
+
}
|
|
410
|
+
function wasFileTruncatedThisSession(filePath) {
|
|
411
|
+
const entry = _files.get(resolveFilesKey(normalizePath(filePath)));
|
|
412
|
+
return entry?.wasTruncated === true;
|
|
413
|
+
}
|
|
414
|
+
function generateSessionId() {
|
|
415
|
+
try {
|
|
416
|
+
return randomUUID();
|
|
417
|
+
} catch {
|
|
418
|
+
return randomBytes(16).toString("hex");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function getSessionId() {
|
|
422
|
+
if (_sessionId !== null) return _sessionId;
|
|
423
|
+
const fromEnv = process.env["CLAUDE_CODE_SESSION_ID"];
|
|
424
|
+
_sessionId = fromEnv !== void 0 && fromEnv !== "" ? fromEnv : generateSessionId();
|
|
425
|
+
return _sessionId;
|
|
426
|
+
}
|
|
427
|
+
function setTranscriptPath(path4, sessionId) {
|
|
428
|
+
if (path4 === "") return;
|
|
429
|
+
_transcriptPath = { path: path4, sessionId };
|
|
430
|
+
}
|
|
431
|
+
function getTranscriptPath() {
|
|
432
|
+
if (_transcriptPath === null) return void 0;
|
|
433
|
+
return _transcriptPath.sessionId === getSessionId() ? _transcriptPath.path : void 0;
|
|
434
|
+
}
|
|
435
|
+
function exportSessionState() {
|
|
436
|
+
return {
|
|
437
|
+
files: Array.from(_files.values()),
|
|
438
|
+
hintsShown: Array.from(_hintsShown),
|
|
439
|
+
..._scheduledPromptCounts.size > 0 ? { scheduledPromptCounts: Array.from(_scheduledPromptCounts.entries()) } : {},
|
|
440
|
+
webFetches: Array.from(_webFetches.entries()),
|
|
441
|
+
bashOutputs: Array.from(_bashOutputs.entries()),
|
|
442
|
+
bashReruns: Array.from(_bashReruns),
|
|
443
|
+
curlDownloads: Array.from(_curlDownloads.entries()),
|
|
444
|
+
fileLineRanges: Array.from(_fileLineRanges.entries()),
|
|
445
|
+
..._fileServedOutputs.size > 0 ? { fileServedOutputs: Array.from(_fileServedOutputs.entries()) } : {},
|
|
446
|
+
cliReads: Array.from(_cliReads),
|
|
447
|
+
pendingLargeFileHints: Array.from(_pendingLargeFileHints.entries()),
|
|
448
|
+
grepQueries: Array.from(_grepQueries.entries()),
|
|
449
|
+
globQueries: Array.from(_globQueries.entries()),
|
|
450
|
+
outstandingAgentSpawns: _outstandingAgentSpawns.map((e) => [e.prompt, e.ts]),
|
|
451
|
+
..._lastTabContext !== null ? { lastTabContextDigest: _lastTabContext } : {},
|
|
452
|
+
..._seenImageHashes.length > 0 ? { seenImageHashes: [..._seenImageHashes] } : {},
|
|
453
|
+
..._compactedAt > 0 ? { compactedAt: _compactedAt } : {}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function importSessionState(s) {
|
|
457
|
+
_files = /* @__PURE__ */ new Map();
|
|
458
|
+
for (const e of s.files) {
|
|
459
|
+
if (e && typeof e.path === "string") _files.set(e.path, e);
|
|
460
|
+
}
|
|
461
|
+
_filesAtLoad = new Map(Array.from(_files, ([key, e]) => [key, e.readCount]));
|
|
462
|
+
_filesFullReadCountAtLoad = new Map(Array.from(_files, ([key, e]) => [key, e.fullReadCount ?? 0]));
|
|
463
|
+
_hintsShown = new Set(s.hintsShown);
|
|
464
|
+
_scheduledPromptCounts = new Map(s.scheduledPromptCounts ?? []);
|
|
465
|
+
_webFetches = new Map(s.webFetches);
|
|
466
|
+
_bashOutputs = new Map(s.bashOutputs);
|
|
467
|
+
_bashReruns = new Set(s.bashReruns ?? []);
|
|
468
|
+
_curlDownloads = new Map(s.curlDownloads);
|
|
469
|
+
_curlDownloadsAtLoad = new Map(_curlDownloads);
|
|
470
|
+
_fileLineRanges = new Map(s.fileLineRanges ?? []);
|
|
471
|
+
_fileLineRangesAtLoad = new Map(_fileLineRanges);
|
|
472
|
+
_fileServedOutputs = new Map(s.fileServedOutputs ?? []);
|
|
473
|
+
_fileServedOutputsAtLoad = new Map(_fileServedOutputs);
|
|
474
|
+
_cliReads = new Set(s.cliReads ?? []);
|
|
475
|
+
_pendingLargeFileHints = new Map(s.pendingLargeFileHints ?? []);
|
|
476
|
+
_pendingLargeFileHintsAtLoad = new Map(_pendingLargeFileHints);
|
|
477
|
+
_grepQueries = new Map(s.grepQueries ?? []);
|
|
478
|
+
_globQueries = new Map(s.globQueries ?? []);
|
|
479
|
+
_outstandingAgentSpawns = (s.outstandingAgentSpawns ?? []).map(([prompt, ts]) => ({ prompt, ts }));
|
|
480
|
+
_outstandingAgentSpawnsAtLoad = [..._outstandingAgentSpawns];
|
|
481
|
+
_lastTabContext = s.lastTabContextDigest ?? null;
|
|
482
|
+
_seenImageHashes = [...s.seenImageHashes ?? []];
|
|
483
|
+
_compactedAt = s.compactedAt ?? 0;
|
|
484
|
+
}
|
|
485
|
+
registerReset(() => {
|
|
486
|
+
_files = /* @__PURE__ */ new Map();
|
|
487
|
+
_filesAtLoad = /* @__PURE__ */ new Map();
|
|
488
|
+
_filesFullReadCountAtLoad = /* @__PURE__ */ new Map();
|
|
489
|
+
_hintsShown = /* @__PURE__ */ new Set();
|
|
490
|
+
_scheduledPromptCounts = /* @__PURE__ */ new Map();
|
|
491
|
+
_webFetches = /* @__PURE__ */ new Map();
|
|
492
|
+
_bashOutputs = /* @__PURE__ */ new Map();
|
|
493
|
+
_bashReruns = /* @__PURE__ */ new Set();
|
|
494
|
+
_curlDownloads = /* @__PURE__ */ new Map();
|
|
495
|
+
_curlDownloadsAtLoad = /* @__PURE__ */ new Map();
|
|
496
|
+
_fileLineRanges = /* @__PURE__ */ new Map();
|
|
497
|
+
_fileLineRangesAtLoad = /* @__PURE__ */ new Map();
|
|
498
|
+
_fileServedOutputs = /* @__PURE__ */ new Map();
|
|
499
|
+
_fileServedOutputsAtLoad = /* @__PURE__ */ new Map();
|
|
500
|
+
_cliReads = /* @__PURE__ */ new Set();
|
|
501
|
+
_pendingLargeFileHints = /* @__PURE__ */ new Map();
|
|
502
|
+
_pendingLargeFileHintsAtLoad = /* @__PURE__ */ new Map();
|
|
503
|
+
_grepQueries = /* @__PURE__ */ new Map();
|
|
504
|
+
_globQueries = /* @__PURE__ */ new Map();
|
|
505
|
+
_outstandingAgentSpawns = [];
|
|
506
|
+
_outstandingAgentSpawnsAtLoad = [];
|
|
507
|
+
_lastTabContext = null;
|
|
508
|
+
_seenImageHashes = [];
|
|
509
|
+
_compactedAt = 0;
|
|
510
|
+
_sessionId = null;
|
|
511
|
+
_transcriptPath = null;
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// src/sessions_dir.ts
|
|
515
|
+
init_define_import_meta_env();
|
|
516
|
+
import * as path from "node:path";
|
|
517
|
+
var SESSIONS_SUBDIR = "sessions";
|
|
518
|
+
function sessionsDir() {
|
|
519
|
+
return path.join(tokenGoatHome(), SESSIONS_SUBDIR);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/session_store.ts
|
|
523
|
+
init_define_import_meta_env();
|
|
524
|
+
import { createHash } from "node:crypto";
|
|
525
|
+
import * as fs2 from "node:fs";
|
|
526
|
+
import * as path2 from "node:path";
|
|
527
|
+
var MAX_FILES = 500;
|
|
528
|
+
var AGENT_SALT_MARKER = sanitizeIdForFilename(":agent:");
|
|
529
|
+
var AGENT_SALT_SEPARATOR = ":agent:";
|
|
530
|
+
var AGENT_DIGEST_CHARS = 12;
|
|
531
|
+
var SALTED_SESSION_MAX = 64 - AGENT_SALT_MARKER.length - AGENT_DIGEST_CHARS;
|
|
532
|
+
function saltedStemPrefix(sessionId) {
|
|
533
|
+
return `${sanitizeIdForFilename(sessionId, SALTED_SESSION_MAX)}${AGENT_SALT_MARKER}`;
|
|
534
|
+
}
|
|
535
|
+
function sessionFileStem(sessionId) {
|
|
536
|
+
const sep = sessionId.indexOf(AGENT_SALT_SEPARATOR);
|
|
537
|
+
if (sep < 0) return sanitizeIdForFilename(sessionId, 64);
|
|
538
|
+
const agentId = sessionId.slice(sep + AGENT_SALT_SEPARATOR.length);
|
|
539
|
+
const digest = createHash("sha256").update(agentId).digest("hex").slice(0, AGENT_DIGEST_CHARS);
|
|
540
|
+
return `${saltedStemPrefix(sessionId.slice(0, sep))}${digest}`;
|
|
541
|
+
}
|
|
542
|
+
function sessionPath(sessionId) {
|
|
543
|
+
return sessionSidecarPath(sessionId, ".json");
|
|
544
|
+
}
|
|
545
|
+
function sessionSidecarPath(sessionId, suffix) {
|
|
546
|
+
if (!sessionId) return null;
|
|
547
|
+
const safe = sessionFileStem(sessionId);
|
|
548
|
+
if (!safe) return null;
|
|
549
|
+
if (suffix.includes("/") || suffix.includes("\\") || suffix.includes("..")) return null;
|
|
550
|
+
const dir = sessionsDir();
|
|
551
|
+
const candidate = path2.join(dir, `${safe}${suffix}`);
|
|
552
|
+
try {
|
|
553
|
+
const rel = path2.relative(dir, candidate);
|
|
554
|
+
if (rel.startsWith("..")) return null;
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
return candidate;
|
|
559
|
+
}
|
|
560
|
+
function asStringPairs(raw) {
|
|
561
|
+
if (!Array.isArray(raw)) return [];
|
|
562
|
+
const out = [];
|
|
563
|
+
for (const item of raw) {
|
|
564
|
+
if (Array.isArray(item) && typeof item[0] === "string" && typeof item[1] === "string") {
|
|
565
|
+
out.push([item[0], item[1]]);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
function asFileEntry(raw) {
|
|
571
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
572
|
+
const o = raw;
|
|
573
|
+
if (typeof o["path"] !== "string" || typeof o["readCount"] !== "number" || typeof o["lastReadAt"] !== "number" || typeof o["wasEdited"] !== "boolean" || typeof o["sizeBytes"] !== "number") {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
let entry = {
|
|
577
|
+
path: o["path"],
|
|
578
|
+
readCount: o["readCount"],
|
|
579
|
+
lastReadAt: o["lastReadAt"],
|
|
580
|
+
wasEdited: o["wasEdited"],
|
|
581
|
+
sizeBytes: o["sizeBytes"]
|
|
582
|
+
};
|
|
583
|
+
if (o["wasTruncated"] === true) entry = { ...entry, wasTruncated: true };
|
|
584
|
+
if (typeof o["lastFullReadAt"] === "number") entry = { ...entry, lastFullReadAt: o["lastFullReadAt"] };
|
|
585
|
+
if (typeof o["fullReadCount"] === "number") entry = { ...entry, fullReadCount: o["fullReadCount"] };
|
|
586
|
+
if (Array.isArray(o["symbols_read"])) {
|
|
587
|
+
const symbols = o["symbols_read"].filter((s) => typeof s === "string");
|
|
588
|
+
if (symbols.length > 0) entry = { ...entry, symbols_read: symbols };
|
|
589
|
+
}
|
|
590
|
+
const rangeIdentity = o["rangeFileIdentity"];
|
|
591
|
+
if (rangeIdentity !== null && typeof rangeIdentity === "object" && typeof rangeIdentity["size"] === "number" && typeof rangeIdentity["mtimeMs"] === "number") {
|
|
592
|
+
entry = { ...entry, rangeFileIdentity: { size: rangeIdentity["size"], mtimeMs: rangeIdentity["mtimeMs"] } };
|
|
593
|
+
}
|
|
594
|
+
return entry;
|
|
595
|
+
}
|
|
596
|
+
function asPyFileEntry(dictKey, raw) {
|
|
597
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
598
|
+
const v = raw;
|
|
599
|
+
const p = typeof v["rel_or_abs"] === "string" ? v["rel_or_abs"] : dictKey;
|
|
600
|
+
if (!p) return null;
|
|
601
|
+
const readCount = typeof v["read_count"] === "number" ? Math.max(1, Math.round(v["read_count"])) : 1;
|
|
602
|
+
const lastReadTs = typeof v["last_read_ts"] === "number" ? v["last_read_ts"] : 0;
|
|
603
|
+
const sizeBytes = typeof v["read_size"] === "number" ? Math.round(v["read_size"]) : 0;
|
|
604
|
+
const wasEdited = typeof v["last_edit_ts"] === "number" && v["last_edit_ts"] > 0;
|
|
605
|
+
return { path: p, readCount, lastReadAt: lastReadTs * 1e3, wasEdited, sizeBytes };
|
|
606
|
+
}
|
|
607
|
+
function asLineRanges(raw) {
|
|
608
|
+
if (!Array.isArray(raw)) return [];
|
|
609
|
+
const out = [];
|
|
610
|
+
for (const pair of raw) {
|
|
611
|
+
if (!Array.isArray(pair) || pair.length !== 2) continue;
|
|
612
|
+
const filePath = pair[0];
|
|
613
|
+
const ranges = pair[1];
|
|
614
|
+
if (typeof filePath !== "string" || !Array.isArray(ranges)) continue;
|
|
615
|
+
const valid = [];
|
|
616
|
+
for (const r of ranges) {
|
|
617
|
+
if (Array.isArray(r) && r.length === 2 && typeof r[0] === "number" && typeof r[1] === "number") valid.push([r[0], r[1]]);
|
|
618
|
+
}
|
|
619
|
+
out.push([filePath, valid]);
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function asServedOutputs(raw) {
|
|
624
|
+
if (!Array.isArray(raw)) return [];
|
|
625
|
+
const out = [];
|
|
626
|
+
for (const pair of raw) {
|
|
627
|
+
if (!Array.isArray(pair) || pair.length !== 2) continue;
|
|
628
|
+
const filePath = pair[0];
|
|
629
|
+
const ids = pair[1];
|
|
630
|
+
if (typeof filePath !== "string" || !Array.isArray(ids)) continue;
|
|
631
|
+
out.push([filePath, ids.filter((id) => typeof id === "string")]);
|
|
632
|
+
}
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
function migrateKeys(pairs, migrate) {
|
|
636
|
+
return pairs.map(([key, value]) => [migrate(key), value]);
|
|
637
|
+
}
|
|
638
|
+
function coerce(raw) {
|
|
639
|
+
const o = raw !== null && typeof raw === "object" ? raw : {};
|
|
640
|
+
const files = [];
|
|
641
|
+
if (Array.isArray(o["files"])) {
|
|
642
|
+
for (const f of o["files"]) {
|
|
643
|
+
const e = asFileEntry(f);
|
|
644
|
+
if (e !== null) files.push(e);
|
|
645
|
+
}
|
|
646
|
+
} else if (o["files"] !== null && typeof o["files"] === "object") {
|
|
647
|
+
for (const [key, val] of Object.entries(o["files"])) {
|
|
648
|
+
const e = asPyFileEntry(key, val);
|
|
649
|
+
if (e !== null) files.push(e);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const hintsShown = Array.isArray(o["hintsShown"]) ? o["hintsShown"].filter((h) => typeof h === "string") : Array.isArray(o["hints_seen"]) ? o["hints_seen"].filter((h) => typeof h === "string") : [];
|
|
653
|
+
const scheduledPromptCounts = Array.isArray(o["scheduledPromptCounts"]) ? o["scheduledPromptCounts"].filter(
|
|
654
|
+
(p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string" && typeof p[1] === "number" && Number.isSafeInteger(p[1]) && p[1] >= 0
|
|
655
|
+
) : [];
|
|
656
|
+
const cliReads = Array.isArray(o["cliReads"]) ? o["cliReads"].filter((h) => typeof h === "string") : [];
|
|
657
|
+
const bashReruns = Array.isArray(o["bashReruns"]) ? o["bashReruns"].filter((h) => typeof h === "string") : [];
|
|
658
|
+
const seenImageHashes = Array.isArray(o["seenImageHashes"]) ? o["seenImageHashes"].filter((h) => typeof h === "string") : [];
|
|
659
|
+
const pendingLargeFileHints = Array.isArray(o["pendingLargeFileHints"]) ? o["pendingLargeFileHints"].filter(
|
|
660
|
+
(p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string" && typeof p[1] === "number"
|
|
661
|
+
) : [];
|
|
662
|
+
const grepQueries = Array.isArray(o["grepQueries"]) ? o["grepQueries"].filter(
|
|
663
|
+
(p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string" && typeof p[1] === "number"
|
|
664
|
+
) : [];
|
|
665
|
+
const globQueries = Array.isArray(o["globQueries"]) ? o["globQueries"].filter(
|
|
666
|
+
(p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string" && typeof p[1] === "number"
|
|
667
|
+
) : [];
|
|
668
|
+
const outstandingAgentSpawns = Array.isArray(o["outstandingAgentSpawns"]) ? o["outstandingAgentSpawns"].filter(
|
|
669
|
+
(p) => Array.isArray(p) && p.length === 2 && typeof p[0] === "string" && typeof p[1] === "number"
|
|
670
|
+
) : [];
|
|
671
|
+
return {
|
|
672
|
+
files,
|
|
673
|
+
hintsShown,
|
|
674
|
+
...scheduledPromptCounts.length > 0 ? { scheduledPromptCounts } : {},
|
|
675
|
+
webFetches: migrateKeys(asStringPairs(o["webFetches"]), migrateWebFetchKey),
|
|
676
|
+
bashOutputs: asStringPairs(o["bashOutputs"]),
|
|
677
|
+
curlDownloads: migrateKeys(asStringPairs(o["curlDownloads"]), migrateCurlDownloadKey),
|
|
678
|
+
fileLineRanges: asLineRanges(o["fileLineRanges"]),
|
|
679
|
+
fileServedOutputs: asServedOutputs(o["fileServedOutputs"]),
|
|
680
|
+
cliReads,
|
|
681
|
+
bashReruns,
|
|
682
|
+
pendingLargeFileHints,
|
|
683
|
+
grepQueries,
|
|
684
|
+
globQueries,
|
|
685
|
+
outstandingAgentSpawns,
|
|
686
|
+
...typeof o["lastTabContextDigest"] === "string" ? { lastTabContextDigest: o["lastTabContextDigest"] } : {},
|
|
687
|
+
...seenImageHashes.length > 0 ? { seenImageHashes } : {},
|
|
688
|
+
...typeof o["compactedAt"] === "number" ? { compactedAt: o["compactedAt"] } : {},
|
|
689
|
+
...typeof o["created_ts"] === "number" ? { created_ts: o["created_ts"] } : {}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function mergeFileEntry(a, b) {
|
|
693
|
+
const newest = a.lastReadAt >= b.lastReadAt ? a : b;
|
|
694
|
+
const baseline = filesReadCountAtLoad().get(b.path) ?? 0;
|
|
695
|
+
const newReadsThisProcess = Math.max(0, b.readCount - baseline);
|
|
696
|
+
let merged = {
|
|
697
|
+
path: a.path,
|
|
698
|
+
readCount: a.readCount + newReadsThisProcess,
|
|
699
|
+
lastReadAt: Math.max(a.lastReadAt, b.lastReadAt),
|
|
700
|
+
wasEdited: a.wasEdited || b.wasEdited,
|
|
701
|
+
sizeBytes: newest.sizeBytes
|
|
702
|
+
};
|
|
703
|
+
if (newest.rangeFileIdentity) merged = { ...merged, rangeFileIdentity: newest.rangeFileIdentity };
|
|
704
|
+
if (a.wasTruncated || b.wasTruncated) merged = { ...merged, wasTruncated: true };
|
|
705
|
+
const lastFullReadAt = Math.max(a.lastFullReadAt ?? 0, b.lastFullReadAt ?? 0);
|
|
706
|
+
if (lastFullReadAt > 0) merged = { ...merged, lastFullReadAt };
|
|
707
|
+
const fullReadBaseline = filesFullReadCountAtLoad().get(b.path) ?? 0;
|
|
708
|
+
const newFullReadsThisProcess = Math.max(0, (b.fullReadCount ?? 0) - fullReadBaseline);
|
|
709
|
+
const fullReadCount = (a.fullReadCount ?? 0) + newFullReadsThisProcess;
|
|
710
|
+
if (fullReadCount > 0) merged = { ...merged, fullReadCount };
|
|
711
|
+
const symbols = Array.from(/* @__PURE__ */ new Set([...a.symbols_read ?? [], ...b.symbols_read ?? []]));
|
|
712
|
+
if (symbols.length > 0) merged = { ...merged, symbols_read: symbols };
|
|
713
|
+
return merged;
|
|
714
|
+
}
|
|
715
|
+
function mergePairs(disk, mem) {
|
|
716
|
+
return Array.from(new Map([...disk, ...mem]).entries());
|
|
717
|
+
}
|
|
718
|
+
function mergeMaxNumberPairs(disk, mem) {
|
|
719
|
+
const merged = new Map(disk);
|
|
720
|
+
for (const [key, value] of mem) {
|
|
721
|
+
merged.set(key, Math.max(merged.get(key) ?? 0, value));
|
|
722
|
+
}
|
|
723
|
+
return Array.from(merged.entries());
|
|
724
|
+
}
|
|
725
|
+
function mergeLineRanges(disk, mem) {
|
|
726
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
727
|
+
for (const [filePath, ranges] of disk) byPath.set(filePath, [...ranges]);
|
|
728
|
+
for (const [filePath, ranges] of mem) {
|
|
729
|
+
const prev = byPath.get(filePath) ?? [];
|
|
730
|
+
const seen = new Set(prev.map(([s, e]) => s + ":" + e));
|
|
731
|
+
for (const [s, e] of ranges) {
|
|
732
|
+
const key = s + ":" + e;
|
|
733
|
+
if (seen.has(key) || prev.length >= MAX_RANGES_PER_FILE) continue;
|
|
734
|
+
prev.push([s, e]);
|
|
735
|
+
seen.add(key);
|
|
736
|
+
}
|
|
737
|
+
byPath.set(filePath, prev);
|
|
738
|
+
}
|
|
739
|
+
return Array.from(byPath.entries());
|
|
740
|
+
}
|
|
741
|
+
var GENERIC_SERVED_OUTPUT_FOLDED_KEY2 = foldPath(normalizePath(GENERIC_SERVED_OUTPUT_KEY));
|
|
742
|
+
function mergeServedOutputs(disk, mem) {
|
|
743
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
744
|
+
for (const [filePath, ids] of disk) byPath.set(filePath, [...ids]);
|
|
745
|
+
for (const [filePath, ids] of mem) {
|
|
746
|
+
const prev = byPath.get(filePath) ?? [];
|
|
747
|
+
const merged = [...prev.filter((id) => !ids.includes(id)), ...ids];
|
|
748
|
+
const cap = filePath === GENERIC_SERVED_OUTPUT_FOLDED_KEY2 ? MAX_GENERIC_SERVED_OUTPUTS : MAX_SERVED_OUTPUTS_PER_FILE;
|
|
749
|
+
if (merged.length > cap) merged.splice(0, merged.length - cap);
|
|
750
|
+
byPath.set(filePath, merged);
|
|
751
|
+
}
|
|
752
|
+
return Array.from(byPath.entries());
|
|
753
|
+
}
|
|
754
|
+
function mergePendingLargeFileHints(disk, mem) {
|
|
755
|
+
const merged = new Map(disk);
|
|
756
|
+
for (const key of consumedPendingLargeFileHintKeys()) merged.delete(key);
|
|
757
|
+
const atLoad = pendingLargeFileHintsAtLoad();
|
|
758
|
+
for (const [key, size] of mem) {
|
|
759
|
+
if (atLoad.get(key) === size) continue;
|
|
760
|
+
merged.set(key, size);
|
|
761
|
+
}
|
|
762
|
+
return Array.from(merged.entries());
|
|
763
|
+
}
|
|
764
|
+
function mergeCurlDownloads(disk, mem) {
|
|
765
|
+
const merged = new Map(disk);
|
|
766
|
+
for (const key of consumedCurlDownloadKeys()) merged.delete(key);
|
|
767
|
+
const atLoad = curlDownloadsAtLoad();
|
|
768
|
+
for (const [url, savedPath] of mem) {
|
|
769
|
+
if (atLoad.get(url) === savedPath) continue;
|
|
770
|
+
merged.set(url, savedPath);
|
|
771
|
+
}
|
|
772
|
+
return Array.from(merged.entries());
|
|
773
|
+
}
|
|
774
|
+
function mergeOutstandingAgentSpawns(disk, mem) {
|
|
775
|
+
const merged = new Map(disk.map(([prompt, ts]) => [outstandingAgentSpawnKey(prompt, ts), [prompt, ts]]));
|
|
776
|
+
for (const key of consumedOutstandingAgentSpawnKeys()) merged.delete(key);
|
|
777
|
+
const atLoad = new Set(outstandingAgentSpawnsAtLoad().map((e) => outstandingAgentSpawnKey(e.prompt, e.ts)));
|
|
778
|
+
for (const [prompt, ts] of mem) {
|
|
779
|
+
const key = outstandingAgentSpawnKey(prompt, ts);
|
|
780
|
+
if (atLoad.has(key)) continue;
|
|
781
|
+
merged.set(key, [prompt, ts]);
|
|
782
|
+
}
|
|
783
|
+
const result = Array.from(merged.values()).sort((a, b) => a[1] - b[1]);
|
|
784
|
+
if (result.length > MAX_OUTSTANDING_AGENT_SPAWNS) {
|
|
785
|
+
result.splice(0, result.length - MAX_OUTSTANDING_AGENT_SPAWNS);
|
|
786
|
+
}
|
|
787
|
+
return result;
|
|
788
|
+
}
|
|
789
|
+
function mergeSessionState(disk, mem) {
|
|
790
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
791
|
+
for (const e of disk.files) byPath.set(foldPath(e.path), e);
|
|
792
|
+
for (const e of mem.files) {
|
|
793
|
+
const key = foldPath(e.path);
|
|
794
|
+
const prev = byPath.get(key);
|
|
795
|
+
byPath.set(key, prev ? mergeFileEntry(prev, e) : e);
|
|
796
|
+
}
|
|
797
|
+
const compactedAt = Math.max(disk.compactedAt ?? 0, mem.compactedAt ?? 0);
|
|
798
|
+
const rawDiskRanges = (disk.compactedAt ?? 0) === compactedAt ? disk.fileLineRanges ?? [] : [];
|
|
799
|
+
const memRanges = (mem.compactedAt ?? 0) === compactedAt ? mem.fileLineRanges ?? [] : [];
|
|
800
|
+
const rawDiskServed = (disk.compactedAt ?? 0) === compactedAt ? disk.fileServedOutputs ?? [] : [];
|
|
801
|
+
const memServed = (mem.compactedAt ?? 0) === compactedAt ? mem.fileServedOutputs ?? [] : [];
|
|
802
|
+
const clearedRangeFiles = new Set(consumedFileLineRangeKeys());
|
|
803
|
+
const clearedServedFiles = new Set(consumedFileServedOutputKeys());
|
|
804
|
+
const diskRanges = rawDiskRanges.filter(([path4]) => !clearedRangeFiles.has(path4));
|
|
805
|
+
const diskServed = rawDiskServed.filter(([path4]) => !clearedServedFiles.has(path4));
|
|
806
|
+
return {
|
|
807
|
+
files: Array.from(byPath.values()),
|
|
808
|
+
hintsShown: Array.from(/* @__PURE__ */ new Set([...disk.hintsShown, ...mem.hintsShown])),
|
|
809
|
+
scheduledPromptCounts: mergeMaxNumberPairs(disk.scheduledPromptCounts ?? [], mem.scheduledPromptCounts ?? []),
|
|
810
|
+
webFetches: mergePairs(disk.webFetches, mem.webFetches),
|
|
811
|
+
bashOutputs: mergePairs(disk.bashOutputs, mem.bashOutputs),
|
|
812
|
+
curlDownloads: mergeCurlDownloads(disk.curlDownloads, mem.curlDownloads),
|
|
813
|
+
fileLineRanges: mergeLineRanges(diskRanges, memRanges),
|
|
814
|
+
fileServedOutputs: mergeServedOutputs(diskServed, memServed),
|
|
815
|
+
...compactedAt > 0 ? { compactedAt } : {},
|
|
816
|
+
cliReads: Array.from(/* @__PURE__ */ new Set([...disk.cliReads ?? [], ...mem.cliReads ?? []])),
|
|
817
|
+
bashReruns: Array.from(/* @__PURE__ */ new Set([...disk.bashReruns ?? [], ...mem.bashReruns ?? []])),
|
|
818
|
+
pendingLargeFileHints: mergePendingLargeFileHints(disk.pendingLargeFileHints ?? [], mem.pendingLargeFileHints ?? []),
|
|
819
|
+
grepQueries: mergePairs(disk.grepQueries ?? [], mem.grepQueries ?? []),
|
|
820
|
+
globQueries: mergePairs(disk.globQueries ?? [], mem.globQueries ?? []),
|
|
821
|
+
outstandingAgentSpawns: mergeOutstandingAgentSpawns(disk.outstandingAgentSpawns ?? [], mem.outstandingAgentSpawns ?? []),
|
|
822
|
+
// An accumulating collection, so union rather than pick a winner: two hook processes can each see a screenshot the other never did. Disk first, then mem, so the order stays oldest-to-newest and the cap evicts the oldest -- the same policy recordSeenImage applies in memory, applied again here because a union of two capped lists can exceed the cap.
|
|
823
|
+
seenImageHashes: mergeSeenImageHashes(disk.seenImageHashes ?? [], mem.seenImageHashes ?? []),
|
|
824
|
+
// Last-seen scalar, not an accumulating collection: prefer mem's value (this process's freshest observation) over disk's, since a newer write always supersedes an older one.
|
|
825
|
+
...mem.lastTabContextDigest !== void 0 ? { lastTabContextDigest: mem.lastTabContextDigest } : disk.lastTabContextDigest !== void 0 ? { lastTabContextDigest: disk.lastTabContextDigest } : {},
|
|
826
|
+
// Prefer the value already on disk: it marks the original creation time, and must never be bumped forward to the merge's "now". `mem` never carries one (it is not tracked in memory), so this is really "keep whatever disk has".
|
|
827
|
+
...disk.created_ts !== void 0 ? { created_ts: disk.created_ts } : mem.created_ts !== void 0 ? { created_ts: mem.created_ts } : {}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function mergeSeenImageHashes(disk, mem) {
|
|
831
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...disk, ...mem]));
|
|
832
|
+
return merged.length > MAX_SEEN_IMAGE_HASHES ? merged.slice(merged.length - MAX_SEEN_IMAGE_HASHES) : merged;
|
|
833
|
+
}
|
|
834
|
+
function capFiles(s, max) {
|
|
835
|
+
if (s.files.length <= max) return s;
|
|
836
|
+
const kept = [...s.files].sort((a, b) => a.wasEdited === b.wasEdited ? b.lastReadAt - a.lastReadAt : a.wasEdited ? -1 : 1).slice(0, max);
|
|
837
|
+
return { ...s, files: kept };
|
|
838
|
+
}
|
|
839
|
+
function readDiskState(p) {
|
|
840
|
+
try {
|
|
841
|
+
if (!fs2.existsSync(p)) return null;
|
|
842
|
+
return coerce(JSON.parse(fs2.readFileSync(p, "utf8")));
|
|
843
|
+
} catch {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function readSessionStateFile(sessionId) {
|
|
848
|
+
const p = sessionPath(sessionId);
|
|
849
|
+
if (!p) return null;
|
|
850
|
+
return readDiskState(p);
|
|
851
|
+
}
|
|
852
|
+
function listSiblingSessionStates(sessionId) {
|
|
853
|
+
if (!sessionId) return [];
|
|
854
|
+
const prefix = saltedStemPrefix(sessionId);
|
|
855
|
+
if (prefix === AGENT_SALT_MARKER) return [];
|
|
856
|
+
const dir = sessionsDir();
|
|
857
|
+
const out = [];
|
|
858
|
+
try {
|
|
859
|
+
if (!fs2.existsSync(dir)) return out;
|
|
860
|
+
for (const file of fs2.readdirSync(dir)) {
|
|
861
|
+
if (!file.endsWith(".json") || !file.startsWith(prefix)) continue;
|
|
862
|
+
const state = readDiskState(path2.join(dir, file));
|
|
863
|
+
if (state !== null) out.push(state);
|
|
864
|
+
}
|
|
865
|
+
} catch {
|
|
866
|
+
return out;
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
function loadSessionState(sessionId) {
|
|
871
|
+
const p = sessionPath(sessionId);
|
|
872
|
+
if (!p) return;
|
|
873
|
+
const disk = readDiskState(p);
|
|
874
|
+
if (disk === null) return;
|
|
875
|
+
importSessionState(disk);
|
|
876
|
+
}
|
|
877
|
+
function saveSessionState(sessionId) {
|
|
878
|
+
const p = sessionPath(sessionId);
|
|
879
|
+
if (!p) return;
|
|
880
|
+
try {
|
|
881
|
+
const dir = path2.dirname(p);
|
|
882
|
+
if (!fs2.existsSync(dir)) ensureDirSync(dir);
|
|
883
|
+
const mem = exportSessionState();
|
|
884
|
+
const writeMerged = () => {
|
|
885
|
+
const disk = readDiskState(p);
|
|
886
|
+
const merged = capFiles(disk ? mergeSessionState(disk, mem) : mem, MAX_FILES);
|
|
887
|
+
if (merged.created_ts === void 0) merged.created_ts = Date.now() / 1e3;
|
|
888
|
+
let json;
|
|
889
|
+
try {
|
|
890
|
+
json = redactSecrets(JSON.stringify(merged)).text;
|
|
891
|
+
} catch {
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
atomicWriteText(p, json);
|
|
895
|
+
return true;
|
|
896
|
+
};
|
|
897
|
+
if (withFileLock(`${p}.lock`, writeMerged, { waitMs: LOCK_WAIT_MS_HARDENED }) === void 0) writeMerged();
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/skill_cache.ts
|
|
903
|
+
init_define_import_meta_env();
|
|
904
|
+
import * as fs3 from "fs/promises";
|
|
905
|
+
import { resolve } from "path";
|
|
906
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2, unlinkSync } from "node:fs";
|
|
907
|
+
var COMPACT_END_MARKER = "<!-- COMPACT_END -->";
|
|
908
|
+
var SKILLS_OUTPUT_SUBDIR = "skills";
|
|
909
|
+
var _skillOutputsDirOverride = null;
|
|
910
|
+
var _skillsSourceDirOverride = null;
|
|
911
|
+
var _pluginsManifestPathOverride = null;
|
|
912
|
+
registerReset(() => {
|
|
913
|
+
_skillOutputsDirOverride = null;
|
|
914
|
+
_skillsSourceDirOverride = null;
|
|
915
|
+
_pluginsManifestPathOverride = null;
|
|
916
|
+
inProcessHitLockQueues.clear();
|
|
917
|
+
});
|
|
918
|
+
function skillOutputsDir() {
|
|
919
|
+
if (_skillOutputsDirOverride) return _skillOutputsDirOverride;
|
|
920
|
+
return resolve(dataDir(), SKILLS_OUTPUT_SUBDIR);
|
|
921
|
+
}
|
|
922
|
+
function skillsSourceDir() {
|
|
923
|
+
if (_skillsSourceDirOverride) return _skillsSourceDirOverride;
|
|
924
|
+
return resolve(claudeConfigDir(), "skills");
|
|
925
|
+
}
|
|
926
|
+
async function ensureSkillsDir() {
|
|
927
|
+
try {
|
|
928
|
+
ensureDirSync(skillOutputsDir());
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function contentHash(content) {
|
|
933
|
+
return shortFingerprint(content);
|
|
934
|
+
}
|
|
935
|
+
function safeSessionFragment(sessionId) {
|
|
936
|
+
let result = "";
|
|
937
|
+
for (const c of sessionId) {
|
|
938
|
+
if (result.length >= 16) break;
|
|
939
|
+
if (/[a-zA-Z0-9_-]/.test(c)) {
|
|
940
|
+
result += c;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return result.slice(0, 16);
|
|
944
|
+
}
|
|
945
|
+
function safeSkillName(skillName) {
|
|
946
|
+
if (!skillName || skillName.length > 128) return null;
|
|
947
|
+
if (!/^[A-Za-z0-9_:-]+$/.test(skillName)) return null;
|
|
948
|
+
return skillName.replace(/[^a-zA-Z0-9_:-]/g, "_");
|
|
949
|
+
}
|
|
950
|
+
function sanitizeSkillId(name) {
|
|
951
|
+
return name.replace(/:/g, "~");
|
|
952
|
+
}
|
|
953
|
+
function sessionSkillPrefix(sessionId, skillName) {
|
|
954
|
+
const safeSession = safeSessionFragment(sessionId);
|
|
955
|
+
return `${safeSession}-${sanitizeSkillId(skillName)}-`;
|
|
956
|
+
}
|
|
957
|
+
function outputIdFor(sessionId, skillName, contentSha) {
|
|
958
|
+
return `${sessionSkillPrefix(sessionId, skillName)}${contentSha}`;
|
|
959
|
+
}
|
|
960
|
+
function* contentLineEntries(lines) {
|
|
961
|
+
let inCodeBlock = false;
|
|
962
|
+
for (let i = 0; i < lines.length; i++) {
|
|
963
|
+
const stripped = lines[i].trim();
|
|
964
|
+
if (isCodeFenceDelimiter(stripped)) {
|
|
965
|
+
inCodeBlock = !inCodeBlock;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (!inCodeBlock) yield [i, stripped];
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function headingLevel(stripped) {
|
|
972
|
+
const match = stripped.match(/^(#{1,6}) (.*)$/);
|
|
973
|
+
if (!match) return 0;
|
|
974
|
+
return match[2].trim() ? match[1].length : 0;
|
|
975
|
+
}
|
|
976
|
+
function collectSectionBody(lines, startIdx, matchedLevel) {
|
|
977
|
+
const bodyLines = [];
|
|
978
|
+
let inBodyCodeBlock = false;
|
|
979
|
+
for (let j = startIdx; j < lines.length; j++) {
|
|
980
|
+
const stripped = lines[j].trim();
|
|
981
|
+
if (isCodeFenceDelimiter(stripped)) {
|
|
982
|
+
inBodyCodeBlock = !inBodyCodeBlock;
|
|
983
|
+
}
|
|
984
|
+
if (!inBodyCodeBlock) {
|
|
985
|
+
const level = headingLevel(stripped);
|
|
986
|
+
if (level >= 1 && level <= matchedLevel) break;
|
|
987
|
+
}
|
|
988
|
+
bodyLines.push(lines[j]);
|
|
989
|
+
}
|
|
990
|
+
return bodyLines;
|
|
991
|
+
}
|
|
992
|
+
function extractCompactFromMarker(body) {
|
|
993
|
+
if (!body || !body.includes(COMPACT_END_MARKER)) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const lines = body.split("\n");
|
|
997
|
+
for (const [i, stripped] of contentLineEntries(lines)) {
|
|
998
|
+
if (stripped === COMPACT_END_MARKER) {
|
|
999
|
+
const preMarker = lines.slice(0, i).join("\n").trim();
|
|
1000
|
+
return preMarker || null;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
function parseSectionOrdinal(heading) {
|
|
1006
|
+
const match = heading.match(/^(.*?)(?:#(\d+))?$/);
|
|
1007
|
+
if (!match) return [heading, 1];
|
|
1008
|
+
const baseHeading = match[1] || heading;
|
|
1009
|
+
const ordinal = match[2] ? Math.max(1, parseInt(match[2], 10)) : 1;
|
|
1010
|
+
return [baseHeading, ordinal];
|
|
1011
|
+
}
|
|
1012
|
+
function extractNamedSection(body, heading) {
|
|
1013
|
+
if (!body || !heading) return null;
|
|
1014
|
+
const [baseHeading, ordinal] = parseSectionOrdinal(heading);
|
|
1015
|
+
const headingLower = stripLower(baseHeading);
|
|
1016
|
+
const lines = body.split("\n");
|
|
1017
|
+
let matchCount = 0;
|
|
1018
|
+
let startIdx = -1;
|
|
1019
|
+
let matchedLevel = 0;
|
|
1020
|
+
for (const [i, stripped] of contentLineEntries(lines)) {
|
|
1021
|
+
const level = headingLevel(stripped);
|
|
1022
|
+
if (level >= 1 && level <= 3) {
|
|
1023
|
+
const headingText = stripLower(stripped.slice(level));
|
|
1024
|
+
if (headingText === headingLower) {
|
|
1025
|
+
matchCount++;
|
|
1026
|
+
if (matchCount === ordinal) {
|
|
1027
|
+
startIdx = i + 1;
|
|
1028
|
+
matchedLevel = level;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (startIdx === -1) return null;
|
|
1035
|
+
const text = collectSectionBody(lines, startIdx, matchedLevel).join("\n").trim();
|
|
1036
|
+
return text || null;
|
|
1037
|
+
}
|
|
1038
|
+
function extractChecklistSection(body) {
|
|
1039
|
+
if (!body) return null;
|
|
1040
|
+
const checklistHeadings = ["checklist", "check list", "to-do", "todo"];
|
|
1041
|
+
const lines = body.split("\n");
|
|
1042
|
+
let bestPriority = checklistHeadings.length;
|
|
1043
|
+
let bestStart = -1;
|
|
1044
|
+
for (const [i, stripped] of contentLineEntries(lines)) {
|
|
1045
|
+
if (stripped.startsWith("## ") && stripped.length > 3) {
|
|
1046
|
+
const headingText = stripLower(stripped.slice(3));
|
|
1047
|
+
for (let p = 0; p < checklistHeadings.length; p++) {
|
|
1048
|
+
if (headingText.startsWith(checklistHeadings[p])) {
|
|
1049
|
+
if (p < bestPriority) {
|
|
1050
|
+
bestPriority = p;
|
|
1051
|
+
bestStart = i;
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (bestStart === -1) return null;
|
|
1059
|
+
const bodyLines = collectSectionBody(lines, bestStart + 1, 2);
|
|
1060
|
+
let text = bodyLines.join("\n").trim();
|
|
1061
|
+
const maxChars = 2e3;
|
|
1062
|
+
if (text.length > maxChars) {
|
|
1063
|
+
const cut = maxChars - 1;
|
|
1064
|
+
text = text.slice(0, cut).trimEnd() + "\u2026";
|
|
1065
|
+
}
|
|
1066
|
+
return text || null;
|
|
1067
|
+
}
|
|
1068
|
+
async function listOutputs() {
|
|
1069
|
+
try {
|
|
1070
|
+
const dir = skillOutputsDir();
|
|
1071
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1072
|
+
const metas = [];
|
|
1073
|
+
for (const entry of entries) {
|
|
1074
|
+
if (!entry.isFile() || !entry.name.endsWith(".meta")) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const content = await fs3.readFile(resolve(dir, entry.name), "utf-8");
|
|
1079
|
+
const meta = JSON.parse(content);
|
|
1080
|
+
metas.push(meta);
|
|
1081
|
+
} catch {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return metas;
|
|
1086
|
+
} catch {
|
|
1087
|
+
return [];
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async function hasSessionOutput(sessionId, skillName) {
|
|
1091
|
+
try {
|
|
1092
|
+
if (!sessionId) return false;
|
|
1093
|
+
const name = safeSkillName(skillName);
|
|
1094
|
+
if (!name) return false;
|
|
1095
|
+
const safeSession = safeSessionFragment(sessionId);
|
|
1096
|
+
const metas = await listOutputs();
|
|
1097
|
+
return metas.some((m) => m.skillName === name && m.outputId.startsWith(`${safeSession}-`));
|
|
1098
|
+
} catch {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async function sessionOutputBodyBytes(sessionId, skillName) {
|
|
1103
|
+
try {
|
|
1104
|
+
if (!sessionId) return null;
|
|
1105
|
+
const name = safeSkillName(skillName);
|
|
1106
|
+
if (!name) return null;
|
|
1107
|
+
const safeSession = safeSessionFragment(sessionId);
|
|
1108
|
+
const metas = await listOutputs();
|
|
1109
|
+
const matches = metas.filter((m) => m.skillName === name && m.outputId.startsWith(`${safeSession}-`));
|
|
1110
|
+
if (matches.length === 0) return null;
|
|
1111
|
+
matches.sort((a, b) => b.ts - a.ts);
|
|
1112
|
+
return matches[0].bodyBytes;
|
|
1113
|
+
} catch {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
async function findCrossSessionEntry(skillName, contentSha) {
|
|
1118
|
+
const name = safeSkillName(skillName);
|
|
1119
|
+
if (!name || !contentSha) return null;
|
|
1120
|
+
try {
|
|
1121
|
+
const metas = await listOutputs();
|
|
1122
|
+
for (const meta of metas) {
|
|
1123
|
+
if (meta.skillName !== name || meta.contentSha !== contentSha) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const dir = skillOutputsDir();
|
|
1127
|
+
const bodyPath = resolve(dir, `${meta.outputId}.txt`);
|
|
1128
|
+
try {
|
|
1129
|
+
const bodyExists = await fs3.access(bodyPath).then(() => true).catch(() => false);
|
|
1130
|
+
if (bodyExists) {
|
|
1131
|
+
return meta;
|
|
1132
|
+
}
|
|
1133
|
+
} catch {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
async function storeOutput(sessionId, skillName, body, opts) {
|
|
1143
|
+
try {
|
|
1144
|
+
await ensureSkillsDir();
|
|
1145
|
+
const name = safeSkillName(skillName);
|
|
1146
|
+
if (!name) return null;
|
|
1147
|
+
const sha = contentHash(body);
|
|
1148
|
+
const existing = await findCrossSessionEntry(skillName, sha);
|
|
1149
|
+
if (existing) {
|
|
1150
|
+
return existing;
|
|
1151
|
+
}
|
|
1152
|
+
const outId = outputIdFor(sessionId, skillName, sha);
|
|
1153
|
+
const dir = skillOutputsDir();
|
|
1154
|
+
const ts = Date.now();
|
|
1155
|
+
const bodyBytes = Buffer.byteLength(body, "utf-8");
|
|
1156
|
+
const truncated = bodyBytes > 256 * 1024;
|
|
1157
|
+
const redactedBody = redactSecrets(body);
|
|
1158
|
+
if (redactedBody.count > 0) recordStat("secret_redacted", 0, redactedBody.count, void 0, SKILLS_OUTPUT_SUBDIR);
|
|
1159
|
+
let storedBody = redactedBody.text;
|
|
1160
|
+
if (truncated) {
|
|
1161
|
+
const buf = Buffer.from(redactedBody.text, "utf-8");
|
|
1162
|
+
let truncStart = Math.max(0, buf.length - 262144);
|
|
1163
|
+
if (truncStart < buf.length) {
|
|
1164
|
+
const byte = buf[truncStart];
|
|
1165
|
+
if ((byte & 192) === 128) {
|
|
1166
|
+
let charStart = truncStart - 1;
|
|
1167
|
+
while (charStart >= 0 && (buf[charStart] & 192) === 128) {
|
|
1168
|
+
charStart--;
|
|
1169
|
+
}
|
|
1170
|
+
if (charStart >= 0) {
|
|
1171
|
+
const leadByte = buf[charStart];
|
|
1172
|
+
let charLen = 1;
|
|
1173
|
+
if ((leadByte & 128) === 0) charLen = 1;
|
|
1174
|
+
else if ((leadByte & 224) === 192) charLen = 2;
|
|
1175
|
+
else if ((leadByte & 240) === 224) charLen = 3;
|
|
1176
|
+
else if ((leadByte & 248) === 240) charLen = 4;
|
|
1177
|
+
truncStart = charStart + charLen;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const truncBuf = buf.slice(truncStart);
|
|
1182
|
+
storedBody = truncBuf.toString("utf-8");
|
|
1183
|
+
}
|
|
1184
|
+
await atomicWriteText(resolve(dir, `${outId}.txt`), storedBody);
|
|
1185
|
+
const meta = {
|
|
1186
|
+
outputId: outId,
|
|
1187
|
+
skillName: name,
|
|
1188
|
+
contentSha: sha,
|
|
1189
|
+
bodyBytes,
|
|
1190
|
+
ts,
|
|
1191
|
+
truncated,
|
|
1192
|
+
sourcePath: opts?.sourcePath || ""
|
|
1193
|
+
};
|
|
1194
|
+
await atomicWriteText(resolve(dir, `${outId}.meta`), JSON.stringify(meta, null, 2));
|
|
1195
|
+
pruneSkillOutputs();
|
|
1196
|
+
return meta;
|
|
1197
|
+
} catch {
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async function storeCompact(sessionId, skillName, compactText, sourceSha) {
|
|
1202
|
+
try {
|
|
1203
|
+
await ensureSkillsDir();
|
|
1204
|
+
const name = safeSkillName(skillName);
|
|
1205
|
+
if (!name) return;
|
|
1206
|
+
const safeSession = safeSessionFragment(sessionId);
|
|
1207
|
+
const fileId = `${safeSession}@${sanitizeSkillId(name)}@compact`;
|
|
1208
|
+
const dir = skillOutputsDir();
|
|
1209
|
+
const redacted = redactSecrets(compactText);
|
|
1210
|
+
if (redacted.count > 0) recordStat("secret_redacted", 0, redacted.count, void 0, SKILLS_OUTPUT_SUBDIR);
|
|
1211
|
+
let text = redacted.text;
|
|
1212
|
+
if (sourceSha) {
|
|
1213
|
+
text = `<!-- source_sha: ${sourceSha.slice(0, 12)} -->
|
|
1214
|
+
${text}`;
|
|
1215
|
+
}
|
|
1216
|
+
await atomicWriteText(resolve(dir, fileId), text);
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function compactSessionSuffix(skillName) {
|
|
1221
|
+
const name = safeSkillName(skillName);
|
|
1222
|
+
return name ? `@${sanitizeSkillId(name)}@compact` : null;
|
|
1223
|
+
}
|
|
1224
|
+
function matchesCompactSuffix(entryName, isFile, suffix) {
|
|
1225
|
+
return isFile && entryName.endsWith("@compact") && entryName.endsWith(suffix);
|
|
1226
|
+
}
|
|
1227
|
+
function getCompactAnySessionSync(skillName) {
|
|
1228
|
+
try {
|
|
1229
|
+
const suffix = compactSessionSuffix(skillName);
|
|
1230
|
+
if (!suffix) return null;
|
|
1231
|
+
const dir = skillOutputsDir();
|
|
1232
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
1233
|
+
for (const entry of entries) {
|
|
1234
|
+
if (!matchesCompactSuffix(entry.name, entry.isFile(), suffix)) continue;
|
|
1235
|
+
try {
|
|
1236
|
+
const text = readFileSync2(resolve(dir, entry.name), "utf-8");
|
|
1237
|
+
if (text.trim()) return text;
|
|
1238
|
+
} catch {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return null;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function extractSourceShaFromCompact(compactText) {
|
|
1248
|
+
if (!compactText) return null;
|
|
1249
|
+
const match = compactText.match(/<!--\s*source_sha:\s*([a-f0-9]{12})\s*-->/);
|
|
1250
|
+
return match ? match[1] : null;
|
|
1251
|
+
}
|
|
1252
|
+
function isCompactStale(compactText, skillName, currentBodySha) {
|
|
1253
|
+
if (!compactText) return null;
|
|
1254
|
+
const embeddedSha = extractSourceShaFromCompact(compactText);
|
|
1255
|
+
if (!embeddedSha) return null;
|
|
1256
|
+
const currentSha = currentBodySha.slice(0, 12);
|
|
1257
|
+
return embeddedSha !== currentSha;
|
|
1258
|
+
}
|
|
1259
|
+
async function readSkillHits(skillName) {
|
|
1260
|
+
try {
|
|
1261
|
+
const name = safeSkillName(skillName);
|
|
1262
|
+
if (!name) return { count: 0, lastTs: 0 };
|
|
1263
|
+
const dir = skillOutputsDir();
|
|
1264
|
+
const hitsFile = resolve(dir, `${sanitizeSkillId(name)}.hits`);
|
|
1265
|
+
const content = await fs3.readFile(hitsFile, "utf-8").catch(() => null);
|
|
1266
|
+
if (content) {
|
|
1267
|
+
const parsed = JSON.parse(content);
|
|
1268
|
+
return parsed;
|
|
1269
|
+
}
|
|
1270
|
+
} catch {
|
|
1271
|
+
}
|
|
1272
|
+
return { count: 0, lastTs: 0 };
|
|
1273
|
+
}
|
|
1274
|
+
var SKILL_HIT_LOCK_RETRY_MS = 5;
|
|
1275
|
+
var SKILL_HIT_LOCK_STALE_MS = 5e3;
|
|
1276
|
+
var SKILL_HIT_LOCK_MAX_ATTEMPTS = Math.ceil(SKILL_HIT_LOCK_STALE_MS / SKILL_HIT_LOCK_RETRY_MS);
|
|
1277
|
+
var inProcessHitLockQueues = /* @__PURE__ */ new Map();
|
|
1278
|
+
function runExclusiveInProcess(key, fn) {
|
|
1279
|
+
const prior = inProcessHitLockQueues.get(key) ?? Promise.resolve();
|
|
1280
|
+
const run = prior.then(fn, fn);
|
|
1281
|
+
inProcessHitLockQueues.set(
|
|
1282
|
+
key,
|
|
1283
|
+
run.then(
|
|
1284
|
+
() => void 0,
|
|
1285
|
+
() => void 0
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
1288
|
+
return run;
|
|
1289
|
+
}
|
|
1290
|
+
async function acquireSkillHitLock(lockPath) {
|
|
1291
|
+
for (let attempt = 0; attempt < SKILL_HIT_LOCK_MAX_ATTEMPTS; attempt++) {
|
|
1292
|
+
try {
|
|
1293
|
+
await fs3.mkdir(lockPath);
|
|
1294
|
+
return true;
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
if (err.code !== "EEXIST") return false;
|
|
1297
|
+
try {
|
|
1298
|
+
const st = await fs3.stat(lockPath);
|
|
1299
|
+
if (Date.now() - st.mtimeMs > SKILL_HIT_LOCK_STALE_MS) {
|
|
1300
|
+
await fs3.rmdir(lockPath).catch(() => {
|
|
1301
|
+
});
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
} catch {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
await new Promise((r) => setTimeout(r, SKILL_HIT_LOCK_RETRY_MS));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
async function incrementSkillHit(skillName) {
|
|
1313
|
+
try {
|
|
1314
|
+
const name = safeSkillName(skillName);
|
|
1315
|
+
if (!name) return;
|
|
1316
|
+
await ensureSkillsDir();
|
|
1317
|
+
const dir = skillOutputsDir();
|
|
1318
|
+
const hitsFile = resolve(dir, `${sanitizeSkillId(name)}.hits`);
|
|
1319
|
+
const lockPath = `${hitsFile}.lock`;
|
|
1320
|
+
await runExclusiveInProcess(lockPath, async () => {
|
|
1321
|
+
const locked = await acquireSkillHitLock(lockPath);
|
|
1322
|
+
if (!locked) return;
|
|
1323
|
+
try {
|
|
1324
|
+
const hits = await readSkillHits(name);
|
|
1325
|
+
hits.count++;
|
|
1326
|
+
hits.lastTs = Date.now();
|
|
1327
|
+
await atomicWriteText(hitsFile, JSON.stringify(hits, null, 2));
|
|
1328
|
+
} finally {
|
|
1329
|
+
await fs3.rmdir(lockPath).catch(() => {
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
} catch {
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function formatAge(ageMs) {
|
|
1337
|
+
const s = Math.floor(ageMs / 1e3);
|
|
1338
|
+
if (s < 60) return `${s}s`;
|
|
1339
|
+
const m = Math.floor(s / 60);
|
|
1340
|
+
if (m < 60) return `${m}m`;
|
|
1341
|
+
const h = Math.floor(m / 60);
|
|
1342
|
+
if (h < 24) return `${h}h`;
|
|
1343
|
+
const d = Math.floor(h / 24);
|
|
1344
|
+
return `${d}d`;
|
|
1345
|
+
}
|
|
1346
|
+
async function listSkills(sessionId) {
|
|
1347
|
+
try {
|
|
1348
|
+
const metas = (await listOutputs()).sort((a, b) => b.ts - a.ts);
|
|
1349
|
+
const results = [];
|
|
1350
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1351
|
+
for (const meta of metas) {
|
|
1352
|
+
if (sessionId && !meta.outputId.startsWith(`${safeSessionFragment(sessionId)}-`)) {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
if (seen.has(meta.skillName)) continue;
|
|
1356
|
+
seen.add(meta.skillName);
|
|
1357
|
+
const dir = skillOutputsDir();
|
|
1358
|
+
const sanitizedName = sanitizeSkillId(meta.skillName);
|
|
1359
|
+
const outputSuffix = `-${sanitizedName}-${meta.contentSha}`;
|
|
1360
|
+
const safeSession = meta.outputId.endsWith(outputSuffix) ? meta.outputId.slice(0, meta.outputId.length - outputSuffix.length) : safeSessionFragment(meta.outputId.split("-")[0]);
|
|
1361
|
+
const compactFileId = `${safeSession}@${sanitizedName}@compact`;
|
|
1362
|
+
let compactLen = 0;
|
|
1363
|
+
let compactText = "";
|
|
1364
|
+
try {
|
|
1365
|
+
const stat2 = await fs3.stat(resolve(dir, compactFileId));
|
|
1366
|
+
compactLen = stat2.size;
|
|
1367
|
+
compactText = await fs3.readFile(resolve(dir, compactFileId), "utf-8").catch(() => "");
|
|
1368
|
+
} catch {
|
|
1369
|
+
compactLen = 0;
|
|
1370
|
+
}
|
|
1371
|
+
const hasMarker = extractCompactFromMarker(
|
|
1372
|
+
await fs3.readFile(resolve(dir, `${meta.outputId}.txt`), "utf-8").catch(() => "")
|
|
1373
|
+
) !== null;
|
|
1374
|
+
const compactStale = isCompactStale(compactText, meta.skillName, meta.contentSha);
|
|
1375
|
+
const { count: hitCount } = await readSkillHits(meta.skillName);
|
|
1376
|
+
const ageMs = Date.now() - meta.ts;
|
|
1377
|
+
results.push({
|
|
1378
|
+
name: meta.skillName,
|
|
1379
|
+
bodyLen: meta.bodyBytes,
|
|
1380
|
+
compactLen,
|
|
1381
|
+
hasMarker,
|
|
1382
|
+
compactStale,
|
|
1383
|
+
hitCount,
|
|
1384
|
+
ageMs
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return results;
|
|
1388
|
+
} catch {
|
|
1389
|
+
return [];
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
async function getSkillFilePath(skillName) {
|
|
1393
|
+
try {
|
|
1394
|
+
const name = safeSkillName(skillName);
|
|
1395
|
+
if (!name) return null;
|
|
1396
|
+
const metas = await listOutputs();
|
|
1397
|
+
for (const meta of metas) {
|
|
1398
|
+
if (meta.skillName === name && meta.sourcePath) {
|
|
1399
|
+
return meta.sourcePath;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return await installedSkillPath(name);
|
|
1403
|
+
} catch {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async function resolvePluginSkillPath(pluginName, skillSlug) {
|
|
1408
|
+
const manifestPath = _pluginsManifestPathOverride ?? resolve(claudeConfigDir(), "plugins", "installed_plugins.json");
|
|
1409
|
+
let raw;
|
|
1410
|
+
try {
|
|
1411
|
+
raw = await fs3.readFile(manifestPath, "utf8");
|
|
1412
|
+
} catch {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
let parsed;
|
|
1416
|
+
try {
|
|
1417
|
+
parsed = JSON.parse(raw);
|
|
1418
|
+
} catch {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1422
|
+
const plugins = parsed["plugins"];
|
|
1423
|
+
if (typeof plugins !== "object" || plugins === null) return null;
|
|
1424
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
1425
|
+
if (key !== pluginName && !key.startsWith(pluginName + "@")) continue;
|
|
1426
|
+
if (!Array.isArray(entries)) continue;
|
|
1427
|
+
for (const entry of entries) {
|
|
1428
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1429
|
+
const installPath = entry["installPath"];
|
|
1430
|
+
if (typeof installPath !== "string" || installPath === "") continue;
|
|
1431
|
+
const diskPath = resolve(installPath, "skills", skillSlug, "SKILL.md");
|
|
1432
|
+
try {
|
|
1433
|
+
await fs3.access(diskPath);
|
|
1434
|
+
return diskPath;
|
|
1435
|
+
} catch {
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
async function installedSkillPath(skillName) {
|
|
1443
|
+
const name = safeSkillName(skillName);
|
|
1444
|
+
if (!name) return null;
|
|
1445
|
+
const colonIdx = name.indexOf(":");
|
|
1446
|
+
if (colonIdx > 0 && colonIdx < name.length - 1) {
|
|
1447
|
+
const pluginPath = await resolvePluginSkillPath(name.slice(0, colonIdx), name.slice(colonIdx + 1));
|
|
1448
|
+
if (pluginPath !== null) return pluginPath;
|
|
1449
|
+
}
|
|
1450
|
+
const diskPath = resolve(skillsSourceDir(), name, "SKILL.md");
|
|
1451
|
+
try {
|
|
1452
|
+
await fs3.access(diskPath);
|
|
1453
|
+
return diskPath;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
1459
|
+
const dir = skillOutputsDir();
|
|
1460
|
+
let removed = 0;
|
|
1461
|
+
try {
|
|
1462
|
+
if (!existsSync2(dir)) return 0;
|
|
1463
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
1464
|
+
const entries = [];
|
|
1465
|
+
for (const file of readdirSync2(dir)) {
|
|
1466
|
+
if (!file.endsWith(".meta")) continue;
|
|
1467
|
+
const outputId = file.slice(0, -".meta".length);
|
|
1468
|
+
let ts;
|
|
1469
|
+
try {
|
|
1470
|
+
const parsed = JSON.parse(readFileSync2(resolve(dir, file), "utf-8"));
|
|
1471
|
+
ts = parsed.ts;
|
|
1472
|
+
} catch {
|
|
1473
|
+
try {
|
|
1474
|
+
ts = statSync2(resolve(dir, file)).mtimeMs;
|
|
1475
|
+
} catch {
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
entries.push({ outputId, ts });
|
|
1480
|
+
}
|
|
1481
|
+
const removeEntry = (outputId) => {
|
|
1482
|
+
for (const ext of [".meta", ".txt"]) {
|
|
1483
|
+
try {
|
|
1484
|
+
unlinkSync(resolve(dir, `${outputId}${ext}`));
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
removed++;
|
|
1489
|
+
};
|
|
1490
|
+
const kept = [];
|
|
1491
|
+
for (const entry of entries) {
|
|
1492
|
+
if (entry.ts < cutoff) {
|
|
1493
|
+
removeEntry(entry.outputId);
|
|
1494
|
+
} else {
|
|
1495
|
+
kept.push(entry);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (kept.length > maxCount) {
|
|
1499
|
+
kept.sort((a, b) => a.ts - b.ts);
|
|
1500
|
+
for (const entry of kept.slice(0, kept.length - maxCount)) {
|
|
1501
|
+
removeEntry(entry.outputId);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
} catch {
|
|
1505
|
+
return removed;
|
|
1506
|
+
}
|
|
1507
|
+
return removed;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/compact.ts
|
|
1511
|
+
init_define_import_meta_env();
|
|
1512
|
+
import * as fs4 from "node:fs";
|
|
1513
|
+
import * as path3 from "node:path";
|
|
1514
|
+
var CONTEXT_AUTOCOMPACT_TOKENS = 66e4;
|
|
1515
|
+
var CATALOG_TOKENS = 10800;
|
|
1516
|
+
var CONTEXT_TIER_WARM = 0.5;
|
|
1517
|
+
var CONTEXT_TIER_HOT = 0.7;
|
|
1518
|
+
var CONTEXT_TIER_CRITICAL = 0.85;
|
|
1519
|
+
var NOISE_EXTS = /* @__PURE__ */ new Set([
|
|
1520
|
+
".pyc",
|
|
1521
|
+
".pyo",
|
|
1522
|
+
".pyd",
|
|
1523
|
+
".class",
|
|
1524
|
+
".o",
|
|
1525
|
+
".obj",
|
|
1526
|
+
".a",
|
|
1527
|
+
".lib",
|
|
1528
|
+
".dll",
|
|
1529
|
+
".so",
|
|
1530
|
+
".dylib",
|
|
1531
|
+
".log",
|
|
1532
|
+
".jsonl",
|
|
1533
|
+
".tmp",
|
|
1534
|
+
".temp",
|
|
1535
|
+
".swp",
|
|
1536
|
+
".swo",
|
|
1537
|
+
".bak",
|
|
1538
|
+
".pid",
|
|
1539
|
+
".lock",
|
|
1540
|
+
".map",
|
|
1541
|
+
".wasm",
|
|
1542
|
+
".gz",
|
|
1543
|
+
".zip",
|
|
1544
|
+
".tar",
|
|
1545
|
+
".tgz",
|
|
1546
|
+
".db",
|
|
1547
|
+
".sqlite",
|
|
1548
|
+
".sqlite3",
|
|
1549
|
+
".db3",
|
|
1550
|
+
".d.ts",
|
|
1551
|
+
".snap",
|
|
1552
|
+
".eot",
|
|
1553
|
+
".ttf",
|
|
1554
|
+
".woff",
|
|
1555
|
+
".woff2",
|
|
1556
|
+
".ico",
|
|
1557
|
+
".pdb",
|
|
1558
|
+
".exe",
|
|
1559
|
+
".bin"
|
|
1560
|
+
]);
|
|
1561
|
+
var NOISE_BASENAMES = /* @__PURE__ */ new Set([
|
|
1562
|
+
".ds_store",
|
|
1563
|
+
"thumbs.db",
|
|
1564
|
+
"desktop.ini",
|
|
1565
|
+
"package-lock.json",
|
|
1566
|
+
"yarn.lock",
|
|
1567
|
+
"pnpm-lock.yaml",
|
|
1568
|
+
"poetry.lock",
|
|
1569
|
+
"uv.lock",
|
|
1570
|
+
"pdm.lock",
|
|
1571
|
+
"cargo.lock",
|
|
1572
|
+
"composer.lock",
|
|
1573
|
+
"gemfile.lock",
|
|
1574
|
+
"coverage.xml",
|
|
1575
|
+
".coverage",
|
|
1576
|
+
"lcov.info"
|
|
1577
|
+
]);
|
|
1578
|
+
var NOISE_SEGMENTS = [
|
|
1579
|
+
"/__pycache__/",
|
|
1580
|
+
"/.git/",
|
|
1581
|
+
"/node_modules/",
|
|
1582
|
+
"/.venv/",
|
|
1583
|
+
"/venv/",
|
|
1584
|
+
"/dist/",
|
|
1585
|
+
"/build/",
|
|
1586
|
+
"/.mypy_cache/",
|
|
1587
|
+
"/.pytest_cache/",
|
|
1588
|
+
"/.ruff_cache/",
|
|
1589
|
+
"/appdata/local/temp/",
|
|
1590
|
+
"/appdata/roaming/",
|
|
1591
|
+
"/tmp/",
|
|
1592
|
+
"/.next/",
|
|
1593
|
+
"/.nuxt/",
|
|
1594
|
+
"/.svelte-kit/",
|
|
1595
|
+
"/.turbo/",
|
|
1596
|
+
"/.parcel-cache/",
|
|
1597
|
+
"/.cache/",
|
|
1598
|
+
"/.tox/",
|
|
1599
|
+
"/coverage/",
|
|
1600
|
+
"/.nyc_output/",
|
|
1601
|
+
"/site-packages/",
|
|
1602
|
+
".egg-info/",
|
|
1603
|
+
"/target/",
|
|
1604
|
+
"/__snapshots__/"
|
|
1605
|
+
];
|
|
1606
|
+
var HARNESS_MULTIPLIER_DEFAULTS = {
|
|
1607
|
+
claudecode: 2,
|
|
1608
|
+
codex: 1.5,
|
|
1609
|
+
opencode: 2.5,
|
|
1610
|
+
gemini: 3,
|
|
1611
|
+
openclaw: 1,
|
|
1612
|
+
pi: 1,
|
|
1613
|
+
hermes: 1,
|
|
1614
|
+
generic: 1
|
|
1615
|
+
};
|
|
1616
|
+
function tierForFraction(fill) {
|
|
1617
|
+
if (fill >= CONTEXT_TIER_CRITICAL) return "critical";
|
|
1618
|
+
if (fill >= CONTEXT_TIER_HOT) return "hot";
|
|
1619
|
+
if (fill >= CONTEXT_TIER_WARM) return "warm";
|
|
1620
|
+
return "cool";
|
|
1621
|
+
}
|
|
1622
|
+
function pressureRawTotal(cache) {
|
|
1623
|
+
const skillTokens = cache.loadedSkillTotalTokens ?? 0;
|
|
1624
|
+
const bashCount = (cache.bashOutputs ?? []).length;
|
|
1625
|
+
const webCount = (cache.webFetches ?? []).length;
|
|
1626
|
+
const files = cache.files ?? [];
|
|
1627
|
+
const readCount = files.length;
|
|
1628
|
+
return skillTokens + CATALOG_TOKENS + bashCount * 500 + webCount * 1e3 + readCount * 200;
|
|
1629
|
+
}
|
|
1630
|
+
var PROMPT_MEASURE_TAIL_BYTES = 1048576;
|
|
1631
|
+
function extractUsageTotal(record) {
|
|
1632
|
+
if (record === null || typeof record !== "object") return null;
|
|
1633
|
+
if (record["type"] !== "assistant") return null;
|
|
1634
|
+
const message = record["message"];
|
|
1635
|
+
if (message === null || typeof message !== "object") return null;
|
|
1636
|
+
const usage = message["usage"];
|
|
1637
|
+
if (usage === null || typeof usage !== "object") return null;
|
|
1638
|
+
const u = usage;
|
|
1639
|
+
const input = u["input_tokens"];
|
|
1640
|
+
const cacheCreation = u["cache_creation_input_tokens"];
|
|
1641
|
+
const cacheRead = u["cache_read_input_tokens"];
|
|
1642
|
+
const output = u["output_tokens"];
|
|
1643
|
+
if (typeof input !== "number" || typeof cacheCreation !== "number" || typeof cacheRead !== "number" || typeof output !== "number") {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
return input + cacheCreation + cacheRead + output;
|
|
1647
|
+
}
|
|
1648
|
+
function measurePromptTokens(transcriptPath) {
|
|
1649
|
+
try {
|
|
1650
|
+
const lines = readTranscriptTail(transcriptPath, PROMPT_MEASURE_TAIL_BYTES);
|
|
1651
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1652
|
+
const line = lines[i];
|
|
1653
|
+
if (line === void 0) continue;
|
|
1654
|
+
const trimmed = line.trim();
|
|
1655
|
+
if (trimmed === "") continue;
|
|
1656
|
+
let parsed;
|
|
1657
|
+
try {
|
|
1658
|
+
parsed = JSON.parse(trimmed);
|
|
1659
|
+
} catch {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
const total = extractUsageTotal(parsed);
|
|
1663
|
+
if (total !== null) return total;
|
|
1664
|
+
}
|
|
1665
|
+
return null;
|
|
1666
|
+
} catch {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function getEffectiveAutoTriggerWindow() {
|
|
1671
|
+
const ca = loadConfig().compact_assist;
|
|
1672
|
+
const isConfigDefault = !isAutoTriggerMultiplierExplicit();
|
|
1673
|
+
const multiplier = getAutoTriggerMultiplier(
|
|
1674
|
+
ca.harness === "auto" ? { configExplicitMultiplier: ca.auto_trigger_multiplier, isConfigDefault } : { configExplicitMultiplier: ca.auto_trigger_multiplier, harness: ca.harness, isConfigDefault }
|
|
1675
|
+
);
|
|
1676
|
+
return CONTEXT_AUTOCOMPACT_TOKENS * multiplier;
|
|
1677
|
+
}
|
|
1678
|
+
function getContextPressure(cache, transcriptPath) {
|
|
1679
|
+
try {
|
|
1680
|
+
const window = getEffectiveAutoTriggerWindow();
|
|
1681
|
+
if (transcriptPath) {
|
|
1682
|
+
const measured = measurePromptTokens(transcriptPath);
|
|
1683
|
+
if (measured !== null) {
|
|
1684
|
+
const fill2 = measured / window;
|
|
1685
|
+
return { fillFraction: fill2, tier: tierForFraction(fill2) };
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (!cache) {
|
|
1689
|
+
return { fillFraction: 0, tier: "cool" };
|
|
1690
|
+
}
|
|
1691
|
+
const total = pressureRawTotal(cache);
|
|
1692
|
+
const fill = total / window;
|
|
1693
|
+
return {
|
|
1694
|
+
fillFraction: fill,
|
|
1695
|
+
tier: tierForFraction(fill)
|
|
1696
|
+
};
|
|
1697
|
+
} catch {
|
|
1698
|
+
return { fillFraction: 0, tier: "cool" };
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function getAutoTriggerMultiplier(opts) {
|
|
1702
|
+
const config = opts?.configExplicitMultiplier ?? 2;
|
|
1703
|
+
let isDefault = opts?.isConfigDefault;
|
|
1704
|
+
if (isDefault === void 0) {
|
|
1705
|
+
isDefault = config === 2;
|
|
1706
|
+
}
|
|
1707
|
+
if (!isDefault && opts?.configExplicitMultiplier !== void 0) {
|
|
1708
|
+
return Math.max(1, Math.min(10, opts.configExplicitMultiplier));
|
|
1709
|
+
}
|
|
1710
|
+
const harness = opts?.harness ?? detectHarness();
|
|
1711
|
+
return Math.max(1, Math.min(10, HARNESS_MULTIPLIER_DEFAULTS[harness] ?? 1));
|
|
1712
|
+
}
|
|
1713
|
+
function isNoisePath(inputPath) {
|
|
1714
|
+
if (!inputPath) {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
const p = normalizePathForwardSlash(inputPath, true);
|
|
1718
|
+
for (const segment of NOISE_SEGMENTS) {
|
|
1719
|
+
if (p.includes(segment)) {
|
|
1720
|
+
return true;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
const slashIdx = p.lastIndexOf("/");
|
|
1724
|
+
const basename = slashIdx >= 0 ? p.slice(slashIdx + 1) : p;
|
|
1725
|
+
if (NOISE_BASENAMES.has(basename)) {
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
if (basename.startsWith(".improve-state-") || basename.startsWith("improve_commit_msg_")) {
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
const dotIdx = basename.lastIndexOf(".");
|
|
1732
|
+
if (dotIdx >= 0) {
|
|
1733
|
+
const ext = basename.slice(dotIdx);
|
|
1734
|
+
if (NOISE_EXTS.has(ext)) {
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
for (const ext of NOISE_EXTS) {
|
|
1739
|
+
if (ext.includes(".") && ext.split(".").length > 2) {
|
|
1740
|
+
if (basename.endsWith(ext)) {
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
function findLatestSessionId() {
|
|
1748
|
+
try {
|
|
1749
|
+
const sessionsDir2 = path3.join(tokenGoatHome(), "sessions");
|
|
1750
|
+
if (!fs4.existsSync(sessionsDir2)) {
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
const files = fs4.readdirSync(sessionsDir2);
|
|
1754
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.includes(AGENT_SALT_MARKER));
|
|
1755
|
+
if (jsonFiles.length === 0) {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
const firstFile = jsonFiles[0];
|
|
1759
|
+
if (!firstFile) {
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
let latestFile = firstFile;
|
|
1763
|
+
let latestMtime = fs4.statSync(path3.join(sessionsDir2, firstFile)).mtimeMs;
|
|
1764
|
+
for (const file of jsonFiles) {
|
|
1765
|
+
const mtime = fs4.statSync(path3.join(sessionsDir2, file)).mtimeMs;
|
|
1766
|
+
if (mtime > latestMtime) {
|
|
1767
|
+
latestFile = file;
|
|
1768
|
+
latestMtime = mtime;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return latestFile.replace(/\.json$/, "");
|
|
1772
|
+
} catch {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
function eventCount(cache) {
|
|
1777
|
+
const files = cache.files ?? [];
|
|
1778
|
+
const editedCount = files.filter((f) => f.wasEdited).length;
|
|
1779
|
+
const bashCount = (cache.bashOutputs ?? []).length;
|
|
1780
|
+
const webCount = (cache.webFetches ?? []).length;
|
|
1781
|
+
const skillHistory = cache.skillHistory ?? {};
|
|
1782
|
+
return files.length + editedCount + bashCount + webCount + Object.keys(skillHistory).length;
|
|
1783
|
+
}
|
|
1784
|
+
function writeSessionManifest(projectHash, sessionId, manifestJson) {
|
|
1785
|
+
const safeSessionId = sessionFileStem(sessionId);
|
|
1786
|
+
if (!safeSessionId) return;
|
|
1787
|
+
const sessionsDir2 = path3.join(dataDir(), "projects", projectHash, "sessions");
|
|
1788
|
+
if (!fs4.existsSync(sessionsDir2)) {
|
|
1789
|
+
ensureDirSync(sessionsDir2);
|
|
1790
|
+
}
|
|
1791
|
+
const dest = path3.join(sessionsDir2, `${safeSessionId}.json`);
|
|
1792
|
+
atomicWriteText(dest, JSON.stringify(manifestJson));
|
|
1793
|
+
}
|
|
1794
|
+
function readAllSessionManifests(projectHash, maxAgeSecs = 3600) {
|
|
1795
|
+
const sessionsDir2 = path3.join(dataDir(), "projects", projectHash, "sessions");
|
|
1796
|
+
if (!fs4.existsSync(sessionsDir2)) {
|
|
1797
|
+
return [];
|
|
1798
|
+
}
|
|
1799
|
+
const now = Date.now() / 1e3;
|
|
1800
|
+
const results = [];
|
|
1801
|
+
try {
|
|
1802
|
+
const files = fs4.readdirSync(sessionsDir2);
|
|
1803
|
+
for (const file of files) {
|
|
1804
|
+
if (!file.endsWith(".json")) {
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
const fullPath = path3.join(sessionsDir2, file);
|
|
1809
|
+
const stat2 = fs4.statSync(fullPath);
|
|
1810
|
+
if (now - stat2.mtimeMs / 1e3 > maxAgeSecs) {
|
|
1811
|
+
try {
|
|
1812
|
+
fs4.unlinkSync(fullPath);
|
|
1813
|
+
} catch {
|
|
1814
|
+
}
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
const text = fs4.readFileSync(fullPath, "utf8");
|
|
1818
|
+
const data = JSON.parse(text);
|
|
1819
|
+
if (typeof data === "object" && data !== null && "files" in data) {
|
|
1820
|
+
results.push(data);
|
|
1821
|
+
}
|
|
1822
|
+
} catch {
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
return results;
|
|
1828
|
+
}
|
|
1829
|
+
function _editedFileCount(cache) {
|
|
1830
|
+
return (cache.files ?? []).filter((f) => f.wasEdited).length;
|
|
1831
|
+
}
|
|
1832
|
+
function _computeActivityMultiplier(ageSecs, editedCount) {
|
|
1833
|
+
const TEN_MIN_SECS = 600;
|
|
1834
|
+
const SIXTY_MIN_SECS = 3600;
|
|
1835
|
+
const EDITS_PER_MIN_DENSITY_THRESHOLD = 0.3;
|
|
1836
|
+
let tier;
|
|
1837
|
+
if (ageSecs < TEN_MIN_SECS) {
|
|
1838
|
+
tier = "young";
|
|
1839
|
+
} else if (ageSecs < SIXTY_MIN_SECS) {
|
|
1840
|
+
tier = "active";
|
|
1841
|
+
} else {
|
|
1842
|
+
tier = "mature";
|
|
1843
|
+
}
|
|
1844
|
+
const baseFactor = {
|
|
1845
|
+
young: 0.6,
|
|
1846
|
+
active: 1,
|
|
1847
|
+
mature: 1.4
|
|
1848
|
+
};
|
|
1849
|
+
let factor = baseFactor[tier];
|
|
1850
|
+
if (ageSecs >= TEN_MIN_SECS) {
|
|
1851
|
+
const editsPerMin = ageSecs > 0 ? editedCount / (ageSecs / 60) : 0;
|
|
1852
|
+
if (editsPerMin < EDITS_PER_MIN_DENSITY_THRESHOLD) {
|
|
1853
|
+
factor = Math.min(factor, 1);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return factor;
|
|
1857
|
+
}
|
|
1858
|
+
function loadSessionCache(sessionId) {
|
|
1859
|
+
const disk = readSessionStateFile(sessionId);
|
|
1860
|
+
if (!disk) {
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
files: disk.files,
|
|
1865
|
+
webFetches: disk.webFetches,
|
|
1866
|
+
bashOutputs: disk.bashOutputs,
|
|
1867
|
+
...disk.created_ts !== void 0 ? { created_ts: disk.created_ts } : {}
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function computeAdaptiveBudget(cache, ageSecs = 0, opts) {
|
|
1871
|
+
const base = 200;
|
|
1872
|
+
const maxTotal = 800;
|
|
1873
|
+
const minTotal = 200;
|
|
1874
|
+
const editedCount = _editedFileCount(cache);
|
|
1875
|
+
const editedBonus = Math.min(200, editedCount * 50);
|
|
1876
|
+
const files = cache.files ?? [];
|
|
1877
|
+
const symbolFiles = files.filter((f) => {
|
|
1878
|
+
const entry = f;
|
|
1879
|
+
return (entry["symbols_read"] ?? []).length > 0;
|
|
1880
|
+
}).length;
|
|
1881
|
+
const symbolsBonus = Math.min(150, symbolFiles * 30);
|
|
1882
|
+
const bashCount = (cache.bashOutputs ?? []).length;
|
|
1883
|
+
const bashBonus = bashCount > 0 ? Math.min(100, Math.max(20, bashCount * 5)) : 0;
|
|
1884
|
+
const webBonus = (cache.webFetches ?? []).length > 0 ? 15 : 0;
|
|
1885
|
+
const diffBonus = opts?.hasPendingDiff ? 50 : 0;
|
|
1886
|
+
const uncommittedBonus = opts?.hasUncommittedChanges ? 10 : 0;
|
|
1887
|
+
const staleFrac = Math.max(0, Math.min(1, opts?.staleCompactFraction ?? 0));
|
|
1888
|
+
const staleBonus = Math.min(60, Math.round(staleFrac * 60));
|
|
1889
|
+
const rawTotal = base + editedBonus + symbolsBonus + bashBonus + webBonus + diffBonus + uncommittedBonus + staleBonus;
|
|
1890
|
+
const factor = _computeActivityMultiplier(ageSecs, editedCount);
|
|
1891
|
+
const total = Math.round(rawTotal * factor);
|
|
1892
|
+
let capMax = maxTotal;
|
|
1893
|
+
if (opts?.contextPressure) {
|
|
1894
|
+
if (opts.contextPressure.tier === "critical") {
|
|
1895
|
+
capMax = Math.min(capMax, 300);
|
|
1896
|
+
} else if (opts.contextPressure.tier === "hot") {
|
|
1897
|
+
capMax = Math.min(capMax, 500);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return Math.max(minTotal, Math.min(capMax, total));
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
export {
|
|
1904
|
+
recordFileRead,
|
|
1905
|
+
recordSymbolRead,
|
|
1906
|
+
recordFileEdit,
|
|
1907
|
+
getSessionFiles,
|
|
1908
|
+
wasFileReadThisSession,
|
|
1909
|
+
wasFileFullyReadThisSession,
|
|
1910
|
+
getCompactedAt,
|
|
1911
|
+
markCompacted,
|
|
1912
|
+
getSessionFileEntry,
|
|
1913
|
+
wasHintShown,
|
|
1914
|
+
markHintShown,
|
|
1915
|
+
recordScheduledPrompt,
|
|
1916
|
+
wasCliReadThisSession,
|
|
1917
|
+
recordCliRead,
|
|
1918
|
+
recordLargeFileHintPending,
|
|
1919
|
+
takePendingLargeFileHint,
|
|
1920
|
+
WEB_FETCH_KEY_SEP,
|
|
1921
|
+
recordWebFetch,
|
|
1922
|
+
getSessionWebFetches,
|
|
1923
|
+
recordBashOutput,
|
|
1924
|
+
getSessionBashOutputs,
|
|
1925
|
+
recordBashRerun,
|
|
1926
|
+
getSessionBashReruns,
|
|
1927
|
+
getBashOutputId,
|
|
1928
|
+
recordGrepQuery,
|
|
1929
|
+
getGrepMatchCount,
|
|
1930
|
+
recordGlobQuery,
|
|
1931
|
+
getGlobMatchCount,
|
|
1932
|
+
setLastTabContext,
|
|
1933
|
+
lastTabContextMatches,
|
|
1934
|
+
hasSeenImage,
|
|
1935
|
+
recordSeenImage,
|
|
1936
|
+
recordOutstandingAgentSpawn,
|
|
1937
|
+
getOutstandingAgentSpawns,
|
|
1938
|
+
removeOutstandingAgentSpawn,
|
|
1939
|
+
recordCurlDownload,
|
|
1940
|
+
getCurlDownloadPath,
|
|
1941
|
+
clearCurlDownload,
|
|
1942
|
+
recordFileLineRange,
|
|
1943
|
+
getFileLineRanges,
|
|
1944
|
+
resetFileLineRanges,
|
|
1945
|
+
GENERIC_SERVED_OUTPUT_KEY,
|
|
1946
|
+
recordFileServedOutput,
|
|
1947
|
+
getFileServedOutputs,
|
|
1948
|
+
markFileTruncated,
|
|
1949
|
+
wasFileTruncatedThisSession,
|
|
1950
|
+
getSessionId,
|
|
1951
|
+
setTranscriptPath,
|
|
1952
|
+
getTranscriptPath,
|
|
1953
|
+
exportSessionState,
|
|
1954
|
+
SESSIONS_SUBDIR,
|
|
1955
|
+
sessionsDir,
|
|
1956
|
+
AGENT_SALT_MARKER,
|
|
1957
|
+
sessionSidecarPath,
|
|
1958
|
+
listSiblingSessionStates,
|
|
1959
|
+
loadSessionState,
|
|
1960
|
+
saveSessionState,
|
|
1961
|
+
CONTEXT_AUTOCOMPACT_TOKENS,
|
|
1962
|
+
getContextPressure,
|
|
1963
|
+
isNoisePath,
|
|
1964
|
+
findLatestSessionId,
|
|
1965
|
+
eventCount,
|
|
1966
|
+
writeSessionManifest,
|
|
1967
|
+
readAllSessionManifests,
|
|
1968
|
+
loadSessionCache,
|
|
1969
|
+
computeAdaptiveBudget,
|
|
1970
|
+
SKILLS_OUTPUT_SUBDIR,
|
|
1971
|
+
skillOutputsDir,
|
|
1972
|
+
contentHash,
|
|
1973
|
+
extractCompactFromMarker,
|
|
1974
|
+
extractNamedSection,
|
|
1975
|
+
extractChecklistSection,
|
|
1976
|
+
listOutputs,
|
|
1977
|
+
hasSessionOutput,
|
|
1978
|
+
sessionOutputBodyBytes,
|
|
1979
|
+
storeOutput,
|
|
1980
|
+
storeCompact,
|
|
1981
|
+
getCompactAnySessionSync,
|
|
1982
|
+
isCompactStale,
|
|
1983
|
+
incrementSkillHit,
|
|
1984
|
+
formatAge,
|
|
1985
|
+
listSkills,
|
|
1986
|
+
getSkillFilePath,
|
|
1987
|
+
installedSkillPath,
|
|
1988
|
+
pruneSkillOutputs
|
|
1989
|
+
};
|