@plur-ai/core 0.9.6 → 0.9.8
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/dist/index.d.ts +27 -0
- package/dist/index.js +153 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2273,6 +2273,33 @@ declare class Plur {
|
|
|
2273
2273
|
* Fast-path hash dedup returns existing on exact match.
|
|
2274
2274
|
*/
|
|
2275
2275
|
learn(statement: string, context?: LearnContext): Engram;
|
|
2276
|
+
/**
|
|
2277
|
+
* Async learn that returns the canonical engram — server-assigned ID
|
|
2278
|
+
* for remote-routed writes, locally-built engram for local writes.
|
|
2279
|
+
*
|
|
2280
|
+
* Use this from async callers (MCP handlers, OpenClaw plugins, etc.)
|
|
2281
|
+
* when the user later needs to reference the engram by ID (forget,
|
|
2282
|
+
* feedback, history). The sync `learn()` returns a local-placeholder
|
|
2283
|
+
* ID for remote-routed writes — the actual server engram has a
|
|
2284
|
+
* different ID, so feedback/forget against the placeholder fails.
|
|
2285
|
+
*
|
|
2286
|
+
* Local writes: just delegates to sync learn(). Same dedup, same
|
|
2287
|
+
* history append, same return shape.
|
|
2288
|
+
*
|
|
2289
|
+
* Remote writes: bypasses local YAML entirely. POSTs to the remote's
|
|
2290
|
+
* /api/v1/engrams, awaits the server's response, and returns an
|
|
2291
|
+
* Engram with the server-assigned id. Throws on remote failure
|
|
2292
|
+
* (caller knows the write didn't land — better UX than a fire-and-
|
|
2293
|
+
* forget that pretends success and leaves the user with a phantom ID).
|
|
2294
|
+
*/
|
|
2295
|
+
learnRouted(statement: string, context?: LearnContext): Promise<Engram>;
|
|
2296
|
+
/**
|
|
2297
|
+
* Build an Engram object without persisting it. Used by learnRouted to
|
|
2298
|
+
* give callers a fully-shaped Engram with the server's ID after the
|
|
2299
|
+
* remote POST completes. Mirrors the construction in learn() but
|
|
2300
|
+
* doesn't acquire the lock or touch disk.
|
|
2301
|
+
*/
|
|
2302
|
+
private _buildEngramShape;
|
|
2276
2303
|
/** Build deps for learn-async module. */
|
|
2277
2304
|
private _learnAsyncDeps;
|
|
2278
2305
|
/** Async learn with LLM-driven deduplication (Ideas 1+2+19). */
|
package/dist/index.js
CHANGED
|
@@ -283,10 +283,31 @@ var PlurConfigSchema = z.object({
|
|
|
283
283
|
// src/config.ts
|
|
284
284
|
function loadConfig(configPath) {
|
|
285
285
|
if (!existsSync3(configPath)) return PlurConfigSchema.parse({});
|
|
286
|
+
let raw;
|
|
286
287
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
raw = yaml.load(readFileSync(configPath, "utf8")) ?? {};
|
|
289
|
+
} catch (err) {
|
|
290
|
+
logger.warning(`[plur:config] cannot parse YAML at ${configPath}: ${err.message} \u2014 falling back to defaults`);
|
|
291
|
+
return PlurConfigSchema.parse({});
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(raw.stores)) {
|
|
294
|
+
const validStores = [];
|
|
295
|
+
for (let i = 0; i < raw.stores.length; i++) {
|
|
296
|
+
const entry = raw.stores[i];
|
|
297
|
+
const parsed = StoreEntrySchema.safeParse(entry);
|
|
298
|
+
if (parsed.success) {
|
|
299
|
+
validStores.push(entry);
|
|
300
|
+
} else {
|
|
301
|
+
const label = entry?.scope ?? entry?.url ?? entry?.path ?? `index ${i}`;
|
|
302
|
+
logger.warning(`[plur:config] dropping invalid stores[${i}] (${label}) from ${configPath}: ${parsed.error.issues.map((it) => it.message).join("; ")}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
raw.stores = validStores;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
return PlurConfigSchema.parse(raw);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
logger.warning(`[plur:config] top-level config invalid at ${configPath}: ${err.message} \u2014 falling back to defaults`);
|
|
290
311
|
return PlurConfigSchema.parse({});
|
|
291
312
|
}
|
|
292
313
|
}
|
|
@@ -1596,8 +1617,23 @@ var RemoteStore = class {
|
|
|
1596
1617
|
* Append a single engram to the remote store. POST /api/v1/engrams
|
|
1597
1618
|
* carries statement + scope + domain + type — the server handles
|
|
1598
1619
|
* ID assignment, content_hash, status.
|
|
1620
|
+
*
|
|
1621
|
+
* Returns void to satisfy the EngramStore interface contract. Callers
|
|
1622
|
+
* that need the server-assigned ID (e.g. so the user can later
|
|
1623
|
+
* forget/feedback on it) should use `appendAndGetServerId()` instead.
|
|
1599
1624
|
*/
|
|
1600
1625
|
async append(engram) {
|
|
1626
|
+
await this.appendAndGetServerId(engram);
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Like append() but returns the server-assigned ID. Required because
|
|
1630
|
+
* the server picks its own ID (e.g. ENG-2026-05-06-007) and ignores
|
|
1631
|
+
* any id we'd send. Without this, callers see the local placeholder
|
|
1632
|
+
* ID (e.g. ENG-2026-0506-017) and a later `forget(id)` against that
|
|
1633
|
+
* placeholder will fail — the engram only exists on the server with
|
|
1634
|
+
* the server's ID.
|
|
1635
|
+
*/
|
|
1636
|
+
async appendAndGetServerId(engram) {
|
|
1601
1637
|
const body = JSON.stringify({
|
|
1602
1638
|
statement: engram.statement,
|
|
1603
1639
|
scope: engram.scope,
|
|
@@ -1613,7 +1649,12 @@ var RemoteStore = class {
|
|
|
1613
1649
|
const text = await r.text().catch(() => "");
|
|
1614
1650
|
throw new Error(`Remote store append failed: ${r.status} ${text}`);
|
|
1615
1651
|
}
|
|
1652
|
+
const data = await r.json().catch(() => ({}));
|
|
1653
|
+
if (!data.id) {
|
|
1654
|
+
throw new Error(`Remote store append succeeded but server returned no id`);
|
|
1655
|
+
}
|
|
1616
1656
|
this.cache = null;
|
|
1657
|
+
return { id: data.id };
|
|
1617
1658
|
}
|
|
1618
1659
|
/**
|
|
1619
1660
|
* `save(all)` — used by migrations to bulk-replace. Not supported
|
|
@@ -3622,6 +3663,115 @@ var Plur = class {
|
|
|
3622
3663
|
return engram;
|
|
3623
3664
|
});
|
|
3624
3665
|
}
|
|
3666
|
+
/**
|
|
3667
|
+
* Async learn that returns the canonical engram — server-assigned ID
|
|
3668
|
+
* for remote-routed writes, locally-built engram for local writes.
|
|
3669
|
+
*
|
|
3670
|
+
* Use this from async callers (MCP handlers, OpenClaw plugins, etc.)
|
|
3671
|
+
* when the user later needs to reference the engram by ID (forget,
|
|
3672
|
+
* feedback, history). The sync `learn()` returns a local-placeholder
|
|
3673
|
+
* ID for remote-routed writes — the actual server engram has a
|
|
3674
|
+
* different ID, so feedback/forget against the placeholder fails.
|
|
3675
|
+
*
|
|
3676
|
+
* Local writes: just delegates to sync learn(). Same dedup, same
|
|
3677
|
+
* history append, same return shape.
|
|
3678
|
+
*
|
|
3679
|
+
* Remote writes: bypasses local YAML entirely. POSTs to the remote's
|
|
3680
|
+
* /api/v1/engrams, awaits the server's response, and returns an
|
|
3681
|
+
* Engram with the server-assigned id. Throws on remote failure
|
|
3682
|
+
* (caller knows the write didn't land — better UX than a fire-and-
|
|
3683
|
+
* forget that pretends success and leaves the user with a phantom ID).
|
|
3684
|
+
*/
|
|
3685
|
+
async learnRouted(statement, context) {
|
|
3686
|
+
if (!this.config.allow_secrets) {
|
|
3687
|
+
const secrets = detectSecrets(statement);
|
|
3688
|
+
if (secrets.length > 0) {
|
|
3689
|
+
throw new Error(`Secret detected in statement: ${secrets[0].pattern}. Use config.allow_secrets to override.`);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
const scope = context?.scope ?? "global";
|
|
3693
|
+
const remoteDriver = this._resolveRemoteStoreForScope(scope);
|
|
3694
|
+
if (!remoteDriver) {
|
|
3695
|
+
return this.learn(statement, context);
|
|
3696
|
+
}
|
|
3697
|
+
const allEngrams = this._loadAllEngrams();
|
|
3698
|
+
const hashMatch = this._hashDedup(statement, allEngrams);
|
|
3699
|
+
if (hashMatch) return hashMatch;
|
|
3700
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3701
|
+
const localPlaceholder = this._buildEngramShape(statement, scope, context, now);
|
|
3702
|
+
const { id: serverId } = await remoteDriver.appendAndGetServerId(localPlaceholder);
|
|
3703
|
+
const serverEngram = { ...localPlaceholder, id: serverId };
|
|
3704
|
+
appendHistory(this.paths.root, {
|
|
3705
|
+
event: "engram_created",
|
|
3706
|
+
engram_id: serverId,
|
|
3707
|
+
timestamp: now,
|
|
3708
|
+
data: { type: serverEngram.type, scope: serverEngram.scope, source: serverEngram.source, routed_to: "remote" }
|
|
3709
|
+
});
|
|
3710
|
+
return serverEngram;
|
|
3711
|
+
}
|
|
3712
|
+
/**
|
|
3713
|
+
* Build an Engram object without persisting it. Used by learnRouted to
|
|
3714
|
+
* give callers a fully-shaped Engram with the server's ID after the
|
|
3715
|
+
* remote POST completes. Mirrors the construction in learn() but
|
|
3716
|
+
* doesn't acquire the lock or touch disk.
|
|
3717
|
+
*/
|
|
3718
|
+
_buildEngramShape(statement, scope, context, now) {
|
|
3719
|
+
const type = context?.type ?? "behavioral";
|
|
3720
|
+
const cogLevel = TYPE_TO_COGNITIVE2[type] ?? "remember";
|
|
3721
|
+
const TYPE_TO_MEMORY_CLASS3 = {
|
|
3722
|
+
behavioral: "semantic",
|
|
3723
|
+
terminological: "semantic",
|
|
3724
|
+
procedural: "procedural",
|
|
3725
|
+
architectural: "semantic"
|
|
3726
|
+
};
|
|
3727
|
+
const memoryClass = context?.memory_class ?? TYPE_TO_MEMORY_CLASS3[type] ?? "semantic";
|
|
3728
|
+
const commitment = context?.commitment ?? "leaning";
|
|
3729
|
+
return {
|
|
3730
|
+
// Placeholder id — overwritten by the server's assigned id before return.
|
|
3731
|
+
// Any consumer that observes this id directly (rather than via learnRouted's
|
|
3732
|
+
// return value) is doing it wrong — log says so.
|
|
3733
|
+
id: "__pending__",
|
|
3734
|
+
version: 2,
|
|
3735
|
+
status: "active",
|
|
3736
|
+
consolidated: false,
|
|
3737
|
+
type,
|
|
3738
|
+
scope,
|
|
3739
|
+
visibility: context?.visibility ?? (context?.domain ? "public" : "private"),
|
|
3740
|
+
statement,
|
|
3741
|
+
rationale: context?.rationale,
|
|
3742
|
+
source: context?.source,
|
|
3743
|
+
domain: context?.domain,
|
|
3744
|
+
activation: {
|
|
3745
|
+
retrieval_strength: 0.7,
|
|
3746
|
+
storage_strength: 1,
|
|
3747
|
+
frequency: 0,
|
|
3748
|
+
last_accessed: now.slice(0, 10)
|
|
3749
|
+
},
|
|
3750
|
+
feedback_signals: { positive: 0, negative: 0, neutral: 0 },
|
|
3751
|
+
knowledge_type: { memory_class: memoryClass, cognitive_level: cogLevel },
|
|
3752
|
+
knowledge_anchors: (context?.knowledge_anchors ?? []).map((a) => ({
|
|
3753
|
+
path: a.path,
|
|
3754
|
+
relevance: a.relevance ?? "supporting",
|
|
3755
|
+
snippet: a.snippet
|
|
3756
|
+
})),
|
|
3757
|
+
associations: [],
|
|
3758
|
+
derivation_count: 1,
|
|
3759
|
+
tags: context?.tags ?? [],
|
|
3760
|
+
pack: null,
|
|
3761
|
+
abstract: context?.abstract ?? null,
|
|
3762
|
+
derived_from: context?.derived_from ?? null,
|
|
3763
|
+
dual_coding: context?.dual_coding,
|
|
3764
|
+
polarity: null,
|
|
3765
|
+
content_hash: computeContentHash(statement),
|
|
3766
|
+
commitment,
|
|
3767
|
+
locked_at: commitment === "locked" ? now : void 0,
|
|
3768
|
+
locked_reason: commitment === "locked" ? context?.locked_reason : void 0,
|
|
3769
|
+
summary: autoSummary(statement, void 0),
|
|
3770
|
+
engram_version: 1,
|
|
3771
|
+
episode_ids: context?.session_episode_id ? [context.session_episode_id] : [],
|
|
3772
|
+
pinned: context?.pinned === true ? true : void 0
|
|
3773
|
+
};
|
|
3774
|
+
}
|
|
3625
3775
|
/** Build deps for learn-async module. */
|
|
3626
3776
|
_learnAsyncDeps() {
|
|
3627
3777
|
return {
|