@yadsh/dsh-kv-persist 0.1.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/LICENSE +21 -0
- package/README.md +110 -0
- package/compatibility.json +16 -0
- package/cordis.patch.yml +4 -0
- package/docs/dsh-kv-persist.md +3311 -0
- package/lib/backends/llama-cpp/backend.d.ts +20 -0
- package/lib/backends/llama-cpp/backend.d.ts.map +1 -0
- package/lib/backends/llama-cpp/backend.js +37 -0
- package/lib/backends/llama-cpp/backend.js.map +1 -0
- package/lib/backends/llama-cpp/client.d.ts +31 -0
- package/lib/backends/llama-cpp/client.d.ts.map +1 -0
- package/lib/backends/llama-cpp/client.js +120 -0
- package/lib/backends/llama-cpp/client.js.map +1 -0
- package/lib/backends/llama-cpp/discovery.d.ts +20 -0
- package/lib/backends/llama-cpp/discovery.d.ts.map +1 -0
- package/lib/backends/llama-cpp/discovery.js +33 -0
- package/lib/backends/llama-cpp/discovery.js.map +1 -0
- package/lib/backends/llama-cpp/types.d.ts +30 -0
- package/lib/backends/llama-cpp/types.d.ts.map +1 -0
- package/lib/backends/llama-cpp/types.js +9 -0
- package/lib/backends/llama-cpp/types.js.map +1 -0
- package/lib/backends/types.d.ts +52 -0
- package/lib/backends/types.d.ts.map +1 -0
- package/lib/backends/types.js +6 -0
- package/lib/backends/types.js.map +1 -0
- package/lib/config.d.ts +258 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +169 -0
- package/lib/config.js.map +1 -0
- package/lib/coordinator/checkpoint-policy.d.ts +19 -0
- package/lib/coordinator/checkpoint-policy.d.ts.map +1 -0
- package/lib/coordinator/checkpoint-policy.js +40 -0
- package/lib/coordinator/checkpoint-policy.js.map +1 -0
- package/lib/coordinator/circuit-breaker.d.ts +29 -0
- package/lib/coordinator/circuit-breaker.d.ts.map +1 -0
- package/lib/coordinator/circuit-breaker.js +62 -0
- package/lib/coordinator/circuit-breaker.js.map +1 -0
- package/lib/coordinator/coordinator.d.ts +74 -0
- package/lib/coordinator/coordinator.d.ts.map +1 -0
- package/lib/coordinator/coordinator.js +542 -0
- package/lib/coordinator/coordinator.js.map +1 -0
- package/lib/coordinator/slot-lease.d.ts +22 -0
- package/lib/coordinator/slot-lease.d.ts.map +1 -0
- package/lib/coordinator/slot-lease.js +38 -0
- package/lib/coordinator/slot-lease.js.map +1 -0
- package/lib/coordinator/state-machine.d.ts +65 -0
- package/lib/coordinator/state-machine.d.ts.map +1 -0
- package/lib/coordinator/state-machine.js +40 -0
- package/lib/coordinator/state-machine.js.map +1 -0
- package/lib/errors.d.ts +82 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +113 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/lib/logging/dsh-home.d.ts +13 -0
- package/lib/logging/dsh-home.d.ts.map +1 -0
- package/lib/logging/dsh-home.js +18 -0
- package/lib/logging/dsh-home.js.map +1 -0
- package/lib/logging/index.d.ts +10 -0
- package/lib/logging/index.d.ts.map +1 -0
- package/lib/logging/index.js +10 -0
- package/lib/logging/index.js.map +1 -0
- package/lib/logging/plugin-logger.d.ts +94 -0
- package/lib/logging/plugin-logger.d.ts.map +1 -0
- package/lib/logging/plugin-logger.js +397 -0
- package/lib/logging/plugin-logger.js.map +1 -0
- package/lib/observability/diagnostics.d.ts +37 -0
- package/lib/observability/diagnostics.d.ts.map +1 -0
- package/lib/observability/diagnostics.js +75 -0
- package/lib/observability/diagnostics.js.map +1 -0
- package/lib/observability/metrics.d.ts +38 -0
- package/lib/observability/metrics.d.ts.map +1 -0
- package/lib/observability/metrics.js +29 -0
- package/lib/observability/metrics.js.map +1 -0
- package/lib/service.d.ts +231 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +232 -0
- package/lib/service.js.map +1 -0
- package/lib/snapshots/fingerprint.d.ts +53 -0
- package/lib/snapshots/fingerprint.d.ts.map +1 -0
- package/lib/snapshots/fingerprint.js +69 -0
- package/lib/snapshots/fingerprint.js.map +1 -0
- package/lib/snapshots/manifest.d.ts +51 -0
- package/lib/snapshots/manifest.d.ts.map +1 -0
- package/lib/snapshots/manifest.js +0 -0
- package/lib/snapshots/manifest.js.map +1 -0
- package/lib/snapshots/naming.d.ts +18 -0
- package/lib/snapshots/naming.d.ts.map +1 -0
- package/lib/snapshots/naming.js +33 -0
- package/lib/snapshots/naming.js.map +1 -0
- package/lib/snapshots/repository.d.ts +52 -0
- package/lib/snapshots/repository.d.ts.map +1 -0
- package/lib/snapshots/repository.js +270 -0
- package/lib/snapshots/repository.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,3311 @@
|
|
|
1
|
+
# dsh-kv-persist
|
|
2
|
+
|
|
3
|
+
> Persistent KV-cache/session-state manager for DeepSeek Harness.
|
|
4
|
+
|
|
5
|
+
**Status:** Draft / Initial Specification
|
|
6
|
+
**Target:** DeepSeek Harness + llama.cpp `llama-server`
|
|
7
|
+
**Initial version:** `0.1.0`
|
|
8
|
+
**Primary backend:** llama.cpp slot save/restore API
|
|
9
|
+
**Future backends:** vLLM / SGLang / other providers exposing reusable prefix/session state
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Summary
|
|
14
|
+
|
|
15
|
+
`dsh-kv-persist` is a DeepSeek Harness infrastructure plugin that persists LLM runtime cache state between sessions and process restarts.
|
|
16
|
+
|
|
17
|
+
The first implementation targets `llama-server` and its slot persistence API:
|
|
18
|
+
|
|
19
|
+
- `GET /slots`
|
|
20
|
+
- `POST /slots/{id}?action=save`
|
|
21
|
+
- `POST /slots/{id}?action=restore`
|
|
22
|
+
- `POST /slots/{id}?action=erase`
|
|
23
|
+
|
|
24
|
+
llama.cpp can persist a slot's prompt/KV state into a file under `--slot-save-path`, then restore that state later.
|
|
25
|
+
|
|
26
|
+
The plugin maps:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
DSH session
|
|
30
|
+
↓
|
|
31
|
+
provider + model + server instance
|
|
32
|
+
↓
|
|
33
|
+
llama.cpp slot
|
|
34
|
+
↓
|
|
35
|
+
persistent snapshot
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Its main purpose is to avoid re-prefilling tens or hundreds of thousands of tokens whenever:
|
|
39
|
+
|
|
40
|
+
- the user switches between DSH sessions;
|
|
41
|
+
- `llama-server` evicts a session from its active slot;
|
|
42
|
+
- DSH is restarted;
|
|
43
|
+
- `llama-server` is restarted;
|
|
44
|
+
- another request temporarily pollutes the active slot;
|
|
45
|
+
- multiple projects share the same local model server.
|
|
46
|
+
|
|
47
|
+
For a large agent prompt this can turn:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
restore session
|
|
51
|
+
→ process 40K–100K prompt tokens again
|
|
52
|
+
→ wait tens/hundreds of seconds
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
into:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
restore session
|
|
59
|
+
→ load KV snapshot
|
|
60
|
+
→ process only changed suffix
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# 2. Goals
|
|
66
|
+
|
|
67
|
+
The plugin MUST:
|
|
68
|
+
|
|
69
|
+
1. Associate persistent cache snapshots with DSH `session.id`.
|
|
70
|
+
2. Automatically restore the appropriate snapshot before a session resumes.
|
|
71
|
+
3. Automatically save dirty cache state according to a configurable checkpoint policy.
|
|
72
|
+
4. Prevent one DSH session from accidentally reusing another session's slot as authoritative state.
|
|
73
|
+
5. Detect incompatible or stale snapshots and fail safely.
|
|
74
|
+
6. Never make the model-visible conversation depend on the snapshot.
|
|
75
|
+
7. Treat KV persistence purely as an optimization.
|
|
76
|
+
8. Fall back to normal cold prompt processing whenever persistence is unavailable.
|
|
77
|
+
9. Work with a normal OpenAI-compatible `llama-server`.
|
|
78
|
+
10. Provide useful observability:
|
|
79
|
+
- restore hit/miss;
|
|
80
|
+
- bytes saved/read;
|
|
81
|
+
- save/restore latency;
|
|
82
|
+
- cache tokens;
|
|
83
|
+
- slot ownership;
|
|
84
|
+
- snapshot age;
|
|
85
|
+
- cold fallback count.
|
|
86
|
+
11. Survive plugin hot reload and normal Cordis disposal cleanly.
|
|
87
|
+
12. Be backend-agnostic internally even though llama.cpp is the first backend.
|
|
88
|
+
|
|
89
|
+
DeepSeek Harness already treats sessions as append-only durable state and derives LLM messages from that state, so persisted KV MUST remain a disposable acceleration layer rather than a second source of truth.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# 3. Non-goals
|
|
94
|
+
|
|
95
|
+
Initial versions MUST NOT:
|
|
96
|
+
|
|
97
|
+
- replace DSH session persistence;
|
|
98
|
+
- store actual conversation history as the authoritative state;
|
|
99
|
+
- attempt to reconstruct missing DSH events from KV;
|
|
100
|
+
- modify model-visible prompts to improve cache hits;
|
|
101
|
+
- expose KV management as an LLM tool;
|
|
102
|
+
- assume snapshots are portable between different models;
|
|
103
|
+
- assume snapshots are portable between arbitrary llama.cpp builds;
|
|
104
|
+
- manage llama-server process startup itself;
|
|
105
|
+
- transparently migrate a snapshot between machines;
|
|
106
|
+
- promise persistence support for every recurrent/hybrid model;
|
|
107
|
+
- require DSH UI modifications.
|
|
108
|
+
|
|
109
|
+
The model should ideally never know this plugin exists.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# 4. Core design principle
|
|
114
|
+
|
|
115
|
+
The following relationship is fundamental:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
DSH session log = truth
|
|
119
|
+
KV snapshot = derived cache
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A snapshot can always be deleted.
|
|
123
|
+
|
|
124
|
+
Deleting:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
$DSH_HOME/cache/dsh-kv-persist/...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
must never destroy the conversation.
|
|
131
|
+
|
|
132
|
+
The worst possible consequence of missing or invalid KV state must be:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
cold prefill
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
not:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
corrupted conversation
|
|
142
|
+
wrong session
|
|
143
|
+
missing messages
|
|
144
|
+
incorrect continuation
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
# 5. Relevant DSH architecture
|
|
150
|
+
|
|
151
|
+
DSH exposes an in-memory `ctx.sessions` service.
|
|
152
|
+
|
|
153
|
+
A `Session` has a stable `session.id` and an append-only sequence of `SessionEvent`s. LLM history is derived from those events rather than stored as a separate mutable chat history. DSH persistence plugins can observe `session/event`, `session/flush`, `session/created`, and `session/disposed`.
|
|
154
|
+
|
|
155
|
+
Model calls eventually pass through:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
agent loop
|
|
159
|
+
↓
|
|
160
|
+
agent/request
|
|
161
|
+
↓
|
|
162
|
+
prepareCall()
|
|
163
|
+
↓
|
|
164
|
+
llm/stream
|
|
165
|
+
↓
|
|
166
|
+
LLM adapter
|
|
167
|
+
↓
|
|
168
|
+
llama-server
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`llm/stream` is explicitly intended to support infrastructure such as caching/logging/routing. Loop-generated `GenerateOptions` are deep-frozen and must be observed rather than mutated.
|
|
172
|
+
|
|
173
|
+
`GenerateOptions` also carries:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
sessionId?: SessionId
|
|
177
|
+
purpose?: 'compaction' | 'session-title'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
which gives the plugin enough information to distinguish ordinary session inference from auxiliary LLM requests.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
# 6. llama.cpp backend
|
|
185
|
+
|
|
186
|
+
## 6.1 Server requirements
|
|
187
|
+
|
|
188
|
+
The llama server should be started with at least:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
llama-server \
|
|
192
|
+
... \
|
|
193
|
+
--slots \
|
|
194
|
+
--slot-save-path /some/path
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
For the initial MVP:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
--parallel 1
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
is strongly recommended.
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
./llama-server \
|
|
209
|
+
-m Qwen3.8-27B-GSQ-RCO-IQ2_XS.gguf \
|
|
210
|
+
--ctx-size 131072 \
|
|
211
|
+
--parallel 1 \
|
|
212
|
+
--flash-attn on \
|
|
213
|
+
--cache-type-k q4_0 \
|
|
214
|
+
--cache-type-v q4_0 \
|
|
215
|
+
--n-gpu-layers all \
|
|
216
|
+
--slot-save-path E:/LLM/kv-cache \
|
|
217
|
+
--host 127.0.0.1 \
|
|
218
|
+
--port 8080
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# 7. Why the MVP should use one slot
|
|
224
|
+
|
|
225
|
+
With:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
--parallel 1
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
there is one inference slot:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
slot 0
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Therefore the plugin doesn't need to modify OpenAI request bodies.
|
|
238
|
+
|
|
239
|
+
It can treat slot `0` as an expensive hardware-backed working register:
|
|
240
|
+
|
|
241
|
+
```text
|
|
242
|
+
┌──────────────┐
|
|
243
|
+
session A ─────► │ │
|
|
244
|
+
session B ─────► │ slot 0 │
|
|
245
|
+
session C ─────► │ │
|
|
246
|
+
└──────────────┘
|
|
247
|
+
↕
|
|
248
|
+
snapshots
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Switching A → B becomes:
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
save A if dirty
|
|
255
|
+
restore B
|
|
256
|
+
run B
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Switching B → A:
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
save B if dirty
|
|
263
|
+
restore A
|
|
264
|
+
run A
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
This makes an excellent first implementation because:
|
|
268
|
+
|
|
269
|
+
- there is no slot allocator;
|
|
270
|
+
- there are no slot races;
|
|
271
|
+
- no llama-specific fields have to enter DSH `GenerateOptions`;
|
|
272
|
+
- the existing DSH LLM adapter can remain untouched.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
# 8. Multi-slot limitation
|
|
277
|
+
|
|
278
|
+
llama.cpp supports specifying:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"id_slot": 2
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
on inference requests, allowing a client to explicitly select a slot.
|
|
287
|
+
|
|
288
|
+
However DSH's `GenerateOptions` deliberately contains provider-neutral fields and loop requests are immutable. `llm/stream` middleware therefore cannot safely do:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
options.id_slot = 2
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
or otherwise inject arbitrary llama.cpp transport fields.
|
|
295
|
+
|
|
296
|
+
Consequently:
|
|
297
|
+
|
|
298
|
+
```text
|
|
299
|
+
v0.1:
|
|
300
|
+
parallel=1
|
|
301
|
+
generic middleware
|
|
302
|
+
slot 0
|
|
303
|
+
|
|
304
|
+
v0.2+:
|
|
305
|
+
managed multi-slot mode
|
|
306
|
+
custom llama.cpp transport adapter
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
This separation should be intentional.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
# 9. High-level architecture
|
|
314
|
+
|
|
315
|
+
```text
|
|
316
|
+
┌──────────────────────────────────────────────────────┐
|
|
317
|
+
│ DeepSeek Harness │
|
|
318
|
+
│ │
|
|
319
|
+
│ ctx.sessions │
|
|
320
|
+
│ │ │
|
|
321
|
+
│ ├──── session lifecycle ───────┐ │
|
|
322
|
+
│ │ │ │
|
|
323
|
+
│ ctx.llm │ │
|
|
324
|
+
│ │ │ │
|
|
325
|
+
│ └──── llm/stream ──────────────┤ │
|
|
326
|
+
│ ▼ │
|
|
327
|
+
│ KvPersistService │
|
|
328
|
+
│ / │ \ │
|
|
329
|
+
│ / │ \ │
|
|
330
|
+
│ Coordinator Metadata Metrics │
|
|
331
|
+
│ │ │
|
|
332
|
+
│ ▼ │
|
|
333
|
+
│ KvBackend interface │
|
|
334
|
+
│ │ │
|
|
335
|
+
│ ▼ │
|
|
336
|
+
│ LlamaCppBackend │
|
|
337
|
+
└───────────────────────┼──────────────────────────────┘
|
|
338
|
+
│ HTTP
|
|
339
|
+
▼
|
|
340
|
+
┌───────────────┐
|
|
341
|
+
│ llama-server │
|
|
342
|
+
│ │
|
|
343
|
+
│ slot 0 │
|
|
344
|
+
│ /slots API │
|
|
345
|
+
└───────┬───────┘
|
|
346
|
+
│
|
|
347
|
+
▼
|
|
348
|
+
--slot-save-path
|
|
349
|
+
│
|
|
350
|
+
┌────────────────┼────────────────┐
|
|
351
|
+
▼ ▼ ▼
|
|
352
|
+
session-A.bin session-B.bin session-C.bin
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
# 10. Package layout
|
|
358
|
+
|
|
359
|
+
Recommended repository structure:
|
|
360
|
+
|
|
361
|
+
```text
|
|
362
|
+
dsh-kv-persist/
|
|
363
|
+
├─ src/
|
|
364
|
+
│ ├─ index.ts
|
|
365
|
+
│ ├─ config.ts
|
|
366
|
+
│ ├─ service.ts
|
|
367
|
+
│ │
|
|
368
|
+
│ ├─ coordinator/
|
|
369
|
+
│ │ ├─ coordinator.ts
|
|
370
|
+
│ │ ├─ slot-lease.ts
|
|
371
|
+
│ │ ├─ state-machine.ts
|
|
372
|
+
│ │ └─ checkpoint-policy.ts
|
|
373
|
+
│ │
|
|
374
|
+
│ ├─ backends/
|
|
375
|
+
│ │ ├─ types.ts
|
|
376
|
+
│ │ └─ llama-cpp/
|
|
377
|
+
│ │ ├─ backend.ts
|
|
378
|
+
│ │ ├─ client.ts
|
|
379
|
+
│ │ ├─ discovery.ts
|
|
380
|
+
│ │ ├─ compatibility.ts
|
|
381
|
+
│ │ └─ types.ts
|
|
382
|
+
│ │
|
|
383
|
+
│ ├─ snapshots/
|
|
384
|
+
│ │ ├─ manifest.ts
|
|
385
|
+
│ │ ├─ naming.ts
|
|
386
|
+
│ │ ├─ fingerprint.ts
|
|
387
|
+
│ │ └─ repository.ts
|
|
388
|
+
│ │
|
|
389
|
+
│ ├─ observability/
|
|
390
|
+
│ │ ├─ metrics.ts
|
|
391
|
+
│ │ └─ diagnostics.ts
|
|
392
|
+
│ │
|
|
393
|
+
│ └─ errors.ts
|
|
394
|
+
│
|
|
395
|
+
├─ test/
|
|
396
|
+
│ ├─ unit/
|
|
397
|
+
│ ├─ integration/
|
|
398
|
+
│ └─ fixtures/
|
|
399
|
+
│
|
|
400
|
+
├─ cordis.patch.yml
|
|
401
|
+
├─ package.json
|
|
402
|
+
├─ tsconfig.json
|
|
403
|
+
├─ README.md
|
|
404
|
+
└─ SPEC.md
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Do not put the entire implementation into `index.ts`.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
# 11. Public service
|
|
412
|
+
|
|
413
|
+
The plugin SHOULD expose:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
ctx.kvPersist
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
through a Cordis `Service`.
|
|
420
|
+
|
|
421
|
+
Approximate API:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
interface KvPersistService {
|
|
425
|
+
status(): Promise<KvPersistStatus>
|
|
426
|
+
|
|
427
|
+
getSessionState(
|
|
428
|
+
sessionId: string,
|
|
429
|
+
): Promise<SessionKvState | undefined>
|
|
430
|
+
|
|
431
|
+
save(
|
|
432
|
+
sessionId: string,
|
|
433
|
+
options?: SaveOptions,
|
|
434
|
+
): Promise<SnapshotResult>
|
|
435
|
+
|
|
436
|
+
restore(
|
|
437
|
+
sessionId: string,
|
|
438
|
+
options?: RestoreOptions,
|
|
439
|
+
): Promise<RestoreResult>
|
|
440
|
+
|
|
441
|
+
invalidate(
|
|
442
|
+
sessionId: string,
|
|
443
|
+
reason?: string,
|
|
444
|
+
): Promise<void>
|
|
445
|
+
|
|
446
|
+
purge(
|
|
447
|
+
sessionId: string,
|
|
448
|
+
): Promise<void>
|
|
449
|
+
|
|
450
|
+
flush(): Promise<void>
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The service makes future UI/CLI plugins possible without depending directly on llama.cpp.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
# 12. Backend abstraction
|
|
459
|
+
|
|
460
|
+
Core code must NOT contain direct `/slots` HTTP calls.
|
|
461
|
+
|
|
462
|
+
Define:
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
interface KvPersistenceBackend {
|
|
466
|
+
readonly kind: string
|
|
467
|
+
|
|
468
|
+
probe(signal?: AbortSignal): Promise<BackendCapabilities>
|
|
469
|
+
|
|
470
|
+
inspectSlots(
|
|
471
|
+
signal?: AbortSignal,
|
|
472
|
+
): Promise<BackendSlot[]>
|
|
473
|
+
|
|
474
|
+
saveSlot(
|
|
475
|
+
slotId: number,
|
|
476
|
+
snapshotKey: string,
|
|
477
|
+
signal?: AbortSignal,
|
|
478
|
+
): Promise<BackendSaveResult>
|
|
479
|
+
|
|
480
|
+
restoreSlot(
|
|
481
|
+
slotId: number,
|
|
482
|
+
snapshotKey: string,
|
|
483
|
+
signal?: AbortSignal,
|
|
484
|
+
): Promise<BackendRestoreResult>
|
|
485
|
+
|
|
486
|
+
eraseSlot(
|
|
487
|
+
slotId: number,
|
|
488
|
+
signal?: AbortSignal,
|
|
489
|
+
): Promise<BackendEraseResult>
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Initial implementation:
|
|
494
|
+
|
|
495
|
+
```text
|
|
496
|
+
KvPersistenceBackend
|
|
497
|
+
└── LlamaCppBackend
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Potential future implementations:
|
|
501
|
+
|
|
502
|
+
```text
|
|
503
|
+
KvPersistenceBackend
|
|
504
|
+
├── LlamaCppBackend
|
|
505
|
+
├── SglangBackend
|
|
506
|
+
├── VllmBackend
|
|
507
|
+
└── CustomGatewayBackend
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
# 13. Snapshot identity
|
|
513
|
+
|
|
514
|
+
A snapshot must never be identified by `sessionId` alone.
|
|
515
|
+
|
|
516
|
+
Conceptual key:
|
|
517
|
+
|
|
518
|
+
```text
|
|
519
|
+
SnapshotKey =
|
|
520
|
+
server instance
|
|
521
|
+
+ provider
|
|
522
|
+
+ model
|
|
523
|
+
+ DSH session
|
|
524
|
+
+ compatibility generation
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Example:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
interface SnapshotIdentity {
|
|
531
|
+
sessionId: string
|
|
532
|
+
|
|
533
|
+
provider: string
|
|
534
|
+
model: string
|
|
535
|
+
|
|
536
|
+
backend: 'llama.cpp'
|
|
537
|
+
|
|
538
|
+
serverInstanceKey: string
|
|
539
|
+
modelFingerprint: string
|
|
540
|
+
|
|
541
|
+
compatibilityVersion: number
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
# 14. Snapshot metadata
|
|
548
|
+
|
|
549
|
+
The plugin keeps its own small metadata record independently from the potentially huge `.bin` file.
|
|
550
|
+
|
|
551
|
+
Example:
|
|
552
|
+
|
|
553
|
+
```json
|
|
554
|
+
{
|
|
555
|
+
"schemaVersion": 1,
|
|
556
|
+
|
|
557
|
+
"sessionId": "01991d...",
|
|
558
|
+
"sessionSeq": 341,
|
|
559
|
+
|
|
560
|
+
"provider": "local-qwen",
|
|
561
|
+
"model": "Qwen3.8-27B-GSQ-RCO-IQ2_XS.gguf",
|
|
562
|
+
|
|
563
|
+
"backend": "llama.cpp",
|
|
564
|
+
"serverInstanceKey": "local-3060",
|
|
565
|
+
"serverEndpointHash": "sha256:...",
|
|
566
|
+
|
|
567
|
+
"modelFingerprint": "sha256:...",
|
|
568
|
+
"runtimeFingerprint": "sha256:...",
|
|
569
|
+
|
|
570
|
+
"slotId": 0,
|
|
571
|
+
|
|
572
|
+
"createdAt": "2026-08-29T20:00:00Z",
|
|
573
|
+
"updatedAt": "2026-08-29T20:42:00Z",
|
|
574
|
+
|
|
575
|
+
"tokens": 48321,
|
|
576
|
+
"bytes": 2384203912,
|
|
577
|
+
|
|
578
|
+
"snapshotFilename": "b3-c5-....bin",
|
|
579
|
+
|
|
580
|
+
"state": "ready"
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
# 15. Runtime fingerprint
|
|
587
|
+
|
|
588
|
+
Restoring arbitrary binary model state is dangerous.
|
|
589
|
+
|
|
590
|
+
Snapshots should therefore have a compatibility fingerprint.
|
|
591
|
+
|
|
592
|
+
Suggested fingerprint inputs:
|
|
593
|
+
|
|
594
|
+
```text
|
|
595
|
+
backend type
|
|
596
|
+
llama.cpp server/build version, when discoverable
|
|
597
|
+
model identifier
|
|
598
|
+
model file fingerprint/configured model key
|
|
599
|
+
context size
|
|
600
|
+
KV K type
|
|
601
|
+
KV V type
|
|
602
|
+
parallel/slot geometry
|
|
603
|
+
relevant recurrent/hybrid mode
|
|
604
|
+
LoRA configuration
|
|
605
|
+
speculative decoding configuration
|
|
606
|
+
plugin snapshot schema generation
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
For values the server cannot expose reliably, configuration should allow an explicit:
|
|
610
|
+
|
|
611
|
+
```yaml
|
|
612
|
+
runtimeKey: qwen38-27b-iq2xs-ctx128k-q4kv-v1
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Changing `runtimeKey` makes old snapshots invisible.
|
|
616
|
+
|
|
617
|
+
This provides a simple manual escape hatch.
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
# 16. Filename generation
|
|
622
|
+
|
|
623
|
+
Raw session IDs or titles SHOULD NOT become filenames.
|
|
624
|
+
|
|
625
|
+
Use:
|
|
626
|
+
|
|
627
|
+
```text
|
|
628
|
+
sha256(
|
|
629
|
+
backendInstanceKey +
|
|
630
|
+
provider +
|
|
631
|
+
model +
|
|
632
|
+
sessionId
|
|
633
|
+
)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Example:
|
|
637
|
+
|
|
638
|
+
```text
|
|
639
|
+
7c/7c856dc594.........bin
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Benefits:
|
|
643
|
+
|
|
644
|
+
- fixed filename length;
|
|
645
|
+
- no unsafe characters;
|
|
646
|
+
- no path traversal;
|
|
647
|
+
- no leaking chat titles;
|
|
648
|
+
- no collisions in normal operation.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
# 17. Snapshot storage
|
|
653
|
+
|
|
654
|
+
Important distinction:
|
|
655
|
+
|
|
656
|
+
```text
|
|
657
|
+
metadata storage
|
|
658
|
+
≠
|
|
659
|
+
KV binary storage
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
The KV binary is created by `llama-server` itself inside:
|
|
663
|
+
|
|
664
|
+
```text
|
|
665
|
+
--slot-save-path
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
The DSH plugin may not even have filesystem access to that directory.
|
|
669
|
+
|
|
670
|
+
Therefore the architecture must support:
|
|
671
|
+
|
|
672
|
+
### Metadata
|
|
673
|
+
|
|
674
|
+
Stored locally by the plugin:
|
|
675
|
+
|
|
676
|
+
```text
|
|
677
|
+
$DSH_HOME/cache/dsh-kv-persist/
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Binary
|
|
681
|
+
|
|
682
|
+
Stored by llama-server:
|
|
683
|
+
|
|
684
|
+
```text
|
|
685
|
+
<LLAMA_SLOT_SAVE_PATH>/
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
The plugin communicates using only the generated filename.
|
|
689
|
+
|
|
690
|
+
This allows:
|
|
691
|
+
|
|
692
|
+
```text
|
|
693
|
+
DSH in container A
|
|
694
|
+
llama-server in container B
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
provided the server owns its snapshot directory.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
# 18. Session state machine
|
|
702
|
+
|
|
703
|
+
Each session can be:
|
|
704
|
+
|
|
705
|
+
```text
|
|
706
|
+
NONE
|
|
707
|
+
no known snapshot
|
|
708
|
+
|
|
709
|
+
COLD
|
|
710
|
+
session exists but no state loaded
|
|
711
|
+
|
|
712
|
+
RESTORING
|
|
713
|
+
snapshot restore in progress
|
|
714
|
+
|
|
715
|
+
ACTIVE_CLEAN
|
|
716
|
+
current server slot corresponds to snapshot
|
|
717
|
+
|
|
718
|
+
ACTIVE_DIRTY
|
|
719
|
+
model has advanced beyond saved snapshot
|
|
720
|
+
|
|
721
|
+
SAVING
|
|
722
|
+
persistence operation in progress
|
|
723
|
+
|
|
724
|
+
SAVED
|
|
725
|
+
latest runtime state has durable snapshot
|
|
726
|
+
|
|
727
|
+
INVALID
|
|
728
|
+
snapshot exists but is incompatible/corrupted
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Typical transitions:
|
|
732
|
+
|
|
733
|
+
```text
|
|
734
|
+
NONE
|
|
735
|
+
↓ first request
|
|
736
|
+
COLD
|
|
737
|
+
↓ normal prefill
|
|
738
|
+
ACTIVE_DIRTY
|
|
739
|
+
↓ checkpoint
|
|
740
|
+
SAVING
|
|
741
|
+
↓
|
|
742
|
+
SAVED
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Resume:
|
|
746
|
+
|
|
747
|
+
```text
|
|
748
|
+
SAVED
|
|
749
|
+
↓ session request
|
|
750
|
+
RESTORING
|
|
751
|
+
↓
|
|
752
|
+
ACTIVE_CLEAN
|
|
753
|
+
↓ inference
|
|
754
|
+
ACTIVE_DIRTY
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Session switch:
|
|
758
|
+
|
|
759
|
+
```text
|
|
760
|
+
A ACTIVE_DIRTY
|
|
761
|
+
↓
|
|
762
|
+
save A
|
|
763
|
+
↓
|
|
764
|
+
A SAVED
|
|
765
|
+
↓
|
|
766
|
+
restore B
|
|
767
|
+
↓
|
|
768
|
+
B ACTIVE_CLEAN
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
# 19. Slot state
|
|
774
|
+
|
|
775
|
+
The coordinator separately tracks the physical slot.
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
interface ManagedSlot {
|
|
779
|
+
id: number
|
|
780
|
+
|
|
781
|
+
ownerSessionId?: string
|
|
782
|
+
|
|
783
|
+
snapshotRevision?: string
|
|
784
|
+
|
|
785
|
+
state:
|
|
786
|
+
| 'unknown'
|
|
787
|
+
| 'idle'
|
|
788
|
+
| 'restoring'
|
|
789
|
+
| 'ready'
|
|
790
|
+
| 'inference'
|
|
791
|
+
| 'dirty'
|
|
792
|
+
| 'saving'
|
|
793
|
+
| 'broken'
|
|
794
|
+
|
|
795
|
+
lastUsedAt?: number
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
For v0.1 there is simply:
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
slot[0]
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
# 20. Critical concurrency rule
|
|
808
|
+
|
|
809
|
+
No two operations may concurrently mutate the same llama slot.
|
|
810
|
+
|
|
811
|
+
The following must share one lock:
|
|
812
|
+
|
|
813
|
+
```text
|
|
814
|
+
restore
|
|
815
|
+
erase
|
|
816
|
+
inference
|
|
817
|
+
save
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
For MVP:
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
serverMutex.runExclusive(...)
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
is sufficient.
|
|
827
|
+
|
|
828
|
+
Conceptually:
|
|
829
|
+
|
|
830
|
+
```text
|
|
831
|
+
acquire slot lease
|
|
832
|
+
↓
|
|
833
|
+
prepare slot
|
|
834
|
+
↓
|
|
835
|
+
run inference
|
|
836
|
+
↓
|
|
837
|
+
update dirty state
|
|
838
|
+
↓
|
|
839
|
+
optional checkpoint
|
|
840
|
+
↓
|
|
841
|
+
release slot lease
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
This lock is absolutely critical.
|
|
845
|
+
|
|
846
|
+
Without it:
|
|
847
|
+
|
|
848
|
+
```text
|
|
849
|
+
session A restoring
|
|
850
|
+
+
|
|
851
|
+
session B starting request
|
|
852
|
+
=
|
|
853
|
+
undefined cache ownership
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
# 21. Request interception
|
|
859
|
+
|
|
860
|
+
The plugin listens to:
|
|
861
|
+
|
|
862
|
+
```text
|
|
863
|
+
llm/stream
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
For each request:
|
|
867
|
+
|
|
868
|
+
```ts
|
|
869
|
+
if (!isManagedProvider(options.provider)) {
|
|
870
|
+
return next()
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (!options.sessionId) {
|
|
874
|
+
return handleAuxiliaryRequest(...)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return coordinator.runSessionRequest({
|
|
878
|
+
sessionId: options.sessionId,
|
|
879
|
+
provider: options.provider,
|
|
880
|
+
model: options.model,
|
|
881
|
+
purpose: options.purpose,
|
|
882
|
+
next,
|
|
883
|
+
})
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
`llm/stream` is an appropriate seam because it wraps every actual streaming model invocation, including adapter dispatch.
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
# 22. Auxiliary requests
|
|
891
|
+
|
|
892
|
+
DSH may make model calls for:
|
|
893
|
+
|
|
894
|
+
```text
|
|
895
|
+
session-title
|
|
896
|
+
compaction
|
|
897
|
+
other future purposes
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
They can pollute slot 0.
|
|
901
|
+
|
|
902
|
+
Therefore they must explicitly participate in coordination.
|
|
903
|
+
|
|
904
|
+
Default:
|
|
905
|
+
|
|
906
|
+
```yaml
|
|
907
|
+
auxiliaryRequests: isolate
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
In single-slot mode:
|
|
911
|
+
|
|
912
|
+
```text
|
|
913
|
+
main session dirty
|
|
914
|
+
↓
|
|
915
|
+
save current main session
|
|
916
|
+
↓
|
|
917
|
+
auxiliary request
|
|
918
|
+
↓
|
|
919
|
+
slot becomes unowned
|
|
920
|
+
↓
|
|
921
|
+
next main request restores its snapshot
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Do NOT accidentally assign an auxiliary request to the currently active DSH session snapshot.
|
|
925
|
+
|
|
926
|
+
`GenerateOptions.purpose` exists specifically to identify current auxiliary call categories.
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
# 23. Restore algorithm
|
|
931
|
+
|
|
932
|
+
Pseudo-code:
|
|
933
|
+
|
|
934
|
+
```ts
|
|
935
|
+
async function prepareSession(sessionId, route) {
|
|
936
|
+
const slot = slot0
|
|
937
|
+
|
|
938
|
+
if (
|
|
939
|
+
slot.ownerSessionId === sessionId &&
|
|
940
|
+
slot.state !== 'broken'
|
|
941
|
+
) {
|
|
942
|
+
return { kind: 'already-active' }
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (slot.ownerSessionId) {
|
|
946
|
+
await checkpointIfNeeded(slot.ownerSessionId)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const snapshot = await repository.findCompatible(
|
|
950
|
+
sessionId,
|
|
951
|
+
route,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
if (!snapshot) {
|
|
955
|
+
await backend.eraseSlot(slot.id)
|
|
956
|
+
|
|
957
|
+
slot.ownerSessionId = sessionId
|
|
958
|
+
slot.state = 'idle'
|
|
959
|
+
|
|
960
|
+
return { kind: 'cold' }
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
const result = await backend.restoreSlot(
|
|
965
|
+
slot.id,
|
|
966
|
+
snapshot.filename,
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
verifyRestore(result, snapshot)
|
|
970
|
+
|
|
971
|
+
slot.ownerSessionId = sessionId
|
|
972
|
+
slot.state = 'ready'
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
kind: 'restored',
|
|
976
|
+
tokens: result.tokens,
|
|
977
|
+
bytes: result.bytes,
|
|
978
|
+
durationMs: result.durationMs,
|
|
979
|
+
}
|
|
980
|
+
} catch (error) {
|
|
981
|
+
markSnapshotInvalid(snapshot, error)
|
|
982
|
+
|
|
983
|
+
await backend.eraseSlot(slot.id)
|
|
984
|
+
|
|
985
|
+
slot.ownerSessionId = sessionId
|
|
986
|
+
slot.state = 'idle'
|
|
987
|
+
|
|
988
|
+
return {
|
|
989
|
+
kind: 'cold-fallback',
|
|
990
|
+
error,
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
The key policy:
|
|
997
|
+
|
|
998
|
+
> Restore failure is never fatal to ordinary inference unless strict mode is explicitly enabled.
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
# 24. Post-restore validation
|
|
1003
|
+
|
|
1004
|
+
An HTTP `200` from llama.cpp is not enough.
|
|
1005
|
+
|
|
1006
|
+
The plugin should validate:
|
|
1007
|
+
|
|
1008
|
+
```text
|
|
1009
|
+
n_restored > 0
|
|
1010
|
+
expected snapshot existed
|
|
1011
|
+
slot endpoint remains healthy
|
|
1012
|
+
reported token count is plausible
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Optionally inspect:
|
|
1016
|
+
|
|
1017
|
+
```text
|
|
1018
|
+
GET /slots
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
after restore.
|
|
1022
|
+
|
|
1023
|
+
For hybrid/recurrent models, introduce:
|
|
1024
|
+
|
|
1025
|
+
```yaml
|
|
1026
|
+
restoreVerification: strict
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
which can require slot state to reflect restored token count before considering restore successful.
|
|
1030
|
+
|
|
1031
|
+
This is useful because persistent state support can vary by llama.cpp model architecture/build.
|
|
1032
|
+
|
|
1033
|
+
---
|
|
1034
|
+
|
|
1035
|
+
# 25. Inference algorithm
|
|
1036
|
+
|
|
1037
|
+
After preparing the slot:
|
|
1038
|
+
|
|
1039
|
+
```text
|
|
1040
|
+
slot owner = session
|
|
1041
|
+
↓
|
|
1042
|
+
call downstream LLM adapter
|
|
1043
|
+
↓
|
|
1044
|
+
consume stream normally
|
|
1045
|
+
↓
|
|
1046
|
+
terminal successful finish
|
|
1047
|
+
↓
|
|
1048
|
+
mark slot dirty
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
Important:
|
|
1052
|
+
|
|
1053
|
+
The plugin MUST preserve streaming.
|
|
1054
|
+
|
|
1055
|
+
It must not buffer the full model response merely to implement persistence.
|
|
1056
|
+
|
|
1057
|
+
Conceptually:
|
|
1058
|
+
|
|
1059
|
+
```ts
|
|
1060
|
+
const downstream = next()
|
|
1061
|
+
|
|
1062
|
+
return async function* () {
|
|
1063
|
+
let succeeded = false
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
for await (const chunk of downstream) {
|
|
1067
|
+
if (
|
|
1068
|
+
chunk.type === 'finish' &&
|
|
1069
|
+
chunk.reason.kind !== 'error' &&
|
|
1070
|
+
chunk.reason.kind !== 'aborted'
|
|
1071
|
+
) {
|
|
1072
|
+
succeeded = true
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
yield chunk
|
|
1076
|
+
}
|
|
1077
|
+
} finally {
|
|
1078
|
+
if (succeeded) {
|
|
1079
|
+
markDirty(sessionId)
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
Care must be taken with Cordis waterfall semantics: resolve `next()` at the correct point and don't accidentally call a consumed waterfall continuation lazily.
|
|
1086
|
+
|
|
1087
|
+
---
|
|
1088
|
+
|
|
1089
|
+
# 26. Checkpoint strategy
|
|
1090
|
+
|
|
1091
|
+
Saving a multi-gigabyte KV file after every generation can destroy the performance win.
|
|
1092
|
+
|
|
1093
|
+
Therefore checkpointing must be configurable.
|
|
1094
|
+
|
|
1095
|
+
Supported strategies:
|
|
1096
|
+
|
|
1097
|
+
```text
|
|
1098
|
+
switch
|
|
1099
|
+
turn
|
|
1100
|
+
step
|
|
1101
|
+
idle
|
|
1102
|
+
shutdown
|
|
1103
|
+
manual
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
### `switch`
|
|
1107
|
+
|
|
1108
|
+
Save only when the physical slot must be reassigned.
|
|
1109
|
+
|
|
1110
|
+
This should be the primary default.
|
|
1111
|
+
|
|
1112
|
+
Example:
|
|
1113
|
+
|
|
1114
|
+
```text
|
|
1115
|
+
A → A → A → A
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
No disk writes.
|
|
1119
|
+
|
|
1120
|
+
Then:
|
|
1121
|
+
|
|
1122
|
+
```text
|
|
1123
|
+
A → B
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
causes one save of A.
|
|
1127
|
+
|
|
1128
|
+
### `turn`
|
|
1129
|
+
|
|
1130
|
+
Persist after every completed user turn.
|
|
1131
|
+
|
|
1132
|
+
Higher durability, much more disk I/O.
|
|
1133
|
+
|
|
1134
|
+
### `step`
|
|
1135
|
+
|
|
1136
|
+
Persist after every successful agent model step.
|
|
1137
|
+
|
|
1138
|
+
Useful for crash-sensitive long autonomous jobs.
|
|
1139
|
+
|
|
1140
|
+
Potentially very expensive.
|
|
1141
|
+
|
|
1142
|
+
### `idle`
|
|
1143
|
+
|
|
1144
|
+
Save dirty state after N seconds without another request.
|
|
1145
|
+
|
|
1146
|
+
Recommended:
|
|
1147
|
+
|
|
1148
|
+
```yaml
|
|
1149
|
+
idleCheckpointMs: 30000
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
### `shutdown`
|
|
1153
|
+
|
|
1154
|
+
Attempt a final checkpoint during normal Cordis disposal.
|
|
1155
|
+
|
|
1156
|
+
### Recommended default
|
|
1157
|
+
|
|
1158
|
+
```text
|
|
1159
|
+
switch + idle + shutdown
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
This provides a good balance between speed and durability.
|
|
1163
|
+
|
|
1164
|
+
---
|
|
1165
|
+
|
|
1166
|
+
# 27. Checkpoint coalescing
|
|
1167
|
+
|
|
1168
|
+
Multiple save requests for the same revision should collapse.
|
|
1169
|
+
|
|
1170
|
+
Example:
|
|
1171
|
+
|
|
1172
|
+
```text
|
|
1173
|
+
turn/end
|
|
1174
|
+
idle timer
|
|
1175
|
+
session/flush
|
|
1176
|
+
plugin shutdown
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
may all happen near one another.
|
|
1180
|
+
|
|
1181
|
+
Use:
|
|
1182
|
+
|
|
1183
|
+
```ts
|
|
1184
|
+
interface SaveGeneration {
|
|
1185
|
+
dirtyRevision: number
|
|
1186
|
+
persistedRevision: number
|
|
1187
|
+
inFlight?: Promise<SnapshotResult>
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
If:
|
|
1192
|
+
|
|
1193
|
+
```text
|
|
1194
|
+
dirtyRevision === persistedRevision
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
do nothing.
|
|
1198
|
+
|
|
1199
|
+
If a save is already running for the current revision:
|
|
1200
|
+
|
|
1201
|
+
```text
|
|
1202
|
+
await existing save
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
rather than starting another.
|
|
1206
|
+
|
|
1207
|
+
---
|
|
1208
|
+
|
|
1209
|
+
# 28. Dirty generations
|
|
1210
|
+
|
|
1211
|
+
Never use a single boolean if avoidable.
|
|
1212
|
+
|
|
1213
|
+
Use monotonic generations:
|
|
1214
|
+
|
|
1215
|
+
```text
|
|
1216
|
+
dirtyRevision = 31
|
|
1217
|
+
savedRevision = 30
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
Then:
|
|
1221
|
+
|
|
1222
|
+
```text
|
|
1223
|
+
dirtyRevision > savedRevision
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
means dirty.
|
|
1227
|
+
|
|
1228
|
+
If inference completes while a save is in progress:
|
|
1229
|
+
|
|
1230
|
+
```text
|
|
1231
|
+
save revision 30
|
|
1232
|
+
new inference → revision 31
|
|
1233
|
+
save finishes
|
|
1234
|
+
savedRevision = 30
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
and state correctly remains dirty.
|
|
1238
|
+
|
|
1239
|
+
---
|
|
1240
|
+
|
|
1241
|
+
# 29. Session sequence tracking
|
|
1242
|
+
|
|
1243
|
+
Snapshot metadata SHOULD include the latest known DSH:
|
|
1244
|
+
|
|
1245
|
+
```text
|
|
1246
|
+
session.seq
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
Example:
|
|
1250
|
+
|
|
1251
|
+
```json
|
|
1252
|
+
{
|
|
1253
|
+
"sessionSeq": 482
|
|
1254
|
+
}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
This does NOT mean KV state is equivalent to every event through seq 482.
|
|
1258
|
+
|
|
1259
|
+
It is primarily:
|
|
1260
|
+
|
|
1261
|
+
- diagnostic metadata;
|
|
1262
|
+
- freshness information;
|
|
1263
|
+
- useful for invalidation;
|
|
1264
|
+
- useful for future exact request fingerprinting.
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
# 30. Prompt/request fingerprints
|
|
1269
|
+
|
|
1270
|
+
Future versions SHOULD record a fingerprint of the request that produced the snapshot.
|
|
1271
|
+
|
|
1272
|
+
Conceptually:
|
|
1273
|
+
|
|
1274
|
+
```text
|
|
1275
|
+
hash(
|
|
1276
|
+
provider
|
|
1277
|
+
model
|
|
1278
|
+
system prompt
|
|
1279
|
+
normalized messages
|
|
1280
|
+
tool schemas
|
|
1281
|
+
relevant template configuration
|
|
1282
|
+
)
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
However this SHOULD NOT be mandatory in v0.1.
|
|
1286
|
+
|
|
1287
|
+
Why?
|
|
1288
|
+
|
|
1289
|
+
Because llama.cpp itself compares the restored prompt cache with the next incoming prompt and can reuse the common prefix while processing the changed suffix.
|
|
1290
|
+
|
|
1291
|
+
The plugin mainly needs to prevent gross incompatibility.
|
|
1292
|
+
|
|
1293
|
+
---
|
|
1294
|
+
|
|
1295
|
+
# 31. Snapshot invalidation
|
|
1296
|
+
|
|
1297
|
+
Snapshots must become invalid when any important runtime identity changes.
|
|
1298
|
+
|
|
1299
|
+
Examples:
|
|
1300
|
+
|
|
1301
|
+
```text
|
|
1302
|
+
model changed
|
|
1303
|
+
model GGUF replaced
|
|
1304
|
+
KV layout changed
|
|
1305
|
+
runtimeKey changed
|
|
1306
|
+
llama backend changed
|
|
1307
|
+
LoRA changed
|
|
1308
|
+
server instance changed incompatibly
|
|
1309
|
+
snapshot restore failed
|
|
1310
|
+
manifest malformed
|
|
1311
|
+
snapshot file missing
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
Invalidation must NOT delete data immediately.
|
|
1315
|
+
|
|
1316
|
+
Prefer:
|
|
1317
|
+
|
|
1318
|
+
```text
|
|
1319
|
+
state = invalid
|
|
1320
|
+
reason = MODEL_FINGERPRINT_CHANGED
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
Cleanup can happen separately.
|
|
1324
|
+
|
|
1325
|
+
---
|
|
1326
|
+
|
|
1327
|
+
# 32. Failure policy
|
|
1328
|
+
|
|
1329
|
+
Persistence is an optimization.
|
|
1330
|
+
|
|
1331
|
+
Default failure behavior:
|
|
1332
|
+
|
|
1333
|
+
```text
|
|
1334
|
+
save failed
|
|
1335
|
+
→ log warning
|
|
1336
|
+
→ continue session
|
|
1337
|
+
|
|
1338
|
+
restore failed
|
|
1339
|
+
→ mark snapshot invalid
|
|
1340
|
+
→ erase slot
|
|
1341
|
+
→ cold prefill
|
|
1342
|
+
→ continue session
|
|
1343
|
+
|
|
1344
|
+
server /slots unavailable
|
|
1345
|
+
→ disable persistence temporarily
|
|
1346
|
+
→ continue ordinary inference
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
Only configuration:
|
|
1350
|
+
|
|
1351
|
+
```yaml
|
|
1352
|
+
strict: true
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
should turn persistence failure into request failure.
|
|
1356
|
+
|
|
1357
|
+
Default:
|
|
1358
|
+
|
|
1359
|
+
```yaml
|
|
1360
|
+
strict: false
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
---
|
|
1364
|
+
|
|
1365
|
+
# 33. Circuit breaker
|
|
1366
|
+
|
|
1367
|
+
If llama slot persistence is broken, we don't want every request to waste seconds retrying it.
|
|
1368
|
+
|
|
1369
|
+
Backend state:
|
|
1370
|
+
|
|
1371
|
+
```text
|
|
1372
|
+
HEALTHY
|
|
1373
|
+
DEGRADED
|
|
1374
|
+
OPEN
|
|
1375
|
+
HALF_OPEN
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
Suggested behavior:
|
|
1379
|
+
|
|
1380
|
+
```text
|
|
1381
|
+
3 consecutive persistence failures
|
|
1382
|
+
→ disable save/restore for 60s
|
|
1383
|
+
|
|
1384
|
+
after 60s
|
|
1385
|
+
→ probe
|
|
1386
|
+
|
|
1387
|
+
probe successful
|
|
1388
|
+
→ resume
|
|
1389
|
+
|
|
1390
|
+
probe fails
|
|
1391
|
+
→ backoff
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
Inference itself remains active.
|
|
1395
|
+
|
|
1396
|
+
---
|
|
1397
|
+
|
|
1398
|
+
# 34. Backend health probe
|
|
1399
|
+
|
|
1400
|
+
At plugin initialization:
|
|
1401
|
+
|
|
1402
|
+
```text
|
|
1403
|
+
GET /slots
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
Validate:
|
|
1407
|
+
|
|
1408
|
+
```text
|
|
1409
|
+
server reachable
|
|
1410
|
+
slots endpoint available
|
|
1411
|
+
expected slot count
|
|
1412
|
+
slot 0 exists for single-slot mode
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
Optionally test persistence capability using a non-destructive mechanism where possible.
|
|
1416
|
+
|
|
1417
|
+
The plugin SHOULD clearly distinguish:
|
|
1418
|
+
|
|
1419
|
+
```text
|
|
1420
|
+
LLM endpoint alive
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
from:
|
|
1424
|
+
|
|
1425
|
+
```text
|
|
1426
|
+
KV persistence supported
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
# 35. Configuration
|
|
1432
|
+
|
|
1433
|
+
Initial configuration proposal:
|
|
1434
|
+
|
|
1435
|
+
```yaml
|
|
1436
|
+
kv-persist:
|
|
1437
|
+
enabled: true
|
|
1438
|
+
|
|
1439
|
+
backend:
|
|
1440
|
+
type: llama.cpp
|
|
1441
|
+
baseURL: http://127.0.0.1:8080
|
|
1442
|
+
|
|
1443
|
+
providers:
|
|
1444
|
+
- local-qwen
|
|
1445
|
+
|
|
1446
|
+
mode: single-slot
|
|
1447
|
+
slotId: 0
|
|
1448
|
+
|
|
1449
|
+
runtimeKey: qwen38-27b-iq2xs-ctx128k-q4kv
|
|
1450
|
+
|
|
1451
|
+
checkpoint:
|
|
1452
|
+
onSwitch: true
|
|
1453
|
+
onShutdown: true
|
|
1454
|
+
onSessionFlush: true
|
|
1455
|
+
|
|
1456
|
+
idleMs: 30000
|
|
1457
|
+
|
|
1458
|
+
onTurnEnd: false
|
|
1459
|
+
onStepEnd: false
|
|
1460
|
+
|
|
1461
|
+
restore:
|
|
1462
|
+
enabled: true
|
|
1463
|
+
verify: true
|
|
1464
|
+
|
|
1465
|
+
failure:
|
|
1466
|
+
strict: false
|
|
1467
|
+
maxConsecutiveFailures: 3
|
|
1468
|
+
cooldownMs: 60000
|
|
1469
|
+
|
|
1470
|
+
metadata:
|
|
1471
|
+
path: ${DSH_HOME}/cache/dsh-kv-persist
|
|
1472
|
+
|
|
1473
|
+
logging:
|
|
1474
|
+
level: info
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
---
|
|
1478
|
+
|
|
1479
|
+
# 36. Config schema
|
|
1480
|
+
|
|
1481
|
+
Using Schemastery-style validation:
|
|
1482
|
+
|
|
1483
|
+
```ts
|
|
1484
|
+
interface Config {
|
|
1485
|
+
enabled?: boolean
|
|
1486
|
+
|
|
1487
|
+
backend: {
|
|
1488
|
+
type: 'llama.cpp'
|
|
1489
|
+
baseURL: string
|
|
1490
|
+
apiKey?: string
|
|
1491
|
+
requestTimeoutMs?: number
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
providers?: string[]
|
|
1495
|
+
|
|
1496
|
+
mode?: 'single-slot' | 'managed-slots'
|
|
1497
|
+
|
|
1498
|
+
slotId?: number
|
|
1499
|
+
|
|
1500
|
+
runtimeKey?: string
|
|
1501
|
+
|
|
1502
|
+
checkpoint?: {
|
|
1503
|
+
onSwitch?: boolean
|
|
1504
|
+
onShutdown?: boolean
|
|
1505
|
+
onSessionFlush?: boolean
|
|
1506
|
+
|
|
1507
|
+
idleMs?: number
|
|
1508
|
+
|
|
1509
|
+
onTurnEnd?: boolean
|
|
1510
|
+
onStepEnd?: boolean
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
restore?: {
|
|
1514
|
+
enabled?: boolean
|
|
1515
|
+
verify?: boolean
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
failure?: {
|
|
1519
|
+
strict?: boolean
|
|
1520
|
+
maxConsecutiveFailures?: number
|
|
1521
|
+
cooldownMs?: number
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
metadata?: {
|
|
1525
|
+
path?: string
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
Recommended defaults:
|
|
1531
|
+
|
|
1532
|
+
```text
|
|
1533
|
+
enabled = true
|
|
1534
|
+
|
|
1535
|
+
mode = single-slot
|
|
1536
|
+
slotId = 0
|
|
1537
|
+
|
|
1538
|
+
onSwitch = true
|
|
1539
|
+
onShutdown = true
|
|
1540
|
+
onSessionFlush = true
|
|
1541
|
+
idleMs = 30000
|
|
1542
|
+
|
|
1543
|
+
onTurnEnd = false
|
|
1544
|
+
onStepEnd = false
|
|
1545
|
+
|
|
1546
|
+
restore.enabled = true
|
|
1547
|
+
restore.verify = true
|
|
1548
|
+
|
|
1549
|
+
strict = false
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
---
|
|
1553
|
+
|
|
1554
|
+
# 37. Automatic provider filtering
|
|
1555
|
+
|
|
1556
|
+
The plugin MUST NOT touch cloud providers by default.
|
|
1557
|
+
|
|
1558
|
+
Example:
|
|
1559
|
+
|
|
1560
|
+
```yaml
|
|
1561
|
+
providers:
|
|
1562
|
+
- local-qwen
|
|
1563
|
+
- local-coder
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
A request to:
|
|
1567
|
+
|
|
1568
|
+
```text
|
|
1569
|
+
deepseek
|
|
1570
|
+
openai
|
|
1571
|
+
anthropic
|
|
1572
|
+
```
|
|
1573
|
+
|
|
1574
|
+
passes straight through.
|
|
1575
|
+
|
|
1576
|
+
Future configuration may support:
|
|
1577
|
+
|
|
1578
|
+
```yaml
|
|
1579
|
+
providers:
|
|
1580
|
+
local-qwen:
|
|
1581
|
+
backend: local-llama
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
for multiple servers.
|
|
1585
|
+
|
|
1586
|
+
---
|
|
1587
|
+
|
|
1588
|
+
# 38. Multiple llama servers
|
|
1589
|
+
|
|
1590
|
+
Architecture should support this eventually:
|
|
1591
|
+
|
|
1592
|
+
```yaml
|
|
1593
|
+
servers:
|
|
1594
|
+
rtx3060:
|
|
1595
|
+
type: llama.cpp
|
|
1596
|
+
baseURL: http://127.0.0.1:8080
|
|
1597
|
+
runtimeKey: qwen38
|
|
1598
|
+
|
|
1599
|
+
rtx4090:
|
|
1600
|
+
type: llama.cpp
|
|
1601
|
+
baseURL: http://192.168.1.42:8080
|
|
1602
|
+
runtimeKey: qwen-coder
|
|
1603
|
+
|
|
1604
|
+
routes:
|
|
1605
|
+
local-qwen:
|
|
1606
|
+
server: rtx3060
|
|
1607
|
+
|
|
1608
|
+
local-coder:
|
|
1609
|
+
server: rtx4090
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
Internally:
|
|
1613
|
+
|
|
1614
|
+
```text
|
|
1615
|
+
Coordinator
|
|
1616
|
+
↓
|
|
1617
|
+
ServerRuntime[]
|
|
1618
|
+
↓
|
|
1619
|
+
independent slot pools + locks
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
Do not use one global mutex across different servers.
|
|
1623
|
+
|
|
1624
|
+
---
|
|
1625
|
+
|
|
1626
|
+
# 39. Metadata repository
|
|
1627
|
+
|
|
1628
|
+
Suggested path:
|
|
1629
|
+
|
|
1630
|
+
```text
|
|
1631
|
+
$DSH_HOME/cache/dsh-kv-persist/
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
Layout:
|
|
1635
|
+
|
|
1636
|
+
```text
|
|
1637
|
+
dsh-kv-persist/
|
|
1638
|
+
├─ instances/
|
|
1639
|
+
│ └─ local-3060/
|
|
1640
|
+
│ └─ sessions/
|
|
1641
|
+
│ ├─ 7c856d....json
|
|
1642
|
+
│ └─ d084fd....json
|
|
1643
|
+
│
|
|
1644
|
+
├─ index.json
|
|
1645
|
+
└─ plugin-state.json
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
Binary KV files SHOULD NOT be copied here automatically.
|
|
1649
|
+
|
|
1650
|
+
---
|
|
1651
|
+
|
|
1652
|
+
# 40. Atomic metadata writes
|
|
1653
|
+
|
|
1654
|
+
Manifest updates should use:
|
|
1655
|
+
|
|
1656
|
+
```text
|
|
1657
|
+
write temp
|
|
1658
|
+
fsync/close
|
|
1659
|
+
rename
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
rather than overwriting JSON directly.
|
|
1663
|
+
|
|
1664
|
+
Example:
|
|
1665
|
+
|
|
1666
|
+
```text
|
|
1667
|
+
session.json.tmp
|
|
1668
|
+
↓
|
|
1669
|
+
rename
|
|
1670
|
+
↓
|
|
1671
|
+
session.json
|
|
1672
|
+
```
|
|
1673
|
+
|
|
1674
|
+
A crash must not leave half-written metadata treated as valid.
|
|
1675
|
+
|
|
1676
|
+
---
|
|
1677
|
+
|
|
1678
|
+
# 41. Snapshot naming generations
|
|
1679
|
+
|
|
1680
|
+
Default strategy:
|
|
1681
|
+
|
|
1682
|
+
```text
|
|
1683
|
+
one rolling snapshot per session
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
rather than:
|
|
1687
|
+
|
|
1688
|
+
```text
|
|
1689
|
+
snapshot-000001.bin
|
|
1690
|
+
snapshot-000002.bin
|
|
1691
|
+
snapshot-000003.bin
|
|
1692
|
+
...
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
because KV snapshots can be enormous.
|
|
1696
|
+
|
|
1697
|
+
Conceptually:
|
|
1698
|
+
|
|
1699
|
+
```text
|
|
1700
|
+
<sessionHash>.bin
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
If llama.cpp cannot safely atomically replace an existing file in a particular build/backend, support two rotating names:
|
|
1704
|
+
|
|
1705
|
+
```text
|
|
1706
|
+
<hash>.a.bin
|
|
1707
|
+
<hash>.b.bin
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
Manifest points to the latest completed generation.
|
|
1711
|
+
|
|
1712
|
+
This prevents an interrupted write from destroying the previous valid checkpoint.
|
|
1713
|
+
|
|
1714
|
+
---
|
|
1715
|
+
|
|
1716
|
+
# 42. Local vs remote server cleanup
|
|
1717
|
+
|
|
1718
|
+
The llama slots API manages save/restore/erase of slot state, but binary file lifecycle may not always be remotely manageable.
|
|
1719
|
+
|
|
1720
|
+
Therefore define:
|
|
1721
|
+
|
|
1722
|
+
```text
|
|
1723
|
+
snapshotBinaryManagement:
|
|
1724
|
+
server-owned
|
|
1725
|
+
shared-filesystem
|
|
1726
|
+
```
|
|
1727
|
+
|
|
1728
|
+
### `server-owned`
|
|
1729
|
+
|
|
1730
|
+
Plugin only knows filenames.
|
|
1731
|
+
|
|
1732
|
+
No direct delete.
|
|
1733
|
+
|
|
1734
|
+
Use rolling filenames to limit growth.
|
|
1735
|
+
|
|
1736
|
+
### `shared-filesystem`
|
|
1737
|
+
|
|
1738
|
+
Plugin is configured with the same physical save directory and may:
|
|
1739
|
+
|
|
1740
|
+
- inspect file size;
|
|
1741
|
+
- delete invalid snapshots;
|
|
1742
|
+
- enforce disk quota;
|
|
1743
|
+
- perform atomic rotation.
|
|
1744
|
+
|
|
1745
|
+
This mode MUST be opt-in.
|
|
1746
|
+
|
|
1747
|
+
---
|
|
1748
|
+
|
|
1749
|
+
# 43. Disk quota
|
|
1750
|
+
|
|
1751
|
+
Future version:
|
|
1752
|
+
|
|
1753
|
+
```yaml
|
|
1754
|
+
retention:
|
|
1755
|
+
maxTotalBytes: 100GB
|
|
1756
|
+
maxSessions: 50
|
|
1757
|
+
maxAgeDays: 30
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
Eviction policy:
|
|
1761
|
+
|
|
1762
|
+
```text
|
|
1763
|
+
invalid first
|
|
1764
|
+
then oldest unused
|
|
1765
|
+
then LRU
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
Never remove the active slot state as part of disk cleanup.
|
|
1769
|
+
|
|
1770
|
+
Only stored snapshots.
|
|
1771
|
+
|
|
1772
|
+
---
|
|
1773
|
+
|
|
1774
|
+
# 44. Security
|
|
1775
|
+
|
|
1776
|
+
The plugin MUST assume the llama management endpoint is privileged.
|
|
1777
|
+
|
|
1778
|
+
Recommended setup:
|
|
1779
|
+
|
|
1780
|
+
```text
|
|
1781
|
+
127.0.0.1
|
|
1782
|
+
or
|
|
1783
|
+
trusted private network
|
|
1784
|
+
or
|
|
1785
|
+
authenticated reverse proxy
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
Do not expose slot management APIs publicly.
|
|
1789
|
+
|
|
1790
|
+
Snapshot filenames must be generated by the plugin and sanitized.
|
|
1791
|
+
|
|
1792
|
+
Never accept:
|
|
1793
|
+
|
|
1794
|
+
```text
|
|
1795
|
+
../../foo
|
|
1796
|
+
C:\whatever
|
|
1797
|
+
/etc/passwd
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
as a raw snapshot filename.
|
|
1801
|
+
|
|
1802
|
+
Only backend-generated opaque keys may reach:
|
|
1803
|
+
|
|
1804
|
+
```text
|
|
1805
|
+
action=save
|
|
1806
|
+
action=restore
|
|
1807
|
+
```
|
|
1808
|
+
|
|
1809
|
+
---
|
|
1810
|
+
|
|
1811
|
+
# 45. Logging
|
|
1812
|
+
|
|
1813
|
+
Recommended structured events:
|
|
1814
|
+
|
|
1815
|
+
```text
|
|
1816
|
+
kv.backend.ready
|
|
1817
|
+
kv.backend.unavailable
|
|
1818
|
+
|
|
1819
|
+
kv.slot.acquire
|
|
1820
|
+
kv.slot.release
|
|
1821
|
+
|
|
1822
|
+
kv.session.cold
|
|
1823
|
+
kv.session.restore.start
|
|
1824
|
+
kv.session.restore.success
|
|
1825
|
+
kv.session.restore.failed
|
|
1826
|
+
|
|
1827
|
+
kv.session.save.start
|
|
1828
|
+
kv.session.save.success
|
|
1829
|
+
kv.session.save.failed
|
|
1830
|
+
|
|
1831
|
+
kv.session.switch
|
|
1832
|
+
|
|
1833
|
+
kv.snapshot.invalidated
|
|
1834
|
+
|
|
1835
|
+
kv.persistence.circuit_open
|
|
1836
|
+
kv.persistence.circuit_recovered
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
Example:
|
|
1840
|
+
|
|
1841
|
+
```text
|
|
1842
|
+
[kv-persist] restored
|
|
1843
|
+
session=7c856d
|
|
1844
|
+
slot=0
|
|
1845
|
+
tokens=48192
|
|
1846
|
+
bytes=2.31GiB
|
|
1847
|
+
duration=418ms
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
Avoid logging full session IDs at normal verbosity if unnecessary.
|
|
1851
|
+
|
|
1852
|
+
Use abbreviated hashes.
|
|
1853
|
+
|
|
1854
|
+
---
|
|
1855
|
+
|
|
1856
|
+
# 46. Metrics
|
|
1857
|
+
|
|
1858
|
+
Expose internal counters through the service and later optional Prometheus integration:
|
|
1859
|
+
|
|
1860
|
+
```text
|
|
1861
|
+
dsh_kv_restore_total
|
|
1862
|
+
dsh_kv_restore_hit_total
|
|
1863
|
+
dsh_kv_restore_miss_total
|
|
1864
|
+
dsh_kv_restore_failure_total
|
|
1865
|
+
|
|
1866
|
+
dsh_kv_save_total
|
|
1867
|
+
dsh_kv_save_failure_total
|
|
1868
|
+
|
|
1869
|
+
dsh_kv_restore_bytes_total
|
|
1870
|
+
dsh_kv_save_bytes_total
|
|
1871
|
+
|
|
1872
|
+
dsh_kv_restore_duration_ms
|
|
1873
|
+
dsh_kv_save_duration_ms
|
|
1874
|
+
|
|
1875
|
+
dsh_kv_cold_prefill_total
|
|
1876
|
+
|
|
1877
|
+
dsh_kv_slot_switch_total
|
|
1878
|
+
|
|
1879
|
+
dsh_kv_snapshot_count
|
|
1880
|
+
dsh_kv_snapshot_bytes
|
|
1881
|
+
```
|
|
1882
|
+
|
|
1883
|
+
Especially useful derived metric:
|
|
1884
|
+
|
|
1885
|
+
```text
|
|
1886
|
+
persistent-cache restore hit rate
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
---
|
|
1890
|
+
|
|
1891
|
+
# 47. Diagnostics API
|
|
1892
|
+
|
|
1893
|
+
`ctx.kvPersist.status()` should return something like:
|
|
1894
|
+
|
|
1895
|
+
```json
|
|
1896
|
+
{
|
|
1897
|
+
"enabled": true,
|
|
1898
|
+
"backend": {
|
|
1899
|
+
"kind": "llama.cpp",
|
|
1900
|
+
"state": "healthy",
|
|
1901
|
+
"endpoint": "http://127.0.0.1:8080"
|
|
1902
|
+
},
|
|
1903
|
+
|
|
1904
|
+
"mode": "single-slot",
|
|
1905
|
+
|
|
1906
|
+
"slots": [
|
|
1907
|
+
{
|
|
1908
|
+
"id": 0,
|
|
1909
|
+
"owner": "7c856d",
|
|
1910
|
+
"state": "dirty"
|
|
1911
|
+
}
|
|
1912
|
+
],
|
|
1913
|
+
|
|
1914
|
+
"snapshots": {
|
|
1915
|
+
"known": 14,
|
|
1916
|
+
"valid": 13,
|
|
1917
|
+
"invalid": 1
|
|
1918
|
+
},
|
|
1919
|
+
|
|
1920
|
+
"stats": {
|
|
1921
|
+
"restores": 23,
|
|
1922
|
+
"restoreHits": 21,
|
|
1923
|
+
"coldStarts": 2,
|
|
1924
|
+
"saves": 18
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1929
|
+
---
|
|
1930
|
+
|
|
1931
|
+
# 48. Optional CLI
|
|
1932
|
+
|
|
1933
|
+
Eventually expose human-facing commands:
|
|
1934
|
+
|
|
1935
|
+
```text
|
|
1936
|
+
dsh kv status
|
|
1937
|
+
dsh kv list
|
|
1938
|
+
dsh kv save <session>
|
|
1939
|
+
dsh kv restore <session>
|
|
1940
|
+
dsh kv invalidate <session>
|
|
1941
|
+
dsh kv purge <session>
|
|
1942
|
+
dsh kv gc
|
|
1943
|
+
dsh kv doctor
|
|
1944
|
+
```
|
|
1945
|
+
|
|
1946
|
+
`doctor` should be particularly useful.
|
|
1947
|
+
|
|
1948
|
+
Example:
|
|
1949
|
+
|
|
1950
|
+
```text
|
|
1951
|
+
$ dsh kv doctor
|
|
1952
|
+
|
|
1953
|
+
Backend: llama.cpp
|
|
1954
|
+
Endpoint: http://127.0.0.1:8080
|
|
1955
|
+
Slots API: OK
|
|
1956
|
+
Slots: 1
|
|
1957
|
+
Configured mode: single-slot
|
|
1958
|
+
Slot 0: idle
|
|
1959
|
+
Save path capability: OK
|
|
1960
|
+
Restore verification: OK
|
|
1961
|
+
Hybrid model persistence: unverified
|
|
1962
|
+
Metadata directory: writable
|
|
1963
|
+
Result: READY
|
|
1964
|
+
```
|
|
1965
|
+
|
|
1966
|
+
---
|
|
1967
|
+
|
|
1968
|
+
# 49. No model-facing tool by default
|
|
1969
|
+
|
|
1970
|
+
Do NOT register:
|
|
1971
|
+
|
|
1972
|
+
```text
|
|
1973
|
+
save_kv_cache
|
|
1974
|
+
restore_kv_cache
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
with `ctx.tools`.
|
|
1978
|
+
|
|
1979
|
+
There is almost no reason for the LLM itself to manage its infrastructure cache.
|
|
1980
|
+
|
|
1981
|
+
It wastes tool schema tokens and introduces failure modes such as:
|
|
1982
|
+
|
|
1983
|
+
```text
|
|
1984
|
+
model decides to purge its own cache
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
Management belongs to:
|
|
1988
|
+
|
|
1989
|
+
```text
|
|
1990
|
+
plugin
|
|
1991
|
+
user CLI
|
|
1992
|
+
UI
|
|
1993
|
+
```
|
|
1994
|
+
|
|
1995
|
+
not to the model.
|
|
1996
|
+
|
|
1997
|
+
---
|
|
1998
|
+
|
|
1999
|
+
# 50. Session lifecycle integration
|
|
2000
|
+
|
|
2001
|
+
Subscribe to session lifecycle for:
|
|
2002
|
+
|
|
2003
|
+
```text
|
|
2004
|
+
session created
|
|
2005
|
+
session flush
|
|
2006
|
+
session disposed
|
|
2007
|
+
turn end
|
|
2008
|
+
```
|
|
2009
|
+
|
|
2010
|
+
Use these events as persistence hints.
|
|
2011
|
+
|
|
2012
|
+
DSH explicitly exposes session persistence hooks and `session/flush` as the durability checkpoint seam.
|
|
2013
|
+
|
|
2014
|
+
Recommended semantics:
|
|
2015
|
+
|
|
2016
|
+
### session created/resumed
|
|
2017
|
+
|
|
2018
|
+
Do not restore immediately.
|
|
2019
|
+
|
|
2020
|
+
Lazy restore on first actual LLM request.
|
|
2021
|
+
|
|
2022
|
+
Reason:
|
|
2023
|
+
|
|
2024
|
+
```text
|
|
2025
|
+
opening a chat in UI
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
should not evict another active KV session unless inference actually occurs.
|
|
2029
|
+
|
|
2030
|
+
### session flush
|
|
2031
|
+
|
|
2032
|
+
If the session currently owns a dirty slot:
|
|
2033
|
+
|
|
2034
|
+
```text
|
|
2035
|
+
checkpoint
|
|
2036
|
+
```
|
|
2037
|
+
|
|
2038
|
+
### session disposed
|
|
2039
|
+
|
|
2040
|
+
If dirty:
|
|
2041
|
+
|
|
2042
|
+
```text
|
|
2043
|
+
checkpoint if configured
|
|
2044
|
+
```
|
|
2045
|
+
|
|
2046
|
+
then remove runtime ownership.
|
|
2047
|
+
|
|
2048
|
+
### turn/end
|
|
2049
|
+
|
|
2050
|
+
Checkpoint only when:
|
|
2051
|
+
|
|
2052
|
+
```yaml
|
|
2053
|
+
checkpoint.onTurnEnd: true
|
|
2054
|
+
```
|
|
2055
|
+
|
|
2056
|
+
---
|
|
2057
|
+
|
|
2058
|
+
# 51. Lazy restore
|
|
2059
|
+
|
|
2060
|
+
This is important enough to be explicit.
|
|
2061
|
+
|
|
2062
|
+
Bad:
|
|
2063
|
+
|
|
2064
|
+
```text
|
|
2065
|
+
user clicks session B
|
|
2066
|
+
→ immediately save A
|
|
2067
|
+
→ restore 3GB B
|
|
2068
|
+
→ user clicks session C
|
|
2069
|
+
→ immediately save B
|
|
2070
|
+
→ restore C
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
Good:
|
|
2074
|
+
|
|
2075
|
+
```text
|
|
2076
|
+
user clicks session B
|
|
2077
|
+
→ nothing
|
|
2078
|
+
|
|
2079
|
+
user actually sends message in B
|
|
2080
|
+
→ switch slot
|
|
2081
|
+
```
|
|
2082
|
+
|
|
2083
|
+
Snapshot management follows inference, not UI navigation.
|
|
2084
|
+
|
|
2085
|
+
---
|
|
2086
|
+
|
|
2087
|
+
# 52. Save-before-evict invariant
|
|
2088
|
+
|
|
2089
|
+
Before assigning a dirty slot to another owner:
|
|
2090
|
+
|
|
2091
|
+
```text
|
|
2092
|
+
MUST attempt save
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
unless configured:
|
|
2096
|
+
|
|
2097
|
+
```yaml
|
|
2098
|
+
checkpoint.onSwitch: false
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
Default invariant:
|
|
2102
|
+
|
|
2103
|
+
```text
|
|
2104
|
+
dirty A
|
|
2105
|
+
+
|
|
2106
|
+
need B
|
|
2107
|
+
=
|
|
2108
|
+
save A before erase/restore B
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
This is the core of session switching.
|
|
2112
|
+
|
|
2113
|
+
---
|
|
2114
|
+
|
|
2115
|
+
# 53. Cold-session behavior
|
|
2116
|
+
|
|
2117
|
+
If no snapshot exists:
|
|
2118
|
+
|
|
2119
|
+
```text
|
|
2120
|
+
erase slot
|
|
2121
|
+
assign owner
|
|
2122
|
+
let llama.cpp process full request
|
|
2123
|
+
mark dirty
|
|
2124
|
+
```
|
|
2125
|
+
|
|
2126
|
+
Do not try to build a snapshot before inference.
|
|
2127
|
+
|
|
2128
|
+
Snapshot will naturally be created by the next checkpoint.
|
|
2129
|
+
|
|
2130
|
+
---
|
|
2131
|
+
|
|
2132
|
+
# 54. Snapshot restore and prompt divergence
|
|
2133
|
+
|
|
2134
|
+
A restored KV snapshot is not assumed to perfectly equal the next DSH request.
|
|
2135
|
+
|
|
2136
|
+
Example:
|
|
2137
|
+
|
|
2138
|
+
Snapshot:
|
|
2139
|
+
|
|
2140
|
+
```text
|
|
2141
|
+
system
|
|
2142
|
+
A
|
|
2143
|
+
assistant A
|
|
2144
|
+
B
|
|
2145
|
+
assistant B
|
|
2146
|
+
```
|
|
2147
|
+
|
|
2148
|
+
Current request:
|
|
2149
|
+
|
|
2150
|
+
```text
|
|
2151
|
+
system
|
|
2152
|
+
A
|
|
2153
|
+
assistant A
|
|
2154
|
+
B
|
|
2155
|
+
assistant B
|
|
2156
|
+
C
|
|
2157
|
+
```
|
|
2158
|
+
|
|
2159
|
+
Ideal outcome:
|
|
2160
|
+
|
|
2161
|
+
```text
|
|
2162
|
+
reuse existing prefix
|
|
2163
|
+
process only C
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2166
|
+
If the system prompt/tool schema changed:
|
|
2167
|
+
|
|
2168
|
+
```text
|
|
2169
|
+
old prefix
|
|
2170
|
+
↓
|
|
2171
|
+
divergence detected by llama.cpp
|
|
2172
|
+
↓
|
|
2173
|
+
recompute changed suffix
|
|
2174
|
+
```
|
|
2175
|
+
|
|
2176
|
+
Thus plugin-side fingerprints are mainly for runtime compatibility and diagnostics rather than replacing llama.cpp's prompt matching.
|
|
2177
|
+
|
|
2178
|
+
---
|
|
2179
|
+
|
|
2180
|
+
# 55. Context compaction
|
|
2181
|
+
|
|
2182
|
+
Compaction changes model-visible history substantially.
|
|
2183
|
+
|
|
2184
|
+
The plugin does not need special correctness logic.
|
|
2185
|
+
|
|
2186
|
+
After compaction:
|
|
2187
|
+
|
|
2188
|
+
```text
|
|
2189
|
+
restored old snapshot
|
|
2190
|
+
↓
|
|
2191
|
+
incoming compacted prompt differs
|
|
2192
|
+
↓
|
|
2193
|
+
llama.cpp finds smaller common prefix
|
|
2194
|
+
↓
|
|
2195
|
+
new prompt is processed
|
|
2196
|
+
↓
|
|
2197
|
+
slot becomes dirty
|
|
2198
|
+
↓
|
|
2199
|
+
next checkpoint replaces snapshot
|
|
2200
|
+
```
|
|
2201
|
+
|
|
2202
|
+
However the plugin should emit diagnostics:
|
|
2203
|
+
|
|
2204
|
+
```text
|
|
2205
|
+
large restored cache
|
|
2206
|
+
low subsequent cache reuse
|
|
2207
|
+
possible compaction/prompt mutation
|
|
2208
|
+
```
|
|
2209
|
+
|
|
2210
|
+
Future versions can proactively invalidate on known compaction events.
|
|
2211
|
+
|
|
2212
|
+
---
|
|
2213
|
+
|
|
2214
|
+
# 56. Provider/model changes inside a session
|
|
2215
|
+
|
|
2216
|
+
DSH allows the request route to change between steps.
|
|
2217
|
+
|
|
2218
|
+
Therefore one DSH session can theoretically contain:
|
|
2219
|
+
|
|
2220
|
+
```text
|
|
2221
|
+
model A
|
|
2222
|
+
→ model B
|
|
2223
|
+
→ model A
|
|
2224
|
+
```
|
|
2225
|
+
|
|
2226
|
+
KV identity must therefore include:
|
|
2227
|
+
|
|
2228
|
+
```text
|
|
2229
|
+
provider + model
|
|
2230
|
+
```
|
|
2231
|
+
|
|
2232
|
+
not only session.
|
|
2233
|
+
|
|
2234
|
+
Conceptually:
|
|
2235
|
+
|
|
2236
|
+
```text
|
|
2237
|
+
session X
|
|
2238
|
+
├─ qwen snapshot
|
|
2239
|
+
└─ coder snapshot
|
|
2240
|
+
```
|
|
2241
|
+
|
|
2242
|
+
MVP may simplify by allowing one current snapshot per:
|
|
2243
|
+
|
|
2244
|
+
```text
|
|
2245
|
+
(session, provider, model)
|
|
2246
|
+
```
|
|
2247
|
+
|
|
2248
|
+
---
|
|
2249
|
+
|
|
2250
|
+
# 57. LoRA and runtime mutations
|
|
2251
|
+
|
|
2252
|
+
If llama-server changes:
|
|
2253
|
+
|
|
2254
|
+
```text
|
|
2255
|
+
LoRA
|
|
2256
|
+
model
|
|
2257
|
+
chat template
|
|
2258
|
+
KV representation
|
|
2259
|
+
other state that affects serialized cache
|
|
2260
|
+
```
|
|
2261
|
+
|
|
2262
|
+
the runtime fingerprint must change or snapshots must be invalidated.
|
|
2263
|
+
|
|
2264
|
+
Never silently restore across obviously different model states.
|
|
2265
|
+
|
|
2266
|
+
---
|
|
2267
|
+
|
|
2268
|
+
# 58. Plugin lifecycle
|
|
2269
|
+
|
|
2270
|
+
Cordis plugins may be unloaded through configuration changes, HMR, explicit disposal, or loss of dependencies; resources external to Cordis should be tied to `ctx.effect()` and cleaned on unload.
|
|
2271
|
+
|
|
2272
|
+
The plugin should therefore register:
|
|
2273
|
+
|
|
2274
|
+
```text
|
|
2275
|
+
timers
|
|
2276
|
+
HTTP resources
|
|
2277
|
+
backend lifecycle
|
|
2278
|
+
shutdown save
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
through proper Cordis effects.
|
|
2282
|
+
|
|
2283
|
+
On dispose:
|
|
2284
|
+
|
|
2285
|
+
```text
|
|
2286
|
+
stop accepting new persistence work
|
|
2287
|
+
↓
|
|
2288
|
+
cancel idle timers
|
|
2289
|
+
↓
|
|
2290
|
+
wait for/abort safe pending operations
|
|
2291
|
+
↓
|
|
2292
|
+
checkpoint active dirty slot if configured
|
|
2293
|
+
↓
|
|
2294
|
+
dispose service
|
|
2295
|
+
```
|
|
2296
|
+
|
|
2297
|
+
---
|
|
2298
|
+
|
|
2299
|
+
# 59. Cancellation
|
|
2300
|
+
|
|
2301
|
+
User cancellation of generation MUST NOT be blocked by a slow snapshot write.
|
|
2302
|
+
|
|
2303
|
+
Inference `AbortSignal` belongs to inference.
|
|
2304
|
+
|
|
2305
|
+
Persistence operations should use their own bounded timeout.
|
|
2306
|
+
|
|
2307
|
+
For example:
|
|
2308
|
+
|
|
2309
|
+
```yaml
|
|
2310
|
+
backend:
|
|
2311
|
+
requestTimeoutMs: 15000
|
|
2312
|
+
```
|
|
2313
|
+
|
|
2314
|
+
If save exceeds timeout:
|
|
2315
|
+
|
|
2316
|
+
```text
|
|
2317
|
+
log
|
|
2318
|
+
mark persistence degraded
|
|
2319
|
+
release workflow
|
|
2320
|
+
```
|
|
2321
|
+
|
|
2322
|
+
Don't leave the agent permanently stuck because an NVMe/cache filesystem is unhappy.
|
|
2323
|
+
|
|
2324
|
+
---
|
|
2325
|
+
|
|
2326
|
+
# 60. Crash semantics
|
|
2327
|
+
|
|
2328
|
+
There are three relevant crashes:
|
|
2329
|
+
|
|
2330
|
+
### DSH crashes
|
|
2331
|
+
|
|
2332
|
+
llama-server remains alive.
|
|
2333
|
+
|
|
2334
|
+
Active slot may still contain valid state.
|
|
2335
|
+
|
|
2336
|
+
v0.1 may ignore this unsaved in-memory opportunity and restore the last durable snapshot.
|
|
2337
|
+
|
|
2338
|
+
Future optimization:
|
|
2339
|
+
|
|
2340
|
+
```text
|
|
2341
|
+
inspect current slot metadata
|
|
2342
|
+
re-associate if ownership can be proven
|
|
2343
|
+
```
|
|
2344
|
+
|
|
2345
|
+
### llama-server crashes
|
|
2346
|
+
|
|
2347
|
+
Only durable snapshots survive.
|
|
2348
|
+
|
|
2349
|
+
After restart:
|
|
2350
|
+
|
|
2351
|
+
```text
|
|
2352
|
+
probe
|
|
2353
|
+
restore snapshot
|
|
2354
|
+
```
|
|
2355
|
+
|
|
2356
|
+
### Machine crashes during snapshot save
|
|
2357
|
+
|
|
2358
|
+
Manifest must continue referencing the previous known-good generation.
|
|
2359
|
+
|
|
2360
|
+
This is why two-file rotation may eventually be useful.
|
|
2361
|
+
|
|
2362
|
+
---
|
|
2363
|
+
|
|
2364
|
+
# 61. Hybrid/recurrent model compatibility
|
|
2365
|
+
|
|
2366
|
+
Qwen3.x hybrid/recurrent architectures make this especially important.
|
|
2367
|
+
|
|
2368
|
+
Define compatibility states:
|
|
2369
|
+
|
|
2370
|
+
```text
|
|
2371
|
+
supported
|
|
2372
|
+
experimental
|
|
2373
|
+
broken
|
|
2374
|
+
unknown
|
|
2375
|
+
```
|
|
2376
|
+
|
|
2377
|
+
Example metadata:
|
|
2378
|
+
|
|
2379
|
+
```json
|
|
2380
|
+
{
|
|
2381
|
+
"persistenceCompatibility": "experimental"
|
|
2382
|
+
}
|
|
2383
|
+
```
|
|
2384
|
+
|
|
2385
|
+
`dsh kv doctor` can perform an opt-in verification:
|
|
2386
|
+
|
|
2387
|
+
```text
|
|
2388
|
+
1. cold prompt
|
|
2389
|
+
2. save
|
|
2390
|
+
3. erase
|
|
2391
|
+
4. restore
|
|
2392
|
+
5. inspect
|
|
2393
|
+
6. identical prompt
|
|
2394
|
+
7. confirm cache reuse
|
|
2395
|
+
```
|
|
2396
|
+
|
|
2397
|
+
An even stronger test:
|
|
2398
|
+
|
|
2399
|
+
```text
|
|
2400
|
+
save
|
|
2401
|
+
restart server manually
|
|
2402
|
+
restore
|
|
2403
|
+
same prompt
|
|
2404
|
+
verify hit
|
|
2405
|
+
```
|
|
2406
|
+
|
|
2407
|
+
The plugin should never assume that receiving `n_restored` means a particular model/build definitely restored usable recurrent state.
|
|
2408
|
+
|
|
2409
|
+
---
|
|
2410
|
+
|
|
2411
|
+
# 62. Compatibility database
|
|
2412
|
+
|
|
2413
|
+
Future versions may contain small rules:
|
|
2414
|
+
|
|
2415
|
+
```ts
|
|
2416
|
+
interface CompatibilityRule {
|
|
2417
|
+
backend: 'llama.cpp'
|
|
2418
|
+
architecture?: string
|
|
2419
|
+
minBuild?: number
|
|
2420
|
+
maxBuild?: number
|
|
2421
|
+
status: 'supported' | 'experimental' | 'broken'
|
|
2422
|
+
note?: string
|
|
2423
|
+
}
|
|
2424
|
+
```
|
|
2425
|
+
|
|
2426
|
+
But avoid hardcoding large brittle version tables initially.
|
|
2427
|
+
|
|
2428
|
+
Prefer runtime verification.
|
|
2429
|
+
|
|
2430
|
+
---
|
|
2431
|
+
|
|
2432
|
+
# 63. Multi-slot architecture
|
|
2433
|
+
|
|
2434
|
+
After MVP, support:
|
|
2435
|
+
|
|
2436
|
+
```text
|
|
2437
|
+
--parallel N
|
|
2438
|
+
```
|
|
2439
|
+
|
|
2440
|
+
with a real slot pool.
|
|
2441
|
+
|
|
2442
|
+
Example N=4:
|
|
2443
|
+
|
|
2444
|
+
```text
|
|
2445
|
+
slot 0 → session A
|
|
2446
|
+
slot 1 → session B
|
|
2447
|
+
slot 2 → session C
|
|
2448
|
+
slot 3 → session D
|
|
2449
|
+
```
|
|
2450
|
+
|
|
2451
|
+
Session E arrives:
|
|
2452
|
+
|
|
2453
|
+
```text
|
|
2454
|
+
choose LRU clean/dirty slot
|
|
2455
|
+
↓
|
|
2456
|
+
save old owner if dirty
|
|
2457
|
+
↓
|
|
2458
|
+
restore E
|
|
2459
|
+
↓
|
|
2460
|
+
bind slot to E
|
|
2461
|
+
```
|
|
2462
|
+
|
|
2463
|
+
---
|
|
2464
|
+
|
|
2465
|
+
# 64. Multi-slot slot selection
|
|
2466
|
+
|
|
2467
|
+
Selection order:
|
|
2468
|
+
|
|
2469
|
+
```text
|
|
2470
|
+
1. slot already owned by requested session
|
|
2471
|
+
2. empty slot
|
|
2472
|
+
3. clean least-recently-used slot
|
|
2473
|
+
4. dirty least-recently-used slot
|
|
2474
|
+
```
|
|
2475
|
+
|
|
2476
|
+
Evicting a dirty slot requires save.
|
|
2477
|
+
|
|
2478
|
+
Pseudo-code:
|
|
2479
|
+
|
|
2480
|
+
```ts
|
|
2481
|
+
function selectSlot(sessionId) {
|
|
2482
|
+
return (
|
|
2483
|
+
ownedBy(sessionId) ??
|
|
2484
|
+
emptySlot() ??
|
|
2485
|
+
lruClean() ??
|
|
2486
|
+
lruDirty()
|
|
2487
|
+
)
|
|
2488
|
+
}
|
|
2489
|
+
```
|
|
2490
|
+
|
|
2491
|
+
---
|
|
2492
|
+
|
|
2493
|
+
# 65. Multi-slot transport
|
|
2494
|
+
|
|
2495
|
+
For managed multi-slot support, introduce:
|
|
2496
|
+
|
|
2497
|
+
```text
|
|
2498
|
+
dsh-llama.cpp adapter
|
|
2499
|
+
```
|
|
2500
|
+
|
|
2501
|
+
or a transport backend capable of injecting:
|
|
2502
|
+
|
|
2503
|
+
```json
|
|
2504
|
+
{
|
|
2505
|
+
"id_slot": 2,
|
|
2506
|
+
"cache_prompt": true
|
|
2507
|
+
}
|
|
2508
|
+
```
|
|
2509
|
+
|
|
2510
|
+
into llama-server requests.
|
|
2511
|
+
|
|
2512
|
+
Potential package architecture:
|
|
2513
|
+
|
|
2514
|
+
```text
|
|
2515
|
+
dsh-kv-persist
|
|
2516
|
+
└─ coordination/service
|
|
2517
|
+
|
|
2518
|
+
dsh-llm-llama-cpp
|
|
2519
|
+
└─ llama-specific transport
|
|
2520
|
+
```
|
|
2521
|
+
|
|
2522
|
+
The two can communicate through:
|
|
2523
|
+
|
|
2524
|
+
```text
|
|
2525
|
+
ctx.kvPersist
|
|
2526
|
+
```
|
|
2527
|
+
|
|
2528
|
+
This is preferable to making the persistence plugin own all OpenAI serialization logic.
|
|
2529
|
+
|
|
2530
|
+
---
|
|
2531
|
+
|
|
2532
|
+
# 66. Alternative multi-slot sidecar
|
|
2533
|
+
|
|
2534
|
+
Another possible backend:
|
|
2535
|
+
|
|
2536
|
+
```text
|
|
2537
|
+
DSH
|
|
2538
|
+
↓
|
|
2539
|
+
normal OpenAI adapter
|
|
2540
|
+
↓
|
|
2541
|
+
local KV-aware reverse proxy
|
|
2542
|
+
↓
|
|
2543
|
+
llama-server
|
|
2544
|
+
```
|
|
2545
|
+
|
|
2546
|
+
Proxy receives a hidden session identifier and injects:
|
|
2547
|
+
|
|
2548
|
+
```text
|
|
2549
|
+
id_slot
|
|
2550
|
+
```
|
|
2551
|
+
|
|
2552
|
+
This is useful if DSH's adapter layer remains intentionally provider-neutral.
|
|
2553
|
+
|
|
2554
|
+
However a native adapter is probably cleaner.
|
|
2555
|
+
|
|
2556
|
+
---
|
|
2557
|
+
|
|
2558
|
+
# 67. Future upstream opportunity
|
|
2559
|
+
|
|
2560
|
+
Potential DSH upstream proposal:
|
|
2561
|
+
|
|
2562
|
+
```ts
|
|
2563
|
+
GenerateOptions.transportMetadata?
|
|
2564
|
+
```
|
|
2565
|
+
|
|
2566
|
+
or an adapter-private request context carrying:
|
|
2567
|
+
|
|
2568
|
+
```text
|
|
2569
|
+
sessionId
|
|
2570
|
+
```
|
|
2571
|
+
|
|
2572
|
+
all the way into adapters.
|
|
2573
|
+
|
|
2574
|
+
DSH already provides `sessionId` as model-hidden routing metadata, so a llama-specific adapter can naturally use that for slot assignment without exposing it to the model.
|
|
2575
|
+
|
|
2576
|
+
Avoid adding llama-specific fields to core DSH vocabulary.
|
|
2577
|
+
|
|
2578
|
+
---
|
|
2579
|
+
|
|
2580
|
+
# 68. MVP scope — v0.1
|
|
2581
|
+
|
|
2582
|
+
The first usable release should contain only:
|
|
2583
|
+
|
|
2584
|
+
```text
|
|
2585
|
+
llama.cpp backend
|
|
2586
|
+
single server
|
|
2587
|
+
single slot
|
|
2588
|
+
explicit managed provider list
|
|
2589
|
+
sessionId → snapshot mapping
|
|
2590
|
+
save on switch
|
|
2591
|
+
idle save
|
|
2592
|
+
save on shutdown/flush
|
|
2593
|
+
lazy restore
|
|
2594
|
+
restore fallback
|
|
2595
|
+
metadata manifests
|
|
2596
|
+
global slot mutex
|
|
2597
|
+
logging
|
|
2598
|
+
status API
|
|
2599
|
+
basic doctor/probe
|
|
2600
|
+
```
|
|
2601
|
+
|
|
2602
|
+
Explicitly NOT in v0.1:
|
|
2603
|
+
|
|
2604
|
+
```text
|
|
2605
|
+
multi-slot
|
|
2606
|
+
UI
|
|
2607
|
+
disk GC
|
|
2608
|
+
multiple servers
|
|
2609
|
+
snapshot migration
|
|
2610
|
+
Prometheus
|
|
2611
|
+
custom adapter
|
|
2612
|
+
automatic server startup
|
|
2613
|
+
```
|
|
2614
|
+
|
|
2615
|
+
Keep v0.1 small enough to actually ship.
|
|
2616
|
+
|
|
2617
|
+
---
|
|
2618
|
+
|
|
2619
|
+
# 69. v0.1 request flow
|
|
2620
|
+
|
|
2621
|
+
Example: first ever session A request.
|
|
2622
|
+
|
|
2623
|
+
```text
|
|
2624
|
+
DSH llm/stream(A)
|
|
2625
|
+
↓
|
|
2626
|
+
plugin sees managed provider
|
|
2627
|
+
↓
|
|
2628
|
+
acquire slot 0
|
|
2629
|
+
↓
|
|
2630
|
+
no current owner
|
|
2631
|
+
↓
|
|
2632
|
+
no snapshot A
|
|
2633
|
+
↓
|
|
2634
|
+
erase slot
|
|
2635
|
+
↓
|
|
2636
|
+
owner = A
|
|
2637
|
+
↓
|
|
2638
|
+
next()
|
|
2639
|
+
↓
|
|
2640
|
+
llama processes prompt
|
|
2641
|
+
↓
|
|
2642
|
+
stream response
|
|
2643
|
+
↓
|
|
2644
|
+
mark A dirty
|
|
2645
|
+
↓
|
|
2646
|
+
release
|
|
2647
|
+
```
|
|
2648
|
+
|
|
2649
|
+
Second request A:
|
|
2650
|
+
|
|
2651
|
+
```text
|
|
2652
|
+
llm/stream(A)
|
|
2653
|
+
↓
|
|
2654
|
+
slot already belongs to A
|
|
2655
|
+
↓
|
|
2656
|
+
no save/restore
|
|
2657
|
+
↓
|
|
2658
|
+
next()
|
|
2659
|
+
↓
|
|
2660
|
+
normal in-memory cache hit
|
|
2661
|
+
```
|
|
2662
|
+
|
|
2663
|
+
This is very important:
|
|
2664
|
+
|
|
2665
|
+
> The plugin must not save/restore when the requested session is already resident.
|
|
2666
|
+
|
|
2667
|
+
Persistence must not make the happy path slower.
|
|
2668
|
+
|
|
2669
|
+
---
|
|
2670
|
+
|
|
2671
|
+
# 70. v0.1 session switch
|
|
2672
|
+
|
|
2673
|
+
A → B:
|
|
2674
|
+
|
|
2675
|
+
```text
|
|
2676
|
+
request B
|
|
2677
|
+
↓
|
|
2678
|
+
acquire slot
|
|
2679
|
+
↓
|
|
2680
|
+
slot owner = A, A dirty
|
|
2681
|
+
↓
|
|
2682
|
+
save A.bin
|
|
2683
|
+
↓
|
|
2684
|
+
mark A saved
|
|
2685
|
+
↓
|
|
2686
|
+
find B snapshot
|
|
2687
|
+
↓
|
|
2688
|
+
restore B.bin
|
|
2689
|
+
↓
|
|
2690
|
+
owner = B
|
|
2691
|
+
↓
|
|
2692
|
+
request B
|
|
2693
|
+
```
|
|
2694
|
+
|
|
2695
|
+
B → A:
|
|
2696
|
+
|
|
2697
|
+
```text
|
|
2698
|
+
save B if dirty
|
|
2699
|
+
restore A
|
|
2700
|
+
run A
|
|
2701
|
+
```
|
|
2702
|
+
|
|
2703
|
+
---
|
|
2704
|
+
|
|
2705
|
+
# 71. v0.1 idle save
|
|
2706
|
+
|
|
2707
|
+
After request A finishes:
|
|
2708
|
+
|
|
2709
|
+
```text
|
|
2710
|
+
A dirty
|
|
2711
|
+
↓
|
|
2712
|
+
start/reset 30s timer
|
|
2713
|
+
```
|
|
2714
|
+
|
|
2715
|
+
If another A request comes within 30 seconds:
|
|
2716
|
+
|
|
2717
|
+
```text
|
|
2718
|
+
cancel/reset timer
|
|
2719
|
+
```
|
|
2720
|
+
|
|
2721
|
+
If idle timer fires:
|
|
2722
|
+
|
|
2723
|
+
```text
|
|
2724
|
+
acquire slot
|
|
2725
|
+
↓
|
|
2726
|
+
confirm slot still owned by A
|
|
2727
|
+
↓
|
|
2728
|
+
confirm same dirty generation
|
|
2729
|
+
↓
|
|
2730
|
+
save
|
|
2731
|
+
↓
|
|
2732
|
+
release
|
|
2733
|
+
```
|
|
2734
|
+
|
|
2735
|
+
Never save based purely on an old timer callback without rechecking ownership.
|
|
2736
|
+
|
|
2737
|
+
---
|
|
2738
|
+
|
|
2739
|
+
# 72. v0.1 auxiliary request flow
|
|
2740
|
+
|
|
2741
|
+
Suppose A is active and DSH starts session-title generation.
|
|
2742
|
+
|
|
2743
|
+
```text
|
|
2744
|
+
A dirty
|
|
2745
|
+
↓
|
|
2746
|
+
aux request detected
|
|
2747
|
+
↓
|
|
2748
|
+
save A
|
|
2749
|
+
↓
|
|
2750
|
+
slot owner cleared
|
|
2751
|
+
↓
|
|
2752
|
+
run title request
|
|
2753
|
+
↓
|
|
2754
|
+
slot owner = auxiliary/unowned
|
|
2755
|
+
```
|
|
2756
|
+
|
|
2757
|
+
Next A request:
|
|
2758
|
+
|
|
2759
|
+
```text
|
|
2760
|
+
restore A
|
|
2761
|
+
```
|
|
2762
|
+
|
|
2763
|
+
This is slower than having a separate aux slot but correct.
|
|
2764
|
+
|
|
2765
|
+
v0.2 multi-slot can reserve:
|
|
2766
|
+
|
|
2767
|
+
```text
|
|
2768
|
+
slot N-1 = auxiliary
|
|
2769
|
+
```
|
|
2770
|
+
|
|
2771
|
+
---
|
|
2772
|
+
|
|
2773
|
+
# 73. Performance targets
|
|
2774
|
+
|
|
2775
|
+
MVP should add almost zero overhead when a session remains resident.
|
|
2776
|
+
|
|
2777
|
+
Resident request overhead target:
|
|
2778
|
+
|
|
2779
|
+
```text
|
|
2780
|
+
< 1 ms plugin CPU overhead
|
|
2781
|
+
0 disk I/O
|
|
2782
|
+
0 extra llama management calls
|
|
2783
|
+
```
|
|
2784
|
+
|
|
2785
|
+
Session restore cost is dominated by backend I/O.
|
|
2786
|
+
|
|
2787
|
+
The plugin should record:
|
|
2788
|
+
|
|
2789
|
+
```text
|
|
2790
|
+
save latency
|
|
2791
|
+
restore latency
|
|
2792
|
+
bytes
|
|
2793
|
+
tokens
|
|
2794
|
+
```
|
|
2795
|
+
|
|
2796
|
+
so the user can compare:
|
|
2797
|
+
|
|
2798
|
+
```text
|
|
2799
|
+
cold prefill time
|
|
2800
|
+
vs
|
|
2801
|
+
restore time
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2804
|
+
---
|
|
2805
|
+
|
|
2806
|
+
# 74. Acceptance criteria for v0.1
|
|
2807
|
+
|
|
2808
|
+
Release `0.1.0` is acceptable when all of the following work:
|
|
2809
|
+
|
|
2810
|
+
1. Start llama-server with slot save path.
|
|
2811
|
+
2. Start DSH with plugin.
|
|
2812
|
+
3. Open session A.
|
|
2813
|
+
4. Send large prompt.
|
|
2814
|
+
5. Slot becomes owned by A.
|
|
2815
|
+
6. Send another A turn.
|
|
2816
|
+
7. No disk save/restore occurs.
|
|
2817
|
+
8. Switch to session B.
|
|
2818
|
+
9. A is saved automatically.
|
|
2819
|
+
10. B runs.
|
|
2820
|
+
11. Switch back to A.
|
|
2821
|
+
12. A snapshot is restored.
|
|
2822
|
+
13. Next request demonstrates substantial prompt-cache reuse.
|
|
2823
|
+
14. Restart DSH.
|
|
2824
|
+
15. Open A and send another message.
|
|
2825
|
+
16. Plugin restores A snapshot.
|
|
2826
|
+
17. Conversation remains correct if snapshot file is manually deleted.
|
|
2827
|
+
18. Conversation remains correct if restore returns an error.
|
|
2828
|
+
19. Unmanaged providers are completely unaffected.
|
|
2829
|
+
20. Plugin hot unload cleans timers/resources.
|
|
2830
|
+
|
|
2831
|
+
---
|
|
2832
|
+
|
|
2833
|
+
# 75. Integration tests
|
|
2834
|
+
|
|
2835
|
+
Minimum integration test suite:
|
|
2836
|
+
|
|
2837
|
+
### Cold start
|
|
2838
|
+
|
|
2839
|
+
```text
|
|
2840
|
+
snapshot absent
|
|
2841
|
+
→ request succeeds
|
|
2842
|
+
→ state dirty
|
|
2843
|
+
```
|
|
2844
|
+
|
|
2845
|
+
### Resident reuse
|
|
2846
|
+
|
|
2847
|
+
```text
|
|
2848
|
+
A request
|
|
2849
|
+
A request
|
|
2850
|
+
→ no save
|
|
2851
|
+
→ no restore
|
|
2852
|
+
```
|
|
2853
|
+
|
|
2854
|
+
### Switch
|
|
2855
|
+
|
|
2856
|
+
```text
|
|
2857
|
+
A
|
|
2858
|
+
B
|
|
2859
|
+
→ save A
|
|
2860
|
+
```
|
|
2861
|
+
|
|
2862
|
+
### Restore
|
|
2863
|
+
|
|
2864
|
+
```text
|
|
2865
|
+
A
|
|
2866
|
+
B
|
|
2867
|
+
A
|
|
2868
|
+
→ restore A
|
|
2869
|
+
```
|
|
2870
|
+
|
|
2871
|
+
### Corrupt snapshot
|
|
2872
|
+
|
|
2873
|
+
```text
|
|
2874
|
+
restore fails
|
|
2875
|
+
→ snapshot invalidated
|
|
2876
|
+
→ cold request succeeds
|
|
2877
|
+
```
|
|
2878
|
+
|
|
2879
|
+
### Backend unavailable
|
|
2880
|
+
|
|
2881
|
+
```text
|
|
2882
|
+
/slots unreachable
|
|
2883
|
+
→ ordinary LLM request still works
|
|
2884
|
+
```
|
|
2885
|
+
|
|
2886
|
+
### Save failure
|
|
2887
|
+
|
|
2888
|
+
```text
|
|
2889
|
+
save A fails
|
|
2890
|
+
→ B still eventually runs in non-strict mode
|
|
2891
|
+
```
|
|
2892
|
+
|
|
2893
|
+
### Auxiliary request
|
|
2894
|
+
|
|
2895
|
+
```text
|
|
2896
|
+
A
|
|
2897
|
+
session-title
|
|
2898
|
+
A
|
|
2899
|
+
→ no incorrect slot ownership
|
|
2900
|
+
```
|
|
2901
|
+
|
|
2902
|
+
### Cancellation
|
|
2903
|
+
|
|
2904
|
+
```text
|
|
2905
|
+
cancel model request
|
|
2906
|
+
→ lock released
|
|
2907
|
+
→ next session still works
|
|
2908
|
+
```
|
|
2909
|
+
|
|
2910
|
+
### HMR/disposal
|
|
2911
|
+
|
|
2912
|
+
```text
|
|
2913
|
+
reload plugin
|
|
2914
|
+
→ no orphan timer
|
|
2915
|
+
→ no dead mutex
|
|
2916
|
+
```
|
|
2917
|
+
|
|
2918
|
+
---
|
|
2919
|
+
|
|
2920
|
+
# 76. Unit tests
|
|
2921
|
+
|
|
2922
|
+
Unit test:
|
|
2923
|
+
|
|
2924
|
+
```text
|
|
2925
|
+
state-machine transitions
|
|
2926
|
+
slot selection
|
|
2927
|
+
snapshot compatibility
|
|
2928
|
+
filename sanitization
|
|
2929
|
+
fingerprint stability
|
|
2930
|
+
dirty revision logic
|
|
2931
|
+
save coalescing
|
|
2932
|
+
idle timer invalidation
|
|
2933
|
+
failure circuit breaker
|
|
2934
|
+
manifest atomicity
|
|
2935
|
+
provider filtering
|
|
2936
|
+
```
|
|
2937
|
+
|
|
2938
|
+
No network should be necessary for these.
|
|
2939
|
+
|
|
2940
|
+
---
|
|
2941
|
+
|
|
2942
|
+
# 77. Fake backend
|
|
2943
|
+
|
|
2944
|
+
Create:
|
|
2945
|
+
|
|
2946
|
+
```ts
|
|
2947
|
+
class FakeKvBackend
|
|
2948
|
+
```
|
|
2949
|
+
|
|
2950
|
+
with deterministic state.
|
|
2951
|
+
|
|
2952
|
+
Example capabilities:
|
|
2953
|
+
|
|
2954
|
+
```ts
|
|
2955
|
+
backend.failNextSave()
|
|
2956
|
+
backend.failNextRestore()
|
|
2957
|
+
backend.delayRestore(100)
|
|
2958
|
+
backend.removeSnapshot(key)
|
|
2959
|
+
backend.corruptSnapshot(key)
|
|
2960
|
+
```
|
|
2961
|
+
|
|
2962
|
+
Most coordinator tests should run against this rather than launching llama-server.
|
|
2963
|
+
|
|
2964
|
+
---
|
|
2965
|
+
|
|
2966
|
+
# 78. Real llama integration test
|
|
2967
|
+
|
|
2968
|
+
Optional test profile:
|
|
2969
|
+
|
|
2970
|
+
```text
|
|
2971
|
+
DSH_KV_TEST_LLAMA_URL=http://127.0.0.1:8080
|
|
2972
|
+
```
|
|
2973
|
+
|
|
2974
|
+
Tests only run when explicitly enabled.
|
|
2975
|
+
|
|
2976
|
+
Never require a GPU in the ordinary CI pipeline.
|
|
2977
|
+
|
|
2978
|
+
---
|
|
2979
|
+
|
|
2980
|
+
# 79. Error taxonomy
|
|
2981
|
+
|
|
2982
|
+
Use stable codes.
|
|
2983
|
+
|
|
2984
|
+
Suggested:
|
|
2985
|
+
|
|
2986
|
+
```text
|
|
2987
|
+
KV_BACKEND_UNAVAILABLE
|
|
2988
|
+
KV_BACKEND_UNSUPPORTED
|
|
2989
|
+
|
|
2990
|
+
KV_SLOT_NOT_FOUND
|
|
2991
|
+
KV_SLOT_BUSY
|
|
2992
|
+
KV_SLOT_STATE_INVALID
|
|
2993
|
+
|
|
2994
|
+
KV_SNAPSHOT_NOT_FOUND
|
|
2995
|
+
KV_SNAPSHOT_INCOMPATIBLE
|
|
2996
|
+
KV_SNAPSHOT_CORRUPT
|
|
2997
|
+
|
|
2998
|
+
KV_SAVE_FAILED
|
|
2999
|
+
KV_RESTORE_FAILED
|
|
3000
|
+
KV_ERASE_FAILED
|
|
3001
|
+
|
|
3002
|
+
KV_MANIFEST_INVALID
|
|
3003
|
+
KV_METADATA_IO
|
|
3004
|
+
|
|
3005
|
+
KV_OPERATION_TIMEOUT
|
|
3006
|
+
|
|
3007
|
+
KV_INVARIANT
|
|
3008
|
+
```
|
|
3009
|
+
|
|
3010
|
+
Infrastructure diagnostics become much easier than matching error strings.
|
|
3011
|
+
|
|
3012
|
+
---
|
|
3013
|
+
|
|
3014
|
+
# 80. Example logs
|
|
3015
|
+
|
|
3016
|
+
First request:
|
|
3017
|
+
|
|
3018
|
+
```text
|
|
3019
|
+
[kv-persist] session cold
|
|
3020
|
+
session=7c856d slot=0
|
|
3021
|
+
```
|
|
3022
|
+
|
|
3023
|
+
Idle checkpoint:
|
|
3024
|
+
|
|
3025
|
+
```text
|
|
3026
|
+
[kv-persist] snapshot saved
|
|
3027
|
+
session=7c856d
|
|
3028
|
+
tokens=48712
|
|
3029
|
+
bytes=2.42GiB
|
|
3030
|
+
save=531ms
|
|
3031
|
+
```
|
|
3032
|
+
|
|
3033
|
+
Resume:
|
|
3034
|
+
|
|
3035
|
+
```text
|
|
3036
|
+
[kv-persist] snapshot restored
|
|
3037
|
+
session=7c856d
|
|
3038
|
+
tokens=48712
|
|
3039
|
+
bytes=2.42GiB
|
|
3040
|
+
restore=188ms
|
|
3041
|
+
```
|
|
3042
|
+
|
|
3043
|
+
Failure:
|
|
3044
|
+
|
|
3045
|
+
```text
|
|
3046
|
+
[kv-persist] restore failed; falling back to cold prefill
|
|
3047
|
+
session=7c856d
|
|
3048
|
+
code=KV_RESTORE_FAILED
|
|
3049
|
+
```
|
|
3050
|
+
|
|
3051
|
+
---
|
|
3052
|
+
|
|
3053
|
+
# 81. User-visible UX
|
|
3054
|
+
|
|
3055
|
+
Most of the time:
|
|
3056
|
+
|
|
3057
|
+
```text
|
|
3058
|
+
nothing
|
|
3059
|
+
```
|
|
3060
|
+
|
|
3061
|
+
It should simply make old local-model sessions resume quickly.
|
|
3062
|
+
|
|
3063
|
+
Potential status line later:
|
|
3064
|
+
|
|
3065
|
+
```text
|
|
3066
|
+
KV: restored 48.7K · 188ms
|
|
3067
|
+
```
|
|
3068
|
+
|
|
3069
|
+
or:
|
|
3070
|
+
|
|
3071
|
+
```text
|
|
3072
|
+
KV: resident
|
|
3073
|
+
```
|
|
3074
|
+
|
|
3075
|
+
or:
|
|
3076
|
+
|
|
3077
|
+
```text
|
|
3078
|
+
KV: cold
|
|
3079
|
+
```
|
|
3080
|
+
|
|
3081
|
+
But this belongs to a later UI integration and should not block the core plugin.
|
|
3082
|
+
|
|
3083
|
+
---
|
|
3084
|
+
|
|
3085
|
+
# 82. Suggested README pitch
|
|
3086
|
+
|
|
3087
|
+
> `dsh-kv-persist` keeps local LLM sessions warm across session switches and restarts.
|
|
3088
|
+
>
|
|
3089
|
+
> It maps DeepSeek Harness sessions to persistent inference-cache snapshots and restores them when a session becomes active again. The initial backend uses llama.cpp's slot save/restore API, allowing large agent contexts to resume without repeating a full prompt prefill.
|
|
3090
|
+
>
|
|
3091
|
+
> KV state is treated strictly as an optimization: DSH's session log remains the source of truth, and any missing, stale, or incompatible cache automatically falls back to normal inference.
|
|
3092
|
+
|
|
3093
|
+
---
|
|
3094
|
+
|
|
3095
|
+
# 83. Roadmap
|
|
3096
|
+
|
|
3097
|
+
## Phase 0 — research/prototype
|
|
3098
|
+
|
|
3099
|
+
- Validate llama.cpp save/restore with target Qwen3.8 build.
|
|
3100
|
+
- Verify restore within same server process.
|
|
3101
|
+
- Verify restore across llama-server restart.
|
|
3102
|
+
- Measure snapshot sizes.
|
|
3103
|
+
- Measure save/restore throughput.
|
|
3104
|
+
- Confirm cache hit after restore.
|
|
3105
|
+
- Document hybrid-model behavior.
|
|
3106
|
+
|
|
3107
|
+
## Phase 1 — MVP / `0.1`
|
|
3108
|
+
|
|
3109
|
+
- Cordis service.
|
|
3110
|
+
- llama.cpp client.
|
|
3111
|
+
- backend probe.
|
|
3112
|
+
- single-slot coordinator.
|
|
3113
|
+
- session mapping.
|
|
3114
|
+
- `llm/stream` wrapper.
|
|
3115
|
+
- lazy restore.
|
|
3116
|
+
- save-before-switch.
|
|
3117
|
+
- idle checkpoint.
|
|
3118
|
+
- shutdown/session-flush checkpoint.
|
|
3119
|
+
- local metadata.
|
|
3120
|
+
- logging.
|
|
3121
|
+
- cold fallback.
|
|
3122
|
+
- fake backend tests.
|
|
3123
|
+
|
|
3124
|
+
## Phase 2 — reliability / `0.2`
|
|
3125
|
+
|
|
3126
|
+
- compatibility fingerprints.
|
|
3127
|
+
- circuit breaker.
|
|
3128
|
+
- snapshot verification.
|
|
3129
|
+
- atomic snapshot rotation.
|
|
3130
|
+
- diagnostics API.
|
|
3131
|
+
- `doctor`.
|
|
3132
|
+
- cleanup tooling.
|
|
3133
|
+
- improved hybrid/recurrent testing.
|
|
3134
|
+
|
|
3135
|
+
## Phase 3 — multi-slot / `0.3`
|
|
3136
|
+
|
|
3137
|
+
- slot pool.
|
|
3138
|
+
- LRU assignment.
|
|
3139
|
+
- explicit slot leases.
|
|
3140
|
+
- llama-specific transport integration.
|
|
3141
|
+
- request `id_slot`.
|
|
3142
|
+
- auxiliary slot reservation.
|
|
3143
|
+
- concurrent sessions.
|
|
3144
|
+
|
|
3145
|
+
## Phase 4 — observability / `0.4`
|
|
3146
|
+
|
|
3147
|
+
- metrics.
|
|
3148
|
+
- cache hit statistics.
|
|
3149
|
+
- storage statistics.
|
|
3150
|
+
- performance comparisons.
|
|
3151
|
+
- optional DSH UI panel.
|
|
3152
|
+
|
|
3153
|
+
## Phase 5 — generalized persistence / `1.0`
|
|
3154
|
+
|
|
3155
|
+
- stable backend interface.
|
|
3156
|
+
- multiple servers.
|
|
3157
|
+
- multiple backends.
|
|
3158
|
+
- retention policies.
|
|
3159
|
+
- documented API for external plugins.
|
|
3160
|
+
- production-hardening.
|
|
3161
|
+
|
|
3162
|
+
---
|
|
3163
|
+
|
|
3164
|
+
# 84. First implementation milestone
|
|
3165
|
+
|
|
3166
|
+
The first prototype should intentionally do almost nothing clever.
|
|
3167
|
+
|
|
3168
|
+
Hardcode/test:
|
|
3169
|
+
|
|
3170
|
+
```text
|
|
3171
|
+
provider = local-qwen
|
|
3172
|
+
slot = 0
|
|
3173
|
+
server = localhost:8080
|
|
3174
|
+
```
|
|
3175
|
+
|
|
3176
|
+
Implement only:
|
|
3177
|
+
|
|
3178
|
+
```text
|
|
3179
|
+
request A
|
|
3180
|
+
request A
|
|
3181
|
+
request B
|
|
3182
|
+
request A
|
|
3183
|
+
```
|
|
3184
|
+
|
|
3185
|
+
Expected management calls:
|
|
3186
|
+
|
|
3187
|
+
```text
|
|
3188
|
+
A #1:
|
|
3189
|
+
erase
|
|
3190
|
+
|
|
3191
|
+
A #2:
|
|
3192
|
+
none
|
|
3193
|
+
|
|
3194
|
+
B:
|
|
3195
|
+
save A
|
|
3196
|
+
erase/restore B
|
|
3197
|
+
|
|
3198
|
+
A #3:
|
|
3199
|
+
save B
|
|
3200
|
+
restore A
|
|
3201
|
+
```
|
|
3202
|
+
|
|
3203
|
+
Once this works reliably, abstract it.
|
|
3204
|
+
|
|
3205
|
+
Do not start by implementing:
|
|
3206
|
+
|
|
3207
|
+
```text
|
|
3208
|
+
multi-server
|
|
3209
|
+
multi-slot
|
|
3210
|
+
GC
|
|
3211
|
+
UI
|
|
3212
|
+
dynamic provider discovery
|
|
3213
|
+
```
|
|
3214
|
+
|
|
3215
|
+
before proving the fundamental cache lifecycle.
|
|
3216
|
+
|
|
3217
|
+
---
|
|
3218
|
+
|
|
3219
|
+
# 85. Key architectural invariants
|
|
3220
|
+
|
|
3221
|
+
These should eventually exist as comments/tests.
|
|
3222
|
+
|
|
3223
|
+
**Invariant 1**
|
|
3224
|
+
|
|
3225
|
+
```text
|
|
3226
|
+
DSH session state never depends on KV persistence.
|
|
3227
|
+
```
|
|
3228
|
+
|
|
3229
|
+
**Invariant 2**
|
|
3230
|
+
|
|
3231
|
+
```text
|
|
3232
|
+
At most one owner controls a physical slot at a time.
|
|
3233
|
+
```
|
|
3234
|
+
|
|
3235
|
+
**Invariant 3**
|
|
3236
|
+
|
|
3237
|
+
```text
|
|
3238
|
+
A dirty slot is checkpointed before reassignment unless policy explicitly disables it.
|
|
3239
|
+
```
|
|
3240
|
+
|
|
3241
|
+
**Invariant 4**
|
|
3242
|
+
|
|
3243
|
+
```text
|
|
3244
|
+
A snapshot is restored only when its runtime identity is compatible.
|
|
3245
|
+
```
|
|
3246
|
+
|
|
3247
|
+
**Invariant 5**
|
|
3248
|
+
|
|
3249
|
+
```text
|
|
3250
|
+
Persistence failure defaults to cold inference.
|
|
3251
|
+
```
|
|
3252
|
+
|
|
3253
|
+
**Invariant 6**
|
|
3254
|
+
|
|
3255
|
+
```text
|
|
3256
|
+
Resident-session requests incur no disk I/O.
|
|
3257
|
+
```
|
|
3258
|
+
|
|
3259
|
+
**Invariant 7**
|
|
3260
|
+
|
|
3261
|
+
```text
|
|
3262
|
+
Auxiliary LLM requests never become authoritative state for a conversation session.
|
|
3263
|
+
```
|
|
3264
|
+
|
|
3265
|
+
**Invariant 8**
|
|
3266
|
+
|
|
3267
|
+
```text
|
|
3268
|
+
All backend mutation operations are serialized per physical slot.
|
|
3269
|
+
```
|
|
3270
|
+
|
|
3271
|
+
**Invariant 9**
|
|
3272
|
+
|
|
3273
|
+
```text
|
|
3274
|
+
Snapshot filenames are plugin-generated opaque identifiers.
|
|
3275
|
+
```
|
|
3276
|
+
|
|
3277
|
+
**Invariant 10**
|
|
3278
|
+
|
|
3279
|
+
```text
|
|
3280
|
+
A successful HTTP restore is not automatically equivalent to a verified usable restore.
|
|
3281
|
+
```
|
|
3282
|
+
|
|
3283
|
+
---
|
|
3284
|
+
|
|
3285
|
+
# 86. Recommended initial technical direction
|
|
3286
|
+
|
|
3287
|
+
For the first release, use:
|
|
3288
|
+
|
|
3289
|
+
```text
|
|
3290
|
+
Cordis plugin
|
|
3291
|
+
+
|
|
3292
|
+
ctx.sessions lifecycle
|
|
3293
|
+
+
|
|
3294
|
+
llm/stream observation
|
|
3295
|
+
+
|
|
3296
|
+
single llama slot
|
|
3297
|
+
+
|
|
3298
|
+
server-side snapshot files
|
|
3299
|
+
```
|
|
3300
|
+
|
|
3301
|
+
Do NOT fork or patch DeepSeek Harness.
|
|
3302
|
+
|
|
3303
|
+
Do NOT replace the existing OpenAI-compatible provider.
|
|
3304
|
+
|
|
3305
|
+
Do NOT modify prompts.
|
|
3306
|
+
|
|
3307
|
+
Do NOT make cache state part of SessionEvent history.
|
|
3308
|
+
|
|
3309
|
+
Once the single-slot implementation proves useful, introduce the llama-specific transport adapter needed for explicit `id_slot` and proper multi-session concurrency.
|
|
3310
|
+
|
|
3311
|
+
This yields a plugin that starts as a small, useful local optimization but has a clean path toward becoming a general persistence/cache coordinator for local inference runtimes.
|