@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.11

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 (66) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
  2. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  3. package/assets/team/agents/code-reviewer.md +48 -0
  4. package/assets/team/agents/docs-maintainer.md +51 -0
  5. package/assets/team/agents/implementation-engineer.md +51 -0
  6. package/assets/team/agents/product-scope-analyst.md +58 -0
  7. package/assets/team/agents/release-engineer.md +55 -0
  8. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  9. package/assets/team/agents/solution-architect.md +51 -0
  10. package/assets/team/agents/verification-engineer.md +51 -0
  11. package/assets/team/team.md +102 -0
  12. package/dist/config/index.js +925 -97
  13. package/dist/index.js +13107 -5618
  14. package/package.json +5 -1
  15. package/src/agents/index.ts +56 -264
  16. package/src/code-agent-traces/index.ts +520 -0
  17. package/src/config/index.ts +5 -0
  18. package/src/config/paths.ts +1 -1
  19. package/src/config/settings.ts +149 -0
  20. package/src/config/store.ts +2 -0
  21. package/src/daemon/index.ts +99 -50
  22. package/src/evolution/candidates/index.ts +564 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +7 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  32. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  33. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  34. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  35. package/src/evolution/evidence/session-memory/types.ts +221 -0
  36. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  37. package/src/evolution/formatters.ts +169 -0
  38. package/src/evolution/index.ts +16 -2356
  39. package/src/evolution/knowledge/index.ts +5427 -0
  40. package/src/evolution/paths.ts +44 -0
  41. package/src/evolution/processor/distillation.ts +518 -0
  42. package/src/evolution/processor/index.ts +3 -0
  43. package/src/evolution/processor/process.ts +528 -0
  44. package/src/{learning → evolution/review}/index.ts +10 -14
  45. package/src/evolution/schema.ts +568 -0
  46. package/src/evolution/shared.ts +758 -0
  47. package/src/evolution/triggers/classification.ts +102 -0
  48. package/src/evolution/triggers/index.ts +295 -0
  49. package/src/hooks/index.ts +438 -179
  50. package/src/index.ts +12 -3
  51. package/src/projects/index.ts +453 -0
  52. package/src/runtime-logs/index.ts +490 -24
  53. package/src/team/index.ts +1429 -185
  54. package/src/team/mcp.ts +9 -5
  55. package/src/team/prompts.ts +141 -0
  56. package/src/utils/errors.ts +13 -0
  57. package/src/utils/fs.ts +40 -0
  58. package/src/utils/hash.ts +9 -0
  59. package/src/utils/ids.ts +12 -0
  60. package/src/utils/index.ts +7 -0
  61. package/src/utils/parsing.ts +11 -0
  62. package/src/utils/text.ts +18 -0
  63. package/src/utils/time.ts +5 -0
  64. package/src/workflow/index.ts +3 -21
  65. package/src/project/index.ts +0 -507
  66. package/src/task/index.ts +0 -840
@@ -0,0 +1,281 @@
1
+ import { createStableId, normalizeTimestamp } from "../../../utils/index.ts";
2
+ import type {
3
+ EvolutionEpisode,
4
+ EvolutionEvidenceEvent,
5
+ EvolutionEvidenceEventKind,
6
+ EvolutionEvidenceWindow,
7
+ EvolutionTriggerReason,
8
+ EvolutionTriggerStrength,
9
+ } from "../../schema.ts";
10
+ import {
11
+ MAX_EVIDENCE_EVENTS,
12
+ createPrivacyFields,
13
+ displayPath,
14
+ sanitizeId,
15
+ sanitizeOptionalId,
16
+ sanitizeText,
17
+ validateEvolutionEvidenceWindow,
18
+ } from "../../shared.ts";
19
+ import {
20
+ createTriggerPolicy,
21
+ decideEvolutionTrigger,
22
+ normalizeEvolutionEventType,
23
+ strongestTrigger,
24
+ } from "../../triggers/classification.ts";
25
+ import { resolveSessionMemoryPaths } from "./paths.ts";
26
+ import { redactSessionMemoryCredentialText } from "./sensitivity.ts";
27
+ import type {
28
+ SessionEvidenceSegmentV1,
29
+ SessionMemorySegmentReason,
30
+ SessionMemorySignal,
31
+ SessionMemorySignalStrength,
32
+ } from "./types.ts";
33
+
34
+ export type SessionSegmentDistillationSkipReason =
35
+ | "sensitive-segment"
36
+ | "segment-deleted"
37
+ | "segment-ignored"
38
+ | "segment-rejected"
39
+ | "no-distillation-signal";
40
+
41
+ export interface SessionSegmentEvidenceAnalysis {
42
+ evidenceWindow: EvolutionEvidenceWindow | null;
43
+ skipReason: SessionSegmentDistillationSkipReason | null;
44
+ warnings: string[];
45
+ }
46
+
47
+ /**
48
+ * Converts a stored Session Memory segment into the metadata-only evidence contract consumed by
49
+ * the existing distillation and OKF activation pipeline. Raw excerpts are deliberately excluded.
50
+ */
51
+ export function analyzeSessionEvidenceSegment(input: {
52
+ homeDir: string;
53
+ segment: SessionEvidenceSegmentV1;
54
+ now?: string | Date;
55
+ }): SessionSegmentEvidenceAnalysis {
56
+ const { segment } = input;
57
+ if (segment.lifecycle.status === "deleted") return skipped("segment-deleted");
58
+ if (segment.lifecycle.status === "ignored") return skipped("segment-ignored");
59
+ if (segment.lifecycle.reviewState === "rejected") return skipped("segment-rejected");
60
+
61
+ const roleId = resolveSegmentRoleId(segment);
62
+ const sourceRefId = "source-session-segment";
63
+ const episodeId = "episode-session-0001";
64
+ const signals = segment.signals.slice(0, MAX_EVIDENCE_EVENTS);
65
+ const events = signals.map((signal, index) =>
66
+ createSegmentEvidenceEvent({
67
+ signal,
68
+ segmentReason: segment.reason,
69
+ segmentStrength: segment.strength,
70
+ roleId,
71
+ sourceRefId,
72
+ episodeId,
73
+ sequence: index + 1,
74
+ }),
75
+ );
76
+ if (!isSegmentEligibleForDistillation(segment, events)) {
77
+ return skipped("no-distillation-signal");
78
+ }
79
+
80
+ const strongest = strongestTrigger(events);
81
+ const episode: EvolutionEpisode = {
82
+ id: episodeId,
83
+ kind: "session",
84
+ status: events.some((event) => event.kind === "error") ? "failed" : "closed",
85
+ roleId,
86
+ taskId: null,
87
+ startedAt: events[0]?.occurredAt ?? segment.createdAt,
88
+ endedAt: events.at(-1)?.occurredAt ?? segment.createdAt,
89
+ eventIds: events.map((event) => event.id),
90
+ triggerStrength: strongest.strength,
91
+ triggerReason: strongest.reason,
92
+ summary: `Session Memory segment ${segment.reason} contains ${events.length} normalized metadata signal(s).`,
93
+ rawContentStored: false,
94
+ };
95
+ const paths = resolveSessionMemoryPaths({
96
+ homeDir: input.homeDir,
97
+ projectKey: segment.projectKey,
98
+ sessionKey: segment.sessionKey,
99
+ });
100
+ const evidenceWindow: EvolutionEvidenceWindow = {
101
+ schemaVersion: 1,
102
+ id: createStableId("segment-evidence", [segment.id]),
103
+ kind: "evidence-window",
104
+ projectKey: sanitizeId(segment.projectKey),
105
+ // A segment needs its own run namespace so multiple segments from one native/team session do
106
+ // not overwrite one another's evidence window, plan, or distillation batch.
107
+ runId: createStableId("segment-run", [segment.projectKey, segment.sessionKey, segment.id]),
108
+ taskId: null,
109
+ createdAt: normalizeTimestamp(input.now),
110
+ sourceRefs: [
111
+ {
112
+ id: sourceRefId,
113
+ kind: "session-memory-segment",
114
+ path: displayPath(input.homeDir, paths.segmentPath(segment.id)),
115
+ roleId,
116
+ rawContentStored: false,
117
+ externalContentCopied: false,
118
+ },
119
+ ],
120
+ events,
121
+ episodes: [episode],
122
+ triggerPolicy: createTriggerPolicy(events),
123
+ privacy: createPrivacyFields({ events, episodes: [episode] }),
124
+ };
125
+ validateEvolutionEvidenceWindow(evidenceWindow);
126
+ return {
127
+ evidenceWindow,
128
+ skipReason: null,
129
+ warnings:
130
+ segment.signals.length > MAX_EVIDENCE_EVENTS
131
+ ? [
132
+ `Session segment metadata signals were truncated from ${segment.signals.length} to ${MAX_EVIDENCE_EVENTS}.`,
133
+ ]
134
+ : [],
135
+ };
136
+ }
137
+
138
+ function skipped(skipReason: SessionSegmentDistillationSkipReason): SessionSegmentEvidenceAnalysis {
139
+ return { evidenceWindow: null, skipReason, warnings: [] };
140
+ }
141
+
142
+ function resolveSegmentRoleId(segment: SessionEvidenceSegmentV1): string | null {
143
+ const explicitRoleId = sanitizeOptionalId(segment.roleId);
144
+ if (explicitRoleId !== null) return explicitRoleId;
145
+ // Ordinary non-team sessions still need a bounded role scope for OKF policy. The configured Code
146
+ // Agent target is a real session attribute; unknown targets remain unscoped and cannot auto-accept.
147
+ return segment.target === "unknown" ? null : sanitizeId(segment.target);
148
+ }
149
+
150
+ function createSegmentEvidenceEvent(input: {
151
+ signal: SessionMemorySignal;
152
+ segmentReason: SessionMemorySegmentReason;
153
+ segmentStrength: SessionMemorySignalStrength;
154
+ roleId: string | null;
155
+ sourceRefId: string;
156
+ episodeId: string;
157
+ sequence: number;
158
+ }): EvolutionEvidenceEvent {
159
+ const eventType = normalizeEvolutionEventType(input.signal.eventType);
160
+ const summary =
161
+ sanitizeText(redactSessionMemoryCredentialText(input.signal.summary).value) ||
162
+ "Session metadata signal observed.";
163
+ const kind = classifySegmentSignal(input.signal, summary);
164
+ const baseTrigger = decideEvolutionTrigger({ eventType, summary, kind });
165
+ const segmentTrigger =
166
+ input.signal.reason === "observed"
167
+ ? null
168
+ : classifySegmentTrigger(input.segmentReason, input.segmentStrength);
169
+ const trigger = segmentTrigger ?? baseTrigger;
170
+ return {
171
+ id: `event-${String(input.sequence).padStart(4, "0")}`,
172
+ kind,
173
+ eventType,
174
+ hookEventId: sanitizeOptionalId(input.signal.eventId),
175
+ occurredAt: input.signal.occurredAt,
176
+ summary,
177
+ roleId: input.roleId,
178
+ taskId: null,
179
+ evidenceRef: `${input.sourceRefId}#signal-${input.sequence}`,
180
+ episodeIds: [input.episodeId],
181
+ triggerStrength: trigger.strength,
182
+ triggerReason: trigger.reason,
183
+ rawContentStored: false,
184
+ };
185
+ }
186
+
187
+ function classifySegmentSignal(
188
+ signal: SessionMemorySignal,
189
+ summary: string,
190
+ ): EvolutionEvidenceEventKind {
191
+ const eventType = normalizeEvolutionEventType(signal.eventType);
192
+ if (
193
+ eventType === "StopFailure" ||
194
+ eventType === "PostToolUseFailure" ||
195
+ eventType === "PermissionDenied" ||
196
+ signal.reason === "failure-signal" ||
197
+ signal.reason === "permission-denied"
198
+ ) {
199
+ return "error";
200
+ }
201
+ if (
202
+ signal.reason === "verification-after-fix" ||
203
+ /verify|test|lint|typecheck|build/i.test(summary)
204
+ ) {
205
+ return "verification";
206
+ }
207
+ if (
208
+ signal.reason === "user-interruption" ||
209
+ signal.reason === "intent-refinement" ||
210
+ signal.reason === "explicit-memory-intent"
211
+ ) {
212
+ return "user-feedback";
213
+ }
214
+ if (
215
+ eventType === "PreToolUse" ||
216
+ eventType === "PostToolUse" ||
217
+ eventType === "PermissionRequest" ||
218
+ eventType === "PostToolBatch"
219
+ ) {
220
+ return "tool-call";
221
+ }
222
+ if (eventType === "SubagentStart" || eventType === "SubagentStop") return "subagent";
223
+ if (/skill/i.test(summary)) return "skill-call";
224
+ if (/review/i.test(summary)) return "review";
225
+ if (
226
+ eventType === "SessionStart" ||
227
+ eventType === "SessionEnd" ||
228
+ eventType === "UserPromptSubmit" ||
229
+ eventType === "Stop" ||
230
+ eventType === "TaskCreated" ||
231
+ eventType === "TaskCompleted"
232
+ ) {
233
+ return "run-state";
234
+ }
235
+ return "trace";
236
+ }
237
+
238
+ function classifySegmentTrigger(
239
+ reason: SessionMemorySegmentReason,
240
+ strength: SessionMemorySignalStrength,
241
+ ): { strength: EvolutionTriggerStrength; reason: EvolutionTriggerReason } {
242
+ if (reason === "failure-signal") return { strength: "strong", reason: "failure-signal" };
243
+ if (reason === "permission-denied") return { strength: "strong", reason: "permission-denied" };
244
+ if (reason === "session-memory-init") return { strength: "none", reason: "none" };
245
+ return {
246
+ strength: strength === "strong" ? "strong" : "conditional",
247
+ reason: "explicit-command",
248
+ };
249
+ }
250
+
251
+ function isSegmentEligibleForDistillation(
252
+ segment: SessionEvidenceSegmentV1,
253
+ events: EvolutionEvidenceEvent[],
254
+ ): boolean {
255
+ if (events.length === 0) return false;
256
+ if (
257
+ segment.reason === "user-interruption" ||
258
+ segment.reason === "failure-signal" ||
259
+ segment.reason === "permission-denied" ||
260
+ segment.reason === "verification-after-fix" ||
261
+ segment.reason === "explicit-memory-intent"
262
+ ) {
263
+ return true;
264
+ }
265
+
266
+ const hasOutcomeSignal =
267
+ segment.normalized.decisions.length > 0 ||
268
+ segment.normalized.failures.length > 0 ||
269
+ segment.normalized.verifications.length > 0 ||
270
+ events.some((event) => event.kind === "error" || event.kind === "verification");
271
+ if (segment.reason === "session-memory-threshold") return hasOutcomeSignal;
272
+ if (segment.reason !== "intent-refinement") return false;
273
+ return (
274
+ hasOutcomeSignal ||
275
+ segment.signals.some(
276
+ (signal) =>
277
+ signal.reason === "explicit-memory-intent" ||
278
+ ["TaskCompleted", "Stop", "SessionEnd", "SubagentStop"].includes(signal.eventType),
279
+ )
280
+ );
281
+ }
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_MAX_RAW_EVENT_BYTES = 64 * 1024;
2
+
3
+ export const EXPLICIT_MEMORY_PATTERN =
4
+ /\b(remember|memorize|do not repeat|don't repeat|this is important|save this|learn this)\b/i;
5
+
6
+ export const FAILURE_PATTERN = /\b(fail|failed|failure|error|issue|exception|crash)\b/i;
7
+
8
+ export const VERIFICATION_PATTERN =
9
+ /\b(test|lint|typecheck|build|verify|verification|passed|success)\b/i;
@@ -0,0 +1,7 @@
1
+ export * from "./analysis.ts";
2
+ export * from "./paths.ts";
3
+ export * from "./policy.ts";
4
+ export * from "./sensitivity.ts";
5
+ export * from "./storage.ts";
6
+ export * from "./types.ts";
7
+ export * from "./updater.ts";
@@ -0,0 +1,29 @@
1
+ import { join } from "node:path";
2
+ import { resolveEvoDevPaths } from "../../../config/paths.ts";
3
+ import { sanitizeStorageId } from "../../../utils/index.ts";
4
+ import type { SessionMemoryPaths } from "./types.ts";
5
+
6
+ export function resolveSessionMemoryPaths(input: {
7
+ homeDir: string;
8
+ projectKey: string;
9
+ sessionKey: string;
10
+ }): SessionMemoryPaths {
11
+ const rootDir = join(resolveEvoDevPaths(input.homeDir).stateDir, "session-memory");
12
+ const projectKey = sanitizeStorageId(input.projectKey, "project");
13
+ const sessionKey = sanitizeStorageId(input.sessionKey, "session");
14
+ const projectDir = join(rootDir, projectKey);
15
+ const sessionDir = join(projectDir, sessionKey);
16
+ const segmentsDir = join(sessionDir, "segments");
17
+ return {
18
+ rootDir,
19
+ projectDir,
20
+ sessionDir,
21
+ statePath: join(sessionDir, "state.json"),
22
+ cursorPath: join(sessionDir, "cursor.json"),
23
+ eventsPath: join(sessionDir, "events.jsonl"),
24
+ segmentsDir,
25
+ segmentPath: (segmentId: string) =>
26
+ join(segmentsDir, `${sanitizeStorageId(segmentId, "segment")}.json`),
27
+ indexPath: join(sessionDir, "index.json"),
28
+ };
29
+ }
@@ -0,0 +1,39 @@
1
+ import { optionalBoolean, positiveInteger } from "../../../utils/index.ts";
2
+ import type { SessionMemoryPolicySnapshot } from "./types.ts";
3
+
4
+ export function createDefaultSessionMemoryPolicy(): SessionMemoryPolicySnapshot {
5
+ return {
6
+ enabled: true,
7
+ storeRawSegments: true,
8
+ maxRawSegmentBytes: 200_000,
9
+ retentionDays: 30,
10
+ minimumMessageTokensToInit: 10_000,
11
+ minimumTokensBetweenUpdate: 5_000,
12
+ toolCallsBetweenUpdates: 9,
13
+ };
14
+ }
15
+
16
+ export function parseSessionMemoryPolicy(
17
+ value: Partial<SessionMemoryPolicySnapshot> | undefined,
18
+ ): SessionMemoryPolicySnapshot {
19
+ const defaults = createDefaultSessionMemoryPolicy();
20
+ if (value === undefined) return defaults;
21
+ return {
22
+ enabled: optionalBoolean(value.enabled, defaults.enabled),
23
+ storeRawSegments: optionalBoolean(value.storeRawSegments, defaults.storeRawSegments),
24
+ maxRawSegmentBytes: positiveInteger(value.maxRawSegmentBytes, defaults.maxRawSegmentBytes),
25
+ retentionDays: positiveInteger(value.retentionDays, defaults.retentionDays),
26
+ minimumMessageTokensToInit: positiveInteger(
27
+ value.minimumMessageTokensToInit,
28
+ defaults.minimumMessageTokensToInit,
29
+ ),
30
+ minimumTokensBetweenUpdate: positiveInteger(
31
+ value.minimumTokensBetweenUpdate,
32
+ defaults.minimumTokensBetweenUpdate,
33
+ ),
34
+ toolCallsBetweenUpdates: positiveInteger(
35
+ value.toolCallsBetweenUpdates,
36
+ defaults.toolCallsBetweenUpdates,
37
+ ),
38
+ };
39
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ createStableId,
3
+ sanitizeStorageId,
4
+ sanitizeSummary,
5
+ sha256Hex,
6
+ truncateUtf8Tail,
7
+ } from "../../../utils/index.ts";
8
+ import { DEFAULT_MAX_RAW_EVENT_BYTES, VERIFICATION_PATTERN } from "./constants.ts";
9
+ import {
10
+ detectSessionMemorySensitivity,
11
+ redactSessionMemoryCredentialText,
12
+ redactSessionMemoryCredentials,
13
+ } from "./sensitivity.ts";
14
+ import { readLineRange } from "./storage.ts";
15
+ import type {
16
+ SessionEvidenceSegmentV1,
17
+ SessionMemoryCursorV1,
18
+ SessionMemoryPaths,
19
+ SessionMemoryRawEventV1,
20
+ SessionMemorySegmentReason,
21
+ SessionMemorySignal,
22
+ SessionMemorySignalStrength,
23
+ SessionMemoryStateV1,
24
+ UpdateSessionMemoryInput,
25
+ } from "./types.ts";
26
+
27
+ export function createRawEvent(
28
+ input: UpdateSessionMemoryInput,
29
+ now: string,
30
+ ): SessionMemoryRawEventV1 {
31
+ const originalRawJson = stringifyPayload(input.rawPayload);
32
+ const redactedPayload = redactSessionMemoryCredentials(input.rawPayload);
33
+ const redactedSummary = redactSessionMemoryCredentialText(input.event.payload.summary);
34
+ const rawJson = stringifyPayload(redactedPayload.value);
35
+ const truncated = truncateUtf8Tail(rawJson, DEFAULT_MAX_RAW_EVENT_BYTES);
36
+ const sensitivity = detectSessionMemorySensitivity(redactedPayload.value);
37
+ return {
38
+ schemaVersion: 1,
39
+ kind: "session-memory-raw-event",
40
+ eventId: input.event.eventId,
41
+ eventType: input.event.type,
42
+ occurredAt: input.event.time.occurredAt,
43
+ receivedAt: now,
44
+ summary: sanitizeSummary(redactedSummary.value),
45
+ rawPayloadJson: truncated.content,
46
+ rawPayloadByteLength: Buffer.byteLength(originalRawJson, "utf8"),
47
+ rawPayloadTruncated: truncated.truncated,
48
+ secretsDetected: sensitivity.classification === "credential",
49
+ sensitivity: sensitivity.classification,
50
+ sensitivityReasons: sensitivity.reasons,
51
+ credentialRedactionApplied: redactedPayload.redacted || redactedSummary.redacted,
52
+ };
53
+ }
54
+
55
+ export function createSignal(
56
+ input: UpdateSessionMemoryInput,
57
+ rawEvent: SessionMemoryRawEventV1,
58
+ rawEventLine: number,
59
+ ): SessionMemorySignal {
60
+ return {
61
+ eventId: input.event.eventId,
62
+ eventType: input.event.type,
63
+ reason: "observed",
64
+ strength: "normal",
65
+ occurredAt: input.event.time.occurredAt,
66
+ summary: rawEvent.summary,
67
+ rawEventLine,
68
+ };
69
+ }
70
+
71
+ export async function createSegment(input: {
72
+ paths: SessionMemoryPaths;
73
+ state: SessionMemoryStateV1;
74
+ cursor: SessionMemoryCursorV1;
75
+ reason: SessionMemorySegmentReason;
76
+ strength: SessionMemorySignalStrength;
77
+ signals: SessionMemorySignal[];
78
+ currentLine: number;
79
+ now: string;
80
+ }): Promise<SessionEvidenceSegmentV1> {
81
+ const fromLine = (input.cursor.lastCapturedLine ?? 0) + 1;
82
+ const toLine = input.currentLine;
83
+ const raw = await readLineRange(input.paths.eventsPath, fromLine, toLine);
84
+ const truncated = truncateUtf8Tail(raw.content, input.state.policy.maxRawSegmentBytes);
85
+ const signalSummaries = input.signals.map((signal) => signal.summary).filter(Boolean);
86
+ const failures = input.signals
87
+ .filter((signal) => signal.reason === "failure-signal")
88
+ .map((signal) => signal.summary);
89
+ const verifications = input.signals
90
+ .filter(
91
+ (signal) =>
92
+ signal.reason === "verification-after-fix" || VERIFICATION_PATTERN.test(signal.summary),
93
+ )
94
+ .map((signal) => signal.summary);
95
+ const segmentId = createStableId("segment", [
96
+ input.state.projectKey,
97
+ input.state.sessionKey,
98
+ input.reason,
99
+ String(fromLine),
100
+ String(toLine),
101
+ input.signals.at(-1)?.eventId ?? input.now,
102
+ ]);
103
+ return {
104
+ schemaVersion: 1,
105
+ kind: "session-evidence-segment",
106
+ id: segmentId,
107
+ projectKey: input.state.projectKey,
108
+ runId: input.state.runId,
109
+ roleId: input.state.roleId,
110
+ sessionKey: input.state.sessionKey,
111
+ target: input.state.target,
112
+ createdAt: input.now,
113
+ reason: input.reason,
114
+ strength: input.strength,
115
+ source: {
116
+ traceRefId: input.cursor.traceRefId,
117
+ sourcePath: input.paths.eventsPath,
118
+ fromOffset: null,
119
+ toOffset: null,
120
+ fromLine,
121
+ toLine,
122
+ fromEventId: input.signals[0]?.eventId ?? null,
123
+ toEventId: input.signals.at(-1)?.eventId ?? null,
124
+ },
125
+ signals: input.signals,
126
+ rawExcerpt: {
127
+ stored: true,
128
+ encoding: "utf8",
129
+ content: truncated.content,
130
+ truncated: raw.truncated || truncated.truncated,
131
+ byteLength: Buffer.byteLength(truncated.content, "utf8"),
132
+ sha256: sha256Hex(truncated.content),
133
+ },
134
+ normalized: {
135
+ summary: createSegmentSummary(input.reason, input.strength, signalSummaries),
136
+ userIntent: firstUserPromptSummary(input.signals),
137
+ intentDelta: intentDeltaSummary(input.signals),
138
+ decisions: [],
139
+ failures,
140
+ verifications,
141
+ touchedTools: collectTouchedTools(raw.content),
142
+ },
143
+ privacy: {
144
+ localOnly: true,
145
+ rawPromptStored: input.signals.some((signal) => signal.eventType === "UserPromptSubmit"),
146
+ rawOutputStored: input.signals.some((signal) =>
147
+ ["PostToolUse", "PostToolUseFailure", "PostToolBatch"].includes(signal.eventType),
148
+ ),
149
+ sourceContentStored: true,
150
+ secretsDetected: raw.secretsDetected,
151
+ sensitivity: raw.sensitivity,
152
+ sensitivityReasons: raw.sensitivityReasons,
153
+ redactionApplied: raw.redactionApplied,
154
+ externalUploadAllowed: false,
155
+ },
156
+ lifecycle: {
157
+ status: "captured",
158
+ reviewState: "not-required",
159
+ consumedByBatchIds: [],
160
+ },
161
+ };
162
+ }
163
+
164
+ export function estimateTokenCount(rawPayloadJson: string, summary: string): number {
165
+ return Math.max(1, Math.ceil((rawPayloadJson.length + summary.length) / 4));
166
+ }
167
+
168
+ function createSegmentSummary(
169
+ reason: SessionMemorySegmentReason,
170
+ strength: SessionMemorySignalStrength,
171
+ summaries: string[],
172
+ ): string {
173
+ const suffix = summaries.length === 0 ? "No event summary." : summaries.slice(-3).join(" / ");
174
+ return sanitizeSummary(`Session segment ${reason} (${strength}): ${suffix}`);
175
+ }
176
+
177
+ function firstUserPromptSummary(signals: SessionMemorySignal[]): string | null {
178
+ return signals.find((signal) => signal.eventType === "UserPromptSubmit")?.summary ?? null;
179
+ }
180
+
181
+ function intentDeltaSummary(signals: SessionMemorySignal[]): string | null {
182
+ const prompts = signals.filter((signal) => signal.eventType === "UserPromptSubmit");
183
+ if (prompts.length < 2) return null;
184
+ return prompts.at(-1)?.summary ?? null;
185
+ }
186
+
187
+ function collectTouchedTools(rawContent: string): string[] {
188
+ const tools = new Set<string>();
189
+ for (const match of rawContent.matchAll(/"tool(?:Name|_name)"\s*:\s*"([^"]+)"/g)) {
190
+ const tool = match[1]?.trim();
191
+ if (tool !== undefined && tool !== "") tools.add(sanitizeStorageId(tool, "tool"));
192
+ }
193
+ return [...tools].slice(0, 20);
194
+ }
195
+
196
+ function stringifyPayload(payload: Record<string, unknown>): string {
197
+ try {
198
+ return JSON.stringify(payload);
199
+ } catch {
200
+ return JSON.stringify({ unserializable: true });
201
+ }
202
+ }