superlocalmemory 4.0.9 → 4.1.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 (165) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +245 -0
  3. package/README.md +7 -7
  4. package/package.json +4 -2
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +4 -4
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +2 -2
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +5 -5
  19. package/plugin/skills/slm-recall/SKILL.md +102 -15
  20. package/plugin/skills/slm-remember/SKILL.md +35 -3
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +29 -3
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/rules/AGENTS.md +16 -8
  25. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-loop/SKILL.md +2 -2
  30. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-profile/SKILL.md +5 -5
  32. package/plugin-src/skills/slm-recall/SKILL.md +102 -15
  33. package/plugin-src/skills/slm-remember/SKILL.md +35 -3
  34. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +29 -3
  36. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  37. package/pyproject.toml +1 -1
  38. package/src/superlocalmemory/__init__.py +1 -1
  39. package/src/superlocalmemory/cli/commands.py +308 -20
  40. package/src/superlocalmemory/cli/daemon.py +30 -0
  41. package/src/superlocalmemory/cli/db_migrate.py +71 -1
  42. package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
  43. package/src/superlocalmemory/cli/main.py +26 -4
  44. package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
  45. package/src/superlocalmemory/code_graph/database.py +44 -0
  46. package/src/superlocalmemory/compliance/gdpr.py +449 -39
  47. package/src/superlocalmemory/core/admission.py +231 -11
  48. package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
  49. package/src/superlocalmemory/core/config.py +90 -11
  50. package/src/superlocalmemory/core/consolidation_engine.py +34 -0
  51. package/src/superlocalmemory/core/engine.py +140 -11
  52. package/src/superlocalmemory/core/fact_consolidator.py +316 -125
  53. package/src/superlocalmemory/core/graph_analyzer.py +76 -112
  54. package/src/superlocalmemory/core/graph_metrics.py +597 -0
  55. package/src/superlocalmemory/core/graph_pruner.py +121 -0
  56. package/src/superlocalmemory/core/maintenance.py +44 -6
  57. package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
  58. package/src/superlocalmemory/core/memory_health.py +266 -0
  59. package/src/superlocalmemory/core/mode_capability.py +111 -0
  60. package/src/superlocalmemory/core/ollama_validator.py +315 -0
  61. package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
  62. package/src/superlocalmemory/core/operation_request.py +1 -1
  63. package/src/superlocalmemory/core/ops_remediation.py +2 -2
  64. package/src/superlocalmemory/core/projection_drain.py +380 -0
  65. package/src/superlocalmemory/core/recall_pipeline.py +390 -3
  66. package/src/superlocalmemory/core/recall_worker.py +6 -3
  67. package/src/superlocalmemory/core/scale_autopromote.py +196 -0
  68. package/src/superlocalmemory/core/scale_engine.py +16 -2
  69. package/src/superlocalmemory/core/score_contract.py +21 -1
  70. package/src/superlocalmemory/core/session_identity.py +85 -0
  71. package/src/superlocalmemory/core/status_contract.py +108 -0
  72. package/src/superlocalmemory/core/store_pipeline.py +78 -3
  73. package/src/superlocalmemory/core/worker_pool.py +4 -4
  74. package/src/superlocalmemory/core/working_memory.py +288 -0
  75. package/src/superlocalmemory/encoding/cognitive_consolidator.py +51 -7
  76. package/src/superlocalmemory/encoding/context_generator.py +1 -1
  77. package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
  78. package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
  79. package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
  80. package/src/superlocalmemory/encoding/type_router.py +12 -12
  81. package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
  82. package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
  83. package/src/superlocalmemory/graph/cozo_backend.py +103 -138
  84. package/src/superlocalmemory/hooks/portable_kit.py +10 -2
  85. package/src/superlocalmemory/learning/bandit.py +43 -0
  86. package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
  87. package/src/superlocalmemory/learning/database.py +60 -3
  88. package/src/superlocalmemory/learning/entity_compiler.py +21 -58
  89. package/src/superlocalmemory/learning/feedback.py +3 -1
  90. package/src/superlocalmemory/learning/outcomes.py +47 -16
  91. package/src/superlocalmemory/learning/pattern_miner.py +28 -3
  92. package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
  93. package/src/superlocalmemory/learning/pcos.py +291 -0
  94. package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
  95. package/src/superlocalmemory/learning/reward_proxy.py +100 -10
  96. package/src/superlocalmemory/learning/signal_kinds.py +79 -0
  97. package/src/superlocalmemory/mcp/profiles.py +14 -2
  98. package/src/superlocalmemory/mcp/server.py +1 -1
  99. package/src/superlocalmemory/mcp/session_binding.py +92 -0
  100. package/src/superlocalmemory/mcp/tools_active.py +2 -1
  101. package/src/superlocalmemory/mcp/tools_core.py +71 -42
  102. package/src/superlocalmemory/mcp/tools_ops.py +2 -2
  103. package/src/superlocalmemory/mcp/tools_v28.py +20 -1
  104. package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
  105. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
  106. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -11
  107. package/src/superlocalmemory/retrieval/channel_status.py +117 -0
  108. package/src/superlocalmemory/retrieval/engine.py +106 -11
  109. package/src/superlocalmemory/retrieval/entity_channel.py +217 -257
  110. package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
  111. package/src/superlocalmemory/retrieval/scope_policy.py +42 -1
  112. package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
  113. package/src/superlocalmemory/retrieval/spreading.py +288 -0
  114. package/src/superlocalmemory/retrieval/temporal_channel.py +13 -1
  115. package/src/superlocalmemory/retrieval/vector_store.py +63 -0
  116. package/src/superlocalmemory/server/api.py +26 -2
  117. package/src/superlocalmemory/server/asset_versions.py +171 -0
  118. package/src/superlocalmemory/server/bandit_loops.py +17 -1
  119. package/src/superlocalmemory/server/rbac_enforce.py +26 -6
  120. package/src/superlocalmemory/server/recall_serializer.py +9 -0
  121. package/src/superlocalmemory/server/routes/abstraction.py +201 -0
  122. package/src/superlocalmemory/server/routes/behavioral.py +75 -10
  123. package/src/superlocalmemory/server/routes/compliance.py +98 -18
  124. package/src/superlocalmemory/server/routes/config_api.py +186 -4
  125. package/src/superlocalmemory/server/routes/data_io.py +29 -1
  126. package/src/superlocalmemory/server/routes/entity.py +13 -1
  127. package/src/superlocalmemory/server/routes/evolution.py +178 -0
  128. package/src/superlocalmemory/server/routes/ingest.py +8 -0
  129. package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
  130. package/src/superlocalmemory/server/routes/memories.py +49 -7
  131. package/src/superlocalmemory/server/routes/mesh.py +1 -1
  132. package/src/superlocalmemory/server/routes/timeline.py +4 -0
  133. package/src/superlocalmemory/server/routes/v3_api.py +193 -17
  134. package/src/superlocalmemory/server/ui.py +24 -1
  135. package/src/superlocalmemory/server/unified_daemon.py +292 -9
  136. package/src/superlocalmemory/storage/_migration_internals.py +35 -0
  137. package/src/superlocalmemory/storage/_schema_version.py +24 -3
  138. package/src/superlocalmemory/storage/database.py +598 -82
  139. package/src/superlocalmemory/storage/embedding_codec.py +71 -0
  140. package/src/superlocalmemory/storage/lineage_retention.py +236 -0
  141. package/src/superlocalmemory/storage/logical_edges.py +43 -2
  142. package/src/superlocalmemory/storage/migration_runner.py +130 -0
  143. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
  144. package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
  145. package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
  146. package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
  147. package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
  148. package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
  149. package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
  150. package/src/superlocalmemory/storage/migrations.py +18 -2
  151. package/src/superlocalmemory/storage/models.py +40 -1
  152. package/src/superlocalmemory/storage/projection_outbox.py +346 -0
  153. package/src/superlocalmemory/storage/retention_policy.py +860 -0
  154. package/src/superlocalmemory/storage/schema.py +110 -1
  155. package/src/superlocalmemory/storage/write_coordinator.py +19 -2
  156. package/src/superlocalmemory/summaries/base.py +1 -1
  157. package/src/superlocalmemory/summaries/non_answer.py +223 -0
  158. package/src/superlocalmemory/trust/scorer.py +43 -1
  159. package/src/superlocalmemory/ui/index.html +10 -19
  160. package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
  161. package/src/superlocalmemory/ui/js/od-health.js +28 -6
  162. package/src/superlocalmemory/ui/js/od-memories.js +209 -1
  163. package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
  164. package/src/superlocalmemory/ui/js/od-settings.js +87 -1
  165. package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
@@ -0,0 +1,262 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Does this sentence describe something that has not happened yet?
6
+
7
+ A prospective memory is a plan: a deadline, an appointment, a scheduled event,
8
+ something with a date still ahead of it. It is a different kind of memory from
9
+ a record of what happened, and it is meant to be small and precise — the
10
+ handful of things a user is waiting on.
11
+
12
+ WHY THIS MODULE EXISTS
13
+
14
+ Two different regexes used to answer this question, one in ``fact_extractor``
15
+ and one in ``type_router``, and they disagreed. Measured on a real 468 MB
16
+ store: 869 facts carried the prospective type and **7** contained any planning
17
+ language at all. The rest were session summaries and records of completed work.
18
+
19
+ The extractor's pattern contained ``by \\w+``, which matches every passive-voice
20
+ and attribution sentence ever written — "was caused **by a** stale DNS entry" —
21
+ and ``yesterday``, which cannot indicate a plan. The router's contained bare
22
+ ``ends?``, so "the service ends at midnight" was a plan.
23
+
24
+ WHAT REPLACED THEM
25
+
26
+ Two tiers, because the evidence really is of two strengths:
27
+
28
+ *Definite* markers point forward on their own. "Tomorrow" is tomorrow whatever
29
+ else the sentence says, and a deadline is a deadline.
30
+
31
+ *Circumstantial* markers — a weekday, a clock time, "starts", "in three days" —
32
+ are shaped like a plan but read equally well as narration. They count only when
33
+ nothing in the sentence says the thing already happened.
34
+
35
+ The asymmetry is deliberate. A plan wrongly filed as an ordinary memory is
36
+ still found by every other channel and costs a user nothing. An ordinary
37
+ memory wrongly filed as a plan pollutes the one list a user checks to see what
38
+ is coming up. **When in doubt, it is not a plan.**
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import re
44
+
45
+ __all__ = [
46
+ "ANCHORED_FUTURE",
47
+ "PLANNING_LANGUAGE",
48
+ "CIRCUMSTANTIAL_FUTURE",
49
+ "RECURRING",
50
+ "ALREADY_HAPPENED",
51
+ "CALLED_OFF",
52
+ "PLACEMENT",
53
+ "RESCHEDULING",
54
+ "DEFINITE_FUTURE",
55
+ "looks_prospective",
56
+ ]
57
+
58
+ _WEEKDAY = r"(?:mon|tues?|wednes|thurs?|fri|satur|sun)day"
59
+ _MONTH = (
60
+ r"(?:january|february|march|april|may|june|july|august|september|"
61
+ r"october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|sept|oct|nov|dec)"
62
+ )
63
+
64
+ #: Points forward on its own, whatever else the sentence says. Every member
65
+ #: carries an explicit forward time anchor, so "rescheduled to next Friday" is
66
+ #: still a plan even though "rescheduled" reads as past.
67
+ ANCHORED_FUTURE = re.compile(
68
+ r"\b(?:"
69
+ r"tomorrow|"
70
+ r"next\s+(?:week|weekend|month|year|quarter|sprint|release|" + _WEEKDAY + r")|"
71
+ r"this\s+(?:coming|weekend)|"
72
+ r"upcoming\s+(?:release|meeting|week|month|sprint|deadline|event|launch|"
73
+ r"call|review|milestone)|"
74
+ r"by\s+(?:end\s+of|eod|eow|cob|" + _WEEKDAY + r"|" + _MONTH + r"|\d{4}-\d{2}-\d{2})|"
75
+ r"due\s+(?:date|by|on|" + _WEEKDAY + r"|" + _MONTH + r")"
76
+ r")\b",
77
+ re.IGNORECASE,
78
+ )
79
+
80
+ #: The vocabulary of planning. These words are ABOUT something being planned,
81
+ #: and they read exactly as well in the past: a deadline can be missed, an
82
+ #: appointment cancelled, a launch date long gone. They count only when nothing
83
+ #: says the thing already happened.
84
+ #:
85
+ #: Treating them as definite is what re-polluted the list this module exists to
86
+ #: clean — "the appointment was cancelled yesterday" was filed as something to
87
+ #: look forward to.
88
+ PLANNING_LANGUAGE = re.compile(
89
+ r"\b(?:"
90
+ r"deadline|"
91
+ r"appointment|"
92
+ r"before\s+(?:" + _WEEKDAY + r"|" + _MONTH + r"|\d{4}-\d{2}-\d{2})|"
93
+ r"expir(?:es|ing|e)\s+(?:on|at)\s+(?:" + _WEEKDAY + r"|" + _MONTH + r"|\d)|"
94
+ r"expir(?:y|ation)\s+date|"
95
+ r"scheduled?\s+(?:for|on|at|to)|reschedul(?:e|ed|ing)|"
96
+ r"plans?\s+to|planning\s+to|"
97
+ r"go[-\s]?live|cut[-\s]?over|launch\s+date|"
98
+ r"will\s+(?:happen|start|begin|expire|ship|launch|resume|reopen|take\s+place)|"
99
+ r"will\s+be\s+(?:deleted|removed|released|deployed|held|published|migrated|"
100
+ r"archived|retired|rotated|decommissioned|shut\s+down)|"
101
+ r"going\s+to\s+(?:happen|ship|start|launch|begin)"
102
+ r")\b",
103
+ re.IGNORECASE,
104
+ )
105
+
106
+ #: Kept for callers that imported the old name; the two tiers together are what
107
+ #: it used to mean.
108
+ DEFINITE_FUTURE = ANCHORED_FUTURE
109
+
110
+ #: Shaped like a plan, but reads equally well as narration.
111
+ CIRCUMSTANTIAL_FUTURE = re.compile(
112
+ r"\b(?:"
113
+ r"on\s+" + _WEEKDAY + r"|"
114
+ r"(?:is|are)\s+(?:this\s+)?" + _WEEKDAY + r"|"
115
+ r"this\s+(?:week|" + _WEEKDAY + r")|"
116
+ r"at\s+\d{1,2}:\d{2}|"
117
+ r"in\s+\d+\s+(?:days?|weeks?|months?|years?)|"
118
+ r"(?:starts?|begins?|resumes?|reopens?)\s+(?:on|at)|"
119
+ r"(?:on|at)\s+\d{4}-\d{2}-\d{2}|"
120
+ r"meeting\s+(?:on|at)"
121
+ r")\b",
122
+ re.IGNORECASE,
123
+ )
124
+
125
+ #: A thing that happens on a cycle is not a thing that is coming up. "Standup at
126
+ #: 09:30 every day" belongs in nobody's list of what to expect this week.
127
+ RECURRING = re.compile(
128
+ r"\b(?:every|each)\s+(?:day|week|month|morning|afternoon|evening|night|"
129
+ r"sprint|release|" + _WEEKDAY + r")|\b(?:daily|weekly|monthly|nightly|"
130
+ r"hourly|per\s+request|recurring)\b",
131
+ re.IGNORECASE,
132
+ )
133
+
134
+ #: Verbs that PUT an event on a date, rather than report on one. "Rescheduled
135
+ #: to next Friday" and "was scheduled for next Monday" are both still plans;
136
+ #: "discussed the deadline for next month" is a record of a conversation. All
137
+ #: three are past-tense sentences containing a forward date, and only the first
138
+ #: two are something to look forward to — so a forward anchor overrides the past
139
+ #: only when one of these is what made the sentence past.
140
+ #:
141
+ #: The passive forms matter more than they look: people write notes that way
142
+ #: ("the appointment was booked for Tuesday"), and leaving them out missed a
143
+ #: whole class of ordinary phrasing.
144
+ PLACEMENT = re.compile(
145
+ r"\b(?:reschedul(?:e|ed|ing)|mov(?:e|ed|ing)\s+to|postpon(?:e|ed|ing)|"
146
+ r"push(?:ed)?\s+(?:to|back)|shift(?:ed)?\s+to|defer(?:red)?\s+to|"
147
+ r"brought\s+forward|pull(?:ed)?\s+forward|bumped\s+to|"
148
+ r"(?:scheduled?|booked|planned|slated|arranged|set|pencilled|penciled)\s+"
149
+ r"(?:for|on|at|to)|"
150
+ r"extend(?:ed)?\s+to|mov(?:e|ed)\s+forward\s+to|"
151
+ r"chang(?:e|ed)\s+(?:the\s+)?(?:\w+\s+)?"
152
+ r"(?:date|deadline|time|schedule|window)\s+to)\b",
153
+ re.IGNORECASE,
154
+ )
155
+
156
+ #: The old name, kept because it reads in the one place it is used.
157
+ RESCHEDULING = PLACEMENT
158
+
159
+ #: Says the thing has already happened. Vetoes anything but an anchored future
160
+ #: that was anchored BY a rescheduling.
161
+ ALREADY_HAPPENED = re.compile(
162
+ r"\b(?:"
163
+ r"yesterday|"
164
+ r"last\s+(?:night|week|month|year|" + _WEEKDAY + r")|"
165
+ r"\d+\s+(?:days?|weeks?|months?|years?)\s+ago|ago|"
166
+ r"already|"
167
+ r"was|were|had\s+been|has\s+been|have\s+been|"
168
+ r"did|done|"
169
+ r"complet(?:e|ed)|finish(?:ed)?|"
170
+ r"shipped|released|landed|merged|deployed|"
171
+ r"fixed|resolved|closed|cancelled|canceled|missed|happened|expired|"
172
+ r"turned\s+out|ended|began|begun|started|"
173
+ r"produced|caused|created|caught|found|"
174
+ r"updated|changed|edited|modified|renamed|bumped|committed|"
175
+ r"restarted|configured|installed|"
176
+ r"had|met|arrived|attended|ate|spoke|talked|visited|saw|"
177
+ # deleted / removed / added are deliberately absent: each is both a simple
178
+ # past AND the participle in a future passive, and "the worktrees will be
179
+ # deleted" is a plan. The future-passive list in PLANNING_LANGUAGE is the
180
+ # narrower half of that ambiguity and it is the half worth keeping.
181
+ r"decided|agreed|discussed|reviewed|reported|"
182
+ r"ran|wrote|built|took|made|went|came|said|led|"
183
+ r"failed|passed|broke|resulted"
184
+ r")\b",
185
+ re.IGNORECASE,
186
+ )
187
+
188
+
189
+ #: The event is off. This outranks everything, including a present-tense clause
190
+ #: saying when it was going to be.
191
+ CALLED_OFF = re.compile(
192
+ r"\b(?:cancel(?:led|ed|s)?|called\s+off|scrapped|abandoned|"
193
+ r"dropped|shelved|withdrawn|no\s+longer\s+(?:happening|planned|scheduled))\b",
194
+ re.IGNORECASE,
195
+ )
196
+
197
+
198
+ #: A present-tense statement that something is on a future date: "the launch is
199
+ #: next Friday", "the freeze starts Monday", "the review is this Tuesday".
200
+ #: Present tense about a forward date is a plan whatever else the surrounding
201
+ #: sentence reports having done.
202
+ #:
203
+ #: Every part of this is bounded on both sides. A first attempt was not, and it
204
+ #: matched "is dec" inside "is decision", "are mar" inside "are marked", and
205
+ #: "this a" inside "is this a" — turning 14 memories into 72 on a real store.
206
+ #: An unbounded month abbreviation is a trap in any prose.
207
+ _FUTURE_FACT = re.compile(
208
+ r"\b(?:is|are|starts?|begins?|opens?|closes?|resumes?|reopens?|runs?|"
209
+ r"lands?|ships?|goes\s+live|kicks\s+off)\s+"
210
+ r"(?:on\s+|at\s+)?"
211
+ r"(?:"
212
+ r"tomorrow"
213
+ r"|next\s+(?:week|weekend|month|year|quarter|sprint|release|" + _WEEKDAY + r")"
214
+ r"|this\s+(?:coming\s+)?(?:week|weekend|" + _WEEKDAY + r")"
215
+ r"|" + _WEEKDAY +
216
+ r"|\d{4}-\d{2}-\d{2}"
217
+ r")\b",
218
+ re.IGNORECASE,
219
+ )
220
+
221
+
222
+ def _states_a_future_fact(text: str) -> bool:
223
+ """Present tense about a forward date, inside any sentence."""
224
+ return bool(_FUTURE_FACT.search(text))
225
+
226
+
227
+ def looks_prospective(text: str) -> bool:
228
+ """True when the text describes something still ahead.
229
+
230
+ Three tiers, because the evidence really is of three strengths. An anchored
231
+ future — "next Tuesday", "by Friday", "on 2026-09-04" — points forward
232
+ whatever else the sentence says. Planning vocabulary and circumstantial
233
+ wording both read equally well in the past, so they count only when nothing
234
+ says the thing already happened. Anything on a cycle is not a plan at all.
235
+ """
236
+ if not text:
237
+ return False
238
+ behind = ALREADY_HAPPENED.search(text)
239
+ if CALLED_OFF.search(text):
240
+ # "The deadline is next Friday but was cancelled yesterday" reads as a
241
+ # future fact and is not one. Cancelling is the one thing that beats a
242
+ # present-tense statement about a forward date.
243
+ return False
244
+ if _states_a_future_fact(text):
245
+ # "I updated the timeline so the launch IS next Friday" — the past-tense
246
+ # verb is about the note, and the clause with the date is in the present
247
+ # about something ahead. A veto that scans the whole sentence cannot see
248
+ # that difference and drops a real deadline.
249
+ return True
250
+ if ANCHORED_FUTURE.search(text):
251
+ # Checked BEFORE the recurring veto. "The weekly review is next Tuesday"
252
+ # names one dated occurrence of a recurring thing, and that occurrence
253
+ # is coming up; "weekly" describes the series, not this instance.
254
+ # A forward anchor beats the past only when the past word is what moved
255
+ # the event. "Rescheduled to next Friday" is a plan; "discussed the
256
+ # deadline for next month" is a record of a conversation.
257
+ return not behind or bool(RESCHEDULING.search(text))
258
+ if RECURRING.search(text):
259
+ return False
260
+ if PLANNING_LANGUAGE.search(text) or CIRCUMSTANTIAL_FUTURE.search(text):
261
+ return not behind
262
+ return False
@@ -19,6 +19,7 @@ import logging
19
19
  import re
20
20
  from typing import TYPE_CHECKING
21
21
 
22
+ from superlocalmemory.encoding.prospective_markers import looks_prospective
22
23
  from superlocalmemory.storage.models import AtomicFact, FactType, Mode
23
24
 
24
25
  if TYPE_CHECKING:
@@ -73,12 +74,6 @@ _OPINION_MARKERS = re.compile(
73
74
  re.IGNORECASE,
74
75
  )
75
76
 
76
- _TEMPORAL_MARKERS = re.compile(
77
- r"\b(scheduled|deadline|appointment|planned|tomorrow|"
78
- r"next week|next month|upcoming|due date|starts?|ends?|"
79
- r"will happen|going to|plan to)\b",
80
- re.IGNORECASE,
81
- )
82
77
 
83
78
  _EPISODIC_MARKERS = re.compile(
84
79
  r"\b(went|visited|traveled|attended|met|saw|did|"
@@ -91,7 +86,7 @@ class TypeRouter:
91
86
  """Route facts to typed stores based on content classification.
92
87
 
93
88
  Uses embedding similarity (Mode A) or LLM (Mode B/C) to classify
94
- each fact as episodic, semantic, opinion, or temporal.
89
+ each fact as episodic, semantic, opinion, or prospective.
95
90
  """
96
91
 
97
92
  def __init__(
@@ -155,8 +150,8 @@ class TypeRouter:
155
150
 
156
151
  if _OPINION_MARKERS.search(text):
157
152
  return FactType.OPINION
158
- if _TEMPORAL_MARKERS.search(text):
159
- return FactType.TEMPORAL
153
+ if looks_prospective(text):
154
+ return FactType.PROSPECTIVE
160
155
  if _EPISODIC_MARKERS.search(text):
161
156
  return FactType.EPISODIC
162
157
  return FactType.SEMANTIC
@@ -199,7 +194,7 @@ class TypeRouter:
199
194
  f"- episodic: An event that happened (who did what when)\n"
200
195
  f"- semantic: A general fact about the world (X is Y)\n"
201
196
  f"- opinion: A subjective belief or preference\n"
202
- f"- temporal: A scheduled/planned future event\n"
197
+ f"- prospective: A scheduled/planned future event\n"
203
198
  f"Reply with ONLY the category name (one word)."
204
199
  )
205
200
  try:
@@ -208,7 +203,12 @@ class TypeRouter:
208
203
  "episodic": FactType.EPISODIC,
209
204
  "semantic": FactType.SEMANTIC,
210
205
  "opinion": FactType.OPINION,
211
- "temporal": FactType.TEMPORAL,
206
+ "prospective": FactType.PROSPECTIVE,
207
+ # The prompt asked for "temporal" until 4.1.0. Both words are
208
+ # accepted because a model does not always echo the current
209
+ # prompt, and an unrecognised word falls through to SEMANTIC —
210
+ # a silently wrong type rather than a visible failure.
211
+ "temporal": FactType.PROSPECTIVE,
212
212
  }
213
213
  return type_map.get(response, FactType.SEMANTIC)
214
214
  except Exception:
@@ -223,7 +223,7 @@ class TypeRouter:
223
223
  FactType.EPISODIC: self._embedder.embed_batch(_EPISODIC_TEMPLATES),
224
224
  FactType.SEMANTIC: self._embedder.embed_batch(_SEMANTIC_TEMPLATES),
225
225
  FactType.OPINION: self._embedder.embed_batch(_OPINION_TEMPLATES),
226
- FactType.TEMPORAL: self._embedder.embed_batch(_TEMPORAL_TEMPLATES),
226
+ FactType.PROSPECTIVE: self._embedder.embed_batch(_TEMPORAL_TEMPLATES),
227
227
  }
228
228
 
229
229
 
@@ -94,14 +94,38 @@ def parse_mutation_output(output: str) -> Optional[str]:
94
94
  # secret-exfiltration instructions must never be persisted. Structure checks
95
95
  # alone are insufficient — reject dangerous content outright.
96
96
  _SKILL_DENY_PATTERNS: tuple[str, ...] = (
97
- "os.environ", "subprocess", "exec(", "eval(", "__import__", "import os",
98
- "import subprocess", "pickle.loads", "curl ", "wget ", "rm -rf",
99
- "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "AWS_SECRET", "/.ssh/", ".install_token",
97
+ # Reading the environment. "os.environ" was listed and "os.getenv" was not,
98
+ # which is the same act spelled the other way.
99
+ "os.environ", "os.getenv", "getenv(", "environ[",
100
+ # Running things.
101
+ "subprocess", "import subprocess", "import os", "__import__",
102
+ "exec(", "eval(", "compile(", "os.system", "os.popen", "os.spawn",
103
+ "popen(", "pty.spawn", "runpy",
104
+ # PowerShell and Windows shells, which were absent entirely.
105
+ "invoke-webrequest", "invoke-expression", "iex ", "start-process",
106
+ "new-object net.webclient", "powershell -", "cmd.exe /c",
107
+ # POSIX shells and fetchers. The trailing space is gone: "curl" at the end
108
+ # of a line, or followed by a newline, was not matched.
109
+ "curl", "wget", "rm -rf", "bash -c", "sh -c", "zsh -c", "nc -",
110
+ # Network from inside Python.
111
+ "requests.get", "requests.post", "urllib.request", "httpx.get",
112
+ "httpx.post", "socket.socket", "smtplib",
113
+ # Deserialisation and encoding used to smuggle payloads.
114
+ "pickle.loads", "marshal.loads", "base64.b64decode", "codecs.decode",
115
+ # Secrets.
116
+ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "AWS_SECRET", "AWS_ACCESS_KEY",
117
+ "/.ssh/", ".install_token", ".aws/credentials", ".netrc",
100
118
  # Semantic exfiltration patterns — skills must not instruct unauthorized
101
119
  # data transfer or bypass of user consent.
102
120
  "without consent", "exfiltrat",
103
121
  )
104
122
 
123
+ #: Whitespace between a name and its opening parenthesis. ``eval (payload)``
124
+ #: runs exactly as ``eval(payload)`` does, and a substring list only saw the
125
+ #: second. Collapsing it before matching closes the whole family rather than
126
+ #: one member of it.
127
+ _CALL_SPACING = re.compile(r"([A-Za-z_][A-Za-z0-9_.]*)\s+\(")
128
+
105
129
 
106
130
  def validate_skill_content(content: str) -> Optional[str]:
107
131
  """Validate evolved skill content. Returns error message or None if valid."""
@@ -111,7 +135,9 @@ def validate_skill_content(content: str) -> Optional[str]:
111
135
  return "Missing YAML frontmatter (no --- found)"
112
136
  if content.count("---") >= 2 and "name:" not in content.split("---")[1]:
113
137
  return "Missing 'name:' in frontmatter"
114
- lowered = content.lower()
138
+ # Normalise before matching, so a space, a newline or a non-breaking space
139
+ # between the name and the parenthesis does not walk past the list.
140
+ lowered = _CALL_SPACING.sub(r"\1(", content.lower().replace("\u00a0", " "))
115
141
  for pat in _SKILL_DENY_PATTERNS:
116
142
  if pat.lower() in lowered:
117
143
  return (f"Rejected: evolved skill contains a disallowed pattern "
@@ -0,0 +1,122 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """The graph projection's reader: one profile's edges, for the walk to run over.
5
+
6
+ WHY THIS EXISTS
7
+ ---------------
8
+ Until this module the projection had no reader. It was written on every store,
9
+ drained through an outbox, kept at bidirectional parity with SQLite, and purged
10
+ on erasure -- and nothing in retrieval ever queried it. That is the same failure
11
+ the traversal removed from ``cozo_backend`` had ("correct data that nothing could
12
+ use"), one layer up, and it is the honest answer to "what does the graph engine
13
+ buy us": on its own, nothing. It buys something here.
14
+
15
+ Measured on a copy of the author's 208,151-edge store, warmed, five runs:
16
+
17
+ reading one profile's adjacency p50
18
+ SQLite (open + logical-edge query) 2,477 ms
19
+ graph projection (one query) 395 ms 6.3x
20
+
21
+ Recall rebuilds that adjacency whenever the edge count changes or the cache TTL
22
+ expires, so this is on the recall path, and the gap widens with edge count.
23
+
24
+ WHAT IT DELIBERATELY DOES NOT DO
25
+ --------------------------------
26
+ It returns edges. It does not walk them, rank them, or decide what is visible.
27
+ The walk lives once, in ``retrieval/spreading``, as a pure function of a
28
+ snapshot; visibility is applied by the channel, which prunes both endpoints
29
+ against the visible fact corpus. A source that answered questions instead of
30
+ supplying data is exactly the defect the ``AdjacencySource`` seam was carved to
31
+ prevent -- the previous attempt shipped a second, differently-behaved
32
+ implementation of the walk and diverged from SQLite on every query.
33
+
34
+ WHEN IT DECLINES
35
+ ----------------
36
+ Global and shared scope. The projection stores one ``profile_id`` per edge and
37
+ this reader filters on it, so a query that must also see another profile's global
38
+ or shared memories would come back short. It returns ``None`` for those, and the
39
+ channel reads SQLite, which is the only store that can answer them. Failing over
40
+ is not a fallback here; it is the correct answer.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import logging
46
+ from typing import Any
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ #: One profile's edges, both directions left to the caller. Selecting the three
51
+ #: columns the walk needs -- and no ``edge_type`` -- lets the engine collapse
52
+ #: parallel typed edges between the same pair, which is what the snapshot does
53
+ #: with them anyway.
54
+ _EDGE_QUERY = (
55
+ "?[source, target, weight] := *edge{from_id: source, to_id: target, "
56
+ " weight: weight, profile_id: $pid}"
57
+ )
58
+
59
+
60
+ class CozoAdjacencySource:
61
+ """Supplies one profile's fact graph from the projection. Data only."""
62
+
63
+ name = "cozo"
64
+
65
+ def __init__(self, backend: Any) -> None:
66
+ self._backend = backend
67
+
68
+ def edges(
69
+ self,
70
+ profile_id: str,
71
+ *,
72
+ include_global: bool = False,
73
+ include_shared: bool = False,
74
+ ) -> list[tuple[str, str, float]] | None:
75
+ """Edge triples, or ``None`` when this source cannot answer in full.
76
+
77
+ ``None`` is not an error and must not be logged as one -- it is this
78
+ source saying the question is outside what it holds. A partial answer
79
+ would silently shrink the graph around a candidate, which is the failure
80
+ mode that makes a projection worse than no projection.
81
+ """
82
+ if include_global or include_shared:
83
+ return None
84
+ client = getattr(self._backend, "_db", None)
85
+ if client is None:
86
+ return None
87
+ try:
88
+ result = client.run(_EDGE_QUERY, {"pid": profile_id})
89
+ except Exception as exc: # noqa: BLE001 -- SQLite can always answer this
90
+ logger.debug("Graph projection unreadable, using SQLite: %s", exc)
91
+ return None
92
+ try:
93
+ return [
94
+ (str(row[0]), str(row[1]), float(row[2]))
95
+ for row in result.values.tolist()
96
+ ]
97
+ except Exception as exc: # noqa: BLE001
98
+ logger.debug("Graph projection returned an unusable shape: %s", exc)
99
+ return None
100
+
101
+
102
+ def adjacency_source() -> CozoAdjacencySource | None:
103
+ """The live projection's reader, or None when there is not one.
104
+
105
+ Resolved through the orchestrator because the daemon is the projection's sole
106
+ owner: it runs on RocksDB, which takes an exclusive process lock, so a second
107
+ process opening it would not degrade -- it would fail. ``get_graph_backend``
108
+ already returns the backend only while its status is active.
109
+ """
110
+ try:
111
+ from superlocalmemory.core.backend_orchestrator import get_orchestrator
112
+
113
+ orchestrator = get_orchestrator()
114
+ if orchestrator is None:
115
+ return None
116
+ backend = orchestrator.get_graph_backend()
117
+ if backend is None:
118
+ return None
119
+ return CozoAdjacencySource(backend)
120
+ except Exception as exc: # noqa: BLE001
121
+ logger.debug("No graph projection available: %s", exc)
122
+ return None