@context-vault/core 2.12.0 → 2.13.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
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ok } from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
export const name = "clear_context";
|
|
5
|
+
|
|
6
|
+
export const description =
|
|
7
|
+
"Reset active in-memory session context without deleting vault entries. Call this when switching projects or topics mid-session. With `scope`, all subsequent get_context calls should filter to that tag/project. Vault data is never modified.";
|
|
8
|
+
|
|
9
|
+
export const inputSchema = {
|
|
10
|
+
scope: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
"Optional tag or project name to focus on going forward. When provided, treat subsequent get_context calls as if filtered to this tag.",
|
|
15
|
+
),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} args
|
|
20
|
+
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} _ctx
|
|
21
|
+
*/
|
|
22
|
+
export function handler({ scope } = {}) {
|
|
23
|
+
const lines = [
|
|
24
|
+
"## Context Reset",
|
|
25
|
+
"",
|
|
26
|
+
"Active session context has been cleared. All previous context from this session should be disregarded.",
|
|
27
|
+
"",
|
|
28
|
+
"Vault entries are unchanged — no data was deleted.",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (scope?.trim()) {
|
|
32
|
+
const trimmed = scope.trim();
|
|
33
|
+
lines.push(
|
|
34
|
+
"",
|
|
35
|
+
`### Active Scope: \`${trimmed}\``,
|
|
36
|
+
"",
|
|
37
|
+
`Going forward, treat \`get_context\` calls as scoped to the tag or project **"${trimmed}"** unless the user explicitly requests a different scope or passes their own tag filters.`,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
lines.push(
|
|
41
|
+
"",
|
|
42
|
+
"No scope set. Use `get_context` normally — all vault entries are accessible.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ok(lines.join("\n"));
|
|
47
|
+
}
|
|
@@ -5,6 +5,87 @@ import { normalizeKind } from "../../core/files.js";
|
|
|
5
5
|
import { ok, err } from "../helpers.js";
|
|
6
6
|
import { isEmbedAvailable } from "../../index/embed.js";
|
|
7
7
|
|
|
8
|
+
const STALE_DUPLICATE_DAYS = 7;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect conflicts among a set of search result entries.
|
|
12
|
+
*
|
|
13
|
+
* Two checks are performed:
|
|
14
|
+
* 1. Supersession: if entry A's `superseded_by` points to any entry B in the
|
|
15
|
+
* result set, A is stale and should be discarded in favour of B.
|
|
16
|
+
* 2. Stale duplicate: two entries share the same kind and at least one common
|
|
17
|
+
* tag, but their `updated_at` timestamps differ by more than
|
|
18
|
+
* STALE_DUPLICATE_DAYS days — suggesting the older one may be outdated.
|
|
19
|
+
*
|
|
20
|
+
* No LLM calls, no new dependencies — pure in-memory set operations on the
|
|
21
|
+
* rows already fetched from the DB.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array} entries - Result rows (as returned by hybridSearch / filter-only mode)
|
|
24
|
+
* @param {import('../types.js').BaseCtx} _ctx - Unused for now; reserved for future DB look-ups
|
|
25
|
+
* @returns {Array<{entry_a_id: string, entry_b_id: string, reason: string, recommendation: string}>}
|
|
26
|
+
*/
|
|
27
|
+
export function detectConflicts(entries, _ctx) {
|
|
28
|
+
const conflicts = [];
|
|
29
|
+
const idSet = new Set(entries.map((e) => e.id));
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.superseded_by && idSet.has(entry.superseded_by)) {
|
|
33
|
+
conflicts.push({
|
|
34
|
+
entry_a_id: entry.id,
|
|
35
|
+
entry_b_id: entry.superseded_by,
|
|
36
|
+
reason: "superseded",
|
|
37
|
+
recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const supersededConflictPairs = new Set(
|
|
43
|
+
conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < entries.length; i++) {
|
|
47
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
48
|
+
const a = entries[i];
|
|
49
|
+
const b = entries[j];
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
supersededConflictPairs.has(`${a.id}|${b.id}`) ||
|
|
53
|
+
supersededConflictPairs.has(`${b.id}|${a.id}`)
|
|
54
|
+
) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (a.kind !== b.kind) continue;
|
|
59
|
+
|
|
60
|
+
const tagsA = a.tags ? JSON.parse(a.tags) : [];
|
|
61
|
+
const tagsB = b.tags ? JSON.parse(b.tags) : [];
|
|
62
|
+
|
|
63
|
+
if (!tagsA.length || !tagsB.length) continue;
|
|
64
|
+
|
|
65
|
+
const tagsSetA = new Set(tagsA);
|
|
66
|
+
const sharedTag = tagsB.some((t) => tagsSetA.has(t));
|
|
67
|
+
if (!sharedTag) continue;
|
|
68
|
+
|
|
69
|
+
const dateA = new Date(a.updated_at || a.created_at);
|
|
70
|
+
const dateB = new Date(b.updated_at || b.created_at);
|
|
71
|
+
if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) continue;
|
|
72
|
+
|
|
73
|
+
const diffDays = Math.abs(dateA - dateB) / 86400000;
|
|
74
|
+
if (diffDays <= STALE_DUPLICATE_DAYS) continue;
|
|
75
|
+
|
|
76
|
+
const [older, newer] = dateA < dateB ? [a, b] : [b, a];
|
|
77
|
+
conflicts.push({
|
|
78
|
+
entry_a_id: older.id,
|
|
79
|
+
entry_b_id: newer.id,
|
|
80
|
+
reason: "stale_duplicate",
|
|
81
|
+
recommendation: `Verify \`${older.id}\` is still accurate — it shares kind "${older.kind}" and tags with \`${newer.id}\` but was last updated ${Math.round(diffDays)} days earlier.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return conflicts;
|
|
87
|
+
}
|
|
88
|
+
|
|
8
89
|
export const name = "get_context";
|
|
9
90
|
|
|
10
91
|
export const description =
|
|
@@ -48,6 +129,12 @@ export const inputSchema = {
|
|
|
48
129
|
.describe(
|
|
49
130
|
"If true, include entries that have been superseded by newer ones. Default: false.",
|
|
50
131
|
),
|
|
132
|
+
detect_conflicts: z
|
|
133
|
+
.boolean()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe(
|
|
136
|
+
"If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.",
|
|
137
|
+
),
|
|
51
138
|
};
|
|
52
139
|
|
|
53
140
|
/**
|
|
@@ -66,6 +153,7 @@ export async function handler(
|
|
|
66
153
|
until,
|
|
67
154
|
limit,
|
|
68
155
|
include_superseded,
|
|
156
|
+
detect_conflicts,
|
|
69
157
|
},
|
|
70
158
|
ctx,
|
|
71
159
|
{ ensureIndexed, reindexFailed },
|
|
@@ -227,6 +315,9 @@ export async function handler(
|
|
|
227
315
|
}
|
|
228
316
|
}
|
|
229
317
|
|
|
318
|
+
// Conflict detection
|
|
319
|
+
const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
|
|
320
|
+
|
|
230
321
|
const lines = [];
|
|
231
322
|
if (reindexFailed)
|
|
232
323
|
lines.push(
|
|
@@ -265,5 +356,23 @@ export async function handler(
|
|
|
265
356
|
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
|
|
266
357
|
lines.push("");
|
|
267
358
|
}
|
|
359
|
+
|
|
360
|
+
if (detect_conflicts) {
|
|
361
|
+
if (conflicts.length === 0) {
|
|
362
|
+
lines.push(
|
|
363
|
+
`## Conflict Detection\n\nNo conflicts detected among results.\n`,
|
|
364
|
+
);
|
|
365
|
+
} else {
|
|
366
|
+
lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
|
|
367
|
+
for (const c of conflicts) {
|
|
368
|
+
lines.push(
|
|
369
|
+
`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
|
|
370
|
+
);
|
|
371
|
+
lines.push(` Recommendation: ${c.recommendation}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push("");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
268
377
|
return ok(lines.join("\n"));
|
|
269
378
|
}
|
package/src/server/tools.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as deleteContext from "./tools/delete-context.js";
|
|
|
11
11
|
import * as submitFeedback from "./tools/submit-feedback.js";
|
|
12
12
|
import * as ingestUrl from "./tools/ingest-url.js";
|
|
13
13
|
import * as contextStatus from "./tools/context-status.js";
|
|
14
|
+
import * as clearContext from "./tools/clear-context.js";
|
|
14
15
|
|
|
15
16
|
const toolModules = [
|
|
16
17
|
getContext,
|
|
@@ -20,6 +21,7 @@ const toolModules = [
|
|
|
20
21
|
submitFeedback,
|
|
21
22
|
ingestUrl,
|
|
22
23
|
contextStatus,
|
|
24
|
+
clearContext,
|
|
23
25
|
];
|
|
24
26
|
|
|
25
27
|
const TOOL_TIMEOUT_MS = 60_000;
|