@evo-dev/core 0.0.1-alpha.3 → 0.0.1-alpha.4

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.
Files changed (45) hide show
  1. package/dist/config/index.js +164 -125
  2. package/dist/index.js +8999 -7914
  3. package/package.json +1 -1
  4. package/src/agents/index.ts +1 -1
  5. package/src/code-agent-traces/index.ts +3 -10
  6. package/src/config/settings.ts +14 -0
  7. package/src/config/store.ts +1 -1
  8. package/src/daemon/index.ts +1 -1
  9. package/src/evolution/candidates/index.ts +235 -0
  10. package/src/evolution/control/index.ts +19 -0
  11. package/src/evolution/evidence/analysis.ts +529 -0
  12. package/src/evolution/evidence/index.ts +3 -0
  13. package/src/evolution/evidence/session-memory/constants.ts +12 -0
  14. package/src/evolution/evidence/session-memory/index.ts +4 -0
  15. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  16. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  17. package/src/evolution/evidence/session-memory/segment.ts +192 -0
  18. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  19. package/src/evolution/evidence/session-memory/storage.ts +145 -0
  20. package/src/evolution/evidence/session-memory/types.ts +207 -0
  21. package/src/evolution/evidence/session-memory/updater.ts +184 -0
  22. package/src/evolution/formatters.ts +169 -0
  23. package/src/evolution/index.ts +12 -2827
  24. package/src/{knowledge → evolution/knowledge}/index.ts +7 -7
  25. package/src/evolution/paths.ts +41 -0
  26. package/src/evolution/processor/distillation.ts +436 -0
  27. package/src/evolution/processor/index.ts +3 -0
  28. package/src/evolution/processor/process.ts +306 -0
  29. package/src/evolution/schema.ts +522 -0
  30. package/src/evolution/shared.ts +677 -0
  31. package/src/evolution/triggers/classification.ts +102 -0
  32. package/src/evolution/triggers/index.ts +295 -0
  33. package/src/hooks/index.ts +54 -7
  34. package/src/index.ts +10 -2
  35. package/src/runtime-logs/index.ts +4 -12
  36. package/src/team/index.ts +1 -1
  37. package/src/utils/errors.ts +13 -0
  38. package/src/utils/fs.ts +40 -0
  39. package/src/utils/hash.ts +9 -0
  40. package/src/utils/ids.ts +12 -0
  41. package/src/utils/index.ts +7 -0
  42. package/src/utils/parsing.ts +11 -0
  43. package/src/utils/text.ts +18 -0
  44. package/src/utils/time.ts +5 -0
  45. /package/src/{learning → evolution/review}/index.ts +0 -0
@@ -0,0 +1,192 @@
1
+ import {
2
+ createStableId,
3
+ sanitizeStorageId,
4
+ sanitizeSummary,
5
+ sha256Hex,
6
+ truncateUtf8Tail,
7
+ } from "../../../utils/index.ts";
8
+ import {
9
+ DEFAULT_MAX_RAW_EVENT_BYTES,
10
+ SENSITIVE_TEXT_PATTERN,
11
+ VERIFICATION_PATTERN,
12
+ } from "./constants.ts";
13
+ import { readLineRange } from "./storage.ts";
14
+ import type {
15
+ SessionEvidenceSegmentV1,
16
+ SessionMemoryCursorV1,
17
+ SessionMemoryPaths,
18
+ SessionMemoryRawEventV1,
19
+ SessionMemorySegmentReason,
20
+ SessionMemorySignal,
21
+ SessionMemorySignalStrength,
22
+ SessionMemoryStateV1,
23
+ UpdateSessionMemoryInput,
24
+ } from "./types.ts";
25
+
26
+ export function createRawEvent(
27
+ input: UpdateSessionMemoryInput,
28
+ now: string,
29
+ ): SessionMemoryRawEventV1 {
30
+ const rawJson = stringifyPayload(input.rawPayload);
31
+ const truncated = truncateUtf8Tail(rawJson, DEFAULT_MAX_RAW_EVENT_BYTES);
32
+ return {
33
+ schemaVersion: 1,
34
+ kind: "session-memory-raw-event",
35
+ eventId: input.event.eventId,
36
+ eventType: input.event.type,
37
+ occurredAt: input.event.time.occurredAt,
38
+ receivedAt: now,
39
+ summary: sanitizeSummary(input.event.payload.summary),
40
+ rawPayloadJson: truncated.content,
41
+ rawPayloadByteLength: Buffer.byteLength(rawJson, "utf8"),
42
+ rawPayloadTruncated: truncated.truncated,
43
+ secretsDetected: SENSITIVE_TEXT_PATTERN.test(rawJson),
44
+ };
45
+ }
46
+
47
+ export function createSignal(
48
+ input: UpdateSessionMemoryInput,
49
+ rawEvent: SessionMemoryRawEventV1,
50
+ rawEventLine: number,
51
+ ): SessionMemorySignal {
52
+ return {
53
+ eventId: input.event.eventId,
54
+ eventType: input.event.type,
55
+ reason: "observed",
56
+ strength: "normal",
57
+ occurredAt: input.event.time.occurredAt,
58
+ summary: rawEvent.summary,
59
+ rawEventLine,
60
+ };
61
+ }
62
+
63
+ export async function createSegment(input: {
64
+ paths: SessionMemoryPaths;
65
+ state: SessionMemoryStateV1;
66
+ cursor: SessionMemoryCursorV1;
67
+ reason: SessionMemorySegmentReason;
68
+ strength: SessionMemorySignalStrength;
69
+ signals: SessionMemorySignal[];
70
+ currentLine: number;
71
+ now: string;
72
+ }): Promise<SessionEvidenceSegmentV1> {
73
+ const fromLine = (input.cursor.lastCapturedLine ?? 0) + 1;
74
+ const toLine = input.currentLine;
75
+ const raw = await readLineRange(input.paths.eventsPath, fromLine, toLine);
76
+ const truncated = truncateUtf8Tail(raw.content, input.state.policy.maxRawSegmentBytes);
77
+ const signalSummaries = input.signals.map((signal) => signal.summary).filter(Boolean);
78
+ const failures = input.signals
79
+ .filter((signal) => signal.reason === "failure-signal")
80
+ .map((signal) => signal.summary);
81
+ const verifications = input.signals
82
+ .filter(
83
+ (signal) =>
84
+ signal.reason === "verification-after-fix" || VERIFICATION_PATTERN.test(signal.summary),
85
+ )
86
+ .map((signal) => signal.summary);
87
+ const segmentId = createStableId("segment", [
88
+ input.state.projectKey,
89
+ input.state.sessionKey,
90
+ input.reason,
91
+ String(fromLine),
92
+ String(toLine),
93
+ input.signals.at(-1)?.eventId ?? input.now,
94
+ ]);
95
+ return {
96
+ schemaVersion: 1,
97
+ kind: "session-evidence-segment",
98
+ id: segmentId,
99
+ projectKey: input.state.projectKey,
100
+ runId: input.state.runId,
101
+ roleId: input.state.roleId,
102
+ sessionKey: input.state.sessionKey,
103
+ target: input.state.target,
104
+ createdAt: input.now,
105
+ reason: input.reason,
106
+ strength: input.strength,
107
+ source: {
108
+ traceRefId: input.cursor.traceRefId,
109
+ sourcePath: input.paths.eventsPath,
110
+ fromOffset: null,
111
+ toOffset: null,
112
+ fromLine,
113
+ toLine,
114
+ fromEventId: input.signals[0]?.eventId ?? null,
115
+ toEventId: input.signals.at(-1)?.eventId ?? null,
116
+ },
117
+ signals: input.signals,
118
+ rawExcerpt: {
119
+ stored: true,
120
+ encoding: "utf8",
121
+ content: truncated.content,
122
+ truncated: raw.truncated || truncated.truncated,
123
+ byteLength: Buffer.byteLength(truncated.content, "utf8"),
124
+ sha256: sha256Hex(truncated.content),
125
+ },
126
+ normalized: {
127
+ summary: createSegmentSummary(input.reason, input.strength, signalSummaries),
128
+ userIntent: firstUserPromptSummary(input.signals),
129
+ intentDelta: intentDeltaSummary(input.signals),
130
+ decisions: [],
131
+ failures,
132
+ verifications,
133
+ touchedTools: collectTouchedTools(raw.content),
134
+ },
135
+ privacy: {
136
+ localOnly: true,
137
+ rawPromptStored: input.signals.some((signal) => signal.eventType === "UserPromptSubmit"),
138
+ rawOutputStored: input.signals.some((signal) =>
139
+ ["PostToolUse", "PostToolUseFailure", "PostToolBatch"].includes(signal.eventType),
140
+ ),
141
+ sourceContentStored: true,
142
+ secretsDetected: raw.secretsDetected,
143
+ redactionApplied: false,
144
+ externalUploadAllowed: false,
145
+ },
146
+ lifecycle: {
147
+ status: "pending-review",
148
+ reviewState: "unreviewed",
149
+ consumedByBatchIds: [],
150
+ },
151
+ };
152
+ }
153
+
154
+ export function estimateTokenCount(rawPayloadJson: string, summary: string): number {
155
+ return Math.max(1, Math.ceil((rawPayloadJson.length + summary.length) / 4));
156
+ }
157
+
158
+ function createSegmentSummary(
159
+ reason: SessionMemorySegmentReason,
160
+ strength: SessionMemorySignalStrength,
161
+ summaries: string[],
162
+ ): string {
163
+ const suffix = summaries.length === 0 ? "No event summary." : summaries.slice(-3).join(" / ");
164
+ return sanitizeSummary(`Session segment ${reason} (${strength}): ${suffix}`);
165
+ }
166
+
167
+ function firstUserPromptSummary(signals: SessionMemorySignal[]): string | null {
168
+ return signals.find((signal) => signal.eventType === "UserPromptSubmit")?.summary ?? null;
169
+ }
170
+
171
+ function intentDeltaSummary(signals: SessionMemorySignal[]): string | null {
172
+ const prompts = signals.filter((signal) => signal.eventType === "UserPromptSubmit");
173
+ if (prompts.length < 2) return null;
174
+ return prompts.at(-1)?.summary ?? null;
175
+ }
176
+
177
+ function collectTouchedTools(rawContent: string): string[] {
178
+ const tools = new Set<string>();
179
+ for (const match of rawContent.matchAll(/"tool(?:Name|_name)"\s*:\s*"([^"]+)"/g)) {
180
+ const tool = match[1]?.trim();
181
+ if (tool !== undefined && tool !== "") tools.add(sanitizeStorageId(tool, "tool"));
182
+ }
183
+ return [...tools].slice(0, 20);
184
+ }
185
+
186
+ function stringifyPayload(payload: Record<string, unknown>): string {
187
+ try {
188
+ return JSON.stringify(payload);
189
+ } catch {
190
+ return JSON.stringify({ unserializable: true });
191
+ }
192
+ }
@@ -0,0 +1,249 @@
1
+ import { createStableId, optionalString } from "../../../utils/index.ts";
2
+ import { EXPLICIT_MEMORY_PATTERN, FAILURE_PATTERN, VERIFICATION_PATTERN } from "./constants.ts";
3
+ import type {
4
+ SessionMemoryPolicySnapshot,
5
+ SessionMemoryRawEventV1,
6
+ SessionMemorySegmentReason,
7
+ SessionMemorySignal,
8
+ SessionMemorySignalStrength,
9
+ SessionMemoryStateV1,
10
+ SessionMemoryTarget,
11
+ UpdateSessionMemoryInput,
12
+ } from "./types.ts";
13
+
14
+ export function createInitialState(input: {
15
+ projectKey: string;
16
+ sessionKey: string;
17
+ target: SessionMemoryTarget;
18
+ runId: string | null;
19
+ roleId: string | null;
20
+ now: string;
21
+ policy: SessionMemoryPolicySnapshot;
22
+ eventId: string;
23
+ tokenEstimate: number;
24
+ }): SessionMemoryStateV1 {
25
+ return {
26
+ schemaVersion: 1,
27
+ kind: "session-memory-state",
28
+ projectKey: input.projectKey,
29
+ sessionKey: input.sessionKey,
30
+ target: input.target,
31
+ runId: input.runId,
32
+ roleId: input.roleId,
33
+ initialized: true,
34
+ createdAt: input.now,
35
+ updatedAt: input.now,
36
+ counters: {
37
+ messageTokenEstimate: 0,
38
+ tokensSinceLastUpdate: 0,
39
+ toolCallsSinceLastUpdate: 0,
40
+ userPromptCount: 0,
41
+ assistantTurnCount: 0,
42
+ toolCallCount: 0,
43
+ failureSignalCount: 0,
44
+ verificationSignalCount: 0,
45
+ },
46
+ activeSpan: {
47
+ spanId: createStableId("span", [input.projectKey, input.sessionKey, input.eventId]),
48
+ status: "open",
49
+ openedAt: input.now,
50
+ closedAt: null,
51
+ openedByEventId: input.eventId,
52
+ lastEventId: input.eventId,
53
+ userPromptCount: 0,
54
+ toolCallCount: 0,
55
+ workStarted: false,
56
+ tokenEstimate: input.tokenEstimate,
57
+ },
58
+ pendingSignals: [],
59
+ lastSegmentId: null,
60
+ lastMemoryUpdateAt: null,
61
+ policy: input.policy,
62
+ };
63
+ }
64
+
65
+ export function applyEventToState(
66
+ state: SessionMemoryStateV1,
67
+ input: {
68
+ input: UpdateSessionMemoryInput;
69
+ now: string;
70
+ tokenEstimate: number;
71
+ policy: SessionMemoryPolicySnapshot;
72
+ teamRunId: string | null;
73
+ teamRoleId: string | null;
74
+ },
75
+ ): void {
76
+ state.policy = input.policy;
77
+ state.runId = input.teamRunId ?? state.runId;
78
+ state.roleId = input.teamRoleId ?? state.roleId;
79
+ state.target = input.input.target;
80
+ state.counters.messageTokenEstimate += input.tokenEstimate;
81
+ state.counters.tokensSinceLastUpdate += input.tokenEstimate;
82
+ if (input.input.event.type === "UserPromptSubmit") {
83
+ state.counters.userPromptCount += 1;
84
+ }
85
+ if (isToolCallCompletion(input.input.event.type)) {
86
+ state.counters.toolCallCount += 1;
87
+ state.counters.toolCallsSinceLastUpdate += 1;
88
+ }
89
+ if (isAssistantTurn(input.input.event.type)) {
90
+ state.counters.assistantTurnCount += 1;
91
+ }
92
+ if (isFailureSignal(input.input.event.type, input.input.event.payload.summary)) {
93
+ state.counters.failureSignalCount += 1;
94
+ }
95
+ if (isVerificationSignal(input.input.event.payload.summary)) {
96
+ state.counters.verificationSignalCount += 1;
97
+ }
98
+
99
+ if (state.activeSpan !== null) {
100
+ state.activeSpan.lastEventId = input.input.event.eventId;
101
+ state.activeSpan.tokenEstimate += input.tokenEstimate;
102
+ if (input.input.event.type === "UserPromptSubmit") {
103
+ state.activeSpan.userPromptCount += 1;
104
+ }
105
+ if (isWorkStartedEvent(input.input.event.type)) {
106
+ state.activeSpan.workStarted = true;
107
+ }
108
+ if (isToolCallCompletion(input.input.event.type)) {
109
+ state.activeSpan.toolCallCount += 1;
110
+ }
111
+ if (isSpanCloseEvent(input.input.event.type)) {
112
+ state.activeSpan.status = "closed";
113
+ state.activeSpan.closedAt = input.now;
114
+ }
115
+ }
116
+ }
117
+
118
+ export function decideSegment(input: {
119
+ previous: SessionMemoryStateV1;
120
+ next: SessionMemoryStateV1;
121
+ input: UpdateSessionMemoryInput;
122
+ rawEvent: SessionMemoryRawEventV1;
123
+ currentSignal: SessionMemorySignal;
124
+ tokenEstimate: number;
125
+ }): { reason: SessionMemorySegmentReason; strength: SessionMemorySignalStrength } | null {
126
+ const eventType = input.input.event.type;
127
+ const promptText = extractPromptText(input.input.rawPayload);
128
+ if (eventType === "UserPromptSubmit") {
129
+ if (input.previous.counters.userPromptCount === 0) return null;
130
+ if (EXPLICIT_MEMORY_PATTERN.test(promptText)) {
131
+ return { reason: "explicit-memory-intent", strength: "strong" };
132
+ }
133
+ if (input.previous.activeSpan?.workStarted === true) {
134
+ reopenInterruptedSpan(
135
+ input.next,
136
+ input.input.event.eventId,
137
+ input.input.receivedAt ?? input.rawEvent.receivedAt,
138
+ );
139
+ return { reason: "user-interruption", strength: "strong" };
140
+ }
141
+ return { reason: "intent-refinement", strength: "strong" };
142
+ }
143
+
144
+ if (eventType === "PermissionDenied") return { reason: "permission-denied", strength: "strong" };
145
+ if (isFailureSignal(eventType, input.input.event.payload.summary)) {
146
+ return { reason: "failure-signal", strength: "strong" };
147
+ }
148
+ if (
149
+ input.previous.counters.failureSignalCount > 0 &&
150
+ isVerificationSignal(input.input.event.payload.summary)
151
+ ) {
152
+ return { reason: "verification-after-fix", strength: "strong" };
153
+ }
154
+
155
+ if (
156
+ input.next.lastMemoryUpdateAt === null &&
157
+ input.next.counters.messageTokenEstimate >= input.next.policy.minimumMessageTokensToInit
158
+ ) {
159
+ return { reason: "session-memory-threshold", strength: "normal" };
160
+ }
161
+ if (
162
+ input.next.lastMemoryUpdateAt !== null &&
163
+ (input.next.counters.tokensSinceLastUpdate >= input.next.policy.minimumTokensBetweenUpdate ||
164
+ input.next.counters.toolCallsSinceLastUpdate >= input.next.policy.toolCallsBetweenUpdates)
165
+ ) {
166
+ return { reason: "session-memory-threshold", strength: "normal" };
167
+ }
168
+ return null;
169
+ }
170
+
171
+ export function cloneState(state: SessionMemoryStateV1): SessionMemoryStateV1 {
172
+ return JSON.parse(JSON.stringify(state)) as SessionMemoryStateV1;
173
+ }
174
+
175
+ export function appendBoundedSignal(
176
+ signals: SessionMemorySignal[],
177
+ signal: SessionMemorySignal,
178
+ ): SessionMemorySignal[] {
179
+ return [...signals, signal].slice(-50);
180
+ }
181
+
182
+ function reopenInterruptedSpan(state: SessionMemoryStateV1, eventId: string, now: string): void {
183
+ if (state.activeSpan !== null) {
184
+ state.activeSpan.status = "interrupted";
185
+ state.activeSpan.closedAt = now;
186
+ }
187
+ state.activeSpan = {
188
+ spanId: createStableId("span", [state.projectKey, state.sessionKey, eventId, now]),
189
+ status: "open",
190
+ openedAt: now,
191
+ closedAt: null,
192
+ openedByEventId: eventId,
193
+ lastEventId: eventId,
194
+ userPromptCount: 1,
195
+ toolCallCount: 0,
196
+ workStarted: false,
197
+ tokenEstimate: 0,
198
+ };
199
+ }
200
+
201
+ function isWorkStartedEvent(eventType: string): boolean {
202
+ return [
203
+ "PreToolUse",
204
+ "PermissionRequest",
205
+ "PostToolUse",
206
+ "PostToolUseFailure",
207
+ "PostToolBatch",
208
+ "TaskCreated",
209
+ "SubagentStart",
210
+ ].includes(eventType);
211
+ }
212
+
213
+ function isToolCallCompletion(eventType: string): boolean {
214
+ return eventType === "PostToolUse" || eventType === "PostToolUseFailure";
215
+ }
216
+
217
+ function isAssistantTurn(eventType: string): boolean {
218
+ return ["PostToolUse", "PostToolUseFailure", "PostToolBatch", "Stop", "StopFailure"].includes(
219
+ eventType,
220
+ );
221
+ }
222
+
223
+ function isSpanCloseEvent(eventType: string): boolean {
224
+ return ["TaskCompleted", "Stop", "SessionEnd", "SubagentStop"].includes(eventType);
225
+ }
226
+
227
+ function isFailureSignal(eventType: string, summary: string): boolean {
228
+ return (
229
+ eventType === "PostToolUseFailure" ||
230
+ eventType === "StopFailure" ||
231
+ FAILURE_PATTERN.test(summary)
232
+ );
233
+ }
234
+
235
+ function isVerificationSignal(summary: string): boolean {
236
+ return VERIFICATION_PATTERN.test(summary);
237
+ }
238
+
239
+ function extractPromptText(payload: Record<string, unknown>): string {
240
+ return (
241
+ optionalString(payload.prompt) ??
242
+ optionalString(payload.user_prompt) ??
243
+ optionalString(payload.userPrompt) ??
244
+ optionalString(payload.query) ??
245
+ optionalString(payload.message) ??
246
+ optionalString(payload.text) ??
247
+ ""
248
+ );
249
+ }
@@ -0,0 +1,145 @@
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { isNotFoundError } from "../../../utils/index.ts";
4
+ import { SENSITIVE_TEXT_PATTERN } from "./constants.ts";
5
+ import type {
6
+ SessionEvidenceSegmentV1,
7
+ SessionMemoryCursorV1,
8
+ SessionMemoryIndexSegment,
9
+ SessionMemoryPaths,
10
+ SessionMemoryRawEventV1,
11
+ SessionMemoryStateV1,
12
+ } from "./types.ts";
13
+
14
+ export async function readSessionState(path: string): Promise<SessionMemoryStateV1 | null> {
15
+ try {
16
+ return JSON.parse(await readFile(path, "utf8")) as SessionMemoryStateV1;
17
+ } catch (error) {
18
+ if (isNotFoundError(error)) return null;
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ export async function readSessionCursor(
24
+ path: string,
25
+ sessionKey: string,
26
+ sourcePath: string,
27
+ now: string,
28
+ ): Promise<SessionMemoryCursorV1> {
29
+ try {
30
+ return JSON.parse(await readFile(path, "utf8")) as SessionMemoryCursorV1;
31
+ } catch (error) {
32
+ if (!isNotFoundError(error)) throw error;
33
+ return {
34
+ schemaVersion: 1,
35
+ kind: "session-memory-cursor",
36
+ sessionKey,
37
+ traceRefId: null,
38
+ sourcePath,
39
+ lastCapturedOffset: null,
40
+ lastCapturedLine: null,
41
+ lastCapturedEventId: null,
42
+ updatedAt: now,
43
+ };
44
+ }
45
+ }
46
+
47
+ export async function appendRawEvent(
48
+ path: string,
49
+ event: SessionMemoryRawEventV1,
50
+ ): Promise<{ lineNumber: number }> {
51
+ const existingLineCount = await countJsonlLines(path);
52
+ await mkdir(dirname(path), { recursive: true });
53
+ await appendFile(path, `${JSON.stringify(event)}\n`, "utf8");
54
+ return { lineNumber: existingLineCount + 1 };
55
+ }
56
+
57
+ export async function readLineRange(
58
+ path: string,
59
+ fromLine: number,
60
+ toLine: number,
61
+ ): Promise<{ content: string; truncated: boolean; secretsDetected: boolean }> {
62
+ const text = await readFile(path, "utf8");
63
+ const lines = text.split("\n").filter((line) => line.trim() !== "");
64
+ const selected = lines.slice(Math.max(0, fromLine - 1), toLine);
65
+ let secretsDetected = false;
66
+ for (const line of selected) {
67
+ try {
68
+ const parsed = JSON.parse(line) as Partial<SessionMemoryRawEventV1>;
69
+ secretsDetected = secretsDetected || parsed.secretsDetected === true;
70
+ } catch {
71
+ secretsDetected = secretsDetected || SENSITIVE_TEXT_PATTERN.test(line);
72
+ }
73
+ }
74
+ return { content: selected.join("\n"), truncated: false, secretsDetected };
75
+ }
76
+
77
+ export async function writeSessionIndex(
78
+ paths: SessionMemoryPaths,
79
+ state: SessionMemoryStateV1,
80
+ segment: SessionEvidenceSegmentV1,
81
+ now: string,
82
+ ): Promise<void> {
83
+ const existing = await readSessionIndex(paths.indexPath);
84
+ const segments = [
85
+ ...existing.segments.filter((item) => item.id !== segment.id),
86
+ {
87
+ id: segment.id,
88
+ reason: segment.reason,
89
+ strength: segment.strength,
90
+ createdAt: segment.createdAt,
91
+ reviewState: segment.lifecycle.reviewState,
92
+ },
93
+ ];
94
+ await writeJson(paths.indexPath, {
95
+ schemaVersion: 1,
96
+ kind: "session-memory-index",
97
+ projectKey: state.projectKey,
98
+ sessionKey: state.sessionKey,
99
+ updatedAt: now,
100
+ lastSegmentId: segment.id,
101
+ segments,
102
+ });
103
+ }
104
+
105
+ export async function writeJson(path: string, value: unknown): Promise<void> {
106
+ await mkdir(dirname(path), { recursive: true });
107
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
108
+ }
109
+
110
+ async function countJsonlLines(path: string): Promise<number> {
111
+ try {
112
+ const text = await readFile(path, "utf8");
113
+ return text.split("\n").filter((line) => line.trim() !== "").length;
114
+ } catch (error) {
115
+ if (isNotFoundError(error)) return 0;
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ async function readSessionIndex(path: string): Promise<{ segments: SessionMemoryIndexSegment[] }> {
121
+ try {
122
+ const parsed = JSON.parse(await readFile(path, "utf8")) as {
123
+ segments?: unknown;
124
+ };
125
+ if (!Array.isArray(parsed.segments)) return { segments: [] };
126
+ return {
127
+ segments: parsed.segments.filter(isSessionIndexSegment),
128
+ };
129
+ } catch (error) {
130
+ if (isNotFoundError(error)) return { segments: [] };
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ function isSessionIndexSegment(value: unknown): value is SessionMemoryIndexSegment {
136
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
137
+ const item = value as Record<string, unknown>;
138
+ return (
139
+ typeof item.id === "string" &&
140
+ typeof item.reason === "string" &&
141
+ typeof item.strength === "string" &&
142
+ typeof item.createdAt === "string" &&
143
+ typeof item.reviewState === "string"
144
+ );
145
+ }