@prestyj/cli 5.8.0 → 5.9.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/assets/skills/evidence-led-ui/LICENSES.md +12 -0
- package/assets/skills/evidence-led-ui/SKILL.md +14 -6
- package/assets/skills/evidence-led-ui/references/anti-defaults.md +9 -1
- package/assets/skills/evidence-led-ui/references/craft-rulings.md +33 -0
- package/assets/skills/evidence-led-ui/references/production-contract.md +41 -21
- package/assets/skills/evidence-led-ui/references/provenance.md +7 -5
- package/assets/skills/evidence-led-ui/references/quality-rubric.md +10 -10
- package/dist/app-sidecar-sessions.d.ts +8 -0
- package/dist/app-sidecar-sessions.d.ts.map +1 -0
- package/dist/app-sidecar-sessions.js +31 -0
- package/dist/app-sidecar-sessions.js.map +1 -0
- package/dist/app-sidecar-sessions.test.d.ts +2 -0
- package/dist/app-sidecar-sessions.test.d.ts.map +1 -0
- package/dist/app-sidecar-sessions.test.js +83 -0
- package/dist/app-sidecar-sessions.test.js.map +1 -0
- package/dist/app-sidecar.js +250 -107
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -26
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-compaction.test.js +19 -0
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +84 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +1 -1
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +4 -1
- package/dist/core/agents.js.map +1 -1
- package/dist/core/auth-storage.d.ts +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +1 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +20 -11
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +16 -3
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +5 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +6 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/goal-controller.test.js +3 -3
- package/dist/core/goal-controller.test.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/input-normalization.test.d.ts +2 -0
- package/dist/core/input-normalization.test.d.ts.map +1 -0
- package/dist/core/input-normalization.test.js +75 -0
- package/dist/core/input-normalization.test.js.map +1 -0
- package/dist/core/loop-breaker.d.ts +38 -1
- package/dist/core/loop-breaker.d.ts.map +1 -1
- package/dist/core/loop-breaker.js +84 -2
- package/dist/core/loop-breaker.js.map +1 -1
- package/dist/core/loop-breaker.test.js +108 -1
- package/dist/core/loop-breaker.test.js.map +1 -1
- package/dist/core/message-images.d.ts +12 -0
- package/dist/core/message-images.d.ts.map +1 -0
- package/dist/core/message-images.js +52 -0
- package/dist/core/message-images.js.map +1 -0
- package/dist/core/message-images.test.d.ts +2 -0
- package/dist/core/message-images.test.d.ts.map +1 -0
- package/dist/core/message-images.test.js +62 -0
- package/dist/core/message-images.test.js.map +1 -0
- package/dist/core/project-discovery.d.ts +1 -1
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +105 -53
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +29 -0
- package/dist/core/project-discovery.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +27 -7
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +257 -124
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-storage.d.ts +57 -0
- package/dist/core/session-storage.d.ts.map +1 -0
- package/dist/core/session-storage.js +568 -0
- package/dist/core/session-storage.js.map +1 -0
- package/dist/core/session-storage.test.d.ts +2 -0
- package/dist/core/session-storage.test.d.ts.map +1 -0
- package/dist/core/session-storage.test.js +269 -0
- package/dist/core/session-storage.test.js.map +1 -0
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +7 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/sidecar-error-reporter.d.ts +12 -0
- package/dist/core/sidecar-error-reporter.d.ts.map +1 -0
- package/dist/core/sidecar-error-reporter.js +57 -0
- package/dist/core/sidecar-error-reporter.js.map +1 -0
- package/dist/core/sidecar-error-reporter.test.d.ts +2 -0
- package/dist/core/sidecar-error-reporter.test.d.ts.map +1 -0
- package/dist/core/sidecar-error-reporter.test.js +61 -0
- package/dist/core/sidecar-error-reporter.test.js.map +1 -0
- package/dist/core/skills.d.ts +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +4 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +7 -0
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +26 -2
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +34 -0
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/core/verify-commands.d.ts.map +1 -1
- package/dist/core/verify-commands.js +1 -6
- package/dist/core/verify-commands.js.map +1 -1
- package/dist/core/verify-commands.test.js +1 -5
- package/dist/core/verify-commands.test.js.map +1 -1
- package/dist/core/workspace-guard.d.ts +38 -0
- package/dist/core/workspace-guard.d.ts.map +1 -0
- package/dist/core/workspace-guard.js +112 -0
- package/dist/core/workspace-guard.js.map +1 -0
- package/dist/core/workspace-guard.test.d.ts +2 -0
- package/dist/core/workspace-guard.test.d.ts.map +1 -0
- package/dist/core/workspace-guard.test.js +86 -0
- package/dist/core/workspace-guard.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +9 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +11 -0
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/session.d.ts +4 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +38 -101
- package/dist/session.js.map +1 -1
- package/dist/session.test.d.ts +2 -0
- package/dist/session.test.d.ts.map +1 -0
- package/dist/session.test.js +35 -0
- package/dist/session.test.js.map +1 -0
- package/dist/system-prompt.d.ts +12 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +86 -19
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +132 -8
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/tools/bash.d.ts +3 -2
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +37 -6
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.js +34 -1
- package/dist/tools/bash.test.js.map +1 -1
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +7 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/edit.test.js +7 -0
- package/dist/tools/edit.test.js.map +1 -1
- package/dist/tools/exit-plan.d.ts.map +1 -1
- package/dist/tools/exit-plan.js +15 -4
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/exit-plan.test.d.ts +2 -0
- package/dist/tools/exit-plan.test.d.ts.map +1 -0
- package/dist/tools/exit-plan.test.js +72 -0
- package/dist/tools/exit-plan.test.js.map +1 -0
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +8 -3
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.js +6 -0
- package/dist/tools/grep.test.js.map +1 -1
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/screenshot.js +1 -1
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/skill.d.ts.map +1 -1
- package/dist/tools/skill.js +4 -2
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/source-path.d.ts.map +1 -1
- package/dist/tools/source-path.js +9 -0
- package/dist/tools/source-path.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +1 -1
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +4 -1
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent-shared.test.js +21 -1
- package/dist/tools/subagent-shared.test.js.map +1 -1
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +13 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +7 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/tools/write.test.js +12 -0
- package/dist/tools/write.test.js.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +17 -5
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-items.d.ts +10 -0
- package/dist/ui/app-items.d.ts.map +1 -1
- package/dist/ui/app-items.js +10 -0
- package/dist/ui/app-items.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/Footer.test.js +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +5 -2
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +49 -8
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/useTranscriptHistory.d.ts +8 -5
- package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
- package/dist/ui/hooks/useTranscriptHistory.js +2 -30
- package/dist/ui/hooks/useTranscriptHistory.js.map +1 -1
- package/dist/utils/http-body.d.ts +19 -0
- package/dist/utils/http-body.d.ts.map +1 -0
- package/dist/utils/http-body.js +58 -0
- package/dist/utils/http-body.js.map +1 -0
- package/dist/utils/http-body.test.d.ts +2 -0
- package/dist/utils/http-body.test.d.ts.map +1 -0
- package/dist/utils/http-body.test.js +68 -0
- package/dist/utils/http-body.test.js.map +1 -0
- package/dist/utils/text.d.ts +9 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +11 -0
- package/dist/utils/text.js.map +1 -0
- package/package.json +5 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Message, type Provider, type Usage } from "@prestyj/ai";
|
|
2
2
|
import type { AgentTurnTiming } from "@prestyj/agent";
|
|
3
3
|
import type { CompletedItem } from "../ui/app-items.js";
|
|
4
|
+
import { type StorageNormalizationMetrics } from "./session-storage.js";
|
|
4
5
|
interface BaseEntry {
|
|
5
6
|
id: string;
|
|
6
7
|
parentId: string | null;
|
|
@@ -122,6 +123,15 @@ export interface SessionInfo {
|
|
|
122
123
|
cwd: string;
|
|
123
124
|
messageCount: number;
|
|
124
125
|
}
|
|
126
|
+
export interface SessionMaintenanceMetrics extends StorageNormalizationMetrics {
|
|
127
|
+
deletedFiles: number;
|
|
128
|
+
deletedBytes: number;
|
|
129
|
+
archivedFiles: number;
|
|
130
|
+
archivedSourceBytes: number;
|
|
131
|
+
archivedBytes: number;
|
|
132
|
+
bytesSaved: number;
|
|
133
|
+
failures: number;
|
|
134
|
+
}
|
|
125
135
|
export interface BranchInfo {
|
|
126
136
|
/** The entry ID where this branch diverges from its parent branch */
|
|
127
137
|
branchPointId: string;
|
|
@@ -133,6 +143,8 @@ export interface BranchInfo {
|
|
|
133
143
|
timestamp: string;
|
|
134
144
|
}
|
|
135
145
|
export declare class SessionManager {
|
|
146
|
+
private static activePathsByRoot;
|
|
147
|
+
private static maintenanceByRoot;
|
|
136
148
|
private sessionsDir;
|
|
137
149
|
private warnedPersistCodes;
|
|
138
150
|
/** Called once per error code when session persistence fails (e.g. ENOSPC). */
|
|
@@ -153,20 +165,22 @@ export declare class SessionManager {
|
|
|
153
165
|
path: string;
|
|
154
166
|
header: SessionHeader;
|
|
155
167
|
}>;
|
|
168
|
+
registerActivePath(sessionPath: string): void;
|
|
169
|
+
unregisterActivePath(sessionPath: string): void;
|
|
170
|
+
private protectedSessionBases;
|
|
156
171
|
load(sessionPath: string): Promise<{
|
|
157
172
|
header: SessionHeader;
|
|
158
173
|
entries: SessionEntry[];
|
|
174
|
+
path: string;
|
|
159
175
|
}>;
|
|
176
|
+
private readSessionInfo;
|
|
177
|
+
private sessionCandidates;
|
|
160
178
|
list(cwd: string): Promise<SessionInfo[]>;
|
|
161
179
|
getMostRecent(cwd: string): Promise<string | null>;
|
|
162
180
|
findById(cwd: string, sessionId: string): Promise<string | null>;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
* is never considered old. Best-effort: per-file errors are skipped so a
|
|
167
|
-
* locked or vanished file can't break startup. Empty project dirs left
|
|
168
|
-
* behind are removed. Returns what was freed for logging.
|
|
169
|
-
*/
|
|
181
|
+
private storageDirectories;
|
|
182
|
+
private logicalSessionBases;
|
|
183
|
+
private removeStoragePath;
|
|
170
184
|
pruneOldSessions(options: {
|
|
171
185
|
maxAgeDays: number;
|
|
172
186
|
keepPaths?: string[];
|
|
@@ -174,6 +188,12 @@ export declare class SessionManager {
|
|
|
174
188
|
deletedFiles: number;
|
|
175
189
|
freedBytes: number;
|
|
176
190
|
}>;
|
|
191
|
+
runMaintenance(options: {
|
|
192
|
+
retentionDays: number;
|
|
193
|
+
keepPaths?: string[];
|
|
194
|
+
now?: number;
|
|
195
|
+
}): Promise<SessionMaintenanceMetrics>;
|
|
196
|
+
private runMaintenanceUnsafe;
|
|
177
197
|
appendEntry(sessionPath: string, entry: SessionEntry): Promise<void>;
|
|
178
198
|
appendTurnMetric(sessionPath: string, payload: TurnMetricPayload): Promise<void>;
|
|
179
199
|
updateLeaf(sessionPath: string, leafId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
2
|
import { createInterface } from "node:readline";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import crypto from "node:crypto";
|
|
6
5
|
import { environmentSecrets, redactValue, } from "@prestyj/ai";
|
|
7
6
|
import { log } from "./logger.js";
|
|
8
7
|
import { encodeCwd } from "./encode-cwd.js";
|
|
8
|
+
import { archiveColdSession, archiveSessionPath, cleanupOldSessionTemps, COLD_SESSION_AGE_DAYS, emptyStorageNormalizationMetrics, hydrateSessionEntry, isSessionPath, normalizeSessionEntryForStorage, openSessionReadStream, plainSessionPath, resolveSessionPath, sessionAssetDir, thawSessionArchive, } from "./session-storage.js";
|
|
9
9
|
export const DISPLAY_ITEM_CUSTOM_KIND = "display_item";
|
|
10
10
|
export const TURN_METRIC_CUSTOM_KIND = "turn_metric";
|
|
11
11
|
function finiteNumber(value) {
|
|
@@ -79,12 +79,14 @@ function isCompletedItemLike(value) {
|
|
|
79
79
|
}
|
|
80
80
|
// ── Session Manager ────────────────────────────────────────
|
|
81
81
|
export class SessionManager {
|
|
82
|
+
static activePathsByRoot = new Map();
|
|
83
|
+
static maintenanceByRoot = new Map();
|
|
82
84
|
sessionsDir;
|
|
83
85
|
warnedPersistCodes = new Set();
|
|
84
86
|
/** Called once per error code when session persistence fails (e.g. ENOSPC). */
|
|
85
87
|
onPersistError;
|
|
86
88
|
constructor(sessionsDir) {
|
|
87
|
-
this.sessionsDir = sessionsDir;
|
|
89
|
+
this.sessionsDir = path.resolve(sessionsDir);
|
|
88
90
|
}
|
|
89
91
|
/**
|
|
90
92
|
* Session persistence must never crash a live session. Disk-full (ENOSPC),
|
|
@@ -132,16 +134,40 @@ export class SessionManager {
|
|
|
132
134
|
await fs.appendFile(filePath, JSON.stringify(header) + "\n", "utf-8");
|
|
133
135
|
return { id, path: filePath, header };
|
|
134
136
|
}
|
|
137
|
+
registerActivePath(sessionPath) {
|
|
138
|
+
const root = this.sessionsDir;
|
|
139
|
+
const active = SessionManager.activePathsByRoot.get(root) ?? new Map();
|
|
140
|
+
const basePath = path.resolve(plainSessionPath(sessionPath));
|
|
141
|
+
active.set(basePath, (active.get(basePath) ?? 0) + 1);
|
|
142
|
+
SessionManager.activePathsByRoot.set(root, active);
|
|
143
|
+
}
|
|
144
|
+
unregisterActivePath(sessionPath) {
|
|
145
|
+
const active = SessionManager.activePathsByRoot.get(this.sessionsDir);
|
|
146
|
+
if (!active)
|
|
147
|
+
return;
|
|
148
|
+
const basePath = path.resolve(plainSessionPath(sessionPath));
|
|
149
|
+
const registrations = active.get(basePath) ?? 0;
|
|
150
|
+
if (registrations <= 1)
|
|
151
|
+
active.delete(basePath);
|
|
152
|
+
else
|
|
153
|
+
active.set(basePath, registrations - 1);
|
|
154
|
+
if (active.size === 0)
|
|
155
|
+
SessionManager.activePathsByRoot.delete(this.sessionsDir);
|
|
156
|
+
}
|
|
157
|
+
protectedSessionBases(keepPaths = []) {
|
|
158
|
+
return new Set([
|
|
159
|
+
...(SessionManager.activePathsByRoot.get(this.sessionsDir)?.keys() ?? []),
|
|
160
|
+
...keepPaths.map((sessionPath) => path.resolve(plainSessionPath(sessionPath))),
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
135
163
|
async load(sessionPath) {
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
164
|
+
// Resuming is a write operation, so transparently thaw a gzip archive and
|
|
165
|
+
// return the effective plain path every future append must use.
|
|
166
|
+
const effectivePath = await thawSessionArchive(sessionPath);
|
|
167
|
+
const { stream } = await openSessionReadStream(effectivePath);
|
|
139
168
|
let header = null;
|
|
140
169
|
const entries = [];
|
|
141
|
-
const rl = createInterface({
|
|
142
|
-
input: createReadStream(sessionPath, { encoding: "utf-8" }),
|
|
143
|
-
crlfDelay: Infinity,
|
|
144
|
-
});
|
|
170
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
145
171
|
for await (const line of rl) {
|
|
146
172
|
if (!line)
|
|
147
173
|
continue;
|
|
@@ -152,12 +178,12 @@ export class SessionManager {
|
|
|
152
178
|
header = parsed;
|
|
153
179
|
}
|
|
154
180
|
else {
|
|
155
|
-
// Upgrade v1 to v2
|
|
156
181
|
const v1 = parsed;
|
|
157
182
|
header = {
|
|
158
183
|
type: "session",
|
|
159
184
|
version: 2,
|
|
160
185
|
id: v1.id,
|
|
186
|
+
conversationId: v1.id,
|
|
161
187
|
timestamp: v1.timestamp,
|
|
162
188
|
cwd: v1.cwd,
|
|
163
189
|
provider: v1.provider,
|
|
@@ -165,164 +191,270 @@ export class SessionManager {
|
|
|
165
191
|
leafId: null,
|
|
166
192
|
};
|
|
167
193
|
}
|
|
194
|
+
continue;
|
|
168
195
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
entry.id = crypto.randomUUID();
|
|
174
|
-
entry.parentId = null;
|
|
175
|
-
}
|
|
176
|
-
entries.push(entry);
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
entries.push(parsed);
|
|
196
|
+
const entry = parsed;
|
|
197
|
+
if (entry.type === "message" && !entry.id) {
|
|
198
|
+
entry.id = crypto.randomUUID();
|
|
199
|
+
entry.parentId = null;
|
|
180
200
|
}
|
|
201
|
+
entries.push(await hydrateSessionEntry(entry, effectivePath));
|
|
181
202
|
}
|
|
182
203
|
catch {
|
|
183
|
-
// Skip malformed JSON lines —
|
|
204
|
+
// Skip malformed JSON lines — cold migration preserves their raw bytes
|
|
205
|
+
// so a future recovery tool still has a chance to repair them.
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
208
|
if (!header) {
|
|
187
209
|
throw new Error(`Invalid session file: no header found in ${sessionPath}`);
|
|
188
210
|
}
|
|
189
|
-
return { header, entries };
|
|
211
|
+
return { header, entries, path: effectivePath };
|
|
190
212
|
}
|
|
191
|
-
async
|
|
192
|
-
|
|
213
|
+
async readSessionInfo(candidatePath) {
|
|
214
|
+
try {
|
|
215
|
+
const resolvedPath = await resolveSessionPath(candidatePath);
|
|
216
|
+
const { stream } = await openSessionReadStream(resolvedPath);
|
|
217
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
218
|
+
let first = null;
|
|
219
|
+
let messageCount = 0;
|
|
220
|
+
let lastActivity = null;
|
|
221
|
+
for await (const line of rl) {
|
|
222
|
+
if (!line)
|
|
223
|
+
continue;
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(line);
|
|
226
|
+
if (!first) {
|
|
227
|
+
if (parsed.type !== "session")
|
|
228
|
+
break;
|
|
229
|
+
first = parsed;
|
|
230
|
+
}
|
|
231
|
+
else if (parsed.type === "message") {
|
|
232
|
+
messageCount += 1;
|
|
233
|
+
if (parsed.timestamp)
|
|
234
|
+
lastActivity = parsed.timestamp;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Skip malformed lines while retaining readable entries around them.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!first || first.type !== "session")
|
|
242
|
+
return null;
|
|
243
|
+
return {
|
|
244
|
+
id: first.id,
|
|
245
|
+
conversationId: first.version === 2
|
|
246
|
+
? (first.conversationId ?? first.id)
|
|
247
|
+
: first.id,
|
|
248
|
+
path: resolvedPath,
|
|
249
|
+
timestamp: first.timestamp,
|
|
250
|
+
lastActivity: lastActivity ?? first.timestamp,
|
|
251
|
+
cwd: first.cwd,
|
|
252
|
+
messageCount,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async sessionCandidates(directory) {
|
|
193
260
|
let files;
|
|
194
261
|
try {
|
|
195
|
-
files = await fs.readdir(
|
|
262
|
+
files = await fs.readdir(directory);
|
|
196
263
|
}
|
|
197
264
|
catch {
|
|
198
265
|
return [];
|
|
199
266
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
267
|
+
return files.filter(isSessionPath).map((file) => path.join(directory, file));
|
|
268
|
+
}
|
|
269
|
+
async list(cwd) {
|
|
270
|
+
const candidates = await this.sessionCandidates(this.dirForCwd(cwd));
|
|
271
|
+
const summaries = await Promise.all(candidates.map((file) => this.readSessionInfo(file)));
|
|
272
|
+
const byConversation = new Map();
|
|
273
|
+
const seenResolvedPaths = new Set();
|
|
274
|
+
for (const summary of summaries) {
|
|
275
|
+
if (!summary || seenResolvedPaths.has(summary.path))
|
|
203
276
|
continue;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
crlfDelay: Infinity,
|
|
210
|
-
});
|
|
211
|
-
let first = null;
|
|
212
|
-
let messageCount = 0;
|
|
213
|
-
let lastActivity = null;
|
|
214
|
-
for await (const line of rl) {
|
|
215
|
-
if (!line)
|
|
216
|
-
continue;
|
|
217
|
-
try {
|
|
218
|
-
const parsed = JSON.parse(line);
|
|
219
|
-
if (!first) {
|
|
220
|
-
if (parsed.type !== "session")
|
|
221
|
-
break;
|
|
222
|
-
first = parsed;
|
|
223
|
-
}
|
|
224
|
-
else if (parsed.type === "message") {
|
|
225
|
-
messageCount++;
|
|
226
|
-
if (parsed.timestamp)
|
|
227
|
-
lastActivity = parsed.timestamp;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
// Skip malformed lines
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (!first || first.type !== "session")
|
|
235
|
-
continue;
|
|
236
|
-
sessions.push({
|
|
237
|
-
id: first.id,
|
|
238
|
-
path: filePath,
|
|
239
|
-
timestamp: first.timestamp,
|
|
240
|
-
lastActivity: lastActivity ?? first.timestamp,
|
|
241
|
-
cwd: first.cwd,
|
|
242
|
-
messageCount,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
catch {
|
|
246
|
-
// Skip corrupt files
|
|
277
|
+
seenResolvedPaths.add(summary.path);
|
|
278
|
+
const current = byConversation.get(summary.conversationId);
|
|
279
|
+
if (!current || summary.lastActivity > current.lastActivity) {
|
|
280
|
+
const { conversationId: _conversationId, ...info } = summary;
|
|
281
|
+
byConversation.set(summary.conversationId, info);
|
|
247
282
|
}
|
|
248
283
|
}
|
|
249
|
-
|
|
250
|
-
sessions.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
|
251
|
-
return sessions;
|
|
284
|
+
return [...byConversation.values()].sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
|
252
285
|
}
|
|
253
286
|
async getMostRecent(cwd) {
|
|
254
287
|
const sessions = await this.list(cwd);
|
|
255
|
-
|
|
256
|
-
return withMessages?.path ?? null;
|
|
288
|
+
return sessions.find((session) => session.messageCount > 0)?.path ?? null;
|
|
257
289
|
}
|
|
258
290
|
async findById(cwd, sessionId) {
|
|
259
|
-
const
|
|
260
|
-
|
|
291
|
+
const candidates = await this.sessionCandidates(this.dirForCwd(cwd));
|
|
292
|
+
for (const candidate of candidates) {
|
|
293
|
+
const summary = await this.readSessionInfo(candidate);
|
|
294
|
+
if (summary?.id === sessionId || summary?.conversationId === sessionId)
|
|
295
|
+
return summary.path;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
261
298
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
* Age is judged by file mtime, so a session that's still being appended to
|
|
265
|
-
* is never considered old. Best-effort: per-file errors are skipped so a
|
|
266
|
-
* locked or vanished file can't break startup. Empty project dirs left
|
|
267
|
-
* behind are removed. Returns what was freed for logging.
|
|
268
|
-
*/
|
|
269
|
-
async pruneOldSessions(options) {
|
|
270
|
-
const result = { deletedFiles: 0, freedBytes: 0 };
|
|
271
|
-
if (options.maxAgeDays <= 0)
|
|
272
|
-
return result;
|
|
273
|
-
const cutoffMs = Date.now() - options.maxAgeDays * 86_400_000;
|
|
274
|
-
const keep = new Set((options.keepPaths ?? []).map((p) => path.resolve(p)));
|
|
275
|
-
let cwdDirs;
|
|
299
|
+
async storageDirectories() {
|
|
300
|
+
let entries;
|
|
276
301
|
try {
|
|
277
|
-
|
|
302
|
+
entries = await fs.readdir(this.sessionsDir, { withFileTypes: true });
|
|
278
303
|
}
|
|
279
304
|
catch {
|
|
280
|
-
return
|
|
305
|
+
return [];
|
|
281
306
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
307
|
+
return entries
|
|
308
|
+
.filter((entry) => entry.isDirectory())
|
|
309
|
+
.map((entry) => path.join(this.sessionsDir, entry.name));
|
|
310
|
+
}
|
|
311
|
+
async logicalSessionBases(directory) {
|
|
312
|
+
let entries;
|
|
313
|
+
try {
|
|
314
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const bases = new Set();
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
const entryPath = path.join(directory, entry.name);
|
|
322
|
+
if (entry.isFile() && isSessionPath(entry.name)) {
|
|
323
|
+
bases.add(path.resolve(plainSessionPath(entryPath)));
|
|
290
324
|
}
|
|
291
|
-
|
|
292
|
-
|
|
325
|
+
else if (entry.isDirectory() && entry.name.endsWith(".jsonl.assets")) {
|
|
326
|
+
bases.add(path.resolve(entryPath.slice(0, -".assets".length)));
|
|
293
327
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
}
|
|
329
|
+
return [...bases];
|
|
330
|
+
}
|
|
331
|
+
async removeStoragePath(targetPath) {
|
|
332
|
+
let stat;
|
|
333
|
+
try {
|
|
334
|
+
stat = await fs.lstat(targetPath);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return { deletedFiles: 0, deletedBytes: 0 };
|
|
338
|
+
}
|
|
339
|
+
if (stat.isDirectory()) {
|
|
340
|
+
const result = { deletedFiles: 0, deletedBytes: 0 };
|
|
341
|
+
for (const name of await fs.readdir(targetPath)) {
|
|
342
|
+
const removed = await this.removeStoragePath(path.join(targetPath, name));
|
|
343
|
+
result.deletedFiles += removed.deletedFiles;
|
|
344
|
+
result.deletedBytes += removed.deletedBytes;
|
|
345
|
+
}
|
|
346
|
+
await fs.rmdir(targetPath).catch(() => { });
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
await fs.unlink(targetPath);
|
|
350
|
+
return { deletedFiles: 1, deletedBytes: stat.size };
|
|
351
|
+
}
|
|
352
|
+
async pruneOldSessions(options) {
|
|
353
|
+
const result = { deletedFiles: 0, freedBytes: 0 };
|
|
354
|
+
if (options.maxAgeDays <= 0)
|
|
355
|
+
return result;
|
|
356
|
+
const cutoffMs = Date.now() - options.maxAgeDays * 86_400_000;
|
|
357
|
+
for (const directory of await this.storageDirectories()) {
|
|
358
|
+
for (const basePath of await this.logicalSessionBases(directory)) {
|
|
359
|
+
if (this.protectedSessionBases(options.keepPaths).has(basePath))
|
|
300
360
|
continue;
|
|
301
361
|
try {
|
|
302
|
-
const
|
|
362
|
+
const resolved = await resolveSessionPath(basePath);
|
|
363
|
+
const stat = await fs.stat(resolved);
|
|
303
364
|
if (stat.mtimeMs >= cutoffMs)
|
|
304
365
|
continue;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
366
|
+
for (const target of [
|
|
367
|
+
basePath,
|
|
368
|
+
archiveSessionPath(basePath),
|
|
369
|
+
sessionAssetDir(basePath),
|
|
370
|
+
]) {
|
|
371
|
+
const removed = await this.removeStoragePath(target);
|
|
372
|
+
result.deletedFiles += removed.deletedFiles;
|
|
373
|
+
result.freedBytes += removed.deletedBytes;
|
|
374
|
+
}
|
|
309
375
|
}
|
|
310
376
|
catch {
|
|
311
|
-
//
|
|
377
|
+
// A raced, corrupt, or inaccessible logical group is skipped safely.
|
|
312
378
|
}
|
|
313
379
|
}
|
|
314
|
-
|
|
315
|
-
await fs.rmdir(dir).catch(() => { });
|
|
316
|
-
}
|
|
380
|
+
await fs.rmdir(directory).catch(() => { });
|
|
317
381
|
}
|
|
318
382
|
return result;
|
|
319
383
|
}
|
|
384
|
+
async runMaintenance(options) {
|
|
385
|
+
const existing = SessionManager.maintenanceByRoot.get(this.sessionsDir);
|
|
386
|
+
if (existing)
|
|
387
|
+
return existing;
|
|
388
|
+
const maintenance = this.runMaintenanceUnsafe(options).finally(() => {
|
|
389
|
+
SessionManager.maintenanceByRoot.delete(this.sessionsDir);
|
|
390
|
+
});
|
|
391
|
+
SessionManager.maintenanceByRoot.set(this.sessionsDir, maintenance);
|
|
392
|
+
return maintenance;
|
|
393
|
+
}
|
|
394
|
+
async runMaintenanceUnsafe(options) {
|
|
395
|
+
const metrics = {
|
|
396
|
+
deletedFiles: 0,
|
|
397
|
+
deletedBytes: 0,
|
|
398
|
+
archivedFiles: 0,
|
|
399
|
+
archivedSourceBytes: 0,
|
|
400
|
+
archivedBytes: 0,
|
|
401
|
+
bytesSaved: 0,
|
|
402
|
+
failures: 0,
|
|
403
|
+
...emptyStorageNormalizationMetrics(),
|
|
404
|
+
};
|
|
405
|
+
const pruned = await this.pruneOldSessions({
|
|
406
|
+
maxAgeDays: options.retentionDays,
|
|
407
|
+
keepPaths: options.keepPaths,
|
|
408
|
+
});
|
|
409
|
+
metrics.deletedFiles = pruned.deletedFiles;
|
|
410
|
+
metrics.deletedBytes = pruned.freedBytes;
|
|
411
|
+
const now = options.now ?? Date.now();
|
|
412
|
+
const coldCutoff = now - COLD_SESSION_AGE_DAYS * 86_400_000;
|
|
413
|
+
for (const directory of await this.storageDirectories()) {
|
|
414
|
+
const tempCleanup = await cleanupOldSessionTemps(directory, now - 86_400_000);
|
|
415
|
+
metrics.deletedFiles += tempCleanup.deletedFiles;
|
|
416
|
+
metrics.deletedBytes += tempCleanup.freedBytes;
|
|
417
|
+
for (const basePath of await this.logicalSessionBases(directory)) {
|
|
418
|
+
if (this.protectedSessionBases(options.keepPaths).has(basePath))
|
|
419
|
+
continue;
|
|
420
|
+
try {
|
|
421
|
+
const resolved = await resolveSessionPath(basePath);
|
|
422
|
+
const stat = await fs.stat(resolved);
|
|
423
|
+
if (stat.mtimeMs >= coldCutoff || resolved.endsWith(".jsonl.gz"))
|
|
424
|
+
continue;
|
|
425
|
+
const archived = await archiveColdSession(resolved);
|
|
426
|
+
if (!archived.archived)
|
|
427
|
+
continue;
|
|
428
|
+
metrics.archivedFiles += 1;
|
|
429
|
+
metrics.archivedSourceBytes += archived.sourceBytes;
|
|
430
|
+
metrics.archivedBytes += archived.archiveBytes;
|
|
431
|
+
metrics.bytesSaved += archived.bytesSaved;
|
|
432
|
+
metrics.truncatedToolTexts += archived.truncatedToolTexts;
|
|
433
|
+
metrics.externalizedMedia += archived.externalizedMedia;
|
|
434
|
+
metrics.omittedPathMedia += archived.omittedPathMedia;
|
|
435
|
+
metrics.removedDisplayItems += archived.removedDisplayItems;
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
metrics.failures += 1;
|
|
439
|
+
log("WARN", "session", "Cold session maintenance failed", {
|
|
440
|
+
path: basePath,
|
|
441
|
+
message: error instanceof Error ? error.message : String(error),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return metrics;
|
|
447
|
+
}
|
|
320
448
|
async appendEntry(sessionPath, entry) {
|
|
321
449
|
try {
|
|
322
|
-
// Persist a sanitized clone. The live conversation remains
|
|
323
|
-
//
|
|
450
|
+
// Persist a sanitized, bounded clone. The live conversation remains
|
|
451
|
+
// untouched so the current turn keeps full tool output and media.
|
|
324
452
|
const safeEntry = redactValue(entry, { secrets: environmentSecrets(process.env) });
|
|
325
|
-
await
|
|
453
|
+
const writablePath = await thawSessionArchive(sessionPath);
|
|
454
|
+
const normalized = await normalizeSessionEntryForStorage(safeEntry, writablePath);
|
|
455
|
+
if (normalized === null)
|
|
456
|
+
return;
|
|
457
|
+
await fs.appendFile(writablePath, `${JSON.stringify(normalized)}\n`, "utf-8");
|
|
326
458
|
}
|
|
327
459
|
catch (error) {
|
|
328
460
|
this.handlePersistError(error, "appendEntry");
|
|
@@ -341,7 +473,8 @@ export class SessionManager {
|
|
|
341
473
|
}
|
|
342
474
|
async updateLeaf(sessionPath, leafId) {
|
|
343
475
|
try {
|
|
344
|
-
await
|
|
476
|
+
const writablePath = await thawSessionArchive(sessionPath);
|
|
477
|
+
await this.updateLeafUnsafe(writablePath, leafId);
|
|
345
478
|
}
|
|
346
479
|
catch (error) {
|
|
347
480
|
this.handlePersistError(error, "updateLeaf");
|