@theokit/sdk-cache 0.3.1 → 1.0.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 CHANGED
@@ -1,5 +1,169 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 643192a: **Breaking:** `CacheEmbedderError` is removed.
8
+
9
+ Nothing ever constructed it, so `catch (e) { if (e instanceof CacheEmbedderError) … }` was a branch
10
+ that could not run. Every embedder failure — on `consult`, on `remember`, and on both plugin hooks —
11
+ degrades to a cache miss or a skipped write, warns on stderr, and increments
12
+ `CacheStats.embedderFailures`, because a cache is an optimisation and must not take the request
13
+ down with it. That counter is how a broken embedder is detected, and the README now says so.
14
+
15
+ Nothing to migrate: any handler for this class was already dead code.
16
+
17
+ ### Minor Changes
18
+
19
+ - ad8f9b9: The `"json"` persistence backend now keeps the promise it was sold on.
20
+
21
+ `Cache.ready()` — which the code's own comment referred to long before it existed — resolves once
22
+ the snapshot has been read, and `consult` / `remember` await hydration themselves, so a lookup
23
+ issued right after construction no longer races the read and misses an entry that is on disk.
24
+
25
+ `Cache.flush()` writes the debounced snapshot and keeps every entry. Writes are debounced 200ms, so
26
+ a once-per-invocation CLI — the process this backend exists for — used to persist nothing unless it
27
+ happened to live longer, and `clear()` was the only public call that forced a write. Nothing
28
+ flushes on teardown: call `flush()` before exiting.
29
+
30
+ Two caches built with the same `dir` and `namespace` now share one store instead of each writing a
31
+ full snapshot and erasing the other's entries. The first construction's `maxEntries` applies.
32
+
33
+ ### Patch Changes
34
+
35
+ - e3f2a82: Public-API documentation reviewed file by file, and corrected wherever it disagreed
36
+ with the code. The docblocks ship in the `.d.ts`, so these read as behaviour changes
37
+ in an editor even though no behaviour changed.
38
+
39
+ The corrections that change what a caller would do:
40
+
41
+ - **`sdk-cache` documented its own premise backwards.** The header example labelled a
42
+ semantic hit as if it avoided the provider call. `asPlugin()` returns the cached
43
+ answer as `recalledContext`, which the agent loop injects as a `<memory-context>`
44
+ block _before_ the prompt — the request still goes to the provider. The two modes
45
+ are now labelled separately, with a table saying which one short-circuits and which
46
+ one seeds.
47
+ - **`sdk-handoff`'s five error classes said "throw".** Under the plugin wiring the
48
+ handler never throws; every failure becomes a tool result `{"ok":false,…}` handed
49
+ back to the model. Each class now says where it is actually observable. The header
50
+ also told readers to `import { Handoff } from "@theokit/sdk"`, from which it was
51
+ extracted.
52
+ - **`sdk-budget`'s `charge()` claimed idempotency across concurrent calls.** The mutex
53
+ serialises, it does not deduplicate: two identical calls record twice. Related, and
54
+ newly documented: with `maxUsd` set, a model missing from the pricing table denies
55
+ every request rather than passing it — and the table matches by exact string, so
56
+ `"openai/gpt-4o"` does not match `"gpt-4o"`.
57
+ - **The three `memory-*` adapters advertised an env-var fallback they do not read**,
58
+ and their peer dependencies are required rather than optional. Their behavioural
59
+ differences are now stated where they break the "interchangeable adapter"
60
+ assumption — honcho ignores `k` and always throws on `delete`; mem0 recalls across
61
+ sessions by design; supermemory ignores `sessionId` entirely.
62
+ - **`sdk-memory`'s `truncated` flag was documented as its own inverse**, and its
63
+ dreaming sweep claimed a mutex it never takes against the writer it names.
64
+ - **`sdk-tools`** corrected `run_vitest`'s unreachable `no_vitest` code, `truncation`'s
65
+ replacement-character claim, and two return shapes missing a live error code.
66
+ - **`acp`/`cli`** corrected sixteen statements including a named error class that is
67
+ not the one raised, a handler documented as calling `fork()` that refuses
68
+ unconditionally, handlers described as pure that mint ids and mutate a store, a
69
+ config loader credited to Zod in a package that does not import it, and a `--force`
70
+ scaffold described as atomic that deletes the destination before the rename.
71
+
72
+ Undocumented public symbols were documented across every package, with each claim
73
+ checked against the implementation rather than inferred from the name.
74
+
75
+ - e368fc1: Every published declaration file now compiles without `skipLibCheck` (#345). The
76
+ DTS rollup emitted symbols as a re-export from a chunk while omitting them from
77
+ that chunk's `import`, and dropped type-only imports from external packages —
78
+ leaving 51 unresolved references across ten of the twelve packages. Nothing broke
79
+ at runtime, and `tsc` stayed green for anyone with `skipLibCheck` on, but a
80
+ consumer running type-aware lint saw every type reached through one degrade to
81
+ `error`.
82
+
83
+ The declarations are repaired at build time from the compiler's own diagnostics.
84
+ No source or API change.
85
+
86
+ - 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.
87
+
88
+ **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.
89
+
90
+ 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.
91
+
92
+ 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.
93
+
94
+ - e3f2a82: `@opentelemetry/api` is now declared as an optional peer dependency, so the spans these two
95
+ packages emit can actually reach a collector.
96
+
97
+ Both lazily `require("@opentelemetry/api")` from their own directory, but neither manifest
98
+ declared it in any dependency field. Under an isolated `node_modules` layout the specifier is
99
+ therefore not linked under the package, the require throws, the loader caches a `null` tracer,
100
+ and every span degrades to a no-op — silently, with no warning, unlike `@theokit/sdk`, which
101
+ prints one when telemetry is enabled and OTel is absent. For `sdk-cache` that covered both of
102
+ its main paths (`cache.lookup` on every send, `cache.store` on every reply), so an operator
103
+ reading a trace saw no cache activity at all and had no way to tell that from a cache that was
104
+ never consulted.
105
+
106
+ The declaration matches `@theokit/sdk`'s: `peerDependencies` plus `peerDependenciesMeta.optional`,
107
+ so nothing is installed for anyone who does not want OTel, and users who do want it get their
108
+ copy linked where the require can find it.
109
+
110
+ - 8d1feaa: `PostAssistantReplyContext` now carries `usedTools`, and `@theokit/sdk-cache` stops caching
111
+ tool-using turns in plugin mode.
112
+
113
+ The cache's D266/EC-10 guard exists because replaying an answer produced by a `write_file` / HTTP
114
+ POST / payment call re-serves the text without the side effect having happened. The
115
+ `post_assistant_reply` hook had no tool signal to key on and passed a literal `false`, so the guard
116
+ never fired on the path that runs automatically — only a hand-written `cache.remember(..., {
117
+ usedTools: true })` reached it.
118
+
119
+ The runtime derives the flag from the run's replayed event stream. A hook handler written against
120
+ the previous shape keeps working; code that CONSTRUCTS a `PostAssistantReplyContext` (test doubles,
121
+ custom emitters) now has to supply the field.
122
+
123
+ - c7385d2: Test runs no longer claim every core on the host.
124
+
125
+ None of the package configs capped `maxWorkers`, so vitest's default applied: `os.availableParallelism()`,
126
+ one fork per core, each booting a full test environment. The repo's `test` script is
127
+ `turbo run test --filter='./packages/*'`, so that default is paid once per package _concurrently_ —
128
+ nproc forks times turbo's concurrency, on nproc cores. Measured on a 12-thread machine during an
129
+ unrelated investigation, two vitest pools alone were enough to reach load average 33.89 with the
130
+ desktop unusable; a full fan-out is several times that.
131
+
132
+ `@theokit/sdk` is the interesting case. B-104 recorded on 2026-08-19 that the `poolOptions.forks.*`
133
+ block was 100% dead in Vitest 4, deleted it, and noted that `fileParallelism: false` was forcing
134
+ `maxWorkers` to 1 unconditionally, so a fork-count knob could not act. B-059 then flipped
135
+ `fileParallelism` to `true` on 2026-08-20, which made the knob able to act again — and nothing
136
+ reintroduced one, so the package silently went back to the uncapped default. That comment has been
137
+ corrected along with the config; it claimed no knob existed, which is no longer true.
138
+
139
+ The cap leaves 4 cores free (`Math.max(2, cpus().length - 4)`), scaling with the runner rather than
140
+ hard-coding one machine's core count. It costs no wall-clock: measured in `theokit-ui`, the full
141
+ suite ran 73.96s at 4 workers against 74.36s at 12, so the parallelism above the cap was already
142
+ noise. Verified as resolved config rather than as file contents — `createVitest` reports
143
+ `maxWorkers: 8` on a 12-thread host, which is the formula, not the default.
144
+
145
+ This changes no published behaviour; it is test tooling only. Refs usetheokit/theokit-ui#51.
146
+
147
+ ## 0.3.2
148
+
149
+ ### Patch Changes
150
+
151
+ - 8790f70: Refuse a `workspace:` range before it can reach npm.
152
+
153
+ Five of this repo's twelve publishable packages declare internal dependencies as `workspace:^`, which
154
+ is correct on disk and becomes an unrecoverable defect if the publish goes out through a tool that
155
+ does not rewrite it: `pnpm` resolves the protocol while packing, `npm` ships the manifest verbatim.
156
+ A version published that way fails to install for everyone and cannot be corrected — only
157
+ deprecated.
158
+
159
+ Every publishable package now runs the guard in `prepublishOnly`, so it fires whichever way the
160
+ publish is invoked, and `pnpm release` runs it once across the repo before `changeset publish`.
161
+
162
+ Note for anyone reading a published manifest: the `prepublishOnly` entry points at a path inside
163
+ this repository. It never runs for a consumer — the hook only fires when the package itself is
164
+ published — and guarding the entry point that a hand-run `npm publish` actually uses was worth the
165
+ cosmetic wart of shipping the line.
166
+
3
167
  ## 0.3.1
4
168
 
5
169
  ### 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 describing the origin of the Work and
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
- - `CacheEmbedderError` embedder runtime failed (rate-limited, network error, etc.). Agent run continues without cache.
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`
@@ -96,7 +96,20 @@ import { Agent } from "@theokit/sdk";
96
96
  import { Cache } from "@theokit/sdk-cache";
97
97
  ```
98
98
 
99
- See `docs/migration/1-x-to-2-0.md` in the monorepo root.
99
+ See the monorepo `CHANGELOG.md` for the 1.x → 2.0 package-split migration notes.
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.
100
113
 
101
114
  ## License
102
115
 
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
- * EC-4 absorbed: memoized so repeated `asPlugin()` calls return the SAME
554
- * plugin descriptor no duplicate hook registration.
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
- usedTools: false,
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
- * Explicit cache lookup callers that want true LLM short-circuit
604
- * call this BEFORE `agent.send()`, then dispatch to the LLM only on miss.
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
- * v1 plugin mode provides recall + context-inject (LLM still called).
607
- * v1.x will add transparent short-circuit via an agent-loop refactor.
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
- * Explicit cache store pair with `consult()` to manually feed the
631
- * cache after dispatching the LLM call yourself.
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
- /** Stats snapshot — primary observable for dogfood verification. */
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
- /** Clear all entries (and flush to disk if JSON backend). */
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
- /** Force-evict expired entries. Returns count removed. */
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
- void store.hydrate();
663
- return store;
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