@theokit/sdk-cache 0.3.2 → 1.0.1
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 +165 -0
- package/LICENSE +2 -2
- package/README.md +14 -1
- package/dist/index.cjs +144 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +330 -29
- package/dist/index.d.ts +330 -29
- package/dist/index.js +145 -28
- package/dist/index.js.map +1 -1
- package/package.json +15 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,170 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1a4bbcf: Raises the `@theokit/sdk` peer floor from `>=4.0.0` to `>=4.54.0`, which is what this package
|
|
8
|
+
actually needs.
|
|
9
|
+
|
|
10
|
+
It reads `PostAssistantReplyContext.usedTools` to avoid replaying an answer that came from a tool
|
|
11
|
+
call, and that property first shipped in 4.54.0. The declared range therefore promised compatibility
|
|
12
|
+
with versions where installing it produces a build that fails:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/cache.ts(239,26): error TS2339: Property 'usedTools' does not exist on
|
|
16
|
+
type 'PostAssistantReplyContext'.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Found by the CI leg that installs each package at the bottom of its own declared range and builds it
|
|
20
|
+
— not by any test in the repository. The existing floor test checks the MAJOR only, so `>=4.0.0`
|
|
21
|
+
satisfied it while being wrong. That limitation is now recorded there, along with this floor pinned
|
|
22
|
+
so it cannot silently drop back.
|
|
23
|
+
|
|
24
|
+
## 1.0.0
|
|
25
|
+
|
|
26
|
+
### Major Changes
|
|
27
|
+
|
|
28
|
+
- 643192a: **Breaking:** `CacheEmbedderError` is removed.
|
|
29
|
+
|
|
30
|
+
Nothing ever constructed it, so `catch (e) { if (e instanceof CacheEmbedderError) … }` was a branch
|
|
31
|
+
that could not run. Every embedder failure — on `consult`, on `remember`, and on both plugin hooks —
|
|
32
|
+
degrades to a cache miss or a skipped write, warns on stderr, and increments
|
|
33
|
+
`CacheStats.embedderFailures`, because a cache is an optimisation and must not take the request
|
|
34
|
+
down with it. That counter is how a broken embedder is detected, and the README now says so.
|
|
35
|
+
|
|
36
|
+
Nothing to migrate: any handler for this class was already dead code.
|
|
37
|
+
|
|
38
|
+
### Minor Changes
|
|
39
|
+
|
|
40
|
+
- ad8f9b9: The `"json"` persistence backend now keeps the promise it was sold on.
|
|
41
|
+
|
|
42
|
+
`Cache.ready()` — which the code's own comment referred to long before it existed — resolves once
|
|
43
|
+
the snapshot has been read, and `consult` / `remember` await hydration themselves, so a lookup
|
|
44
|
+
issued right after construction no longer races the read and misses an entry that is on disk.
|
|
45
|
+
|
|
46
|
+
`Cache.flush()` writes the debounced snapshot and keeps every entry. Writes are debounced 200ms, so
|
|
47
|
+
a once-per-invocation CLI — the process this backend exists for — used to persist nothing unless it
|
|
48
|
+
happened to live longer, and `clear()` was the only public call that forced a write. Nothing
|
|
49
|
+
flushes on teardown: call `flush()` before exiting.
|
|
50
|
+
|
|
51
|
+
Two caches built with the same `dir` and `namespace` now share one store instead of each writing a
|
|
52
|
+
full snapshot and erasing the other's entries. The first construction's `maxEntries` applies.
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- e3f2a82: Public-API documentation reviewed file by file, and corrected wherever it disagreed
|
|
57
|
+
with the code. The docblocks ship in the `.d.ts`, so these read as behaviour changes
|
|
58
|
+
in an editor even though no behaviour changed.
|
|
59
|
+
|
|
60
|
+
The corrections that change what a caller would do:
|
|
61
|
+
|
|
62
|
+
- **`sdk-cache` documented its own premise backwards.** The header example labelled a
|
|
63
|
+
semantic hit as if it avoided the provider call. `asPlugin()` returns the cached
|
|
64
|
+
answer as `recalledContext`, which the agent loop injects as a `<memory-context>`
|
|
65
|
+
block _before_ the prompt — the request still goes to the provider. The two modes
|
|
66
|
+
are now labelled separately, with a table saying which one short-circuits and which
|
|
67
|
+
one seeds.
|
|
68
|
+
- **`sdk-handoff`'s five error classes said "throw".** Under the plugin wiring the
|
|
69
|
+
handler never throws; every failure becomes a tool result `{"ok":false,…}` handed
|
|
70
|
+
back to the model. Each class now says where it is actually observable. The header
|
|
71
|
+
also told readers to `import { Handoff } from "@theokit/sdk"`, from which it was
|
|
72
|
+
extracted.
|
|
73
|
+
- **`sdk-budget`'s `charge()` claimed idempotency across concurrent calls.** The mutex
|
|
74
|
+
serialises, it does not deduplicate: two identical calls record twice. Related, and
|
|
75
|
+
newly documented: with `maxUsd` set, a model missing from the pricing table denies
|
|
76
|
+
every request rather than passing it — and the table matches by exact string, so
|
|
77
|
+
`"openai/gpt-4o"` does not match `"gpt-4o"`.
|
|
78
|
+
- **The three `memory-*` adapters advertised an env-var fallback they do not read**,
|
|
79
|
+
and their peer dependencies are required rather than optional. Their behavioural
|
|
80
|
+
differences are now stated where they break the "interchangeable adapter"
|
|
81
|
+
assumption — honcho ignores `k` and always throws on `delete`; mem0 recalls across
|
|
82
|
+
sessions by design; supermemory ignores `sessionId` entirely.
|
|
83
|
+
- **`sdk-memory`'s `truncated` flag was documented as its own inverse**, and its
|
|
84
|
+
dreaming sweep claimed a mutex it never takes against the writer it names.
|
|
85
|
+
- **`sdk-tools`** corrected `run_vitest`'s unreachable `no_vitest` code, `truncation`'s
|
|
86
|
+
replacement-character claim, and two return shapes missing a live error code.
|
|
87
|
+
- **`acp`/`cli`** corrected sixteen statements including a named error class that is
|
|
88
|
+
not the one raised, a handler documented as calling `fork()` that refuses
|
|
89
|
+
unconditionally, handlers described as pure that mint ids and mutate a store, a
|
|
90
|
+
config loader credited to Zod in a package that does not import it, and a `--force`
|
|
91
|
+
scaffold described as atomic that deletes the destination before the rename.
|
|
92
|
+
|
|
93
|
+
Undocumented public symbols were documented across every package, with each claim
|
|
94
|
+
checked against the implementation rather than inferred from the name.
|
|
95
|
+
|
|
96
|
+
- e368fc1: Every published declaration file now compiles without `skipLibCheck` (#345). The
|
|
97
|
+
DTS rollup emitted symbols as a re-export from a chunk while omitting them from
|
|
98
|
+
that chunk's `import`, and dropped type-only imports from external packages —
|
|
99
|
+
leaving 51 unresolved references across ten of the twelve packages. Nothing broke
|
|
100
|
+
at runtime, and `tsc` stayed green for anyone with `skipLibCheck` on, but a
|
|
101
|
+
consumer running type-aware lint saw every type reached through one degrade to
|
|
102
|
+
`error`.
|
|
103
|
+
|
|
104
|
+
The declarations are repaired at build time from the compiler's own diagnostics.
|
|
105
|
+
No source or API change.
|
|
106
|
+
|
|
107
|
+
- e699569: **The repository moved to the official `usetheokit` organization.** Every `repository`, `bugs` and `homepage` field now points there, along with the README, `CONTRIBUTING.md`, `SECURITY.md` and the issue templates. Existing clones and any URL already published keep working — GitHub redirects a transferred repository permanently — so this is a correctness fix for the metadata npm renders, not a break.
|
|
108
|
+
|
|
109
|
+
**The Apache-2.0 text every package ships was replaced with the official one.** The copy distributed until now had paragraph 4(d) truncated: it read "except as required for describing the origin of the Work and reproducing the content of the NOTICE file", dropping "reasonable and customary use" from the licensed clause. §4(d) governs what a redistributor must do with attribution notices, and the omission narrowed it.
|
|
110
|
+
|
|
111
|
+
That matters more than a typo would. The manifests declare the SPDX identifier `Apache-2.0`, which is an assertion that the terms are _the_ Apache-2.0 terms — a licence scanner resolves the identifier and never reads the file. A consumer's compliance review, which does read the file, would find a body that no longer matches the identifier and has no name of its own. Every `LICENSE` in this repository is now byte-identical to the canonical text, with the appendix filled in.
|
|
112
|
+
|
|
113
|
+
Nothing else about the terms changed: the licence is the same licence it has always been meant to be, and no package changes what it grants.
|
|
114
|
+
|
|
115
|
+
- e3f2a82: `@opentelemetry/api` is now declared as an optional peer dependency, so the spans these two
|
|
116
|
+
packages emit can actually reach a collector.
|
|
117
|
+
|
|
118
|
+
Both lazily `require("@opentelemetry/api")` from their own directory, but neither manifest
|
|
119
|
+
declared it in any dependency field. Under an isolated `node_modules` layout the specifier is
|
|
120
|
+
therefore not linked under the package, the require throws, the loader caches a `null` tracer,
|
|
121
|
+
and every span degrades to a no-op — silently, with no warning, unlike `@theokit/sdk`, which
|
|
122
|
+
prints one when telemetry is enabled and OTel is absent. For `sdk-cache` that covered both of
|
|
123
|
+
its main paths (`cache.lookup` on every send, `cache.store` on every reply), so an operator
|
|
124
|
+
reading a trace saw no cache activity at all and had no way to tell that from a cache that was
|
|
125
|
+
never consulted.
|
|
126
|
+
|
|
127
|
+
The declaration matches `@theokit/sdk`'s: `peerDependencies` plus `peerDependenciesMeta.optional`,
|
|
128
|
+
so nothing is installed for anyone who does not want OTel, and users who do want it get their
|
|
129
|
+
copy linked where the require can find it.
|
|
130
|
+
|
|
131
|
+
- 8d1feaa: `PostAssistantReplyContext` now carries `usedTools`, and `@theokit/sdk-cache` stops caching
|
|
132
|
+
tool-using turns in plugin mode.
|
|
133
|
+
|
|
134
|
+
The cache's D266/EC-10 guard exists because replaying an answer produced by a `write_file` / HTTP
|
|
135
|
+
POST / payment call re-serves the text without the side effect having happened. The
|
|
136
|
+
`post_assistant_reply` hook had no tool signal to key on and passed a literal `false`, so the guard
|
|
137
|
+
never fired on the path that runs automatically — only a hand-written `cache.remember(..., {
|
|
138
|
+
usedTools: true })` reached it.
|
|
139
|
+
|
|
140
|
+
The runtime derives the flag from the run's replayed event stream. A hook handler written against
|
|
141
|
+
the previous shape keeps working; code that CONSTRUCTS a `PostAssistantReplyContext` (test doubles,
|
|
142
|
+
custom emitters) now has to supply the field.
|
|
143
|
+
|
|
144
|
+
- c7385d2: Test runs no longer claim every core on the host.
|
|
145
|
+
|
|
146
|
+
None of the package configs capped `maxWorkers`, so vitest's default applied: `os.availableParallelism()`,
|
|
147
|
+
one fork per core, each booting a full test environment. The repo's `test` script is
|
|
148
|
+
`turbo run test --filter='./packages/*'`, so that default is paid once per package _concurrently_ —
|
|
149
|
+
nproc forks times turbo's concurrency, on nproc cores. Measured on a 12-thread machine during an
|
|
150
|
+
unrelated investigation, two vitest pools alone were enough to reach load average 33.89 with the
|
|
151
|
+
desktop unusable; a full fan-out is several times that.
|
|
152
|
+
|
|
153
|
+
`@theokit/sdk` is the interesting case. B-104 recorded on 2026-08-19 that the `poolOptions.forks.*`
|
|
154
|
+
block was 100% dead in Vitest 4, deleted it, and noted that `fileParallelism: false` was forcing
|
|
155
|
+
`maxWorkers` to 1 unconditionally, so a fork-count knob could not act. B-059 then flipped
|
|
156
|
+
`fileParallelism` to `true` on 2026-08-20, which made the knob able to act again — and nothing
|
|
157
|
+
reintroduced one, so the package silently went back to the uncapped default. That comment has been
|
|
158
|
+
corrected along with the config; it claimed no knob existed, which is no longer true.
|
|
159
|
+
|
|
160
|
+
The cap leaves 4 cores free (`Math.max(2, cpus().length - 4)`), scaling with the runner rather than
|
|
161
|
+
hard-coding one machine's core count. It costs no wall-clock: measured in `theokit-ui`, the full
|
|
162
|
+
suite ran 73.96s at 4 workers against 74.36s at 12, so the parallelism above the cap was already
|
|
163
|
+
noise. Verified as resolved config rather than as file contents — `createVitest` reports
|
|
164
|
+
`maxWorkers: 8` on a 12-thread host, which is the formula, not the default.
|
|
165
|
+
|
|
166
|
+
This changes no published behaviour; it is test tooling only. Refs usetheokit/theokit-ui#51.
|
|
167
|
+
|
|
3
168
|
## 0.3.2
|
|
4
169
|
|
|
5
170
|
### Patch Changes
|
package/LICENSE
CHANGED
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
|
|
138
138
|
6. Trademarks. This License does not grant permission to use the trade
|
|
139
139
|
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
-
except as required for
|
|
141
|
-
reproducing the content of the NOTICE file.
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
142
|
|
|
143
143
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
144
|
agreed to in writing, Licensor provides the Work (and each
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Clears the in-memory state. For the JSON file store, deletes the persisted state
|
|
|
72
72
|
|
|
73
73
|
## Errors
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
An embedder failure never surfaces as an error: every path degrades to a cache miss or a skipped write, warns on stderr, and increments `CacheStats.embedderFailures`. That counter is how you detect a broken embedder.
|
|
76
76
|
- `CacheInvalidTtlError` — TTL string failed parse (e.g., `"-5m"` or `"abc"`). Thrown pre-construction.
|
|
77
77
|
|
|
78
78
|
## How it fits with `@theokit/sdk`
|
|
@@ -98,6 +98,19 @@ import { Cache } from "@theokit/sdk-cache";
|
|
|
98
98
|
|
|
99
99
|
See the monorepo `CHANGELOG.md` for the 1.x → 2.0 package-split migration notes.
|
|
100
100
|
|
|
101
|
+
## API reference
|
|
102
|
+
|
|
103
|
+
Every symbol this package exports, with the exact specifier to import it from, is in the generated
|
|
104
|
+
capability map that ships inside `@theokit/sdk`:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
node_modules/@theokit/sdk/docs/harness-capability-map.md # symbol -> import specifier
|
|
108
|
+
node_modules/@theokit/sdk/docs/error-codes.md # every `code` an error can carry
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both are generated from the built type declarations, so they describe the version you installed
|
|
112
|
+
rather than the version someone wrote a page about.
|
|
113
|
+
|
|
101
114
|
## License
|
|
102
115
|
|
|
103
116
|
Apache-2.0.
|
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var path = require('path');
|
|
3
4
|
var sdk = require('@theokit/sdk');
|
|
4
5
|
var persistence = require('@theokit/sdk/persistence');
|
|
5
6
|
var zod = require('zod');
|
|
6
7
|
var crypto = require('crypto');
|
|
7
8
|
var module$1 = require('module');
|
|
8
9
|
var promises = require('fs/promises');
|
|
9
|
-
var path = require('path');
|
|
10
10
|
|
|
11
11
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
12
|
// src/cache.ts
|
|
@@ -277,14 +277,6 @@ var InMemoryCacheStore = class {
|
|
|
277
277
|
};
|
|
278
278
|
|
|
279
279
|
// src/types/cache.ts
|
|
280
|
-
var CacheEmbedderError = class extends Error {
|
|
281
|
-
name = "CacheEmbedderError";
|
|
282
|
-
cause;
|
|
283
|
-
constructor(message, cause) {
|
|
284
|
-
super(`Cache embedder failed: ${message}`);
|
|
285
|
-
if (cause !== void 0) this.cause = cause;
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
280
|
var CacheInvalidTtlError = class extends Error {
|
|
289
281
|
constructor(input) {
|
|
290
282
|
super(
|
|
@@ -524,13 +516,14 @@ var DEFAULT_TTL = { default: "1h" };
|
|
|
524
516
|
var DEFAULT_NAMESPACE = "global";
|
|
525
517
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
526
518
|
var Cache = class _Cache {
|
|
527
|
-
constructor(embedder, threshold, ttl, namespace, modelId, store) {
|
|
519
|
+
constructor(embedder, threshold, ttl, namespace, modelId, store, hydrated) {
|
|
528
520
|
this.embedder = embedder;
|
|
529
521
|
this.threshold = threshold;
|
|
530
522
|
this.ttl = ttl;
|
|
531
523
|
this.namespace = namespace;
|
|
532
524
|
this.modelId = modelId;
|
|
533
525
|
this.store = store;
|
|
526
|
+
this.hydrated = hydrated;
|
|
534
527
|
}
|
|
535
528
|
embedder;
|
|
536
529
|
threshold;
|
|
@@ -538,7 +531,54 @@ var Cache = class _Cache {
|
|
|
538
531
|
namespace;
|
|
539
532
|
modelId;
|
|
540
533
|
store;
|
|
534
|
+
hydrated;
|
|
541
535
|
_plugin;
|
|
536
|
+
/**
|
|
537
|
+
* Resolves once the `"json"` backend has finished reading its snapshot; resolves immediately on
|
|
538
|
+
* the in-memory backend.
|
|
539
|
+
*
|
|
540
|
+
* You rarely need it: `consult` and `remember` await hydration themselves, so a cache is correct
|
|
541
|
+
* without it. It exists for a caller who wants the read charged to startup rather than to the
|
|
542
|
+
* first lookup — and because the code promised it long before it existed (#359).
|
|
543
|
+
*
|
|
544
|
+
* A corrupt or unreadable snapshot resolves normally with an empty cache and a warning on
|
|
545
|
+
* stderr; a cache must not take the process down.
|
|
546
|
+
*/
|
|
547
|
+
async ready() {
|
|
548
|
+
await this.hydrated;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Write the pending snapshot to disk now, keeping every entry. No-op on the in-memory backend.
|
|
552
|
+
*
|
|
553
|
+
* Writes are debounced 200ms, so a process that remembers something and exits inside that window
|
|
554
|
+
* persists nothing — precisely the once-per-invocation CLI the `"json"` backend exists for. Call
|
|
555
|
+
* this before exiting. Nothing flushes on teardown: an `exit` handler cannot await, and a library
|
|
556
|
+
* installing a process-level hook is a side effect the caller did not ask for.
|
|
557
|
+
*
|
|
558
|
+
* Until #359 the only public call that wrote the snapshot was `clear()`, which also destroyed
|
|
559
|
+
* everything you wanted to persist.
|
|
560
|
+
*/
|
|
561
|
+
async flush() {
|
|
562
|
+
const store = this.store;
|
|
563
|
+
if (typeof store.flush === "function") await store.flush();
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Build a cache. Validates `options` with Zod and THROWS `ZodError` on a bad shape — an
|
|
567
|
+
* `embedder` missing `{ id, dimension, embed }`, a `threshold` outside `0..2`, a `namespace`
|
|
568
|
+
* longer than 64 chars, or `persistence: { backend: "json" }` without a `dir`.
|
|
569
|
+
*
|
|
570
|
+
* Defaults: `threshold` 0.85, `ttl` `{ default: "1h" }`, `namespace` `"global"`, `maxEntries`
|
|
571
|
+
* 1000 (LRU), `persistence` in-memory. `modelId` defaults to the literal string `"unknown"`,
|
|
572
|
+
* which is a real namespace value and not a wildcard: entries stored while `modelId` was
|
|
573
|
+
* defaulted are only ever returned to lookups that also default it.
|
|
574
|
+
*
|
|
575
|
+
* With `persistence: { backend: "json", dir }` the snapshot is read in the background and this
|
|
576
|
+
* call does not await it — but `consult` and `remember` do, so a lookup issued immediately after
|
|
577
|
+
* construction still sees what is on disk. Await {@link Cache.ready} to charge the read to
|
|
578
|
+
* startup instead of to the first lookup. Two caches built with the same `dir` and `namespace`
|
|
579
|
+
* share one store, so they cannot overwrite each other's snapshot; the FIRST one's `maxEntries`
|
|
580
|
+
* is the one that applies.
|
|
581
|
+
*/
|
|
542
582
|
static semantic(options) {
|
|
543
583
|
CacheSemanticOptionsSchema.parse(options);
|
|
544
584
|
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
@@ -546,12 +586,28 @@ var Cache = class _Cache {
|
|
|
546
586
|
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
547
587
|
const modelId = options.modelId ?? "unknown";
|
|
548
588
|
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
549
|
-
const store = createStore(namespace, maxEntries, options.persistence);
|
|
550
|
-
return new _Cache(options.embedder, threshold, ttl, namespace, modelId, store);
|
|
589
|
+
const { store, hydrated } = createStore(namespace, maxEntries, options.persistence);
|
|
590
|
+
return new _Cache(options.embedder, threshold, ttl, namespace, modelId, store, hydrated);
|
|
551
591
|
}
|
|
552
592
|
/**
|
|
553
|
-
*
|
|
554
|
-
*
|
|
593
|
+
* A `Plugin` for `Agent.create({ plugins: [...] })` that reads the cache before each user turn
|
|
594
|
+
* and writes it after each assistant reply.
|
|
595
|
+
*
|
|
596
|
+
* READ THIS BEFORE BUDGETING FOR IT. A hit does NOT skip the model call. The hook returns the
|
|
597
|
+
* cached response as `PreUserSendResult.recalledContext`, which the agent loop injects as a
|
|
598
|
+
* `<memory-context>` block ahead of the prompt — the request still goes to the provider, still
|
|
599
|
+
* costs tokens, and still returns whatever the model makes of that context, which need not be
|
|
600
|
+
* the cached text. Use {@link Cache.consult} / {@link Cache.remember} when the point is to avoid
|
|
601
|
+
* the call.
|
|
602
|
+
*
|
|
603
|
+
* A turn that invoked tools is NOT cached: the store hook reads
|
|
604
|
+
* `PostAssistantReplyContext.usedTools`, which the runtime derives from the run's tool calls
|
|
605
|
+
* (#358). Replaying such an answer would hand a later caller the result of a write that never
|
|
606
|
+
* happened. Until that signal existed the hook passed a literal `false` and cached those turns,
|
|
607
|
+
* despite the package's stated intent.
|
|
608
|
+
*
|
|
609
|
+
* Memoized: repeated calls return the SAME plugin, so registering it twice does not double the
|
|
610
|
+
* hooks.
|
|
555
611
|
*/
|
|
556
612
|
asPlugin() {
|
|
557
613
|
if (this._plugin !== void 0) return this._plugin;
|
|
@@ -586,7 +642,13 @@ var Cache = class _Cache {
|
|
|
586
642
|
await performStore({
|
|
587
643
|
prompt: c.prompt,
|
|
588
644
|
response: c.reply,
|
|
589
|
-
|
|
645
|
+
// EC-10 / D266 — never cache a reply the run produced with tools; replaying it hands a
|
|
646
|
+
// later caller the RESULT of a write without the write having happened. This used to
|
|
647
|
+
// be the literal `false` under a comment describing a `tool_call_id` heuristic that was
|
|
648
|
+
// never implemented, so the guard fired only for a hand-written `remember(...)` call
|
|
649
|
+
// and never on the plugin path, which is the one that runs automatically. #358 added
|
|
650
|
+
// the signal to `PostAssistantReplyContext`; this reads it.
|
|
651
|
+
usedTools: c.usedTools,
|
|
590
652
|
store: cache.store,
|
|
591
653
|
embedder: cache.embedder,
|
|
592
654
|
ttl: cache.ttl,
|
|
@@ -600,13 +662,28 @@ var Cache = class _Cache {
|
|
|
600
662
|
return this._plugin;
|
|
601
663
|
}
|
|
602
664
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
665
|
+
* Look a prompt up. Call it BEFORE dispatching to the model and skip the call on a hit — this is
|
|
666
|
+
* the only path in this package that actually avoids an LLM request.
|
|
667
|
+
*
|
|
668
|
+
* ```ts
|
|
669
|
+
* const hit = await cache.consult(prompt);
|
|
670
|
+
* if (hit.hit) return hit.response;
|
|
671
|
+
* ```
|
|
672
|
+
*
|
|
673
|
+
* `source` says which stage matched: `"kv"` is an exact-key match and costs NO embedding call;
|
|
674
|
+
* `"semantic"` means the prompt was embedded and a stored vector came within `threshold`, and
|
|
675
|
+
* only then is `distance` present (cosine distance, so smaller is closer).
|
|
605
676
|
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
677
|
+
* NEVER THROWS on an embedder failure. It degrades to `{ hit: false }`, logs a warning on
|
|
678
|
+
* stderr, and increments {@link CacheStats.embedderFailures} — a cache must not take the request
|
|
679
|
+
* down with it. A cache that has silently stopped hitting is that counter climbing, not a cold
|
|
680
|
+
* cache.
|
|
681
|
+
*
|
|
682
|
+
* An empty or whitespace-only prompt returns `{ hit: false }` and counts as a MISS. A prompt
|
|
683
|
+
* matching {@link CacheTTLConfig.exclude} returns `{ hit: false }` and counts as `excluded`.
|
|
608
684
|
*/
|
|
609
685
|
async consult(prompt) {
|
|
686
|
+
await this.hydrated;
|
|
610
687
|
const result = await performLookup({
|
|
611
688
|
prompt,
|
|
612
689
|
store: this.store,
|
|
@@ -627,10 +704,25 @@ var Cache = class _Cache {
|
|
|
627
704
|
return { hit: false };
|
|
628
705
|
}
|
|
629
706
|
/**
|
|
630
|
-
*
|
|
631
|
-
*
|
|
707
|
+
* Store a prompt/response pair. Pair it with {@link Cache.consult} after you dispatched the model
|
|
708
|
+
* call yourself.
|
|
709
|
+
*
|
|
710
|
+
* Pass `{ usedTools: true }` when the answer came from a run that invoked tools and replaying it
|
|
711
|
+
* would lose the side effects — the write is then skipped entirely.
|
|
712
|
+
*
|
|
713
|
+
* Silently writes nothing when the prompt is empty/whitespace, the response is empty, the prompt
|
|
714
|
+
* matches {@link CacheTTLConfig.exclude}, or the embedder fails (that last case increments
|
|
715
|
+
* {@link CacheStats.embedderFailures}). It resolves in every one of those cases: a resolved
|
|
716
|
+
* promise is not evidence that an entry exists — read {@link CacheStats.entries} if you need
|
|
717
|
+
* that.
|
|
718
|
+
*
|
|
719
|
+
* Writing beyond `maxEntries` evicts the least-recently-used entry. On the `"json"` backend the
|
|
720
|
+
* disk write is DEBOUNCED by 200 ms, so a process that exits right after this resolves loses the
|
|
721
|
+
* entry unless it calls {@link Cache.flush} — which writes the snapshot and keeps every entry.
|
|
722
|
+
* Nothing flushes on teardown.
|
|
632
723
|
*/
|
|
633
724
|
async remember(prompt, response, opts) {
|
|
725
|
+
await this.hydrated;
|
|
634
726
|
await performStore({
|
|
635
727
|
prompt,
|
|
636
728
|
response,
|
|
@@ -642,27 +734,52 @@ var Cache = class _Cache {
|
|
|
642
734
|
modelId: this.modelId
|
|
643
735
|
});
|
|
644
736
|
}
|
|
645
|
-
/**
|
|
737
|
+
/**
|
|
738
|
+
* Counter snapshot for this instance. See {@link CacheStats} for what each counter separates —
|
|
739
|
+
* in particular `misses` vs `excluded` vs `embedderFailures`, which is how you tell a cold cache
|
|
740
|
+
* from a too-broad exclude regex from a broken embedder.
|
|
741
|
+
*
|
|
742
|
+
* Process-local: the `"json"` backend persists entries, never counters, so a restart reports
|
|
743
|
+
* zeros against a warm file.
|
|
744
|
+
*/
|
|
646
745
|
stats() {
|
|
647
746
|
return this.store.stats();
|
|
648
747
|
}
|
|
649
|
-
/**
|
|
748
|
+
/**
|
|
749
|
+
* Drop every entry. On the `"json"` backend this also forces the debounced snapshot to disk
|
|
750
|
+
* immediately, so it is the one public call that guarantees the file matches memory.
|
|
751
|
+
*
|
|
752
|
+
* Counters are NOT reset — `stats()` keeps reporting the hits and misses accumulated before the
|
|
753
|
+
* clear, so `entries: 0` alongside a non-zero `kvHits` is expected, not a bug.
|
|
754
|
+
*/
|
|
650
755
|
async clear() {
|
|
651
756
|
await this.store.clear();
|
|
652
757
|
}
|
|
653
|
-
/**
|
|
758
|
+
/**
|
|
759
|
+
* Remove every entry whose TTL has elapsed, returning how many were dropped, and add that to
|
|
760
|
+
* {@link CacheStats.evicted}.
|
|
761
|
+
*
|
|
762
|
+
* Optional housekeeping: expired entries are already skipped on lookup and dropped when touched,
|
|
763
|
+
* so this only reclaims memory for entries nobody asks for. `now` exists to make the sweep
|
|
764
|
+
* testable; leave it out in production.
|
|
765
|
+
*/
|
|
654
766
|
evictExpired(now = Date.now()) {
|
|
655
767
|
return this.store.evictExpired(now);
|
|
656
768
|
}
|
|
657
769
|
};
|
|
770
|
+
var jsonStores = /* @__PURE__ */ new Map();
|
|
658
771
|
function createStore(namespace, maxEntries, persistence) {
|
|
659
772
|
if (persistence?.backend === "json") {
|
|
660
773
|
const dir = persistence.dir;
|
|
774
|
+
const key = `${path.resolve(dir)}\0${namespace}`;
|
|
775
|
+
const existing = jsonStores.get(key);
|
|
776
|
+
if (existing !== void 0) return existing;
|
|
661
777
|
const store = new JsonFileCacheStore(dir, namespace, maxEntries);
|
|
662
|
-
|
|
663
|
-
|
|
778
|
+
const entry = { store, hydrated: store.hydrate() };
|
|
779
|
+
jsonStores.set(key, entry);
|
|
780
|
+
return entry;
|
|
664
781
|
}
|
|
665
|
-
return new InMemoryCacheStore(maxEntries);
|
|
782
|
+
return { store: new InMemoryCacheStore(maxEntries), hydrated: Promise.resolve() };
|
|
666
783
|
}
|
|
667
784
|
|
|
668
785
|
// src/lexical-embedder.ts
|
|
@@ -688,7 +805,6 @@ function createLexicalEmbedder(dimension = 256) {
|
|
|
688
805
|
}
|
|
689
806
|
|
|
690
807
|
exports.Cache = Cache;
|
|
691
|
-
exports.CacheEmbedderError = CacheEmbedderError;
|
|
692
808
|
exports.CacheInvalidTtlError = CacheInvalidTtlError;
|
|
693
809
|
exports.createLexicalEmbedder = createLexicalEmbedder;
|
|
694
810
|
//# sourceMappingURL=index.cjs.map
|