@evomap/evolver-core 2.0.0-beta.11 → 2.0.0-beta.13
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.
- package/dist/hooks/hooks.js +1 -0
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +8 -6
- package/dist/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -1
- package/dist/signals/cycleHistoryFromEvents.js +17 -8
- package/dist/signals/expand.js +10 -1
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +31 -6
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +42 -0
- package/dist/signals/signalGate.js +1 -1
- package/package.json +1 -1
package/dist/hooks/hooks.js
CHANGED
|
@@ -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
|
|
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 ?? '')}
|
|
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
|
-
|
|
119
|
-
.filter(([, count]) => count >= 2)
|
|
120
|
-
|
|
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
|
}
|
package/dist/schema/signal.d.ts
CHANGED
|
@@ -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;
|
package/dist/schema/signal.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
package/dist/signals/expand.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
}
|