@theokit/sdk 4.60.0 → 4.61.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/CHANGELOG.md +35 -0
- package/dist/{agent-26RMDG4U.cjs → agent-M53C3PAA.cjs} +8 -8
- package/dist/{agent-26RMDG4U.cjs.map → agent-M53C3PAA.cjs.map} +1 -1
- package/dist/{agent-6ABK2DPG.js → agent-P42GA6RM.js} +7 -7
- package/dist/{agent-6ABK2DPG.js.map → agent-P42GA6RM.js.map} +1 -1
- package/dist/{chunk-KKK3FZ4A.cjs → chunk-2DG7KW4L.cjs} +3 -3
- package/dist/{chunk-KKK3FZ4A.cjs.map → chunk-2DG7KW4L.cjs.map} +1 -1
- package/dist/chunk-2UCFUSPW.cjs +468 -0
- package/dist/chunk-2UCFUSPW.cjs.map +1 -0
- package/dist/{chunk-E5MWNLRX.js → chunk-532CSLYU.js} +3 -3
- package/dist/{chunk-E5MWNLRX.js.map → chunk-532CSLYU.js.map} +1 -1
- package/dist/{chunk-3HDTP4ZO.cjs → chunk-5IT6DUOO.cjs} +4 -4
- package/dist/{chunk-3HDTP4ZO.cjs.map → chunk-5IT6DUOO.cjs.map} +1 -1
- package/dist/{chunk-Y2KJ7MYO.cjs → chunk-6OBIWHDR.cjs} +14 -14
- package/dist/{chunk-Y2KJ7MYO.cjs.map → chunk-6OBIWHDR.cjs.map} +1 -1
- package/dist/{chunk-76XTUTKY.js → chunk-GQZKDGZM.js} +167 -13
- package/dist/chunk-GQZKDGZM.js.map +1 -0
- package/dist/{chunk-43GFJ5SD.cjs → chunk-P5LCASTC.cjs} +19 -4
- package/dist/chunk-P5LCASTC.cjs.map +1 -0
- package/dist/{chunk-7OXXYBHD.js → chunk-QEKI3YKI.js} +3 -3
- package/dist/{chunk-7OXXYBHD.js.map → chunk-QEKI3YKI.js.map} +1 -1
- package/dist/{chunk-23GPJGRR.cjs → chunk-R7YIVL3K.cjs} +217 -63
- package/dist/chunk-R7YIVL3K.cjs.map +1 -0
- package/dist/chunk-WCLDJSMY.js +456 -0
- package/dist/chunk-WCLDJSMY.js.map +1 -0
- package/dist/{chunk-WG7R5W6R.js → chunk-YEXA3PGR.js} +3 -3
- package/dist/{chunk-WG7R5W6R.js.map → chunk-YEXA3PGR.js.map} +1 -1
- package/dist/{chunk-IACR5LEM.js → chunk-YXGAW7BB.js} +17 -2
- package/dist/chunk-YXGAW7BB.js.map +1 -0
- package/dist/{compact-session-YHFQIXV5.cjs → compact-session-GJXJD73F.cjs} +11 -11
- package/dist/{compact-session-YHFQIXV5.cjs.map → compact-session-GJXJD73F.cjs.map} +1 -1
- package/dist/{compact-session-QGDNP45U.js → compact-session-TSMQOIHO.js} +3 -3
- package/dist/{compact-session-QGDNP45U.js.map → compact-session-TSMQOIHO.js.map} +1 -1
- package/dist/cron.cjs +7 -7
- package/dist/cron.js +6 -6
- package/dist/eval.cjs +6 -6
- package/dist/eval.js +5 -5
- package/dist/{index-manager-KP2JAXKN.js → index-manager-AHAYJ33H.js} +4 -4
- package/dist/{index-manager-KP2JAXKN.js.map → index-manager-AHAYJ33H.js.map} +1 -1
- package/dist/{index-manager-YUORPS4G.cjs → index-manager-RHPFVFSC.cjs} +5 -5
- package/dist/{index-manager-YUORPS4G.cjs.map → index-manager-RHPFVFSC.cjs.map} +1 -1
- package/dist/index.cjs +69 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +42 -9
- package/dist/index.js.map +1 -1
- package/dist/{inject-session-MLCJMYDG.cjs → inject-session-PPSO2IDD.cjs} +4 -4
- package/dist/{inject-session-MLCJMYDG.cjs.map → inject-session-PPSO2IDD.cjs.map} +1 -1
- package/dist/{inject-session-RBQZ45UM.js → inject-session-ZCQUB3IT.js} +3 -3
- package/dist/{inject-session-RBQZ45UM.js.map → inject-session-ZCQUB3IT.js.map} +1 -1
- package/dist/internal/memory/dreaming/phases.d.ts +21 -1
- package/dist/internal/memory/storage/index.cjs +10 -10
- package/dist/internal/memory/storage/index.js +1 -1
- package/dist/internal/memory/storage/memory-file.d.cts +41 -5
- package/dist/internal/memory/storage/memory-file.d.ts +41 -5
- package/dist/internal/memory/storage/threat-scan.d.cts +62 -0
- package/dist/internal/memory/storage/threat-scan.d.ts +62 -0
- package/dist/internal/memory/types.d.ts +28 -0
- package/dist/internal/runtime/memory/select-facts.d.ts +63 -0
- package/dist/internal/runtime/system-prompt/sources/memory-provider.d.ts +6 -1
- package/docs/error-codes.md +3 -2
- package/docs/harness-capability-map.md +9 -7
- package/docs/memory-decisions.md +207 -0
- package/package.json +1 -1
- package/dist/chunk-23GPJGRR.cjs.map +0 -1
- package/dist/chunk-43GFJ5SD.cjs.map +0 -1
- package/dist/chunk-76XTUTKY.js.map +0 -1
- package/dist/chunk-IACR5LEM.js.map +0 -1
- package/dist/chunk-JVERBPRD.js +0 -191
- package/dist/chunk-JVERBPRD.js.map +0 -1
- package/dist/chunk-NY25NBZB.cjs +0 -203
- package/dist/chunk-NY25NBZB.cjs.map +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Memory subsystem — decisions that are not obvious from the code
|
|
2
|
+
|
|
3
|
+
Five decisions that a reader would otherwise be right to call bugs. Each names where the behaviour
|
|
4
|
+
lives, so the next person to touch it can disagree on purpose rather than "fix" it by accident.
|
|
5
|
+
|
|
6
|
+
Written after a conformance audit against an external memory contract found that three of these
|
|
7
|
+
existed only in commit messages and one only in a conversation. A decision nobody can find is a
|
|
8
|
+
decision the next refactor deletes.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Running an agent creates `.theokit/`, even when memory goes to the Claude Code directory
|
|
13
|
+
|
|
14
|
+
**What happens.** Reading a Claude Code project creates nothing. Running an agent creates:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
.theokit/agents/registry.json on Agent.create
|
|
18
|
+
.theokit/memory/.index/memory.sqlite on the first send, when memory is enabled
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Why it looks wrong.** With `local.sessionDir` set, memory *facts* are written to
|
|
22
|
+
`<sessionDir>/projects/<encoded-cwd>/memory/` — where the CLI reads them. So a project that never
|
|
23
|
+
adopted this SDK ends up with a `.theokit/` directory anyway, which reads like the "write where the
|
|
24
|
+
CLI reads" promise leaking.
|
|
25
|
+
|
|
26
|
+
**Why it is right.** What lands in `.theokit/` is this SDK's own state, and neither piece has a
|
|
27
|
+
Claude Code shape to be written in:
|
|
28
|
+
|
|
29
|
+
- `agents/registry.json` is the live-agent address book (`internal/runtime/registry/agent-registry.ts`).
|
|
30
|
+
The CLI has no equivalent.
|
|
31
|
+
- `memory/.index/memory.sqlite` is the search index (`sdk-memory/internal/index/index-db.ts:84-91`).
|
|
32
|
+
**The CLI has no index format**, so there is no CLI location that could hold it. Putting it beside
|
|
33
|
+
the facts in the CLI's directory would put a binary artefact the CLI does not understand inside a
|
|
34
|
+
directory the CLI manages.
|
|
35
|
+
|
|
36
|
+
The facts — the thing a user recorded and would lose — go where the CLI reads. The index is derived
|
|
37
|
+
data that can be rebuilt from them.
|
|
38
|
+
|
|
39
|
+
**How to check it still holds:** `tests/claude-code-e2e-compat.test.ts`, the pair
|
|
40
|
+
`test_reading_a_cli_project_creates_nothing_in_it` /
|
|
41
|
+
`test_running_an_agent_creates_theokit_and_only_sdk_state_in_it`. The second exists so the first is
|
|
42
|
+
not vacuous — it asserts the directory contains *only* `agents` and `memory`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Four memory kinds, and a kind is never inferred
|
|
47
|
+
|
|
48
|
+
`internal/memory/types.ts:32` declares `user | feedback | project | reference`, validated at the
|
|
49
|
+
storage boundary (`internal/memory/storage/markdown-store.ts:195`, `invalid_memory_kind`).
|
|
50
|
+
|
|
51
|
+
**Known divergence.** An external contract this SDK was audited against specifies **nine** kinds in
|
|
52
|
+
three retention buckets (atomic / consolidatable / lifecycle-managed). This SDK has four.
|
|
53
|
+
|
|
54
|
+
**Why it has not been widened.** The nine exist to drive differentiated retention, and **this SDK has
|
|
55
|
+
no retention at all** — no TTL, no pruning, no decay (searched `ttl|prune|expire|retention` across
|
|
56
|
+
both packages; the only hits are the recall result cache, which expires *results*, not entries).
|
|
57
|
+
Nine names over a regime where nothing expires are nine names for one behaviour. Retention has to
|
|
58
|
+
exist before a vocabulary that differentiates it is worth anything.
|
|
59
|
+
|
|
60
|
+
> **One of this argument's two legs has since been removed, and the note stays so the argument
|
|
61
|
+
> cannot be defended with it later.** An earlier version of this section also said "and no buckets".
|
|
62
|
+
> That stopped being true in `a655ac4d`: `dreaming/phases.ts:48-49` now has `ATOMIC_KINDS` and
|
|
63
|
+
> `CONSOLIDATABLE_KINDS`, and `dedupPolicy` grades three levels by them. The buckets exist; what
|
|
64
|
+
> still does not exist is the retention they would govern.
|
|
65
|
+
|
|
66
|
+
**The stronger reason, found after the first one weakened: the four are not a subset, they are the
|
|
67
|
+
partner's vocabulary.** This store's format is shared with the Claude Code CLI, and every memory the
|
|
68
|
+
CLI has written on one developer machine — 688 files, 2026-08 — carries one of exactly these four:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
425 project 167 feedback 74 reference 7 user (15 with no type)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Zero others. Adding a fifth value means writing files whose `type` the interop partner has never
|
|
75
|
+
emitted. That is evidence about what it WRITES, not proof about what it ACCEPTS on read — but it
|
|
76
|
+
means there is no precedent, and the burden belongs to whoever adds one unilaterally.
|
|
77
|
+
|
|
78
|
+
**`failure_heuristic` is the one the retention argument does not cover, and the interop argument
|
|
79
|
+
does.** It changes *what is written* (a trigger→resolution pair) rather than how an entry expires, so
|
|
80
|
+
"no retention to differentiate" says nothing about it — and nothing has adopted it (`grep
|
|
81
|
+
failure_heuristic packages/sdk/src` → zero). It is not declined on retention grounds; it is held by
|
|
82
|
+
the same shared-format question as the other four, and unblocking it means establishing what the CLI
|
|
83
|
+
does with a `type` it does not emit.
|
|
84
|
+
|
|
85
|
+
**The part that is not negotiable:** a kind is never inferred (`types.ts:29`). A wrong kind is worse
|
|
86
|
+
than none, because it makes retention and recall confident about the wrong thing.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 3. Re-recording a fact overwrites it; there is no `Invalidated` state
|
|
91
|
+
|
|
92
|
+
`markdown-store.ts:204` writes `<slug>.md` through `replaceFileAtomic`, and `nextIndex`
|
|
93
|
+
(`:222-235`) keeps exactly one index line per name.
|
|
94
|
+
|
|
95
|
+
**Known divergence.** The same external contract requires a contradiction to produce an
|
|
96
|
+
`Invalidated` entry plus a supersession chain, not an overwrite.
|
|
97
|
+
|
|
98
|
+
**Why the index is right as it is.** The index is a map from memory to file. Two lines for one file
|
|
99
|
+
is a map that disagrees with itself, and the CLI reads that index.
|
|
100
|
+
|
|
101
|
+
**What the entry is NAMED by changed, and it is worth knowing why.** The slug and the index title
|
|
102
|
+
used to be the fact's whole sentence. They are now a short topic name — `MemoryFact` carries optional
|
|
103
|
+
`title` and `description` so a writer can author them, and derives them when it does not. The
|
|
104
|
+
derivation is mechanical on purpose and does not pretend to be authorship, the same rule this store
|
|
105
|
+
applies to `kind`.
|
|
106
|
+
|
|
107
|
+
**Two distinct facts that share a subject now coexist; they used to overwrite each other.** A topic
|
|
108
|
+
slug is a lossy summary, and lossy summaries collide: `"fact A"`, `"fact B"` and `"fact C"` all
|
|
109
|
+
derive `fact`. Naming by the whole sentence made collisions rare by accident; naming by subject made
|
|
110
|
+
them ordinary. `resolveName` (`markdown-store.ts:280`) settles it by the only thing that can — the
|
|
111
|
+
text: the same text keeps the same file and increments corroboration, different text takes
|
|
112
|
+
`topic-2`. Found by the golden `multiple appends each get a file`, not by review — the failure was
|
|
113
|
+
silent data loss, and nothing about the reasoning would have surfaced it.
|
|
114
|
+
|
|
115
|
+
So the section heading stays true for what it describes, re-recording the *same* fact, and the case
|
|
116
|
+
it never covered is now covered.
|
|
117
|
+
|
|
118
|
+
That change closed #446, where a passphrase the model had just refused to store was written into the
|
|
119
|
+
**filename** and the index line. The reason it works is that it is not a rule about secrets: a rule
|
|
120
|
+
about secrets has to recognise one, and `redactSecrets` had already demonstrated it does not
|
|
121
|
+
recognise `sirius-zzq417`. Naming the memory by its **subject** drops the tail of the sentence
|
|
122
|
+
whatever the tail happens to be. Closed by construction, not by detection — which is the only kind
|
|
123
|
+
of closure available when the dangerous input is indistinguishable from a safe one.
|
|
124
|
+
|
|
125
|
+
**Why this is still a gap.** "The index names the current entry" and "the store keeps the
|
|
126
|
+
supersession chain" are not in conflict — they are different files. The resolution, when it is
|
|
127
|
+
built, is the index pointing at the current entry while the entry file carries its own chain. What
|
|
128
|
+
exists today is only the first half.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 4. The session transcript is a DAG, not a linear log
|
|
133
|
+
|
|
134
|
+
`internal/persistence/session-transcript.ts:4-11,77-78` — records carry `uuid`/`parentUuid`, and
|
|
135
|
+
`appendCompactBoundary` starts a new root.
|
|
136
|
+
|
|
137
|
+
**Deliberate and forced.** The format IS the Claude Code record shape. Bidirectional CLI
|
|
138
|
+
compatibility is a product requirement, so a linear session model is not available to choose: it
|
|
139
|
+
would make transcripts this SDK writes unreadable by the CLI and vice versa.
|
|
140
|
+
|
|
141
|
+
Where an external contract prescribes a linear session model, this is a documented middle-ground —
|
|
142
|
+
the contract is deciding a question that a SDK with an imposed wire format does not get to answer.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 5. Recall is lexical by default; dense vectors only when a consumer asks for them
|
|
147
|
+
|
|
148
|
+
Two different things are called recall:
|
|
149
|
+
|
|
150
|
+
- **Default.** `internal/local-agent/local-agent-send.ts:257-272` reads the store, then ranks and
|
|
151
|
+
cuts it through `selectFactsForInjection` — lexical relevance against the user's message, fused
|
|
152
|
+
with recency, capped at 10 entries and a byte budget derived from 15,000 tokens. No embeddings on
|
|
153
|
+
this path at all (`runtime/memory/select-facts.ts:117-118`).
|
|
154
|
+
- **The `memory_search` tool.** Its index is NOT opt-in: `local-agent-memory.ts:82-98` opens
|
|
155
|
+
`IndexManager` whenever `memory.enabled` is true. The dense vectors are — `maybeCreateEmbeddingRuntime`
|
|
156
|
+
(`:198-200`) returns `undefined` unless `memory.index.embedding` is configured, so by default the
|
|
157
|
+
index runs text-only. The `vectorWeight ?? 0.6` blend at `index-manager-helpers.ts:21` weighs a
|
|
158
|
+
vector that, unconfigured, was never computed.
|
|
159
|
+
|
|
160
|
+
So no dense vector participates in scoring under a default configuration. One does only when a
|
|
161
|
+
consumer names an embedding provider.
|
|
162
|
+
|
|
163
|
+
**Known divergence,** on the opt-in path: the external contract forbids dense vectors in agent-memory
|
|
164
|
+
recall below a measured threshold (>1000 entries *and* demonstrated lexical degradation). Neither has
|
|
165
|
+
been measured here.
|
|
166
|
+
|
|
167
|
+
**The bigger gap WAS the default path, and it was not this divergence.** A contract arbitrating
|
|
168
|
+
*between* retrieval methods does not cover the absence of one — and until `721a311f` there was no
|
|
169
|
+
method on this path at all. That is fixed; the history is kept because the failure mode is worth
|
|
170
|
+
recognising again, and because it explains why the ranking signal has to depend on the question. A
|
|
171
|
+
first version cut by recency alone, and a live run showed the answering fact dropped for being old
|
|
172
|
+
rather than irrelevant. Bounding cost and choosing what survives are two jobs, and a cap does only
|
|
173
|
+
the first.
|
|
174
|
+
|
|
175
|
+
**What that default path used to inject, measured 2026-08 against the published artefact.** 100 real stores on one developer machine, 687
|
|
176
|
+
entries, ~3.2 KB per entry. Feeding the largest — 66 entries, 275 KB on disk — to
|
|
177
|
+
`readFactsFromMarkdown` returns all 66 and **246K characters, roughly 66K tokens injected into every
|
|
178
|
+
turn's system prompt**, before the user has said anything. Nine more stores are already past the
|
|
179
|
+
point where the injection exceeds a session budget.
|
|
180
|
+
|
|
181
|
+
Two things make this worse than a size problem. The CLI's store is read **whether or not
|
|
182
|
+
`local.sessionDir` is set**, so a consumer who never opted into CLI interop still gets it. And the
|
|
183
|
+
cost is invisible: nothing errors, the window just fills, and the tokens are not attributed to
|
|
184
|
+
memory. An earlier version of this document said "at 64 entries it does not hurt" — that was an
|
|
185
|
+
assumption, and measuring it is what disproved it.
|
|
186
|
+
|
|
187
|
+
`metadata.modified` is already written (`markdown-store.ts:210`) and parsed (`:177`) and consumed by
|
|
188
|
+
nothing — the data for a staleness signal is on disk today, unused.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Known gaps, recorded so they are not rediscovered
|
|
193
|
+
|
|
194
|
+
Each row says what would close it, not only that it is open — the two invite very different work.
|
|
195
|
+
|
|
196
|
+
| gap | where | what would close it |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| ~~The default recall path has no selection at all~~ — **closed** in `721a311f` | `local-agent-send.ts:257-272` | Closed: lexical relevance + recency, capped. Measured end to end against the real model — a 67-entry store went from ~66K to **13,606 tokens on the wire**, and the answering fact is recalled whether it is the oldest or the newest entry. |
|
|
199
|
+
| The CLI's store is read whether or not `local.sessionDir` is set | `markdown-store.ts:114` — `claudeProjectMemoryDir(cwd)` is an unconditional read root | A decision, not a fix: it is deliberate (*write one, read all*, so a consumer's existing memories are never orphaned) and it means a consumer who never opted into CLI interop still receives what the CLI accumulated in that project. Cheap while the cap holds; revisit if the cap is ever removed. |
|
|
200
|
+
| No retention of any kind — no TTL, prune, or decay | searched `ttl\|prune\|expire\|retention` across both packages: zero | A per-kind TTL plus a prune step in the sweep. Needs the kind vocabulary to mean something first (§ 2), which is why this orders before widening it. |
|
|
201
|
+
| Quarantine marks but does not constrain | `memory-file.ts:96`, `memory-provider.ts:48` | Implemented: three states, and `[unconfirmed]` on entries the store counted once. **Measured against the real model and it does not close the hole** — a planted memory alone is acted on 5/5, and beside a corroborated contradiction it is still asserted ~62% of runs (n=32). Marking influences the model; it does not constrain it. The guarantee is not available at this layer: blocking an uncorroborated entry would break the promise that a fact written once is recallable next session. |
|
|
202
|
+
| A planted memory can make the agent ACT | measured: `RELEASE_OVERRIDE.txt` created in 2 of 6 runs | Register the permission layer — `PermissionPlugin.create(new PermissionEngine(…))` — which blocks it every time. This is the half that IS closable at the tool boundary; the informational half above is not. Any deployment where the memory directory is writable by anything other than this agent needs it. |
|
|
203
|
+
| `description` is written as a copy of the body | `markdown-store.ts:205-211` — the *reader* (`:174-177`) already handles a distinct description correctly | Stop writing one when nobody declared it. The role is a one-line recall aid; deriving it mechanically would be inferring the situation, which § 2's rule already forbids for `kind`. Absent is a valid state and the reader already falls back to the body. |
|
|
204
|
+
| The topic-name deriver filters English function words only | `memory-file.ts:73` | A store in another language keeps that language's function words in the slug and the index title — `de`, `do`, `para` survive, so the name is longer and noisier. **Never lossy:** collisions are caught by `resolveName` comparing text, not by the stopword list. Real stores here are bilingual, so this is a partial parity, not a complete one. Closes with a per-language list, or by not needing one. |
|
|
205
|
+
| The dream sweep never filters by kind before dedup | `dreaming/phases.ts:34` — `lightPhase` never reads `kind` | Partition by kind before `lightPhase`. Nothing is deleted today, but a consolidated note can blend two distinct entries, and the note is what search returns. |
|
|
206
|
+
| The dream sweep does not update the index | `dreaming/run.ts:53-96` writes `notes/` and never syncs | An `IndexManager.sync` after `writeConsolidatedNotes`. |
|
|
207
|
+
| Recall fires every turn, cached on query hash, not on store manifest | `internal/local-agent/local-agent-memory.ts:87`; `sdk-memory/internal/active-memory/active-memory-cache.ts:66` | A second skip condition beside the existing one — hash of the store manifest, so an unchanged store skips even when the question changes. The query cache is not wrong; it answers a different question. |
|
package/package.json
CHANGED