@evomap/evolver 1.87.1 → 1.87.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +9 -8
- package/README.zh-CN.md +9 -8
- package/index.js +30 -11
- package/package.json +1 -1
- package/scripts/build_binaries.js +31 -7
- package/src/atp/atpExecute.js +35 -8
- package/src/atp/autoBuyer.js +155 -21
- package/src/atp/autoDeliver.js +16 -0
- package/src/atp/cli.js +98 -0
- package/src/atp/cliAutobuyPrompt.js +57 -64
- package/src/atp/hubClient.js +42 -4
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/forceUpdate.js +2 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetStore.js +52 -5
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +6 -2
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/sanitize.js +57 -3
- package/src/gep/selector.js +1 -1
- package/src/gep/selfPR.js +34 -1
- package/src/gep/skill2gep.js +108 -29
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/index.js +29 -9
- package/src/proxy/lifecycle/manager.js +97 -37
- package/src/proxy/router/messages_route.js +105 -5
- package/src/proxy/sync/engine.js +68 -31
package/README.ja-JP.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver はこの結論を実装に落とし込んだオープンソースエンジンです。GEP プロトコルの下で、エージェントの経験を場当たり的なプロンプトやスキルドキュメントではなく、Gene と Capsule として符号化します。*なぜ* Evolver が長いスキルドキュメントではなく Gene にこだわるのか疑問に思ったことがあるなら、読むべきはこの論文です。
|
|
34
34
|
>
|
|
35
|
-
> 応用事例を見たい方へ:[OpenClaw x EvoMap:CritPt 評価レポート](https://evomap.ai/blog/openclaw-critpt-report) では、OpenClaw エージェントが CritPt Physics Solver 上の 5 バージョン(Beta → v2.2)にわたって、同じ Gene ベース進化ループによってスコアを
|
|
35
|
+
> 応用事例を見たい方へ:[OpenClaw x EvoMap:CritPt 評価レポート](https://evomap.ai/blog/openclaw-critpt-report) では、OpenClaw エージェントが CritPt Physics Solver 上の 5 バージョン(Beta → v2.2)にわたって、同じ Gene ベース進化ループによってスコアを 9.1% から 18.57% まで押し上げる全過程を、トークンコストの軌跡、遺伝子活性化マップ、そして推論が再利用可能な Gene に圧縮されるときに現れる「トークンが上昇してから下降する」シグネチャとともに詳述しています。
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
package/README.ko-KR.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver는 이 결과를 실제로 구현하는 오픈소스 엔진입니다. GEP 프로토콜 아래 에이전트의 경험을 임시 프롬프트나 스킬 문서가 아니라 Gene과 Capsule로 인코딩합니다. *왜* Evolver가 더 긴 스킬 문서 대신 Gene을 고집하는지 궁금했다면, 바로 이 논문을 읽어야 합니다.
|
|
34
34
|
>
|
|
35
|
-
> 적용 사례가 궁금하신가요? [OpenClaw x EvoMap: CritPt 평가 보고서](https://evomap.ai/blog/openclaw-critpt-report)는 동일한 Gene 기반 진화 루프가 OpenClaw 에이전트를 CritPt Physics Solver의 5개 버전(Beta → v2.2)에 걸쳐
|
|
35
|
+
> 적용 사례가 궁금하신가요? [OpenClaw x EvoMap: CritPt 평가 보고서](https://evomap.ai/blog/openclaw-critpt-report)는 동일한 Gene 기반 진화 루프가 OpenClaw 에이전트를 CritPt Physics Solver의 5개 버전(Beta → v2.2)에 걸쳐 9.1%에서 18.57%까지 끌어올리는 과정을, 전체 토큰 비용 궤적, 유전자 활성화 매핑, 그리고 추론이 재사용 가능한 Gene으로 압축될 때 나타나는 "토큰이 먼저 상승한 뒤 하강하는" 시그니처와 함께 단계별로 보여줍니다.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver is the open-source engine that puts this result into practice: it encodes agent experience as Genes and Capsules under the GEP protocol, not as ad hoc prompts or skill docs. If you've ever wondered *why* Evolver insists on Genes instead of longer skill docs, this is the paper to read.
|
|
34
34
|
>
|
|
35
|
-
> Want the applied version? [OpenClaw x EvoMap: CritPt Evaluation Report](https://evomap.ai/blog/openclaw-critpt-report) walks through how the same Gene-based evolution loop drives an OpenClaw agent from
|
|
35
|
+
> Want the applied version? [OpenClaw x EvoMap: CritPt Evaluation Report](https://evomap.ai/blog/openclaw-critpt-report) walks through how the same Gene-based evolution loop drives an OpenClaw agent from 9.1% to 18.57% on CritPt Physics Solver across five versions (Beta -> v2.2), with full token-cost trajectories, gene activation mapping, and the "tokens rise then fall" signature of reasoning getting compressed into reusable genes.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -184,7 +184,7 @@ Every `evolver <flag>` invocation in the rest of this README maps 1:1 to `node i
|
|
|
184
184
|
**Evolver is a prompt generator, not a code patcher.** Each evolution cycle:
|
|
185
185
|
|
|
186
186
|
1. Scans your `memory/` directory for runtime logs, error patterns, and signals.
|
|
187
|
-
2. Selects the best-matching [Gene or Capsule](https://evomap.ai/wiki) from
|
|
187
|
+
2. Selects the best-matching [Gene or Capsule](https://evomap.ai/wiki) from the local GEP asset store.
|
|
188
188
|
3. Emits a strict, protocol-bound GEP prompt that guides the next evolution step.
|
|
189
189
|
4. Records an auditable [EvolutionEvent](https://evomap.ai/wiki) for traceability.
|
|
190
190
|
|
|
@@ -377,16 +377,17 @@ The [evomap.ai](https://evomap.ai) dashboard has a "Worker" toggle on the node d
|
|
|
377
377
|
|
|
378
378
|
This repo includes a protocol-constrained prompt mode based on [GEP (Genome Evolution Protocol)](https://evomap.ai/wiki).
|
|
379
379
|
|
|
380
|
-
- **Structured assets** live in
|
|
381
|
-
-
|
|
382
|
-
-
|
|
383
|
-
-
|
|
380
|
+
- **Structured runtime assets** live in `<workspace>/.evolver/gep/` by default:
|
|
381
|
+
- `<workspace>/.evolver/gep/genes.json`
|
|
382
|
+
- `<workspace>/.evolver/gep/capsules.json`
|
|
383
|
+
- `<workspace>/.evolver/gep/events.jsonl`
|
|
384
|
+
- Set `GEP_ASSETS_DIR` to place the runtime asset store elsewhere.
|
|
384
385
|
- **Selector** logic uses extracted signals to prefer existing Genes/Capsules and emits a JSON selector decision in the prompt.
|
|
385
386
|
- **Constraints**: Only the DNA emoji is allowed in documentation; all other emoji are disallowed.
|
|
386
387
|
|
|
387
388
|
### Your local asset store is never overwritten by upgrades
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
`<workspace>/.evolver/gep/genes.json`, `<workspace>/.evolver/gep/capsules.json`, and `<workspace>/.evolver/gep/events.jsonl` are owned by your runtime and ignored by git. `assets/gep/` is reserved for bundled starter assets. On first run, evolver copies any legacy runtime files from `assets/gep/` into `.evolver/gep/` without deleting the originals, then seeds `genes.json` from the bundled starter genes only when no local `genes.json` exists.
|
|
390
391
|
|
|
391
392
|
If you ran an older evolver version that wiped your local assets, pull back everything you Promoted or published to the Hub with a single command:
|
|
392
393
|
|
|
@@ -396,7 +397,7 @@ A2A_HUB_URL=https://evomap.ai evolver sync --scope=all --export=backup.gepx
|
|
|
396
397
|
|
|
397
398
|
This hits `/a2a/assets/purchased` (Promoted-to-you plus self-purchased) and `/a2a/assets/published-by-me` (your own drafts and published assets), re-materializes the full payloads into `genes.json` / `capsules.json`, and packs a portable `.gepx` bundle. Previously-purchased payloads re-fetch at zero cost.
|
|
398
399
|
|
|
399
|
-
Purely local assets that were never uploaded to the Hub have no remote copy -- recover them from
|
|
400
|
+
Purely local assets that were never uploaded to the Hub have no remote copy -- recover them from `.evolver/gep/`, from an older `assets/gep/` checkout, or from disk snapshots.
|
|
400
401
|
|
|
401
402
|
## Configuration & Decoupling
|
|
402
403
|
|
package/README.zh-CN.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
>
|
|
33
33
|
> Evolver 正是把这一结论落地的开源引擎:它基于 GEP 协议,把 Agent 的经验沉淀为 Gene 与 Capsule,而不是散落的 prompt 或技能文档。如果你想知道 *为什么* Evolver 坚持使用 Gene 而不是更长的 skill 文档,这就是那篇该读的论文。
|
|
34
34
|
>
|
|
35
|
-
> 想看应用落地的样本?[OpenClaw x EvoMap:CritPt 评测报告](https://evomap.ai/blog/openclaw-critpt-report) 以 OpenClaw Agent 在 CritPt Physics Solver 上的五个版本演进(Beta → v2.2)为例,完整拆解了同一套 Gene 进化闭环如何把得分从
|
|
35
|
+
> 想看应用落地的样本?[OpenClaw x EvoMap:CritPt 评测报告](https://evomap.ai/blog/openclaw-critpt-report) 以 OpenClaw Agent 在 CritPt Physics Solver 上的五个版本演进(Beta → v2.2)为例,完整拆解了同一套 Gene 进化闭环如何把得分从 9.1% 推到 18.57%,并给出 token 成本轨迹、基因激活映射,以及推理被压缩成可复用基因后所呈现的「token 先升后降」特征。
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -159,7 +159,7 @@ evolver --loop
|
|
|
159
159
|
**Evolver 是一个提示词生成器,不是代码修改器。** 每个进化周期:
|
|
160
160
|
|
|
161
161
|
1. 扫描 `memory/` 目录中的运行日志、错误模式和信号。
|
|
162
|
-
2.
|
|
162
|
+
2. 从本地 GEP 资产库中选择最匹配的 [Gene 或 Capsule](https://evomap.ai/wiki)。
|
|
163
163
|
3. 输出一份严格的、受协议约束的 GEP 提示词来引导下一步进化。
|
|
164
164
|
4. 记录可审计的 [EvolutionEvent](https://evomap.ai/wiki) 以便追溯。
|
|
165
165
|
|
|
@@ -351,16 +351,17 @@ WORKER_ENABLED=1 WORKER_DOMAINS=repair,harden WORKER_MAX_LOAD=3 evolver --loop
|
|
|
351
351
|
|
|
352
352
|
本仓库内置基于 [GEP(基因组进化协议)](https://evomap.ai/wiki)的协议受限提示词模式。
|
|
353
353
|
|
|
354
|
-
-
|
|
355
|
-
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
354
|
+
- **结构化运行时资产目录**:默认位于 `<workspace>/.evolver/gep/`
|
|
355
|
+
- `<workspace>/.evolver/gep/genes.json`
|
|
356
|
+
- `<workspace>/.evolver/gep/capsules.json`
|
|
357
|
+
- `<workspace>/.evolver/gep/events.jsonl`
|
|
358
|
+
- 可通过 `GEP_ASSETS_DIR` 把运行时资产库放到其他位置。
|
|
358
359
|
- **Selector 选择器**:根据日志提取 signals,优先复用已有 Gene/Capsule,并在提示词中输出可审计的 Selector 决策 JSON。
|
|
359
360
|
- **约束**:除 🧬 外,禁止使用其他 emoji。
|
|
360
361
|
|
|
361
362
|
### 升级不再覆盖你的本地资产库
|
|
362
363
|
|
|
363
|
-
|
|
364
|
+
`<workspace>/.evolver/gep/genes.json`、`<workspace>/.evolver/gep/capsules.json`、`<workspace>/.evolver/gep/events.jsonl` 属于你本地运行时,并被 git 忽略。`assets/gep/` 保留给随包发布的 starter 资产。首次运行时,evolver 会把旧版遗留在 `assets/gep/` 的运行时文件复制到 `.evolver/gep/`,不会删除原文件;只有在本地 `genes.json` 不存在时,才会从随包 starter Gene 初始化。
|
|
364
365
|
|
|
365
366
|
如果你之前用老版本被覆盖过,现在可以一键把所有被 Promoted 给你、以及你自己上传到 Hub 的资产拉回来:
|
|
366
367
|
|
|
@@ -370,7 +371,7 @@ A2A_HUB_URL=https://evomap.ai evolver sync --scope=all --export=backup.gepx
|
|
|
370
371
|
|
|
371
372
|
它会去 `/a2a/assets/purchased`(被 Promoted 给你 + 自购)和 `/a2a/assets/published-by-me`(你自己发布的,含 draft)拉回完整 payload,直接回写 `genes.json` / `capsules.json`,并顺便打成 `.gepx` 整包备份。已购买过的 payload 这次重新拉取不收费。
|
|
372
373
|
|
|
373
|
-
纯本地、从未上传过的资产 Hub 没有副本,只能从
|
|
374
|
+
纯本地、从未上传过的资产 Hub 没有副本,只能从 `.evolver/gep/`、旧版 `assets/gep/` checkout 或磁盘快照找回。
|
|
374
375
|
|
|
375
376
|
## 配置与解耦
|
|
376
377
|
|
package/index.js
CHANGED
|
@@ -491,10 +491,11 @@ async function main() {
|
|
|
491
491
|
console.warn('[ATP] Auto-init failed: ' + (atpInitErr && atpInitErr.message || atpInitErr));
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
// ATP: capability-gap auto-buyer.
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
494
|
+
// ATP: capability-gap auto-buyer. OPT-IN as of consent-required
|
|
495
|
+
// change — new installs do not auto-spend until the user explicitly
|
|
496
|
+
// runs `evolver atp enable` or answers `y` at the first-run prompt.
|
|
497
|
+
// Also starts the merchant-side auto-deliver daemon so claimed ATP
|
|
498
|
+
// tasks actually call submitDelivery and settle instead of expiring.
|
|
498
499
|
try {
|
|
499
500
|
try {
|
|
500
501
|
const { runPrompt } = require('./src/atp/cliAutobuyPrompt');
|
|
@@ -502,16 +503,31 @@ async function main() {
|
|
|
502
503
|
} catch (promptErr) {
|
|
503
504
|
console.warn('[ATP-AutoBuyer] first-run prompt failed: ' + (promptErr && promptErr.message || promptErr));
|
|
504
505
|
}
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
if (
|
|
506
|
+
const { autoBuyer } = require('./src/atp');
|
|
507
|
+
const consent = autoBuyer.getConsent();
|
|
508
|
+
if (consent.enabled) {
|
|
508
509
|
const hubUrl = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || '';
|
|
509
510
|
if (hubUrl) {
|
|
510
|
-
const { autoBuyer } = require('./src/atp');
|
|
511
511
|
autoBuyer.start({
|
|
512
512
|
dailyCap: Number(process.env.ATP_AUTOBUY_DAILY_CAP_CREDITS) || undefined,
|
|
513
513
|
perOrderCap: Number(process.env.ATP_AUTOBUY_PER_ORDER_CAP_CREDITS) || undefined,
|
|
514
514
|
});
|
|
515
|
+
if (consent.source === 'default') {
|
|
516
|
+
// First-run on a non-TTY (daemon, hook, CI) where the prompt
|
|
517
|
+
// could not fire AND no env override + no ack file. autoBuyer
|
|
518
|
+
// is starting with the default-on policy — surface a single
|
|
519
|
+
// WARN per process so users see what is happening and how to
|
|
520
|
+
// opt out, instead of discovering it via a credit balance dip.
|
|
521
|
+
let safeHubUrl;
|
|
522
|
+
try { safeHubUrl = new URL(hubUrl).origin; }
|
|
523
|
+
catch { safeHubUrl = '(configured)'; }
|
|
524
|
+
console.warn('[ATP-AutoBuyer] ATP auto-spend is ON (default for new installs).');
|
|
525
|
+
console.warn(' Hub: ' + safeHubUrl + ' Caps: ' +
|
|
526
|
+
(process.env.ATP_AUTOBUY_DAILY_CAP_CREDITS || '50') + ' credits/day, ' +
|
|
527
|
+
(process.env.ATP_AUTOBUY_PER_ORDER_CAP_CREDITS || '10') + '/order' +
|
|
528
|
+
' (cold-start half-cap for the first 5 min).');
|
|
529
|
+
console.warn(' To opt out: evolver atp disable (or EVOLVER_ATP_AUTOBUY=off)');
|
|
530
|
+
}
|
|
515
531
|
} else {
|
|
516
532
|
console.warn('[ATP-AutoBuyer] autobuy enabled but no hub URL configured, skipping.');
|
|
517
533
|
}
|
|
@@ -1815,7 +1831,7 @@ async function main() {
|
|
|
1815
1831
|
process.exit(1);
|
|
1816
1832
|
}
|
|
1817
1833
|
|
|
1818
|
-
} else if (command === 'buy' || command === 'orders' || command === 'verify') {
|
|
1834
|
+
} else if (command === 'buy' || command === 'orders' || command === 'verify' || command === 'atp') {
|
|
1819
1835
|
try {
|
|
1820
1836
|
const atpCli = require('./src/atp/cli');
|
|
1821
1837
|
const subArgs = args.slice(1); // drop the command token (e.g. "buy") itself
|
|
@@ -1827,9 +1843,12 @@ async function main() {
|
|
|
1827
1843
|
} else if (command === 'orders') {
|
|
1828
1844
|
parsed = atpCli.parseOrdersArgs(subArgs);
|
|
1829
1845
|
runner = atpCli.runOrders;
|
|
1830
|
-
} else {
|
|
1846
|
+
} else if (command === 'verify') {
|
|
1831
1847
|
parsed = atpCli.parseVerifyArgs(subArgs);
|
|
1832
1848
|
runner = atpCli.runVerify;
|
|
1849
|
+
} else {
|
|
1850
|
+
parsed = atpCli.parseAtpArgs(subArgs);
|
|
1851
|
+
runner = atpCli.runAtp;
|
|
1833
1852
|
}
|
|
1834
1853
|
if (!parsed.ok) {
|
|
1835
1854
|
console.error('[ATP] ' + parsed.error);
|
|
@@ -1844,7 +1863,7 @@ async function main() {
|
|
|
1844
1863
|
}
|
|
1845
1864
|
|
|
1846
1865
|
} else {
|
|
1847
|
-
console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|buy|orders|verify|atp-complete] [--loop]
|
|
1866
|
+
console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|buy|orders|verify|atp|atp-complete] [--loop]
|
|
1848
1867
|
- fetch flags:
|
|
1849
1868
|
- --skill=<id> | -s <id> (skill ID to download)
|
|
1850
1869
|
- --out=<dir> (output directory, default: ./skills/<skill_id>)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.87.
|
|
3
|
+
"version": "1.87.3",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -192,7 +192,14 @@ step('Stage 1 — bun bundle (resolve require tree to one file)');
|
|
|
192
192
|
ensureDir(STAGE_DIR);
|
|
193
193
|
const BUNDLED_JS = path.join(STAGE_DIR, 'bundled.js');
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
// `--external '@napi-rs/keyring'`: keyring is an optional dep loaded via
|
|
196
|
+
// dynamic require() in workspace-id; bun otherwise tries to bundle the
|
|
197
|
+
// platform-specific `.node` file as a second output asset, which makes
|
|
198
|
+
// `bun build … --outfile=…` fail with "cannot write multiple output files
|
|
199
|
+
// without an output directory". Treating it as external preserves the
|
|
200
|
+
// existing optional-fallback behaviour (require throws → FS path used) in
|
|
201
|
+
// the standalone binaries.
|
|
202
|
+
run('bun', ['build', ENTRY, '--target=node', `--outfile=${BUNDLED_JS}`, '--external', '@napi-rs/keyring']);
|
|
196
203
|
|
|
197
204
|
const bundleSize = OPTS.dryRun ? 0 : fs.statSync(BUNDLED_JS).size;
|
|
198
205
|
console.log(` bundled.js: ${(bundleSize / 1024 / 1024).toFixed(2)} MB`);
|
|
@@ -266,13 +273,30 @@ if (!OPTS.skipObfuscate) {
|
|
|
266
273
|
const obfSecs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
267
274
|
|
|
268
275
|
const check = spawnSync('node', ['--check', OBF_JS], { encoding: 'utf8' });
|
|
269
|
-
if (check.status
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
276
|
+
if (check.status !== 0) {
|
|
277
|
+
lastValidationErr = (check.stderr || check.stdout || '').split('\n').slice(0, 3).join(' | ');
|
|
278
|
+
console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output failed node --check (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
|
|
279
|
+
continue;
|
|
273
280
|
}
|
|
274
|
-
|
|
275
|
-
|
|
281
|
+
// Second gate: bun's compile-time parser is stricter than node's.
|
|
282
|
+
// 1.87.x (post `@napi-rs/keyring` dep) revealed that ~5% of obfuscator
|
|
283
|
+
// outputs that pass `node --check` still trip bun with errors like
|
|
284
|
+
// `Expected "in" but found ","`. Probe with a cheap bundle-only call
|
|
285
|
+
// (no --compile, native target) to fail fast and feed back into the
|
|
286
|
+
// seed-perturbation loop instead of dying in stage 3.
|
|
287
|
+
const bunProbe = spawnSync('bun', [
|
|
288
|
+
'build', OBF_JS,
|
|
289
|
+
'--target=bun',
|
|
290
|
+
`--outfile=${path.join(STAGE_DIR, 'bundled.obf.bunprobe.js')}`,
|
|
291
|
+
], { encoding: 'utf8' });
|
|
292
|
+
if (bunProbe.status !== 0) {
|
|
293
|
+
lastValidationErr = (bunProbe.stderr || bunProbe.stdout || '').split('\n').slice(0, 3).join(' | ');
|
|
294
|
+
console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output rejected by bun parser (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
console.log(` obfuscation: ${obfSecs}s, output ${(obfSize / 1024 / 1024).toFixed(2)} MB (attempt ${attempt}/${MAX_OBF_ATTEMPTS}, seed=0x${usedSeed.toString(16)})`);
|
|
298
|
+
succeeded = true;
|
|
299
|
+
break;
|
|
276
300
|
}
|
|
277
301
|
if (!succeeded) {
|
|
278
302
|
console.error(` ERROR: javascript-obfuscator produced syntactically invalid output in ${MAX_OBF_ATTEMPTS} attempts.`);
|
package/src/atp/atpExecute.js
CHANGED
|
@@ -27,6 +27,7 @@ const https = require('https');
|
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
|
|
29
29
|
const { computeAssetId } = require('../gep/contentHash');
|
|
30
|
+
const { enforceHubScheme, strictHttpsAgent } = require('../gep/hubFetch');
|
|
30
31
|
const {
|
|
31
32
|
getNodeId,
|
|
32
33
|
getHubUrl,
|
|
@@ -114,6 +115,19 @@ function _publishUrl() {
|
|
|
114
115
|
|
|
115
116
|
function _postJson(urlStr, body, timeoutMs) {
|
|
116
117
|
return new Promise(function (resolve) {
|
|
118
|
+
// Same TLS posture as hubFetch: refuse plain http:// unless
|
|
119
|
+
// EVOMAP_HUB_ALLOW_INSECURE=1. Before this guard the function
|
|
120
|
+
// silently fell back to `lib = http` for any non-https URL, so an
|
|
121
|
+
// operator override `A2A_HUB_URL=http://...` would send /a2a/publish
|
|
122
|
+
// and /a2a/task/complete in cleartext while hubFetch-routed calls
|
|
123
|
+
// (e.g. /a2a/verify-solidify) refused the same URL — inconsistent
|
|
124
|
+
// TLS enforcement across modules.
|
|
125
|
+
try {
|
|
126
|
+
enforceHubScheme(urlStr);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
resolve({ ok: false, error: 'tls_refused: ' + (e && e.message) });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
117
131
|
let parsed;
|
|
118
132
|
try {
|
|
119
133
|
parsed = new URL(urlStr);
|
|
@@ -128,15 +142,28 @@ function _postJson(urlStr, body, timeoutMs) {
|
|
|
128
142
|
{ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
129
143
|
buildHubHeaders() || {},
|
|
130
144
|
);
|
|
145
|
+
// Pin TLS cert verification for https calls so a globally-disabled
|
|
146
|
+
// NODE_TLS_REJECT_UNAUTHORIZED=0 cannot weaken the Hub channel
|
|
147
|
+
// (Cursor Security Reviewer #160 Medium). hubFetch enforces the
|
|
148
|
+
// same via its undici dispatcher; this is the Node-native-https
|
|
149
|
+
// equivalent.
|
|
150
|
+
//
|
|
151
|
+
// Skipped under EVOMAP_HUB_ALLOW_INSECURE=1 so local-dev / self-
|
|
152
|
+
// signed mock hubs that legitimately rely on
|
|
153
|
+
// NODE_TLS_REJECT_UNAUTHORIZED=0 still work.
|
|
154
|
+
const requestOpts = {
|
|
155
|
+
hostname: parsed.hostname,
|
|
156
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
157
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: headers,
|
|
160
|
+
timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
|
|
161
|
+
};
|
|
162
|
+
if (isHttps && process.env.EVOMAP_HUB_ALLOW_INSECURE !== '1') {
|
|
163
|
+
requestOpts.agent = strictHttpsAgent;
|
|
164
|
+
}
|
|
131
165
|
const req = lib.request(
|
|
132
|
-
|
|
133
|
-
hostname: parsed.hostname,
|
|
134
|
-
port: parsed.port || (isHttps ? 443 : 80),
|
|
135
|
-
path: parsed.pathname + (parsed.search || ''),
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: headers,
|
|
138
|
-
timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
|
|
139
|
-
},
|
|
166
|
+
requestOpts,
|
|
140
167
|
function (res) {
|
|
141
168
|
const chunks = [];
|
|
142
169
|
res.on('data', function (c) { chunks.push(c); });
|
package/src/atp/autoBuyer.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
// ATP Auto-Buyer (opt-
|
|
1
|
+
// ATP Auto-Buyer (opt-in: requires explicit consent before auto-spending)
|
|
2
2
|
// Converts capability gaps into ATP orders with strict budget caps and
|
|
3
|
-
// 24h question-level deduplication.
|
|
4
|
-
// EVOLVER_ATP_AUTOBUY=off. Budget caps:
|
|
3
|
+
// 24h question-level deduplication. Budget caps:
|
|
5
4
|
// ATP_AUTOBUY_DAILY_CAP_CREDITS (default 50)
|
|
6
5
|
// ATP_AUTOBUY_PER_ORDER_CAP_CREDITS (default 10)
|
|
7
6
|
// Cold-start safety: the first 5 minutes after process start use a half-cap
|
|
8
7
|
// to protect against misconfiguration loops on restart storms.
|
|
9
8
|
//
|
|
9
|
+
// Consent resolution (in order):
|
|
10
|
+
// 1. EVOLVER_ATP_AUTOBUY=on|off env — explicit operator override wins.
|
|
11
|
+
// 2. ack file at <memory>/atp-autobuy-ack.json with `{enabled: bool}` —
|
|
12
|
+
// written by first-run prompt (cliAutobuyPrompt) or `evolver atp
|
|
13
|
+
// enable|disable`.
|
|
14
|
+
// 3. No signal → OFF. New installs never auto-spend before the user has
|
|
15
|
+
// explicitly opted in (consumer protection: ATP spends real credits).
|
|
16
|
+
//
|
|
10
17
|
// Integration contract:
|
|
11
18
|
// 1) Call start({ dailyCap, perOrderCap }) once at Evolver boot. The
|
|
12
19
|
// evolve loop does this at the top of every cycle; start() is
|
|
@@ -29,8 +36,15 @@ const DEFAULT_DAILY_CAP = 50;
|
|
|
29
36
|
const DEFAULT_PER_ORDER_CAP = 10;
|
|
30
37
|
const DEFAULT_ORDER_TIMEOUT_MS = 3000;
|
|
31
38
|
const COLD_START_WINDOW_MS = 5 * 60 * 1000;
|
|
39
|
+
// Successful orders dedup for 24h so the same capability gap is only paid for
|
|
40
|
+
// once per day. Failed orders dedup for 5 minutes only — long enough to
|
|
41
|
+
// absorb tight retry loops (the original goal of "don't hammer the hub")
|
|
42
|
+
// without making the user wait 24h to retry a question after a transient
|
|
43
|
+
// 503/network blip.
|
|
32
44
|
const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
45
|
+
const DEDUP_FAILURE_TTL_MS = 5 * 60 * 1000;
|
|
33
46
|
const LEDGER_FILENAME = 'atp-autobuyer-ledger.json';
|
|
47
|
+
const ACK_FILENAME = 'atp-autobuy-ack.json';
|
|
34
48
|
|
|
35
49
|
let _started = false;
|
|
36
50
|
let _startedAt = 0;
|
|
@@ -44,18 +58,57 @@ function _ledgerPath() {
|
|
|
44
58
|
return path.join(getMemoryDir(), LEDGER_FILENAME);
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
function _ackPath() {
|
|
62
|
+
return path.join(getMemoryDir(), ACK_FILENAME);
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
function _todayKey(now) {
|
|
48
66
|
const d = new Date(typeof now === 'number' ? now : Date.now());
|
|
49
67
|
return d.toISOString().slice(0, 10); // UTC YYYY-MM-DD
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
function _readAck() {
|
|
71
|
+
try {
|
|
72
|
+
const p = _ackPath();
|
|
73
|
+
if (!fs.existsSync(p)) return null;
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
75
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
76
|
+
if (typeof parsed.enabled !== 'boolean') return null;
|
|
77
|
+
return parsed;
|
|
78
|
+
} catch (_) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Resolve consent. Returns:
|
|
84
|
+
// { enabled: true, source: 'env'|'ack'|'default' }
|
|
85
|
+
// { enabled: false, source: 'env'|'ack' }
|
|
86
|
+
// The `default` case is the new-install path (no env override, no ack file):
|
|
87
|
+
// auto-spend defaults ON, gated by the daily/per-order caps and the cold-start
|
|
88
|
+
// half-cap window. The first-run prompt and `evolver atp disable` remain the
|
|
89
|
+
// opt-out paths for users who do not want auto-spend; once an explicit ack is
|
|
90
|
+
// recorded the source flips to 'ack' and the user's choice (either way) wins
|
|
91
|
+
// over this default.
|
|
92
|
+
function getConsent() {
|
|
93
|
+
const envRaw = process.env.EVOLVER_ATP_AUTOBUY;
|
|
94
|
+
if (typeof envRaw === 'string') {
|
|
95
|
+
// Trim BEFORE the length check so whitespace-only values
|
|
96
|
+
// (e.g. EVOLVER_ATP_AUTOBUY=" ") count as unset, matching the
|
|
97
|
+
// classify() helper in cliAutobuyPrompt.js. Without this alignment a
|
|
98
|
+
// whitespace value would skip the prompt in classify (treats as unset
|
|
99
|
+
// → 'eligible') but still enter this env branch, trim to "", fail to
|
|
100
|
+
// match 'off'/'0'/'false', and silently return enabled=true.
|
|
101
|
+
const s = envRaw.trim().toLowerCase();
|
|
102
|
+
if (s.length > 0) {
|
|
103
|
+
const enabled = s !== 'off' && s !== '0' && s !== 'false';
|
|
104
|
+
return { enabled, source: 'env' };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const ack = _readAck();
|
|
108
|
+
if (ack) {
|
|
109
|
+
return { enabled: ack.enabled === true, source: 'ack' };
|
|
110
|
+
}
|
|
111
|
+
return { enabled: true, source: 'default' };
|
|
59
112
|
}
|
|
60
113
|
|
|
61
114
|
function _emptyLedger() {
|
|
@@ -98,13 +151,22 @@ function _rotateIfNewDay(ledger, now) {
|
|
|
98
151
|
}
|
|
99
152
|
|
|
100
153
|
function _pruneDedup(ledger, now) {
|
|
101
|
-
const
|
|
154
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
102
155
|
const out = {};
|
|
103
156
|
const src = ledger.dedup || {};
|
|
104
157
|
const keys = Object.keys(src);
|
|
105
158
|
for (let i = 0; i < keys.length; i++) {
|
|
106
159
|
const k = keys[i];
|
|
107
|
-
|
|
160
|
+
const entry = src[k];
|
|
161
|
+
// Legacy ledgers written by older versions stored plain timestamps; treat
|
|
162
|
+
// them as successful orders (the original behaviour) so an upgrade does
|
|
163
|
+
// not suddenly forget recent dedups.
|
|
164
|
+
if (typeof entry === 'number') {
|
|
165
|
+
if (entry >= nowMs - DEDUP_TTL_MS) out[k] = entry;
|
|
166
|
+
} else if (entry && typeof entry.ts === 'number') {
|
|
167
|
+
const ttl = entry.failed ? DEDUP_FAILURE_TTL_MS : DEDUP_TTL_MS;
|
|
168
|
+
if (entry.ts >= nowMs - ttl) out[k] = entry;
|
|
169
|
+
}
|
|
108
170
|
}
|
|
109
171
|
ledger.dedup = out;
|
|
110
172
|
return ledger;
|
|
@@ -125,7 +187,8 @@ function _effectiveCap(value, now) {
|
|
|
125
187
|
|
|
126
188
|
function start(opts) {
|
|
127
189
|
if (_started) return;
|
|
128
|
-
|
|
190
|
+
const consent = getConsent();
|
|
191
|
+
if (!consent.enabled) return;
|
|
129
192
|
_started = true;
|
|
130
193
|
_startedAt = Date.now();
|
|
131
194
|
const dailyCap = Math.max(0, Math.floor(Number((opts && opts.dailyCap) || process.env.ATP_AUTOBUY_DAILY_CAP_CREDITS) || DEFAULT_DAILY_CAP));
|
|
@@ -157,11 +220,36 @@ function _withTimeout(promise, timeoutMs) {
|
|
|
157
220
|
]);
|
|
158
221
|
}
|
|
159
222
|
|
|
160
|
-
|
|
161
|
-
|
|
223
|
+
// Single-flight queue: serialize the read → cap-check → placeOrder → write
|
|
224
|
+
// pipeline so two concurrent considerOrder() calls cannot both pass the cap
|
|
225
|
+
// check on the same ledger snapshot and silently double-spend.
|
|
226
|
+
//
|
|
227
|
+
// Without this, two parallel calls (e.g. user runs Claude Code in two tabs
|
|
228
|
+
// through the same proxy, or two capability gaps fire in the same tick) both
|
|
229
|
+
// read spent=40, both compute remaining=10, both await placeOrder, both
|
|
230
|
+
// increment to spent=50, and write — silently exceeding the daily cap by one
|
|
231
|
+
// full order each. autoBuyer is single-process so an in-memory queue is
|
|
232
|
+
// sufficient; a file lock would only be needed if multiple OS processes
|
|
233
|
+
// shared the same ledger file (not the current deployment model).
|
|
234
|
+
let _orderQueue = Promise.resolve();
|
|
235
|
+
|
|
236
|
+
function considerOrder(opts) {
|
|
237
|
+
if (!_started) return Promise.resolve({ ok: false, skipped: true, reason: 'not_started' });
|
|
162
238
|
if (!opts || !Array.isArray(opts.capabilities) || opts.capabilities.length === 0) {
|
|
163
|
-
return { ok: false, skipped: true, reason: 'no_capabilities' };
|
|
239
|
+
return Promise.resolve({ ok: false, skipped: true, reason: 'no_capabilities' });
|
|
164
240
|
}
|
|
241
|
+
const next = _orderQueue.then(
|
|
242
|
+
() => _considerOrderSerialized(opts),
|
|
243
|
+
() => _considerOrderSerialized(opts), // never let a prior rejection break the chain
|
|
244
|
+
);
|
|
245
|
+
// Swallow rejection on the queue tail so a single thrown error here does
|
|
246
|
+
// not poison every subsequent call; the original `next` promise still
|
|
247
|
+
// surfaces the error to the caller.
|
|
248
|
+
_orderQueue = next.then(() => {}, () => {});
|
|
249
|
+
return next;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function _considerOrderSerialized(opts) {
|
|
165
253
|
const now = Date.now();
|
|
166
254
|
let ledger = _readLedger();
|
|
167
255
|
ledger = _rotateIfNewDay(ledger, now);
|
|
@@ -200,19 +288,49 @@ async function considerOrder(opts) {
|
|
|
200
288
|
|
|
201
289
|
if (result && result.ok) {
|
|
202
290
|
ledger.spent = (ledger.spent || 0) + budget;
|
|
203
|
-
ledger.dedup[hash] = now;
|
|
291
|
+
ledger.dedup[hash] = { ts: now, failed: false };
|
|
204
292
|
_writeLedger(ledger);
|
|
205
293
|
console.log('[ATP-AutoBuyer] Order placed: ' + (result.data && result.data.order_id) + ' budget=' + budget + ' remaining_today=' + Math.max(0, dailyCap - ledger.spent));
|
|
206
294
|
return { ok: true, data: result.data, spent: budget };
|
|
207
295
|
}
|
|
208
296
|
|
|
209
|
-
// On failure
|
|
210
|
-
// capability gap
|
|
211
|
-
|
|
297
|
+
// On failure record a SHORT-TTL dedup entry (5 min) so we don't hammer the
|
|
298
|
+
// hub for the same capability gap inside a tight retry loop, but the user
|
|
299
|
+
// can retry the same question once the transient error clears — far better
|
|
300
|
+
// than the previous 24h block for a single 503.
|
|
301
|
+
ledger.dedup[hash] = { ts: now, failed: true };
|
|
212
302
|
_writeLedger(ledger);
|
|
213
303
|
return { ok: false, error: (result && result.error) || 'unknown_error' };
|
|
214
304
|
}
|
|
215
305
|
|
|
306
|
+
// Write the consent ack file. Used by `evolver atp enable|disable` and the
|
|
307
|
+
// first-run prompt. `enabled=true` persists opt-in; `enabled=false` persists
|
|
308
|
+
// explicit opt-out so the prompt does not re-ask next session. Atomic write
|
|
309
|
+
// via .tmp + rename so a crash mid-write never produces a corrupt ack file.
|
|
310
|
+
function setConsent(enabled) {
|
|
311
|
+
const dir = getMemoryDir();
|
|
312
|
+
const body = {
|
|
313
|
+
enabled: !!enabled,
|
|
314
|
+
acknowledged_at: new Date().toISOString(),
|
|
315
|
+
version: 1,
|
|
316
|
+
};
|
|
317
|
+
const tmp = _ackPath() + '.tmp';
|
|
318
|
+
// Single try/catch over the whole pipeline. Previously the mkdirSync was
|
|
319
|
+
// wrapped in its own swallowing try/catch, so an EACCES on the parent dir
|
|
320
|
+
// would surface to the caller as a confusing ENOENT from writeFileSync.
|
|
321
|
+
// Surface the original error verbatim and best-effort clean up any
|
|
322
|
+
// partial .tmp file so a retry from a TTY prompt sees a clean slate.
|
|
323
|
+
try {
|
|
324
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
325
|
+
fs.writeFileSync(tmp, JSON.stringify(body, null, 2) + '\n', 'utf8');
|
|
326
|
+
fs.renameSync(tmp, _ackPath());
|
|
327
|
+
} catch (err) {
|
|
328
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
return body;
|
|
332
|
+
}
|
|
333
|
+
|
|
216
334
|
// Test-only reset, not exported by default.
|
|
217
335
|
function _resetForTests() {
|
|
218
336
|
_started = false;
|
|
@@ -222,15 +340,30 @@ function _resetForTests() {
|
|
|
222
340
|
perOrderCap: DEFAULT_PER_ORDER_CAP,
|
|
223
341
|
timeoutMs: DEFAULT_ORDER_TIMEOUT_MS,
|
|
224
342
|
};
|
|
343
|
+
_orderQueue = Promise.resolve();
|
|
225
344
|
}
|
|
226
345
|
|
|
227
346
|
module.exports = {
|
|
347
|
+
// Lifecycle.
|
|
228
348
|
start,
|
|
229
349
|
stop,
|
|
230
350
|
isStarted,
|
|
231
351
|
considerOrder,
|
|
352
|
+
// Consent surface — public API. Production callers (CLI runAtp,
|
|
353
|
+
// cliAutobuyPrompt, the daemon run loop) MUST use these, not the
|
|
354
|
+
// __internals duplicates below, so the "test-only" contract on
|
|
355
|
+
// __internals stays honest (Bugbot PR #141 R6).
|
|
356
|
+
getConsent,
|
|
357
|
+
setConsent,
|
|
358
|
+
getAckPath: _ackPath,
|
|
359
|
+
readAck: _readAck,
|
|
360
|
+
ACK_FILENAME,
|
|
232
361
|
// Exposed for tests and diagnostics only; callers should not depend on
|
|
233
|
-
// these internals in production code paths.
|
|
362
|
+
// these internals in production code paths. Ack-file helpers
|
|
363
|
+
// (getAckPath / readAck / ACK_FILENAME) are intentionally NOT mirrored
|
|
364
|
+
// here — production and tests both go through the public surface above,
|
|
365
|
+
// so a single source of truth survives schema changes (Bugbot PR #141 R6
|
|
366
|
+
// follow-up: keep "test-only" honest, no production caller may reach in).
|
|
234
367
|
__internals: {
|
|
235
368
|
readLedger: _readLedger,
|
|
236
369
|
writeLedger: _writeLedger,
|
|
@@ -242,6 +375,7 @@ module.exports = {
|
|
|
242
375
|
DEFAULT_PER_ORDER_CAP,
|
|
243
376
|
COLD_START_WINDOW_MS,
|
|
244
377
|
DEDUP_TTL_MS,
|
|
378
|
+
DEDUP_FAILURE_TTL_MS,
|
|
245
379
|
LEDGER_FILENAME,
|
|
246
380
|
},
|
|
247
381
|
},
|
package/src/atp/autoDeliver.js
CHANGED
|
@@ -155,6 +155,18 @@ function start(opts) {
|
|
|
155
155
|
_pollInterval = setInterval(function () {
|
|
156
156
|
_tick().catch(function () { /* swallowed in _tick */ });
|
|
157
157
|
}, _pollMs);
|
|
158
|
+
// .unref() so this background poller does NOT keep the Node event
|
|
159
|
+
// loop alive on its own. `evolver run` (single-shot) writes its
|
|
160
|
+
// artifacts and expects to exit — without unref, the setInterval
|
|
161
|
+
// handle pins the process and the run sits as a residual `node`
|
|
162
|
+
// process until manually killed (public issue #553). `evolver --loop`
|
|
163
|
+
// (daemon) keeps the foreground evolve loop alive on its own
|
|
164
|
+
// schedule, so an unref'd poller still polls — unref only changes
|
|
165
|
+
// whether THIS handle alone keeps the loop alive, not whether the
|
|
166
|
+
// handle fires.
|
|
167
|
+
if (_pollInterval && typeof _pollInterval.unref === 'function') {
|
|
168
|
+
_pollInterval.unref();
|
|
169
|
+
}
|
|
158
170
|
// Do not await -- fire the first tick asynchronously so start() returns
|
|
159
171
|
// immediately. This matches the autoBuyer start() semantics.
|
|
160
172
|
_tick().catch(function () { /* swallowed in _tick */ });
|
|
@@ -189,6 +201,10 @@ module.exports = {
|
|
|
189
201
|
writeLedger: _writeLedger,
|
|
190
202
|
buildProofPayload: _buildProofPayload,
|
|
191
203
|
resetForTests: _resetForTests,
|
|
204
|
+
// Test-only accessor for the active poll Timeout. Used to assert
|
|
205
|
+
// the timer was `.unref()`ed so it does not pin the Node event
|
|
206
|
+
// loop (regression guard for public issue #553).
|
|
207
|
+
getPollIntervalForTest: () => _pollInterval,
|
|
192
208
|
constants: {
|
|
193
209
|
DEFAULT_POLL_MS,
|
|
194
210
|
MIN_POLL_MS,
|