@context-vault/core 2.14.0 → 2.17.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/package.json +1 -1
- package/src/capture/index.js +11 -0
- package/src/consolidation/index.js +112 -0
- package/src/constants.js +7 -2
- package/src/core/categories.js +10 -0
- package/src/core/config.js +46 -2
- package/src/core/status.js +28 -2
- package/src/index/db.js +102 -9
- package/src/index/index.js +48 -21
- package/src/index.js +4 -0
- package/src/retrieve/index.js +268 -64
- package/src/server/tools/context-status.js +7 -0
- package/src/server/tools/create-snapshot.js +12 -3
- package/src/server/tools/get-context.js +311 -11
- package/src/server/tools/ingest-project.js +244 -0
- package/src/server/tools/list-buckets.js +116 -0
- package/src/server/tools/save-context.js +190 -19
- package/src/server/tools/session-start.js +285 -0
- package/src/server/tools.js +6 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok } from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
export const name = "list_buckets";
|
|
5
|
+
|
|
6
|
+
export const description =
|
|
7
|
+
"List all registered bucket entities in the vault. Buckets are named scopes used to group entries via 'bucket:' prefixed tags. Returns each bucket's name, description, parent, and optional entry count.";
|
|
8
|
+
|
|
9
|
+
export const inputSchema = {
|
|
10
|
+
include_counts: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
"Include count of entries tagged with each bucket (default true). Set false to skip the count queries for faster response.",
|
|
15
|
+
),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} args
|
|
20
|
+
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
|
|
21
|
+
* @param {import('../types.js').ToolShared} shared
|
|
22
|
+
*/
|
|
23
|
+
export async function handler(
|
|
24
|
+
{ include_counts = true },
|
|
25
|
+
ctx,
|
|
26
|
+
{ ensureIndexed, reindexFailed },
|
|
27
|
+
) {
|
|
28
|
+
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
29
|
+
|
|
30
|
+
await ensureIndexed();
|
|
31
|
+
|
|
32
|
+
const userClause = userId !== undefined ? "AND user_id = ?" : "";
|
|
33
|
+
const userParams = userId !== undefined ? [userId] : [];
|
|
34
|
+
|
|
35
|
+
const buckets = ctx.db
|
|
36
|
+
.prepare(
|
|
37
|
+
`SELECT id, title, identity_key, body, tags, meta, created_at, updated_at
|
|
38
|
+
FROM vault
|
|
39
|
+
WHERE kind = 'bucket'
|
|
40
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
41
|
+
AND superseded_by IS NULL
|
|
42
|
+
${userClause}
|
|
43
|
+
ORDER BY title ASC`,
|
|
44
|
+
)
|
|
45
|
+
.all(...userParams);
|
|
46
|
+
|
|
47
|
+
if (!buckets.length) {
|
|
48
|
+
return ok(
|
|
49
|
+
"No buckets registered.\n\nCreate one with `save_context(kind: \"bucket\", identity_key: \"bucket:myproject\", title: \"My Project\", body: \"...\")` to register a bucket.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lines = [];
|
|
54
|
+
if (reindexFailed) {
|
|
55
|
+
lines.push(
|
|
56
|
+
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
lines.push(`## Registered Buckets (${buckets.length})\n`);
|
|
60
|
+
|
|
61
|
+
for (const b of buckets) {
|
|
62
|
+
let meta = {};
|
|
63
|
+
if (b.meta) {
|
|
64
|
+
try {
|
|
65
|
+
meta = typeof b.meta === "string" ? JSON.parse(b.meta) : b.meta;
|
|
66
|
+
} catch {
|
|
67
|
+
meta = {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bucketTags = b.tags ? JSON.parse(b.tags) : [];
|
|
72
|
+
const name = b.identity_key
|
|
73
|
+
? b.identity_key.replace(/^bucket:/, "")
|
|
74
|
+
: b.title || b.id;
|
|
75
|
+
const parent = meta.parent || null;
|
|
76
|
+
|
|
77
|
+
let entryCount = null;
|
|
78
|
+
if (include_counts && b.identity_key) {
|
|
79
|
+
const countUserClause =
|
|
80
|
+
userId !== undefined ? "AND user_id = ?" : "";
|
|
81
|
+
const countParams = userId !== undefined ? [userId] : [];
|
|
82
|
+
const row = ctx.db
|
|
83
|
+
.prepare(
|
|
84
|
+
`SELECT COUNT(*) as c FROM vault
|
|
85
|
+
WHERE tags LIKE ?
|
|
86
|
+
AND kind != 'bucket'
|
|
87
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
88
|
+
AND superseded_by IS NULL
|
|
89
|
+
${countUserClause}`,
|
|
90
|
+
)
|
|
91
|
+
.get(`%"${b.identity_key}"%`, ...countParams);
|
|
92
|
+
entryCount = row ? row.c : 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const titleDisplay = b.title || name;
|
|
96
|
+
const headerParts = [`**${titleDisplay}**`];
|
|
97
|
+
if (b.identity_key) headerParts.push(`\`${b.identity_key}\``);
|
|
98
|
+
if (parent) headerParts.push(`parent: ${parent}`);
|
|
99
|
+
if (entryCount !== null) headerParts.push(`${entryCount} entries`);
|
|
100
|
+
lines.push(`- ${headerParts.join(" — ")}`);
|
|
101
|
+
|
|
102
|
+
if (b.body) {
|
|
103
|
+
const preview = b.body.replace(/\n+/g, " ").trim().slice(0, 120);
|
|
104
|
+
lines.push(` ${preview}${b.body.length > 120 ? "…" : ""}`);
|
|
105
|
+
}
|
|
106
|
+
if (bucketTags.length) {
|
|
107
|
+
lines.push(` tags: ${bucketTags.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
lines.push(
|
|
112
|
+
"\n_Register a new bucket with `save_context(kind: \"bucket\", identity_key: \"bucket:<name>\", title: \"...\", body: \"...\")`_",
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return ok(lines.join("\n"));
|
|
116
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { captureAndIndex, updateEntryFile } from "../../capture/index.js";
|
|
3
3
|
import { indexEntry } from "../../index/index.js";
|
|
4
|
-
import { categoryFor } from "../../core/categories.js";
|
|
4
|
+
import { categoryFor, defaultTierFor } from "../../core/categories.js";
|
|
5
5
|
import { normalizeKind } from "../../core/files.js";
|
|
6
6
|
import { ok, err, ensureVaultExists, ensureValidKind } from "../helpers.js";
|
|
7
7
|
import { maybeShowFeedbackPrompt } from "../../core/telemetry.js";
|
|
@@ -17,8 +17,16 @@ import {
|
|
|
17
17
|
} from "../../constants.js";
|
|
18
18
|
|
|
19
19
|
const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
|
|
20
|
+
const SKIP_THRESHOLD = 0.95;
|
|
21
|
+
const UPDATE_THRESHOLD = 0.85;
|
|
20
22
|
|
|
21
|
-
async function findSimilar(
|
|
23
|
+
async function findSimilar(
|
|
24
|
+
ctx,
|
|
25
|
+
embedding,
|
|
26
|
+
threshold,
|
|
27
|
+
userId,
|
|
28
|
+
{ hydrate = false } = {},
|
|
29
|
+
) {
|
|
22
30
|
try {
|
|
23
31
|
const vecCount = ctx.db
|
|
24
32
|
.prepare("SELECT COUNT(*) as c FROM vault_vec")
|
|
@@ -35,14 +43,15 @@ async function findSimilar(ctx, embedding, threshold, userId) {
|
|
|
35
43
|
|
|
36
44
|
const rowids = vecRows.map((vr) => vr.rowid);
|
|
37
45
|
const placeholders = rowids.map(() => "?").join(",");
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const columns = hydrate
|
|
47
|
+
? "rowid, id, title, body, kind, tags, category, user_id, updated_at"
|
|
48
|
+
: "rowid, id, title, category, user_id";
|
|
49
|
+
const hydratedRows = ctx.db
|
|
50
|
+
.prepare(`SELECT ${columns} FROM vault WHERE rowid IN (${placeholders})`)
|
|
42
51
|
.all(...rowids);
|
|
43
52
|
|
|
44
53
|
const byRowid = new Map();
|
|
45
|
-
for (const row of
|
|
54
|
+
for (const row of hydratedRows) byRowid.set(row.rowid, row);
|
|
46
55
|
|
|
47
56
|
const results = [];
|
|
48
57
|
for (const vr of vecRows) {
|
|
@@ -52,7 +61,14 @@ async function findSimilar(ctx, embedding, threshold, userId) {
|
|
|
52
61
|
if (!row) continue;
|
|
53
62
|
if (userId !== undefined && row.user_id !== userId) continue;
|
|
54
63
|
if (row.category === "entity") continue;
|
|
55
|
-
|
|
64
|
+
const entry = { id: row.id, title: row.title, score: similarity };
|
|
65
|
+
if (hydrate) {
|
|
66
|
+
entry.body = row.body;
|
|
67
|
+
entry.kind = row.kind;
|
|
68
|
+
entry.tags = row.tags;
|
|
69
|
+
entry.updated_at = row.updated_at;
|
|
70
|
+
}
|
|
71
|
+
results.push(entry);
|
|
56
72
|
}
|
|
57
73
|
return results;
|
|
58
74
|
} catch {
|
|
@@ -73,6 +89,68 @@ function formatSimilarWarning(similar) {
|
|
|
73
89
|
return lines.join("\n");
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
export function buildConflictCandidates(similarEntries) {
|
|
93
|
+
return similarEntries.map((entry) => {
|
|
94
|
+
let suggested_action;
|
|
95
|
+
let reasoning_context;
|
|
96
|
+
|
|
97
|
+
if (entry.score >= SKIP_THRESHOLD) {
|
|
98
|
+
suggested_action = "SKIP";
|
|
99
|
+
reasoning_context =
|
|
100
|
+
`Near-duplicate detected (${(entry.score * 100).toFixed(0)}% similarity)` +
|
|
101
|
+
`${entry.title ? ` with "${entry.title}"` : ""}. ` +
|
|
102
|
+
`Content is nearly identical — saving would create a redundant entry. ` +
|
|
103
|
+
`Use save_context with id: "${entry.id}" to update instead, or skip saving entirely.`;
|
|
104
|
+
} else if (entry.score >= UPDATE_THRESHOLD) {
|
|
105
|
+
suggested_action = "UPDATE";
|
|
106
|
+
reasoning_context =
|
|
107
|
+
`High content similarity (${(entry.score * 100).toFixed(0)}%)` +
|
|
108
|
+
`${entry.title ? ` with "${entry.title}"` : ""}. ` +
|
|
109
|
+
`Likely the same knowledge — consider updating this entry via save_context with id: "${entry.id}".`;
|
|
110
|
+
} else {
|
|
111
|
+
suggested_action = "ADD";
|
|
112
|
+
reasoning_context =
|
|
113
|
+
`Moderate similarity (${(entry.score * 100).toFixed(0)}%)` +
|
|
114
|
+
`${entry.title ? ` with "${entry.title}"` : ""}. ` +
|
|
115
|
+
`Content is related but distinct enough to coexist.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let parsedTags = [];
|
|
119
|
+
if (entry.tags) {
|
|
120
|
+
try {
|
|
121
|
+
parsedTags =
|
|
122
|
+
typeof entry.tags === "string" ? JSON.parse(entry.tags) : entry.tags;
|
|
123
|
+
} catch {
|
|
124
|
+
parsedTags = [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
id: entry.id,
|
|
130
|
+
title: entry.title || null,
|
|
131
|
+
body: entry.body || null,
|
|
132
|
+
kind: entry.kind || null,
|
|
133
|
+
tags: parsedTags,
|
|
134
|
+
score: entry.score,
|
|
135
|
+
updated_at: entry.updated_at || null,
|
|
136
|
+
suggested_action,
|
|
137
|
+
reasoning_context,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatConflictSuggestions(candidates) {
|
|
143
|
+
const lines = ["", "── Conflict Resolution Suggestions ──"];
|
|
144
|
+
for (const c of candidates) {
|
|
145
|
+
const titleDisplay = c.title ? `"${c.title}"` : "(no title)";
|
|
146
|
+
lines.push(
|
|
147
|
+
` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}`,
|
|
148
|
+
);
|
|
149
|
+
lines.push(` ${c.reasoning_context}`);
|
|
150
|
+
}
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
76
154
|
/**
|
|
77
155
|
* Validate input fields for save_context. Returns an error response or null.
|
|
78
156
|
*/
|
|
@@ -189,7 +267,9 @@ export const inputSchema = {
|
|
|
189
267
|
tags: z
|
|
190
268
|
.array(z.string())
|
|
191
269
|
.optional()
|
|
192
|
-
.describe(
|
|
270
|
+
.describe(
|
|
271
|
+
"Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval.",
|
|
272
|
+
),
|
|
193
273
|
meta: z
|
|
194
274
|
.any()
|
|
195
275
|
.optional()
|
|
@@ -214,6 +294,25 @@ export const inputSchema = {
|
|
|
214
294
|
.describe(
|
|
215
295
|
"Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.",
|
|
216
296
|
),
|
|
297
|
+
source_files: z
|
|
298
|
+
.array(
|
|
299
|
+
z.object({
|
|
300
|
+
path: z.string().describe("File path (absolute or relative to cwd)"),
|
|
301
|
+
hash: z
|
|
302
|
+
.string()
|
|
303
|
+
.describe("SHA-256 hash of the file contents at observation time"),
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
306
|
+
.optional()
|
|
307
|
+
.describe(
|
|
308
|
+
"Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.",
|
|
309
|
+
),
|
|
310
|
+
tier: z
|
|
311
|
+
.enum(["ephemeral", "working", "durable"])
|
|
312
|
+
.optional()
|
|
313
|
+
.describe(
|
|
314
|
+
"Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified.",
|
|
315
|
+
),
|
|
217
316
|
dry_run: z
|
|
218
317
|
.boolean()
|
|
219
318
|
.optional()
|
|
@@ -228,6 +327,18 @@ export const inputSchema = {
|
|
|
228
327
|
.describe(
|
|
229
328
|
"Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.",
|
|
230
329
|
),
|
|
330
|
+
tier: z
|
|
331
|
+
.enum(["ephemeral", "working", "durable"])
|
|
332
|
+
.optional()
|
|
333
|
+
.describe(
|
|
334
|
+
"Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified.",
|
|
335
|
+
),
|
|
336
|
+
conflict_resolution: z
|
|
337
|
+
.enum(["suggest", "off"])
|
|
338
|
+
.optional()
|
|
339
|
+
.describe(
|
|
340
|
+
'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).',
|
|
341
|
+
),
|
|
231
342
|
};
|
|
232
343
|
|
|
233
344
|
/**
|
|
@@ -248,14 +359,18 @@ export async function handler(
|
|
|
248
359
|
identity_key,
|
|
249
360
|
expires_at,
|
|
250
361
|
supersedes,
|
|
362
|
+
source_files,
|
|
251
363
|
dry_run,
|
|
252
364
|
similarity_threshold,
|
|
365
|
+
tier,
|
|
366
|
+
conflict_resolution,
|
|
253
367
|
},
|
|
254
368
|
ctx,
|
|
255
369
|
{ ensureIndexed },
|
|
256
370
|
) {
|
|
257
371
|
const { config } = ctx;
|
|
258
372
|
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
373
|
+
const suggestMode = conflict_resolution !== "off";
|
|
259
374
|
|
|
260
375
|
const vaultErr = ensureVaultExists(config);
|
|
261
376
|
if (vaultErr) return vaultErr;
|
|
@@ -313,6 +428,7 @@ export async function handler(
|
|
|
313
428
|
source,
|
|
314
429
|
expires_at,
|
|
315
430
|
supersedes,
|
|
431
|
+
source_files,
|
|
316
432
|
});
|
|
317
433
|
await indexEntry(ctx, entry);
|
|
318
434
|
const relPath = entry.filePath
|
|
@@ -359,6 +475,7 @@ export async function handler(
|
|
|
359
475
|
queryEmbedding,
|
|
360
476
|
threshold,
|
|
361
477
|
userId,
|
|
478
|
+
{ hydrate: suggestMode },
|
|
362
479
|
);
|
|
363
480
|
}
|
|
364
481
|
}
|
|
@@ -366,16 +483,31 @@ export async function handler(
|
|
|
366
483
|
if (dry_run) {
|
|
367
484
|
const parts = ["(dry run — nothing saved)"];
|
|
368
485
|
if (similarEntries.length) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
486
|
+
if (suggestMode) {
|
|
487
|
+
const candidates = buildConflictCandidates(similarEntries);
|
|
488
|
+
parts.push("", "⚠ Similar entries already exist:");
|
|
489
|
+
for (const e of similarEntries) {
|
|
490
|
+
const score = e.score.toFixed(2);
|
|
491
|
+
const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
|
|
492
|
+
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
493
|
+
}
|
|
494
|
+
parts.push(formatConflictSuggestions(candidates));
|
|
495
|
+
parts.push(
|
|
496
|
+
"",
|
|
497
|
+
"Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
|
|
498
|
+
);
|
|
499
|
+
} else {
|
|
500
|
+
parts.push("", "⚠ Similar entries already exist:");
|
|
501
|
+
for (const e of similarEntries) {
|
|
502
|
+
const score = e.score.toFixed(2);
|
|
503
|
+
const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
|
|
504
|
+
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
505
|
+
}
|
|
506
|
+
parts.push(
|
|
507
|
+
"",
|
|
508
|
+
"Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
|
|
509
|
+
);
|
|
374
510
|
}
|
|
375
|
-
parts.push(
|
|
376
|
-
"",
|
|
377
|
-
"Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
|
|
378
|
-
);
|
|
379
511
|
} else {
|
|
380
512
|
parts.push("", "No similar entries found. Safe to save.");
|
|
381
513
|
}
|
|
@@ -386,6 +518,8 @@ export async function handler(
|
|
|
386
518
|
if (folder) mergedMeta.folder = folder;
|
|
387
519
|
const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
|
|
388
520
|
|
|
521
|
+
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
522
|
+
|
|
389
523
|
const entry = await captureAndIndex(ctx, {
|
|
390
524
|
kind: normalizedKind,
|
|
391
525
|
title,
|
|
@@ -397,7 +531,9 @@ export async function handler(
|
|
|
397
531
|
identity_key,
|
|
398
532
|
expires_at,
|
|
399
533
|
supersedes,
|
|
534
|
+
source_files,
|
|
400
535
|
userId,
|
|
536
|
+
tier: effectiveTier,
|
|
401
537
|
});
|
|
402
538
|
|
|
403
539
|
if (ctx.config?.dataDir) {
|
|
@@ -410,9 +546,44 @@ export async function handler(
|
|
|
410
546
|
const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
|
|
411
547
|
if (title) parts.push(` title: ${title}`);
|
|
412
548
|
if (tags?.length) parts.push(` tags: ${tags.join(", ")}`);
|
|
549
|
+
parts.push(` tier: ${effectiveTier}`);
|
|
413
550
|
parts.push("", "_Use this id to update or delete later._");
|
|
551
|
+
const hasBucketTag = (tags || []).some(
|
|
552
|
+
(t) => typeof t === "string" && t.startsWith("bucket:"),
|
|
553
|
+
);
|
|
554
|
+
if (tags && tags.length > 0 && !hasBucketTag) {
|
|
555
|
+
parts.push(
|
|
556
|
+
"",
|
|
557
|
+
"_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._",
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const bucketTags = (tags || []).filter(
|
|
561
|
+
(t) => typeof t === "string" && t.startsWith("bucket:"),
|
|
562
|
+
);
|
|
563
|
+
for (const bt of bucketTags) {
|
|
564
|
+
const bucketUserClause = userId !== undefined ? "AND user_id = ?" : "";
|
|
565
|
+
const bucketParams =
|
|
566
|
+
userId !== undefined ? [bt, userId] : [bt];
|
|
567
|
+
const exists = ctx.db
|
|
568
|
+
.prepare(
|
|
569
|
+
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`,
|
|
570
|
+
)
|
|
571
|
+
.get(...bucketParams);
|
|
572
|
+
if (!exists) {
|
|
573
|
+
parts.push(
|
|
574
|
+
``,
|
|
575
|
+
`_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
414
579
|
if (similarEntries.length) {
|
|
415
|
-
|
|
580
|
+
if (suggestMode) {
|
|
581
|
+
const candidates = buildConflictCandidates(similarEntries);
|
|
582
|
+
parts.push(formatSimilarWarning(similarEntries));
|
|
583
|
+
parts.push(formatConflictSuggestions(candidates));
|
|
584
|
+
} else {
|
|
585
|
+
parts.push(formatSimilarWarning(similarEntries));
|
|
586
|
+
}
|
|
416
587
|
}
|
|
417
588
|
|
|
418
589
|
const criticalLimit = config.thresholds?.totalEntries?.critical;
|