@evo-dev/core 0.0.1-alpha.2 → 0.0.1-alpha.20
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/coding/knowledge-distillation/SKILL.md +5 -3
- package/assets/team/agents/code-reviewer.md +48 -0
- package/assets/team/agents/docs-maintainer.md +51 -0
- package/assets/team/agents/implementation-engineer.md +51 -0
- package/assets/team/agents/product-scope-analyst.md +58 -0
- package/assets/team/agents/release-engineer.md +55 -0
- package/assets/team/agents/security-boundary-reviewer.md +50 -0
- package/assets/team/agents/solution-architect.md +51 -0
- package/assets/team/agents/verification-engineer.md +51 -0
- package/assets/team/team.md +102 -0
- package/dist/assets/index.js +5 -5
- package/dist/config/index.js +793 -241
- package/dist/index.js +20840 -12908
- package/dist/plugins/index.js +13 -13
- package/package.json +1 -1
- package/src/agents/index.ts +1 -265
- package/src/code-agent-traces/index.ts +11 -12
- package/src/config/index.ts +2 -0
- package/src/config/settings.ts +116 -7
- package/src/config/store.ts +1 -1
- package/src/daemon/index.ts +1 -41
- package/src/evolution/candidates/index.ts +730 -0
- package/src/evolution/control/index.ts +20 -0
- package/src/evolution/evidence/analysis.ts +533 -0
- package/src/evolution/evidence/index.ts +3 -0
- package/src/evolution/evidence/session-memory/analysis.ts +287 -0
- package/src/evolution/evidence/session-memory/constants.ts +9 -0
- package/src/evolution/evidence/session-memory/index.ts +9 -0
- package/src/evolution/evidence/session-memory/paths.ts +29 -0
- package/src/evolution/evidence/session-memory/policy.ts +39 -0
- package/src/evolution/evidence/session-memory/retention.ts +643 -0
- package/src/evolution/evidence/session-memory/segment.ts +216 -0
- package/src/evolution/evidence/session-memory/semantic-packet.ts +408 -0
- package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
- package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
- package/src/evolution/evidence/session-memory/storage.ts +744 -0
- package/src/evolution/evidence/session-memory/types.ts +296 -0
- package/src/evolution/evidence/session-memory/updater.ts +199 -0
- package/src/evolution/formatters.ts +169 -0
- package/src/evolution/imports/apply.ts +435 -0
- package/src/evolution/imports/diff.ts +472 -0
- package/src/evolution/imports/index.ts +7 -0
- package/src/evolution/imports/materialize.ts +640 -0
- package/src/evolution/imports/paths.ts +129 -0
- package/src/evolution/imports/stage.ts +414 -0
- package/src/evolution/imports/storage.ts +952 -0
- package/src/evolution/imports/types.ts +226 -0
- package/src/evolution/index.ts +19 -2827
- package/src/evolution/knowledge/change-store.ts +558 -0
- package/src/evolution/knowledge/changes.ts +459 -0
- package/src/evolution/knowledge/freshness.ts +69 -0
- package/src/{knowledge → evolution/knowledge}/index.ts +1532 -206
- package/src/evolution/knowledge/review.ts +446 -0
- package/src/evolution/knowledge/support.ts +135 -0
- package/src/evolution/paths.ts +44 -0
- package/src/evolution/processor/distillation.ts +518 -0
- package/src/evolution/processor/index.ts +3 -0
- package/src/evolution/processor/process.ts +594 -0
- package/src/{learning → evolution/review}/index.ts +10 -14
- package/src/evolution/schema.ts +639 -0
- package/src/evolution/shared.ts +1053 -0
- package/src/evolution/triggers/classification.ts +102 -0
- package/src/evolution/triggers/index.ts +295 -0
- package/src/hooks/index.ts +281 -197
- package/src/index.ts +15 -4
- package/src/projects/index.ts +934 -0
- package/src/runtime-logs/index.ts +100 -13
- package/src/team/index.ts +582 -3
- package/src/utils/errors.ts +13 -0
- package/src/utils/fs.ts +40 -0
- package/src/utils/hash.ts +9 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/parsing.ts +11 -0
- package/src/utils/text.ts +18 -0
- package/src/utils/time.ts +5 -0
- package/src/workflow/index.ts +3 -21
- package/src/project/index.ts +0 -507
- package/src/task/index.ts +0 -840
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
appendFile,
|
|
4
|
+
chmod,
|
|
5
|
+
link,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdir,
|
|
8
|
+
open,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
rename,
|
|
12
|
+
rm,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { resolveEvoDevPaths } from "../../../config/paths.ts";
|
|
17
|
+
import { isFileExistsError, isNotFoundError, sha256Hex } from "../../../utils/index.ts";
|
|
18
|
+
import { resolveSessionMemoryPaths } from "./paths.ts";
|
|
19
|
+
import { detectSessionMemorySensitivity, mergeSessionMemorySensitivity } from "./sensitivity.ts";
|
|
20
|
+
import type {
|
|
21
|
+
SessionEvidenceSegmentV1,
|
|
22
|
+
SessionMemoryCursorV1,
|
|
23
|
+
SessionMemoryIndexSegment,
|
|
24
|
+
SessionMemoryPaths,
|
|
25
|
+
SessionMemoryRawEventV1,
|
|
26
|
+
SessionMemorySensitivity,
|
|
27
|
+
SessionMemorySensitivityReason,
|
|
28
|
+
SessionMemoryStateV1,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
|
|
31
|
+
export class SessionMemoryEvidenceError extends Error {
|
|
32
|
+
readonly code: "not-found" | "invalid";
|
|
33
|
+
|
|
34
|
+
constructor(code: "not-found" | "invalid", message: string) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "SessionMemoryEvidenceError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function listSessionEvidenceSegments(input: {
|
|
42
|
+
homeDir: string;
|
|
43
|
+
projectKey?: string;
|
|
44
|
+
}): Promise<SessionEvidenceSegmentV1[]> {
|
|
45
|
+
const rootDir = join(resolveEvoDevPaths(input.homeDir).stateDir, "session-memory");
|
|
46
|
+
const projectDirs =
|
|
47
|
+
input.projectKey === undefined ? await listDirectoryNames(rootDir) : [input.projectKey];
|
|
48
|
+
const segments: SessionEvidenceSegmentV1[] = [];
|
|
49
|
+
for (const projectKey of projectDirs) {
|
|
50
|
+
const paths = resolveSessionMemoryPaths({
|
|
51
|
+
homeDir: input.homeDir,
|
|
52
|
+
projectKey,
|
|
53
|
+
sessionKey: "list",
|
|
54
|
+
});
|
|
55
|
+
for (const sessionKey of await listDirectoryNames(paths.projectDir)) {
|
|
56
|
+
const sessionPaths = resolveSessionMemoryPaths({
|
|
57
|
+
homeDir: input.homeDir,
|
|
58
|
+
projectKey,
|
|
59
|
+
sessionKey,
|
|
60
|
+
});
|
|
61
|
+
for (const entry of await listJsonFiles(sessionPaths.segmentsDir)) {
|
|
62
|
+
try {
|
|
63
|
+
segments.push(parseSessionEvidenceSegment(JSON.parse(await readFile(entry, "utf8"))));
|
|
64
|
+
} catch {
|
|
65
|
+
// Invalid local evidence is omitted from listings and remains available for diagnostics.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return segments.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function listSessionMemoryStates(input: {
|
|
74
|
+
homeDir: string;
|
|
75
|
+
projectKey?: string;
|
|
76
|
+
}): Promise<SessionMemoryStateV1[]> {
|
|
77
|
+
const rootDir = join(resolveEvoDevPaths(input.homeDir).stateDir, "session-memory");
|
|
78
|
+
const projectDirs =
|
|
79
|
+
input.projectKey === undefined ? await listDirectoryNames(rootDir) : [input.projectKey];
|
|
80
|
+
const states: SessionMemoryStateV1[] = [];
|
|
81
|
+
for (const projectKey of projectDirs) {
|
|
82
|
+
const projectDir = join(rootDir, projectKey);
|
|
83
|
+
for (const sessionKey of await listDirectoryNames(projectDir)) {
|
|
84
|
+
try {
|
|
85
|
+
const state = parseSessionMemoryState(
|
|
86
|
+
JSON.parse(await readFile(join(projectDir, sessionKey, "state.json"), "utf8")) as unknown,
|
|
87
|
+
);
|
|
88
|
+
if (state.projectKey === projectKey && state.sessionKey === sessionKey) states.push(state);
|
|
89
|
+
} catch {
|
|
90
|
+
// Invalid local state is omitted from listings and remains available for diagnostics.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return states.sort(
|
|
95
|
+
(left, right) =>
|
|
96
|
+
right.updatedAt.localeCompare(left.updatedAt) ||
|
|
97
|
+
left.sessionKey.localeCompare(right.sessionKey),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function readSessionEvidenceSegment(input: {
|
|
102
|
+
homeDir: string;
|
|
103
|
+
projectKey: string;
|
|
104
|
+
sessionKey: string;
|
|
105
|
+
segmentId: string;
|
|
106
|
+
}): Promise<SessionEvidenceSegmentV1> {
|
|
107
|
+
const paths = resolveSessionMemoryPaths(input);
|
|
108
|
+
try {
|
|
109
|
+
const segment = parseSessionEvidenceSegment(
|
|
110
|
+
JSON.parse(await readFile(paths.segmentPath(input.segmentId), "utf8")) as unknown,
|
|
111
|
+
);
|
|
112
|
+
if (
|
|
113
|
+
segment.id !== input.segmentId ||
|
|
114
|
+
segment.projectKey !== input.projectKey ||
|
|
115
|
+
segment.sessionKey !== input.sessionKey
|
|
116
|
+
) {
|
|
117
|
+
throw new SessionMemoryEvidenceError(
|
|
118
|
+
"invalid",
|
|
119
|
+
"Session evidence identity does not match its storage path.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return segment;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof SessionMemoryEvidenceError) throw error;
|
|
125
|
+
if (isNotFoundError(error)) {
|
|
126
|
+
throw new SessionMemoryEvidenceError("not-found", "Session evidence segment was not found.");
|
|
127
|
+
}
|
|
128
|
+
throw new SessionMemoryEvidenceError("invalid", "Session evidence segment is invalid.");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function updateSessionEvidenceSegmentLifecycle(input: {
|
|
133
|
+
homeDir: string;
|
|
134
|
+
projectKey: string;
|
|
135
|
+
sessionKey: string;
|
|
136
|
+
segmentId: string;
|
|
137
|
+
status: "distilled" | "ignored";
|
|
138
|
+
consumedByBatchId?: string;
|
|
139
|
+
}): Promise<SessionEvidenceSegmentV1> {
|
|
140
|
+
const segment = await readSessionEvidenceSegment(input);
|
|
141
|
+
const consumedByBatchIds =
|
|
142
|
+
input.consumedByBatchId === undefined
|
|
143
|
+
? segment.lifecycle.consumedByBatchIds
|
|
144
|
+
: [...new Set([...segment.lifecycle.consumedByBatchIds, input.consumedByBatchId])];
|
|
145
|
+
const updated: SessionEvidenceSegmentV1 = {
|
|
146
|
+
...segment,
|
|
147
|
+
lifecycle: {
|
|
148
|
+
...segment.lifecycle,
|
|
149
|
+
status: input.status,
|
|
150
|
+
consumedByBatchIds,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const paths = resolveSessionMemoryPaths(input);
|
|
154
|
+
await writeJson(paths.segmentPath(input.segmentId), updated);
|
|
155
|
+
return updated;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function readSessionState(path: string): Promise<SessionMemoryStateV1 | null> {
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(await readFile(path, "utf8")) as SessionMemoryStateV1;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (isNotFoundError(error)) return null;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function readSessionCursor(
|
|
168
|
+
path: string,
|
|
169
|
+
sessionKey: string,
|
|
170
|
+
sourcePath: string,
|
|
171
|
+
now: string,
|
|
172
|
+
): Promise<SessionMemoryCursorV1> {
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(await readFile(path, "utf8")) as SessionMemoryCursorV1;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!isNotFoundError(error)) throw error;
|
|
177
|
+
return {
|
|
178
|
+
schemaVersion: 1,
|
|
179
|
+
kind: "session-memory-cursor",
|
|
180
|
+
sessionKey,
|
|
181
|
+
traceRefId: null,
|
|
182
|
+
sourcePath,
|
|
183
|
+
lastCapturedOffset: null,
|
|
184
|
+
lastCapturedLine: null,
|
|
185
|
+
lastCapturedEventId: null,
|
|
186
|
+
updatedAt: now,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function appendRawEvent(
|
|
192
|
+
path: string,
|
|
193
|
+
event: SessionMemoryRawEventV1,
|
|
194
|
+
): Promise<{ lineNumber: number }> {
|
|
195
|
+
const existingLineCount = await countJsonlLines(path);
|
|
196
|
+
await mkdir(dirname(path), { recursive: true });
|
|
197
|
+
await appendFile(path, `${JSON.stringify(event)}\n`, "utf8");
|
|
198
|
+
return { lineNumber: existingLineCount + 1 };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function resetSessionRawEvents(path: string): Promise<void> {
|
|
202
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
203
|
+
await writeFile(path, "", { encoding: "utf8", mode: 0o600 });
|
|
204
|
+
await chmod(path, 0o600);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function rewriteSessionRawEvents(
|
|
208
|
+
path: string,
|
|
209
|
+
lines: string[],
|
|
210
|
+
expectedCurrent: string,
|
|
211
|
+
): Promise<boolean> {
|
|
212
|
+
await ensurePrivateSessionMemoryDirectory(dirname(path));
|
|
213
|
+
const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
|
|
214
|
+
try {
|
|
215
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
216
|
+
try {
|
|
217
|
+
await handle.writeFile(lines.length === 0 ? "" : `${lines.join("\n")}\n`, "utf8");
|
|
218
|
+
await handle.sync();
|
|
219
|
+
} finally {
|
|
220
|
+
await handle.close();
|
|
221
|
+
}
|
|
222
|
+
const current = await readFile(path, "utf8").catch(() => null);
|
|
223
|
+
if (current !== expectedCurrent) return false;
|
|
224
|
+
await rename(temporaryPath, path);
|
|
225
|
+
await chmod(path, 0o600);
|
|
226
|
+
return true;
|
|
227
|
+
} finally {
|
|
228
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function readLineRange(
|
|
233
|
+
path: string,
|
|
234
|
+
fromLine: number,
|
|
235
|
+
toLine: number,
|
|
236
|
+
): Promise<{
|
|
237
|
+
content: string;
|
|
238
|
+
truncated: boolean;
|
|
239
|
+
secretsDetected: boolean;
|
|
240
|
+
sensitivity: SessionMemorySensitivity;
|
|
241
|
+
sensitivityReasons: SessionMemorySensitivityReason[];
|
|
242
|
+
redactionApplied: boolean;
|
|
243
|
+
}> {
|
|
244
|
+
const text = await readFile(path, "utf8");
|
|
245
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
246
|
+
const selected = lines.slice(Math.max(0, fromLine - 1), toLine);
|
|
247
|
+
const detections = [];
|
|
248
|
+
let redactionApplied = false;
|
|
249
|
+
for (const line of selected) {
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(line) as Partial<SessionMemoryRawEventV1>;
|
|
252
|
+
redactionApplied ||= parsed.credentialRedactionApplied === true;
|
|
253
|
+
if (parsed.sensitivity !== undefined) {
|
|
254
|
+
detections.push({
|
|
255
|
+
classification: parsed.sensitivity,
|
|
256
|
+
reasons: [...(parsed.sensitivityReasons ?? [])],
|
|
257
|
+
});
|
|
258
|
+
} else if (typeof parsed.rawPayloadJson === "string") {
|
|
259
|
+
try {
|
|
260
|
+
detections.push(
|
|
261
|
+
detectSessionMemorySensitivity(JSON.parse(parsed.rawPayloadJson) as unknown),
|
|
262
|
+
);
|
|
263
|
+
} catch {
|
|
264
|
+
detections.push(detectSessionMemorySensitivity(parsed.rawPayloadJson));
|
|
265
|
+
}
|
|
266
|
+
} else if (parsed.secretsDetected === true) {
|
|
267
|
+
detections.push({
|
|
268
|
+
classification: "credential" as const,
|
|
269
|
+
reasons: ["legacy-sensitive-flag" as const],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
detections.push(detectSessionMemorySensitivity(line));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const sensitivity = mergeSessionMemorySensitivity(detections);
|
|
277
|
+
return {
|
|
278
|
+
content: selected.join("\n"),
|
|
279
|
+
truncated: false,
|
|
280
|
+
secretsDetected: sensitivity.classification === "credential",
|
|
281
|
+
sensitivity: sensitivity.classification,
|
|
282
|
+
sensitivityReasons: sensitivity.reasons,
|
|
283
|
+
redactionApplied,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function writeSessionIndex(
|
|
288
|
+
paths: SessionMemoryPaths,
|
|
289
|
+
state: SessionMemoryStateV1,
|
|
290
|
+
segment: SessionEvidenceSegmentV1,
|
|
291
|
+
now: string,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
const existing = await readSessionIndex(paths.indexPath);
|
|
294
|
+
const segments = [
|
|
295
|
+
...existing.segments.filter((item) => item.id !== segment.id),
|
|
296
|
+
{
|
|
297
|
+
id: segment.id,
|
|
298
|
+
reason: segment.reason,
|
|
299
|
+
strength: segment.strength,
|
|
300
|
+
createdAt: segment.createdAt,
|
|
301
|
+
reviewState: segment.lifecycle.reviewState,
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
await writeJson(paths.indexPath, {
|
|
305
|
+
schemaVersion: 1,
|
|
306
|
+
kind: "session-memory-index",
|
|
307
|
+
projectKey: state.projectKey,
|
|
308
|
+
sessionKey: state.sessionKey,
|
|
309
|
+
updatedAt: now,
|
|
310
|
+
lastSegmentId: segment.id,
|
|
311
|
+
segments,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function writeHistoricalSessionEvidenceSegment(input: {
|
|
316
|
+
homeDir: string;
|
|
317
|
+
paths: SessionMemoryPaths;
|
|
318
|
+
segment: SessionEvidenceSegmentV1;
|
|
319
|
+
}): Promise<{ created: boolean; segment: SessionEvidenceSegmentV1 }> {
|
|
320
|
+
if (input.segment.origin?.kind !== "historical-import" || input.segment.retention === undefined) {
|
|
321
|
+
throw new Error("Historical Session Evidence requires origin and retention metadata.");
|
|
322
|
+
}
|
|
323
|
+
assertSessionMemoryPathsMatchSegment(input.homeDir, input.paths, input.segment);
|
|
324
|
+
await ensurePrivateSessionMemoryDirectory(input.paths.segmentsDir);
|
|
325
|
+
const path = input.paths.segmentPath(input.segment.id);
|
|
326
|
+
const created = await writePrivateImmutableText(
|
|
327
|
+
path,
|
|
328
|
+
`${JSON.stringify(input.segment, null, 2)}\n`,
|
|
329
|
+
);
|
|
330
|
+
if (created) {
|
|
331
|
+
return { created: true, segment: input.segment };
|
|
332
|
+
}
|
|
333
|
+
const existingInfo = await lstat(path);
|
|
334
|
+
if (!existingInfo.isFile() || existingInfo.isSymbolicLink()) {
|
|
335
|
+
throw new Error("Historical Session Evidence path is not a regular state file.");
|
|
336
|
+
}
|
|
337
|
+
const existing = await readSessionEvidenceSegment({
|
|
338
|
+
homeDir: input.homeDir,
|
|
339
|
+
projectKey: input.segment.projectKey,
|
|
340
|
+
sessionKey: input.segment.sessionKey,
|
|
341
|
+
segmentId: input.segment.id,
|
|
342
|
+
});
|
|
343
|
+
assertHistoricalSegmentCompatible(existing, input.segment);
|
|
344
|
+
await chmod(path, 0o600);
|
|
345
|
+
return { created: false, segment: existing };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function writeHistoricalSessionStateAndIndex(input: {
|
|
349
|
+
homeDir: string;
|
|
350
|
+
paths: SessionMemoryPaths;
|
|
351
|
+
projectKey: string;
|
|
352
|
+
sessionKey: string;
|
|
353
|
+
target: SessionMemoryStateV1["target"];
|
|
354
|
+
roleId: string | null;
|
|
355
|
+
policy: SessionMemoryStateV1["policy"];
|
|
356
|
+
now: string;
|
|
357
|
+
}): Promise<{ state: SessionMemoryStateV1; segmentIds: string[] }> {
|
|
358
|
+
const expectedPaths = resolveSessionMemoryPaths({
|
|
359
|
+
homeDir: input.homeDir,
|
|
360
|
+
projectKey: input.projectKey,
|
|
361
|
+
sessionKey: input.sessionKey,
|
|
362
|
+
});
|
|
363
|
+
if (
|
|
364
|
+
expectedPaths.rootDir !== input.paths.rootDir ||
|
|
365
|
+
expectedPaths.projectDir !== input.paths.projectDir ||
|
|
366
|
+
expectedPaths.sessionDir !== input.paths.sessionDir ||
|
|
367
|
+
expectedPaths.segmentsDir !== input.paths.segmentsDir ||
|
|
368
|
+
expectedPaths.statePath !== input.paths.statePath ||
|
|
369
|
+
expectedPaths.indexPath !== input.paths.indexPath
|
|
370
|
+
) {
|
|
371
|
+
throw new Error("Historical Session Memory state path does not match its identity.");
|
|
372
|
+
}
|
|
373
|
+
await ensurePrivateSessionMemoryDirectory(input.paths.rootDir);
|
|
374
|
+
await ensurePrivateSessionMemoryDirectory(input.paths.projectDir);
|
|
375
|
+
await ensurePrivateSessionMemoryDirectory(input.paths.sessionDir);
|
|
376
|
+
await ensurePrivateSessionMemoryDirectory(input.paths.segmentsDir);
|
|
377
|
+
const existing = await readSessionState(input.paths.statePath);
|
|
378
|
+
const segments = await readSessionSegmentsFromDirectory(input.paths.segmentsDir);
|
|
379
|
+
const ordered = segments.sort(
|
|
380
|
+
(left, right) =>
|
|
381
|
+
left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),
|
|
382
|
+
);
|
|
383
|
+
const last = ordered.at(-1) ?? null;
|
|
384
|
+
const state: SessionMemoryStateV1 = {
|
|
385
|
+
schemaVersion: 1,
|
|
386
|
+
kind: "session-memory-state",
|
|
387
|
+
projectKey: input.projectKey,
|
|
388
|
+
sessionKey: input.sessionKey,
|
|
389
|
+
target: input.target,
|
|
390
|
+
runId: null,
|
|
391
|
+
roleId: input.roleId,
|
|
392
|
+
initialized: true,
|
|
393
|
+
createdAt: existing?.createdAt ?? input.now,
|
|
394
|
+
updatedAt: input.now,
|
|
395
|
+
counters: existing?.counters ?? {
|
|
396
|
+
messageTokenEstimate: 0,
|
|
397
|
+
tokensSinceLastUpdate: 0,
|
|
398
|
+
toolCallsSinceLastUpdate: 0,
|
|
399
|
+
userPromptCount: 0,
|
|
400
|
+
assistantTurnCount: 0,
|
|
401
|
+
toolCallCount: 0,
|
|
402
|
+
failureSignalCount: 0,
|
|
403
|
+
verificationSignalCount: 0,
|
|
404
|
+
},
|
|
405
|
+
activeSpan: null,
|
|
406
|
+
pendingSignals: [],
|
|
407
|
+
lastSegmentId: last?.id ?? null,
|
|
408
|
+
lastMemoryUpdateAt: input.now,
|
|
409
|
+
policy: input.policy,
|
|
410
|
+
};
|
|
411
|
+
const index = {
|
|
412
|
+
schemaVersion: 1,
|
|
413
|
+
kind: "session-memory-index",
|
|
414
|
+
projectKey: input.projectKey,
|
|
415
|
+
sessionKey: input.sessionKey,
|
|
416
|
+
updatedAt: input.now,
|
|
417
|
+
lastSegmentId: last?.id ?? null,
|
|
418
|
+
segments: ordered.map((segment) => ({
|
|
419
|
+
id: segment.id,
|
|
420
|
+
reason: segment.reason,
|
|
421
|
+
strength: segment.strength,
|
|
422
|
+
createdAt: segment.createdAt,
|
|
423
|
+
reviewState: segment.lifecycle.reviewState,
|
|
424
|
+
})),
|
|
425
|
+
};
|
|
426
|
+
await writePrivateAtomicJson(input.paths.statePath, state);
|
|
427
|
+
await writePrivateAtomicJson(input.paths.indexPath, index);
|
|
428
|
+
return { state, segmentIds: ordered.map((segment) => segment.id) };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function writeSessionEvidenceSegmentAtomic(input: {
|
|
432
|
+
homeDir: string;
|
|
433
|
+
segment: SessionEvidenceSegmentV1;
|
|
434
|
+
}): Promise<void> {
|
|
435
|
+
assertSessionEvidenceRetentionState(input.segment);
|
|
436
|
+
const paths = resolveSessionMemoryPaths({
|
|
437
|
+
homeDir: input.homeDir,
|
|
438
|
+
projectKey: input.segment.projectKey,
|
|
439
|
+
sessionKey: input.segment.sessionKey,
|
|
440
|
+
});
|
|
441
|
+
await writePrivateAtomicJson(paths.segmentPath(input.segment.id), input.segment);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function assertSessionEvidenceRetentionState(segment: SessionEvidenceSegmentV1): void {
|
|
445
|
+
if (segment.retention === undefined) {
|
|
446
|
+
throw new Error("Atomic Session Evidence rewrite requires retention metadata.");
|
|
447
|
+
}
|
|
448
|
+
if (segment.retention.rawState === "available") {
|
|
449
|
+
if (
|
|
450
|
+
!segment.rawExcerpt.stored ||
|
|
451
|
+
segment.rawExcerpt.byteLength !== Buffer.byteLength(segment.rawExcerpt.content, "utf8") ||
|
|
452
|
+
segment.rawExcerpt.sha256 !== sha256Hex(segment.rawExcerpt.content) ||
|
|
453
|
+
segment.retention.originalRawSha256 !== segment.rawExcerpt.sha256
|
|
454
|
+
) {
|
|
455
|
+
throw new Error("Available Session Evidence raw state is invalid.");
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
segment.rawExcerpt.stored ||
|
|
461
|
+
segment.rawExcerpt.content !== "" ||
|
|
462
|
+
segment.rawExcerpt.byteLength !== 0 ||
|
|
463
|
+
segment.rawExcerpt.sha256 !== sha256Hex("") ||
|
|
464
|
+
segment.retention.rawPurgedAt === null ||
|
|
465
|
+
segment.lifecycle.status !== "raw-expired"
|
|
466
|
+
) {
|
|
467
|
+
throw new Error("Purged Session Evidence raw state is invalid.");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export async function writeJson(path: string, value: unknown): Promise<void> {
|
|
472
|
+
await mkdir(dirname(path), { recursive: true });
|
|
473
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function readSessionSegmentsFromDirectory(
|
|
477
|
+
segmentsDir: string,
|
|
478
|
+
): Promise<SessionEvidenceSegmentV1[]> {
|
|
479
|
+
const segments: SessionEvidenceSegmentV1[] = [];
|
|
480
|
+
for (const path of await listJsonFiles(segmentsDir)) {
|
|
481
|
+
segments.push(parseSessionEvidenceSegment(JSON.parse(await readFile(path, "utf8"))));
|
|
482
|
+
}
|
|
483
|
+
return segments;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function ensurePrivateSessionMemoryDirectory(path: string): Promise<void> {
|
|
487
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
488
|
+
await chmod(path, 0o700);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function writePrivateAtomicJson(path: string, value: unknown): Promise<void> {
|
|
492
|
+
await ensurePrivateSessionMemoryDirectory(dirname(path));
|
|
493
|
+
const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
|
|
494
|
+
try {
|
|
495
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
496
|
+
try {
|
|
497
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
498
|
+
await handle.sync();
|
|
499
|
+
} finally {
|
|
500
|
+
await handle.close();
|
|
501
|
+
}
|
|
502
|
+
await rename(temporaryPath, path);
|
|
503
|
+
await chmod(path, 0o600);
|
|
504
|
+
} finally {
|
|
505
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function writePrivateImmutableText(path: string, content: string): Promise<boolean> {
|
|
510
|
+
await ensurePrivateSessionMemoryDirectory(dirname(path));
|
|
511
|
+
const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
|
|
512
|
+
try {
|
|
513
|
+
const handle = await open(temporaryPath, "wx", 0o600);
|
|
514
|
+
try {
|
|
515
|
+
await handle.writeFile(content, "utf8");
|
|
516
|
+
await handle.sync();
|
|
517
|
+
} finally {
|
|
518
|
+
await handle.close();
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await link(temporaryPath, path);
|
|
522
|
+
await chmod(path, 0o600);
|
|
523
|
+
return true;
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (!isFileExistsError(error)) throw error;
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
} finally {
|
|
529
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function assertHistoricalSegmentCompatible(
|
|
534
|
+
existing: SessionEvidenceSegmentV1,
|
|
535
|
+
expected: SessionEvidenceSegmentV1,
|
|
536
|
+
): void {
|
|
537
|
+
const existingOrigin = existing.origin;
|
|
538
|
+
const expectedOrigin = expected.origin;
|
|
539
|
+
const existingRetention = existing.retention;
|
|
540
|
+
const expectedRetention = expected.retention;
|
|
541
|
+
if (
|
|
542
|
+
existingOrigin?.kind !== "historical-import" ||
|
|
543
|
+
expectedOrigin?.kind !== "historical-import" ||
|
|
544
|
+
existingRetention === undefined ||
|
|
545
|
+
expectedRetention === undefined
|
|
546
|
+
) {
|
|
547
|
+
throw new Error("Historical Session Evidence identity conflict.");
|
|
548
|
+
}
|
|
549
|
+
const existingProjection = {
|
|
550
|
+
schemaVersion: existing.schemaVersion,
|
|
551
|
+
kind: existing.kind,
|
|
552
|
+
id: existing.id,
|
|
553
|
+
projectKey: existing.projectKey,
|
|
554
|
+
runId: existing.runId,
|
|
555
|
+
roleId: existing.roleId,
|
|
556
|
+
sessionKey: existing.sessionKey,
|
|
557
|
+
target: existing.target,
|
|
558
|
+
createdAt: existing.createdAt,
|
|
559
|
+
reason: existing.reason,
|
|
560
|
+
strength: existing.strength,
|
|
561
|
+
origin: existingOrigin,
|
|
562
|
+
source: existing.source,
|
|
563
|
+
signals: existing.signals,
|
|
564
|
+
normalized: existing.normalized,
|
|
565
|
+
privacy: {
|
|
566
|
+
localOnly: existing.privacy.localOnly,
|
|
567
|
+
secretsDetected: existing.privacy.secretsDetected,
|
|
568
|
+
sensitivity: existing.privacy.sensitivity,
|
|
569
|
+
sensitivityReasons: existing.privacy.sensitivityReasons,
|
|
570
|
+
redactionApplied: existing.privacy.redactionApplied,
|
|
571
|
+
externalUploadAllowed: existing.privacy.externalUploadAllowed,
|
|
572
|
+
},
|
|
573
|
+
rawTruncated: existing.rawExcerpt.truncated,
|
|
574
|
+
originalRawSha256: existingRetention.originalRawSha256,
|
|
575
|
+
retentionPolicyDays: existingRetention.policyDays,
|
|
576
|
+
retentionExpiresAt: existingRetention.expiresAt,
|
|
577
|
+
};
|
|
578
|
+
const expectedProjection = {
|
|
579
|
+
schemaVersion: expected.schemaVersion,
|
|
580
|
+
kind: expected.kind,
|
|
581
|
+
id: expected.id,
|
|
582
|
+
projectKey: expected.projectKey,
|
|
583
|
+
runId: expected.runId,
|
|
584
|
+
roleId: expected.roleId,
|
|
585
|
+
sessionKey: expected.sessionKey,
|
|
586
|
+
target: expected.target,
|
|
587
|
+
createdAt: expected.createdAt,
|
|
588
|
+
reason: expected.reason,
|
|
589
|
+
strength: expected.strength,
|
|
590
|
+
origin: expectedOrigin,
|
|
591
|
+
source: expected.source,
|
|
592
|
+
signals: expected.signals,
|
|
593
|
+
normalized: expected.normalized,
|
|
594
|
+
privacy: {
|
|
595
|
+
localOnly: expected.privacy.localOnly,
|
|
596
|
+
secretsDetected: expected.privacy.secretsDetected,
|
|
597
|
+
sensitivity: expected.privacy.sensitivity,
|
|
598
|
+
sensitivityReasons: expected.privacy.sensitivityReasons,
|
|
599
|
+
redactionApplied: expected.privacy.redactionApplied,
|
|
600
|
+
externalUploadAllowed: expected.privacy.externalUploadAllowed,
|
|
601
|
+
},
|
|
602
|
+
rawTruncated: expected.rawExcerpt.truncated,
|
|
603
|
+
originalRawSha256: expectedRetention.originalRawSha256,
|
|
604
|
+
retentionPolicyDays: expectedRetention.policyDays,
|
|
605
|
+
retentionExpiresAt: expectedRetention.expiresAt,
|
|
606
|
+
};
|
|
607
|
+
if (
|
|
608
|
+
JSON.stringify(existingProjection) !== JSON.stringify(expectedProjection) ||
|
|
609
|
+
(existingRetention.rawState === "available" &&
|
|
610
|
+
(!existing.rawExcerpt.stored ||
|
|
611
|
+
existing.rawExcerpt.encoding !== "utf8" ||
|
|
612
|
+
existing.rawExcerpt.byteLength !== Buffer.byteLength(existing.rawExcerpt.content, "utf8") ||
|
|
613
|
+
existing.rawExcerpt.sha256 !== sha256Hex(existing.rawExcerpt.content) ||
|
|
614
|
+
existing.rawExcerpt.sha256 !== expected.rawExcerpt.sha256))
|
|
615
|
+
) {
|
|
616
|
+
throw new Error("Historical Session Evidence immutable content conflict.");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function assertSessionMemoryPathsMatchSegment(
|
|
621
|
+
homeDir: string,
|
|
622
|
+
paths: SessionMemoryPaths,
|
|
623
|
+
segment: SessionEvidenceSegmentV1,
|
|
624
|
+
): void {
|
|
625
|
+
const expected = resolveSessionMemoryPaths({
|
|
626
|
+
homeDir,
|
|
627
|
+
projectKey: segment.projectKey,
|
|
628
|
+
sessionKey: segment.sessionKey,
|
|
629
|
+
});
|
|
630
|
+
if (
|
|
631
|
+
paths.rootDir !== expected.rootDir ||
|
|
632
|
+
paths.projectDir !== expected.projectDir ||
|
|
633
|
+
paths.sessionDir !== expected.sessionDir ||
|
|
634
|
+
paths.segmentsDir !== expected.segmentsDir ||
|
|
635
|
+
paths.segmentPath(segment.id) !== expected.segmentPath(segment.id)
|
|
636
|
+
) {
|
|
637
|
+
throw new Error("Historical Session Evidence path does not match its identity.");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function countJsonlLines(path: string): Promise<number> {
|
|
642
|
+
try {
|
|
643
|
+
const text = await readFile(path, "utf8");
|
|
644
|
+
return text.split("\n").filter((line) => line.trim() !== "").length;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (isNotFoundError(error)) return 0;
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function readSessionIndex(path: string): Promise<{ segments: SessionMemoryIndexSegment[] }> {
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(await readFile(path, "utf8")) as {
|
|
654
|
+
segments?: unknown;
|
|
655
|
+
};
|
|
656
|
+
if (!Array.isArray(parsed.segments)) return { segments: [] };
|
|
657
|
+
return {
|
|
658
|
+
segments: parsed.segments.filter(isSessionIndexSegment),
|
|
659
|
+
};
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (isNotFoundError(error)) return { segments: [] };
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function isSessionIndexSegment(value: unknown): value is SessionMemoryIndexSegment {
|
|
667
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
668
|
+
const item = value as Record<string, unknown>;
|
|
669
|
+
return (
|
|
670
|
+
typeof item.id === "string" &&
|
|
671
|
+
typeof item.reason === "string" &&
|
|
672
|
+
typeof item.strength === "string" &&
|
|
673
|
+
typeof item.createdAt === "string" &&
|
|
674
|
+
typeof item.reviewState === "string"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function parseSessionEvidenceSegment(value: unknown): SessionEvidenceSegmentV1 {
|
|
679
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
680
|
+
throw new SessionMemoryEvidenceError("invalid", "Session evidence must be an object.");
|
|
681
|
+
}
|
|
682
|
+
const segment = value as Partial<SessionEvidenceSegmentV1>;
|
|
683
|
+
if (
|
|
684
|
+
segment.schemaVersion !== 1 ||
|
|
685
|
+
segment.kind !== "session-evidence-segment" ||
|
|
686
|
+
typeof segment.id !== "string" ||
|
|
687
|
+
typeof segment.projectKey !== "string" ||
|
|
688
|
+
typeof segment.sessionKey !== "string" ||
|
|
689
|
+
typeof segment.createdAt !== "string" ||
|
|
690
|
+
typeof segment.normalized !== "object" ||
|
|
691
|
+
segment.normalized === null ||
|
|
692
|
+
typeof segment.rawExcerpt !== "object" ||
|
|
693
|
+
segment.rawExcerpt === null
|
|
694
|
+
) {
|
|
695
|
+
throw new SessionMemoryEvidenceError("invalid", "Session evidence fields are invalid.");
|
|
696
|
+
}
|
|
697
|
+
return segment as SessionEvidenceSegmentV1;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function parseSessionMemoryState(value: unknown): SessionMemoryStateV1 {
|
|
701
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
702
|
+
throw new SessionMemoryEvidenceError("invalid", "Session Memory state must be an object.");
|
|
703
|
+
}
|
|
704
|
+
const state = value as Partial<SessionMemoryStateV1>;
|
|
705
|
+
if (
|
|
706
|
+
state.schemaVersion !== 1 ||
|
|
707
|
+
state.kind !== "session-memory-state" ||
|
|
708
|
+
typeof state.projectKey !== "string" ||
|
|
709
|
+
typeof state.sessionKey !== "string" ||
|
|
710
|
+
!(state.runId === null || typeof state.runId === "string") ||
|
|
711
|
+
!(state.roleId === null || typeof state.roleId === "string") ||
|
|
712
|
+
typeof state.createdAt !== "string" ||
|
|
713
|
+
typeof state.updatedAt !== "string" ||
|
|
714
|
+
typeof state.counters !== "object" ||
|
|
715
|
+
state.counters === null
|
|
716
|
+
) {
|
|
717
|
+
throw new SessionMemoryEvidenceError("invalid", "Session Memory state fields are invalid.");
|
|
718
|
+
}
|
|
719
|
+
return state as SessionMemoryStateV1;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function listDirectoryNames(path: string): Promise<string[]> {
|
|
723
|
+
try {
|
|
724
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
725
|
+
.filter((entry) => entry.isDirectory())
|
|
726
|
+
.map((entry) => entry.name)
|
|
727
|
+
.sort();
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (isNotFoundError(error)) return [];
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function listJsonFiles(path: string): Promise<string[]> {
|
|
735
|
+
try {
|
|
736
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
737
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
738
|
+
.map((entry) => join(path, entry.name))
|
|
739
|
+
.sort();
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (isNotFoundError(error)) return [];
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
}
|