copresence 0.1.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ${{ matrix.os }}
6
+ strategy:
7
+ matrix:
8
+ os: [ubuntu-latest, macos-latest]
9
+ python: ["3.10", "3.11", "3.12"]
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with: { python-version: "${{ matrix.python }}" }
14
+ - run: pip install -e . pytest
15
+ - run: pytest tests/ -q
16
+ - run: copresence demo
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ copresence_layers/
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: copresence
3
+ Version: 0.1.0
4
+ Summary: Associative memory for event streams: link moments by co-presence (what was there), not just similarity (what it resembles).
5
+ Project-URL: Homepage, https://github.com/myfjin/copresence
6
+ Author: Illia Hladkyi
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ Keywords: agents,associative,context,memory,recall,spreading-activation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.10
15
+ Provides-Extra: embeddings
16
+ Requires-Dist: sentence-transformers>=2.2; extra == 'embeddings'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # copresence
20
+
21
+ **Associative memory for event streams: link moments by co-presence (what was
22
+ _there_), not just similarity (what it _resembles_). A complement to embedding
23
+ recall, not a replacement.**
24
+
25
+ Zero dependencies. Pure stdlib. `pip install copresence`
26
+
27
+ ## The black t-shirt law
28
+
29
+ You're trying to remember a conversation from years ago. Searching your memory by
30
+ *content* gives you nothing — the conversation doesn't resemble anything you can
31
+ name. Then a friend says: *"you were wearing that black t-shirt… the kettle was
32
+ on… it was the day the heater broke"* — and the whole context **rebuilds itself**.
33
+
34
+ The t-shirt never resembled the conversation. It was simply **there**.
35
+
36
+ Embedding-based recall (RAG, vector search) is content-similarity: it finds what
37
+ *resembles* the query. It systematically misses everything that was merely
38
+ *co-present* — the peripheral details, the same-day decisions, the parallel
39
+ threads. `copresence` adds that missing axis: it links moments **by having shared
40
+ a scene, a day, an era**, and recalls by **spreading activation** through those
41
+ links.
42
+
43
+ ## Quickstart
44
+
45
+ ```python
46
+ from copresence import Store, ref, hash_embedder, probe
47
+
48
+ events = [ # any event stream: chat logs, journals, tickets, commits…
49
+ {"timestamp": "2026-01-05T10:00:00+00:00", "speaker": "a", "content": "…"},
50
+ # {"timestamp": ISO-8601, "speaker": str, "content": str}
51
+ ]
52
+
53
+ store = Store("./layers")
54
+ store.link(events) # co-presence links: scene + day + era
55
+ store.notice(ref(events[3]), "me", "this mattered") # authored mark
56
+
57
+ # recall by cascade: seeds spread through co-presence, not similarity
58
+ adj = store.graph("me")
59
+ context = store.cascade({ref(events[3])}, adj)
60
+ ```
61
+
62
+ Or just run the built-in demonstration:
63
+
64
+ ```
65
+ pip install copresence
66
+ copresence demo
67
+ ```
68
+
69
+ which builds a small synthetic corpus where a red kettle whistles near lighthouse
70
+ work (separate moments, same scenes), shows that similarity recall ranks the
71
+ target poorly for the kettle cue, and then rebuilds it through the co-presence
72
+ cascade. Deterministic; runs anywhere; no model download.
73
+
74
+ ## Concepts
75
+
76
+ - **Layers are sovereign.** Every member (person, agent) gets their own link
77
+ files. `links.jsonl` is *authored* — appended, never rebuilt. `auto_links.jsonl`
78
+ is *derived* — rebuilt by the linker on every run. Structural co-presence is
79
+ shared because it's computable from timestamps alone.
80
+ - **"That day" is a memory node.** Co-presence works at three scales: adjacency
81
+ (the scene), day-hubs, era-hubs (the week). Touching one moment can pull what
82
+ else the day held.
83
+ - **Presence weighting.** Not every co-occurrence is equal: moments carrying
84
+ gravity markers or notice-marks weigh their links up; incidental co-occurrence
85
+ weighs half.
86
+ - **Decay with resurrection.** Unused links fade toward a dormant floor — dormancy,
87
+ never death. When a cascade touches a dormant link, it re-animates the *whole
88
+ scene* around it, not just the node.
89
+ - **Warming.** `warm()` scores which *dormant* moments are heating up under the
90
+ current context (heat = kinship-to-now × dormancy), with a `should_interrupt`
91
+ attention boundary — the caller decides whether to speak. Feed it the record's
92
+ **tail**, not a query: continuity anchors in time, not similarity. We learned
93
+ that from a failed probe and kept the law.
94
+ - **Discovery.** `discover_regular()` maps the day-to-day roads (themes alive
95
+ across many days); `discover_deep()` hunts dormant chains — strong kinship
96
+ across a long gap, never cross-referenced. These are **candidates, not claims**:
97
+ whether a deep pair "hits" is the member's subjective call.
98
+ - **Resonance.** Which moments live in one member's layer (private wealth), two,
99
+ or all? The field is computed; verdicts are not — private wealth is never
100
+ forced shared.
101
+
102
+ ## Embedders
103
+
104
+ The cascade, linker, notices and layers are pure stdlib. `discover`, `warm` and
105
+ `probe` need an embedder — any callable `list[str] -> vectors`:
106
+
107
+ ```python
108
+ from copresence import hash_embedder # zero-dep hashing bag-of-words (a TOY —
109
+ # fine for demos/tests, no real semantics)
110
+ from copresence import st_embedder # real: pip install copresence[embeddings]
111
+ embed = st_embedder() # sentence-transformers MiniLM
112
+ ```
113
+
114
+ ## Honest notes
115
+
116
+ - The mechanism is young. In our own (private) corpus of ~520 events, a target
117
+ that content-recall ranked 265th was rebuilt by a two-hop cascade from six
118
+ peripheral cues (similarity 0.03–0.18 to the query), and a negative control —
119
+ the same cues against a different-day target — was correctly *not* reached.
120
+ The bundled demo reproduces the same geometry synthetically on your machine;
121
+ it proves the mechanism, not magic on your data.
122
+ - Mood/register link heuristics exist but are **experimental and off by default**
123
+ (`Store(registers=...)`) — our own instance is too young to have proven them.
124
+ - The hashing embedder is a toy. Real corpora deserve a real embedder.
125
+ - `discover_deep` can surface echo-shaped pairs (a summary of a moment vs the
126
+ moment). The identity cap catches most; your judgment catches the rest.
127
+
128
+ ## License
129
+
130
+ Apache-2.0
@@ -0,0 +1,112 @@
1
+ # copresence
2
+
3
+ **Associative memory for event streams: link moments by co-presence (what was
4
+ _there_), not just similarity (what it _resembles_). A complement to embedding
5
+ recall, not a replacement.**
6
+
7
+ Zero dependencies. Pure stdlib. `pip install copresence`
8
+
9
+ ## The black t-shirt law
10
+
11
+ You're trying to remember a conversation from years ago. Searching your memory by
12
+ *content* gives you nothing — the conversation doesn't resemble anything you can
13
+ name. Then a friend says: *"you were wearing that black t-shirt… the kettle was
14
+ on… it was the day the heater broke"* — and the whole context **rebuilds itself**.
15
+
16
+ The t-shirt never resembled the conversation. It was simply **there**.
17
+
18
+ Embedding-based recall (RAG, vector search) is content-similarity: it finds what
19
+ *resembles* the query. It systematically misses everything that was merely
20
+ *co-present* — the peripheral details, the same-day decisions, the parallel
21
+ threads. `copresence` adds that missing axis: it links moments **by having shared
22
+ a scene, a day, an era**, and recalls by **spreading activation** through those
23
+ links.
24
+
25
+ ## Quickstart
26
+
27
+ ```python
28
+ from copresence import Store, ref, hash_embedder, probe
29
+
30
+ events = [ # any event stream: chat logs, journals, tickets, commits…
31
+ {"timestamp": "2026-01-05T10:00:00+00:00", "speaker": "a", "content": "…"},
32
+ # {"timestamp": ISO-8601, "speaker": str, "content": str}
33
+ ]
34
+
35
+ store = Store("./layers")
36
+ store.link(events) # co-presence links: scene + day + era
37
+ store.notice(ref(events[3]), "me", "this mattered") # authored mark
38
+
39
+ # recall by cascade: seeds spread through co-presence, not similarity
40
+ adj = store.graph("me")
41
+ context = store.cascade({ref(events[3])}, adj)
42
+ ```
43
+
44
+ Or just run the built-in demonstration:
45
+
46
+ ```
47
+ pip install copresence
48
+ copresence demo
49
+ ```
50
+
51
+ which builds a small synthetic corpus where a red kettle whistles near lighthouse
52
+ work (separate moments, same scenes), shows that similarity recall ranks the
53
+ target poorly for the kettle cue, and then rebuilds it through the co-presence
54
+ cascade. Deterministic; runs anywhere; no model download.
55
+
56
+ ## Concepts
57
+
58
+ - **Layers are sovereign.** Every member (person, agent) gets their own link
59
+ files. `links.jsonl` is *authored* — appended, never rebuilt. `auto_links.jsonl`
60
+ is *derived* — rebuilt by the linker on every run. Structural co-presence is
61
+ shared because it's computable from timestamps alone.
62
+ - **"That day" is a memory node.** Co-presence works at three scales: adjacency
63
+ (the scene), day-hubs, era-hubs (the week). Touching one moment can pull what
64
+ else the day held.
65
+ - **Presence weighting.** Not every co-occurrence is equal: moments carrying
66
+ gravity markers or notice-marks weigh their links up; incidental co-occurrence
67
+ weighs half.
68
+ - **Decay with resurrection.** Unused links fade toward a dormant floor — dormancy,
69
+ never death. When a cascade touches a dormant link, it re-animates the *whole
70
+ scene* around it, not just the node.
71
+ - **Warming.** `warm()` scores which *dormant* moments are heating up under the
72
+ current context (heat = kinship-to-now × dormancy), with a `should_interrupt`
73
+ attention boundary — the caller decides whether to speak. Feed it the record's
74
+ **tail**, not a query: continuity anchors in time, not similarity. We learned
75
+ that from a failed probe and kept the law.
76
+ - **Discovery.** `discover_regular()` maps the day-to-day roads (themes alive
77
+ across many days); `discover_deep()` hunts dormant chains — strong kinship
78
+ across a long gap, never cross-referenced. These are **candidates, not claims**:
79
+ whether a deep pair "hits" is the member's subjective call.
80
+ - **Resonance.** Which moments live in one member's layer (private wealth), two,
81
+ or all? The field is computed; verdicts are not — private wealth is never
82
+ forced shared.
83
+
84
+ ## Embedders
85
+
86
+ The cascade, linker, notices and layers are pure stdlib. `discover`, `warm` and
87
+ `probe` need an embedder — any callable `list[str] -> vectors`:
88
+
89
+ ```python
90
+ from copresence import hash_embedder # zero-dep hashing bag-of-words (a TOY —
91
+ # fine for demos/tests, no real semantics)
92
+ from copresence import st_embedder # real: pip install copresence[embeddings]
93
+ embed = st_embedder() # sentence-transformers MiniLM
94
+ ```
95
+
96
+ ## Honest notes
97
+
98
+ - The mechanism is young. In our own (private) corpus of ~520 events, a target
99
+ that content-recall ranked 265th was rebuilt by a two-hop cascade from six
100
+ peripheral cues (similarity 0.03–0.18 to the query), and a negative control —
101
+ the same cues against a different-day target — was correctly *not* reached.
102
+ The bundled demo reproduces the same geometry synthetically on your machine;
103
+ it proves the mechanism, not magic on your data.
104
+ - Mood/register link heuristics exist but are **experimental and off by default**
105
+ (`Store(registers=...)`) — our own instance is too young to have proven them.
106
+ - The hashing embedder is a toy. Real corpora deserve a real embedder.
107
+ - `discover_deep` can surface echo-shaped pairs (a summary of a moment vs the
108
+ moment). The identity cap catches most; your judgment catches the rest.
109
+
110
+ ## License
111
+
112
+ Apache-2.0
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "copresence"
7
+ version = "0.1.0"
8
+ description = "Associative memory for event streams: link moments by co-presence (what was there), not just similarity (what it resembles)."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Illia Hladkyi" }]
13
+ keywords = ["memory", "associative", "recall", "agents", "context", "spreading-activation"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ embeddings = ["sentence-transformers>=2.2"]
23
+
24
+ [project.scripts]
25
+ copresence = "copresence.cli:main"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/myfjin/copresence"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/copresence"]
@@ -0,0 +1,12 @@
1
+ """copresence — associative memory for event streams.
2
+
3
+ Link moments by co-presence (what was THERE), not just similarity (what it
4
+ RESEMBLES). A complement to embedding recall, not a replacement.
5
+ """
6
+ from .core import Store, ref
7
+ from .recall import (discover_deep, discover_regular, hash_embedder, probe,
8
+ st_embedder, warm)
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = ["Store", "ref", "hash_embedder", "st_embedder",
12
+ "discover_regular", "discover_deep", "warm", "probe", "__version__"]
@@ -0,0 +1,63 @@
1
+ """copresence CLI — link, notice, discover, warm, demo over an events.jsonl.
2
+
3
+ Events file: one JSON object per line: {"timestamp": ISO-8601, "speaker": str,
4
+ "content": str}. Layers live under --root (default ./copresence_layers).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ from pathlib import Path
11
+
12
+ from .core import Store, ref
13
+ from .recall import discover_deep, discover_regular, hash_embedder, warm
14
+
15
+
16
+ def _events(p: str) -> list[dict]:
17
+ return [json.loads(l) for l in Path(p).read_text(encoding="utf-8").splitlines() if l.strip()]
18
+
19
+
20
+ def main(argv=None):
21
+ ap = argparse.ArgumentParser(prog="copresence", description=__doc__)
22
+ ap.add_argument("cmd", choices=["link", "notice", "discover", "warm", "demo"])
23
+ ap.add_argument("--events", default="events.jsonl")
24
+ ap.add_argument("--root", default="copresence_layers")
25
+ ap.add_argument("--member", default="member")
26
+ ap.add_argument("--note", default="")
27
+ ap.add_argument("--ref", default="last")
28
+ ap.add_argument("--gap", type=int, default=7)
29
+ a = ap.parse_args(argv)
30
+
31
+ if a.cmd == "demo":
32
+ from .demo import run
33
+ run()
34
+ return
35
+ store = Store(a.root)
36
+ ev = _events(a.events)
37
+ if a.cmd == "link":
38
+ print(json.dumps(store.link(ev)))
39
+ elif a.cmd == "notice":
40
+ r = ref(ev[-1]) if a.ref == "last" else a.ref
41
+ row = store.notice(r, a.member, a.note)
42
+ print(f"noticed {row['from']} by {a.member} ({row['link_id']})")
43
+ elif a.cmd == "discover":
44
+ X = hash_embedder([e["content"] for e in ev])
45
+ print("== regular roads (themes alive across days) ==")
46
+ for r in discover_regular(ev, X):
47
+ print(f" {r['days_alive']}d alive · {r['n_events']} events · “{r['exemplar'][:70]}”")
48
+ print(f"== deep candidates (kin across ≥{a.gap}d, never cross-referenced) ==")
49
+ for d in discover_deep(ev, X, gap_days=a.gap):
50
+ print(f" sim {d['sim']} across {d['gap_days']}d")
51
+ print(f" THEN “{d['then']['excerpt'][:70]}”\n NOW “{d['now']['excerpt'][:70]}”")
52
+ print("(candidates, not claims — whether one HITS is the member's call)")
53
+ elif a.cmd == "warm":
54
+ X = hash_embedder([e["content"] for e in ev])
55
+ tail = " ".join(e["content"][:400] for e in ev[-3:])
56
+ v = hash_embedder([tail])[0]
57
+ for w in warm(ev, X, v):
58
+ mark = " ⚡" if w["should_interrupt"] else ""
59
+ print(f" heat {w['heat']} · {w['age_days']}d dormant{mark} · “{w['excerpt'][:70]}”")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
@@ -0,0 +1,223 @@
1
+ """copresence.core — link moments by co-presence (what was THERE),
2
+ not just similarity (what it RESEMBLES).
3
+
4
+ Pure stdlib. The mechanics:
5
+
6
+ EVENTS a list of {"timestamp": ISO-8601, "speaker": str, "content": str}
7
+ LAYERS per-member link files under a root dir:
8
+ <root>/<member>/links.jsonl AUTHORED — appended, never rebuilt
9
+ <root>/<member>/auto_links.jsonl DERIVED — rebuilt by the linker
10
+ <root>/_structural/ fact-derived co-presence (shared)
11
+ LINKER co-presence at three scales: adjacency (the scene), day-hubs ("that day"
12
+ is itself a memory node), era-hubs (the week). Weights carry PRESENCE
13
+ (gravity moments outweigh incidental co-occurrence) and DECAY at read
14
+ time (dormancy, never death).
15
+ CASCADE spreading activation with RESURRECTION: touching a dormant link
16
+ re-animates its whole scene, not just the node.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ from collections import defaultdict
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+
26
+ # ── tunables (documented defaults; override via Store kwargs) ───────────────────
27
+ ADJ_MIN = 45 # minutes: adjacency = the scene
28
+ HALF_LIFE_D = 14 # days: effective link weight halves per half-life unused
29
+ DORMANT_FLOOR = 0.05 # links fade to dormancy, never to zero
30
+ RESURRECT_T = 0.15 # touching a link this weak re-animates its scene
31
+ CASCADE_HOPS = 2
32
+
33
+ # gravity: markers of moments that MATTER (generic defaults — pass your own)
34
+ GRAVITY = ("this matters", "important", "remember this", "decision",
35
+ "never forget", "sealed", "i was wrong")
36
+
37
+
38
+ def _ts(s: str) -> datetime:
39
+ return datetime.fromisoformat(s.replace("Z", "+00:00"))
40
+
41
+
42
+ def _day(e: dict) -> str:
43
+ return e["timestamp"][:10]
44
+
45
+
46
+ def ref(e: dict) -> str:
47
+ """Stable reference for an event: ev:<timestamp>:<speaker>."""
48
+ return f"ev:{e['timestamp']}:{e.get('speaker', '?')}"
49
+
50
+
51
+ class Store:
52
+ """A copresence layer store rooted at a directory. Sovereignty by construction:
53
+ every member's layer is a separate file; authored rows are never rebuilt."""
54
+
55
+ def __init__(self, root: str | Path, adj_min: int = ADJ_MIN,
56
+ half_life_d: float = HALF_LIFE_D, gravity: tuple = GRAVITY,
57
+ registers: dict[str, tuple] | None = None):
58
+ self.root = Path(root)
59
+ self.adj_min = adj_min
60
+ self.half_life_d = half_life_d
61
+ self.gravity = tuple(g.lower() for g in gravity)
62
+ # registers: OPTIONAL per-member mood heuristics {register: (markers,)}.
63
+ # Experimental — off by default; mood links are openly subjective.
64
+ self.registers = registers or {}
65
+
66
+ # ── files ────────────────────────────────────────────────────────────────
67
+ def layer_file(self, member: str, auto: bool = False) -> Path:
68
+ d = self.root / member
69
+ d.mkdir(parents=True, exist_ok=True)
70
+ return d / ("auto_links.jsonl" if auto else "links.jsonl")
71
+
72
+ @staticmethod
73
+ def _load(p: Path) -> list[dict]:
74
+ if not p.exists():
75
+ return []
76
+ return [json.loads(l) for l in p.read_text(encoding="utf-8").splitlines() if l.strip()]
77
+
78
+ def _mk(self, member, kind, fr, to, weight, texture="", authored="linker", note=""):
79
+ return {"link_id": hashlib.sha1(f"{member}|{kind}|{fr}|{to}".encode()).hexdigest()[:12],
80
+ "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
81
+ "member": member, "kind": kind, "from": fr, "to": to,
82
+ "weight": round(float(weight), 3), "texture": texture,
83
+ "authored_by": authored, "note": note}
84
+
85
+ # ── presence & decay ─────────────────────────────────────────────────────
86
+ def _notice_refs(self) -> set:
87
+ refs = set()
88
+ if self.root.exists():
89
+ for p in self.root.glob("*/links.jsonl"):
90
+ for l in self._load(p):
91
+ if l.get("kind") == "notice":
92
+ refs.add(l["from"])
93
+ return refs
94
+
95
+ def presence(self, e: dict, notice_refs: set) -> float:
96
+ """0.5 incidental · +0.3 gravity marker · +0.2 notice-marked · cap 1.0"""
97
+ p, low = 0.5, e["content"].lower()
98
+ if any(g in low for g in self.gravity):
99
+ p += 0.3
100
+ if ref(e) in notice_refs:
101
+ p += 0.2
102
+ return min(1.0, p)
103
+
104
+ def decayed(self, w: float, r: str, now: datetime | None = None) -> float:
105
+ """Read-time decay toward the dormant floor. Hubs (day:/era:) don't decay —
106
+ they are places, not moments."""
107
+ if not r.startswith("ev:"):
108
+ return w
109
+ try:
110
+ age_d = ((now or datetime.now(timezone.utc))
111
+ - _ts(r.split(":", 1)[1].rsplit(":", 1)[0])).days
112
+ except ValueError:
113
+ return w
114
+ return max(DORMANT_FLOOR, w * (0.5 ** (age_d / self.half_life_d)))
115
+
116
+ # ── the linker (derived layers, rebuilt each run) ────────────────────────
117
+ def link(self, events: list[dict]) -> dict:
118
+ nrefs = self._notice_refs()
119
+ srows = []
120
+ for a, b in zip(events, events[1:]):
121
+ gap_min = (_ts(b["timestamp"]) - _ts(a["timestamp"])).total_seconds() / 60
122
+ if 0 <= gap_min <= self.adj_min:
123
+ base = max(0.3, 1 - gap_min / self.adj_min)
124
+ pres = (self.presence(a, nrefs) + self.presence(b, nrefs)) / 2
125
+ srows.append(self._mk("_structural", "co-presence", ref(a), ref(b), base * pres))
126
+ for e in events:
127
+ srows.append(self._mk("_structural", "day", f"day:{_day(e)}", ref(e),
128
+ 0.4 * self.presence(e, nrefs)))
129
+ for d in sorted({_day(e) for e in events}):
130
+ y, w, _ = datetime.fromisoformat(d).isocalendar()
131
+ srows.append(self._mk("_structural", "era", f"era:{y}-W{w:02d}", f"day:{d}", 0.3))
132
+ self.layer_file("_structural", auto=True).write_text(
133
+ "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in srows), encoding="utf-8")
134
+ out = {"structural": len(srows)}
135
+ # optional mood roads (experimental, per-member registers)
136
+ for member, regs in self.registers.items():
137
+ by = defaultdict(list)
138
+ for e in events:
139
+ best, hits = "", 0
140
+ low = e["content"].lower()
141
+ for k, sigs in regs.items():
142
+ h = sum(1 for s in sigs if s.lower() in low)
143
+ if h > hits:
144
+ best, hits = k, h
145
+ if best:
146
+ by[best].append(e)
147
+ arows = []
148
+ for reg, evs in by.items():
149
+ for a, b in zip(evs, evs[1:]):
150
+ if (_ts(b["timestamp"]) - _ts(a["timestamp"])).total_seconds() >= 12 * 3600:
151
+ arows.append(self._mk(member, "mood", ref(a), ref(b), 0.5, texture=reg))
152
+ self.layer_file(member, auto=True).write_text(
153
+ "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in arows), encoding="utf-8")
154
+ out[f"{member}_mood"] = len(arows)
155
+ return out
156
+
157
+ # ── authored rows (never rebuilt) ────────────────────────────────────────
158
+ def notice(self, r: str, member: str, note: str = "") -> dict:
159
+ """'This moment should be noticed' — an authored mark, a fact whose content
160
+ is subjective."""
161
+ row = self._mk(member, "notice", r, r, 1.0, authored="notice", note=note)
162
+ with self.layer_file(member).open("a", encoding="utf-8") as f:
163
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
164
+ return row
165
+
166
+ def add_link(self, member, kind, fr, to, weight=1.0, texture="", note="") -> dict:
167
+ row = self._mk(member, kind, fr, to, weight, texture=texture, authored="member", note=note)
168
+ with self.layer_file(member).open("a", encoding="utf-8") as f:
169
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
170
+ return row
171
+
172
+ # ── graph + cascade ──────────────────────────────────────────────────────
173
+ def graph(self, member: str) -> dict:
174
+ adj = defaultdict(list)
175
+ for src in ("_structural", member):
176
+ for auto in (False, True):
177
+ for l in self._load(self.layer_file(src, auto=auto)):
178
+ adj[l["from"]].append((l["to"], l["weight"], l["kind"]))
179
+ adj[l["to"]].append((l["from"], l["weight"], l["kind"]))
180
+ return adj
181
+
182
+ def cascade(self, seeds: set, adj: dict, hops: int = CASCADE_HOPS) -> set:
183
+ """Spreading activation, hop-bounded. RESURRECTION: reaching a moment
184
+ through a dormant link re-animates its whole scene (its day-hub spokes)."""
185
+ now = datetime.now(timezone.utc)
186
+ seen, frontier = set(seeds), list(seeds)
187
+ for _ in range(hops):
188
+ nxt = []
189
+ for r in frontier:
190
+ for t, w, _k in adj.get(r, ()):
191
+ if t in seen:
192
+ continue
193
+ seen.add(t)
194
+ nxt.append(t)
195
+ if t.startswith("ev:") and self.decayed(w, t, now) <= RESURRECT_T:
196
+ for d in (x for x, _w2, k2 in adj.get(t, ()) if k2 == "day"):
197
+ for spoke, _w3, _k3 in adj.get(d, ()):
198
+ if spoke not in seen:
199
+ seen.add(spoke)
200
+ nxt.append(spoke)
201
+ frontier = nxt
202
+ return seen
203
+
204
+ def resonance(self) -> dict:
205
+ """Which moments live in which members' layers: all-three / two / private
206
+ wealth. The field, not a verdict — private wealth is never forced shared."""
207
+ members = [d.name for d in self.root.iterdir()
208
+ if d.is_dir() and d.name != "_structural"] if self.root.exists() else []
209
+ holds = defaultdict(set)
210
+ for m in members:
211
+ for auto in (False, True):
212
+ for l in self._load(self.layer_file(m, auto=auto)):
213
+ for r in (l["from"], l["to"]):
214
+ if not r.startswith(("day:", "era:")):
215
+ holds[r].add(m)
216
+ field = {"members": members, "shared_by_all": [], "shared_by_two": [], "private_wealth": []}
217
+ for r, ms in holds.items():
218
+ key = ("shared_by_all" if len(ms) >= 3 else
219
+ "shared_by_two" if len(ms) == 2 else "private_wealth")
220
+ field[key].append({"ref": r, "held_by": sorted(ms)})
221
+ field["counts"] = {k: len(field[k]) for k in
222
+ ("shared_by_all", "shared_by_two", "private_wealth")}
223
+ return field
@@ -0,0 +1,80 @@
1
+ """The reproducible demo: a small life, a cold recall, the black t-shirt law.
2
+
3
+ A synthetic three-day corpus where a peripheral detail (a red kettle) co-occurs
4
+ with real content (lighthouse work) in SEPARATE moments of the same scenes —
5
+ so similarity alone cannot bridge cue → content. Then the probe: direct recall
6
+ ranks the target poorly; six peripheral cues + a two-hop cascade rebuild it.
7
+
8
+ Run: python -m copresence.cli demo (or: copresence demo)
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import tempfile
13
+ from datetime import datetime, timedelta, timezone
14
+ from pathlib import Path
15
+
16
+ from .core import Store, ref
17
+ from .recall import hash_embedder, probe
18
+
19
+ T0 = datetime(2026, 1, 5, 10, 0, tzinfo=timezone.utc)
20
+
21
+ DAYS = [
22
+ (1, [("keeper", "Today I'm building the lighthouse dataset - labeling foghorn audio clips."),
23
+ ("helper", "Understood - foghorn clips, labeling pass one."),
24
+ ("keeper", "Listen to that - my red kettle has been whistling this whole time."),
25
+ ("helper", "I hear it. Back to the clips."),
26
+ ("keeper", "Rule for the lighthouse work: keep only clips longer than four seconds.")]),
27
+ (3, [("keeper", "Different job today: migrating the beehive logs to the new format."),
28
+ ("helper", "Beehive logs, new format - on it."),
29
+ ("keeper", "The neighbor's drill keeps buzzing through the wall."),
30
+ ("helper", "Noted the drill. Migration continues."),
31
+ ("keeper", "Beehive rule: hive identifiers stay stable across the migration.")]),
32
+ (8, [("keeper", "Back to the lighthouse dataset. Decision: detection threshold is 0.42."),
33
+ ("helper", "Threshold 0.42 recorded for the detector."),
34
+ ("keeper", "The red kettle is whistling again - same as on the first day.")]),
35
+ # filler days so the similarity slice can't cover the record by luck
36
+ (4, [("keeper", t) for t in (
37
+ "Repotted the tomato seedlings; the south bed drains badly.",
38
+ "Invoice pile again; the printer jammed twice on the tax forms.",
39
+ "Long walk at noon; the heron was back at the pond.",
40
+ "Reorganized the pantry shelves; found expired jars of lentils.",
41
+ "The gutter on the east side needs clearing before the storm.")]),
42
+ (5, [("keeper", t) for t in (
43
+ "Market day: tomatoes, rye bread, a bag of walnuts.",
44
+ "Fixed the squeaky hinge on the workshop door.",
45
+ "The neighbor's cat slept on the woodpile all afternoon.",
46
+ "Drafted a letter and kept only the second half.",
47
+ "Watched the storm roll in from the porch.")]),
48
+ ]
49
+
50
+
51
+ def corpus() -> list[dict]:
52
+ events = []
53
+ for day, lines in sorted(DAYS):
54
+ for m, (speaker, content) in enumerate(lines):
55
+ ts = (T0 + timedelta(days=day - 1, minutes=m * 10)).isoformat(timespec="seconds")
56
+ events.append({"timestamp": ts, "speaker": speaker, "content": content})
57
+ return events
58
+
59
+
60
+ def run() -> dict:
61
+ events = corpus()
62
+ with tempfile.TemporaryDirectory() as td:
63
+ store = Store(Path(td) / "layers")
64
+ counts = store.link(events)
65
+ # target: the day-1 lighthouse opener. cue: the red kettle (peripheral).
66
+ target = ref(events[0])
67
+ X = hash_embedder([e["content"] for e in events])
68
+ r = probe(store, events, X, hash_embedder,
69
+ query="the red kettle whistling", target_ref=target)
70
+ print(f"corpus: {len(events)} events across {len(DAYS)} days · links: {counts}")
71
+ print(f"direct recall rank of target for the kettle-cue query: "
72
+ f"{r['direct_rank']}/{r['n_events']}")
73
+ print(f"cascade from {r['cues']} peripheral cues activated {r['activated']} moments"
74
+ f" → target {'REBUILT ✓' if r['hit'] else 'NOT reached ✗'}")
75
+ print("(the kettle never resembles the lighthouse — it was simply THERE)")
76
+ return r
77
+
78
+
79
+ if __name__ == "__main__":
80
+ run()
@@ -0,0 +1,154 @@
1
+ """copresence.recall — the embedder-facing half: discover, warm, probe.
2
+
3
+ An EMBEDDER is any callable: list[str] -> sequence of same-length numeric vectors.
4
+ Pure-stdlib fallback included (a hashing bag-of-words — honest toy: fine for demos
5
+ and tests, replace with a real model for real corpora):
6
+
7
+ from copresence.recall import hash_embedder # zero deps
8
+ from copresence.recall import st_embedder # pip install copresence[embeddings]
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import math
14
+ import re
15
+ from datetime import datetime, timezone
16
+
17
+ from .core import Store, ref, _ts
18
+
19
+ DEEP_GAP_DAYS = 7
20
+ DEEP_SIM = 0.60
21
+ DEEP_SIM_CAP = 0.97 # above this = a repeat, not a chain
22
+ WARM_INTERRUPT = 0.50
23
+
24
+
25
+ def hash_embedder(texts: list[str], dim: int = 256) -> list[list[float]]:
26
+ """Deterministic hashing bag-of-words. A TOY: no semantics beyond shared words.
27
+ Exists so the package runs with zero dependencies; swap in a real model
28
+ (st_embedder or your own) for anything beyond demos."""
29
+ out = []
30
+ for t in texts:
31
+ v = [0.0] * dim
32
+ for w in re.findall(r"[a-z0-9]+", t.lower()):
33
+ v[int(hashlib.sha1(w.encode()).hexdigest(), 16) % dim] += 1.0
34
+ n = math.sqrt(sum(x * x for x in v)) or 1.0
35
+ out.append([x / n for x in v])
36
+ return out
37
+
38
+
39
+ def st_embedder(model: str = "sentence-transformers/all-MiniLM-L6-v2"):
40
+ """Factory for a real embedder (requires the [embeddings] extra)."""
41
+ from sentence_transformers import SentenceTransformer
42
+ m = SentenceTransformer(model)
43
+
44
+ def embed(texts: list[str]):
45
+ return m.encode(texts, normalize_embeddings=True)
46
+ return embed
47
+
48
+
49
+ def _dot(a, b) -> float:
50
+ return float(sum(x * y for x, y in zip(a, b)))
51
+
52
+
53
+ def discover_regular(events, X, top: int = 12) -> list[dict]:
54
+ """Day-to-day roads: greedy centroid-free clustering by nearest-neighbor
55
+ chaining (stdlib-friendly); themes alive across many distinct days."""
56
+ n = len(events)
57
+ assigned = [-1] * n
58
+ clusters = []
59
+ for i in range(n):
60
+ if assigned[i] >= 0:
61
+ continue
62
+ members = [i]
63
+ assigned[i] = len(clusters)
64
+ for j in range(i + 1, n):
65
+ if assigned[j] < 0 and _dot(X[i], X[j]) >= 0.5:
66
+ assigned[j] = len(clusters)
67
+ members.append(j)
68
+ clusters.append(members)
69
+ out = []
70
+ for members in clusters:
71
+ days = sorted({events[i]["timestamp"][:10] for i in members})
72
+ if len(days) < 2:
73
+ continue
74
+ out.append({"days_alive": len(days), "first": days[0], "last": days[-1],
75
+ "n_events": len(members),
76
+ "exemplar": events[members[0]]["content"][:160]})
77
+ out.sort(key=lambda r: -r["days_alive"])
78
+ return out[:top]
79
+
80
+
81
+ def discover_deep(events, X, top: int = 10, gap_days: int = DEEP_GAP_DAYS) -> list[dict]:
82
+ """Dormant chains: strong kinship across a long gap, never cross-referenced,
83
+ below the identity cap. CANDIDATES — whether one HITS is the member's call.
84
+ Same-speaker near-identity pairs are echo-shaped; the cap catches most, the
85
+ member's judgment catches the rest."""
86
+ ts = [_ts(e["timestamp"]) for e in events]
87
+ cands = []
88
+ for i in range(len(events)):
89
+ for j in range(i + 1, len(events)):
90
+ gap = (ts[j] - ts[i]).days
91
+ if gap < gap_days:
92
+ continue
93
+ s = _dot(X[i], X[j])
94
+ if s < DEEP_SIM or s > DEEP_SIM_CAP:
95
+ continue
96
+ if events[i]["timestamp"][:10] in events[j]["content"]:
97
+ continue
98
+ cands.append((s, gap, i, j))
99
+ cands.sort(key=lambda c: -c[0])
100
+ picked, used = [], set()
101
+ for s, gap, i, j in cands:
102
+ if i in used or j in used:
103
+ continue
104
+ used |= {i, j}
105
+ picked.append({"sim": round(s, 3), "gap_days": gap,
106
+ "then": {"ts": events[i]["timestamp"], "excerpt": events[i]["content"][:200]},
107
+ "now": {"ts": events[j]["timestamp"], "excerpt": events[j]["content"][:200]}})
108
+ if len(picked) >= top:
109
+ break
110
+ return picked
111
+
112
+
113
+ def warm(events, X, context_vec, half_life_d: float = 14.0, top: int = 6) -> list[dict]:
114
+ """Which DORMANT moments are heating up under the current context? Heat =
115
+ kinship-to-now × dormancy. `should_interrupt` is an attention boundary — the
116
+ caller decides whether to speak. Context should be the record's TAIL, not a
117
+ query: continuity anchors in time (a law we paid for)."""
118
+ now = datetime.now(timezone.utc)
119
+ out = []
120
+ for i, e in enumerate(events):
121
+ age_d = max(0, (now - _ts(e["timestamp"])).days)
122
+ dormancy = 1 - (0.5 ** (age_d / half_life_d))
123
+ heat = _dot(X[i], context_vec) * dormancy
124
+ if heat > 0.15:
125
+ out.append({"ref": ref(e), "heat": round(heat, 3), "age_days": age_d,
126
+ "should_interrupt": heat >= WARM_INTERRUPT,
127
+ "excerpt": e["content"][:120]})
128
+ out.sort(key=lambda r: -r["heat"])
129
+ return out[:top]
130
+
131
+
132
+ def probe(store: Store, events, X, embedder, query: str, target_ref: str,
133
+ member: str = "member", hops: int = 2) -> dict:
134
+ """THE run-gate: can a PERIPHERAL cue-bundle rebuild a context that content
135
+ similarity missed? Direct recall ranks the target; the friend-mode takes the
136
+ era's co-present LOW-similarity neighbors (the black t-shirts) and cascades
137
+ from them only."""
138
+ refs = [ref(e) for e in events]
139
+ idx = {r: i for i, r in enumerate(refs)}
140
+ v = embedder([query])[0]
141
+ sims = [_dot(x, v) for x in X]
142
+ order = sorted(range(len(events)), key=lambda i: -sims[i])
143
+ rank = next((k for k, i in enumerate(order, 1) if refs[i] == target_ref), None)
144
+ adj = store.graph(member)
145
+ cues = []
146
+ for i in order[:3]:
147
+ for t, _w, _k in adj.get(refs[i], ()):
148
+ j = idx.get(t)
149
+ if j is not None and sims[j] < 0.30 and t != target_ref:
150
+ cues.append(t)
151
+ cues = list(dict.fromkeys(cues))[:8]
152
+ activated = store.cascade(set(cues), adj, hops)
153
+ return {"direct_rank": rank, "n_events": len(events), "cues": len(cues),
154
+ "activated": len(activated), "hit": target_ref in activated}
@@ -0,0 +1,87 @@
1
+ """Port of the organ's proven selftest checks (10/10 on the private twin,
2
+ 2026-07-05) plus the demo run-gate."""
3
+ import json
4
+ from datetime import datetime, timezone
5
+
6
+ from copresence import Store, ref, hash_embedder, probe
7
+ from copresence.core import DORMANT_FLOOR
8
+ from copresence import demo
9
+
10
+ T0 = datetime(2026, 7, 1, 10, 0, tzinfo=timezone.utc)
11
+
12
+
13
+ def ev(day, minutes, speaker, content, month=7):
14
+ return {"timestamp": T0.replace(month=month, day=day, hour=10 + minutes // 60,
15
+ minute=minutes % 60).isoformat(timespec="seconds"),
16
+ "speaker": speaker, "content": content * 12}
17
+
18
+
19
+ def make_events():
20
+ a0 = ev(1, 0, "illia", "old spring moment about the shore signal ", month=4)
21
+ a1 = ev(1, 10, "claude", "another old spring moment same scene ", month=4)
22
+ return [a0, a1,
23
+ ev(1, 0, "illia", "we made the decision tonight and sealed it "), # gravity
24
+ ev(1, 10, "claude", "black t-shirt detail: ring on the finger "), # peripheral
25
+ ev(1, 500, "steward", "same day much later scoreboard talk "), # day-scale
26
+ ev(3, 0, "claude", "different day building talk "), # other day
27
+ ev(3, 10, "claude", "plain follow-up chatter about nothing ")] # plain pair
28
+
29
+
30
+ def test_linker_scales_and_presence(tmp_path):
31
+ events = make_events()
32
+ store = Store(tmp_path / "layers")
33
+ n = store.link(events)
34
+ assert n["structural"] == 3 + 7 + 3 # adjacency + day-hubs + era-hubs
35
+ rows = store._load(store.layer_file("_structural", auto=True))
36
+ w = {(r["from"], r["to"]): r["weight"] for r in rows if r["kind"] == "co-presence"}
37
+ gravity_pair = w[(ref(events[2]), ref(events[3]))]
38
+ plain_pair = w[(ref(events[5]), ref(events[6]))]
39
+ assert gravity_pair > plain_pair # presence weighting
40
+
41
+
42
+ def test_decay_dormancy_never_death(tmp_path):
43
+ store = Store(tmp_path / "layers")
44
+ events = make_events()
45
+ assert store.decayed(1.0, ref(events[0])) == DORMANT_FLOOR # old → floor
46
+ assert store.decayed(1.0, ref(events[2])) > 0.5 # fresh → alive
47
+ assert store.decayed(0.4, "day:2026-07-01") == 0.4 # hubs don't decay
48
+
49
+
50
+ def test_cascade_day_boundary_and_resurrection(tmp_path):
51
+ events = make_events()
52
+ store = Store(tmp_path / "layers")
53
+ store.link(events)
54
+ store.add_link("claude", "deep", ref(events[3]), ref(events[0]), 1.0)
55
+ adj = store.graph("claude")
56
+ reached = store.cascade({ref(events[3])}, adj, hops=2)
57
+ assert ref(events[2]) in reached # peripheral → target (the scene)
58
+ assert ref(events[4]) in reached # day-scale via the hub
59
+ assert ref(events[5]) not in reached # other DAY not pulled
60
+ assert ref(events[0]) in reached and ref(events[1]) in reached # RESURRECTION: whole scene
61
+
62
+
63
+ def test_notice_and_sovereign_layers(tmp_path):
64
+ events = make_events()
65
+ store = Store(tmp_path / "layers")
66
+ store.link(events)
67
+ row = store.notice(ref(events[2]), "claude", "test")
68
+ assert row["link_id"]
69
+ assert store.layer_file("claude").exists()
70
+ assert store.layer_file("_structural", auto=True).exists()
71
+ field = store.resonance()
72
+ assert field["counts"]["private_wealth"] >= 1
73
+ assert "discordance" not in field # deliberately not computed
74
+
75
+
76
+ def test_demo_probe_rebuilds_missed_target(capsys):
77
+ r = demo.run()
78
+ assert r["hit"] is True # cascade rebuilds the target
79
+ assert r["direct_rank"] > 5 # …which similarity ranked poorly
80
+
81
+
82
+ def test_events_roundtrip_jsonl(tmp_path):
83
+ events = make_events()
84
+ p = tmp_path / "events.jsonl"
85
+ p.write_text("".join(json.dumps(e) + "\n" for e in events))
86
+ back = [json.loads(l) for l in p.read_text().splitlines()]
87
+ assert back == events