@plur-ai/mcp 0.21.0 → 0.21.2
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/README.md +31 -0
- package/dist/{chunk-UINIIUBY.js → chunk-22X2SQNV.js} +253 -45
- package/dist/{chunk-WA3JKTZ7.js → chunk-OQDR6UF6.js} +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +92 -54
- package/dist/server-QOHVV2Z7.js +1007 -0
- package/dist/tools-export.js +2 -2
- package/package.json +2 -2
- package/dist/server-EUMEOVXB.js +0 -483
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OQDR6UF6.js";
|
|
4
4
|
|
|
5
5
|
// src/telemetry.ts
|
|
6
6
|
import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
|
|
@@ -17,7 +17,27 @@ function recordTelemetry(event) {
|
|
|
17
17
|
import { existsSync, unlinkSync } from "fs";
|
|
18
18
|
import { join, dirname, resolve } from "path";
|
|
19
19
|
import { homedir } from "os";
|
|
20
|
-
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, NO_SESSION, findProjectConfigPath, readProjectConfigFromPath, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE, mcpRemoteWarningLine, doctorRemoteRemediation, normalizeEndpointUrl, REMOTE_STATUS_TTL_MS, PROBE_CLEARABLE_STATES,
|
|
20
|
+
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, NO_SESSION, findProjectConfigPath, readProjectConfigFromPath, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE, mcpRemoteWarningLine, doctorRemoteRemediation, normalizeEndpointUrl, REMOTE_STATUS_TTL_MS, PROBE_CLEARABLE_STATES, summariseProvenance, formatLayer3, renderProvenanceSummary, describeNeedsAction, summarizeOutbox, folderMapProblem, tokenEnvUnsetDetail, tokenEnvUnsetFix, resolveRecallDeadlineMs } from "@plur-ai/core";
|
|
21
|
+
|
|
22
|
+
// src/folder-map-advice.ts
|
|
23
|
+
import { folderRepairCommand } from "@plur-ai/core";
|
|
24
|
+
function folderMapAdvice(problem, root) {
|
|
25
|
+
const command = problem.fixable ? folderRepairCommand(root) : null;
|
|
26
|
+
if (command) {
|
|
27
|
+
const summary = problem.repair_summary;
|
|
28
|
+
return {
|
|
29
|
+
command,
|
|
30
|
+
...summary ? { summary } : {},
|
|
31
|
+
text: `PLUR can repair this${summary ? `; the repair changes ${summary}` : ""}. Show the user what is wrong and what the repair changes, and ask whether to repair the file (a backup is saved first; they can see the full change by running plur folders repair in a terminal). Only after the user agrees, run: ${command}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (problem.fixable) return { text: "The user can repair it by running plur folders repair in a terminal (it shows the change and asks first)." };
|
|
35
|
+
return {
|
|
36
|
+
text: problem.line !== void 0 ? `plur folders repair cannot fix this automatically: the user has to fix line ${problem.line} of that file by hand (plur folders repair then checks it).` : "plur folders repair cannot fix this automatically: the user has to fix or remove that file by hand."
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/tools.ts
|
|
21
41
|
import { z } from "zod";
|
|
22
42
|
function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
23
43
|
return async (prompt) => {
|
|
@@ -51,6 +71,22 @@ function formatAge(ageMs) {
|
|
|
51
71
|
if (h < 24) return `${h}h ago`;
|
|
52
72
|
return `${Math.floor(h / 24)}d ago`;
|
|
53
73
|
}
|
|
74
|
+
function attachRecallReport(response, meta) {
|
|
75
|
+
const remote = meta.remote ?? { state: "not_dialed", hosts: [] };
|
|
76
|
+
response.remote = remote;
|
|
77
|
+
response.results_complete = meta.results_complete ?? true;
|
|
78
|
+
const notes = [];
|
|
79
|
+
if (meta.local_complete === false) {
|
|
80
|
+
notes.push("Local search did not finish within the recall deadline \u2014 results are incomplete; retrying is fine.");
|
|
81
|
+
}
|
|
82
|
+
if (remote.hosts.some((h) => h.detail === "recall_deadline")) {
|
|
83
|
+
notes.push("The server did not answer within the recall deadline \u2014 results may be missing team engrams this call.");
|
|
84
|
+
}
|
|
85
|
+
if (notes.length > 0) {
|
|
86
|
+
const line = notes.join(" ");
|
|
87
|
+
response.warning = typeof response.warning === "string" && response.warning.length > 0 ? `${response.warning} ${line}` : line;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
54
90
|
function attachRemoteStoreDegradation(response, plur) {
|
|
55
91
|
let status;
|
|
56
92
|
try {
|
|
@@ -69,21 +105,25 @@ function attachRemoteStoreDegradation(response, plur) {
|
|
|
69
105
|
response.warning = typeof response.warning === "string" && response.warning.length > 0 ? `${response.warning} ${line}` : line;
|
|
70
106
|
}
|
|
71
107
|
var recallHandler = async (args, plur) => {
|
|
108
|
+
const deadline_at = Date.now() + resolveRecallDeadlineMs();
|
|
72
109
|
const mode = args.mode ?? "hybrid";
|
|
73
110
|
if (mode === "keyword") {
|
|
74
|
-
const
|
|
111
|
+
const kwMeta = await plur.recallWithMeta(args.query, {
|
|
75
112
|
scope: args.scope,
|
|
76
113
|
domain: args.domain,
|
|
77
114
|
limit: args.limit,
|
|
78
115
|
remote_timeout_ms: 2e3,
|
|
79
116
|
// MCP recall remote budget (#776)
|
|
117
|
+
deadline_at,
|
|
80
118
|
// #243: session default scope (incl. mid-session plur_session_scope
|
|
81
119
|
// changes) establishes the remote dialing org context when no explicit
|
|
82
120
|
// scope filter is passed. Same rule as writes (E7, formal R2): not
|
|
83
121
|
// exactly one open session and no id → NO_SESSION, never the process
|
|
84
|
-
// slot the last-started session owns.
|
|
85
|
-
|
|
122
|
+
// slot the last-started session owns. #1566: with no session default
|
|
123
|
+
// of its own, the workspace's scope — the one resolver writes use.
|
|
124
|
+
session: await _readSession(args, plur)
|
|
86
125
|
});
|
|
126
|
+
const results = kwMeta.engrams;
|
|
87
127
|
const response2 = {
|
|
88
128
|
results: results.map((e) => {
|
|
89
129
|
const supersededBy = e.relations?.superseded_by;
|
|
@@ -92,7 +132,11 @@ var recallHandler = async (args, plur) => {
|
|
|
92
132
|
const measuredUnder = raw.measured_under;
|
|
93
133
|
const measuredAnnotation = measuredUnder ? " [measured under: " + Object.entries(measuredUnder).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ") + "]" : "";
|
|
94
134
|
return {
|
|
95
|
-
id
|
|
135
|
+
// The id plur_learn returned for this engram (F3): a team row keeps
|
|
136
|
+
// its store prefix (`ENG-<PREFIX>-…`), so it never shares an id with
|
|
137
|
+
// a local engram minted the same day, and forget/feedback/pin route
|
|
138
|
+
// it to its store. A local engram's id has no prefix.
|
|
139
|
+
id: e.id,
|
|
96
140
|
statement: e.statement + annotation + measuredAnnotation,
|
|
97
141
|
type: e.type,
|
|
98
142
|
scope: e.scope,
|
|
@@ -113,6 +157,7 @@ var recallHandler = async (args, plur) => {
|
|
|
113
157
|
mode: "keyword"
|
|
114
158
|
};
|
|
115
159
|
attachRemoteStoreDegradation(response2, plur);
|
|
160
|
+
attachRecallReport(response2, kwMeta);
|
|
116
161
|
return response2;
|
|
117
162
|
}
|
|
118
163
|
const budget = args.budget;
|
|
@@ -124,10 +169,12 @@ var recallHandler = async (args, plur) => {
|
|
|
124
169
|
limit: fetchLimit,
|
|
125
170
|
remote_timeout_ms: 2e3,
|
|
126
171
|
// MCP recall remote budget (#776)
|
|
172
|
+
deadline_at,
|
|
127
173
|
// #243: session default scope (incl. mid-session plur_session_scope
|
|
128
174
|
// changes) establishes the remote dialing org context when no explicit
|
|
129
|
-
// scope filter is passed. Same rule as writes (E7, formal R2)
|
|
130
|
-
session
|
|
175
|
+
// scope filter is passed. Same rule as writes (E7, formal R2), and the
|
|
176
|
+
// same workspace scope when the session has no default (#1566).
|
|
177
|
+
session: await _readSession(args, plur)
|
|
131
178
|
});
|
|
132
179
|
recordTelemetry("recall");
|
|
133
180
|
const truncatedByCount = budget?.max_results != null && meta.engrams.length > cap;
|
|
@@ -156,7 +203,8 @@ var recallHandler = async (args, plur) => {
|
|
|
156
203
|
const measuredUnder = raw.measured_under;
|
|
157
204
|
const measuredAnnotation = measuredUnder ? " [measured under: " + Object.entries(measuredUnder).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ") + "]" : "";
|
|
158
205
|
const base = {
|
|
159
|
-
id:
|
|
206
|
+
id: e.id,
|
|
207
|
+
// same id plur_learn returned — see the keyword branch (F3)
|
|
160
208
|
statement: e.statement + annotation + measuredAnnotation,
|
|
161
209
|
type: e.type,
|
|
162
210
|
scope: e.scope,
|
|
@@ -178,8 +226,9 @@ var recallHandler = async (args, plur) => {
|
|
|
178
226
|
mode: meta.mode
|
|
179
227
|
};
|
|
180
228
|
if (meta.mode === "hybrid-degraded") {
|
|
181
|
-
response.warning = `Embedding layer unavailable \u2014 results are BM25-only. Run plur_doctor for diagnosis. Last error: ${meta.embedderError ?? "unknown"}`;
|
|
229
|
+
response.warning = meta.degraded_reason ? meta.embedderError ?? meta.degraded_reason : `Embedding layer unavailable \u2014 results are BM25-only. Run plur_doctor for diagnosis. Last error: ${meta.embedderError ?? "unknown"}`;
|
|
182
230
|
}
|
|
231
|
+
if (meta.degraded_reason) response.degraded_reason = meta.degraded_reason;
|
|
183
232
|
if (resolveRerankerName() !== "off") {
|
|
184
233
|
response.reranked = meta.reranked ?? 0;
|
|
185
234
|
const rr = plur.rerankerStatus();
|
|
@@ -189,6 +238,7 @@ var recallHandler = async (args, plur) => {
|
|
|
189
238
|
}
|
|
190
239
|
}
|
|
191
240
|
attachRemoteStoreDegradation(response, plur);
|
|
241
|
+
attachRecallReport(response, meta);
|
|
192
242
|
return response;
|
|
193
243
|
};
|
|
194
244
|
var RECALL_HYBRID_DEPRECATION = "plur_recall_hybrid is deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). This alias will be removed in 0.18.";
|
|
@@ -422,12 +472,51 @@ function trustCommand(dir, storageRoot, platform = process.platform) {
|
|
|
422
472
|
const store = _shellWord(resolve(storageRoot), platform);
|
|
423
473
|
return store === null ? null : `plur --path ${store} trust ${target}`;
|
|
424
474
|
}
|
|
475
|
+
function folderMapStatus(root) {
|
|
476
|
+
let p;
|
|
477
|
+
try {
|
|
478
|
+
p = folderMapProblem(root);
|
|
479
|
+
} catch {
|
|
480
|
+
return {};
|
|
481
|
+
}
|
|
482
|
+
if (!p) return {};
|
|
483
|
+
const advice = folderMapAdvice(p, root);
|
|
484
|
+
return {
|
|
485
|
+
folder_map: {
|
|
486
|
+
ok: false,
|
|
487
|
+
file: p.file,
|
|
488
|
+
problem: p.problem,
|
|
489
|
+
...p.line !== void 0 ? { line: p.line } : {},
|
|
490
|
+
...p.column !== void 0 ? { column: p.column } : {},
|
|
491
|
+
fixable: p.fixable,
|
|
492
|
+
...advice.command ? { repair_command: advice.command } : {},
|
|
493
|
+
...advice.summary ? { repair_summary: advice.summary } : {},
|
|
494
|
+
advice: `Memory tools are paused until it is fixed. ${advice.text}`
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function folderOnCommand(entry, storageRoot, platform = process.platform) {
|
|
499
|
+
if (_UNSAFE_PATH_CHARS.test(entry)) return null;
|
|
500
|
+
const target = _shellWord(entry, platform);
|
|
501
|
+
if (target === null) return null;
|
|
502
|
+
if (!storageRoot || resolve(storageRoot) === resolve(join(homedir(), ".plur"))) return `plur folders set ${target} --on`;
|
|
503
|
+
const store = _shellWord(resolve(storageRoot), platform);
|
|
504
|
+
return store === null ? null : `plur --path ${store} folders set ${target} --on`;
|
|
505
|
+
}
|
|
506
|
+
function redactStoreErrors(errors) {
|
|
507
|
+
return Object.fromEntries(Object.entries(errors).map(([k, v]) => [k, String(v).split("\n", 1)[0].slice(0, 300)]));
|
|
508
|
+
}
|
|
425
509
|
var UNTRUSTED_SCOPE_GRAMMAR = /^(?:global|[a-z][a-z0-9-]*:[A-Za-z0-9][A-Za-z0-9._@/:-]{0,199})$/;
|
|
426
510
|
var UNTRUSTED_DOMAIN_GRAMMAR = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/;
|
|
427
511
|
var _UNSAFE_PATH_CHARS = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/;
|
|
428
512
|
function _escapedText(s) {
|
|
429
513
|
return JSON.stringify(s.length > 1024 ? s.slice(0, 1024) + "\u2026" : s).replace(/[\u007f-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
430
514
|
}
|
|
515
|
+
var FOLDER_SCOPE = /* @__PURE__ */ Symbol("plur.folderScope");
|
|
516
|
+
function _folderContext(args) {
|
|
517
|
+
const c = args[FOLDER_SCOPE];
|
|
518
|
+
return c && typeof c === "object" && typeof c.resolve === "function" ? c : null;
|
|
519
|
+
}
|
|
431
520
|
function readTrustedProjectConfig(trust) {
|
|
432
521
|
const configPath = findProjectConfigPath();
|
|
433
522
|
const raw = readProjectConfigFromPath(configPath);
|
|
@@ -472,6 +561,44 @@ var NO_SESSION_SLOT_WARNING = "No session is open, so this is the process-defaul
|
|
|
472
561
|
function _resolveWriteSession(args) {
|
|
473
562
|
return _resolveInjectionSession(args) ?? NO_SESSION;
|
|
474
563
|
}
|
|
564
|
+
var FOLDER_SCOPE_SESSION_PREFIX = "\0plur:folder-scope:";
|
|
565
|
+
function _knownSession(id, plur, viaServer) {
|
|
566
|
+
if (id.startsWith("\0")) return false;
|
|
567
|
+
if (_sessionTelemetry.has(id)) return true;
|
|
568
|
+
if (viaServer) return false;
|
|
569
|
+
try {
|
|
570
|
+
return plur.trackedSessionScopes().includes(id);
|
|
571
|
+
} catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function _writeSession(args, plur, base, read = false) {
|
|
576
|
+
const explicit = typeof args.session_id === "string" && args.session_id.length > 0 ? args.session_id : void 0;
|
|
577
|
+
const chosen = base !== void 0 ? base : explicit ?? _implicitSessionId();
|
|
578
|
+
const ctx = _folderContext(args);
|
|
579
|
+
if (!ctx) return chosen ?? NO_SESSION;
|
|
580
|
+
const session = chosen !== void 0 && _knownSession(chosen, plur, true) ? chosen : NO_SESSION;
|
|
581
|
+
let ws = null;
|
|
582
|
+
try {
|
|
583
|
+
ws = read && ctx.admitted ? ctx.admitted() : await ctx.resolve();
|
|
584
|
+
} catch {
|
|
585
|
+
ws = null;
|
|
586
|
+
}
|
|
587
|
+
if (session !== NO_SESSION) {
|
|
588
|
+
const record = _sessionTelemetry.get(session);
|
|
589
|
+
const own = plur.getSessionScope({ session });
|
|
590
|
+
if (record?.scope_adjusted) return session;
|
|
591
|
+
if (record && own != null && ws !== null && record.workspace_key === ws.key && record.workspace_scope === ws.scope) return session;
|
|
592
|
+
}
|
|
593
|
+
const scope = ws?.scope ?? null;
|
|
594
|
+
if (scope === null) return NO_SESSION;
|
|
595
|
+
const key = FOLDER_SCOPE_SESSION_PREFIX + scope;
|
|
596
|
+
plur.setSessionScope(scope, { session: key });
|
|
597
|
+
return key;
|
|
598
|
+
}
|
|
599
|
+
function _readSession(args, plur) {
|
|
600
|
+
return _writeSession(args, plur, void 0, true);
|
|
601
|
+
}
|
|
475
602
|
function _resolveScopeSession(args) {
|
|
476
603
|
const explicit = args.session_id;
|
|
477
604
|
if (typeof explicit === "string" && explicit.length > 0) {
|
|
@@ -587,7 +714,9 @@ function buildAdminDispatchTool(all) {
|
|
|
587
714
|
return { ...validated.errorPayload, error: inner.startsWith(`${action}:`) ? inner : `${action}: ${inner}` };
|
|
588
715
|
}
|
|
589
716
|
try {
|
|
590
|
-
|
|
717
|
+
const carried = args[FOLDER_SCOPE];
|
|
718
|
+
const data = carried !== void 0 ? { ...validated.data, [FOLDER_SCOPE]: carried } : validated.data;
|
|
719
|
+
return await target.handler(data, plur);
|
|
591
720
|
} catch (err) {
|
|
592
721
|
const message = err?.message ?? String(err);
|
|
593
722
|
throw new Error(message.startsWith(`${action}:`) ? message : `${action}: ${message}`);
|
|
@@ -762,13 +891,15 @@ function getAllToolDefinitions() {
|
|
|
762
891
|
// #243: resolve which session's default scope governs this write —
|
|
763
892
|
// explicit session_id first, else the lone open session. Never
|
|
764
893
|
// persisted on the engram (LearnContext.session selects a scope, it
|
|
765
|
-
// is not part of one).
|
|
766
|
-
|
|
894
|
+
// is not part of one). The workspace's answer where the session
|
|
895
|
+
// gives none, or its roots changed (#1562, #1563 review round 2).
|
|
896
|
+
session: await _writeSession(args, plur),
|
|
767
897
|
llm
|
|
768
898
|
};
|
|
769
899
|
const explicitScope = typeof args.scope === "string" && args.scope.length > 0;
|
|
770
|
-
const scopeHint = (engramScope, wasRouted) => {
|
|
900
|
+
const scopeHint = (engramScope, wasRouted, delivery) => {
|
|
771
901
|
if (explicitScope || wasRouted || isSharedScope(engramScope)) return {};
|
|
902
|
+
if (delivery !== "local") return {};
|
|
772
903
|
let remote = [];
|
|
773
904
|
try {
|
|
774
905
|
remote = plur.getWritableRemoteScopes();
|
|
@@ -776,6 +907,7 @@ function getAllToolDefinitions() {
|
|
|
776
907
|
return {};
|
|
777
908
|
}
|
|
778
909
|
if (remote.length === 0) return {};
|
|
910
|
+
if (remote.some((s) => s.scope === engramScope)) return {};
|
|
779
911
|
const scopes = remote.map((s) => `"${s.scope}"`).join(", ");
|
|
780
912
|
return { scope_hint: `Stored at "${engramScope}" because no scope was passed, but a team store is configured (${scopes}). If this is team/engineering knowledge, re-learn it with an explicit scope so it reaches the shared store; keep genuinely personal notes at the default scope.` };
|
|
781
913
|
};
|
|
@@ -823,7 +955,7 @@ function getAllToolDefinitions() {
|
|
|
823
955
|
const routeRefused = engram.structured_data?._routeRefused;
|
|
824
956
|
mcpCanary.signal("learn_activity");
|
|
825
957
|
recordTelemetry("learn");
|
|
826
|
-
const dedup = isOutbox ? void 0 : await plur.nearDuplicates(statement, context, engram
|
|
958
|
+
const dedup = isOutbox ? void 0 : await plur.nearDuplicates(statement, context, plur.readIdFor(engram));
|
|
827
959
|
const redraft = (() => {
|
|
828
960
|
const ids = args.supersedes;
|
|
829
961
|
if (!ids?.length) return void 0;
|
|
@@ -861,6 +993,8 @@ function getAllToolDefinitions() {
|
|
|
861
993
|
// more specific `warning` below (outbox, demotion, refusal) still
|
|
862
994
|
// wins that key; `delivery_warning` keeps this one either way.
|
|
863
995
|
delivery: delivered.delivery,
|
|
996
|
+
// 0.21.1: why it was queued (rejected token vs unreachable server).
|
|
997
|
+
...delivered.reason ? { delivery_reason: delivered.reason, delivery_reason_code: delivered.reason_code } : {},
|
|
864
998
|
...delivered.warning ? { delivery_warning: delivered.warning, warning: delivered.warning } : {},
|
|
865
999
|
...dedup?.near_duplicates?.length ? { dedup } : {},
|
|
866
1000
|
...redraft ? { redraft } : {},
|
|
@@ -869,9 +1003,9 @@ function getAllToolDefinitions() {
|
|
|
869
1003
|
return c ? { composition: c } : {};
|
|
870
1004
|
})(),
|
|
871
1005
|
...temporalEcho(engram),
|
|
872
|
-
...scopeHint(engram.scope, !!routed),
|
|
1006
|
+
...scopeHint(engram.scope, !!routed, delivered.delivery),
|
|
873
1007
|
...domainHint(!!routed),
|
|
874
|
-
...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
|
|
1008
|
+
...isOutbox ? { outbox: true, warning: delivered.reason ?? "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
|
|
875
1009
|
...demoted ? { demoted: true, requested_scope: demoted.from, warning: `Sensitive content (${demoted.patterns}) detected \u2014 stored at "${demoted.to}"/private instead of the requested shared scope "${demoted.from}". If this is a false positive, re-scope deliberately.` } : {},
|
|
876
1010
|
...routed ? { routed: { scope: routed.scope, confidence: routed.confidence, reason: routed.reason }, info: `No scope was provided; auto-routed to "${routed.scope}" (confidence ${routed.confidence}) because its content matched that scope's covers. Pass an explicit scope to override.` } : {},
|
|
877
1011
|
// #1115: a shared scope matched but was NOT adopted. Said plainly,
|
|
@@ -902,9 +1036,10 @@ function getAllToolDefinitions() {
|
|
|
902
1036
|
type: engram.type,
|
|
903
1037
|
...learnDecision(engram),
|
|
904
1038
|
delivery: delivered.delivery,
|
|
1039
|
+
...delivered.reason ? { delivery_reason: delivered.reason, delivery_reason_code: delivered.reason_code } : {},
|
|
905
1040
|
...delivered.warning ? { delivery_warning: delivered.warning } : {},
|
|
906
1041
|
...temporalEcho(engram),
|
|
907
|
-
...scopeHint(engram.scope, !!routedFallback),
|
|
1042
|
+
...scopeHint(engram.scope, !!routedFallback, delivered.delivery),
|
|
908
1043
|
...domainHint(!!routedFallback),
|
|
909
1044
|
...isOutbox ? { outbox: true } : {},
|
|
910
1045
|
// The routed write can fail for reasons that have nothing to do
|
|
@@ -966,7 +1101,7 @@ function getAllToolDefinitions() {
|
|
|
966
1101
|
if (raw.length === 0) {
|
|
967
1102
|
return { ids: [], results: [], stats: { added: 0, updated: 0, merged: 0, noops: 0, failed: 0 }, failures: [], warning: "No engrams provided \u2014 pass a non-empty `engrams` array." };
|
|
968
1103
|
}
|
|
969
|
-
const batchSession =
|
|
1104
|
+
const batchSession = await _writeSession(args, plur);
|
|
970
1105
|
const projectDomain = readTrustedProjectConfig(plur).domain ?? void 0;
|
|
971
1106
|
const items = raw.map((e) => ({
|
|
972
1107
|
statement: sanitizeStatement(e.statement),
|
|
@@ -1066,9 +1201,14 @@ function getAllToolDefinitions() {
|
|
|
1066
1201
|
const isOutbox = !!r.engram.structured_data?._outbox;
|
|
1067
1202
|
const routed = r.engram.structured_data?._routed;
|
|
1068
1203
|
const routeRefused = r.engram.structured_data?._routeRefused;
|
|
1204
|
+
const requested = r.input_index !== void 0 ? raw[r.input_index]?.scope : void 0;
|
|
1205
|
+
const delivered = plur.deliveryOf(r.engram, typeof requested === "string" ? requested : void 0);
|
|
1069
1206
|
return {
|
|
1070
1207
|
input_index: r.input_index,
|
|
1071
1208
|
id: isOutbox ? r.engram.id : plur.readIdFor(r.engram),
|
|
1209
|
+
delivery: delivered.delivery,
|
|
1210
|
+
...delivered.reason ? { delivery_reason: delivered.reason, delivery_reason_code: delivered.reason_code } : {},
|
|
1211
|
+
...delivered.warning ? { delivery_warning: delivered.warning } : {},
|
|
1072
1212
|
statement: r.engram.statement,
|
|
1073
1213
|
scope: r.engram.scope,
|
|
1074
1214
|
type: r.engram.type,
|
|
@@ -1091,7 +1231,7 @@ function getAllToolDefinitions() {
|
|
|
1091
1231
|
},
|
|
1092
1232
|
{
|
|
1093
1233
|
name: "plur_recall",
|
|
1094
|
-
description: 'Search engrams by topic. Default mode is hybrid (BM25 + local embeddings via RRF) \u2014 set mode:"keyword" for BM25-only. Local search plus, when a configured enterprise store is part of the current project/work, one live timeout-bounded recall per remote host merged in (a `remote_stores` block + warning appears when a host is degraded; no host configured or implicated = fully local). Note: a project-scope filter also returns personal-family engrams (local, global, user:*, agent:*); an explicit scope=global recall returns ALL personal-family engrams \u2014 wider than scope=global INJECT, which is targeted to the global namespace only.',
|
|
1234
|
+
description: 'Search engrams by topic. Default mode is hybrid (BM25 + local embeddings via RRF) \u2014 set mode:"keyword" for BM25-only. Local search plus, when a configured enterprise store is part of the current project/work \u2014 or when the scope (or the default scope this session registered itself) is a personal user: scope and a remote store is configured with that same scope (exact match, case-insensitive; that rule adds only the matching store, though a store set to dial: always or a trusted project remote can still add others) \u2014 one live timeout-bounded recall per remote host merged in (a `remote_stores` block + warning appears when a host is degraded; no host configured or implicated = fully local). Note: a project-scope filter also returns personal-family engrams (local, global, user:*, agent:*); an explicit scope=global recall returns ALL personal-family engrams \u2014 wider than scope=global INJECT, which is targeted to the global namespace only.',
|
|
1095
1235
|
annotations: { title: "Recall", readOnlyHint: true, idempotentHint: true },
|
|
1096
1236
|
inputSchema: {
|
|
1097
1237
|
type: "object",
|
|
@@ -1118,7 +1258,7 @@ function getAllToolDefinitions() {
|
|
|
1118
1258
|
},
|
|
1119
1259
|
{
|
|
1120
1260
|
name: "plur_recall_hybrid",
|
|
1121
|
-
description: "[Deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). Alias kept for backwards compatibility; removal earliest 0.18.] Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion, plus the live enterprise-store recall leg when one is configured and project-relevant.",
|
|
1261
|
+
description: "[Deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). Alias kept for backwards compatibility; removal earliest 0.18.] Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion, plus the live enterprise-store recall leg when one is configured and project-relevant, or when the scope is a personal user: scope matching the own scope of a configured remote store (exact, case-insensitive; adds only that store, while dial: always and a trusted project remote still apply).",
|
|
1122
1262
|
annotations: { title: "Recall (hybrid) [deprecated alias]", readOnlyHint: true, idempotentHint: true },
|
|
1123
1263
|
inputSchema: {
|
|
1124
1264
|
type: "object",
|
|
@@ -1195,12 +1335,18 @@ function getAllToolDefinitions() {
|
|
|
1195
1335
|
required: ["task"]
|
|
1196
1336
|
},
|
|
1197
1337
|
handler: async (args, plur) => {
|
|
1338
|
+
const deadline_at = Date.now() + resolveRecallDeadlineMs();
|
|
1198
1339
|
const session_id = _resolveWriteSession(args);
|
|
1199
1340
|
const result = await plur.injectHybrid(args.task, {
|
|
1200
1341
|
budget: args.budget,
|
|
1201
1342
|
scope: args.scope,
|
|
1202
1343
|
source: "inject",
|
|
1203
|
-
session_id
|
|
1344
|
+
session_id,
|
|
1345
|
+
deadline_at,
|
|
1346
|
+
// #1566: the dialing context follows the same rule as writes — the
|
|
1347
|
+
// workspace's scope when the session has no default of its own.
|
|
1348
|
+
// session_id above stays the caller's for attribution.
|
|
1349
|
+
dial_session: await _readSession(args, plur)
|
|
1204
1350
|
});
|
|
1205
1351
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1206
1352
|
const response = {
|
|
@@ -1209,7 +1355,11 @@ function getAllToolDefinitions() {
|
|
|
1209
1355
|
count: result.count,
|
|
1210
1356
|
tokens_used: result.tokens_used,
|
|
1211
1357
|
injected_ids: result.injected_ids,
|
|
1212
|
-
|
|
1358
|
+
// #1586 round 4: a degraded injection says so, instead of a fixed 'hybrid'.
|
|
1359
|
+
// (Kept as 'hybrid' otherwise, as this reply always said.)
|
|
1360
|
+
mode: result.mode === "hybrid-degraded" ? "hybrid-degraded" : "hybrid",
|
|
1361
|
+
...result.degraded_reason ? { degraded_reason: result.degraded_reason } : {},
|
|
1362
|
+
...(result.mode === "hybrid-degraded" || result.degraded_reason) && result.embedder_error ? { embedder_error: result.embedder_error } : {},
|
|
1213
1363
|
// #181: unresolved-tension warnings — flag contradicted context
|
|
1214
1364
|
...result.warnings ? { warnings: result.warnings } : {},
|
|
1215
1365
|
// #1142: pinned engrams that did not fit. `pinned: true` reads as a
|
|
@@ -1218,6 +1368,7 @@ function getAllToolDefinitions() {
|
|
|
1218
1368
|
...result.omitted_pinned?.length ? { omitted_pinned: result.omitted_pinned } : {}
|
|
1219
1369
|
};
|
|
1220
1370
|
attachRemoteStoreDegradation(response, plur);
|
|
1371
|
+
attachRecallReport(response, result);
|
|
1221
1372
|
return response;
|
|
1222
1373
|
}
|
|
1223
1374
|
},
|
|
@@ -1269,8 +1420,8 @@ function getAllToolDefinitions() {
|
|
|
1269
1420
|
return { mode: "batch", results, summary };
|
|
1270
1421
|
}
|
|
1271
1422
|
try {
|
|
1272
|
-
await plur.feedback(args.id, args.signal, args.scope);
|
|
1273
|
-
return { success: true, id: args.id, signal: args.signal };
|
|
1423
|
+
const { warnings } = await plur.feedback(args.id, args.signal, args.scope);
|
|
1424
|
+
return { success: true, id: args.id, signal: args.signal, ...warnings.length > 0 ? { warnings } : {} };
|
|
1274
1425
|
} catch (err) {
|
|
1275
1426
|
if (err.message?.includes("readonly store")) {
|
|
1276
1427
|
return { success: false, id: args.id, signal: args.signal, note: "Engram is in a readonly store. Feedback noted for this session but not persisted." };
|
|
@@ -1288,7 +1439,8 @@ function getAllToolDefinitions() {
|
|
|
1288
1439
|
properties: {
|
|
1289
1440
|
id: { type: "string", description: "Engram ID to pin or unpin" },
|
|
1290
1441
|
pinned: { type: "boolean", description: "Target value (default true)" },
|
|
1291
|
-
list: { type: "boolean", description: "If true, just return the current set of pinned engrams (no mutation)" }
|
|
1442
|
+
list: { type: "boolean", description: "If true, just return the current set of pinned engrams (no mutation)" },
|
|
1443
|
+
scope: { type: "string", description: `Which store holds it. Ids are minted per store, so one bare id can name a local engram and an unrelated remote one; such an id is refused. Pass "primary" for the local engram, or a remote store's scope (or the namespaced ENG-XXX-\u2026 id from recall) for the remote one.` }
|
|
1292
1444
|
}
|
|
1293
1445
|
},
|
|
1294
1446
|
handler: async (args, plur) => {
|
|
@@ -1305,7 +1457,7 @@ function getAllToolDefinitions() {
|
|
|
1305
1457
|
if (!args.id) throw new Error("Provide id (or list:true to list pinned)");
|
|
1306
1458
|
const target = args.pinned ?? true;
|
|
1307
1459
|
if (target === true) {
|
|
1308
|
-
const q = await plur.pinnedQuota(args.id);
|
|
1460
|
+
const q = await plur.pinnedQuota(args.id, args.scope ? { scope: args.scope } : void 0);
|
|
1309
1461
|
if (q.candidate && !q.candidate.fits) {
|
|
1310
1462
|
const deficit = q.candidate.would_be - q.quota;
|
|
1311
1463
|
const covering = [];
|
|
@@ -1330,7 +1482,7 @@ function getAllToolDefinitions() {
|
|
|
1330
1482
|
};
|
|
1331
1483
|
}
|
|
1332
1484
|
}
|
|
1333
|
-
const updated = await plur.setPinnedAsync(args.id, target);
|
|
1485
|
+
const updated = await plur.setPinnedAsync(args.id, target, args.scope ? { scope: args.scope } : void 0);
|
|
1334
1486
|
if (!updated) throw new Error(`Engram not found: ${args.id}`);
|
|
1335
1487
|
return {
|
|
1336
1488
|
id: updated.id,
|
|
@@ -1361,18 +1513,18 @@ function getAllToolDefinitions() {
|
|
|
1361
1513
|
const engram = scope ? void 0 : await plur.getById(args.id);
|
|
1362
1514
|
if (engram) {
|
|
1363
1515
|
if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
|
|
1364
|
-
await plur.forget(args.id, args.reason, { force: true });
|
|
1365
|
-
return { success: true, retired: { id: engram.id, statement: engram.statement } };
|
|
1516
|
+
const { warnings: warnings2 } = await plur.forget(args.id, args.reason, { force: true });
|
|
1517
|
+
return { success: true, retired: { id: engram.id, statement: engram.statement }, ...warnings2.length > 0 ? { warnings: warnings2 } : {} };
|
|
1366
1518
|
}
|
|
1367
|
-
await plur.forget(args.id, args.reason, { force: true, ...scope ? { scope } : {} });
|
|
1368
|
-
return { success: true, retired: { id: args.id, ...scope ? { scope } : {} } };
|
|
1519
|
+
const { warnings } = await plur.forget(args.id, args.reason, { force: true, ...scope ? { scope } : {} });
|
|
1520
|
+
return { success: true, retired: { id: args.id, ...scope ? { scope } : {} }, ...warnings.length > 0 ? { warnings } : {} };
|
|
1369
1521
|
}
|
|
1370
1522
|
if (args.search) {
|
|
1371
1523
|
const matches = await plur.recall(args.search, { limit: 100, remote: false });
|
|
1372
1524
|
if (matches.length === 0) return { success: false, error: `No active engrams matching "${args.search}"` };
|
|
1373
1525
|
if (matches.length === 1) {
|
|
1374
|
-
await plur.forget(matches[0].id, args.reason, { force: true });
|
|
1375
|
-
return { success: true, retired: { id: matches[0].id, statement: matches[0].statement } };
|
|
1526
|
+
const { warnings } = await plur.forget(matches[0].id, args.reason, { force: true });
|
|
1527
|
+
return { success: true, retired: { id: matches[0].id, statement: matches[0].statement }, ...warnings.length > 0 ? { warnings } : {} };
|
|
1376
1528
|
}
|
|
1377
1529
|
return {
|
|
1378
1530
|
success: false,
|
|
@@ -1918,7 +2070,7 @@ function getAllToolDefinitions() {
|
|
|
1918
2070
|
// Artifacts that could not be read (audit 2026-08-03, finding 14).
|
|
1919
2071
|
// Core reports these; this hand-built response dropped them, so an
|
|
1920
2072
|
// agent asking for status saw a healthy-looking `pack_count: 0`.
|
|
1921
|
-
...status.store_errors ? { store_errors: status.store_errors } : {},
|
|
2073
|
+
...status.store_errors ? { store_errors: redactStoreErrors(status.store_errors) } : {},
|
|
1922
2074
|
// Spreading-activation drop counters — absent when both are zero.
|
|
1923
2075
|
...status.spread_drops ? { spread_drops: status.spread_drops } : {},
|
|
1924
2076
|
// Version check (issue #151)
|
|
@@ -1929,7 +2081,11 @@ function getAllToolDefinitions() {
|
|
|
1929
2081
|
behind: minorVersionsBehind(versionCheck.current, versionCheck.latest)
|
|
1930
2082
|
}
|
|
1931
2083
|
} : {},
|
|
1932
|
-
capabilities: await mcpCanary.status()
|
|
2084
|
+
capabilities: await mcpCanary.status(),
|
|
2085
|
+
// #1526: a broken folders.yaml pauses every memory tool. Status is
|
|
2086
|
+
// where an agent looks, so it names the line, the problem in plain
|
|
2087
|
+
// words, and the repair (run only after the user agrees).
|
|
2088
|
+
...folderMapStatus(plur.storageRoot)
|
|
1933
2089
|
};
|
|
1934
2090
|
}
|
|
1935
2091
|
},
|
|
@@ -2212,6 +2368,9 @@ function getAllToolDefinitions() {
|
|
|
2212
2368
|
detail: soon ? `Reachable, but token expires in ${h.tokenExpiresInDays}d \u2014 reauth soon` : `Reachable, auth valid${expiresNote}`
|
|
2213
2369
|
});
|
|
2214
2370
|
if (soon) remediation.push(`Remote ${h.url}: token expires in ${h.tokenExpiresInDays}d \u2014 mint a new token (<host>/me/api-keys), update ~/.plur/config.yaml, restart.`);
|
|
2371
|
+
} else if (h.tokenEnvUnset) {
|
|
2372
|
+
checks.push({ check: `remote store: ${h.url}`, ok: false, detail: tokenEnvUnsetDetail(h.tokenEnvUnset, h.scopes?.[0] ?? h.url) });
|
|
2373
|
+
remediation.push(tokenEnvUnsetFix(h.url, h.tokenEnvUnset));
|
|
2215
2374
|
} else if (h.status === "auth_expired") {
|
|
2216
2375
|
checks.push({ check: `remote store: ${h.url}`, ok: false, detail: `AUTH FAILED${expiresNote} \u2014 team-scoped writes are queuing to the outbox, not syncing. (${h.reason ?? ""})` });
|
|
2217
2376
|
remediation.push(`Remote ${h.url}: re-authenticate \u2014 open <host>/auth/github (or <host>/me/api-keys) in a browser, paste the token into ~/.plur/config.yaml, then restart Claude/MCP so it reloads. Queued engrams flush on next session_start.`);
|
|
@@ -2257,9 +2416,18 @@ function getAllToolDefinitions() {
|
|
|
2257
2416
|
} catch {
|
|
2258
2417
|
}
|
|
2259
2418
|
const tool_surface = describeToolSurface();
|
|
2419
|
+
const folderMap = folderMapStatus(plur.storageRoot);
|
|
2420
|
+
if (folderMap.folder_map) {
|
|
2421
|
+
const fm = folderMap.folder_map;
|
|
2422
|
+
checks.push({ check: "folder map", ok: false, detail: `${fm.file} ${fm.problem}` });
|
|
2423
|
+
remediation.push(`Folder map: ${fm.file} ${fm.problem}. ${fm.advice}`);
|
|
2424
|
+
} else {
|
|
2425
|
+
checks.push({ check: "folder map", ok: true, detail: "folders.yaml is readable (or absent: no decisions yet)" });
|
|
2426
|
+
}
|
|
2260
2427
|
return {
|
|
2261
2428
|
ok: checks.every((c) => c.ok),
|
|
2262
2429
|
checks,
|
|
2430
|
+
...folderMap,
|
|
2263
2431
|
embedder: {
|
|
2264
2432
|
before_probe: before,
|
|
2265
2433
|
after_probe: after
|
|
@@ -2288,6 +2456,7 @@ function getAllToolDefinitions() {
|
|
|
2288
2456
|
required: ["task"]
|
|
2289
2457
|
},
|
|
2290
2458
|
handler: async (args, plur) => {
|
|
2459
|
+
const deadline_at = Date.now() + resolveRecallDeadlineMs();
|
|
2291
2460
|
mcpCanary.reset();
|
|
2292
2461
|
mcpCanary.signal("session_start_hook");
|
|
2293
2462
|
const crypto = await import("crypto");
|
|
@@ -2323,16 +2492,30 @@ function getAllToolDefinitions() {
|
|
|
2323
2492
|
});
|
|
2324
2493
|
const projectConfig = readTrustedProjectConfig(plur);
|
|
2325
2494
|
const explicit_default_scope = args.default_scope ?? null;
|
|
2326
|
-
const
|
|
2327
|
-
|
|
2495
|
+
const folderCtx = _folderContext(args);
|
|
2496
|
+
let workspace = null;
|
|
2497
|
+
if (folderCtx) {
|
|
2498
|
+
try {
|
|
2499
|
+
workspace = await folderCtx.resolve();
|
|
2500
|
+
} catch {
|
|
2501
|
+
workspace = null;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
const folder_scope = folderCtx ? workspace?.scope ?? null : null;
|
|
2505
|
+
const default_scope = explicit_default_scope ?? (folderCtx ? folder_scope : projectConfig.scope ?? null) ?? null;
|
|
2506
|
+
const scope_source = explicit_default_scope ? "caller" : folder_scope && folder_scope !== projectConfig.scope ? "folder-map" : default_scope ? "project-config" : "none";
|
|
2328
2507
|
const default_domain = projectConfig.domain ?? null;
|
|
2329
|
-
plur.setSessionScope(default_scope);
|
|
2508
|
+
if (!folderCtx) plur.setSessionScope(default_scope);
|
|
2330
2509
|
plur.setSessionScope(default_scope, { session: session_id });
|
|
2331
2510
|
{
|
|
2332
2511
|
const t = _sessionTelemetry.get(session_id);
|
|
2333
2512
|
if (t) {
|
|
2334
2513
|
t.default_scope = default_scope;
|
|
2335
2514
|
t.default_scope_source = scope_source;
|
|
2515
|
+
if (workspace) {
|
|
2516
|
+
t.workspace_key = workspace.key;
|
|
2517
|
+
t.workspace_scope = workspace.scope;
|
|
2518
|
+
}
|
|
2336
2519
|
}
|
|
2337
2520
|
}
|
|
2338
2521
|
const status = await plur.status().catch(() => null);
|
|
@@ -2341,19 +2524,27 @@ function getAllToolDefinitions() {
|
|
|
2341
2524
|
episode_count: status?.episode_count ?? 0,
|
|
2342
2525
|
pack_count: status?.pack_count ?? 0
|
|
2343
2526
|
};
|
|
2344
|
-
const store_errors = status?.store_errors;
|
|
2527
|
+
const store_errors = status?.store_errors ? redactStoreErrors(status.store_errors) : void 0;
|
|
2345
2528
|
await plur.warmRemoteCaches().catch(() => {
|
|
2346
2529
|
});
|
|
2347
2530
|
let engrams = null;
|
|
2531
|
+
let injectionReport = {};
|
|
2348
2532
|
try {
|
|
2349
2533
|
const result = await plur.injectHybrid(task, {
|
|
2350
2534
|
scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
|
|
2351
2535
|
session_id,
|
|
2352
2536
|
// stamped on the co_injection provenance event (#452)
|
|
2353
2537
|
source: "session_start",
|
|
2354
|
-
remote_timeout_ms: 5e3
|
|
2538
|
+
remote_timeout_ms: 5e3,
|
|
2355
2539
|
// session_start warm budget (#776)
|
|
2540
|
+
deadline_at
|
|
2356
2541
|
});
|
|
2542
|
+
injectionReport = {
|
|
2543
|
+
remote: result.remote,
|
|
2544
|
+
results_complete: result.results_complete,
|
|
2545
|
+
...result.degraded_reason ? { degraded_reason: result.degraded_reason } : {},
|
|
2546
|
+
...(result.mode === "hybrid-degraded" || result.degraded_reason) && result.embedder_error ? { embedder_error: result.embedder_error } : {}
|
|
2547
|
+
};
|
|
2357
2548
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
2358
2549
|
if (result.count > 0) {
|
|
2359
2550
|
const lines = [];
|
|
@@ -2418,7 +2609,11 @@ ${guide}`;
|
|
|
2418
2609
|
|
|
2419
2610
|
${guide}`;
|
|
2420
2611
|
}
|
|
2421
|
-
if (scope_source === "
|
|
2612
|
+
if (scope_source === "folder-map") {
|
|
2613
|
+
guide += `
|
|
2614
|
+
|
|
2615
|
+
This folder's scope: "${default_scope}" (from your folder map, plur folders). plur_learn calls without an explicit scope will be tagged with this scope. Pass scope: "global" only for genuinely cross-project knowledge.`;
|
|
2616
|
+
} else if (scope_source === "project-config") {
|
|
2422
2617
|
guide += `
|
|
2423
2618
|
|
|
2424
2619
|
Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current project). plur_learn calls without an explicit scope will be tagged with this scope, keeping this project's knowledge separate from your other projects. Pass scope: "global" only for genuinely cross-project knowledge (general coding conventions, language gotchas, tool quirks).`;
|
|
@@ -2500,6 +2695,11 @@ Tool profile "${session_tool_profile}": most plur_* tools are not exposed by nam
|
|
|
2500
2695
|
...default_scope ? { default_scope, scope_source } : {},
|
|
2501
2696
|
...default_domain ? { default_domain, domain_source: "project-config" } : {},
|
|
2502
2697
|
...projectConfig.warning ? { project_config_warning: projectConfig.warning } : {},
|
|
2698
|
+
// #1586 audit L6: the injection's per-call remote report (added fields).
|
|
2699
|
+
remote: injectionReport.remote ?? { state: "not_dialed", hosts: [] },
|
|
2700
|
+
results_complete: injectionReport.results_complete ?? false,
|
|
2701
|
+
...injectionReport.degraded_reason ? { degraded_reason: injectionReport.degraded_reason } : {},
|
|
2702
|
+
...injectionReport.embedder_error ? { embedder_error: injectionReport.embedder_error } : {},
|
|
2503
2703
|
// Ask LLM to check back — MCP can't push, but we can request a follow-up
|
|
2504
2704
|
follow_up: store_stats.engram_count === 0 ? "This is a fresh store with 0 engrams. After your first exchange with the user, review what you learned and call plur_learn for any corrections, preferences, or patterns. Build the memory from this session." : void 0,
|
|
2505
2705
|
// On fresh install, suggest hook setup for reliable injection
|
|
@@ -2612,7 +2812,8 @@ Tool profile "${session_tool_profile}": most plur_* tools are not exposed by nam
|
|
|
2612
2812
|
...warning ? { warning } : {}
|
|
2613
2813
|
});
|
|
2614
2814
|
}
|
|
2615
|
-
const
|
|
2815
|
+
const clearCtx = _folderContext(args);
|
|
2816
|
+
const restored = record !== void 0 ? record.default_scope ?? null : clearCtx ? null : readTrustedProjectConfig(plur).scope ?? null;
|
|
2616
2817
|
const restored_source = record !== void 0 ? record.default_scope_source === "caller" ? "session-start" : record.default_scope_source ?? "none" : restored != null ? "project-config" : "none";
|
|
2617
2818
|
const { previous, next } = plur.adjustSessionScope(restored, { session, reason, trigger: "clear" });
|
|
2618
2819
|
if (record) record.scope_adjusted = false;
|
|
@@ -2700,8 +2901,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2700
2901
|
try {
|
|
2701
2902
|
await plur.learnRouted(sanitizeStatement(statement), {
|
|
2702
2903
|
type,
|
|
2703
|
-
// E7: no resolvable session → no session default
|
|
2704
|
-
|
|
2904
|
+
// E7: no resolvable session → no session default; through the
|
|
2905
|
+
// server, the workspace's answer (#1563 review round 2).
|
|
2906
|
+
session: await _writeSession(args, plur, endSession),
|
|
2705
2907
|
domain: projectDomain,
|
|
2706
2908
|
// Link the engram back to the session that produced it (#960).
|
|
2707
2909
|
session_episode_id: episode.id,
|
|
@@ -3135,6 +3337,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
3135
3337
|
handler: async (args, plur) => {
|
|
3136
3338
|
const engram = await plur.episodeToEngram(args.episode_id, {
|
|
3137
3339
|
scope: args.scope,
|
|
3340
|
+
// The same default as every other new write (#1563 review round 3,
|
|
3341
|
+
// N1): never the process-wide slot.
|
|
3342
|
+
session: await _writeSession(args, plur),
|
|
3138
3343
|
domain: args.domain,
|
|
3139
3344
|
tags: args.tags
|
|
3140
3345
|
});
|
|
@@ -3365,9 +3570,12 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
3365
3570
|
}
|
|
3366
3571
|
|
|
3367
3572
|
export {
|
|
3573
|
+
folderMapAdvice,
|
|
3368
3574
|
registerFlushOnExit,
|
|
3369
3575
|
validateToolArgs,
|
|
3370
3576
|
mcpCanary,
|
|
3577
|
+
folderOnCommand,
|
|
3578
|
+
FOLDER_SCOPE,
|
|
3371
3579
|
CURSOR_CORE_TOOL_NAMES,
|
|
3372
3580
|
resolveToolProfile,
|
|
3373
3581
|
setActiveToolProfile,
|