@remnic/core 9.16.3 → 9.17.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.
Files changed (241) hide show
  1. package/dist/access-admin-ops-surface.d.ts +8 -8
  2. package/dist/access-admin-ops-surface.js +2 -2
  3. package/dist/access-boundary.d.ts +8 -8
  4. package/dist/access-boundary.js +2 -2
  5. package/dist/access-cli.js +14 -14
  6. package/dist/access-http.d.ts +8 -8
  7. package/dist/access-http.js +6 -6
  8. package/dist/access-identity-continuity-surface.d.ts +6 -6
  9. package/dist/access-identity-continuity-surface.js +2 -2
  10. package/dist/access-lcm-surface.d.ts +8 -8
  11. package/dist/access-lcm-surface.js +2 -2
  12. package/dist/access-mcp.d.ts +8 -8
  13. package/dist/access-mcp.js +5 -5
  14. package/dist/access-memory-search-fanout.d.ts +2 -2
  15. package/dist/{access-namespace-preflight-DwhEFYy3.d.ts → access-namespace-preflight-_yxq3PYy.d.ts} +1 -1
  16. package/dist/access-namespace-preflight.d.ts +3 -3
  17. package/dist/access-namespace-preflight.js +2 -2
  18. package/dist/access-observe-write-surface.d.ts +8 -8
  19. package/dist/access-observe-write-surface.js +2 -2
  20. package/dist/access-operations-batch.js +3 -3
  21. package/dist/access-operations.d.ts +8 -8
  22. package/dist/access-operations.js +4 -4
  23. package/dist/access-recall-concurrency.d.ts +8 -8
  24. package/dist/access-recall-concurrency.js +2 -2
  25. package/dist/access-recall-response.d.ts +8 -8
  26. package/dist/access-recall-response.js +2 -2
  27. package/dist/access-recall-surface.d.ts +8 -8
  28. package/dist/access-recall-surface.js +2 -2
  29. package/dist/{access-service-CSq1Wp2Z.d.ts → access-service-CSm242ks.d.ts} +6 -6
  30. package/dist/access-service.d.ts +8 -8
  31. package/dist/access-service.js +2 -2
  32. package/dist/access-surface-catalog.d.ts +8 -8
  33. package/dist/action-confidence.d.ts +1 -1
  34. package/dist/active-memory-bridge.d.ts +1 -1
  35. package/dist/active-recall.d.ts +1 -1
  36. package/dist/active-recall.js +2 -2
  37. package/dist/{auto-sync-UZG3XQZ5.js → auto-sync-IH3G25MT.js} +2 -2
  38. package/dist/behavior-learner.d.ts +1 -1
  39. package/dist/behavior-signals.d.ts +1 -1
  40. package/dist/bootstrap.d.ts +6 -6
  41. package/dist/briefing.d.ts +2 -2
  42. package/dist/buffer-surprise-report.d.ts +1 -1
  43. package/dist/buffer.d.ts +2 -2
  44. package/dist/bulk-import/index.d.ts +3 -3
  45. package/dist/calibration.d.ts +1 -1
  46. package/dist/capabilities.d.ts +1 -1
  47. package/dist/{catalog-B9uga4sG.d.ts → catalog-9qLG_2OV.d.ts} +1 -1
  48. package/dist/causal-behavior.d.ts +1 -1
  49. package/dist/causal-consolidation.d.ts +1 -1
  50. package/dist/causal-trajectory-graph.d.ts +1 -1
  51. package/dist/{chunk-YHURQ3M5.js → chunk-52EFQMQC.js} +269 -6
  52. package/dist/chunk-52EFQMQC.js.map +1 -0
  53. package/dist/{chunk-GEDBTSYU.js → chunk-7O2C2OOF.js} +4 -4
  54. package/dist/{chunk-OXT5TQDS.js → chunk-B6P3QDBF.js} +2 -2
  55. package/dist/{chunk-5EKOBRCA.js → chunk-BCNVNUQG.js} +2 -2
  56. package/dist/{chunk-TUNI4GB3.js → chunk-BGBPDAIN.js} +2 -2
  57. package/dist/{chunk-V6LYAXGW.js → chunk-F7BWFIDK.js} +2 -2
  58. package/dist/{chunk-BTK2PIFL.js → chunk-GBT4TFV7.js} +2 -2
  59. package/dist/{chunk-JOCXI2O5.js → chunk-K2B2TMF3.js} +2 -2
  60. package/dist/{chunk-VLIY55Q5.js → chunk-KQED7RCV.js} +7 -7
  61. package/dist/{chunk-GSQASASV.js → chunk-MONWCSVL.js} +65 -4
  62. package/dist/chunk-MONWCSVL.js.map +1 -0
  63. package/dist/{chunk-5SB7SC2G.js → chunk-UX3ZXN34.js} +3 -1
  64. package/dist/{chunk-M6TZQDTX.js → chunk-VZC7ORJE.js} +2 -2
  65. package/dist/{chunk-ANGUOE2N.js → chunk-WIUYMCPL.js} +2 -2
  66. package/dist/{chunk-DETNBTZS.js → chunk-XSBUROEB.js} +20 -4
  67. package/dist/chunk-XSBUROEB.js.map +1 -0
  68. package/dist/{cli-DYLGXxuu.d.ts → cli-BLZHYY2A.d.ts} +5 -5
  69. package/dist/cli.d.ts +10 -10
  70. package/dist/cli.js +11 -11
  71. package/dist/compounding/engine.d.ts +2 -2
  72. package/dist/compounding/preference-consolidator.d.ts +1 -1
  73. package/dist/compression-optimizer.d.ts +1 -1
  74. package/dist/config.d.ts +1 -1
  75. package/dist/config.js +2 -2
  76. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  77. package/dist/connectors/codex-materialize.d.ts +1 -1
  78. package/dist/connectors/index.d.ts +1 -1
  79. package/dist/consolidation-provenance-check.d.ts +2 -2
  80. package/dist/consolidation-undo.d.ts +2 -2
  81. package/dist/contradiction/index.d.ts +2 -2
  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 +2 -2
  94. package/dist/entity-schema.d.ts +1 -1
  95. package/dist/explicit-capture.d.ts +6 -6
  96. package/dist/extraction-faithfulness.d.ts +1 -1
  97. package/dist/extraction-judge-telemetry.d.ts +1 -1
  98. package/dist/extraction-judge-training.d.ts +1 -1
  99. package/dist/extraction-judge.d.ts +1 -1
  100. package/dist/extraction.d.ts +1 -1
  101. package/dist/fallback-llm.d.ts +1 -1
  102. package/dist/graph-dashboard-diff.d.ts +1 -1
  103. package/dist/graph-dashboard-key.d.ts +1 -1
  104. package/dist/graph-dashboard-parser.d.ts +1 -1
  105. package/dist/graph-edge-reinforcement.d.ts +1 -1
  106. package/dist/graph-snapshot.d.ts +1 -1
  107. package/dist/graph.d.ts +1 -1
  108. package/dist/importance.d.ts +1 -1
  109. package/dist/importers/index.d.ts +1 -1
  110. package/dist/in-flight-reads.d.ts +1 -1
  111. package/dist/index.d.ts +98 -23
  112. package/dist/index.js +20 -14
  113. package/dist/intent.d.ts +1 -1
  114. package/dist/lcm/engine.d.ts +1 -1
  115. package/dist/lcm/index.d.ts +1 -1
  116. package/dist/lcm/tools.d.ts +1 -1
  117. package/dist/lifecycle.d.ts +1 -1
  118. package/dist/live-connectors-runner.d.ts +1 -1
  119. package/dist/local-llm.d.ts +1 -1
  120. package/dist/local-model-endpoint.d.ts +1 -1
  121. package/dist/maintenance/memory-governance.d.ts +1 -1
  122. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  123. package/dist/mcp-memory-inspector-app.d.ts +8 -8
  124. package/dist/memory-action-policy.d.ts +1 -1
  125. package/dist/memory-cache.d.ts +1 -1
  126. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  127. package/dist/memory-projection-store.d.ts +1 -1
  128. package/dist/memory-provenance.d.ts +1 -1
  129. package/dist/memory-worth-outcomes.d.ts +2 -2
  130. package/dist/models-json.d.ts +1 -1
  131. package/dist/namespaces/migrate.d.ts +3 -3
  132. package/dist/namespaces/principal.d.ts +1 -1
  133. package/dist/namespaces/search.d.ts +1 -1
  134. package/dist/namespaces/storage.d.ts +3 -3
  135. package/dist/native-knowledge.d.ts +1 -1
  136. package/dist/offline-sync-impression-drain.d.ts +1 -1
  137. package/dist/offline-sync-impression-drain.js +1 -1
  138. package/dist/operator-toolkit.d.ts +2 -2
  139. package/dist/operator-toolkit.js +3 -3
  140. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  141. package/dist/orchestration/maintenance.d.ts +3 -3
  142. package/dist/orchestration/maintenance.js +2 -2
  143. package/dist/{orchestrator-BHx-DWCa.d.ts → orchestrator-CplS9U3U.d.ts} +5 -5
  144. package/dist/orchestrator.d.ts +6 -6
  145. package/dist/orchestrator.js +14 -14
  146. package/dist/patterns-cli.d.ts +1 -1
  147. package/dist/{pipeline-DDHSE876.d.ts → pipeline-DB4CbFqo.d.ts} +1 -1
  148. package/dist/policy-runtime.d.ts +1 -1
  149. package/dist/proactive-contention.d.ts +1 -1
  150. package/dist/provenance.d.ts +1 -1
  151. package/dist/{qmd-C4WVh6BM.d.ts → qmd-BxZmOLHq.d.ts} +1 -1
  152. package/dist/qmd-preflight.d.ts +2 -2
  153. package/dist/qmd-recall-cache.d.ts +1 -1
  154. package/dist/qmd.d.ts +2 -2
  155. package/dist/recall-concurrency-config.d.ts +1 -1
  156. package/dist/recall-disclosure-escalation.d.ts +1 -1
  157. package/dist/recall-explain-renderer.d.ts +1 -1
  158. package/dist/recall-memory-map.d.ts +1 -1
  159. package/dist/recall-planner-llm.d.ts +1 -1
  160. package/dist/recall-state.d.ts +1 -1
  161. package/dist/recall-tag-filter.d.ts +1 -1
  162. package/dist/recall-timings.d.ts +1 -1
  163. package/dist/recall-xray-cli.d.ts +1 -1
  164. package/dist/recall-xray-renderer.d.ts +1 -1
  165. package/dist/recall-xray.d.ts +1 -1
  166. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  167. package/dist/replay/normalizers/claude.d.ts +1 -1
  168. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  169. package/dist/replay/normalizers/shared.d.ts +1 -1
  170. package/dist/replay/runner.d.ts +1 -1
  171. package/dist/replay/types.d.ts +1 -1
  172. package/dist/resolve-auth-token.d.ts +1 -1
  173. package/dist/resume-bundles.js +3 -3
  174. package/dist/retrieval-agents.d.ts +2 -2
  175. package/dist/retrieval-tiers.d.ts +1 -1
  176. package/dist/routing/engine.d.ts +1 -1
  177. package/dist/routing/store.d.ts +1 -1
  178. package/dist/salvage-envelope.d.ts +1 -1
  179. package/dist/{scope-profiles-EqMXwzCz.d.ts → scope-profiles-NsQQo1Lt.d.ts} +1 -1
  180. package/dist/search/embed-helper.d.ts +1 -1
  181. package/dist/search/factory.d.ts +1 -1
  182. package/dist/search/index.d.ts +1 -1
  183. package/dist/search/lancedb-backend.d.ts +1 -1
  184. package/dist/search/meilisearch-backend.d.ts +1 -1
  185. package/dist/search/noop-backend.d.ts +1 -1
  186. package/dist/search/orama-backend.d.ts +1 -1
  187. package/dist/search/port.d.ts +1 -1
  188. package/dist/search/remote-backend.d.ts +1 -1
  189. package/dist/{semantic-consolidation-CYpvwmQc.d.ts → semantic-consolidation-DW85h30n.d.ts} +1 -1
  190. package/dist/semantic-consolidation.d.ts +2 -2
  191. package/dist/semantic-rule-verifier.d.ts +1 -1
  192. package/dist/session-observer-bands.d.ts +1 -1
  193. package/dist/session-observer-state.d.ts +1 -1
  194. package/dist/shared-context/manager.d.ts +1 -1
  195. package/dist/signal.d.ts +1 -1
  196. package/dist/{storage-Dxd7zVZH.d.ts → storage-o5BTAa8d.d.ts} +1 -1
  197. package/dist/storage.d.ts +2 -2
  198. package/dist/summarizer.d.ts +1 -1
  199. package/dist/summary-snapshot.d.ts +1 -1
  200. package/dist/temporal-supersession.d.ts +2 -2
  201. package/dist/temporal-timeline-recall.d.ts +1 -1
  202. package/dist/temporal-validity.d.ts +1 -1
  203. package/dist/threading.d.ts +1 -1
  204. package/dist/tier-migration.d.ts +2 -2
  205. package/dist/tier-routing.d.ts +1 -1
  206. package/dist/topics.d.ts +1 -1
  207. package/dist/transcript.d.ts +1 -1
  208. package/dist/trust-score-stage.d.ts +1 -1
  209. package/dist/trust-score.d.ts +1 -1
  210. package/dist/{types-OUCJALBK.d.ts → types-V3dT-pen.d.ts} +14 -2
  211. package/dist/types.d.ts +1 -1
  212. package/dist/utility-runtime.d.ts +1 -1
  213. package/dist/write-envelope.d.ts +1 -1
  214. package/package.json +2 -2
  215. package/src/activity/config.test.ts +42 -4
  216. package/src/activity/config.ts +83 -8
  217. package/src/activity/index.ts +5 -3
  218. package/src/activity/memory-gen.test.ts +436 -0
  219. package/src/activity/memory-gen.ts +409 -0
  220. package/src/activity/runner.test.ts +3 -0
  221. package/src/activity/scheduler.test.ts +3 -0
  222. package/src/activity/sync-registration.test.ts +2 -1
  223. package/src/activity/types.ts +15 -1
  224. package/src/config.test.ts +7 -0
  225. package/src/offline-sync-impression-drain.test.ts +54 -0
  226. package/src/offline-sync-impression-drain.ts +35 -3
  227. package/dist/chunk-DETNBTZS.js.map +0 -1
  228. package/dist/chunk-GSQASASV.js.map +0 -1
  229. package/dist/chunk-YHURQ3M5.js.map +0 -1
  230. /package/dist/{auto-sync-UZG3XQZ5.js.map → auto-sync-IH3G25MT.js.map} +0 -0
  231. /package/dist/{chunk-GEDBTSYU.js.map → chunk-7O2C2OOF.js.map} +0 -0
  232. /package/dist/{chunk-OXT5TQDS.js.map → chunk-B6P3QDBF.js.map} +0 -0
  233. /package/dist/{chunk-5EKOBRCA.js.map → chunk-BCNVNUQG.js.map} +0 -0
  234. /package/dist/{chunk-TUNI4GB3.js.map → chunk-BGBPDAIN.js.map} +0 -0
  235. /package/dist/{chunk-V6LYAXGW.js.map → chunk-F7BWFIDK.js.map} +0 -0
  236. /package/dist/{chunk-BTK2PIFL.js.map → chunk-GBT4TFV7.js.map} +0 -0
  237. /package/dist/{chunk-JOCXI2O5.js.map → chunk-K2B2TMF3.js.map} +0 -0
  238. /package/dist/{chunk-VLIY55Q5.js.map → chunk-KQED7RCV.js.map} +0 -0
  239. /package/dist/{chunk-5SB7SC2G.js.map → chunk-UX3ZXN34.js.map} +0 -0
  240. /package/dist/{chunk-M6TZQDTX.js.map → chunk-VZC7ORJE.js.map} +0 -0
  241. /package/dist/{chunk-ANGUOE2N.js.map → chunk-WIUYMCPL.js.map} +0 -0
@@ -0,0 +1,409 @@
1
+ import { scoreImportance, isAboveImportanceThreshold } from "../importance.js";
2
+ import { getVerdictKind, type JudgeCandidate, type JudgeVerdict, type JudgeVerdictKind } from "../extraction-judge.js";
3
+ import { log } from "../logger.js";
4
+ import type { ExtractedFact, ExtractionResult, ImportanceScore, MemoryStatus } from "../types.js";
5
+ import {
6
+ composeMemoryEnvelope,
7
+ type MemoryWriteInput,
8
+ type SealedMemoryEnvelope,
9
+ type WriteContext,
10
+ } from "../write-envelope.js";
11
+ import { computeTrustScore, decideSmart, type SmartDecision } from "../wearables/trust.js";
12
+ import { activityDayWindow } from "./digest.js";
13
+ import type { ActivityConfig } from "./types.js";
14
+
15
+ const ACTIVITY_ALLOWED_CATEGORIES: Record<string, true> = {
16
+ decision: true,
17
+ commitment: true,
18
+ preference: true,
19
+ moment: true,
20
+ };
21
+ const FIRST_PERSON = /\b(?:i|we|my|our|i['’]ve|we['’]ve)\b/i;
22
+ /**
23
+ * Attribution detection. On-screen text routinely quotes other people, so a
24
+ * first-person pronoun is not ownership evidence. Two shapes are rejected,
25
+ * case-insensitively, with the speaker slot excluding bare first-person
26
+ * pronouns (word-anchored) so "We decided" stays eligible while names that
27
+ * merely begin with a pronoun ("Wendy", "Ian") do not:
28
+ * - verb attribution: "Alice wrote: I decided ..." (leading speaker + verb)
29
+ * - leading label: "Alice: I decided ..." (chat/comment sender headers)
30
+ * Common document labels ("Note:", "TODO:") are not senders and stay eligible.
31
+ */
32
+ const ATTRIBUTION_SPEAKER = String.raw`(?!(?:i|we|us|me|my|our)\b)[A-Za-z][\w.'’-]*`;
33
+ const SPEECH_VERB =
34
+ String.raw`(?:said|says|wrote|writes|posted|typed|asked|replied|messaged|commented|noted|announced|added|responded|mentioned|told)`;
35
+ const VERB_ATTRIBUTION = new RegExp(String.raw`^\s*${ATTRIBUTION_SPEAKER}\s+${SPEECH_VERB}\b`, "i");
36
+ const LABEL_ATTRIBUTION = new RegExp(
37
+ String.raw`^\s*(${ATTRIBUTION_SPEAKER}(?:\s+[A-Za-z][\w.'’-]*){0,3})\s*:\s*`,
38
+ "i",
39
+ );
40
+ // A sender named inside an otherwise-allowlisted label ("Update from Alice:",
41
+ // "Note by Bob:") is still attribution. Case-sensitive on the capitalized name
42
+ // so generic labels ("Summary by section:") stay eligible.
43
+ const LABEL_NAMES_SENDER = /\b(?:from|by)\s+[A-Z][a-z]/;
44
+ const NON_SENDER_LABELS: Record<string, true> = {
45
+ note: true, notes: true, todo: true, fixme: true, reminder: true, update: true,
46
+ updates: true, warning: true, info: true, tip: true, tips: true, summary: true,
47
+ status: true, tldr: true, fyi: true, idea: true, goal: true, goals: true,
48
+ plan: true, plans: true, action: true, actions: true, context: true, question: true,
49
+ };
50
+ const TRUST_ATTRIBUTE_KEYS: Record<string, true> = {
51
+ trustscore: true,
52
+ trustdecision: true,
53
+ judgeverdict: true,
54
+ };
55
+
56
+ export interface ActivityMemoryWriter {
57
+ /**
58
+ * Locate an existing activity memory for this day whose normalized content
59
+ * matches, or null. The dedup identity is `(activity day, normalized content)`:
60
+ * `dayStartUtc`/`dayEndUtc` bound the half-open day window and `normalizedKey`
61
+ * is trim+lowercase content — so whitespace/case variants collapse, while the
62
+ * SAME text on a different day is NOT a duplicate (recurring actions persist
63
+ * per day). Enables dedup and in-place promotion instead of duplicate or
64
+ * frozen-pending records.
65
+ */
66
+ findActivityMemoryByContent(
67
+ normalizedKey: string,
68
+ dayStartUtc: string,
69
+ dayEndUtc: string,
70
+ ): Promise<{ id: string; status: MemoryStatus | undefined } | null>;
71
+ /**
72
+ * Count active + pending_review activity memories whose event time falls in
73
+ * the half-open [startUtc, endUtc) day window. Seeds `maxMemoriesPerDay` so
74
+ * the cap holds across repeated digest runs on the same day rather than
75
+ * resetting on every call.
76
+ */
77
+ countActivityMemoriesForDay(startUtc: string, endUtc: string): Promise<number>;
78
+ /**
79
+ * Promote a pending_review activity memory to active, merging trust evidence.
80
+ * Returns false when the row is missing or no longer pending.
81
+ */
82
+ promoteActivityMemory(
83
+ id: string,
84
+ attributeUpdates: Record<string, string>,
85
+ confidence?: number,
86
+ ): Promise<boolean>;
87
+ /**
88
+ * Demote a pending_review activity memory to rejected on a fresh judge-reject
89
+ * re-verdict. Active rows are never auto-demoted. Returns false when missing
90
+ * or no longer pending.
91
+ */
92
+ demoteActivityMemory(id: string, attributeUpdates: Record<string, string>): Promise<boolean>;
93
+ writeSealedMemory(
94
+ envelope: SealedMemoryEnvelope,
95
+ extras: { status: MemoryStatus; contentHashSource: string; importance?: ImportanceScore },
96
+ ): Promise<{ tombstoneBlocked?: boolean }>;
97
+ }
98
+
99
+ export interface ActivityMemoryGenerationDeps {
100
+ extract(digestBody: string): Promise<ExtractionResult>;
101
+ judge(candidates: JudgeCandidate[]): Promise<Map<number, JudgeVerdict>>;
102
+ writer: ActivityMemoryWriter;
103
+ now?: () => Date;
104
+ }
105
+
106
+ export interface ActivityMemoryGenerationResult {
107
+ created: number;
108
+ /** Earlier pending_review writes promoted to active by a stronger reassessment. */
109
+ promoted: number;
110
+ /** Earlier pending_review writes retired by a fresh judge-reject verdict. */
111
+ demoted: number;
112
+ pendingReview: number;
113
+ rejectedDisplayedContent: number;
114
+ rejectedByJudge: number;
115
+ skipped: number;
116
+ }
117
+
118
+ /**
119
+ * Activity snapshots contain arbitrary third-party text. Only durable claims
120
+ * explicitly framed as the user's or team's own action, decision, commitment,
121
+ * preference, or moment may enter the trust gate. A first-person pronoun alone
122
+ * is not ownership evidence: quoted or attributed third-party speech (e.g.
123
+ * "Alice wrote: I decided to leave") is rejected.
124
+ */
125
+ export function isEligibleActivityFact(fact: ExtractionResult["facts"][number]): boolean {
126
+ if (ACTIVITY_ALLOWED_CATEGORIES[fact.category] !== true) return false;
127
+ if (!FIRST_PERSON.test(fact.content)) return false;
128
+ if (VERB_ATTRIBUTION.test(fact.content)) return false;
129
+ const label = LABEL_ATTRIBUTION.exec(fact.content);
130
+ if (label !== null) {
131
+ const labelText = label[1].trim();
132
+ const firstToken = labelText.split(/\s+/)[0].toLowerCase();
133
+ if (NON_SENDER_LABELS[firstToken] !== true || LABEL_NAMES_SENDER.test(labelText)) return false;
134
+ }
135
+ return true;
136
+ }
137
+
138
+ /**
139
+ * Compose an activity envelope in salvage mode (issue #1989 pattern): digest
140
+ * facts are MACHINE-generated, so one malformed optional field must not abort a
141
+ * whole day's writes. Dropped fields are warn-logged, never silent.
142
+ */
143
+ function composeSalvagedActivityEnvelope(
144
+ input: MemoryWriteInput,
145
+ ctx: WriteContext,
146
+ ): SealedMemoryEnvelope {
147
+ const envelope = composeMemoryEnvelope(input, ctx, { salvage: true });
148
+ if (envelope.salvageNotes.length > 0) {
149
+ log.warn(`activity write salvaged invalid fields: ${envelope.salvageNotes.join("; ")}`);
150
+ }
151
+ return envelope;
152
+ }
153
+
154
+ interface WritableCandidate {
155
+ fact: ExtractedFact;
156
+ key: string;
157
+ status: Extract<MemoryStatus, "active" | "pending_review">;
158
+ trust: number;
159
+ trustDecision: string;
160
+ judgeVerdict?: JudgeVerdictKind;
161
+ importance: ImportanceScore;
162
+ }
163
+
164
+ export async function generateActivityMemories(
165
+ date: string,
166
+ digestBody: string,
167
+ config: ActivityConfig,
168
+ deps: ActivityMemoryGenerationDeps,
169
+ ): Promise<ActivityMemoryGenerationResult> {
170
+ const result: ActivityMemoryGenerationResult = {
171
+ created: 0,
172
+ promoted: 0,
173
+ demoted: 0,
174
+ pendingReview: 0,
175
+ rejectedDisplayedContent: 0,
176
+ rejectedByJudge: 0,
177
+ skipped: 0,
178
+ };
179
+ // `extractionMode` is the independent extraction gate (per the config contract);
180
+ // `enabled` gates source ingestion, not extraction of a supplied digest.
181
+ if (config.extractionMode !== "smart" || digestBody.trim().length === 0) return result;
182
+
183
+ // Event-time bound: the digest's local day. Backfilled or delayed digests
184
+ // then land on the day they describe rather than at write time.
185
+ const { startUtc, endUtc } = activityDayWindow(date, config.timezone);
186
+
187
+ let extracted: ExtractionResult;
188
+ try {
189
+ extracted = await deps.extract(digestBody);
190
+ } catch (err) {
191
+ // A provider/parse failure must not throw out of the pass; return the
192
+ // structured zero-result so the caller can retry on the next digest run.
193
+ log.warn(
194
+ `activity extraction failed; skipping the day's memory pass: ${err instanceof Error ? err.message : String(err)}`,
195
+ );
196
+ return result;
197
+ }
198
+ if (extracted.extractionFailure !== undefined) {
199
+ // In-band failure marker (partial/failed provider result): treat like a
200
+ // thrown extraction — warn and return the structured zero-result.
201
+ log.warn(
202
+ `activity extraction reported a failure; skipping the day's memory pass: ${extracted.extractionFailure}`,
203
+ );
204
+ return result;
205
+ }
206
+ const candidates = extracted.facts.filter((fact) => {
207
+ if (!isEligibleActivityFact(fact)) {
208
+ result.rejectedDisplayedContent += 1;
209
+ return false;
210
+ }
211
+ const importance = scoreImportance(fact.content, fact.category, fact.tags);
212
+ if (
213
+ !Number.isFinite(fact.confidence) ||
214
+ fact.confidence < config.minConfidence ||
215
+ !isAboveImportanceThreshold(importance.level, config.minImportance)
216
+ ) {
217
+ result.skipped += 1;
218
+ return false;
219
+ }
220
+ return true;
221
+ });
222
+ let verdicts: Map<number, JudgeVerdict>;
223
+ try {
224
+ verdicts = await deps.judge(candidates.map((fact) => ({
225
+ text: fact.content,
226
+ category: fact.category,
227
+ confidence: fact.confidence,
228
+ tags: fact.tags,
229
+ importanceLevel: scoreImportance(fact.content, fact.category, fact.tags).level,
230
+ })));
231
+ } catch (err) {
232
+ // Degraded smart mode (wearables parity): a judge failure must not abort the
233
+ // day's writes. Fall back to trust scoring alone (no judge accept boost).
234
+ log.warn(
235
+ `activity extraction judge unavailable; using trust scoring only: ${err instanceof Error ? err.message : String(err)}`,
236
+ );
237
+ verdicts = new Map();
238
+ }
239
+
240
+ // Score every candidate once, then keep the STRONGEST (highest-trust) copy per
241
+ // normalized key so a weaker earlier duplicate (e.g. a judge-rejected one)
242
+ // cannot crowd out a stronger later one. The day is fixed for this digest, so
243
+ // the normalized content alone keys the in-run set.
244
+ interface ScoredCandidate {
245
+ fact: ExtractedFact;
246
+ key: string;
247
+ trust: number;
248
+ verdictKind?: JudgeVerdictKind;
249
+ decision: SmartDecision;
250
+ importance: ImportanceScore;
251
+ }
252
+ const strongestByKey = new Map<string, ScoredCandidate>();
253
+ for (let index = 0; index < candidates.length; index += 1) {
254
+ const fact = candidates[index];
255
+ const verdict = verdicts.get(index);
256
+ const verdictKind = verdict === undefined ? undefined : getVerdictKind(verdict);
257
+ const trust = computeTrustScore({
258
+ extractionConfidence: fact.confidence,
259
+ sourceTrust: config.sourceTrust,
260
+ judgeVerdict: verdictKind,
261
+ evidence: { corroboratedBySources: [] },
262
+ });
263
+ const decision = decideSmart(trust, verdictKind, {
264
+ autoApproveTrust: config.autoApproveTrust,
265
+ reviewTrust: config.reviewTrust,
266
+ });
267
+ // Dedup key: trim + lowercase so whitespace/case variants collapse (day-scoped).
268
+ const key = fact.content.trim().toLowerCase();
269
+ const scored: ScoredCandidate = {
270
+ fact,
271
+ key,
272
+ trust,
273
+ verdictKind,
274
+ decision,
275
+ importance: scoreImportance(fact.content, fact.category, fact.tags),
276
+ };
277
+ const prev = strongestByKey.get(key);
278
+ if (prev === undefined) {
279
+ strongestByKey.set(key, scored);
280
+ } else {
281
+ // In-run duplicate: keep the copy with the more durable decision (active >
282
+ // review > drop), breaking ties by trust — so a higher-confidence reject
283
+ // cannot crowd out a lower-trust accepted copy.
284
+ result.skipped += 1;
285
+ const rankOf = (outcome: SmartDecision["outcome"]): number =>
286
+ outcome === "active" ? 2 : outcome === "review" ? 1 : 0;
287
+ const curRank = rankOf(scored.decision.outcome);
288
+ const prevRank = rankOf(prev.decision.outcome);
289
+ if (curRank > prevRank || (curRank === prevRank && trust > prev.trust)) {
290
+ strongestByKey.set(key, scored);
291
+ }
292
+ }
293
+ }
294
+
295
+ const writable: WritableCandidate[] = [];
296
+ for (const scored of strongestByKey.values()) {
297
+ const existing = await deps.writer.findActivityMemoryByContent(scored.key, startUtc, endUtc);
298
+ if (existing !== null) {
299
+ // Same-day duplicate of a persisted row: reassess in place; never write new
300
+ // or use a cap slot. (The same text on another day is not found here.)
301
+ if (existing.status === "pending_review") {
302
+ if (scored.decision.outcome === "drop" && scored.decision.reason === "judge-rejected") {
303
+ if (
304
+ await deps.writer.demoteActivityMemory(existing.id, {
305
+ trustScore: scored.trust.toFixed(3),
306
+ trustDecision: "demoted-by-rejection",
307
+ judgeVerdict: "reject",
308
+ })
309
+ ) {
310
+ result.demoted += 1;
311
+ continue;
312
+ }
313
+ } else if (scored.decision.outcome === "active") {
314
+ if (
315
+ await deps.writer.promoteActivityMemory(
316
+ existing.id,
317
+ {
318
+ trustScore: scored.trust.toFixed(3),
319
+ trustDecision: "promoted-by-reassessment",
320
+ ...(scored.verdictKind !== undefined ? { judgeVerdict: scored.verdictKind } : {}),
321
+ },
322
+ scored.trust,
323
+ )
324
+ ) {
325
+ result.promoted += 1;
326
+ continue;
327
+ }
328
+ }
329
+ }
330
+ result.skipped += 1;
331
+ continue;
332
+ }
333
+ if (scored.decision.outcome === "drop") {
334
+ if (scored.decision.reason === "judge-rejected") result.rejectedByJudge += 1;
335
+ else result.skipped += 1;
336
+ continue;
337
+ }
338
+ writable.push({
339
+ fact: scored.fact,
340
+ key: scored.key,
341
+ status: scored.decision.outcome === "active" ? "active" : "pending_review",
342
+ trust: scored.trust,
343
+ trustDecision: scored.decision.reason,
344
+ ...(scored.verdictKind !== undefined ? { judgeVerdict: scored.verdictKind } : {}),
345
+ importance: scored.importance,
346
+ });
347
+ }
348
+ writable.sort((a, b) => {
349
+ if (a.trust !== b.trust) return b.trust - a.trust;
350
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
351
+ });
352
+
353
+ // Seed the cap from memories already persisted for this day so repeated digest
354
+ // runs cannot each write up to the limit. `0` stays uncapped.
355
+ const alreadyWritten =
356
+ config.maxMemoriesPerDay > 0 ? await deps.writer.countActivityMemoriesForDay(startUtc, endUtc) : 0;
357
+ let remaining =
358
+ config.maxMemoriesPerDay > 0
359
+ ? Math.max(0, config.maxMemoriesPerDay - alreadyWritten)
360
+ : Number.POSITIVE_INFINITY;
361
+
362
+ for (const entry of writable) {
363
+ if (remaining <= 0) {
364
+ result.skipped += 1;
365
+ continue;
366
+ }
367
+ // Trust fields FIRST so the salvage-mode 64-entry cap can never trim these
368
+ // load-bearing keys; extractor attributes ride along after and never override
369
+ // them (collisions are compared against the canonical lowercase form).
370
+ const structuredAttributes: Record<string, string> = {
371
+ trustScore: entry.trust.toFixed(3),
372
+ trustDecision: entry.trustDecision,
373
+ ...(entry.judgeVerdict !== undefined ? { judgeVerdict: entry.judgeVerdict } : {}),
374
+ };
375
+ for (const [key, value] of Object.entries(entry.fact.structuredAttributes ?? {})) {
376
+ if (TRUST_ATTRIBUTE_KEYS[key.trim().toLowerCase()] !== true) structuredAttributes[key] = value;
377
+ }
378
+ const envelope = composeSalvagedActivityEnvelope(
379
+ {
380
+ content: entry.fact.content,
381
+ category: entry.fact.category,
382
+ tags: [...entry.fact.tags, "activity"],
383
+ entityRef: entry.fact.entityRef,
384
+ // Persist the decision-derived trust as confidence; extractor attributes
385
+ // ride along, with trust score/decision + judge verdict added last so
386
+ // downstream sees the active/pending rationale (wearable-path parity).
387
+ confidence: entry.trust,
388
+ validAt: startUtc,
389
+ structuredAttributes,
390
+ sourceConnector: "activity",
391
+ sourceReason: "screen activity digest",
392
+ },
393
+ { source: "activity", now: deps.now },
394
+ );
395
+ const write = await deps.writer.writeSealedMemory(envelope, {
396
+ status: entry.status,
397
+ contentHashSource: entry.key,
398
+ importance: entry.importance,
399
+ });
400
+ if (write.tombstoneBlocked) {
401
+ result.skipped += 1;
402
+ continue;
403
+ }
404
+ if (entry.status === "active") result.created += 1;
405
+ else result.pendingReview += 1;
406
+ remaining -= 1;
407
+ }
408
+ return result;
409
+ }
@@ -8,6 +8,7 @@ import { activityDatabasePath, ActivityStore } from "./store.js";
8
8
  import { activityDigestPath } from "./digest.js";
9
9
  import { runActivitySyncOnce } from "./runner.js";
10
10
  import { activityCursorKey } from "./pipeline.js";
11
+ import { defaultActivityConfig } from "./config.js";
11
12
  import type {
12
13
  ActivityConfig,
13
14
  ActivitySnapshot,
@@ -55,6 +56,7 @@ function fixtureClient(
55
56
 
56
57
  function enabledConfig(overrides: Partial<ActivityConfig> = {}): ActivityConfig {
57
58
  return {
59
+ ...defaultActivityConfig(),
58
60
  enabled: true,
59
61
  timezone: "UTC",
60
62
  syncDays: 1,
@@ -70,6 +72,7 @@ test("disabled config makes no client, no HTTP call, and no store write", async
70
72
  let clientBuilt = false;
71
73
  const summary = await runActivitySyncOnce({
72
74
  config: {
75
+ ...defaultActivityConfig(),
73
76
  enabled: false,
74
77
  timezone: "UTC",
75
78
  syncDays: 1,
@@ -8,6 +8,7 @@ import { ACTIVITY_SYNC_DEFAULT_INTERVAL_MS, ActivitySyncScheduler } from "./sche
8
8
  import { runActivitySyncOnce, type ActivitySyncRunSummary } from "./runner.js";
9
9
  import { ActivityStore } from "./store.js";
10
10
  import { activityCursorKey } from "./pipeline.js";
11
+ import { defaultActivityConfig } from "./config.js";
11
12
  import type { ActivityConfig, ActivitySnapshot, ActivitySourceClient } from "./types.js";
12
13
 
13
14
  /** A hand-driven timer: records armed callbacks + intervals, fires on demand. */
@@ -53,6 +54,7 @@ const SUMMARY: ActivitySyncRunSummary = {
53
54
 
54
55
  function enabledConfig(): ActivityConfig {
55
56
  return {
57
+ ...defaultActivityConfig(),
56
58
  enabled: true,
57
59
  timezone: "UTC",
58
60
  syncDays: 1,
@@ -63,6 +65,7 @@ function enabledConfig(): ActivityConfig {
63
65
 
64
66
  function disabledConfig(): ActivityConfig {
65
67
  return {
68
+ ...defaultActivityConfig(),
66
69
  enabled: false,
67
70
  timezone: "UTC",
68
71
  syncDays: 1,
@@ -8,6 +8,7 @@ import {
8
8
  type ActivitySyncRegistrarDeps,
9
9
  type ActivitySyncSchedulerLike,
10
10
  } from "./sync-registration.js";
11
+ import { defaultActivityConfig } from "./config.js";
11
12
 
12
13
  class FakeScheduler implements ActivitySyncSchedulerLike {
13
14
  starts = 0;
@@ -33,7 +34,7 @@ function makeDeps(overrides: Partial<ActivitySyncRegistrarDeps> = {}): {
33
34
  const created: FakeScheduler[] = [];
34
35
  const state = { retries: 0 };
35
36
  const deps: ActivitySyncRegistrarDeps = {
36
- config: { enabled: true, timezone: "UTC", syncDays: 1, autoSyncIntervalMinutes: 15, sources: [] },
37
+ config: { ...defaultActivityConfig(), enabled: true, sources: [] },
37
38
  memoryDir: "/tmp/remnic-activity-registrar",
38
39
  qmdCollection: "openclaw-engram",
39
40
  secureStoreEnabled: false,
@@ -11,6 +11,10 @@
11
11
  * All timestamps are UTC ISO-8601; day bucketing is half-open [start, end).
12
12
  */
13
13
 
14
+ import type { ImportanceLevel } from "../types.js";
15
+
16
+ export type ActivityExtractionMode = "off" | "smart";
17
+
14
18
  /** One captured on-screen text snapshot (a single window at a single instant). */
15
19
  export interface ActivitySnapshot {
16
20
  /** Store row id (assigned on insert; absent before persistence). */
@@ -92,7 +96,7 @@ export interface ActivitySourceConfig {
92
96
  token?: string;
93
97
  }
94
98
 
95
- /** Opt-in activity synchronization settings. */
99
+ /** Opt-in activity synchronization + trust-gated extraction settings. */
96
100
  export interface ActivityConfig {
97
101
  enabled: boolean;
98
102
  timezone: string;
@@ -100,4 +104,14 @@ export interface ActivityConfig {
100
104
  /** Periodic auto-sync cadence in minutes (default 15). */
101
105
  autoSyncIntervalMinutes: number;
102
106
  sources: ActivitySourceConfig[];
107
+ /** `off` keeps activity searchable only; `smart` trust-gates durable first-person claims. */
108
+ extractionMode: ActivityExtractionMode;
109
+ /** Baseline confidence in activity as a source of the user's own actions. */
110
+ sourceTrust: number;
111
+ autoApproveTrust: number;
112
+ reviewTrust: number;
113
+ minConfidence: number;
114
+ minImportance: ImportanceLevel;
115
+ /** `0` means no count cap. */
116
+ maxMemoriesPerDay: number;
103
117
  }
@@ -2365,5 +2365,12 @@ test("parseConfig forwards activity source settings", () => {
2365
2365
  syncDays: 2,
2366
2366
  autoSyncIntervalMinutes: 15,
2367
2367
  sources: [{ machineLabel: "fixture-machine", baseUrl: "http://127.0.0.1:4319" }],
2368
+ extractionMode: "off",
2369
+ sourceTrust: 0.6,
2370
+ autoApproveTrust: 0.8,
2371
+ reviewTrust: 0.5,
2372
+ minConfidence: 0.7,
2373
+ minImportance: "normal",
2374
+ maxMemoriesPerDay: 0,
2368
2375
  });
2369
2376
  });
@@ -8,7 +8,9 @@ import {
8
8
  drainPendingImpressionsForOfflineSync,
9
9
  drainPendingLifecycleForOfflineSync,
10
10
  getOfflineSyncStorage,
11
+ listOfflineSyncNamespaces,
11
12
  } from "./offline-sync-impression-drain.js";
13
+ import { namespaceIdentityToken } from "./namespaces/identity.js";
12
14
  import { isEncryptedFile } from "./secure-store/secure-fs.js";
13
15
  import { StorageManager } from "./storage.js";
14
16
 
@@ -314,3 +316,55 @@ test("getOfflineSyncStorage never double-folds the root ledger when it is listed
314
316
 
315
317
  assert.deepEqual(lifecycleDrains, ["root", "team"], "root ledger is folded exactly once");
316
318
  });
319
+
320
+ test("listOfflineSyncNamespaces decodes canonical token dirs and skips names the router would reject", async () => {
321
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-ns-enum-"));
322
+ try {
323
+ // A project namespace whose canonical token dir `ns-<hex>` exceeds the
324
+ // 64-char route-namespace cap: `getStorage` on the RAW basename throws
325
+ // `unsafe namespace` and 500s the whole root snapshot-stream (the regression).
326
+ const longName = "generalist-project-origin-6d1ad1f2";
327
+ const longToken = namespaceIdentityToken(longName); // ns-<hex>, 71 chars
328
+ assert.ok(longToken.length > 64, "token dir must exceed the route-namespace cap");
329
+ // A namespace whose DECODED name itself exceeds the 64-char route cap:
330
+ // decoding succeeds but isSafeRouteNamespace must still reject it.
331
+ const overCapName = `generalist-project-tag-${"x".repeat(60)}`;
332
+ const overCapToken = namespaceIdentityToken(overCapName);
333
+ assert.ok(overCapName.length > 64, "decoded name must exceed the route cap");
334
+ for (const dir of [longToken, overCapToken, "blend", "ns-default"]) {
335
+ await mkdir(path.join(root, "namespaces", dir, "state"), { recursive: true });
336
+ await writeFile(
337
+ path.join(root, "namespaces", dir, LIFECYCLE_LEDGER_REL),
338
+ "",
339
+ "utf-8",
340
+ );
341
+ }
342
+ const host = {
343
+ config: {
344
+ memoryDir: root,
345
+ defaultNamespace: "generalist",
346
+ sharedNamespace: "shared",
347
+ namespacePolicies: [],
348
+ },
349
+ namespaceCatalog: { listNamespaces: async () => [] },
350
+ } as unknown as Parameters<typeof listOfflineSyncNamespaces>[0];
351
+
352
+ const names = new Set(await listOfflineSyncNamespaces(host));
353
+
354
+ // The canonical token dir is decoded back to its namespace NAME...
355
+ assert.ok(names.has(longName), "canonical token dir must be decoded to its namespace name");
356
+ // ...never surfaced as the raw >64-char token that getStorage would reject.
357
+ assert.ok(!names.has(longToken), "raw over-cap token must never be enumerated as a namespace");
358
+ // A decoded name that is ITSELF over the route cap is skipped entirely
359
+ // (the isSafeRouteNamespace rejection branch).
360
+ assert.ok(!names.has(overCapName), "over-cap decoded name must be skipped");
361
+ assert.ok(!names.has(overCapToken), "over-cap raw token must be skipped");
362
+ // A raw plain-named dir is used verbatim.
363
+ assert.ok(names.has("blend"), "plain namespace dir must be enumerated verbatim");
364
+ // The literal ns-default token decodes to the EMPTY default identity and
365
+ // must map to the configured default namespace, not be skipped as "".
366
+ assert.ok(names.has("generalist"), "ns-default dir maps to the configured default namespace");
367
+ } finally {
368
+ await rm(root, { recursive: true, force: true });
369
+ }
370
+ });
@@ -12,6 +12,8 @@ import { isErrnoCode } from "./utils/errno.js";
12
12
  import { assertPathInsideRoot, pathIsInside } from "./utils/path-containment.js";
13
13
  import { displayErrorDetail } from "./runtime/better-sqlite.js";
14
14
  import { getConfiguredNamespaces } from "./scopes/scope-plan.js";
15
+ import { isSafeRouteNamespace } from "./routing/engine.js";
16
+ import { namespaceIdentityFromToken } from "./namespaces/identity.js";
15
17
  import type { PluginConfig } from "./types.js";
16
18
 
17
19
  type PendingImpressionDrain = () => Promise<{ pendingDeferred: boolean }>;
@@ -89,7 +91,17 @@ export async function getOfflineSyncStorage<T extends PendingLifecycleDrain & {
89
91
  if (path.resolve(storage.dir) === memoryRoot && orchestrator.listOfflineSyncNamespaces) {
90
92
  const drained = new Set<string>([memoryRoot]);
91
93
  for (const ns of await orchestrator.listOfflineSyncNamespaces()) {
92
- const nsStorage = await orchestrator.getStorage(ns);
94
+ // A single unresolvable namespace (e.g. a legacy canonical token dir whose
95
+ // name exceeds the route-namespace length cap) must not abort the whole root
96
+ // snapshot — degrade to draining the rest. listOfflineSyncNamespaces already
97
+ // normalizes token dirs, so this is defense-in-depth.
98
+ let nsStorage: T;
99
+ try {
100
+ nsStorage = await orchestrator.getStorage(ns);
101
+ } catch (err) {
102
+ log.debug(`offline-sync root drain: skipping namespace ${ns} (non-fatal): ${err}`);
103
+ continue;
104
+ }
93
105
  const nsDir = path.resolve(nsStorage.dir);
94
106
  if (drained.has(nsDir)) continue;
95
107
  drained.add(nsDir);
@@ -114,8 +126,28 @@ export async function listOfflineSyncNamespaces(host: OfflineSyncNamespaceHost):
114
126
  try {
115
127
  for (const ledgerPath of await offlineSyncLifecycleLedgerPaths(host.config.memoryDir)) {
116
128
  const namespaceDir = path.dirname(path.dirname(ledgerPath));
117
- if (path.basename(path.dirname(namespaceDir)) === "namespaces") {
118
- names.add(path.basename(namespaceDir));
129
+ if (path.basename(path.dirname(namespaceDir)) !== "namespaces") continue;
130
+ // A namespace is stored under its canonical token dir `ns-<hex>`. Decode the
131
+ // token back to the namespace NAME so the drain fanout resolves the SAME
132
+ // storage the token dir belongs to (the catalog rebuild scanner decodes the
133
+ // same way, catalog.ts finishRebuild). A raw dir already named as a plain
134
+ // namespace is used verbatim. Skip any name the router would reject — e.g. a
135
+ // canonical token dir whose decoded-or-raw name exceeds isSafeRouteNamespace's
136
+ // 64-char cap — because getStorage() would throw `unsafe namespace` and 500
137
+ // the whole root snapshot-stream (the offline-sync regression this fixes).
138
+ const dirName = path.basename(namespaceDir);
139
+ // namespaceIdentityFromToken returns "" for the ns-default token (the
140
+ // default-namespace identity) — map that to the configured default
141
+ // namespace instead of skipping the directory as an unsafe empty name.
142
+ const decoded = namespaceIdentityFromToken(dirName);
143
+ const name =
144
+ decoded === null
145
+ ? dirName
146
+ : decoded.length > 0
147
+ ? decoded
148
+ : host.config.defaultNamespace;
149
+ if (isSafeRouteNamespace(name)) {
150
+ names.add(name);
119
151
  }
120
152
  }
121
153
  } catch {