instar 1.3.117 → 1.3.118
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/dist/memory/EmbeddingProvider.d.ts +41 -0
- package/dist/memory/EmbeddingProvider.d.ts.map +1 -1
- package/dist/memory/EmbeddingProvider.js +106 -22
- package/dist/memory/EmbeddingProvider.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.118.md +51 -0
- package/upgrades/side-effects/onnx-idle-unload.md +36 -0
|
@@ -20,6 +20,23 @@ export interface EmbeddingProviderConfig {
|
|
|
20
20
|
dimensions?: number;
|
|
21
21
|
/** Maximum text length to embed in characters (default: 8192) */
|
|
22
22
|
maxTextLength?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Idle-unload window in ms. After this long with no embed() call, the loaded
|
|
25
|
+
* ONNX pipeline is disposed to free its thread pool, which busy-spins even
|
|
26
|
+
* when idle (measured: ~3.6% of a core on a quiet box, up to ~44% on a
|
|
27
|
+
* contended one — pure waste while an agent isn't doing memory work). The
|
|
28
|
+
* next embed() lazily reloads (~1-3s, verified-identical output). Default
|
|
29
|
+
* 300000 (5 min). Set 0 to disable (keep the model resident forever — the
|
|
30
|
+
* prior behavior).
|
|
31
|
+
*/
|
|
32
|
+
idleUnloadMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Test/advanced seam: factory that produces the feature-extraction pipeline.
|
|
35
|
+
* Defaults to the real `@huggingface/transformers` import. Tests inject a mock
|
|
36
|
+
* (a callable with a `dispose()`) to exercise the idle-unload lifecycle
|
|
37
|
+
* without loading the 80MB model.
|
|
38
|
+
*/
|
|
39
|
+
pipelineFactory?: (modelName: string) => Promise<any>;
|
|
23
40
|
}
|
|
24
41
|
export declare class EmbeddingProvider {
|
|
25
42
|
private pipeline;
|
|
@@ -27,7 +44,13 @@ export declare class EmbeddingProvider {
|
|
|
27
44
|
private readonly modelName;
|
|
28
45
|
readonly dimensions: number;
|
|
29
46
|
private readonly maxTextLength;
|
|
47
|
+
private readonly idleUnloadMs;
|
|
48
|
+
private readonly pipelineFactory?;
|
|
30
49
|
private vecExtensionLoaded;
|
|
50
|
+
/** In-flight embed() calls — the idle-unload timer never disposes while > 0. */
|
|
51
|
+
private inFlight;
|
|
52
|
+
/** Rolling idle-unload timer (reset on every embed); unref'd so it can't keep the process alive. */
|
|
53
|
+
private idleTimer;
|
|
31
54
|
constructor(config?: EmbeddingProviderConfig);
|
|
32
55
|
/**
|
|
33
56
|
* Ensure the model is loaded. Safe to call multiple times — only loads once.
|
|
@@ -38,6 +61,24 @@ export declare class EmbeddingProvider {
|
|
|
38
61
|
* Whether the model is loaded and ready for embedding.
|
|
39
62
|
*/
|
|
40
63
|
get isReady(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Arm (or re-arm) the rolling idle-unload timer. Called after every embed so
|
|
66
|
+
* the window restarts on each use — an actively-embedding agent keeps the
|
|
67
|
+
* model resident; one that goes quiet for `idleUnloadMs` unloads it.
|
|
68
|
+
*/
|
|
69
|
+
private scheduleIdleUnload;
|
|
70
|
+
/**
|
|
71
|
+
* Dispose the loaded pipeline if it's idle — frees the ONNX thread pool that
|
|
72
|
+
* busy-spins even when no embedding is happening. Guarded on inFlight so a
|
|
73
|
+
* long-running batch that outlasts the window is never disposed mid-flight;
|
|
74
|
+
* its completion re-arms the timer.
|
|
75
|
+
*/
|
|
76
|
+
private maybeUnload;
|
|
77
|
+
/**
|
|
78
|
+
* Explicitly release the model + cancel the idle timer (shutdown / tests).
|
|
79
|
+
* Idempotent.
|
|
80
|
+
*/
|
|
81
|
+
dispose(): Promise<void>;
|
|
41
82
|
/**
|
|
42
83
|
* Generate a normalized embedding for a single text string.
|
|
43
84
|
* Lazy-initializes the model on first call.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbeddingProvider.d.ts","sourceRoot":"","sources":["../../src/memory/EmbeddingProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,KAAK,QAAQ,GAAG,OAAO,gBAAgB,EAAE,QAAQ,CAAC;AAElD,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"EmbeddingProvider.d.ts","sourceRoot":"","sources":["../../src/memory/EmbeddingProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,KAAK,QAAQ,GAAG,OAAO,gBAAgB,EAAE,QAAQ,CAAC;AAElD,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;CACvD;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAsC;IACvE,OAAO,CAAC,kBAAkB,CAAyB;IAEnD,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAK;IACrB,oGAAoG;IACpG,OAAO,CAAC,SAAS,CAA8C;gBAEnD,MAAM,CAAC,EAAE,uBAAuB;IAU5C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAWnB,SAAS;IAWvB;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;;;;OAKG;YACW,WAAW;IAazB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB9B;;;OAGG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAmBhD;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAmC1D;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO;IAgBvC,OAAO,CAAC,gBAAgB,CAA4C;IAEpE;;;;;OAKG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;IAcvC;;;OAGG;IACH,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM;IAIhD;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY;IAO/C;;;OAGG;IACH,MAAM,CAAC,gBAAgB,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,GAAG,MAAM;CAUlE"}
|
|
@@ -18,11 +18,19 @@ export class EmbeddingProvider {
|
|
|
18
18
|
modelName;
|
|
19
19
|
dimensions;
|
|
20
20
|
maxTextLength;
|
|
21
|
+
idleUnloadMs;
|
|
22
|
+
pipelineFactory;
|
|
21
23
|
vecExtensionLoaded = new WeakSet();
|
|
24
|
+
/** In-flight embed() calls — the idle-unload timer never disposes while > 0. */
|
|
25
|
+
inFlight = 0;
|
|
26
|
+
/** Rolling idle-unload timer (reset on every embed); unref'd so it can't keep the process alive. */
|
|
27
|
+
idleTimer = null;
|
|
22
28
|
constructor(config) {
|
|
23
29
|
this.modelName = config?.modelName ?? 'Xenova/all-MiniLM-L6-v2';
|
|
24
30
|
this.dimensions = config?.dimensions ?? 384;
|
|
25
31
|
this.maxTextLength = config?.maxTextLength ?? 8192;
|
|
32
|
+
this.idleUnloadMs = config?.idleUnloadMs ?? 300_000;
|
|
33
|
+
this.pipelineFactory = config?.pipelineFactory;
|
|
26
34
|
}
|
|
27
35
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
28
36
|
/**
|
|
@@ -39,6 +47,10 @@ export class EmbeddingProvider {
|
|
|
39
47
|
await this.loading;
|
|
40
48
|
}
|
|
41
49
|
async loadModel() {
|
|
50
|
+
if (this.pipelineFactory) {
|
|
51
|
+
this.pipeline = await this.pipelineFactory(this.modelName);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
42
54
|
const { pipeline: createPipeline } = await import('@huggingface/transformers');
|
|
43
55
|
this.pipeline = await createPipeline('feature-extraction', this.modelName, {
|
|
44
56
|
dtype: 'fp32',
|
|
@@ -50,20 +62,83 @@ export class EmbeddingProvider {
|
|
|
50
62
|
get isReady() {
|
|
51
63
|
return this.pipeline !== null;
|
|
52
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Arm (or re-arm) the rolling idle-unload timer. Called after every embed so
|
|
67
|
+
* the window restarts on each use — an actively-embedding agent keeps the
|
|
68
|
+
* model resident; one that goes quiet for `idleUnloadMs` unloads it.
|
|
69
|
+
*/
|
|
70
|
+
scheduleIdleUnload() {
|
|
71
|
+
if (this.idleUnloadMs <= 0)
|
|
72
|
+
return; // disabled — keep resident
|
|
73
|
+
if (this.idleTimer)
|
|
74
|
+
clearTimeout(this.idleTimer);
|
|
75
|
+
this.idleTimer = setTimeout(() => {
|
|
76
|
+
this.idleTimer = null;
|
|
77
|
+
void this.maybeUnload();
|
|
78
|
+
}, this.idleUnloadMs);
|
|
79
|
+
// Never let the idle timer hold the process open on its own.
|
|
80
|
+
this.idleTimer.unref?.();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Dispose the loaded pipeline if it's idle — frees the ONNX thread pool that
|
|
84
|
+
* busy-spins even when no embedding is happening. Guarded on inFlight so a
|
|
85
|
+
* long-running batch that outlasts the window is never disposed mid-flight;
|
|
86
|
+
* its completion re-arms the timer.
|
|
87
|
+
*/
|
|
88
|
+
async maybeUnload() {
|
|
89
|
+
if (this.inFlight > 0 || !this.pipeline)
|
|
90
|
+
return;
|
|
91
|
+
const p = this.pipeline;
|
|
92
|
+
this.pipeline = null;
|
|
93
|
+
this.loading = null;
|
|
94
|
+
try {
|
|
95
|
+
await p?.dispose?.();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// @silent-fallback-ok — dispose is best-effort cleanup; failing to free
|
|
99
|
+
// the session is not fatal (the next embed reloads a fresh pipeline).
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Explicitly release the model + cancel the idle timer (shutdown / tests).
|
|
104
|
+
* Idempotent.
|
|
105
|
+
*/
|
|
106
|
+
async dispose() {
|
|
107
|
+
if (this.idleTimer) {
|
|
108
|
+
clearTimeout(this.idleTimer);
|
|
109
|
+
this.idleTimer = null;
|
|
110
|
+
}
|
|
111
|
+
if (this.pipeline) {
|
|
112
|
+
const p = this.pipeline;
|
|
113
|
+
this.pipeline = null;
|
|
114
|
+
this.loading = null;
|
|
115
|
+
try {
|
|
116
|
+
await p?.dispose?.();
|
|
117
|
+
}
|
|
118
|
+
catch { /* @silent-fallback-ok — best-effort cleanup */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
53
121
|
// ─── Embedding ──────────────────────────────────────────────────
|
|
54
122
|
/**
|
|
55
123
|
* Generate a normalized embedding for a single text string.
|
|
56
124
|
* Lazy-initializes the model on first call.
|
|
57
125
|
*/
|
|
58
126
|
async embed(text) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
this.inFlight++;
|
|
128
|
+
try {
|
|
129
|
+
await this.initialize();
|
|
130
|
+
const truncated = text.slice(0, this.maxTextLength);
|
|
131
|
+
const output = await this.pipeline(truncated, {
|
|
132
|
+
pooling: 'mean',
|
|
133
|
+
normalize: true,
|
|
134
|
+
});
|
|
135
|
+
// output.data is a Float32Array from the ONNX model
|
|
136
|
+
return new Float32Array(output.data);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
this.inFlight--;
|
|
140
|
+
this.scheduleIdleUnload();
|
|
141
|
+
}
|
|
67
142
|
}
|
|
68
143
|
/**
|
|
69
144
|
* Generate normalized embeddings for multiple texts.
|
|
@@ -72,24 +147,33 @@ export class EmbeddingProvider {
|
|
|
72
147
|
async embedBatch(texts) {
|
|
73
148
|
if (texts.length === 0)
|
|
74
149
|
return [];
|
|
150
|
+
// Single-text delegates to embed(), which owns its own inFlight + idle-timer
|
|
151
|
+
// bookkeeping — don't double-count here.
|
|
75
152
|
if (texts.length === 1)
|
|
76
153
|
return [await this.embed(texts[0])];
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
for (
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
154
|
+
this.inFlight++;
|
|
155
|
+
try {
|
|
156
|
+
await this.initialize();
|
|
157
|
+
const truncated = texts.map(t => t.slice(0, this.maxTextLength));
|
|
158
|
+
const results = [];
|
|
159
|
+
// Process in batches of 32 to avoid memory pressure
|
|
160
|
+
const batchSize = 32;
|
|
161
|
+
for (let i = 0; i < truncated.length; i += batchSize) {
|
|
162
|
+
const batch = truncated.slice(i, i + batchSize);
|
|
163
|
+
for (const text of batch) {
|
|
164
|
+
const output = await this.pipeline(text, {
|
|
165
|
+
pooling: 'mean',
|
|
166
|
+
normalize: true,
|
|
167
|
+
});
|
|
168
|
+
results.push(new Float32Array(output.data));
|
|
169
|
+
}
|
|
90
170
|
}
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
this.inFlight--;
|
|
175
|
+
this.scheduleIdleUnload();
|
|
91
176
|
}
|
|
92
|
-
return results;
|
|
93
177
|
}
|
|
94
178
|
// ─── sqlite-vec Extension ───────────────────────────────────────
|
|
95
179
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmbeddingProvider.js","sourceRoot":"","sources":["../../src/memory/EmbeddingProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"EmbeddingProvider.js","sourceRoot":"","sources":["../../src/memory/EmbeddingProvider.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-30T11:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-30T11:54:17.258Z",
|
|
5
|
+
"instarVersion": "1.3.118",
|
|
6
6
|
"entryCount": 193,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Instar Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
**The shared local embedding model now unloads when idle, freeing the CPU its
|
|
8
|
+
ONNX thread pool was busy-spinning on.**
|
|
9
|
+
|
|
10
|
+
`EmbeddingProvider` (backing SemanticMemory / MemoryIndex / TopicMemory) loaded the
|
|
11
|
+
all-MiniLM-L6-v2 ONNX model lazily and then kept it resident forever. onnxruntime's
|
|
12
|
+
thread pool busy-spins even when idle — measured live via `process.cpuUsage()` at
|
|
13
|
+
~3.6% of a core on a quiet box, and previously ~44% on a contended agent that was
|
|
14
|
+
supposedly paused. With several agents per box each holding an idle, spinning
|
|
15
|
+
model, that's a real multiplied load source.
|
|
16
|
+
|
|
17
|
+
Fix: after `idleUnloadMs` (default 300000 = 5 min) with no `embed()`, the pipeline
|
|
18
|
+
is disposed (thread pool freed) and lazily reloaded on the next embed. A rolling
|
|
19
|
+
timer resets on every embed (active agents stay resident), an `inFlight` guard
|
|
20
|
+
prevents disposing mid-batch, and the timer is `unref()`'d. Set `idleUnloadMs: 0`
|
|
21
|
+
to keep the prior keep-resident behavior. Verified live: idle CPU 3.6% → 0.0% after
|
|
22
|
+
dispose, and dispose→reload→embed produces an identical vector.
|
|
23
|
+
|
|
24
|
+
## What to Tell Your User
|
|
25
|
+
|
|
26
|
+
Each agent keeps a small local AI model loaded to search its own memories, and the
|
|
27
|
+
engine running that model was quietly burning CPU even when the agent was idle —
|
|
28
|
+
its worker threads spin in a tight loop instead of sleeping. On a machine running
|
|
29
|
+
several agents, that added up to real wasted CPU. I changed it so the model unloads
|
|
30
|
+
after a few minutes of not being used and reloads in about a second the next time
|
|
31
|
+
it's needed. Active memory work is unaffected (the timer resets every time it's
|
|
32
|
+
used); only genuinely idle agents stop wasting CPU. I confirmed the model produces
|
|
33
|
+
the exact same results after an unload-and-reload, so nothing is lost.
|
|
34
|
+
|
|
35
|
+
## Summary of New Capabilities
|
|
36
|
+
|
|
37
|
+
| Capability | How to Use |
|
|
38
|
+
|-----------|-----------|
|
|
39
|
+
| Idle-unload of the embedding model | Automatic — frees the ONNX thread pool after ~5 min idle, reloads on next use |
|
|
40
|
+
| Keep the model resident | Construct the embedding provider with the idle window set to zero |
|
|
41
|
+
|
|
42
|
+
## Evidence
|
|
43
|
+
|
|
44
|
+
- **Live measurement:** loaded-but-idle model used ~3.6% of a core (3s window,
|
|
45
|
+
`process.cpuUsage()`); ~44% previously observed on a contended agent. After
|
|
46
|
+
dispose: 0.0%. dispose→reload→embed produced an identical embedding.
|
|
47
|
+
- **Tests:** `tests/unit/EmbeddingProvider-idle-unload.test.ts` (5, mock pipeline +
|
|
48
|
+
fake timers) — dispose-after-idle + lazy reload, resident-while-active,
|
|
49
|
+
disable-via-zero, inFlight guard, explicit dispose. `tsc` clean.
|
|
50
|
+
- Spec: `docs/specs/onnx-idle-unload.md`. Side-effects:
|
|
51
|
+
`upgrades/side-effects/onnx-idle-unload.md`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Side-effects — EmbeddingProvider idle-unload
|
|
2
|
+
|
|
3
|
+
**Change:** `EmbeddingProvider` disposes its loaded ONNX pipeline after
|
|
4
|
+
`idleUnloadMs` (default 5 min) of no `embed()` call, lazily reloading on the next
|
|
5
|
+
embed. New `inFlight` guard, rolling idle timer, and a public `dispose()`.
|
|
6
|
+
|
|
7
|
+
## Behavioral side-effects
|
|
8
|
+
|
|
9
|
+
- **First embed after a long idle costs ~1-3s more** (a model reload). All
|
|
10
|
+
subsequent embeds during active use are unaffected — the idle timer resets on
|
|
11
|
+
every embed, so a busy agent keeps the model resident. Memory search/index are
|
|
12
|
+
not latency-critical, so the occasional reload is an acceptable trade for
|
|
13
|
+
eliminating the idle thread-pool spin (~3.6%-44% of a core).
|
|
14
|
+
- **`isReady` is now dynamic.** Previously it returned true forever once loaded;
|
|
15
|
+
now it returns false while the model is unloaded between idle periods. No caller
|
|
16
|
+
uses `isReady` to gate embeds (verified) — `embed()` lazily re-initializes
|
|
17
|
+
regardless — so this is observational only.
|
|
18
|
+
- **Embeddings are unchanged.** Verified live: dispose→reload→embed produces a
|
|
19
|
+
byte-identical vector. Unloading/reloading never alters output.
|
|
20
|
+
- **Default-ON.** The production singleton constructs with defaults, so all agents
|
|
21
|
+
get 5-min idle-unload on update. Set `idleUnloadMs: 0` (constructor config) to
|
|
22
|
+
restore the prior keep-resident behavior.
|
|
23
|
+
|
|
24
|
+
## Blast radius
|
|
25
|
+
|
|
26
|
+
- Scoped to `src/memory/EmbeddingProvider.ts`. SemanticMemory / MemoryIndex /
|
|
27
|
+
TopicMemory consume it transparently (they call `embed()`/`embedBatch()`, which
|
|
28
|
+
handle reload internally). No call-site changes.
|
|
29
|
+
- The idle timer is `unref()`'d — it can never keep the process alive.
|
|
30
|
+
- No config schema, hook, or HTTP changes. No new dependencies.
|
|
31
|
+
|
|
32
|
+
## Migration parity
|
|
33
|
+
|
|
34
|
+
None required — internal runtime logic in `src/`. No agent-installed file
|
|
35
|
+
(`.claude/settings.json`, `.instar/config.json`, CLAUDE.md template, hook, skill)
|
|
36
|
+
changes. Existing agents receive it on their normal version update.
|