@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 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
- const raw = yaml.load(readFileSync(configPath, "utf8"));
288
- return PlurConfigSchema.parse(raw ?? {});
289
- } catch {
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/core",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",