sessionmemory 0.6.0__tar.gz → 0.7.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.
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/PKG-INFO +17 -13
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/README.md +15 -11
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/pyproject.toml +11 -11
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/pyproject.toml.orig +11 -11
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/new.py +20 -1
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/search.py +16 -5
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/bootstrap.py +1 -1
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/fieldindex.py +35 -11
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/inject.py +11 -4
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/paths.py +9 -1
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/__init__.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/cli.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/__init__.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/_common.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/delete.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/doctor.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/export.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/init.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/inject.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/log.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/project.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/commands/reindex.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/__init__.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/atomic.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/backlog.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/config.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/doctor.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/embed.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/export.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/field.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/frontmatter.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/gitinfo.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/ids.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/log.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/registry.py +0 -0
- {sessionmemory-0.6.0 → sessionmemory-0.7.0}/src/sessionmemory/lib/resolve.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: sessionmemory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Durable memory for coding agents, one folder of searchable pages per project.
|
|
5
5
|
Author: Nathaniel Landau
|
|
6
6
|
Author-email: Nathaniel Landau <github@natelandau.com>
|
|
7
|
-
Requires-Dist: fastembed>=0.8.
|
|
7
|
+
Requires-Dist: fastembed>=0.8.1
|
|
8
8
|
Requires-Dist: nclutils>=3.4.4
|
|
9
9
|
Requires-Dist: pyyaml>=6.0.3
|
|
10
10
|
Requires-Dist: sqlite-vec>=0.1.9
|
|
@@ -159,22 +159,19 @@ The CLI does two things. It finds pages by meaning, and it creates pages. Readin
|
|
|
159
159
|
editing a page is a job for your editor or your agent's own tools.
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
sessionmemory search "
|
|
162
|
+
sessionmemory search "stripe event delivered twice" --cwd .
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
```
|
|
166
166
|
~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
|
|
167
167
|
Stripe retries a webhook for 72 hours, so the handler must be idempotent
|
|
168
168
|
Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat.
|
|
169
|
-
|
|
170
|
-
~/repos/my-vault/projects/invoice-api/learnings/the-nightly-reconciliation-job-must-start-after-the-02-00-bank-feed.md
|
|
171
|
-
The nightly reconciliation job must start after the 02:00 bank feed
|
|
172
|
-
The bank feed lands at 02:00 UTC; a reconciliation run before it reports every open invoice as unpaid.
|
|
173
169
|
```
|
|
174
170
|
|
|
175
171
|
A result is a path, a title, and a summary. A paraphrase finds the page, because search
|
|
176
|
-
ranks by meaning and not by words in common. A
|
|
177
|
-
|
|
172
|
+
ranks by meaning and not by words in common. A hit has to stand out from the rest of the
|
|
173
|
+
project's pages, so a query that nothing answers returns no results rather than the
|
|
174
|
+
nearest pages. Pass `--read` to print every hit in full.
|
|
178
175
|
|
|
179
176
|
```bash
|
|
180
177
|
sessionmemory new learning \
|
|
@@ -205,14 +202,17 @@ project holding four learnings, one spec, one plan, and two open backlog items:
|
|
|
205
202
|
Durable memory for this project lives in a vault of markdown pages. Nothing below is
|
|
206
203
|
loaded for you: the titles are what the vault holds, and each is one `sessionmemory search`
|
|
207
204
|
away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
|
|
208
|
-
`specs
|
|
205
|
+
`specs/`, `runbooks/`, and `backlog.md`, which are ordinary files you Read and Edit.
|
|
209
206
|
`sessionmemory project --json` prints every path.
|
|
210
207
|
|
|
211
208
|
- Before assuming nothing was written down, search: `sessionmemory search "<words>"`
|
|
212
209
|
prints each hit's path, title, and summary, and `--read` prints every hit's whole
|
|
213
|
-
page in one call.
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
page in one call. Search with a few distinctive words, such as names, identifiers,
|
|
211
|
+
or error text, rather than a sentence. No hits means nothing is recorded, not that
|
|
212
|
+
the query needs loosening.
|
|
213
|
+
- Past sessions, one page each: `sessionmemory search "<words>" --logs`. Search them
|
|
214
|
+
for why something was decided, what happened the last time an area changed, or
|
|
215
|
+
whether a fix was already tried.
|
|
216
216
|
- Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
|
|
217
217
|
(feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
|
|
218
218
|
`- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
|
|
@@ -225,6 +225,10 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
|
|
|
225
225
|
directory and read any spec whose name matches, so a decision already made is not
|
|
226
226
|
made again. `sessionmemory new spec --title "..." --cwd .` creates one and prints its
|
|
227
227
|
path. Edit it directly after that.
|
|
228
|
+
- Runbooks: `runbooks/` holds step-by-step procedures to follow by hand, such as
|
|
229
|
+
configuring a service, rotating a credential, or recovering from an outage.
|
|
230
|
+
Whenever you write one, create it with `sessionmemory new runbook --title "..." --cwd .`,
|
|
231
|
+
which prints its path, never in another folder or the repository.
|
|
228
232
|
- Learnings are captured at session end, not by you mid-session. When the user asks
|
|
229
233
|
to keep one now: `sessionmemory new learning --title "..." --summary "..." --cwd .`
|
|
230
234
|
creates the page and prints the path to write prose into. Title and summary state
|
|
@@ -144,22 +144,19 @@ The CLI does two things. It finds pages by meaning, and it creates pages. Readin
|
|
|
144
144
|
editing a page is a job for your editor or your agent's own tools.
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
sessionmemory search "
|
|
147
|
+
sessionmemory search "stripe event delivered twice" --cwd .
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
```
|
|
151
151
|
~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
|
|
152
152
|
Stripe retries a webhook for 72 hours, so the handler must be idempotent
|
|
153
153
|
Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat.
|
|
154
|
-
|
|
155
|
-
~/repos/my-vault/projects/invoice-api/learnings/the-nightly-reconciliation-job-must-start-after-the-02-00-bank-feed.md
|
|
156
|
-
The nightly reconciliation job must start after the 02:00 bank feed
|
|
157
|
-
The bank feed lands at 02:00 UTC; a reconciliation run before it reports every open invoice as unpaid.
|
|
158
154
|
```
|
|
159
155
|
|
|
160
156
|
A result is a path, a title, and a summary. A paraphrase finds the page, because search
|
|
161
|
-
ranks by meaning and not by words in common. A
|
|
162
|
-
|
|
157
|
+
ranks by meaning and not by words in common. A hit has to stand out from the rest of the
|
|
158
|
+
project's pages, so a query that nothing answers returns no results rather than the
|
|
159
|
+
nearest pages. Pass `--read` to print every hit in full.
|
|
163
160
|
|
|
164
161
|
```bash
|
|
165
162
|
sessionmemory new learning \
|
|
@@ -190,14 +187,17 @@ project holding four learnings, one spec, one plan, and two open backlog items:
|
|
|
190
187
|
Durable memory for this project lives in a vault of markdown pages. Nothing below is
|
|
191
188
|
loaded for you: the titles are what the vault holds, and each is one `sessionmemory search`
|
|
192
189
|
away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
|
|
193
|
-
`specs
|
|
190
|
+
`specs/`, `runbooks/`, and `backlog.md`, which are ordinary files you Read and Edit.
|
|
194
191
|
`sessionmemory project --json` prints every path.
|
|
195
192
|
|
|
196
193
|
- Before assuming nothing was written down, search: `sessionmemory search "<words>"`
|
|
197
194
|
prints each hit's path, title, and summary, and `--read` prints every hit's whole
|
|
198
|
-
page in one call.
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
page in one call. Search with a few distinctive words, such as names, identifiers,
|
|
196
|
+
or error text, rather than a sentence. No hits means nothing is recorded, not that
|
|
197
|
+
the query needs loosening.
|
|
198
|
+
- Past sessions, one page each: `sessionmemory search "<words>" --logs`. Search them
|
|
199
|
+
for why something was decided, what happened the last time an area changed, or
|
|
200
|
+
whether a fix was already tried.
|
|
201
201
|
- Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
|
|
202
202
|
(feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
|
|
203
203
|
`- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
|
|
@@ -210,6 +210,10 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
|
|
|
210
210
|
directory and read any spec whose name matches, so a decision already made is not
|
|
211
211
|
made again. `sessionmemory new spec --title "..." --cwd .` creates one and prints its
|
|
212
212
|
path. Edit it directly after that.
|
|
213
|
+
- Runbooks: `runbooks/` holds step-by-step procedures to follow by hand, such as
|
|
214
|
+
configuring a service, rotating a credential, or recovering from an outage.
|
|
215
|
+
Whenever you write one, create it with `sessionmemory new runbook --title "..." --cwd .`,
|
|
216
|
+
which prints its path, never in another folder or the repository.
|
|
213
217
|
- Learnings are captured at session end, not by you mid-session. When the user asks
|
|
214
218
|
to keep one now: `sessionmemory new learning --title "..." --summary "..." --cwd .`
|
|
215
219
|
creates the page and prints the path to write prose into. Title and summary state
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
dependencies = [
|
|
3
|
-
"fastembed>=0.8.
|
|
3
|
+
"fastembed>=0.8.1",
|
|
4
4
|
"nclutils>=3.4.4",
|
|
5
5
|
"pyyaml>=6.0.3",
|
|
6
6
|
"sqlite-vec>=0.1.9",
|
|
@@ -11,7 +11,7 @@ description = "Durable memory for coding agents, one folder of searchable pages
|
|
|
11
11
|
name = "sessionmemory"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.13,<3.15"
|
|
14
|
-
version = "0.
|
|
14
|
+
version = "0.7.0"
|
|
15
15
|
|
|
16
16
|
[[project.authors]]
|
|
17
17
|
name = "Nathaniel Landau"
|
|
@@ -22,22 +22,22 @@ sessionmemory = "sessionmemory.cli:main"
|
|
|
22
22
|
|
|
23
23
|
[dependency-groups]
|
|
24
24
|
dev = [
|
|
25
|
-
"commitizen>=4.
|
|
26
|
-
"coverage>=7.16.
|
|
27
|
-
"duty>=1.
|
|
28
|
-
"prek>=0.5.
|
|
25
|
+
"commitizen>=4.19.0",
|
|
26
|
+
"coverage>=7.16.2",
|
|
27
|
+
"duty>=1.10.0",
|
|
28
|
+
"prek>=0.5.4",
|
|
29
29
|
"pytest-clarity>=1.0.1",
|
|
30
30
|
"pytest-cov>=7.1.0",
|
|
31
31
|
"pytest-devtools>=1.3.0",
|
|
32
|
-
"pytest-mock>=3.
|
|
32
|
+
"pytest-mock>=3.16.0",
|
|
33
33
|
"pytest-xdist>=3.8.0",
|
|
34
34
|
"pytest>=9.1.1",
|
|
35
35
|
"rich>=15.0.0",
|
|
36
|
-
"ruff>=0.16.
|
|
36
|
+
"ruff>=0.16.10",
|
|
37
37
|
"shellcheck-py>=0.11.0.1",
|
|
38
|
-
"ty>=0.0.
|
|
39
|
-
"types-pyyaml>=6.0.12.
|
|
40
|
-
"typos>=1.50.
|
|
38
|
+
"ty>=0.0.84",
|
|
39
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
40
|
+
"typos>=1.50.3",
|
|
41
41
|
"yamllint>=1.38.0",
|
|
42
42
|
]
|
|
43
43
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
authors = [{ name = "Nathaniel Landau", email = "github@natelandau.com" }]
|
|
3
3
|
dependencies = [
|
|
4
|
-
"fastembed>=0.8.
|
|
4
|
+
"fastembed>=0.8.1",
|
|
5
5
|
"nclutils>=3.4.4",
|
|
6
6
|
"pyyaml>=6.0.3",
|
|
7
7
|
"sqlite-vec>=0.1.9",
|
|
@@ -12,29 +12,29 @@
|
|
|
12
12
|
name = "sessionmemory"
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.13,<3.15"
|
|
15
|
-
version = "0.
|
|
15
|
+
version = "0.7.0"
|
|
16
16
|
|
|
17
17
|
[project.scripts]
|
|
18
18
|
sessionmemory = "sessionmemory.cli:main"
|
|
19
19
|
|
|
20
20
|
[dependency-groups]
|
|
21
21
|
dev = [
|
|
22
|
-
"commitizen>=4.
|
|
23
|
-
"coverage>=7.16.
|
|
24
|
-
"duty>=1.
|
|
25
|
-
"prek>=0.5.
|
|
22
|
+
"commitizen>=4.19.0",
|
|
23
|
+
"coverage>=7.16.2",
|
|
24
|
+
"duty>=1.10.0",
|
|
25
|
+
"prek>=0.5.4",
|
|
26
26
|
"pytest-clarity>=1.0.1",
|
|
27
27
|
"pytest-cov>=7.1.0",
|
|
28
28
|
"pytest-devtools>=1.3.0",
|
|
29
|
-
"pytest-mock>=3.
|
|
29
|
+
"pytest-mock>=3.16.0",
|
|
30
30
|
"pytest-xdist>=3.8.0",
|
|
31
31
|
"pytest>=9.1.1",
|
|
32
32
|
"rich>=15.0.0",
|
|
33
|
-
"ruff>=0.16.
|
|
33
|
+
"ruff>=0.16.10",
|
|
34
34
|
"shellcheck-py>=0.11.0.1",
|
|
35
|
-
"ty>=0.0.
|
|
36
|
-
"types-pyyaml>=6.0.12.
|
|
37
|
-
"typos>=1.50.
|
|
35
|
+
"ty>=0.0.84",
|
|
36
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
37
|
+
"typos>=1.50.3",
|
|
38
38
|
"yamllint>=1.38.0",
|
|
39
39
|
]
|
|
40
40
|
|
|
@@ -18,7 +18,9 @@ from sessionmemory.commands._common import (
|
|
|
18
18
|
from sessionmemory.lib import backlog, field, paths
|
|
19
19
|
from sessionmemory.lib.config import now, today
|
|
20
20
|
|
|
21
|
-
app = typer.Typer(
|
|
21
|
+
app = typer.Typer(
|
|
22
|
+
no_args_is_help=True, help="Create a learning, spec, plan, runbook, or backlog item."
|
|
23
|
+
)
|
|
22
24
|
|
|
23
25
|
# Typer lists an Enum's members in --help and rejects anything else at parse time, so the
|
|
24
26
|
# allowed sets are spelled once, in lib/backlog, and mirrored here as choices.
|
|
@@ -112,6 +114,23 @@ def new_plan(
|
|
|
112
114
|
)
|
|
113
115
|
|
|
114
116
|
|
|
117
|
+
@app.command("runbook")
|
|
118
|
+
def new_runbook(
|
|
119
|
+
title: str = TITLE,
|
|
120
|
+
body: str = BODY,
|
|
121
|
+
body_file: Path | None = BODY_FILE,
|
|
122
|
+
cwd: Path | None = CWD,
|
|
123
|
+
*,
|
|
124
|
+
as_json: bool = JSON,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Create a runbook for this project."""
|
|
127
|
+
vault = require_vault()
|
|
128
|
+
slug = require_project(vault, cwd)
|
|
129
|
+
_new_document(
|
|
130
|
+
paths.runbooks_dir(vault, slug), title, resolve_body(body, body_file), as_json=as_json
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
115
134
|
@app.command("backlog", help="Add one open item to this project's backlog.md.")
|
|
116
135
|
def new_backlog(
|
|
117
136
|
kind: Kind = KIND,
|
|
@@ -27,17 +27,25 @@ MAX_DISTANCE = typer.Option(
|
|
|
27
27
|
max=2.0,
|
|
28
28
|
help="Farthest cosine distance that still counts as a hit.",
|
|
29
29
|
)
|
|
30
|
+
MIN_MARGIN = typer.Option(
|
|
31
|
+
fieldindex.DEFAULT_MIN_MARGIN,
|
|
32
|
+
"--min-margin",
|
|
33
|
+
min=0.0,
|
|
34
|
+
max=2.0,
|
|
35
|
+
help="How much nearer than the field's median page a hit must sit. 0 turns this off.",
|
|
36
|
+
)
|
|
30
37
|
READ = typer.Option(False, "--read", help="Print each hit's whole file under its path.") # noqa: FBT003
|
|
31
38
|
CWD = typer.Option(None, "--cwd", help="Directory to resolve the project from.")
|
|
32
39
|
JSON = typer.Option(False, "--json", help="Emit JSON instead of prose.") # noqa: FBT003
|
|
33
40
|
|
|
34
41
|
|
|
35
|
-
def search_command(
|
|
42
|
+
def search_command( # noqa: PLR0913
|
|
36
43
|
query: str = QUERY,
|
|
37
44
|
*,
|
|
38
45
|
logs: bool = LOGS,
|
|
39
46
|
limit: int = LIMIT,
|
|
40
47
|
max_distance: float = MAX_DISTANCE,
|
|
48
|
+
min_margin: float = MIN_MARGIN,
|
|
41
49
|
read: bool = READ,
|
|
42
50
|
cwd: Path | None = CWD,
|
|
43
51
|
as_json: bool = JSON,
|
|
@@ -49,7 +57,12 @@ def search_command(
|
|
|
49
57
|
slug = require_project(vault, cwd)
|
|
50
58
|
directory = paths.logs_dir(vault, slug) if logs else paths.learnings_dir(vault, slug)
|
|
51
59
|
hits = fieldindex.search(
|
|
52
|
-
directory,
|
|
60
|
+
directory,
|
|
61
|
+
build_embedder(),
|
|
62
|
+
query,
|
|
63
|
+
limit=limit,
|
|
64
|
+
max_distance=max_distance,
|
|
65
|
+
min_margin=min_margin,
|
|
53
66
|
)
|
|
54
67
|
|
|
55
68
|
if as_json:
|
|
@@ -67,9 +80,7 @@ def search_command(
|
|
|
67
80
|
emit_json(payload)
|
|
68
81
|
return
|
|
69
82
|
if not hits:
|
|
70
|
-
pp.info(
|
|
71
|
-
f"no results within distance {max_distance}; raise --max-distance to see farther pages"
|
|
72
|
-
)
|
|
83
|
+
pp.info("no results: nothing recorded matches this query")
|
|
73
84
|
return
|
|
74
85
|
# A path, a title, a summary, and a page are all things a caller copies or parses,
|
|
75
86
|
# so nothing here may be styled.
|
|
@@ -46,7 +46,7 @@ One folder per project under `projects/`. Inside each:
|
|
|
46
46
|
|
|
47
47
|
- `learnings/` is a field: flat markdown pages, embedded and searchable.
|
|
48
48
|
- `logs/` is a second field, one page per session, searched on request.
|
|
49
|
-
- `specs/`, `plans/`, and `backlog.md` are plain files, never indexed.
|
|
49
|
+
- `specs/`, `plans/`, `runbooks/`, and `backlog.md` are plain files, never indexed.
|
|
50
50
|
- `backlog.md` is the list of open items for the project, one line each, sized by
|
|
51
51
|
effort and grouped by commit type.
|
|
52
52
|
|
|
@@ -14,6 +14,7 @@ import datetime
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import json
|
|
16
16
|
import sqlite3
|
|
17
|
+
import statistics
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from typing import TYPE_CHECKING
|
|
19
20
|
|
|
@@ -26,11 +27,22 @@ if TYPE_CHECKING:
|
|
|
26
27
|
|
|
27
28
|
from sessionmemory.lib.embed import Embedder
|
|
28
29
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# sits at 0.45 or beyond. It matches the reference implementation's default for the model.
|
|
30
|
+
# The reference implementation's default cutoff for nomic-embed-text-v1.5. On its own it
|
|
31
|
+
# admits most unrelated queries, so it is only a ceiling and the margin below decides.
|
|
32
32
|
DEFAULT_MAX_DISTANCE = 0.45
|
|
33
33
|
|
|
34
|
+
# Measured with nomic-embed-text-v1.5 on 120 labeled queries across four projects' learnings
|
|
35
|
+
# and logs: a page that answers the query sits at least this much nearer than the field's
|
|
36
|
+
# median page, and the nearest page to an unrelated query does not. The rule is relative
|
|
37
|
+
# because phrasing a query as a question lowers every distance at once, and because a log,
|
|
38
|
+
# which summarizes a whole session, sits near every query about its project.
|
|
39
|
+
DEFAULT_MIN_MARGIN = 0.11
|
|
40
|
+
|
|
41
|
+
# A median over fewer pages than this is noise, so a small field measures against the
|
|
42
|
+
# median background seen across the labeled queries instead.
|
|
43
|
+
MIN_BACKGROUND_PAGES = 8
|
|
44
|
+
FALLBACK_BACKGROUND = 0.49
|
|
45
|
+
|
|
34
46
|
_SCHEMA = """
|
|
35
47
|
CREATE TABLE IF NOT EXISTS pages (
|
|
36
48
|
filename TEXT PRIMARY KEY,
|
|
@@ -165,11 +177,14 @@ def search(
|
|
|
165
177
|
*,
|
|
166
178
|
limit: int,
|
|
167
179
|
max_distance: float = DEFAULT_MAX_DISTANCE,
|
|
180
|
+
min_margin: float = DEFAULT_MIN_MARGIN,
|
|
168
181
|
) -> list[Hit]:
|
|
169
|
-
"""Return the pages
|
|
182
|
+
"""Return the pages that stand out as nearest to `query`, nearest first, refreshing the index first.
|
|
170
183
|
|
|
171
|
-
A
|
|
172
|
-
|
|
184
|
+
A hit sits within `max_distance` and at least `min_margin` nearer than the field's
|
|
185
|
+
median page. A cutoff rather than a bare top-k, so a query nothing answers returns
|
|
186
|
+
nothing instead of the nearest pages dressed up as hits. A `min_margin` of 0 leaves
|
|
187
|
+
only the absolute cutoff.
|
|
173
188
|
"""
|
|
174
189
|
if not field_dir.is_dir():
|
|
175
190
|
return []
|
|
@@ -177,16 +192,25 @@ def search(
|
|
|
177
192
|
try:
|
|
178
193
|
_refresh(conn, field_dir, embedder)
|
|
179
194
|
rows = conn.execute(
|
|
180
|
-
"SELECT
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
" WHERE distance <= ? ORDER BY distance LIMIT ?",
|
|
184
|
-
(sqlite_vec.serialize_float32(embedder.encode_query(query)), max_distance, limit),
|
|
195
|
+
"SELECT filename, frontmatter, vec_distance_cosine(embedding, ?) AS distance"
|
|
196
|
+
" FROM pages ORDER BY distance",
|
|
197
|
+
(sqlite_vec.serialize_float32(embedder.encode_query(query)),),
|
|
185
198
|
).fetchall()
|
|
186
199
|
finally:
|
|
187
200
|
conn.close()
|
|
201
|
+
ceiling = max_distance
|
|
202
|
+
if min_margin > 0:
|
|
203
|
+
distances = [float(row["distance"]) for row in rows]
|
|
204
|
+
background = (
|
|
205
|
+
statistics.median(distances)
|
|
206
|
+
if len(distances) >= MIN_BACKGROUND_PAGES
|
|
207
|
+
else FALLBACK_BACKGROUND
|
|
208
|
+
)
|
|
209
|
+
ceiling = min(ceiling, background - min_margin)
|
|
188
210
|
hits = []
|
|
189
211
|
for row in rows:
|
|
212
|
+
if float(row["distance"]) > ceiling or len(hits) == limit:
|
|
213
|
+
break
|
|
190
214
|
meta = json.loads(row["frontmatter"])
|
|
191
215
|
title = meta.get("title")
|
|
192
216
|
summary = meta.get("summary")
|
|
@@ -68,14 +68,17 @@ GUIDANCE = """## Using this vault
|
|
|
68
68
|
Durable memory for this project lives in a vault of markdown pages. Nothing below is
|
|
69
69
|
loaded for you: the titles are what the vault holds, and each is one `{command} search`
|
|
70
70
|
away. The project's folder has `learnings/` and `logs/`, searched by meaning, beside
|
|
71
|
-
`specs
|
|
71
|
+
`specs/`, `runbooks/`, and `backlog.md`, which are ordinary files you Read and Edit.
|
|
72
72
|
`{command} project --json` prints every path.
|
|
73
73
|
|
|
74
74
|
- Before assuming nothing was written down, search: `{command} search "<words>"`
|
|
75
75
|
prints each hit's path, title, and summary, and `--read` prints every hit's whole
|
|
76
|
-
page in one call.
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
page in one call. Search with a few distinctive words, such as names, identifiers,
|
|
77
|
+
or error text, rather than a sentence. No hits means nothing is recorded, not that
|
|
78
|
+
the query needs loosening.
|
|
79
|
+
- Past sessions, one page each: `{command} search "<words>" --logs`. Search them
|
|
80
|
+
for why something was decided, what happened the last time an area changed, or
|
|
81
|
+
whether a fix was already tried.
|
|
79
82
|
- Open work: read `backlog.md`. An item is one line under a `## <kind>` heading
|
|
80
83
|
(feat, fix, refactor, perf, docs, test, build, ci), sized S, M, or L:
|
|
81
84
|
`- [S] <imperative description> - <YYYY-MM-DD> [#topic]`. Add one with
|
|
@@ -88,6 +91,10 @@ away. The project's folder has `learnings/` and `logs/`, searched by meaning, be
|
|
|
88
91
|
directory and read any spec whose name matches, so a decision already made is not
|
|
89
92
|
made again. `{command} new spec --title "..." --cwd .` creates one and prints its
|
|
90
93
|
path. Edit it directly after that.
|
|
94
|
+
- Runbooks: `runbooks/` holds step-by-step procedures to follow by hand, such as
|
|
95
|
+
configuring a service, rotating a credential, or recovering from an outage.
|
|
96
|
+
Whenever you write one, create it with `{command} new runbook --title "..." --cwd .`,
|
|
97
|
+
which prints its path, never in another folder or the repository.
|
|
91
98
|
- Learnings are captured at session end, not by you mid-session. When the user asks
|
|
92
99
|
to keep one now: `{command} new learning --title "..." --summary "..." --cwd .`
|
|
93
100
|
creates the page and prints the path to write prose into. Title and summary state
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Where a project's files live inside the vault.
|
|
2
2
|
|
|
3
3
|
`learnings/` and `logs/` are fields: flat directories of pages, each with its own
|
|
4
|
-
index. `specs/`, `plans/`, and `backlog.md` sit beside them and are never
|
|
4
|
+
index. `specs/`, `plans/`, `runbooks/`, and `backlog.md` sit beside them and are never
|
|
5
|
+
indexed. A
|
|
5
6
|
project's files are found by its slug and nothing else; there is no global scope.
|
|
6
7
|
"""
|
|
7
8
|
|
|
@@ -19,6 +20,7 @@ LEARNINGS_DIR = "learnings"
|
|
|
19
20
|
LOGS_DIR = "logs"
|
|
20
21
|
SPECS_DIR = "specs"
|
|
21
22
|
PLANS_DIR = "plans"
|
|
23
|
+
RUNBOOKS_DIR = "runbooks"
|
|
22
24
|
BACKLOG_FILE = "backlog.md"
|
|
23
25
|
|
|
24
26
|
FIELD_DIRS: tuple[str, ...] = (LEARNINGS_DIR, LOGS_DIR)
|
|
@@ -49,6 +51,11 @@ def plans_dir(vault: Path, slug: str) -> Path:
|
|
|
49
51
|
return project_dir(vault, slug) / PLANS_DIR
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
def runbooks_dir(vault: Path, slug: str) -> Path:
|
|
55
|
+
"""Return the project's runbooks folder."""
|
|
56
|
+
return project_dir(vault, slug) / RUNBOOKS_DIR
|
|
57
|
+
|
|
58
|
+
|
|
52
59
|
def backlog_path(vault: Path, slug: str) -> Path:
|
|
53
60
|
"""Return the project's backlog checklist file."""
|
|
54
61
|
return project_dir(vault, slug) / BACKLOG_FILE
|
|
@@ -65,6 +72,7 @@ def project_paths(vault: Path, slug: str) -> dict[str, str]:
|
|
|
65
72
|
"logs": str(logs_dir(vault, slug)),
|
|
66
73
|
"specs": str(specs_dir(vault, slug)),
|
|
67
74
|
"plans": str(plans_dir(vault, slug)),
|
|
75
|
+
"runbooks": str(runbooks_dir(vault, slug)),
|
|
68
76
|
"backlog": str(backlog_path(vault, slug)),
|
|
69
77
|
}
|
|
70
78
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|