@evo-dev/core 0.0.1-alpha.11 → 0.0.1-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +9229 -6005
- package/package.json +1 -1
- package/src/evolution/candidates/index.ts +116 -0
- package/src/evolution/evidence/session-memory/analysis.ts +7 -1
- package/src/evolution/evidence/session-memory/index.ts +2 -0
- package/src/evolution/evidence/session-memory/retention.ts +361 -0
- package/src/evolution/evidence/session-memory/semantic-packet.ts +408 -0
- package/src/evolution/evidence/session-memory/storage.ts +336 -2
- package/src/evolution/evidence/session-memory/types.ts +75 -3
- 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 +3 -0
- package/src/evolution/schema.ts +44 -1
- package/src/evolution/shared.ts +187 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStableId,
|
|
3
|
+
sanitizeStorageId,
|
|
4
|
+
sanitizeSummary,
|
|
5
|
+
sha256Hex,
|
|
6
|
+
} from "../../utils/index.ts";
|
|
7
|
+
import {
|
|
8
|
+
type HistoricalImportStoredRecordV1,
|
|
9
|
+
type SessionEvidenceSegmentV1,
|
|
10
|
+
type SessionMemoryPolicySnapshot,
|
|
11
|
+
type SessionMemorySensitivity,
|
|
12
|
+
type SessionMemorySensitivityReason,
|
|
13
|
+
type SessionMemorySignal,
|
|
14
|
+
detectSessionMemorySensitivity,
|
|
15
|
+
redactSessionMemoryCredentials,
|
|
16
|
+
} from "../evidence/session-memory/index.ts";
|
|
17
|
+
import {
|
|
18
|
+
createTrajectoryImportConfigHash,
|
|
19
|
+
createTrajectoryImportPreviewId,
|
|
20
|
+
createTrajectoryImportStableRecordKeyHash,
|
|
21
|
+
} from "./diff.ts";
|
|
22
|
+
import type {
|
|
23
|
+
CanonicalImportRecordType,
|
|
24
|
+
CanonicalImportSnapshot,
|
|
25
|
+
TrajectoryImportPreviewBuildResult,
|
|
26
|
+
TrajectoryImportPreviewV1,
|
|
27
|
+
TrajectoryImportPreviousSnapshotV1,
|
|
28
|
+
TrajectoryImportSource,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
|
|
31
|
+
export const TRAJECTORY_IMPORT_MATERIALIZER_VERSION = 1 as const;
|
|
32
|
+
const MIN_HISTORICAL_SEGMENT_BYTES = 1_024;
|
|
33
|
+
const MAX_HISTORICAL_SIGNALS = 120;
|
|
34
|
+
|
|
35
|
+
interface PreparedCanonicalRecord {
|
|
36
|
+
recordKeyHash: string;
|
|
37
|
+
recordType: CanonicalImportRecordType;
|
|
38
|
+
canonicalJson: string;
|
|
39
|
+
canonicalValue: unknown;
|
|
40
|
+
credentialRedacted: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PreparedHistoricalImportSegment {
|
|
44
|
+
id: string;
|
|
45
|
+
firstRecordKeyHash: string;
|
|
46
|
+
lastRecordKeyHash: string;
|
|
47
|
+
recordCount: number;
|
|
48
|
+
rawExcerpt: {
|
|
49
|
+
content: string;
|
|
50
|
+
byteLength: number;
|
|
51
|
+
sha256: string;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
};
|
|
54
|
+
normalized: SessionEvidenceSegmentV1["normalized"];
|
|
55
|
+
signals: SessionMemorySignal[];
|
|
56
|
+
sensitivity: SessionMemorySensitivity;
|
|
57
|
+
sensitivityReasons: SessionMemorySensitivityReason[];
|
|
58
|
+
redactionApplied: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TrajectoryImportMaterializationPlan {
|
|
62
|
+
preview: TrajectoryImportPreviewV1;
|
|
63
|
+
comparisonSnapshot: TrajectoryImportPreviousSnapshotV1;
|
|
64
|
+
policy: SessionMemoryPolicySnapshot;
|
|
65
|
+
preparedSegments: PreparedHistoricalImportSegment[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CreateTrajectoryImportMaterializationPlanInput {
|
|
69
|
+
snapshot: CanonicalImportSnapshot;
|
|
70
|
+
previewBuild: TrajectoryImportPreviewBuildResult;
|
|
71
|
+
previous?: TrajectoryImportPreviousSnapshotV1 | null;
|
|
72
|
+
policy: SessionMemoryPolicySnapshot;
|
|
73
|
+
automation: {
|
|
74
|
+
knowledge: boolean;
|
|
75
|
+
semanticKnowledge: boolean;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createTrajectoryImportMaterializationPlan(
|
|
80
|
+
input: CreateTrajectoryImportMaterializationPlanInput,
|
|
81
|
+
): TrajectoryImportMaterializationPlan {
|
|
82
|
+
const blockers = new Set(input.previewBuild.preview.blockers);
|
|
83
|
+
if (!input.policy.enabled) blockers.add("session-memory-disabled");
|
|
84
|
+
if (!input.policy.storeRawSegments) blockers.add("session-memory-raw-storage-disabled");
|
|
85
|
+
if (input.policy.maxRawSegmentBytes < MIN_HISTORICAL_SEGMENT_BYTES) {
|
|
86
|
+
blockers.add("materializer-byte-budget-too-small");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prepared =
|
|
90
|
+
blockers.size === 0
|
|
91
|
+
? prepareHistoricalSegments({
|
|
92
|
+
snapshot: input.snapshot,
|
|
93
|
+
comparisonSnapshot: input.previewBuild.comparisonSnapshot,
|
|
94
|
+
previous: input.previous ?? null,
|
|
95
|
+
maxSegmentBytes: input.policy.maxRawSegmentBytes,
|
|
96
|
+
blockers,
|
|
97
|
+
})
|
|
98
|
+
: { segments: [], selectedRecords: 0, credentialRedactions: 0 };
|
|
99
|
+
const automationEligible = input.automation.knowledge && input.automation.semanticKnowledge;
|
|
100
|
+
const { id: _basePreviewId, ...base } = input.previewBuild.preview;
|
|
101
|
+
const previewWithoutId: Omit<TrajectoryImportPreviewV1, "id"> = {
|
|
102
|
+
...base,
|
|
103
|
+
contracts: {
|
|
104
|
+
...base.contracts,
|
|
105
|
+
materializerVersion: TRAJECTORY_IMPORT_MATERIALIZER_VERSION,
|
|
106
|
+
materializerConfigHash: createTrajectoryImportConfigHash({
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
enabled: input.policy.enabled,
|
|
109
|
+
storeRawSegments: input.policy.storeRawSegments,
|
|
110
|
+
maxRawSegmentBytes: input.policy.maxRawSegmentBytes,
|
|
111
|
+
retentionDays: input.policy.retentionDays,
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
materialization: {
|
|
115
|
+
selectedRecords: prepared.selectedRecords,
|
|
116
|
+
estimatedSegments: prepared.segments.length,
|
|
117
|
+
truncatedSegments: prepared.segments.filter((segment) => segment.rawExcerpt.truncated).length,
|
|
118
|
+
credentialRedactions: prepared.credentialRedactions,
|
|
119
|
+
estimatedTriggers: prepared.segments.length,
|
|
120
|
+
retentionDays: input.policy.retentionDays,
|
|
121
|
+
},
|
|
122
|
+
downstream: {
|
|
123
|
+
semanticProcessing: automationEligible ? "automation-eligible" : "queue-only",
|
|
124
|
+
automationKnowledge: input.automation.knowledge,
|
|
125
|
+
automationSemanticKnowledge: input.automation.semanticKnowledge,
|
|
126
|
+
mayInvokeConfiguredProviderAfterApply: automationEligible,
|
|
127
|
+
},
|
|
128
|
+
blockers: [...blockers].sort(),
|
|
129
|
+
canApply: base.projectKey !== null && blockers.size === 0,
|
|
130
|
+
};
|
|
131
|
+
const { schemaVersion, kind, ...previewFields } = previewWithoutId;
|
|
132
|
+
const preview: TrajectoryImportPreviewV1 = {
|
|
133
|
+
schemaVersion,
|
|
134
|
+
kind,
|
|
135
|
+
id: createTrajectoryImportPreviewId(previewWithoutId),
|
|
136
|
+
...previewFields,
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
preview,
|
|
140
|
+
comparisonSnapshot: input.previewBuild.comparisonSnapshot,
|
|
141
|
+
policy: input.policy,
|
|
142
|
+
preparedSegments: prepared.segments,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createHistoricalSessionEvidenceSegments(input: {
|
|
147
|
+
plan: TrajectoryImportMaterializationPlan;
|
|
148
|
+
createdAt: string;
|
|
149
|
+
}): SessionEvidenceSegmentV1[] {
|
|
150
|
+
const projectKey = input.plan.preview.projectKey;
|
|
151
|
+
if (projectKey === null) {
|
|
152
|
+
throw new Error("Historical materialization requires a projectKey.");
|
|
153
|
+
}
|
|
154
|
+
const createdAt = normalizeIsoTimestamp(input.createdAt);
|
|
155
|
+
const expiresAt = addDays(createdAt, input.plan.policy.retentionDays);
|
|
156
|
+
const sessionKey = `historical-${input.plan.preview.sourceKey}`;
|
|
157
|
+
const target = sourceTarget(input.plan.preview.source);
|
|
158
|
+
return input.plan.preparedSegments.map((prepared) => ({
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
kind: "session-evidence-segment",
|
|
161
|
+
id: prepared.id,
|
|
162
|
+
projectKey,
|
|
163
|
+
runId: null,
|
|
164
|
+
roleId: input.plan.preview.scope.roleId,
|
|
165
|
+
sessionKey,
|
|
166
|
+
target,
|
|
167
|
+
createdAt,
|
|
168
|
+
reason: "historical-import",
|
|
169
|
+
strength: "normal",
|
|
170
|
+
origin: {
|
|
171
|
+
kind: "historical-import",
|
|
172
|
+
importId: input.plan.preview.id,
|
|
173
|
+
generationId: input.plan.preview.generationId,
|
|
174
|
+
snapshotId: input.plan.preview.snapshotId,
|
|
175
|
+
sourceKey: input.plan.preview.sourceKey,
|
|
176
|
+
firstRecordKeyHash: prepared.firstRecordKeyHash,
|
|
177
|
+
lastRecordKeyHash: prepared.lastRecordKeyHash,
|
|
178
|
+
recordCount: prepared.recordCount,
|
|
179
|
+
},
|
|
180
|
+
retention: {
|
|
181
|
+
policyDays: input.plan.policy.retentionDays,
|
|
182
|
+
expiresAt,
|
|
183
|
+
rawState: "available",
|
|
184
|
+
rawPurgedAt: null,
|
|
185
|
+
originalRawSha256: prepared.rawExcerpt.sha256,
|
|
186
|
+
},
|
|
187
|
+
source: {
|
|
188
|
+
traceRefId: null,
|
|
189
|
+
sourcePath: null,
|
|
190
|
+
fromOffset: null,
|
|
191
|
+
toOffset: null,
|
|
192
|
+
fromLine: null,
|
|
193
|
+
toLine: null,
|
|
194
|
+
fromEventId: null,
|
|
195
|
+
toEventId: null,
|
|
196
|
+
},
|
|
197
|
+
signals: prepared.signals,
|
|
198
|
+
rawExcerpt: {
|
|
199
|
+
stored: true,
|
|
200
|
+
encoding: "utf8",
|
|
201
|
+
content: prepared.rawExcerpt.content,
|
|
202
|
+
truncated: prepared.rawExcerpt.truncated,
|
|
203
|
+
byteLength: prepared.rawExcerpt.byteLength,
|
|
204
|
+
sha256: prepared.rawExcerpt.sha256,
|
|
205
|
+
},
|
|
206
|
+
normalized: prepared.normalized,
|
|
207
|
+
privacy: {
|
|
208
|
+
localOnly: true,
|
|
209
|
+
rawPromptStored: prepared.signals.some((signal) => signal.eventType === "UserPromptSubmit"),
|
|
210
|
+
rawOutputStored: prepared.signals.some((signal) =>
|
|
211
|
+
["PostToolUse", "PostToolUseFailure"].includes(signal.eventType),
|
|
212
|
+
),
|
|
213
|
+
sourceContentStored: true,
|
|
214
|
+
secretsDetected: false,
|
|
215
|
+
sensitivity: prepared.sensitivity,
|
|
216
|
+
sensitivityReasons: prepared.sensitivityReasons,
|
|
217
|
+
redactionApplied: prepared.redactionApplied,
|
|
218
|
+
externalUploadAllowed: false,
|
|
219
|
+
},
|
|
220
|
+
lifecycle: {
|
|
221
|
+
status: "captured",
|
|
222
|
+
reviewState: "not-required",
|
|
223
|
+
consumedByBatchIds: [],
|
|
224
|
+
},
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function prepareHistoricalSegments(input: {
|
|
229
|
+
snapshot: CanonicalImportSnapshot;
|
|
230
|
+
comparisonSnapshot: TrajectoryImportPreviousSnapshotV1;
|
|
231
|
+
previous: TrajectoryImportPreviousSnapshotV1 | null;
|
|
232
|
+
maxSegmentBytes: number;
|
|
233
|
+
blockers: Set<string>;
|
|
234
|
+
}): {
|
|
235
|
+
segments: PreparedHistoricalImportSegment[];
|
|
236
|
+
selectedRecords: number;
|
|
237
|
+
credentialRedactions: number;
|
|
238
|
+
} {
|
|
239
|
+
if (input.snapshot.groups.length !== 1) {
|
|
240
|
+
input.blockers.add("materializer-source-group-mismatch");
|
|
241
|
+
return { segments: [], selectedRecords: 0, credentialRedactions: 0 };
|
|
242
|
+
}
|
|
243
|
+
const byKey = new Map<string, PreparedCanonicalRecord>();
|
|
244
|
+
for (const record of input.snapshot.groups[0]?.records ?? []) {
|
|
245
|
+
const recordKeyHash = createTrajectoryImportStableRecordKeyHash(
|
|
246
|
+
input.comparisonSnapshot.sourceKey,
|
|
247
|
+
record.recordId,
|
|
248
|
+
);
|
|
249
|
+
let canonicalValue: unknown;
|
|
250
|
+
try {
|
|
251
|
+
canonicalValue = JSON.parse(record.recordJson) as unknown;
|
|
252
|
+
} catch {
|
|
253
|
+
input.blockers.add("materializer-record-json-invalid");
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const redacted = redactSessionMemoryCredentials(canonicalValue);
|
|
257
|
+
const canonicalJson = JSON.stringify(redacted.value);
|
|
258
|
+
if (detectSessionMemorySensitivity(canonicalJson).classification === "credential") {
|
|
259
|
+
input.blockers.add("materializer-credential-remains");
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
byKey.set(recordKeyHash, {
|
|
263
|
+
recordKeyHash,
|
|
264
|
+
recordType: record.recordType,
|
|
265
|
+
canonicalJson,
|
|
266
|
+
canonicalValue: redacted.value,
|
|
267
|
+
credentialRedacted: redacted.redacted,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const ordered: PreparedCanonicalRecord[] = [];
|
|
272
|
+
for (const indexed of input.comparisonSnapshot.records) {
|
|
273
|
+
const record = byKey.get(indexed.stableRecordKeyHash);
|
|
274
|
+
if (record === undefined || record.recordType !== indexed.recordType) {
|
|
275
|
+
input.blockers.add("materializer-record-index-mismatch");
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
ordered.push(record);
|
|
279
|
+
}
|
|
280
|
+
if (input.blockers.size > 0) {
|
|
281
|
+
return { segments: [], selectedRecords: 0, credentialRedactions: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const previousKeys = new Set(
|
|
285
|
+
(input.previous?.records ?? []).map((record) => record.stableRecordKeyHash),
|
|
286
|
+
);
|
|
287
|
+
const newIndexes = ordered.flatMap((record, index) =>
|
|
288
|
+
previousKeys.has(record.recordKeyHash) ? [] : [index],
|
|
289
|
+
);
|
|
290
|
+
const windows = mergeWindows(
|
|
291
|
+
contiguousRuns(newIndexes).map(([start, end]) => expandConversationWindow(ordered, start, end)),
|
|
292
|
+
);
|
|
293
|
+
const segments: PreparedHistoricalImportSegment[] = [];
|
|
294
|
+
let selectedRecords = 0;
|
|
295
|
+
let credentialRedactions = 0;
|
|
296
|
+
|
|
297
|
+
for (const [start, end] of windows) {
|
|
298
|
+
const selected = ordered.slice(start, end + 1).filter((record) => record.recordType !== "meta");
|
|
299
|
+
const chunks = splitStoredRecords(selected, input.maxSegmentBytes, input.blockers);
|
|
300
|
+
for (const chunk of chunks) {
|
|
301
|
+
if (chunk.records.length === 0) continue;
|
|
302
|
+
const rawContent = chunk.lines.join("\n");
|
|
303
|
+
const first = chunk.records[0];
|
|
304
|
+
const last = chunk.records.at(-1);
|
|
305
|
+
if (first === undefined || last === undefined) continue;
|
|
306
|
+
const rawSha256 = sha256Hex(rawContent);
|
|
307
|
+
const id = createStableId("historical-segment", [
|
|
308
|
+
input.comparisonSnapshot.sourceKey,
|
|
309
|
+
input.comparisonSnapshot.generationId,
|
|
310
|
+
input.comparisonSnapshot.snapshotId,
|
|
311
|
+
first.recordKeyHash,
|
|
312
|
+
last.recordKeyHash,
|
|
313
|
+
rawSha256,
|
|
314
|
+
]);
|
|
315
|
+
const sensitivity = detectSessionMemorySensitivity(rawContent);
|
|
316
|
+
if (sensitivity.classification === "credential") {
|
|
317
|
+
input.blockers.add("materializer-credential-remains");
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const derived = deriveHistoricalMetadata(chunk.records, id);
|
|
321
|
+
segments.push({
|
|
322
|
+
id,
|
|
323
|
+
firstRecordKeyHash: first.recordKeyHash,
|
|
324
|
+
lastRecordKeyHash: last.recordKeyHash,
|
|
325
|
+
recordCount: chunk.records.length,
|
|
326
|
+
rawExcerpt: {
|
|
327
|
+
content: rawContent,
|
|
328
|
+
byteLength: Buffer.byteLength(rawContent, "utf8"),
|
|
329
|
+
sha256: rawSha256,
|
|
330
|
+
truncated: chunk.truncated,
|
|
331
|
+
},
|
|
332
|
+
normalized: derived.normalized,
|
|
333
|
+
signals: derived.signals,
|
|
334
|
+
sensitivity: sensitivity.classification,
|
|
335
|
+
sensitivityReasons: sensitivity.reasons,
|
|
336
|
+
redactionApplied: chunk.records.some((record) => record.credentialRedacted),
|
|
337
|
+
});
|
|
338
|
+
selectedRecords += chunk.records.length;
|
|
339
|
+
credentialRedactions += chunk.records.filter((record) => record.credentialRedacted).length;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (input.blockers.size > 0) {
|
|
344
|
+
return { segments: [], selectedRecords: 0, credentialRedactions: 0 };
|
|
345
|
+
}
|
|
346
|
+
return { segments, selectedRecords, credentialRedactions };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function contiguousRuns(indexes: number[]): Array<[number, number]> {
|
|
350
|
+
const runs: Array<[number, number]> = [];
|
|
351
|
+
for (const index of indexes) {
|
|
352
|
+
const last = runs.at(-1);
|
|
353
|
+
if (last !== undefined && index === last[1] + 1) last[1] = index;
|
|
354
|
+
else runs.push([index, index]);
|
|
355
|
+
}
|
|
356
|
+
return runs;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function expandConversationWindow(
|
|
360
|
+
records: PreparedCanonicalRecord[],
|
|
361
|
+
start: number,
|
|
362
|
+
end: number,
|
|
363
|
+
): [number, number] {
|
|
364
|
+
let expandedStart = start;
|
|
365
|
+
for (let index = start; index >= 0; index -= 1) {
|
|
366
|
+
if (records[index]?.recordType === "user") {
|
|
367
|
+
expandedStart = index;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
let expandedEnd = end;
|
|
372
|
+
for (let index = end; index < records.length; index += 1) {
|
|
373
|
+
if (records[index]?.recordType === "assistant") {
|
|
374
|
+
expandedEnd = index;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return [expandedStart, expandedEnd];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function mergeWindows(windows: Array<[number, number]>): Array<[number, number]> {
|
|
382
|
+
const merged: Array<[number, number]> = [];
|
|
383
|
+
for (const window of windows) {
|
|
384
|
+
const last = merged.at(-1);
|
|
385
|
+
if (last !== undefined && window[0] <= last[1] + 1) {
|
|
386
|
+
last[1] = Math.max(last[1], window[1]);
|
|
387
|
+
} else {
|
|
388
|
+
merged.push([...window]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return merged;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function splitStoredRecords(
|
|
395
|
+
records: PreparedCanonicalRecord[],
|
|
396
|
+
maxBytes: number,
|
|
397
|
+
blockers: Set<string>,
|
|
398
|
+
): Array<{
|
|
399
|
+
records: PreparedCanonicalRecord[];
|
|
400
|
+
lines: string[];
|
|
401
|
+
truncated: boolean;
|
|
402
|
+
}> {
|
|
403
|
+
const chunks: Array<{
|
|
404
|
+
records: PreparedCanonicalRecord[];
|
|
405
|
+
lines: string[];
|
|
406
|
+
truncated: boolean;
|
|
407
|
+
}> = [];
|
|
408
|
+
let current = {
|
|
409
|
+
records: [] as PreparedCanonicalRecord[],
|
|
410
|
+
lines: [] as string[],
|
|
411
|
+
truncated: false,
|
|
412
|
+
};
|
|
413
|
+
for (const record of records) {
|
|
414
|
+
const fitted = fitStoredRecord(record, maxBytes);
|
|
415
|
+
if (fitted === null) {
|
|
416
|
+
blockers.add("materializer-byte-budget-too-small");
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
const separatorBytes = current.lines.length === 0 ? 0 : 1;
|
|
420
|
+
const currentBytes = Buffer.byteLength(current.lines.join("\n"), "utf8");
|
|
421
|
+
const lineBytes = Buffer.byteLength(fitted.line, "utf8");
|
|
422
|
+
if (current.lines.length > 0 && currentBytes + separatorBytes + lineBytes > maxBytes) {
|
|
423
|
+
chunks.push(current);
|
|
424
|
+
current = { records: [], lines: [], truncated: false };
|
|
425
|
+
}
|
|
426
|
+
current.records.push(record);
|
|
427
|
+
current.lines.push(fitted.line);
|
|
428
|
+
current.truncated ||= fitted.truncated;
|
|
429
|
+
}
|
|
430
|
+
if (current.lines.length > 0) chunks.push(current);
|
|
431
|
+
return chunks;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function fitStoredRecord(
|
|
435
|
+
record: PreparedCanonicalRecord,
|
|
436
|
+
maxBytes: number,
|
|
437
|
+
): { line: string; truncated: boolean } | null {
|
|
438
|
+
const recordType = record.recordType;
|
|
439
|
+
if (recordType === "meta") {
|
|
440
|
+
throw new Error("Historical materializer cannot persist meta records.");
|
|
441
|
+
}
|
|
442
|
+
const createLine = (canonicalJson: string, truncated: boolean): string =>
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
schemaVersion: 1,
|
|
445
|
+
kind: "historical-import-record",
|
|
446
|
+
recordKeyHash: record.recordKeyHash,
|
|
447
|
+
recordType,
|
|
448
|
+
canonicalJson,
|
|
449
|
+
canonicalJsonTruncated: truncated,
|
|
450
|
+
} satisfies HistoricalImportStoredRecordV1);
|
|
451
|
+
const complete = createLine(record.canonicalJson, false);
|
|
452
|
+
if (Buffer.byteLength(complete, "utf8") <= maxBytes) {
|
|
453
|
+
return { line: complete, truncated: false };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let low = 0;
|
|
457
|
+
let high = record.canonicalJson.length;
|
|
458
|
+
let best: string | null = null;
|
|
459
|
+
while (low <= high) {
|
|
460
|
+
const middle = Math.floor((low + high) / 2);
|
|
461
|
+
const excerpt = `${record.canonicalJson.slice(0, middle)}…`;
|
|
462
|
+
const line = createLine(excerpt, true);
|
|
463
|
+
if (Buffer.byteLength(line, "utf8") <= maxBytes) {
|
|
464
|
+
best = line;
|
|
465
|
+
low = middle + 1;
|
|
466
|
+
} else {
|
|
467
|
+
high = middle - 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return best === null ? null : { line: best, truncated: true };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function deriveHistoricalMetadata(
|
|
474
|
+
records: PreparedCanonicalRecord[],
|
|
475
|
+
segmentId: string,
|
|
476
|
+
): {
|
|
477
|
+
normalized: SessionEvidenceSegmentV1["normalized"];
|
|
478
|
+
signals: SessionMemorySignal[];
|
|
479
|
+
} {
|
|
480
|
+
const userTexts: string[] = [];
|
|
481
|
+
const failures: string[] = [];
|
|
482
|
+
const verifications: string[] = [];
|
|
483
|
+
const touchedTools = new Set<string>();
|
|
484
|
+
const signals: SessionMemorySignal[] = [];
|
|
485
|
+
|
|
486
|
+
for (const [index, record] of records.entries()) {
|
|
487
|
+
const parsed = asRecord(record.canonicalValue);
|
|
488
|
+
const content = extractCanonicalContent(parsed);
|
|
489
|
+
if (record.recordType === "user" && content !== null) userTexts.push(content);
|
|
490
|
+
for (const tool of extractToolNames(parsed)) touchedTools.add(tool);
|
|
491
|
+
if (isFailureRecord(record, parsed, content)) {
|
|
492
|
+
failures.push(sanitizeSummary(content ?? "Historical tool failure observed."));
|
|
493
|
+
}
|
|
494
|
+
if (isVerificationRecord(record, parsed, content)) {
|
|
495
|
+
verifications.push(sanitizeSummary(content ?? "Historical verification observed."));
|
|
496
|
+
}
|
|
497
|
+
signals.push({
|
|
498
|
+
eventId: createStableId("historical-event", [segmentId, record.recordKeyHash]),
|
|
499
|
+
eventType: historicalEventType(record, parsed, content),
|
|
500
|
+
reason: "observed",
|
|
501
|
+
strength: "normal",
|
|
502
|
+
occurredAt: readTimestamp(parsed),
|
|
503
|
+
summary: historicalSignalSummary(record, parsed, content),
|
|
504
|
+
rawEventLine: index + 1,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const boundedSignals = signals.slice(0, MAX_HISTORICAL_SIGNALS);
|
|
508
|
+
if (boundedSignals.length === 0) {
|
|
509
|
+
boundedSignals.push({
|
|
510
|
+
eventId: createStableId("historical-event", [segmentId, "summary"]),
|
|
511
|
+
eventType: "unknown",
|
|
512
|
+
reason: "historical-import",
|
|
513
|
+
strength: "normal",
|
|
514
|
+
occurredAt: null,
|
|
515
|
+
summary: "Historical transcript evidence imported.",
|
|
516
|
+
rawEventLine: 0,
|
|
517
|
+
});
|
|
518
|
+
} else {
|
|
519
|
+
const last = boundedSignals.at(-1);
|
|
520
|
+
if (last !== undefined) last.reason = "historical-import";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
normalized: {
|
|
525
|
+
summary: sanitizeSummary(
|
|
526
|
+
`Historical import segment contains ${records.length} bounded canonical record(s).`,
|
|
527
|
+
),
|
|
528
|
+
userIntent: userTexts[0] === undefined ? null : sanitizeSummary(userTexts[0]),
|
|
529
|
+
intentDelta:
|
|
530
|
+
userTexts.length < 2 ? null : sanitizeSummary(userTexts.at(-1) ?? userTexts[0] ?? ""),
|
|
531
|
+
decisions: [],
|
|
532
|
+
failures: failures.slice(0, 8),
|
|
533
|
+
verifications: verifications.slice(0, 8),
|
|
534
|
+
touchedTools: [...touchedTools].slice(0, 20),
|
|
535
|
+
},
|
|
536
|
+
signals: boundedSignals,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function historicalEventType(
|
|
541
|
+
record: PreparedCanonicalRecord,
|
|
542
|
+
parsed: Record<string, unknown> | null,
|
|
543
|
+
content: string | null,
|
|
544
|
+
): string {
|
|
545
|
+
if (record.recordType === "user") return "UserPromptSubmit";
|
|
546
|
+
if (record.recordType === "assistant") return "Stop";
|
|
547
|
+
if (record.recordType === "assistant-tool-call") return "PreToolUse";
|
|
548
|
+
if (record.recordType === "tool") {
|
|
549
|
+
return isFailureRecord(record, parsed, content) ? "PostToolUseFailure" : "PostToolUse";
|
|
550
|
+
}
|
|
551
|
+
return "unknown";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function historicalSignalSummary(
|
|
555
|
+
record: PreparedCanonicalRecord,
|
|
556
|
+
parsed: Record<string, unknown> | null,
|
|
557
|
+
content: string | null,
|
|
558
|
+
): string {
|
|
559
|
+
const tools = extractToolNames(parsed);
|
|
560
|
+
if (tools.length > 0) return sanitizeSummary(`Historical tool evidence: ${tools.join(", ")}.`);
|
|
561
|
+
if (content !== null) return sanitizeSummary(content);
|
|
562
|
+
return sanitizeSummary(`Historical ${record.recordType} record observed.`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function isFailureRecord(
|
|
566
|
+
record: PreparedCanonicalRecord,
|
|
567
|
+
parsed: Record<string, unknown> | null,
|
|
568
|
+
content: string | null,
|
|
569
|
+
): boolean {
|
|
570
|
+
if (record.recordType !== "tool") return false;
|
|
571
|
+
const exitCode = readExitCode(parsed);
|
|
572
|
+
return (
|
|
573
|
+
(exitCode !== null && exitCode !== 0) ||
|
|
574
|
+
/\b(?:fail(?:ed|ure)?|error|denied|exception|non-zero)\b/iu.test(content ?? "")
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function isVerificationRecord(
|
|
579
|
+
record: PreparedCanonicalRecord,
|
|
580
|
+
parsed: Record<string, unknown> | null,
|
|
581
|
+
content: string | null,
|
|
582
|
+
): boolean {
|
|
583
|
+
const comparable = `${extractToolNames(parsed).join(" ")} ${content ?? ""}`;
|
|
584
|
+
return (
|
|
585
|
+
(record.recordType === "assistant-tool-call" || record.recordType === "tool") &&
|
|
586
|
+
/\b(?:test|lint|typecheck|build|verify|check)\b/iu.test(comparable)
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function extractToolNames(parsed: Record<string, unknown> | null): string[] {
|
|
591
|
+
if (parsed === null || !Array.isArray(parsed.tool_calls)) return [];
|
|
592
|
+
return parsed.tool_calls.flatMap((value) => {
|
|
593
|
+
const record = asRecord(value);
|
|
594
|
+
if (record === null || typeof record.name !== "string" || record.name.trim() === "") return [];
|
|
595
|
+
return [sanitizeStorageId(record.name, "tool")];
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function extractCanonicalContent(parsed: Record<string, unknown> | null): string | null {
|
|
600
|
+
if (parsed === null) return null;
|
|
601
|
+
if (typeof parsed.content === "string" && parsed.content.trim() !== "") return parsed.content;
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function readTimestamp(parsed: Record<string, unknown> | null): string | null {
|
|
606
|
+
if (parsed === null || typeof parsed.timestamp !== "string") return null;
|
|
607
|
+
return Number.isFinite(Date.parse(parsed.timestamp)) ? parsed.timestamp : null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function readExitCode(parsed: Record<string, unknown> | null): number | null {
|
|
611
|
+
if (parsed === null) return null;
|
|
612
|
+
for (const field of ["exit_code", "exitCode"]) {
|
|
613
|
+
const value = parsed[field];
|
|
614
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
620
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
621
|
+
? (value as Record<string, unknown>)
|
|
622
|
+
: null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function sourceTarget(source: TrajectoryImportSource): "claude" | "codex" | "unknown" {
|
|
626
|
+
if (source === "claude-code") return "claude";
|
|
627
|
+
if (source === "codex") return "codex";
|
|
628
|
+
return "unknown";
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function normalizeIsoTimestamp(value: string): string {
|
|
632
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
633
|
+
throw new Error("Historical materialization requires an ISO timestamp.");
|
|
634
|
+
}
|
|
635
|
+
return new Date(value).toISOString();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function addDays(value: string, days: number): string {
|
|
639
|
+
return new Date(Date.parse(value) + days * 24 * 60 * 60 * 1000).toISOString();
|
|
640
|
+
}
|