dsh-engram 0.6.0 → 0.7.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 +89 -27
- package/README.zh.md +71 -23
- package/docs/PROPOSAL-evolution.zh.md +25 -1
- package/docs/RELEASE-NOTES-v0.7.0.md +57 -0
- package/lib/anchor.js +124 -0
- package/lib/client.js +4 -0
- package/lib/entity-page.js +200 -0
- package/lib/episode.js +163 -0
- package/lib/index-block.js +74 -4
- package/lib/index.js +82 -1
- package/lib/settings.js +6 -0
- package/lib/store.js +48 -5
- package/lib/tools.js +25 -3
- package/package.json +1 -1
package/lib/store.js
CHANGED
|
@@ -22,6 +22,7 @@ import path from "node:path";
|
|
|
22
22
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
23
23
|
import { hashText, byRecency, uuid, now, truncate as truncateText, bm25Rank, tokenize as tokenizeLocal, dedupeBySession } from "./util.js";
|
|
24
24
|
import { deriveObservations } from "./obs.js";
|
|
25
|
+
import { makeFileAnchor } from "./anchor.js";
|
|
25
26
|
import { redactText } from "./redact.js";
|
|
26
27
|
|
|
27
28
|
export const DOMAIN_VERSION = 1;
|
|
@@ -36,8 +37,21 @@ const MEMORY_KINDS = [
|
|
|
36
37
|
"insight",
|
|
37
38
|
"handoff",
|
|
38
39
|
"task",
|
|
40
|
+
"episode",
|
|
39
41
|
];
|
|
40
42
|
|
|
43
|
+
const AnchorSchema = z.object({
|
|
44
|
+
/** Absolute file path the claim was written against. */
|
|
45
|
+
path: z.string().min(1),
|
|
46
|
+
/** Stat identity at write time — cheap revalidation (no read). */
|
|
47
|
+
mtimeMs: z.number().nonnegative(),
|
|
48
|
+
size: z.number().int().nonnegative(),
|
|
49
|
+
/** sha256[0..32] for files under the hash cap; null for large files. */
|
|
50
|
+
hash: z.string().nullable().default(null),
|
|
51
|
+
/** When the anchor was captured (epoch ms). */
|
|
52
|
+
at: z.number().int().nonnegative(),
|
|
53
|
+
});
|
|
54
|
+
|
|
41
55
|
const MemorySchema = z.object({
|
|
42
56
|
/** Stable id (table key). */
|
|
43
57
|
id: z.string().min(1),
|
|
@@ -61,6 +75,8 @@ const MemorySchema = z.object({
|
|
|
61
75
|
lastAccessAt: z.number().int().nonnegative().nullable().default(null),
|
|
62
76
|
/** Optional file anchor (absolute path). Sharpens error revival and recall display. */
|
|
63
77
|
filePath: z.string().nullable().default(null),
|
|
78
|
+
/** Revalidatable stat/hash identity of `filePath` at write time (v0.7-C). */
|
|
79
|
+
anchor: AnchorSchema.nullable().default(null),
|
|
64
80
|
/** 0..1 static signal; index threshold is minIndexSignal. */
|
|
65
81
|
signal: z.number().min(0).max(1).default(0.5),
|
|
66
82
|
status: z.enum(["active", "archived"]).default("active"),
|
|
@@ -448,6 +464,12 @@ export async function openEngramDomain(facility) {
|
|
|
448
464
|
if ([...text].length > config.maxMemoryChars) {
|
|
449
465
|
throw new EngramError("ENGRAM_CAP_EXCEEDED", `memory text is ${[...text].length} characters, over the cap of ${config.maxMemoryChars}`);
|
|
450
466
|
}
|
|
467
|
+
const kind = entry.kind ?? "fact";
|
|
468
|
+
const anchor = entry.anchor !== void 0
|
|
469
|
+
? entry.anchor
|
|
470
|
+
: (typeof entry.filePath === "string" && entry.filePath.length > 0
|
|
471
|
+
? makeFileAnchor(entry.filePath, { baseDir: entry.workspace, now: now() })
|
|
472
|
+
: null);
|
|
451
473
|
const all = allMemories(entry.workspace);
|
|
452
474
|
let supersedes = validateRelationTarget(entry.workspace, entry.supersedes ?? null, "supersedes", all);
|
|
453
475
|
const contradicts = validateRelationTarget(entry.workspace, entry.contradicts ?? null, "contradicts", all);
|
|
@@ -456,7 +478,12 @@ export async function openEngramDomain(facility) {
|
|
|
456
478
|
// Repeat of the exact same failure is itself a signal: refresh recency
|
|
457
479
|
// AND climb the hit counter (errors only) so the [ENGRAM] index can
|
|
458
480
|
// eventually surface it via promoteHits.
|
|
459
|
-
const next = {
|
|
481
|
+
const next = {
|
|
482
|
+
...dup,
|
|
483
|
+
updatedAt: now(),
|
|
484
|
+
...(dup.kind === "error" ? { hits: (dup.hits ?? 0) + 1 } : {}),
|
|
485
|
+
...(anchor !== null ? { anchor, filePath: dup.filePath ?? entry.filePath ?? null } : {}),
|
|
486
|
+
};
|
|
460
487
|
await memories.put(dup.id, next);
|
|
461
488
|
return { stored: next, duplicated: true, id: dup.id, revived: dup.kind === "error" };
|
|
462
489
|
}
|
|
@@ -472,7 +499,13 @@ export async function openEngramDomain(facility) {
|
|
|
472
499
|
prev.filePath !== null && entry.filePath !== null && prev.filePath === entry.filePath;
|
|
473
500
|
if (!samePath && ratio < REPEAT_OVERLAP) continue;
|
|
474
501
|
if (samePath && ratio < SAME_PATH_OVERLAP) continue;
|
|
475
|
-
const revived = {
|
|
502
|
+
const revived = {
|
|
503
|
+
...prev,
|
|
504
|
+
updatedAt: now(),
|
|
505
|
+
hits: (prev.hits ?? 0) + 1,
|
|
506
|
+
filePath: prev.filePath ?? entry.filePath ?? null,
|
|
507
|
+
...(anchor !== null ? { anchor } : {}),
|
|
508
|
+
};
|
|
476
509
|
await memories.put(prev.id, revived);
|
|
477
510
|
return { stored: revived, duplicated: true, id: prev.id, revived: true };
|
|
478
511
|
}
|
|
@@ -487,16 +520,25 @@ export async function openEngramDomain(facility) {
|
|
|
487
520
|
const auto = autoSupersedeCandidate(all, entry, text);
|
|
488
521
|
if (auto !== null) supersedes = auto.id;
|
|
489
522
|
}
|
|
523
|
+
// Layered TTL (v0.7-C): user rules and proven procedures are permanent;
|
|
524
|
+
// session episodes are short-lived; errors age out faster than facts.
|
|
525
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
526
|
+
const ttlDays = (() => {
|
|
527
|
+
if (kind === "preference" || kind === "procedure") return 0;
|
|
528
|
+
if (kind === "episode") return config.episodeTtlDays ?? 14;
|
|
529
|
+
if (kind === "error") return config.errorTtlDays ?? 60;
|
|
530
|
+
return config.expireDays ?? 180;
|
|
531
|
+
})();
|
|
490
532
|
const expiresAt =
|
|
491
533
|
entry.expiresAt !== void 0 && entry.expiresAt !== null
|
|
492
534
|
? entry.expiresAt
|
|
493
|
-
:
|
|
494
|
-
? now() +
|
|
535
|
+
: ttlDays > 0
|
|
536
|
+
? now() + ttlDays * DAY_MS
|
|
495
537
|
: null;
|
|
496
538
|
const stored = {
|
|
497
539
|
id: uuid(),
|
|
498
540
|
workspace: entry.workspace,
|
|
499
|
-
kind
|
|
541
|
+
kind,
|
|
500
542
|
text,
|
|
501
543
|
tags: entry.tags ?? [],
|
|
502
544
|
entity: entry.entity ?? null,
|
|
@@ -507,6 +549,7 @@ export async function openEngramDomain(facility) {
|
|
|
507
549
|
hits: 0,
|
|
508
550
|
lastAccessAt: null,
|
|
509
551
|
filePath: entry.filePath ?? null,
|
|
552
|
+
anchor: anchor ?? null,
|
|
510
553
|
signal: typeof entry.signal === "number" ? Math.max(0, Math.min(1, entry.signal)) : 0.5,
|
|
511
554
|
status: "active",
|
|
512
555
|
supersedes,
|
package/lib/tools.js
CHANGED
|
@@ -19,6 +19,7 @@ import { z } from "zod";
|
|
|
19
19
|
import { basename } from "node:path";
|
|
20
20
|
import { EngramError, hasCode } from "./store.js";
|
|
21
21
|
import { buildEsrStatusView } from "./index-block.js";
|
|
22
|
+
import { buildEntityPage, renderEntityPage } from "./entity-page.js";
|
|
22
23
|
import { truncate, shortId, slugId, fmtDate, escapeLt, workspaceKey as wk, uuid } from "./util.js";
|
|
23
24
|
import { hasCJK, searchSessionsCJK } from "./session-cjk.js";
|
|
24
25
|
|
|
@@ -285,19 +286,40 @@ export function registerTools(ctx, service) {
|
|
|
285
286
|
// ── engram_detail ───────────────────────────────────────────────────
|
|
286
287
|
registerTool({
|
|
287
288
|
name: "engram_detail",
|
|
288
|
-
description: "Full record of one memory id
|
|
289
|
+
description: "Full record of one memory id, or an entity page (truth/timeline/open loops/conflicts/relations) when `entity` is given.",
|
|
289
290
|
parameters: {
|
|
290
291
|
type: "object",
|
|
291
292
|
properties: {
|
|
292
293
|
id: { type: "string", description: "Memory id (e.g. the #xxxxx marker in the [ENGRAM] index is the first 8 chars)" },
|
|
294
|
+
entity: { type: "string", description: "Entity id or name — returns the full page (truth / timeline / open loops / conflicts / relations)" },
|
|
293
295
|
},
|
|
294
|
-
required: ["id"],
|
|
295
296
|
},
|
|
296
297
|
output: TEXT_OUTPUT,
|
|
297
298
|
execute: async (raw, exec) => {
|
|
298
|
-
const args = parse(
|
|
299
|
+
const args = parse(
|
|
300
|
+
z.object({ id: z.string().optional(), entity: z.string().optional() }),
|
|
301
|
+
raw,
|
|
302
|
+
"engram_detail",
|
|
303
|
+
);
|
|
299
304
|
const caller = callerOf(exec);
|
|
300
305
|
const domain = await requireDomain(service);
|
|
306
|
+
if (typeof args.entity === "string" && args.entity.trim().length > 0) {
|
|
307
|
+
const page = buildEntityPage(domain, caller.cwd, args.entity);
|
|
308
|
+
const registered = (domain.listEntities?.(caller.cwd) ?? []).some(
|
|
309
|
+
(entity) => entity.id === page?.entity?.id || entity.name === page?.entity?.name,
|
|
310
|
+
);
|
|
311
|
+
if (page === null || (!registered && page.counts.memories === 0 && page.counts.openTasks === 0)) {
|
|
312
|
+
const names = (domain.listEntities?.(caller.cwd) ?? []).slice(0, 8).map((entity) => entity.name);
|
|
313
|
+
throw new EngramError(
|
|
314
|
+
"ENGRAM_NOT_FOUND",
|
|
315
|
+
`no entity "${args.entity}" in this workspace${names.length > 0 ? ` (known: ${names.join(", ")})` : ""}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return renderEntityPage(page);
|
|
319
|
+
}
|
|
320
|
+
if (typeof args.id !== "string" || args.id.length === 0) {
|
|
321
|
+
throw new EngramError("ENGRAM_INVALID_ARGS", "engram_detail requires `id` or `entity`");
|
|
322
|
+
}
|
|
301
323
|
const id = resolveId(domain, caller.cwd, args.id);
|
|
302
324
|
const m = domain.getMemory(caller.cwd, id);
|
|
303
325
|
if (m === void 0) throw new EngramError("ENGRAM_NOT_FOUND", `no active memory "${args.id}" in this workspace`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engram",
|
|
3
3
|
"description": "Minimalist long-term memory for DeepSeek Harness, distilling symbolic-index + pi-esr ideas: zero-LLM auto-capture, a symbolic [ENGRAM] index with progressive disclosure, and an ESR-lite evidence-closure protocol (esr_task / esr_node / esr_close / esr_link). Real agent-behaviour telemetry (usage rollup + /stats) and a deterministic offline recall benchmark (npm run eval). Hot path is model-free; storage on ctx.storageDomain; web memory viewer, observability panel and config card through DSH's native settings slots.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"packageManager": "pnpm@11.22.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|