dsh-loci 2.10.0 → 2.10.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/CHANGELOG.md +31 -0
- package/package.json +1 -1
- package/src/host/api/routes.js +10 -3
- package/src/host/index.js +1 -1
- package/src/host/store/store.js +26 -2
- package/src/host/tools/tools.js +7 -0
- package/src/mcp/store-facade.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) from 1.0.0.
|
|
6
6
|
|
|
7
|
+
## [2.10.1] — 2026-11-15
|
|
8
|
+
|
|
9
|
+
Three more ways a scope mistake could have answered with the wrong data. Found by checking
|
|
10
|
+
the last unverified claims from the audit instead of trusting the previous pass.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`/api/loci/preview?cwd=…` returned 400.** The route read `resolved.readKeys` and
|
|
15
|
+
`resolved.label`, neither of which `resolveScopes` returns, so any caller that supplied a
|
|
16
|
+
`cwd` or `session` got `Cannot read properties of undefined (reading 'length')`. The panel
|
|
17
|
+
never sends those parameters, which is why it survived: a latent failure on the one
|
|
18
|
+
endpoint that answers "what does *this* conversation get". `/api/loci/knowledge` had the
|
|
19
|
+
same defect.
|
|
20
|
+
|
|
21
|
+
- **An explicit empty scope set meant "the whole store".** `stats({ scopeKeys: [] })` and
|
|
22
|
+
`usageReport({ scopeKeys: [] })` dropped the filter, while `list([])` and
|
|
23
|
+
`countPending([])` returned nothing. That asymmetry turns a scope-resolution failure into
|
|
24
|
+
a confident answer about every project on the machine. Now: omitted means the whole
|
|
25
|
+
database (an admin question), `[]` means nothing.
|
|
26
|
+
|
|
27
|
+
- **`update`, `pin`, `unpin`, `restore` and `merge` ignored the read set.** Only `loci_get`
|
|
28
|
+
and `loci_forget` were guarded in 2.10.0. `update` moved a foreign entry into the calling
|
|
29
|
+
session's scope, `pin` changed how it sorts in another project's index, and `restore`
|
|
30
|
+
could un-delete an entry a different project had removed.
|
|
31
|
+
|
|
32
|
+
### Note
|
|
33
|
+
|
|
34
|
+
`test/unit/scope-semantics.test.mjs` pins all three. The empty-versus-absent distinction is
|
|
35
|
+
asserted in both directions, because the failure mode is a silent widening rather than a
|
|
36
|
+
crash.
|
|
37
|
+
|
|
7
38
|
## [2.10.0] — 2026-11-15
|
|
8
39
|
|
|
9
40
|
The last four findings from the audit. All four are the same shape: an operation that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loci",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"description": "Layered long-term memory for DeepSeek Harness: the prompt sees only a stable index, full text is one lookup away. Bi-temporal facts, strict scope isolation, verified-only injection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/host/api/routes.js
CHANGED
|
@@ -116,8 +116,15 @@ export function registerApiRoutes(ctx, deps) {
|
|
|
116
116
|
const sessionId = url.searchParams.get('session') ?? undefined;
|
|
117
117
|
const scoped = cwd !== undefined || sessionId !== undefined;
|
|
118
118
|
const resolved = scoped ? resolveScopes({ cwd, sessionId }) : undefined;
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// `resolveScopes` returns `{session, workspace, project, global, labels, cwd,
|
|
120
|
+
// projectRoot}` — there is no `readKeys` and no `label`, so this route threw
|
|
121
|
+
// "Cannot read properties of undefined (reading 'length')" whenever a caller supplied
|
|
122
|
+
// `?cwd=`. The panel never does, which is why it survived: a latent 400 on the one
|
|
123
|
+
// endpoint that answers "what does this conversation get".
|
|
124
|
+
const scopeKeys = scoped ? readScopeKeys(resolved, { includeGlobal: config.includeGlobal !== false }) : store.allScopeKeys();
|
|
125
|
+
const scopeLabel = scoped
|
|
126
|
+
? [resolved.labels.workspace, resolved.labels.project].filter((value, index, all) => all.indexOf(value) === index).join(' / ')
|
|
127
|
+
: `all ${scopeKeys.length} stored scope(s)`;
|
|
121
128
|
return json({
|
|
122
129
|
...renderIndexBody({
|
|
123
130
|
store,
|
|
@@ -153,7 +160,7 @@ export function registerApiRoutes(ctx, deps) {
|
|
|
153
160
|
const scopeKeys =
|
|
154
161
|
cwd === undefined && sessionId === undefined
|
|
155
162
|
? store.allScopeKeys()
|
|
156
|
-
: resolveScopes({ cwd, sessionId }).
|
|
163
|
+
: readScopeKeys(resolveScopes({ cwd, sessionId }), { includeGlobal: config.includeGlobal !== false });
|
|
157
164
|
return json(buildKnowledge(store, { limit, scopeKeys }));
|
|
158
165
|
});
|
|
159
166
|
|
package/src/host/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import { registerCommands } from './commands.js';
|
|
|
23
23
|
import { runDoctor } from './doctor.js';
|
|
24
24
|
|
|
25
25
|
/** Version stamped into the database and diagnostics. */
|
|
26
|
-
export const VERSION = '2.10.
|
|
26
|
+
export const VERSION = '2.10.1';
|
|
27
27
|
|
|
28
28
|
/** Optional collaborators; without them the plugin still serves tools. */
|
|
29
29
|
export const inject = ['systemPrompt', 'tools'];
|
package/src/host/store/store.js
CHANGED
|
@@ -1048,8 +1048,29 @@ export class Store {
|
|
|
1048
1048
|
*/
|
|
1049
1049
|
stats(options = {}) {
|
|
1050
1050
|
const keys = options.scopeKeys ?? [];
|
|
1051
|
-
|
|
1052
|
-
|
|
1051
|
+
// undefined means nobody asked for a scope (an admin whole-store view); [] means
|
|
1052
|
+
// someone asked for a scope and got no keys — which must answer *nothing*, not
|
|
1053
|
+
// *everything*. It used to drop the filter in both cases, so a scope-resolution
|
|
1054
|
+
// failure turned into a confident answer about every project on the machine.
|
|
1055
|
+
if (Array.isArray(options.scopeKeys) && options.scopeKeys.length === 0) {
|
|
1056
|
+
return {
|
|
1057
|
+
schemaVersion: this.schemaVersion(),
|
|
1058
|
+
indexRev: this.indexRev(),
|
|
1059
|
+
revisions: 0,
|
|
1060
|
+
heads: 0,
|
|
1061
|
+
byStatus: {},
|
|
1062
|
+
byKind: {},
|
|
1063
|
+
byScope: {},
|
|
1064
|
+
hostVersion: this.hostVersion ?? readMeta(this.db, META_KEYS.hostVersion) ?? null,
|
|
1065
|
+
pendingCandidates: 0,
|
|
1066
|
+
disputed: 0,
|
|
1067
|
+
lastInjection: null,
|
|
1068
|
+
dbBytes: existsSync(this.paths.db) ? statSync(this.paths.db).size : 0,
|
|
1069
|
+
vector: { ...this.vector, ...this.embedding.health() },
|
|
1070
|
+
memoryRoot: this.memoryRoot,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const marks = keys.length > 0 ? ` AND m.scope_key IN (${keys.map(() => '?').join(', ')})` : '';
|
|
1053
1074
|
const byStatus = {};
|
|
1054
1075
|
for (const row of this.db
|
|
1055
1076
|
.prepare(
|
|
@@ -1200,6 +1221,9 @@ export class Store {
|
|
|
1200
1221
|
usageReport(options = {}) {
|
|
1201
1222
|
const keys = options.scopeKeys ?? [];
|
|
1202
1223
|
const limit = Math.max(1, Math.min(200, options.limit ?? 20));
|
|
1224
|
+
// Same rule as `stats`: an explicit empty list means no rows; an absent one means the
|
|
1225
|
+
// whole store, which is what the panel's admin report asks for.
|
|
1226
|
+
if (Array.isArray(options.scopeKeys) && options.scopeKeys.length === 0) return [];
|
|
1203
1227
|
const marks = keys.length > 0 ? ` AND m.scope_key IN (${keys.map(() => '?').join(', ')})` : '';
|
|
1204
1228
|
const rows = this.db
|
|
1205
1229
|
.prepare(
|
package/src/host/tools/tools.js
CHANGED
|
@@ -209,11 +209,18 @@ export function createTools(deps) {
|
|
|
209
209
|
const context = execContext(exec);
|
|
210
210
|
const op = oneOf(args, 'op', ['create', 'update', 'merge', 'pin', 'unpin', 'restore'], 'create');
|
|
211
211
|
const id = str(args, 'id', { fallback: undefined });
|
|
212
|
+
// Every id-addressed op has to respect the read set, not just the two literally named
|
|
213
|
+
// get/forget. `update` moved a foreign entry into this session's scope, `pin` changed
|
|
214
|
+
// how it sorts in another project's index, and `restore` could un-delete something a
|
|
215
|
+
// different project had removed.
|
|
216
|
+
const idScopeKeys = () => readScopeKeys(context.scopes, { includeGlobal: config('includeGlobal') });
|
|
217
|
+
if (id !== undefined && op !== 'create') assertInScope(store, id, idScopeKeys(), `loci_write ${op}`);
|
|
212
218
|
|
|
213
219
|
if (op === 'merge') {
|
|
214
220
|
if (id === undefined) throw new Error('id is required for merge (the entry to keep)');
|
|
215
221
|
const sourceIds = Array.isArray(args?.sourceIds) ? args.sourceIds.filter((value) => typeof value === 'string') : [];
|
|
216
222
|
if (sourceIds.length === 0) throw new Error('sourceIds is required for merge (at least one id to fold in)');
|
|
223
|
+
for (const sourceId of sourceIds) assertInScope(store, sourceId, idScopeKeys(), 'loci_write merge source');
|
|
217
224
|
const result = store.merge({
|
|
218
225
|
targetId: id,
|
|
219
226
|
sourceIds,
|
package/src/mcp/store-facade.js
CHANGED