@evo-dev/core 0.0.1-alpha.2 → 0.0.1-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +5 -3
  2. package/assets/team/agents/code-reviewer.md +48 -0
  3. package/assets/team/agents/docs-maintainer.md +51 -0
  4. package/assets/team/agents/implementation-engineer.md +51 -0
  5. package/assets/team/agents/product-scope-analyst.md +58 -0
  6. package/assets/team/agents/release-engineer.md +55 -0
  7. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  8. package/assets/team/agents/solution-architect.md +51 -0
  9. package/assets/team/agents/verification-engineer.md +51 -0
  10. package/assets/team/team.md +102 -0
  11. package/dist/assets/index.js +5 -5
  12. package/dist/config/index.js +793 -241
  13. package/dist/index.js +20840 -12908
  14. package/dist/plugins/index.js +13 -13
  15. package/package.json +1 -1
  16. package/src/agents/index.ts +1 -265
  17. package/src/code-agent-traces/index.ts +11 -12
  18. package/src/config/index.ts +2 -0
  19. package/src/config/settings.ts +116 -7
  20. package/src/config/store.ts +1 -1
  21. package/src/daemon/index.ts +1 -41
  22. package/src/evolution/candidates/index.ts +730 -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 +287 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +9 -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/retention.ts +643 -0
  32. package/src/evolution/evidence/session-memory/segment.ts +216 -0
  33. package/src/evolution/evidence/session-memory/semantic-packet.ts +408 -0
  34. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  35. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  36. package/src/evolution/evidence/session-memory/storage.ts +744 -0
  37. package/src/evolution/evidence/session-memory/types.ts +296 -0
  38. package/src/evolution/evidence/session-memory/updater.ts +199 -0
  39. package/src/evolution/formatters.ts +169 -0
  40. package/src/evolution/imports/apply.ts +435 -0
  41. package/src/evolution/imports/diff.ts +472 -0
  42. package/src/evolution/imports/index.ts +7 -0
  43. package/src/evolution/imports/materialize.ts +640 -0
  44. package/src/evolution/imports/paths.ts +129 -0
  45. package/src/evolution/imports/stage.ts +414 -0
  46. package/src/evolution/imports/storage.ts +952 -0
  47. package/src/evolution/imports/types.ts +226 -0
  48. package/src/evolution/index.ts +19 -2827
  49. package/src/evolution/knowledge/change-store.ts +558 -0
  50. package/src/evolution/knowledge/changes.ts +459 -0
  51. package/src/evolution/knowledge/freshness.ts +69 -0
  52. package/src/{knowledge → evolution/knowledge}/index.ts +1532 -206
  53. package/src/evolution/knowledge/review.ts +446 -0
  54. package/src/evolution/knowledge/support.ts +135 -0
  55. package/src/evolution/paths.ts +44 -0
  56. package/src/evolution/processor/distillation.ts +518 -0
  57. package/src/evolution/processor/index.ts +3 -0
  58. package/src/evolution/processor/process.ts +594 -0
  59. package/src/{learning → evolution/review}/index.ts +10 -14
  60. package/src/evolution/schema.ts +639 -0
  61. package/src/evolution/shared.ts +1053 -0
  62. package/src/evolution/triggers/classification.ts +102 -0
  63. package/src/evolution/triggers/index.ts +295 -0
  64. package/src/hooks/index.ts +281 -197
  65. package/src/index.ts +15 -4
  66. package/src/projects/index.ts +934 -0
  67. package/src/runtime-logs/index.ts +100 -13
  68. package/src/team/index.ts +582 -3
  69. package/src/utils/errors.ts +13 -0
  70. package/src/utils/fs.ts +40 -0
  71. package/src/utils/hash.ts +9 -0
  72. package/src/utils/ids.ts +12 -0
  73. package/src/utils/index.ts +7 -0
  74. package/src/utils/parsing.ts +11 -0
  75. package/src/utils/text.ts +18 -0
  76. package/src/utils/time.ts +5 -0
  77. package/src/workflow/index.ts +3 -21
  78. package/src/project/index.ts +0 -507
  79. package/src/task/index.ts +0 -840
@@ -0,0 +1,216 @@
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
+ const rawSha256 = sha256Hex(truncated.content);
104
+ return {
105
+ schemaVersion: 1,
106
+ kind: "session-evidence-segment",
107
+ id: segmentId,
108
+ projectKey: input.state.projectKey,
109
+ runId: input.state.runId,
110
+ roleId: input.state.roleId,
111
+ sessionKey: input.state.sessionKey,
112
+ target: input.state.target,
113
+ createdAt: input.now,
114
+ reason: input.reason,
115
+ strength: input.strength,
116
+ retention: {
117
+ policyDays: input.state.policy.retentionDays,
118
+ expiresAt: addDays(input.now, input.state.policy.retentionDays),
119
+ rawState: "available",
120
+ rawPurgedAt: null,
121
+ originalRawSha256: rawSha256,
122
+ },
123
+ source: {
124
+ traceRefId: input.cursor.traceRefId,
125
+ sourcePath: input.paths.eventsPath,
126
+ fromOffset: null,
127
+ toOffset: null,
128
+ fromLine,
129
+ toLine,
130
+ fromEventId: input.signals[0]?.eventId ?? null,
131
+ toEventId: input.signals.at(-1)?.eventId ?? null,
132
+ },
133
+ signals: input.signals,
134
+ rawExcerpt: {
135
+ stored: true,
136
+ encoding: "utf8",
137
+ content: truncated.content,
138
+ truncated: raw.truncated || truncated.truncated,
139
+ byteLength: Buffer.byteLength(truncated.content, "utf8"),
140
+ sha256: rawSha256,
141
+ },
142
+ normalized: {
143
+ summary: createSegmentSummary(input.reason, input.strength, signalSummaries),
144
+ userIntent: firstUserPromptSummary(input.signals),
145
+ intentDelta: intentDeltaSummary(input.signals),
146
+ decisions: [],
147
+ failures,
148
+ verifications,
149
+ touchedTools: collectTouchedTools(raw.content),
150
+ },
151
+ privacy: {
152
+ localOnly: true,
153
+ rawPromptStored: input.signals.some((signal) => signal.eventType === "UserPromptSubmit"),
154
+ rawOutputStored: input.signals.some((signal) =>
155
+ ["PostToolUse", "PostToolUseFailure", "PostToolBatch"].includes(signal.eventType),
156
+ ),
157
+ sourceContentStored: true,
158
+ secretsDetected: raw.secretsDetected,
159
+ sensitivity: raw.sensitivity,
160
+ sensitivityReasons: raw.sensitivityReasons,
161
+ redactionApplied: raw.redactionApplied,
162
+ externalUploadAllowed: false,
163
+ },
164
+ lifecycle: {
165
+ status: "captured",
166
+ reviewState: "not-required",
167
+ consumedByBatchIds: [],
168
+ },
169
+ };
170
+ }
171
+
172
+ function addDays(value: string, days: number): string {
173
+ const timestamp = Date.parse(value);
174
+ if (!Number.isFinite(timestamp)) throw new Error("Invalid Session Evidence retention timestamp.");
175
+ return new Date(timestamp + days * 24 * 60 * 60 * 1000).toISOString();
176
+ }
177
+
178
+ export function estimateTokenCount(rawPayloadJson: string, summary: string): number {
179
+ return Math.max(1, Math.ceil((rawPayloadJson.length + summary.length) / 4));
180
+ }
181
+
182
+ function createSegmentSummary(
183
+ reason: SessionMemorySegmentReason,
184
+ strength: SessionMemorySignalStrength,
185
+ summaries: string[],
186
+ ): string {
187
+ const suffix = summaries.length === 0 ? "No event summary." : summaries.slice(-3).join(" / ");
188
+ return sanitizeSummary(`Session segment ${reason} (${strength}): ${suffix}`);
189
+ }
190
+
191
+ function firstUserPromptSummary(signals: SessionMemorySignal[]): string | null {
192
+ return signals.find((signal) => signal.eventType === "UserPromptSubmit")?.summary ?? null;
193
+ }
194
+
195
+ function intentDeltaSummary(signals: SessionMemorySignal[]): string | null {
196
+ const prompts = signals.filter((signal) => signal.eventType === "UserPromptSubmit");
197
+ if (prompts.length < 2) return null;
198
+ return prompts.at(-1)?.summary ?? null;
199
+ }
200
+
201
+ function collectTouchedTools(rawContent: string): string[] {
202
+ const tools = new Set<string>();
203
+ for (const match of rawContent.matchAll(/"tool(?:Name|_name)"\s*:\s*"([^"]+)"/g)) {
204
+ const tool = match[1]?.trim();
205
+ if (tool !== undefined && tool !== "") tools.add(sanitizeStorageId(tool, "tool"));
206
+ }
207
+ return [...tools].slice(0, 20);
208
+ }
209
+
210
+ function stringifyPayload(payload: Record<string, unknown>): string {
211
+ try {
212
+ return JSON.stringify(payload);
213
+ } catch {
214
+ return JSON.stringify({ unserializable: true });
215
+ }
216
+ }
@@ -0,0 +1,408 @@
1
+ import { sha256Hex } from "../../../utils/index.ts";
2
+ import {
3
+ detectSessionMemorySensitivity,
4
+ redactSessionMemoryCredentialText,
5
+ } from "./sensitivity.ts";
6
+ import type {
7
+ HistoricalImportStoredRecordV1,
8
+ SemanticEvidencePacketV1,
9
+ SessionEvidenceSegmentV1,
10
+ } from "./types.ts";
11
+
12
+ const DEFAULT_MAX_PACKET_CHARS = 48_000;
13
+ const MAX_CONVERSATION_MESSAGES = 12;
14
+ const MAX_TOOL_EVIDENCE = 12;
15
+ const MAX_TOOL_EXCERPT_CHARS = 4_000;
16
+
17
+ interface ParsedHistoricalRecord {
18
+ stored: HistoricalImportStoredRecordV1;
19
+ canonical: Record<string, unknown> | null;
20
+ redacted: boolean;
21
+ order: number;
22
+ }
23
+
24
+ interface ConversationCandidate {
25
+ role: "user" | "assistant";
26
+ text: string;
27
+ recordKeyHash: string;
28
+ order: number;
29
+ priority: number;
30
+ }
31
+
32
+ interface ToolCallCandidate {
33
+ recordKeyHash: string;
34
+ callId: string | null;
35
+ toolName: string;
36
+ input: string | null;
37
+ inputTruncated: boolean;
38
+ order: number;
39
+ }
40
+
41
+ export function createHistoricalSemanticEvidencePacket(input: {
42
+ segment: SessionEvidenceSegmentV1;
43
+ maxChars?: number;
44
+ }): SemanticEvidencePacketV1 {
45
+ if (input.segment.origin?.kind !== "historical-import") {
46
+ throw new Error("Semantic historical packet requires historical import origin.");
47
+ }
48
+ if (!input.segment.rawExcerpt.stored || input.segment.retention?.rawState === "purged") {
49
+ throw new Error("Historical raw evidence is no longer available.");
50
+ }
51
+ const maxChars = input.maxChars ?? DEFAULT_MAX_PACKET_CHARS;
52
+ if (!Number.isSafeInteger(maxChars) || maxChars < 1_000) {
53
+ throw new Error("Semantic evidence packet budget is too small.");
54
+ }
55
+
56
+ const records = parseHistoricalRecords(input.segment.rawExcerpt.content);
57
+ const conversationCandidates: ConversationCandidate[] = [];
58
+ const toolCalls = new Map<string, ToolCallCandidate>();
59
+ const anonymousToolCalls: ToolCallCandidate[] = [];
60
+ const toolResults: Array<{
61
+ recordKeyHash: string;
62
+ callId: string | null;
63
+ output: string | null;
64
+ outputTruncated: boolean;
65
+ status: string | null;
66
+ exitCode: number | null;
67
+ order: number;
68
+ }> = [];
69
+ let redactionCount = 0;
70
+ let sourceTruncated = input.segment.rawExcerpt.truncated;
71
+
72
+ for (const record of records) {
73
+ redactionCount += record.redacted ? 1 : 0;
74
+ sourceTruncated ||= record.stored.canonicalJsonTruncated;
75
+ const content = readText(record.canonical?.content);
76
+ if (
77
+ content !== null &&
78
+ (record.stored.recordType === "user" || record.stored.recordType === "assistant")
79
+ ) {
80
+ const role = record.stored.recordType;
81
+ conversationCandidates.push({
82
+ role,
83
+ text: content,
84
+ recordKeyHash: record.stored.recordKeyHash,
85
+ order: record.order,
86
+ priority: conversationPriority(role, content, record.order, records.length),
87
+ });
88
+ }
89
+ if (record.stored.recordType === "assistant-tool-call") {
90
+ const calls = Array.isArray(record.canonical?.tool_calls) ? record.canonical.tool_calls : [];
91
+ for (const value of calls) {
92
+ const call = asRecord(value);
93
+ if (call === null) continue;
94
+ const toolName = readText(call.name) ?? "unknown-tool";
95
+ const callId = readText(call.id);
96
+ const inputText = stringifyBoundedToolValue(call.args);
97
+ const candidate: ToolCallCandidate = {
98
+ recordKeyHash: record.stored.recordKeyHash,
99
+ callId,
100
+ toolName,
101
+ input: inputText.value,
102
+ inputTruncated: inputText.truncated,
103
+ order: record.order,
104
+ };
105
+ if (callId === null) anonymousToolCalls.push(candidate);
106
+ else toolCalls.set(callId, candidate);
107
+ }
108
+ }
109
+ if (record.stored.recordType === "tool") {
110
+ const output = stringifyBoundedToolValue(record.canonical?.content);
111
+ toolResults.push({
112
+ recordKeyHash: record.stored.recordKeyHash,
113
+ callId: readText(record.canonical?.tool_call_id),
114
+ output: output.value,
115
+ outputTruncated: output.truncated,
116
+ status: readText(record.canonical?.status),
117
+ exitCode: readInteger(record.canonical?.exit_code, record.canonical?.exitCode),
118
+ order: record.order,
119
+ });
120
+ }
121
+ }
122
+
123
+ const conversation = selectConversation(conversationCandidates, maxChars);
124
+ const toolEvidence = toolResults.flatMap((result) => {
125
+ const call =
126
+ (result.callId === null ? undefined : toolCalls.get(result.callId)) ??
127
+ anonymousToolCalls.filter((candidate) => candidate.order <= result.order).at(-1);
128
+ const reason = classifyToolEvidence(call, result);
129
+ if (reason === null) return [];
130
+ return [
131
+ {
132
+ reason,
133
+ toolName: call?.toolName ?? "unknown-tool",
134
+ safeInputExcerpt: call?.input ?? null,
135
+ outputExcerpt: result.output,
136
+ status: result.status,
137
+ exitCode: result.exitCode,
138
+ inputTruncated: call?.inputTruncated ?? false,
139
+ outputTruncated: result.outputTruncated,
140
+ order: result.order,
141
+ recordKeys: [...(call === undefined ? [] : [call.recordKeyHash]), result.recordKeyHash],
142
+ },
143
+ ];
144
+ });
145
+ toolEvidence.sort(
146
+ (left, right) =>
147
+ toolReasonPriority(left.reason) - toolReasonPriority(right.reason) ||
148
+ left.order - right.order,
149
+ );
150
+
151
+ const selectedToolEvidence = toolEvidence.slice(0, MAX_TOOL_EVIDENCE);
152
+ const selectedKeys = new Set(conversation.map((message) => message.recordKeyHash));
153
+ for (const evidence of selectedToolEvidence) {
154
+ for (const key of evidence.recordKeys) selectedKeys.add(key);
155
+ }
156
+ const touchedPaths = collectTouchedPaths([
157
+ ...conversation.map((message) => message.text),
158
+ ...selectedToolEvidence.flatMap((evidence) => [
159
+ evidence.safeInputExcerpt ?? "",
160
+ evidence.outputExcerpt ?? "",
161
+ ]),
162
+ ]);
163
+ const packet: SemanticEvidencePacketV1 = {
164
+ schemaVersion: 1,
165
+ kind: "semantic-evidence-packet",
166
+ projectKey: input.segment.projectKey,
167
+ segmentId: input.segment.id,
168
+ conversation: conversation.map(({ role, text, recordKeyHash }) => ({
169
+ role,
170
+ text,
171
+ recordKeyHash,
172
+ })),
173
+ toolEvidence: selectedToolEvidence.map(
174
+ ({ order: _order, recordKeys: _recordKeys, ...evidence }) => evidence,
175
+ ),
176
+ touchedPaths,
177
+ selection: {
178
+ selectedRecords: selectedKeys.size,
179
+ droppedRecords: Math.max(0, records.length - selectedKeys.size),
180
+ redactionCount,
181
+ truncated:
182
+ sourceTruncated ||
183
+ conversation.length < conversationCandidates.length ||
184
+ selectedToolEvidence.length < toolEvidence.length ||
185
+ selectedToolEvidence.some(
186
+ (evidence) => evidence.inputTruncated || evidence.outputTruncated,
187
+ ),
188
+ },
189
+ privacy: {
190
+ containsPrivateContent: true,
191
+ credentialRedacted: true,
192
+ rawSegmentIncluded: false,
193
+ },
194
+ };
195
+ enforcePacketBudget(packet, maxChars);
196
+ if (detectSessionMemorySensitivity(packet).classification === "credential") {
197
+ throw new Error("Semantic evidence packet still contains a credential.");
198
+ }
199
+ return packet;
200
+ }
201
+
202
+ function parseHistoricalRecords(content: string): ParsedHistoricalRecord[] {
203
+ return content
204
+ .split("\n")
205
+ .filter((line) => line.trim() !== "")
206
+ .map((line, order) => {
207
+ let value: unknown;
208
+ try {
209
+ value = JSON.parse(line) as unknown;
210
+ } catch {
211
+ throw new Error("Historical Session Evidence contains invalid JSONL.");
212
+ }
213
+ const record = asRecord(value);
214
+ if (
215
+ record === null ||
216
+ record.schemaVersion !== 1 ||
217
+ record.kind !== "historical-import-record" ||
218
+ typeof record.recordKeyHash !== "string" ||
219
+ typeof record.recordType !== "string" ||
220
+ typeof record.canonicalJson !== "string" ||
221
+ typeof record.canonicalJsonTruncated !== "boolean"
222
+ ) {
223
+ throw new Error("Historical Session Evidence record is invalid.");
224
+ }
225
+ const redacted = redactSessionMemoryCredentialText(record.canonicalJson);
226
+ let canonical: Record<string, unknown> | null = null;
227
+ if (!record.canonicalJsonTruncated) {
228
+ try {
229
+ canonical = asRecord(JSON.parse(redacted.value) as unknown);
230
+ } catch {
231
+ throw new Error("Historical canonical evidence contains invalid JSON.");
232
+ }
233
+ }
234
+ return {
235
+ stored: {
236
+ schemaVersion: 1,
237
+ kind: "historical-import-record",
238
+ recordKeyHash: record.recordKeyHash,
239
+ recordType: expectStoredRecordType(record.recordType),
240
+ canonicalJson: redacted.value,
241
+ canonicalJsonTruncated: record.canonicalJsonTruncated,
242
+ },
243
+ canonical,
244
+ redacted: redacted.redacted,
245
+ order,
246
+ };
247
+ });
248
+ }
249
+
250
+ function selectConversation(
251
+ candidates: ConversationCandidate[],
252
+ maxChars: number,
253
+ ): ConversationCandidate[] {
254
+ const selected: ConversationCandidate[] = [];
255
+ let used = 0;
256
+ for (const candidate of [...candidates].sort(
257
+ (left, right) => right.priority - left.priority || left.order - right.order,
258
+ )) {
259
+ if (selected.length >= MAX_CONVERSATION_MESSAGES) break;
260
+ const safe = redactSessionMemoryCredentialText(candidate.text);
261
+ const remaining = Math.max(0, maxChars - used);
262
+ if (remaining < 64) break;
263
+ const text = safe.value.slice(0, Math.min(safe.value.length, remaining));
264
+ selected.push({ ...candidate, text });
265
+ used += text.length;
266
+ }
267
+ return selected.sort((left, right) => left.order - right.order);
268
+ }
269
+
270
+ function conversationPriority(
271
+ role: "user" | "assistant",
272
+ text: string,
273
+ order: number,
274
+ total: number,
275
+ ): number {
276
+ let priority = role === "user" ? 20 : 10;
277
+ if (/\b(?:instead|correction|actually|must|should|do not|don't|wrong|require)\b/iu.test(text)) {
278
+ priority += 40;
279
+ }
280
+ if (order >= total - 2) priority += 15;
281
+ return priority;
282
+ }
283
+
284
+ function classifyToolEvidence(
285
+ call: ToolCallCandidate | undefined,
286
+ result: {
287
+ output: string | null;
288
+ status: string | null;
289
+ exitCode: number | null;
290
+ },
291
+ ): SemanticEvidencePacketV1["toolEvidence"][number]["reason"] | null {
292
+ const comparable = `${call?.toolName ?? ""} ${call?.input ?? ""} ${result.output ?? ""} ${
293
+ result.status ?? ""
294
+ }`;
295
+ if (/\b(?:permission|denied|unauthorized|forbidden)\b/iu.test(comparable)) {
296
+ return "permission";
297
+ }
298
+ if (/\b(?:rollback|revert|restore)\b/iu.test(comparable)) return "rollback";
299
+ const failureComparable = comparable.replace(/\b0\s+fail(?:ed|ure|ures)?\b/giu, "");
300
+ if (
301
+ (result.exitCode !== null && result.exitCode !== 0) ||
302
+ /\b(?:fail(?:ed|ure)?|error|exception|non-zero)\b/iu.test(failureComparable)
303
+ ) {
304
+ return "failure";
305
+ }
306
+ if (
307
+ /\b(?:test|lint|typecheck|build|verify|check)\b/iu.test(comparable) &&
308
+ !/\b(?:fail(?:ed|ure)?|error)\b/iu.test(failureComparable)
309
+ ) {
310
+ return "verification";
311
+ }
312
+ return null;
313
+ }
314
+
315
+ function stringifyBoundedToolValue(value: unknown): {
316
+ value: string | null;
317
+ truncated: boolean;
318
+ } {
319
+ if (value === null || value === undefined) return { value: null, truncated: false };
320
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
321
+ const safe = redactSessionMemoryCredentialText(raw);
322
+ if (safe.value.length <= MAX_TOOL_EXCERPT_CHARS) {
323
+ return { value: safe.value, truncated: false };
324
+ }
325
+ return {
326
+ value: `${safe.value.slice(0, MAX_TOOL_EXCERPT_CHARS - 1)}…`,
327
+ truncated: true,
328
+ };
329
+ }
330
+
331
+ function collectTouchedPaths(values: string[]): string[] {
332
+ const paths = new Set<string>();
333
+ for (const value of values) {
334
+ for (const match of value.matchAll(
335
+ /(?:^|[\s"'`(])((?:(?:\/|\.{1,2}\/)[A-Za-z0-9._~@%+,:=-]+|[A-Za-z0-9._~@%+=-]+\/[A-Za-z0-9._~@%+,:=-]+)(?:\/[A-Za-z0-9._~@%+,:=-]+)*)/gu,
336
+ )) {
337
+ const path = match[1];
338
+ if (path !== undefined) paths.add(path.slice(0, 500));
339
+ if (paths.size >= 20) return [...paths];
340
+ }
341
+ }
342
+ return [...paths];
343
+ }
344
+
345
+ function enforcePacketBudget(packet: SemanticEvidencePacketV1, maxChars: number): void {
346
+ while (JSON.stringify(packet).length > maxChars && packet.toolEvidence.length > 0) {
347
+ packet.toolEvidence.pop();
348
+ packet.selection.truncated = true;
349
+ }
350
+ while (JSON.stringify(packet).length > maxChars && packet.conversation.length > 1) {
351
+ packet.conversation.splice(1, 1);
352
+ packet.selection.truncated = true;
353
+ }
354
+ if (JSON.stringify(packet).length > maxChars) {
355
+ const message = packet.conversation[0];
356
+ if (message !== undefined) {
357
+ const over = JSON.stringify(packet).length - maxChars;
358
+ message.text = `${message.text.slice(0, Math.max(0, message.text.length - over - 1))}…`;
359
+ packet.selection.truncated = true;
360
+ }
361
+ }
362
+ if (JSON.stringify(packet).length > maxChars) {
363
+ throw new Error("Semantic evidence packet could not fit its bounded budget.");
364
+ }
365
+ }
366
+
367
+ function toolReasonPriority(
368
+ reason: SemanticEvidencePacketV1["toolEvidence"][number]["reason"],
369
+ ): number {
370
+ if (reason === "failure") return 0;
371
+ if (reason === "permission") return 1;
372
+ if (reason === "rollback") return 2;
373
+ return 3;
374
+ }
375
+
376
+ function expectStoredRecordType(value: string): HistoricalImportStoredRecordV1["recordType"] {
377
+ if (
378
+ value === "user" ||
379
+ value === "reasoning" ||
380
+ value === "assistant" ||
381
+ value === "assistant-tool-call" ||
382
+ value === "tool"
383
+ ) {
384
+ return value;
385
+ }
386
+ throw new Error("Historical Session Evidence record type is invalid.");
387
+ }
388
+
389
+ function readText(value: unknown): string | null {
390
+ return typeof value === "string" && value.trim() !== "" ? value : null;
391
+ }
392
+
393
+ function readInteger(...values: unknown[]): number | null {
394
+ for (const value of values) {
395
+ if (typeof value === "number" && Number.isInteger(value)) return value;
396
+ }
397
+ return null;
398
+ }
399
+
400
+ function asRecord(value: unknown): Record<string, unknown> | null {
401
+ return value !== null && typeof value === "object" && !Array.isArray(value)
402
+ ? (value as Record<string, unknown>)
403
+ : null;
404
+ }
405
+
406
+ export function createSemanticPacketMessageKey(value: string): string {
407
+ return sha256Hex(value);
408
+ }