@newrelic/preflight 1.71.0 → 1.72.0
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/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +2 -9
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/dashboard/subagent-timeline-store.d.ts +12 -1
- package/dist/dashboard/subagent-timeline-store.d.ts.map +1 -1
- package/dist/dashboard/subagent-timeline-store.js +32 -4
- package/dist/dashboard/subagent-timeline-store.js.map +1 -1
- package/dist/dashboard/workflow-store.d.ts.map +1 -1
- package/dist/dashboard/workflow-store.js +2 -3
- package/dist/dashboard/workflow-store.js.map +1 -1
- package/dist/hooks/event-processor.d.ts +20 -5
- package/dist/hooks/event-processor.d.ts.map +1 -1
- package/dist/hooks/event-processor.js +135 -39
- package/dist/hooks/event-processor.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/parent-transcript-watcher.d.ts +1 -1
- package/dist/hooks/parent-transcript-watcher.d.ts.map +1 -1
- package/dist/hooks/parent-transcript-watcher.js +6 -2
- package/dist/hooks/parent-transcript-watcher.js.map +1 -1
- package/dist/hooks/session-resolver.d.ts.map +1 -1
- package/dist/hooks/session-resolver.js +2 -1
- package/dist/hooks/session-resolver.js.map +1 -1
- package/dist/hooks/subagent-watcher.d.ts +3 -1
- package/dist/hooks/subagent-watcher.d.ts.map +1 -1
- package/dist/hooks/subagent-watcher.js +6 -4
- package/dist/hooks/subagent-watcher.js.map +1 -1
- package/dist/hooks/workflow-watcher.d.ts.map +1 -1
- package/dist/hooks/workflow-watcher.js +2 -2
- package/dist/hooks/workflow-watcher.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -60
- package/dist/index.js.map +1 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +81 -7
- package/dist/install/cli.js.map +1 -1
- package/dist/install/copilot-install-helper.d.ts +1 -0
- package/dist/install/copilot-install-helper.d.ts.map +1 -1
- package/dist/install/copilot-install-helper.js +82 -26
- package/dist/install/copilot-install-helper.js.map +1 -1
- package/dist/install/diagnostics.d.ts.map +1 -1
- package/dist/install/diagnostics.js +6 -3
- package/dist/install/diagnostics.js.map +1 -1
- package/dist/install/install-helper.d.ts +9 -0
- package/dist/install/install-helper.d.ts.map +1 -1
- package/dist/install/install-helper.js +42 -17
- package/dist/install/install-helper.js.map +1 -1
- package/dist/install/schedule.d.ts.map +1 -1
- package/dist/install/schedule.js +4 -1
- package/dist/install/schedule.js.map +1 -1
- package/dist/install/target-user.d.ts +33 -0
- package/dist/install/target-user.d.ts.map +1 -0
- package/dist/install/target-user.js +121 -0
- package/dist/install/target-user.js.map +1 -0
- package/dist/lib/claude-config-dir.d.ts +3 -0
- package/dist/lib/claude-config-dir.d.ts.map +1 -0
- package/dist/lib/claude-config-dir.js +10 -0
- package/dist/lib/claude-config-dir.js.map +1 -0
- package/dist/lib/claude-settings-env.d.ts +18 -0
- package/dist/lib/claude-settings-env.d.ts.map +1 -0
- package/dist/lib/claude-settings-env.js +78 -0
- package/dist/lib/claude-settings-env.js.map +1 -0
- package/dist/lib/date.d.ts +7 -0
- package/dist/lib/date.d.ts.map +1 -1
- package/dist/lib/date.js +11 -1
- package/dist/lib/date.js.map +1 -1
- package/dist/lib/subagent-transcript-parser.d.ts +2 -0
- package/dist/lib/subagent-transcript-parser.d.ts.map +1 -1
- package/dist/lib/subagent-transcript-parser.js +5 -0
- package/dist/lib/subagent-transcript-parser.js.map +1 -1
- package/dist/lib/transcript-types.d.ts +4 -0
- package/dist/lib/transcript-types.d.ts.map +1 -1
- package/dist/metrics/cost-forecast.d.ts +2 -0
- package/dist/metrics/cost-forecast.d.ts.map +1 -1
- package/dist/metrics/cost-forecast.js +46 -24
- package/dist/metrics/cost-forecast.js.map +1 -1
- package/dist/metrics/cost-tracker.d.ts +3 -3
- package/dist/metrics/cost-tracker.js +3 -3
- package/dist/metrics/efficiency-score.d.ts +2 -2
- package/dist/metrics/efficiency-score.js +2 -2
- package/dist/metrics/first-save-baselines.d.ts +60 -0
- package/dist/metrics/first-save-baselines.d.ts.map +1 -0
- package/dist/metrics/first-save-baselines.js +77 -0
- package/dist/metrics/first-save-baselines.js.map +1 -0
- package/dist/metrics/local-session-aggregator.d.ts +2 -1
- package/dist/metrics/local-session-aggregator.d.ts.map +1 -1
- package/dist/metrics/local-session-aggregator.js +21 -0
- package/dist/metrics/local-session-aggregator.js.map +1 -1
- package/dist/metrics/model-pricing.d.ts +1 -1
- package/dist/metrics/model-pricing.d.ts.map +1 -1
- package/dist/metrics/model-pricing.js +2 -2
- package/dist/metrics/model-pricing.js.map +1 -1
- package/dist/metrics/model-usage-tracker.d.ts +2 -2
- package/dist/metrics/model-usage-tracker.js +2 -2
- package/dist/metrics/pricing-init.d.ts.map +1 -1
- package/dist/metrics/pricing-init.js +5 -3
- package/dist/metrics/pricing-init.js.map +1 -1
- package/dist/metrics/pricing-overlay.d.ts.map +1 -1
- package/dist/metrics/pricing-overlay.js +1 -4
- package/dist/metrics/pricing-overlay.js.map +1 -1
- package/dist/metrics/pricing-refresh.d.ts +16 -4
- package/dist/metrics/pricing-refresh.d.ts.map +1 -1
- package/dist/metrics/pricing-refresh.js +47 -1
- package/dist/metrics/pricing-refresh.js.map +1 -1
- package/dist/metrics/quality-proxy-tracker.d.ts +2 -2
- package/dist/metrics/quality-proxy-tracker.js +2 -2
- package/dist/metrics/session-tracker.d.ts +5 -1
- package/dist/metrics/session-tracker.d.ts.map +1 -1
- package/dist/metrics/session-tracker.js +58 -17
- package/dist/metrics/session-tracker.js.map +1 -1
- package/dist/metrics/task-detector.d.ts +2 -2
- package/dist/metrics/task-detector.js +2 -2
- package/dist/metrics/token-usage.d.ts +6 -0
- package/dist/metrics/token-usage.d.ts.map +1 -0
- package/dist/metrics/token-usage.js +34 -0
- package/dist/metrics/token-usage.js.map +1 -0
- package/dist/metrics/turn-cost-attributor.d.ts.map +1 -1
- package/dist/metrics/turn-cost-attributor.js +3 -0
- package/dist/metrics/turn-cost-attributor.js.map +1 -1
- package/dist/platforms/copilot-adapter.d.ts.map +1 -1
- package/dist/platforms/copilot-adapter.js +3 -0
- package/dist/platforms/copilot-adapter.js.map +1 -1
- package/dist/platforms/copilot-sdk-adapter.d.ts.map +1 -1
- package/dist/platforms/copilot-sdk-adapter.js +3 -0
- package/dist/platforms/copilot-sdk-adapter.js.map +1 -1
- package/dist/storage/drain-claims.d.ts +33 -10
- package/dist/storage/drain-claims.d.ts.map +1 -1
- package/dist/storage/drain-claims.js +78 -13
- package/dist/storage/drain-claims.js.map +1 -1
- package/dist/storage/local-store.d.ts +21 -0
- package/dist/storage/local-store.d.ts.map +1 -1
- package/dist/storage/local-store.js +118 -19
- package/dist/storage/local-store.js.map +1 -1
- package/dist/storage/retention.d.ts.map +1 -1
- package/dist/storage/retention.js +7 -6
- package/dist/storage/retention.js.map +1 -1
- package/dist/storage/session-store.d.ts +16 -2
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/storage/session-store.js +242 -37
- package/dist/storage/session-store.js.map +1 -1
- package/dist/storage/types.d.ts +7 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +4 -0
- package/dist/storage/types.js.map +1 -1
- package/dist/tools/cost-tools.d.ts.map +1 -1
- package/dist/tools/cost-tools.js +1 -0
- package/dist/tools/cost-tools.js.map +1 -1
- package/dist/web/assets/{index-U9DPZ-Tb.js → index-CZvLy2Qy.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +4 -1
|
@@ -2,12 +2,13 @@ import { readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { createLogger } from '../shared/index.js';
|
|
4
4
|
const logger = createLogger('retention');
|
|
5
|
+
const PURGED_SUFFIXES = ['.json', '.corrupt', '.tmp'];
|
|
5
6
|
/**
|
|
6
|
-
* Delete every
|
|
7
|
-
* than `retainDays`. Shared by `purgeOldSessions` and
|
|
7
|
+
* Delete every `PURGED_SUFFIXES` file in `<storagePath>/<subdir>` whose mtime
|
|
8
|
+
* is older than `retainDays`. Shared by `purgeOldSessions` and
|
|
8
9
|
* `purgeOldWeeklySummaries` — only the subdirectory and log label differ.
|
|
9
10
|
*/
|
|
10
|
-
function
|
|
11
|
+
function purgeOldFiles(storagePath, subdir, retainDays, fileKind) {
|
|
11
12
|
const dir = resolve(storagePath, subdir);
|
|
12
13
|
// Files are deleted if mtime < cutoff. Files exactly at the cutoff (rare in practice) are retained.
|
|
13
14
|
const cutoffMs = Date.now() - retainDays * 24 * 60 * 60 * 1000;
|
|
@@ -20,7 +21,7 @@ function purgeOldJsonFiles(storagePath, subdir, retainDays, fileKind) {
|
|
|
20
21
|
return 0; // directory doesn't exist yet
|
|
21
22
|
}
|
|
22
23
|
for (const file of files) {
|
|
23
|
-
if (!file.endsWith(
|
|
24
|
+
if (!PURGED_SUFFIXES.some((suffix) => file.endsWith(suffix)))
|
|
24
25
|
continue;
|
|
25
26
|
const fullPath = resolve(dir, file);
|
|
26
27
|
try {
|
|
@@ -56,9 +57,9 @@ function purgeOldJsonFiles(storagePath, subdir, retainDays, fileKind) {
|
|
|
56
57
|
return deletedCount;
|
|
57
58
|
}
|
|
58
59
|
export function purgeOldSessions(storagePath, retainDays) {
|
|
59
|
-
return
|
|
60
|
+
return purgeOldFiles(storagePath, 'sessions', retainDays, 'session');
|
|
60
61
|
}
|
|
61
62
|
export function purgeOldWeeklySummaries(storagePath, retainDays) {
|
|
62
|
-
return
|
|
63
|
+
return purgeOldFiles(storagePath, 'weekly_summaries', retainDays, 'weekly summary');
|
|
63
64
|
}
|
|
64
65
|
//# sourceMappingURL=retention.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/storage/retention.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;AAEzC;;;;GAIG;AACH,SAAS,
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/storage/retention.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;AAEzC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;AAEtD;;;;GAIG;AACH,SAAS,aAAa,CACpB,WAAmB,EACnB,MAAc,EACd,UAAkB,EAClB,QAAgB;IAEhB,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACzC,oGAAoG;IACpG,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC/D,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC,CAAC,8BAA8B;IAC1C,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAAE,SAAS;QACvE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAChC,IAAI,IAAI,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC;gBAC5B,IAAI,CAAC;oBACH,UAAU,CAAC,QAAQ,CAAC,CAAC;oBACrB,YAAY,EAAE,CAAC;oBACf,MAAM,CAAC,KAAK,CAAC,cAAc,QAAQ,OAAO,EAAE;wBAC1C,IAAI;wBACJ,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC;qBAC9D,CAAC,CAAC;gBACL,CAAC;gBAAC,OAAO,SAAS,EAAE,CAAC;oBACnB,MAAM,IAAI,GAAI,SAAmC,CAAC,IAAI,CAAC;oBACvD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;wBACtB,sDAAsD;wBACtD,YAAY,EAAE,CAAC;oBACjB,CAAC;yBAAM,CAAC;wBACN,MAAM,CAAC,IAAI,CAAC,oBAAoB,QAAQ,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;oBACvF,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,0BAA0B,QAAQ,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IAED,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC,cAAc,QAAQ,QAAQ,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,UAAkB;IACtE,OAAO,aAAa,CAAC,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAmB,EAAE,UAAkB;IAC7E,OAAO,aAAa,CAAC,WAAW,EAAE,kBAAkB,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC;AACtF,CAAC"}
|
|
@@ -285,9 +285,23 @@ export declare class SessionStore {
|
|
|
285
285
|
storagePath: string;
|
|
286
286
|
});
|
|
287
287
|
getSessionsDir(): string;
|
|
288
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Returns null when nothing was written, else the file the write merged
|
|
290
|
+
* against (`previous`, null for a new file).
|
|
291
|
+
*/
|
|
292
|
+
saveSession(summary: FullSessionSummary): {
|
|
293
|
+
readonly previous: FullSessionSummary | null;
|
|
294
|
+
} | null;
|
|
289
295
|
loadSession(sessionId: string): FullSessionSummary | null;
|
|
290
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* Like loadSession(), but also returns the file path it was loaded from — used by saveSession() to detect and clean up a stale file after a merge relocates a session to a different filename.
|
|
298
|
+
*
|
|
299
|
+
* An unreadable file is skipped rather than ending the search: returning null
|
|
300
|
+
* there made saveSession() treat the session as new and start a fresh file
|
|
301
|
+
* on every later save. Sibling files for one id are not merged here,
|
|
302
|
+
* because files split that way hold disjoint activity that mergeSummaries'
|
|
303
|
+
* per-counter max would drop.
|
|
304
|
+
*/
|
|
291
305
|
private loadSessionWithPath;
|
|
292
306
|
listSessions(options?: ListSessionsOptions): SessionFileInfo[];
|
|
293
307
|
loadAllSessions(options?: LoadSessionsOptions): FullSessionSummary[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/storage/session-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/storage/session-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* `buildSessionSummary()` pulls getMetrics() from each tracker and aggregates
|
|
12
12
|
* task-level data into a single FullSessionSummary.
|
|
13
13
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
15
|
-
import {
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
16
17
|
import { createLogger } from '../shared/index.js';
|
|
17
18
|
import { redactIdentifier, redactSensitive } from '../config.js';
|
|
18
19
|
import { stripRepoNameCredentials } from '../lib/git-remote.js';
|
|
@@ -389,10 +390,14 @@ export class SessionStore {
|
|
|
389
390
|
getSessionsDir() {
|
|
390
391
|
return this.sessionsDir;
|
|
391
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Returns null when nothing was written, else the file the write merged
|
|
395
|
+
* against (`previous`, null for a new file).
|
|
396
|
+
*/
|
|
392
397
|
saveSession(summary) {
|
|
393
398
|
if (!/^[A-Za-z0-9_-]{1,128}$/.test(summary.sessionId) || summary.sessionId === 'unknown') {
|
|
394
399
|
logger.warn('Rejecting invalid sessionId for file path', { sessionId: summary.sessionId });
|
|
395
|
-
return;
|
|
400
|
+
return null;
|
|
396
401
|
}
|
|
397
402
|
if (!existsSync(this.sessionsDir)) {
|
|
398
403
|
mkdirSync(this.sessionsDir, { recursive: true, mode: 0o700 });
|
|
@@ -408,7 +413,7 @@ export class SessionStore {
|
|
|
408
413
|
// merged (inflated, since counters take the max) result to a NEW file
|
|
409
414
|
// while leaving the OLD file on disk untouched — the session then
|
|
410
415
|
// appeared twice in history with overlapping, double-counted totals.
|
|
411
|
-
const existingWithPath = this.loadSessionWithPath(summary.sessionId);
|
|
416
|
+
const { existing: existingWithPath, filesRead } = this.loadSessionWithPath(summary.sessionId);
|
|
412
417
|
// Every MCP server process checkpoints every 30s, including windows that
|
|
413
418
|
// never saw a tool call or a token. Writing those first-time files filled
|
|
414
419
|
// history with model-less zero-activity rows that dragged down every
|
|
@@ -417,7 +422,7 @@ export class SessionStore {
|
|
|
417
422
|
logger.debug('Skipping first write of a session with no recorded usage', {
|
|
418
423
|
sessionId: summary.sessionId,
|
|
419
424
|
});
|
|
420
|
-
return;
|
|
425
|
+
return null;
|
|
421
426
|
}
|
|
422
427
|
// A second process saving under the same sessionId (e.g. two MCP
|
|
423
428
|
// servers both resumed/forked against one real session ID) would
|
|
@@ -437,7 +442,7 @@ export class SessionStore {
|
|
|
437
442
|
logger.warn('Refusing to overwrite recorded session with an empty summary', {
|
|
438
443
|
sessionId: summary.sessionId,
|
|
439
444
|
});
|
|
440
|
-
return;
|
|
445
|
+
return null;
|
|
441
446
|
}
|
|
442
447
|
// Two processes legitimately share one session id: the CLI/editor
|
|
443
448
|
// spawns an MCP engine that owns the session natively, while a
|
|
@@ -465,8 +470,28 @@ export class SessionStore {
|
|
|
465
470
|
if (!filepath.startsWith(this.sessionsDir + sep)) {
|
|
466
471
|
throw new Error(`Session path escaped storage directory: ${filepath}`);
|
|
467
472
|
}
|
|
473
|
+
const unparsedFilesReplaced = filesRead.filter((file) => file.parsed.kind !== 'clean' &&
|
|
474
|
+
(file.filepath === filepath || file.filepath === existingWithPath?.filepath));
|
|
475
|
+
// Write-then-rename so two processes saving the same session id can never
|
|
476
|
+
// interleave bytes in the target file (the temp name does not end in .json,
|
|
477
|
+
// so loaders never see it).
|
|
478
|
+
const content = JSON.stringify(toWrite, null, 2) + '\n';
|
|
479
|
+
const tmpPath = `${filepath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
468
480
|
try {
|
|
469
|
-
|
|
481
|
+
try {
|
|
482
|
+
writeFileSync(tmpPath, content, { mode: 0o600 });
|
|
483
|
+
if (!unparsedFilesReplaced.every(copiedAsideUnchanged))
|
|
484
|
+
return null;
|
|
485
|
+
renameIntoPlace(tmpPath, filepath, content);
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
try {
|
|
489
|
+
unlinkSync(tmpPath);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Renamed into place, or never written.
|
|
493
|
+
}
|
|
494
|
+
}
|
|
470
495
|
// The merge landed on a different filename than where the existing
|
|
471
496
|
// record lived (a date-rollover case) — remove the now-superseded
|
|
472
497
|
// file so the session isn't left duplicated across two files.
|
|
@@ -483,6 +508,7 @@ export class SessionStore {
|
|
|
483
508
|
}
|
|
484
509
|
}
|
|
485
510
|
logger.debug('Session saved', { sessionId: toWrite.sessionId, filename });
|
|
511
|
+
return { previous: existingWithPath?.summary ?? null };
|
|
486
512
|
}
|
|
487
513
|
catch (err) {
|
|
488
514
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -491,42 +517,57 @@ export class SessionStore {
|
|
|
491
517
|
filename,
|
|
492
518
|
error: message,
|
|
493
519
|
});
|
|
520
|
+
return null;
|
|
494
521
|
}
|
|
495
522
|
}
|
|
496
523
|
loadSession(sessionId) {
|
|
497
|
-
return this.loadSessionWithPath(sessionId)?.summary ?? null;
|
|
524
|
+
return this.loadSessionWithPath(sessionId).existing?.summary ?? null;
|
|
498
525
|
}
|
|
499
|
-
/**
|
|
526
|
+
/**
|
|
527
|
+
* Like loadSession(), but also returns the file path it was loaded from — used by saveSession() to detect and clean up a stale file after a merge relocates a session to a different filename.
|
|
528
|
+
*
|
|
529
|
+
* An unreadable file is skipped rather than ending the search: returning null
|
|
530
|
+
* there made saveSession() treat the session as new and start a fresh file
|
|
531
|
+
* on every later save. Sibling files for one id are not merged here,
|
|
532
|
+
* because files split that way hold disjoint activity that mergeSummaries'
|
|
533
|
+
* per-counter max would drop.
|
|
534
|
+
*/
|
|
500
535
|
loadSessionWithPath(sessionId) {
|
|
501
536
|
if (!existsSync(this.sessionsDir))
|
|
502
|
-
return null;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const filepath =
|
|
537
|
+
return { existing: null, filesRead: [] };
|
|
538
|
+
const names = readdirSync(this.sessionsDir)
|
|
539
|
+
.filter((file) => parseSessionFilename(file)?.sessionId === sessionId)
|
|
540
|
+
.sort();
|
|
541
|
+
const filesRead = [];
|
|
542
|
+
for (const file of names) {
|
|
543
|
+
const filepath = resolve(this.sessionsDir, file);
|
|
509
544
|
try {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
return { summary: session, filepath };
|
|
545
|
+
const bytes = readFileSync(filepath);
|
|
546
|
+
filesRead.push({
|
|
547
|
+
filepath,
|
|
548
|
+
bytes,
|
|
549
|
+
parsed: parseSessionFile(bytes.toString('utf-8'), filepath),
|
|
550
|
+
});
|
|
517
551
|
}
|
|
518
552
|
catch {
|
|
519
553
|
logger.warn('Failed to read session file', { file });
|
|
520
554
|
}
|
|
521
555
|
}
|
|
522
|
-
|
|
556
|
+
for (const { filepath, parsed } of filesRead) {
|
|
557
|
+
const summary = usableSummary(parsed, () => names.length - filesRead.filter((f) => f.parsed.kind === 'unreadable').length);
|
|
558
|
+
if (summary)
|
|
559
|
+
return { existing: { summary, filepath }, filesRead };
|
|
560
|
+
}
|
|
561
|
+
return { existing: null, filesRead };
|
|
523
562
|
}
|
|
524
563
|
listSessions(options) {
|
|
525
564
|
if (!existsSync(this.sessionsDir))
|
|
526
565
|
return [];
|
|
527
566
|
const sinceDate = options?.since ? formatDate(options.since) : null;
|
|
528
567
|
const results = [];
|
|
529
|
-
|
|
568
|
+
const names = readdirSync(this.sessionsDir);
|
|
569
|
+
const parsingFilesForId = parsingFileCounter(this.sessionsDir, names.flatMap((name) => parseSessionFilename(name) ?? []));
|
|
570
|
+
for (const file of names) {
|
|
530
571
|
if (!file.endsWith('.json'))
|
|
531
572
|
continue;
|
|
532
573
|
const parsed = parseSessionFilename(file);
|
|
@@ -536,9 +577,9 @@ export class SessionStore {
|
|
|
536
577
|
continue;
|
|
537
578
|
if (options?.developer) {
|
|
538
579
|
try {
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
580
|
+
const filepath = join(this.sessionsDir, file);
|
|
581
|
+
const summary = usableSummary(parseSessionFile(readFileSync(filepath, 'utf-8'), filepath), () => parsingFilesForId(parsed.sessionId));
|
|
582
|
+
if (!summary || summary.developer !== options.developer)
|
|
542
583
|
continue;
|
|
543
584
|
}
|
|
544
585
|
catch {
|
|
@@ -555,12 +596,10 @@ export class SessionStore {
|
|
|
555
596
|
const sinceDate = options?.since ? formatDate(options.since) : null;
|
|
556
597
|
const modifiedSinceMs = options?.includeModifiedSince?.getTime() ?? null;
|
|
557
598
|
const results = [];
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
if (!parsed)
|
|
563
|
-
continue;
|
|
599
|
+
const sessionFiles = readdirSync(this.sessionsDir).flatMap((file) => parseSessionFilename(file) ?? []);
|
|
600
|
+
const parsingFilesForId = parsingFileCounter(this.sessionsDir, sessionFiles);
|
|
601
|
+
for (const parsed of sessionFiles) {
|
|
602
|
+
const file = parsed.filename;
|
|
564
603
|
if (sinceDate && parsed.date < sinceDate) {
|
|
565
604
|
if (modifiedSinceMs === null)
|
|
566
605
|
continue;
|
|
@@ -573,8 +612,8 @@ export class SessionStore {
|
|
|
573
612
|
}
|
|
574
613
|
}
|
|
575
614
|
try {
|
|
576
|
-
const
|
|
577
|
-
const session =
|
|
615
|
+
const filepath = join(this.sessionsDir, file);
|
|
616
|
+
const session = usableSummary(parseSessionFile(readFileSync(filepath, 'utf-8'), filepath), () => parsingFilesForId(parsed.sessionId));
|
|
578
617
|
if (!session)
|
|
579
618
|
continue;
|
|
580
619
|
// Synthetic session IDs (`local-*`, `proxy-*`, `pending-*`) are
|
|
@@ -1315,11 +1354,77 @@ export function deserializeFullSessionSummary(obj) {
|
|
|
1315
1354
|
attribution: parseAttribution(obj.attribution),
|
|
1316
1355
|
};
|
|
1317
1356
|
}
|
|
1357
|
+
const warnedKeys = new Set();
|
|
1358
|
+
function warnOnce(key, message, data) {
|
|
1359
|
+
if (warnedKeys.has(key))
|
|
1360
|
+
return;
|
|
1361
|
+
warnedKeys.add(key);
|
|
1362
|
+
logger.warn(message, data);
|
|
1363
|
+
}
|
|
1364
|
+
function parseSessionFile(raw, filepath) {
|
|
1365
|
+
const summary = parseSummary(raw);
|
|
1366
|
+
if (summary)
|
|
1367
|
+
return { kind: 'clean', summary };
|
|
1368
|
+
const first = firstTopLevelJsonDocument(raw);
|
|
1369
|
+
const recovered = first === null ? null : parseSummary(first);
|
|
1370
|
+
const file = basename(filepath);
|
|
1371
|
+
if (recovered) {
|
|
1372
|
+
warnOnce(`recovered:${filepath}`, 'Recovered first JSON document from corrupt session file', {
|
|
1373
|
+
file,
|
|
1374
|
+
});
|
|
1375
|
+
return { kind: 'recovered', summary: recovered };
|
|
1376
|
+
}
|
|
1377
|
+
warnOnce(`unreadable:${filepath}`, 'Failed to deserialize session file', { file });
|
|
1378
|
+
return { kind: 'unreadable' };
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* A recovered document is used only when its file is the only one of its id
|
|
1382
|
+
* that parses. Beside a parsing sibling it may repeat activity the sibling
|
|
1383
|
+
* already holds, so adding the two could double count. It stays unread there,
|
|
1384
|
+
* as it was before recovery existed, until a session's split files are
|
|
1385
|
+
* combined. A sibling that parses as unreadable holds nothing to double count,
|
|
1386
|
+
* so it does not hide the recovered file. A sibling that cannot be read at all
|
|
1387
|
+
* might hold anything, so it counts as parsing. The count includes the file
|
|
1388
|
+
* itself and is only asked for when the file is recovered.
|
|
1389
|
+
*/
|
|
1390
|
+
function usableSummary(parsed, parsingFilesForId) {
|
|
1391
|
+
if (parsed.kind === 'unreadable')
|
|
1392
|
+
return null;
|
|
1393
|
+
return parsed.kind === 'clean' || parsingFilesForId() === 1 ? parsed.summary : null;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Counts, per session id, the files that parse as clean or recovered. A file
|
|
1397
|
+
* that cannot be read at all might hold anything, so it counts as parsing.
|
|
1398
|
+
* Reads an id's files once, on first ask.
|
|
1399
|
+
*/
|
|
1400
|
+
function parsingFileCounter(sessionsDir, files) {
|
|
1401
|
+
const filenamesById = new Map();
|
|
1402
|
+
for (const { sessionId, filename } of files) {
|
|
1403
|
+
filenamesById.set(sessionId, [...(filenamesById.get(sessionId) ?? []), filename]);
|
|
1404
|
+
}
|
|
1405
|
+
const counts = new Map();
|
|
1406
|
+
return (sessionId) => {
|
|
1407
|
+
const known = counts.get(sessionId);
|
|
1408
|
+
if (known !== undefined)
|
|
1409
|
+
return known;
|
|
1410
|
+
const count = (filenamesById.get(sessionId) ?? []).filter((filename) => {
|
|
1411
|
+
const filepath = join(sessionsDir, filename);
|
|
1412
|
+
try {
|
|
1413
|
+
return parseSessionFile(readFileSync(filepath, 'utf-8'), filepath).kind !== 'unreadable';
|
|
1414
|
+
}
|
|
1415
|
+
catch {
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
}).length;
|
|
1419
|
+
counts.set(sessionId, count);
|
|
1420
|
+
return count;
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1318
1423
|
/**
|
|
1319
1424
|
* Parse a raw session JSON string and extract known fields. Delegates field
|
|
1320
1425
|
* extraction to `deserializeFullSessionSummary` to avoid duplication.
|
|
1321
1426
|
*/
|
|
1322
|
-
function
|
|
1427
|
+
function parseSummary(raw) {
|
|
1323
1428
|
let parsed;
|
|
1324
1429
|
try {
|
|
1325
1430
|
parsed = JSON.parse(raw);
|
|
@@ -1331,6 +1436,106 @@ function deserializeSession(raw) {
|
|
|
1331
1436
|
return null;
|
|
1332
1437
|
return deserializeFullSessionSummary(parsed);
|
|
1333
1438
|
}
|
|
1439
|
+
function copiedAsideUnchanged(file) {
|
|
1440
|
+
const name = basename(file.filepath);
|
|
1441
|
+
try {
|
|
1442
|
+
if (!readFileSync(file.filepath).equals(file.bytes)) {
|
|
1443
|
+
logger.warn('Session file changed since it was read; skipping this save', { file: name });
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
if (hasCorruptCopy(file))
|
|
1447
|
+
return true;
|
|
1448
|
+
const suffix = `${Date.now()}.${process.pid}.${randomBytes(4).toString('hex')}`;
|
|
1449
|
+
writeFileSync(`${file.filepath}.${suffix}.corrupt`, file.bytes, {
|
|
1450
|
+
mode: 0o600,
|
|
1451
|
+
flag: 'wx',
|
|
1452
|
+
});
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
catch (err) {
|
|
1456
|
+
logger.warn('Could not copy an unparsed session file aside; skipping this save', {
|
|
1457
|
+
file: name,
|
|
1458
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1459
|
+
});
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/** True when a `.corrupt` copy of these exact bytes already sits beside the file. */
|
|
1464
|
+
function hasCorruptCopy(file) {
|
|
1465
|
+
const dir = dirname(file.filepath);
|
|
1466
|
+
const prefix = `${basename(file.filepath)}.`;
|
|
1467
|
+
return readdirSync(dir).some((entry) => {
|
|
1468
|
+
if (!entry.startsWith(prefix) || !entry.endsWith('.corrupt'))
|
|
1469
|
+
return false;
|
|
1470
|
+
try {
|
|
1471
|
+
return readFileSync(join(dir, entry)).equals(file.bytes);
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
const RENAME_RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
1479
|
+
const RENAME_BACKOFF_MS = [10, 20, 40, 80];
|
|
1480
|
+
const SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
1481
|
+
/**
|
|
1482
|
+
* Windows refuses to rename over a file another process has open, which
|
|
1483
|
+
* antivirus scanners do briefly after each write. Retry with backoff, then
|
|
1484
|
+
* write the target directly so saves never silently stop.
|
|
1485
|
+
*/
|
|
1486
|
+
function renameIntoPlace(tmpPath, filepath, content) {
|
|
1487
|
+
for (let attempt = 0;; attempt++) {
|
|
1488
|
+
try {
|
|
1489
|
+
renameSync(tmpPath, filepath);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
catch (err) {
|
|
1493
|
+
const code = err.code ?? '';
|
|
1494
|
+
if (!RENAME_RETRY_CODES.has(code))
|
|
1495
|
+
throw err;
|
|
1496
|
+
if (process.platform !== 'win32' || attempt === RENAME_BACKOFF_MS.length)
|
|
1497
|
+
break;
|
|
1498
|
+
Atomics.wait(SLEEP_CELL, 0, 0, RENAME_BACKOFF_MS[attempt]);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
try {
|
|
1502
|
+
writeFileSync(filepath, content, { mode: 0o600 });
|
|
1503
|
+
}
|
|
1504
|
+
catch (err) {
|
|
1505
|
+
warnOnce(`rename-failed:${filepath}`, 'Could not rename session file into place and the direct write failed too', { file: basename(filepath), error: err instanceof Error ? err.message : String(err) });
|
|
1506
|
+
throw err;
|
|
1507
|
+
}
|
|
1508
|
+
warnOnce(`rename:${filepath}`, 'Could not rename session file into place; wrote it directly', {
|
|
1509
|
+
file: basename(filepath),
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
/** Returns the leading `{...}` of `raw` (string- and escape-aware), or null if it never closes. */
|
|
1513
|
+
function firstTopLevelJsonDocument(raw) {
|
|
1514
|
+
const start = raw.search(/\S/);
|
|
1515
|
+
if (start === -1 || raw[start] !== '{')
|
|
1516
|
+
return null;
|
|
1517
|
+
let depth = 0;
|
|
1518
|
+
let inString = false;
|
|
1519
|
+
for (let i = start; i < raw.length; i++) {
|
|
1520
|
+
const ch = raw[i];
|
|
1521
|
+
if (inString) {
|
|
1522
|
+
if (ch === '\\')
|
|
1523
|
+
i++;
|
|
1524
|
+
else if (ch === '"')
|
|
1525
|
+
inString = false;
|
|
1526
|
+
}
|
|
1527
|
+
else if (ch === '"') {
|
|
1528
|
+
inString = true;
|
|
1529
|
+
}
|
|
1530
|
+
else if (ch === '{') {
|
|
1531
|
+
depth++;
|
|
1532
|
+
}
|
|
1533
|
+
else if (ch === '}' && --depth === 0) {
|
|
1534
|
+
return raw.slice(start, i + 1);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1334
1539
|
function parseSessionFilename(filename) {
|
|
1335
1540
|
// Expected format: YYYY-MM-DD_sessionId.json
|
|
1336
1541
|
const match = filename.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.json$/);
|