@evo-dev/core 0.0.1-alpha → 0.0.1-alpha.10

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 (85) hide show
  1. package/assets/agents/review/code-reviewer/examples.md +1 -1
  2. package/assets/agents/review/code-reviewer/prompt.md +1 -1
  3. package/assets/agents/review/code-reviewer/verification.md +1 -1
  4. package/assets/skills/coding/knowledge-distillation/SKILL.md +251 -0
  5. package/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  6. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
  7. package/assets/team/agents/code-reviewer.md +48 -0
  8. package/assets/team/agents/docs-maintainer.md +51 -0
  9. package/assets/team/agents/implementation-engineer.md +51 -0
  10. package/assets/team/agents/product-scope-analyst.md +58 -0
  11. package/assets/team/agents/release-engineer.md +55 -0
  12. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  13. package/assets/team/agents/solution-architect.md +51 -0
  14. package/assets/team/agents/verification-engineer.md +51 -0
  15. package/assets/team/team.md +102 -0
  16. package/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  17. package/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  18. package/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  19. package/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  20. package/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  21. package/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  22. package/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  23. package/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  24. package/dist/config/index.js +1115 -81
  25. package/dist/index.js +13796 -2196
  26. package/dist/plugins/index.js +32 -32
  27. package/package.json +5 -1
  28. package/src/agents/index.ts +63 -292
  29. package/src/code-agent-traces/index.ts +520 -0
  30. package/src/config/index.ts +7 -0
  31. package/src/config/paths.ts +30 -0
  32. package/src/config/settings.ts +201 -0
  33. package/src/config/store.ts +152 -0
  34. package/src/daemon/index.ts +462 -40
  35. package/src/evolution/candidates/index.ts +564 -0
  36. package/src/evolution/control/index.ts +20 -0
  37. package/src/evolution/evidence/analysis.ts +533 -0
  38. package/src/evolution/evidence/index.ts +3 -0
  39. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  40. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  41. package/src/evolution/evidence/session-memory/index.ts +7 -0
  42. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  43. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  44. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  45. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  46. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  47. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  48. package/src/evolution/evidence/session-memory/types.ts +221 -0
  49. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  50. package/src/evolution/formatters.ts +169 -0
  51. package/src/evolution/index.ts +16 -0
  52. package/src/evolution/knowledge/index.ts +5427 -0
  53. package/src/evolution/paths.ts +44 -0
  54. package/src/evolution/processor/distillation.ts +518 -0
  55. package/src/evolution/processor/index.ts +3 -0
  56. package/src/evolution/processor/process.ts +528 -0
  57. package/src/{learning → evolution/review}/index.ts +10 -14
  58. package/src/evolution/schema.ts +568 -0
  59. package/src/evolution/shared.ts +758 -0
  60. package/src/evolution/triggers/classification.ts +102 -0
  61. package/src/evolution/triggers/index.ts +295 -0
  62. package/src/hooks/index.ts +652 -376
  63. package/src/index.ts +16 -3
  64. package/src/pack/index.ts +13 -13
  65. package/src/plugins/capabilities.ts +40 -42
  66. package/src/plugins/index.ts +0 -1
  67. package/src/plugins/types.ts +4 -0
  68. package/src/projects/index.ts +453 -0
  69. package/src/protected-zones/index.ts +29 -11
  70. package/src/runtime-logs/index.ts +790 -0
  71. package/src/sync/orchestrator.ts +6 -0
  72. package/src/team/index.ts +3642 -0
  73. package/src/team/mcp.ts +405 -0
  74. package/src/team/prompts.ts +141 -0
  75. package/src/utils/errors.ts +13 -0
  76. package/src/utils/fs.ts +40 -0
  77. package/src/utils/hash.ts +9 -0
  78. package/src/utils/ids.ts +12 -0
  79. package/src/utils/index.ts +7 -0
  80. package/src/utils/parsing.ts +11 -0
  81. package/src/utils/text.ts +18 -0
  82. package/src/utils/time.ts +5 -0
  83. package/src/workflow/index.ts +6 -24
  84. package/src/project/index.ts +0 -507
  85. package/src/task/index.ts +0 -840
@@ -0,0 +1,528 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rm, stat } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { resolveEvoDevPaths } from "../../config/paths.ts";
5
+ import { isFileExistsError, normalizeTimestamp } from "../../utils/index.ts";
6
+ import { analyzeEvolutionRun } from "../evidence/analysis.ts";
7
+ import { analyzeSessionEvidenceSegment } from "../evidence/session-memory/analysis.ts";
8
+ import {
9
+ readSessionEvidenceSegment,
10
+ updateSessionEvidenceSegmentLifecycle,
11
+ } from "../evidence/session-memory/storage.ts";
12
+ import type {
13
+ EvolutionProcessInput,
14
+ EvolutionProcessProgress,
15
+ EvolutionProcessResult,
16
+ EvolutionTriggerStatus,
17
+ } from "../schema.ts";
18
+ import { sanitizeText } from "../shared.ts";
19
+ import {
20
+ groupTriggersByRun,
21
+ listEvolutionTriggers,
22
+ listSegmentEvolutionTriggers,
23
+ updateSegmentTriggers,
24
+ updateTriggers,
25
+ } from "../triggers/index.ts";
26
+ import {
27
+ activateCuratedSessionKnowledgePlan,
28
+ activateEvolutionDistillationBatch,
29
+ createEvolutionDistillationBatch,
30
+ } from "./distillation.ts";
31
+
32
+ const PROCESS_LOCK_STALE_MS = 5 * 60 * 1000;
33
+ const PROCESS_LOCK_HEARTBEAT_MS = 30 * 1000;
34
+ const PROCESSING_TRIGGER_STALE_MS = PROCESS_LOCK_STALE_MS;
35
+ const MAX_TRANSIENT_TRIGGER_ATTEMPTS = 3;
36
+
37
+ interface EvolutionProcessLock {
38
+ path: string;
39
+ ownerId: string;
40
+ handle: Awaited<ReturnType<typeof open>>;
41
+ timer: ReturnType<typeof setInterval>;
42
+ heartbeat: Promise<void> | null;
43
+ stopped: boolean;
44
+ lost: boolean;
45
+ }
46
+
47
+ export async function processEvolutionTriggers(
48
+ input: EvolutionProcessInput,
49
+ ): Promise<EvolutionProcessResult> {
50
+ const now = normalizeTimestamp(input.now);
51
+ const limit = input.limit ?? 20;
52
+ const dryRun = input.dryRun === true;
53
+ const warnings: string[] = [];
54
+ const lock = dryRun ? null : await acquireEvolutionProcessLock(input.homeDir, now);
55
+ if (!dryRun && lock === null) {
56
+ warnings.push("Evolution trigger processor is already running; this pass was skipped.");
57
+ const result: EvolutionProcessResult = {
58
+ processed: 0,
59
+ consumed: 0,
60
+ skipped: 0,
61
+ failed: 0,
62
+ pending: 0,
63
+ triggerIds: [],
64
+ batchIds: [],
65
+ warnings,
66
+ dryRun,
67
+ };
68
+ await reportProgress(input, result, "completed");
69
+ return result;
70
+ }
71
+
72
+ try {
73
+ if (!dryRun) {
74
+ await reconcileRetryableEvolutionTriggers(input, now);
75
+ }
76
+ const pendingSegments =
77
+ dryRun || input.distillSessionKnowledge !== undefined
78
+ ? (
79
+ await listSegmentEvolutionTriggers({
80
+ homeDir: input.homeDir,
81
+ projectKey: input.projectKey,
82
+ runId: input.runId,
83
+ status: "pending",
84
+ })
85
+ ).slice(0, limit)
86
+ : [];
87
+ const remainingLimit = Math.max(0, limit - pendingSegments.length);
88
+ const pending =
89
+ remainingLimit === 0
90
+ ? []
91
+ : (
92
+ await listEvolutionTriggers({
93
+ homeDir: input.homeDir,
94
+ projectKey: input.projectKey,
95
+ runId: input.runId,
96
+ status: "pending",
97
+ })
98
+ ).slice(0, remainingLimit);
99
+ const grouped = groupTriggersByRun(pending);
100
+ const result: EvolutionProcessResult = {
101
+ processed: 0,
102
+ consumed: 0,
103
+ skipped: 0,
104
+ failed: 0,
105
+ pending: 0,
106
+ triggerIds: [
107
+ ...pendingSegments.map((trigger) => trigger.id),
108
+ ...pending.map((trigger) => trigger.id),
109
+ ],
110
+ batchIds: [],
111
+ warnings,
112
+ dryRun,
113
+ };
114
+ await reportProgress(input, result, "started");
115
+
116
+ for (const trigger of pendingSegments) {
117
+ result.processed += 1;
118
+ if (dryRun) {
119
+ result.pending += 1;
120
+ await reportProgress(input, result, "updated");
121
+ continue;
122
+ }
123
+ await updateSegmentTriggers(input.homeDir, [trigger], {
124
+ status: "processing",
125
+ updatedAt: now,
126
+ attempts: (current) => current.attempts + 1,
127
+ });
128
+ const attemptedTrigger = {
129
+ ...trigger,
130
+ status: "processing" as const,
131
+ updatedAt: now,
132
+ attempts: trigger.attempts + 1,
133
+ };
134
+ try {
135
+ const segmentIdentity = {
136
+ homeDir: input.homeDir,
137
+ projectKey: trigger.projectKey,
138
+ sessionKey: trigger.sessionKey,
139
+ segmentId: trigger.segmentId,
140
+ };
141
+ const segment = await readSessionEvidenceSegment(segmentIdentity);
142
+ const existingBatchId = segment.lifecycle.consumedByBatchIds.at(-1);
143
+ if (segment.lifecycle.status === "distilled" && existingBatchId !== undefined) {
144
+ result.consumed += 1;
145
+ result.batchIds.push(existingBatchId);
146
+ await updateSegmentTriggers(input.homeDir, [attemptedTrigger], {
147
+ status: "consumed",
148
+ updatedAt: now,
149
+ processedBatchId: existingBatchId,
150
+ lastError: null,
151
+ });
152
+ continue;
153
+ }
154
+
155
+ const analysis = analyzeSessionEvidenceSegment({
156
+ homeDir: input.homeDir,
157
+ segment,
158
+ now,
159
+ });
160
+ warnings.push(...analysis.warnings);
161
+ if (analysis.evidenceWindow === null) {
162
+ const skipReason = analysis.skipReason ?? "no-distillation-signal";
163
+ if (skipReason === "sensitive-segment" || skipReason === "no-distillation-signal") {
164
+ await updateSessionEvidenceSegmentLifecycle({
165
+ ...segmentIdentity,
166
+ status: "ignored",
167
+ });
168
+ }
169
+ result.skipped += 1;
170
+ await updateSegmentTriggers(input.homeDir, [attemptedTrigger], {
171
+ status: "skipped",
172
+ updatedAt: now,
173
+ lastError: skipReason,
174
+ });
175
+ continue;
176
+ }
177
+ if (!analysis.evidenceWindow.triggerPolicy.distillRecommended) {
178
+ await updateSessionEvidenceSegmentLifecycle({
179
+ ...segmentIdentity,
180
+ status: "ignored",
181
+ });
182
+ result.skipped += 1;
183
+ await updateSegmentTriggers(input.homeDir, [attemptedTrigger], {
184
+ status: "skipped",
185
+ updatedAt: now,
186
+ lastError: "no-distillation-signal",
187
+ });
188
+ continue;
189
+ }
190
+
191
+ const distiller = input.distillSessionKnowledge;
192
+ if (distiller === undefined) {
193
+ throw new Error("Semantic session knowledge distiller is unavailable.");
194
+ }
195
+ const plan = await distiller({
196
+ homeDir: input.homeDir,
197
+ segment,
198
+ evidenceWindow: analysis.evidenceWindow,
199
+ now,
200
+ });
201
+ if (lock !== null) await assertEvolutionProcessLockOwned(lock);
202
+ const curated = await activateCuratedSessionKnowledgePlan({
203
+ homeDir: input.homeDir,
204
+ evidenceWindow: analysis.evidenceWindow,
205
+ plan,
206
+ warnings: analysis.warnings,
207
+ now,
208
+ overwrite: true,
209
+ });
210
+ const batch = curated.batch;
211
+ await updateSessionEvidenceSegmentLifecycle({
212
+ ...segmentIdentity,
213
+ status: "distilled",
214
+ consumedByBatchId: batch.id,
215
+ });
216
+ result.consumed += 1;
217
+ result.batchIds.push(batch.id);
218
+ await updateSegmentTriggers(input.homeDir, [attemptedTrigger], {
219
+ status: "consumed",
220
+ updatedAt: now,
221
+ processedBatchId: batch.id,
222
+ lastError: null,
223
+ });
224
+ } catch (error) {
225
+ const message = sanitizeText(error instanceof Error ? error.message : String(error));
226
+ warnings.push(message);
227
+ if (lock?.lost) {
228
+ result.pending += 1;
229
+ await reportProgress(input, result, "completed");
230
+ return result;
231
+ }
232
+ const retry =
233
+ isTransientEvolutionError(error) &&
234
+ attemptedTrigger.attempts < MAX_TRANSIENT_TRIGGER_ATTEMPTS;
235
+ if (retry) result.pending += 1;
236
+ else result.failed += 1;
237
+ await updateSegmentTriggers(input.homeDir, [attemptedTrigger], {
238
+ status: retry ? "pending" : "failed",
239
+ updatedAt: now,
240
+ lastError: message,
241
+ });
242
+ } finally {
243
+ await reportProgress(input, result, "updated");
244
+ }
245
+ }
246
+
247
+ for (const triggers of grouped) {
248
+ result.processed += triggers.length;
249
+ if (dryRun) {
250
+ result.pending += triggers.length;
251
+ await reportProgress(input, result, "updated");
252
+ continue;
253
+ }
254
+ await updateTriggers(input.homeDir, triggers, {
255
+ status: "processing",
256
+ updatedAt: now,
257
+ attempts: (trigger) => trigger.attempts + 1,
258
+ });
259
+ const attemptedTriggers = triggers.map((trigger) => ({
260
+ ...trigger,
261
+ status: "processing" as const,
262
+ updatedAt: now,
263
+ attempts: trigger.attempts + 1,
264
+ }));
265
+
266
+ try {
267
+ const first = attemptedTriggers[0];
268
+ if (first === undefined) continue;
269
+ const analysis = await analyzeEvolutionRun({
270
+ homeDir: input.homeDir,
271
+ projectKey: first.projectKey,
272
+ runId: first.runId,
273
+ now,
274
+ });
275
+ warnings.push(...analysis.warnings);
276
+ const missingTrace = attemptedTriggers.some(
277
+ (trigger) =>
278
+ trigger.eventId !== null &&
279
+ !analysis.evidenceWindow.events.some((event) => event.hookEventId === trigger.eventId),
280
+ );
281
+ if (missingTrace) {
282
+ result.pending += triggers.length;
283
+ await updateTriggers(input.homeDir, attemptedTriggers, {
284
+ status: "pending",
285
+ updatedAt: now,
286
+ lastError: "waiting-for-trace",
287
+ });
288
+ continue;
289
+ }
290
+ if (!analysis.evidenceWindow.triggerPolicy.distillRecommended) {
291
+ result.skipped += triggers.length;
292
+ await updateTriggers(input.homeDir, attemptedTriggers, {
293
+ status: "skipped",
294
+ updatedAt: now,
295
+ lastError: "no-distillation-signal",
296
+ });
297
+ continue;
298
+ }
299
+
300
+ const batch = createEvolutionDistillationBatch({
301
+ evidenceWindow: analysis.evidenceWindow,
302
+ warnings: analysis.warnings,
303
+ now,
304
+ });
305
+ if (lock !== null) await assertEvolutionProcessLockOwned(lock);
306
+ await activateEvolutionDistillationBatch({
307
+ homeDir: input.homeDir,
308
+ batch,
309
+ overwrite: true,
310
+ });
311
+ result.consumed += triggers.length;
312
+ result.batchIds.push(batch.id);
313
+ await updateTriggers(input.homeDir, attemptedTriggers, {
314
+ status: "consumed",
315
+ updatedAt: now,
316
+ processedBatchId: batch.id,
317
+ lastError: null,
318
+ });
319
+ } catch (error) {
320
+ const message = sanitizeText(error instanceof Error ? error.message : String(error));
321
+ warnings.push(message);
322
+ if (lock?.lost) {
323
+ result.pending += triggers.length;
324
+ await reportProgress(input, result, "completed");
325
+ return result;
326
+ }
327
+ const retry =
328
+ isTransientEvolutionError(error) &&
329
+ attemptedTriggers.every((trigger) => trigger.attempts < MAX_TRANSIENT_TRIGGER_ATTEMPTS);
330
+ if (retry) result.pending += triggers.length;
331
+ else result.failed += triggers.length;
332
+ await updateTriggers(input.homeDir, attemptedTriggers, {
333
+ status: retry ? "pending" : "failed",
334
+ updatedAt: now,
335
+ lastError: message,
336
+ });
337
+ } finally {
338
+ await reportProgress(input, result, "updated");
339
+ }
340
+ }
341
+
342
+ await reportProgress(input, result, "completed");
343
+ return result;
344
+ } finally {
345
+ if (lock !== null) await releaseEvolutionProcessLock(lock);
346
+ }
347
+ }
348
+
349
+ async function reportProgress(
350
+ input: EvolutionProcessInput,
351
+ result: EvolutionProcessResult,
352
+ phase: EvolutionProcessProgress["phase"],
353
+ ): Promise<void> {
354
+ if (input.onProgress === undefined) return;
355
+ try {
356
+ await input.onProgress({
357
+ phase,
358
+ total: result.triggerIds.length,
359
+ processed: result.processed,
360
+ consumed: result.consumed,
361
+ skipped: result.skipped,
362
+ failed: result.failed,
363
+ pending: result.pending,
364
+ });
365
+ } catch {
366
+ // Progress persistence/observers must not change evolution processing outcomes.
367
+ }
368
+ }
369
+
370
+ async function acquireEvolutionProcessLock(
371
+ homeDir: string,
372
+ now: string,
373
+ ): Promise<EvolutionProcessLock | null> {
374
+ const paths = resolveEvoDevPaths(homeDir);
375
+ const lockPath = join(paths.stateDir, "evolution", ".process.lock");
376
+ const ownerId = randomUUID();
377
+ await mkdir(dirname(lockPath), { recursive: true });
378
+ try {
379
+ const handle = await open(lockPath, "wx");
380
+ try {
381
+ await handle.writeFile(
382
+ `${JSON.stringify(
383
+ {
384
+ schemaVersion: 1,
385
+ kind: "evolution-process-lock",
386
+ ownerId,
387
+ createdAt: now,
388
+ heartbeatAt: now,
389
+ pid: process.pid,
390
+ },
391
+ null,
392
+ 2,
393
+ )}\n`,
394
+ "utf8",
395
+ );
396
+ } catch (error) {
397
+ await handle.close().catch(() => undefined);
398
+ await rm(lockPath, { force: true }).catch(() => undefined);
399
+ throw error;
400
+ }
401
+ const lock: EvolutionProcessLock = {
402
+ path: lockPath,
403
+ ownerId,
404
+ handle,
405
+ timer: undefined as unknown as ReturnType<typeof setInterval>,
406
+ heartbeat: null,
407
+ stopped: false,
408
+ lost: false,
409
+ };
410
+ lock.timer = setInterval(() => {
411
+ if (lock.stopped || lock.heartbeat !== null) return;
412
+ lock.heartbeat = heartbeatEvolutionProcessLock(lock).finally(() => {
413
+ lock.heartbeat = null;
414
+ });
415
+ }, PROCESS_LOCK_HEARTBEAT_MS);
416
+ lock.timer.unref?.();
417
+ return lock;
418
+ } catch (error) {
419
+ if (!isFileExistsError(error)) throw error;
420
+ const current = await stat(lockPath).catch(() => null);
421
+ if (current !== null && Date.now() - current.mtimeMs > PROCESS_LOCK_STALE_MS) {
422
+ await rm(lockPath, { force: true });
423
+ return acquireEvolutionProcessLock(homeDir, now);
424
+ }
425
+ return null;
426
+ }
427
+ }
428
+
429
+ async function heartbeatEvolutionProcessLock(lock: EvolutionProcessLock): Promise<void> {
430
+ if (!(await evolutionProcessLockIsOwned(lock))) {
431
+ lock.lost = true;
432
+ return;
433
+ }
434
+ try {
435
+ const now = new Date();
436
+ await lock.handle.utimes(now, now);
437
+ } catch {
438
+ lock.lost = true;
439
+ }
440
+ }
441
+
442
+ async function assertEvolutionProcessLockOwned(lock: EvolutionProcessLock): Promise<void> {
443
+ if (lock.lost || !(await evolutionProcessLockIsOwned(lock))) {
444
+ lock.lost = true;
445
+ throw new Error("Evolution process lock ownership was lost during processing.");
446
+ }
447
+ }
448
+
449
+ async function evolutionProcessLockIsOwned(lock: EvolutionProcessLock): Promise<boolean> {
450
+ try {
451
+ const value = JSON.parse(await readFile(lock.path, "utf8")) as { ownerId?: unknown };
452
+ return value.ownerId === lock.ownerId;
453
+ } catch {
454
+ return false;
455
+ }
456
+ }
457
+
458
+ async function releaseEvolutionProcessLock(lock: EvolutionProcessLock): Promise<void> {
459
+ lock.stopped = true;
460
+ clearInterval(lock.timer);
461
+ await lock.heartbeat?.catch(() => undefined);
462
+ await lock.handle.close().catch(() => undefined);
463
+ if (await evolutionProcessLockIsOwned(lock)) {
464
+ await rm(lock.path, { force: true });
465
+ }
466
+ }
467
+
468
+ async function reconcileRetryableEvolutionTriggers(
469
+ input: EvolutionProcessInput,
470
+ now: string,
471
+ ): Promise<void> {
472
+ const [segmentTriggers, legacyTriggers] = await Promise.all([
473
+ listSegmentEvolutionTriggers({
474
+ homeDir: input.homeDir,
475
+ projectKey: input.projectKey,
476
+ runId: input.runId,
477
+ }),
478
+ listEvolutionTriggers({
479
+ homeDir: input.homeDir,
480
+ projectKey: input.projectKey,
481
+ runId: input.runId,
482
+ }),
483
+ ]);
484
+ const nowMs = Date.parse(now);
485
+ const retryableSegments = segmentTriggers.filter((trigger) =>
486
+ shouldRequeueTrigger(trigger, nowMs),
487
+ );
488
+ const retryableLegacy = legacyTriggers.filter((trigger) => shouldRequeueTrigger(trigger, nowMs));
489
+ await Promise.all([
490
+ updateSegmentTriggers(input.homeDir, retryableSegments, {
491
+ status: "pending",
492
+ updatedAt: now,
493
+ lastError: "Retrying a transient failure or stale processing lease.",
494
+ }),
495
+ updateTriggers(input.homeDir, retryableLegacy, {
496
+ status: "pending",
497
+ updatedAt: now,
498
+ lastError: "Retrying a transient failure or stale processing lease.",
499
+ }),
500
+ ]);
501
+ }
502
+
503
+ function shouldRequeueTrigger(
504
+ trigger: {
505
+ status: EvolutionTriggerStatus;
506
+ attempts: number;
507
+ updatedAt: string;
508
+ lastError: string | null;
509
+ },
510
+ nowMs: number,
511
+ ): boolean {
512
+ if (trigger.attempts >= MAX_TRANSIENT_TRIGGER_ATTEMPTS) return false;
513
+ if (trigger.status === "failed") return isTransientEvolutionError(trigger.lastError ?? "");
514
+ if (trigger.status !== "processing") return false;
515
+ const updatedAt = Date.parse(trigger.updatedAt);
516
+ return Number.isFinite(nowMs) && Number.isFinite(updatedAt)
517
+ ? nowMs - updatedAt >= PROCESSING_TRIGGER_STALE_MS
518
+ : false;
519
+ }
520
+
521
+ function isTransientEvolutionError(error: unknown): boolean {
522
+ const message = (error instanceof Error ? `${error.name}: ${error.message}` : String(error))
523
+ .toLowerCase()
524
+ .trim();
525
+ return /(?:timed?\s*out|timeout|rate[\s-]*limit|too many requests|\b429\b|network|econn(?:reset|refused|aborted)|eai_again|etimedout|socket|temporar|service unavailable|\b50[0234]\b|fetch failed)/u.test(
526
+ message,
527
+ );
528
+ }
@@ -1,5 +1,9 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import {
4
+ detectSessionMemorySensitivity,
5
+ redactSessionMemoryCredentialText,
6
+ } from "../evidence/session-memory/sensitivity.ts";
3
7
 
4
8
  export type LearningCandidateStatus = "candidate" | "rejected" | "deferred";
5
9
  export type LearningCandidateKind =
@@ -31,7 +35,6 @@ export interface LearningCandidate {
31
35
  };
32
36
  provenance: {
33
37
  taskId: string | null;
34
- taskContractRef: string | null;
35
38
  workflowRunId: string | null;
36
39
  evidenceRefs: string[];
37
40
  sourceType:
@@ -167,8 +170,6 @@ const FORBIDDEN_RAW_KEYS = new Set([
167
170
  "transcriptbody",
168
171
  "transcripttext",
169
172
  ]);
170
- const SENSITIVE_TEXT_PATTERN =
171
- /https?:\/\/\S+|\b(secret|token|password|passwd|private|internal|api[_-]?key|apikey|credential|credentials|raw log|raw logs|raw output|raw source|raw prompt|shell history|command history)\b/i;
172
173
  const PROTECTED_PATH_PATTERN =
173
174
  /(^|[~/\\])(?:USER|KNOWLEDGE|LEARNING|OBSERVABILITY|PACKS|RELEASES|logs?|memory|\.env[^/\\]*)(?:$|[/\\])|PROJECTS[/\\][^/\\]+[/\\]LEARNING(?:$|[/\\])|\.evodev[/\\](?:USER|KNOWLEDGE|LEARNING|OBSERVABILITY|PACKS|RELEASES)(?:$|[/\\])/i;
174
175
 
@@ -198,7 +199,6 @@ export function createLearningCandidate(input: {
198
199
  },
199
200
  provenance: {
200
201
  taskId: sanitizeNullableId(input.provenance.taskId),
201
- taskContractRef: sanitizeNullableText(input.provenance.taskContractRef),
202
202
  workflowRunId: sanitizeNullableId(input.provenance.workflowRunId),
203
203
  evidenceRefs: input.provenance.evidenceRefs.map(sanitizeText),
204
204
  sourceType: input.provenance.sourceType,
@@ -626,13 +626,9 @@ async function parseJsonOrJsonlFile<T>(
626
626
  }
627
627
 
628
628
  function candidatePathFields(candidate: LearningCandidate): Array<[string, string]> {
629
- return [
630
- ["provenance.taskContractRef", candidate.provenance.taskContractRef],
631
- ...candidate.provenance.evidenceRefs.map((ref, index): [string, string] => [
632
- `provenance.evidenceRefs[${index}]`,
633
- ref,
634
- ]),
635
- ].filter((entry): entry is [string, string] => typeof entry[1] === "string");
629
+ return candidate.provenance.evidenceRefs
630
+ .map((ref, index): [string, string] => [`provenance.evidenceRefs[${index}]`, ref])
631
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string");
636
632
  }
637
633
 
638
634
  function isCandidateStale(candidate: LearningCandidate, now: string | undefined): boolean {
@@ -663,8 +659,8 @@ function assertStringArrayField(field: string, value: unknown): void {
663
659
  function assertNoForbiddenContent(value: unknown): void {
664
660
  if (typeof value === "string") {
665
661
  if (value === "local-private") return;
666
- if (SENSITIVE_TEXT_PATTERN.test(value)) {
667
- throw new Error("Learning candidate contains sensitive content.");
662
+ if (detectSessionMemorySensitivity(value).classification === "credential") {
663
+ throw new Error("Learning candidate contains a credential.");
668
664
  }
669
665
  return;
670
666
  }
@@ -684,7 +680,7 @@ function assertNoForbiddenContent(value: unknown): void {
684
680
  }
685
681
 
686
682
  function sanitizeText(value: string): string {
687
- return value.replace(SENSITIVE_TEXT_PATTERN, "[redacted]").slice(0, 500);
683
+ return redactSessionMemoryCredentialText(value).value.slice(0, 500);
688
684
  }
689
685
 
690
686
  function sanitizeNullableText(value: string | null): string | null {