dsh-continual-evolve 0.15.2 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.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-190%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-194%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-97%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-91%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
@@ -22,7 +22,7 @@ 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. **Two 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) 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. On hosts without the tools service the plugin degrades to "tell the user" phrasing and stays functional.
25
- 3. **Proposal-based extraction** — the only background 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.
25
+ 3. **Proposal-based extraction** — the only background 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
26
  4. **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. A hygiene pass rides on every applied run: orphan FTS rows are dropped and secret-bearing rows are quarantined (hidden, never deleted). The read-only card surfaces quarantine anomalies.
27
27
  5. **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), and patrol warnings. 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.
28
28
 
@@ -71,7 +71,7 @@ Profile patch example:
71
71
 
72
72
  ```bash
73
73
  pnpm install && pnpm build # deps + clean build -> lib/
74
- pnpm test # vitest (190 tests)
74
+ pnpm test # vitest (194 tests)
75
75
  pnpm test:coverage # v8 coverage, CI-enforced thresholds
76
76
  pnpm lint # oxlint src test client
77
77
  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-190%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-194%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-97%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-91%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-100%25-brightgreen)]()
@@ -22,7 +22,7 @@ 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 正文上限)后在一个事务内落盘。`memory_read` 按 id 取正文、按关键词检索——空格分隔的关键词取 AND,短词与 trigram 未命中时回退字面子串匹配;注入索引只有钩子。宿主没有工具服务时降级为"告诉用户"措辞,注入不受影响。
25
- 3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账。
25
+ 3. **提案制提取** —— 插件唯一的后台自动化,全程 `extraction_log` 可对账:每个 `agent/turn-stopping` 调度**该会话自己**的边界,状态按会话隔离(运行中只保留最新边界——合并而非排队;`compaction/start` 与会话收尾触发 drain),**没有空闲计时器**。一次运行读取该会话游标之后的轨迹增量,用 FTS 取相似既有记忆喂给一次 LLM 调用,模型的结构化提案过同一套门禁落盘;失败与被拒不推游标,下次触发重试。内部 agent、空增量、无真实用户文本的轮次、当轮已有显式 `memory_write` 的轮次机械跳过——每次跳过都记账,同一会话连续的空转轮次合成一行并累计唤醒次数。
26
26
  4. **回滚靠账本,不靠机制** —— 每次变更落一条带 before/after 全文快照的 `extraction_log` 行;undo 就是取 before 重写。每次应用后随跑一次巡检:孤儿 FTS 行清除、含密钥条目隔离(隐藏,不物理删除)。只读卡片显形巡检异常。
27
27
  5. **只读卡片** —— 挂在官方插件管理的本 bundle 页面(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡,内容对齐 ZCode 设置的记忆选项卡——下拉作用域选择器列出库里的工作区分区(默认选中宿主当前工作区)、搜索框、每条记忆的相对更新时间、点击行内展开正文预览(5 MiB 上限)、巡检异常警告。显示名与描述走宿主 `locale/*.json` 通道双语;宿主主题 token、`Button` 原语、中英文案跟随 DSH 语言。卡片是数据库的请求时投影,没有编辑路径。
28
28
 
@@ -71,7 +71,7 @@ profile patch 示例:
71
71
 
72
72
  ```bash
73
73
  pnpm install && pnpm build # 依赖 + 干净构建 -> lib/
74
- pnpm test # vitest(190 例)
74
+ pnpm test # vitest(194 例)
75
75
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
76
76
  pnpm lint # oxlint src test client
77
77
  pnpm check:pack # 发布产物一致性(prepack 也会跑)
package/lib/extraction.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { boundaryCursorOf, evaluateSlice, eventsAfterCursor, isInternalAgent, seqOfCursor, serializeIncrement } from "./extraction-surface.js";
2
2
  import { EXTRACTION_SYSTEM_PROMPT, extractionPrompt, manifestLineOf, parseExtractionAnswer } from "./extraction-prompt.js";
3
3
  import { cwdOf } from "./memory-section.js";
4
+ /** Head length of a failed run's raw answer kept in the ledger `files` column. */
5
+ const FAILURE_RAW_MAX = 2048;
4
6
  /**
5
7
  * Run one extraction pass for a target. Never throws: every terminal path
6
8
  * is either a landed application, a ledgered skip, or a ledgered failure.
@@ -76,7 +78,9 @@ export async function runExtraction(deps, target, opts) {
76
78
  const parsed = parseExtractionAnswer(text);
77
79
  if (parsed.decision === "error") {
78
80
  // A malformed answer is a failed run: the increment stays unconsumed
79
- // and the next trigger retries it.
81
+ // and the next trigger retries it. The answer's head rides in the
82
+ // ledger row — the stream is gone after this, so without it the
83
+ // failure is undiagnosable after the fact.
80
84
  store.logSkipped({
81
85
  ...ledgerBase,
82
86
  skipReason: parsed.reason,
@@ -84,6 +88,7 @@ export async function runExtraction(deps, target, opts) {
84
88
  model: target.model,
85
89
  usage,
86
90
  durationMs: Date.now() - started,
91
+ files: text.slice(0, FAILURE_RAW_MAX),
87
92
  });
88
93
  return { status: "failed", skipReason: parsed.reason };
89
94
  }
package/lib/store.d.ts CHANGED
@@ -30,7 +30,12 @@ export interface MemoryRecord {
30
30
  createdAt: string;
31
31
  updatedAt: string;
32
32
  }
33
- /** One ledger row (audit + rollback via embedded before/after snapshots). */
33
+ /**
34
+ * One ledger row (audit + rollback via embedded before/after snapshots).
35
+ * Consecutive mechanical skips of the same kind share one row, whose
36
+ * `occurrences` column counts the wake-ups it stands for (`1` for everything
37
+ * else); the interface never sets it — the store owns that column.
38
+ */
34
39
  export interface LedgerEntry {
35
40
  runId?: string | undefined;
36
41
  sessionId?: string | undefined;
@@ -42,7 +47,7 @@ export interface LedgerEntry {
42
47
  durationMs?: number | undefined;
43
48
  model?: string | undefined;
44
49
  usage?: unknown;
45
- /** Applied/attempted mutations: `{id, action, before?, after?, sourceSeqs}`. */
50
+ /** Applied/attempted mutations: `{id, action, before?, after?, sourceSeqs}`; a parse-failed run instead carries the raw answer's head. */
46
51
  files?: unknown;
47
52
  }
48
53
  /** Workspace rows the card's selector lists. */
package/lib/store.js CHANGED
@@ -26,6 +26,15 @@ export const MD_IMPORT_STATE_KEY = "md-imported";
26
26
  export const GLOBAL_WORKSPACE = "";
27
27
  /** Preview size cap for the card's content endpoint (ZCode's viewer uses 5 MiB). */
28
28
  export const MEMORY_FILE_PREVIEW_LIMIT = 5 * 1024 * 1024;
29
+ /** Ledger column counting the wake-ups one coalesced row stands for. */
30
+ const LEDGER_OCCURRENCES = "occurrences";
31
+ /**
32
+ * Skips a busy-but-quiet session repeats verbatim, turn after turn: the
33
+ * ledger keeps one counted row instead of one row per wake-up. Fault-shaped
34
+ * skips (`no-route`, `no-surface`, `no-workspace`) and every failure stay one
35
+ * row per occurrence — a persistent fault deserves a timeline, not a counter.
36
+ */
37
+ const COALESCED_SKIP_REASONS = new Set(["no-user-prose", "no-new-events"]);
29
38
  const DDL = `
30
39
  PRAGMA journal_mode=WAL;
31
40
  PRAGMA busy_timeout=5000;CREATE TABLE IF NOT EXISTS memories (
@@ -58,7 +67,7 @@ CREATE TABLE IF NOT EXISTS extraction_log (
58
67
  trigger TEXT CHECK (trigger IN ('turn-debounce','compaction','close-drain','explicit','import')),
59
68
  decision TEXT, skip_reason TEXT,
60
69
  status TEXT, duration_ms INTEGER, model TEXT, usage TEXT,
61
- files TEXT
70
+ files TEXT, occurrences INTEGER NOT NULL DEFAULT 1
62
71
  );
63
72
  CREATE TABLE IF NOT EXISTS state (
64
73
  workspace_id TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL,
@@ -119,6 +128,7 @@ export async function openMemoryStore(explicitPath) {
119
128
  throw new StoreUnavailableError(`node:sqlite unavailable (Node ≥ 22.5 required) — memory store disabled: ${error instanceof Error ? error.message : String(error)}`);
120
129
  }
121
130
  database.exec(DDL);
131
+ ensureLedgerOccurrences(database);
122
132
  const stmts = {
123
133
  get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
124
134
  list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
@@ -137,6 +147,9 @@ export async function openMemoryStore(explicitPath) {
137
147
  "WHERE status = 'active' AND workspace_id <> '' GROUP BY workspace_id ORDER BY ts DESC LIMIT 1"),
138
148
  ledger: database.prepare("INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, files) " +
139
149
  "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
150
+ latestSkip: database.prepare("SELECT rowid AS rid, skip_reason AS reason, status FROM extraction_log " +
151
+ "WHERE workspace_id IS ? AND session_id IS ? ORDER BY ts DESC, rowid DESC LIMIT 1"),
152
+ bumpSkip: database.prepare(`UPDATE extraction_log SET ts = ?, ${LEDGER_OCCURRENCES} = ${LEDGER_OCCURRENCES} + 1 WHERE rowid = ?`),
140
153
  prune: database.prepare("DELETE FROM extraction_log WHERE ts < ?"),
141
154
  ftsOrphans: database.prepare("SELECT f.rowid AS rid FROM memories_fts f LEFT JOIN memories m ON m.workspace_id = f.workspace_id AND m.id = f.id WHERE m.id IS NULL"),
142
155
  ftsDrop: database.prepare("DELETE FROM memories_fts WHERE rowid = ?"),
@@ -161,6 +174,27 @@ export async function openMemoryStore(explicitPath) {
161
174
  });
162
175
  return stmt.all(workspaceId, ...patterns, limit).map(recordOf);
163
176
  };
177
+ /**
178
+ * Fold a repeat of the previous row's mechanical skip into that row: the
179
+ * quiet turns of one session are one cursor position, so they are one
180
+ * ledger row carrying how many wake-ups it stands for. Only a
181
+ * *consecutive* repeat coalesces — anything in between (a model decision,
182
+ * an applied batch, a different reason, another session) starts a new row,
183
+ * which is exactly what "the same cursor position" means here. The bumped
184
+ * row takes the newest `ts`, so recency queries still see the session as
185
+ * active.
186
+ */
187
+ const coalesceSkip = (entry) => {
188
+ if (entry.status !== "skipped" || entry.skipReason === undefined || !COALESCED_SKIP_REASONS.has(entry.skipReason)) {
189
+ return false;
190
+ }
191
+ const latest = stmts.latestSkip.get(entry.workspaceId ?? null, entry.sessionId ?? null);
192
+ if (latest === undefined || latest.status !== "skipped" || latest.reason !== entry.skipReason) {
193
+ return false;
194
+ }
195
+ stmts.bumpSkip.run(new Date().toISOString(), latest.rid);
196
+ return true;
197
+ };
164
198
  const store = {
165
199
  path,
166
200
  get(workspaceId, id) {
@@ -190,6 +224,9 @@ export async function openMemoryStore(explicitPath) {
190
224
  return applyProposals({ database, now: () => new Date().toISOString() }, ctx, proposals).outcomes;
191
225
  },
192
226
  logSkipped(entry) {
227
+ if (coalesceSkip(entry)) {
228
+ return;
229
+ }
193
230
  writeLedger(stmts.ledger, entry);
194
231
  },
195
232
  cursor(workspaceId, sessionId) {
@@ -255,6 +292,18 @@ export async function openMemoryStore(explicitPath) {
255
292
  };
256
293
  return store;
257
294
  }
295
+ /**
296
+ * Add the ledger's `occurrences` column to a store created before coalescing
297
+ * existed. The DDL only covers fresh files, and the plugin is the sole writer
298
+ * of an existing user database — the one migration the ledger needs.
299
+ */
300
+ function ensureLedgerOccurrences(database) {
301
+ const columns = database.prepare("SELECT name FROM pragma_table_info('extraction_log')").all();
302
+ if (columns.some((column) => column.name === LEDGER_OCCURRENCES)) {
303
+ return;
304
+ }
305
+ database.exec(`ALTER TABLE extraction_log ADD COLUMN ${LEDGER_OCCURRENCES} INTEGER NOT NULL DEFAULT 1`);
306
+ }
258
307
  /** JSON-encode the optional ledger fields and insert the row (never throws past a warn). */
259
308
  function writeLedger(stmt, entry) {
260
309
  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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
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": {