@evomap/evolver 1.90.0 → 2.0.0-beta.1

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 -3670
  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/src/adapters/claudeCode.js +0 -194
  38. package/src/adapters/codex.js +0 -216
  39. package/src/adapters/cursor.js +0 -91
  40. package/src/adapters/hookAdapter.js +0 -469
  41. package/src/adapters/kiro.js +0 -195
  42. package/src/adapters/opencode.js +0 -326
  43. package/src/adapters/scripts/_lockPaths.js +0 -74
  44. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  45. package/src/adapters/scripts/_runtimePaths.js +0 -440
  46. package/src/adapters/scripts/evolver-session-end.js +0 -321
  47. package/src/adapters/scripts/evolver-session-start.js +0 -587
  48. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  49. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  50. package/src/atp/atpExecute.js +0 -283
  51. package/src/atp/atpTaskPickup.js +0 -233
  52. package/src/atp/autoBuyer.js +0 -382
  53. package/src/atp/autoDeliver.js +0 -215
  54. package/src/atp/cli.js +0 -354
  55. package/src/atp/cliAutobuyPrompt.js +0 -154
  56. package/src/atp/consumerAgent.js +0 -157
  57. package/src/atp/defaultHandler.js +0 -69
  58. package/src/atp/heartbeatSignalsHandler.js +0 -254
  59. package/src/atp/hubClient.js +0 -317
  60. package/src/atp/index.js +0 -38
  61. package/src/atp/merchantAgent.js +0 -118
  62. package/src/atp/protocol.js +0 -41
  63. package/src/atp/questionComposer.js +0 -133
  64. package/src/atp/serviceHelper.js +0 -92
  65. package/src/canary.js +0 -13
  66. package/src/config.js +0 -351
  67. package/src/evolve/guards.js +0 -1
  68. package/src/evolve/pipeline/collect.js +0 -1
  69. package/src/evolve/pipeline/dispatch.js +0 -1
  70. package/src/evolve/pipeline/enrich.js +0 -1
  71. package/src/evolve/pipeline/hub.js +0 -1
  72. package/src/evolve/pipeline/select.js +0 -1
  73. package/src/evolve/pipeline/signals.js +0 -1
  74. package/src/evolve/utils.js +0 -1
  75. package/src/evolve.js +0 -1
  76. package/src/experiment/agentRunner.js +0 -229
  77. package/src/experiment/cli.js +0 -159
  78. package/src/experiment/comparison.js +0 -233
  79. package/src/experiment/metrics.js +0 -75
  80. package/src/forceUpdate.js +0 -991
  81. package/src/gep/a2a.js +0 -173
  82. package/src/gep/a2aProtocol.js +0 -1
  83. package/src/gep/analyzer.js +0 -35
  84. package/src/gep/antiAbuseTelemetry.js +0 -1
  85. package/src/gep/assetCallLog.js +0 -197
  86. package/src/gep/assetStore.js +0 -723
  87. package/src/gep/assets.js +0 -36
  88. package/src/gep/autoDistillConv.js +0 -1
  89. package/src/gep/autoDistillLlm.js +0 -1
  90. package/src/gep/bridge.js +0 -138
  91. package/src/gep/candidateEval.js +0 -1
  92. package/src/gep/candidates.js +0 -1
  93. package/src/gep/claimNudge.js +0 -121
  94. package/src/gep/cliContracts.js +0 -1190
  95. package/src/gep/contentHash.js +0 -1
  96. package/src/gep/conversationDistiller.js +0 -1
  97. package/src/gep/conversationSniffer.js +0 -1
  98. package/src/gep/crypto.js +0 -1
  99. package/src/gep/curriculum.js +0 -1
  100. package/src/gep/deviceId.js +0 -1
  101. package/src/gep/directoryClient.js +0 -115
  102. package/src/gep/envFingerprint.js +0 -1
  103. package/src/gep/epigenetics.js +0 -1
  104. package/src/gep/execBridge.js +0 -1
  105. package/src/gep/executionTrace.js +0 -291
  106. package/src/gep/explore.js +0 -1
  107. package/src/gep/featureFlags.js +0 -121
  108. package/src/gep/gitOps.js +0 -265
  109. package/src/gep/hash.js +0 -1
  110. package/src/gep/hostErrorClassifier.js +0 -34
  111. package/src/gep/hubFetch.js +0 -1
  112. package/src/gep/hubReview.js +0 -1
  113. package/src/gep/hubSearch.js +0 -1
  114. package/src/gep/hubVerify.js +0 -1
  115. package/src/gep/idleScheduler.js +0 -400
  116. package/src/gep/issueReporter.js +0 -416
  117. package/src/gep/learningSignals.js +0 -1
  118. package/src/gep/llmReview.js +0 -92
  119. package/src/gep/localStateAwareness.js +0 -243
  120. package/src/gep/mailboxTransport.js +0 -119
  121. package/src/gep/memoryGraph.js +0 -1
  122. package/src/gep/memoryGraphAdapter.js +0 -1
  123. package/src/gep/mutation.js +0 -1
  124. package/src/gep/narrativeMemory.js +0 -1
  125. package/src/gep/oauthLogin.js +0 -181
  126. package/src/gep/openPRRegistry.js +0 -1
  127. package/src/gep/paths.js +0 -522
  128. package/src/gep/personality.js +0 -1
  129. package/src/gep/policyCheck.js +0 -1
  130. package/src/gep/portable.js +0 -103
  131. package/src/gep/privacyClient.js +0 -235
  132. package/src/gep/prompt.js +0 -1
  133. package/src/gep/questionGenerator.js +0 -518
  134. package/src/gep/recallInject.js +0 -1
  135. package/src/gep/recallVerifier.js +0 -1
  136. package/src/gep/reflection.js +0 -1
  137. package/src/gep/sanitize.js +0 -264
  138. package/src/gep/savingsCore.js +0 -1
  139. package/src/gep/schemas/capsule.js +0 -170
  140. package/src/gep/schemas/gene.js +0 -154
  141. package/src/gep/schemas/index.js +0 -8
  142. package/src/gep/schemas/protocol.js +0 -51
  143. package/src/gep/schemas/task.js +0 -74
  144. package/src/gep/selector.js +0 -1
  145. package/src/gep/selfPR.js +0 -469
  146. package/src/gep/signals.js +0 -776
  147. package/src/gep/skill2gep.js +0 -1056
  148. package/src/gep/skill2gepAudit.js +0 -303
  149. package/src/gep/skill2recipes.js +0 -511
  150. package/src/gep/skillDistiller.js +0 -1
  151. package/src/gep/skillPublisher.js +0 -358
  152. package/src/gep/solidify.js +0 -1
  153. package/src/gep/strategy.js +0 -1
  154. package/src/gep/taskReceiver.js +0 -575
  155. package/src/gep/tokenSavings.js +0 -1
  156. package/src/gep/trajectoryExport.js +0 -1
  157. package/src/gep/validationReport.js +0 -55
  158. package/src/gep/validator/index.js +0 -411
  159. package/src/gep/validator/reporter.js +0 -210
  160. package/src/gep/validator/sandboxExecutor.js +0 -480
  161. package/src/gep/validator/stakeBootstrap.js +0 -357
  162. package/src/gep/workspaceKeychain.js +0 -1
  163. package/src/ops/cleanup.js +0 -80
  164. package/src/ops/commentary.js +0 -60
  165. package/src/ops/health_check.js +0 -104
  166. package/src/ops/index.js +0 -11
  167. package/src/ops/innovation.js +0 -67
  168. package/src/ops/lifecycle.js +0 -798
  169. package/src/ops/self_repair.js +0 -76
  170. package/src/ops/skills_monitor.js +0 -147
  171. package/src/ops/trigger.js +0 -33
  172. package/src/proxy/clientSettings.js +0 -405
  173. package/src/proxy/envelope.js +0 -59
  174. package/src/proxy/extensions/dmHandler.js +0 -45
  175. package/src/proxy/extensions/sessionHandler.js +0 -141
  176. package/src/proxy/extensions/skillUpdater.js +0 -64
  177. package/src/proxy/extensions/traceControl.js +0 -1
  178. package/src/proxy/index.js +0 -1395
  179. package/src/proxy/inject.js +0 -1
  180. package/src/proxy/lifecycle/manager.js +0 -1568
  181. package/src/proxy/mailbox/state.js +0 -207
  182. package/src/proxy/mailbox/store.js +0 -602
  183. package/src/proxy/router/cache_passthrough.js +0 -26
  184. package/src/proxy/router/features.js +0 -84
  185. package/src/proxy/router/gemini_route.js +0 -154
  186. package/src/proxy/router/messages_route.js +0 -535
  187. package/src/proxy/router/model_router.js +0 -113
  188. package/src/proxy/router/models_route.js +0 -52
  189. package/src/proxy/router/ollama_route.js +0 -103
  190. package/src/proxy/router/responses_route.js +0 -170
  191. package/src/proxy/router/vertex_route.js +0 -110
  192. package/src/proxy/server/http.js +0 -363
  193. package/src/proxy/server/routes.js +0 -558
  194. package/src/proxy/server/settings.js +0 -115
  195. package/src/proxy/sync/engine.js +0 -179
  196. package/src/proxy/sync/inbound.js +0 -211
  197. package/src/proxy/sync/outbound.js +0 -361
  198. package/src/proxy/task/monitor.js +0 -131
  199. package/src/proxy/trace/extractor.js +0 -1
  200. package/src/proxy/trace/usage.js +0 -1
  201. package/src/solo/breaker.js +0 -25
  202. package/src/solo/gitGuard.js +0 -65
  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
- };