superlocalmemory 4.0.10 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +12 -2
- package/CHANGELOG.md +244 -0
- package/README.md +40 -75
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +357 -18
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +24 -2
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +36 -6
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +31 -3
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +64 -3
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +210 -256
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +20 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/server/api.py +24 -5
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_health.py +87 -10
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +191 -15
- package/src/superlocalmemory/server/ui.py +20 -4
- package/src/superlocalmemory/server/unified_daemon.py +241 -7
- package/src/superlocalmemory/storage/_migration_internals.py +54 -2
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +477 -59
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +119 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +60 -36
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +35 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +9 -18
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +19 -0
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- 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
|
|
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
|
|
159
|
-
return FactType.
|
|
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"-
|
|
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
|
-
"
|
|
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.
|
|
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"
|
|
98
|
-
|
|
99
|
-
"
|
|
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
|
-
|
|
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
|