@remnic/core 9.33.0 → 9.35.0

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.
Files changed (113) hide show
  1. package/dist/access-admin-ops-surface.js +16 -16
  2. package/dist/access-authorization-probe.js +17 -17
  3. package/dist/access-boundary.js +16 -16
  4. package/dist/access-cli.js +39 -38
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +16 -16
  7. package/dist/access-http.js +25 -25
  8. package/dist/access-identity-continuity-surface.js +16 -16
  9. package/dist/access-lcm-surface.js +16 -16
  10. package/dist/access-mcp.js +20 -20
  11. package/dist/access-namespace-preflight.js +16 -16
  12. package/dist/access-observe-write-surface.js +16 -16
  13. package/dist/access-operations-batch.js +17 -17
  14. package/dist/access-operations.js +19 -19
  15. package/dist/access-recall-concurrency.js +16 -16
  16. package/dist/access-recall-response.js +16 -16
  17. package/dist/access-recall-surface.js +16 -16
  18. package/dist/access-schema.js +3 -3
  19. package/dist/access-service.js +16 -16
  20. package/dist/adapters/index.js +4 -4
  21. package/dist/adapters/registry.js +2 -2
  22. package/dist/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-7FJQINUR.js} +2 -2
  23. package/dist/{chunk-BXLOS5AJ.js → chunk-2NLLXCJG.js} +2 -2
  24. package/dist/{chunk-SPPL77NI.js → chunk-2NUC473W.js} +49 -29
  25. package/dist/chunk-2NUC473W.js.map +1 -0
  26. package/dist/{chunk-N2B56SIS.js → chunk-2SHIWGY7.js} +2 -2
  27. package/dist/{chunk-I5DQ6KZK.js → chunk-3F2LKUMU.js} +2 -2
  28. package/dist/{chunk-ATUURSST.js → chunk-5FHE4JI7.js} +3 -3
  29. package/dist/{chunk-YFQMW2TH.js → chunk-5HTAJCTO.js} +11 -11
  30. package/dist/{chunk-6NPZ3FP2.js → chunk-65ZPVYTA.js} +2 -2
  31. package/dist/{chunk-MPH263X2.js → chunk-AHZYNPLX.js} +2 -2
  32. package/dist/{chunk-77CNV7DY.js → chunk-BLAKS3CP.js} +4 -4
  33. package/dist/{chunk-J6BYDVPL.js → chunk-EH5U2YCX.js} +2 -2
  34. package/dist/{chunk-PTN6UAUA.js → chunk-ES36HTSC.js} +4 -4
  35. package/dist/{chunk-SIKCTPRK.js → chunk-GHR7WHZX.js} +3 -3
  36. package/dist/{chunk-TITQYVKD.js → chunk-HEVWWPTL.js} +16 -16
  37. package/dist/{chunk-V2MVDDJF.js → chunk-IOQFPVGY.js} +2 -2
  38. package/dist/{chunk-VDX2J7OX.js → chunk-KQAFEZQX.js} +2 -2
  39. package/dist/chunk-KWMXR7XS.js +138 -0
  40. package/dist/chunk-KWMXR7XS.js.map +1 -0
  41. package/dist/{chunk-7RZJWZ46.js → chunk-LEPOP46W.js} +5 -5
  42. package/dist/{chunk-PYHOAXTI.js → chunk-LRGSLV7M.js} +4 -4
  43. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  44. package/dist/{chunk-RYHWZ52A.js → chunk-S2BBISHU.js} +4 -4
  45. package/dist/{chunk-YJ7PWEWI.js → chunk-SA3WJJF2.js} +2 -2
  46. package/dist/{chunk-C4PEMCP4.js → chunk-SLPLPGJQ.js} +7 -7
  47. package/dist/{chunk-6JE3HGS4.js → chunk-T4HH5Q3U.js} +2 -2
  48. package/dist/{chunk-EHNENZHZ.js → chunk-TJLLYV2X.js} +3 -3
  49. package/dist/{chunk-SFW2NABJ.js → chunk-UZQCNOPS.js} +7 -7
  50. package/dist/{chunk-DQEMWVMT.js → chunk-X7Y7WX73.js} +1 -1
  51. package/dist/{chunk-6NNNURLK.js → chunk-Z54EM5YZ.js} +4 -4
  52. package/dist/cli.js +33 -33
  53. package/dist/conversation-index/backend.js +2 -2
  54. package/dist/corpus-watermark.js +11 -11
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +54 -53
  57. package/dist/lcm/engine.js +2 -2
  58. package/dist/lcm/index.js +2 -2
  59. package/dist/namespaces/migrate.js +10 -10
  60. package/dist/namespaces/search.js +9 -9
  61. package/dist/operator-doctor-corpus.js +12 -12
  62. package/dist/operator-toolkit.js +13 -13
  63. package/dist/orchestrator.js +39 -38
  64. package/dist/reconcile/plan.d.ts +175 -0
  65. package/dist/reconcile/plan.js +420 -0
  66. package/dist/reconcile/plan.js.map +1 -0
  67. package/dist/search/factory.js +8 -8
  68. package/dist/search/index.js +12 -12
  69. package/dist/search/lancedb-backend.js +2 -2
  70. package/dist/search/meilisearch-backend.js +2 -2
  71. package/dist/search/orama-backend.js +2 -2
  72. package/dist/tool-scoped-memory.d.ts +47 -0
  73. package/dist/tool-scoped-memory.js +12 -0
  74. package/dist/tool-scoped-memory.js.map +1 -0
  75. package/dist/transfer/backup.js +2 -2
  76. package/dist/transfer/capsule-export.js +2 -2
  77. package/dist/transfer/capsule-import.js +2 -2
  78. package/dist/transfer/import-sqlite.js +2 -2
  79. package/package.json +12 -2
  80. package/src/extraction-redaction-rules.test.ts +53 -0
  81. package/src/index.ts +1 -1
  82. package/src/orchestration/extraction-persist.ts +31 -32
  83. package/src/reconcile/plan.test.ts +1013 -0
  84. package/src/reconcile/plan.ts +813 -0
  85. package/src/tool-scoped-memory.test.ts +199 -0
  86. package/src/tool-scoped-memory.ts +190 -0
  87. package/dist/chunk-SPPL77NI.js.map +0 -1
  88. /package/dist/{capsule-crypto-YO5QJ6L3.js.map → capsule-crypto-7FJQINUR.js.map} +0 -0
  89. /package/dist/{chunk-BXLOS5AJ.js.map → chunk-2NLLXCJG.js.map} +0 -0
  90. /package/dist/{chunk-N2B56SIS.js.map → chunk-2SHIWGY7.js.map} +0 -0
  91. /package/dist/{chunk-I5DQ6KZK.js.map → chunk-3F2LKUMU.js.map} +0 -0
  92. /package/dist/{chunk-ATUURSST.js.map → chunk-5FHE4JI7.js.map} +0 -0
  93. /package/dist/{chunk-YFQMW2TH.js.map → chunk-5HTAJCTO.js.map} +0 -0
  94. /package/dist/{chunk-6NPZ3FP2.js.map → chunk-65ZPVYTA.js.map} +0 -0
  95. /package/dist/{chunk-MPH263X2.js.map → chunk-AHZYNPLX.js.map} +0 -0
  96. /package/dist/{chunk-77CNV7DY.js.map → chunk-BLAKS3CP.js.map} +0 -0
  97. /package/dist/{chunk-J6BYDVPL.js.map → chunk-EH5U2YCX.js.map} +0 -0
  98. /package/dist/{chunk-PTN6UAUA.js.map → chunk-ES36HTSC.js.map} +0 -0
  99. /package/dist/{chunk-SIKCTPRK.js.map → chunk-GHR7WHZX.js.map} +0 -0
  100. /package/dist/{chunk-TITQYVKD.js.map → chunk-HEVWWPTL.js.map} +0 -0
  101. /package/dist/{chunk-V2MVDDJF.js.map → chunk-IOQFPVGY.js.map} +0 -0
  102. /package/dist/{chunk-VDX2J7OX.js.map → chunk-KQAFEZQX.js.map} +0 -0
  103. /package/dist/{chunk-7RZJWZ46.js.map → chunk-LEPOP46W.js.map} +0 -0
  104. /package/dist/{chunk-PYHOAXTI.js.map → chunk-LRGSLV7M.js.map} +0 -0
  105. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  106. /package/dist/{chunk-RYHWZ52A.js.map → chunk-S2BBISHU.js.map} +0 -0
  107. /package/dist/{chunk-YJ7PWEWI.js.map → chunk-SA3WJJF2.js.map} +0 -0
  108. /package/dist/{chunk-C4PEMCP4.js.map → chunk-SLPLPGJQ.js.map} +0 -0
  109. /package/dist/{chunk-6JE3HGS4.js.map → chunk-T4HH5Q3U.js.map} +0 -0
  110. /package/dist/{chunk-EHNENZHZ.js.map → chunk-TJLLYV2X.js.map} +0 -0
  111. /package/dist/{chunk-SFW2NABJ.js.map → chunk-UZQCNOPS.js.map} +0 -0
  112. /package/dist/{chunk-DQEMWVMT.js.map → chunk-X7Y7WX73.js.map} +0 -0
  113. /package/dist/{chunk-6NNNURLK.js.map → chunk-Z54EM5YZ.js.map} +0 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tool-scoped memory guard (issue #2183).
3
+ *
4
+ * A tool-related fact extracted from a specific agent integration carries an
5
+ * UNQUALIFIED tool name — "use the search tool" — with no agent identity
6
+ * attached (the conversation handed to the extraction LLM is rendered as
7
+ * `[role] content` only). Promoting such a fact to the shared namespace lets a
8
+ * DIFFERENT integration that exposes a same-named but incompatible tool (Pi
9
+ * `search` = repo code search vs OpenClaw `search` = web search) consume it.
10
+ *
11
+ * `referencesAgentSpecificTool` flags fact text that references a specific
12
+ * tool/command invocation rather than portable knowledge. The promotion guard
13
+ * in `orchestration/extraction-persist.ts` uses it — when the producing
14
+ * integration is known (`sourceConnector`) — to keep such a fact in that
15
+ * integration's own namespace.
16
+ *
17
+ * Tuning bias: a FALSE POSITIVE is cheap — the fact stays in the narrower
18
+ * namespace, the documented safe default. A FALSE NEGATIVE is the actual
19
+ * cross-integration collision bug, so detection leans permissive — but it must
20
+ * not fire on ordinary prose.
21
+ */
22
+ declare function referencesAgentSpecificTool(content: string): boolean;
23
+ interface ToolScopeWithholdInputs {
24
+ content: string;
25
+ sourceConnector?: string;
26
+ procedureSteps?: ReadonlyArray<{
27
+ intent?: string;
28
+ expectedOutcome?: string;
29
+ toolCall?: {
30
+ kind?: string;
31
+ };
32
+ }>;
33
+ }
34
+ declare function withholdToolScopedFromSharedNamespace({ content, sourceConnector, procedureSteps, }: ToolScopeWithholdInputs): boolean;
35
+ interface GlobalFactPromotionInputs {
36
+ scope: string | null | undefined;
37
+ content: string;
38
+ sourceConnector?: string;
39
+ procedureSteps?: ReadonlyArray<{
40
+ toolCall?: {
41
+ kind?: string;
42
+ };
43
+ }>;
44
+ }
45
+ declare function shouldPromoteGlobalFactToShared(inputs: GlobalFactPromotionInputs): boolean;
46
+
47
+ export { type GlobalFactPromotionInputs, type ToolScopeWithholdInputs, referencesAgentSpecificTool, shouldPromoteGlobalFactToShared, withholdToolScopedFromSharedNamespace };
@@ -0,0 +1,12 @@
1
+ import {
2
+ referencesAgentSpecificTool,
3
+ shouldPromoteGlobalFactToShared,
4
+ withholdToolScopedFromSharedNamespace
5
+ } from "./chunk-KWMXR7XS.js";
6
+ import "./chunk-PZ5AY32C.js";
7
+ export {
8
+ referencesAgentSpecificTool,
9
+ shouldPromoteGlobalFactToShared,
10
+ withholdToolScopedFromSharedNamespace
11
+ };
12
+ //# sourceMappingURL=tool-scoped-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  backupMemoryDir
3
- } from "../chunk-BXLOS5AJ.js";
3
+ } from "../chunk-2NLLXCJG.js";
4
4
  import "../chunk-765K3SAT.js";
5
- import "../chunk-DQEMWVMT.js";
5
+ import "../chunk-X7Y7WX73.js";
6
6
  import "../chunk-J4EB7DNW.js";
7
7
  import "../chunk-BJMBJZ2Y.js";
8
8
  import "../chunk-UKJAGEXH.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  exportCapsule,
3
3
  isValidCapsuleSince
4
- } from "../chunk-VDX2J7OX.js";
4
+ } from "../chunk-KQAFEZQX.js";
5
5
  import "../chunk-WEHSQBFR.js";
6
- import "../chunk-DQEMWVMT.js";
6
+ import "../chunk-X7Y7WX73.js";
7
7
  import "../chunk-J4EB7DNW.js";
8
8
  import "../chunk-BJMBJZ2Y.js";
9
9
  import "../chunk-UKJAGEXH.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  importCapsule
3
- } from "../chunk-6NPZ3FP2.js";
3
+ } from "../chunk-65ZPVYTA.js";
4
4
  import "../chunk-6TAUHL33.js";
5
5
  import "../chunk-IJ7HK452.js";
6
6
  import "../chunk-PDJTHRAW.js";
7
7
  import "../chunk-XMMESCPD.js";
8
8
  import "../chunk-42JKGUFJ.js";
9
9
  import "../chunk-WEHSQBFR.js";
10
- import "../chunk-DQEMWVMT.js";
10
+ import "../chunk-X7Y7WX73.js";
11
11
  import "../chunk-BJMBJZ2Y.js";
12
12
  import "../chunk-UKJAGEXH.js";
13
13
  import "../chunk-FP2373TW.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  importSqlite
3
- } from "../chunk-PYHOAXTI.js";
4
- import "../chunk-Z734BLO3.js";
3
+ } from "../chunk-LRGSLV7M.js";
5
4
  import "../chunk-G4IAEX6D.js";
5
+ import "../chunk-Z734BLO3.js";
6
6
  import "../chunk-RL6NEVXT.js";
7
7
  import "../chunk-GCGJW34D.js";
8
8
  import "../chunk-PZ5AY32C.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.33.0",
3
+ "version": "9.35.0",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -156,6 +156,16 @@
156
156
  "remnic-source": "./src/adapters/claude-code.ts",
157
157
  "import": "./dist/adapters/claude-code.js"
158
158
  },
159
+ "./reconcile/plan": {
160
+ "types": "./dist/reconcile/plan.d.ts",
161
+ "remnic-source": "./src/reconcile/plan.ts",
162
+ "import": "./dist/reconcile/plan.js"
163
+ },
164
+ "./reconcile/plan.js": {
165
+ "types": "./dist/reconcile/plan.d.ts",
166
+ "remnic-source": "./src/reconcile/plan.ts",
167
+ "import": "./dist/reconcile/plan.js"
168
+ },
159
169
  "./adapters/codex": {
160
170
  "types": "./dist/adapters/codex.d.ts",
161
171
  "remnic-source": "./src/adapters/codex.ts",
@@ -3076,7 +3086,7 @@
3076
3086
  "core"
3077
3087
  ],
3078
3088
  "peerDependencies": {
3079
- "@remnic/coding-graph": "^9.33.0"
3089
+ "@remnic/coding-graph": "^9.35.0"
3080
3090
  },
3081
3091
  "peerDependenciesMeta": {
3082
3092
  "@remnic/coding-graph": {
@@ -20,6 +20,7 @@ import {
20
20
  type CompiledRedactionRule,
21
21
  } from "./extraction-redaction-rules.js";
22
22
  import { validateRedactionPattern } from "./correction/correction-contract.js";
23
+ import { shouldPromoteGlobalFactToShared } from "./tool-scoped-memory.js";
23
24
 
24
25
  const withTempDir = <T>(fn: (dir: string) => Promise<T>): Promise<T> =>
25
26
  managedWithTempDir(fn, "remnic-redact-");
@@ -378,3 +379,55 @@ test("#1713 (codex): non-global fact is NOT scope-routed to shared", async () =>
378
379
  });
379
380
  });
380
381
  });
382
+ test("#2183: pre-judge consults the SESSION namespace rules for a tool-scoped global fact (not shared)", async () => {
383
+ // The pre-judge namespace prediction and the write-loop scope-routing block
384
+ // share shouldPromoteGlobalFactToShared, so a tool-scoped global fact from a
385
+ // known integration is predicted to stay in the session namespace: its
386
+ // redaction is evaluated against the SESSION rules, not the shared ones. A
387
+ // portable global fact is still predicted shared and consults shared rules.
388
+ // This is the read/write namespace-invariant coupling the guard preserves.
389
+ await withTempDir(async (sourceDir) => {
390
+ await withTempDir(async (sharedDir) => {
391
+ await writeRule(sourceDir, "session-only-secret");
392
+ await writeRule(sharedDir, "shared-only-secret");
393
+
394
+ const sourceNamespace: string = "default";
395
+ const sharedNamespace: string = "shared";
396
+ const sourceConnector = "pi";
397
+
398
+ // Mirror the pre-judge pass: predicted target namespace = shared ONLY
399
+ // when shouldPromoteGlobalFactToShared says so; load that namespace's
400
+ // rules and combine with the base (source) rules.
401
+ async function consultedRulesFor(fact: { scope: string; content: string }): Promise<CompiledRedactionRule[]> {
402
+ const baseRules = await loadRedactionRules(sourceDir);
403
+ const promote = shouldPromoteGlobalFactToShared({
404
+ scope: fact.scope,
405
+ content: fact.content,
406
+ sourceConnector,
407
+ });
408
+ const factNs = promote ? sharedNamespace : undefined;
409
+ const targetRules =
410
+ factNs && factNs !== sourceNamespace ? await loadRedactionRules(sharedDir) : [];
411
+ return [...baseRules, ...targetRules];
412
+ }
413
+
414
+ // (a) tool-scoped global fact + sourceConnector -> predicted SESSION.
415
+ const toolRules = await consultedRulesFor({ scope: "global", content: "Prefer the search tool when locating code." });
416
+ assert.ok(
417
+ contentMatchesRedactionRules("session-only-secret here", toolRules),
418
+ "session rules are consulted for a tool-scoped global fact",
419
+ );
420
+ assert.ok(
421
+ !contentMatchesRedactionRules("shared-only-secret here", toolRules),
422
+ "shared rules are NOT consulted for a tool-scoped global fact (the #2183 fix)",
423
+ );
424
+
425
+ // (b) portable global fact + sourceConnector -> predicted SHARED.
426
+ const portableRules = await consultedRulesFor({ scope: "global", content: "User prefers dark mode in all editors" });
427
+ assert.ok(
428
+ contentMatchesRedactionRules("shared-only-secret here", portableRules),
429
+ "shared rules ARE consulted for a portable global fact (no regression)",
430
+ );
431
+ });
432
+ });
433
+ });
package/src/index.ts CHANGED
@@ -1439,7 +1439,6 @@ export {
1439
1439
  type ForkLineage,
1440
1440
  } from "./transfer/capsule-fork.js";
1441
1441
 
1442
-
1443
1442
  // ---------------------------------------------------------------------------
1444
1443
  // Coding-graph engine contract (#1551 PR1) — types owned by core,
1445
1444
  // implemented by @remnic/coding-graph (à-la-carte optional).
@@ -1460,3 +1459,4 @@ export type {
1460
1459
  ParseResult,
1461
1460
  SymbolIR,
1462
1461
  } from "./coding/coding-graph-types.js";
1462
+ export type { ResolvedScopeProfilePlan } from "./namespaces/scope-profiles.js";
@@ -82,6 +82,7 @@ import {
82
82
  stripCitationForTemplate,
83
83
  } from "../source-attribution.js";
84
84
  import { classifyMemoryKind } from "../himem.js";
85
+ import { shouldPromoteGlobalFactToShared, withholdToolScopedFromSharedNamespace } from "../tool-scoped-memory.js";
85
86
  import {
86
87
  buildBehaviorSignalsForMemory,
87
88
  dedupeBehaviorSignalsByMemoryAndHash,
@@ -394,6 +395,7 @@ export class ExtractionPersistCoordinator {
394
395
  // (pre-judge + write-loop) so no new scattered config.*Enabled read is
395
396
  // introduced (ratchet scatteredConfigFlagReads; see #1523).
396
397
  const namespacesEnabled = resolveNamespaceCapabilities(this.deps.config).namespaces;
398
+ const sourceConnector = sourceContext?.sourceConnector; // #2183 tool-scope guard input
397
399
  const promoteMemoryToProfileTargets = async (options: {
398
400
  sourceStorage: StorageManager;
399
401
  category: string;
@@ -607,11 +609,14 @@ export class ExtractionPersistCoordinator {
607
609
  observedAt?: string;
608
610
  eventTimeSource?: "extracted" | "assumed";
609
611
  source: string;
612
+ sourceConnector?: string;
613
+ procedureSteps?: ReadonlyArray<{ toolCall?: { kind?: string } }>;
610
614
  /** Claim-level provenance spans (issue #1575 PR 2). */
611
615
  sources?: ProvenanceSource[];
612
616
  provenance?: "verified" | "unverified" | "none";
613
617
  }): Promise<void> => {
614
618
  await promoteMemoryToProfileTargets(options);
619
+ if (lifecycleCaps.extractionScopeClassification && withholdToolScopedFromSharedNamespace(options)) return;
615
620
  if (
616
621
  !shouldPromoteToShared(
617
622
  this.deps.config,
@@ -629,7 +634,6 @@ export class ExtractionPersistCoordinator {
629
634
  this.deps.config.sharedNamespace,
630
635
  );
631
636
  // Dedup gate: canonicalize content before hashing.
632
- //
633
637
  // Issue #369 (PR #401): When inline attribution is enabled,
634
638
  // `applyInlineCitation` appends a timestamp-bearing marker (e.g.
635
639
  // `[Source: ..., ts=2026-04-11T...]`). Because the timestamp changes
@@ -639,7 +643,6 @@ export class ExtractionPersistCoordinator {
639
643
  // `fact.content`, which can already carry an inline citation (e.g. a
640
644
  // relayed or reprocessed fact). Strip any pre-existing citation so the
641
645
  // dedup key matches the hash stored from the original un-cited write.
642
- //
643
646
  // PR #402 round-6 (Fix #2 / chatgpt-codex P1 PRRT_kwDORJXyws56U74n):
644
647
  // Compute the enriched content before the hash-dedup check so the
645
648
  // lookup uses the same content that writeMemory will actually store.
@@ -652,7 +655,6 @@ export class ExtractionPersistCoordinator {
652
655
  // key order and casing are canonical — identical to the enrichment
653
656
  // applied by storage.writeMemory — preventing spurious hash misses
654
657
  // when attribute maps arrive with different insertion orders or casing.
655
- //
656
658
  // Fix #4 (Low PRRT_kwDORJXyws56VHth): sanitize the base content before
657
659
  // building dedupContent. writeMemory runs sanitizeMemoryContent on the
658
660
  // enriched body before hashing; if sanitization redacts the content to
@@ -660,7 +662,6 @@ export class ExtractionPersistCoordinator {
660
662
  // the raw form. Computing dedupContent from sanitized.text here ensures
661
663
  // the hash lookup and the normalizedIncoming comparison both use the
662
664
  // same content that writeMemory will actually store.
663
- //
664
665
  // Combined fix: strip any pre-existing citation FIRST to obtain
665
666
  // rawContent (the canonical body), then sanitize rawContent (not
666
667
  // options.content) when building dedupContent, so that citation
@@ -1350,20 +1351,18 @@ export class ExtractionPersistCoordinator {
1350
1351
  // + PBJEe/PBKAj).
1351
1352
  let factRedactionRules = preJudgeRedactionRules;
1352
1353
  let factNs = preRoutedNamespaceByFact[fi];
1353
- // #1713 (codex PRRT_kwDORJXyws6PBj5X): scope classification can route
1354
- // a scope=global fact to the shared namespace independent of routing
1355
- // rules. If so, the pre-judge filter must consult the shared
1356
- // namespace's rules too, or a never-store pattern under shared is
1357
- // missed at pre-judge and only caught at the write gate — letting the
1358
- // content reach the extraction judge/training path. Mirror the write
1359
- // loop's scope-routing conditions exactly.
1354
+ // #1713: predict the same target namespace the write loop will route
1355
+ // this global fact to (shared, via shouldPromoteGlobalFactToShared) so
1356
+ // the pre-judge redaction filter consults that namespace's rules.
1360
1357
  if (
1361
1358
  !factNs &&
1362
1359
  lifecycleCaps.extractionScopeClassification &&
1363
1360
  namespacesEnabled &&
1364
1361
  f.scope === "global" &&
1365
1362
  profileAllowsSharedWrites &&
1366
- this.deps.storageDirNamespace(storage.dir) !== this.deps.config.sharedNamespace
1363
+ this.deps.storageDirNamespace(storage.dir) !== this.deps.config.sharedNamespace &&
1364
+ shouldPromoteGlobalFactToShared({ scope: f.scope, content: f.content,
1365
+ sourceConnector, procedureSteps: f.procedureSteps })
1367
1366
  ) {
1368
1367
  factNs = this.deps.config.sharedNamespace;
1369
1368
  }
@@ -1611,14 +1610,9 @@ export class ExtractionPersistCoordinator {
1611
1610
  }
1612
1611
  }
1613
1612
 
1614
- // Scope-based namespace routing: when scope classification is enabled
1615
- // and the LLM tagged this fact as "global", route it to the shared
1616
- // namespace so cross-project knowledge is visible everywhere. Only
1617
- // applies when namespaces are enabled and the fact was not already
1618
- // routed to a specific namespace by a routing rule (routing rules
1619
- // that set an explicit namespace take precedence; category-only rules
1620
- // do not block scope routing). Rule 30: gated by
1621
- // extractionScopeClassificationEnabled.
1613
+ // Scope-based namespace routing: a `global` fact (scope classification
1614
+ // on, no explicit routing-rule namespace) is promoted to the shared
1615
+ // namespace unless the tool-scope guard withholds it (#2183).
1622
1616
  if (
1623
1617
  lifecycleCaps.extractionScopeClassification &&
1624
1618
  namespacesEnabled &&
@@ -1627,18 +1621,21 @@ export class ExtractionPersistCoordinator {
1627
1621
  ) {
1628
1622
  const currentNs = this.deps.storageDirNamespace(targetStorage.dir);
1629
1623
  if (currentNs !== this.deps.config.sharedNamespace && profileAllowsSharedWrites) {
1630
- try {
1631
- targetStorage = await this.deps.getStorageRouter().storageFor(
1632
- this.deps.config.sharedNamespace,
1633
- );
1634
- targetNamespaceName = this.deps.config.sharedNamespace;
1635
- log.debug(
1636
- `scope-routing: fact "${fact.content.slice(0, 60)}…" routed to shared namespace (scope=global)`,
1637
- );
1638
- } catch (scopeRouteErr) {
1639
- log.warn(
1640
- `scope-routing: failed to resolve shared namespace storage; writing to session namespace (fail-open): ${scopeRouteErr}`,
1641
- );
1624
+ if (shouldPromoteGlobalFactToShared({ scope: fact.scope, content: fact.content,
1625
+ sourceConnector, procedureSteps: fact.procedureSteps })) {
1626
+ try {
1627
+ targetStorage = await this.deps.getStorageRouter().storageFor(
1628
+ this.deps.config.sharedNamespace,
1629
+ );
1630
+ targetNamespaceName = this.deps.config.sharedNamespace;
1631
+ log.debug(
1632
+ `scope-routing: fact "${fact.content.slice(0, 60)}…" routed to shared namespace (scope=global)`,
1633
+ );
1634
+ } catch (scopeRouteErr) {
1635
+ log.warn(
1636
+ `scope-routing: failed to resolve shared namespace storage; writing to session namespace (fail-open): ${scopeRouteErr}`,
1637
+ );
1638
+ }
1642
1639
  }
1643
1640
  } else if (currentNs !== this.deps.config.sharedNamespace) {
1644
1641
  log.debug(
@@ -2404,6 +2401,7 @@ export class ExtractionPersistCoordinator {
2404
2401
  : {}),
2405
2402
  source: extractionWriteSource,
2406
2403
  ...(extractionSourceConnector ? { sourceConnector: extractionSourceConnector } : {}),
2404
+ ...(fact.procedureSteps && fact.procedureSteps.length ? { procedureSteps: fact.procedureSteps } : {}),
2407
2405
  ...(fact.sources && fact.sources.length > 0 ? { sources: fact.sources } : {}),
2408
2406
  ...(fact.provenance ? { provenance: fact.provenance } : {}),
2409
2407
  });
@@ -2704,6 +2702,7 @@ export class ExtractionPersistCoordinator {
2704
2702
  : {}),
2705
2703
  source: extractionWriteSource,
2706
2704
  ...(extractionSourceConnector ? { sourceConnector: extractionSourceConnector } : {}),
2705
+ ...(fact.procedureSteps && fact.procedureSteps.length ? { procedureSteps: fact.procedureSteps } : {}),
2707
2706
  ...(fact.sources && fact.sources.length > 0 ? { sources: fact.sources } : {}),
2708
2707
  ...(fact.provenance ? { provenance: fact.provenance } : {}),
2709
2708
  });