recallnest 3.0.0 → 4.0.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/CHANGELOG.md +304 -0
- package/README.md +251 -258
- package/README_CN.md +226 -247
- package/assets/ui/app.js +6 -1
- package/bin/recallnest.mjs +20 -37
- package/docs/api-reference.md +8 -0
- package/docs/architecture.md +18 -13
- package/eval/cases-canary.json +107 -9
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
- package/integrations/examples/langchain/memory-chain.py +4 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
- package/package.json +12 -3
- package/src/activity-counter.ts +1 -0
- package/src/adaptive-retrieval.ts +23 -0
- package/src/api-server.ts +13 -11
- package/src/asset-sync.ts +26 -2
- package/src/audit-log.ts +2 -0
- package/src/auto-gc.ts +4 -3
- package/src/belief-history.ts +137 -31
- package/src/capture-engine.ts +459 -71
- package/src/cascade-forget.ts +7 -3
- package/src/checkpoint-mirror.ts +417 -0
- package/src/cli.ts +574 -54
- package/src/conflict-engine.ts +43 -15
- package/src/consolidation-engine.ts +24 -3
- package/src/context-collapse-renderer.ts +8 -1
- package/src/context-composer-stable.ts +5 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +136 -0
- package/src/flock.ts +97 -0
- package/src/forget-engine.ts +342 -58
- package/src/gateway-server.ts +4 -0
- package/src/ingest-plan.ts +56 -0
- package/src/ingest.ts +294 -121
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-extractor.ts +17 -0
- package/src/kg-store.ts +1 -1
- package/src/known-item-eval.ts +455 -0
- package/src/llm-client.ts +13 -26
- package/src/llm-consolidation.ts +2 -2
- package/src/mcp-tool-deps.ts +6 -0
- package/src/mcp-tools-advanced.ts +80 -45
- package/src/mcp-tools-core.ts +23 -17
- package/src/mcp-tools-governance.ts +13 -7
- package/src/memory-assets.ts +144 -8
- package/src/memory-boundaries.ts +19 -0
- package/src/memory-evolution.ts +9 -0
- package/src/memory-lint.ts +1 -1
- package/src/memory-output.ts +311 -14
- package/src/memory-reconcile.ts +1437 -0
- package/src/memory-schema.ts +11 -0
- package/src/memory-store-port.ts +1 -0
- package/src/minis-archive.ts +151 -0
- package/src/noise-filter.ts +15 -2
- package/src/pii-detector.ts +31 -1
- package/src/pivot-apply.ts +55 -5
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/preference-matcher.ts +40 -28
- package/src/prospective-memory.ts +7 -4
- package/src/retriever.ts +297 -57
- package/src/retroactive-boost.ts +3 -2
- package/src/runtime-config.ts +15 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +20 -1
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +59 -12
- package/src/skill-promotion.ts +67 -1
- package/src/store-receipts.ts +109 -0
- package/src/store-write-budget.ts +37 -0
- package/src/store.ts +298 -57
- package/src/text-fingerprint.ts +25 -0
- package/src/trigger-store.ts +443 -0
- package/src/ui-server.ts +22 -21
- package/src/version-manager.ts +19 -5
- package/src/write-verifier.ts +16 -11
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
package/src/flock.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel advisory file locks (flock(2)) reached through bun:ffi, for distill-lock.ts.
|
|
3
|
+
*
|
|
4
|
+
* Why the kernel: a flock belongs to an open file description, and the kernel drops it when the
|
|
5
|
+
* last descriptor closes — including when the holding process exits or is SIGKILLed. So "is the
|
|
6
|
+
* holder still alive" is answered by the kernel, and nobody ever has to decide that a lock is
|
|
7
|
+
* stale and take it away from its holder.
|
|
8
|
+
*
|
|
9
|
+
* Only `flock(int, int)` and the errno accessor are called; both are non-variadic, so the FFI
|
|
10
|
+
* signatures below match the C ABI on x86-64 and arm64 alike (fcntl/open are variadic and would
|
|
11
|
+
* not be safe to declare this way on Apple arm64, which passes variadic arguments on the stack).
|
|
12
|
+
*
|
|
13
|
+
* - Linux (glibc): `flock` and `__errno_location` in libc.so.6. LOCK_EX=2, LOCK_NB=4, LOCK_UN=8,
|
|
14
|
+
* EWOULDBLOCK=EAGAIN=11, EINTR=4, O_CLOEXEC=0o2000000.
|
|
15
|
+
* - macOS: `flock` and `__error` in libSystem.B.dylib (served from the dyld shared cache, so
|
|
16
|
+
* dlopen by its install name works without the file on disk). Same LOCK_* values
|
|
17
|
+
* (<sys/fcntl.h>), EWOULDBLOCK=EAGAIN=35, EINTR=4, O_CLOEXEC=0x01000000.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { dlopen, FFIType, read, type Pointer } from "bun:ffi";
|
|
21
|
+
import { constants, openSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
const LOCK_EX = 2;
|
|
24
|
+
const LOCK_NB = 4;
|
|
25
|
+
const LOCK_UN = 8;
|
|
26
|
+
const EINTR = 4;
|
|
27
|
+
|
|
28
|
+
interface Platform {
|
|
29
|
+
libPath: string;
|
|
30
|
+
errnoSymbol: "__errno_location" | "__error";
|
|
31
|
+
ewouldblock: number;
|
|
32
|
+
oCloexec: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function platform(): Platform {
|
|
36
|
+
if (process.platform === "linux") {
|
|
37
|
+
return { libPath: "libc.so.6", errnoSymbol: "__errno_location", ewouldblock: 11, oCloexec: 0o2000000 };
|
|
38
|
+
}
|
|
39
|
+
if (process.platform === "darwin") {
|
|
40
|
+
return { libPath: "/usr/lib/libSystem.B.dylib", errnoSymbol: "__error", ewouldblock: 35, oCloexec: 0x01000000 };
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`cross-process locks need flock(2); platform '${process.platform}' is not supported`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Flock {
|
|
46
|
+
flock: (fd: number, op: number) => number;
|
|
47
|
+
errno: () => number;
|
|
48
|
+
ewouldblock: number;
|
|
49
|
+
oCloexec: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let loaded: Flock | null = null;
|
|
53
|
+
|
|
54
|
+
function lib(): Flock {
|
|
55
|
+
if (loaded) return loaded;
|
|
56
|
+
const p = platform();
|
|
57
|
+
const handle = dlopen(p.libPath, {
|
|
58
|
+
flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
|
|
59
|
+
[p.errnoSymbol]: { args: [], returns: FFIType.ptr },
|
|
60
|
+
});
|
|
61
|
+
const symbols = handle.symbols as unknown as Record<string, (...args: number[]) => unknown>;
|
|
62
|
+
const flockFn = symbols.flock!;
|
|
63
|
+
const errnoFn = symbols[p.errnoSymbol]!;
|
|
64
|
+
loaded = {
|
|
65
|
+
flock: (fd, op) => flockFn(fd, op) as number,
|
|
66
|
+
errno: () => read.i32(errnoFn() as Pointer, 0),
|
|
67
|
+
ewouldblock: p.ewouldblock,
|
|
68
|
+
oCloexec: p.oCloexec,
|
|
69
|
+
};
|
|
70
|
+
return loaded;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Open (creating if needed) a lock file for reading and writing, close-on-exec so a child
|
|
75
|
+
* process can never inherit the descriptor and with it the lock.
|
|
76
|
+
*/
|
|
77
|
+
export function openLockFile(path: string): number {
|
|
78
|
+
return openSync(path, constants.O_RDWR | constants.O_CREAT | lib().oCloexec, 0o644);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Non-blocking exclusive flock. true = acquired, false = another open file description holds it. */
|
|
82
|
+
export function tryLockExclusive(fd: number): boolean {
|
|
83
|
+
const l = lib();
|
|
84
|
+
for (;;) {
|
|
85
|
+
if (l.flock(fd, LOCK_EX | LOCK_NB) === 0) return true;
|
|
86
|
+
// Read errno immediately: nothing runs between the FFI return and this call.
|
|
87
|
+
const errno = l.errno();
|
|
88
|
+
if (errno === l.ewouldblock) return false;
|
|
89
|
+
if (errno === EINTR) continue;
|
|
90
|
+
throw new Error(`flock(LOCK_EX|LOCK_NB) failed on fd ${fd}: errno ${errno}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Explicit unlock. Closing the descriptor also unlocks; this is for clarity before close. */
|
|
95
|
+
export function unlockFd(fd: number): void {
|
|
96
|
+
lib().flock(fd, LOCK_UN);
|
|
97
|
+
}
|
package/src/forget-engine.ts
CHANGED
|
@@ -1,19 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Forget Engine —
|
|
2
|
+
* Forget Engine — ethics-aware memory deletion, with a read-back.
|
|
3
3
|
*
|
|
4
|
-
* Philosophy: "The right to forget" —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Philosophy: "The right to forget" — a memory can be explicitly removed, the
|
|
5
|
+
* removal is auditable, and it reaches the places this engine knows the memory
|
|
6
|
+
* left a copy or a pointer in.
|
|
7
7
|
*
|
|
8
|
-
* Sequence
|
|
9
|
-
*
|
|
8
|
+
* Sequence (the step comments in forgetMemory() carry the same numbers and names;
|
|
9
|
+
* forget-cascade-completeness.test.ts fails if the two lists drift apart):
|
|
10
|
+
* 1. Fetch target
|
|
10
11
|
* 2. Privacy tier check (durable requires explicit confirm)
|
|
11
|
-
* 3. Evidence
|
|
12
|
-
* 4.
|
|
13
|
-
* 5. Pin archive (
|
|
14
|
-
* 6.
|
|
15
|
-
* 7.
|
|
16
|
-
* 8.
|
|
12
|
+
* 3. Evidence snapshot (returned to the caller, not persisted)
|
|
13
|
+
* 4. Trigger rows (delete its rows in memory_triggers; a failure aborts before anything is touched)
|
|
14
|
+
* 5. Pin archive (delete the indexed copy of each pin made from it, then move the pin files out of data/pins; a failure aborts, the primary row stays)
|
|
15
|
+
* 6. KG triples (KGStore.deleteBySource; a failure is logged and the forget continues)
|
|
16
|
+
* 7. Cascade demote (related memories lose importance, via cascade-forget.ts)
|
|
17
|
+
* 8. Evolution breadcrumb (mark the row archived with a "forgotten:" note just before deleting it)
|
|
18
|
+
* 9. Primary delete (remove the row from LanceDB)
|
|
19
|
+
* 10. Read-back (fetch the id again; count what is left in triggers, pins, pin index rows and KG)
|
|
20
|
+
* 11. Audit log (record the operation and what the read-back found)
|
|
21
|
+
*
|
|
22
|
+
* Steps 4 and 5 fail closed and run before the primary delete: once the row is
|
|
23
|
+
* gone, forget() can no longer find the id, so whatever they left behind would
|
|
24
|
+
* have no tool to remove it.
|
|
25
|
+
*
|
|
26
|
+
* When a forget stops part-way:
|
|
27
|
+
* - at step 4, nothing has been changed;
|
|
28
|
+
* - at step 5, the memory is untouched and still active, but its trigger rows, pin
|
|
29
|
+
* index rows or some pin files may already be gone;
|
|
30
|
+
* - at step 9 or 10 (the delete threw, or the row is still readable afterwards), the
|
|
31
|
+
* row stays in the table marked archived with the "forgotten:" note — default recall
|
|
32
|
+
* skips it and the memory-file reconciler treats its text as forgotten — while its
|
|
33
|
+
* triggers, pins and KG triples are gone. That is a soft forget; running forget
|
|
34
|
+
* again finishes it.
|
|
35
|
+
* Whenever something was already changed, the error says what, and a
|
|
36
|
+
* `forget_incomplete` audit entry records it. That entry deliberately carries no
|
|
37
|
+
* `norm=` fingerprint and is not a `forget` operation, so the reconciler does not
|
|
38
|
+
* read an aborted forget as a completed one.
|
|
39
|
+
*
|
|
40
|
+
* Not covered — forgetting a memory does NOT touch:
|
|
41
|
+
* - memories derived from it by dream / consolidation (insights, patterns), or
|
|
42
|
+
* evolution references held by other rows (supersedes, sourceMemories, consolidatedInto);
|
|
43
|
+
* - brief assets and exports that quoted it;
|
|
44
|
+
* - document slices or transcript chunks in other scopes that say the same thing;
|
|
45
|
+
* - the archived pin files themselves (data/archive/forgotten-pins keeps the snippet so a
|
|
46
|
+
* mistaken forget can be undone; remove the file by hand if the content has to go);
|
|
47
|
+
* - older LanceDB table versions, backups, and the session transcripts it came from.
|
|
48
|
+
* The audit entry of a completed forget carries the normalized text fingerprint so
|
|
49
|
+
* that the memory-file reconciler does not put the same text back (memory-reconcile.ts).
|
|
50
|
+
*
|
|
51
|
+
* Until 2026-10-05 this header promised propagation to "all derived artifacts
|
|
52
|
+
* (KG triples, pins, evolution chains)" while the body had no pin code at all, the
|
|
53
|
+
* memory_triggers side table (added 2026-09-22) was never cleaned, and nothing
|
|
54
|
+
* read back after the delete.
|
|
17
55
|
*/
|
|
18
56
|
|
|
19
57
|
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
@@ -22,6 +60,7 @@ import type { AuditLogger } from "./audit-log.js";
|
|
|
22
60
|
import { parsePrivacyTier, type PrivacyTier } from "./memory-schema.js";
|
|
23
61
|
import { parseEvolution, patchEvolution } from "./memory-evolution.js";
|
|
24
62
|
import { cascadeForget, type CascadeForgetConfig, DEFAULT_CASCADE_FORGET_CONFIG } from "./cascade-forget.js";
|
|
63
|
+
import { textFingerprint } from "./text-fingerprint.js";
|
|
25
64
|
|
|
26
65
|
// ---------------------------------------------------------------------------
|
|
27
66
|
// Types
|
|
@@ -51,6 +90,38 @@ export interface ForgetEvidence {
|
|
|
51
90
|
reason?: string;
|
|
52
91
|
}
|
|
53
92
|
|
|
93
|
+
/** The places the read-back looks. */
|
|
94
|
+
export type ForgetLayer = "primary" | "triggers" | "pins" | "pin-index" | "kg";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* What the read-back after the delete found.
|
|
98
|
+
* A count is `null` when that layer is not wired in, or when counting it failed; the
|
|
99
|
+
* second case is also listed in `unverified`, so "not there" and "could not look" stay apart.
|
|
100
|
+
*/
|
|
101
|
+
export interface ForgetVerification {
|
|
102
|
+
/** gone = the id no longer resolves; present = it still does; unverified = the read itself failed */
|
|
103
|
+
primary: "gone" | "present" | "unverified";
|
|
104
|
+
triggerRowsLeft: number | null;
|
|
105
|
+
pinsLeft: number | null;
|
|
106
|
+
pinIndexRowsLeft: number | null;
|
|
107
|
+
kgTriplesLeft: number | null;
|
|
108
|
+
/** Layers whose check threw. Empty when everything that could be checked was checked. */
|
|
109
|
+
unverified: ForgetLayer[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One word for a read-back. `clean` is the only state in which the forget can be
|
|
114
|
+
* reported as done without a caveat: the id is gone, nothing was found left over,
|
|
115
|
+
* and no check failed.
|
|
116
|
+
*/
|
|
117
|
+
export function summarizeVerification(v: ForgetVerification): "clean" | "leftovers" | "unverified" | "present" {
|
|
118
|
+
if (v.primary === "present") return "present";
|
|
119
|
+
const left = (v.triggerRowsLeft ?? 0) + (v.pinsLeft ?? 0) + (v.pinIndexRowsLeft ?? 0) + (v.kgTriplesLeft ?? 0);
|
|
120
|
+
if (left > 0) return "leftovers";
|
|
121
|
+
if (v.unverified.length > 0) return "unverified";
|
|
122
|
+
return "clean";
|
|
123
|
+
}
|
|
124
|
+
|
|
54
125
|
export interface ForgetResult {
|
|
55
126
|
/** Whether the memory was successfully forgotten */
|
|
56
127
|
success: boolean;
|
|
@@ -58,19 +129,163 @@ export interface ForgetResult {
|
|
|
58
129
|
memoryId: string;
|
|
59
130
|
/** Evidence snapshot (for audit/undo) */
|
|
60
131
|
evidence?: ForgetEvidence;
|
|
61
|
-
/**
|
|
132
|
+
/** Whether the KG cleanup ran without error */
|
|
62
133
|
kgTriplesRemoved: boolean;
|
|
134
|
+
/** Rows removed from memory_triggers (null when no trigger store is wired in) */
|
|
135
|
+
triggerRowsRemoved: number | null;
|
|
136
|
+
/** Pin files moved to the archive (null when no pin archive is wired in) */
|
|
137
|
+
pinsArchived: number | null;
|
|
138
|
+
/** Indexed copies of those pins removed from the main table (null when the step was not reached) */
|
|
139
|
+
pinIndexRowsRemoved: number | null;
|
|
63
140
|
/** Cascade demote results */
|
|
64
141
|
cascadeResult: { demotedCount: number; demotedIds: string[] };
|
|
142
|
+
/** Read-back after the primary delete (absent when the forget stopped before deleting) */
|
|
143
|
+
verification?: ForgetVerification;
|
|
65
144
|
/** Error message if failed */
|
|
66
145
|
error?: string;
|
|
67
146
|
}
|
|
68
147
|
|
|
148
|
+
/** The slice of TriggerStore the forget engine needs. */
|
|
149
|
+
export interface ForgetTriggerStore {
|
|
150
|
+
/**
|
|
151
|
+
* Delete every trigger row of this memory; resolves to the number of rows it counted just
|
|
152
|
+
* before deleting. A row that another process adds in between is not in that number, and
|
|
153
|
+
* when the number is 0 no delete is issued at all — the read-back (step 10) counts again.
|
|
154
|
+
*/
|
|
155
|
+
deleteForMemory(memoryId: string): Promise<number>;
|
|
156
|
+
countForMemory(memoryId: string): Promise<number>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Pin files made from a memory (memory-assets.ts: archivePinAssetsForMemory / findPinAssetsForMemory). */
|
|
160
|
+
export interface ForgetPinArchive {
|
|
161
|
+
/**
|
|
162
|
+
* Move this memory's pin files out of the pins directory; returns how many were moved.
|
|
163
|
+
* If it fails after some files were already moved, the error it throws carries that number
|
|
164
|
+
* as `archivedCount` — otherwise the engine would report that nothing had been changed.
|
|
165
|
+
*/
|
|
166
|
+
archiveForMemory(memoryId: string): number;
|
|
167
|
+
/** How many pin files made from this memory are still in the pins directory. */
|
|
168
|
+
countForMemory(memoryId: string): number;
|
|
169
|
+
}
|
|
170
|
+
|
|
69
171
|
export interface ForgetByIdDeps {
|
|
70
172
|
store: MemoryStore;
|
|
71
173
|
kgStore?: KGStore | null;
|
|
72
174
|
auditLogger?: AuditLogger | null;
|
|
73
175
|
cascadeConfig?: CascadeForgetConfig;
|
|
176
|
+
/** Omit to skip step 4 (callers that have no trigger side table). */
|
|
177
|
+
triggerStore?: ForgetTriggerStore | null;
|
|
178
|
+
/** Omit to skip the file half of step 5 (the indexed copies live in `store` and are always handled). */
|
|
179
|
+
pins?: ForgetPinArchive | null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** What a forget has already changed at the moment it stops or finishes. */
|
|
183
|
+
interface ForgetProgress {
|
|
184
|
+
triggerRowsRemoved: number | null;
|
|
185
|
+
pinIndexRowsRemoved: number | null;
|
|
186
|
+
pinsArchived: number | null;
|
|
187
|
+
kgTriplesRemoved: boolean;
|
|
188
|
+
cascadeResult: { demotedCount: number; demotedIds: string[] };
|
|
189
|
+
breadcrumbWritten: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function errorMessage(err: unknown): string {
|
|
193
|
+
return err instanceof Error ? err.message : String(err);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** How many pin files a failed pin archive had already moved, when its error says so (see ForgetPinArchive). */
|
|
197
|
+
function archivedCountOf(err: unknown): number | null {
|
|
198
|
+
if (typeof err !== "object" || err === null || !("archivedCount" in err)) return null;
|
|
199
|
+
const n = (err as { archivedCount: unknown }).archivedCount;
|
|
200
|
+
return typeof n === "number" && Number.isInteger(n) && n > 0 ? n : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Quote a value so that it pastes into a POSIX shell command line as one argument. */
|
|
204
|
+
function shellQuote(value: string): string {
|
|
205
|
+
return /^[A-Za-z0-9_.:\/@%+=,-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function countOrNa(n: number | null): string {
|
|
209
|
+
return n === null ? "n/a" : String(n);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function failed(memoryId: string, error: string, partial: Partial<ForgetResult> = {}): ForgetResult {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
memoryId,
|
|
216
|
+
kgTriplesRemoved: false,
|
|
217
|
+
triggerRowsRemoved: null,
|
|
218
|
+
pinsArchived: null,
|
|
219
|
+
pinIndexRowsRemoved: null,
|
|
220
|
+
cascadeResult: { demotedCount: 0, demotedIds: [] },
|
|
221
|
+
error,
|
|
222
|
+
...partial,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resultFields(progress: ForgetProgress): Pick<ForgetResult, "kgTriplesRemoved" | "triggerRowsRemoved" | "pinsArchived" | "pinIndexRowsRemoved" | "cascadeResult"> {
|
|
227
|
+
return {
|
|
228
|
+
kgTriplesRemoved: progress.kgTriplesRemoved,
|
|
229
|
+
triggerRowsRemoved: progress.triggerRowsRemoved,
|
|
230
|
+
pinsArchived: progress.pinsArchived,
|
|
231
|
+
pinIndexRowsRemoved: progress.pinIndexRowsRemoved,
|
|
232
|
+
cascadeResult: progress.cascadeResult,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** In plain words, what has already been changed. Empty when nothing has. */
|
|
237
|
+
function describeProgress(progress: ForgetProgress): string[] {
|
|
238
|
+
const parts: string[] = [];
|
|
239
|
+
if (progress.triggerRowsRemoved) parts.push(`${progress.triggerRowsRemoved} trigger row(s) removed`);
|
|
240
|
+
if (progress.pinIndexRowsRemoved) parts.push(`${progress.pinIndexRowsRemoved} pin index row(s) removed`);
|
|
241
|
+
if (progress.pinsArchived) parts.push(`${progress.pinsArchived} pin file(s) archived`);
|
|
242
|
+
if (progress.kgTriplesRemoved) parts.push("KG cleanup ran");
|
|
243
|
+
if (progress.cascadeResult.demotedCount) parts.push(`${progress.cascadeResult.demotedCount} related memories demoted`);
|
|
244
|
+
if (progress.breadcrumbWritten) parts.push('row marked archived with a "forgotten:" note');
|
|
245
|
+
return parts;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Rows in the main table that index a pin made from this memory. `pin_memory` writes one
|
|
250
|
+
* per pin (asset-sync.ts: scope `asset:<pin id>`, text = title + summary + snippet,
|
|
251
|
+
* metadata.originalMemoryId), and search returns them like any other row.
|
|
252
|
+
*/
|
|
253
|
+
async function findPinIndexRows(store: MemoryStore, memoryId: string): Promise<MemoryEntry[]> {
|
|
254
|
+
const rows = await store.list(["asset"], undefined, Number.MAX_SAFE_INTEGER, 0);
|
|
255
|
+
return rows.filter((row) => {
|
|
256
|
+
if (!row.scope.startsWith("asset:")) return false;
|
|
257
|
+
try {
|
|
258
|
+
const meta = JSON.parse(row.metadata || "{}") as Record<string, unknown>;
|
|
259
|
+
return meta.assetType === "pinned-memory" && meta.originalMemoryId === memoryId;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Step 10. Checks by id only: every retrieval path can return a row only if it is still in the table. */
|
|
267
|
+
async function readBack(deps: ForgetByIdDeps, id: string, scopeFilter?: string[]): Promise<ForgetVerification> {
|
|
268
|
+
const { store, kgStore, triggerStore, pins } = deps;
|
|
269
|
+
const unverified: ForgetLayer[] = [];
|
|
270
|
+
const check = async <T>(layer: ForgetLayer, look: () => T | Promise<T>): Promise<T | null> => {
|
|
271
|
+
try {
|
|
272
|
+
return await look();
|
|
273
|
+
} catch (err) {
|
|
274
|
+
unverified.push(layer);
|
|
275
|
+
console.error(`[recallnest] Read-back of ${layer} failed after forget:`, errorMessage(err));
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const row = await check("primary", async () => ((await store.get(id, scopeFilter)) ? "present" as const : "gone" as const));
|
|
281
|
+
const triggerRowsLeft = triggerStore ? await check("triggers", () => triggerStore.countForMemory(id)) : null;
|
|
282
|
+
const pinsLeft = pins ? await check("pins", () => pins.countForMemory(id)) : null;
|
|
283
|
+
const pinIndexRowsLeft = await check("pin-index", async () => (await findPinIndexRows(store, id)).length);
|
|
284
|
+
const kgTriplesLeft = kgStore && typeof kgStore.getTriplesBySourceMemories === "function"
|
|
285
|
+
? await check("kg", async () => (await kgStore.getTriplesBySourceMemories([id])).get(id)?.length ?? 0)
|
|
286
|
+
: null;
|
|
287
|
+
|
|
288
|
+
return { primary: row ?? "unverified", triggerRowsLeft, pinsLeft, pinIndexRowsLeft, kgTriplesLeft, unverified };
|
|
74
289
|
}
|
|
75
290
|
|
|
76
291
|
// ---------------------------------------------------------------------------
|
|
@@ -81,34 +296,22 @@ export async function forgetMemory(
|
|
|
81
296
|
deps: ForgetByIdDeps,
|
|
82
297
|
request: ForgetRequest,
|
|
83
298
|
): Promise<ForgetResult> {
|
|
84
|
-
const { store, kgStore, auditLogger, cascadeConfig } = deps;
|
|
299
|
+
const { store, kgStore, auditLogger, cascadeConfig, triggerStore, pins } = deps;
|
|
85
300
|
const { memoryId, confirm, reason, scopeFilter } = request;
|
|
86
301
|
|
|
87
302
|
// 1. Fetch target
|
|
88
303
|
const entry = await store.get(memoryId, scopeFilter);
|
|
89
304
|
if (!entry) {
|
|
90
|
-
return {
|
|
91
|
-
success: false,
|
|
92
|
-
memoryId,
|
|
93
|
-
kgTriplesRemoved: false,
|
|
94
|
-
cascadeResult: { demotedCount: 0, demotedIds: [] },
|
|
95
|
-
error: `Memory ${memoryId} not found`,
|
|
96
|
-
};
|
|
305
|
+
return failed(memoryId, `Memory ${memoryId} not found`);
|
|
97
306
|
}
|
|
98
307
|
|
|
99
308
|
// 2. Privacy tier check
|
|
100
309
|
const privacyTier = parsePrivacyTier(entry.metadata);
|
|
101
310
|
if (privacyTier === "durable" && !confirm) {
|
|
102
|
-
return {
|
|
103
|
-
success: false,
|
|
104
|
-
memoryId: entry.id,
|
|
105
|
-
kgTriplesRemoved: false,
|
|
106
|
-
cascadeResult: { demotedCount: 0, demotedIds: [] },
|
|
107
|
-
error: `Memory ${entry.id} has privacy tier "durable" — set confirm=true to proceed`,
|
|
108
|
-
};
|
|
311
|
+
return failed(entry.id, `Memory ${entry.id} has privacy tier "durable" — set confirm=true to proceed`);
|
|
109
312
|
}
|
|
110
313
|
|
|
111
|
-
// 3. Evidence
|
|
314
|
+
// 3. Evidence snapshot
|
|
112
315
|
const evolution = parseEvolution(entry.metadata, entry.timestamp);
|
|
113
316
|
const evidence: ForgetEvidence = {
|
|
114
317
|
entry: { ...entry },
|
|
@@ -118,82 +321,160 @@ export async function forgetMemory(
|
|
|
118
321
|
reason,
|
|
119
322
|
};
|
|
120
323
|
|
|
121
|
-
|
|
122
|
-
|
|
324
|
+
const progress: ForgetProgress = {
|
|
325
|
+
triggerRowsRemoved: null,
|
|
326
|
+
pinIndexRowsRemoved: null,
|
|
327
|
+
pinsArchived: null,
|
|
328
|
+
kgTriplesRemoved: false,
|
|
329
|
+
cascadeResult: { demotedCount: 0, demotedIds: [] },
|
|
330
|
+
breadcrumbWritten: false,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Stops the forget after something may already have been changed: says what in the error,
|
|
334
|
+
// and leaves a `forget_incomplete` audit entry (see the file header for why it is not a `forget`).
|
|
335
|
+
const stopIncomplete = (
|
|
336
|
+
stage: "pin-archive" | "delete" | "read-back",
|
|
337
|
+
problem: string,
|
|
338
|
+
nextStep: string,
|
|
339
|
+
extra: Partial<ForgetResult> = {},
|
|
340
|
+
): ForgetResult => {
|
|
341
|
+
const done = describeProgress(progress);
|
|
342
|
+
if (done.length > 0) {
|
|
343
|
+
try {
|
|
344
|
+
auditLogger?.log({
|
|
345
|
+
operation: "forget_incomplete",
|
|
346
|
+
scope: entry.scope,
|
|
347
|
+
memoryId: entry.id,
|
|
348
|
+
actor: "system",
|
|
349
|
+
details: `stage=${stage} triggers=${countOrNa(progress.triggerRowsRemoved)} pins=${countOrNa(progress.pinsArchived)} pinIndex=${countOrNa(progress.pinIndexRowsRemoved)} kg=${progress.kgTriplesRemoved ? "ran" : "no"} cascade=${progress.cascadeResult.demotedCount} breadcrumb=${progress.breadcrumbWritten ? "yes" : "no"} error=${problem}`,
|
|
350
|
+
});
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.error("[recallnest] Audit log failed during an incomplete forget:", errorMessage(err));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const already = done.length > 0 ? ` Already done and not rolled back: ${done.join("; ")}.` : "";
|
|
356
|
+
return failed(entry.id, `${problem}${already} ${nextStep}`, { evidence, ...resultFields(progress), ...extra });
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// 4. Trigger rows — fail closed (see the file header)
|
|
360
|
+
if (triggerStore) {
|
|
361
|
+
try {
|
|
362
|
+
progress.triggerRowsRemoved = await triggerStore.deleteForMemory(entry.id);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
return failed(entry.id, `Trigger cleanup failed: ${errorMessage(err)}. Nothing was deleted; retry the forget.`, { evidence });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 5. Pin archive — fail closed. A pin is a file under data/pins and an indexed copy in the
|
|
369
|
+
// main table; the copy goes first, because it is the half that search returns.
|
|
370
|
+
try {
|
|
371
|
+
const indexRows = await findPinIndexRows(store, entry.id);
|
|
372
|
+
progress.pinIndexRowsRemoved = 0;
|
|
373
|
+
for (const row of indexRows) {
|
|
374
|
+
// false: the row was already gone (removed between the listing above and this call), so it is not counted
|
|
375
|
+
if (await store.delete(row.id)) progress.pinIndexRowsRemoved += 1;
|
|
376
|
+
}
|
|
377
|
+
if (pins) progress.pinsArchived = pins.archiveForMemory(entry.id);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
// The archive may have moved some files before it failed. Without this, the error below
|
|
380
|
+
// and the audit entry would both say that nothing had been changed.
|
|
381
|
+
const movedBeforeFailing = archivedCountOf(err);
|
|
382
|
+
if (movedBeforeFailing !== null) progress.pinsArchived = movedBeforeFailing;
|
|
383
|
+
const restoreTriggers = progress.triggerRowsRemoved
|
|
384
|
+
? ` To keep the memory instead, restore its trigger rows with \`triggers-backfill --rebuild --apply --scope ${shellQuote(entry.scope)}\`.`
|
|
385
|
+
: "";
|
|
386
|
+
return stopIncomplete(
|
|
387
|
+
"pin-archive",
|
|
388
|
+
`Pin archive failed: ${errorMessage(err)}.`,
|
|
389
|
+
`The memory itself was not deleted and is still active. Retry the forget.${restoreTriggers}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 6. KG triples
|
|
123
394
|
if (kgStore) {
|
|
124
395
|
try {
|
|
125
396
|
await kgStore.deleteBySource(entry.id);
|
|
126
|
-
kgTriplesRemoved = true;
|
|
397
|
+
progress.kgTriplesRemoved = true;
|
|
127
398
|
} catch (err) {
|
|
128
|
-
console.error("[recallnest] KG cleanup failed during forget:",
|
|
399
|
+
console.error("[recallnest] KG cleanup failed during forget:", errorMessage(err));
|
|
129
400
|
}
|
|
130
401
|
}
|
|
131
402
|
|
|
132
|
-
//
|
|
133
|
-
let cascadeResult = { demotedCount: 0, demotedIds: [] as string[] };
|
|
403
|
+
// 7. Cascade demote
|
|
134
404
|
try {
|
|
135
|
-
cascadeResult = await cascadeForget(
|
|
405
|
+
progress.cascadeResult = await cascadeForget(
|
|
136
406
|
store,
|
|
137
407
|
{ id: entry.id, vector: entry.vector, scope: entry.scope },
|
|
138
408
|
cascadeConfig ?? DEFAULT_CASCADE_FORGET_CONFIG,
|
|
139
409
|
);
|
|
140
410
|
} catch (err) {
|
|
141
|
-
console.error("[recallnest] Cascade demote failed during forget:",
|
|
411
|
+
console.error("[recallnest] Cascade demote failed during forget:", errorMessage(err));
|
|
142
412
|
}
|
|
143
413
|
|
|
144
|
-
//
|
|
414
|
+
// 8. Evolution breadcrumb
|
|
145
415
|
try {
|
|
146
416
|
const patchedMetadata = patchEvolution(entry.metadata, {
|
|
147
417
|
status: "archived" as any,
|
|
148
418
|
evolutionNote: `forgotten: ${reason || "user request"}`,
|
|
149
419
|
});
|
|
150
|
-
await store.update(entry.id, { metadata: patchedMetadata }, scopeFilter);
|
|
420
|
+
progress.breadcrumbWritten = (await store.update(entry.id, { metadata: patchedMetadata }, scopeFilter)) !== null;
|
|
151
421
|
} catch (err) {
|
|
152
|
-
console.error("[recallnest] Evolution patch failed during forget:",
|
|
422
|
+
console.error("[recallnest] Evolution patch failed during forget:", errorMessage(err));
|
|
153
423
|
}
|
|
154
424
|
|
|
155
|
-
|
|
425
|
+
const rowState = () => (progress.breadcrumbWritten
|
|
426
|
+
? 'The row is still in the table, marked archived with a "forgotten:" note: default recall skips it and the memory-file reconciler treats its text as forgotten.'
|
|
427
|
+
: "The row is still in the table and still active.");
|
|
428
|
+
|
|
429
|
+
// 9. Primary delete
|
|
156
430
|
try {
|
|
157
431
|
await store.delete(entry.id, scopeFilter);
|
|
158
432
|
} catch (err) {
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
memoryId: entry.id,
|
|
162
|
-
evidence,
|
|
163
|
-
kgTriplesRemoved,
|
|
164
|
-
cascadeResult,
|
|
165
|
-
error: `Delete failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
-
};
|
|
433
|
+
return stopIncomplete("delete", `Delete failed: ${errorMessage(err)}.`, `${rowState()} Run forget again to finish.`);
|
|
167
434
|
}
|
|
168
435
|
|
|
169
|
-
//
|
|
436
|
+
// 10. Read-back
|
|
437
|
+
const verification = await readBack(deps, entry.id, scopeFilter);
|
|
438
|
+
if (verification.primary === "present") {
|
|
439
|
+
return stopIncomplete(
|
|
440
|
+
"read-back",
|
|
441
|
+
`Delete reported success but memory ${entry.id} is still readable.`,
|
|
442
|
+
`${rowState()} Run forget again to finish.`,
|
|
443
|
+
{ verification },
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 11. Audit log
|
|
170
448
|
try {
|
|
171
449
|
auditLogger?.log({
|
|
172
450
|
operation: "forget",
|
|
173
451
|
scope: entry.scope,
|
|
174
452
|
memoryId: entry.id,
|
|
175
453
|
actor: "system",
|
|
176
|
-
|
|
454
|
+
// norm=<归一文本指纹> 放最前:删的是这一行,要忘的是这段文字。记忆文件对账据此不把同文的另一行
|
|
455
|
+
// 恢复回来(memory-reconcile.ts),只认 id 的话换个空白就绕过去了。
|
|
456
|
+
// reason 是自由文本,放最后:details 超 200 字会被截断,前面的字段不能被它挤掉。
|
|
457
|
+
details: `norm=${textFingerprint(entry.text)} tier=${privacyTier} triggers=${countOrNa(progress.triggerRowsRemoved)} pins=${countOrNa(progress.pinsArchived)} pinIndex=${countOrNa(progress.pinIndexRowsRemoved)} verify=${summarizeVerification(verification)} cascade=${progress.cascadeResult.demotedCount} reason=${reason || "none"}`,
|
|
177
458
|
});
|
|
178
|
-
if (cascadeResult.demotedCount > 0) {
|
|
459
|
+
if (progress.cascadeResult.demotedCount > 0) {
|
|
179
460
|
auditLogger?.log({
|
|
180
461
|
operation: "cascade_forget",
|
|
181
462
|
scope: entry.scope,
|
|
182
463
|
memoryId: entry.id,
|
|
183
464
|
actor: "system",
|
|
184
|
-
details: `demoted ${cascadeResult.demotedCount} related memories: ${cascadeResult.demotedIds.map(id => id.slice(0, 8)).join(",")}`,
|
|
465
|
+
details: `demoted ${progress.cascadeResult.demotedCount} related memories: ${progress.cascadeResult.demotedIds.map(id => id.slice(0, 8)).join(",")}`,
|
|
185
466
|
});
|
|
186
467
|
}
|
|
187
468
|
} catch (err) {
|
|
188
|
-
console.error("[recallnest] Audit log failed during forget:",
|
|
469
|
+
console.error("[recallnest] Audit log failed during forget:", errorMessage(err));
|
|
189
470
|
}
|
|
190
471
|
|
|
191
472
|
return {
|
|
192
473
|
success: true,
|
|
193
474
|
memoryId: entry.id,
|
|
194
475
|
evidence,
|
|
195
|
-
|
|
196
|
-
|
|
476
|
+
...resultFields(progress),
|
|
477
|
+
verification,
|
|
197
478
|
};
|
|
198
479
|
}
|
|
199
480
|
|
|
@@ -248,9 +529,12 @@ export async function forgetByScope(
|
|
|
248
529
|
}
|
|
249
530
|
}
|
|
250
531
|
|
|
251
|
-
// Bulk KG scope cleanup
|
|
532
|
+
// Bulk KG scope cleanup — only when every entry was forgotten. An entry whose forget stopped
|
|
533
|
+
// (its trigger or pin cleanup failed) is still an active memory, and wiping the whole scope
|
|
534
|
+
// would strip its triples anyway. The entries that were forgotten have already lost theirs
|
|
535
|
+
// in step 6 of forgetMemory (deleteBySource).
|
|
252
536
|
let kgScopeCleared = false;
|
|
253
|
-
if (kgStore) {
|
|
537
|
+
if (kgStore && failedCount === 0) {
|
|
254
538
|
try {
|
|
255
539
|
await kgStore.deleteByScope(scope);
|
|
256
540
|
kgScopeCleared = true;
|
package/src/gateway-server.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { homedir } from "node:os";
|
|
|
44
44
|
import { join, isAbsolute } from "node:path";
|
|
45
45
|
import { execFile } from "node:child_process";
|
|
46
46
|
import * as envConfig from "./env-config.js";
|
|
47
|
+
import { HTTP_WRITE_IDLE_TIMEOUT_S } from "./store-write-budget.js";
|
|
47
48
|
|
|
48
49
|
const HOME = homedir();
|
|
49
50
|
const PORT = clampInt(envConfig.gatewayPortRaw(), 8791, 1, 65535);
|
|
@@ -195,6 +196,9 @@ function fileSearch(query: string, scope: string, limit: number): Promise<FileHi
|
|
|
195
196
|
// ── server ───────────────────────────────────────────────────────────────────
|
|
196
197
|
const server = Bun.serve({
|
|
197
198
|
port: PORT,
|
|
199
|
+
// Writes can wait up to 30 s for store-write before they start; Bun's 10 s default would drop
|
|
200
|
+
// the connection while the handler still commits.
|
|
201
|
+
idleTimeout: HTTP_WRITE_IDLE_TIMEOUT_S,
|
|
198
202
|
hostname: HOST,
|
|
199
203
|
async fetch(request) {
|
|
200
204
|
const id = randomUUID().slice(0, 8);
|