dsh-continual-evolve 0.15.2 → 0.15.4
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 +3 -3
- package/README.zh.md +3 -3
- package/lib/extraction.d.ts +5 -2
- package/lib/extraction.js +90 -8
- package/lib/store.d.ts +7 -2
- package/lib/store.js +50 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -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 (
|
|
74
|
+
pnpm test # vitest (199 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
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -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(
|
|
74
|
+
pnpm test # vitest(199 例)
|
|
75
75
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
76
76
|
pnpm lint # oxlint src test client
|
|
77
77
|
pnpm check:pack # 发布产物一致性(prepack 也会跑)
|
package/lib/extraction.d.ts
CHANGED
|
@@ -34,8 +34,11 @@ export interface RunOptions {
|
|
|
34
34
|
incrementMaxChars?: number;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Run one extraction pass for a target. Never throws
|
|
38
|
-
* is
|
|
37
|
+
* Run one extraction pass for a target. Never throws unless the ledger
|
|
38
|
+
* itself is unwritable: every terminal path is either a landed application,
|
|
39
|
+
* a ledgered skip, or a ledgered failure — anything that escapes the
|
|
40
|
+
* per-stage handling inside lands a failed `unexpected-error` ledger row
|
|
41
|
+
* here, so no failure ends as a scheduler warn without an account trace.
|
|
39
42
|
*/
|
|
40
43
|
export declare function runExtraction(deps: ExtractionDeps, target: ExtractionTarget, opts: RunOptions): Promise<ExtractionResult>;
|
|
41
44
|
/** The event wiring surface (duck-typed cordis context). */
|
package/lib/extraction.js
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
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;
|
|
6
|
+
/** Head length of an unexpected error's message/stack trace kept in the ledger `files` column. */
|
|
7
|
+
const UNEXPECTED_ERROR_RAW_MAX = 2048;
|
|
8
|
+
/** Head length of an error message embedded in a ledger `skip_reason`. */
|
|
9
|
+
const ERROR_REASON_MAX = 300;
|
|
10
|
+
/** Split an unexpected throw into a ledger-sized reason head and file body. */
|
|
11
|
+
function errorText(error) {
|
|
12
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
13
|
+
const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : message;
|
|
14
|
+
return { message, detail: `${message}\n${stack}`.slice(0, UNEXPECTED_ERROR_RAW_MAX) };
|
|
15
|
+
}
|
|
4
16
|
/**
|
|
5
|
-
* Run one extraction pass for a target. Never throws
|
|
6
|
-
* is
|
|
17
|
+
* Run one extraction pass for a target. Never throws unless the ledger
|
|
18
|
+
* itself is unwritable: every terminal path is either a landed application,
|
|
19
|
+
* a ledgered skip, or a ledgered failure — anything that escapes the
|
|
20
|
+
* per-stage handling inside lands a failed `unexpected-error` ledger row
|
|
21
|
+
* here, so no failure ends as a scheduler warn without an account trace.
|
|
7
22
|
*/
|
|
8
23
|
export async function runExtraction(deps, target, opts) {
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
try {
|
|
26
|
+
return await runExtractionInner(deps, target, opts);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const { message, detail } = errorText(error);
|
|
30
|
+
const reason = `unexpected-error: ${message.slice(0, ERROR_REASON_MAX)}`;
|
|
31
|
+
try {
|
|
32
|
+
deps.store.logSkipped({
|
|
33
|
+
sessionId: target.sessionId,
|
|
34
|
+
workspaceId: target.workspaceId,
|
|
35
|
+
trigger: opts.trigger,
|
|
36
|
+
skipReason: reason,
|
|
37
|
+
status: "failed",
|
|
38
|
+
model: target.model,
|
|
39
|
+
durationMs: Date.now() - started,
|
|
40
|
+
files: detail,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (ledgerError) {
|
|
44
|
+
// The ledger itself is down — nothing left to write to. Chain
|
|
45
|
+
// both errors so the scheduler's last-resort row keeps the
|
|
46
|
+
// triggering failure, not just the ledger fault.
|
|
47
|
+
throw new Error(`extraction failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
48
|
+
}
|
|
49
|
+
return { status: "failed", skipReason: reason };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The pipeline body: one bounded increment of one session, one LLM call, one
|
|
54
|
+
* gated transactional application, one ledger row, one cursor.
|
|
55
|
+
*/
|
|
56
|
+
async function runExtractionInner(deps, target, opts) {
|
|
9
57
|
const { store } = deps;
|
|
10
58
|
const ledgerBase = {
|
|
11
59
|
sessionId: target.sessionId,
|
|
@@ -35,8 +83,14 @@ export async function runExtraction(deps, target, opts) {
|
|
|
35
83
|
all = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
36
84
|
}
|
|
37
85
|
catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
86
|
+
const { message, detail } = errorText(error);
|
|
87
|
+
try {
|
|
88
|
+
store.logSkipped({ ...ledgerBase, skipReason: "surface-read-failed", status: "failed", files: detail });
|
|
89
|
+
}
|
|
90
|
+
catch (ledgerError) {
|
|
91
|
+
throw new Error(`surface read failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
92
|
+
}
|
|
93
|
+
return { status: "failed", skipReason: `surface read failed: ${message}` };
|
|
40
94
|
}
|
|
41
95
|
const events = eventsAfterCursor(all, cursor === undefined ? undefined : `seq:${cursor}`);
|
|
42
96
|
const slice = evaluateSlice({ events, internalAgent: false });
|
|
@@ -70,13 +124,21 @@ export async function runExtraction(deps, target, opts) {
|
|
|
70
124
|
usage = answer.usage;
|
|
71
125
|
}
|
|
72
126
|
catch (error) {
|
|
73
|
-
|
|
74
|
-
|
|
127
|
+
const { message, detail } = errorText(error);
|
|
128
|
+
try {
|
|
129
|
+
store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started, files: detail });
|
|
130
|
+
}
|
|
131
|
+
catch (ledgerError) {
|
|
132
|
+
throw new Error(`llm call failed (${message}); failure ledger write also failed (${errorText(ledgerError).message})`);
|
|
133
|
+
}
|
|
134
|
+
return { status: "failed", skipReason: `llm call failed: ${message}` };
|
|
75
135
|
}
|
|
76
136
|
const parsed = parseExtractionAnswer(text);
|
|
77
137
|
if (parsed.decision === "error") {
|
|
78
138
|
// A malformed answer is a failed run: the increment stays unconsumed
|
|
79
|
-
// and the next trigger retries it.
|
|
139
|
+
// and the next trigger retries it. The answer's head rides in the
|
|
140
|
+
// ledger row — the stream is gone after this, so without it the
|
|
141
|
+
// failure is undiagnosable after the fact.
|
|
80
142
|
store.logSkipped({
|
|
81
143
|
...ledgerBase,
|
|
82
144
|
skipReason: parsed.reason,
|
|
@@ -84,6 +146,7 @@ export async function runExtraction(deps, target, opts) {
|
|
|
84
146
|
model: target.model,
|
|
85
147
|
usage,
|
|
86
148
|
durationMs: Date.now() - started,
|
|
149
|
+
files: text.slice(0, FAILURE_RAW_MAX),
|
|
87
150
|
});
|
|
88
151
|
return { status: "failed", skipReason: parsed.reason };
|
|
89
152
|
}
|
|
@@ -208,7 +271,26 @@ export function createExtractionScheduler(host, deps) {
|
|
|
208
271
|
provider: target.provider,
|
|
209
272
|
model: target.model,
|
|
210
273
|
}, { trigger }).catch((error) => {
|
|
211
|
-
|
|
274
|
+
// Last resort: runExtraction ledgers its own unexpected failures,
|
|
275
|
+
// so this fires only when even that failed (the ledger write
|
|
276
|
+
// threw) — retry one ledger row here before the warn, so the
|
|
277
|
+
// failure still has an account trace.
|
|
278
|
+
const { message, detail } = errorText(error);
|
|
279
|
+
try {
|
|
280
|
+
deps.store.logSkipped({
|
|
281
|
+
sessionId: target.sessionId,
|
|
282
|
+
workspaceId: cwdOf(target.agent),
|
|
283
|
+
trigger,
|
|
284
|
+
skipReason: `unexpected-error: ${message.slice(0, ERROR_REASON_MAX)}`,
|
|
285
|
+
status: "failed",
|
|
286
|
+
model: target.model,
|
|
287
|
+
files: detail,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// The ledger is down — the warn below is the last resort.
|
|
292
|
+
}
|
|
293
|
+
host.logger("continual-evolve").warn(`extraction run failed unexpectedly: ${message}`);
|
|
212
294
|
}).finally(() => {
|
|
213
295
|
slot.running = false;
|
|
214
296
|
if (slot.pending !== null) {
|
package/lib/store.d.ts
CHANGED
|
@@ -30,7 +30,12 @@ export interface MemoryRecord {
|
|
|
30
30
|
createdAt: string;
|
|
31
31
|
updatedAt: string;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
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.
|
|
3
|
+
"version": "0.15.4",
|
|
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": {
|