context-vault 3.13.0 → 3.16.1
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/bin/cli.js +263 -414
- package/dist/error-log.d.ts +2 -0
- package/dist/error-log.d.ts.map +1 -1
- package/dist/error-log.js +31 -1
- package/dist/error-log.js.map +1 -1
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +4 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/server.js +23 -426
- package/dist/server.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +17 -0
- package/dist/status.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +26 -1
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/delete-context.d.ts +1 -1
- package/dist/tools/delete-context.d.ts.map +1 -1
- package/dist/tools/delete-context.js +15 -2
- package/dist/tools/delete-context.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +3 -2
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts +7 -15
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +570 -111
- package/dist/tools/list-context.js.map +1 -1
- package/dist/tools/publish-to-team.js +1 -1
- package/dist/tools/publish-to-team.js.map +1 -1
- package/dist/tools/save-context.js +2 -2
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts +20 -7
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +406 -439
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +4 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.js +8 -0
- package/node_modules/@context-vault/core/dist/categories.js.map +1 -1
- package/node_modules/@context-vault/core/dist/compact.d.ts +38 -0
- package/node_modules/@context-vault/core/dist/compact.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/compact.js +127 -0
- package/node_modules/@context-vault/core/dist/compact.js.map +1 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +12 -0
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +40 -4
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +6 -2
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +5 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +13 -1
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +50 -5
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/tier-analysis.d.ts +36 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.js +227 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.js.map +1 -0
- package/node_modules/@context-vault/core/dist/types.d.ts +12 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/watch.d.ts +21 -0
- package/node_modules/@context-vault/core/dist/watch.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/watch.js +230 -0
- package/node_modules/@context-vault/core/dist/watch.js.map +1 -0
- package/node_modules/@context-vault/core/package.json +13 -1
- package/node_modules/@context-vault/core/src/capture.ts +4 -0
- package/node_modules/@context-vault/core/src/categories.ts +8 -0
- package/node_modules/@context-vault/core/src/compact.ts +183 -0
- package/node_modules/@context-vault/core/src/config.ts +8 -0
- package/node_modules/@context-vault/core/src/db.ts +40 -4
- package/node_modules/@context-vault/core/src/main.ts +10 -0
- package/node_modules/@context-vault/core/src/search.ts +55 -4
- package/node_modules/@context-vault/core/src/tier-analysis.ts +299 -0
- package/node_modules/@context-vault/core/src/types.ts +10 -0
- package/node_modules/@context-vault/core/src/watch.ts +269 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +26 -1
- package/src/error-log.ts +30 -0
- package/src/register-tools.ts +4 -0
- package/src/server.ts +23 -423
- package/src/status.ts +17 -0
- package/src/tools/context-status.ts +30 -1
- package/src/tools/delete-context.ts +10 -5
- package/src/tools/get-context.ts +3 -2
- package/src/tools/list-context.ts +620 -119
- package/src/tools/publish-to-team.ts +1 -1
- package/src/tools/save-context.ts +2 -2
- package/src/tools/session-start.ts +444 -484
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Types
|
|
2
2
|
export type {
|
|
3
3
|
VaultConfig,
|
|
4
|
+
WatchConfig,
|
|
4
5
|
RemoteConfig,
|
|
5
6
|
RecallConfig,
|
|
6
7
|
ConsolidationConfig,
|
|
@@ -97,6 +98,7 @@ export {
|
|
|
97
98
|
recencyDecayScore,
|
|
98
99
|
dotProduct,
|
|
99
100
|
reciprocalRankFusion,
|
|
101
|
+
computeHeatForEntry,
|
|
100
102
|
} from './search.js';
|
|
101
103
|
|
|
102
104
|
// Capture
|
|
@@ -105,5 +107,13 @@ export { writeEntry, updateEntryFile, captureAndIndex } from './capture.js';
|
|
|
105
107
|
// Indexing
|
|
106
108
|
export { shouldIndex } from './indexing.js';
|
|
107
109
|
|
|
110
|
+
// Compact
|
|
111
|
+
export { compact, restoreCompactedBody } from './compact.js';
|
|
112
|
+
export type { CompactCtx, CompactOptions, CompactResult } from './compact.js';
|
|
113
|
+
|
|
108
114
|
// Ingest URL
|
|
109
115
|
export { htmlToMarkdown, extractHtmlContent, ingestUrl } from './ingest-url.js';
|
|
116
|
+
|
|
117
|
+
// Watch
|
|
118
|
+
export { startWatcher } from './watch.js';
|
|
119
|
+
export type { WatcherOptions, VaultWatcher } from './watch.js';
|
|
@@ -124,6 +124,7 @@ export async function hybridSearch(
|
|
|
124
124
|
includeSuperseeded = false,
|
|
125
125
|
includeEphemeral = false,
|
|
126
126
|
contextEmbedding = null,
|
|
127
|
+
trackMeta,
|
|
127
128
|
} = opts;
|
|
128
129
|
|
|
129
130
|
const rowMap = new Map<string, VaultEntry>();
|
|
@@ -339,7 +340,8 @@ export async function hybridSearch(
|
|
|
339
340
|
entry.recall_count ?? 0,
|
|
340
341
|
entry.last_recalled_at ?? null
|
|
341
342
|
);
|
|
342
|
-
|
|
343
|
+
const durable = entry.tier === 'durable' ? 1.3 : 1.0;
|
|
344
|
+
rrfScores.set(id, (rrfScores.get(id) ?? 0) * boost * recall * durable);
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
const candidates: SearchResult[] = [...rowMap.values()].map((entry) => ({
|
|
@@ -389,13 +391,13 @@ export async function hybridSearch(
|
|
|
389
391
|
if (vec) selectedVecs.push(vec);
|
|
390
392
|
}
|
|
391
393
|
const dedupedPage = injectDiscoverySlots(selected.slice(offset, offset + limit), candidates);
|
|
392
|
-
trackAccess(ctx, dedupedPage);
|
|
394
|
+
trackAccess(ctx, dedupedPage, trackMeta);
|
|
393
395
|
return dedupedPage;
|
|
394
396
|
}
|
|
395
397
|
|
|
396
398
|
const page = candidates.slice(offset, offset + limit);
|
|
397
399
|
const finalPage = injectDiscoverySlots(page, candidates);
|
|
398
|
-
trackAccess(ctx, finalPage);
|
|
400
|
+
trackAccess(ctx, finalPage, trackMeta);
|
|
399
401
|
return finalPage;
|
|
400
402
|
}
|
|
401
403
|
|
|
@@ -428,7 +430,7 @@ export function setSessionId(id: string): void {
|
|
|
428
430
|
_sessionId = id;
|
|
429
431
|
}
|
|
430
432
|
|
|
431
|
-
export function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
|
|
433
|
+
export function trackAccess(ctx: BaseCtx, entries: SearchResult[], meta?: { query?: string; sessionGoal?: string }): void {
|
|
432
434
|
if (!entries.length) return;
|
|
433
435
|
|
|
434
436
|
const ids = entries.map((e) => e.id);
|
|
@@ -478,4 +480,53 @@ export function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
|
|
|
478
480
|
// Non-fatal
|
|
479
481
|
}
|
|
480
482
|
}
|
|
483
|
+
|
|
484
|
+
// Write per-access rows to access_log for adaptive tiering
|
|
485
|
+
try {
|
|
486
|
+
const insertLog = ctx.db.prepare(
|
|
487
|
+
`INSERT INTO access_log (entry_id, query, session_id, session_goal, accessed_at) VALUES (?, ?, ?, ?, ?)`
|
|
488
|
+
);
|
|
489
|
+
const sid = _sessionId || 'default';
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
insertLog.run(entry.id, meta?.query ?? null, sid, meta?.sessionGoal ?? null, now);
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
// Non-fatal
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Compute and write heat_tier for each accessed entry
|
|
498
|
+
try {
|
|
499
|
+
const updateHeat = ctx.db.prepare(`UPDATE vault SET heat_tier = ? WHERE id = ?`);
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
const recallCount = (entry.recall_count ?? 0) + 1;
|
|
502
|
+
const recallSessions = entry.recall_sessions ?? 0;
|
|
503
|
+
// Just recalled, so recency bonus is 10 (0 days since last recall)
|
|
504
|
+
const heat = (recallCount * 3) + (recallSessions * 2) + 10;
|
|
505
|
+
const tier = heat > 10 ? 'hot' : heat >= 1 ? 'warm' : null;
|
|
506
|
+
updateHeat.run(tier, entry.id);
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// Non-fatal
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function computeHeatForEntry(entry: { recall_count: number; recall_sessions: number; last_recalled_at: string | null; created_at: string }): { heat: number; tier: 'hot' | 'warm' | 'cold' | null } {
|
|
514
|
+
const recallCount = entry.recall_count ?? 0;
|
|
515
|
+
const recallSessions = entry.recall_sessions ?? 0;
|
|
516
|
+
|
|
517
|
+
let recencyBonus = 0;
|
|
518
|
+
if (entry.last_recalled_at) {
|
|
519
|
+
const daysSinceRecall = (Date.now() - new Date(entry.last_recalled_at).getTime()) / 86400000;
|
|
520
|
+
recencyBonus = Math.max(0, 10 - daysSinceRecall);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const heat = (recallCount * 3) + (recallSessions * 2) + recencyBonus;
|
|
524
|
+
|
|
525
|
+
if (heat > 10) return { heat, tier: 'hot' };
|
|
526
|
+
if (heat >= 1) return { heat, tier: 'warm' };
|
|
527
|
+
|
|
528
|
+
const ageDays = (Date.now() - new Date(entry.created_at).getTime()) / 86400000;
|
|
529
|
+
if (ageDays > 30) return { heat, tier: 'cold' };
|
|
530
|
+
|
|
531
|
+
return { heat, tier: null };
|
|
481
532
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { BaseCtx, CaptureResult } from './types.js';
|
|
2
|
+
import { captureAndIndex } from './capture.js';
|
|
3
|
+
|
|
4
|
+
export interface TierAnalysisOptions {
|
|
5
|
+
hotThreshold?: number;
|
|
6
|
+
hotDays?: number;
|
|
7
|
+
coldDays?: number;
|
|
8
|
+
coAccessThreshold?: number;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HotEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string | null;
|
|
15
|
+
accessCount: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ColdEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string | null;
|
|
21
|
+
lastAccessed: string | null;
|
|
22
|
+
coldSummary: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CoAccessBundle {
|
|
26
|
+
entries: Array<{ id: string; title: string | null }>;
|
|
27
|
+
totalWeight: number;
|
|
28
|
+
briefId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TierAnalysisReport {
|
|
32
|
+
hotEntries: HotEntry[];
|
|
33
|
+
coldEntries: ColdEntry[];
|
|
34
|
+
warmReset: number;
|
|
35
|
+
bundles: CoAccessBundle[];
|
|
36
|
+
accessLogPruned: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function computeColdSummary(body: string): string {
|
|
40
|
+
const firstSentence = body.split(/[.!?]\s/)[0];
|
|
41
|
+
return (firstSentence || body).slice(0, 200).trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findClusters(pairs: Array<{ a: string; b: string; count: number }>): Map<string, Set<string>> {
|
|
45
|
+
const parent = new Map<string, string>();
|
|
46
|
+
function find(x: string): string {
|
|
47
|
+
if (!parent.has(x)) parent.set(x, x);
|
|
48
|
+
if (parent.get(x) !== x) parent.set(x, find(parent.get(x)!));
|
|
49
|
+
return parent.get(x)!;
|
|
50
|
+
}
|
|
51
|
+
function union(a: string, b: string) {
|
|
52
|
+
const ra = find(a);
|
|
53
|
+
const rb = find(b);
|
|
54
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const { a, b } of pairs) {
|
|
58
|
+
union(a, b);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const clusters = new Map<string, Set<string>>();
|
|
62
|
+
for (const node of parent.keys()) {
|
|
63
|
+
const root = find(node);
|
|
64
|
+
if (!clusters.has(root)) clusters.set(root, new Set());
|
|
65
|
+
clusters.get(root)!.add(node);
|
|
66
|
+
}
|
|
67
|
+
return clusters;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runTierAnalysis(
|
|
71
|
+
ctx: BaseCtx,
|
|
72
|
+
opts: TierAnalysisOptions = {}
|
|
73
|
+
): Promise<TierAnalysisReport> {
|
|
74
|
+
const hotThreshold = opts.hotThreshold ?? 5;
|
|
75
|
+
const hotDays = opts.hotDays ?? 7;
|
|
76
|
+
const coldDays = opts.coldDays ?? 30;
|
|
77
|
+
const coAccessThreshold = opts.coAccessThreshold ?? 3;
|
|
78
|
+
const dryRun = opts.dryRun ?? false;
|
|
79
|
+
|
|
80
|
+
const report: TierAnalysisReport = {
|
|
81
|
+
hotEntries: [],
|
|
82
|
+
coldEntries: [],
|
|
83
|
+
warmReset: 0,
|
|
84
|
+
bundles: [],
|
|
85
|
+
accessLogPruned: 0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// --- Hot detection ---
|
|
89
|
+
const hotRows = ctx.db
|
|
90
|
+
.prepare(
|
|
91
|
+
`SELECT v.id, v.title, COUNT(a.id) as access_count
|
|
92
|
+
FROM vault v
|
|
93
|
+
JOIN access_log a ON a.entry_id = v.id
|
|
94
|
+
WHERE a.accessed_at >= datetime('now', ? || ' days')
|
|
95
|
+
AND v.tier != 'ephemeral'
|
|
96
|
+
AND v.superseded_by IS NULL
|
|
97
|
+
GROUP BY v.id
|
|
98
|
+
HAVING COUNT(a.id) >= ?`
|
|
99
|
+
)
|
|
100
|
+
.all(`-${hotDays}`, hotThreshold) as Array<{ id: string; title: string | null; access_count: number }>;
|
|
101
|
+
|
|
102
|
+
for (const row of hotRows) {
|
|
103
|
+
report.hotEntries.push({
|
|
104
|
+
id: row.id,
|
|
105
|
+
title: row.title,
|
|
106
|
+
accessCount: row.access_count,
|
|
107
|
+
});
|
|
108
|
+
if (!dryRun) {
|
|
109
|
+
ctx.db.prepare(`UPDATE vault SET heat_tier = 'hot' WHERE id = ?`).run(row.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Cold detection ---
|
|
114
|
+
const coldDayStr = `-${coldDays}`;
|
|
115
|
+
const coldRows = ctx.db
|
|
116
|
+
.prepare(
|
|
117
|
+
`SELECT v.id, v.title, v.body, v.last_accessed_at
|
|
118
|
+
FROM vault v
|
|
119
|
+
WHERE v.tier != 'durable'
|
|
120
|
+
AND v.superseded_by IS NULL
|
|
121
|
+
AND v.category != 'event'
|
|
122
|
+
AND (v.last_accessed_at IS NULL OR v.last_accessed_at < datetime('now', ? || ' days'))
|
|
123
|
+
AND NOT EXISTS (
|
|
124
|
+
SELECT 1 FROM access_log a
|
|
125
|
+
WHERE a.entry_id = v.id
|
|
126
|
+
AND a.accessed_at >= datetime('now', ? || ' days')
|
|
127
|
+
)`
|
|
128
|
+
)
|
|
129
|
+
.all(coldDayStr, coldDayStr) as Array<{ id: string; title: string | null; body: string; last_accessed_at: string | null }>;
|
|
130
|
+
|
|
131
|
+
for (const row of coldRows) {
|
|
132
|
+
const summary = computeColdSummary(row.body);
|
|
133
|
+
report.coldEntries.push({
|
|
134
|
+
id: row.id,
|
|
135
|
+
title: row.title,
|
|
136
|
+
lastAccessed: row.last_accessed_at,
|
|
137
|
+
coldSummary: summary,
|
|
138
|
+
});
|
|
139
|
+
if (!dryRun) {
|
|
140
|
+
ctx.db
|
|
141
|
+
.prepare(
|
|
142
|
+
`UPDATE vault SET heat_tier = 'cold', meta = json_set(COALESCE(meta, '{}'), '$.cold_summary', ?) WHERE id = ?`
|
|
143
|
+
)
|
|
144
|
+
.run(summary, row.id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Warm reset: clear stale heat_tier from entries that no longer qualify ---
|
|
149
|
+
const hotIds = new Set(report.hotEntries.map((e) => e.id));
|
|
150
|
+
const coldIds = new Set(report.coldEntries.map((e) => e.id));
|
|
151
|
+
if (!dryRun) {
|
|
152
|
+
// Clear heat_tier from entries that are no longer hot or cold
|
|
153
|
+
const staleHeat = ctx.db
|
|
154
|
+
.prepare(
|
|
155
|
+
`SELECT id, heat_tier FROM vault WHERE heat_tier IS NOT NULL AND superseded_by IS NULL`
|
|
156
|
+
)
|
|
157
|
+
.all() as Array<{ id: string; heat_tier: string }>;
|
|
158
|
+
|
|
159
|
+
const clearStmt = ctx.db.prepare(`UPDATE vault SET heat_tier = NULL WHERE id = ?`);
|
|
160
|
+
for (const row of staleHeat) {
|
|
161
|
+
if ((row.heat_tier === 'hot' && !hotIds.has(row.id)) ||
|
|
162
|
+
(row.heat_tier === 'cold' && !coldIds.has(row.id))) {
|
|
163
|
+
clearStmt.run(row.id);
|
|
164
|
+
report.warmReset++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Co-access bundle detection ---
|
|
170
|
+
const coAccessPairs = ctx.db
|
|
171
|
+
.prepare(
|
|
172
|
+
`SELECT c.entry_a, c.entry_b, c.count,
|
|
173
|
+
a.title as title_a, b.title as title_b
|
|
174
|
+
FROM co_retrievals c
|
|
175
|
+
JOIN vault a ON a.id = c.entry_a
|
|
176
|
+
JOIN vault b ON b.id = c.entry_b
|
|
177
|
+
WHERE c.count >= ?
|
|
178
|
+
AND a.superseded_by IS NULL
|
|
179
|
+
AND b.superseded_by IS NULL
|
|
180
|
+
ORDER BY c.count DESC
|
|
181
|
+
LIMIT 50`
|
|
182
|
+
)
|
|
183
|
+
.all(coAccessThreshold) as Array<{
|
|
184
|
+
entry_a: string;
|
|
185
|
+
entry_b: string;
|
|
186
|
+
count: number;
|
|
187
|
+
title_a: string | null;
|
|
188
|
+
title_b: string | null;
|
|
189
|
+
}>;
|
|
190
|
+
|
|
191
|
+
if (coAccessPairs.length > 0) {
|
|
192
|
+
const titleMap = new Map<string, string | null>();
|
|
193
|
+
const pairsForClustering: Array<{ a: string; b: string; count: number }> = [];
|
|
194
|
+
for (const p of coAccessPairs) {
|
|
195
|
+
titleMap.set(p.entry_a, p.title_a);
|
|
196
|
+
titleMap.set(p.entry_b, p.title_b);
|
|
197
|
+
pairsForClustering.push({ a: p.entry_a, b: p.entry_b, count: p.count });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const clusters = findClusters(pairsForClustering);
|
|
201
|
+
|
|
202
|
+
for (const [, members] of clusters) {
|
|
203
|
+
if (members.size < 3) continue;
|
|
204
|
+
|
|
205
|
+
const entries = [...members].map((id) => ({
|
|
206
|
+
id,
|
|
207
|
+
title: titleMap.get(id) ?? null,
|
|
208
|
+
}));
|
|
209
|
+
const totalWeight = pairsForClustering
|
|
210
|
+
.filter((p) => members.has(p.a) && members.has(p.b))
|
|
211
|
+
.reduce((sum, p) => sum + p.count, 0);
|
|
212
|
+
|
|
213
|
+
const bundle: CoAccessBundle = { entries, totalWeight };
|
|
214
|
+
|
|
215
|
+
if (!dryRun) {
|
|
216
|
+
// Check if a bundle brief already exists for this cluster
|
|
217
|
+
const entryIds = entries.map((e) => e.id).sort();
|
|
218
|
+
const bundleTag = `co-access-bundle:${entryIds.slice(0, 3).join('-')}`;
|
|
219
|
+
const existing = ctx.db
|
|
220
|
+
.prepare(`SELECT id FROM vault WHERE kind = 'brief' AND tags LIKE ?`)
|
|
221
|
+
.get(`%${bundleTag}%`);
|
|
222
|
+
|
|
223
|
+
if (!existing) {
|
|
224
|
+
const body = [
|
|
225
|
+
`Co-access bundle (auto-generated by adaptive tiering).`,
|
|
226
|
+
`These entries are frequently retrieved together (total weight: ${totalWeight}).`,
|
|
227
|
+
'',
|
|
228
|
+
...entries.map((e) => `- **${e.title || '(untitled)'}** (\`${e.id}\`)`),
|
|
229
|
+
].join('\n');
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result: CaptureResult = await captureAndIndex(ctx, {
|
|
233
|
+
kind: 'brief',
|
|
234
|
+
title: `Co-access bundle: ${entries
|
|
235
|
+
.slice(0, 3)
|
|
236
|
+
.map((e) => e.title || e.id.slice(-8))
|
|
237
|
+
.join(', ')}`,
|
|
238
|
+
body,
|
|
239
|
+
tags: ['co-access-bundle', bundleTag, 'auto-tiering'],
|
|
240
|
+
tier: 'working',
|
|
241
|
+
related_to: entryIds,
|
|
242
|
+
});
|
|
243
|
+
bundle.briefId = result.id;
|
|
244
|
+
} catch {
|
|
245
|
+
// Non-fatal
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
report.bundles.push(bundle);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- Housekeeping: prune old access_log rows ---
|
|
255
|
+
if (!dryRun) {
|
|
256
|
+
try {
|
|
257
|
+
const pruneResult = ctx.db
|
|
258
|
+
.prepare(`DELETE FROM access_log WHERE accessed_at < datetime('now', '-90 days')`)
|
|
259
|
+
.run();
|
|
260
|
+
report.accessLogPruned = (pruneResult as any)?.changes ?? 0;
|
|
261
|
+
} catch {
|
|
262
|
+
// Non-fatal
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// --- Persist tiering config as vault entry ---
|
|
267
|
+
if (!dryRun) {
|
|
268
|
+
try {
|
|
269
|
+
const configBody = [
|
|
270
|
+
`Adaptive tiering parameters last applied: ${new Date().toISOString()}`,
|
|
271
|
+
'',
|
|
272
|
+
`- Hot threshold: ${hotThreshold} accesses / ${hotDays} days`,
|
|
273
|
+
`- Cold threshold: ${coldDays} days no access`,
|
|
274
|
+
`- Co-access bundle threshold: ${coAccessThreshold}`,
|
|
275
|
+
`- Access log retention: 90 days`,
|
|
276
|
+
'',
|
|
277
|
+
`## Last Run Results`,
|
|
278
|
+
`- Hot entries: ${report.hotEntries.length}`,
|
|
279
|
+
`- Cold entries: ${report.coldEntries.length}`,
|
|
280
|
+
`- Warm resets: ${report.warmReset}`,
|
|
281
|
+
`- Bundles created: ${report.bundles.filter((b) => b.briefId).length}`,
|
|
282
|
+
`- Access log pruned: ${report.accessLogPruned} rows`,
|
|
283
|
+
].join('\n');
|
|
284
|
+
|
|
285
|
+
await captureAndIndex(ctx, {
|
|
286
|
+
kind: 'reference',
|
|
287
|
+
title: 'Tiering Config',
|
|
288
|
+
body: configBody,
|
|
289
|
+
identity_key: 'tiering-config',
|
|
290
|
+
tier: 'durable',
|
|
291
|
+
tags: ['tiering', 'config'],
|
|
292
|
+
});
|
|
293
|
+
} catch {
|
|
294
|
+
// Non-fatal
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return report;
|
|
299
|
+
}
|
|
@@ -22,6 +22,12 @@ export interface RemoteConfig {
|
|
|
22
22
|
teamId?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface WatchConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
path?: string;
|
|
28
|
+
debounceMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export interface VaultConfig {
|
|
26
32
|
vaultDir: string;
|
|
27
33
|
dataDir: string;
|
|
@@ -39,6 +45,7 @@ export interface VaultConfig {
|
|
|
39
45
|
autoInsights: AutoInsightsConfig;
|
|
40
46
|
indexing: IndexingConfig;
|
|
41
47
|
remote?: RemoteConfig;
|
|
48
|
+
watch?: WatchConfig;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
export interface RecallConfig {
|
|
@@ -106,6 +113,7 @@ export interface VaultEntry {
|
|
|
106
113
|
recall_count: number;
|
|
107
114
|
recall_sessions: number;
|
|
108
115
|
last_recalled_at: string | null;
|
|
116
|
+
heat_tier: string | null;
|
|
109
117
|
rowid?: number;
|
|
110
118
|
}
|
|
111
119
|
|
|
@@ -204,4 +212,6 @@ export interface SearchOptions {
|
|
|
204
212
|
includeEphemeral?: boolean;
|
|
205
213
|
/** Pre-computed context embedding for contextual reinstatement boosting. */
|
|
206
214
|
contextEmbedding?: Float32Array | null;
|
|
215
|
+
/** Metadata forwarded to access_log rows written by trackAccess. */
|
|
216
|
+
trackMeta?: { query?: string; sessionGoal?: string };
|
|
207
217
|
}
|