@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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/compatibility.json +16 -0
  4. package/cordis.patch.yml +4 -0
  5. package/docs/dsh-kv-persist.md +3311 -0
  6. package/lib/backends/llama-cpp/backend.d.ts +20 -0
  7. package/lib/backends/llama-cpp/backend.d.ts.map +1 -0
  8. package/lib/backends/llama-cpp/backend.js +37 -0
  9. package/lib/backends/llama-cpp/backend.js.map +1 -0
  10. package/lib/backends/llama-cpp/client.d.ts +31 -0
  11. package/lib/backends/llama-cpp/client.d.ts.map +1 -0
  12. package/lib/backends/llama-cpp/client.js +120 -0
  13. package/lib/backends/llama-cpp/client.js.map +1 -0
  14. package/lib/backends/llama-cpp/discovery.d.ts +20 -0
  15. package/lib/backends/llama-cpp/discovery.d.ts.map +1 -0
  16. package/lib/backends/llama-cpp/discovery.js +33 -0
  17. package/lib/backends/llama-cpp/discovery.js.map +1 -0
  18. package/lib/backends/llama-cpp/types.d.ts +30 -0
  19. package/lib/backends/llama-cpp/types.d.ts.map +1 -0
  20. package/lib/backends/llama-cpp/types.js +9 -0
  21. package/lib/backends/llama-cpp/types.js.map +1 -0
  22. package/lib/backends/types.d.ts +52 -0
  23. package/lib/backends/types.d.ts.map +1 -0
  24. package/lib/backends/types.js +6 -0
  25. package/lib/backends/types.js.map +1 -0
  26. package/lib/config.d.ts +258 -0
  27. package/lib/config.d.ts.map +1 -0
  28. package/lib/config.js +169 -0
  29. package/lib/config.js.map +1 -0
  30. package/lib/coordinator/checkpoint-policy.d.ts +19 -0
  31. package/lib/coordinator/checkpoint-policy.d.ts.map +1 -0
  32. package/lib/coordinator/checkpoint-policy.js +40 -0
  33. package/lib/coordinator/checkpoint-policy.js.map +1 -0
  34. package/lib/coordinator/circuit-breaker.d.ts +29 -0
  35. package/lib/coordinator/circuit-breaker.d.ts.map +1 -0
  36. package/lib/coordinator/circuit-breaker.js +62 -0
  37. package/lib/coordinator/circuit-breaker.js.map +1 -0
  38. package/lib/coordinator/coordinator.d.ts +74 -0
  39. package/lib/coordinator/coordinator.d.ts.map +1 -0
  40. package/lib/coordinator/coordinator.js +542 -0
  41. package/lib/coordinator/coordinator.js.map +1 -0
  42. package/lib/coordinator/slot-lease.d.ts +22 -0
  43. package/lib/coordinator/slot-lease.d.ts.map +1 -0
  44. package/lib/coordinator/slot-lease.js +38 -0
  45. package/lib/coordinator/slot-lease.js.map +1 -0
  46. package/lib/coordinator/state-machine.d.ts +65 -0
  47. package/lib/coordinator/state-machine.d.ts.map +1 -0
  48. package/lib/coordinator/state-machine.js +40 -0
  49. package/lib/coordinator/state-machine.js.map +1 -0
  50. package/lib/errors.d.ts +82 -0
  51. package/lib/errors.d.ts.map +1 -0
  52. package/lib/errors.js +113 -0
  53. package/lib/errors.js.map +1 -0
  54. package/lib/index.d.ts +26 -0
  55. package/lib/index.d.ts.map +1 -0
  56. package/lib/index.js +17 -0
  57. package/lib/index.js.map +1 -0
  58. package/lib/logging/dsh-home.d.ts +13 -0
  59. package/lib/logging/dsh-home.d.ts.map +1 -0
  60. package/lib/logging/dsh-home.js +18 -0
  61. package/lib/logging/dsh-home.js.map +1 -0
  62. package/lib/logging/index.d.ts +10 -0
  63. package/lib/logging/index.d.ts.map +1 -0
  64. package/lib/logging/index.js +10 -0
  65. package/lib/logging/index.js.map +1 -0
  66. package/lib/logging/plugin-logger.d.ts +94 -0
  67. package/lib/logging/plugin-logger.d.ts.map +1 -0
  68. package/lib/logging/plugin-logger.js +397 -0
  69. package/lib/logging/plugin-logger.js.map +1 -0
  70. package/lib/observability/diagnostics.d.ts +37 -0
  71. package/lib/observability/diagnostics.d.ts.map +1 -0
  72. package/lib/observability/diagnostics.js +75 -0
  73. package/lib/observability/diagnostics.js.map +1 -0
  74. package/lib/observability/metrics.d.ts +38 -0
  75. package/lib/observability/metrics.d.ts.map +1 -0
  76. package/lib/observability/metrics.js +29 -0
  77. package/lib/observability/metrics.js.map +1 -0
  78. package/lib/service.d.ts +231 -0
  79. package/lib/service.d.ts.map +1 -0
  80. package/lib/service.js +232 -0
  81. package/lib/service.js.map +1 -0
  82. package/lib/snapshots/fingerprint.d.ts +53 -0
  83. package/lib/snapshots/fingerprint.d.ts.map +1 -0
  84. package/lib/snapshots/fingerprint.js +69 -0
  85. package/lib/snapshots/fingerprint.js.map +1 -0
  86. package/lib/snapshots/manifest.d.ts +51 -0
  87. package/lib/snapshots/manifest.d.ts.map +1 -0
  88. package/lib/snapshots/manifest.js +0 -0
  89. package/lib/snapshots/manifest.js.map +1 -0
  90. package/lib/snapshots/naming.d.ts +18 -0
  91. package/lib/snapshots/naming.d.ts.map +1 -0
  92. package/lib/snapshots/naming.js +33 -0
  93. package/lib/snapshots/naming.js.map +1 -0
  94. package/lib/snapshots/repository.d.ts +52 -0
  95. package/lib/snapshots/repository.d.ts.map +1 -0
  96. package/lib/snapshots/repository.js +270 -0
  97. package/lib/snapshots/repository.js.map +1 -0
  98. 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.