@plur-ai/mcp 0.19.4 → 0.20.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/README.md +1 -1
- package/dist/chunk-445S5QNU.js +6 -0
- package/dist/{chunk-6HBPG6IR.js → chunk-SWQDW42Z.js} +397 -53
- package/dist/index.js +5 -3
- package/dist/{packs-cli-YQTKUTWY.js → packs-cli-XXZTSBHW.js} +8 -2
- package/dist/{server-IJLE4NG4.js → server-LODRVEQB.js} +4 -2
- package/dist/tools-export.js +2 -1
- package/package.json +4 -3
- package/packs/effective-memory/SKILL.md +17 -8
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ By default (lean profile), your agent gets 12 tools. Everything else is reachabl
|
|
|
63
63
|
| `plur_tensions_purge` | Clear stale/resolved tensions |
|
|
64
64
|
| `plur_admin` | Dispatch to any other tool: `{ action: "plur_packs_install", args: {...} }` |
|
|
65
65
|
|
|
66
|
-
Less commonly needed tools (`plur_recall_hybrid`, `plur_inject_hybrid`, `plur_learn_batch`, `plur_ingest`, `plur_sync`, `plur_packs_install`, `plur_packs_list`, `plur_capture`, `plur_timeline`, and more) are all reachable via `plur_admin`. Set `PLUR_TOOL_PROFILE=full` to expose all
|
|
66
|
+
Less commonly needed tools (`plur_recall_hybrid`, `plur_inject_hybrid`, `plur_learn_batch`, `plur_ingest`, `plur_sync`, `plur_packs_install`, `plur_packs_list`, `plur_capture`, `plur_timeline`, `plur_provenance`, and more) are all reachable via `plur_admin`. Set `PLUR_TOOL_PROFILE=full` to expose all 44 tools directly.
|
|
67
67
|
|
|
68
68
|
A `plur_*` name missing from `tools/list` means it moved behind the gateway, not that the server is down. `plur_admin { action: "help" }` returns every action with a one-line description and its argument schema; `plur_doctor` reports the same inventory as `tool_surface`.
|
|
69
69
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION
|
|
3
|
+
} from "./chunk-445S5QNU.js";
|
|
4
|
+
|
|
1
5
|
// src/telemetry.ts
|
|
2
6
|
import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
|
|
3
7
|
function recordTelemetry(event) {
|
|
@@ -9,14 +13,11 @@ function recordTelemetry(event) {
|
|
|
9
13
|
}
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
// src/version.ts
|
|
13
|
-
var VERSION = "0.19.4";
|
|
14
|
-
|
|
15
16
|
// src/tools.ts
|
|
16
17
|
import { existsSync, unlinkSync } from "fs";
|
|
17
18
|
import { join } from "path";
|
|
18
19
|
import { homedir } from "os";
|
|
19
|
-
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE, mcpRemoteWarningLine, doctorRemoteRemediation, normalizeEndpointUrl, REMOTE_STATUS_TTL_MS, PROBE_CLEARABLE_STATES } from "@plur-ai/core";
|
|
20
|
+
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE, mcpRemoteWarningLine, doctorRemoteRemediation, normalizeEndpointUrl, REMOTE_STATUS_TTL_MS, PROBE_CLEARABLE_STATES, bareEngramId, summariseProvenance, renderProvenanceSummary } from "@plur-ai/core";
|
|
20
21
|
import { z } from "zod";
|
|
21
22
|
function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
22
23
|
return async (prompt) => {
|
|
@@ -89,7 +90,7 @@ var recallHandler = async (args, plur) => {
|
|
|
89
90
|
const measuredUnder = raw.measured_under;
|
|
90
91
|
const measuredAnnotation = measuredUnder ? " [measured under: " + Object.entries(measuredUnder).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ") + "]" : "";
|
|
91
92
|
return {
|
|
92
|
-
id: e.id,
|
|
93
|
+
id: raw._originalId ?? bareEngramId(e.id),
|
|
93
94
|
statement: e.statement + annotation + measuredAnnotation,
|
|
94
95
|
type: e.type,
|
|
95
96
|
scope: e.scope,
|
|
@@ -153,7 +154,7 @@ var recallHandler = async (args, plur) => {
|
|
|
153
154
|
const measuredUnder = raw.measured_under;
|
|
154
155
|
const measuredAnnotation = measuredUnder ? " [measured under: " + Object.entries(measuredUnder).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ") + "]" : "";
|
|
155
156
|
const base = {
|
|
156
|
-
id: e.id,
|
|
157
|
+
id: raw._originalId ?? bareEngramId(e.id),
|
|
157
158
|
statement: e.statement + annotation + measuredAnnotation,
|
|
158
159
|
type: e.type,
|
|
159
160
|
scope: e.scope,
|
|
@@ -299,7 +300,11 @@ var PLUR_GUIDE = `## PLUR Quick Start
|
|
|
299
300
|
5. Call **plur_session_end** before the conversation ends \u2014 suggest new engrams
|
|
300
301
|
|
|
301
302
|
### Core Tools
|
|
302
|
-
- **plur_learn** \u2014
|
|
303
|
+
- **plur_learn** \u2014 one assertion per call, small enough to act on at a glance. The
|
|
304
|
+
mechanism goes in \`rationale\`, the evidence in \`source\`. Call it often, and do not
|
|
305
|
+
force one: a bad engram costs injection budget forever, a missed one costs a re-ask.
|
|
306
|
+
For anything beyond a one-line correction, use the \`plur-create-engrams\` skill \u2014
|
|
307
|
+
it is the authoring contract, not a style preference.
|
|
303
308
|
- **plur_recall** \u2014 search engrams by topic (default: hybrid BM25 + embeddings; use mode:"keyword" for BM25-only)
|
|
304
309
|
- **plur_forget** \u2014 retire an outdated engram`;
|
|
305
310
|
function getLlmFunction() {
|
|
@@ -318,6 +323,32 @@ function sanitizeStatement(raw) {
|
|
|
318
323
|
}
|
|
319
324
|
return raw.slice(0, cut).trimEnd();
|
|
320
325
|
}
|
|
326
|
+
function composeHints(statement, rationale, source) {
|
|
327
|
+
const hints = [];
|
|
328
|
+
const chars = statement.length;
|
|
329
|
+
if (chars <= 400) return void 0;
|
|
330
|
+
if (/\b(on|proven|observed|stated|decided|confirmed)\s+20\d\d-\d\d-\d\d/i.test(statement)) {
|
|
331
|
+
hints.push("carries a dated observation \u2014 that is a citation, move it to `source`");
|
|
332
|
+
}
|
|
333
|
+
const engRefs = statement.match(/\b(ENG|ABS|META)-[A-Za-z0-9-]+/g);
|
|
334
|
+
if (engRefs && engRefs.length >= 2) {
|
|
335
|
+
hints.push(`names ${engRefs.length} other engrams \u2014 use relations.supersedes, or cite them in \`rationale\``);
|
|
336
|
+
}
|
|
337
|
+
if (/\b(because|since|the reason is|which is why)\b/i.test(statement) && !rationale) {
|
|
338
|
+
hints.push("argues its own case inline while `rationale` is empty \u2014 move the mechanism there");
|
|
339
|
+
}
|
|
340
|
+
if (/\b(and also|additionally|separately|furthermore)\b/i.test(statement)) {
|
|
341
|
+
hints.push('contains "and also" \u2014 that is a second engram, split it');
|
|
342
|
+
}
|
|
343
|
+
if (!rationale) {
|
|
344
|
+
hints.push("`rationale` is empty on a long statement: state the mechanism that makes this true, and therefore when it stops being true");
|
|
345
|
+
}
|
|
346
|
+
if (!source) {
|
|
347
|
+
hints.push("`source` is empty: where did this come from");
|
|
348
|
+
}
|
|
349
|
+
if (!hints.length) return void 0;
|
|
350
|
+
return { chars, misplaced: hints };
|
|
351
|
+
}
|
|
321
352
|
var mcpCanary = new CapabilityCanary({ threshold: 10 });
|
|
322
353
|
mcpCanary.expect({
|
|
323
354
|
id: "session_start_hook",
|
|
@@ -514,12 +545,12 @@ function getAllToolDefinitions() {
|
|
|
514
545
|
return [
|
|
515
546
|
{
|
|
516
547
|
name: "plur_learn",
|
|
517
|
-
description: "Create an engram \u2014 record a reusable learning, preference, or correction. A write is never suppressed by similarity: exact content-hash duplicates NOOP, and anything merely SIMILAR is written and reported back in `dedup.near_duplicates` (closest existing engrams
|
|
548
|
+
description: "Create an engram \u2014 record a reusable learning, preference, or correction. A write is never suppressed by similarity: exact content-hash duplicates NOOP, and anything merely SIMILAR is written and reported back in `dedup.near_duplicates` (closest existing engrams, their cosine scores, and a preview of each neighbour's own statement \u2014 read them before moving on; that is what they are for) so you can supersede or merge deliberately. High similarity is a reason to look, not a decision \u2014 cosine cannot tell a duplicate from a correction of it. Multi-agent note: in an orchestration that spawns subagents, have the PARENT session own plur_learn writes \u2014 spawned subagents should return their findings as text for the parent to persist, rather than each calling plur_learn (tool availability is not guaranteed in every subagent context). See plur-ai/plur#281.",
|
|
518
549
|
annotations: { title: "Learn", destructiveHint: false, idempotentHint: false },
|
|
519
550
|
inputSchema: {
|
|
520
551
|
type: "object",
|
|
521
552
|
properties: {
|
|
522
|
-
statement: { type: "string", description:
|
|
553
|
+
statement: { type: "string", description: 'ONE assertion, written so someone who was not there can act on it. Route the rest to the field whose job it is: the mechanism that makes it true goes in `rationale`, where it came from in `source`, when it applies in `tags`/`domain`. An "and also" means a second engram. Good: "Never name a client unless the user names them first, say the customer." Typical 100-300 chars; past ~600 you are carrying another field content. Length is a symptom, not the rule.' },
|
|
523
554
|
type: {
|
|
524
555
|
type: "string",
|
|
525
556
|
enum: ["behavioral", "terminological", "procedural", "architectural"],
|
|
@@ -528,10 +559,10 @@ function getAllToolDefinitions() {
|
|
|
528
559
|
scope: { type: "string", description: "Namespace, e.g. global, project:myapp" },
|
|
529
560
|
domain: { type: "string", description: "Domain tag, e.g. software.deployment" },
|
|
530
561
|
tags: { type: "array", items: { type: "string" }, description: "Searchable keyword tags \u2014 contribute to BM25/embedding recall, so concrete keywords pay off" },
|
|
531
|
-
rationale: { type: "string", description: "
|
|
562
|
+
rationale: { type: "string", description: 'The mechanism that makes the statement true, and therefore the condition under which it would STOP being true. One sentence. "Because the user said so on <date>" is a citation, not a mechanism: that belongs in `source`. This text is indexed, so a real mechanism also carries concrete nouns a future query can match. NOTE: constraints render without rationale (plur-ai/plur#1144), so a mechanism a prohibition needs the model to weigh must stay in the statement.' },
|
|
532
563
|
source: { type: "string", description: "Origin of this knowledge (URL, conversation ref, etc.)" },
|
|
533
564
|
pinned: { type: "boolean", description: "Always-load flag. If true, this engram bypasses the keyword-relevance gate at injection time. Use sparingly: meta-rules, safety conventions, core operating principles only." },
|
|
534
|
-
commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked", "draft"], description: "How firmly the user has committed to this belief (default: leaning). `draft` marks the engram as pending human approval
|
|
565
|
+
commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked", "draft"], description: "How firmly the user has committed to this belief (default: leaning). `draft` marks the engram as pending human approval: core stores and RECALLS it normally but NEVER injects it (#1141), so an unapproved rule cannot shape agent behaviour. Retrieval stays open because reviewing something requires reading it." },
|
|
535
566
|
locked_reason: { type: "string", description: "Why this engram is locked (only meaningful when commitment=locked)" },
|
|
536
567
|
valid_from: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge becomes valid \u2014 inject/recall skip the engram before this date (#347)" },
|
|
537
568
|
valid_until: { type: "string", description: 'ISO date (YYYY-MM-DD) the knowledge expires \u2014 inject/recall skip the engram after this date. Set this for any time-bound fact (offers, deadlines, temporary endpoints). When omitted, an explicit expiry phrase in the statement ("valid until 31 May 2026") is auto-parsed and echoed back (#347)' },
|
|
@@ -539,7 +570,7 @@ function getAllToolDefinitions() {
|
|
|
539
570
|
session_id: { type: "string", description: "Session this write belongs to (from plur_session_start). Resolves the session default scope (incl. mid-session plur_session_scope changes) when no explicit scope is passed. Optional when one session is open; pass it when several are (#243)." },
|
|
540
571
|
measured_under: {
|
|
541
572
|
type: "object",
|
|
542
|
-
description: "Measurement context for numeric or benchmark-derived claims (#869). Records the conditions under which the asserted value was measured \u2014 model, source_type, hardware, dataset, date. When present,
|
|
573
|
+
description: "Measurement context for numeric or benchmark-derived claims (#869). Records the conditions under which the asserted value was measured \u2014 model, source_type, hardware, dataset, date. When present, the tension scanner does not treat two measurements from the same store taken under different configurations as a contradiction (the skipped pair is reported in the scan result). Omit for non-numeric engrams.",
|
|
543
574
|
properties: {
|
|
544
575
|
model: { type: "string", description: 'Model or system variant (e.g. "claude-opus-4", "gpt-4o")' },
|
|
545
576
|
source_type: { type: "string", description: 'Source environment type (e.g. "local-git", "gitlab", "bench", "production")' },
|
|
@@ -547,6 +578,59 @@ function getAllToolDefinitions() {
|
|
|
547
578
|
dataset: { type: "string", description: 'Dataset or workload identifier (e.g. "LongMemEval-S", "plur-bench-2026-Q2")' },
|
|
548
579
|
date: { type: "string", description: "ISO date (YYYY-MM-DD) the measurement was taken" }
|
|
549
580
|
}
|
|
581
|
+
},
|
|
582
|
+
attribution: {
|
|
583
|
+
type: "object",
|
|
584
|
+
description: 'Who is answerable for this memory (#961). Every sub-field optional; OMIT rather than guess \u2014 a memory with no agent is honest, one with an invented agent is worse than none. Set asserted_by to "unidentified" when nobody is identified, rather than leaving it out: absence cannot be told apart from a memory written before this existed.',
|
|
585
|
+
properties: {
|
|
586
|
+
asserted_by: { type: "string", description: 'Who or what asserted it. Any address: a local name, a Decentralized Identifier, or "unidentified".' },
|
|
587
|
+
runtime: {
|
|
588
|
+
type: "object",
|
|
589
|
+
description: "The software writing this. Usually known, so usually worth setting.",
|
|
590
|
+
properties: { name: { type: "string" }, version: { type: "string" } }
|
|
591
|
+
},
|
|
592
|
+
model: {
|
|
593
|
+
type: "object",
|
|
594
|
+
description: "The model behind the statement, if one was involved. Prompt TEXT is never stored, only a hash.",
|
|
595
|
+
properties: {
|
|
596
|
+
name: { type: "string" },
|
|
597
|
+
prompt_id: { type: "string" },
|
|
598
|
+
prompt_version: { type: "string" },
|
|
599
|
+
prompt_sha256: { type: "string" }
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
tool: {
|
|
603
|
+
type: "object",
|
|
604
|
+
description: "An extractor or importer, with its version.",
|
|
605
|
+
properties: { name: { type: "string" }, version: { type: "string" } }
|
|
606
|
+
},
|
|
607
|
+
on_behalf_of: { type: "string", description: "The party the runtime acted for." }
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
claim_class: {
|
|
611
|
+
type: "string",
|
|
612
|
+
enum: ["observed", "documented", "structural", "asserted", "inferred", "revised"],
|
|
613
|
+
description: 'What KIND of claim this is (#963), and the most useful single field for anyone later deciding how much to trust it. Use "asserted" when a PERSON stated it outright, "inferred" when YOU worked it out, "documented" when you took it from prose someone wrote, "observed" for a record of something that happened, "structural" when read off the shape of an artifact, "revised" for a rewrite. Omit only when it genuinely cannot be determined.'
|
|
614
|
+
},
|
|
615
|
+
license: {
|
|
616
|
+
type: "string",
|
|
617
|
+
description: 'Which licence governs reuse of this memory, as an SPDX-style identifier such as "cc-by-4.0" or "apache-2.0". Set it only when the user has actually said which licence applies \u2014 do NOT guess one. Left unset, a default applies that nobody chose, and a provenance record reports it as unchosen rather than presenting it as a decision.'
|
|
618
|
+
},
|
|
619
|
+
// Deliberately exposed to the LLM, reversing #139, which kept
|
|
620
|
+
// `visibility` off this schema because `public` is what gates pack
|
|
621
|
+
// export and shared git sync. Without it an agent cannot mark
|
|
622
|
+
// anything shareable, so every pack built from agent-written
|
|
623
|
+
// memories was empty (#970) — the walkthrough and the agent
|
|
624
|
+
// conversation demo both depend on it. The reason #139 was cautious
|
|
625
|
+
// still holds, and is answered elsewhere: every path `public` opens
|
|
626
|
+
// (pack export, shared sync, remote push, rescope, explicit update,
|
|
627
|
+
// outbox flush) scans the FULL engram — statement plus every other
|
|
628
|
+
// field, `attribution` and `license` included — before content
|
|
629
|
+
// leaves the machine. See `engramContentFields` in core.
|
|
630
|
+
visibility: {
|
|
631
|
+
type: "string",
|
|
632
|
+
enum: ["private", "public", "template"],
|
|
633
|
+
description: 'Whether this memory may leave this machine. Defaults to "private", which means it is EXCLUDED from every exported pack. Set "public" only when the user has said this is shareable with others \u2014 it is their decision, not yours. Without this an agent cannot mark anything shareable at all, so every memory it writes is private forever and any pack built from them is empty.'
|
|
550
634
|
}
|
|
551
635
|
},
|
|
552
636
|
required: ["statement"]
|
|
@@ -556,17 +640,34 @@ function getAllToolDefinitions() {
|
|
|
556
640
|
const context = {
|
|
557
641
|
type: args.type,
|
|
558
642
|
scope: args.scope,
|
|
559
|
-
domain
|
|
643
|
+
// .plur.yaml `domain:` as the default (#1148). The key was parsed by
|
|
644
|
+
// project-config and consumed nowhere, so setting it was a silent
|
|
645
|
+
// no-op — the same shape as injection.pinned_ratio before #1142.
|
|
646
|
+
// Domain is not decorative: scoreEngram counts every matching
|
|
647
|
+
// hierarchy segment as a FULL term hit, double the weight of a
|
|
648
|
+
// statement word, so a missing domain forfeits the strongest
|
|
649
|
+
// retrieval signal an author has. Explicit argument always wins.
|
|
650
|
+
domain: args.domain ?? readProjectConfig().domain ?? void 0,
|
|
560
651
|
source: args.source,
|
|
561
652
|
tags: args.tags,
|
|
562
653
|
rationale: args.rationale,
|
|
563
654
|
commitment: args.commitment,
|
|
564
655
|
locked_reason: args.locked_reason,
|
|
656
|
+
// Quota-gated below, before the write — `plur_pin` was the only
|
|
657
|
+
// guarded entry point, and writing a NEW pinned engram is the other
|
|
658
|
+
// normal way to create a pin (#1138 review).
|
|
565
659
|
pinned: args.pinned,
|
|
566
660
|
valid_from: args.valid_from,
|
|
567
661
|
valid_until: args.valid_until,
|
|
568
662
|
supersedes: args.supersedes,
|
|
569
663
|
measured_under: args.measured_under,
|
|
664
|
+
// Who is answerable, and what kind of claim this is (#961, #963).
|
|
665
|
+
// Passed through untouched: we never invent an agent, and we never
|
|
666
|
+
// guess a claim class the caller did not state.
|
|
667
|
+
attribution: args.attribution,
|
|
668
|
+
claim_class: args.claim_class,
|
|
669
|
+
license: args.license,
|
|
670
|
+
visibility: args.visibility,
|
|
570
671
|
// #243: resolve which session's default scope governs this write —
|
|
571
672
|
// explicit session_id first, else the lone open session. Never
|
|
572
673
|
// persisted on the engram (LearnContext.session selects a scope, it
|
|
@@ -608,6 +709,20 @@ function getAllToolDefinitions() {
|
|
|
608
709
|
};
|
|
609
710
|
};
|
|
610
711
|
const statement = sanitizeStatement(args.statement);
|
|
712
|
+
if (context.pinned === true) {
|
|
713
|
+
const q = await plur.pinnedQuota();
|
|
714
|
+
if (q.free <= 0) {
|
|
715
|
+
return {
|
|
716
|
+
success: false,
|
|
717
|
+
error: "pinned_quota_exceeded",
|
|
718
|
+
quota: q.quota,
|
|
719
|
+
used: q.used,
|
|
720
|
+
free: q.free,
|
|
721
|
+
pinned_count: q.count,
|
|
722
|
+
note: "The pinned set has no room left, so this engram cannot be pinned \u2014 a pin that does not fit is dropped at injection time, which is the silent failure the quota exists to prevent. Learn it unpinned (drop `pinned`), or unpin something first with plur_pin {list:true} to see the set and its costs, or raise `injection_budget` / `injection.pinned_ratio` in ~/.plur/config.yaml. The statement was NOT stored \u2014 re-send it once you have decided."
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
611
726
|
try {
|
|
612
727
|
const engram = await plur.learnRouted(statement, context);
|
|
613
728
|
const isOutbox = !!engram.structured_data?._outbox;
|
|
@@ -616,6 +731,20 @@ function getAllToolDefinitions() {
|
|
|
616
731
|
mcpCanary.signal("learn_activity");
|
|
617
732
|
recordTelemetry("learn");
|
|
618
733
|
const dedup = isOutbox ? void 0 : await plur.nearDuplicates(statement, context, engram.id);
|
|
734
|
+
const redraft = (() => {
|
|
735
|
+
const ids = args.supersedes;
|
|
736
|
+
if (!ids?.length) return void 0;
|
|
737
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
738
|
+
const sameDay = ids.filter((id) => {
|
|
739
|
+
const m = /(\d{4})-(\d{2})-?(\d{2})/.exec(id);
|
|
740
|
+
return m ? `${m[1]}-${m[2]}-${m[3]}` === today : false;
|
|
741
|
+
});
|
|
742
|
+
if (!sameDay.length) return void 0;
|
|
743
|
+
return {
|
|
744
|
+
superseded_today: sameDay,
|
|
745
|
+
note: "You are replacing an engram minted today \u2014 that is a redraft, not a correction, and it leaves a chain of near-identical records behind. Think the assertion through once and write it once. If the earlier one was simply wrong, retire it with plur_forget instead of stacking another supersede."
|
|
746
|
+
};
|
|
747
|
+
})();
|
|
619
748
|
return {
|
|
620
749
|
// #914: report the id in the form plur_recall hands back, so a
|
|
621
750
|
// caller that records what it just learned and passes it to
|
|
@@ -632,6 +761,11 @@ function getAllToolDefinitions() {
|
|
|
632
761
|
content_hash: engram.content_hash,
|
|
633
762
|
decision: "ADD",
|
|
634
763
|
...dedup?.near_duplicates?.length ? { dedup } : {},
|
|
764
|
+
...redraft ? { redraft } : {},
|
|
765
|
+
...(() => {
|
|
766
|
+
const c = composeHints(statement, context?.rationale, context?.source);
|
|
767
|
+
return c ? { composition: c } : {};
|
|
768
|
+
})(),
|
|
635
769
|
...temporalEcho(engram),
|
|
636
770
|
...scopeHint(engram.scope, !!routed),
|
|
637
771
|
...domainHint(!!routed),
|
|
@@ -646,7 +780,11 @@ function getAllToolDefinitions() {
|
|
|
646
780
|
mcpCanary.signal("learn_activity");
|
|
647
781
|
recordTelemetry("learn");
|
|
648
782
|
return {
|
|
649
|
-
|
|
783
|
+
// Mirror the happy-path fix (#914): report the namespaced form so a
|
|
784
|
+
// caller holding this id can pass it to plur_forget / plur_feedback
|
|
785
|
+
// without hitting the collision the id form mismatch causes.
|
|
786
|
+
// Outbox engrams stay local-form (same rule as line 1149).
|
|
787
|
+
id: isOutbox ? engram.id : plur.readIdFor(engram),
|
|
650
788
|
statement: engram.statement,
|
|
651
789
|
scope: engram.scope,
|
|
652
790
|
type: engram.type,
|
|
@@ -673,7 +811,7 @@ function getAllToolDefinitions() {
|
|
|
673
811
|
items: {
|
|
674
812
|
type: "object",
|
|
675
813
|
properties: {
|
|
676
|
-
statement: { type: "string", description:
|
|
814
|
+
statement: { type: "string", description: 'ONE assertion, written so someone who was not there can act on it. Route the rest to the field whose job it is: the mechanism that makes it true goes in `rationale`, where it came from in `source`, when it applies in `tags`/`domain`. An "and also" means a second engram. Good: "Never name a client unless the user names them first, say the customer." Typical 100-300 chars; past ~600 you are carrying another field content. Length is a symptom, not the rule.' },
|
|
677
815
|
type: { type: "string", enum: ["behavioral", "terminological", "procedural", "architectural"], description: "Category of the engram" },
|
|
678
816
|
scope: { type: "string", description: "Namespace, e.g. global, project:myapp" },
|
|
679
817
|
domain: { type: "string", description: "Domain tag, e.g. software.deployment" },
|
|
@@ -681,7 +819,7 @@ function getAllToolDefinitions() {
|
|
|
681
819
|
rationale: { type: "string", description: "Why this knowledge matters \u2014 also enters the search corpus" },
|
|
682
820
|
source: { type: "string", description: "Origin of this knowledge (URL, conversation ref, etc.)" },
|
|
683
821
|
pinned: { type: "boolean", description: "Always-load flag. Use sparingly: meta-rules, safety conventions, core principles." },
|
|
684
|
-
commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked", "draft"], description: "How firmly the user has committed (default: leaning). `draft` marks the engram as pending human approval
|
|
822
|
+
commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked", "draft"], description: "How firmly the user has committed (default: leaning). `draft` marks the engram as pending human approval: core stores and RECALLS it normally but NEVER injects it (#1141), so an unapproved rule cannot shape agent behaviour. Retrieval stays open because reviewing something requires reading it." },
|
|
685
823
|
valid_from: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge becomes valid" },
|
|
686
824
|
valid_until: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge expires" },
|
|
687
825
|
measured_under: {
|
|
@@ -735,7 +873,10 @@ function getAllToolDefinitions() {
|
|
|
735
873
|
recordTelemetry("learn");
|
|
736
874
|
const ids = raw.map(() => null);
|
|
737
875
|
for (const r of results) {
|
|
738
|
-
if (r.input_index !== void 0)
|
|
876
|
+
if (r.input_index !== void 0) {
|
|
877
|
+
const isOutbox = !!r.engram.structured_data?._outbox;
|
|
878
|
+
ids[r.input_index] = isOutbox ? r.engram.id : plur.readIdFor(r.engram);
|
|
879
|
+
}
|
|
739
880
|
}
|
|
740
881
|
let batchDomainHint = {};
|
|
741
882
|
const routedInputs = /* @__PURE__ */ new Set();
|
|
@@ -755,19 +896,22 @@ function getAllToolDefinitions() {
|
|
|
755
896
|
}
|
|
756
897
|
return {
|
|
757
898
|
ids,
|
|
758
|
-
results: results.map((r) =>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
899
|
+
results: results.map((r) => {
|
|
900
|
+
const isOutbox = !!r.engram.structured_data?._outbox;
|
|
901
|
+
return {
|
|
902
|
+
input_index: r.input_index,
|
|
903
|
+
id: isOutbox ? r.engram.id : plur.readIdFor(r.engram),
|
|
904
|
+
statement: r.engram.statement,
|
|
905
|
+
scope: r.engram.scope,
|
|
906
|
+
type: r.engram.type,
|
|
907
|
+
decision: r.decision,
|
|
908
|
+
...r.existing_id ? { existing_id: r.existing_id } : {},
|
|
909
|
+
// #856 audit: `dedup` was computed and then dropped here, so the
|
|
910
|
+
// reporting it exists for reached no caller — "anything below the
|
|
911
|
+
// bar is still reported" was not observable anywhere.
|
|
912
|
+
...r.dedup ? { dedup: r.dedup } : {}
|
|
913
|
+
};
|
|
914
|
+
}),
|
|
771
915
|
stats,
|
|
772
916
|
...batchDomainHint,
|
|
773
917
|
...failures.length > 0 ? { failures, warning: `${failures.length} of ${raw.length} engram(s) failed to persist; the rest were written.` } : {}
|
|
@@ -857,7 +1001,11 @@ function getAllToolDefinitions() {
|
|
|
857
1001
|
tokens_used: result.tokens_used,
|
|
858
1002
|
injected_ids: result.injected_ids,
|
|
859
1003
|
// #181: unresolved-tension warnings — flag contradicted context
|
|
860
|
-
...result.warnings ? { warnings: result.warnings } : {}
|
|
1004
|
+
...result.warnings ? { warnings: result.warnings } : {},
|
|
1005
|
+
// #1142: pinned engrams that did not fit. `pinned: true` reads as a
|
|
1006
|
+
// promise; it is priority-subject-to-capacity, and a caller must be
|
|
1007
|
+
// able to see what it did not get.
|
|
1008
|
+
...result.omitted_pinned?.length ? { omitted_pinned: result.omitted_pinned } : {}
|
|
861
1009
|
};
|
|
862
1010
|
}
|
|
863
1011
|
},
|
|
@@ -892,7 +1040,11 @@ function getAllToolDefinitions() {
|
|
|
892
1040
|
injected_ids: result.injected_ids,
|
|
893
1041
|
mode: "hybrid",
|
|
894
1042
|
// #181: unresolved-tension warnings — flag contradicted context
|
|
895
|
-
...result.warnings ? { warnings: result.warnings } : {}
|
|
1043
|
+
...result.warnings ? { warnings: result.warnings } : {},
|
|
1044
|
+
// #1142: pinned engrams that did not fit. `pinned: true` reads as a
|
|
1045
|
+
// promise; it is priority-subject-to-capacity, and a caller must be
|
|
1046
|
+
// able to see what it did not get.
|
|
1047
|
+
...result.omitted_pinned?.length ? { omitted_pinned: result.omitted_pinned } : {}
|
|
896
1048
|
};
|
|
897
1049
|
attachRemoteStoreDegradation(response, plur);
|
|
898
1050
|
return response;
|
|
@@ -958,7 +1110,7 @@ function getAllToolDefinitions() {
|
|
|
958
1110
|
},
|
|
959
1111
|
{
|
|
960
1112
|
name: "plur_pin",
|
|
961
|
-
description:
|
|
1113
|
+
description: `Toggle the always-load (pinned) flag on an engram. Pinned engrams bypass the keyword-relevance gate and load on every session regardless of overlap with the task. Use sparingly \u2014 meta-rules, safety conventions, core operating principles; a fact you need only sometimes should be recalled, not pinned. The pinned set has a QUOTA (injection_budget \xD7 injection.pinned_ratio): "always-load" only means anything if the set fits, so a pin that would exceed it is REFUSED with the current usage and unpin suggestions rather than silently dropping something already pinned. Resolve it by unpinning something or raising the limit \u2014 the choice is the user's, so surface it rather than picking one. Pass {id, pinned:true} to pin, {id, pinned:false} to unpin, {list:true} to list the set with its quota usage.`,
|
|
962
1114
|
annotations: { title: "Pin", destructiveHint: false, idempotentHint: true },
|
|
963
1115
|
inputSchema: {
|
|
964
1116
|
type: "object",
|
|
@@ -971,13 +1123,42 @@ function getAllToolDefinitions() {
|
|
|
971
1123
|
handler: async (args, plur) => {
|
|
972
1124
|
if (args.list === true) {
|
|
973
1125
|
const pinned = await plur.listPinned();
|
|
1126
|
+
const q = await plur.pinnedQuota();
|
|
974
1127
|
return {
|
|
975
1128
|
count: pinned.length,
|
|
1129
|
+
quota: { tokens: q.quota, used: q.used, free: q.free, over: q.over },
|
|
1130
|
+
...q.over ? { warning: `Pinned engrams use ${q.used} tokens against a ${q.quota}-token quota. The overflow is dropped at injection time, so some pinned engrams are NOT being loaded. Unpin some, or raise injection_budget / injection.pinned_ratio.` } : {},
|
|
976
1131
|
pinned: pinned.map((e) => ({ id: e.id, statement: e.statement, scope: e.scope, domain: e.domain }))
|
|
977
1132
|
};
|
|
978
1133
|
}
|
|
979
1134
|
if (!args.id) throw new Error("Provide id (or list:true to list pinned)");
|
|
980
1135
|
const target = args.pinned ?? true;
|
|
1136
|
+
if (target === true) {
|
|
1137
|
+
const q = await plur.pinnedQuota(args.id);
|
|
1138
|
+
if (q.candidate && !q.candidate.fits) {
|
|
1139
|
+
const deficit = q.candidate.would_be - q.quota;
|
|
1140
|
+
const covering = [];
|
|
1141
|
+
let freed = 0;
|
|
1142
|
+
for (const e of q.entries) {
|
|
1143
|
+
if (freed >= deficit) break;
|
|
1144
|
+
covering.push(e);
|
|
1145
|
+
freed += e.cost;
|
|
1146
|
+
}
|
|
1147
|
+
const suggestions = covering.slice(0, 5).map((e) => ({ id: e.id, frees: e.cost, net_feedback: e.net_feedback, last_accessed: e.last_accessed, statement: e.statement.slice(0, 100) }));
|
|
1148
|
+
return {
|
|
1149
|
+
success: false,
|
|
1150
|
+
error: "pinned_quota_exceeded",
|
|
1151
|
+
quota: q.quota,
|
|
1152
|
+
used: q.used,
|
|
1153
|
+
this_engram_cost: q.candidate.cost,
|
|
1154
|
+
would_be: q.candidate.would_be,
|
|
1155
|
+
over_by: deficit,
|
|
1156
|
+
unpin_candidates: suggestions,
|
|
1157
|
+
unpins_needed: covering.length,
|
|
1158
|
+
note: `Pinned engrams are always-load, so the set cannot exceed its share of the injection budget \u2014 over-committing means silently dropping something already pinned. ` + (covering.length > suggestions.length ? `At least ${covering.length} unpins are needed to fit this one; the ${suggestions.length} largest are listed. ` : `Unpin ${covering.length === 1 ? "the suggestion" : "the suggestions"} below to fit this one. `) + "Or raise `injection_budget` / `injection.pinned_ratio` in ~/.plur/config.yaml. Candidates are ordered by what they free, NOT by importance: feedback covers ~4% of engrams so it cannot rank them, and a ranking that looks authoritative would put a thumb on a decision only the user can make."
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
981
1162
|
const updated = await plur.setPinnedAsync(args.id, target);
|
|
982
1163
|
if (!updated) throw new Error(`Engram not found: ${args.id}`);
|
|
983
1164
|
return {
|
|
@@ -996,6 +1177,10 @@ function getAllToolDefinitions() {
|
|
|
996
1177
|
properties: {
|
|
997
1178
|
id: { type: "string", description: "Exact engram ID to retire" },
|
|
998
1179
|
search: { type: "string", description: "Search term to find engram to retire" },
|
|
1180
|
+
reason: {
|
|
1181
|
+
type: "string",
|
|
1182
|
+
description: "Why this is being retired. Recorded in the history log and on the engram (#959). Say what changed, not just that something did."
|
|
1183
|
+
},
|
|
999
1184
|
scope: { type: "string", description: 'Which store holds it (#831). Ids are minted per store, so one id can name several unrelated engrams. Pass "primary" to stay on disk \u2014 the local primary store and any local secondary stores, never a remote \u2014 or a remote scope (e.g. "group:plur/plur-ai/engineering") to target that server. A scope matching no configured store is rejected, not guessed at. Omit it and an id resolving in two places is refused.' }
|
|
1000
1185
|
}
|
|
1001
1186
|
},
|
|
@@ -1005,17 +1190,17 @@ function getAllToolDefinitions() {
|
|
|
1005
1190
|
const engram = scope ? void 0 : await plur.getById(args.id);
|
|
1006
1191
|
if (engram) {
|
|
1007
1192
|
if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
|
|
1008
|
-
await plur.forget(args.id,
|
|
1193
|
+
await plur.forget(args.id, args.reason, { force: true });
|
|
1009
1194
|
return { success: true, retired: { id: engram.id, statement: engram.statement } };
|
|
1010
1195
|
}
|
|
1011
|
-
await plur.forget(args.id,
|
|
1196
|
+
await plur.forget(args.id, args.reason, { force: true, ...scope ? { scope } : {} });
|
|
1012
1197
|
return { success: true, retired: { id: args.id, ...scope ? { scope } : {} } };
|
|
1013
1198
|
}
|
|
1014
1199
|
if (args.search) {
|
|
1015
1200
|
const matches = await plur.recall(args.search, { limit: 100, remote: false });
|
|
1016
1201
|
if (matches.length === 0) return { success: false, error: `No active engrams matching "${args.search}"` };
|
|
1017
1202
|
if (matches.length === 1) {
|
|
1018
|
-
await plur.forget(matches[0].id,
|
|
1203
|
+
await plur.forget(matches[0].id, args.reason, { force: true });
|
|
1019
1204
|
return { success: true, retired: { id: matches[0].id, statement: matches[0].statement } };
|
|
1020
1205
|
}
|
|
1021
1206
|
return {
|
|
@@ -1162,6 +1347,20 @@ function getAllToolDefinitions() {
|
|
|
1162
1347
|
conflicts: result.conflicts,
|
|
1163
1348
|
security: result.security,
|
|
1164
1349
|
registry: result.registry,
|
|
1350
|
+
// ENGRAM-STANDARD-v1 §5.6.5: what was neutralized, by field, and the
|
|
1351
|
+
// integrity verdict with "shipped none" distinct from "matched". Both
|
|
1352
|
+
// were computed and then dropped at this surface, so an agent
|
|
1353
|
+
// installing a pack could not tell the user either.
|
|
1354
|
+
neutralized: result.neutralized,
|
|
1355
|
+
integrity_check: result.integrity_check,
|
|
1356
|
+
// The four provenance counts §5.6.5 requires a consumer to report:
|
|
1357
|
+
// records found against engrams shipped, how many were unreadable,
|
|
1358
|
+
// how many name an engram the pack does not contain, and how many
|
|
1359
|
+
// engrams have no record. Computed by the preview install already
|
|
1360
|
+
// runs, and dropped at this surface until now — so an agent
|
|
1361
|
+
// installing a pack could not tell the user any of it. Absent when
|
|
1362
|
+
// the pack shipped no provenance directory at all.
|
|
1363
|
+
provenance: result.provenance,
|
|
1165
1364
|
success: true
|
|
1166
1365
|
};
|
|
1167
1366
|
}
|
|
@@ -1518,6 +1717,8 @@ function getAllToolDefinitions() {
|
|
|
1518
1717
|
// Core reports these; this hand-built response dropped them, so an
|
|
1519
1718
|
// agent asking for status saw a healthy-looking `pack_count: 0`.
|
|
1520
1719
|
...status.store_errors ? { store_errors: status.store_errors } : {},
|
|
1720
|
+
// Spreading-activation drop counters — absent when both are zero.
|
|
1721
|
+
...status.spread_drops ? { spread_drops: status.spread_drops } : {},
|
|
1521
1722
|
// Version check (issue #151)
|
|
1522
1723
|
...versionCheck?.updateAvailable && versionCheck.latest ? {
|
|
1523
1724
|
update_available: {
|
|
@@ -1530,9 +1731,99 @@ function getAllToolDefinitions() {
|
|
|
1530
1731
|
};
|
|
1531
1732
|
}
|
|
1532
1733
|
},
|
|
1734
|
+
{
|
|
1735
|
+
name: "plur_provenance",
|
|
1736
|
+
description: 'Where a memory came from: who asserted it, whether a person stated it or a model worked it out, when, what it came from, and whether you may reuse it. Read-only. Accepts an engram id or a search term \u2014 nobody remembers ids. IMPORTANT when relaying to the user: report the `not_recorded` list as prominently as the rest. A memory written before provenance was captured genuinely cannot say who asserted it, and presenting the record as complete would make it look more authoritative than it is. Nothing here is guessed. Prefer relaying `summary`; ask for format "record" only when a machine-readable document is actually needed. NOT plur_receipt: this describes the origin of ONE memory; plur_receipt counts how the whole store is being used.',
|
|
1737
|
+
annotations: { title: "Where a memory came from", readOnlyHint: true, idempotentHint: true },
|
|
1738
|
+
inputSchema: {
|
|
1739
|
+
type: "object",
|
|
1740
|
+
properties: {
|
|
1741
|
+
id: { type: "string", description: "Exact engram id, e.g. ENG-2026-08-21-086" },
|
|
1742
|
+
search: { type: "string", description: "Find the engram by what it says, if you do not know its id" },
|
|
1743
|
+
format: {
|
|
1744
|
+
type: "string",
|
|
1745
|
+
enum: ["summary", "record"],
|
|
1746
|
+
description: "summary (default) is prose a person can read. record is the JSON-LD document, for machines."
|
|
1747
|
+
}
|
|
1748
|
+
// No `save` here. This tool is annotated read-only and idempotent,
|
|
1749
|
+
// and a host may run it without asking on that basis; a flag that
|
|
1750
|
+
// writes files would make the annotation a lie. Writing a record is
|
|
1751
|
+
// `plur provenance --write` on the command line, or
|
|
1752
|
+
// `plur.writeProvenance()`.
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
handler: async (args, plur) => {
|
|
1756
|
+
let id = args.id;
|
|
1757
|
+
let matchedStatement;
|
|
1758
|
+
let matchCount = 0;
|
|
1759
|
+
const ellipsise = (t, n) => {
|
|
1760
|
+
const chars = Array.from(t);
|
|
1761
|
+
return chars.length > n ? `${chars.slice(0, n).join("")}\u2026` : t;
|
|
1762
|
+
};
|
|
1763
|
+
let alternatives = [];
|
|
1764
|
+
if (!id && typeof args.search === "string" && args.search.length) {
|
|
1765
|
+
const LIST = 3;
|
|
1766
|
+
const matches = await plur.recall(args.search, { limit: 25, remote: false });
|
|
1767
|
+
if (!matches.length) {
|
|
1768
|
+
return {
|
|
1769
|
+
found: false,
|
|
1770
|
+
message: `Nothing matched "${args.search}". Try different words, or pass an exact id.`
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
id = matches[0].id;
|
|
1774
|
+
matchedStatement = matches[0].statement;
|
|
1775
|
+
matchCount = matches.length;
|
|
1776
|
+
alternatives = matches.slice(1, LIST).map((m) => ({ id: m.id, statement: ellipsise(m.statement, 80) }));
|
|
1777
|
+
}
|
|
1778
|
+
if (!id) {
|
|
1779
|
+
return { found: false, message: "Pass either an engram id or a search term." };
|
|
1780
|
+
}
|
|
1781
|
+
const record = await plur.provenanceFor(id, { mode: "portable" });
|
|
1782
|
+
if (!record) {
|
|
1783
|
+
return { found: false, message: `No engram with id ${id}.` };
|
|
1784
|
+
}
|
|
1785
|
+
if (args.format !== void 0 && args.format !== "summary" && args.format !== "record") {
|
|
1786
|
+
return {
|
|
1787
|
+
found: false,
|
|
1788
|
+
message: `Unknown format "${String(args.format)}". Use "summary" for prose or "record" for the JSON-LD document.`
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
const summary = summariseProvenance(record);
|
|
1792
|
+
if (args.format === "record") {
|
|
1793
|
+
return {
|
|
1794
|
+
found: true,
|
|
1795
|
+
engram_id: id,
|
|
1796
|
+
record,
|
|
1797
|
+
// The same answers the summary gives. Asking for the document used
|
|
1798
|
+
// to mean losing every reuse verdict, so the same question got an
|
|
1799
|
+
// answer through one surface and silence through the other.
|
|
1800
|
+
...summary.fields,
|
|
1801
|
+
not_recorded: summary.missing,
|
|
1802
|
+
complete: summary.complete
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
return {
|
|
1806
|
+
found: true,
|
|
1807
|
+
engram_id: id,
|
|
1808
|
+
...matchedStatement ? { matched: matchedStatement.slice(0, 200) } : {},
|
|
1809
|
+
summary: renderProvenanceSummary(summary),
|
|
1810
|
+
// Structured values, NOT a line-split of the prose above. `facts`
|
|
1811
|
+
// used to be exactly that: the same text a second time, which an
|
|
1812
|
+
// agent pays for twice and cannot parse either copy of.
|
|
1813
|
+
...summary.fields,
|
|
1814
|
+
not_recorded: summary.missing,
|
|
1815
|
+
complete: summary.complete,
|
|
1816
|
+
...matchCount > 1 ? {
|
|
1817
|
+
note: `${matchCount} engrams matched "${String(args.search)}"; this is the closest.` + (matchCount - 1 > alternatives.length ? ` Showing ${alternatives.length} of the other ${matchCount - 1}.` : ""),
|
|
1818
|
+
match_count: matchCount,
|
|
1819
|
+
other_matches: alternatives
|
|
1820
|
+
} : {}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
},
|
|
1533
1824
|
{
|
|
1534
1825
|
name: "plur_receipt",
|
|
1535
|
-
description: 'Counted report of what your memory retrieved for you: engrams stored, how many were retrieved and how often, which are most relied on, and how much of the store is dormant. Local and read-only; every figure is directly counted, never estimated. IMPORTANT when relaying to the user: `activation_rate` is COVERAGE over the logging window (\u2248 how much of the store was surfaced), NOT a quality or effectiveness score \u2014 it is naturally low and FALLS as more engrams are added, so never present it as "memory is N% effective". A `summary` line is included; prefer relaying that.',
|
|
1826
|
+
description: 'Counted report of what your memory retrieved for you: engrams stored, how many were retrieved and how often, which are most relied on, and how much of the store is dormant. Local and read-only; every figure is directly counted, never estimated. IMPORTANT when relaying to the user: `activation_rate` is COVERAGE over the logging window (\u2248 how much of the store was surfaced), NOT a quality or effectiveness score \u2014 it is naturally low and FALLS as more engrams are added, so never present it as "memory is N% effective". A `summary` line is included; prefer relaying that. NOT plur_provenance: this counts usage across the whole store; plur_provenance says where a single memory came from.',
|
|
1536
1827
|
annotations: { title: "Memory receipt", readOnlyHint: true, idempotentHint: true },
|
|
1537
1828
|
inputSchema: {
|
|
1538
1829
|
type: "object",
|
|
@@ -1822,6 +2113,7 @@ function getAllToolDefinitions() {
|
|
|
1822
2113
|
const explicit_default_scope = args.default_scope ?? null;
|
|
1823
2114
|
const default_scope = explicit_default_scope ?? projectConfig.scope ?? null;
|
|
1824
2115
|
const scope_source = explicit_default_scope ? "caller" : projectConfig.scope ? "project-config" : "none";
|
|
2116
|
+
const default_domain = projectConfig.domain ?? null;
|
|
1825
2117
|
plur.setSessionScope(default_scope);
|
|
1826
2118
|
plur.setSessionScope(default_scope, { session: session_id });
|
|
1827
2119
|
{
|
|
@@ -1853,9 +2145,15 @@ function getAllToolDefinitions() {
|
|
|
1853
2145
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1854
2146
|
if (result.count > 0) {
|
|
1855
2147
|
const lines = [];
|
|
1856
|
-
if (result.
|
|
1857
|
-
if (result.
|
|
2148
|
+
if (result.constraints) lines.push("## CONSTRAINTS\n", result.constraints);
|
|
2149
|
+
if (result.directives) lines.push("\n## DIRECTIVES\n", result.directives);
|
|
1858
2150
|
if (result.consider) lines.push("\n## ALSO CONSIDER\n", result.consider);
|
|
2151
|
+
if (result.omitted_pinned?.length) {
|
|
2152
|
+
lines.push(
|
|
2153
|
+
"\n## PINNED, NOT LOADED\n",
|
|
2154
|
+
`${result.omitted_pinned.length} pinned engram(s) did not fit this injection: ${result.omitted_pinned.map((o) => o.id).join(", ")}. Treat them as unread, not as absent \u2014 recall one explicitly if the task touches it.`
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
1859
2157
|
engrams = { text: lines.join("\n"), count: result.count, injected_ids: result.injected_ids };
|
|
1860
2158
|
}
|
|
1861
2159
|
} catch {
|
|
@@ -1867,9 +2165,15 @@ function getAllToolDefinitions() {
|
|
|
1867
2165
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1868
2166
|
if (result.count > 0) {
|
|
1869
2167
|
const lines = [];
|
|
1870
|
-
if (result.
|
|
1871
|
-
if (result.
|
|
2168
|
+
if (result.constraints) lines.push("## CONSTRAINTS\n", result.constraints);
|
|
2169
|
+
if (result.directives) lines.push("\n## DIRECTIVES\n", result.directives);
|
|
1872
2170
|
if (result.consider) lines.push("\n## ALSO CONSIDER\n", result.consider);
|
|
2171
|
+
if (result.omitted_pinned?.length) {
|
|
2172
|
+
lines.push(
|
|
2173
|
+
"\n## PINNED, NOT LOADED\n",
|
|
2174
|
+
`${result.omitted_pinned.length} pinned engram(s) did not fit this injection: ${result.omitted_pinned.map((o) => o.id).join(", ")}. Treat them as unread, not as absent \u2014 recall one explicitly if the task touches it.`
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
1873
2177
|
engrams = { text: lines.join("\n"), count: result.count, injected_ids: result.injected_ids };
|
|
1874
2178
|
}
|
|
1875
2179
|
}
|
|
@@ -1972,6 +2276,7 @@ Tool profile "${session_tool_profile}": most plur_* tools are not exposed by nam
|
|
|
1972
2276
|
// Remote scope routing info (#229)
|
|
1973
2277
|
...remote_scopes.length > 0 ? { remote_scopes } : {},
|
|
1974
2278
|
...default_scope ? { default_scope, scope_source } : {},
|
|
2279
|
+
...default_domain ? { default_domain, domain_source: "project-config" } : {},
|
|
1975
2280
|
// Ask LLM to check back — MCP can't push, but we can request a follow-up
|
|
1976
2281
|
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,
|
|
1977
2282
|
// On fresh install, suggest hook setup for reliable injection
|
|
@@ -2128,7 +2433,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2128
2433
|
const summary = args.summary;
|
|
2129
2434
|
const session_id = args.session_id;
|
|
2130
2435
|
const suggestions = args.engram_suggestions;
|
|
2131
|
-
|
|
2436
|
+
const items = [];
|
|
2132
2437
|
if (Array.isArray(suggestions) && suggestions.length) {
|
|
2133
2438
|
for (let i = 0; i < suggestions.length; i++) {
|
|
2134
2439
|
const s = suggestions[i];
|
|
@@ -2145,14 +2450,31 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2145
2450
|
`engram_suggestions[${i}] must be a string or {statement: string, type?: string}, got ${typeof s}`
|
|
2146
2451
|
);
|
|
2147
2452
|
}
|
|
2148
|
-
|
|
2149
|
-
engrams_created++;
|
|
2453
|
+
items.push({ statement, type });
|
|
2150
2454
|
}
|
|
2151
2455
|
}
|
|
2152
2456
|
const episode = plur.capture(summary, {
|
|
2153
2457
|
session_id,
|
|
2154
2458
|
channel: "mcp"
|
|
2155
2459
|
});
|
|
2460
|
+
let engrams_created = 0;
|
|
2461
|
+
const engrams_failed = [];
|
|
2462
|
+
for (let i = 0; i < items.length; i++) {
|
|
2463
|
+
const { statement, type } = items[i];
|
|
2464
|
+
try {
|
|
2465
|
+
await plur.learn(statement, {
|
|
2466
|
+
type,
|
|
2467
|
+
// Link the engram back to the session that produced it (#960).
|
|
2468
|
+
session_episode_id: episode.id,
|
|
2469
|
+
// An end-of-session summary is the model's reading of what
|
|
2470
|
+
// happened, not something the user stated outright (#963).
|
|
2471
|
+
claim_class: "inferred"
|
|
2472
|
+
});
|
|
2473
|
+
engrams_created++;
|
|
2474
|
+
} catch (err) {
|
|
2475
|
+
engrams_failed.push({ index: i, statement: statement.slice(0, 80), error: err.message });
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2156
2478
|
const telemetry = session_id ? _sessionTelemetry.get(session_id) : void 0;
|
|
2157
2479
|
const injection_summary = telemetry && telemetry.injection_calls > 0 ? {
|
|
2158
2480
|
pack_counts: { ...telemetry.pack_counts },
|
|
@@ -2179,10 +2501,11 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2179
2501
|
const status = await plur.status();
|
|
2180
2502
|
return {
|
|
2181
2503
|
engrams_created,
|
|
2504
|
+
...engrams_failed.length ? { engrams_failed } : {},
|
|
2182
2505
|
episode_id: episode.id,
|
|
2183
2506
|
total_engrams: status.engram_count,
|
|
2184
2507
|
...injection_summary ? { injection_summary } : {},
|
|
2185
|
-
hint: engrams_created === 0 ? "No engrams captured this session. If any corrections, preferences, or patterns came up, consider calling plur_learn before ending." : void 0
|
|
2508
|
+
hint: engrams_failed.length ? `${engrams_failed.length} suggestion(s) could not be stored \u2014 see engrams_failed. The rest were.` : engrams_created === 0 ? "No engrams captured this session. If any corrections, preferences, or patterns came up, consider calling plur_learn before ending." : void 0
|
|
2186
2509
|
};
|
|
2187
2510
|
}
|
|
2188
2511
|
},
|
|
@@ -2459,12 +2782,15 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2459
2782
|
batch_size: args.batch_size,
|
|
2460
2783
|
temporal_domains: tensionsConfig.temporal_domains,
|
|
2461
2784
|
snapshot_pairs: tensionsConfig.snapshot_pairs,
|
|
2785
|
+
measured_under_pairs: tensionsConfig.measured_under_pairs,
|
|
2462
2786
|
temporal_discount: args.temporal_discount ?? tensionsConfig.temporal_discount,
|
|
2463
2787
|
...persist ? { exclude_pairs: new Set(plur.suppressedTensionPairKeys()) } : {}
|
|
2464
2788
|
});
|
|
2465
2789
|
const persisted = persist && result.tensions.length > 0 ? await plur.recordTensions(result.tensions) : void 0;
|
|
2466
2790
|
return {
|
|
2467
2791
|
pairs_checked: result.pairs_checked,
|
|
2792
|
+
// #869 review: policy skips are reported, never silent.
|
|
2793
|
+
skipped: result.skipped,
|
|
2468
2794
|
count: result.new_tensions,
|
|
2469
2795
|
...persisted ? { persisted_new: persisted.new_count } : {},
|
|
2470
2796
|
tensions: result.tensions.map((t, i) => ({
|
|
@@ -2642,7 +2968,11 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2642
2968
|
filter_tags: { type: "array", items: { type: "string" }, description: "Filter by tags" },
|
|
2643
2969
|
filter_type: { type: "string", enum: ["behavioral", "procedural", "architectural", "terminological"], description: "Filter by engram type" },
|
|
2644
2970
|
output_dir: { type: "string", description: "Output directory (default: ~/plur-packs/<name>)" },
|
|
2645
|
-
creator: { type: "string", description: "Creator name" }
|
|
2971
|
+
creator: { type: "string", description: "Creator name" },
|
|
2972
|
+
license: {
|
|
2973
|
+
type: "string",
|
|
2974
|
+
description: 'Licence for the pack as a collection, e.g. "cc-by-4.0", "apache-2.0", "cc0-1.0", or "unlicensed" to grant nothing. REQUIRED unless the user has set provenance.default_license in their config \u2014 export fails without one. Ask the user which licence applies; do NOT guess. A pack goes to strangers, and leaving this blank does not leave it blank: a share-alike default fills in that nobody agreed to.'
|
|
2975
|
+
}
|
|
2646
2976
|
},
|
|
2647
2977
|
required: ["name"]
|
|
2648
2978
|
},
|
|
@@ -2665,12 +2995,27 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2665
2995
|
const { homedir: homedir2 } = await import("os");
|
|
2666
2996
|
const { join: join2 } = await import("path");
|
|
2667
2997
|
const outputDir = args.output_dir || join2(homedir2(), "plur-packs", name);
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2998
|
+
let result;
|
|
2999
|
+
try {
|
|
3000
|
+
result = plur.exportPack(engrams, outputDir, {
|
|
3001
|
+
name,
|
|
3002
|
+
version: "1.0.0",
|
|
3003
|
+
description: args.description,
|
|
3004
|
+
creator: args.creator || void 0,
|
|
3005
|
+
license: args.license || void 0
|
|
3006
|
+
});
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
const message = err.message;
|
|
3009
|
+
if (/needs a licence/.test(message)) {
|
|
3010
|
+
return {
|
|
3011
|
+
exported: false,
|
|
3012
|
+
error: "This pack has no licence, and export will not choose one.",
|
|
3013
|
+
next_step: 'Ask the user which licence applies to this pack (for example cc-by-4.0, apache-2.0, cc0-1.0, or "unlicensed" to grant nothing) and call plur_packs_export again with `license` set. The user can also set provenance.default_license in their config to answer once.',
|
|
3014
|
+
name
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
throw err;
|
|
3018
|
+
}
|
|
2674
3019
|
return {
|
|
2675
3020
|
path: result.path,
|
|
2676
3021
|
engram_count: result.engram_count,
|
|
@@ -2752,7 +3097,6 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2752
3097
|
|
|
2753
3098
|
export {
|
|
2754
3099
|
registerFlushOnExit,
|
|
2755
|
-
VERSION,
|
|
2756
3100
|
validateToolArgs,
|
|
2757
3101
|
mcpCanary,
|
|
2758
3102
|
CURSOR_CORE_TOOL_NAMES,
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION
|
|
4
|
+
} from "./chunk-445S5QNU.js";
|
|
2
5
|
|
|
3
6
|
// src/index.ts
|
|
4
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
5
8
|
import { join } from "path";
|
|
6
9
|
import { fileURLToPath } from "url";
|
|
7
10
|
import { homedir, platform } from "os";
|
|
8
|
-
var VERSION = "0.19.4";
|
|
9
11
|
var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
|
|
10
12
|
|
|
11
13
|
Usage:
|
|
@@ -295,7 +297,7 @@ async function runInit() {
|
|
|
295
297
|
async function runPacks() {
|
|
296
298
|
const plurPath = process.env.PLUR_PATH ?? join(homedir(), ".plur");
|
|
297
299
|
const { Plur } = await import("@plur-ai/core");
|
|
298
|
-
const { packsCommand } = await import("./packs-cli-
|
|
300
|
+
const { packsCommand } = await import("./packs-cli-XXZTSBHW.js");
|
|
299
301
|
const plur = new Plur({ path: plurPath });
|
|
300
302
|
const result = await packsCommand(process.argv.slice(3), plur);
|
|
301
303
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
@@ -321,7 +323,7 @@ if (arg === "packs") {
|
|
|
321
323
|
process.exit(0);
|
|
322
324
|
}
|
|
323
325
|
if (arg === "serve" || arg === void 0) {
|
|
324
|
-
const { runStdio } = await import("./server-
|
|
326
|
+
const { runStdio } = await import("./server-LODRVEQB.js");
|
|
325
327
|
runStdio().catch((err) => {
|
|
326
328
|
console.error("Failed to start PLUR MCP server:", err);
|
|
327
329
|
process.exit(1);
|
|
@@ -7,8 +7,14 @@ async function packsCommand(args, plur) {
|
|
|
7
7
|
if (!arg) return fail("Usage: plur-mcp packs install <path>\n");
|
|
8
8
|
try {
|
|
9
9
|
const result = await plur.installPack(arg);
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let out = `Installed pack '${result.name}' (${result.installed} engrams)
|
|
11
|
+
`;
|
|
12
|
+
const n = result.neutralized;
|
|
13
|
+
if (n?.pinned_stripped) out += ` neutralized: pinned removed from ${n.pinned_stripped} engram(s)
|
|
14
|
+
`;
|
|
15
|
+
if (n?.locked_downgraded) out += ` neutralized: commitment: locked downgraded to decided on ${n.locked_downgraded} engram(s)
|
|
16
|
+
`;
|
|
17
|
+
return ok(out);
|
|
12
18
|
} catch (err) {
|
|
13
19
|
return fail(`Error: ${err.message}
|
|
14
20
|
`);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CURSOR_CORE_TOOL_NAMES,
|
|
3
|
-
VERSION,
|
|
4
3
|
getToolDefinitions,
|
|
5
4
|
mcpCanary,
|
|
6
5
|
registerFlushOnExit,
|
|
7
6
|
resolveToolProfile,
|
|
8
7
|
setActiveToolProfile,
|
|
9
8
|
validateToolArgs
|
|
10
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-SWQDW42Z.js";
|
|
10
|
+
import {
|
|
11
|
+
VERSION
|
|
12
|
+
} from "./chunk-445S5QNU.js";
|
|
11
13
|
|
|
12
14
|
// src/server.ts
|
|
13
15
|
import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
package/dist/tools-export.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plur-ai/mcp",
|
|
3
3
|
"mcpName": "io.github.plur-ai/plur",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"plur-mcp": "dist/index.js"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
|
17
17
|
"@modelcontextprotocol/core": "2.0.0-beta.4",
|
|
18
18
|
"zod": "^3.23.0",
|
|
19
|
-
"@plur-ai/core": "0.
|
|
19
|
+
"@plur-ai/core": "0.20.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^25.5.0"
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup",
|
|
54
|
-
"test": "vitest run"
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"pretest": "npm run build"
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -11,11 +11,14 @@ x-datacore:
|
|
|
11
11
|
match_terms: [memory, learn, remember, session, feedback, engram, forget, correction, preference, recall, verification, safety, plur]
|
|
12
12
|
domain: plur.best-practices
|
|
13
13
|
engram_count: 12
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
14
|
+
# The engrams below still carry `pinned: true`, but INSTALL STRIPS IT.
|
|
15
|
+
# `sanitizePackEngrams` removes `pinned` and downgrades `commitment: locked`
|
|
16
|
+
# from every pack, this one included, because a pack is an archive from a
|
|
17
|
+
# stranger and its author does not get to decide what is always in front of
|
|
18
|
+
# the recipient's model. The flags are left in place so the intent is
|
|
19
|
+
# legible and so the behaviour returns if the host ever grows a way to
|
|
20
|
+
# grant it deliberately. `injection_policy: on_match` is what actually
|
|
21
|
+
# governs this pack. See plur-ai/plur#1019.
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
# Effective Memory
|
|
@@ -24,7 +27,9 @@ Your agent has memory. These habits make it actually useful.
|
|
|
24
27
|
|
|
25
28
|
Without them, memory is a growing pile of assertions nobody retrieves. With them, memory compounds — each session builds on the last, corrections stick, and the agent gets measurably better over time.
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
These engrams cover the meta-rules every agent needs regardless of domain: how to capture corrections, when to recall before answering, what "verified" means, how to stay safe with destructive actions, and why never to type a weekday from memory.
|
|
31
|
+
|
|
32
|
+
They were written to be **pinned** — always eligible for injection, bypassing keyword gating. That is no longer what happens. Install strips `pinned` from every pack, this one included, so these engrams are matched on keywords like any other (`injection_policy: on_match`, with the `match_terms` above). The stripping is right — a pack author should not be able to occupy a recipient's context unconditionally — but it means this pack's meta-rules only surface when a session's wording happens to touch them.
|
|
28
33
|
|
|
29
34
|
## Install
|
|
30
35
|
|
|
@@ -46,9 +51,13 @@ npx @plur-ai/cli@0.9.4 packs install effective-memory
|
|
|
46
51
|
- **Discipline** — read before edit; don't ask "want to continue?" mid-task.
|
|
47
52
|
- **Time** — never type a day-of-week from memory.
|
|
48
53
|
|
|
49
|
-
## Why pinned
|
|
54
|
+
## Why these were written pinned, and what happens instead
|
|
55
|
+
|
|
56
|
+
Pinned engrams bypass the keyword-relevance gate in `scoreEngram` and the per-pack and per-domain caps in `fillTokenBudget`. Cross-cutting meta-rules are the case that justifies it: "call `plur_learn` when corrected" is relevant to every session and keyword-matches almost none of them.
|
|
57
|
+
|
|
58
|
+
**Install strips the flag**, so that is not the behaviour you get. `sanitizePackEngrams` removes `pinned` and downgrades `commitment: locked` from every installed pack, and it is right to — a pack is an archive from a stranger, and letting its author decide what is permanently in front of your model is a privilege no producer should take by shipping a YAML field.
|
|
50
59
|
|
|
51
|
-
|
|
60
|
+
The consequence is that this pack's rules are keyword-gated, which is a weaker guarantee than they were designed for. Whether a host should be able to grant always-inject to a pack it trusts deliberately — as opposed to a pack claiming it — is an open question, tracked in plur-ai/plur#1019.
|
|
52
61
|
|
|
53
62
|
## Versioning
|
|
54
63
|
|