@newrelic/preflight 1.70.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/README.md +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +11 -16
- 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/collector-script.d.ts.map +1 -1
- package/dist/hooks/collector-script.js +14 -8
- package/dist/hooks/collector-script.js.map +1 -1
- package/dist/hooks/copilot-app-usage-watcher.d.ts.map +1 -1
- package/dist/hooks/copilot-app-usage-watcher.js +3 -4
- package/dist/hooks/copilot-app-usage-watcher.js.map +1 -1
- package/dist/hooks/copilot-usage-watcher.d.ts +25 -5
- package/dist/hooks/copilot-usage-watcher.d.ts.map +1 -1
- package/dist/hooks/copilot-usage-watcher.js +254 -92
- package/dist/hooks/copilot-usage-watcher.js.map +1 -1
- package/dist/hooks/event-processor.d.ts +24 -11
- package/dist/hooks/event-processor.d.ts.map +1 -1
- package/dist/hooks/event-processor.js +139 -45
- 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 +11 -6
- 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 +21 -15
- package/dist/hooks/subagent-watcher.d.ts.map +1 -1
- package/dist/hooks/subagent-watcher.js +89 -73
- package/dist/hooks/subagent-watcher.js.map +1 -1
- package/dist/hooks/throttled-warning.d.ts +5 -5
- package/dist/hooks/throttled-warning.d.ts.map +1 -1
- package/dist/hooks/throttled-warning.js +14 -12
- package/dist/hooks/throttled-warning.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 +57 -62
- package/dist/index.js.map +1 -1
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +131 -17
- 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/credentials.d.ts +23 -0
- package/dist/install/credentials.d.ts.map +1 -0
- package/dist/install/credentials.js +57 -0
- package/dist/install/credentials.js.map +1 -0
- 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/setup-wizard.d.ts.map +1 -1
- package/dist/install/setup-wizard.js +3 -3
- package/dist/install/setup-wizard.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 +6 -5
- package/dist/metrics/local-session-aggregator.d.ts.map +1 -1
- package/dist/metrics/local-session-aggregator.js +34 -6
- 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 +82 -0
- package/dist/storage/drain-claims.d.ts.map +1 -0
- package/dist/storage/drain-claims.js +239 -0
- package/dist/storage/drain-claims.js.map +1 -0
- package/dist/storage/hook-buffer.d.ts +7 -0
- package/dist/storage/hook-buffer.d.ts.map +1 -0
- package/dist/storage/hook-buffer.js +132 -0
- package/dist/storage/hook-buffer.js.map +1 -0
- package/dist/storage/local-store.d.ts +38 -16
- package/dist/storage/local-store.d.ts.map +1 -1
- package/dist/storage/local-store.js +228 -72
- 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 +37 -8
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/storage/session-store.js +255 -48
- 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
|
@@ -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":"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"}
|
|
@@ -212,15 +212,20 @@ export interface AttributableActivity {
|
|
|
212
212
|
readonly subagentCostUsd?: number;
|
|
213
213
|
readonly timeline?: ReadonlyArray<unknown>;
|
|
214
214
|
}
|
|
215
|
+
export interface RecordedUsage extends AttributableActivity {
|
|
216
|
+
readonly estimatedCostUsd?: number | null;
|
|
217
|
+
readonly tokensInput?: number;
|
|
218
|
+
readonly tokensOutput?: number;
|
|
219
|
+
readonly tokensThinking?: number;
|
|
220
|
+
}
|
|
215
221
|
/**
|
|
216
222
|
* Does this summary represent work we can attribute to a real session?
|
|
217
223
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* contribute a pro-rated cost when they carry no per-day buckets.
|
|
224
|
+
* The narrower of the two definitions of "not empty" for session
|
|
225
|
+
* persistence; `hasRecordedUsage` is the wider and gates the writes. This one
|
|
226
|
+
* is for the `/api/sessions/today/aggregate` route, which uses it to decide
|
|
227
|
+
* which persisted rows may contribute a pro-rated cost when they carry no
|
|
228
|
+
* per-day buckets. Callers must never re-implement either definition.
|
|
224
229
|
*
|
|
225
230
|
* Subagent spend counts as activity on purpose. A session whose parent process
|
|
226
231
|
* died while its subagent transcripts were still being tailed has an empty
|
|
@@ -232,6 +237,16 @@ export interface AttributableActivity {
|
|
|
232
237
|
* — is still false, which is what keeps the empty-clobber guard intact.
|
|
233
238
|
*/
|
|
234
239
|
export declare function hasAttributableActivity(s: AttributableActivity): boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Did this session record anything worth a file on disk? Wider than
|
|
242
|
+
* `hasAttributableActivity`: a chat-only session has no tool calls but real
|
|
243
|
+
* cost and tokens, and must still be kept. A session with none of these is an
|
|
244
|
+
* idle MCP process (a window that was opened and never used). It gates the
|
|
245
|
+
* aggregator's summaries, every write `SessionStore.saveSession()` accepts,
|
|
246
|
+
* the existing file an empty write must not clobber, and the History list in
|
|
247
|
+
* `/api/sessions`.
|
|
248
|
+
*/
|
|
249
|
+
export declare function hasRecordedUsage(s: RecordedUsage): boolean;
|
|
235
250
|
export interface SessionFileInfo {
|
|
236
251
|
readonly filename: string;
|
|
237
252
|
readonly sessionId: string;
|
|
@@ -270,9 +285,23 @@ export declare class SessionStore {
|
|
|
270
285
|
storagePath: string;
|
|
271
286
|
});
|
|
272
287
|
getSessionsDir(): string;
|
|
273
|
-
|
|
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;
|
|
274
295
|
loadSession(sessionId: string): FullSessionSummary | null;
|
|
275
|
-
/**
|
|
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
|
+
*/
|
|
276
305
|
private loadSessionWithPath;
|
|
277
306
|
listSessions(options?: ListSessionsOptions): SessionFileInfo[];
|
|
278
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';
|
|
@@ -41,12 +42,11 @@ export function toPersistedAntiPatterns(patterns) {
|
|
|
41
42
|
/**
|
|
42
43
|
* Does this summary represent work we can attribute to a real session?
|
|
43
44
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* contribute a pro-rated cost when they carry no per-day buckets.
|
|
45
|
+
* The narrower of the two definitions of "not empty" for session
|
|
46
|
+
* persistence; `hasRecordedUsage` is the wider and gates the writes. This one
|
|
47
|
+
* is for the `/api/sessions/today/aggregate` route, which uses it to decide
|
|
48
|
+
* which persisted rows may contribute a pro-rated cost when they carry no
|
|
49
|
+
* per-day buckets. Callers must never re-implement either definition.
|
|
50
50
|
*
|
|
51
51
|
* Subagent spend counts as activity on purpose. A session whose parent process
|
|
52
52
|
* died while its subagent transcripts were still being tailed has an empty
|
|
@@ -66,9 +66,12 @@ export function hasAttributableActivity(s) {
|
|
|
66
66
|
* Did this session record anything worth a file on disk? Wider than
|
|
67
67
|
* `hasAttributableActivity`: a chat-only session has no tool calls but real
|
|
68
68
|
* cost and tokens, and must still be kept. A session with none of these is an
|
|
69
|
-
* idle MCP process (a window that was opened and never used).
|
|
69
|
+
* idle MCP process (a window that was opened and never used). It gates the
|
|
70
|
+
* aggregator's summaries, every write `SessionStore.saveSession()` accepts,
|
|
71
|
+
* the existing file an empty write must not clobber, and the History list in
|
|
72
|
+
* `/api/sessions`.
|
|
70
73
|
*/
|
|
71
|
-
function hasRecordedUsage(s) {
|
|
74
|
+
export function hasRecordedUsage(s) {
|
|
72
75
|
return (hasAttributableActivity(s) ||
|
|
73
76
|
(s.estimatedCostUsd ?? 0) > 0 ||
|
|
74
77
|
(s.tokensInput ?? 0) + (s.tokensOutput ?? 0) + (s.tokensThinking ?? 0) > 0);
|
|
@@ -387,10 +390,14 @@ export class SessionStore {
|
|
|
387
390
|
getSessionsDir() {
|
|
388
391
|
return this.sessionsDir;
|
|
389
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Returns null when nothing was written, else the file the write merged
|
|
395
|
+
* against (`previous`, null for a new file).
|
|
396
|
+
*/
|
|
390
397
|
saveSession(summary) {
|
|
391
398
|
if (!/^[A-Za-z0-9_-]{1,128}$/.test(summary.sessionId) || summary.sessionId === 'unknown') {
|
|
392
399
|
logger.warn('Rejecting invalid sessionId for file path', { sessionId: summary.sessionId });
|
|
393
|
-
return;
|
|
400
|
+
return null;
|
|
394
401
|
}
|
|
395
402
|
if (!existsSync(this.sessionsDir)) {
|
|
396
403
|
mkdirSync(this.sessionsDir, { recursive: true, mode: 0o700 });
|
|
@@ -406,7 +413,7 @@ export class SessionStore {
|
|
|
406
413
|
// merged (inflated, since counters take the max) result to a NEW file
|
|
407
414
|
// while leaving the OLD file on disk untouched — the session then
|
|
408
415
|
// appeared twice in history with overlapping, double-counted totals.
|
|
409
|
-
const existingWithPath = this.loadSessionWithPath(summary.sessionId);
|
|
416
|
+
const { existing: existingWithPath, filesRead } = this.loadSessionWithPath(summary.sessionId);
|
|
410
417
|
// Every MCP server process checkpoints every 30s, including windows that
|
|
411
418
|
// never saw a tool call or a token. Writing those first-time files filled
|
|
412
419
|
// history with model-less zero-activity rows that dragged down every
|
|
@@ -415,7 +422,7 @@ export class SessionStore {
|
|
|
415
422
|
logger.debug('Skipping first write of a session with no recorded usage', {
|
|
416
423
|
sessionId: summary.sessionId,
|
|
417
424
|
});
|
|
418
|
-
return;
|
|
425
|
+
return null;
|
|
419
426
|
}
|
|
420
427
|
// A second process saving under the same sessionId (e.g. two MCP
|
|
421
428
|
// servers both resumed/forked against one real session ID) would
|
|
@@ -426,16 +433,16 @@ export class SessionStore {
|
|
|
426
433
|
// never observed any hook activity would replace a fully recorded session
|
|
427
434
|
// with an all-zero summary, erasing tool calls, tokens and cost from the
|
|
428
435
|
// dashboard on the next refresh. Never let an empty summary clobber a
|
|
429
|
-
// non-empty one
|
|
430
|
-
//
|
|
436
|
+
// non-empty one. A summary with cost or tokens but no tool calls, such as
|
|
437
|
+
// a restarted dashboard's parent-only rollup, is not empty: it merges.
|
|
431
438
|
let toWrite = summary;
|
|
432
439
|
if (existingWithPath) {
|
|
433
440
|
const { summary: existing } = existingWithPath;
|
|
434
|
-
if (
|
|
441
|
+
if (hasRecordedUsage(existing) && !hasRecordedUsage(summary)) {
|
|
435
442
|
logger.warn('Refusing to overwrite recorded session with an empty summary', {
|
|
436
443
|
sessionId: summary.sessionId,
|
|
437
444
|
});
|
|
438
|
-
return;
|
|
445
|
+
return null;
|
|
439
446
|
}
|
|
440
447
|
// Two processes legitimately share one session id: the CLI/editor
|
|
441
448
|
// spawns an MCP engine that owns the session natively, while a
|
|
@@ -463,8 +470,28 @@ export class SessionStore {
|
|
|
463
470
|
if (!filepath.startsWith(this.sessionsDir + sep)) {
|
|
464
471
|
throw new Error(`Session path escaped storage directory: ${filepath}`);
|
|
465
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`;
|
|
466
480
|
try {
|
|
467
|
-
|
|
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
|
+
}
|
|
468
495
|
// The merge landed on a different filename than where the existing
|
|
469
496
|
// record lived (a date-rollover case) — remove the now-superseded
|
|
470
497
|
// file so the session isn't left duplicated across two files.
|
|
@@ -481,6 +508,7 @@ export class SessionStore {
|
|
|
481
508
|
}
|
|
482
509
|
}
|
|
483
510
|
logger.debug('Session saved', { sessionId: toWrite.sessionId, filename });
|
|
511
|
+
return { previous: existingWithPath?.summary ?? null };
|
|
484
512
|
}
|
|
485
513
|
catch (err) {
|
|
486
514
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -489,42 +517,57 @@ export class SessionStore {
|
|
|
489
517
|
filename,
|
|
490
518
|
error: message,
|
|
491
519
|
});
|
|
520
|
+
return null;
|
|
492
521
|
}
|
|
493
522
|
}
|
|
494
523
|
loadSession(sessionId) {
|
|
495
|
-
return this.loadSessionWithPath(sessionId)?.summary ?? null;
|
|
524
|
+
return this.loadSessionWithPath(sessionId).existing?.summary ?? null;
|
|
496
525
|
}
|
|
497
|
-
/**
|
|
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
|
+
*/
|
|
498
535
|
loadSessionWithPath(sessionId) {
|
|
499
536
|
if (!existsSync(this.sessionsDir))
|
|
500
|
-
return null;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
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);
|
|
507
544
|
try {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
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
|
+
});
|
|
515
551
|
}
|
|
516
552
|
catch {
|
|
517
553
|
logger.warn('Failed to read session file', { file });
|
|
518
554
|
}
|
|
519
555
|
}
|
|
520
|
-
|
|
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 };
|
|
521
562
|
}
|
|
522
563
|
listSessions(options) {
|
|
523
564
|
if (!existsSync(this.sessionsDir))
|
|
524
565
|
return [];
|
|
525
566
|
const sinceDate = options?.since ? formatDate(options.since) : null;
|
|
526
567
|
const results = [];
|
|
527
|
-
|
|
568
|
+
const names = readdirSync(this.sessionsDir);
|
|
569
|
+
const parsingFilesForId = parsingFileCounter(this.sessionsDir, names.flatMap((name) => parseSessionFilename(name) ?? []));
|
|
570
|
+
for (const file of names) {
|
|
528
571
|
if (!file.endsWith('.json'))
|
|
529
572
|
continue;
|
|
530
573
|
const parsed = parseSessionFilename(file);
|
|
@@ -534,9 +577,9 @@ export class SessionStore {
|
|
|
534
577
|
continue;
|
|
535
578
|
if (options?.developer) {
|
|
536
579
|
try {
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
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)
|
|
540
583
|
continue;
|
|
541
584
|
}
|
|
542
585
|
catch {
|
|
@@ -553,12 +596,10 @@ export class SessionStore {
|
|
|
553
596
|
const sinceDate = options?.since ? formatDate(options.since) : null;
|
|
554
597
|
const modifiedSinceMs = options?.includeModifiedSince?.getTime() ?? null;
|
|
555
598
|
const results = [];
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
if (!parsed)
|
|
561
|
-
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;
|
|
562
603
|
if (sinceDate && parsed.date < sinceDate) {
|
|
563
604
|
if (modifiedSinceMs === null)
|
|
564
605
|
continue;
|
|
@@ -571,8 +612,8 @@ export class SessionStore {
|
|
|
571
612
|
}
|
|
572
613
|
}
|
|
573
614
|
try {
|
|
574
|
-
const
|
|
575
|
-
const session =
|
|
615
|
+
const filepath = join(this.sessionsDir, file);
|
|
616
|
+
const session = usableSummary(parseSessionFile(readFileSync(filepath, 'utf-8'), filepath), () => parsingFilesForId(parsed.sessionId));
|
|
576
617
|
if (!session)
|
|
577
618
|
continue;
|
|
578
619
|
// Synthetic session IDs (`local-*`, `proxy-*`, `pending-*`) are
|
|
@@ -1313,11 +1354,77 @@ export function deserializeFullSessionSummary(obj) {
|
|
|
1313
1354
|
attribution: parseAttribution(obj.attribution),
|
|
1314
1355
|
};
|
|
1315
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
|
+
}
|
|
1316
1423
|
/**
|
|
1317
1424
|
* Parse a raw session JSON string and extract known fields. Delegates field
|
|
1318
1425
|
* extraction to `deserializeFullSessionSummary` to avoid duplication.
|
|
1319
1426
|
*/
|
|
1320
|
-
function
|
|
1427
|
+
function parseSummary(raw) {
|
|
1321
1428
|
let parsed;
|
|
1322
1429
|
try {
|
|
1323
1430
|
parsed = JSON.parse(raw);
|
|
@@ -1329,6 +1436,106 @@ function deserializeSession(raw) {
|
|
|
1329
1436
|
return null;
|
|
1330
1437
|
return deserializeFullSessionSummary(parsed);
|
|
1331
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
|
+
}
|
|
1332
1539
|
function parseSessionFilename(filename) {
|
|
1333
1540
|
// Expected format: YYYY-MM-DD_sessionId.json
|
|
1334
1541
|
const match = filename.match(/^(\d{4}-\d{2}-\d{2})_(.+)\.json$/);
|