@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/{agent-26RMDG4U.cjs → agent-M53C3PAA.cjs} +8 -8
  3. package/dist/{agent-26RMDG4U.cjs.map → agent-M53C3PAA.cjs.map} +1 -1
  4. package/dist/{agent-6ABK2DPG.js → agent-P42GA6RM.js} +7 -7
  5. package/dist/{agent-6ABK2DPG.js.map → agent-P42GA6RM.js.map} +1 -1
  6. package/dist/{chunk-KKK3FZ4A.cjs → chunk-2DG7KW4L.cjs} +3 -3
  7. package/dist/{chunk-KKK3FZ4A.cjs.map → chunk-2DG7KW4L.cjs.map} +1 -1
  8. package/dist/chunk-2UCFUSPW.cjs +468 -0
  9. package/dist/chunk-2UCFUSPW.cjs.map +1 -0
  10. package/dist/{chunk-E5MWNLRX.js → chunk-532CSLYU.js} +3 -3
  11. package/dist/{chunk-E5MWNLRX.js.map → chunk-532CSLYU.js.map} +1 -1
  12. package/dist/{chunk-3HDTP4ZO.cjs → chunk-5IT6DUOO.cjs} +4 -4
  13. package/dist/{chunk-3HDTP4ZO.cjs.map → chunk-5IT6DUOO.cjs.map} +1 -1
  14. package/dist/{chunk-Y2KJ7MYO.cjs → chunk-6OBIWHDR.cjs} +14 -14
  15. package/dist/{chunk-Y2KJ7MYO.cjs.map → chunk-6OBIWHDR.cjs.map} +1 -1
  16. package/dist/{chunk-76XTUTKY.js → chunk-GQZKDGZM.js} +167 -13
  17. package/dist/chunk-GQZKDGZM.js.map +1 -0
  18. package/dist/{chunk-43GFJ5SD.cjs → chunk-P5LCASTC.cjs} +19 -4
  19. package/dist/chunk-P5LCASTC.cjs.map +1 -0
  20. package/dist/{chunk-7OXXYBHD.js → chunk-QEKI3YKI.js} +3 -3
  21. package/dist/{chunk-7OXXYBHD.js.map → chunk-QEKI3YKI.js.map} +1 -1
  22. package/dist/{chunk-23GPJGRR.cjs → chunk-R7YIVL3K.cjs} +217 -63
  23. package/dist/chunk-R7YIVL3K.cjs.map +1 -0
  24. package/dist/chunk-WCLDJSMY.js +456 -0
  25. package/dist/chunk-WCLDJSMY.js.map +1 -0
  26. package/dist/{chunk-WG7R5W6R.js → chunk-YEXA3PGR.js} +3 -3
  27. package/dist/{chunk-WG7R5W6R.js.map → chunk-YEXA3PGR.js.map} +1 -1
  28. package/dist/{chunk-IACR5LEM.js → chunk-YXGAW7BB.js} +17 -2
  29. package/dist/chunk-YXGAW7BB.js.map +1 -0
  30. package/dist/{compact-session-YHFQIXV5.cjs → compact-session-GJXJD73F.cjs} +11 -11
  31. package/dist/{compact-session-YHFQIXV5.cjs.map → compact-session-GJXJD73F.cjs.map} +1 -1
  32. package/dist/{compact-session-QGDNP45U.js → compact-session-TSMQOIHO.js} +3 -3
  33. package/dist/{compact-session-QGDNP45U.js.map → compact-session-TSMQOIHO.js.map} +1 -1
  34. package/dist/cron.cjs +7 -7
  35. package/dist/cron.js +6 -6
  36. package/dist/eval.cjs +6 -6
  37. package/dist/eval.js +5 -5
  38. package/dist/{index-manager-KP2JAXKN.js → index-manager-AHAYJ33H.js} +4 -4
  39. package/dist/{index-manager-KP2JAXKN.js.map → index-manager-AHAYJ33H.js.map} +1 -1
  40. package/dist/{index-manager-YUORPS4G.cjs → index-manager-RHPFVFSC.cjs} +5 -5
  41. package/dist/{index-manager-YUORPS4G.cjs.map → index-manager-RHPFVFSC.cjs.map} +1 -1
  42. package/dist/index.cjs +69 -36
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.js +42 -9
  45. package/dist/index.js.map +1 -1
  46. package/dist/{inject-session-MLCJMYDG.cjs → inject-session-PPSO2IDD.cjs} +4 -4
  47. package/dist/{inject-session-MLCJMYDG.cjs.map → inject-session-PPSO2IDD.cjs.map} +1 -1
  48. package/dist/{inject-session-RBQZ45UM.js → inject-session-ZCQUB3IT.js} +3 -3
  49. package/dist/{inject-session-RBQZ45UM.js.map → inject-session-ZCQUB3IT.js.map} +1 -1
  50. package/dist/internal/memory/dreaming/phases.d.ts +21 -1
  51. package/dist/internal/memory/storage/index.cjs +10 -10
  52. package/dist/internal/memory/storage/index.js +1 -1
  53. package/dist/internal/memory/storage/memory-file.d.cts +41 -5
  54. package/dist/internal/memory/storage/memory-file.d.ts +41 -5
  55. package/dist/internal/memory/storage/threat-scan.d.cts +62 -0
  56. package/dist/internal/memory/storage/threat-scan.d.ts +62 -0
  57. package/dist/internal/memory/types.d.ts +28 -0
  58. package/dist/internal/runtime/memory/select-facts.d.ts +63 -0
  59. package/dist/internal/runtime/system-prompt/sources/memory-provider.d.ts +6 -1
  60. package/docs/error-codes.md +3 -2
  61. package/docs/harness-capability-map.md +9 -7
  62. package/docs/memory-decisions.md +207 -0
  63. package/package.json +1 -1
  64. package/dist/chunk-23GPJGRR.cjs.map +0 -1
  65. package/dist/chunk-43GFJ5SD.cjs.map +0 -1
  66. package/dist/chunk-76XTUTKY.js.map +0 -1
  67. package/dist/chunk-IACR5LEM.js.map +0 -1
  68. package/dist/chunk-JVERBPRD.js +0 -191
  69. package/dist/chunk-JVERBPRD.js.map +0 -1
  70. package/dist/chunk-NY25NBZB.cjs +0 -203
  71. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theokit/sdk",
3
- "version": "4.60.0",
3
+ "version": "4.61.0",
4
4
  "description": "TypeScript SDK for the Theo agent harness — same surface, local or cloud.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/usetheokit/theokit-sdk#readme",