dsh-hypatia-auto-memory 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -16
- package/README.zh-CN.md +420 -0
- package/lib/client.js +31 -31
- package/package.json +5 -3
- package/skills/hypatia/SKILL.md +56 -9
- package/skills/hypatia-memory/SKILL.md +42 -15
- package/src/cascade.js +5 -1
- package/src/collector.js +27 -1
- package/src/config.js +5 -1
- package/src/consolidator.js +6 -3
- package/src/content-policy.js +5 -1
- package/src/hypatia-cli.js +72 -4
- package/src/hypatia-client.js +14 -0
- package/src/hypatia-mcp.js +92 -31
- package/src/writer.js +47 -18
package/lib/client.js
CHANGED
|
@@ -108,46 +108,46 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
document.head.appendChild(tag);
|
|
109
109
|
}
|
|
110
110
|
var SettingsCard_module_css_default = {
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"chevron": "YVNYya_chevron",
|
|
111
|
+
"limitsTitle": "YVNYya_limitsTitle",
|
|
112
|
+
"modelGroup": "YVNYya_modelGroup",
|
|
114
113
|
"model": "YVNYya_model",
|
|
114
|
+
"headText": "YVNYya_headText",
|
|
115
|
+
"card": "YVNYya_card",
|
|
116
|
+
"readOnly": "YVNYya_readOnly",
|
|
117
|
+
"cardOpen": "YVNYya_cardOpen",
|
|
118
|
+
"hint": "YVNYya_hint",
|
|
119
|
+
"group": "YVNYya_group",
|
|
120
|
+
"label": "YVNYya_label",
|
|
121
|
+
"providerName": "YVNYya_providerName",
|
|
115
122
|
"catalogError": "YVNYya_catalogError",
|
|
116
|
-
"
|
|
123
|
+
"chevronOpen": "YVNYya_chevronOpen",
|
|
124
|
+
"pending": "YVNYya_pending",
|
|
125
|
+
"unavailable": "YVNYya_unavailable",
|
|
117
126
|
"input": "YVNYya_input",
|
|
118
|
-
"header": "YVNYya_header",
|
|
119
|
-
"group": "YVNYya_group",
|
|
120
|
-
"advancedHint": "YVNYya_advancedHint",
|
|
121
|
-
"hint": "YVNYya_hint",
|
|
122
|
-
"modelSelection": "YVNYya_modelSelection",
|
|
123
|
-
"switch": "YVNYya_switch",
|
|
124
|
-
"readOnly": "YVNYya_readOnly",
|
|
125
|
-
"fieldHead": "YVNYya_fieldHead",
|
|
126
|
-
"switchOn": "YVNYya_switchOn",
|
|
127
127
|
"reset": "YVNYya_reset",
|
|
128
|
+
"description": "YVNYya_description",
|
|
128
129
|
"select": "YVNYya_select",
|
|
129
|
-
"card": "YVNYya_card",
|
|
130
|
-
"body": "YVNYya_body",
|
|
131
|
-
"cardOpen": "YVNYya_cardOpen",
|
|
132
|
-
"modelName": "YVNYya_modelName",
|
|
133
|
-
"limitsTitle": "YVNYya_limitsTitle",
|
|
134
|
-
"label": "YVNYya_label",
|
|
135
|
-
"pairedFields": "YVNYya_pairedFields",
|
|
136
|
-
"switchThumb": "YVNYya_switchThumb",
|
|
137
|
-
"status": "YVNYya_status",
|
|
138
|
-
"notice": "YVNYya_notice",
|
|
139
130
|
"field": "YVNYya_field",
|
|
140
|
-
"
|
|
141
|
-
"
|
|
131
|
+
"switchOn": "YVNYya_switchOn",
|
|
132
|
+
"advancedHint": "YVNYya_advancedHint",
|
|
142
133
|
"name": "YVNYya_name",
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"warning": "YVNYya_warning",
|
|
134
|
+
"status": "YVNYya_status",
|
|
135
|
+
"switchThumb": "YVNYya_switchThumb",
|
|
146
136
|
"route": "YVNYya_route",
|
|
147
|
-
"
|
|
137
|
+
"models": "YVNYya_models",
|
|
138
|
+
"groupTitle": "YVNYya_groupTitle",
|
|
139
|
+
"error": "YVNYya_error",
|
|
140
|
+
"modelSelection": "YVNYya_modelSelection",
|
|
148
141
|
"footer": "YVNYya_footer",
|
|
149
|
-
"
|
|
150
|
-
"
|
|
142
|
+
"pairedFields": "YVNYya_pairedFields",
|
|
143
|
+
"body": "YVNYya_body",
|
|
144
|
+
"modelName": "YVNYya_modelName",
|
|
145
|
+
"switch": "YVNYya_switch",
|
|
146
|
+
"chevron": "YVNYya_chevron",
|
|
147
|
+
"notice": "YVNYya_notice",
|
|
148
|
+
"header": "YVNYya_header",
|
|
149
|
+
"fieldHead": "YVNYya_fieldHead",
|
|
150
|
+
"warning": "YVNYya_warning"
|
|
151
151
|
};
|
|
152
152
|
//#endregion
|
|
153
153
|
//#region src/client/locales.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hypatia-auto-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Event-driven Hypatia memory for DSH: automatic conversation logging via session events, queued background consolidation (span summaries + work-unit extraction) with a dedicated model route, and recall injection — zero per-call model work in the main session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"node": "^22.19.0 || >=24.0.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
49
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.2-rc.1",
|
|
50
48
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
51
49
|
"zod": "^4.4.3"
|
|
52
50
|
},
|
|
53
51
|
"peerDependencies": {
|
|
54
52
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
53
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.2-rc.1",
|
|
55
55
|
"react": "^18.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
|
|
65
65
|
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.2-rc.1",
|
|
66
66
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.2-rc.1",
|
|
67
69
|
"@types/react": "^18.2.0",
|
|
68
70
|
"lightningcss": "^1.32.0",
|
|
69
71
|
"react": "^18.2.0",
|
package/skills/hypatia/SKILL.md
CHANGED
|
@@ -68,6 +68,38 @@ hypatia knowledge-create "prefer immutable" -d "always create new objects" --tag
|
|
|
68
68
|
hypatia knowledge-create "no mock DB" -d "never mock database in tests" --tags "taboo" --scopes "my-project,"
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
### Listing the scopes and tags already in use
|
|
72
|
+
|
|
73
|
+
A scope or tag you invent is not an error — the entry is stored, and every later
|
|
74
|
+
lookup by the value you meant misses it. Check before you write:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# What this shelf already uses, with how many entries carry each
|
|
78
|
+
hypatia scope list --count
|
|
79
|
+
hypatia tag list --count
|
|
80
|
+
|
|
81
|
+
# Exact values, when you are going to write one back
|
|
82
|
+
hypatia scope list --json # [{"value":"","entries":12},{"value":"my-project",…}]
|
|
83
|
+
|
|
84
|
+
# Confirm one spelling; exit 0 if in use, 1 if not
|
|
85
|
+
hypatia tag exists rule
|
|
86
|
+
hypatia scope exists my-project
|
|
87
|
+
hypatia scope exists "" # the global scope
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The plain listing prints the global scope as `(global)`. That is a label for the
|
|
91
|
+
terminal, not the value — the value is the empty string, and `--json` and
|
|
92
|
+
`exists` both give it back verbatim. Never copy `(global)` into `--scopes`.
|
|
93
|
+
|
|
94
|
+
Both cover knowledge and statements. An entry that declares no scopes at all is
|
|
95
|
+
not listed as global.
|
|
96
|
+
|
|
97
|
+
| User says | Command |
|
|
98
|
+
|---|---|
|
|
99
|
+
| "what scopes/projects are in here?" | `hypatia scope list --count` |
|
|
100
|
+
| "what tags exist?" / "which labels are used?" | `hypatia tag list --count` |
|
|
101
|
+
| "is there already a `rule` tag?" | `hypatia tag exists rule` |
|
|
102
|
+
|
|
71
103
|
## Knowledge CRUD
|
|
72
104
|
|
|
73
105
|
Knowledge entries are independent information points with a name, content, and tags.
|
|
@@ -75,14 +107,17 @@ Knowledge entries are independent information points with a name, content, and t
|
|
|
75
107
|
### Create
|
|
76
108
|
|
|
77
109
|
```
|
|
78
|
-
hypatia knowledge-create <name> -d "<data>" -t "<tag1,tag2>" --figures "archive://path/to/file"
|
|
110
|
+
hypatia knowledge-create <name> -d "<data>" -t "<tag1,tag2>" --figures "archive://path/to/file" [--no-embed]
|
|
79
111
|
```
|
|
80
112
|
|
|
113
|
+
`--no-embed` keeps the entry out of the vector index: it is still stored and still found by `search` and `query`, but it is never embedded, so it costs no model call on the way in and is not a `similar` result. Use it for raw session logs and other bulk that carries no distilled knowledge. A shelf can skip whole tags instead, with `embedding.skip_tags` in `shelf.toml`.
|
|
114
|
+
|
|
81
115
|
| User says | Command |
|
|
82
116
|
|---|---|
|
|
83
117
|
| "remember Rust as a systems programming language" | `hypatia knowledge-create "Rust" -d "systems programming language" -t "language,compiled"` |
|
|
84
118
|
| "save knowledge about Go with tags language and compiled" | `hypatia knowledge-create "Go" -d "" -t "language,compiled"` |
|
|
85
119
|
| "store that Python is a scripting language, tag it as dynamic" | `hypatia knowledge-create "Python" -d "scripting language" -t "dynamic"` |
|
|
120
|
+
| "log this turn but don't make it searchable by meaning" | `hypatia knowledge-create "msg-42" -d "<turn>" -t "message" --no-embed` |
|
|
86
121
|
|
|
87
122
|
### Read
|
|
88
123
|
|
|
@@ -97,9 +132,11 @@ hypatia knowledge-get <name>
|
|
|
97
132
|
### Update
|
|
98
133
|
|
|
99
134
|
```
|
|
100
|
-
hypatia knowledge-update <name> [-d "<data>"] [-t "<tags>"] [--synonyms "<csv>"] [--figures "<refs>"] [--scopes "<scopes>"]
|
|
135
|
+
hypatia knowledge-update <name> [-d "<data>"] [-t "<tags>"] [--synonyms "<csv>"] [--figures "<refs>"] [--scopes "<scopes>"] [--no-embed | --embed]
|
|
101
136
|
```
|
|
102
137
|
|
|
138
|
+
`--no-embed` takes the entry out of the vector index and discards the vector it had; `--embed` puts it back, and it is embedded on the next flush. The two are mutually exclusive. `--embed` does not override the shelf's `embedding.skip_tags`: an entry can ask for less indexing than its shelf, never more.
|
|
139
|
+
|
|
103
140
|
Only the fields you pass change. An omitted field keeps its stored value, and exactly `""` clears a field, such as `-t ""`; for tags, `","` or `" "` would store blank tags instead. `--scopes` replaces the stored scopes and is parsed as on create: end the list with a comma, such as `"q,"`, to include the global scope, or the entry drops out of global lookups. The entry keeps its `created_at`. Its old vector is discarded, and a new one is generated on the next flush, as after `knowledge-create`. Updating an entry that does not exist is an error. An update that changes nothing prints `Knowledge unchanged: <name>` and writes nothing.
|
|
104
141
|
|
|
105
142
|
| User says | Command |
|
|
@@ -129,9 +166,11 @@ When the user asks to store or remember information, **always create statements
|
|
|
129
166
|
**Pattern**: After creating a knowledge entry, identify entities mentioned in the content and create `$triple` relationships between them. At minimum, create one `is_a` statement for every new knowledge entry.
|
|
130
167
|
|
|
131
168
|
```
|
|
132
|
-
hypatia statement-create <head> <relation> <tail> -d "<data>"
|
|
169
|
+
hypatia statement-create <head> <relation> <tail> -d "<data>" [--no-embed]
|
|
133
170
|
```
|
|
134
171
|
|
|
172
|
+
`--no-embed` works as it does on `knowledge-create`: the triple is still stored and still traversable, but gets no vector. Two differences: statements carry no tags, so a shelf's `embedding.skip_tags` cannot reach them and bulk links must say `--no-embed` themselves; and there is no `statement-update`, so the choice is made once at creation.
|
|
173
|
+
|
|
135
174
|
### Triple Extraction Patterns
|
|
136
175
|
|
|
137
176
|
| User says | head | relation | tail |
|
|
@@ -341,19 +380,25 @@ hypatia search <query> [-c <catalog>] [--limit N] [--offset N]
|
|
|
341
380
|
Semantic search using embedding vectors. Finds entries with similar meaning, even when keywords don't match.
|
|
342
381
|
|
|
343
382
|
```
|
|
344
|
-
hypatia similar <query> [--limit N]
|
|
383
|
+
hypatia similar <query> [--limit N] [-t knowledge|statement|both] [--tags <a,b>] [--exclude-tags <a,b>] [--where '<JSE condition>']
|
|
345
384
|
```
|
|
346
385
|
|
|
386
|
+
`--tags` keeps entries carrying at least one of the tags, `--exclude-tags` drops entries carrying any of them, and `--where` takes a JSE condition such as `["$contains", "scopes", "project-a"]` (`$eq`, `$like`, `$contains`, `$and`/`$or`/`$not` and the other filters `$knowledge` takes, but not `$search`, `$similar` or `$k-hop`). They narrow the entries before ranking, so `--limit N` still returns N entries whenever that many qualify. `--exclude-tags message,summary,session` keeps the session-log layer from crowding out the knowledge distilled from it. With the default target `both` the filter applies to statements too, so a condition on `name` needs `-t knowledge`.
|
|
387
|
+
|
|
347
388
|
Requires an embedding model configured in `shelf.toml` (default: BAAI/bge-m3).
|
|
348
389
|
|
|
349
390
|
Entries are embedded in batches after they are written or updated. With a remote embedding API, `similar` can miss entries changed in the last minute; `search` finds them at once.
|
|
350
391
|
|
|
392
|
+
Entries written with `--no-embed`, and entries carrying a tag the shelf lists in `embedding.skip_tags`, are never embedded and so are never `similar` results. `search` and `query` still find them. Each entry keeps the answer it was written with, so after changing `skip_tags` run `hypatia backfill` once to settle what is already stored.
|
|
393
|
+
|
|
351
394
|
### Examples
|
|
352
395
|
|
|
353
396
|
| User says | Command |
|
|
354
397
|
|---|---|
|
|
355
398
|
| "find similar to distributed systems" | `hypatia similar "distributed systems"` |
|
|
356
399
|
| "semantic search for memory management" | `hypatia similar "memory management" --limit 5` |
|
|
400
|
+
| "what do we know about auth, not the chat logs" | `hypatia similar "auth" --exclude-tags message,summary,session` |
|
|
401
|
+
| "rules like this one for project-a" | `hypatia similar "<rule text>" -t knowledge --tags rule --where '["$contains", "scopes", "project-a"]'` |
|
|
357
402
|
|
|
358
403
|
## JSE Query Translation
|
|
359
404
|
|
|
@@ -559,15 +604,17 @@ even read-only SQLite needs to create WAL/journal temp files next to the databas
|
|
|
559
604
|
go straight to the immutable read-only URI, which needs no journal and no directory writes:
|
|
560
605
|
|
|
561
606
|
```bash
|
|
562
|
-
sqlite3 "file
|
|
607
|
+
sqlite3 "file:<shelf-dir>/hypatia.sqlite?mode=ro&immutable=1" "SELECT count(*) FROM knowledge;"
|
|
563
608
|
```
|
|
564
609
|
|
|
565
610
|
- Use only for **reads** (SELECT). Never write through this path — `immutable=1` tells SQLite the
|
|
566
611
|
file never changes, so writes would corrupt state or be lost.
|
|
567
|
-
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
612
|
+
- First, get `<shelf-dir>` from `hypatia list`, which prints the directory each shelf is open at.
|
|
613
|
+
Do not assume `~/.hypatia/<shelf>/`: any shelf, `default` included, can be registered elsewhere.
|
|
614
|
+
If the CLI itself cannot run, read the paths from `~/.hypatia/shelves.json`.
|
|
615
|
+
- Then check the `-wal`/`-shm` siblings; if a WAL exists and is non-empty, `immutable=1` may miss
|
|
616
|
+
recent un-checkpointed rows — in that case prefer requesting wider sandbox permissions for a
|
|
617
|
+
normal read instead.
|
|
571
618
|
- Schema hints: tables include `knowledge` and `statement`; statement triples are stored in
|
|
572
619
|
`head` / `relation` / `tail` columns.
|
|
573
620
|
|
|
@@ -25,6 +25,11 @@ Do not do these by hand — you would only create duplicates:
|
|
|
25
25
|
- **Session seed.** Project and global entries tagged `rule` or `taboo` are
|
|
26
26
|
injected into your context at session start. Nothing else is pushed to you.
|
|
27
27
|
|
|
28
|
+
The log layer — `msg-*`, `session-*`, and the `belongTo` / `summary` edges —
|
|
29
|
+
is written **without vectors** (`--no-embed`) on a hypatia that has the flag.
|
|
30
|
+
It is stored and full-text searchable, but `similar` does not rank it. Entries
|
|
31
|
+
logged before that, or by an older hypatia, still carry vectors.
|
|
32
|
+
|
|
28
33
|
## Which shelf
|
|
29
34
|
|
|
30
35
|
The plugin writes to one hypatia shelf, chosen in its settings. When that is
|
|
@@ -47,7 +52,8 @@ costs a wrong answer that contradicts a decision already made.
|
|
|
47
52
|
### Semantic search — the default
|
|
48
53
|
|
|
49
54
|
```bash
|
|
50
|
-
hypatia similar "<what you actually want to know>" -t knowledge --limit
|
|
55
|
+
hypatia similar "<what you actually want to know>" -t knowledge --limit 5 \
|
|
56
|
+
--exclude-tags message,summary,session,hypatia-dream-run
|
|
51
57
|
```
|
|
52
58
|
|
|
53
59
|
Write the query as the *idea* you are looking for, not the user's words verbatim.
|
|
@@ -55,8 +61,11 @@ Rows come back as `{"name", "content" (object), "distance"}`; lower `distance` i
|
|
|
55
61
|
closer. If it exits non-zero with `model unavailable` / `no embedding provider`,
|
|
56
62
|
this shelf has no embedding model — fall back to `search` and carry on.
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
`--exclude-tags` leaves the operational layer out *before* ranking, so five rows
|
|
65
|
+
asked for are five rows of knowledge. Add `--where '["$contains","scopes","<PROJECT>"]'`
|
|
66
|
+
to stay inside this project. If the flag is rejected as an `unexpected argument`,
|
|
67
|
+
the hypatia is older than #35: drop it, ask for `--limit 20`, and discard the
|
|
68
|
+
operational rows yourself (see below).
|
|
60
69
|
|
|
61
70
|
### Keyword search — for exact identifiers
|
|
62
71
|
|
|
@@ -79,10 +88,21 @@ hypatia query '["$knowledge", ["$contains","tags","rule"],
|
|
|
79
88
|
hypatia query '["$statement", ["$k-hop", "<entry-name>", "$*", 2]]'
|
|
80
89
|
```
|
|
81
90
|
|
|
82
|
-
`<PROJECT>` is the
|
|
83
|
-
|
|
84
|
-
surrounding whitespace
|
|
85
|
-
|
|
91
|
+
`<PROJECT>` is the scope the plugin writes this workspace under: the basename
|
|
92
|
+
of its git root (of the workspace itself outside git), with commas turned into
|
|
93
|
+
`_`, surrounding whitespace dropped, and the filesystem root written `/`. You
|
|
94
|
+
do not have to derive it — the plugin has already logged this session under it,
|
|
95
|
+
so it is on the shelf:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
hypatia scope list --count # every scope in use, with how many entries carry it
|
|
99
|
+
hypatia scope exists "<PROJECT>" # exit 0 if in use, 1 if not
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The plain listing prints the global scope as `(global)`; that is a label, the
|
|
103
|
+
value is `""`. Use `--json` when you will write a value back. Both need hypatia
|
|
104
|
+
#30; an older one answers `unrecognized subcommand`, and then the rule above is
|
|
105
|
+
all there is.
|
|
86
106
|
|
|
87
107
|
### Always exclude the operational layer
|
|
88
108
|
|
|
@@ -92,13 +112,14 @@ name starts with `msg-`, `sum`, `session-`, or `hypatia-dream-run-`, and rows
|
|
|
92
112
|
tagged `message`, `session`, `summary`, or `hypatia-dream-run`. Read `wu-*`,
|
|
93
113
|
`rule`, `taboo`, and ordinary named entries.
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
That layer is most of the shelf — every message is logged — and it matches
|
|
116
|
+
well: a raw `msg-*` holds the very words of the conversation you are asking
|
|
117
|
+
about. Measured on a live shelf, 7 of the 10 nearest `similar` rows were
|
|
118
|
+
operational. `similar --exclude-tags` handles it; `search` has no such flag, and
|
|
119
|
+
neither can see an operational entry that lost its tags, so still check names.
|
|
120
|
+
Whenever you filter after the fact, ask for `--limit 20`, and if too few rows
|
|
121
|
+
survive, search again with a larger limit or a different wording before
|
|
122
|
+
concluding that nothing is stored.
|
|
102
123
|
|
|
103
124
|
Reach into `msg-*` only when the user asks what was literally said, and prefer
|
|
104
125
|
reaching it by walking `summary` statements down from a `sum-*` entry.
|
|
@@ -119,8 +140,14 @@ hypatia knowledge-create "<short-kebab-name>" \
|
|
|
119
140
|
--scopes "<PROJECT>"
|
|
120
141
|
```
|
|
121
142
|
|
|
143
|
+
- **Write the scope the plugin uses**, exactly. The session seed loads rules by
|
|
144
|
+
that spelling; a rule under `my-app` is invisible to a plugin that logs under
|
|
145
|
+
`my_app`. `hypatia scope list --count` shows it — the scope this project's
|
|
146
|
+
`msg-*` entries carry — and `hypatia scope exists "<PROJECT>"` confirms it
|
|
147
|
+
before you write.
|
|
122
148
|
- `--tags rule` for conventions to follow, `taboo` for never-do items,
|
|
123
|
-
`memory` for durable facts.
|
|
149
|
+
`memory` for durable facts. Beyond those three, reuse what `hypatia tag list`
|
|
150
|
+
already shows before inventing a label.
|
|
124
151
|
- **Scope syntax matters.** `--scopes "<PROJECT>"` is project-only.
|
|
125
152
|
`--scopes "<PROJECT>,"` — with a *trailing comma* — also marks it global.
|
|
126
153
|
`--scopes ""` writes no scope at all, which is not the same as global and will
|
package/src/cascade.js
CHANGED
|
@@ -179,8 +179,12 @@ export function createCascade({ cli, llm, selectRoute, getConfig, status }) {
|
|
|
179
179
|
// under a different name and the first entry keeps only its partial set.
|
|
180
180
|
// Nothing is lost — every member still ends up archived exactly once — but
|
|
181
181
|
// the grouping boundary is not the one the first attempt intended.
|
|
182
|
+
// The archive entry is embedded — it is distilled knowledge, and the entry
|
|
183
|
+
// point for drilling down. Its edges are not: they are for the traversal
|
|
184
|
+
// and for `$not-summaried`, and nobody looks for a `summary` edge by
|
|
185
|
+
// meaning (see writer.js `writeSummary`).
|
|
182
186
|
for (const member of members) {
|
|
183
|
-
await cli.statementCreate(name, 'summary', member, { scopes: [task.project] })
|
|
187
|
+
await cli.statementCreate(name, 'summary', member, { scopes: [task.project], embed: false })
|
|
184
188
|
}
|
|
185
189
|
status.info(`cascade: tier ${level} archived ${members.length} entries as ${name}`)
|
|
186
190
|
}
|
package/src/collector.js
CHANGED
|
@@ -59,6 +59,26 @@ const RECORDED_TYPES = new Set([
|
|
|
59
59
|
'tool/result',
|
|
60
60
|
])
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether an assistant/message event carries anything worth logging on its own.
|
|
64
|
+
*
|
|
65
|
+
* A turn that only issued tool calls (no text, no reasoning, no interruption
|
|
66
|
+
* marker) adds nothing to the conversation record — the tool outcomes already
|
|
67
|
+
* live in the session log if needed. Skipping these avoids msg-* entries whose
|
|
68
|
+
* entire body would be `(tool calls only)` or an empty placeholder.
|
|
69
|
+
*
|
|
70
|
+
* @param {any} event
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function hasAssistantContent(event) {
|
|
74
|
+
const blocks = event.data?.message?.content ?? []
|
|
75
|
+
for (const block of blocks) {
|
|
76
|
+
if (block.type === 'text' && String(block.text ?? '').trim() !== '') return true
|
|
77
|
+
if (block.type === 'image') return true
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
/**
|
|
63
83
|
* Whether this event becomes one `msg-*` entry.
|
|
64
84
|
*
|
|
@@ -66,11 +86,17 @@ const RECORDED_TYPES = new Set([
|
|
|
66
86
|
* output) are excluded: they are not things the human said, and re-logging them
|
|
67
87
|
* would let memory amplify into itself.
|
|
68
88
|
*
|
|
89
|
+
* Assistant turns whose only content is tool calls are also excluded: they have
|
|
90
|
+
* no substantive message to remember.
|
|
91
|
+
*
|
|
69
92
|
* @param {any} event
|
|
70
93
|
* @returns {boolean}
|
|
71
94
|
*/
|
|
72
95
|
export function isLoggableMessage(event) {
|
|
73
|
-
if (event?.type === 'assistant/message')
|
|
96
|
+
if (event?.type === 'assistant/message') {
|
|
97
|
+
const interrupted = event.data?.interrupted === true
|
|
98
|
+
return interrupted || hasAssistantContent(event)
|
|
99
|
+
}
|
|
74
100
|
return event?.type === 'user/message' && event.data?.source?.kind === 'user'
|
|
75
101
|
}
|
|
76
102
|
|
package/src/config.js
CHANGED
|
@@ -35,7 +35,11 @@ export const DEFAULTS = {
|
|
|
35
35
|
// User messages were never capped, so a pasted log went into hypatia whole.
|
|
36
36
|
// Generous, because what the user typed is the part most worth keeping.
|
|
37
37
|
maxUserChars: 32000,
|
|
38
|
-
|
|
38
|
+
// Tool-call ledgers (name / count / success / duration) are intentionally
|
|
39
|
+
// off by default: they still reveal which tools were used and when, and the
|
|
40
|
+
// user has asked to avoid writing any tool-call-derived data to hypatia.
|
|
41
|
+
// Set this to true to restore the previous behavior.
|
|
42
|
+
toolLedger: false,
|
|
39
43
|
},
|
|
40
44
|
consolidation: {
|
|
41
45
|
enabled: true,
|
package/src/consolidator.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module dsh-hypatia-auto-memory/consolidator
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { absolutizeDates, blocksToText, eventDate,
|
|
17
|
+
import { absolutizeDates, blocksToText, eventDate, redactSecrets } from './content-policy.js'
|
|
18
18
|
import { countLoggableMessages, isCompactionReplacement, isLoggableMessage } from './collector.js'
|
|
19
19
|
import { messageName, summaryName } from './writer.js'
|
|
20
20
|
import { EMPTY_PROGRESS, advanceProgress } from './progress.js'
|
|
@@ -178,8 +178,11 @@ export function buildTranscript(events, config, now = new Date(), { before } = {
|
|
|
178
178
|
const callId = event.data?.message?.source?.callId
|
|
179
179
|
const name = toolNames.get(callId) ?? 'tool'
|
|
180
180
|
const ok = event.data?.error === undefined
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
// Tool outputs are intentionally kept out of the consolidation transcript.
|
|
182
|
+
// The model only sees that a tool was called and whether it succeeded;
|
|
183
|
+
// the actual result text never reaches the remote model or the summaries
|
|
184
|
+
// and work units derived from it.
|
|
185
|
+
entries.push({ seq: event.seq, line: `T: ${name} ${ok ? '✅' : '❌'}` })
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
|
package/src/content-policy.js
CHANGED
|
@@ -277,7 +277,11 @@ export function blocksToText(blocks) {
|
|
|
277
277
|
for (const block of blocks ?? []) {
|
|
278
278
|
if (block.type === 'text') parts.push(block.text)
|
|
279
279
|
else if (block.type === 'image') parts.push('[image]')
|
|
280
|
-
|
|
280
|
+
// Thinking / reasoning blocks are internal monologue, not something the
|
|
281
|
+
// assistant said to the user. Including them created `[reasoning]`
|
|
282
|
+
// placeholder entries with no substantive content; skip them so only the
|
|
283
|
+
// final text / image output reaches the log.
|
|
284
|
+
else if (block.type === 'thinking' || block.type === 'reasoning') continue
|
|
281
285
|
else if (block.type === 'tool-result') parts.push(blocksToText(block.content ?? []))
|
|
282
286
|
// A tool-call block is the model asking for a tool, not something it said.
|
|
283
287
|
// Rendered as `[tool-call]` it filled the content of half the stored
|
package/src/hypatia-cli.js
CHANGED
|
@@ -172,6 +172,56 @@ export function createHypatiaCli(ctx, config) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* What this binary can do beyond the baseline every call relies on.
|
|
177
|
+
*
|
|
178
|
+
* Read off `--help`, the one place clap states a flag's existence: an older
|
|
179
|
+
* binary answers an unknown flag with a usage error (exit 2) *instead of*
|
|
180
|
+
* running the command, so a write that guessed at `--no-embed` would fail
|
|
181
|
+
* outright on a binary from before hypatia #26, and a `similar` guessing at
|
|
182
|
+
* `--exclude-tags` on one from before #35. Both write commands are asked
|
|
183
|
+
* about `--no-embed` — they gained it together, but the answer is only worth
|
|
184
|
+
* having if it is true of each command it gates.
|
|
185
|
+
*
|
|
186
|
+
* @returns {Promise<{noEmbed: boolean, similarFilters: boolean}>}
|
|
187
|
+
*/
|
|
188
|
+
async function probeFeatures() {
|
|
189
|
+
const [knowledge, statement, similar] = await Promise.all([
|
|
190
|
+
run(['knowledge-create', '--help']),
|
|
191
|
+
run(['statement-create', '--help']),
|
|
192
|
+
run(['similar', '--help']),
|
|
193
|
+
])
|
|
194
|
+
for (const result of [knowledge, statement, similar]) {
|
|
195
|
+
if (result.exitCode !== 0) {
|
|
196
|
+
throw new HypatiaCliError(`hypatia --help exited ${result.exitCode}`, {
|
|
197
|
+
code: 'NONZERO_EXIT',
|
|
198
|
+
stderr: result.stderr.trim().slice(0, 500),
|
|
199
|
+
exitCode: result.exitCode,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
noEmbed: knowledge.stdout.includes('--no-embed') && statement.stdout.includes('--no-embed'),
|
|
205
|
+
similarFilters: similar.stdout.includes('--exclude-tags'),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The probe's answer, kept for the life of the client; a probe that failed is asked again. */
|
|
210
|
+
let featuresProbe
|
|
211
|
+
|
|
212
|
+
function features() {
|
|
213
|
+
if (featuresProbe === undefined) {
|
|
214
|
+
featuresProbe = probeFeatures().catch(() => {
|
|
215
|
+
// A binary that cannot even print its help fails the next real call
|
|
216
|
+
// with its own error; the answer here only decides which flags that
|
|
217
|
+
// call carries, and the conservative answer is none.
|
|
218
|
+
featuresProbe = undefined
|
|
219
|
+
return { noEmbed: false, similarFilters: false }
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
return featuresProbe
|
|
223
|
+
}
|
|
224
|
+
|
|
175
225
|
/**
|
|
176
226
|
* Whether stdout is hypatia's empty-result sentinel.
|
|
177
227
|
*
|
|
@@ -196,6 +246,7 @@ export function createHypatiaCli(ctx, config) {
|
|
|
196
246
|
return {
|
|
197
247
|
run,
|
|
198
248
|
runOk,
|
|
249
|
+
features,
|
|
199
250
|
|
|
200
251
|
/**
|
|
201
252
|
* Read one knowledge entry. Missing entries are a normal result, not an
|
|
@@ -211,7 +262,11 @@ export function createHypatiaCli(ctx, config) {
|
|
|
211
262
|
},
|
|
212
263
|
|
|
213
264
|
/**
|
|
214
|
-
*
|
|
265
|
+
* `embed: false` keeps the entry out of the vector index (hypatia #26); on
|
|
266
|
+
* a binary without the flag it is dropped, and the entry is embedded as it
|
|
267
|
+
* always was.
|
|
268
|
+
*
|
|
269
|
+
* @param {{data: string, tags?: string[], scopes?: string[], embed?: boolean, shelf?: string}} entry
|
|
215
270
|
* @returns {Promise<boolean>} true when this call created the entry.
|
|
216
271
|
*/
|
|
217
272
|
async knowledgeCreate(name, entry) {
|
|
@@ -222,11 +277,15 @@ export function createHypatiaCli(ctx, config) {
|
|
|
222
277
|
if (entry.data) argv.push(`--data=${fitDataArgument(entry.data)}`)
|
|
223
278
|
if (entry.tags && entry.tags.length > 0) argv.push('--tags', entry.tags.join(','))
|
|
224
279
|
if (entry.scopes && entry.scopes.length > 0) argv.push('--scopes', entry.scopes.join(','))
|
|
280
|
+
if (entry.embed === false && (await features()).noEmbed) argv.push('--no-embed')
|
|
225
281
|
return runCreate(argv)
|
|
226
282
|
},
|
|
227
283
|
|
|
228
284
|
/**
|
|
229
|
-
*
|
|
285
|
+
* `embed: false` as on `knowledgeCreate`. A statement has no update
|
|
286
|
+
* command, so the choice is made once, here.
|
|
287
|
+
*
|
|
288
|
+
* @param {{data?: string, scopes?: string[], embed?: boolean, shelf?: string}} [entry]
|
|
230
289
|
* @returns {Promise<boolean>} true when this call created the triple.
|
|
231
290
|
*/
|
|
232
291
|
async statementCreate(head, relation, tail, entry = {}) {
|
|
@@ -239,6 +298,7 @@ export function createHypatiaCli(ctx, config) {
|
|
|
239
298
|
// expose to clap's flag parsing.
|
|
240
299
|
if (entry.data) argv.push(`--data=${fitDataArgument(entry.data)}`)
|
|
241
300
|
if (entry.scopes && entry.scopes.length > 0) argv.push('--scopes', entry.scopes.join(','))
|
|
301
|
+
if (entry.embed === false && (await features()).noEmbed) argv.push('--no-embed')
|
|
242
302
|
return runCreate(argv)
|
|
243
303
|
},
|
|
244
304
|
|
|
@@ -258,10 +318,18 @@ export function createHypatiaCli(ctx, config) {
|
|
|
258
318
|
/**
|
|
259
319
|
* Vector-similarity search — the recall path's primary ranking tool.
|
|
260
320
|
* NOTE the flag is `-t/--target` (unlike keyword search's `-c`).
|
|
321
|
+
*
|
|
322
|
+
* `excludeTags` leaves entries carrying any of those tags out BEFORE the
|
|
323
|
+
* ranking (hypatia #35), so `limit` rows come back whenever that many
|
|
324
|
+
* qualify. A binary without the flag ranks everything; the caller reads
|
|
325
|
+
* `features()` to know whether it must over-fetch and filter instead.
|
|
326
|
+
*
|
|
261
327
|
* @returns {Promise<any[]>}
|
|
262
328
|
*/
|
|
263
|
-
async similar(query, { target = 'knowledge', limit = 5, shelf = shelfOf() } = {}) {
|
|
264
|
-
const
|
|
329
|
+
async similar(query, { target = 'knowledge', limit = 5, shelf = shelfOf(), excludeTags = [] } = {}) {
|
|
330
|
+
const argv = ['similar', query, '-t', target, '--limit', String(limit), '--shelf', shelf]
|
|
331
|
+
if (excludeTags.length > 0 && (await features()).similarFilters) argv.push('--exclude-tags', excludeTags.join(','))
|
|
332
|
+
const { stdout } = await runOk(argv)
|
|
265
333
|
if (isEmptyResult(stdout)) return []
|
|
266
334
|
const parsed = parseJson(stdout, 'similar')
|
|
267
335
|
return Array.isArray(parsed) ? parsed : []
|
package/src/hypatia-client.js
CHANGED
|
@@ -54,7 +54,21 @@ export function createHypatiaClient(ctx, config, { status, connection }) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Logged once, so the profile log says which flags this run's writes carry. */
|
|
58
|
+
let announced = false
|
|
59
|
+
async function features() {
|
|
60
|
+
const found = await via('features', [])
|
|
61
|
+
if (!announced) {
|
|
62
|
+
announced = true
|
|
63
|
+
const yes = (flag) => (flag ? 'yes' : 'no')
|
|
64
|
+
status.info?.(`hypatia over ${usesMcp() ? 'mcp' : 'cli'}: no-embed ${yes(found.noEmbed)}, similar filters ${yes(found.similarFilters)}`)
|
|
65
|
+
}
|
|
66
|
+
return found
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
return {
|
|
70
|
+
/** What the binary can do beyond the baseline (see hypatia-cli.js `features`). */
|
|
71
|
+
features,
|
|
58
72
|
knowledgeGet: (...args) => via('knowledgeGet', args),
|
|
59
73
|
knowledgeCreate: (...args) => via('knowledgeCreate', args),
|
|
60
74
|
statementCreate: (...args) => via('statementCreate', args),
|