@evomap/evolver 1.89.20 → 2.0.0-beta.0

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 (231) hide show
  1. package/README.md +27 -564
  2. package/bin/evolver-llm-proxy.js +3 -0
  3. package/bin/evolver-mcp.js +2 -0
  4. package/bin/evolver-proxy.js +3 -0
  5. package/bin/evolver.js +4 -0
  6. package/index.js +1 -3586
  7. package/package.json +32 -62
  8. package/CONTRIBUTING.md +0 -19
  9. package/LICENSE +0 -641
  10. package/README.ja-JP.md +0 -521
  11. package/README.ko-KR.md +0 -520
  12. package/README.zh-CN.md +0 -531
  13. package/SKILL.md +0 -365
  14. package/assets/cover.png +0 -0
  15. package/assets/gep/genes.seed.json +0 -496
  16. package/conformance/savings-core/constants.json +0 -30
  17. package/conformance/savings-core/golden-vectors.json +0 -333
  18. package/scripts/a2a_export.js +0 -63
  19. package/scripts/a2a_ingest.js +0 -79
  20. package/scripts/a2a_promote.js +0 -118
  21. package/scripts/analyze_by_skill.js +0 -121
  22. package/scripts/build_binaries.js +0 -479
  23. package/scripts/check-changelog.js +0 -166
  24. package/scripts/extract_log.js +0 -85
  25. package/scripts/generate_history.js +0 -75
  26. package/scripts/gep_append_event.js +0 -96
  27. package/scripts/gep_personality_report.js +0 -234
  28. package/scripts/human_report.js +0 -147
  29. package/scripts/recall-verify-report.js +0 -234
  30. package/scripts/recover_loop.js +0 -61
  31. package/scripts/refresh_stars_badge.js +0 -168
  32. package/scripts/seed-merchants.js +0 -91
  33. package/scripts/skill2recipes.js +0 -118
  34. package/scripts/suggest_version.js +0 -89
  35. package/scripts/validate-modules.js +0 -38
  36. package/scripts/validate-suite.js +0 -78
  37. package/skills/_meta/SKILL.md +0 -41
  38. package/skills/index.json +0 -14
  39. package/src/adapters/claudeCode.js +0 -194
  40. package/src/adapters/codex.js +0 -216
  41. package/src/adapters/cursor.js +0 -91
  42. package/src/adapters/hookAdapter.js +0 -469
  43. package/src/adapters/kiro.js +0 -195
  44. package/src/adapters/opencode.js +0 -326
  45. package/src/adapters/scripts/_lockPaths.js +0 -74
  46. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  47. package/src/adapters/scripts/_runtimePaths.js +0 -440
  48. package/src/adapters/scripts/evolver-session-end.js +0 -321
  49. package/src/adapters/scripts/evolver-session-start.js +0 -587
  50. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  51. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  52. package/src/atp/atpExecute.js +0 -283
  53. package/src/atp/atpTaskPickup.js +0 -233
  54. package/src/atp/autoBuyer.js +0 -382
  55. package/src/atp/autoDeliver.js +0 -215
  56. package/src/atp/cli.js +0 -354
  57. package/src/atp/cliAutobuyPrompt.js +0 -154
  58. package/src/atp/consumerAgent.js +0 -157
  59. package/src/atp/defaultHandler.js +0 -69
  60. package/src/atp/heartbeatSignalsHandler.js +0 -254
  61. package/src/atp/hubClient.js +0 -317
  62. package/src/atp/index.js +0 -38
  63. package/src/atp/merchantAgent.js +0 -118
  64. package/src/atp/protocol.js +0 -41
  65. package/src/atp/questionComposer.js +0 -133
  66. package/src/atp/serviceHelper.js +0 -92
  67. package/src/canary.js +0 -13
  68. package/src/config.js +0 -351
  69. package/src/evolve/guards.js +0 -1
  70. package/src/evolve/pipeline/collect.js +0 -1
  71. package/src/evolve/pipeline/dispatch.js +0 -1
  72. package/src/evolve/pipeline/enrich.js +0 -1
  73. package/src/evolve/pipeline/hub.js +0 -1
  74. package/src/evolve/pipeline/select.js +0 -1
  75. package/src/evolve/pipeline/signals.js +0 -1
  76. package/src/evolve/utils.js +0 -1
  77. package/src/evolve.js +0 -1
  78. package/src/experiment/agentRunner.js +0 -229
  79. package/src/experiment/cli.js +0 -159
  80. package/src/experiment/comparison.js +0 -233
  81. package/src/experiment/metrics.js +0 -75
  82. package/src/forceUpdate.js +0 -991
  83. package/src/gep/a2a.js +0 -173
  84. package/src/gep/a2aProtocol.js +0 -1
  85. package/src/gep/analyzer.js +0 -35
  86. package/src/gep/antiAbuseTelemetry.js +0 -1
  87. package/src/gep/assetCallLog.js +0 -197
  88. package/src/gep/assetStore.js +0 -723
  89. package/src/gep/assets.js +0 -36
  90. package/src/gep/autoDistillConv.js +0 -1
  91. package/src/gep/autoDistillLlm.js +0 -1
  92. package/src/gep/bridge.js +0 -138
  93. package/src/gep/candidateEval.js +0 -1
  94. package/src/gep/candidates.js +0 -1
  95. package/src/gep/claimNudge.js +0 -121
  96. package/src/gep/cliContracts.js +0 -1190
  97. package/src/gep/contentHash.js +0 -1
  98. package/src/gep/conversationDistiller.js +0 -1
  99. package/src/gep/conversationSniffer.js +0 -1
  100. package/src/gep/crypto.js +0 -1
  101. package/src/gep/curriculum.js +0 -1
  102. package/src/gep/deviceId.js +0 -1
  103. package/src/gep/directoryClient.js +0 -115
  104. package/src/gep/envFingerprint.js +0 -1
  105. package/src/gep/epigenetics.js +0 -1
  106. package/src/gep/execBridge.js +0 -1
  107. package/src/gep/executionTrace.js +0 -291
  108. package/src/gep/explore.js +0 -1
  109. package/src/gep/featureFlags.js +0 -121
  110. package/src/gep/gitOps.js +0 -265
  111. package/src/gep/hash.js +0 -1
  112. package/src/gep/hostErrorClassifier.js +0 -34
  113. package/src/gep/hubFetch.js +0 -1
  114. package/src/gep/hubReview.js +0 -1
  115. package/src/gep/hubSearch.js +0 -1
  116. package/src/gep/hubVerify.js +0 -1
  117. package/src/gep/idleScheduler.js +0 -400
  118. package/src/gep/issueReporter.js +0 -416
  119. package/src/gep/learningSignals.js +0 -1
  120. package/src/gep/llmReview.js +0 -92
  121. package/src/gep/localStateAwareness.js +0 -243
  122. package/src/gep/mailboxTransport.js +0 -119
  123. package/src/gep/memoryGraph.js +0 -1
  124. package/src/gep/memoryGraphAdapter.js +0 -1
  125. package/src/gep/mutation.js +0 -1
  126. package/src/gep/narrativeMemory.js +0 -1
  127. package/src/gep/oauthLogin.js +0 -181
  128. package/src/gep/openPRRegistry.js +0 -1
  129. package/src/gep/paths.js +0 -522
  130. package/src/gep/personality.js +0 -1
  131. package/src/gep/policyCheck.js +0 -1
  132. package/src/gep/portable.js +0 -103
  133. package/src/gep/privacyClient.js +0 -235
  134. package/src/gep/prompt.js +0 -1
  135. package/src/gep/questionGenerator.js +0 -518
  136. package/src/gep/recallInject.js +0 -1
  137. package/src/gep/recallVerifier.js +0 -1
  138. package/src/gep/reflection.js +0 -1
  139. package/src/gep/sanitize.js +0 -264
  140. package/src/gep/savingsCore.js +0 -1
  141. package/src/gep/schemas/capsule.js +0 -170
  142. package/src/gep/schemas/gene.js +0 -154
  143. package/src/gep/schemas/index.js +0 -8
  144. package/src/gep/schemas/protocol.js +0 -51
  145. package/src/gep/schemas/task.js +0 -74
  146. package/src/gep/selector.js +0 -1
  147. package/src/gep/selfPR.js +0 -469
  148. package/src/gep/signals.js +0 -776
  149. package/src/gep/skill2gep.js +0 -1056
  150. package/src/gep/skill2gepAudit.js +0 -303
  151. package/src/gep/skill2recipes.js +0 -511
  152. package/src/gep/skillDistiller.js +0 -1
  153. package/src/gep/skillPublisher.js +0 -358
  154. package/src/gep/solidify.js +0 -1
  155. package/src/gep/strategy.js +0 -1
  156. package/src/gep/taskReceiver.js +0 -575
  157. package/src/gep/tokenSavings.js +0 -1
  158. package/src/gep/trajectoryExport.js +0 -1
  159. package/src/gep/validationReport.js +0 -55
  160. package/src/gep/validator/index.js +0 -411
  161. package/src/gep/validator/reporter.js +0 -210
  162. package/src/gep/validator/sandboxExecutor.js +0 -480
  163. package/src/gep/validator/stakeBootstrap.js +0 -357
  164. package/src/gep/workspaceKeychain.js +0 -1
  165. package/src/ops/cleanup.js +0 -80
  166. package/src/ops/commentary.js +0 -60
  167. package/src/ops/health_check.js +0 -104
  168. package/src/ops/index.js +0 -11
  169. package/src/ops/innovation.js +0 -67
  170. package/src/ops/lifecycle.js +0 -798
  171. package/src/ops/self_repair.js +0 -76
  172. package/src/ops/skills_monitor.js +0 -147
  173. package/src/ops/trigger.js +0 -33
  174. package/src/proxy/clientSettings.js +0 -405
  175. package/src/proxy/envelope.js +0 -59
  176. package/src/proxy/extensions/dmHandler.js +0 -45
  177. package/src/proxy/extensions/sessionHandler.js +0 -141
  178. package/src/proxy/extensions/skillUpdater.js +0 -64
  179. package/src/proxy/extensions/traceControl.js +0 -1
  180. package/src/proxy/index.js +0 -1395
  181. package/src/proxy/inject.js +0 -1
  182. package/src/proxy/lifecycle/manager.js +0 -1568
  183. package/src/proxy/mailbox/state.js +0 -207
  184. package/src/proxy/mailbox/store.js +0 -590
  185. package/src/proxy/router/cache_passthrough.js +0 -26
  186. package/src/proxy/router/features.js +0 -84
  187. package/src/proxy/router/gemini_route.js +0 -154
  188. package/src/proxy/router/messages_route.js +0 -535
  189. package/src/proxy/router/model_router.js +0 -113
  190. package/src/proxy/router/models_route.js +0 -52
  191. package/src/proxy/router/ollama_route.js +0 -103
  192. package/src/proxy/router/responses_route.js +0 -170
  193. package/src/proxy/router/vertex_route.js +0 -110
  194. package/src/proxy/server/http.js +0 -363
  195. package/src/proxy/server/routes.js +0 -558
  196. package/src/proxy/server/settings.js +0 -115
  197. package/src/proxy/sync/engine.js +0 -179
  198. package/src/proxy/sync/inbound.js +0 -211
  199. package/src/proxy/sync/outbound.js +0 -320
  200. package/src/proxy/task/monitor.js +0 -131
  201. package/src/proxy/trace/extractor.js +0 -1
  202. package/src/proxy/trace/usage.js +0 -1
  203. package/src/webui/client/clientJs/assets.js +0 -111
  204. package/src/webui/client/clientJs/bootstrap.js +0 -92
  205. package/src/webui/client/clientJs/common.js +0 -77
  206. package/src/webui/client/clientJs/i18n.js +0 -366
  207. package/src/webui/client/clientJs/index.js +0 -35
  208. package/src/webui/client/clientJs/interactions.js +0 -351
  209. package/src/webui/client/clientJs/overview.js +0 -152
  210. package/src/webui/client/clientJs/personality.js +0 -285
  211. package/src/webui/client/clientJs/pipelines.js +0 -330
  212. package/src/webui/client/indexHtml.js +0 -221
  213. package/src/webui/client/static.js +0 -23
  214. package/src/webui/client/stylesCss.js +0 -639
  215. package/src/webui/client/vendor/README.md +0 -15
  216. package/src/webui/client/vendor/echarts.min.js +0 -45
  217. package/src/webui/index.js +0 -14
  218. package/src/webui/observer/assets.js +0 -146
  219. package/src/webui/observer/index.js +0 -37
  220. package/src/webui/observer/interactions.js +0 -127
  221. package/src/webui/observer/jsonl.js +0 -75
  222. package/src/webui/observer/paths.js +0 -46
  223. package/src/webui/observer/personality.js +0 -43
  224. package/src/webui/observer/pipelineEvents.js +0 -58
  225. package/src/webui/observer/redact.js +0 -63
  226. package/src/webui/observer/runs.js +0 -356
  227. package/src/webui/observer/safety.js +0 -57
  228. package/src/webui/observer/skills.js +0 -70
  229. package/src/webui/observer/status.js +0 -71
  230. package/src/webui/server/http.js +0 -138
  231. package/src/webui/server/routes.js +0 -41
@@ -1,776 +0,0 @@
1
- var { isHostClientError } = require('./hostErrorClassifier');
2
-
3
- // Opportunity signal names (shared with mutation.js and personality.js).
4
- var OPPORTUNITY_SIGNALS = [
5
- 'user_feature_request',
6
- 'user_improvement_suggestion',
7
- 'perf_bottleneck',
8
- 'capability_gap',
9
- 'stable_success_plateau',
10
- 'external_opportunity',
11
- 'recurring_error',
12
- 'unsupported_input_type',
13
- 'evolution_stagnation_detected',
14
- 'repair_loop_detected',
15
- 'force_innovation_after_repair_loop',
16
- 'tool_bypass',
17
- 'curriculum_target',
18
- 'issue_already_resolved',
19
- 'openclaw_self_healed',
20
- 'empty_cycle_loop_detected',
21
- 'explore_opportunity',
22
- 'hub_search_miss_with_problem',
23
- 'plateau_pivot_required',
24
- 'plateau_pivot_suggested',
25
- ];
26
-
27
- function hasOpportunitySignal(signals) {
28
- var list = Array.isArray(signals) ? signals : [];
29
- for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
30
- var name = OPPORTUNITY_SIGNALS[i];
31
- if (list.includes(name)) return true;
32
- if (list.some(function (s) { return String(s).startsWith(name + ':'); })) return true;
33
- }
34
- return false;
35
- }
36
-
37
- // A cycle that produced no file/line changes is a no-op ("empty cycle"), not a
38
- // genuine failure -- even when its outcome is recorded as `failed`. The most
39
- // common way a `failed` event ends up with zero blast radius is a claimed-
40
- // success cycle rejected by the anti-forgery guard (`zero_blast_radius_with_
41
- // success`): the sub-agent declared success but touched nothing -- e.g. an
42
- // `innovate` gene that only produces metadata/strategy and never edits code.
43
- // Empty cycles are tracked separately (consecutiveEmptyCycles) and drive
44
- // graceful steady-state degradation, NOT the failure-loop ban. Single source of
45
- // truth so the empty/failure split stays consistent across every counter below.
46
- function _isEmptyCycle(evt) {
47
- if (!evt) return false;
48
- if (evt.meta && evt.meta.empty_cycle) return true;
49
- var br = evt.blast_radius;
50
- return !!(br && br.files === 0 && br.lines === 0);
51
- }
52
-
53
- // Build a de-duplication set from recent evolution events.
54
- // Returns an object: { suppressedSignals: Set<string>, recentIntents: string[], consecutiveRepairCount: number }
55
- function analyzeRecentHistory(recentEvents) {
56
- if (!Array.isArray(recentEvents) || recentEvents.length === 0) {
57
- return { suppressedSignals: new Set(), recentIntents: [], consecutiveRepairCount: 0 };
58
- }
59
- // Take only the last 10 events
60
- var recent = recentEvents.slice(-10);
61
-
62
- // Count consecutive same-intent runs at the tail
63
- var consecutiveRepairCount = 0;
64
- for (var i = recent.length - 1; i >= 0; i--) {
65
- if (recent[i].intent === 'repair') {
66
- consecutiveRepairCount++;
67
- } else {
68
- break;
69
- }
70
- }
71
-
72
- // Count signal frequency in last 8 events: signal -> count
73
- var signalFreq = {};
74
- var geneFreq = {};
75
- var tail = recent.slice(-8);
76
- for (var j = 0; j < tail.length; j++) {
77
- var evt = tail[j];
78
- var sigs = Array.isArray(evt.signals) ? evt.signals : [];
79
- for (var k = 0; k < sigs.length; k++) {
80
- var s = String(sigs[k]);
81
- // Normalize: strip details suffix so frequency keys match dedup filter keys
82
- var key = s.startsWith('errsig:') ? 'errsig'
83
- : s.startsWith('recurring_errsig') ? 'recurring_errsig'
84
- : s.startsWith('user_feature_request:') ? 'user_feature_request'
85
- : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion'
86
- : s;
87
- signalFreq[key] = (signalFreq[key] || 0) + 1;
88
- }
89
- var genes = Array.isArray(evt.genes_used) ? evt.genes_used : [];
90
- for (var g = 0; g < genes.length; g++) {
91
- geneFreq[String(genes[g])] = (geneFreq[String(genes[g])] || 0) + 1;
92
- }
93
- }
94
-
95
- // Suppress signals that appeared in 3+ of the last 8 events (they are being over-processed)
96
- var suppressedSignals = new Set();
97
- var entries = Object.entries(signalFreq);
98
- for (var ei = 0; ei < entries.length; ei++) {
99
- if (entries[ei][1] >= 3) {
100
- suppressedSignals.add(entries[ei][0]);
101
- }
102
- }
103
-
104
- var recentIntents = recent.map(function(e) { return e.intent || 'unknown'; });
105
-
106
- // Count empty cycles (no file/line changes) in last 8 events.
107
- // High ratio indicates the evolver is spinning without producing real changes.
108
- var emptyCycleCount = 0;
109
- for (var ec = 0; ec < tail.length; ec++) {
110
- if (_isEmptyCycle(tail[ec])) {
111
- emptyCycleCount++;
112
- }
113
- }
114
-
115
- // Count consecutive empty cycles at the tail (not just total in last 8).
116
- // This detects saturation: the evolver has exhausted innovation space and keeps producing
117
- // zero-change cycles. Used to trigger graceful degradation to steady-state mode.
118
- var consecutiveEmptyCycles = 0;
119
- for (var se = recent.length - 1; se >= 0; se--) {
120
- if (_isEmptyCycle(recent[se])) {
121
- consecutiveEmptyCycles++;
122
- } else {
123
- break;
124
- }
125
- }
126
-
127
- // Count consecutive *productive* failures at the tail of recent events.
128
- // This tells the evolver "you have been failing N times in a row -- slow down."
129
- //
130
- // A no-op (zero-blast) cycle breaks the streak instead of extending it: it is
131
- // already counted as an empty cycle above, which drives graceful steady-state
132
- // degradation. Without this guard a gene that keeps producing empty cycles --
133
- // or a sub-agent whose claimed-success cycles are anti-forgery-rejected to
134
- // `failed` with zero blast radius -- would double-trigger failure_loop_detected
135
- // + ban_gene and file a spurious recurring-failure GitHub issue (see #577).
136
- var consecutiveFailureCount = 0;
137
- for (var cf = recent.length - 1; cf >= 0; cf--) {
138
- if (_isEmptyCycle(recent[cf])) break;
139
- var outcome = recent[cf].outcome;
140
- if (outcome && outcome.status === 'failed') {
141
- consecutiveFailureCount++;
142
- } else {
143
- break;
144
- }
145
- }
146
-
147
- // Count total *productive* failures in last 8 events (failure ratio).
148
- // Empty (zero-blast) cycles are excluded for the same reason as the
149
- // consecutive-failure streak above -- a no-op is saturation, not a failure.
150
- var recentFailureCount = 0;
151
- for (var rf = 0; rf < tail.length; rf++) {
152
- if (_isEmptyCycle(tail[rf])) continue;
153
- var rfOut = tail[rf].outcome;
154
- if (rfOut && rfOut.status === 'failed') recentFailureCount++;
155
- }
156
-
157
- return {
158
- suppressedSignals: suppressedSignals,
159
- recentIntents: recentIntents,
160
- consecutiveRepairCount: consecutiveRepairCount,
161
- emptyCycleCount: emptyCycleCount,
162
- consecutiveEmptyCycles: consecutiveEmptyCycles,
163
- consecutiveFailureCount: consecutiveFailureCount,
164
- recentFailureCount: recentFailureCount,
165
- recentFailureRatio: tail.length > 0 ? recentFailureCount / tail.length : 0,
166
- signalFreq: signalFreq,
167
- geneFreq: geneFreq,
168
- };
169
- }
170
-
171
- // ---------------------------------------------------------------------------
172
- // Signal Extraction Strategy: Weighted Keyword Scoring (Layer 2)
173
- // Unlike regex (binary hit/miss), keyword scoring accumulates weighted
174
- // evidence from multiple keywords and fires only when confidence exceeds
175
- // a threshold. This catches fuzzy/distributed patterns that no single
176
- // regex can match.
177
- // ---------------------------------------------------------------------------
178
- var SIGNAL_PROFILES = {
179
- perf_bottleneck: {
180
- keywords: { 'slow': 3, 'timeout': 4, 'timed out': 4, 'latency': 3, 'bottleneck': 5,
181
- 'lag': 2, 'delay': 2, 'hung': 3, 'freeze': 3, 'unresponsive': 4,
182
- 'took too long': 4, 'high cpu': 4, 'high memory': 4, 'oom': 5,
183
- 'out of memory': 5, 'performance': 2, 'throttle': 3 },
184
- threshold: 6,
185
- },
186
- capability_gap: {
187
- keywords: { 'not supported': 5, 'cannot': 1, 'unsupported': 4, 'not implemented': 5,
188
- 'no way to': 3, 'missing feature': 5, 'not available': 3,
189
- 'no support for': 4, 'unavailable': 3, 'incompatible': 3 },
190
- threshold: 5,
191
- },
192
- user_feature_request: {
193
- keywords: { 'add': 1, 'implement': 3, 'create': 2, 'build': 2, 'feature': 3,
194
- 'i want': 3, 'i need': 3, 'we need': 3, 'please add': 4,
195
- 'new function': 4, 'new module': 4, 'endpoint': 2, 'capability': 2,
196
- 'support for': 2 },
197
- threshold: 6,
198
- },
199
- user_improvement_suggestion: {
200
- keywords: { 'improve': 3, 'enhance': 3, 'upgrade': 3, 'refactor': 4,
201
- 'clean up': 3, 'simplify': 3, 'streamline': 3, 'optimize': 3,
202
- 'could be better': 4, 'should be': 2, 'more efficient': 3 },
203
- threshold: 5,
204
- },
205
- recurring_error: {
206
- keywords: { 'error': 1, 'exception': 2, 'failed': 1, 'crash': 4,
207
- 'again': 1, 'still': 1, 'keeps': 2, 'repeatedly': 4,
208
- 'same error': 5, 'still failing': 5, 'not fixed': 4 },
209
- threshold: 7,
210
- },
211
- tool_bypass: {
212
- keywords: { 'exec': 2, 'shell': 2, 'subprocess': 3, 'child_process': 3,
213
- 'curl': 2, 'wget': 2, 'ad-hoc': 3, 'workaround': 3,
214
- 'hack': 2, 'manual': 1 },
215
- threshold: 6,
216
- },
217
- evolution_stagnation_detected: {
218
- keywords: { 'no change': 4, 'same result': 4, 'stuck': 3, 'plateau': 4,
219
- 'stagnant': 5, 'no progress': 5, 'spinning': 3, 'idle': 2,
220
- 'nothing new': 4, 'exhausted': 3 },
221
- threshold: 6,
222
- },
223
- };
224
-
225
- function _extractKeywordScore(lower) {
226
- var scored = [];
227
- var profileKeys = Object.keys(SIGNAL_PROFILES);
228
- for (var pi = 0; pi < profileKeys.length; pi++) {
229
- var signalName = profileKeys[pi];
230
- var profile = SIGNAL_PROFILES[signalName];
231
- var totalScore = 0;
232
- var kwKeys = Object.keys(profile.keywords);
233
- for (var ki = 0; ki < kwKeys.length; ki++) {
234
- var kw = kwKeys[ki];
235
- var weight = profile.keywords[kw];
236
- var idx = 0;
237
- var count = 0;
238
- while (idx < lower.length && count < 20) {
239
- var pos = lower.indexOf(kw, idx);
240
- if (pos === -1) break;
241
- count++;
242
- idx = pos + kw.length;
243
- }
244
- totalScore += count * weight;
245
- }
246
- if (totalScore >= profile.threshold) {
247
- scored.push(signalName);
248
- }
249
- }
250
- return scored;
251
- }
252
-
253
- // ---------------------------------------------------------------------------
254
- // Signal Extraction Strategy: LLM Semantic Analysis (Layer 3)
255
- // Sends a corpus summary to the Hub for LLM-based signal extraction.
256
- // Rate-limited to every N evolution cycles. Falls back silently on failure.
257
- // ---------------------------------------------------------------------------
258
- var _llmSignalCycleCount = 0;
259
- var LLM_SIGNAL_INTERVAL = 5;
260
-
261
- function _curlHubIpFamilyArgs(env) {
262
- return _curlHubIpFamilyAttempts(env)[0].slice();
263
- }
264
-
265
- function _resolveCurlHubIpFamily(env) {
266
- var source = env || process.env;
267
- var raw = String(source.EVOMAP_HUB_IP_FAMILY || 'ipv4first').trim().toLowerCase();
268
- if (raw === 'ipv4' || raw === 'v4' || raw === '4' || raw === 'ipv4first' || raw === 'ipv4-first') return 'ipv4first';
269
- if (raw === 'ipv4only' || raw === 'ipv4-only') return 'ipv4only';
270
- if (raw === 'auto' || raw === 'dualstack' || raw === 'dual-stack') return 'auto';
271
- throw new Error('[signals] EVOMAP_HUB_IP_FAMILY must be "ipv4", "ipv4-only", or "auto" - got ' + JSON.stringify(source.EVOMAP_HUB_IP_FAMILY));
272
- }
273
-
274
- function _curlHubIpFamilyAttempts(env) {
275
- var family = _resolveCurlHubIpFamily(env);
276
- if (family === 'auto') return [[]];
277
- if (family === 'ipv4only') return [['-4']];
278
- // Match hubFetch's default Hub egress posture: IPv4-first with a dual-stack
279
- // fallback unless EVOMAP_HUB_IP_FAMILY explicitly disables fallback.
280
- return [['-4'], []];
281
- }
282
-
283
- function _curlHeaderConfigValue(value) {
284
- return '"' + String(value)
285
- .replace(/\\/g, '\\\\')
286
- .replace(/"/g, '\\"')
287
- .replace(/\r/g, '\\r')
288
- .replace(/\n/g, '\\n')
289
- .replace(/\t/g, '\\t') + '"';
290
- }
291
-
292
- function _curlAuthConfig(nodeSecret) {
293
- return 'header = ' + _curlHeaderConfigValue('Authorization: Bearer ' + nodeSecret) + '\n';
294
- }
295
-
296
- function _withTempPayloadFile(payload, fn) {
297
- var fs = require('fs');
298
- var os = require('os');
299
- var path = require('path');
300
- var dir = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-signal-'));
301
- var file = path.join(dir, 'payload.json');
302
- try {
303
- fs.writeFileSync(file, payload, { encoding: 'utf8', mode: 0o600 });
304
- return fn(file);
305
- } finally {
306
- try { fs.unlinkSync(file); } catch (_) {}
307
- try { fs.rmdirSync(dir); } catch (_) {}
308
- }
309
- }
310
-
311
- function _extractLLM(corpus) {
312
- _llmSignalCycleCount++;
313
- if (_llmSignalCycleCount % LLM_SIGNAL_INTERVAL !== 1) return [];
314
-
315
- try {
316
- var getHubUrl = require('./a2aProtocol').getHubUrl;
317
- var getHubNodeSecret = require('./a2aProtocol').getHubNodeSecret;
318
- var getNodeId = require('./a2aProtocol').getNodeId;
319
- var enforceHubScheme = require('./hubFetch').enforceHubScheme;
320
- var hubUrl = getHubUrl();
321
- var nodeSecret = getHubNodeSecret();
322
- if (!hubUrl || !nodeSecret) return [];
323
-
324
- var summary = corpus.slice(0, 2000);
325
- var postData = JSON.stringify({
326
- corpus_summary: summary,
327
- signal_types: OPPORTUNITY_SIGNALS,
328
- sender_id: getNodeId() || undefined,
329
- });
330
-
331
- var url = hubUrl.replace(/\/+$/, '') + '/a2a/signal/analyze';
332
- enforceHubScheme(url);
333
-
334
- // Use execFileSync (no shell) + curl argv array so URL and file paths are
335
- // passed as discrete argv entries. The bearer token is supplied through
336
- // curl config on stdin, not argv, so process listings do not expose it.
337
- // Sync HTTP is required because this runs inside a spin-wait loop where
338
- // Node's async http callbacks cannot fire.
339
- var execFileSync = require('child_process').execFileSync;
340
- var stdout = '';
341
- try {
342
- stdout = _withTempPayloadFile(postData, function (payloadFile) {
343
- var attempts = _curlHubIpFamilyAttempts(process.env);
344
- var lastErr = null;
345
- for (var i = 0; i < attempts.length; i++) {
346
- var curlArgs = [
347
- '-sS', '-m', '10',
348
- ].concat(attempts[i], [
349
- '-X', 'POST',
350
- '-H', 'Content-Type: application/json',
351
- '--data-binary', '@' + payloadFile,
352
- '-K', '-',
353
- url,
354
- ]);
355
- try {
356
- return execFileSync('curl', curlArgs, {
357
- input: _curlAuthConfig(nodeSecret),
358
- timeout: 12000,
359
- windowsHide: true,
360
- stdio: ['pipe', 'pipe', 'pipe'],
361
- encoding: 'utf8',
362
- });
363
- } catch (err) {
364
- lastErr = err;
365
- }
366
- }
367
- throw lastErr || new Error('curl_failed');
368
- });
369
- } catch (_) {
370
- return [];
371
- }
372
-
373
- if (!stdout || typeof stdout !== 'string') return [];
374
-
375
- var parsed = JSON.parse(stdout);
376
- if (Array.isArray(parsed.signals)) {
377
- return parsed.signals.filter(function (s) {
378
- return typeof s === 'string' && s.length > 0 && s.length < 200;
379
- }).slice(0, 10);
380
- }
381
- return [];
382
- } catch (e) {
383
- return [];
384
- }
385
- }
386
-
387
- // ---------------------------------------------------------------------------
388
- // Signal Merge: combine results from all extraction strategies.
389
- // Deduplicates and logs per-layer contribution for observability.
390
- // ---------------------------------------------------------------------------
391
- function _mergeSignals(regexSignals, scoreSignals, llmSignals) {
392
- var merged = new Set();
393
- var ri, si, li;
394
- for (ri = 0; ri < regexSignals.length; ri++) merged.add(regexSignals[ri]);
395
- for (si = 0; si < scoreSignals.length; si++) merged.add(scoreSignals[si]);
396
- for (li = 0; li < llmSignals.length; li++) merged.add(llmSignals[li]);
397
-
398
- var scoreOnly = scoreSignals.filter(function (s) { return !regexSignals.includes(s); });
399
- var llmOnly = llmSignals.filter(function (s) { return !regexSignals.includes(s) && !scoreSignals.includes(s); });
400
- var overlap = regexSignals.filter(function (s) { return scoreSignals.includes(s) || llmSignals.includes(s); });
401
-
402
- if (scoreOnly.length > 0 || llmOnly.length > 0 || overlap.length > 0) {
403
- console.log('[Signals] Multi-strategy: regex=' + regexSignals.length +
404
- ', score=' + scoreSignals.length +
405
- ', llm=' + llmSignals.length +
406
- ', merged=' + merged.size +
407
- (scoreOnly.length > 0 ? ' | score-only: ' + scoreOnly.join(', ') : '') +
408
- (llmOnly.length > 0 ? ' | llm-only: ' + llmOnly.join(', ') : '') +
409
- (overlap.length > 0 ? ' | confirmed: ' + overlap.join(', ') : ''));
410
- }
411
-
412
- return Array.from(merged);
413
- }
414
-
415
- // ---------------------------------------------------------------------------
416
- // Signal Extraction Strategy: Regex Pattern Matching (Layer 1)
417
- // Deterministic, zero-latency, hand-crafted rules for known signal patterns.
418
- // ---------------------------------------------------------------------------
419
- function _extractRegex(corpus, lower, errorHit) {
420
- var signals = [];
421
-
422
- if (errorHit) signals.push('log_error');
423
-
424
- try {
425
- var lines = corpus
426
- .split('\n')
427
- .map(function (l) { return String(l || '').trim(); })
428
- .filter(Boolean);
429
-
430
- var errLine =
431
- lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/i.test(l); }) ||
432
- null;
433
-
434
- if (errLine) {
435
- var clipped = errLine.replace(/\s+/g, ' ').slice(0, 260);
436
- signals.push('errsig:' + clipped);
437
- }
438
- } catch (e) { /* error-line extraction non-critical */ }
439
-
440
- if (lower.includes('memory.md missing')) signals.push('memory_missing');
441
- if (lower.includes('user.md missing')) signals.push('user_missing');
442
- if (lower.includes('key missing')) signals.push('integration_key_missing');
443
- if (lower.includes('no session logs found') || lower.includes('no jsonl files')) signals.push('session_logs_missing');
444
- if (process.platform === 'win32' && (lower.includes('pgrep') || lower.includes('ps aux') || lower.includes('cat >') || lower.includes('heredoc'))) {
445
- signals.push('windows_shell_incompatible');
446
- }
447
- if (lower.includes('path.resolve(__dirname, \'../../../')) signals.push('path_outside_workspace');
448
-
449
- // Protocol-specific drift signals
450
- if (lower.includes('prompt') && !lower.includes('evolutionevent')) signals.push('protocol_drift');
451
-
452
- // --- Recurring error detection (robustness signals) ---
453
- // Count repeated identical errors -- these indicate systemic issues that need automated fixes
454
- try {
455
- var errorCounts = {};
456
- var errPatterns = corpus.match(/(?:LLM error|"error"|"status":\s*"error")[^}]{0,200}/gi) || [];
457
- for (var ep = 0; ep < errPatterns.length; ep++) {
458
- // Normalize to a short key
459
- var key = errPatterns[ep].replace(/\s+/g, ' ').slice(0, 100);
460
- errorCounts[key] = (errorCounts[key] || 0) + 1;
461
- }
462
- var recurringErrors = Object.entries(errorCounts).filter(function (e) { return e[1] >= 3; });
463
- if (recurringErrors.length > 0) {
464
- signals.push('recurring_error');
465
- // Include the top recurring error signature for the agent to diagnose
466
- var topErr = recurringErrors.sort(function (a, b) { return b[1] - a[1]; })[0];
467
- signals.push('recurring_errsig(' + topErr[1] + 'x):' + topErr[0].slice(0, 150));
468
- }
469
- } catch (e) { /* recurring error detection non-critical */ }
470
-
471
- // --- Unsupported input type (e.g. GIF, video formats the LLM can't handle) ---
472
- if (/unsupported mime|unsupported.*type|invalid.*mime/i.test(lower)) {
473
- signals.push('unsupported_input_type');
474
- }
475
-
476
- // --- Opportunity signals (innovation / feature requests) ---
477
- // Support 4 languages: EN, ZH-CN, ZH-TW, JA. Attach snippet for selector/prompt use.
478
-
479
- var featureRequestSnippet = '';
480
- var featEn = corpus.match(/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,120}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i);
481
- if (featEn) featureRequestSnippet = featEn[0].replace(/\s+/g, ' ').trim().slice(0, 200);
482
- if (!featureRequestSnippet && /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) {
483
- var featWant = corpus.match(/.{0,80}\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b.{0,80}/i);
484
- featureRequestSnippet = featWant ? featWant[0].replace(/\s+/g, ' ').trim().slice(0, 200) : 'feature request';
485
- }
486
- if (!featureRequestSnippet && /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus)) {
487
- var featZh = corpus.match(/.{0,100}(加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能).{0,100}/);
488
- if (featZh) featureRequestSnippet = featZh[0].replace(/\s+/g, ' ').trim().slice(0, 200);
489
- if (!featureRequestSnippet && /我想/.test(corpus)) {
490
- var featWantZh = corpus.match(/我想\s*[,,\.。、\s]*([\s\S]{0,400})/);
491
- featureRequestSnippet = featWantZh ? (featWantZh[1].replace(/\s+/g, ' ').trim().slice(0, 200) || '功能需求') : '功能需求';
492
- }
493
- if (!featureRequestSnippet) featureRequestSnippet = '功能需求';
494
- }
495
- if (!featureRequestSnippet && /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus)) {
496
- var featTw = corpus.match(/.{0,100}(加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加).{0,100}/);
497
- featureRequestSnippet = featTw ? featTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '功能需求';
498
- }
499
- if (!featureRequestSnippet && /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) {
500
- var featJa = corpus.match(/.{0,100}(追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい).{0,100}/);
501
- featureRequestSnippet = featJa ? featJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '機能要望';
502
- }
503
- if (featureRequestSnippet || /\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus) ||
504
- /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower) ||
505
- /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus) ||
506
- /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus) ||
507
- /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) {
508
- signals.push('user_feature_request');
509
- if (featureRequestSnippet) signals.push('user_feature_request:' + featureRequestSnippet);
510
- }
511
-
512
- // user_improvement_suggestion: 4 languages + snippet
513
- var improvementSnippet = '';
514
- if (!errorHit) {
515
- var impEn = corpus.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/i);
516
- if (impEn) improvementSnippet = impEn[0].replace(/\s+/g, ' ').trim().slice(0, 200);
517
- if (!improvementSnippet && /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus)) {
518
- var impZh = corpus.match(/.{0,100}(改进一下|优化一下|简化|重构|整理一下|弄得更好).{0,100}/);
519
- improvementSnippet = impZh ? impZh[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改进建议';
520
- }
521
- if (!improvementSnippet && /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus)) {
522
- var impTw = corpus.match(/.{0,100}(改進一下|優化一下|簡化|重構|整理一下|弄得更好).{0,100}/);
523
- improvementSnippet = impTw ? impTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改進建議';
524
- }
525
- if (!improvementSnippet && /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus)) {
526
- var impJa = corpus.match(/.{0,100}(改善|最適化|簡素化|リファクタ|良くして|改良).{0,100}/);
527
- improvementSnippet = impJa ? impJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改善要望';
528
- }
529
- var hasImprovement = improvementSnippet ||
530
- /\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower) ||
531
- /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus) ||
532
- /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus) ||
533
- /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus);
534
- if (hasImprovement) {
535
- signals.push('user_improvement_suggestion');
536
- if (improvementSnippet) signals.push('user_improvement_suggestion:' + improvementSnippet);
537
- }
538
- }
539
-
540
- // perf_bottleneck: performance issues detected
541
- if (/\b(slow|timeout|timed?\s*out|latency|bottleneck|took too long|performance issue|high cpu|high memory|oom|out of memory)\b/i.test(lower)) {
542
- signals.push('perf_bottleneck');
543
- }
544
-
545
- // capability_gap: something is explicitly unsupported or missing
546
- if (/\b(not supported|cannot|doesn'?t support|no way to|missing feature|unsupported|not available|not implemented|no support for)\b/i.test(lower)) {
547
- // Only fire if it is not just a missing file/config signal
548
- if (!signals.includes('memory_missing') && !signals.includes('user_missing') && !signals.includes('session_logs_missing')) {
549
- signals.push('capability_gap');
550
- }
551
- }
552
-
553
- // --- Tool Usage Analytics ---
554
- var toolUsage = {};
555
- var toolMatches = corpus.match(/\[TOOL:\s*([\w-]+)\]/g) || [];
556
-
557
- // Extract exec commands to identify benign loops (like watchdog checks)
558
- var execCommands = corpus.match(/exec: (node\s+[\w\/\.-]+\.js\s+ensure)/g) || [];
559
- var benignExecCount = execCommands.length;
560
-
561
- for (var i = 0; i < toolMatches.length; i++) {
562
- var toolName = toolMatches[i].match(/\[TOOL:\s*([\w-]+)\]/)[1];
563
- toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
564
- }
565
-
566
- // Adjust exec count by subtracting benign commands
567
- if (toolUsage['exec']) {
568
- toolUsage['exec'] = Math.max(0, toolUsage['exec'] - benignExecCount);
569
- }
570
-
571
- Object.keys(toolUsage).forEach(function(tool) {
572
- if (toolUsage[tool] >= 10) { // Bumped threshold from 5 to 10
573
- signals.push('high_tool_usage:' + tool);
574
- }
575
- // Detect repeated exec usage (often a sign of manual loops or inefficient automation)
576
- if (tool === 'exec' && toolUsage[tool] >= 5) { // Bumped threshold from 3 to 5
577
- signals.push('repeated_tool_usage:exec');
578
- }
579
- });
580
-
581
- // --- Tool bypass detection ---
582
- // When the agent uses shell/exec to run ad-hoc scripts instead of registered tools,
583
- // it indicates a tool integrity issue (bypassing the tool layer).
584
- var bypassPatterns = [
585
- /node\s+\S+\.m?js/,
586
- /npx\s+/,
587
- /curl\s+.*api/i,
588
- /python\s+\S+\.py/,
589
- ];
590
- var execContent = corpus.match(/exec:.*$/gm) || [];
591
- for (var bpi = 0; bpi < execContent.length; bpi++) {
592
- var line = execContent[bpi];
593
- for (var bpj = 0; bpj < bypassPatterns.length; bpj++) {
594
- if (bypassPatterns[bpj].test(line)) {
595
- signals.push('tool_bypass');
596
- bpi = execContent.length;
597
- break;
598
- }
599
- }
600
- }
601
-
602
- return signals;
603
- }
604
-
605
- // ---------------------------------------------------------------------------
606
- // extractSignals: Multi-strategy orchestrator.
607
- // Calls three extraction layers (regex, keyword scoring, LLM semantic),
608
- // merges their outputs, then applies post-processing (prioritization,
609
- // history-based dedup, innovation forcing).
610
- // Signature and return type are unchanged -- callers are not affected.
611
- // ---------------------------------------------------------------------------
612
- function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, userSnippet, recentEvents }) {
613
- var corpus = [
614
- String(recentSessionTranscript || ''),
615
- String(todayLog || ''),
616
- String(memorySnippet || ''),
617
- String(userSnippet || ''),
618
- ].join('\n');
619
- var lower = corpus.toLowerCase();
620
-
621
- var history = analyzeRecentHistory(recentEvents || []);
622
-
623
- // Is the recent failure corpus dominated by an unrecoverable host/LLM client
624
- // error (4xx: malformed request / auth / quota)? Such failures are the host
625
- // Agent's provider config, not the Gene -- they must not feed the
626
- // failure-loop streak / ban_gene path below (#571, follow-up to #534).
627
- var hostClientError = isHostClientError(corpus);
628
-
629
- var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/.test(lower);
630
-
631
- // Layer 1: Regex (deterministic, 0ms)
632
- var regexSignals = _extractRegex(corpus, lower, errorHit);
633
-
634
- // Layer 2: Weighted keyword scoring (statistical, 0ms)
635
- var scoreSignals = _extractKeywordScore(lower);
636
-
637
- // Layer 3: LLM semantic analysis (rate-limited, async, optional)
638
- var llmSignals = _extractLLM(corpus);
639
-
640
- // Merge all layers
641
- var signals = _mergeSignals(regexSignals, scoreSignals, llmSignals);
642
-
643
- // --- Post-processing (applies to merged signal set) ---
644
-
645
- // Signal prioritization: remove cosmetic signals when actionable ones exist
646
- var actionable = signals.filter(function (s) {
647
- return s !== 'user_missing' && s !== 'memory_missing' && s !== 'session_logs_missing' && s !== 'windows_shell_incompatible';
648
- });
649
- if (actionable.length > 0) {
650
- signals = actionable;
651
- }
652
-
653
- // De-duplication: suppress signals that have been over-processed in recent history
654
- if (history.suppressedSignals.size > 0) {
655
- var beforeDedup = signals.length;
656
- signals = signals.filter(function (s) {
657
- var key = s.startsWith('errsig:') ? 'errsig'
658
- : s.startsWith('recurring_errsig') ? 'recurring_errsig'
659
- : s.startsWith('user_feature_request:') ? 'user_feature_request'
660
- : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion'
661
- : s;
662
- return !history.suppressedSignals.has(key);
663
- });
664
- if (beforeDedup > 0 && signals.length === 0) {
665
- signals.push('evolution_stagnation_detected');
666
- signals.push('stable_success_plateau');
667
- }
668
- }
669
-
670
- // Force innovation after 3+ consecutive repairs
671
- if (history.consecutiveRepairCount >= 3) {
672
- signals = signals.filter(function (s) {
673
- return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
674
- });
675
- if (signals.length === 0) {
676
- signals.push('repair_loop_detected');
677
- signals.push('stable_success_plateau');
678
- }
679
- signals.push('force_innovation_after_repair_loop');
680
- }
681
-
682
- // Force innovation after too many empty cycles (zero blast radius)
683
- if (history.emptyCycleCount >= 4) {
684
- signals = signals.filter(function (s) {
685
- return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
686
- });
687
- if (!signals.includes('empty_cycle_loop_detected')) signals.push('empty_cycle_loop_detected');
688
- if (!signals.includes('stable_success_plateau')) signals.push('stable_success_plateau');
689
- }
690
-
691
- // Saturation detection (graceful degradation)
692
- if (history.consecutiveEmptyCycles >= 5) {
693
- if (!signals.includes('force_steady_state')) signals.push('force_steady_state');
694
- if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
695
- } else if (history.consecutiveEmptyCycles >= 3) {
696
- if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
697
- }
698
-
699
- // Exploration opportunity: when saturated, inject explore signal so the
700
- // idle gating path can trigger proactive exploration instead of sleeping.
701
- if (history.consecutiveEmptyCycles >= 3 && !signals.includes('explore_opportunity')) {
702
- signals.push('explore_opportunity');
703
- }
704
-
705
- // Failure streak awareness
706
- if (history.consecutiveFailureCount >= 3) {
707
- if (hostClientError) {
708
- // Non-Gene-attributable: an unrecoverable host/LLM 4xx is driving the
709
- // streak. Surface an actionable signal pointing at the host LLM config
710
- // and skip the streak/ban path so we never ban an innocent Gene or trip
711
- // failure_loop_detected (#571).
712
- if (!signals.includes('host_llm_client_error')) signals.push('host_llm_client_error');
713
- } else {
714
- signals.push('consecutive_failure_streak_' + history.consecutiveFailureCount);
715
- if (history.consecutiveFailureCount >= 5) {
716
- signals.push('failure_loop_detected');
717
- var topGene = null;
718
- var topGeneCount = 0;
719
- var gfEntries = Object.entries(history.geneFreq);
720
- for (var gfi = 0; gfi < gfEntries.length; gfi++) {
721
- if (gfEntries[gfi][1] > topGeneCount) {
722
- topGeneCount = gfEntries[gfi][1];
723
- topGene = gfEntries[gfi][0];
724
- }
725
- }
726
- if (topGene) {
727
- signals.push('ban_gene:' + topGene);
728
- }
729
- }
730
- }
731
- }
732
-
733
- // High failure ratio in recent history (>= 75% failed in last 8 cycles)
734
- if (history.recentFailureRatio >= 0.75) {
735
- if (hostClientError) {
736
- // Same rationale as the streak block: a host/LLM 4xx is not the Gene's
737
- // fault, so do not force innovation away from it (#571).
738
- if (!signals.includes('host_llm_client_error')) signals.push('host_llm_client_error');
739
- } else {
740
- signals.push('high_failure_ratio');
741
- signals.push('force_innovation_after_repair_loop');
742
- }
743
- }
744
-
745
- // Plateau detection: recent scores trending down or stagnant.
746
- // Uses score data from recentEvents to detect diminishing returns.
747
- if (Array.isArray(recentEvents) && recentEvents.length >= 4) {
748
- var recentScores = recentEvents.slice(-6).map(function (e) {
749
- return e.outcome && typeof e.outcome.score === 'number' ? e.outcome.score : -1;
750
- }).filter(function (s) { return s >= 0; });
751
- if (recentScores.length >= 3) {
752
- var avgScore = recentScores.reduce(function (a, b) { return a + b; }, 0) / recentScores.length;
753
- var improving = recentScores.length >= 2 && recentScores[recentScores.length - 1] > recentScores[recentScores.length - 2] + 0.05;
754
- if (avgScore < 0.35 && !improving) {
755
- signals.push('plateau_pivot_required');
756
- } else if (avgScore < 0.55 && !improving && history.consecutiveRepairCount >= 2) {
757
- signals.push('plateau_pivot_suggested');
758
- }
759
- }
760
- }
761
-
762
- // If no signals at all, add a default innovation signal
763
- if (signals.length === 0) {
764
- signals.push('stable_success_plateau');
765
- }
766
-
767
- return Array.from(new Set(signals));
768
- }
769
-
770
- module.exports = {
771
- extractSignals, hasOpportunitySignal, analyzeRecentHistory,
772
- OPPORTUNITY_SIGNALS, SIGNAL_PROFILES,
773
- _extractRegex, _extractKeywordScore, _extractLLM, _mergeSignals,
774
- _curlHubIpFamilyArgs, _curlHubIpFamilyAttempts, _curlAuthConfig,
775
- _isEmptyCycle,
776
- };