@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,643 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createStableId, normalizeTimestamp, sha256Hex } from "../../../utils/index.ts";
|
|
3
|
+
import { readEvolutionReviewSnapshot } from "../../candidates/index.ts";
|
|
4
|
+
import type { EvolutionReviewSnapshot, SegmentEvolutionTriggerRecord } from "../../schema.ts";
|
|
5
|
+
import { listSegmentEvolutionTriggers } from "../../triggers/index.ts";
|
|
6
|
+
import { resolveSessionMemoryPaths } from "./paths.ts";
|
|
7
|
+
import {
|
|
8
|
+
listSessionEvidenceSegments,
|
|
9
|
+
listSessionMemoryStates,
|
|
10
|
+
readSessionEvidenceSegment,
|
|
11
|
+
rewriteSessionRawEvents,
|
|
12
|
+
writeSessionEvidenceSegmentAtomic,
|
|
13
|
+
} from "./storage.ts";
|
|
14
|
+
import type { SessionEvidenceSegmentV1, SessionMemoryRawEventV1 } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_RETENTION_LIMIT = 20;
|
|
17
|
+
const MAX_RETENTION_LIMIT = 10_000;
|
|
18
|
+
const DEFAULT_LEGACY_RETENTION_DAYS = 30;
|
|
19
|
+
|
|
20
|
+
export type SessionEvidenceRetentionProtectionReason =
|
|
21
|
+
| "trigger-missing"
|
|
22
|
+
| "trigger-not-terminal"
|
|
23
|
+
| "segment-review-pending"
|
|
24
|
+
| "derived-review-pending"
|
|
25
|
+
| "retention-invalid"
|
|
26
|
+
| "segment-changed";
|
|
27
|
+
|
|
28
|
+
export type HistoricalSessionEvidenceRetentionProtectionReason =
|
|
29
|
+
SessionEvidenceRetentionProtectionReason;
|
|
30
|
+
|
|
31
|
+
export interface PurgeExpiredSessionEvidenceResult {
|
|
32
|
+
scanned: number;
|
|
33
|
+
due: number;
|
|
34
|
+
purged: number;
|
|
35
|
+
alreadyPurged: number;
|
|
36
|
+
protected: Array<{
|
|
37
|
+
projectKey: string;
|
|
38
|
+
segmentId: string;
|
|
39
|
+
reason: SessionEvidenceRetentionProtectionReason;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PurgeExpiredHistoricalSessionEvidenceResult = PurgeExpiredSessionEvidenceResult;
|
|
44
|
+
|
|
45
|
+
export interface SessionEvidenceRetentionInspection {
|
|
46
|
+
available: number;
|
|
47
|
+
purged: number;
|
|
48
|
+
overdue: number;
|
|
49
|
+
purgeEligible: number;
|
|
50
|
+
protected: number;
|
|
51
|
+
legacyWithoutRetention: number;
|
|
52
|
+
legacyOverdue: number;
|
|
53
|
+
legacyRawBytes: number;
|
|
54
|
+
legacyOverdueRawBytes: number;
|
|
55
|
+
expiredCapturedEventPayloads: number;
|
|
56
|
+
expiredCapturedEventRawBytes: number;
|
|
57
|
+
protectionReasons: Partial<Record<SessionEvidenceRetentionProtectionReason, number>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type HistoricalSessionEvidenceRetentionInspection = SessionEvidenceRetentionInspection;
|
|
61
|
+
|
|
62
|
+
export interface LegacySessionEvidenceRetentionMigrationResult {
|
|
63
|
+
migrated: number;
|
|
64
|
+
expiredCapturedEventPayloads: number;
|
|
65
|
+
expiredCapturedEventRawBytes: number;
|
|
66
|
+
purge: PurgeExpiredSessionEvidenceResult;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface RetentionContext {
|
|
70
|
+
reviewSnapshots: Map<string, Promise<EvolutionReviewSnapshot>>;
|
|
71
|
+
triggerMaps: Map<string, Promise<Map<string, SegmentEvolutionTriggerRecord[]>>>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function inspectSessionEvidenceRetention(input: {
|
|
75
|
+
homeDir: string;
|
|
76
|
+
projectKey?: string;
|
|
77
|
+
now?: string | Date;
|
|
78
|
+
}): Promise<SessionEvidenceRetentionInspection> {
|
|
79
|
+
const result = await inspectRetention(input, () => true);
|
|
80
|
+
const expiredEvents = await expireCapturedRawEvents({ ...input, apply: false });
|
|
81
|
+
result.expiredCapturedEventPayloads = expiredEvents.payloads;
|
|
82
|
+
result.expiredCapturedEventRawBytes = expiredEvents.rawBytes;
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function inspectHistoricalSessionEvidenceRetention(input: {
|
|
87
|
+
homeDir: string;
|
|
88
|
+
projectKey?: string;
|
|
89
|
+
now?: string | Date;
|
|
90
|
+
}): Promise<HistoricalSessionEvidenceRetentionInspection> {
|
|
91
|
+
return inspectRetention(input, (segment) => segment.origin?.kind === "historical-import");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function inspectRetention(
|
|
95
|
+
input: { homeDir: string; projectKey?: string; now?: string | Date },
|
|
96
|
+
include: (segment: SessionEvidenceSegmentV1) => boolean,
|
|
97
|
+
): Promise<SessionEvidenceRetentionInspection> {
|
|
98
|
+
const now = normalizeTimestamp(input.now);
|
|
99
|
+
const segments = (
|
|
100
|
+
await listSessionEvidenceSegments({
|
|
101
|
+
homeDir: input.homeDir,
|
|
102
|
+
projectKey: input.projectKey,
|
|
103
|
+
})
|
|
104
|
+
).filter(include);
|
|
105
|
+
const retentionDays = await loadLegacyRetentionDays(input.homeDir, input.projectKey);
|
|
106
|
+
const result = emptyInspection();
|
|
107
|
+
const context = createRetentionContext();
|
|
108
|
+
|
|
109
|
+
for (const segment of segments) {
|
|
110
|
+
if (segment.retention === undefined) {
|
|
111
|
+
const rawBytes = segment.rawExcerpt.stored ? segment.rawExcerpt.byteLength : 0;
|
|
112
|
+
result.legacyWithoutRetention += 1;
|
|
113
|
+
result.legacyRawBytes += rawBytes;
|
|
114
|
+
const expiresAt = legacyExpiresAt(segment, retentionDays);
|
|
115
|
+
if (Date.parse(expiresAt) <= Date.parse(now)) {
|
|
116
|
+
result.legacyOverdue += 1;
|
|
117
|
+
result.legacyOverdueRawBytes += rawBytes;
|
|
118
|
+
const protection = await findRetentionProtection({
|
|
119
|
+
homeDir: input.homeDir,
|
|
120
|
+
segment,
|
|
121
|
+
context,
|
|
122
|
+
});
|
|
123
|
+
if (protection === null) result.purgeEligible += 1;
|
|
124
|
+
else addProtection(result, protection);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (segment.retention.rawState === "purged") {
|
|
129
|
+
result.purged += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
result.available += 1;
|
|
133
|
+
if (!hasValidAvailableRetention(segment)) {
|
|
134
|
+
result.overdue += 1;
|
|
135
|
+
addProtection(result, "retention-invalid");
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (Date.parse(segment.retention.expiresAt) > Date.parse(now)) continue;
|
|
139
|
+
result.overdue += 1;
|
|
140
|
+
const protection = await findRetentionProtection({
|
|
141
|
+
homeDir: input.homeDir,
|
|
142
|
+
segment,
|
|
143
|
+
context,
|
|
144
|
+
});
|
|
145
|
+
if (protection === null) result.purgeEligible += 1;
|
|
146
|
+
else addProtection(result, protection);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function purgeExpiredSessionEvidence(input: {
|
|
152
|
+
homeDir: string;
|
|
153
|
+
projectKey?: string;
|
|
154
|
+
now?: string | Date;
|
|
155
|
+
limit?: number;
|
|
156
|
+
}): Promise<PurgeExpiredSessionEvidenceResult> {
|
|
157
|
+
return purgeRetention(input, () => true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function purgeExpiredHistoricalSessionEvidence(input: {
|
|
161
|
+
homeDir: string;
|
|
162
|
+
projectKey?: string;
|
|
163
|
+
now?: string | Date;
|
|
164
|
+
limit?: number;
|
|
165
|
+
}): Promise<PurgeExpiredHistoricalSessionEvidenceResult> {
|
|
166
|
+
return purgeRetention(input, (segment) => segment.origin?.kind === "historical-import");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function purgeRetention(
|
|
170
|
+
input: { homeDir: string; projectKey?: string; now?: string | Date; limit?: number },
|
|
171
|
+
include: (segment: SessionEvidenceSegmentV1) => boolean,
|
|
172
|
+
): Promise<PurgeExpiredSessionEvidenceResult> {
|
|
173
|
+
const now = normalizeTimestamp(input.now);
|
|
174
|
+
const limit = normalizeLimit(input.limit);
|
|
175
|
+
const segments = (
|
|
176
|
+
await listSessionEvidenceSegments({
|
|
177
|
+
homeDir: input.homeDir,
|
|
178
|
+
projectKey: input.projectKey,
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
.filter((segment) => include(segment) && segment.retention !== undefined)
|
|
182
|
+
.sort(
|
|
183
|
+
(left, right) =>
|
|
184
|
+
(left.retention?.expiresAt ?? "").localeCompare(right.retention?.expiresAt ?? "") ||
|
|
185
|
+
left.id.localeCompare(right.id),
|
|
186
|
+
);
|
|
187
|
+
const result: PurgeExpiredSessionEvidenceResult = {
|
|
188
|
+
scanned: 0,
|
|
189
|
+
due: 0,
|
|
190
|
+
purged: 0,
|
|
191
|
+
alreadyPurged: segments.filter((segment) => segment.retention?.rawState === "purged").length,
|
|
192
|
+
protected: [],
|
|
193
|
+
};
|
|
194
|
+
const context = createRetentionContext();
|
|
195
|
+
|
|
196
|
+
for (const segment of segments) {
|
|
197
|
+
if (result.scanned >= limit) break;
|
|
198
|
+
if (segment.retention === undefined || segment.retention.rawState === "purged") continue;
|
|
199
|
+
result.scanned += 1;
|
|
200
|
+
if (!hasValidAvailableRetention(segment)) {
|
|
201
|
+
result.due += 1;
|
|
202
|
+
result.protected.push({
|
|
203
|
+
projectKey: segment.projectKey,
|
|
204
|
+
segmentId: segment.id,
|
|
205
|
+
reason: "retention-invalid",
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (Date.parse(segment.retention.expiresAt) > Date.parse(now)) continue;
|
|
210
|
+
result.due += 1;
|
|
211
|
+
|
|
212
|
+
const protection = await findRetentionProtection({
|
|
213
|
+
homeDir: input.homeDir,
|
|
214
|
+
segment,
|
|
215
|
+
context,
|
|
216
|
+
});
|
|
217
|
+
if (protection !== null) {
|
|
218
|
+
result.protected.push({
|
|
219
|
+
projectKey: segment.projectKey,
|
|
220
|
+
segmentId: segment.id,
|
|
221
|
+
reason: protection,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const current = await readSessionEvidenceSegment({
|
|
227
|
+
homeDir: input.homeDir,
|
|
228
|
+
projectKey: segment.projectKey,
|
|
229
|
+
sessionKey: segment.sessionKey,
|
|
230
|
+
segmentId: segment.id,
|
|
231
|
+
});
|
|
232
|
+
if (
|
|
233
|
+
!hasValidAvailableRetention(current) ||
|
|
234
|
+
current.retention?.rawState !== "available" ||
|
|
235
|
+
current.retention.expiresAt !== segment.retention.expiresAt ||
|
|
236
|
+
!current.rawExcerpt.stored ||
|
|
237
|
+
current.rawExcerpt.sha256 !== segment.rawExcerpt.sha256 ||
|
|
238
|
+
current.lifecycle.reviewState !== segment.lifecycle.reviewState
|
|
239
|
+
) {
|
|
240
|
+
result.protected.push({
|
|
241
|
+
projectKey: segment.projectKey,
|
|
242
|
+
segmentId: segment.id,
|
|
243
|
+
reason: "segment-changed",
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const freshProtection = await findRetentionProtection({
|
|
248
|
+
homeDir: input.homeDir,
|
|
249
|
+
segment: current,
|
|
250
|
+
context: createRetentionContext(),
|
|
251
|
+
});
|
|
252
|
+
if (freshProtection !== null) {
|
|
253
|
+
result.protected.push({
|
|
254
|
+
projectKey: current.projectKey,
|
|
255
|
+
segmentId: current.id,
|
|
256
|
+
reason: freshProtection,
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await writeSessionEvidenceSegmentAtomic({
|
|
262
|
+
homeDir: input.homeDir,
|
|
263
|
+
segment: createPurgedSegment(current, now),
|
|
264
|
+
});
|
|
265
|
+
result.purged += 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function migrateLegacySessionEvidenceRetention(input: {
|
|
272
|
+
homeDir: string;
|
|
273
|
+
projectKey?: string;
|
|
274
|
+
now?: string | Date;
|
|
275
|
+
}): Promise<LegacySessionEvidenceRetentionMigrationResult> {
|
|
276
|
+
const now = normalizeTimestamp(input.now);
|
|
277
|
+
const retentionDays = await loadLegacyRetentionDays(input.homeDir, input.projectKey);
|
|
278
|
+
const segments = await listSessionEvidenceSegments({
|
|
279
|
+
homeDir: input.homeDir,
|
|
280
|
+
projectKey: input.projectKey,
|
|
281
|
+
});
|
|
282
|
+
let migrated = 0;
|
|
283
|
+
const context = createRetentionContext();
|
|
284
|
+
for (const segment of segments) {
|
|
285
|
+
if (segment.retention !== undefined || !segment.rawExcerpt.stored) continue;
|
|
286
|
+
const policyDays = retentionDays.get(sessionKey(segment)) ?? DEFAULT_LEGACY_RETENTION_DAYS;
|
|
287
|
+
if (Date.parse(addDays(segment.createdAt, policyDays)) > Date.parse(now)) continue;
|
|
288
|
+
const protection = await findRetentionProtection({
|
|
289
|
+
homeDir: input.homeDir,
|
|
290
|
+
segment,
|
|
291
|
+
context,
|
|
292
|
+
});
|
|
293
|
+
if (protection !== null) continue;
|
|
294
|
+
const current = await readSessionEvidenceSegment({
|
|
295
|
+
homeDir: input.homeDir,
|
|
296
|
+
projectKey: segment.projectKey,
|
|
297
|
+
sessionKey: segment.sessionKey,
|
|
298
|
+
segmentId: segment.id,
|
|
299
|
+
});
|
|
300
|
+
if (current.retention !== undefined || JSON.stringify(current) !== JSON.stringify(segment)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
await writeSessionEvidenceSegmentAtomic({
|
|
304
|
+
homeDir: input.homeDir,
|
|
305
|
+
segment: {
|
|
306
|
+
...current,
|
|
307
|
+
retention: {
|
|
308
|
+
policyDays,
|
|
309
|
+
expiresAt: addDays(current.createdAt, policyDays),
|
|
310
|
+
rawState: "available",
|
|
311
|
+
rawPurgedAt: null,
|
|
312
|
+
originalRawSha256: current.rawExcerpt.sha256,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
migrated += 1;
|
|
317
|
+
}
|
|
318
|
+
const expiredEvents = await expireCapturedRawEvents({
|
|
319
|
+
homeDir: input.homeDir,
|
|
320
|
+
projectKey: input.projectKey,
|
|
321
|
+
now,
|
|
322
|
+
apply: true,
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
migrated,
|
|
326
|
+
expiredCapturedEventPayloads: expiredEvents.payloads,
|
|
327
|
+
expiredCapturedEventRawBytes: expiredEvents.rawBytes,
|
|
328
|
+
purge: await purgeExpiredSessionEvidence({
|
|
329
|
+
homeDir: input.homeDir,
|
|
330
|
+
projectKey: input.projectKey,
|
|
331
|
+
now,
|
|
332
|
+
limit: Math.max(DEFAULT_RETENTION_LIMIT, segments.length),
|
|
333
|
+
}),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function emptyInspection(): SessionEvidenceRetentionInspection {
|
|
338
|
+
return {
|
|
339
|
+
available: 0,
|
|
340
|
+
purged: 0,
|
|
341
|
+
overdue: 0,
|
|
342
|
+
purgeEligible: 0,
|
|
343
|
+
protected: 0,
|
|
344
|
+
legacyWithoutRetention: 0,
|
|
345
|
+
legacyOverdue: 0,
|
|
346
|
+
legacyRawBytes: 0,
|
|
347
|
+
legacyOverdueRawBytes: 0,
|
|
348
|
+
expiredCapturedEventPayloads: 0,
|
|
349
|
+
expiredCapturedEventRawBytes: 0,
|
|
350
|
+
protectionReasons: {},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function addProtection(
|
|
355
|
+
result: SessionEvidenceRetentionInspection,
|
|
356
|
+
reason: SessionEvidenceRetentionProtectionReason,
|
|
357
|
+
): void {
|
|
358
|
+
result.protected += 1;
|
|
359
|
+
result.protectionReasons[reason] = (result.protectionReasons[reason] ?? 0) + 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function normalizeLimit(value: number | undefined): number {
|
|
363
|
+
const limit = value ?? DEFAULT_RETENTION_LIMIT;
|
|
364
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RETENTION_LIMIT) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Session Evidence retention limit must be between 1 and ${MAX_RETENTION_LIMIT}.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return limit;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function hasValidAvailableRetention(segment: SessionEvidenceSegmentV1): boolean {
|
|
373
|
+
const retention = segment.retention;
|
|
374
|
+
return (
|
|
375
|
+
retention !== undefined &&
|
|
376
|
+
retention.rawState === "available" &&
|
|
377
|
+
retention.rawPurgedAt === null &&
|
|
378
|
+
Number.isFinite(Date.parse(retention.expiresAt)) &&
|
|
379
|
+
/^[a-f0-9]{64}$/u.test(retention.originalRawSha256) &&
|
|
380
|
+
segment.rawExcerpt.stored &&
|
|
381
|
+
segment.rawExcerpt.byteLength === Buffer.byteLength(segment.rawExcerpt.content, "utf8") &&
|
|
382
|
+
segment.rawExcerpt.sha256 === sha256Hex(segment.rawExcerpt.content) &&
|
|
383
|
+
retention.originalRawSha256 === segment.rawExcerpt.sha256
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function findRetentionProtection(input: {
|
|
388
|
+
homeDir: string;
|
|
389
|
+
segment: SessionEvidenceSegmentV1;
|
|
390
|
+
context: RetentionContext;
|
|
391
|
+
}): Promise<SessionEvidenceRetentionProtectionReason | null> {
|
|
392
|
+
if (
|
|
393
|
+
input.segment.lifecycle.reviewState === "unreviewed" ||
|
|
394
|
+
input.segment.lifecycle.reviewState === "deferred"
|
|
395
|
+
) {
|
|
396
|
+
return "segment-review-pending";
|
|
397
|
+
}
|
|
398
|
+
const trigger = await findSegmentTrigger(input.homeDir, input.segment, input.context);
|
|
399
|
+
if (trigger === null) {
|
|
400
|
+
if (input.segment.reason === "session-memory-threshold") return null;
|
|
401
|
+
return "trigger-missing";
|
|
402
|
+
}
|
|
403
|
+
if (!isTerminalTrigger(trigger)) return "trigger-not-terminal";
|
|
404
|
+
|
|
405
|
+
let snapshot = input.context.reviewSnapshots.get(input.segment.projectKey);
|
|
406
|
+
if (snapshot === undefined) {
|
|
407
|
+
snapshot = readEvolutionReviewSnapshot({
|
|
408
|
+
homeDir: input.homeDir,
|
|
409
|
+
projectKey: input.segment.projectKey,
|
|
410
|
+
});
|
|
411
|
+
input.context.reviewSnapshots.set(input.segment.projectKey, snapshot);
|
|
412
|
+
}
|
|
413
|
+
return hasPendingDerivedReview(await snapshot, input.segment) ? "derived-review-pending" : null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function findSegmentTrigger(
|
|
417
|
+
homeDir: string,
|
|
418
|
+
segment: SessionEvidenceSegmentV1,
|
|
419
|
+
context: RetentionContext,
|
|
420
|
+
): Promise<SegmentEvolutionTriggerRecord | null> {
|
|
421
|
+
let triggerMap = context.triggerMaps.get(segment.projectKey);
|
|
422
|
+
if (triggerMap === undefined) {
|
|
423
|
+
triggerMap = listSegmentEvolutionTriggers({ homeDir, projectKey: segment.projectKey }).then(
|
|
424
|
+
(triggers) => {
|
|
425
|
+
const map = new Map<string, SegmentEvolutionTriggerRecord[]>();
|
|
426
|
+
for (const trigger of triggers) {
|
|
427
|
+
const key = `${trigger.sessionKey}\0${trigger.segmentId}`;
|
|
428
|
+
map.set(key, [...(map.get(key) ?? []), trigger]);
|
|
429
|
+
}
|
|
430
|
+
return map;
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
context.triggerMaps.set(segment.projectKey, triggerMap);
|
|
434
|
+
}
|
|
435
|
+
const matches = (await triggerMap).get(`${segment.sessionKey}\0${segment.id}`) ?? [];
|
|
436
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isTerminalTrigger(trigger: SegmentEvolutionTriggerRecord): boolean {
|
|
440
|
+
return trigger.status === "consumed" || trigger.status === "skipped";
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function hasPendingDerivedReview(
|
|
444
|
+
snapshot: EvolutionReviewSnapshot,
|
|
445
|
+
segment: SessionEvidenceSegmentV1,
|
|
446
|
+
): boolean {
|
|
447
|
+
const runId = createStableId("segment-run", [segment.projectKey, segment.sessionKey, segment.id]);
|
|
448
|
+
if (
|
|
449
|
+
snapshot.knowledgeRecords.some(
|
|
450
|
+
(record) =>
|
|
451
|
+
record.provenance.runId === runId &&
|
|
452
|
+
!["auto-accepted", "accepted", "rejected", "deprecated", "superseded", "revoked"].includes(
|
|
453
|
+
record.reviewState,
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
snapshot.evosCases.some(
|
|
461
|
+
(record) =>
|
|
462
|
+
record.provenance.runId === runId &&
|
|
463
|
+
!["auto-accepted", "accepted", "rejected", "deprecated", "superseded", "revoked"].includes(
|
|
464
|
+
record.reviewState,
|
|
465
|
+
),
|
|
466
|
+
)
|
|
467
|
+
) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
if (
|
|
471
|
+
snapshot.repoProposals.some(
|
|
472
|
+
(proposal) =>
|
|
473
|
+
proposal.provenance.runId === runId &&
|
|
474
|
+
proposal.reviewState !== "rejected" &&
|
|
475
|
+
proposal.reviewState !== "applied" &&
|
|
476
|
+
proposal.reviewState !== "superseded",
|
|
477
|
+
)
|
|
478
|
+
) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return snapshot.reviewCandidates.some(
|
|
482
|
+
(candidate) =>
|
|
483
|
+
candidate.runId === runId &&
|
|
484
|
+
candidate.reviewState !== "accepted" &&
|
|
485
|
+
candidate.reviewState !== "rejected",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function createPurgedSegment(
|
|
490
|
+
segment: SessionEvidenceSegmentV1,
|
|
491
|
+
now: string,
|
|
492
|
+
): SessionEvidenceSegmentV1 {
|
|
493
|
+
const retention = segment.retention;
|
|
494
|
+
if (retention === undefined || retention.rawState !== "available") {
|
|
495
|
+
throw new Error("Session Evidence raw body is not available for purge.");
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
...segment,
|
|
499
|
+
retention: {
|
|
500
|
+
...retention,
|
|
501
|
+
rawState: "purged",
|
|
502
|
+
rawPurgedAt: now,
|
|
503
|
+
},
|
|
504
|
+
rawExcerpt: {
|
|
505
|
+
...segment.rawExcerpt,
|
|
506
|
+
stored: false,
|
|
507
|
+
content: "",
|
|
508
|
+
byteLength: 0,
|
|
509
|
+
sha256: sha256Hex(""),
|
|
510
|
+
},
|
|
511
|
+
privacy: {
|
|
512
|
+
...segment.privacy,
|
|
513
|
+
rawPromptStored: false,
|
|
514
|
+
rawOutputStored: false,
|
|
515
|
+
sourceContentStored: false,
|
|
516
|
+
},
|
|
517
|
+
lifecycle: {
|
|
518
|
+
...segment.lifecycle,
|
|
519
|
+
status: "raw-expired",
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function createRetentionContext(): RetentionContext {
|
|
525
|
+
return {
|
|
526
|
+
reviewSnapshots: new Map(),
|
|
527
|
+
triggerMaps: new Map(),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function loadLegacyRetentionDays(
|
|
532
|
+
homeDir: string,
|
|
533
|
+
projectKey?: string,
|
|
534
|
+
): Promise<Map<string, number>> {
|
|
535
|
+
const states = await listSessionMemoryStates({ homeDir, projectKey });
|
|
536
|
+
return new Map(
|
|
537
|
+
states.map((state) => [`${state.projectKey}\0${state.sessionKey}`, state.policy.retentionDays]),
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function legacyExpiresAt(
|
|
542
|
+
segment: SessionEvidenceSegmentV1,
|
|
543
|
+
retentionDays: Map<string, number>,
|
|
544
|
+
): string {
|
|
545
|
+
return addDays(
|
|
546
|
+
segment.createdAt,
|
|
547
|
+
retentionDays.get(sessionKey(segment)) ?? DEFAULT_LEGACY_RETENTION_DAYS,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function sessionKey(segment: SessionEvidenceSegmentV1): string {
|
|
552
|
+
return `${segment.projectKey}\0${segment.sessionKey}`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function addDays(value: string, days: number): string {
|
|
556
|
+
const timestamp = Date.parse(value);
|
|
557
|
+
if (!Number.isFinite(timestamp)) throw new Error("Invalid Session Evidence retention timestamp.");
|
|
558
|
+
return new Date(timestamp + days * 24 * 60 * 60 * 1000).toISOString();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function expireCapturedRawEvents(input: {
|
|
562
|
+
homeDir: string;
|
|
563
|
+
projectKey?: string;
|
|
564
|
+
now?: string | Date;
|
|
565
|
+
apply: boolean;
|
|
566
|
+
}): Promise<{ payloads: number; rawBytes: number }> {
|
|
567
|
+
const now = Date.parse(normalizeTimestamp(input.now));
|
|
568
|
+
const states = await listSessionMemoryStates({
|
|
569
|
+
homeDir: input.homeDir,
|
|
570
|
+
projectKey: input.projectKey,
|
|
571
|
+
});
|
|
572
|
+
let payloads = 0;
|
|
573
|
+
let rawBytes = 0;
|
|
574
|
+
for (const state of states) {
|
|
575
|
+
const stateUpdatedAt = Date.parse(state.updatedAt);
|
|
576
|
+
if (Number.isFinite(stateUpdatedAt) && now - stateUpdatedAt < 5 * 60 * 1000) continue;
|
|
577
|
+
const paths = resolveSessionMemoryPaths({
|
|
578
|
+
homeDir: input.homeDir,
|
|
579
|
+
projectKey: state.projectKey,
|
|
580
|
+
sessionKey: state.sessionKey,
|
|
581
|
+
});
|
|
582
|
+
const cursor = await readCursor(paths.cursorPath);
|
|
583
|
+
const capturedLine = cursor?.lastCapturedLine ?? 0;
|
|
584
|
+
if (capturedLine <= 0) continue;
|
|
585
|
+
let text: string;
|
|
586
|
+
try {
|
|
587
|
+
text = await readFile(paths.eventsPath, "utf8");
|
|
588
|
+
} catch {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
592
|
+
let changed = false;
|
|
593
|
+
let filePayloads = 0;
|
|
594
|
+
let fileRawBytes = 0;
|
|
595
|
+
const nextLines = lines.map((line, index) => {
|
|
596
|
+
if (index >= capturedLine) return line;
|
|
597
|
+
let event: SessionMemoryRawEventV1;
|
|
598
|
+
try {
|
|
599
|
+
event = JSON.parse(line) as SessionMemoryRawEventV1;
|
|
600
|
+
} catch {
|
|
601
|
+
return line;
|
|
602
|
+
}
|
|
603
|
+
const expiresAt =
|
|
604
|
+
Date.parse(event.receivedAt) + state.policy.retentionDays * 24 * 60 * 60 * 1000;
|
|
605
|
+
if (
|
|
606
|
+
!Number.isFinite(expiresAt) ||
|
|
607
|
+
expiresAt > now ||
|
|
608
|
+
event.rawPayloadExpired === true ||
|
|
609
|
+
event.rawPayloadJson === "{}"
|
|
610
|
+
) {
|
|
611
|
+
return line;
|
|
612
|
+
}
|
|
613
|
+
filePayloads += 1;
|
|
614
|
+
fileRawBytes += Buffer.byteLength(event.rawPayloadJson, "utf8");
|
|
615
|
+
if (!input.apply) return line;
|
|
616
|
+
changed = true;
|
|
617
|
+
return JSON.stringify({
|
|
618
|
+
...event,
|
|
619
|
+
rawPayloadJson: "{}",
|
|
620
|
+
rawPayloadTruncated: false,
|
|
621
|
+
rawPayloadExpired: true,
|
|
622
|
+
} satisfies SessionMemoryRawEventV1);
|
|
623
|
+
});
|
|
624
|
+
if (!input.apply) {
|
|
625
|
+
payloads += filePayloads;
|
|
626
|
+
rawBytes += fileRawBytes;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (changed && (await rewriteSessionRawEvents(paths.eventsPath, nextLines, text))) {
|
|
630
|
+
payloads += filePayloads;
|
|
631
|
+
rawBytes += fileRawBytes;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return { payloads, rawBytes };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function readCursor(path: string): Promise<{ lastCapturedLine: number | null } | null> {
|
|
638
|
+
try {
|
|
639
|
+
return JSON.parse(await readFile(path, "utf8")) as { lastCapturedLine: number | null };
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|