dsh-continual-evolve 0.14.2 → 0.15.0
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 +33 -21
- package/README.zh.md +33 -21
- package/client/client.js +32 -43
- package/lib/card-routes.d.ts +3 -3
- package/lib/card-routes.js +38 -50
- package/lib/extraction-prompt.d.ts +52 -0
- package/lib/extraction-prompt.js +139 -0
- package/lib/extraction-surface.d.ts +66 -0
- package/lib/extraction-surface.js +186 -0
- package/lib/extraction.d.ts +60 -0
- package/lib/extraction.js +269 -0
- package/lib/import-md.d.ts +15 -0
- package/lib/import-md.js +140 -0
- package/lib/index.d.ts +63 -27
- package/lib/index.js +121 -65
- package/lib/memory-guide.d.ts +22 -14
- package/lib/memory-guide.js +33 -76
- package/lib/memory-rules.d.ts +56 -0
- package/lib/memory-rules.js +152 -0
- package/lib/memory-section.d.ts +26 -26
- package/lib/memory-section.js +48 -82
- package/lib/memory-snapshot.d.ts +43 -59
- package/lib/memory-snapshot.js +101 -141
- package/lib/memory-tools.d.ts +34 -0
- package/lib/memory-tools.js +142 -0
- package/lib/store-apply.d.ts +46 -0
- package/lib/store-apply.js +177 -0
- package/lib/store.d.ts +95 -0
- package/lib/store.js +207 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { fieldGateReasons, isValidMemoryId, slugifyId, } from "./memory-rules.js";
|
|
2
|
+
function recordOf(row) {
|
|
3
|
+
return {
|
|
4
|
+
workspaceId: row.workspace_id,
|
|
5
|
+
id: row.id,
|
|
6
|
+
type: row.type,
|
|
7
|
+
title: row.title,
|
|
8
|
+
description: row.description,
|
|
9
|
+
body: row.body,
|
|
10
|
+
status: row.status,
|
|
11
|
+
sourceSession: row.source_session,
|
|
12
|
+
sourceSeqs: row.source_seqs,
|
|
13
|
+
sourceRun: row.source_run,
|
|
14
|
+
createdAt: row.created_at,
|
|
15
|
+
updatedAt: row.updated_at,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const INSERT_SQL = "INSERT INTO memories (workspace_id, id, type, title, description, body, status, source_session, source_seqs, source_run, created_at, updated_at) " +
|
|
19
|
+
"VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)";
|
|
20
|
+
const UPDATE_SQL = "UPDATE memories SET type = ?, title = ?, description = ?, body = ?, updated_at = ? WHERE workspace_id = ? AND id = ?";
|
|
21
|
+
const DELETE_SQL = "DELETE FROM memories WHERE workspace_id = ? AND id = ?";
|
|
22
|
+
const FTS_DROP_SQL = "DELETE FROM memories_fts WHERE rowid IN (SELECT rowid FROM memories_fts WHERE workspace_id = ? AND id = ?)";
|
|
23
|
+
const FTS_INSERT_SQL = "INSERT INTO memories_fts (workspace_id, id, title, description, body) " +
|
|
24
|
+
"SELECT workspace_id, id, title, description, body FROM memories WHERE workspace_id = ? AND id = ?";
|
|
25
|
+
/** Re-sync the FTS row for one memory id from its table row (delete + insert). */
|
|
26
|
+
function syncFts(db, workspaceId, id) {
|
|
27
|
+
db.database.prepare(FTS_DROP_SQL).run(workspaceId, id);
|
|
28
|
+
db.database.prepare(FTS_INSERT_SQL).run(workspaceId, id);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate and apply the whole batch atomically. A batch with any invalid
|
|
32
|
+
* proposal mutates nothing and still lands a `rejected` ledger row; an
|
|
33
|
+
* applied batch lands an `applied` row with full before/after snapshots.
|
|
34
|
+
*/
|
|
35
|
+
export function applyProposals(db, ctx, proposals) {
|
|
36
|
+
const started = Date.now();
|
|
37
|
+
const outcomes = [];
|
|
38
|
+
const files = [];
|
|
39
|
+
const runId = ctx.runId ?? (ctx.trigger === "explicit" ? "explicit" : undefined);
|
|
40
|
+
const get = db.database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?");
|
|
41
|
+
const insert = db.database.prepare(INSERT_SQL);
|
|
42
|
+
const update = db.database.prepare(UPDATE_SQL);
|
|
43
|
+
const del = db.database.prepare(DELETE_SQL);
|
|
44
|
+
db.database.exec("BEGIN");
|
|
45
|
+
for (const proposal of proposals) {
|
|
46
|
+
const outcome = applyOne(db, ctx, proposal, { get, insert, update, del }, files);
|
|
47
|
+
outcomes.push(outcome);
|
|
48
|
+
if (!outcome.ok) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const applied = outcomes.every((outcome) => outcome.ok);
|
|
53
|
+
db.database.exec(applied ? "COMMIT" : "ROLLBACK");
|
|
54
|
+
const ledger = {
|
|
55
|
+
runId,
|
|
56
|
+
sessionId: ctx.sessionId,
|
|
57
|
+
workspaceId: ctx.workspaceId,
|
|
58
|
+
trigger: ctx.trigger,
|
|
59
|
+
decision: ctx.decision,
|
|
60
|
+
status: applied ? "applied" : "rejected",
|
|
61
|
+
durationMs: Date.now() - started,
|
|
62
|
+
model: ctx.model,
|
|
63
|
+
usage: ctx.usage,
|
|
64
|
+
files: applied ? files : [],
|
|
65
|
+
};
|
|
66
|
+
if (!applied) {
|
|
67
|
+
ledger.decision = `${ctx.decision ?? ""} first failure: ${outcomes.find((o) => !o.ok)?.reason ?? ""}`.trim();
|
|
68
|
+
}
|
|
69
|
+
writeLedger(db.database.prepare(LEDGER_SQL), ledger);
|
|
70
|
+
return { outcomes, applied };
|
|
71
|
+
}
|
|
72
|
+
const LEDGER_SQL = "INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, files) " +
|
|
73
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
|
74
|
+
function writeLedger(stmt, entry) {
|
|
75
|
+
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));
|
|
76
|
+
}
|
|
77
|
+
/** Validate one proposal and land it; appends the audit payload on success. */
|
|
78
|
+
function applyOne(db, ctx, proposal, stmts, files) {
|
|
79
|
+
const action = proposal.action;
|
|
80
|
+
if (action !== "create" && action !== "update" && action !== "delete") {
|
|
81
|
+
return fail(proposal, `unknown action ${String(action)}`);
|
|
82
|
+
}
|
|
83
|
+
const id = idFor(proposal);
|
|
84
|
+
if (!isValidMemoryId(id)) {
|
|
85
|
+
return fail(proposal, id.length === 0 ? "id missing and cannot be derived from the title" : `malformed id "${id}"`);
|
|
86
|
+
}
|
|
87
|
+
const existingRow = stmts.get.get(ctx.workspaceId, id);
|
|
88
|
+
const existing = existingRow === undefined ? undefined : recordOf(existingRow);
|
|
89
|
+
if (action === "delete") {
|
|
90
|
+
if (existing === undefined) {
|
|
91
|
+
return fail(proposal, `no memory "${id}" to delete`);
|
|
92
|
+
}
|
|
93
|
+
stmts.del.run(ctx.workspaceId, id);
|
|
94
|
+
syncFts(db, ctx.workspaceId, id);
|
|
95
|
+
files.push({ id, action, before: existing });
|
|
96
|
+
return { id, action, ok: true };
|
|
97
|
+
}
|
|
98
|
+
const fields = mergedFields(proposal, existing, action);
|
|
99
|
+
if ("reason" in fields) {
|
|
100
|
+
return fail(proposal, fields.reason);
|
|
101
|
+
}
|
|
102
|
+
const reasons = fieldGateReasons(fields.fields);
|
|
103
|
+
if (reasons.length > 0) {
|
|
104
|
+
return fail(proposal, reasons[0] ?? "invalid record");
|
|
105
|
+
}
|
|
106
|
+
if (action === "create") {
|
|
107
|
+
if (existing !== undefined) {
|
|
108
|
+
return fail(proposal, `memory "${id}" already exists — update it instead of creating a duplicate`);
|
|
109
|
+
}
|
|
110
|
+
const now = db.now();
|
|
111
|
+
stmts.insert.run(ctx.workspaceId, id, fields.fields.type, fields.fields.title, fields.fields.description, fields.fields.body, ctx.sessionId ?? null, proposal.sourceSeqs ?? null, ctx.runId ?? (ctx.trigger === "explicit" ? "explicit" : null), now, now);
|
|
112
|
+
syncFts(db, ctx.workspaceId, id);
|
|
113
|
+
files.push({
|
|
114
|
+
id,
|
|
115
|
+
action,
|
|
116
|
+
after: recordOf(stmts.get.get(ctx.workspaceId, id)),
|
|
117
|
+
sourceSeqs: proposal.sourceSeqs,
|
|
118
|
+
});
|
|
119
|
+
return { id, action, ok: true };
|
|
120
|
+
}
|
|
121
|
+
// action === "update"
|
|
122
|
+
if (existing === undefined) {
|
|
123
|
+
return fail(proposal, `no memory "${id}" to update — create it instead`);
|
|
124
|
+
}
|
|
125
|
+
stmts.update.run(fields.fields.type, fields.fields.title, fields.fields.description, fields.fields.body, db.now(), ctx.workspaceId, id);
|
|
126
|
+
syncFts(db, ctx.workspaceId, id);
|
|
127
|
+
files.push({ id, action, before: existing, after: recordOf(stmts.get.get(ctx.workspaceId, id)) });
|
|
128
|
+
return { id, action, ok: true };
|
|
129
|
+
}
|
|
130
|
+
/** Failure outcome, carrying the proposal id when one was readable. */
|
|
131
|
+
function fail(proposal, reason) {
|
|
132
|
+
return { id: proposal.id ?? "", action: proposal.action, ok: false, reason };
|
|
133
|
+
}
|
|
134
|
+
/** The proposal id, derived from the title (slug, CJK-hash fallback) on create. */
|
|
135
|
+
function idFor(proposal) {
|
|
136
|
+
if (proposal.id !== undefined && proposal.id.trim().length > 0) {
|
|
137
|
+
return proposal.id.trim();
|
|
138
|
+
}
|
|
139
|
+
if (proposal.action === "create" && typeof proposal.title === "string") {
|
|
140
|
+
return slugifyId(proposal.title);
|
|
141
|
+
}
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The merged field values a create/update would write. Create needs all
|
|
146
|
+
* fields supplied; update merges over the stored record. A missing-required
|
|
147
|
+
* result carries a `reason` instead.
|
|
148
|
+
*/
|
|
149
|
+
function mergedFields(proposal, existing, action) {
|
|
150
|
+
const pick = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
151
|
+
if (action === "create") {
|
|
152
|
+
if (typeof proposal.type !== "string" || typeof proposal.title !== "string" ||
|
|
153
|
+
typeof proposal.description !== "string" || typeof proposal.body !== "string") {
|
|
154
|
+
return { reason: "create requires id?, type, title, description, body" };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
fields: {
|
|
158
|
+
type: proposal.type,
|
|
159
|
+
title: proposal.title,
|
|
160
|
+
description: proposal.description,
|
|
161
|
+
body: proposal.body,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (existing === undefined) {
|
|
166
|
+
return { reason: "no memory to merge into" };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
fields: {
|
|
170
|
+
type: (typeof proposal.type === "string" ? proposal.type : existing.type),
|
|
171
|
+
title: pick(proposal.title, existing.title),
|
|
172
|
+
description: pick(proposal.description, existing.description),
|
|
173
|
+
body: pick(proposal.body, existing.body),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=store-apply.js.map
|
package/lib/store.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { MemoryProposal } from "./memory-rules.js";
|
|
2
|
+
import { type ProposalContext, type ProposalOutcome } from "./store-apply.js";
|
|
3
|
+
export type { MemoryProposal, ProposalContext, ProposalOutcome };
|
|
4
|
+
/** Thrown when the runtime lacks `node:sqlite` — the degrade signal. */
|
|
5
|
+
export declare class StoreUnavailableError extends Error {
|
|
6
|
+
}
|
|
7
|
+
/** State-table keys owned by the store (partitioned per workspace, `""` = global). */
|
|
8
|
+
export declare const CURSOR_KEY_PREFIX = "seq:";
|
|
9
|
+
export declare const PATROL_STATE_KEY = "patrol:last";
|
|
10
|
+
export declare const MD_IMPORT_STATE_KEY = "md-imported";
|
|
11
|
+
/** Global partition id for workspace-independent state rows. */
|
|
12
|
+
export declare const GLOBAL_WORKSPACE = "";
|
|
13
|
+
/** Preview size cap for the card's content endpoint (ZCode's viewer uses 5 MiB). */
|
|
14
|
+
export declare const MEMORY_FILE_PREVIEW_LIMIT: number;
|
|
15
|
+
/** One stored memory record (snake_case columns camelCased). */
|
|
16
|
+
export interface MemoryRecord {
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
id: string;
|
|
19
|
+
type: "user" | "feedback" | "reference";
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
body: string;
|
|
23
|
+
status: "active" | "quarantined" | "archived";
|
|
24
|
+
/** Origin session id, `null` for imports. */
|
|
25
|
+
sourceSession: string | null;
|
|
26
|
+
/** Trajectory seq range the fact came from ("820-831"), `null` when unknown. */
|
|
27
|
+
sourceSeqs: string | null;
|
|
28
|
+
/** The extraction run that wrote it; `'explicit'` for tool writes. */
|
|
29
|
+
sourceRun: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
}
|
|
33
|
+
/** One ledger row (audit + rollback via embedded before/after snapshots). */
|
|
34
|
+
export interface LedgerEntry {
|
|
35
|
+
runId?: string | undefined;
|
|
36
|
+
sessionId?: string | undefined;
|
|
37
|
+
workspaceId?: string | undefined;
|
|
38
|
+
trigger: "turn-debounce" | "compaction" | "close-drain" | "explicit" | "import";
|
|
39
|
+
decision?: string | undefined;
|
|
40
|
+
skipReason?: string | undefined;
|
|
41
|
+
status?: string | undefined;
|
|
42
|
+
durationMs?: number | undefined;
|
|
43
|
+
model?: string | undefined;
|
|
44
|
+
usage?: unknown;
|
|
45
|
+
/** Applied/attempted mutations: `{id, action, before?, after?, sourceSeqs}`. */
|
|
46
|
+
files?: unknown;
|
|
47
|
+
}
|
|
48
|
+
/** Workspace rows the card's selector lists. */
|
|
49
|
+
export interface WorkspaceSummary {
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
activeCount: number;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
}
|
|
54
|
+
/** What a patrol pass found and did. */
|
|
55
|
+
export interface PatrolResult {
|
|
56
|
+
ts: string;
|
|
57
|
+
/** Orphan FTS rows (no matching memory) removed. */
|
|
58
|
+
orphanFtsRows: number;
|
|
59
|
+
/** Records quarantined by the secret screen, with the redacted reasons. */
|
|
60
|
+
quarantined: {
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
id: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
}[];
|
|
65
|
+
}
|
|
66
|
+
/** The store surface every consumer (tool, extraction, card, injection) uses. */
|
|
67
|
+
export interface MemoryStore {
|
|
68
|
+
readonly path: string;
|
|
69
|
+
get(workspaceId: string, id: string): MemoryRecord | undefined;
|
|
70
|
+
/** Active records, injection order: feedback > user > reference, then newest. */
|
|
71
|
+
list(workspaceId: string): MemoryRecord[];
|
|
72
|
+
/** Every non-archived record regardless of status (the card's projection). */
|
|
73
|
+
listAll(workspaceId: string): MemoryRecord[];
|
|
74
|
+
/** FTS search (trigram, workspace-scoped); <3-char queries fall back to LIKE. */
|
|
75
|
+
search(workspaceId: string, query: string, limit?: number): MemoryRecord[];
|
|
76
|
+
/** The sole write gate: validate all proposals, apply atomically, ledger. */
|
|
77
|
+
applyProposals(ctx: ProposalContext, proposals: readonly MemoryProposal[]): ProposalOutcome[];
|
|
78
|
+
/** Ledger a run that produced no mutations (skips must be auditable too). */
|
|
79
|
+
logSkipped(entry: LedgerEntry): void;
|
|
80
|
+
cursor(workspaceId: string, sessionId: string): number | undefined;
|
|
81
|
+
setCursor(workspaceId: string, sessionId: string, seq: number): void;
|
|
82
|
+
state(workspaceId: string, key: string): string | undefined;
|
|
83
|
+
setState(workspaceId: string, key: string, value: string): void;
|
|
84
|
+
listWorkspaces(): WorkspaceSummary[];
|
|
85
|
+
/** Hygiene pass: orphan FTS cleanup + secret quarantine. */
|
|
86
|
+
patrol(): PatrolResult;
|
|
87
|
+
/** Ledger rows older than the cutoff, removed (the one unbounded table). */
|
|
88
|
+
pruneLedger(beforeIso: string): number;
|
|
89
|
+
close(): void;
|
|
90
|
+
}
|
|
91
|
+
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
92
|
+
export declare function defaultStorePath(): string;
|
|
93
|
+
/** Open (creating on first use) the central store. */
|
|
94
|
+
export declare function openMemoryStore(explicitPath?: string): Promise<MemoryStore>;
|
|
95
|
+
//# sourceMappingURL=store.d.ts.map
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The central SQLite memory store: one database file
|
|
3
|
+
* (`~/.dsh/evolve/memory.db`, rows partitioned by workspace path) holding
|
|
4
|
+
* every memory record, its FTS index, the extraction ledger, and per-session
|
|
5
|
+
* extraction cursors. The plugin's code is the literal sole writer — models
|
|
6
|
+
* propose, `applyProposals` gates and lands. See ADR
|
|
7
|
+
* `2026-10-06-sqlite-single-store`.
|
|
8
|
+
*
|
|
9
|
+
* Requires `node:sqlite` (Node ≥ 22.5; the desktop host's EnsureNode runtime
|
|
10
|
+
* guarantees v26). Unavailability is a typed error the entry point degrades
|
|
11
|
+
* on — injection and tooling switch off, the host keeps running.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { secretLeakReason } from "./memory-rules.js";
|
|
17
|
+
import { applyProposals } from "./store-apply.js";
|
|
18
|
+
/** Thrown when the runtime lacks `node:sqlite` — the degrade signal. */
|
|
19
|
+
export class StoreUnavailableError extends Error {
|
|
20
|
+
}
|
|
21
|
+
/** State-table keys owned by the store (partitioned per workspace, `""` = global). */
|
|
22
|
+
export const CURSOR_KEY_PREFIX = "seq:";
|
|
23
|
+
export const PATROL_STATE_KEY = "patrol:last";
|
|
24
|
+
export const MD_IMPORT_STATE_KEY = "md-imported";
|
|
25
|
+
/** Global partition id for workspace-independent state rows. */
|
|
26
|
+
export const GLOBAL_WORKSPACE = "";
|
|
27
|
+
/** Preview size cap for the card's content endpoint (ZCode's viewer uses 5 MiB). */
|
|
28
|
+
export const MEMORY_FILE_PREVIEW_LIMIT = 5 * 1024 * 1024;
|
|
29
|
+
const DDL = `
|
|
30
|
+
PRAGMA journal_mode=WAL;
|
|
31
|
+
PRAGMA busy_timeout=5000;CREATE TABLE IF NOT EXISTS memories (
|
|
32
|
+
workspace_id TEXT NOT NULL,
|
|
33
|
+
id TEXT NOT NULL,
|
|
34
|
+
type TEXT NOT NULL CHECK (type IN ('user','feedback','reference')),
|
|
35
|
+
title TEXT NOT NULL,
|
|
36
|
+
description TEXT NOT NULL,
|
|
37
|
+
body TEXT NOT NULL CHECK (length(body) <= 65536),
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
39
|
+
CHECK (status IN ('active','quarantined','archived')),
|
|
40
|
+
source_session TEXT,
|
|
41
|
+
source_seqs TEXT,
|
|
42
|
+
source_run TEXT,
|
|
43
|
+
created_at TEXT NOT NULL,
|
|
44
|
+
updated_at TEXT NOT NULL,
|
|
45
|
+
PRIMARY KEY (workspace_id, id)
|
|
46
|
+
) STRICT;
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_mem_ws_status ON memories(workspace_id, status);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_mem_ws_updated ON memories(workspace_id, updated_at);
|
|
49
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
50
|
+
workspace_id, id, title, description, body, tokenize='trigram');
|
|
51
|
+
-- FTS rows are synced in code by applyProposals (plain DELETE + INSERT), not
|
|
52
|
+
-- by triggers: node:sqlite rejects the FTS5 'delete'-command INSERT that
|
|
53
|
+
-- AFTER DELETE/UPDATE triggers would need (SQL logic error, verified on
|
|
54
|
+
-- SQLite 3.53.4). The sole-writer design makes code-side sync reliable;
|
|
55
|
+
-- patrol() reconciles any residue.
|
|
56
|
+
CREATE TABLE IF NOT EXISTS extraction_log (
|
|
57
|
+
run_id TEXT, ts TEXT NOT NULL, session_id TEXT, workspace_id TEXT,
|
|
58
|
+
trigger TEXT CHECK (trigger IN ('turn-debounce','compaction','close-drain','explicit','import')),
|
|
59
|
+
decision TEXT, skip_reason TEXT,
|
|
60
|
+
status TEXT, duration_ms INTEGER, model TEXT, usage TEXT,
|
|
61
|
+
files TEXT
|
|
62
|
+
);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS state (
|
|
64
|
+
workspace_id TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL,
|
|
65
|
+
PRIMARY KEY (workspace_id, key)
|
|
66
|
+
) STRICT;
|
|
67
|
+
`;
|
|
68
|
+
/** Default database location: `$DSH_HOME/evolve/memory.db`, else `~/.dsh/…`. */
|
|
69
|
+
export function defaultStorePath() {
|
|
70
|
+
const home = process.env.DSH_HOME?.trim();
|
|
71
|
+
return resolve(home && home.length > 0 ? home : join(homedir(), ".dsh"), "evolve", "memory.db");
|
|
72
|
+
}
|
|
73
|
+
function recordOf(row) {
|
|
74
|
+
return {
|
|
75
|
+
workspaceId: row.workspace_id,
|
|
76
|
+
id: row.id,
|
|
77
|
+
type: row.type,
|
|
78
|
+
title: row.title,
|
|
79
|
+
description: row.description,
|
|
80
|
+
body: row.body,
|
|
81
|
+
status: row.status,
|
|
82
|
+
sourceSession: row.source_session,
|
|
83
|
+
sourceSeqs: row.source_seqs,
|
|
84
|
+
sourceRun: row.source_run,
|
|
85
|
+
createdAt: row.created_at,
|
|
86
|
+
updatedAt: row.updated_at,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const LIST_ORDER = "ORDER BY CASE type WHEN 'feedback' THEN 0 WHEN 'user' THEN 1 ELSE 2 END, updated_at DESC";
|
|
90
|
+
/** Quote a user query as an FTS5 phrase (trigram needs ≥3 chars to match). */
|
|
91
|
+
function ftsPhrase(query) {
|
|
92
|
+
return `"${query.replaceAll('"', '""')}"`;
|
|
93
|
+
}
|
|
94
|
+
/** Open (creating on first use) the central store. */
|
|
95
|
+
export async function openMemoryStore(explicitPath) {
|
|
96
|
+
const path = explicitPath === undefined ? defaultStorePath() : resolve(explicitPath);
|
|
97
|
+
let database;
|
|
98
|
+
try {
|
|
99
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
100
|
+
// The parent directory must exist before SQLite opens the file — a
|
|
101
|
+
// fresh `~/.dsh` has no `evolve/` yet.
|
|
102
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
103
|
+
database = new DatabaseSync(path);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new StoreUnavailableError(`node:sqlite unavailable (Node ≥ 22.5 required) — memory store disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
database.exec(DDL);
|
|
109
|
+
const stmts = {
|
|
110
|
+
get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
|
|
111
|
+
list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
|
|
112
|
+
listAll: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status != 'archived' ${LIST_ORDER}`),
|
|
113
|
+
fts: database.prepare(`SELECT m.* FROM memories m WHERE (m.workspace_id, m.id) IN (
|
|
114
|
+
SELECT workspace_id, id FROM memories_fts WHERE memories_fts MATCH ? AND workspace_id = ?
|
|
115
|
+
) AND m.status = 'active' ${LIST_ORDER} LIMIT ?`),
|
|
116
|
+
like: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active'
|
|
117
|
+
AND (title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR body LIKE ? ESCAPE '\\') ${LIST_ORDER} LIMIT ?`),
|
|
118
|
+
stateGet: database.prepare("SELECT value FROM state WHERE workspace_id = ? AND key = ?"),
|
|
119
|
+
stateSet: database.prepare("INSERT INTO state (workspace_id, key, value) VALUES (?, ?, ?) " +
|
|
120
|
+
"ON CONFLICT (workspace_id, key) DO UPDATE SET value = excluded.value"),
|
|
121
|
+
cursorGet: database.prepare("SELECT CAST(value AS INTEGER) AS seq FROM state WHERE workspace_id = ? AND key = ?"),
|
|
122
|
+
workspaces: database.prepare("SELECT workspace_id, COUNT(*) AS n, MAX(updated_at) AS u FROM memories WHERE status = 'active' GROUP BY workspace_id ORDER BY u DESC"),
|
|
123
|
+
ledger: database.prepare("INSERT INTO extraction_log (run_id, ts, session_id, workspace_id, trigger, decision, skip_reason, status, duration_ms, model, usage, files) " +
|
|
124
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
125
|
+
prune: database.prepare("DELETE FROM extraction_log WHERE ts < ?"),
|
|
126
|
+
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"),
|
|
127
|
+
ftsDrop: database.prepare("DELETE FROM memories_fts WHERE rowid = ?"),
|
|
128
|
+
activeRows: database.prepare("SELECT * FROM memories WHERE status = 'active'"),
|
|
129
|
+
quarantine: database.prepare("UPDATE memories SET status = 'quarantined' WHERE workspace_id = ? AND id = ?"),
|
|
130
|
+
};
|
|
131
|
+
const store = {
|
|
132
|
+
path,
|
|
133
|
+
get(workspaceId, id) {
|
|
134
|
+
const row = stmts.get.get(workspaceId, id);
|
|
135
|
+
return row === undefined ? undefined : recordOf(row);
|
|
136
|
+
},
|
|
137
|
+
list(workspaceId) {
|
|
138
|
+
return stmts.list.all(workspaceId).map(recordOf);
|
|
139
|
+
},
|
|
140
|
+
listAll(workspaceId) {
|
|
141
|
+
return stmts.listAll.all(workspaceId).map(recordOf);
|
|
142
|
+
},
|
|
143
|
+
search(workspaceId, query, limit = 20) {
|
|
144
|
+
const trimmed = query.trim();
|
|
145
|
+
if (trimmed.length < 3) {
|
|
146
|
+
const like = `%${trimmed.replaceAll(/[\\%_]/g, "\\$&")}%`;
|
|
147
|
+
return stmts.like.all(workspaceId, like, like, like, limit).map(recordOf);
|
|
148
|
+
}
|
|
149
|
+
return stmts.fts.all(ftsPhrase(trimmed), workspaceId, limit).map(recordOf);
|
|
150
|
+
},
|
|
151
|
+
applyProposals(ctx, proposals) {
|
|
152
|
+
return applyProposals({ database, now: () => new Date().toISOString() }, ctx, proposals).outcomes;
|
|
153
|
+
},
|
|
154
|
+
logSkipped(entry) {
|
|
155
|
+
writeLedger(stmts.ledger, entry);
|
|
156
|
+
},
|
|
157
|
+
cursor(workspaceId, sessionId) {
|
|
158
|
+
const row = stmts.cursorGet.get(workspaceId, CURSOR_KEY_PREFIX + sessionId);
|
|
159
|
+
return row?.seq === null || row === undefined ? undefined : row.seq;
|
|
160
|
+
},
|
|
161
|
+
setCursor(workspaceId, sessionId, seq) {
|
|
162
|
+
stmts.stateSet.run(workspaceId, CURSOR_KEY_PREFIX + sessionId, String(seq));
|
|
163
|
+
},
|
|
164
|
+
state(workspaceId, key) {
|
|
165
|
+
const row = stmts.stateGet.get(workspaceId, key);
|
|
166
|
+
return row?.value;
|
|
167
|
+
},
|
|
168
|
+
setState(workspaceId, key, value) {
|
|
169
|
+
stmts.stateSet.run(workspaceId, key, value);
|
|
170
|
+
},
|
|
171
|
+
listWorkspaces() {
|
|
172
|
+
return stmts.workspaces.all().map((row) => ({
|
|
173
|
+
workspaceId: row.workspace_id,
|
|
174
|
+
activeCount: row.n,
|
|
175
|
+
updatedAt: row.u,
|
|
176
|
+
}));
|
|
177
|
+
},
|
|
178
|
+
patrol() {
|
|
179
|
+
const result = { ts: new Date().toISOString(), orphanFtsRows: 0, quarantined: [] };
|
|
180
|
+
for (const row of stmts.ftsOrphans.all()) {
|
|
181
|
+
stmts.ftsDrop.run(row.rid);
|
|
182
|
+
result.orphanFtsRows += 1;
|
|
183
|
+
}
|
|
184
|
+
for (const row of stmts.activeRows.all()) {
|
|
185
|
+
const reason = secretLeakReason(`${row.title}\n${row.description}\n${row.body}`);
|
|
186
|
+
if (reason !== undefined) {
|
|
187
|
+
stmts.quarantine.run(row.workspace_id, row.id);
|
|
188
|
+
result.quarantined.push({ workspaceId: row.workspace_id, id: row.id, reason });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
stmts.stateSet.run(GLOBAL_WORKSPACE, PATROL_STATE_KEY, JSON.stringify(result));
|
|
192
|
+
return result;
|
|
193
|
+
},
|
|
194
|
+
pruneLedger(beforeIso) {
|
|
195
|
+
return Number(stmts.prune.run(beforeIso).changes);
|
|
196
|
+
},
|
|
197
|
+
close() {
|
|
198
|
+
database.close();
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
return store;
|
|
202
|
+
}
|
|
203
|
+
/** JSON-encode the optional ledger fields and insert the row (never throws past a warn). */
|
|
204
|
+
function writeLedger(stmt, entry) {
|
|
205
|
+
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));
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=store.js.map
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Workspace Memory",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Central SQLite memory store partitioned by workspace: session-start index injection, gated explicit writes and proposal-based extraction."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ZCode-style workspace memory for DeepSeek Harness:
|
|
3
|
+
"version": "0.15.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|