@evomap/evolver-core 2.0.0-beta.12 → 2.0.0-beta.14

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.
@@ -69,6 +69,7 @@ export function composeSessionStartWithRecap(base, recapCtx, estimateTokensOrOpt
69
69
  export function signalsFromToolUse(event) {
70
70
  const turn = {
71
71
  toolName: event.toolName,
72
+ ...(event.toolResult ? { toolResult: event.toolResult } : {}),
72
73
  ...(event.isError && event.toolResult ? { errorMessage: event.toolResult } : {}),
73
74
  ...(event.text ? { text: event.text } : {}),
74
75
  isMeta: false,
@@ -54,6 +54,10 @@ export interface QuestionGenerationResult {
54
54
  state: QuestionGeneratorState;
55
55
  changed: boolean;
56
56
  }
57
- export declare function extractTopicKeywords(transcript: string | undefined, memory: string | undefined, max?: number): string[];
57
+ export interface TopicKeywordOptions {
58
+ minOccurrences?: number;
59
+ preserveSourceOrder?: boolean;
60
+ }
61
+ export declare function extractTopicKeywords(transcript: string | undefined, memory: string | undefined, max?: number, opts?: TopicKeywordOptions): string[];
58
62
  export declare function generateQuestions(input?: GenerateQuestionsInput): QuestionGenerationResult;
59
63
  export declare function generateUrgentQuestions(input?: GenerateUrgentQuestionsInput): QuestionGenerationResult;
@@ -21,7 +21,7 @@ const EXPLORE_STOPWORDS = new Set([
21
21
  'agent', 'about', 'into', 'then', 'than', 'they', 'them', 'their', 'there',
22
22
  'here', 'will', 'would', 'could', 'should', 'been', 'were', 'using', 'used',
23
23
  'cycle', 'evolution', 'error', 'errors', 'failed', 'failure', 'null', 'undefined',
24
- 'true', 'false', 'console', 'return', 'function', 'const', 'value', 'result',
24
+ 'true', 'false', 'console', 'return', 'function', 'const', 'value', 'result', 'redacted',
25
25
  ]);
26
26
  const SENSITIVE_TOPIC_RE = /secret|token|api[_-]?key|password|passwd|credential|bearer|authorization|cookie|session[_-]?id|private[_-]?key|oauth|refresh[_-]?token/i;
27
27
  const LONG_OPAQUE_RE = /^[a-z0-9_-]{32,}$/i;
@@ -106,8 +106,8 @@ function safeTopicToken(token) {
106
106
  return false;
107
107
  return true;
108
108
  }
109
- export function extractTopicKeywords(transcript, memory, max = 5) {
110
- const text = `${String(transcript ?? '')} ${String(memory ?? '')}`.toLowerCase();
109
+ export function extractTopicKeywords(transcript, memory, max = 5, opts = {}) {
110
+ const text = redactString(`${String(transcript ?? '')} ${String(memory ?? '')}`).toLowerCase();
111
111
  const words = text.match(/[a-z][a-z0-9_-]{4,}/g) ?? [];
112
112
  const freq = new Map();
113
113
  for (const word of words) {
@@ -115,9 +115,11 @@ export function extractTopicKeywords(transcript, memory, max = 5) {
115
115
  continue;
116
116
  freq.set(word, (freq.get(word) ?? 0) + 1);
117
117
  }
118
- return [...freq.entries()]
119
- .filter(([, count]) => count >= 2)
120
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
118
+ const entries = [...freq.entries()]
119
+ .filter(([, count]) => count >= Math.max(1, Math.trunc(opts.minOccurrences ?? 2)));
120
+ if (!opts.preserveSourceOrder)
121
+ entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
122
+ return entries
121
123
  .map(([word]) => word)
122
124
  .slice(0, max);
123
125
  }
@@ -7,7 +7,7 @@ export declare const problemSignature: z.ZodString;
7
7
  export declare const signal: z.ZodObject<{
8
8
  signalId: z.ZodString;
9
9
  fromMaterial: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
10
- kind: z.ZodEnum<["strong_structured", "weak_corpus", "agent_marked"]>;
10
+ kind: z.ZodEnum<["strong_structured", "weak_corpus", "agent_marked", "verified_success"]>;
11
11
  text: z.ZodString;
12
12
  score: z.ZodDefault<z.ZodNumber>;
13
13
  eventSignature: z.ZodString;
@@ -16,7 +16,7 @@ export declare const signal: z.ZodObject<{
16
16
  discoveredAt: z.ZodString;
17
17
  extensions: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
18
18
  }, "strip", z.ZodTypeAny, {
19
- kind: "strong_structured" | "weak_corpus" | "agent_marked";
19
+ kind: "strong_structured" | "weak_corpus" | "agent_marked" | "verified_success";
20
20
  extensions: Record<string, unknown>;
21
21
  signalId: string;
22
22
  fromMaterial: string[];
@@ -27,7 +27,7 @@ export declare const signal: z.ZodObject<{
27
27
  signatureV: number;
28
28
  discoveredAt: string;
29
29
  }, {
30
- kind: "strong_structured" | "weak_corpus" | "agent_marked";
30
+ kind: "strong_structured" | "weak_corpus" | "agent_marked" | "verified_success";
31
31
  signalId: string;
32
32
  text: string;
33
33
  eventSignature: string;
@@ -8,7 +8,7 @@ export const problemSignature = z.string();
8
8
  export const signal = z.object({
9
9
  signalId: ulid,
10
10
  fromMaterial: z.array(ulid).default([]),
11
- kind: z.enum(['strong_structured', 'weak_corpus', 'agent_marked']),
11
+ kind: z.enum(['strong_structured', 'weak_corpus', 'agent_marked', 'verified_success']),
12
12
  text: z.string(),
13
13
  score: z.number().min(0).max(1).default(0),
14
14
  eventSignature,
@@ -8,7 +8,8 @@
8
8
  // - intent: mutation.built.payload.category (the cycle's GepCategory), if emitted
9
9
  // - genesUsed: decision.gene_selected.payload.selectedGeneId || cycle.*.payload.gene
10
10
  // - score: cycle.*.payload.outcome.score
11
- // - blastRadius:capsule.produced.payload.blastRadius, if emitted (else absent not counted as empty)
11
+ // - blastRadius:capsule.produced.payload.blastRadius, if emitted. Productive {0,0} is omitted because non-git
12
+ // proofs use it as "unknown"; measured/no-value {0,0} remains an empty-cycle marker.
12
13
  // Only terminal cycles (a cycle.solidified or cycle.failed) become records; in-flight cycles are skipped.
13
14
  function payloadObj(e) {
14
15
  return e.payload && typeof e.payload === 'object' ? e.payload : {};
@@ -22,13 +23,12 @@ function cycleIdOf(p) {
22
23
  */
23
24
  export function cycleRecordsFromEvents(events) {
24
25
  const byCycle = new Map();
25
- const order = [];
26
+ const terminalOrder = [];
26
27
  const get = (id) => {
27
28
  let a = byCycle.get(id);
28
29
  if (!a) {
29
30
  a = { terminal: false };
30
31
  byCycle.set(id, a);
31
- order.push(id);
32
32
  }
33
33
  return a;
34
34
  };
@@ -53,15 +53,24 @@ export function cycleRecordsFromEvents(events) {
53
53
  const br = p['blastRadius'];
54
54
  if (br && typeof br === 'object') {
55
55
  const o = br;
56
- a.blastRadius = {
57
- ...(typeof o['files'] === 'number' ? { files: o['files'] } : {}),
58
- ...(typeof o['lines'] === 'number' ? { lines: o['lines'] } : {}),
59
- };
56
+ const files = typeof o['files'] === 'number' ? o['files'] : undefined;
57
+ const lines = typeof o['lines'] === 'number' ? o['lines'] : undefined;
58
+ // Non-git proofs use {0,0} as "blast unknown". producedValue is the authoritative distinction between
59
+ // those productive cycles and a measured zero-change git cycle.
60
+ const productiveUnknownBlast = p['producedValue'] === true && files === 0 && lines === 0;
61
+ if (!productiveUnknownBlast) {
62
+ a.blastRadius = {
63
+ ...(files !== undefined ? { files } : {}),
64
+ ...(lines !== undefined ? { lines } : {}),
65
+ };
66
+ }
60
67
  }
61
68
  break;
62
69
  }
63
70
  case 'cycle.solidified':
64
71
  case 'cycle.failed': {
72
+ if (!a.terminal)
73
+ terminalOrder.push(id);
65
74
  a.terminal = true;
66
75
  a.status = e.type === 'cycle.solidified' ? 'success' : 'failed';
67
76
  if (typeof p['gene'] === 'string')
@@ -77,7 +86,7 @@ export function cycleRecordsFromEvents(events) {
77
86
  }
78
87
  }
79
88
  const records = [];
80
- for (const id of order) {
89
+ for (const id of terminalOrder) {
81
90
  const a = byCycle.get(id);
82
91
  if (!a.terminal)
83
92
  continue; // skip in-flight cycles
@@ -19,7 +19,16 @@ const EXPANSION_RULES = [
19
19
  // 功能/機能/기능 alone are too broad: as unanchored substrings they match the malfunction vocabulary
20
20
  // (機能不全 / 기능장애 / 功能障碍), which are reliability problems — use the v1 #99 feature-request compounds.
21
21
  { re: /(feature|capability_gap|user_feature_request|external_opportunity|stagnation recommendation|功能请求|機能リクエスト|기능요청|能力缺口|機能ギャップ|역량공백|改进建议|改善提案|개선제안|外部机会|外部機会|외부기회)/, tags: ['problem:capability', 'action:innovate'] },
22
- { re: /(stagnation|plateau|steady_state|saturation|empty_cycle_loop|loop_detected|recurring)/, tags: ['problem:stagnation', 'action:innovate'] },
22
+ // Negative lookbehind on `plateau`: `stable_success_plateau` contains the substring `plateau`, but it is a
23
+ // SUCCESS signal (#578), not a stagnation signal. Without (?<!success_) the stagnation rule would fire on it,
24
+ // producing contradictory tags (problem:stagnation + signal:success) in a single expandSignals pass.
25
+ { re: /(stagnation|(?<!success_)plateau|steady_state|saturation|empty_cycle_loop|loop_detected|recurring)/, tags: ['problem:stagnation', 'action:innovate'] },
26
+ // Success signals (v2-native, #578): a stable success plateau or resolved issue expands to
27
+ // signal:success + action:optimize/action:innovate so a gene tagged for optimization/innovation can be selected
28
+ // when the loop is succeeding — enabling "learn from what works" rather than only "fix what's broken".
29
+ // `verified[-_]success` matches both the underscore form (meta-signal naming) and the hyphen form
30
+ // (distillPrimitives signalTokens token) so success genes distilled from sessions are also expanded.
31
+ { re: /(stable_success_plateau|issue_already_resolved|openclaw_self_healed|self_healed|resolved|verified[-_]success|success_prose)/, tags: ['signal:success', 'action:optimize', 'action:innovate'] },
23
32
  { re: /(task|worker|heartbeat|hub|commitment|assignment|orchestration)/, tags: ['area:orchestration'] },
24
33
  // Tool-integrity (ported from v1 #99 gene_tool_integrity): bypassing a registered tool or looping on raw
25
34
  // shell is an orchestration-discipline / validation risk. CN/JA/KO aliases included for recall parity.
@@ -6,8 +6,8 @@ export interface SignalSourceTurn {
6
6
  errorMessage?: string;
7
7
  isMeta?: boolean;
8
8
  }
9
- /** 信号强度三条腿(批注#13): strong=零成本结构化判断 / agent=自标记 / weak=需 LLM 分析. */
10
- export type SignalStrength = 'strong' | 'agent' | 'weak';
9
+ /** 信号强度四条腿(批注#13): strong=零成本结构化错误判断 / agent=自标记 / weak=需 LLM 分析 / success=成功信号(Issue#578). */
10
+ export type SignalStrength = 'strong' | 'agent' | 'weak' | 'success';
11
11
  export interface ExtractedSignal {
12
12
  id: string;
13
13
  strength: SignalStrength;
@@ -4,7 +4,16 @@ const AGENT_MARKERS = [/\bEVOLVE_SIGNAL:\s*(.+)/i, /\[SIGNAL\]\s*(.+)/i];
4
4
  /** 结构化强信号: 栈/退出码/显式 Error — 零成本判断, 不动 LLM. */
5
5
  const STRONG_TEXT = /(^|\n)\s*(Error:|Traceback|Exception|FAILED|panic:|\bexit code [1-9])/;
6
6
  /** 弱信号: 表达困难但无结构, 需 LLM 拼语境. */
7
- const DIFFICULTY = /(无法|失败|搞不定|stuck|can'?t|unable to|not working|报错|卡住)/i;
7
+ const DIFFICULTY = /(无法|失败|搞不定|stuck|can'?t|unable to|not working|fail(?:ed|ing|ure)?|broken|did not pass|no tests? passed|报错|卡住)/i;
8
+ /** 成功信号: 工具执行成功/测试通过/构建成功 — 零成本结构化判断, 不动 LLM(Issue#578).
9
+ * 注意: bare `verified` 和 `green` 已移至 SUCCESS_PROSE(needsAnalysis:true), 因为它们在非成功语境下高频出现
10
+ * ("verified the bug exists" / "green field" / "green button") 且 SUCCESS_TEXT 的 needsAnalysis:false 无 LLM 兜底.
11
+ * Emoji are accepted only with test-result context; a bare checklist mark is not proof of session success. */
12
+ const SUCCESS_TEXT = /\b(exit code:?\s*0|all tests?\s+pass(ed)?|tests?\s+pass(ed)?|validation\s+pass(ed)?|build\s+succeed(ed)?|published\s+successfully|completed\s+successfully)\b|(?:^|\n)\s*(?:✓\s*(?:\d+\s+)?(?:tests?\s+)?pass(ed)?\b|(?:Tests|Test Files):?\s+[1-9]\d*\s+pass(ed)?\b|(?:=+\s*)?[1-9]\d*\s+pass(ed)?(?:\s+in\s+\d+(?:\.\d+)?s?|\s*=*\s*$)|PASS\s+\S+|ok\s+\S+\s+(?:\d+(?:\.\d+)?s|\(cached\))(?:\s|$))|\btests?\s+🟢(?:\s|$)/im;
13
+ /** Deterministic success must fail closed when the same result carries a current negative outcome. */
14
+ const SUCCESS_CONFLICT = /\b(?:no|zero|0)(?:\s+of\s+\d+)?\s+tests?\s+pass(ed)?\b|\bsome\s+tests?\s+pass(ed)?\b|\b(?:tests?|validation|verification|build)\s+(?:fail(?:ed|ing|ure)?|did\s+not\s+pass)\b|\b\d+\s+(?:tests?\s+)?failed\b|\b(?:implementation\s+)?(?:still\s+|remains?\s+)(?:broken|failing|not\s+working)\b|\b(?:not|never|did\s+not|has\s+not|hasn't|have\s+not|haven't)\s+(?:succeed(ed)?|complete(d)?\s+successfully)\b/i;
15
+ /** 成功措辞(非结构化但表达完成/解决): 需上下文确认(Issue#578). bare `verified` 也在此层: "verified the fix" vs "verified the bug" 需 LLM 判断. */
16
+ const SUCCESS_PROSE = /\b(successful(?:ly)?|verified|resolved|fixed|working\s+now|works\s+correctly|problem\s+solved|issue\s+resolved|done|complete)\b/i;
8
17
  /**
9
18
  * Harness-coordination noise — agent/tool mechanics, NOT engineering problems to evolve genes for.
10
19
  * Observation showed these dominate the "strong" leg (tool errors) and drown out real problems: the agent
@@ -33,7 +42,8 @@ export function extractSignals(turns) {
33
42
  continue;
34
43
  }
35
44
  const text = t.text ?? '';
36
- if (!text.trim())
45
+ const toolResult = t.toolResult ?? '';
46
+ if (!text.trim() && !toolResult.trim())
37
47
  continue;
38
48
  // 腿3 agent: 显式标记
39
49
  const marker = AGENT_MARKERS.map((re) => text.match(re)).find(Boolean);
@@ -41,16 +51,31 @@ export function extractSignals(turns) {
41
51
  out.push({ id: makeUlid(), strength: 'agent', kind: 'agent_marked', text: (marker[1] ?? text).trim().slice(0, 2000), needsAnalysis: false });
42
52
  continue;
43
53
  }
44
- // 1 strong: 结构化 Error 文本(同样滤掉 harness 噪声)
45
- if (STRONG_TEXT.test(text)) {
46
- if (!isHarnessNoise(text)) {
47
- out.push({ id: makeUlid(), strength: 'strong', kind: 'structured_error', text: text.slice(0, 2000), needsAnalysis: false });
54
+ // Leg 1 strong: inspect both fields. Some adapters populate only toolResult, while older callers may populate
55
+ // text as well; a failure in either representation must outrank a positive substring in the other.
56
+ const resultText = toolResult.trim() ? toolResult : text;
57
+ const structuredFailureText = STRONG_TEXT.test(text) ? text : (STRONG_TEXT.test(toolResult) ? toolResult : '');
58
+ if (structuredFailureText) {
59
+ if (!isHarnessNoise(structuredFailureText)) {
60
+ out.push({ id: makeUlid(), strength: 'strong', kind: 'structured_error', text: structuredFailureText.slice(0, 2000), ...(t.toolName ? { toolName: t.toolName } : {}), needsAnalysis: false });
61
+ }
62
+ continue;
63
+ }
64
+ // Leg 4 success: prefer the actual tool result; adapters intentionally keep tool-turn text empty.
65
+ if (SUCCESS_TEXT.test(resultText) && !SUCCESS_CONFLICT.test(resultText)) {
66
+ if (!isHarnessNoise(resultText)) {
67
+ out.push({ id: makeUlid(), strength: 'success', kind: 'verified_success', text: resultText.slice(0, 2000), ...(t.toolName ? { toolName: t.toolName } : {}), needsAnalysis: false });
48
68
  }
49
69
  continue;
50
70
  }
51
71
  // 腿2 weak: 困难措辞无结构 → 交 LLM
52
72
  if (DIFFICULTY.test(text)) {
53
73
  out.push({ id: makeUlid(), strength: 'weak', kind: 'difficulty', text: text.slice(0, 2000), needsAnalysis: true });
74
+ continue;
75
+ }
76
+ // 腿4 success: 非结构化成功措辞(resolved/fixed/working now 等) → 需上下文确认(Issue#578)
77
+ if (SUCCESS_PROSE.test(text)) {
78
+ out.push({ id: makeUlid(), strength: 'success', kind: 'success_prose', text: text.slice(0, 2000), needsAnalysis: true });
54
79
  }
55
80
  }
56
81
  return out;
@@ -8,6 +8,10 @@ export interface CycleHistory {
8
8
  consecutiveEmptyCycles: number;
9
9
  /** Trailing run of consecutive failed cycles at the tail. */
10
10
  consecutiveFailureCount: number;
11
+ /** Trailing run of consecutive successful cycles at the tail. */
12
+ consecutiveSuccessCount: number;
13
+ /** Successful cycles within the recent window. */
14
+ successCycleCount: number;
11
15
  /** Fraction of the recent window that failed (0..1). */
12
16
  recentFailureRatio: number;
13
17
  /** geneId → times used within the recent window (informs which gene dominates a failure loop). */
@@ -27,6 +27,9 @@ function isEmptyCycle(r) {
27
27
  function isFailed(r) {
28
28
  return r.outcome?.status === 'failed';
29
29
  }
30
+ function isSuccess(r) {
31
+ return r.outcome?.status === 'success';
32
+ }
30
33
  /**
31
34
  * Derive the v1 history counters from a normalized cycle-record window (newest LAST), mirroring v1
32
35
  * analyzeRecentHistory. Pure + deterministic. Use {@link cycleRecordsFromEvents} to adapt the v2 event log.
@@ -37,6 +40,8 @@ export function deriveCycleHistory(records) {
37
40
  emptyCycleCount: 0,
38
41
  consecutiveEmptyCycles: 0,
39
42
  consecutiveFailureCount: 0,
43
+ consecutiveSuccessCount: 0,
44
+ successCycleCount: 0,
40
45
  recentFailureRatio: 0,
41
46
  geneFreq: {},
42
47
  };
@@ -90,11 +95,32 @@ export function deriveCycleHistory(records) {
90
95
  if (isFailed(r))
91
96
  recentFailureCount++;
92
97
  }
98
+ // Consecutive successes at the tail (stable success plateau detection). Empty cycles are zero-blast
99
+ // no-ops: like the failure streak, they break the success streak instead of extending it.
100
+ let consecutiveSuccessCount = 0;
101
+ for (let i = recent.length - 1; i >= 0; i--) {
102
+ if (isEmptyCycle(recent[i]))
103
+ break;
104
+ if (isSuccess(recent[i]))
105
+ consecutiveSuccessCount++;
106
+ else
107
+ break;
108
+ }
109
+ // Successful cycles within the frequency window (non-empty successes, same caliber as recentFailureRatio).
110
+ let successCycleCount = 0;
111
+ for (const r of tail) {
112
+ if (isEmptyCycle(r))
113
+ continue;
114
+ if (isSuccess(r))
115
+ successCycleCount++;
116
+ }
93
117
  return {
94
118
  consecutiveRepairCount,
95
119
  emptyCycleCount,
96
120
  consecutiveEmptyCycles,
97
121
  consecutiveFailureCount,
122
+ consecutiveSuccessCount,
123
+ successCycleCount,
98
124
  recentFailureRatio: nonEmptyCycleCount > 0 ? recentFailureCount / nonEmptyCycleCount : 0,
99
125
  geneFreq,
100
126
  };
@@ -153,6 +179,22 @@ export function computeMetaSignals(history) {
153
179
  signals.push('high_failure_ratio');
154
180
  signals.push('force_innovation_after_repair_loop');
155
181
  }
182
+ // Stable success plateau: 5+ consecutive successes → system is in a stable success plateau.
183
+ // This is the SUCCESS counterpart of failure_loop_detected: instead of signaling "stuck failing",
184
+ // it signals "consistently succeeding" — the loop should distill WHAT WORKED into reusable genes.
185
+ if (history.consecutiveSuccessCount >= 5) {
186
+ signals.push('stable_success_plateau');
187
+ }
188
+ // Issue already resolved: 3+ consecutive successes after a prior failure context suggests
189
+ // the problem is solved — downstream can skip redundant repair attempts.
190
+ if (history.consecutiveSuccessCount >= 3 && history.recentFailureRatio > 0) {
191
+ signals.push('issue_already_resolved');
192
+ }
193
+ // Self-healed requires the current tail to be successful. Window totals alone would mislabel
194
+ // [success, success, failed] as recovered even though the latest outcome is a failure.
195
+ if (history.consecutiveSuccessCount > 0 && history.successCycleCount >= 2 && history.recentFailureRatio > 0 && history.recentFailureRatio < 0.5) {
196
+ signals.push('openclaw_self_healed');
197
+ }
156
198
  // De-dup while preserving first-seen order.
157
199
  return [...new Set(signals)];
158
200
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { normalizeText } from '../signatures/signatures.js';
3
- const STRENGTH_WEIGHT = { strong: 1.0, agent: 0.8, weak: 0.4 };
3
+ const STRENGTH_WEIGHT = { strong: 1.0, agent: 0.8, success: 0.7, weak: 0.4 };
4
4
  function signatureOf(s) {
5
5
  return createHash('sha1').update(`${s.kind}\x1f${normalizeText(s.text)}`).digest('hex').slice(0, 16);
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-core",
3
- "version": "2.0.0-beta.12",
3
+ "version": "2.0.0-beta.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "hub-无关核心: 算法引擎/原材料/mailbox/资产库/workflow",