@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.
@@ -1,7 +1,20 @@
1
- import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
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";
2
15
  import { dirname, join } from "node:path";
3
16
  import { resolveEvoDevPaths } from "../../../config/paths.ts";
4
- import { isNotFoundError } from "../../../utils/index.ts";
17
+ import { isFileExistsError, isNotFoundError, sha256Hex } from "../../../utils/index.ts";
5
18
  import { resolveSessionMemoryPaths } from "./paths.ts";
6
19
  import { detectSessionMemorySensitivity, mergeSessionMemorySensitivity } from "./sensitivity.ts";
7
20
  import type {
@@ -268,11 +281,332 @@ export async function writeSessionIndex(
268
281
  });
269
282
  }
270
283
 
284
+ export async function writeHistoricalSessionEvidenceSegment(input: {
285
+ homeDir: string;
286
+ paths: SessionMemoryPaths;
287
+ segment: SessionEvidenceSegmentV1;
288
+ }): Promise<{ created: boolean; segment: SessionEvidenceSegmentV1 }> {
289
+ if (input.segment.origin?.kind !== "historical-import" || input.segment.retention === undefined) {
290
+ throw new Error("Historical Session Evidence requires origin and retention metadata.");
291
+ }
292
+ assertSessionMemoryPathsMatchSegment(input.homeDir, input.paths, input.segment);
293
+ await ensurePrivateSessionMemoryDirectory(input.paths.segmentsDir);
294
+ const path = input.paths.segmentPath(input.segment.id);
295
+ const created = await writePrivateImmutableText(
296
+ path,
297
+ `${JSON.stringify(input.segment, null, 2)}\n`,
298
+ );
299
+ if (created) {
300
+ return { created: true, segment: input.segment };
301
+ }
302
+ const existingInfo = await lstat(path);
303
+ if (!existingInfo.isFile() || existingInfo.isSymbolicLink()) {
304
+ throw new Error("Historical Session Evidence path is not a regular state file.");
305
+ }
306
+ const existing = await readSessionEvidenceSegment({
307
+ homeDir: input.homeDir,
308
+ projectKey: input.segment.projectKey,
309
+ sessionKey: input.segment.sessionKey,
310
+ segmentId: input.segment.id,
311
+ });
312
+ assertHistoricalSegmentCompatible(existing, input.segment);
313
+ await chmod(path, 0o600);
314
+ return { created: false, segment: existing };
315
+ }
316
+
317
+ export async function writeHistoricalSessionStateAndIndex(input: {
318
+ homeDir: string;
319
+ paths: SessionMemoryPaths;
320
+ projectKey: string;
321
+ sessionKey: string;
322
+ target: SessionMemoryStateV1["target"];
323
+ roleId: string | null;
324
+ policy: SessionMemoryStateV1["policy"];
325
+ now: string;
326
+ }): Promise<{ state: SessionMemoryStateV1; segmentIds: string[] }> {
327
+ const expectedPaths = resolveSessionMemoryPaths({
328
+ homeDir: input.homeDir,
329
+ projectKey: input.projectKey,
330
+ sessionKey: input.sessionKey,
331
+ });
332
+ if (
333
+ expectedPaths.rootDir !== input.paths.rootDir ||
334
+ expectedPaths.projectDir !== input.paths.projectDir ||
335
+ expectedPaths.sessionDir !== input.paths.sessionDir ||
336
+ expectedPaths.segmentsDir !== input.paths.segmentsDir ||
337
+ expectedPaths.statePath !== input.paths.statePath ||
338
+ expectedPaths.indexPath !== input.paths.indexPath
339
+ ) {
340
+ throw new Error("Historical Session Memory state path does not match its identity.");
341
+ }
342
+ await ensurePrivateSessionMemoryDirectory(input.paths.rootDir);
343
+ await ensurePrivateSessionMemoryDirectory(input.paths.projectDir);
344
+ await ensurePrivateSessionMemoryDirectory(input.paths.sessionDir);
345
+ await ensurePrivateSessionMemoryDirectory(input.paths.segmentsDir);
346
+ const existing = await readSessionState(input.paths.statePath);
347
+ const segments = await readSessionSegmentsFromDirectory(input.paths.segmentsDir);
348
+ const ordered = segments.sort(
349
+ (left, right) =>
350
+ left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),
351
+ );
352
+ const last = ordered.at(-1) ?? null;
353
+ const state: SessionMemoryStateV1 = {
354
+ schemaVersion: 1,
355
+ kind: "session-memory-state",
356
+ projectKey: input.projectKey,
357
+ sessionKey: input.sessionKey,
358
+ target: input.target,
359
+ runId: null,
360
+ roleId: input.roleId,
361
+ initialized: true,
362
+ createdAt: existing?.createdAt ?? input.now,
363
+ updatedAt: input.now,
364
+ counters: existing?.counters ?? {
365
+ messageTokenEstimate: 0,
366
+ tokensSinceLastUpdate: 0,
367
+ toolCallsSinceLastUpdate: 0,
368
+ userPromptCount: 0,
369
+ assistantTurnCount: 0,
370
+ toolCallCount: 0,
371
+ failureSignalCount: 0,
372
+ verificationSignalCount: 0,
373
+ },
374
+ activeSpan: null,
375
+ pendingSignals: [],
376
+ lastSegmentId: last?.id ?? null,
377
+ lastMemoryUpdateAt: input.now,
378
+ policy: input.policy,
379
+ };
380
+ const index = {
381
+ schemaVersion: 1,
382
+ kind: "session-memory-index",
383
+ projectKey: input.projectKey,
384
+ sessionKey: input.sessionKey,
385
+ updatedAt: input.now,
386
+ lastSegmentId: last?.id ?? null,
387
+ segments: ordered.map((segment) => ({
388
+ id: segment.id,
389
+ reason: segment.reason,
390
+ strength: segment.strength,
391
+ createdAt: segment.createdAt,
392
+ reviewState: segment.lifecycle.reviewState,
393
+ })),
394
+ };
395
+ await writePrivateAtomicJson(input.paths.statePath, state);
396
+ await writePrivateAtomicJson(input.paths.indexPath, index);
397
+ return { state, segmentIds: ordered.map((segment) => segment.id) };
398
+ }
399
+
400
+ export async function writeSessionEvidenceSegmentAtomic(input: {
401
+ homeDir: string;
402
+ segment: SessionEvidenceSegmentV1;
403
+ }): Promise<void> {
404
+ assertHistoricalRetentionState(input.segment);
405
+ const paths = resolveSessionMemoryPaths({
406
+ homeDir: input.homeDir,
407
+ projectKey: input.segment.projectKey,
408
+ sessionKey: input.segment.sessionKey,
409
+ });
410
+ await writePrivateAtomicJson(paths.segmentPath(input.segment.id), input.segment);
411
+ }
412
+
413
+ function assertHistoricalRetentionState(segment: SessionEvidenceSegmentV1): void {
414
+ if (segment.origin?.kind !== "historical-import" || segment.retention === undefined) {
415
+ throw new Error("Atomic Session Evidence rewrite is limited to historical retention.");
416
+ }
417
+ if (segment.retention.rawState === "available") {
418
+ if (
419
+ !segment.rawExcerpt.stored ||
420
+ segment.rawExcerpt.byteLength !== Buffer.byteLength(segment.rawExcerpt.content, "utf8") ||
421
+ segment.rawExcerpt.sha256 !== sha256Hex(segment.rawExcerpt.content) ||
422
+ segment.retention.originalRawSha256 !== segment.rawExcerpt.sha256
423
+ ) {
424
+ throw new Error("Available historical Session Evidence raw state is invalid.");
425
+ }
426
+ return;
427
+ }
428
+ if (
429
+ segment.rawExcerpt.stored ||
430
+ segment.rawExcerpt.content !== "" ||
431
+ segment.rawExcerpt.byteLength !== 0 ||
432
+ segment.rawExcerpt.sha256 !== sha256Hex("") ||
433
+ segment.retention.rawPurgedAt === null ||
434
+ segment.lifecycle.status !== "raw-expired"
435
+ ) {
436
+ throw new Error("Purged historical Session Evidence raw state is invalid.");
437
+ }
438
+ }
439
+
271
440
  export async function writeJson(path: string, value: unknown): Promise<void> {
272
441
  await mkdir(dirname(path), { recursive: true });
273
442
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
274
443
  }
275
444
 
445
+ async function readSessionSegmentsFromDirectory(
446
+ segmentsDir: string,
447
+ ): Promise<SessionEvidenceSegmentV1[]> {
448
+ const segments: SessionEvidenceSegmentV1[] = [];
449
+ for (const path of await listJsonFiles(segmentsDir)) {
450
+ segments.push(parseSessionEvidenceSegment(JSON.parse(await readFile(path, "utf8"))));
451
+ }
452
+ return segments;
453
+ }
454
+
455
+ async function ensurePrivateSessionMemoryDirectory(path: string): Promise<void> {
456
+ await mkdir(path, { recursive: true, mode: 0o700 });
457
+ await chmod(path, 0o700);
458
+ }
459
+
460
+ async function writePrivateAtomicJson(path: string, value: unknown): Promise<void> {
461
+ await ensurePrivateSessionMemoryDirectory(dirname(path));
462
+ const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
463
+ try {
464
+ const handle = await open(temporaryPath, "wx", 0o600);
465
+ try {
466
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
467
+ await handle.sync();
468
+ } finally {
469
+ await handle.close();
470
+ }
471
+ await rename(temporaryPath, path);
472
+ await chmod(path, 0o600);
473
+ } finally {
474
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
475
+ }
476
+ }
477
+
478
+ async function writePrivateImmutableText(path: string, content: string): Promise<boolean> {
479
+ await ensurePrivateSessionMemoryDirectory(dirname(path));
480
+ const temporaryPath = join(dirname(path), `.${randomUUID()}.tmp`);
481
+ try {
482
+ const handle = await open(temporaryPath, "wx", 0o600);
483
+ try {
484
+ await handle.writeFile(content, "utf8");
485
+ await handle.sync();
486
+ } finally {
487
+ await handle.close();
488
+ }
489
+ try {
490
+ await link(temporaryPath, path);
491
+ await chmod(path, 0o600);
492
+ return true;
493
+ } catch (error) {
494
+ if (!isFileExistsError(error)) throw error;
495
+ return false;
496
+ }
497
+ } finally {
498
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
499
+ }
500
+ }
501
+
502
+ function assertHistoricalSegmentCompatible(
503
+ existing: SessionEvidenceSegmentV1,
504
+ expected: SessionEvidenceSegmentV1,
505
+ ): void {
506
+ const existingOrigin = existing.origin;
507
+ const expectedOrigin = expected.origin;
508
+ const existingRetention = existing.retention;
509
+ const expectedRetention = expected.retention;
510
+ if (
511
+ existingOrigin?.kind !== "historical-import" ||
512
+ expectedOrigin?.kind !== "historical-import" ||
513
+ existingRetention === undefined ||
514
+ expectedRetention === undefined
515
+ ) {
516
+ throw new Error("Historical Session Evidence identity conflict.");
517
+ }
518
+ const existingProjection = {
519
+ schemaVersion: existing.schemaVersion,
520
+ kind: existing.kind,
521
+ id: existing.id,
522
+ projectKey: existing.projectKey,
523
+ runId: existing.runId,
524
+ roleId: existing.roleId,
525
+ sessionKey: existing.sessionKey,
526
+ target: existing.target,
527
+ createdAt: existing.createdAt,
528
+ reason: existing.reason,
529
+ strength: existing.strength,
530
+ origin: existingOrigin,
531
+ source: existing.source,
532
+ signals: existing.signals,
533
+ normalized: existing.normalized,
534
+ privacy: {
535
+ localOnly: existing.privacy.localOnly,
536
+ secretsDetected: existing.privacy.secretsDetected,
537
+ sensitivity: existing.privacy.sensitivity,
538
+ sensitivityReasons: existing.privacy.sensitivityReasons,
539
+ redactionApplied: existing.privacy.redactionApplied,
540
+ externalUploadAllowed: existing.privacy.externalUploadAllowed,
541
+ },
542
+ rawTruncated: existing.rawExcerpt.truncated,
543
+ originalRawSha256: existingRetention.originalRawSha256,
544
+ retentionPolicyDays: existingRetention.policyDays,
545
+ retentionExpiresAt: existingRetention.expiresAt,
546
+ };
547
+ const expectedProjection = {
548
+ schemaVersion: expected.schemaVersion,
549
+ kind: expected.kind,
550
+ id: expected.id,
551
+ projectKey: expected.projectKey,
552
+ runId: expected.runId,
553
+ roleId: expected.roleId,
554
+ sessionKey: expected.sessionKey,
555
+ target: expected.target,
556
+ createdAt: expected.createdAt,
557
+ reason: expected.reason,
558
+ strength: expected.strength,
559
+ origin: expectedOrigin,
560
+ source: expected.source,
561
+ signals: expected.signals,
562
+ normalized: expected.normalized,
563
+ privacy: {
564
+ localOnly: expected.privacy.localOnly,
565
+ secretsDetected: expected.privacy.secretsDetected,
566
+ sensitivity: expected.privacy.sensitivity,
567
+ sensitivityReasons: expected.privacy.sensitivityReasons,
568
+ redactionApplied: expected.privacy.redactionApplied,
569
+ externalUploadAllowed: expected.privacy.externalUploadAllowed,
570
+ },
571
+ rawTruncated: expected.rawExcerpt.truncated,
572
+ originalRawSha256: expectedRetention.originalRawSha256,
573
+ retentionPolicyDays: expectedRetention.policyDays,
574
+ retentionExpiresAt: expectedRetention.expiresAt,
575
+ };
576
+ if (
577
+ JSON.stringify(existingProjection) !== JSON.stringify(expectedProjection) ||
578
+ (existingRetention.rawState === "available" &&
579
+ (!existing.rawExcerpt.stored ||
580
+ existing.rawExcerpt.encoding !== "utf8" ||
581
+ existing.rawExcerpt.byteLength !== Buffer.byteLength(existing.rawExcerpt.content, "utf8") ||
582
+ existing.rawExcerpt.sha256 !== sha256Hex(existing.rawExcerpt.content) ||
583
+ existing.rawExcerpt.sha256 !== expected.rawExcerpt.sha256))
584
+ ) {
585
+ throw new Error("Historical Session Evidence immutable content conflict.");
586
+ }
587
+ }
588
+
589
+ function assertSessionMemoryPathsMatchSegment(
590
+ homeDir: string,
591
+ paths: SessionMemoryPaths,
592
+ segment: SessionEvidenceSegmentV1,
593
+ ): void {
594
+ const expected = resolveSessionMemoryPaths({
595
+ homeDir,
596
+ projectKey: segment.projectKey,
597
+ sessionKey: segment.sessionKey,
598
+ });
599
+ if (
600
+ paths.rootDir !== expected.rootDir ||
601
+ paths.projectDir !== expected.projectDir ||
602
+ paths.sessionDir !== expected.sessionDir ||
603
+ paths.segmentsDir !== expected.segmentsDir ||
604
+ paths.segmentPath(segment.id) !== expected.segmentPath(segment.id)
605
+ ) {
606
+ throw new Error("Historical Session Evidence path does not match its identity.");
607
+ }
608
+ }
609
+
276
610
  async function countJsonlLines(path: string): Promise<number> {
277
611
  try {
278
612
  const text = await readFile(path, "utf8");
@@ -7,7 +7,8 @@ export type SessionMemorySegmentReason =
7
7
  | "failure-signal"
8
8
  | "permission-denied"
9
9
  | "verification-after-fix"
10
- | "explicit-memory-intent";
10
+ | "explicit-memory-intent"
11
+ | "historical-import";
11
12
  export type SessionMemorySignalStrength = "normal" | "strong";
12
13
  export type SessionMemorySensitivity = "safe-metadata" | "private-content" | "credential";
13
14
  export type SessionMemorySensitivityReason =
@@ -92,6 +93,68 @@ export interface SessionMemoryCursorV1 {
92
93
  updatedAt: string;
93
94
  }
94
95
 
96
+ export interface HistoricalImportOriginV1 {
97
+ kind: "historical-import";
98
+ importId: string;
99
+ generationId: string;
100
+ snapshotId: string;
101
+ sourceKey: string;
102
+ firstRecordKeyHash: string;
103
+ lastRecordKeyHash: string;
104
+ recordCount: number;
105
+ }
106
+
107
+ export interface HistoricalImportRetentionV1 {
108
+ policyDays: number;
109
+ expiresAt: string;
110
+ rawState: "available" | "purged";
111
+ rawPurgedAt: string | null;
112
+ originalRawSha256: string;
113
+ }
114
+
115
+ export interface HistoricalImportStoredRecordV1 {
116
+ schemaVersion: 1;
117
+ kind: "historical-import-record";
118
+ recordKeyHash: string;
119
+ recordType: "user" | "reasoning" | "assistant" | "assistant-tool-call" | "tool";
120
+ canonicalJson: string;
121
+ canonicalJsonTruncated: boolean;
122
+ }
123
+
124
+ export interface SemanticEvidencePacketV1 {
125
+ schemaVersion: 1;
126
+ kind: "semantic-evidence-packet";
127
+ projectKey: string;
128
+ segmentId: string;
129
+ conversation: Array<{
130
+ role: "user" | "assistant";
131
+ text: string;
132
+ recordKeyHash: string;
133
+ }>;
134
+ toolEvidence: Array<{
135
+ reason: "failure" | "verification" | "permission" | "rollback";
136
+ toolName: string;
137
+ safeInputExcerpt: string | null;
138
+ outputExcerpt: string | null;
139
+ status: string | null;
140
+ exitCode: number | null;
141
+ inputTruncated: boolean;
142
+ outputTruncated: boolean;
143
+ }>;
144
+ touchedPaths: string[];
145
+ selection: {
146
+ selectedRecords: number;
147
+ droppedRecords: number;
148
+ redactionCount: number;
149
+ truncated: boolean;
150
+ };
151
+ privacy: {
152
+ containsPrivateContent: true;
153
+ credentialRedacted: true;
154
+ rawSegmentIncluded: false;
155
+ };
156
+ }
157
+
95
158
  export interface SessionEvidenceSegmentV1 {
96
159
  schemaVersion: 1;
97
160
  kind: "session-evidence-segment";
@@ -104,6 +167,8 @@ export interface SessionEvidenceSegmentV1 {
104
167
  createdAt: string;
105
168
  reason: SessionMemorySegmentReason;
106
169
  strength: SessionMemorySignalStrength;
170
+ origin?: HistoricalImportOriginV1;
171
+ retention?: HistoricalImportRetentionV1;
107
172
  source: {
108
173
  traceRefId: string | null;
109
174
  sourcePath: string | null;
@@ -116,7 +181,7 @@ export interface SessionEvidenceSegmentV1 {
116
181
  };
117
182
  signals: SessionMemorySignal[];
118
183
  rawExcerpt: {
119
- stored: true;
184
+ stored: boolean;
120
185
  encoding: "utf8";
121
186
  content: string;
122
187
  truncated: boolean;
@@ -144,7 +209,14 @@ export interface SessionEvidenceSegmentV1 {
144
209
  externalUploadAllowed: false;
145
210
  };
146
211
  lifecycle: {
147
- status: "captured" | "pending-review" | "reviewed" | "distilled" | "ignored" | "deleted";
212
+ status:
213
+ | "captured"
214
+ | "pending-review"
215
+ | "reviewed"
216
+ | "distilled"
217
+ | "ignored"
218
+ | "deleted"
219
+ | "raw-expired";
148
220
  reviewState: "not-required" | "unreviewed" | "accepted" | "rejected" | "deferred";
149
221
  consumedByBatchIds: string[];
150
222
  };