dsh-continual-evolve 0.15.7 → 0.15.8

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.md CHANGED
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-319%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-328%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-90%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
@@ -22,8 +22,8 @@ Reusable experience gathered in one session (user preferences, pitfalls, project
22
22
 
23
23
  1. **Session-start injection** — the plugin registers a single system-prompt section: the workspace's memory index as a store query (hooks only, whole-line truncated over budget, ordered feedback > user > reference), the central-library path, and a three-line guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
24
24
  2. **Four tools, one writer** — `memory_write` (create/update/delete) is the explicit path for "remember …" / "forget …": every proposal passes the code gates (id shape, type enum, the feedback Why/How contract, secret screening carried over from v1, 64KB body cap, optional `corrects` links to the same-workspace active memory being superseded) inside one transaction. `memory_read` fetches bodies by id and searches by keyword — whitespace-separated terms are AND-combined, with literal substring matching for short terms and whenever the trigram index misses; the injected index carries hooks only. Each memory body served by id increments that memory's view counter (views + last viewed, the same presence heartbeat as skills). `skill_write` (create/update/delete in the project, plus an explicit move-to-global) sediments reusable procedures through the same discipline (kebab ids, secret screening, 64KB cap, no duplicate ids or bodies across scopes); every applied skill is materialized by the plugin as `<workspace>/.agents/skills/<id>/SKILL.md` — the database is the source of truth, the files are a read-only projection patrol restores on drift. `skill_read` fetches skill bodies by id and searches project plus global skills (the project shadows a same-named global); each body served by id increments that skill's view counter (views + last viewed), which the card shows as a presence heartbeat — viewed, not applied, so long-unviewed rows are removal candidates while pruning itself stays human. On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
25
- 3. **Proposal-based extraction** — the trajectory-to-memory automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for.
26
- 4. **Autonomous content-quality loop** — the second background automation, independent from extraction by construction (own prompts, own daily budget, own `review_log` ledger, no cursor; only the store's write gates are shared): a cheap gate model call decides whether a memory/skill entry deserves a close look, and only approvals run the evolve pass that judges content quality (memories on accuracy/usability/reusability/duplication/freshness, skills on trigger precision/executability/reusability/duplication) and proposes the rewrite. New writes are reviewed as they land, the existing stock drains through a bounded backlog queue, and untouched entries requeue after 30 days. Two hard guarantees bound the autonomy: delete never lands automatically (rewrite or quarantine only — quarantine hides, never deletes) and oversized rewrites degrade to quarantine. A daily cap (default 100 items, new > corrects-linked > stale > spot-check priority, the tail drops instead of rolling over) bounds the token spend, and every terminal path lands one `item-review` verdict row plus one daily rollup row (budget vs reviewed vs backlog vs dropped, tokens split by cheap/evolve purpose) for later replay. Ships in trial mode (`review.dryRun`, default on): verdicts ledgered, nothing applied.
25
+ 3. **Proposal-based extraction** — the trajectory-to-memory automation, audited in the `extraction_log`: every `agent/turn-stopping` schedules that session's boundary, with state kept **per session** (a run in flight keeps only its newest boundary — coalescing, not queueing; `compaction/start` and session close drain it) and **no idle timer**. A run reads the session increment since its cursor, feeds FTS-similar existing memories to one LLM call, and the model's structured proposal lands through the same gates; failures and rejections never advance the cursor. A cheap pre-gate (increment head + ownership triple-test, tiny verdict) runs first: refusals consume the increment as a model skip without the full call, and an unclear gate fails open to the full call — worst case degrades to the old single-call behavior, never to a stall. Full and gate usages ledger in separate `usage` / `cheap_usage` columns. Internal agents, empty increments, turns without real user prose, and turns that already carried an explicit `memory_write` are mechanically skipped — and every skip is ledgered, consecutive quiet turns of one session as a single row carrying how many wake-ups it stands for.
26
+ 4. **Autonomous content-quality loop** — the second background automation, independent from extraction by construction (own prompts, own daily budget, own `review_log` ledger, no cursor; only the store's write gates are shared): a cheap gate model call decides whether a memory/skill entry deserves a close look, and only approvals run the evolve pass that judges content quality (memories on home-ownership plus accuracy/usability/reusability/duplication/freshness, skills on home-ownership plus trigger precision/executability/reusability/duplication) and proposes the rewrite. The same three home tests (user/environment vs repo-version subject, name-stripped sentence, repo facts live in ADR/docs) guard the extraction prompt, so repo-version specifics never enter as memories in the first place. Gate/leg failures ledger the error text instead of a bare code. New writes are reviewed as they land, the existing stock drains through a bounded backlog queue, and untouched entries requeue after 30 days. Two hard guarantees bound the autonomy: delete never lands automatically (rewrite or quarantine only — quarantine hides, never deletes) and oversized rewrites degrade to quarantine. A daily cap (default 100 items, new > corrects-linked > stale > spot-check priority, the tail drops instead of rolling over) bounds the token spend, and every terminal path lands one `item-review` verdict row plus one daily rollup row (budget vs reviewed vs backlog vs dropped, tokens split by cheap/evolve purpose) for later replay. Ships in trial mode (`review.dryRun`, default on): verdicts ledgered, nothing applied.
27
27
  5. **Rollback is a ledger, not a mechanism** — every mutation lands an `extraction_log` row with full before/after snapshots; undo means rewriting the `before` values. Skill mutations land the same way in `skill_log`, review verdicts in `review_log`. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted; a quarantined skill's materialized file is removed so it stops loading). The read-only card surfaces quarantine anomalies.
28
28
  6. **Read-only card** — mounted on the plugin's page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card and aligned with ZCode's Settings → Memory viewer: a dropdown scope selector over the store's workspace partitions (opening on the host's current workspace), a search box, per-entry relative updated times, click-to-preview bodies (5 MiB cap), patrol warnings, and a skills section listing the workspace's project skills plus visible global skills (global rows badged, project shadowing global) with the same preview, each row showing its view count. Localized through the host's `locale/*.json` metadata channel; host theme tokens, the `Button` primitive, and zh/en copy that tracks the DSH language. The card is a request-time projection of the database and has no edit path.
29
29
 
@@ -78,7 +78,7 @@ Profile patch example:
78
78
 
79
79
  ```bash
80
80
  pnpm install && pnpm build # deps + clean build -> lib/
81
- pnpm test # vitest (319 tests)
81
+ pnpm test # vitest (328 tests)
82
82
  pnpm test:coverage # v8 coverage, CI-enforced thresholds
83
83
  pnpm lint # oxlint src test client
84
84
  pnpm check:pack # published-artifact consistency (also runs on prepack)
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-319%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-328%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-96%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-90%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
@@ -22,8 +22,8 @@ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项
22
22
 
23
23
  1. **开局注入** —— 插件只注册一个 system-prompt section:注入本工作区的记忆索引(查库结果,只含钩子,超预算按整行截断,feedback > user > reference 排序)、中央库路径、三行指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
24
24
  2. **四个工具,一个写者** —— `memory_write`(create/update/delete)承接显式的记住/忘掉:每条提案过代码门禁(id 规范、type 枚举、feedback 的 Why/How 契约、v1 移植的 secret 筛查、64KB 正文上限、可选 `corrects` 指向被替代的同工作区 active 记忆)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。按 id 取到的每份记忆正文计一次查阅(与技能同纪律的存在心跳)。`skill_write`(项目内 create/update/delete + 显式搬到全局)沉淀可复用的操作规程,走同一套纪律(短横线 id、secret 筛查、64KB 上限、跨范围禁重名重文);每条落地的技能由插件代码物化为 `<workspace>/.agents/skills/<id>/SKILL.md`——库是唯一真相,文件是只读投影,漂移由巡检按库还原。`skill_read` 按 id 取技能正文、检索项目与全局技能(同名时项目遮蔽全局);按 id 取到的每份正文计一次查阅(查阅次数 + 上次查阅,卡片展示)。这是存在心跳——只证明被翻过,不证明有用:长期零查阅是删除候选,剪枝拍板仍需人工。宿主没有工具服务时降级为告诉用户措辞,注入不受影响。
25
- 3. **提案制提取** —— 轨迹到记忆的自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。
26
- 4. **内容质量自进化环** —— 第二条后台自动化,与提取在结构上独立(自有提示词、自有日预算、自有 `review_log` 账本、无游标;只共享库的写门):便宜门模型先判某条记忆/技能值不值得细看,批了才跑贵改写,按内容质量打分(记忆五维:准确/可用/可复用/不重复/不过时;技能四维:触发精度/可执行完备/可复用/不重复)并给出改写提案。新写即审、存量走有界积压队列排空、长期未动 30 天回炉。两条硬保证框住自动化:删除永不自动落盘(只有改写与隔离——隔离隐藏不删),超幅改写自动转隔离。日上限默认 100 条(新写 > 被纠正牵连 > 陈旧 > 抽查,尾部丢弃不滚雪球)框住 token 开销;每条终态落一行 `item-review` verdict 行、每天合一行日 rollup(预算 vs 实审 vs 积压 vs 丢弃,token 按便宜/贵分账),方便以后复盘。默认试运行(`review.dryRun` 开):只记 verdict,不落盘。
25
+ 3. **提案制提取** —— 轨迹到记忆的自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。贵调用之前先过一道便宜预检门(增量前 4KB + 归属三测,小 verdict):拒了直接按 model-skip 消费游标,不烧全量 prompt;门不清不楚就透传给贵调用——最差退化成老单调用,永不 stall。贵用量与门用量分 `usage` / `cheap_usage` 两列记账。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。
26
+ 4. **内容质量自进化环** —— 第二条后台自动化,与提取在结构上独立(自有提示词、自有日预算、自有 `review_log` 账本、无游标;只共享库的写门):便宜门模型先判某条记忆/技能值不值得细看,批了才跑贵改写,按内容质量打分(记忆:归属 + 准确/可用/可复用/不重复/不过时五维;技能:归属 + 触发精度/可执行完备/可复用/不重复四维)并给出改写提案。同一套归属三测(主语是用户/环境还是某仓库的某版机制、去名后还成立吗、仓库事实住 ADR/docs)也守在提取 prompt 门口,版本细节从源头就不进记忆;评审调用失败记错误原文,不记秃码。新写即审、存量走有界积压队列排空、长期未动 30 天回炉。两条硬保证框住自动化:删除永不自动落盘(只有改写与隔离——隔离隐藏不删),超幅改写自动转隔离。日上限默认 100 条(新写 > 被纠正牵连 > 陈旧 > 抽查,尾部丢弃不滚雪球)框住 token 开销;每条终态落一行 `item-review` verdict 行、每天合一行日 rollup(预算 vs 实审 vs 积压 vs 丢弃,token 按便宜/贵分账),方便以后复盘。默认试运行(`review.dryRun` 开):只记 verdict,不落盘。
27
27
  5. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。技能变更同样落 `skill_log`,评审 verdict 落 `review_log`。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除;被隔离技能的物化文件一并撤下,使其不再被加载)。只读卡片显形巡检异常。
28
28
  6. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区(默认选中宿主当前工作区)、搜索框、每条记忆的相对更新时间、点击行内展开正文预览(5 MiB 上限)、巡检异常警告,以及技能区(本工作区技能 + 可见的全局技能,全局行打标记,同名时项目遮蔽全局),预览同记忆,每行显示查阅次数。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
29
29
 
@@ -78,7 +78,7 @@ profile patch 示例:
78
78
 
79
79
  ```bash
80
80
  pnpm install && pnpm build # 依赖 + 干净构建 -> lib/
81
- pnpm test # vitest(319 例)
81
+ pnpm test # vitest(328 例)
82
82
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
83
83
  pnpm lint # oxlint src test client
84
84
  pnpm check:pack # 发布产物一致性(prepack 也会跑)
@@ -10,7 +10,23 @@
10
10
  * feedback contract) decide — the proposal is never trusted on its own.
11
11
  */
12
12
  /** The extraction model's system prompt (Chinese — memory content is Chinese-first). */
13
- export declare const EXTRACTION_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u63D0\u53D6\u4E13\u5458\u3002\u4F60\u7684\u552F\u4E00\u4EFB\u52A1\uFF1A\u4ECE\u4F1A\u8BDD\u8F68\u8FF9\u589E\u91CF\u91CC\u5224\u65AD\u54EA\u4E9B\u4FE1\u606F\u503C\u5F97\u5B58\u5165\u7528\u6237\u7684\u8DE8\u4F1A\u8BDD\u6301\u4E45\u8BB0\u5FC6\uFF0C\u5E76\u63D0\u51FA\u7ED3\u6784\u5316\u63D0\u6848\u3002\u4F60\u4E0D\u6267\u884C\u4EFB\u52A1\u3001\u4E0D\u56DE\u7B54\u7528\u6237\u3001\u4E0D\u89E3\u91CA\u63A8\u7406\u3002\n\n## \u4F55\u65F6\u5199\u5165\u8BB0\u5FC6\n\n<type name=\"user\">\n <when_to_save>\u5F97\u77E5\u7528\u6237\u7684\u753B\u50CF\u4E0E\u73AF\u5883\u4E8B\u5B9E\uFF1A\u89D2\u8272\u3001\u6280\u672F\u6808\u3001\u7ECF\u9A8C\u6C34\u5E73\u3001\u504F\u597D\uFF0C\u4EE5\u53CA\u673A\u5668\u4E0E\u5DE5\u5177\u94FE\uFF08\u64CD\u4F5C\u7CFB\u7EDF\u3001\u88C5\u4E86\u4EC0\u4E48\u3001\u914D\u7F6E\u5728\u54EA\uFF09\u3002\u4F8B\uFF1A\"\u6211\u5728 Fedora \u4E0A\u5DE5\u4F5C\uFF0C\u65E5\u5E38\u8DD1 dotnet-desktop profile\"\u3002</when_to_save>\n</type>\n\n<type name=\"feedback\">\n <when_to_save>\u4E24\u7C7B\u4FE1\u53F7\uFF1A\n \uFF081\uFF09\u7EA0\u6B63\u2014\u2014\u7528\u6237\u8BF4\"\u4E0D\u662F\u8FD9\u6837\"\"\u522B\"\"\u505C\u4E0B\"\u7684\u90A3\u4E00\u8F6E\uFF0C\u5B58\u88AB\u7EA0\u6B63\u7684\u505A\u6CD5\u4E0E\u6B63\u786E\u505A\u6CD5\uFF1B\n \uFF082\uFF09\u786E\u8BA4\u2014\u2014\u66F4\u5B89\u9759\u4F46\u540C\u6837\u8981\u7D27\uFF1A\u7528\u6237\u63A5\u53D7\u4E86\u4F60\u4E0D\u5BFB\u5E38\u7684\u9009\u62E9\u800C\u65E0\u5F02\u8BAE\uFF0C\u6216\u660E\u786E\u8BF4\"\u5BF9\uFF0C\u5C31\u8FD9\u6837\"\u3002\u53EA\u5B58\u7EA0\u9519\u4F1A\u8BA9\u884C\u4E3A\u8D8A\u6765\u8D8A\u4FDD\u5B88\u3002\n \u4F8B\uFF1A\"\u6D4B\u8BD5\u522B mock \u6570\u636E\u5E93\uFF0C\u4E0A\u6B21 mock \u8FC7\u4E86\u7EBF\u4E0A\u8FC1\u79FB\u6302\u4E86\"\u2014\u2014\u5B58\u89C4\u5219\u5E76\u5E26\u4E0A\u4E8B\u6545\u539F\u56E0\u3002</when_to_save>\n <body_structure>\u89C4\u5219\u5148\u884C\uFF0C\u968F\u540E\u5FC5\u5E26\u4E24\u884C\uFF1A**Why:**\uFF08\u7528\u6237\u7ED9\u7684\u7406\u7531\u2014\u2014\u77E5\u9053\u4E3A\u4EC0\u4E48\u624D\u80FD\u5224\u65AD\u8FB9\u7F18\u60C5\u51B5\uFF09\u548C **How to apply:**\uFF08\u4F55\u65F6\u4F55\u5730\u751F\u6548\uFF09\u3002\u7F3A\u8FD9\u4E24\u884C\u7684\u63D0\u6848\u4F1A\u88AB\u673A\u68B0\u62D2\u7EDD\u3002</body_structure>\n</type>\n\n<type name=\"reference\">\n <when_to_save>\u7528\u6237\u63D0\u5230\u672A\u6765\u4F1A\u7528\u5230\u7684\u8D44\u6E90\u6307\u9488\uFF1AURL\u3001\u4EEA\u8868\u76D8\u3001\u5DE5\u5355\u3001\u672C\u5730\u6570\u636E\u5E93\u3001\u6587\u6863\u4F4D\u7F6E\u3002</when_to_save>\n</type>\n\n**\u8FB9\u754C\u5224\u636E\uFF1A\u6362\u4E00\u4E2A\u4ED3\u5E93\u8FD8\u6709\u7528\u7684\u4FE1\u606F\u624D\u8FDB\u8BB0\u5FC6\u3002**\u51B3\u7B56\u3001\u67B6\u6784\u53D6\u820D\u3001\u8FDB\u884C\u4E2D\u72B6\u6001\u53EA\u5BF9\u5F53\u524D\u4ED3\u5E93\u6709\u4EF7\u503C\u2014\u2014\u5B83\u4EEC\u8D70\u4ED3\u5E93\u7684 ADR/\u6587\u6863\u8DEF\u7EBF\uFF0C\u7EDD\u4E0D\u63D0\u6848\u3002\u80FD\u4ECE\u4EE3\u7801\u3001git \u5386\u53F2\u3001\u4ED3\u5E93\u6587\u4EF6\u91CC\u91CD\u65B0\u63A8\u5BFC\u51FA\u6765\u7684\u4E8B\u5B9E\u4E0D\u5B58\uFF1B\u4E34\u65F6\u4EFB\u52A1\u72B6\u6001\u3001\u4E00\u6B21\u6027\u8C03\u8BD5\u8FC7\u7A0B\u3001\u65E0\u590D\u73B0\u8BC1\u636E\u7684\u95EE\u9898\u4FEE\u590D\u4E0D\u5B58\u3002\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u8F6C\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\u3002\n\n## \u66F4\u65B0\u4F18\u5148\u4E8E\u65B0\u5EFA\n\n\u5DF2\u6709\u8BB0\u5FC6\u7684\u72B6\u6001\u53D8\u4E86\uFF0C\u63D0\u6848 update \u6539\u539F\u6761\u76EE\uFF1B\u65B0\u65E7\u5E76\u5B58\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\u3002\u8981\u66F4\u65B0\u6216\u5220\u9664\u7684\u6761\u76EE\u4F1A\u8FDE\u540C\u5168\u6587\u7ED9\u4F60\uFF0C\u63D0\u6848\u91CC\u5199\u5B83\u7684 id\u3002\u6CA1\u6709\u503C\u5F97\u6C89\u6DC0\u7684\u4E1C\u897F\u5C31\u660E\u786E\u8BF4 skip\u2014\u2014\u5B81\u7F3A\u52FF\u6EE5\u3002\n\n## \u8F93\u51FA\u683C\u5F0F\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EE3\u7801\u56F4\u680F\u3001\u4E0D\u8981\u89E3\u91CA\uFF1A\n\n{\"decision\":\"apply\"|\"skip\",\"reason\":\"<\u4E00\u53E5\u8BDD\u4F9D\u636E>\",\"proposals\":[{\"action\":\"create\"|\"update\"|\"delete\",\"id\":\"<kebab-slug\uFF0Cupdate/delete \u5FC5\u586B>\",\"type\":\"user\"|\"feedback\"|\"reference\",\"title\":\"<\u77ED\u6807\u9898>\",\"description\":\"<\u4E00\u53E5\u8BDD\u76F8\u5173\u6027\u94A9\u5B50>\",\"body\":\"<\u6B63\u6587>\",\"sourceSeqs\":\"<\u7528\u6237\u539F\u8BDD\u6240\u5728 seq\uFF0C\u5982 820-831\uFF0C\u53EF\u7701\u7565>\"}]}";
13
+ export declare const EXTRACTION_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u63D0\u53D6\u4E13\u5458\u3002\u4F60\u7684\u552F\u4E00\u4EFB\u52A1\uFF1A\u4ECE\u4F1A\u8BDD\u8F68\u8FF9\u589E\u91CF\u91CC\u5224\u65AD\u54EA\u4E9B\u4FE1\u606F\u503C\u5F97\u5B58\u5165\u7528\u6237\u7684\u8DE8\u4F1A\u8BDD\u6301\u4E45\u8BB0\u5FC6\uFF0C\u5E76\u63D0\u51FA\u7ED3\u6784\u5316\u63D0\u6848\u3002\u4F60\u4E0D\u6267\u884C\u4EFB\u52A1\u3001\u4E0D\u56DE\u7B54\u7528\u6237\u3001\u4E0D\u89E3\u91CA\u63A8\u7406\u3002\n\n## \u4F55\u65F6\u5199\u5165\u8BB0\u5FC6\n\n<type name=\"user\">\n <when_to_save>\u5F97\u77E5\u7528\u6237\u7684\u753B\u50CF\u4E0E\u73AF\u5883\u4E8B\u5B9E\uFF1A\u89D2\u8272\u3001\u6280\u672F\u6808\u3001\u7ECF\u9A8C\u6C34\u5E73\u3001\u504F\u597D\uFF0C\u4EE5\u53CA\u673A\u5668\u4E0E\u5DE5\u5177\u94FE\uFF08\u64CD\u4F5C\u7CFB\u7EDF\u3001\u88C5\u4E86\u4EC0\u4E48\u3001\u914D\u7F6E\u5728\u54EA\uFF09\u3002\u4F8B\uFF1A\"\u6211\u5728 Fedora \u4E0A\u5DE5\u4F5C\uFF0C\u65E5\u5E38\u8DD1 dotnet-desktop profile\"\u3002</when_to_save>\n</type>\n\n<type name=\"feedback\">\n <when_to_save>\u4E24\u7C7B\u4FE1\u53F7\uFF1A\n \uFF081\uFF09\u7EA0\u6B63\u2014\u2014\u7528\u6237\u8BF4\"\u4E0D\u662F\u8FD9\u6837\"\"\u522B\"\"\u505C\u4E0B\"\u7684\u90A3\u4E00\u8F6E\uFF0C\u5B58\u88AB\u7EA0\u6B63\u7684\u505A\u6CD5\u4E0E\u6B63\u786E\u505A\u6CD5\uFF1B\n \uFF082\uFF09\u786E\u8BA4\u2014\u2014\u66F4\u5B89\u9759\u4F46\u540C\u6837\u8981\u7D27\uFF1A\u7528\u6237\u63A5\u53D7\u4E86\u4F60\u4E0D\u5BFB\u5E38\u7684\u9009\u62E9\u800C\u65E0\u5F02\u8BAE\uFF0C\u6216\u660E\u786E\u8BF4\"\u5BF9\uFF0C\u5C31\u8FD9\u6837\"\u3002\u53EA\u5B58\u7EA0\u9519\u4F1A\u8BA9\u884C\u4E3A\u8D8A\u6765\u8D8A\u4FDD\u5B88\u3002\n \u4F8B\uFF1A\"\u6D4B\u8BD5\u522B mock \u6570\u636E\u5E93\uFF0C\u4E0A\u6B21 mock \u8FC7\u4E86\u7EBF\u4E0A\u8FC1\u79FB\u6302\u4E86\"\u2014\u2014\u5B58\u89C4\u5219\u5E76\u5E26\u4E0A\u4E8B\u6545\u539F\u56E0\u3002</when_to_save>\n <body_structure>\u89C4\u5219\u5148\u884C\uFF0C\u968F\u540E\u5FC5\u5E26\u4E24\u884C\uFF1A**Why:**\uFF08\u7528\u6237\u7ED9\u7684\u7406\u7531\u2014\u2014\u77E5\u9053\u4E3A\u4EC0\u4E48\u624D\u80FD\u5224\u65AD\u8FB9\u7F18\u60C5\u51B5\uFF09\u548C **How to apply:**\uFF08\u4F55\u65F6\u4F55\u5730\u751F\u6548\uFF09\u3002\u7F3A\u8FD9\u4E24\u884C\u7684\u63D0\u6848\u4F1A\u88AB\u673A\u68B0\u62D2\u7EDD\u3002</body_structure>\n</type>\n\n<type name=\"reference\">\n <when_to_save>\u7528\u6237\u63D0\u5230\u672A\u6765\u4F1A\u7528\u5230\u7684\u8D44\u6E90\u6307\u9488\uFF1AURL\u3001\u4EEA\u8868\u76D8\u3001\u5DE5\u5355\u3001\u672C\u5730\u6570\u636E\u5E93\u3001\u6587\u6863\u4F4D\u7F6E\u3002</when_to_save>\n</type>\n\n**\u8FB9\u754C\u5224\u636E\uFF1A\u6362\u4E00\u4E2A\u4ED3\u5E93\u8FD8\u6709\u7528\u7684\u4FE1\u606F\u624D\u8FDB\u8BB0\u5FC6\u3002**\u51B3\u7B56\u3001\u67B6\u6784\u53D6\u820D\u3001\u8FDB\u884C\u4E2D\u72B6\u6001\u53EA\u5BF9\u5F53\u524D\u4ED3\u5E93\u6709\u4EF7\u503C\u2014\u2014\u5B83\u4EEC\u8D70\u4ED3\u5E93\u7684 ADR/\u6587\u6863\u8DEF\u7EBF\uFF0C\u7EDD\u4E0D\u63D0\u6848\u3002\u80FD\u4ECE\u4EE3\u7801\u3001git \u5386\u53F2\u3001\u4ED3\u5E93\u6587\u4EF6\u91CC\u91CD\u65B0\u63A8\u5BFC\u51FA\u6765\u7684\u4E8B\u5B9E\u4E0D\u5B58\uFF1B\u4E34\u65F6\u4EFB\u52A1\u72B6\u6001\u3001\u4E00\u6B21\u6027\u8C03\u8BD5\u8FC7\u7A0B\u3001\u65E0\u590D\u73B0\u8BC1\u636E\u7684\u95EE\u9898\u4FEE\u590D\u4E0D\u5B58\u3002\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u8F6C\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\u3002\n\n**\u5F52\u5C5E\u4E09\u6D4B\uFF08\u4EFB\u4E00\u4E0D\u901A\u8FC7\u5373\u4E0D\u63D0\u6848\uFF0C\u4E0E\u4E0A\u9762\u7684\u8FB9\u754C\u5224\u636E\u662F\u540C\u4E00\u628A\u5C3A\u5B50\uFF09\uFF1A**\n1. \u4E3B\u8BED\u6D4B\u8BD5\u2014\u2014\u8FD9\u6761\u7684\u4E3B\u8BED\u662F\"\u6211/\u73AF\u5883\"\uFF0C\u8FD8\u662F\"\u8FD9\u4E2A\u4ED3\u5E93\u7684\u67D0\u7248\u673A\u5236\"\uFF1F\u7248\u672C\u53F7\u3001\u9608\u503C\u3001\u5F00\u5173\u540D\u4E00\u51FA\u73B0\uFF0C\u4E3B\u8BED\u5C31\u662F\u4ED3\u5E93\uFF0C\u4E0D\u63D0\u6848\u3002\n2. \u53BB\u540D\u6D4B\u8BD5\u2014\u2014\u628A\u4ED3\u5E93\u540D\u3001\u7248\u672C\u53F7\u3001\u673A\u5236\u540D\u5168\u62B9\u6389\uFF0C\u53E5\u5B50\u8FD8\u6210\u7ACB\u5417\uFF1F\u4E0D\u6210\u7ACB\uFF0C\u4E0D\u63D0\u6848\u3002\n3. \u5F52\u5C5E\u6D4B\u8BD5\u2014\u2014\u4ED3\u5E93\u4E8B\u5B9E\uFF08\u8BBE\u8BA1\u62CD\u677F\u3001\u7248\u672C\u7EC6\u8282\u3001\u4EFB\u52A1\u72B6\u6001\uFF09\u4F4F ADR/docs/\u4EA4\u63A5\uFF1B\u8BB0\u5FC6\u53EA\u6536\u8DDF\u4EBA\u8D70\u7684\uFF08\u534F\u4F5C\u504F\u597D\u3001\u73AF\u5883\u4E8B\u5B9E\u3001\u8DE8\u9879\u76EE\u505A\u6CD5\uFF09\u3002\n\n## \u66F4\u65B0\u4F18\u5148\u4E8E\u65B0\u5EFA\n\n\u5DF2\u6709\u8BB0\u5FC6\u7684\u72B6\u6001\u53D8\u4E86\uFF0C\u63D0\u6848 update \u6539\u539F\u6761\u76EE\uFF1B\u65B0\u65E7\u5E76\u5B58\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\u3002\u8981\u66F4\u65B0\u6216\u5220\u9664\u7684\u6761\u76EE\u4F1A\u8FDE\u540C\u5168\u6587\u7ED9\u4F60\uFF0C\u63D0\u6848\u91CC\u5199\u5B83\u7684 id\u3002\u6CA1\u6709\u503C\u5F97\u6C89\u6DC0\u7684\u4E1C\u897F\u5C31\u660E\u786E\u8BF4 skip\u2014\u2014\u5B81\u7F3A\u52FF\u6EE5\u3002\n\n## \u8F93\u51FA\u683C\u5F0F\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EE3\u7801\u56F4\u680F\u3001\u4E0D\u8981\u89E3\u91CA\uFF1A\n\n{\"decision\":\"apply\"|\"skip\",\"reason\":\"<\u4E00\u53E5\u8BDD\u4F9D\u636E>\",\"proposals\":[{\"action\":\"create\"|\"update\"|\"delete\",\"id\":\"<kebab-slug\uFF0Cupdate/delete \u5FC5\u586B>\",\"type\":\"user\"|\"feedback\"|\"reference\",\"title\":\"<\u77ED\u6807\u9898>\",\"description\":\"<\u4E00\u53E5\u8BDD\u76F8\u5173\u6027\u94A9\u5B50>\",\"body\":\"<\u6B63\u6587>\",\"sourceSeqs\":\"<\u7528\u6237\u539F\u8BDD\u6240\u5728 seq\uFF0C\u5982 820-831\uFF0C\u53EF\u7701\u7565>\"}]}";
14
+ /** The pre-gate system prompt: a small verdict before the expensive full call. */
15
+ export declare const EXTRACTION_CHEAP_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u63D0\u53D6\u7684\u9884\u68C0\u95E8\u3002\u53EA\u5224\u8FD9\u8F6E\u8F68\u8FF9\u589E\u91CF\u91CC\u6709\u6CA1\u6709\u503C\u5F97\u6C89\u6DC0\u7684\u8DE8\u4F1A\u8BDD\u8BB0\u5FC6\uFF08\u7528\u6237\u753B\u50CF\u4E0E\u73AF\u5883\u4E8B\u5B9E\u3001\u53EF\u590D\u7528\u7684\u505A\u6CD5\u7EA0\u6B63\u4E0E\u786E\u8BA4\u3001\u672A\u6765\u4F1A\u7528\u7684\u8D44\u6E90\u6307\u9488\uFF09\uFF0C\u5148\u8FC7\u5F52\u5C5E\u4E09\u6D4B\uFF1A\u4E3B\u8BED\u662F\u7528\u6237/\u73AF\u5883\u8FD8\u662F\u672C\u4ED3\u5E93\u7684\u67D0\u7248\u673A\u5236\uFF1B\u62B9\u6389\u4ED3\u5E93\u540D\u7248\u672C\u53F7\u673A\u5236\u540D\u540E\u8FD8\u6210\u7ACB\u5417\uFF1B\u4ED3\u5E93\u4E8B\u5B9E\u4F4F ADR/docs\u3002\u5F52\u5C5E\u4E0D\u8FC7\u76F4\u63A5\u5224 false\u3002\u53EA\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u4EE3\u7801\u56F4\u680F\u3001\u4E0D\u8981\u89E3\u91CA\uFF1A\n\n{\"worthExtracting\":true|false,\"reason\":\"<\u4E00\u53E5\u8BDD\u4F9D\u636E>\"}";
16
+ /** Head length of the serialized increment the pre-gate reads (small by design). */
17
+ export declare const CHEAP_INCREMENT_MAX = 4000;
18
+ /** The pre-gate prompt: increment head only — no manifest, no candidates. */
19
+ export declare function cheapPrompt(input: {
20
+ incrementHead: string;
21
+ userSeqs: readonly number[];
22
+ }): string;
23
+ /** The pre-gate verdict; `null` when the model did not return verdict JSON. */
24
+ export interface CheapVerdict {
25
+ worth: boolean;
26
+ reason: string;
27
+ }
28
+ /** Parse the pre-gate answer. `null` means fail-open: run the full call. */
29
+ export declare function parseCheapAnswer(text: string): CheapVerdict | null;
14
30
  /** The existing-memory manifest line shown to the model. */
15
31
  export declare function manifestLineOf(record: {
16
32
  id: string;
@@ -32,6 +32,11 @@ export const EXTRACTION_SYSTEM_PROMPT = `你是记忆提取专员。你的唯一
32
32
 
33
33
  **边界判据:换一个仓库还有用的信息才进记忆。**决策、架构取舍、进行中状态只对当前仓库有价值——它们走仓库的 ADR/文档路线,绝不提案。能从代码、git 历史、仓库文件里重新推导出来的事实不存;临时任务状态、一次性调试过程、无复现证据的问题修复不存。相对日期一律转成绝对日期("周四"→"2026-10-08")。
34
34
 
35
+ **归属三测(任一不通过即不提案,与上面的边界判据是同一把尺子):**
36
+ 1. 主语测试——这条的主语是"我/环境",还是"这个仓库的某版机制"?版本号、阈值、开关名一出现,主语就是仓库,不提案。
37
+ 2. 去名测试——把仓库名、版本号、机制名全抹掉,句子还成立吗?不成立,不提案。
38
+ 3. 归属测试——仓库事实(设计拍板、版本细节、任务状态)住 ADR/docs/交接;记忆只收跟人走的(协作偏好、环境事实、跨项目做法)。
39
+
35
40
  ## 更新优先于新建
36
41
 
37
42
  已有记忆的状态变了,提案 update 改原条目;新旧并存比没有记忆更糟。要更新或删除的条目会连同全文给你,提案里写它的 id。没有值得沉淀的东西就明确说 skip——宁缺勿滥。
@@ -41,6 +46,49 @@ export const EXTRACTION_SYSTEM_PROMPT = `你是记忆提取专员。你的唯一
41
46
  只输出一个 JSON 对象,不要代码围栏、不要解释:
42
47
 
43
48
  {"decision":"apply"|"skip","reason":"<一句话依据>","proposals":[{"action":"create"|"update"|"delete","id":"<kebab-slug,update/delete 必填>","type":"user"|"feedback"|"reference","title":"<短标题>","description":"<一句话相关性钩子>","body":"<正文>","sourceSeqs":"<用户原话所在 seq,如 820-831,可省略>"}]}`;
49
+ /** The pre-gate system prompt: a small verdict before the expensive full call. */
50
+ export const EXTRACTION_CHEAP_SYSTEM_PROMPT = `你是记忆提取的预检门。只判这轮轨迹增量里有没有值得沉淀的跨会话记忆(用户画像与环境事实、可复用的做法纠正与确认、未来会用的资源指针),先过归属三测:主语是用户/环境还是本仓库的某版机制;抹掉仓库名版本号机制名后还成立吗;仓库事实住 ADR/docs。归属不过直接判 false。只输出 JSON,不要代码围栏、不要解释:
51
+
52
+ {"worthExtracting":true|false,"reason":"<一句话依据>"}`;
53
+ /** Head length of the serialized increment the pre-gate reads (small by design). */
54
+ export const CHEAP_INCREMENT_MAX = 4000;
55
+ /** The pre-gate prompt: increment head only — no manifest, no candidates. */
56
+ export function cheapPrompt(input) {
57
+ return [
58
+ "(用户直接发言所在 seq:",
59
+ `${input.userSeqs.length > 0 ? input.userSeqs.join(",") : "无"})`,
60
+ input.incrementHead,
61
+ "按系统指令输出 JSON 判定。",
62
+ ].join("\n");
63
+ }
64
+ /** Parse the pre-gate answer. `null` means fail-open: run the full call. */
65
+ export function parseCheapAnswer(text) {
66
+ const stripped = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
67
+ const start = stripped.indexOf("{");
68
+ const end = stripped.lastIndexOf("}");
69
+ if (start < 0 || end <= start) {
70
+ return null;
71
+ }
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(stripped.slice(start, end + 1));
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ // Defensive: the sliced text always starts with "{", so a successful
80
+ // JSON.parse here is always an object — same guard as parseExtractionAnswer.
81
+ /* v8 ignore next 3 */
82
+ if (typeof parsed !== "object" || parsed === null) {
83
+ return null;
84
+ }
85
+ const answer = parsed;
86
+ if (typeof answer.worthExtracting !== "boolean") {
87
+ return null;
88
+ }
89
+ const reason = typeof answer.reason === "string" && answer.reason.length > 0 ? answer.reason : "无理由";
90
+ return { worth: answer.worthExtracting, reason: reason.slice(0, 300) };
91
+ }
44
92
  /** The existing-memory manifest line shown to the model. */
45
93
  export function manifestLineOf(record) {
46
94
  return `- id=${record.id}|type=${record.type}|title=${record.title}|钩子=${record.description}`;
package/lib/extraction.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { boundaryCursorOf, evaluateSlice, eventsAfterCursor, isInternalAgent, seqOfCursor, serializeIncrement } from "./extraction-surface.js";
2
- import { EXTRACTION_SYSTEM_PROMPT, extractionPrompt, manifestLineOf, parseExtractionAnswer } from "./extraction-prompt.js";
2
+ import { CHEAP_INCREMENT_MAX, EXTRACTION_CHEAP_SYSTEM_PROMPT, EXTRACTION_SYSTEM_PROMPT, cheapPrompt, extractionPrompt, manifestLineOf, parseCheapAnswer, parseExtractionAnswer } from "./extraction-prompt.js";
3
3
  import { cwdOf } from "./memory-section.js";
4
4
  /** Head length of a failed run's raw answer kept in the ledger `files` column. */
5
5
  const FAILURE_RAW_MAX = 2048;
@@ -107,6 +107,35 @@ async function runExtractionInner(deps, target, opts) {
107
107
  }
108
108
  };
109
109
  const increment = serializeIncrement(slice.events, opts.incrementMaxChars);
110
+ // ── Cheap pre-gate: a small verdict before the expensive full call
111
+ // (manifest + full-body candidates + full increment). A "not worth it"
112
+ // consumes the increment exactly like a model skip; an unclear gate
113
+ // (unparseable/failed) fails OPEN to the full call — worst case degrades
114
+ // to the old single-call behavior, never to a stall.
115
+ let cheapUsage;
116
+ {
117
+ const gateStarted = Date.now();
118
+ try {
119
+ const gate = await streamOnce(deps.llm, target, cheapPrompt({ incrementHead: increment.slice(0, CHEAP_INCREMENT_MAX), userSeqs: slice.userSeqs }), EXTRACTION_CHEAP_SYSTEM_PROMPT, 512);
120
+ cheapUsage = gate.usage;
121
+ const verdict = parseCheapAnswer(gate.text);
122
+ if (verdict !== null && !verdict.worth) {
123
+ advanceCursor();
124
+ store.logSkipped({
125
+ ...ledgerBase,
126
+ skipReason: `model-skip (cheap-gate): ${verdict.reason}`,
127
+ status: "skipped",
128
+ model: target.model,
129
+ cheapUsage,
130
+ durationMs: Date.now() - gateStarted,
131
+ });
132
+ return { status: "skipped", skipReason: verdict.reason, cursor: boundarySeq };
133
+ }
134
+ }
135
+ catch {
136
+ // Fail open: the full call below is the old behavior.
137
+ }
138
+ }
110
139
  const manifest = store.list(target.workspaceId).slice(0, 50).map(manifestLineOf);
111
140
  const candidates = candidateRecords(store, target.workspaceId, slice.userText);
112
141
  const prompt = extractionPrompt({
@@ -126,7 +155,7 @@ async function runExtractionInner(deps, target, opts) {
126
155
  catch (error) {
127
156
  const { message, detail } = errorText(error);
128
157
  try {
129
- store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started, files: detail });
158
+ store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started, cheapUsage, files: detail });
130
159
  }
131
160
  catch (ledgerError) {
132
161
  throw new Error(`llm call failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
@@ -145,6 +174,7 @@ async function runExtractionInner(deps, target, opts) {
145
174
  status: "failed",
146
175
  model: target.model,
147
176
  usage,
177
+ cheapUsage,
148
178
  durationMs: Date.now() - started,
149
179
  files: text.slice(0, FAILURE_RAW_MAX),
150
180
  });
@@ -160,6 +190,7 @@ async function runExtractionInner(deps, target, opts) {
160
190
  status: "skipped",
161
191
  model: target.model,
162
192
  usage,
193
+ cheapUsage,
163
194
  durationMs: Date.now() - started,
164
195
  });
165
196
  return { status: "skipped", skipReason: parsed.reason, cursor: boundarySeq };
@@ -171,6 +202,7 @@ async function runExtractionInner(deps, target, opts) {
171
202
  runId: `extract:${target.sessionId}:${boundarySeq ?? "tail"}`,
172
203
  model: target.model,
173
204
  usage,
205
+ cheapUsage,
174
206
  decision: parsed.reason,
175
207
  }, parsed.proposals);
176
208
  if (!outcomes.every((outcome) => outcome.ok)) {
@@ -193,7 +225,7 @@ function formatCandidate(record) {
193
225
  return [`# 候选 id=${record.id}(type=${record.type})`, `title=${record.title}`, `钩子=${record.description}`, "全文:", record.body].join("\n");
194
226
  }
195
227
  /** One streaming text call with the host session identity forwarded. */
196
- async function streamOnce(llm, target, prompt) {
228
+ async function streamOnce(llm, target, prompt, system = EXTRACTION_SYSTEM_PROMPT, maxTokens = 4096) {
197
229
  let text = "";
198
230
  let usage;
199
231
  let finish;
@@ -201,8 +233,8 @@ async function streamOnce(llm, target, prompt) {
201
233
  provider: target.provider,
202
234
  model: target.model,
203
235
  messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
204
- system: EXTRACTION_SYSTEM_PROMPT,
205
- maxTokens: 4096,
236
+ system,
237
+ maxTokens,
206
238
  // MissingSessionID pitfall: direct calls must carry the host session.
207
239
  sessionId: target.sessionId,
208
240
  };
@@ -11,11 +11,11 @@
11
11
  * deliberately behavior-blind: there are no task datasets here (unlike
12
12
  * Hermes GEPA), only the item text plus its provenance.
13
13
  */
14
- export declare const REVIEW_CHEAP_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u95E8\u3002\u53EA\u5224\u8FD9\u6761\u8BB0\u5FC6/\u6280\u80FD\u503C\u4E0D\u503C\u5F97\u7EC6\u770B\uFF1A\u6709\u660E\u786E\u4E8B\u5B9E\u3001\u505A\u6CD5\u3001\u504F\u597D\u6216\u53EF\u590D\u7528\u89C4\u7A0B\u5C31\u6279\uFF1B\u7A7A\u8BDD\u3001\u7EAF\u8FC7\u7A0B\u8BB0\u5F55\u3001\u4E00\u6B21\u6027\u7684\u77AC\u65F6\u8F93\u51FA\u3001\u5F53\u8F6E\u5DF2\u53EF\u63A8\u5BFC\u7684\u4E00\u5F8B\u62D2\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"worthReview\": true|false, \"reason\": \"\u4E00\u53E5\u8BDD\"}\u3002";
15
- /** Memory rubric: accurate, usable, reusable, non-duplicate, fresh. */
16
- export declare const REVIEW_MEMORY_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u6309\u4E94\u7EF4\u5224\u8FD9\u6761\u8BB0\u5FC6\uFF1A\u2460\u51C6\u786E\uFF08\u4E8B\u5B9E\u53EF\u4FE1\u3001\u65E0\u5E7B\u89C9\uFF09\uFF1B\u2461\u53EF\u7528\uFF08Why/How \u5177\u4F53\u3001\u53EF\u6267\u884C\uFF0C\u53CD\u9988\u7C7B\u5FC5\u987B\u5E26 Why \u4E0E How to apply\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u672A\u6765\u4F1A\u8BDD\u7528\u5F97\u4E0A\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E7\u6761\u6BD4\uFF0C\u6709\u91CD\u590D\u5C31\u6307\u51FA\u6765\uFF09\uFF1B\u2464\u4E0D\u8FC7\u65F6\uFF08\u672A\u88AB\u7EA0\u6B63\u94FE\u53D6\u4EE3\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\", \"corrects\"?: \"\u88AB\u53D6\u4EE3\u7684\u540C\u533A\u6761\u76EEid\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6\u3002\u7981\u5220\u9664\uFF0C\u7981\u7F16\u9020\u5F15\u7528\u3002";
17
- /** Skill rubric: trigger precision, executability, reusability, non-duplicate. */
18
- export declare const REVIEW_SKILL_SYSTEM_PROMPT = "\u4F60\u662F\u6280\u80FD\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u6309\u56DB\u7EF4\u5224\u8FD9\u6761\u6280\u80FD\uFF1A\u2460\u89E6\u53D1\u7CBE\u5EA6\uFF08description \u662F\u5426\u8BA9\u4EBA/\u6A21\u578B\u5728\u8BE5\u7528\u65F6\u60F3\u8D77\u3001\u4E0D\u8BE5\u7528\u65F6\u4E0D\u6253\u6270\uFF09\uFF1B\u2461\u6B63\u6587\u53EF\u6267\u884C\u5B8C\u5907\uFF08\u6B65\u9AA4\u5B8C\u6574\u3001\u8F93\u5165\u8F93\u51FA\u660E\u786E\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u8DE8\u4EFB\u52A1\u53EF\u7528\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E2\u6709\u6280\u80FD\u6BD4\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6\u3002\u7981\u5220\u9664\u3002";
14
+ export declare const REVIEW_CHEAP_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u95E8\u3002\u5148\u8FC7\u5F52\u5C5E\u4E09\u6D4B\uFF1A\u2460\u4E3B\u8BED\u662F\u7528\u6237/\u73AF\u5883\u8FD8\u662F\u67D0\u4ED3\u5E93\u7684\u67D0\u7248\u673A\u5236\uFF08\u7248\u672C\u53F7\u3001\u9608\u503C\u3001\u5F00\u5173\u540D\u51FA\u73B0\u5373\u540E\u8005\uFF09\uFF1B\u2461\u62B9\u6389\u4ED3\u5E93\u540D\u7248\u672C\u53F7\u673A\u5236\u540D\u540E\u53E5\u5B50\u8FD8\u6210\u7ACB\u5417\uFF1B\u2462\u4ED3\u5E93\u4E8B\u5B9E\u4F4F ADR/docs\uFF0C\u8BB0\u5FC6\u53EA\u6536\u8DDF\u4EBA\u8D70\u7684\u3002\u5F52\u5C5E\u4E0D\u8FC7\u76F4\u63A5\u5224 false \u5E76\u6CE8\u660E\u653E\u9519\u5BB6\u3002\u518D\u5224\u503C\u4E0D\u503C\u5F97\u7EC6\u770B\uFF1A\u6709\u660E\u786E\u4E8B\u5B9E\u3001\u505A\u6CD5\u3001\u504F\u597D\u6216\u53EF\u590D\u7528\u89C4\u7A0B\u5C31\u6279\uFF1B\u7A7A\u8BDD\u3001\u7EAF\u8FC7\u7A0B\u8BB0\u5F55\u3001\u4E00\u6B21\u6027\u7684\u77AC\u65F6\u8F93\u51FA\u3001\u5F53\u8F6E\u5DF2\u53EF\u63A8\u5BFC\u7684\u4E00\u5F8B\u62D2\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"worthReview\": true|false, \"reason\": \"\u4E00\u53E5\u8BDD\"}\u3002";
15
+ /** Memory rubric: accurate, usable, reusable, non-duplicate, fresh, right-home. */
16
+ export declare const REVIEW_MEMORY_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u5148\u8FC7\u5F52\u5C5E\u4E09\u6D4B\uFF08\u4E3B\u8BED\u662F\u7528\u6237/\u73AF\u5883\u8FD8\u662F\u67D0\u4ED3\u5E93\u7684\u67D0\u7248\u673A\u5236\uFF1B\u62B9\u6389\u4ED3\u5E93\u540D\u7248\u672C\u53F7\u673A\u5236\u540D\u540E\u8FD8\u6210\u7ACB\u5417\uFF1B\u4ED3\u5E93\u4E8B\u5B9E\u4F4F ADR/docs\uFF09\uFF1A\u5F52\u5C5E\u4E0D\u8FC7\u76F4\u63A5 quarantine \u5E76\u6CE8\u660E\u653E\u9519\u5BB6\u3002\u518D\u6309\u4E94\u7EF4\u5224\uFF1A\u2460\u51C6\u786E\uFF08\u4E8B\u5B9E\u53EF\u4FE1\u3001\u65E0\u5E7B\u89C9\uFF09\uFF1B\u2461\u53EF\u7528\uFF08Why/How \u5177\u4F53\u3001\u53EF\u6267\u884C\uFF0C\u53CD\u9988\u7C7B\u5FC5\u987B\u5E26 Why \u4E0E How to apply\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u672A\u6765\u4F1A\u8BDD\u7528\u5F97\u4E0A\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E7\u6761\u6BD4\uFF0C\u6709\u91CD\u590D\u5C31\u6307\u51FA\u6765\uFF09\uFF1B\u2464\u4E0D\u8FC7\u65F6\uFF08\u672A\u88AB\u7EA0\u6B63\u94FE\u53D6\u4EE3\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\", \"corrects\"?: \"\u88AB\u53D6\u4EE3\u7684\u540C\u533A\u6761\u76EEid\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6/\u653E\u9519\u5BB6\u3002\u7981\u5220\u9664\uFF0C\u7981\u7F16\u9020\u5F15\u7528\u3002";
17
+ /** Skill rubric: trigger precision, executability, reusability, non-duplicate, right-home. */
18
+ export declare const REVIEW_SKILL_SYSTEM_PROMPT = "\u4F60\u662F\u6280\u80FD\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u5148\u8FC7\u5F52\u5C5E\u4E09\u6D4B\uFF08\u6280\u80FD\u662F\u5426\u7ED1\u5B9A\u5355\u4E00\u4ED3\u5E93\u7684\u8DEF\u5F84\u4E0E\u673A\u5236\uFF1B\u62B9\u6389\u4ED3\u5E93\u540D\u540E\u8FD8\u6210\u7ACB\u5417\uFF09\uFF1A\u653E\u9519\u5BB6\u76F4\u63A5 quarantine\u3002\u518D\u6309\u56DB\u7EF4\u5224\uFF1A\u2460\u89E6\u53D1\u7CBE\u5EA6\uFF08description \u662F\u5426\u8BA9\u4EBA/\u6A21\u578B\u5728\u8BE5\u7528\u65F6\u60F3\u8D77\u3001\u4E0D\u8BE5\u7528\u65F6\u4E0D\u6253\u6270\uFF09\uFF1B\u2461\u6B63\u6587\u53EF\u6267\u884C\u5B8C\u5907\uFF08\u6B65\u9AA4\u5B8C\u6574\u3001\u8F93\u5165\u8F93\u51FA\u660E\u786E\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u8DE8\u4EFB\u52A1\u53EF\u7528\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E2\u6709\u6280\u80FD\u6BD4\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6/\u653E\u9519\u5BB6\u3002\u7981\u5220\u9664\u3002";
19
19
  export interface GateVerdict {
20
20
  worthReview: boolean;
21
21
  reason: string;
@@ -11,11 +11,11 @@
11
11
  * deliberately behavior-blind: there are no task datasets here (unlike
12
12
  * Hermes GEPA), only the item text plus its provenance.
13
13
  */
14
- export const REVIEW_CHEAP_SYSTEM_PROMPT = "你是记忆库的内容评审门。只判这条记忆/技能值不值得细看:有明确事实、做法、偏好或可复用规程就批;空话、纯过程记录、一次性的瞬时输出、当轮已可推导的一律拒。只输出 JSON:{\"worthReview\": true|false, \"reason\": \"一句话\"}。";
15
- /** Memory rubric: accurate, usable, reusable, non-duplicate, fresh. */
16
- export const REVIEW_MEMORY_SYSTEM_PROMPT = "你是记忆库的内容评审。按五维判这条记忆:①准确(事实可信、无幻觉);②可用(Why/How 具体、可执行,反馈类必须带 Why 与 How to apply);③可复用(未来会话用得上);④不重复(与候选旧条比,有重复就指出来);⑤不过时(未被纠正链取代)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\", \"corrects\"?: \"被取代的同区条目id\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时。禁删除,禁编造引用。";
17
- /** Skill rubric: trigger precision, executability, reusability, non-duplicate. */
18
- export const REVIEW_SKILL_SYSTEM_PROMPT = "你是技能库的内容评审。按四维判这条技能:①触发精度(description 是否让人/模型在该用时想起、不该用时不打扰);②正文可执行完备(步骤完整、输入输出明确);③可复用(跨任务可用);④不重复(与候选既有技能比)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时。禁删除。";
14
+ export const REVIEW_CHEAP_SYSTEM_PROMPT = "你是记忆库的内容评审门。先过归属三测:①主语是用户/环境还是某仓库的某版机制(版本号、阈值、开关名出现即后者);②抹掉仓库名版本号机制名后句子还成立吗;③仓库事实住 ADR/docs,记忆只收跟人走的。归属不过直接判 false 并注明放错家。再判值不值得细看:有明确事实、做法、偏好或可复用规程就批;空话、纯过程记录、一次性的瞬时输出、当轮已可推导的一律拒。只输出 JSON:{\"worthReview\": true|false, \"reason\": \"一句话\"}。";
15
+ /** Memory rubric: accurate, usable, reusable, non-duplicate, fresh, right-home. */
16
+ export const REVIEW_MEMORY_SYSTEM_PROMPT = "你是记忆库的内容评审。先过归属三测(主语是用户/环境还是某仓库的某版机制;抹掉仓库名版本号机制名后还成立吗;仓库事实住 ADR/docs):归属不过直接 quarantine 并注明放错家。再按五维判:①准确(事实可信、无幻觉);②可用(Why/How 具体、可执行,反馈类必须带 Why 与 How to apply);③可复用(未来会话用得上);④不重复(与候选旧条比,有重复就指出来);⑤不过时(未被纠正链取代)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\", \"corrects\"?: \"被取代的同区条目id\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时/放错家。禁删除,禁编造引用。";
17
+ /** Skill rubric: trigger precision, executability, reusability, non-duplicate, right-home. */
18
+ export const REVIEW_SKILL_SYSTEM_PROMPT = "你是技能库的内容评审。先过归属三测(技能是否绑定单一仓库的路径与机制;抹掉仓库名后还成立吗):放错家直接 quarantine。再按四维判:①触发精度(description 是否让人/模型在该用时想起、不该用时不打扰);②正文可执行完备(步骤完整、输入输出明确);③可复用(跨任务可用);④不重复(与候选既有技能比)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时/放错家。禁删除。";
19
19
  /** The cheap-gate prompt for one item (id + hook + body, truncated by caller). */
20
20
  export function gatePrompt(entry) {
21
21
  return [
package/lib/review.d.ts CHANGED
@@ -68,6 +68,8 @@ export declare function collectCandidates(store: MemoryStore, opts?: {
68
68
  * discipline.
69
69
  */
70
70
  export declare function runReviewBatch(deps: ReviewDeps, candidates: readonly ReviewCandidate[], opts: ReviewRunOptions): Promise<ReviewRunSummary>;
71
+ /** One-line error head for ledger rows (fail-loud shape, capped length). */
72
+ export declare function failureHead(error: unknown, max?: number): string;
71
73
  /** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
72
74
  export declare function todayUtc(): string;
73
75
  /** The event wiring surface (duck-typed cordis context, mirrors extraction). */
package/lib/review.js CHANGED
@@ -211,8 +211,11 @@ async function reviewOne(deps, runId, candidate, opts, summary, skipReasons) {
211
211
  summary.cheapToken += totalOf(gateRaw);
212
212
  gateUsage = normalizedOf(gateRaw);
213
213
  }
214
- catch {
215
- deps.store.logReview({ ...base, verdict: "skip", reason: "gate-llm-failed", applied: 0 });
214
+ catch (error) {
215
+ // The error text rides the row: a bare `gate-llm-failed` hid a ~50%
216
+ // per-item failure rate in production (2026-10-12) with no way to
217
+ // tell transient route faults from systematic ones.
218
+ deps.store.logReview({ ...base, verdict: "skip", reason: `gate-llm-failed: ${failureHead(error)}`, applied: 0 });
216
219
  skipReasons.push("gate-llm-failed");
217
220
  return;
218
221
  }
@@ -254,8 +257,8 @@ async function reviewOne(deps, runId, candidate, opts, summary, skipReasons) {
254
257
  }
255
258
  landRewrite(deps, base, candidate, opts, evolved, gateUsage, evolveRaw, summary);
256
259
  }
257
- catch {
258
- deps.store.logReview({ ...base, verdict: "skip", reason: "evolve-llm-failed", cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
260
+ catch (error) {
261
+ deps.store.logReview({ ...base, verdict: "skip", reason: `evolve-llm-failed: ${failureHead(error)}`, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
259
262
  skipReasons.push("evolve-llm-failed");
260
263
  }
261
264
  }
@@ -324,6 +327,12 @@ function landRewrite(deps, base, candidate, opts, evolved, gateUsage, evolveRaw,
324
327
  function totalOf(raw) {
325
328
  return normalizeUsage(raw)?.total ?? 0;
326
329
  }
330
+ /** One-line error head for ledger rows (fail-loud shape, capped length). */
331
+ export function failureHead(error, max = 120) {
332
+ const message = error instanceof Error ? error.message : String(error);
333
+ const head = message.split("\n", 1)[0] ?? "";
334
+ return head.length > max ? `${head.slice(0, max)}…` : head;
335
+ }
327
336
  function normalizedOf(raw) {
328
337
  return normalizeUsage(raw) ?? zeroUsage();
329
338
  }
@@ -26,6 +26,8 @@ export interface ProposalContext {
26
26
  runId?: string | undefined;
27
27
  model?: string | undefined;
28
28
  usage?: unknown;
29
+ /** Pre-gate call payload (extraction cheap gate); carried into the ledger row. */
30
+ cheapUsage?: unknown;
29
31
  /** One-line model decision summary for the ledger. */
30
32
  decision?: string | undefined;
31
33
  }
@@ -62,6 +62,7 @@ export function applyProposals(db, ctx, proposals) {
62
62
  durationMs: Date.now() - started,
63
63
  model: ctx.model,
64
64
  usage: ctx.usage,
65
+ cheapUsage: ctx.cheapUsage,
65
66
  files: applied ? files : [],
66
67
  };
67
68
  if (!applied) {
@@ -70,10 +71,10 @@ export function applyProposals(db, ctx, proposals) {
70
71
  writeLedger(db.database.prepare(LEDGER_SQL), ledger);
71
72
  return { outcomes, applied };
72
73
  }
73
- const LEDGER_SQL = "INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, files) " +
74
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
74
+ const LEDGER_SQL = "INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, cheap_usage, files) " +
75
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
75
76
  function writeLedger(stmt, entry) {
76
- stmt.run(entry.runId ?? null, new Date().toISOString(), entry.sessionId ?? null, entry.workspaceId ?? null, entry.trigger, entry.decision ?? null, entry.skipReason ?? null, entry.status ?? null, entry.durationMs ?? null, entry.model ?? null, entry.usage === undefined ? null : JSON.stringify(entry.usage), entry.files === undefined ? null : JSON.stringify(entry.files));
77
+ stmt.run(entry.runId ?? null, new Date().toISOString(), entry.sessionId ?? null, entry.workspaceId ?? null, entry.trigger, entry.decision ?? null, entry.skipReason ?? null, entry.status ?? null, entry.durationMs ?? null, entry.model ?? null, entry.usage === undefined ? null : JSON.stringify(entry.usage), entry.cheapUsage === undefined ? null : JSON.stringify(entry.cheapUsage), entry.files === undefined ? null : JSON.stringify(entry.files));
77
78
  }
78
79
  /** Validate one proposal and land it; appends the audit payload on success. */
79
80
  function applyOne(db, ctx, proposal, stmts, files) {
package/lib/store.d.ts CHANGED
@@ -132,6 +132,8 @@ export interface LedgerEntry {
132
132
  durationMs?: number | undefined;
133
133
  model?: string | undefined;
134
134
  usage?: unknown;
135
+ /** Pre-gate call payload (extraction cheap gate); `undefined` when the run never reached it. */
136
+ cheapUsage?: unknown;
135
137
  /** Applied/attempted mutations: `{id, action, before?, after?, sourceSeqs}`; a parse-failed run instead carries the raw answer's head. */
136
138
  files?: unknown;
137
139
  }
package/lib/store.js CHANGED
@@ -70,6 +70,7 @@ CREATE TABLE IF NOT EXISTS extraction_log (
70
70
  trigger TEXT CHECK (trigger IN ('turn-debounce','compaction','close-drain','explicit','import','item-review')),
71
71
  decision TEXT, skip_reason TEXT,
72
72
  status TEXT, duration_ms INTEGER, model TEXT, usage TEXT,
73
+ cheap_usage TEXT,
73
74
  files TEXT, occurrences INTEGER NOT NULL DEFAULT 1
74
75
  );
75
76
  CREATE TABLE IF NOT EXISTS state (
@@ -300,6 +301,7 @@ export async function openMemoryStore(explicitPath) {
300
301
  database.exec(DDL);
301
302
  ensureLedgerOccurrences(database);
302
303
  ensureMemoriesCorrects(database);
304
+ ensureExtractionCheapUsage(database);
303
305
  const stmts = {
304
306
  get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
305
307
  list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
@@ -318,8 +320,8 @@ export async function openMemoryStore(explicitPath) {
318
320
  "WHERE status = 'active' AND workspace_id <> '' GROUP BY workspace_id ORDER BY ts DESC LIMIT 1"),
319
321
  recentSkill: database.prepare("SELECT workspace_id, MAX(ts) AS ts FROM skill_log " +
320
322
  "WHERE workspace_id IS NOT NULL AND workspace_id <> '' GROUP BY workspace_id ORDER BY ts DESC LIMIT 1"),
321
- ledger: database.prepare("INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, files) " +
322
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
323
+ ledger: database.prepare("INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, cheap_usage, files) " +
324
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
323
325
  latestSkip: database.prepare("SELECT rowid AS rid, skip_reason AS reason, status FROM extraction_log " +
324
326
  "WHERE workspace_id IS ? AND session_id IS ? ORDER BY ts DESC, rowid DESC LIMIT 1"),
325
327
  bumpSkip: database.prepare(`UPDATE extraction_log SET ts = ?, ${LEDGER_OCCURRENCES} = ${LEDGER_OCCURRENCES} + 1 WHERE rowid = ?`),
@@ -988,6 +990,18 @@ function ensureMemoriesCorrects(database) {
988
990
  }
989
991
  database.exec("ALTER TABLE memories ADD COLUMN corrects TEXT");
990
992
  }
993
+ /**
994
+ * Add the extraction ledger `cheap_usage` column to a store created before
995
+ * the cheap pre-gate existed. Same single-migration discipline as the
996
+ * columns above.
997
+ */
998
+ function ensureExtractionCheapUsage(database) {
999
+ const columns = database.prepare("SELECT name FROM pragma_table_info('extraction_log')").all();
1000
+ if (columns.some((column) => column.name === "cheap_usage")) {
1001
+ return;
1002
+ }
1003
+ database.exec("ALTER TABLE extraction_log ADD COLUMN cheap_usage TEXT");
1004
+ }
991
1005
  /** Parse the daily skip-counts map; corrupt payloads degrade to empty. */
992
1006
  function parseCounts(raw) {
993
1007
  if (raw === null) {
@@ -1012,6 +1026,6 @@ function parseCounts(raw) {
1012
1026
  }
1013
1027
  /** JSON-encode the optional ledger fields and insert the row (never throws past a warn). */
1014
1028
  function writeLedger(stmt, entry) {
1015
- stmt.run(entry.runId ?? null, new Date().toISOString(), entry.sessionId ?? null, entry.workspaceId ?? null, entry.trigger, entry.decision ?? null, entry.skipReason ?? null, entry.status ?? null, entry.durationMs ?? null, entry.model ?? null, entry.usage === undefined ? null : JSON.stringify(entry.usage), entry.files === undefined ? null : JSON.stringify(entry.files));
1029
+ stmt.run(entry.runId ?? null, new Date().toISOString(), entry.sessionId ?? null, entry.workspaceId ?? null, entry.trigger, entry.decision ?? null, entry.skipReason ?? null, entry.status ?? null, entry.durationMs ?? null, entry.model ?? null, entry.usage === undefined ? null : JSON.stringify(entry.usage), entry.cheapUsage === undefined ? null : JSON.stringify(entry.cheapUsage), entry.files === undefined ? null : JSON.stringify(entry.files));
1016
1030
  }
1017
1031
  //# sourceMappingURL=store.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.15.7",
3
+ "version": "0.15.8",
4
4
  "description": "ZCode-style workspace memory for DeepSeek Harness: one central SQLite store partitioned by workspace, index injection, and sole-writer gated writes.",
5
5
  "license": "MIT",
6
6
  "repository": {