@compr/opscontext-mcp 2.6.0 → 2.6.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 CHANGED
@@ -4,6 +4,21 @@ All notable changes to OpsContext for AI Agents (previously ContextEngine — MC
4
4
 
5
5
  > Entries for 2.2.0 through 2.4.0 were not backfilled here; see `docs/sessions/SESSION_19` through `SESSION_21` for those releases.
6
6
 
7
+ ## [2.6.1] 2026-09-06: readers never embed; a store record without tags no longer breaks search
8
+
9
+ ### Fixed
10
+
11
+ - **`search_context` and `score_project` answered "Cannot read properties of undefined (reading
12
+ 'join')"** on every server, 2.5.9 included, while `save_learning` worked. `searchLearnings`
13
+ dereferenced `tags.join` on 20 agent-saved store records that have no `tags` array, inside the
14
+ firewall's learning injection, which runs for every tool that passes a context hint. Optional
15
+ fields are now read with fallbacks (`[LEARNING-FIELDS-ARE-OPTIONAL-ON-READ]`); the records are
16
+ not rewritten.
17
+ - **A reader that starts before its indexer has published** (every reader when all windows
18
+ restart at once) used to build locally and embed the whole corpus itself. It now builds
19
+ keyword-only, embeds nothing, and adopts the shared index the moment it appears. The trial
20
+ script starts all three servers together and asserts zero embed runs on the readers.
21
+
7
22
  ## [2.6.0] 2026-09-05: One indexer, many readers
8
23
 
9
24
  Every Claude Code chat spawns its own MCP server, plus launchd, plus VS Code. Measured on
package/dist/cli.js CHANGED
@@ -831,7 +831,7 @@ async function cliSaveLearning(args) {
831
831
  console.log(` Project: ${learning.project}`);
832
832
  if (learning.context)
833
833
  console.log(` Context: ${learning.context}`);
834
- console.log(` Tags: ${learning.tags.join(", ")}`);
834
+ console.log(` Tags: ${(learning.tags ?? []).join(", ")}`); // [LOCK] [LEARNING-FIELDS-ARE-OPTIONAL-ON-READ]
835
835
  }
836
836
  async function cliDeleteLearning(id) {
837
837
  if (!id) {
package/dist/index.js CHANGED
@@ -1083,7 +1083,7 @@ server.tool("save_learning", "Save a permanent operational rule learned during a
1083
1083
  `- **ID:** \`${learning.id}\``,
1084
1084
  `- **Category:** ${category}`,
1085
1085
  project ? `- **Project:** ${project}` : "",
1086
- `- **Tags:** ${learning.tags.join(", ")}`,
1086
+ `- **Tags:** ${(learning.tags ?? []).join(", ")}`, // [LOCK] [LEARNING-FIELDS-ARE-OPTIONAL-ON-READ]
1087
1087
  ``,
1088
1088
  `📊 Store: ${stats.total} learnings across ${Object.keys(stats.categories).length} categories`,
1089
1089
  ``,
@@ -1378,7 +1378,7 @@ async function main() {
1378
1378
  // always loads for whoever embeds or answers queries. [LOCK] [EMBEDDINGS-ARE-CONTENT-ADDRESSED]
1379
1379
  // A reader that adopted the index already has its vectors and loads the model on its first
1380
1380
  // semantic query, not before: 300 MB per process is worth waiting for.
1381
- if (!adopted) {
1381
+ if (!adopted && role === "indexer") {
1382
1382
  vectorStore = loadEmbeddingStore().vectors;
1383
1383
  if (vectorStore.size > 0)
1384
1384
  console.error(`[ContextEngine] 💾 Embedding store: ${vectorStore.size} vectors`);
@@ -1390,6 +1390,12 @@ async function main() {
1390
1390
  publishIndex();
1391
1391
  });
1392
1392
  }
1393
+ else if (!adopted) {
1394
+ // A reader that started before its indexer published (every window restarted at once):
1395
+ // keyword search from its own build, no embedding of its own, the index adopted the moment
1396
+ // it appears. Only indexers embed; that is the whole point. [LOCK] [ONE-INDEXER-MANY-READERS]
1397
+ console.error(`[ContextEngine] ⏳ Reader without an index: keyword search only until pid ${indexerPid ?? "?"} publishes`);
1398
+ }
1393
1399
  // 5. Watch (indexer) or poll (reader), and keep the election running
1394
1400
  if (role === "indexer")
1395
1401
  startWatching();
package/dist/learnings.js CHANGED
@@ -448,7 +448,16 @@ export function searchLearnings(query) {
448
448
  return store.learnings;
449
449
  const scored = [];
450
450
  for (const learning of store.learnings) {
451
- const text = `${learning.category} ${learning.rule} ${learning.context} ${learning.project || ""} ${learning.tags.join(" ")}`.toLowerCase();
451
+ // [LOCKED] [LEARNING-FIELDS-ARE-OPTIONAL-ON-READ] 2026-09-06
452
+ // [NEVER] dereference an optional field of a store record (tags, context, project) without a
453
+ // fallback on a read path, and [NEVER] "fix" such a record by rewriting the store.
454
+ // WHY: 20 agent-saved records (s57_*, s78_*, kept on purpose in the SESSION_25 prune) have no
455
+ // `tags` array. `learning.tags.join` threw here, inside the firewall's learning injection,
456
+ // so every tool that passes a context hint (search_context, score_project, ...) answered
457
+ // "Cannot read properties of undefined (reading 'join')" on every server, on 2.5.9 as on
458
+ // 2.6.0, while save_learning (exempt from the firewall) worked. Another chat noticed.
459
+ // FIX: read with fallbacks; the records stay as they are (the store is data, not ours to edit).
460
+ const text = `${learning.category} ${learning.rule} ${learning.context ?? ""} ${learning.project || ""} ${(learning.tags ?? []).join(" ")}`.toLowerCase();
452
461
  let score = 0;
453
462
  for (const token of tokens) {
454
463
  if (text.includes(token)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compr/opscontext-mcp",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "OpsContext for AI Agents — read-only fleet visibility (PM2/nginx/Docker/git/cron) + tamper-evident audit log + policy-as-code hooks. The ops + compliance layer Claude Code can't grow natively.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",