@remnic/core 9.3.655 → 9.3.657

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 (249) hide show
  1. package/dist/access-cli.js +22 -22
  2. package/dist/access-http.d.ts +4 -4
  3. package/dist/access-http.js +10 -10
  4. package/dist/access-mcp.d.ts +4 -4
  5. package/dist/access-mcp.js +9 -9
  6. package/dist/access-schema.d.ts +10 -10
  7. package/dist/{access-service-BEJvriUt.d.ts → access-service-D_nbpexW.d.ts} +33 -2
  8. package/dist/access-service.d.ts +4 -4
  9. package/dist/access-service.js +8 -8
  10. package/dist/action-confidence.d.ts +1 -1
  11. package/dist/active-memory-bridge.d.ts +1 -1
  12. package/dist/active-recall.d.ts +1 -1
  13. package/dist/active-recall.js +1 -1
  14. package/dist/behavior-learner.d.ts +1 -1
  15. package/dist/behavior-signals.d.ts +1 -1
  16. package/dist/bootstrap.d.ts +3 -3
  17. package/dist/briefing.d.ts +1 -1
  18. package/dist/briefing.js +3 -3
  19. package/dist/buffer-surprise-report.d.ts +1 -1
  20. package/dist/buffer.d.ts +1 -1
  21. package/dist/calibration.d.ts +1 -1
  22. package/dist/causal-behavior.d.ts +1 -1
  23. package/dist/causal-consolidation.d.ts +1 -1
  24. package/dist/causal-consolidation.js +4 -4
  25. package/dist/{chunk-PVE7KSQP.js → chunk-2BD7DG37.js} +2 -2
  26. package/dist/{chunk-54LOUIBE.js → chunk-2MXEVL75.js} +2 -2
  27. package/dist/{chunk-55ZMNKMQ.js → chunk-4UL7VPTD.js} +276 -57
  28. package/dist/chunk-4UL7VPTD.js.map +1 -0
  29. package/dist/{chunk-COVZLGMR.js → chunk-54XF2FY7.js} +17 -17
  30. package/dist/{chunk-UYNFWZWG.js → chunk-AGJKWOKV.js} +2 -2
  31. package/dist/{chunk-TDZSSJV4.js → chunk-AZBV4RRY.js} +1 -1
  32. package/dist/chunk-AZBV4RRY.js.map +1 -0
  33. package/dist/{chunk-KOI765XP.js → chunk-CTAV55JM.js} +241 -1
  34. package/dist/chunk-CTAV55JM.js.map +1 -0
  35. package/dist/{chunk-A3Y37UWI.js → chunk-DIBWFCLA.js} +3 -3
  36. package/dist/{chunk-QDVQ4AN2.js → chunk-DR67OK4E.js} +5 -5
  37. package/dist/{chunk-XBIACVCO.js → chunk-EC2AYKRX.js} +2 -2
  38. package/dist/{chunk-IQ53ZSXV.js → chunk-GCYFUTUC.js} +2 -2
  39. package/dist/{chunk-YYN3LIYA.js → chunk-GSHW5VVD.js} +5 -5
  40. package/dist/chunk-GYSYLGNE.js +650 -0
  41. package/dist/chunk-GYSYLGNE.js.map +1 -0
  42. package/dist/{chunk-NRBGRZW4.js → chunk-IOZ5WBWD.js} +2 -2
  43. package/dist/{chunk-NCSJKK23.js → chunk-JSVFEHLL.js} +7 -5
  44. package/dist/chunk-JSVFEHLL.js.map +1 -0
  45. package/dist/{chunk-7LWRCOP7.js → chunk-LZTFCAKE.js} +2 -2
  46. package/dist/{chunk-TEO46GMM.js → chunk-NXCK7DO7.js} +2 -2
  47. package/dist/{chunk-XOFXKASO.js → chunk-PEPHBH2W.js} +2 -2
  48. package/dist/{chunk-WDTUYOLS.js → chunk-QZRKNA5F.js} +2 -2
  49. package/dist/{chunk-PS3SYNHP.js → chunk-R5DB26G6.js} +2 -2
  50. package/dist/{chunk-5QD3QD76.js → chunk-RDW5G6DO.js} +659 -123
  51. package/dist/chunk-RDW5G6DO.js.map +1 -0
  52. package/dist/{chunk-BGKXTVNG.js → chunk-SWDHVH2P.js} +2 -2
  53. package/dist/{chunk-67G4T7KI.js → chunk-SXYCVRLK.js} +3 -3
  54. package/dist/{chunk-UCEABZZN.js → chunk-TFFZUFEP.js} +7 -5
  55. package/dist/chunk-TFFZUFEP.js.map +1 -0
  56. package/dist/{chunk-UCEDY5M7.js → chunk-TIJYQXDI.js} +2 -2
  57. package/dist/{chunk-2RCGZ67B.js → chunk-VAEAGTEQ.js} +3 -3
  58. package/dist/{chunk-XRKQOQLY.js → chunk-WIKMCJUR.js} +2 -2
  59. package/dist/{chunk-KZZ4YAEC.js → chunk-WWMHAMAY.js} +2 -2
  60. package/dist/{chunk-OKW6F5S5.js → chunk-YEZHZCUO.js} +4 -4
  61. package/dist/{chunk-5FOCXX5E.js → chunk-YVVQUAOO.js} +3 -3
  62. package/dist/{chunk-5FOCXX5E.js.map → chunk-YVVQUAOO.js.map} +1 -1
  63. package/dist/{chunk-3XGWCZ63.js → chunk-YXLT4EMM.js} +2 -2
  64. package/dist/{chunk-PTMJ2FH2.js → chunk-Z6UDTNY6.js} +2 -2
  65. package/dist/{cli-BGahB_d3.d.ts → cli-aYxSuPvP.d.ts} +3 -3
  66. package/dist/cli.d.ts +5 -5
  67. package/dist/cli.js +22 -22
  68. package/dist/compounding/engine.d.ts +1 -1
  69. package/dist/compounding/engine.js +3 -3
  70. package/dist/compounding/preference-consolidator.d.ts +1 -1
  71. package/dist/compression-optimizer.d.ts +1 -1
  72. package/dist/config.d.ts +1 -1
  73. package/dist/config.js +1 -1
  74. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  75. package/dist/connectors/codex-materialize-runner.js +3 -3
  76. package/dist/connectors/codex-materialize.d.ts +1 -1
  77. package/dist/connectors/index.d.ts +1 -1
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/consolidation-provenance-check.d.ts +1 -1
  80. package/dist/consolidation-undo.d.ts +1 -1
  81. package/dist/contradiction/index.d.ts +1 -1
  82. package/dist/conversation-index/backend.d.ts +1 -1
  83. package/dist/conversation-index/chunker.d.ts +1 -1
  84. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  85. package/dist/conversation-index/indexer.d.ts +1 -1
  86. package/dist/conversation-index/search.d.ts +1 -1
  87. package/dist/day-summary.d.ts +1 -1
  88. package/dist/delinearize.d.ts +1 -1
  89. package/dist/direct-answer-wiring.d.ts +1 -1
  90. package/dist/direct-answer.d.ts +1 -1
  91. package/dist/embedding-fallback.d.ts +1 -1
  92. package/dist/enrichment/index.d.ts +1 -1
  93. package/dist/entity-retrieval.d.ts +1 -1
  94. package/dist/entity-retrieval.js +3 -3
  95. package/dist/entity-schema.d.ts +1 -1
  96. package/dist/explicit-capture.d.ts +3 -3
  97. package/dist/explicit-cue-recall.js +2 -2
  98. package/dist/extraction-judge-telemetry.d.ts +1 -1
  99. package/dist/extraction-judge-training.d.ts +1 -1
  100. package/dist/extraction-judge.d.ts +1 -1
  101. package/dist/extraction.d.ts +1 -1
  102. package/dist/fallback-llm.d.ts +1 -1
  103. package/dist/focused-list-recall.js +2 -2
  104. package/dist/identity-continuity.d.ts +1 -1
  105. package/dist/importance.d.ts +1 -1
  106. package/dist/index.d.ts +121 -121
  107. package/dist/index.js +32 -32
  108. package/dist/intent.d.ts +1 -1
  109. package/dist/lcm/engine.d.ts +1 -1
  110. package/dist/lcm/index.d.ts +1 -1
  111. package/dist/lcm/tools.d.ts +1 -1
  112. package/dist/lcm-fallback-read.js +1 -1
  113. package/dist/lifecycle.d.ts +1 -1
  114. package/dist/live-connectors-runner.d.ts +1 -1
  115. package/dist/local-llm.d.ts +1 -1
  116. package/dist/maintenance/memory-governance.d.ts +1 -1
  117. package/dist/maintenance/memory-governance.js +3 -3
  118. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  119. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  120. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  121. package/dist/memory-action-policy.d.ts +1 -1
  122. package/dist/memory-cache.d.ts +1 -1
  123. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  124. package/dist/memory-projection-store.d.ts +1 -1
  125. package/dist/memory-provenance.d.ts +1 -1
  126. package/dist/memory-worth-outcomes.d.ts +1 -1
  127. package/dist/models-json.d.ts +1 -1
  128. package/dist/namespaces/migrate.d.ts +1 -1
  129. package/dist/namespaces/migrate.js +4 -4
  130. package/dist/namespaces/principal.d.ts +1 -1
  131. package/dist/namespaces/search.d.ts +1 -1
  132. package/dist/namespaces/storage.d.ts +1 -1
  133. package/dist/namespaces/storage.js +3 -3
  134. package/dist/native-knowledge.d.ts +1 -1
  135. package/dist/operator-toolkit.d.ts +1 -1
  136. package/dist/operator-toolkit.js +7 -7
  137. package/dist/{orchestrator-BgzZlWxH.d.ts → orchestrator-D1wcmPNj.d.ts} +8 -2
  138. package/dist/orchestrator.d.ts +3 -3
  139. package/dist/orchestrator.js +18 -18
  140. package/dist/patterns-cli.d.ts +1 -1
  141. package/dist/policy-runtime.d.ts +1 -1
  142. package/dist/qmd-recall-cache.d.ts +1 -1
  143. package/dist/qmd.d.ts +1 -1
  144. package/dist/recall-disclosure-escalation.d.ts +1 -1
  145. package/dist/recall-explain-renderer.d.ts +1 -1
  146. package/dist/recall-explain-renderer.js +3 -3
  147. package/dist/recall-planner-llm.d.ts +1 -1
  148. package/dist/recall-state.d.ts +1 -1
  149. package/dist/recall-tag-filter.d.ts +1 -1
  150. package/dist/recall-xray-cli.d.ts +1 -1
  151. package/dist/recall-xray-cli.js +4 -4
  152. package/dist/recall-xray-renderer.d.ts +1 -1
  153. package/dist/recall-xray-renderer.js +3 -3
  154. package/dist/recall-xray.d.ts +1 -1
  155. package/dist/recall-xray.js +2 -2
  156. package/dist/resolve-auth-token.d.ts +1 -1
  157. package/dist/response-guidance-recall.js +2 -2
  158. package/dist/resume-bundles.js +2 -2
  159. package/dist/retrieval-agents.d.ts +1 -1
  160. package/dist/retrieval-tiers.d.ts +1 -1
  161. package/dist/routing/engine.d.ts +1 -1
  162. package/dist/routing/store.d.ts +1 -1
  163. package/dist/schemas.d.ts +22 -22
  164. package/dist/search/embed-helper.d.ts +1 -1
  165. package/dist/search/factory.d.ts +1 -1
  166. package/dist/search/index.d.ts +1 -1
  167. package/dist/search/lancedb-backend.d.ts +1 -1
  168. package/dist/search/meilisearch-backend.d.ts +1 -1
  169. package/dist/search/noop-backend.d.ts +1 -1
  170. package/dist/search/orama-backend.d.ts +1 -1
  171. package/dist/search/port.d.ts +1 -1
  172. package/dist/search/remote-backend.d.ts +1 -1
  173. package/dist/{semantic-consolidation-Z8d_uMq8.d.ts → semantic-consolidation-MWOdNtSE.d.ts} +1 -1
  174. package/dist/semantic-consolidation.d.ts +2 -2
  175. package/dist/semantic-consolidation.js +4 -4
  176. package/dist/semantic-rule-promotion.js +3 -3
  177. package/dist/semantic-rule-verifier.d.ts +3 -2
  178. package/dist/semantic-rule-verifier.js +5 -3
  179. package/dist/session-observer-bands.d.ts +1 -1
  180. package/dist/session-observer-state.d.ts +1 -1
  181. package/dist/shared-context/manager.d.ts +1 -1
  182. package/dist/signal.d.ts +1 -1
  183. package/dist/storage.d.ts +1 -1
  184. package/dist/storage.js +2 -2
  185. package/dist/summarizer.d.ts +1 -1
  186. package/dist/summary-snapshot.d.ts +1 -1
  187. package/dist/targeted-fact-recall.js +2 -2
  188. package/dist/temporal-supersession.d.ts +1 -1
  189. package/dist/temporal-validity.d.ts +1 -1
  190. package/dist/threading.d.ts +1 -1
  191. package/dist/tier-migration.d.ts +1 -1
  192. package/dist/tier-routing.d.ts +1 -1
  193. package/dist/topics.d.ts +1 -1
  194. package/dist/transcript.d.ts +1 -1
  195. package/dist/transfer/types.d.ts +12 -12
  196. package/dist/{types-2OPlQWJG.d.ts → types-CgcCpUrf.d.ts} +39 -1
  197. package/dist/types.d.ts +1 -1
  198. package/dist/types.js +1 -1
  199. package/dist/utility-runtime.d.ts +1 -1
  200. package/dist/verified-recall.d.ts +2 -1
  201. package/dist/verified-recall.js +5 -3
  202. package/package.json +1 -1
  203. package/src/access-service-observe-lcm-parity.test.ts +86 -1
  204. package/src/access-service-observe-scope.test.ts +283 -1
  205. package/src/access-service-raw-excerpt-read-gate.test.ts +53 -0
  206. package/src/access-service.ts +391 -93
  207. package/src/coding/coding-namespace.ts +0 -3
  208. package/src/config.ts +282 -0
  209. package/src/lcm-fallback-read.ts +2 -6
  210. package/src/namespaces/scope-profiles.test.ts +1074 -0
  211. package/src/namespaces/scope-profiles.ts +456 -0
  212. package/src/orchestrator-flush.test.ts +142 -0
  213. package/src/orchestrator-source-attribution.test.ts +73 -0
  214. package/src/orchestrator.ts +835 -163
  215. package/src/semantic-rule-verifier.ts +13 -6
  216. package/src/types.ts +52 -0
  217. package/src/verified-recall.ts +10 -6
  218. package/dist/chunk-55ZMNKMQ.js.map +0 -1
  219. package/dist/chunk-5QD3QD76.js.map +0 -1
  220. package/dist/chunk-KOI765XP.js.map +0 -1
  221. package/dist/chunk-MMJANTJX.js +0 -339
  222. package/dist/chunk-MMJANTJX.js.map +0 -1
  223. package/dist/chunk-NCSJKK23.js.map +0 -1
  224. package/dist/chunk-TDZSSJV4.js.map +0 -1
  225. package/dist/chunk-UCEABZZN.js.map +0 -1
  226. /package/dist/{chunk-PVE7KSQP.js.map → chunk-2BD7DG37.js.map} +0 -0
  227. /package/dist/{chunk-54LOUIBE.js.map → chunk-2MXEVL75.js.map} +0 -0
  228. /package/dist/{chunk-COVZLGMR.js.map → chunk-54XF2FY7.js.map} +0 -0
  229. /package/dist/{chunk-UYNFWZWG.js.map → chunk-AGJKWOKV.js.map} +0 -0
  230. /package/dist/{chunk-A3Y37UWI.js.map → chunk-DIBWFCLA.js.map} +0 -0
  231. /package/dist/{chunk-QDVQ4AN2.js.map → chunk-DR67OK4E.js.map} +0 -0
  232. /package/dist/{chunk-XBIACVCO.js.map → chunk-EC2AYKRX.js.map} +0 -0
  233. /package/dist/{chunk-IQ53ZSXV.js.map → chunk-GCYFUTUC.js.map} +0 -0
  234. /package/dist/{chunk-YYN3LIYA.js.map → chunk-GSHW5VVD.js.map} +0 -0
  235. /package/dist/{chunk-NRBGRZW4.js.map → chunk-IOZ5WBWD.js.map} +0 -0
  236. /package/dist/{chunk-7LWRCOP7.js.map → chunk-LZTFCAKE.js.map} +0 -0
  237. /package/dist/{chunk-TEO46GMM.js.map → chunk-NXCK7DO7.js.map} +0 -0
  238. /package/dist/{chunk-XOFXKASO.js.map → chunk-PEPHBH2W.js.map} +0 -0
  239. /package/dist/{chunk-WDTUYOLS.js.map → chunk-QZRKNA5F.js.map} +0 -0
  240. /package/dist/{chunk-PS3SYNHP.js.map → chunk-R5DB26G6.js.map} +0 -0
  241. /package/dist/{chunk-BGKXTVNG.js.map → chunk-SWDHVH2P.js.map} +0 -0
  242. /package/dist/{chunk-67G4T7KI.js.map → chunk-SXYCVRLK.js.map} +0 -0
  243. /package/dist/{chunk-UCEDY5M7.js.map → chunk-TIJYQXDI.js.map} +0 -0
  244. /package/dist/{chunk-2RCGZ67B.js.map → chunk-VAEAGTEQ.js.map} +0 -0
  245. /package/dist/{chunk-XRKQOQLY.js.map → chunk-WIKMCJUR.js.map} +0 -0
  246. /package/dist/{chunk-KZZ4YAEC.js.map → chunk-WWMHAMAY.js.map} +0 -0
  247. /package/dist/{chunk-OKW6F5S5.js.map → chunk-YEZHZCUO.js.map} +0 -0
  248. /package/dist/{chunk-3XGWCZ63.js.map → chunk-YXLT4EMM.js.map} +0 -0
  249. /package/dist/{chunk-PTMJ2FH2.js.map → chunk-Z6UDTNY6.js.map} +0 -0
@@ -0,0 +1,456 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { combineNamespaces, type CodingNamespaceOverlay } from "../coding/coding-namespace.js";
4
+ import { stableHash } from "../coding/git-context.js";
5
+ import { isSafeRouteNamespace } from "../routing/engine.js";
6
+ import type {
7
+ CodingContext,
8
+ PluginConfig,
9
+ ScopeProfileConfig,
10
+ ScopeProfileLayerId,
11
+ ScopeProfilePromotionTarget,
12
+ ScopeTeamConfig,
13
+ } from "../types.js";
14
+ import { canReadNamespace, canWriteNamespace, defaultNamespaceForPrincipal } from "./principal.js";
15
+
16
+ type ScopeProfileCodingOverlay = Pick<CodingNamespaceOverlay, "namespace" | "readFallbacks">;
17
+
18
+ export interface ScopeProfileLayerResolution {
19
+ id: ScopeProfileLayerId;
20
+ kind: "user-project" | "team-project" | "user-global" | "server-shared";
21
+ namespace?: string;
22
+ readable: boolean;
23
+ writable: boolean;
24
+ promotable: boolean;
25
+ reason: string;
26
+ }
27
+
28
+ export interface ScopeProfilePromotionResolution {
29
+ target: ScopeProfilePromotionTarget;
30
+ namespace?: string;
31
+ authorized: boolean;
32
+ reason: string;
33
+ }
34
+
35
+ export interface ResolvedScopeProfilePlan {
36
+ profileId: string;
37
+ profile: ScopeProfileConfig;
38
+ baseNamespace: string;
39
+ writeLayer: ScopeProfileLayerId;
40
+ writeNamespace: string;
41
+ readNamespaces: string[];
42
+ layers: ScopeProfileLayerResolution[];
43
+ promotionTargets: ScopeProfilePromotionResolution[];
44
+ warnings: string[];
45
+ }
46
+
47
+ export interface ResolveScopeProfilePlanOptions {
48
+ config: PluginConfig;
49
+ principal?: string;
50
+ codingContext?: CodingContext | null;
51
+ codingOverlay?: ScopeProfileCodingOverlay | null;
52
+ }
53
+
54
+ function activeScopeProfile(config: PluginConfig): { profileId: string; profile: ScopeProfileConfig } | null {
55
+ const profileId = config.defaultScopeProfile;
56
+ if (!profileId) return null;
57
+ const profile = (config.scopeProfiles ?? {})[profileId];
58
+ return profile ? { profileId, profile } : null;
59
+ }
60
+
61
+ function principalListed(list: string[], principal: string | undefined): boolean {
62
+ if (!principal) return false;
63
+ return list.includes(principal) || list.includes("*");
64
+ }
65
+
66
+ function derivedScopeProfileSelfNamespace(principal: string | undefined, config: PluginConfig): string | null {
67
+ if (!principal || principal === config.defaultNamespace || principal === config.sharedNamespace) return null;
68
+ if (isSafeRouteNamespace(principal)) return principal;
69
+ return "principal-" + createHash("sha256").update(principal).digest("hex").slice(0, 54);
70
+ }
71
+
72
+ function scopeProfileSelfNamespace(principal: string | undefined, config: PluginConfig): string {
73
+ const existing = defaultNamespaceForPrincipal(principal, config);
74
+ if (existing !== config.defaultNamespace) return existing;
75
+ return derivedScopeProfileSelfNamespace(principal, config) ?? existing;
76
+ }
77
+
78
+ function hasExplicitNamespacePolicy(namespace: string, config: PluginConfig): boolean {
79
+ return (config.namespacePolicies ?? []).some((policy) => policy.name === namespace);
80
+ }
81
+
82
+ function isScopeProfileImplicitSelfNamespace(
83
+ principal: string | undefined,
84
+ namespace: string,
85
+ config: PluginConfig,
86
+ ): boolean {
87
+ const derived = derivedScopeProfileSelfNamespace(principal, config);
88
+ return Boolean(
89
+ derived &&
90
+ namespace === derived &&
91
+ namespace !== config.defaultNamespace &&
92
+ namespace !== config.sharedNamespace &&
93
+ isSafeRouteNamespace(namespace) &&
94
+ !hasExplicitNamespacePolicy(namespace, config),
95
+ );
96
+ }
97
+
98
+ function canReadScopeProfileNamespace(
99
+ principal: string | undefined,
100
+ namespace: string,
101
+ config: PluginConfig,
102
+ ): boolean {
103
+ return isScopeProfileImplicitSelfNamespace(principal, namespace, config) || canReadNamespace(principal, namespace, config);
104
+ }
105
+
106
+ function canWriteScopeProfileNamespace(
107
+ principal: string | undefined,
108
+ namespace: string,
109
+ config: PluginConfig,
110
+ ): boolean {
111
+ return isScopeProfileImplicitSelfNamespace(principal, namespace, config) || canWriteNamespace(principal, namespace, config);
112
+ }
113
+
114
+ function resolveTeam(
115
+ config: PluginConfig,
116
+ profile: ScopeProfileConfig,
117
+ principal: string | undefined,
118
+ ): { teamId: string; team: ScopeTeamConfig } | null {
119
+ const configuredTeamId = profile.teamProject?.teamId;
120
+ if (configuredTeamId) {
121
+ const configured = (config.teams ?? {})[configuredTeamId];
122
+ return configured ? { teamId: configuredTeamId, team: configured } : null;
123
+ }
124
+ const readableTeams = Object.entries(config.teams ?? {}).filter(([, team]) =>
125
+ principalListed(team.principals, principal) || principalListed(team.read, principal),
126
+ );
127
+ const needsWritableTeam = profile.writeDefault === "teamProject" || profile.readOrder.includes("teamProject");
128
+ if (needsWritableTeam) {
129
+ const writableTeam = readableTeams.find(([, team]) => principalListed(team.write, principal));
130
+ if (writableTeam) return { teamId: writableTeam[0], team: writableTeam[1] };
131
+ }
132
+ const needsPromotableTeam =
133
+ profile.promotionTargets.includes("teamProject") || profile.autoPromote.targets.includes("teamProject");
134
+ if (needsPromotableTeam) {
135
+ const promotableTeam = readableTeams.find(
136
+ ([, team]) => principalListed(team.promote, principal) || principalListed(team.write, principal),
137
+ );
138
+ if (promotableTeam) return { teamId: promotableTeam[0], team: promotableTeam[1] };
139
+ }
140
+ const firstReadableTeam = readableTeams[0];
141
+ return firstReadableTeam
142
+ ? { teamId: firstReadableTeam[0], team: firstReadableTeam[1] }
143
+ : null;
144
+ }
145
+
146
+ function renderTeamProjectNamespace(params: {
147
+ template: string;
148
+ teamId: string;
149
+ principal: string | undefined;
150
+ codingContext: CodingContext;
151
+ codingOverlay: ScopeProfileCodingOverlay;
152
+ }): { namespace: string; unknownPlaceholders: string[] } {
153
+ const replacements: Record<string, string> = {
154
+ teamId: params.teamId,
155
+ principal: params.principal ?? "anonymous",
156
+ projectId: params.codingContext.projectId,
157
+ projectHash: stableHash(params.codingContext.projectId),
158
+ projectNamespace: params.codingOverlay.namespace,
159
+ };
160
+ const unknownPlaceholders: string[] = [];
161
+ const namespace = params.template.replace(/\{([A-Za-z][A-Za-z0-9]*)\}/g, (match, key: string) => {
162
+ const replacement = replacements[key];
163
+ if (replacement !== undefined) return replacement;
164
+ if (!unknownPlaceholders.includes(key)) unknownPlaceholders.push(key);
165
+ return match;
166
+ });
167
+ return { namespace, unknownPlaceholders };
168
+ }
169
+
170
+ function resolveLayer(params: {
171
+ id: ScopeProfileLayerId;
172
+ config: PluginConfig;
173
+ profile: ScopeProfileConfig;
174
+ principal: string | undefined;
175
+ baseNamespace: string;
176
+ codingContext: CodingContext | null | undefined;
177
+ codingOverlay: ScopeProfileCodingOverlay | null | undefined;
178
+ }): ScopeProfileLayerResolution {
179
+ const { id, config, profile, principal, baseNamespace, codingContext, codingOverlay } = params;
180
+ if (id === "userGlobal") {
181
+ return {
182
+ id,
183
+ kind: "user-global",
184
+ namespace: baseNamespace,
185
+ readable: canReadScopeProfileNamespace(principal, baseNamespace, config),
186
+ writable: canWriteScopeProfileNamespace(principal, baseNamespace, config),
187
+ promotable: canWriteScopeProfileNamespace(principal, baseNamespace, config),
188
+ reason: "principal self/global namespace",
189
+ };
190
+ }
191
+ if (id === "serverShared") {
192
+ return {
193
+ id,
194
+ kind: "server-shared",
195
+ namespace: config.sharedNamespace,
196
+ readable: canReadNamespace(principal, config.sharedNamespace, config),
197
+ writable: canWriteNamespace(principal, config.sharedNamespace, config),
198
+ promotable: canWriteNamespace(principal, config.sharedNamespace, config),
199
+ reason: "configured shared namespace",
200
+ };
201
+ }
202
+ if (id === "userProject") {
203
+ if (!codingContext || !codingOverlay) {
204
+ return {
205
+ id,
206
+ kind: "user-project",
207
+ readable: false,
208
+ writable: false,
209
+ promotable: false,
210
+ reason: "missing project context",
211
+ };
212
+ }
213
+ const namespace = combineNamespaces(baseNamespace, codingOverlay.namespace);
214
+ const explicitProjectPolicy = hasExplicitNamespacePolicy(namespace, config);
215
+ const baseReadable = canReadScopeProfileNamespace(principal, baseNamespace, config);
216
+ const baseWritable = canWriteScopeProfileNamespace(principal, baseNamespace, config);
217
+ const projectReadable = explicitProjectPolicy
218
+ ? canReadNamespace(principal, namespace, config)
219
+ : baseReadable;
220
+ const projectWritable = explicitProjectPolicy
221
+ ? canWriteNamespace(principal, namespace, config)
222
+ : baseWritable;
223
+ return {
224
+ id,
225
+ kind: "user-project",
226
+ namespace,
227
+ readable: projectReadable,
228
+ writable: projectWritable,
229
+ promotable: projectWritable,
230
+ reason: explicitProjectPolicy
231
+ ? "explicit user-project namespace policy"
232
+ : baseReadable || baseWritable
233
+ ? "principal project namespace derived from coding context"
234
+ : "principal base namespace is not authorized",
235
+ };
236
+ }
237
+ const team = resolveTeam(config, profile, principal);
238
+ if (!team) {
239
+ return {
240
+ id,
241
+ kind: "team-project",
242
+ readable: false,
243
+ writable: false,
244
+ promotable: false,
245
+ reason: "no authorized team mapping",
246
+ };
247
+ }
248
+ if (!codingContext || !codingOverlay) {
249
+ return {
250
+ id,
251
+ kind: "team-project",
252
+ readable: false,
253
+ writable: false,
254
+ promotable: false,
255
+ reason: "missing project context",
256
+ };
257
+ }
258
+ const template =
259
+ profile.teamProject?.namespaceTemplate ??
260
+ team.team.projectNamespaceTemplate ??
261
+ "team-{teamId}-project-{projectHash}";
262
+ const renderedNamespace = renderTeamProjectNamespace({
263
+ template,
264
+ teamId: team.teamId,
265
+ principal,
266
+ codingContext,
267
+ codingOverlay,
268
+ });
269
+ const namespace = renderedNamespace.namespace.trim();
270
+ if (renderedNamespace.unknownPlaceholders.length > 0) {
271
+ return {
272
+ id,
273
+ kind: "team-project",
274
+ namespace,
275
+ readable: false,
276
+ writable: false,
277
+ promotable: false,
278
+ reason: `unknown team-project namespace template placeholder(s): ${renderedNamespace.unknownPlaceholders.join(", ")}`,
279
+ };
280
+ }
281
+ if (!namespace || !isSafeRouteNamespace(namespace)) {
282
+ return {
283
+ id,
284
+ kind: "team-project",
285
+ namespace,
286
+ readable: false,
287
+ writable: false,
288
+ promotable: false,
289
+ reason: "team-project namespace template resolved to an unsafe namespace",
290
+ };
291
+ }
292
+ const teamReadable = principalListed(team.team.read, principal) || principalListed(team.team.principals, principal);
293
+ const teamWritable = principalListed(team.team.write, principal);
294
+ const teamPromotable = principalListed(team.team.promote, principal) || principalListed(team.team.write, principal);
295
+ const userProjectSuffix = `-${codingOverlay.namespace}`;
296
+ const userProjectBase = namespace.endsWith(userProjectSuffix)
297
+ ? namespace.slice(0, -userProjectSuffix.length)
298
+ : "";
299
+ const dynamicUserProjectCollision =
300
+ userProjectBase.length > 0 &&
301
+ (userProjectBase === config.defaultNamespace ||
302
+ userProjectBase === config.sharedNamespace ||
303
+ (config.namespacePolicies ?? []).some((policy) => policy.name === userProjectBase));
304
+ const protectedNamespace =
305
+ namespace === config.defaultNamespace ||
306
+ namespace === config.sharedNamespace ||
307
+ dynamicUserProjectCollision ||
308
+ (config.namespacePolicies ?? []).some((policy) => policy.name === namespace);
309
+ const policyReadable = !protectedNamespace || canReadNamespace(principal, namespace, config);
310
+ const policyWritable = !protectedNamespace || canWriteNamespace(principal, namespace, config);
311
+ const policyBlocked = protectedNamespace && (!policyReadable || !policyWritable);
312
+ return {
313
+ id,
314
+ kind: "team-project",
315
+ namespace,
316
+ readable: teamReadable && policyReadable,
317
+ writable: teamWritable && policyWritable,
318
+ promotable: teamPromotable && policyWritable,
319
+ reason: policyBlocked
320
+ ? "team-project namespace collides with a protected namespace policy"
321
+ : "trusted team-project namespace derived from team and project config",
322
+ };
323
+ }
324
+
325
+ export function resolveScopeProfilePlan(
326
+ options: ResolveScopeProfilePlanOptions,
327
+ ): ResolvedScopeProfilePlan | null {
328
+ const active = activeScopeProfile(options.config);
329
+ if (!active || !options.config.namespacesEnabled) return null;
330
+
331
+ const baseNamespace = scopeProfileSelfNamespace(options.principal, options.config);
332
+ const layerIds = Array.from(
333
+ new Set<ScopeProfileLayerId>([
334
+ ...active.profile.readOrder,
335
+ active.profile.writeDefault,
336
+ "userGlobal",
337
+ ...active.profile.promotionTargets.filter((target): target is ScopeProfileLayerId =>
338
+ ["userProject", "teamProject", "userGlobal", "serverShared"].includes(target),
339
+ ),
340
+ ]),
341
+ );
342
+ const layerMap = new Map<ScopeProfileLayerId, ScopeProfileLayerResolution>();
343
+ for (const id of layerIds) {
344
+ layerMap.set(
345
+ id,
346
+ resolveLayer({
347
+ id,
348
+ config: options.config,
349
+ profile: active.profile,
350
+ principal: options.principal,
351
+ baseNamespace,
352
+ codingContext: options.codingContext,
353
+ codingOverlay: options.codingOverlay,
354
+ }),
355
+ );
356
+ }
357
+
358
+ const readNamespaces: string[] = [];
359
+ for (const id of active.profile.readOrder) {
360
+ const layer = layerMap.get(id);
361
+ if (layer?.readable && layer.namespace && !readNamespaces.includes(layer.namespace)) {
362
+ readNamespaces.push(layer.namespace);
363
+ }
364
+ }
365
+
366
+ const preferredWriteLayer = layerMap.get(active.profile.writeDefault);
367
+ const readableWriteLayers = active.profile.readOrder
368
+ .map((id) => layerMap.get(id))
369
+ .filter(
370
+ (layer): layer is ScopeProfileLayerResolution =>
371
+ Boolean(layer?.writable && layer.namespace && readNamespaces.includes(layer.namespace)),
372
+ );
373
+ const fallbackWriteLayer =
374
+ preferredWriteLayer?.writable &&
375
+ preferredWriteLayer.namespace &&
376
+ readNamespaces.includes(preferredWriteLayer.namespace)
377
+ ? preferredWriteLayer
378
+ : readableWriteLayers[0];
379
+ const warnings: string[] = [];
380
+ if (!fallbackWriteLayer?.namespace) {
381
+ warnings.push(`scope profile ${active.profileId} has no writable layer inside the profile read stack; writes disabled`);
382
+ } else if (fallbackWriteLayer.id !== active.profile.writeDefault) {
383
+ warnings.push(
384
+ `scope profile ${active.profileId} writeDefault ${active.profile.writeDefault} unavailable: ${preferredWriteLayer?.reason ?? "not resolved"}`,
385
+ );
386
+ }
387
+
388
+ const promotionTargets = active.profile.promotionTargets.map((target) => {
389
+ const layer = layerMap.get(target as ScopeProfileLayerId);
390
+ if (!layer) {
391
+ return {
392
+ target,
393
+ authorized: false,
394
+ reason: "promotion target did not resolve to a profile layer",
395
+ };
396
+ }
397
+ return {
398
+ target,
399
+ namespace: layer.namespace,
400
+ authorized: layer.promotable && Boolean(layer.namespace),
401
+ reason: layer.reason,
402
+ };
403
+ });
404
+
405
+ return {
406
+ profileId: active.profileId,
407
+ profile: active.profile,
408
+ baseNamespace,
409
+ writeLayer: fallbackWriteLayer?.id ?? active.profile.writeDefault,
410
+ writeNamespace: fallbackWriteLayer?.namespace ?? "",
411
+ readNamespaces,
412
+ layers: [...layerMap.values()],
413
+ promotionTargets,
414
+ warnings,
415
+ };
416
+ }
417
+
418
+ export function expandScopeProfileReadNamespaces(options: {
419
+ profilePlan: ResolvedScopeProfilePlan;
420
+ principalSelfNamespace: string;
421
+ config: PluginConfig;
422
+ principal?: string;
423
+ codingOverlay?: ScopeProfileCodingOverlay | null;
424
+ legacyRecallNamespaces?: string[];
425
+ }): string[] {
426
+ if (options.profilePlan.readNamespaces.length === 0) {
427
+ return [];
428
+ }
429
+ const out = [...options.profilePlan.readNamespaces];
430
+ const add = (namespace: string | undefined): void => {
431
+ if (namespace && !out.includes(namespace)) out.push(namespace);
432
+ };
433
+ const userProjectReadable =
434
+ options.profilePlan.profile.readOrder.includes("userProject") &&
435
+ options.profilePlan.layers.some(
436
+ (layer) => layer.id === "userProject" && layer.readable && layer.namespace,
437
+ );
438
+ const userGlobalReadable =
439
+ options.profilePlan.profile.readOrder.includes("userGlobal") &&
440
+ options.profilePlan.layers.some(
441
+ (layer) => layer.id === "userGlobal" && layer.readable && layer.namespace,
442
+ );
443
+ if (userProjectReadable) {
444
+ for (const fallback of options.codingOverlay?.readFallbacks ?? []) {
445
+ if (fallback === "" && !userGlobalReadable) continue;
446
+ const fallbackNamespace = combineNamespaces(options.principalSelfNamespace, fallback);
447
+ if (
448
+ !hasExplicitNamespacePolicy(fallbackNamespace, options.config) ||
449
+ canReadScopeProfileNamespace(options.principal, fallbackNamespace, options.config)
450
+ ) {
451
+ add(fallbackNamespace);
452
+ }
453
+ }
454
+ }
455
+ return out;
456
+ }
@@ -8,6 +8,7 @@ import {
8
8
  Orchestrator,
9
9
  } from "./orchestrator.js";
10
10
  import { parseConfig } from "./config.js";
11
+ import { stableHash } from "./coding/git-context.js";
11
12
  import type { BufferTurn } from "./types.js";
12
13
  import type { ImportTurn } from "./bulk-import/types.js";
13
14
  import { namespaceIdentityToken } from "./namespaces/identity.js";
@@ -635,6 +636,147 @@ test("flushSession drains every discovered buffer for the session", async () =>
635
636
  ]);
636
637
  });
637
638
 
639
+ test("runExtraction skips active scope profile writes when no layer is writable", async () => {
640
+ const config = parseConfig({
641
+ namespacesEnabled: true,
642
+ defaultNamespace: "default",
643
+ sharedNamespace: "shared",
644
+ defaultScopeProfile: "teamCoding",
645
+ codingMode: { projectScope: true },
646
+ principalFromSessionKeyMode: "prefix",
647
+ principalFromSessionKeyRules: [{ match: "pi-observer:", principal: "pi-observer" }],
648
+ namespacePolicies: [
649
+ { name: "pi-observer", readPrincipals: ["pi-observer"], writePrincipals: [] },
650
+ ],
651
+ scopeProfiles: {
652
+ teamCoding: {
653
+ readOrder: ["teamProject"],
654
+ writeDefault: "teamProject",
655
+ promotionTargets: ["teamProject"],
656
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
657
+ },
658
+ },
659
+ teams: {
660
+ pi: {
661
+ principals: ["pi-observer"],
662
+ read: ["pi-observer"],
663
+ write: [],
664
+ promote: [],
665
+ },
666
+ },
667
+ });
668
+ config.extractionMinChars = 0;
669
+ config.extractionMinUserTurns = 1;
670
+
671
+ let clearCalls = 0;
672
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
673
+ orchestrator.config = config;
674
+ orchestrator.buffer = {
675
+ clearAfterExtraction: async () => {
676
+ clearCalls += 1;
677
+ },
678
+ };
679
+ orchestrator.getCodingContextForSession = () => ({
680
+ projectId: "tag:remnic",
681
+ branch: null,
682
+ rootPath: "tag:remnic",
683
+ defaultBranch: "main",
684
+ });
685
+ orchestrator.storageRouter = {
686
+ storageFor: async () => {
687
+ throw new Error("unauthorized profile write must not choose fallback storage");
688
+ },
689
+ };
690
+
691
+ const result = await orchestrator.runExtraction(
692
+ [makeTurn("pi-observer:abc123", "remember unauthorized profile target")],
693
+ { bufferKey: "pi-observer:abc123" },
694
+ );
695
+
696
+ assert.equal(result.status, "skipped");
697
+ assert.equal(result.reason, "scope_profile_no_writable_layer");
698
+ assert.equal(clearCalls, 1);
699
+ });
700
+
701
+ test("runExtraction writes buffered turns to active scope profile write layer", async () => {
702
+ const config = parseConfig({
703
+ namespacesEnabled: true,
704
+ defaultNamespace: "default",
705
+ sharedNamespace: "shared",
706
+ defaultScopeProfile: "teamCoding",
707
+ codingMode: { projectScope: true },
708
+ principalFromSessionKeyMode: "prefix",
709
+ principalFromSessionKeyRules: [{ match: "pi-observer:", principal: "pi-observer" }],
710
+ namespacePolicies: [
711
+ { name: "pi-observer", readPrincipals: ["pi-observer"], writePrincipals: ["pi-observer"] },
712
+ ],
713
+ scopeProfiles: {
714
+ teamCoding: {
715
+ readOrder: ["teamProject"],
716
+ writeDefault: "teamProject",
717
+ promotionTargets: ["teamProject"],
718
+ teamProject: { namespaceTemplate: "team-{teamId}-project-{projectHash}" },
719
+ },
720
+ },
721
+ teams: {
722
+ pi: {
723
+ principals: ["pi-observer"],
724
+ read: ["pi-observer"],
725
+ write: ["pi-observer"],
726
+ promote: ["pi-observer"],
727
+ },
728
+ },
729
+ });
730
+ config.extractionMinChars = 0;
731
+ config.extractionMinUserTurns = 1;
732
+
733
+ const turn = {
734
+ ...makeTurn("pi-observer:abc123", "remember the team profile target"),
735
+ persistProcessedFingerprint: true,
736
+ };
737
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
738
+ orchestrator.config = config;
739
+ orchestrator.buffer = { clearAfterExtraction: async () => undefined };
740
+ orchestrator.getCodingContextForSession = () => ({
741
+ projectId: "tag:remnic",
742
+ branch: null,
743
+ rootPath: "tag:remnic",
744
+ defaultBranch: "main",
745
+ });
746
+ let requestedNamespace: string | undefined;
747
+ orchestrator.storageRouter = {
748
+ storageFor: async (namespace: string) => {
749
+ requestedNamespace = namespace;
750
+ return {
751
+ listEntityNames: async () => [],
752
+ loadMeta: async () => ({
753
+ extractionCount: 0,
754
+ lastExtractionAt: null,
755
+ lastConsolidationAt: null,
756
+ totalMemories: 0,
757
+ totalEntities: 0,
758
+ processedExtractionFingerprints: [
759
+ {
760
+ fingerprint: orchestrator.buildExtractionFingerprint([turn], "pi-observer:abc123"),
761
+ observedAt: "2026-04-15T00:00:00.000Z",
762
+ },
763
+ ],
764
+ }),
765
+ saveMeta: async () => undefined,
766
+ };
767
+ },
768
+ };
769
+ orchestrator.extraction = {
770
+ extract: async () => {
771
+ throw new Error("extraction should be skipped by processed fingerprint");
772
+ },
773
+ };
774
+
775
+ await orchestrator.runExtraction([turn], { bufferKey: "pi-observer:abc123" });
776
+
777
+ assert.equal(requestedNamespace, `team-pi-project-${stableHash("tag:remnic")}`);
778
+ });
779
+
638
780
  test("runExtraction skips batches whose persisted fingerprint already exists in storage meta", async () => {
639
781
  const config = parseConfig({});
640
782
  config.extractionMinChars = 0;
@@ -699,3 +699,76 @@ test("dedup: pre-tagged fact is not re-persisted when canonical body is already
699
699
  "only one copy of the fact must be stored",
700
700
  );
701
701
  });
702
+
703
+
704
+ test("dedup: exact matches are scoped to the target namespace storage", async () => {
705
+ const { orchestrator, storage, memoryDir } = await makeOrchestrator({
706
+ factDeduplicationEnabled: true,
707
+ namespacesEnabled: true,
708
+ defaultNamespace: "default",
709
+ sharedNamespace: "shared",
710
+ });
711
+
712
+ const stateDir = path.join(memoryDir, "state");
713
+ await mkdir(stateDir, { recursive: true });
714
+ const hashIndex = new ContentHashIndex(stateDir);
715
+ await hashIndex.load();
716
+ (orchestrator as any).contentHashIndex = hashIndex;
717
+
718
+ const tenantStorage = await orchestrator.getStorage("alice-project");
719
+ await tenantStorage.ensureDirectories();
720
+
721
+ const rawBody = "The Helios tenant keeps staging feature flags isolated.";
722
+ const firstIds = await orchestrator.persistExtraction(
723
+ {
724
+ facts: [makeFact(rawBody)],
725
+ entities: [],
726
+ relationships: [],
727
+ questions: [],
728
+ profileUpdates: [],
729
+ } as ExtractionResult,
730
+ storage,
731
+ null,
732
+ { sessionKey: "agent:alice:default", principal: "alice" },
733
+ );
734
+ assert.equal(firstIds.length, 1, "default namespace write must succeed");
735
+
736
+ const tenantIds = await orchestrator.persistExtraction(
737
+ {
738
+ facts: [makeFact(rawBody)],
739
+ entities: [],
740
+ relationships: [],
741
+ questions: [],
742
+ profileUpdates: [],
743
+ } as ExtractionResult,
744
+ tenantStorage,
745
+ null,
746
+ { sessionKey: "agent:alice:project", principal: "alice" },
747
+ );
748
+ assert.equal(
749
+ tenantIds.length,
750
+ 1,
751
+ "a duplicate in another namespace must not be suppressed by the default hash index",
752
+ );
753
+
754
+ const tenantDuplicateIds = await orchestrator.persistExtraction(
755
+ {
756
+ facts: [makeFact(rawBody)],
757
+ entities: [],
758
+ relationships: [],
759
+ questions: [],
760
+ profileUpdates: [],
761
+ } as ExtractionResult,
762
+ tenantStorage,
763
+ null,
764
+ { sessionKey: "agent:alice:project", principal: "alice" },
765
+ );
766
+ assert.equal(
767
+ tenantDuplicateIds.length,
768
+ 0,
769
+ "the same namespace storage must still exact-dedup repeated facts",
770
+ );
771
+
772
+ assert.equal((await storage.readAllMemories()).length, 1);
773
+ assert.equal((await tenantStorage.readAllMemories()).length, 1);
774
+ });