Skip to content

Keep Evolver out of the model context: report at turn end, distill at session end - #51

Merged
forrestlinfeng merged 6 commits into
mainfrom
feat/hook-only-reuse-report
Oct 23, 2026
Merged

forrestlinfeng merged 6 commits into
mainfrom
feat/hook-only-reuse-report

Conversation

@forrestlinfeng

@forrestlinfeng forrestlinfeng commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Nothing of Evolver but the recalled steps enters the model's context. Reporting and distillation move to session events; commands run in the plugin.

 apply(ctx)
-  ctx.tools.register(8 × evolver_*)          # sent with every model request (~5.3k chars)
-  skills.registerProvider(capability-evolver) # catalog every turn, 5.5k-char body once loaded
+  if config.skillEnabled: register the rewritten skill   # off by default; names no tool
-  commands → agent.followup("call evolver_…")
+  commands → run in the plugin, answer in the UI   # only /evolver-evolve still prompts the agent
   agent/pre-step → inject recalled steps
-    "…nothing more to search… report with evolver_asset_reuse_result for <id>"
+    "Apply them where they fit."
-  tools/execute → inject "[Evolution Signal] Detected …"
-  agent/turn-stopping → (none)
   session/event turn/end
     report every injected strategy with the turn's evidence      # unchanged
+    remember a distillation candidate (verified, used tools, reused nothing)
+  next prompt reads as a correction → drop the candidate
+  session/flush → distill the candidate once (host model, digest of the turn, not the history)
+  session/disposed → report strategies whose turn never ended as aborted
+  agent/session-start → report a killed session's open strategies, distill its leftover candidate
src/
├── session-distill.js  # new: candidate rules, digest prompt, /conversation/distill body, stale takeover
├── commands.js         # rewritten: status/search/distill in the plugin, sync/review/run/solidify via the evolver CLI
├── injected-assets.js  # ledger keeps sessionId; takeStaleUnreported
├── index.js            # wiring above
└── prime.js            # injected text
removed: assets/commands/{distill,review,run,search,solidify,status,sync}.md
rewritten, opt-in: src/skill.js + assets/skills/capability-evolver/SKILL.md (skillEnabled, default false)
kept, not registered: src/tools.js (Proxy operations the commands run)

Evidence

  • Before: dsh-local-bench layer 4, 60 tasks × 2 attempts, real Hub recall, same period. Stock 0.2.1: first-step context 7.9k (no plugin 5.8k), skill loaded in 62/120 trials, manual reuse reports in 53 (20 mismatched, 40 success). On the pairs both arms solved, stock's paired median tokens was +14%/+15% on procedural and logic tasks.
    After: the same plugin without tools, skill or nudges ("clean"): first-step context 6.2k, zero skill loads, manual reports or distill calls; on the pairs both solved, −32% total tokens on procedural tasks, −14% (median −28%) on logic tasks. This PR is that form plus hook-only reporting and session-end distillation.
  • Session-end distillation uses a digest of the turn, not the history: in the earlier trigger experiment the digest cost a median 8k tokens per distillation (0.3% of the task) with 15/20 Genes passing, against 84k and 12/20 for the whole history.
  • npm test: 167 pass, 0 fail. New test/session-distill.test.js (9): candidate rules, digest prompt, Proxy body with distill_usage, a verified turn distilled exactly once at session end, a corrected turn not distilled, an open strategy reported failed/aborted on session/disposed, a killed session's reports and candidate taken over at the next session start. apply test now asserts zero tools and zero skills.

Merge Danger

Door: two-way

Revert restores the tools, skill and old commands; state files added (~/.evolver/state/distill-pending.json, sessionId in injected ledgers) are ignored by older versions.

Blast Radius: agent-surface

  • Agents can no longer call evolver_search_assets / evolver_fetch_asset / evolver_publish_asset / evolver_poll / evolver_ack themselves; people use /evolver-search, /evolver-sync etc. Anything that relied on the model publishing assets or acking mailbox messages stops.
  • Model-written reuse verdicts (L5 reports, mismatched) disappear; misfit now shows only as applied: unused from the hook. In the benchmark the model reported misleading assets as success in most trials, so this loses little signal.
  • Session-end distillation assumes dsh emits session/flush when a session closes; if it also flushes mid-session the candidate is distilled early (a later verified turn makes a new one). Worth confirming in a real dsh session.
  • The recall still runs once per turn (not first turn only as in the measured "clean" build); benchmark tasks are single-turn, so the two measured the same.

🤖 Generated with Claude Code

… session end

The plugin no longer registers evolver_* tools or the capability-evolver skill, and the injected strategy
ends with "Apply them where they fit." instead of asking the agent to search, report or distill. Reuse is
reported by the turn-end hook (now also as aborted when a session goes away or is killed mid-turn); a
verified turn leaves a candidate on disk, distilled once at session end from a digest of the turn through
the host model. /evolver-* commands run in the plugin and answer in the UI.

Measured on 60 tasks x 2 attempts with real Hub recall (dsh-local-bench, layer 4): the form without tools
or skill used the fewest context tokens per step, and the stock plugin loaded the skill in 62/120 trials
and reported by hand in 53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Sentinel 正在审核此变更,尚未形成最终结论。

大 PR 拆片审核中

  • Sentinel 判断这个 PR 规模较大,已经拆成多个 slice 分别审核,所以会比普通 PR 花更久。
  • 当前 check 会保持进行中;这不是绿色通过,也不是红色阻塞。
  • Sentinel 会继续完成 slice review、Cross-Slice Verifier 和聚合;如果后续发现红区问题,会更新本 head 的审核结论。
  • 规模等级: large
  • Slice 数量: 8

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-dsh-plugin"、pr_number=51、head_sha="15f3772b6e9d2ad6d34a94694c321b9760867e37",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 15f3772b6e9d

… default)

The skill is rewritten for the hook-only design: it explains what runs on its own and points to the
/evolver-* commands, names no tool, and no longer tells the agent to load it before every substantive
task. Off by default, because its catalog line rides every turn and its body every later step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 23, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Sentinel 已审核此变更,并发现 1 个已核验问题。

另有 17 个需要人工确认的风险;黄色 advisory 不阻塞合并,建议开发 agent review advisory 评论并逐条确认。

审核覆盖提示: 2 个 hunk/条目缺少完整输入;它们不作为 bug 报告。

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-dsh-plugin"、pr_number=51、head_sha="883c43a8068c775b3b3370ff84533017915ab14f",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 883c43a8068c

@evomap-aiqa-bot

Copy link
Copy Markdown

Sentinel 保留了 17 个尚未确认的审核候选。这些黄色条目不阻塞合并;红色 P0/P1 与其他独立硬门禁不受影响。 单一猜测、缺少变更行证据或纯 checklist 的候选会统一降为 P3。
开发 agent:请 review 以下 advisory,并逐条给出“已验证无风险 / 已修复 / 转为后续事项”的结论;不要只做整体确认。

  • [P2] 仅凭账本 mtime 会把仍在运行的长耗时会话判定为中止 — ledger 的 mtime 只反映注入或报告写入,并不代表会话存活状态。运行时间超过 staleMs 的活...(truncated) · src/injected-assets.js · advisory_423c8d63cb13f27f4fcf22ed
  • [P2] 按空白拆分参数会破坏带空格的命令值 — rawInput.trim().split(/\s+/)不是命令行参数解析器;它会在引号内部继续拆分,并将引号字符原...(truncated) · src/commands.js · advisory_4e8e4a2bdb1f48c8badc218a
  • [P2] 没有任何成功检查的 turn 仍被视为可蒸馏候选 — 新测试将 checks: [] 的记录认定为“verified”候选。逻辑只排除显式失败的检查,却没有要��...(truncated) · test/session-distill.test.js · advisory_fb0953786018e523ff0eab4b
  • [P2] 移除了未安装全局 CLI 时的 npx 回退 — 新实现固定执行 PATH 中的 evolver 二进制;找不到二进制时只返回 execFile 错误,不再使用�...(truncated) · src/commands.js · advisory_66ecfb16d6f739093e007880
  • [P2] 取消或超时只终止 CLI 根进程,可能遗留后台子进程 — execFile 的 AbortSignal 和 timeout 仅向直接启动的 evolver 进程发送终止信号;代码没有 Unix ...(truncated) · src/commands.js · advisory_bdf620c48353320ccb9e9e7d
  • [P3] 蒸馏失败前即删除候选,重试无法恢复 — takeCandidates() 在模型调用和 /conversation/distill 持久化成功前就从 distill-pending.json 删除候�...(truncated) · src/session-distill.js · advisory_9ebaf91e8fce5cdc8e4bee68
  • [P3] 仅凭超时接管仍活跃会话的候选与资产 — takeCandidates({ sessionId: null, staleMs, now }) 的接口及新增断言表明,catch-up 仅按候选时间领取��...(truncated) · src/session-distill.js · advisory_820ca9548ef2fa8e10ae1a65
  • [P3] 旧版本账本因缺少 sessionId 被永久排除在 stale 报告之外 — PR 为新记录增加 sessionId,同时以 .filter((asset) => asset.sessionId) 作为 stale 消费条件。升级��...(truncated) · src/injected-assets.js · advisory_594c91537d54e104407b78db
  • [P3] 活跃但空闲的会话会被当作中止会话提前蒸馏 — 候选唯一的活动性信息是创建时间 candidate.at。takeCandidates() 将任何超过 staleMs 的其他会...(truncated) · src/index.js · advisory_5bef37a077c90b735d9e97fd
  • [P3] 自动蒸馏将原始会话内容发送并持久化到代理服务 — candidateOf 收集原始用户提示、最终回答、命令、检查命令和文件路径,并写入本地状态文�...(truncated) · src/session-distill.js · advisory_22a50b0e57f8aea95719a3fb
  • [P3] 自动蒸馏跨越会话边界发送未脱敏的提示与命令 — 新增测试明确要求蒸馏提示包含原始用户任务、执行命令、检查命令及路径,并在会话结束...(truncated) · test/session-distill.test.js · advisory_1dee3d47652c2f809a029686
  • [P3] 仅按候选年龄会把仍活跃的会话当作已中止 — leftovers 判断只检查 candidate.at 是否超过 STALE_SESSION_MS,没有会话关闭标记、进程存活信息...(truncated) · src/session-distill.js · advisory_a5d841afe5448c6ddfba027d
  • [P3] 全局候选文件的读改写可覆盖其他进程的更新 — rememberCandidate()、dropCandidate() 和 takeCandidates() 都对同一个 distill-pending.json 执行无锁的�...(truncated) · src/session-distill.js · advisory_7ee50af8d7b7c10a7850fcd0
  • [P3] 自动蒸馏可能持久化并发送会话中的敏感内容 — PR 明确将任务、命令、文件路径、检查及最终答案组成摘要,交给会话模型后发送至 Proxy ��...(truncated) · README.md · advisory_e17ec8ff1d050bdc1c95a5fd
  • [P3] 蒸馏任务领取后先删除,执行失败会永久丢失 — 候选任务在异步蒸馏成功前即从持久状态中删除,领取和确认完成被合并成一次破坏性操作...(truncated) · src/session-distill.js · advisory_6e8da23ba6074a2e9dfb5532
  • [P3] 删除 status 命令资产可能移除 Evolver 健康检查入口 — 本次 PR 完整删除了 status.md。如果该目录中的 Markdown 文件就是命令注册或命令提示入口,�...(truncated) · assets/commands/status.md · advisory_72986aa22baae0126e3b2527
  • [P3] 破坏性领取后的蒸馏失败可能永久丢失候选 — 新测试明确要求 takeCandidates 第一次调用后立即删除候选,而 distillCandidate 又存在 no_model...(truncated) · src/session-distill.js·advisory_7a61ea10eccc0fbbb14cdf6e`

需要完整证据时,可用行末 finding_key 查询 Sentinel PR review context。

由 Sentinel 审核 · 目标提交 883c43a8068c

Comment thread src/session-distill.js Outdated
const pending = readState(pendingPath(), {});
const due = Object.entries(pending)
.filter(([id, candidate]) => id === sessionId || now - Number(candidate?.at) > staleMs);
if (due.length === 0) return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

蒸馏任务在异步执行前即被删除,失败后无法重试

🟠 严重程度:中(P2)
takeCandidates() 在模型调用开始前便从 distill-pending.json 删除候选并写盘;之后 distillCandidate() 遇到无模型、超时、流式调用异常、无法解析 JSON 或代理请求失败时只返回失败状态。distillSession() 和 distillLeftovers() 均未把候选写回,因此临时失败被错误地当作生命周期完成,重试与下次启动补偿路径都无法再发现该任务。

Finding:finding_2c0451dc04801473cb308a36

…sions

Addresses the Sentinel review of #51:
- A candidate is no longer removed before the model call: it is settled only once distilled, and a failed
  try (no model, timeout, unparsed answer, Proxy error) keeps it for the next flush or session start, up to
  three tries. One distillation per session runs at a time.
- A session counts as killed only when the dsh process that wrote its ledger or candidate is gone (signal-0
  probe on the recorded pid), not when it has been idle for a while; entries without a pid are left alone.
- Keys and tokens are stripped from the prompt, commands, checks and answer before a candidate is written.
- Command arguments keep quoted words together; a missing `evolver` executable falls back to
  `npx -y @evomap/evolver` again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@forrestlinfeng

Copy link
Copy Markdown
Contributor Author

Sentinel review of 883c43a addressed in e5baf2b (172 tests pass):

Items Resolution
Candidate deleted before distillation; failure loses it (verified P2 + 3 advisories) Fixed. dueCandidates is read-only; settleCandidate removes a candidate only after the Proxy accepted it, otherwise counts the try (given up after 3). One distillation per session at a time. Tests: a candidate leaves the disk only once distilled…, a failed distillation keeps the candidate for the next try.
Idle/long sessions taken for killed (mtime / candidate age, 4 advisories) Fixed. Ledger entries and candidates record the writer's pid; takeover only when that process is gone (process.kill(pid, 0) → ESRCH). STALE_SESSION_MS removed. Test: an idle session keeps its open strategies: only a gone process counts as killed.
Distillation sends unredacted prompt/commands/answer (3 advisories) Fixed. candidateOf runs everything through withoutSecrets before writing to disk. Test: keys and tokens never reach the candidate.
Whitespace argument split; npx fallback removed Fixed. Quote-aware argumentsOf; evolver ENOENT falls back to npx -y @evomap/evolver. Test: command arguments keep quoted words together, and a missing evolver falls back to npx.
status command asset deleted False positive. /evolver-status still exists, now run in the plugin (src/commands.js).
Turn without any check becomes a candidate By design. Many tasks run no test; a failed check excludes the turn, a correction drops it, and the Proxy quality gate decides; nothing is published.
Old ledgers without sessionId not back-filled By design. Without session id/pid the owner can't be shown dead and the report couldn't carry the right task id; those entries are still reported by their own session's turn end.
Non-atomic read-modify-write of state files; CLI cancel kills root process only Follow-up. Same pattern as existing retention-pending.json / reports-outbox.json; to be fixed once for all state files and CLI calls.

🤖 Generated with Claude Code

@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Sentinel 已审核此变更,并发现 3 个已核验问题。

另有 16 个需要人工确认的风险;黄色 advisory 不阻塞合并,建议开发 agent review advisory 评论并逐条确认。

审核覆盖提示: 2 个 hunk/条目缺少完整输入;它们不作为 bug 报告。

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-dsh-plugin"、pr_number=51、head_sha="e5baf2b9d86df77ef4807d6d13370a70e2588d74",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 e5baf2b9d86d

@evomap-aiqa-bot

Copy link
Copy Markdown

Sentinel 保留了 16 个尚未确认的审核候选。这些黄色条目不阻塞合并;红色 P0/P1 与其他独立硬门禁不受影响。 单一猜测、缺少变更行证据或纯 checklist 的候选会统一降为 P3。
开发 agent:请 review 以下 advisory,并逐条给出“已验证无风险 / 已修复 / 转为后续事项”的结论;不要只做整体确认。

  • [P2] 候选文件的非原子读改写会覆盖并发会话状态 — rememberCandidate、dropCandidate 和 settleCandidate 都先读取整个 distill-pending.json,再写回完整�...(truncated) · src/session-distill.js · advisory_74532b8d3e6e06672eb72f9a
  • [P2] 升级前遗留的未报告条目永远无法被孤儿扫描领取 — 旧格式条目只有 turn、injectedAt 和 strategyText;新扫描器硬性要求 entry.sessionId,并使用�...(truncated) · src/injected-assets.js · advisory_88e220095bdc66ef69630339
  • [P2] 孤儿领取的非原子读改写会覆盖并发更新或复活已删除账本 — takeOrphanedUnreported()先读取完整 ledger 快照,随后直接用该旧快照生成并覆盖写入。读取和��...(truncated) · src/injected-assets.js · advisory_2d1913ed4297da9b86df63fd
  • [P2] 新增测试把无成功检查的回合认定为“已验证” — 测试标题要求候选必须是“verified”,但首个断言明确允许完全没有检查结果的已完成回合�...(truncated) · test/session-distill.test.js · advisory_75fcffcf049cb00eb64731d8
  • [P2] 取消或超时可能遗留 CLI 启动的后台进程 — execFile 的 signal 和 timeout 只终止直接启动的 evolver/npx 进程;实现没有创建并终止进��...(truncated) · src/commands.js · advisory_eb3e9797c7318a0c04d96027
  • [P3] 带转义引号的 CLI 参数会被错误拆分 — 新增 argumentsOf() 只切换单双引号状态,未处理反斜杠转义。处于双引号中的 \" 会被误作�...(truncated) · src/commands.js · advisory_ee1cbb2af9e4dfedd5ca9f0c
  • [P3] 没有任何验证检查的 turn 仍会被标记为可蒸馏 — skipReason 仅拒绝“最后一项检查显式失败”的记录。空 checks 数组时 record.checks.at(-1)?.fail...(truncated) · src/session-distill.js·advisory_5e55e7824755d8afd936a0c4`
  • [P3] 转义引号会被错误解析并静默改变 CLI 参数 — 新增的 argumentsOf() 只记录当前引号类型,没有处理反斜杠转义。双引号中的 \" 会被当作�...(truncated) · src/commands.js · advisory_67b38e2fe68184631ee752b2
  • [P3] 缺少本地 CLI 时执行未锁定版本的远程包 — 新增测试明确规定回退执行 npx -y @evomap/evolver。包名没有版本或摘要约束,-y 又跳过确认...(truncated) · src/commands.js · advisory_ef2cf97a50f1aa3c22f11e77
  • [P3] 遗留候选会使用新会话的代理身份上传 — distill-pending.json 中的候选只记录 session、PID、模型和内容,没有记录项目、代理端点或认��...(truncated) · src/index.js · advisory_8a62aab2d1ade6b9bbea265b
  • [P3] 删除 status 命令定义导致健康检查入口消失 — 本 PR 删除了唯一的 status.md 命令定义,其中包含命令描述、调用 evolver_status 的流程以及 ...(truncated) · assets/commands/status.md · advisory_92c1bcea31f430269d5d9bd4
  • [P3] 并发扫描可重复领取同一条孤儿记录 — 两个扫描器可以先后读取同一份未报告快照,再各自标记并返回相同条目;领取过程没有可...(truncated) · src/injected-assets.js · advisory_6a07ffcdc71b4155a9eb1cd4
  • [P3] CLI 缺失时自动执行未锁定版本的 npm 包 — 当前新增测试要求在 evolver 返回 ENOENT 后执行 npx -y @evomap/evolver ...。包名未携带固定版�...(truncated) · src/commands.js · advisory_31a82e1623ce8174cfcc968e
  • [P3] 孤儿候选领取协议跨模块不具备原子性 — injected-assets 的孤儿扫描与 session-distill 的候选领取依赖非原子的读取、判定、改写。两��...(truncated) · src/injected-assets.js`、`src/session-distill.js · advisory_2e663a4d14e559321c2736d9
  • [P3] 蒸馏任务失败或被替换后可能永久失去调度 — 当前蒸馏任务失败或被新候选替换后没有安排后续执行;同时,恢复侧的孤儿扫描不能覆盖...(truncated) · src/session-distill.js`、`src/injected-assets.js · advisory_982fa5e9f1d302e893548aca
  • [P3] 重复资产保留旧 PID,会把恢复后的活动注入判成孤儿 — rememberInjected() 传入当前 process.pid,但 withInjected() 遇到已存在的 assetId 会原样返回旧��...(truncated) · src/injected-assets.js · advisory_40182dad4b722a7c53ee9a0c

需要完整证据时,可用行末 finding_key 查询 Sentinel PR review context。

由 Sentinel 审核 · 目标提交 e5baf2b9d86d

Comment thread src/session-distill.js Outdated
const pending = readState(pendingPath(), {});
const current = pending[sessionId];
if (!current || current.at !== candidate.at) return;
const attempts = (current.attempts ?? 0) + 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

失败或被新候选替换的蒸馏任务没有后续调度

🟠 严重程度:中(P2)
失败时只增加 attempts 并保留候选,没有定时或即时重试。下一个 session 启动时,distillLeftovers() 只处理 owner PID 已退出的候选;如果宿主进程继续运行,已 disposed 的旧 session 候选不会再被领取。类似地,旧候选蒸馏期间产生新候选时,at 校验会保留新候选,但共享旧 inFlight 的最终 flush 不会为新候选再调度一次。

Finding:finding_fc2929d3b984283e18c36354

Comment thread src/session-distill.js Outdated
* process still runs keeps its candidate however long it waits.
*/
export function dueCandidates({ sessionId, isGone = ownerIsGone }) {
return Object.entries(readState(pendingPath(), {}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

孤儿候选没有跨进程原子领取

🟠 严重程度:中(P2)
dueCandidates() 只读取并筛选候选,网络提交完成后才由 settleCandidate() 删除;没有文件锁、claim 状态或 CAS。inFlight 只是单进程 Map,因此多个宿主可同时领取并持久化同一候选。各写入函数还采用独立的读改写,跨进程 settle/remember/drop 时可覆盖其他 session 的更新。

Finding:finding_fe6fa8af74728b2f5a0b86bc

Comment thread src/session-distill.js
checks: record.checks.map(({ command, failed }) => ({ command: withoutSecrets(command), failed })),
answer: withoutSecrets(record.answer).slice(0, ANSWER_MAX_CHARS),
model,
turn,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

文件路径和手动 focus 绕过秘密脱敏

🟠 严重程度:中(P2)
prompt、命令、检查和答案经过 withoutSecrets(),但 record.paths 原样写盘并加入模型提示;命令入口传入的 focus 也被原样插入提示。模型生成的 args 在提交代理前同样未再次脱敏,因此模型若复述路径或 focus,内容还会继续进入持久化代理请求。

Finding:finding_7700e577b672de3073fb1b6e

Addresses the second Sentinel review of #51:
- `updateState()` reads, changes and writes a state file under an exclusive lock whose dead owner is taken
  over, so two dsh processes sharing `~/.evolver/state` no longer overwrite each other. The candidate file
  and the injection ledgers go through it; four parallel writers keep all 60 entries, against 17 unlocked.
- A session that ends marks its candidate closed, so a distillation that failed at its end is tried again
  at the next session start instead of waiting for the host process to die. Leftovers are claimed under the
  lock by one process at a time, and a failed try releases the claim.
- Touched paths and a `/evolver-distill` focus go through `withoutSecrets()` like the rest of the digest.
- Every ledger entry is stamped with the live session and pid on each injection, so entries written before
  this version, or by the process a resumed session ran in, are neither orphaned nor unclaimable.
- A cancelled or timed-out CLI command takes down the process group it leads, and the npx fallback is held
  to `@evomap/evolver@^2.0.39`, the range this plugin is built against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 23, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@forrestlinfeng

Copy link
Copy Markdown
Contributor Author

Sentinel review of e5baf2b addressed in 7dc84d0 (178 tests pass). sentinel_submit_review_resolution answers 403 credentials_missing from here, so the per-item conclusions are below.

Verified findings

Item Resolution
No follow-up scheduling for a failed or replaced distillation (finding_fc2929d3) Fixed. A session that ends marks its candidate closed, so a try that failed at session/flush no longer waits for the host process to die: the next session start claims and retries it, up to the three tries MAX_ATTEMPTS always promised. A candidate written while an older one is in flight is picked up the same way. Test: a distillation that failed at the end of a session is claimed and tried again at the next session start (fails on e5baf2b, with no /conversation/distill after the new session starts).
Orphan candidates are claimed without cross-process atomicity (finding_fe6fa8af) Fixed. dueCandidates() now returns only the calling session's own candidate. A candidate whose session is over is taken by claimLeftoverCandidates(), which writes claimedBy: <pid> inside one locked read-modify-write (below), skips what a live process already claimed, and re-claims a candidate whose claimer died. A failed try releases the claim rather than holding it. Test: a leftover candidate is claimed by one process only, and again once the claim dies.
Paths and a manual focus bypass redaction (finding_7700e577) Fixed. candidateOf runs record.paths through withoutSecrets, and distillPrompt redacts the /evolver-distill focus before it reaches the model. Test: keys and tokens never reach the candidate, nor the prompt a manual focus writes — a canary in a path and in the focus is gone from both the candidate and the prompt.

Advisories

Item Resolution
Non-atomic read-modify-write of the candidate file and of the ledgers; orphan claim can resurrect a deleted ledger; two scanners can take the same entry (4 advisories, P2/P3) Fixed. state-file.js gains updateState(target, fallback, mutate), which holds an exclusive lock file (O_EXCL create, writer pid inside, a lock whose owner process is gone is taken over, 2s wait then best effort) around read-change-write. distill-pending.json and every injection ledger go through it. Test: an update is read, changed and written under a lock, so parallel writers keep every entry — 4 child processes × 15 keys into one file keep all 60; the same body with plain readState/writeState keeps 17 of 60.
Cancel or timeout may leave the CLI's background children running (P2) Fixed. CLI runs are spawned detached and cancellation or the 10-minute timeout signals the process group (-pid), SIGTERM then SIGKILL after 5s. Windows keeps the single-process kill, having no group to signal.
Entries left by an earlier version are never claimable by the orphan scan (P2); a repeated asset keeps the old pid and a resumed session's injections read as orphaned (P3) Fixed. A ledger belongs to the session it is named after, so each injection now stamps every entry in that file with the live sessionId and pid. Remaining limit, stated plainly: an entry from before this version whose session never injects again has no recoverable session id — it is reported by its own session's turn end if that session returns, and pruned after the existing 7 days otherwise.
Escaped quotes split CLI arguments wrongly (2 advisories, P3) Fixed. argumentsOf handles backslash escapes (\" inside double quotes stays a quote, a\ b stays one argument; inside single quotes a backslash is literal, as in a shell). Test in apply.test.js.
The npx fallback runs an unpinned package (2 advisories, P3) Fixed. The fallback is npx -y @evomap/evolver@^2.0.39 — MIN_EVOLVER_VERSION, the range this plugin is built against. It is a version range, not an integrity digest: it bounds the API, it does not pin a tarball, and the registry is still trusted the same way the existing npm install -g setup path trusts it.
A turn with no check at all still becomes a candidate (2 advisories, P2/P3) Verified, no risk — by design. "Verified" here means nothing contradicted the turn: most real tasks run no test, an explicitly failed check excludes the turn, a correcting next prompt drops the candidate, and the core's own quality gate decides whether a Gene is kept. Nothing is published (publish: false). Requiring a passing check would silence distillation on most sessions, which is the measured point of the feature.
A leftover candidate uploads under a new session's proxy identity (P3) Verified, no risk. The Proxy is a local per-machine service on 127.0.0.1:<port>; the request carries no per-session or per-account credential, and the leftover was written by the same user on the same machine. A changed proxyPort points at a different local Proxy of the same user, not at another account.
Deleting assets/commands/status.md removes the health-check entry point (P3, repeated) False positive. /evolver-status still exists; it is registered in src/commands.js and answers from the Proxy in the plugin, which is the whole point of the PR. Covered by apply.test.js.

🤖 Generated with Claude Code

@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Sentinel 已审核此变更,并发现 1 个已核验问题。

另有 18 个需要人工确认的风险;黄色 advisory 不阻塞合并,建议开发 agent review advisory 评论并逐条确认。

审核覆盖提示: 2 个 hunk/条目缺少完整输入;它们不作为 bug 报告。

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-dsh-plugin"、pr_number=51、head_sha="7dc84d077405037483a56586495a8c5d1484c29f",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 7dc84d077405

@evomap-aiqa-bot

Copy link
Copy Markdown

Sentinel 保留了 18 个尚未确认的审核候选。这些黄色条目不阻塞合并;红色 P0/P1 与其他独立硬门禁不受影响。 单一猜测、缺少变更行证据或纯 checklist 的候选会统一降为 P3。
开发 agent:请 review 以下 advisory,并逐条给出“已验证无风险 / 已修复 / 转为后续事项”的结论;不要只做整体确认。

  • [P1] 陈旧锁回收可删除另一进程刚取得的锁 — 一个进程确认旧锁的 owner 已死亡后,在删除锁文件前没有验证该文件仍是自己检查过的那��...(truncated) · src/state-file.js · advisory_32400b959a5c31fedf5589d3
  • [P2] /evolver-evolve 仍会把本地记忆带入模型上下文 — README 宣称除召回步骤外插件不会向模型上下文放入任何内容,且仅说明启用技能会增加上��...(truncated) · README.zh.md · advisory_0ddea28fb9097e934e51fe3a
  • [P2] 测试将未验证的回合认定为可蒸馏候选 — 新增测试明确要求一个没有任何检查结果的已完成回合返回 null,即被视为可生成候选。��...(truncated) · test/session-distill.test.js · advisory_ca5d99e24af595a6fe619f5b
  • [P2] 无任何成功校验的回合仍会被作为“已验证”策略蒸馏 — 候选筛选仅拒绝“存在且失败”的检查,却不要求至少一个通过的检查。于是没有正确性证...(truncated) · test/session-distill.test.js · advisory_b245b1b6870a1357ab4df18f
  • [P2] 取消 /evolver-distill 后蒸馏仍会继续并写入状态 — 新增 handler 只解构 agent 和 rawInput,没有接收或转交框架提供的 signal。因此取消不会传�...(truncated) · src/commands.js · advisory_88ba08873c79e1c67357c5f4
  • [P3] Windows 取消或超时不能终止 CLI 的后台子进程 — 新的 execute() 在非 Windows 平台以 detached 进程组方式启动 CLI,并以负 PID 终止整个进程组;�...(truncated) · src/commands.js · advisory_158319705567a35a32c7b08a
  • [P3] 仅用 PID 标识账本所有者会在 PID 复用后漏报中止会话 — 新实现只保存数值 PID,并通过 isGone(entry.pid) 判断原进程是否消失。PID 会被操作系统复用�...(truncated) · src/injected-assets.js · advisory_74c97a70b5242917709c2428
  • [P3] CLI 回退未形成一致的版本完整性边界 — 首选路径直接执行 PATH 中任意版本的 evolver,不检查 MIN_EVOLVER_VERSION;仅在 ENOENT 时才�...(truncated) · src/commands.js · advisory_a85bd84d408f45467f56882a
  • [P3] 所谓“固定”回退实际允许静默执行未来发布的 npm 包 — 新增测试把 npx -y @evomap/evolver@^${MIN_EVOLVER_VERSION} 固化为 ENOENT 回退。-y 跳过确认,而 ^ �...(truncated) · test/apply.test.js · advisory_3ec8cd8d1968d6108269e96f
  • [P3] 所谓“固定”的 npx 回退实际使用可漂移的 semver 范围 — 新测试将 caret 范围 ^${MIN_EVOLVER_VERSION} 固化为回退合约,但该范围不是固定版本。每次缺��...(truncated) · test/apply.test.js · advisory_4e52bbe8a1f6fc33c65d352d
  • [P3] 远端已落库但本地未结算时会重复蒸馏 — /conversation/distill 成功持久化后,代码才从 distill-pending.json 删除候选。若进程在两步之间...(truncated) · src/index.js · advisory_8b8e1004dfe1b8b23a33ba62
  • [P3] 删除 status 命令资产可能使健康检查入口消失 — PR 完整删除了 status 命令的定义文件,且当前 diff 没有新增替代文件或迁移入口;若命令��...(truncated) · assets/commands/status.md · advisory_4d7944514518164f58df90d6
  • [P3] LLM 尚未注入时的 flush 会消耗重试并最终删除候选 — distillCandidate()在 llm 为空时返回 no_model,但 run()对所有非 sent 状态无差别调用 settleC...(truncated) · src/index.js·advisory_5a249c8fe38b15cb01aaeefc`
  • [P3] 失败分支可能清空 last-known-good 数据 — 状态链探针:failure branch writes data/cache-like state; verify it does not clear useful last-known-good content · src/state-file.js · slice:api_contract_02:state_chain_7283127e7f0d407b
  • [P3] 已取消或被纠正的蒸馏任务仍可提交并覆盖新状态 — 蒸馏任务启动后缺少可传播到提交和落库阶段的任务代次或取消令牌。命令层的取消、纠正...(truncated) · src/session-distill.js · advisory_1d5e7116fb8d3351188e4d35
  • [P3] 异步完成可能覆盖更新后的状态 — 状态链探针:late async completion may overwrite newer local/store state because no request/version/abort guard is ...(truncated) · src/state-file.js · slice:api_contract_02:state_chain_f28e210596df79ac
  • [P3] 纠正或新候选无法阻止旧蒸馏任务继续提交 — dropCandidate() 和 rememberCandidate()只修改磁盘状态;已经启动的 run()持有旧候选快照,没有...(truncated) · src/index.js · advisory_7cb92878d2e74ee14cae09d7
  • [P3] 蒸馏响应未校验列表字段类型,可能向接口发送非法请求 — argumentsOf()仅要求 summary 为非空字符串;strategy、validation、signals、preconditions若是字...(truncated) · src/session-distill.js · advisory_9042d1e3397d23714d66c7cd

需要完整证据时,可用行末 finding_key 查询 Sentinel PR review context。

由 Sentinel 审核 · 目标提交 7dc84d077405

Comment thread src/index.js
distillLeftovers() {
for (const due of claimLeftoverCandidates()) once(due).catch(() => {});
},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

纠正提示无法取消已经开始的陈旧蒸馏

🟠 严重程度:中(P2)
纠正路径只通过 dropCandidate(sessionId)删除磁盘候选,没有取消对应的 inFlight 调用,也没有在代理 POST 前重新验证候选是否仍是当前权威版本。因此,删除成功后,已启动的模型请求仍可完成并将这个“已判定不值得蒸馏”的旧候选持久化;之后 settleCandidate()因候选已不存在而仅成为空操作。

Finding:finding_1f29b180026f775562b7cf5d

A correction now also aborts a distillation already running for the session, and every distillation asks once
more whether its candidate is still the one on disk before it writes to the Proxy, so a turn judged not worth
keeping is never persisted.

A lock carries a token unique to its acquisition. A dead owner's lock is moved aside before it is deleted and put
back when what was moved is not the lock that was inspected, and a process releases a lock only while it still
holds its own token, so reclaiming a stale lock can no longer delete one another process just took.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@forrestlinfeng

Copy link
Copy Markdown
Contributor Author

Sentinel review of 7dc84d0: the two items below are fixed in 20f20d9 (181 tests pass).

Item Resolution
A correction cannot cancel a stale distillation that already started (finding_1f29b180026f775562b7cf5d, P2) Fixed. A correction now calls distiller.forget(sessionId), which drops the candidate and aborts the session's in-flight distillation. Before the Proxy POST, distillCandidate checks isCurrentCandidate() again, so a candidate that was dropped or replaced while the model was answering is never persisted. Tests: a candidate dropped or cancelled while the model answers is never sent to the Proxy; a correction that arrives while the turn is being distilled stops that distillation.
Reclaiming a stale lock can delete a lock another process just acquired (advisory_32400b959a5c31fedf5589d3, P1) Fixed. Every lock records pid:token, with a token unique to that acquisition. A dead owner's lock is first renamed aside. It is deleted only if the renamed file still holds the dead token that was inspected; otherwise it is linked back. A process releases a lock only while the lock still holds its own token. Test: a dead owner's lock is taken over, and a lock someone else holds by the time of release is left to them.

The remaining yellow advisories are unchanged by this commit.

@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Sentinel 正在审核此变更,尚未形成最终结论。

大 PR 拆片审核中

  • Sentinel 判断这个 PR 规模较大,已经拆成多个 slice 分别审核,所以会比普通 PR 花更久。
  • 当前 check 会保持进行中;这不是绿色通过,也不是红色阻塞。
  • Sentinel 会继续完成 slice review、Cross-Slice Verifier 和聚合;如果后续发现红区问题,会更新本 head 的审核结论。
  • 规模等级: large
  • Slice 数量: 10

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-dsh-plugin"、pr_number=51、head_sha="20f20d9d4f8c8b7c5cd6aa1af78cc2a9f88919f4",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 20f20d9d4f8c

The candidate leaves the disk once the Proxy answers, a moment after the request arrives; asserting right after
the request raced the settle on slower CI runners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@evomap-aiqa-bot

evomap-aiqa-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Sentinel 已审核此变更,并发现 2 个已核验问题。

另有 20 个需要人工确认的风险;黄色 advisory 不阻塞合并,建议开发 agent review advisory 评论并逐条确认。

审核覆盖提示: 2 个 hunk/条目缺少完整输入;它们不作为 bug 报告。

开发 agent 查询详情: 调用 MCP 工具 sentinel_pr_review_context,传入 owner_repo="evomap/evolver-dsh-plugin"、pr_number=51、head_sha="0a824f3b1f2080887256e27c06236dddb3bdc168",并按条目追加 finding_id 或 finding_key。

由 Sentinel 审核 · 目标提交 0a824f3b1f20

@evomap-aiqa-bot

Copy link
Copy Markdown

Sentinel 保留了 20 个尚未确认的审核候选。这些黄色条目不阻塞合并;红色 P0/P1 与其他独立硬门禁不受影响。 单一猜测、缺少变更行证据或纯 checklist 的候选会统一降为 P3。
开发 agent:请 review 以下 advisory,并逐条给出“已验证无风险 / 已修复 / 转为后续事项”的结论;不要只做整体确认。

  • [P2] /evolver-evolve 违反“命令不经过模型”的上下文隔离承诺 — 新增文档明确宣称所有 /evolver-* 命令由插件直接执行且“不经过模型”,但保留的 assets/c...(truncated) · README.zh.md·advisory_c58e5b91b8482c6fe41688f3`
  • [P2] 升级前遗留的孤儿条目仍无法被领取 — 旧格式条目没有 sessionId 和 pid,新增扫描条件要求 entry.sessionId 为真,因此未被恢复并�...(truncated) · src/injected-assets.js · advisory_31ae3ccc86da9fef0e94f00f
  • [P2] 无任何成功校验的回合仍被认定为“已验证” — 测试把 checks: [] 的记录断言为可接受候选,同时测试名称称候选必须“verified”。这会固��...(truncated) · test/session-distill.test.js · advisory_54779b10eea0b88fc5934e8c
  • [P3] 会话释放期间可并发重复蒸馏同一候选 — 正在执行的本会话蒸馏不会在候选上写入 claimedBy。session/disposed 新增的 closeCandidate() 立...(truncated) · src/index.js · advisory_380eaaacdb5005f38d2d3f29
  • [P3] 未导入 Node crypto 模块却调用 randomBytes — 代码调用 crypto.randomBytes() 生成锁 token,但本文件没有从 node:crypto 导入 crypto 或 randomByt...(truncated) · src/state-file.js·advisory_2fa0be5370bd2526a191069f`
  • [P3] 测试将无成功检查的回合认定为“已验证” — 新增测试把“没有任何检查结果”的已完成回合定义为可蒸馏候选。逻辑只需要没有失败检...(truncated) · test/session-distill.test.js · advisory_7cd4ea4c2e5c09e5cae9bf61
  • [P3] 等待超时后无锁写入会覆盖仍在执行的持锁更新 — acquire() 超时返回 null,但 updateState() 随后仍执行完整的读取、修改和写回。此时原持锁...(truncated) · src/state-file.js · advisory_b685b5ce613bf2485a31ca54
  • [P3] 陈旧锁恢复窗口会让两个进程同时持有锁 — reclaim() 先把当前锁重命名到 aside;若发现移走的是另一进程刚取得的新锁,再用 linkSync(a...(truncated) · src/state-file.js·advisory_67ff523e6ac4e7f3df451796`
  • [P3] 没有成功检查的回合被描述为已验证并持久化 — 筛选只排除最后一项显式失败的检查,不要求存在成功检查;蒸馏提示却将该任务称为“fin...(truncated) · src/session-distill.js · advisory_d0f2eb210414781d7a23f260
  • [P3] 取消 /evolver-distill 不会取消蒸馏请求 — CLI 命令处理器会接收并转交 signal,但 distill 命令没有解构或传递该信号给 distillNow。��...(truncated) · src/commands.js · advisory_184360a4cd6387f0ae5da7f7
  • [P3] Windows 取消 CLI 时不会终止其后台子进程 — 新实现仅在非 Windows 平台以负 PID 向进程组发送信号;Windows 分支对直接启动的 npx/evolver ...(truncated) · src/commands.js · advisory_4918f9e221b2780d4db8973d
  • [P3] 取消后的延迟 SIGKILL 可能命中已复用的进程组 — endTree() 发送 SIGTERM 后无条件安排 5 秒后的 SIGKILL,但 CLI 提前退出时没有取消该定时器�...(truncated) · src/commands.js · advisory_52cf81684c9753eea6482285
  • [P3] 失败或未完成的后续回合不会清除旧候选 — 后续回合被 skipReason 排除时直接返回,已有候选仍留在磁盘上。除非后续提示被识别为 cor...(truncated) · src/index.js · advisory_c0acc51ee36e150b53330039
  • [P3] 失败或未执行验证的发布任务仍可进入蒸馏链 — skipReason()只检查最后一条检查是否失败,没有要求存在成功检查,也没有拒绝更早失败但�...(truncated) · src/session-distill.js · advisory_be1ce57ffad48627b5a10a4c
  • [P3] 失败重试可能重复持久化同一候选 — /conversation/distill 请求没有稳定的候选 ID 或幂等键;如果服务端已执行 persist: true,但客�...(truncated) · src/session-distill.js · advisory_81b957dcaa6f05b68cde1c4c
  • [P3] 未闭合引号可吞掉 --dry-run,命令仍被执行 — argumentsOf() 到输入结束时不检查 quote 是否仍非空,而是直接把当前缓冲区作为参数提交��...(truncated) · src/commands.js · advisory_b9f30b3874919c359bd02071
  • [P3] 纠正提示无法撤销已经开始提交的蒸馏 — 纠正路径只删除本地候选并中止请求。若 Proxy 在 abort 到达前已处理 persist: true,远端 Gene ...(truncated) · src/session-distill.js · advisory_bdeda102e16f688e4ebfa85b
  • [P3] 删除状态命令定义可能导致 /evolver-status 不可用 — 本 PR 完整删除了状态命令的唯一变更文件;该文件包含命令描述、evolver_status 调用约定以...(truncated) · assets/commands/status.md · advisory_e99c351cabdb8fde0ebf64a5
  • [P3] 失败分支可能清空 last-known-good 数据 — 状态链探针:failure branch writes data/cache-like state; verify it does not clear useful last-known-good content · src/state-file.js · slice:release_integrity_03:state_chain_7283127e7f0d407b
  • [P3] 异步完成可能覆盖更新后的状态 — 状态链探针:late async completion may overwrite newer local/store state because no request/version/abort guard is ...(truncated) · src/state-file.js · slice:release_integrity_03:state_chain_f28e210596df79ac

需要完整证据时,可用行末 finding_key 查询 Sentinel PR review context。

由 Sentinel 审核 · 目标提交 0a824f3b1f20

Comment thread src/state-file.js
fs.renameSync(lockPath, aside);
} catch {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

三方陈旧锁竞争仍可让两个 owner 同时进入临界区

🔴 严重程度:高(P1)
A 在读取旧 token 后执行 renameSync(lockPath, aside),可能实际移走 B 刚创建的新锁。A 检测到 token 不同后尝试用硬链接恢复,但如果 C 已占用 lockPath,linkSync() 会失败且异常被吞掉;随后 A 删除 aside。结果 B 的有效锁被永久移除,而 C 持有路径上的新锁,B 与 C 可同时执行受保护的读改写。

Finding:finding_99a609880648b92290cd6088

Comment thread src/state-file.js
const lockPath = `${target}.lock`;
const token = acquire(lockPath, Date.now() + LOCK_WAIT_MS);
try {
const next = mutate(readState(target, fallback));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

锁等待超时后仍执行无锁读改写

🟠 严重程度:中(P2)
acquire() 在超时或非 EEXIST 错误时返回 null,但 updateState() 随后仍执行完整的 readState → mutate → writeState。此时持锁进程也可能继续写入,两个完整快照会互相覆盖。代码注释明确将该行为作为“不阻塞会话”的回退,但它绕过了本次新增锁要提供的互斥硬边界。

Finding:finding_90ff6295456d59694728cd5f

@forrestlinfeng
forrestlinfeng merged commit 0d99d6d into main Oct 23, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant