@jbarket/loomai 0.5.0 → 0.6.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 +114 -1
- package/README.md +97 -0
- package/dist/backends/dedup.d.ts +23 -0
- package/dist/backends/dedup.js +100 -0
- package/dist/backends/embedding-runtime.js +38 -9
- package/dist/backends/episodes.js +1 -1
- package/dist/backends/fastembed.js +30 -4
- package/dist/backends/migrations.js +200 -0
- package/dist/backends/mmr.d.ts +8 -0
- package/dist/backends/mmr.js +18 -4
- package/dist/backends/people.d.ts +192 -0
- package/dist/backends/people.js +824 -0
- package/dist/backends/proposals.js +1 -1
- package/dist/backends/recorder-health.d.ts +34 -0
- package/dist/backends/recorder-health.js +117 -0
- package/dist/backends/salience.d.ts +39 -0
- package/dist/backends/salience.js +52 -4
- package/dist/backends/scope.d.ts +56 -0
- package/dist/backends/scope.js +75 -0
- package/dist/backends/sqlite-knowledge.d.ts +24 -4
- package/dist/backends/sqlite-knowledge.js +253 -91
- package/dist/backends/sqlite-vec.d.ts +32 -1
- package/dist/backends/sqlite-vec.js +394 -75
- package/dist/backends/types.d.ts +203 -3
- package/dist/categories.d.ts +2 -0
- package/dist/categories.js +2 -0
- package/dist/cli/args.d.ts +16 -1
- package/dist/cli/args.js +45 -1
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +57 -3
- package/dist/cli/knowledge.js +4 -1
- package/dist/cli/memory.js +2 -2
- package/dist/cli/person.d.ts +2 -0
- package/dist/cli/person.js +116 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.js +10 -0
- package/dist/cli/remote.d.ts +36 -0
- package/dist/cli/remote.js +173 -0
- package/dist/cli/serve.d.ts +7 -0
- package/dist/cli/serve.js +94 -5
- package/dist/cli/subcommands.d.ts +1 -1
- package/dist/cli/subcommands.js +1 -1
- package/dist/cli/update.js +30 -11
- package/dist/clients.js +7 -4
- package/dist/config.d.ts +14 -0
- package/dist/config.js +25 -2
- package/dist/index.js +22 -7
- package/dist/injection/harnesses.js +9 -3
- package/dist/install/harnesses.js +7 -1
- package/dist/server.d.ts +8 -1
- package/dist/server.js +461 -374
- package/dist/tools/dossier.js +5 -1
- package/dist/tools/eurorack-import.js +3 -0
- package/dist/tools/identity.d.ts +2 -0
- package/dist/tools/identity.js +35 -2
- package/dist/tools/knowledge-history.d.ts +6 -1
- package/dist/tools/knowledge-history.js +26 -8
- package/dist/tools/knowledge-maintain.d.ts +5 -0
- package/dist/tools/knowledge-maintain.js +92 -5
- package/dist/tools/knowledge-recall.d.ts +7 -1
- package/dist/tools/knowledge-recall.js +53 -3
- package/dist/tools/knowledge-verify.d.ts +11 -1
- package/dist/tools/knowledge-verify.js +17 -4
- package/dist/tools/knowledge-write.d.ts +8 -1
- package/dist/tools/knowledge-write.js +27 -9
- package/dist/tools/memory-history.d.ts +5 -0
- package/dist/tools/memory-history.js +52 -0
- package/dist/tools/memory-list.js +2 -1
- package/dist/tools/memory-merge.d.ts +2 -0
- package/dist/tools/memory-merge.js +40 -0
- package/dist/tools/memory-revision-restore.d.ts +5 -0
- package/dist/tools/memory-revision-restore.js +33 -0
- package/dist/tools/person.d.ts +32 -0
- package/dist/tools/person.js +111 -0
- package/dist/tools/prune.js +1 -1
- package/dist/tools/recall.js +7 -1
- package/dist/tools/remember.js +18 -2
- package/dist/tools/update.js +5 -1
- package/dist/transport/cli-endpoint.d.ts +13 -0
- package/dist/transport/cli-endpoint.js +97 -0
- package/dist/transport/guards.d.ts +14 -0
- package/dist/transport/guards.js +26 -0
- package/dist/transport/http-server.d.ts +28 -1
- package/dist/transport/http-server.js +156 -18
- package/dist/transport/knowledge-server.d.ts +11 -0
- package/dist/transport/knowledge-server.js +30 -0
- package/dist/transport/knowledge-tools.d.ts +50 -0
- package/dist/transport/knowledge-tools.js +290 -0
- package/dist/transport/mcp-app-hello.d.ts +1 -1
- package/dist/widgets/display-modes.html +62 -56
- package/dist/widgets/hello.html +62 -56
- package/dist/widgets/proposals.html +62 -56
- package/dist/widgets/top-of-mind.html +60 -54
- package/package.json +7 -5
|
@@ -43,7 +43,7 @@ function hasTable(db, table) {
|
|
|
43
43
|
function openProposalsDb(contextDir) {
|
|
44
44
|
const dbPath = resolveSqliteDbPath(contextDir);
|
|
45
45
|
const db = new BetterSqlite3(dbPath);
|
|
46
|
-
db.pragma('busy_timeout =
|
|
46
|
+
db.pragma('busy_timeout = 5000');
|
|
47
47
|
if (!hasTable(db, 'proposals'))
|
|
48
48
|
runMigrations(db, { strict: false });
|
|
49
49
|
return db;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface RecorderHealth {
|
|
2
|
+
/** How many episode write-or-read attempts have failed in a row since lastSuccess. */
|
|
3
|
+
consecutiveFailures: number;
|
|
4
|
+
/** ISO timestamp of the first failure in the current streak, null when healthy. */
|
|
5
|
+
failingSince: string | null;
|
|
6
|
+
/** Scrubbed error string from the last failure (≤200 chars, no absolute paths). */
|
|
7
|
+
lastError: string | null;
|
|
8
|
+
/** ISO timestamp of the last successful episode write, null if never recorded. */
|
|
9
|
+
lastSuccess: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** Number of consecutive failures before identity.ts injects the warning block. */
|
|
12
|
+
export declare const FAILURE_THRESHOLD = 3;
|
|
13
|
+
export declare function ledgerPath(contextDir: string): string;
|
|
14
|
+
/** Read the health ledger synchronously. Returns HEALTHY when absent or unreadable. */
|
|
15
|
+
export declare function readHealth(contextDir: string): RecorderHealth;
|
|
16
|
+
/**
|
|
17
|
+
* Record a successful episode write. Resets the consecutive failure streak.
|
|
18
|
+
* The lastError field is preserved so diagnostics survive a single success.
|
|
19
|
+
*/
|
|
20
|
+
export declare function recordSuccess(contextDir: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Record a failed episode write or tape-read attempt. Increments the streak.
|
|
23
|
+
* failingSince is set to the first failure's timestamp and never moved forward
|
|
24
|
+
* until a success resets the streak.
|
|
25
|
+
*/
|
|
26
|
+
export declare function recordFailure(contextDir: string, error: unknown): void;
|
|
27
|
+
/**
|
|
28
|
+
* Build the warning block to inject into the boot context, or null when healthy.
|
|
29
|
+
*
|
|
30
|
+
* Returned string is a complete markdown section. Callers append it BELOW the
|
|
31
|
+
* rest of the identity payload — placed last so it is freshest in the model's
|
|
32
|
+
* working memory when its first reply fires.
|
|
33
|
+
*/
|
|
34
|
+
export declare function warningBlock(health: RecorderHealth): string | null;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorder health ledger (t-562) — tracks consecutive episode-write failures.
|
|
3
|
+
*
|
|
4
|
+
* Written by:
|
|
5
|
+
* - the episode write path (remember.ts) on success and failure
|
|
6
|
+
* - the tape-read path (identity.ts catch) on failure
|
|
7
|
+
* Read by:
|
|
8
|
+
* - identity.ts, which appends a warning block below the identity payload
|
|
9
|
+
* when the recorder has failed FAILURE_THRESHOLD consecutive times.
|
|
10
|
+
*
|
|
11
|
+
* Inspired by observer-health.ts in claude-mem @ fd0ecf0. The ledger is a
|
|
12
|
+
* small JSON file in the context dir alongside memories.db — one per agent.
|
|
13
|
+
* All reads/writes are synchronous and never throw: the health ledger must
|
|
14
|
+
* never cascade into a deeper failure.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/** Number of consecutive failures before identity.ts injects the warning block. */
|
|
19
|
+
export const FAILURE_THRESHOLD = 3;
|
|
20
|
+
const LEDGER_FILENAME = 'recorder-health.json';
|
|
21
|
+
const HEALTHY = {
|
|
22
|
+
consecutiveFailures: 0,
|
|
23
|
+
failingSince: null,
|
|
24
|
+
lastError: null,
|
|
25
|
+
lastSuccess: null,
|
|
26
|
+
};
|
|
27
|
+
export function ledgerPath(contextDir) {
|
|
28
|
+
return join(contextDir, LEDGER_FILENAME);
|
|
29
|
+
}
|
|
30
|
+
/** Read the health ledger synchronously. Returns HEALTHY when absent or unreadable. */
|
|
31
|
+
export function readHealth(contextDir) {
|
|
32
|
+
try {
|
|
33
|
+
const path = ledgerPath(contextDir);
|
|
34
|
+
if (!existsSync(path))
|
|
35
|
+
return { ...HEALTHY };
|
|
36
|
+
const raw = readFileSync(path, 'utf-8');
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
return {
|
|
39
|
+
consecutiveFailures: typeof parsed.consecutiveFailures === 'number' ? parsed.consecutiveFailures : 0,
|
|
40
|
+
failingSince: typeof parsed.failingSince === 'string' ? parsed.failingSince : null,
|
|
41
|
+
lastError: typeof parsed.lastError === 'string' ? parsed.lastError : null,
|
|
42
|
+
lastSuccess: typeof parsed.lastSuccess === 'string' ? parsed.lastSuccess : null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { ...HEALTHY };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Scrub a raw error for the ledger: trim to 200 chars, redact absolute paths. */
|
|
50
|
+
function scrubError(error) {
|
|
51
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
52
|
+
return raw.replace(/\/[^\s:,'"]+/g, '<path>').slice(0, 200);
|
|
53
|
+
}
|
|
54
|
+
/** Write the health ledger synchronously. Silently swallows write errors. */
|
|
55
|
+
function writeHealth(contextDir, health) {
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(ledgerPath(contextDir), JSON.stringify(health, null, 2), 'utf-8');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// writing the health ledger must never block or throw
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Record a successful episode write. Resets the consecutive failure streak.
|
|
65
|
+
* The lastError field is preserved so diagnostics survive a single success.
|
|
66
|
+
*/
|
|
67
|
+
export function recordSuccess(contextDir) {
|
|
68
|
+
const prev = readHealth(contextDir);
|
|
69
|
+
writeHealth(contextDir, {
|
|
70
|
+
consecutiveFailures: 0,
|
|
71
|
+
failingSince: null,
|
|
72
|
+
lastError: prev.lastError,
|
|
73
|
+
lastSuccess: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record a failed episode write or tape-read attempt. Increments the streak.
|
|
78
|
+
* failingSince is set to the first failure's timestamp and never moved forward
|
|
79
|
+
* until a success resets the streak.
|
|
80
|
+
*/
|
|
81
|
+
export function recordFailure(contextDir, error) {
|
|
82
|
+
const prev = readHealth(contextDir);
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
writeHealth(contextDir, {
|
|
85
|
+
consecutiveFailures: prev.consecutiveFailures + 1,
|
|
86
|
+
failingSince: prev.failingSince ?? now,
|
|
87
|
+
lastError: scrubError(error),
|
|
88
|
+
lastSuccess: prev.lastSuccess,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the warning block to inject into the boot context, or null when healthy.
|
|
93
|
+
*
|
|
94
|
+
* Returned string is a complete markdown section. Callers append it BELOW the
|
|
95
|
+
* rest of the identity payload — placed last so it is freshest in the model's
|
|
96
|
+
* working memory when its first reply fires.
|
|
97
|
+
*/
|
|
98
|
+
export function warningBlock(health) {
|
|
99
|
+
if (health.consecutiveFailures < FAILURE_THRESHOLD)
|
|
100
|
+
return null;
|
|
101
|
+
const since = health.failingSince ? ` since ${health.failingSince}` : '';
|
|
102
|
+
const errLine = health.lastError
|
|
103
|
+
? `\n\nLast error: \`${health.lastError}\``
|
|
104
|
+
: '';
|
|
105
|
+
const successLine = health.lastSuccess
|
|
106
|
+
? `\nLast success: ${health.lastSuccess}`
|
|
107
|
+
: '\nNo successful write on record.';
|
|
108
|
+
return (`# ⚠️ Recorder Health Warning\n\n` +
|
|
109
|
+
`The episode recorder has failed **${health.consecutiveFailures} consecutive` +
|
|
110
|
+
` time${health.consecutiveFailures === 1 ? '' : 's'}**${since}.\n` +
|
|
111
|
+
`Episodes may not be reaching the tape — future bodies will wake without context.\n` +
|
|
112
|
+
successLine +
|
|
113
|
+
errLine +
|
|
114
|
+
`\n\n` +
|
|
115
|
+
`Check \`~/.local/state/art-episodes/hook.log\` and the loom episode store. ` +
|
|
116
|
+
`Call \`mcp__loom__remember\` with category \`episode\` to exercise the write path.`);
|
|
117
|
+
}
|
|
@@ -10,6 +10,41 @@ export declare const DEFAULT_HALF_LIFE_DAYS = 30;
|
|
|
10
10
|
export declare function temperature(lastTouchIso: string, category: string, nowMs: number): number;
|
|
11
11
|
export type Tier = 'Hot' | 'Warm' | 'Cool';
|
|
12
12
|
export declare function tierOf(temp: number): Tier;
|
|
13
|
+
/**
|
|
14
|
+
* Feedback confidence model (t-338, parent t-325).
|
|
15
|
+
*
|
|
16
|
+
* Stolen from affaan-m/ECC's instinct design (confidence range 0.3-0.9,
|
|
17
|
+
* evidence-backed, +0.05 per confirming observation, -0.1 per contradicting
|
|
18
|
+
* one) — deliberately NOT their implementation. In ECC that arithmetic lives
|
|
19
|
+
* only as prose in a prompt (agents/observer.md) fed to an LLM; instinct-cli.py
|
|
20
|
+
* never computes, adjusts, or decays the number anywhere. Here the arithmetic
|
|
21
|
+
* is plain functions loom's own code calls from remember()/update() — never a
|
|
22
|
+
* prompt. Decay reuses `temperature()` above (same HALF_LIVES table) instead
|
|
23
|
+
* of a second, separate -0.02/week rule: one decay mechanism, not two.
|
|
24
|
+
*/
|
|
25
|
+
export declare const FEEDBACK_CONFIDENCE_MIN = 0.3;
|
|
26
|
+
export declare const FEEDBACK_CONFIDENCE_MAX = 0.9;
|
|
27
|
+
/** Starting value for a newly observed feedback memory, and the value decay relaxes toward. */
|
|
28
|
+
export declare const FEEDBACK_CONFIDENCE_DEFAULT = 0.6;
|
|
29
|
+
export declare const FEEDBACK_CONFIRM_DELTA = 0.05;
|
|
30
|
+
export declare const FEEDBACK_CONTRADICT_DELTA = -0.1;
|
|
31
|
+
export declare function clampConfidence(value: number): number;
|
|
32
|
+
/** Apply one confirming or contradicting observation to a stored confidence value. */
|
|
33
|
+
export declare function applyObservation(current: number, kind: 'confirm' | 'contradict'): number;
|
|
34
|
+
/**
|
|
35
|
+
* Confidence after decay: relax the evidence-weighted raw value toward the
|
|
36
|
+
* neutral default as the memory cools, scaled by the SAME exponential
|
|
37
|
+
* half-life curve as salience (`temperature()`, same HALF_LIVES table) — a
|
|
38
|
+
* fully-decayed (temp→0) memory reads as neutral, a freshly-touched one
|
|
39
|
+
* (temp=1) reads as its raw evidence-backed value.
|
|
40
|
+
*
|
|
41
|
+
* `lastTouchIso` should be the last OBSERVATION time (the row's
|
|
42
|
+
* `observed_at`, falling back to `created`) — not `updated` and not
|
|
43
|
+
* `last_accessed`. A recall, an edit, an archive or a restore isn't
|
|
44
|
+
* evidence and mustn't reheat confidence; only a confirm/contradict (which
|
|
45
|
+
* goes through update() and stamps `observed_at`) is.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decayedConfidence(rawConfidence: number, lastTouchIso: string, category: string, nowMs: number): number;
|
|
13
48
|
/**
|
|
14
49
|
* Recompute and STORE the salience scalar for every non-archived memory. Called
|
|
15
50
|
* by the consolidation lane (stored + lane-refresh, not computed-on-read).
|
|
@@ -21,6 +56,8 @@ export interface DigestRow {
|
|
|
21
56
|
category: string;
|
|
22
57
|
content: string;
|
|
23
58
|
salience: number;
|
|
59
|
+
/** Sourcing tier — null on legacy records without the column. */
|
|
60
|
+
sourcing: string | null;
|
|
24
61
|
}
|
|
25
62
|
export interface DigestOptions {
|
|
26
63
|
/** Approximate token budget for the whole digest. */
|
|
@@ -43,6 +80,8 @@ export interface DigestAtom {
|
|
|
43
80
|
content: string;
|
|
44
81
|
salience: number;
|
|
45
82
|
tier: Tier;
|
|
83
|
+
/** Sourcing tier — null on legacy records. */
|
|
84
|
+
sourcing: string | null;
|
|
46
85
|
}
|
|
47
86
|
/**
|
|
48
87
|
* Structured salience digest for a UI widget: the hottest authored atoms,
|
|
@@ -48,6 +48,49 @@ export function tierOf(temp) {
|
|
|
48
48
|
return 'Warm';
|
|
49
49
|
return 'Cool';
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Feedback confidence model (t-338, parent t-325).
|
|
53
|
+
*
|
|
54
|
+
* Stolen from affaan-m/ECC's instinct design (confidence range 0.3-0.9,
|
|
55
|
+
* evidence-backed, +0.05 per confirming observation, -0.1 per contradicting
|
|
56
|
+
* one) — deliberately NOT their implementation. In ECC that arithmetic lives
|
|
57
|
+
* only as prose in a prompt (agents/observer.md) fed to an LLM; instinct-cli.py
|
|
58
|
+
* never computes, adjusts, or decays the number anywhere. Here the arithmetic
|
|
59
|
+
* is plain functions loom's own code calls from remember()/update() — never a
|
|
60
|
+
* prompt. Decay reuses `temperature()` above (same HALF_LIVES table) instead
|
|
61
|
+
* of a second, separate -0.02/week rule: one decay mechanism, not two.
|
|
62
|
+
*/
|
|
63
|
+
export const FEEDBACK_CONFIDENCE_MIN = 0.3;
|
|
64
|
+
export const FEEDBACK_CONFIDENCE_MAX = 0.9;
|
|
65
|
+
/** Starting value for a newly observed feedback memory, and the value decay relaxes toward. */
|
|
66
|
+
export const FEEDBACK_CONFIDENCE_DEFAULT = 0.6;
|
|
67
|
+
export const FEEDBACK_CONFIRM_DELTA = 0.05;
|
|
68
|
+
export const FEEDBACK_CONTRADICT_DELTA = -0.1;
|
|
69
|
+
export function clampConfidence(value) {
|
|
70
|
+
return Math.min(FEEDBACK_CONFIDENCE_MAX, Math.max(FEEDBACK_CONFIDENCE_MIN, value));
|
|
71
|
+
}
|
|
72
|
+
/** Apply one confirming or contradicting observation to a stored confidence value. */
|
|
73
|
+
export function applyObservation(current, kind) {
|
|
74
|
+
const delta = kind === 'confirm' ? FEEDBACK_CONFIRM_DELTA : FEEDBACK_CONTRADICT_DELTA;
|
|
75
|
+
return clampConfidence(current + delta);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Confidence after decay: relax the evidence-weighted raw value toward the
|
|
79
|
+
* neutral default as the memory cools, scaled by the SAME exponential
|
|
80
|
+
* half-life curve as salience (`temperature()`, same HALF_LIVES table) — a
|
|
81
|
+
* fully-decayed (temp→0) memory reads as neutral, a freshly-touched one
|
|
82
|
+
* (temp=1) reads as its raw evidence-backed value.
|
|
83
|
+
*
|
|
84
|
+
* `lastTouchIso` should be the last OBSERVATION time (the row's
|
|
85
|
+
* `observed_at`, falling back to `created`) — not `updated` and not
|
|
86
|
+
* `last_accessed`. A recall, an edit, an archive or a restore isn't
|
|
87
|
+
* evidence and mustn't reheat confidence; only a confirm/contradict (which
|
|
88
|
+
* goes through update() and stamps `observed_at`) is.
|
|
89
|
+
*/
|
|
90
|
+
export function decayedConfidence(rawConfidence, lastTouchIso, category, nowMs) {
|
|
91
|
+
const temp = temperature(lastTouchIso, category, nowMs);
|
|
92
|
+
return FEEDBACK_CONFIDENCE_DEFAULT + (rawConfidence - FEEDBACK_CONFIDENCE_DEFAULT) * temp;
|
|
93
|
+
}
|
|
51
94
|
/** Most-recent-touch timestamp for a row (recall/write reheat through these). */
|
|
52
95
|
function lastTouch(r) {
|
|
53
96
|
return r.last_accessed ?? r.updated ?? r.created;
|
|
@@ -77,7 +120,10 @@ const DEFAULT_CHARS_PER_TOKEN = 4;
|
|
|
77
120
|
function atomLine(r) {
|
|
78
121
|
const body = r.content.replace(/\s+/g, ' ').trim();
|
|
79
122
|
const clipped = body.length > 200 ? body.slice(0, 197).trimEnd() + '…' : body;
|
|
80
|
-
|
|
123
|
+
// Show sourcing tag when present so readers calibrate confidence on inject.
|
|
124
|
+
// 'inferred' is the critical one: AI-generated reasoning, not ground truth.
|
|
125
|
+
const tag = r.sourcing ? `, ${r.sourcing}` : '';
|
|
126
|
+
return `- **${r.title}** (${r.category}${tag}) — ${clipped}`;
|
|
81
127
|
}
|
|
82
128
|
/**
|
|
83
129
|
* Assemble the boot digest from already-recomputed salience: order hottest-first,
|
|
@@ -120,7 +166,7 @@ function openMemoriesDb(contextDir, readonly) {
|
|
|
120
166
|
if (!existsSync(dbPath))
|
|
121
167
|
return null;
|
|
122
168
|
const db = new BetterSqlite3(dbPath, { readonly });
|
|
123
|
-
db.pragma('busy_timeout =
|
|
169
|
+
db.pragma('busy_timeout = 5000');
|
|
124
170
|
return db;
|
|
125
171
|
}
|
|
126
172
|
/** Lane entry point: recompute + store salience for a context. Returns count (or null if no store). */
|
|
@@ -153,7 +199,7 @@ export function digestData(contextDir, limit = 40) {
|
|
|
153
199
|
return { atoms: [], total: 0 };
|
|
154
200
|
const total = db.prepare('SELECT COUNT(*) AS n FROM memories WHERE archived = 0').get().n;
|
|
155
201
|
const rows = db
|
|
156
|
-
.prepare('SELECT title, category, content, salience FROM memories WHERE archived = 0 AND category != ? ORDER BY salience DESC LIMIT ?')
|
|
202
|
+
.prepare('SELECT title, category, content, salience, sourcing FROM memories WHERE archived = 0 AND category != ? ORDER BY salience DESC LIMIT ?')
|
|
157
203
|
.all(EPISODE_CATEGORY, limit);
|
|
158
204
|
const atoms = rows.map((r) => ({ ...r, tier: tierOf(r.salience) }));
|
|
159
205
|
return { atoms, total };
|
|
@@ -176,7 +222,9 @@ export function digestForContext(contextDir, opts = {}) {
|
|
|
176
222
|
.prepare(
|
|
177
223
|
// Episodes are the tape, not the digest: a fresh episode is always
|
|
178
224
|
// salience 1.0 and would crowd Top of Mind with what just happened.
|
|
179
|
-
|
|
225
|
+
// sourcing: LEFT JOIN-style null-safe select — old stores without the
|
|
226
|
+
// column return NULL which DigestRow.sourcing accepts.
|
|
227
|
+
'SELECT title, category, content, salience, sourcing FROM memories WHERE archived = 0 AND category != ? ORDER BY salience DESC LIMIT 100')
|
|
180
228
|
.all(EPISODE_CATEGORY);
|
|
181
229
|
return assembleDigest(rows, opts);
|
|
182
230
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory scope discipline (t-337b, from the 2026-09-01 affaan-m/ECC teardown).
|
|
3
|
+
*
|
|
4
|
+
* loom has always carried a `project` tag on memories, but nothing stopped a
|
|
5
|
+
* preference learned while working on one project from surfacing while the
|
|
6
|
+
* agent is working on an unrelated one — the tag was metadata, not a filter.
|
|
7
|
+
*
|
|
8
|
+
* Every memory now has an *effective scope*: 'project' (only visible when
|
|
9
|
+
* recalling within the project it was written for) or 'global' (visible
|
|
10
|
+
* everywhere). ECC's instinct-promotion model is the source: default to the
|
|
11
|
+
* narrower scope, promote to global only on deliberate signal — here, an
|
|
12
|
+
* explicit `scope` at write time, since loom has no cross-project id to
|
|
13
|
+
* watch for repetition against.
|
|
14
|
+
*
|
|
15
|
+
* The category split below follows ECC's scope-decision table as far as
|
|
16
|
+
* loom's own category vocabulary lets it: 'user' (a fact about the person)
|
|
17
|
+
* and 'reference' (a pointer to an external system) are inherently
|
|
18
|
+
* cross-project, so they default global even when a project happens to be
|
|
19
|
+
* attached. Every other category defaults to 'project' — including 'self'
|
|
20
|
+
* and 'feedback', where ECC's real split lives (language/framework/style/
|
|
21
|
+
* error-handling feedback is project-specific; security/git/tool-workflow
|
|
22
|
+
* feedback is universal). Category alone can't tell those apart; that's a
|
|
23
|
+
* per-memory authoring call, made with `scope: 'global'` at write time, not
|
|
24
|
+
* a product decision this module can bake in — see t-337's comment thread
|
|
25
|
+
* for the open question on backfilling existing records.
|
|
26
|
+
*/
|
|
27
|
+
/** A memory's visibility: 'project' (scoped) or 'global' (everywhere). */
|
|
28
|
+
export type MemoryScope = 'project' | 'global';
|
|
29
|
+
/** Default scope for a category, 'project' for anything not in the table (safe default). */
|
|
30
|
+
export declare function defaultScopeForCategory(category: string): MemoryScope;
|
|
31
|
+
/** The subset of a memory record that scope decisions need. */
|
|
32
|
+
export interface ScopableMemory {
|
|
33
|
+
category: string;
|
|
34
|
+
project?: string | null;
|
|
35
|
+
/** Explicit author override from write time, if any. Anything else is ignored (treated as unset). */
|
|
36
|
+
scope?: string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The scope actually in force for a memory: the explicit override if one was
|
|
40
|
+
* set at write time, else the category default — but only when the memory
|
|
41
|
+
* is tagged to a project at all. A memory with no project has nothing to
|
|
42
|
+
* scope against and is global regardless of category.
|
|
43
|
+
*/
|
|
44
|
+
export declare function effectiveScope(mem: ScopableMemory): MemoryScope;
|
|
45
|
+
/**
|
|
46
|
+
* Recall-time enforcement: is `mem` visible to a caller currently working
|
|
47
|
+
* within `recallProject`?
|
|
48
|
+
*
|
|
49
|
+
* `recallProject` null/undefined means the caller isn't scoping their recall
|
|
50
|
+
* to a project — there is nothing to enforce, so every memory stays visible
|
|
51
|
+
* (unchanged pre-t-337 behaviour: an unscoped recall searches everything).
|
|
52
|
+
* Once a project is given: memories tagged to that exact project are always
|
|
53
|
+
* visible, everything else is visible only if its effective scope is
|
|
54
|
+
* 'global'.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isVisibleInProject(mem: ScopableMemory, recallProject: string | null | undefined): boolean;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory scope discipline (t-337b, from the 2026-09-01 affaan-m/ECC teardown).
|
|
3
|
+
*
|
|
4
|
+
* loom has always carried a `project` tag on memories, but nothing stopped a
|
|
5
|
+
* preference learned while working on one project from surfacing while the
|
|
6
|
+
* agent is working on an unrelated one — the tag was metadata, not a filter.
|
|
7
|
+
*
|
|
8
|
+
* Every memory now has an *effective scope*: 'project' (only visible when
|
|
9
|
+
* recalling within the project it was written for) or 'global' (visible
|
|
10
|
+
* everywhere). ECC's instinct-promotion model is the source: default to the
|
|
11
|
+
* narrower scope, promote to global only on deliberate signal — here, an
|
|
12
|
+
* explicit `scope` at write time, since loom has no cross-project id to
|
|
13
|
+
* watch for repetition against.
|
|
14
|
+
*
|
|
15
|
+
* The category split below follows ECC's scope-decision table as far as
|
|
16
|
+
* loom's own category vocabulary lets it: 'user' (a fact about the person)
|
|
17
|
+
* and 'reference' (a pointer to an external system) are inherently
|
|
18
|
+
* cross-project, so they default global even when a project happens to be
|
|
19
|
+
* attached. Every other category defaults to 'project' — including 'self'
|
|
20
|
+
* and 'feedback', where ECC's real split lives (language/framework/style/
|
|
21
|
+
* error-handling feedback is project-specific; security/git/tool-workflow
|
|
22
|
+
* feedback is universal). Category alone can't tell those apart; that's a
|
|
23
|
+
* per-memory authoring call, made with `scope: 'global'` at write time, not
|
|
24
|
+
* a product decision this module can bake in — see t-337's comment thread
|
|
25
|
+
* for the open question on backfilling existing records.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Default scope by category, applied only when a memory carries a project
|
|
29
|
+
* tag. An untagged memory has nothing to scope against and is global by
|
|
30
|
+
* construction — see `effectiveScope`.
|
|
31
|
+
*/
|
|
32
|
+
const CATEGORY_DEFAULT_SCOPE = {
|
|
33
|
+
user: 'global',
|
|
34
|
+
reference: 'global',
|
|
35
|
+
project: 'project',
|
|
36
|
+
pursuit: 'project',
|
|
37
|
+
episode: 'project',
|
|
38
|
+
self: 'project',
|
|
39
|
+
feedback: 'project',
|
|
40
|
+
};
|
|
41
|
+
/** Default scope for a category, 'project' for anything not in the table (safe default). */
|
|
42
|
+
export function defaultScopeForCategory(category) {
|
|
43
|
+
return CATEGORY_DEFAULT_SCOPE[category] ?? 'project';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The scope actually in force for a memory: the explicit override if one was
|
|
47
|
+
* set at write time, else the category default — but only when the memory
|
|
48
|
+
* is tagged to a project at all. A memory with no project has nothing to
|
|
49
|
+
* scope against and is global regardless of category.
|
|
50
|
+
*/
|
|
51
|
+
export function effectiveScope(mem) {
|
|
52
|
+
if (mem.scope === 'project' || mem.scope === 'global')
|
|
53
|
+
return mem.scope;
|
|
54
|
+
if (!mem.project)
|
|
55
|
+
return 'global';
|
|
56
|
+
return defaultScopeForCategory(mem.category);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Recall-time enforcement: is `mem` visible to a caller currently working
|
|
60
|
+
* within `recallProject`?
|
|
61
|
+
*
|
|
62
|
+
* `recallProject` null/undefined means the caller isn't scoping their recall
|
|
63
|
+
* to a project — there is nothing to enforce, so every memory stays visible
|
|
64
|
+
* (unchanged pre-t-337 behaviour: an unscoped recall searches everything).
|
|
65
|
+
* Once a project is given: memories tagged to that exact project are always
|
|
66
|
+
* visible, everything else is visible only if its effective scope is
|
|
67
|
+
* 'global'.
|
|
68
|
+
*/
|
|
69
|
+
export function isVisibleInProject(mem, recallProject) {
|
|
70
|
+
if (!recallProject)
|
|
71
|
+
return true;
|
|
72
|
+
if ((mem.project ?? null) === recallProject)
|
|
73
|
+
return true;
|
|
74
|
+
return effectiveScope(mem) === 'global';
|
|
75
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type { KnowledgeBackend, KnowledgePageInput, KnowledgeCitationInput, KnowledgePageWithCitations, KnowledgeQueryInput, KnowledgeWriteResult, KnowledgeArchiveInput, KnowledgeArchiveResult, KnowledgeRestoreInput, KnowledgeRestoreResult, KnowledgeSupersededInput, KnowledgeSupersededResult, KnowledgeMoveInput, KnowledgeMoveResult, KnowledgeMergeInput, KnowledgeMergeResult, KnowledgePurgeInput, KnowledgePurgeResult, KnowledgeVerifyInput, KnowledgeVerifyResult, KnowledgeRevision, KnowledgeRevisionMeta, KnowledgeRevisionRestoreInput, KnowledgeRevisionRestoreResult } from './types.js';
|
|
1
|
+
import type { KnowledgeBackend, KnowledgePageInput, KnowledgeCitationInput, KnowledgePageWithCitations, KnowledgeQueryInput, KnowledgeWriteResult, KnowledgeArchiveInput, KnowledgeArchiveResult, KnowledgeRestoreInput, KnowledgeRestoreResult, KnowledgeSupersededInput, KnowledgeSupersededResult, KnowledgeMoveInput, KnowledgeMoveResult, KnowledgeMergeInput, KnowledgeMergeResult, KnowledgePurgeInput, KnowledgePurgeResult, KnowledgeVerifyInput, KnowledgeVerifyResult, KnowledgeVerificationRecord, KnowledgeRevision, KnowledgeRevisionMeta, KnowledgeRevisionRestoreInput, KnowledgeRevisionRestoreResult, KnowledgeAccessStat } from './types.js';
|
|
2
2
|
export interface SqliteKnowledgeConfig {
|
|
3
3
|
/** Absolute path to the knowledge SQLite database file */
|
|
4
4
|
dbPath: string;
|
|
5
|
+
/**
|
|
6
|
+
* SQLite busy_timeout in ms — how long to retry a locked write before
|
|
7
|
+
* surfacing SQLITE_BUSY. Defaults to 5000; pass a small value in tests
|
|
8
|
+
* that exercise the SQLITE_BUSY path so they don't wait the full window.
|
|
9
|
+
*/
|
|
10
|
+
busyTimeoutMs?: number;
|
|
5
11
|
}
|
|
6
12
|
export declare class SqliteKnowledgeBackend implements KnowledgeBackend {
|
|
7
13
|
private readonly config;
|
|
@@ -10,6 +16,7 @@ export declare class SqliteKnowledgeBackend implements KnowledgeBackend {
|
|
|
10
16
|
writePage(input: KnowledgePageInput): Promise<KnowledgeWriteResult>;
|
|
11
17
|
getPage(slug: string, opts?: {
|
|
12
18
|
stampAccess?: boolean;
|
|
19
|
+
identity?: string;
|
|
13
20
|
}): Promise<KnowledgePageWithCitations | null>;
|
|
14
21
|
listPages(input?: KnowledgeQueryInput): Promise<KnowledgePageWithCitations[]>;
|
|
15
22
|
queryPages(input: KnowledgeQueryInput): Promise<KnowledgePageWithCitations[]>;
|
|
@@ -20,18 +27,31 @@ export declare class SqliteKnowledgeBackend implements KnowledgeBackend {
|
|
|
20
27
|
mergePages(input: KnowledgeMergeInput): Promise<KnowledgeMergeResult>;
|
|
21
28
|
purgePages(input: KnowledgePurgeInput): Promise<KnowledgePurgeResult>;
|
|
22
29
|
verifyPages(input: KnowledgeVerifyInput): Promise<KnowledgeVerifyResult>;
|
|
30
|
+
getVerifications(slug: string): Promise<KnowledgeVerificationRecord[]>;
|
|
23
31
|
listRevisions(slug: string): Promise<KnowledgeRevisionMeta[]>;
|
|
24
32
|
getRevision(revisionId: number): Promise<KnowledgeRevision | null>;
|
|
25
33
|
restoreRevision(input: KnowledgeRevisionRestoreInput): Promise<KnowledgeRevisionRestoreResult>;
|
|
26
34
|
/**
|
|
27
|
-
* Insert a body snapshot and prune beyond the per-page cap.
|
|
28
|
-
* transaction
|
|
29
|
-
*
|
|
35
|
+
* Insert a body snapshot and prune beyond the per-page cap. Always called
|
|
36
|
+
* inside a transaction owned by the caller (writePage or restoreRevision).
|
|
37
|
+
* The inner retryWrite calls run bare statements — safe inside an outer
|
|
38
|
+
* transaction; the outer transaction is the atomicity boundary.
|
|
30
39
|
*/
|
|
31
40
|
private snapshotRevision;
|
|
32
41
|
addCitations(slug: string, citations: KnowledgeCitationInput[]): Promise<number>;
|
|
42
|
+
getAccessStats(pageId: number): Promise<KnowledgeAccessStat[]>;
|
|
33
43
|
close(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Stamp a real read on each page id: bump the shared pages.hit_count /
|
|
46
|
+
* last_accessed aggregate (unchanged legacy behavior — this is what
|
|
47
|
+
* knowledge_maintain's cold/expansion ranking reads) AND upsert the
|
|
48
|
+
* per-identity row in knowledge_access (t-677 attribution). Both updates
|
|
49
|
+
* happen in one transaction so a crash between them can't desync the
|
|
50
|
+
* aggregate from the per-reader breakdown.
|
|
51
|
+
*/
|
|
52
|
+
private recordAccess;
|
|
34
53
|
private ensureOpen;
|
|
35
54
|
private fetchCitationsForPage;
|
|
36
55
|
private initSchema;
|
|
37
56
|
}
|
|
57
|
+
export declare function deriveSlaClass(anchor: string | null | undefined, domain: string): 'device' | 'software' | 'default';
|