@aexol/spectral 0.9.44 → 0.9.45
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crawler.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/crawler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"crawler.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/crawler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAEV,WAAW,EACX,WAAW,EAGZ,MAAM,aAAa,CAAC;AAgHrB,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GACjC,OAAO,CAAC,WAAW,CAAC,CA+LtB"}
|
|
@@ -9,6 +9,26 @@ const DEFAULT_CRAWL_CONFIG = {
|
|
|
9
9
|
crawlDelayMs: 500,
|
|
10
10
|
includeExternal: false,
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Strip query parameters and hash for URL identity comparison.
|
|
14
|
+
* Two URLs that differ only by query params or hash should be considered
|
|
15
|
+
* the same page for crawling purposes — prevents re-crawling the same page
|
|
16
|
+
* hundreds of times when query params encode ephemeral UI state.
|
|
17
|
+
*/
|
|
18
|
+
function urlIdentity(url) {
|
|
19
|
+
try {
|
|
20
|
+
const u = new URL(url);
|
|
21
|
+
u.search = "";
|
|
22
|
+
u.hash = "";
|
|
23
|
+
if (u.pathname.length > 1 && u.pathname.endsWith("/")) {
|
|
24
|
+
u.pathname = u.pathname.replace(/\/+$/, "");
|
|
25
|
+
}
|
|
26
|
+
return u.href;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
12
32
|
function extractInternalLinks(html, baseUrl) {
|
|
13
33
|
const links = [];
|
|
14
34
|
const hrefRegex = /<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
@@ -18,22 +38,20 @@ function extractInternalLinks(html, baseUrl) {
|
|
|
18
38
|
const resolved = new URL(match[1], baseUrl).href;
|
|
19
39
|
const base = new URL(baseUrl);
|
|
20
40
|
const resolvedUrl = new URL(resolved);
|
|
21
|
-
// Only include same-host, non-fragment, non-anchor links
|
|
22
41
|
if (resolvedUrl.hostname === base.hostname &&
|
|
23
42
|
resolvedUrl.protocol.startsWith("http") &&
|
|
24
43
|
!resolved.includes("#") &&
|
|
25
44
|
!resolved.includes("javascript:") &&
|
|
26
|
-
!resolved.includes("mailto:")
|
|
27
|
-
|
|
45
|
+
!resolved.includes("mailto:") &&
|
|
46
|
+
!resolved.includes("/cdn-cgi/")) {
|
|
28
47
|
const normalized = normalizeUrl(resolved);
|
|
29
48
|
links.push(normalized);
|
|
30
49
|
}
|
|
31
50
|
}
|
|
32
51
|
catch {
|
|
33
|
-
// skip malformed
|
|
52
|
+
// skip malformed hrefs
|
|
34
53
|
}
|
|
35
54
|
}
|
|
36
|
-
// Deduplicate
|
|
37
55
|
return [...new Set(links)];
|
|
38
56
|
}
|
|
39
57
|
function parseRobotsTxtForDisallowed(content, userAgent) {
|
|
@@ -53,7 +71,8 @@ function parseRobotsTxtForDisallowed(content, userAgent) {
|
|
|
53
71
|
if (key === "user-agent") {
|
|
54
72
|
currentUserAgents.push(value.toLowerCase());
|
|
55
73
|
}
|
|
56
|
-
else if (inRelevantBlock ||
|
|
74
|
+
else if (inRelevantBlock ||
|
|
75
|
+
currentUserAgents.some((ua) => ua === "*" || ua === userAgent.toLowerCase())) {
|
|
57
76
|
inRelevantBlock = true;
|
|
58
77
|
if (key === "disallow") {
|
|
59
78
|
if (value)
|
|
@@ -91,7 +110,6 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
91
110
|
const cfg = { ...DEFAULT_CRAWL_CONFIG, ...crawlConfig };
|
|
92
111
|
const normalizedStart = normalizeUrl(startUrl);
|
|
93
112
|
const baseHost = new URL(normalizedStart).hostname;
|
|
94
|
-
// Fetch robots.txt
|
|
95
113
|
let disallowedPaths = [];
|
|
96
114
|
let robotsCrawlDelay = null;
|
|
97
115
|
if (cfg.respectRobotsTxt) {
|
|
@@ -104,23 +122,31 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
catch {
|
|
107
|
-
// robots.txt
|
|
125
|
+
// robots.txt not available — crawl freely
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
|
-
const delayMs = robotsCrawlDelay
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
128
|
+
const delayMs = robotsCrawlDelay
|
|
129
|
+
? robotsCrawlDelay * 1000
|
|
130
|
+
: cfg.crawlDelayMs;
|
|
131
|
+
const visitedIdentities = new Set();
|
|
132
|
+
const queuedIdentities = new Set();
|
|
133
|
+
const queue = [];
|
|
134
|
+
const startIdentity = urlIdentity(normalizedStart);
|
|
135
|
+
queuedIdentities.add(startIdentity);
|
|
136
|
+
queue.push({ url: normalizedStart, depth: 0 });
|
|
114
137
|
const pages = [];
|
|
115
138
|
const brokenLinks = [];
|
|
116
139
|
const errors = [];
|
|
117
140
|
const allDiscoveredLinks = new Set();
|
|
118
|
-
while (queue.length > 0 &&
|
|
119
|
-
|
|
141
|
+
while (queue.length > 0 &&
|
|
142
|
+
visitedIdentities.size < cfg.maxPages) {
|
|
120
143
|
const batch = [];
|
|
121
|
-
while (batch.length < cfg.concurrency &&
|
|
144
|
+
while (batch.length < cfg.concurrency &&
|
|
145
|
+
queue.length > 0 &&
|
|
146
|
+
visitedIdentities.size + batch.length < cfg.maxPages) {
|
|
122
147
|
const next = queue.shift();
|
|
123
|
-
|
|
148
|
+
const identity = urlIdentity(next.url);
|
|
149
|
+
if (visitedIdentities.has(identity))
|
|
124
150
|
continue;
|
|
125
151
|
if (isDisallowed(next.url, disallowedPaths))
|
|
126
152
|
continue;
|
|
@@ -165,21 +191,28 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
165
191
|
continue;
|
|
166
192
|
}
|
|
167
193
|
const page = result.value;
|
|
168
|
-
|
|
194
|
+
const identity = urlIdentity(page.url);
|
|
195
|
+
visitedIdentities.add(identity);
|
|
169
196
|
pages.push(page);
|
|
170
|
-
// Check for broken external links from page data
|
|
171
197
|
if (page.pageData) {
|
|
172
198
|
for (const link of page.pageData.links) {
|
|
173
199
|
if (link.type === "external")
|
|
174
200
|
continue;
|
|
175
201
|
allDiscoveredLinks.add(link.href);
|
|
176
|
-
// Internal links we haven't visited yet — add to queue
|
|
177
202
|
const normalized = normalizeUrl(link.href);
|
|
178
|
-
|
|
203
|
+
const linkIdentity = urlIdentity(normalized);
|
|
204
|
+
if (!visitedIdentities.has(linkIdentity) &&
|
|
205
|
+
!queuedIdentities.has(linkIdentity) &&
|
|
206
|
+
linkIdentity !== identity) {
|
|
179
207
|
try {
|
|
180
208
|
const linkUrl = new URL(normalized);
|
|
181
|
-
if (linkUrl.hostname === baseHost &&
|
|
182
|
-
|
|
209
|
+
if (linkUrl.hostname === baseHost &&
|
|
210
|
+
page.depth < cfg.maxDepth) {
|
|
211
|
+
queuedIdentities.add(linkIdentity);
|
|
212
|
+
queue.push({
|
|
213
|
+
url: normalized,
|
|
214
|
+
depth: page.depth + 1,
|
|
215
|
+
});
|
|
183
216
|
}
|
|
184
217
|
}
|
|
185
218
|
catch {
|
|
@@ -188,13 +221,12 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
188
221
|
}
|
|
189
222
|
}
|
|
190
223
|
}
|
|
191
|
-
// Delay between batches
|
|
192
224
|
if (delayMs > 0) {
|
|
193
225
|
await sleep(delayMs);
|
|
194
226
|
}
|
|
195
227
|
}
|
|
196
228
|
}
|
|
197
|
-
// Detect broken links
|
|
229
|
+
// Detect broken links (pages crawled with 4xx/5xx status)
|
|
198
230
|
for (const page of pages) {
|
|
199
231
|
if (page.statusCode >= 400) {
|
|
200
232
|
brokenLinks.push({
|
|
@@ -205,9 +237,7 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
205
237
|
});
|
|
206
238
|
}
|
|
207
239
|
}
|
|
208
|
-
// Detect duplicate content
|
|
209
240
|
const duplicateGroups = detectDuplicates(pages);
|
|
210
|
-
// Orphan pages = pages only discovered via crawling that weren't linked from any other page
|
|
211
241
|
const linkedPages = new Set();
|
|
212
242
|
for (const page of pages) {
|
|
213
243
|
for (const link of page.linksFound) {
|
|
@@ -217,7 +247,6 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
217
247
|
const orphanPages = pages
|
|
218
248
|
.filter((p) => p.depth === 0 && !linkedPages.has(p.url))
|
|
219
249
|
.map((p) => p.url);
|
|
220
|
-
// Build link graph
|
|
221
250
|
const linkGraph = pages.map((p) => ({
|
|
222
251
|
url: p.url,
|
|
223
252
|
internalInlinks: pages.reduce((count, other) => count + (other.linksFound.includes(p.url) ? 1 : 0), 0),
|
|
@@ -241,10 +270,20 @@ export async function crawlSite(startUrl, config, crawlConfig) {
|
|
|
241
270
|
}
|
|
242
271
|
function detectDuplicates(pages) {
|
|
243
272
|
const groups = [];
|
|
273
|
+
// Use urlIdentity for deduplication so /studio/ and /studio/?pako=...
|
|
274
|
+
// are counted as the same page
|
|
275
|
+
const identitySet = new Set();
|
|
276
|
+
const uniquePages = pages.filter((p) => {
|
|
277
|
+
const id = urlIdentity(p.url);
|
|
278
|
+
if (identitySet.has(id))
|
|
279
|
+
return false;
|
|
280
|
+
identitySet.add(id);
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
244
283
|
const titleMap = new Map();
|
|
245
284
|
const descMap = new Map();
|
|
246
285
|
const h1Map = new Map();
|
|
247
|
-
for (const page of
|
|
286
|
+
for (const page of uniquePages) {
|
|
248
287
|
if (!page.pageData)
|
|
249
288
|
continue;
|
|
250
289
|
const title = page.pageData.title?.trim().toLowerCase();
|
|
@@ -269,17 +308,17 @@ function detectDuplicates(pages) {
|
|
|
269
308
|
h1Map.get(h1).push(page.url);
|
|
270
309
|
}
|
|
271
310
|
}
|
|
272
|
-
for (const [
|
|
311
|
+
for (const [value, urls] of titleMap) {
|
|
273
312
|
if (urls.length > 1)
|
|
274
|
-
groups.push({ type: "title", value
|
|
313
|
+
groups.push({ type: "title", value, pageUrls: urls });
|
|
275
314
|
}
|
|
276
|
-
for (const [
|
|
315
|
+
for (const [value, urls] of descMap) {
|
|
277
316
|
if (urls.length > 1)
|
|
278
|
-
groups.push({ type: "description", value
|
|
317
|
+
groups.push({ type: "description", value, pageUrls: urls });
|
|
279
318
|
}
|
|
280
|
-
for (const [
|
|
319
|
+
for (const [value, urls] of h1Map) {
|
|
281
320
|
if (urls.length > 1)
|
|
282
|
-
groups.push({ type: "h1", value
|
|
321
|
+
groups.push({ type: "h1", value, pageUrls: urls });
|
|
283
322
|
}
|
|
284
323
|
return groups;
|
|
285
324
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAMH,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAejD,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EAEtB,WAAW,EACX,WAAW,EACX,wBAAwB,EAGzB,MAAM,WAAW,CAAC;AAEnB;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,2EAA2E;IAC3E,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,wEAAwE;IACxE,MAAM,IAAI,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,IAAI,IAAI,CAAC;IAChB;;;;;OAKG;IACH,OAAO,CAAC,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD;;;OAGG;IACH,kBAAkB,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACtD;;;;;;OAMG;IACH,QAAQ,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,oBAAoB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;;;;;OAKG;IACH,wBAAwB,CAAC,IAAI,MAAM,GAAG,SAAS,CAAC;IAChD;;;;OAIG;IACH,eAAe,CAAC,IAAI;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAC;IACzG,gBAAgB,CAAC,IAAI,KAAK,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC,CAAC;IACH,iBAAiB,CAAC,IAAI;QACpB,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;QACtE,QAAQ,EAAE;YACR,QAAQ,EAAE,OAAO,CAAC;YAClB,UAAU,EAAE,OAAO,CAAC;YACpB,UAAU,EAAE,OAAO,CAAC;YACpB,MAAM,EAAE,OAAO,CAAC;SACjB,CAAC;KACH,CAAC;CACH;AAED,iDAAiD;AACjD,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,kBAAkB,KAAK,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAMH,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAejD,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EAEtB,WAAW,EACX,WAAW,EACX,wBAAwB,EAGzB,MAAM,WAAW,CAAC;AAEnB;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,2EAA2E;IAC3E,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,wEAAwE;IACxE,MAAM,IAAI,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,IAAI,IAAI,CAAC;IAChB;;;;;OAKG;IACH,OAAO,CAAC,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD;;;OAGG;IACH,kBAAkB,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACtD;;;;;;OAMG;IACH,QAAQ,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,oBAAoB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;;;;;OAKG;IACH,wBAAwB,CAAC,IAAI,MAAM,GAAG,SAAS,CAAC;IAChD;;;;OAIG;IACH,eAAe,CAAC,IAAI;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAC;IACzG,gBAAgB,CAAC,IAAI,KAAK,CAAC;QACzB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC,CAAC;IACH,iBAAiB,CAAC,IAAI;QACpB,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;QACtE,QAAQ,EAAE;YACR,QAAQ,EAAE,OAAO,CAAC;YAClB,UAAU,EAAE,OAAO,CAAC;YACpB,UAAU,EAAE,OAAO,CAAC;YACpB,MAAM,EAAE,OAAO,CAAC;SACjB,CAAC;KACH,CAAC;CACH;AAED,iDAAiD;AACjD,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,kBAAkB,KAAK,UAAU,CAAC;AAkKrE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;IACtE,QAAQ,EAAE;QACR,QAAQ,EAAE,OAAO,CAAC;QAClB,UAAU,EAAE,OAAO,CAAC;QACpB,UAAU,EAAE,OAAO,CAAC;QACpB,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;IACF,WAAW,EAAE;QACX,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,YAAY,EAAE;QACZ,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,UAAU,EAAE;QACV,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,QAAQ,EAAE;QACR,2BAA2B,EAAE,MAAM,CAAC;QACpC,yBAAyB,EAAE,MAAM,CAAC;QAClC,qBAAqB,EAAE,MAAM,CAAC;KAC/B,CAAC;CACH;AAiCD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC3C,4FAA4F;IAC5F,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB;;qEAEiE;IACjE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,wGAAwG;IACxG,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,yFAAyF;IACzF,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,0EAA0E;IAC1E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,YAAY,CAAC;IACpB;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAS;gBAEb,IAAI,EAAE,2BAA2B;IA4B7C;;;;;;;;OAQG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,YAAY;IAkC/D;;;;OAIG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI;IAOvD,6EAA6E;IAC7E,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIzC;;;;;OAKG;IACH,uBAAuB,IAAI,GAAG,CAAC,MAAM,CAAC;IAOtC;;;;;OAKG;IACG,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAU5C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB;IAiE9D,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,wBAAwB;IA8D9D,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCtD;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,eAAe,EAAE,EAC1B,eAAe,CAAC,EAAE,MAAM,EACxB,IAAI,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACnC,OAAO,CAAC,IAAI,CAAC;IA2MhB;;;OAGG;IACH,OAAO,IAAI,IAAI;IAef,sEAAsE;IACtE,WAAW,IAAI,MAAM;IAIrB;;;;;;;;;OASG;IACH,eAAe,IAAI,MAAM;IAQzB;;;;;;;;OAQG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAsDnC;;;;;;;OAOG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IA4B7C;;;;;OAKG;IACH,qBAAqB,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IAM1D;;;;;;;;OAQG;IACH,aAAa,CACX,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACtB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI;IAkBP;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;IA8D1B;;;;;;;;;;;;;;OAcG;IACH;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,eAAe;YA6BT,qBAAqB;IAgCnC,OAAO,CAAC,YAAY;IAqKpB,OAAO,CAAC,iBAAiB;IAsSzB;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB;IA+BzB,OAAO,CAAC,SAAS;IAqBjB;;;;OAIG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAevC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CA2BzB"}
|
|
@@ -125,80 +125,6 @@ function selectHistoryForBridge(history, hasMemorySnapshot) {
|
|
|
125
125
|
const selected = history.slice(Math.max(0, start + 1));
|
|
126
126
|
return selected.length > 0 ? selected : [history[history.length - 1]];
|
|
127
127
|
}
|
|
128
|
-
function prunePersistedHistoryAfterCompaction(store, sessionId, bridge) {
|
|
129
|
-
if (!bridge.getSessionBranch)
|
|
130
|
-
return;
|
|
131
|
-
try {
|
|
132
|
-
const entries = bridge.getSessionBranch();
|
|
133
|
-
// Pi appends a CompactionEntry (type "compaction") at the end whose
|
|
134
|
-
// firstKeptEntryId marks the first branch entry that survives in LLM context.
|
|
135
|
-
// We scan backwards so we find the most recent compaction (there should be
|
|
136
|
-
// exactly one per compaction pass).
|
|
137
|
-
const lastCompaction = [...entries].reverse().find((entry) => entry.type === "compaction");
|
|
138
|
-
const firstKeptEntryId = lastCompaction?.firstKeptEntryId;
|
|
139
|
-
if (!firstKeptEntryId) {
|
|
140
|
-
console.log(`[spectral] prune: skipped – no compaction entry (or no firstKeptEntryId) ` +
|
|
141
|
-
`in branch for ${sessionId} (${entries.length} entries)`);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const keptEntryIndex = entries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
145
|
-
if (keptEntryIndex === -1) {
|
|
146
|
-
console.log(`[spectral] prune: skipped – firstKeptEntryId "${firstKeptEntryId}" ` +
|
|
147
|
-
`not found in ${entries.length} branch entries for ${sessionId}`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
// Count user + assistant messages in the kept portion of the branch.
|
|
151
|
-
// Tool-result entries are separate branch entries but are NOT separate
|
|
152
|
-
// SQLite rows – they live inside the assistant message's events_jsonl.
|
|
153
|
-
// Counting them would inflate the count and make the prune a silent no-op.
|
|
154
|
-
let keptPersistedMessageCount = 0;
|
|
155
|
-
for (let i = keptEntryIndex; i < entries.length; i++) {
|
|
156
|
-
const entry = entries[i];
|
|
157
|
-
if (entry.type !== "message")
|
|
158
|
-
continue;
|
|
159
|
-
const role = entry.message && typeof entry.message === "object" && "role" in entry.message
|
|
160
|
-
? entry.message.role
|
|
161
|
-
: undefined;
|
|
162
|
-
if (role === "user" || role === "assistant" || role === "system") {
|
|
163
|
-
keptPersistedMessageCount += 1;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const existing = store.getMessages(sessionId);
|
|
167
|
-
if (existing.length === 0)
|
|
168
|
-
return;
|
|
169
|
-
console.log(`[spectral] prune: ${sessionId} – branch has ${entries.length} entries, ` +
|
|
170
|
-
`kept portion starts at index ${keptEntryIndex} (id=${firstKeptEntryId}), ` +
|
|
171
|
-
`${keptPersistedMessageCount} user+assistant entries kept, ` +
|
|
172
|
-
`${existing.length} SQLite messages`);
|
|
173
|
-
if (keptPersistedMessageCount <= 0) {
|
|
174
|
-
console.log(`[spectral] prune: skipped – no kept messages for ${sessionId}`);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (keptPersistedMessageCount >= existing.length) {
|
|
178
|
-
console.log(`[spectral] prune: skipped – kept message count (${keptPersistedMessageCount}) ` +
|
|
179
|
-
`>= SQLite message count (${existing.length}) for ${sessionId}`);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const keptMessages = existing.slice(-keptPersistedMessageCount);
|
|
183
|
-
const pruned = existing.length - keptMessages.length;
|
|
184
|
-
store.deleteMessagesBySession(sessionId);
|
|
185
|
-
for (const msg of keptMessages) {
|
|
186
|
-
store.appendMessage(sessionId, {
|
|
187
|
-
id: msg.id,
|
|
188
|
-
role: msg.role,
|
|
189
|
-
content: msg.content,
|
|
190
|
-
eventsJsonl: msg.events,
|
|
191
|
-
createdAt: msg.createdAt,
|
|
192
|
-
images: msg.images,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
console.log(`[spectral] prune: finished – dropped ${pruned} pre-compaction message(s) ` +
|
|
196
|
-
`for ${sessionId}, ${keptMessages.length} kept`);
|
|
197
|
-
}
|
|
198
|
-
catch (err) {
|
|
199
|
-
console.warn(`[spectral] warn: failed to prune persisted history after compaction for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
128
|
function defaultMemoryStatus(cwd) {
|
|
203
129
|
const config = loadConfig(cwd);
|
|
204
130
|
return {
|
|
@@ -484,7 +410,6 @@ export class SessionStreamManager {
|
|
|
484
410
|
// Prune persisted history synchronously after compaction, so even if the
|
|
485
411
|
// compaction_end event handler races or misses, SQLite reflects the
|
|
486
412
|
// compacted state before the next user prompt or reconnect.
|
|
487
|
-
prunePersistedHistoryAfterCompaction(this.store, sessionId, stream.bridge);
|
|
488
413
|
}
|
|
489
414
|
finally {
|
|
490
415
|
if (stream.compacting)
|
|
@@ -1270,9 +1195,9 @@ export class SessionStreamManager {
|
|
|
1270
1195
|
}
|
|
1271
1196
|
if (event.type === "compaction_end") {
|
|
1272
1197
|
stream.compacting = false;
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1198
|
+
// Keep the full persisted history in SQLite after compaction.
|
|
1199
|
+
// The LLM context is already compacted via the bridge/session manager;
|
|
1200
|
+
// landing can still load older messages through the REST endpoint.
|
|
1276
1201
|
persistObservationalMemorySnapshot(this.store, stream.sessionId, stream.bridge);
|
|
1277
1202
|
// Drain the prompt queue after compaction finishes. During
|
|
1278
1203
|
// compaction, auto-dequeue in agent_end is skipped to prevent the
|