velesdb-memory 0.14.0

VelesDB-memory: local-first MCP memory server for AI agents (remember/recall/relate/forget/why + deterministic context compiler).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
//! Data model of the context compiler: the request/response value types.
//!
//! Like [`crate::model`], these are pure data with `Serialize`/`Deserialize` +
//! `JsonSchema` derives, so the domain types double as the MCP wire types —
//! no duplicate DTO layer. Invariants the compiler upholds over these shapes:
//! same request ⇒ byte-identical [`CompiledContext`] (determinism), and the
//! assembled content never exceeds the request's token budget.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use super::chunk::ChunkPolicy;
use super::insights::CompilationInsights;

/// What the compiler decided to do with one fragment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ContextAction {
    /// Emitted verbatim — critical content (code, constraints, exact values).
    Preserve,
    /// Emitted as a deterministic structured reduction (never a generative
    /// summary) — e.g. repeated log lines collapsed with a count.
    Abstract,
    /// Not emitted, but recoverable through its `ctx://source/<id>` handle.
    Retrieve,
    /// Not emitted and not externalized — redundant content (duplicates).
    Drop,
    /// Emitted verbatim at the front of the output, forming a stable prefix
    /// that maximizes provider prompt-cache hits across compilations.
    Cache,
}

/// How much fidelity a compiled context may have lost versus its input.
///
/// Ordered: `Low < Medium < High`, so callers can compare against a policy
/// threshold.
#[derive(
    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum FidelityRisk {
    /// Nothing was lost: everything fit, only exact duplicates were dropped.
    #[default]
    Low,
    /// Recoverable reductions happened: abstractions, or non-critical
    /// fragments externalized behind retrieval handles.
    Medium,
    /// Critical content (a preserve-classified fragment) could not be packed
    /// — the caller should consider retrieving it or raising the budget.
    High,
}

/// Inline media payload attached to a [`ContextFragment`] (US-009, PR1:
/// screenshots/images only). `ContextFragment::content` stays the
/// text/caption — often empty for a bare screenshot — while the pixels live
/// here, base64-encoded so the JSON wire never needs a binary frame.
///
/// The fragment packs atomically (see [`super::pieces`] in the compiler)
/// and its token cost comes from [`super::estimator::ImageTokenEstimator`].
/// A media fragment that cannot fit the budget is externalized behind a
/// `ctx://source` handle exactly like text (US-009, PR2: the memory bridge
/// persists the bytes behind it — see
/// [`crate::MemoryService::retrieve_context_source`]); the memoryless core
/// compiler mints the same handle either way, since it never knows whether
/// a resolver is attached. See the crate README's "media fragments"
/// section.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MediaRef {
    /// Declared MIME type (e.g. `"image/png"`, `"image/jpeg"`). Only PNG and
    /// JPEG headers are sniffed for dimensions; any other value (or an
    /// unreadable header) falls back to a deterministic, safe over-count
    /// (see [`super::estimator::ImageTokenEstimator`]) — never rejected for
    /// an unrecognized mime alone.
    pub mime: String,
    /// The raw media bytes, base64-encoded (standard alphabet, padded).
    /// Capped at [`crate::limits::MAX_MEDIA_BYTES`] and validated for
    /// well-formedness at compile time — a request carrying an oversized or
    /// malformed payload is rejected before any other work.
    pub bytes_b64: String,
}

/// The resolved original behind a `ctx://source/<hash>` handle (US-002:
/// text sources; US-009 PR2 extends this with the fragment's inline media,
/// when it carried one). `media` is `#[serde(default)]`: every source
/// stored before PR2, and every text-only fragment since, round-trips with
/// `media: None` — the exact pre-PR2 shape for a caller reading only
/// `.content`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ContextSource {
    /// The original fragment content, byte for byte (a media fragment's
    /// caption — often empty for a bare screenshot).
    pub content: String,
    /// The original media payload, when the fragment carried one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub media: Option<MediaRef>,
}

/// One unit of caller-supplied context to compile.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct ContextFragment {
    /// Caller-side identifier. When absent, the compiler derives a stable
    /// content-addressed id (see [`super::fragment_id`]). Accepts a JSON
    /// number or a decimal string on input (see
    /// [`super::wire::deserialize_optional_id`]) — a caller that got a
    /// `fragment_id` back as a string (e.g. under
    /// [`CompilePolicy::ids_as_strings`]) can resubmit it unchanged.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::wire::deserialize_optional_id"
    )]
    pub id: Option<u64>,
    /// The fragment text. `#[serde(default)]` (V2b-1): a `path` fragment
    /// carries no `content` on the wire — the adapter resolves `path` into
    /// `content` in a pre-pass before the pure compiler core ever sees the
    /// request, so this stays the only field the core reads.
    #[serde(default)]
    pub content: String,
    /// Read this file's content from disk in place of an inline `content`
    /// (V2b-1 path ingestion). `path` is EXCLUSIVE — it is resolved into
    /// `content` before the compiler core runs, so a fragment carrying
    /// `path` together with `content` or `media` is rejected. `content` and
    /// `media` together are fine, and are the intended shape for an image
    /// and its caption. A fragment carrying none of the three is rejected
    /// as well. Requires the server to be started
    /// with `VELESDB_MEMORY_INGEST_ROOTS` set (a colon/semicolon-separated
    /// allowlist of directories, platform `PATH`-list syntax); otherwise
    /// every `path` fragment fails with an explicit "ingestion disabled"
    /// error. The path must be absolute and resolve (after following
    /// symlinks) to a plain file under one of the configured roots, no
    /// larger than [`crate::limits::MAX_INGEST_FILE_BYTES`] and valid UTF-8.
    /// The **pure compiler core never reads this field** — resolution is an
    /// adapter-side I/O pre-pass (see the crate's `context::ingest`
    /// module); a `path` fragment that reaches [`super::ContextCompiler`]
    /// unresolved is rejected with
    /// [`crate::error::MemoryError::IngestDisabled`].
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    /// Free-form kind hint (`"code"`, `"log"`, `"prose"`, …) — classification
    /// works without it, but honors it when present.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    /// Caller priority, higher packs first (default `0`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub priority: Option<u8>,
    /// Caller metadata. Recognized keys: `"verbatim": true` forces
    /// [`ContextAction::Preserve`]; `"cache": true` forces
    /// [`ContextAction::Cache`].
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Map<String, Value>>,
    /// Inline media payload (US-009, PR1). `None` (the default) keeps every
    /// pre-0.9.0 request wire-compatible. When set, the fragment packs as one
    /// atomic piece — see [`MediaRef`].
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub media: Option<MediaRef>,
}

/// Which memories the compiler may pull in alongside the caller's fragments.
/// Consumed by the memory bridge (US-002); carried in the request shape from
/// the start so the wire contract does not change when it lands.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct MemoryScope {
    /// Restrict recalled memories to this project facet.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project: Option<String>,
    /// How many memories to consider (adapter-clamped).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "crate::wire::lenient"
    )]
    pub k: Option<usize>,
    /// Graph-walk depth of the fused recall (default 2). Deeper hops reach
    /// longer cause/fix chains from the vector seed.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "crate::wire::lenient"
    )]
    pub hops: Option<usize>,
    /// Fusion weight added to graph-reached memories (default 0.15). Raise
    /// it (e.g. `0.5`–`0.8`) when pulling from curated fact chains built
    /// with `relate`: evidence that shares **no vocabulary** with the query
    /// can then out-rank lexically-noisy near-misses — the tri-engine's
    /// answer to the purely lexical relevance of caller fragments.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "crate::wire::lenient"
    )]
    pub graph_boost: Option<f64>,
}

/// Usage-driven importance weights of the memory-bridge blend (US-002 of
/// EPIC-P-071): how much a pulled memory's learned RL confidence and its
/// batch-relative recency tilt the fused similarity ranking.
///
/// The blend only ever applies to the pool the fused vector+graph similarity
/// already selected — confidence is *not* relevance, so a heavily reinforced
/// but off-topic fact can never enter the pool through these weights. Per
/// pulled memory the ranking key becomes
/// `fused_norm + confidence_weight·(confidence − 0.5)·2 + recency_weight·recency_norm`,
/// clock-free and deterministic (recency is min-max normalised **within the
/// pulled batch**, never against wall time).
///
/// Both weights at `0.0` disable the blend entirely: the output is
/// byte-identical to the 0.8.0 behaviour (pinned by a golden test). The
/// defaults are **active** on purpose — upgrading from 0.8.0 with the
/// default policy, RL-reinforced memories rank higher out of the box; zero
/// the weights to restore the exact 0.8.0 ordering.
///
/// Recommended range for both weights: `[0.0, 1.0]` (at `1.0` a term can
/// fully offset the similarity gap within the pool). Values outside that
/// range are **accepted verbatim, never clamped** — a negative weight
/// deliberately inverts its term (e.g. demote reinforced facts), a weight
/// above `1.0` lets the term dominate similarity. Only the recorded
/// decision `relevance` is clamped into `[0, 1]`; the ranking itself uses
/// the raw blended score.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct ImportanceWeights {
    /// Weight of the learned RL confidence (`_veles_rl_*`, fed by
    /// [`feedback`](crate::MemoryService::feedback)). A memory with no
    /// feedback history counts as the neutral `0.5`, contributing exactly
    /// `0`. Default `0.2`.
    pub confidence: f64,
    /// Weight of the batch-relative recency term. Inert unless
    /// [`Self::recency_field`] is also set. Default `0.1`.
    pub recency: f64,
    /// Caller metadata key holding each memory's **numeric** timestamp-like
    /// value. `None` (the default) disables the recency term completely —
    /// there is no standard key to guess. The scale must be monotone and
    /// homogeneous across the batch (e.g. `YYYYMMDD` integers as in
    /// [`crate::format_dated_context`], or an epoch); it is documented, not
    /// verified at run time. Values are min-max normalised over the pulled
    /// memories that carry the key; a memory without the key contributes `0`
    /// (never penalised), and a degenerate batch (`max == min`) contributes
    /// `0` for all.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub recency_field: Option<String>,
}

impl Default for ImportanceWeights {
    fn default() -> Self {
        Self {
            confidence: 0.2,
            recency: 0.1,
            recency_field: None,
        }
    }
}

/// Tuning knobs of one compilation. `Default` is the recommended profile.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
#[schemars(transform = crate::schema::strip_int_formats)]
#[allow(clippy::struct_excessive_bools)]
pub struct CompilePolicy {
    /// Tokens kept aside for the model's answer; the compiler packs into
    /// `token_budget − response_reserve_tokens`. Default `0`: the caller
    /// knows their generation length, the compiler does not guess it.
    pub response_reserve_tokens: u64,
    /// Collapse near-duplicates (case/whitespace variants) in addition to
    /// exact duplicates. Default `true`.
    pub near_dup_dedup: bool,
    /// Rule ids to disable (e.g. `"abstract.log_dedup"`). Disabled rules are
    /// skipped during classification; their fragments fall through to the
    /// next matching rule.
    pub disabled_rules: Vec<String>,
    /// How oversized fragments are split before packing. Only
    /// [`ChunkPolicy::max_chunk_bytes`] and [`ChunkPolicy::boundary`] apply
    /// here — the compiler forces `overlap_bytes` to `0`, since it emits
    /// pieces by concatenation and an overlap prefix would duplicate content
    /// reported as verbatim. `overlap_bytes` is honoured only by the
    /// standalone [`crate::context::chunk::chunk_text`] API.
    pub chunk: ChunkPolicy,
    /// Memory bridge only: record a compilation event (metadata and hashes,
    /// **never fragment content**) so savings stay aggregatable. Default
    /// `true`; set `false` to opt out entirely.
    pub record_events: bool,
    /// Memory bridge only: store each distinct fragment's original (as an
    /// internal system fact, invisible to normal recall) so its
    /// `ctx://source/<hash>` handle round-trips. Default `true`.
    pub store_sources: bool,
    /// TTL applied to stored sources (`None` keeps them until forgotten).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_ttl_seconds: Option<u64>,
    /// TTL applied to compilation events (`None` keeps them).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub event_ttl_seconds: Option<u64>,
    /// Caller-supplied pricing table so the insights also report the
    /// estimated cost avoided for [`super::model::CompileRequest::target_model`] — the
    /// **wire channel** for cost accounting (MCP and the bindings cannot
    /// reach the Rust-only [`super::ContextCompiler::with_pricing`] builder).
    /// Takes precedence over a builder-injected table. `None` (default)
    /// reports tokens only.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pricing: Option<super::insights::PricingTable>,
    /// Memory bridge only: usage-driven importance blend over the pulled
    /// memories (RL confidence + batch-relative recency). The struct-level
    /// `#[serde(default)]` keeps 0.8.0 requests wire-compatible.
    pub importance: ImportanceWeights,
    /// Opt-in, deterministic: before `abstract.log_dedup` groups a `kind =
    /// "log"` fragment's repeated lines, mask each line's volatile prefix
    /// (ISO/syslog timestamps, bracketed hex/pid counters) with **fixed**
    /// patterns — never a caller-supplied regex, so the collapse stays
    /// reproducible — so lines identical modulo timestamp collapse into one
    /// annotated line instead of surviving as distinct entries. The emitted
    /// line is still the first occurrence's exact bytes; only the grouping
    /// key changes. Default `false`: masking is opt-in because it changes
    /// what "duplicate" means for logs, so callers who rely on the previous
    /// byte-exact grouping keep it unless they ask. See the crate README's
    /// "Normalizing timestamped logs" section for the exact patterns.
    pub normalize_log_timestamps: bool,
    /// Wire-compat opt-in for the MCP context tools (`compile_context`,
    /// `explain_compilation`): when `true`, every [`super::wire::ID_KEYS`]
    /// field of the RESPONSE (`fragment_id`, `content_hash`, `memory_id`,
    /// `fragment_ids`) is rewritten into its decimal-string form, through
    /// the exact same tree walk the Node and WASM bindings already apply on
    /// every response ([`super::wire::stringify_id_fields`]). A raw MCP
    /// client — one that talks JSON-RPC directly, without either binding —
    /// parses ids as JS `number`s (IEEE-754 doubles), which silently lose
    /// precision above 2^53; string ids round-trip exactly. Default
    /// `false`: existing MCP clients keep today's byte-identical numeric
    /// response unless they opt in.
    pub ids_as_strings: bool,
    /// Quick win (V2a-2): when `true`, `sections` and `decisions` are
    /// emptied out of the response after compilation — `content`,
    /// `insights`, `risk`, `warnings`, `sources`, and `retrieval_handles`
    /// are unaffected. The full audit trail (`sections`/`decisions`) is
    /// still recoverable: re-compile the same request without
    /// `slim_response` (compilation is deterministic, so nothing is lost,
    /// only not sent this time). Default `false`.
    pub slim_response: bool,
}

impl Default for CompilePolicy {
    fn default() -> Self {
        Self {
            response_reserve_tokens: 0,
            near_dup_dedup: true,
            disabled_rules: Vec::new(),
            chunk: ChunkPolicy::default(),
            record_events: true,
            store_sources: true,
            source_ttl_seconds: None,
            event_ttl_seconds: None,
            pricing: None,
            importance: ImportanceWeights::default(),
            normalize_log_timestamps: false,
            ids_as_strings: false,
            slim_response: false,
        }
    }
}

/// Aggregated savings over the recorded compilation events (memory bridge).
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct ContextSavings {
    /// Number of compilation events aggregated.
    pub events: u64,
    /// Sum of estimated input tokens across events.
    pub tokens_in: u64,
    /// Sum of estimated output tokens across events.
    pub tokens_out: u64,
    /// Sum of estimated tokens saved across events.
    pub tokens_saved: u64,
    /// Estimated cost avoided, in micro-units, keyed by currency (events
    /// priced under different pricing tables never silently mix).
    pub cost_saved_micros_by_currency: std::collections::BTreeMap<String, u64>,
    /// `true` when the aggregation hit the recall cap
    /// ([`crate::limits::MAX_RECALL_LIMIT`]) — older events beyond the cap
    /// were not folded in.
    pub truncated: bool,
}

/// A full compile request: what to compile, under which budget, for whom.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct CompileRequest {
    /// What the agent is working on — drives relevance scoring.
    pub query: String,
    /// The context fragments to compile.
    pub fragments: Vec<ContextFragment>,
    /// Project facet, recorded in provenance and used by the memory bridge.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project: Option<String>,
    /// Target model name — selects the row of the pricing table
    /// ([`CompilePolicy::pricing`] on the wire, or the Rust
    /// [`super::ContextCompiler::with_pricing`] builder) for cost insights.
    /// Without a table, insights report tokens only.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_model: Option<String>,
    /// Hard token ceiling for the assembled content.
    #[serde(deserialize_with = "crate::wire::lenient")]
    pub token_budget: u64,
    /// Which memories may be pulled in (US-002; ignored by the memoryless core).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub memory_scope: Option<MemoryScope>,
    /// Per-request policy override; `None` uses the compiler's policy.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy: Option<CompilePolicy>,
}

/// Where a section sits in the assembled output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SectionKind {
    /// The stable, cache-marked prefix.
    Cache,
    /// The main compiled body.
    Body,
}

/// One contiguous block of the assembled output.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct CompiledSection {
    /// Which block this is.
    pub kind: SectionKind,
    /// The block's text (verbatim slice of [`CompiledContext::content`]).
    pub content: String,
    /// Ids of the fragments emitted into this block, in emission order.
    pub fragment_ids: Vec<u64>,
}

/// A pointer from a compiled output back to one original fragment.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct SourceReference {
    /// The fragment this source refers to. Accepts a JSON number OR a
    /// decimal string on input: a `fragment_id` is an FNV-1a 64 content hash
    /// (see [`ContextDecision::content_hash`]), so it is almost always above
    /// 2^53 — a float-lossy JSON client that reads one back from a compiled
    /// context and resubmits it inside a working context would otherwise
    /// corrupt it silently. Same accepted-forms rule as issue #1468's ids;
    /// the serialized (output) shape is unchanged.
    #[serde(deserialize_with = "crate::model::deserialize_id")]
    pub fragment_id: u64,
    /// Recoverable address of the original content (`ctx://source/<id>`).
    pub handle: String,
    // Un doc-comment de CHAMP est publie tel quel dans la description du
    // schema annonce (`docs/reference/mcp-tools.json`) : l'archeologie va donc
    // ici, en commentaire ordinaire, et le contrat seul va au-dessus.
    //
    // Aller-retour casse jusqu'au 2026-07-29, trouve en interrogeant le
    // serveur plutot qu'en relisant le code : `memory_id` fait partie des
    // `super::wire::ID_KEYS`, donc une reponse sous
    // `CompilePolicy::ids_as_strings` l'emet en CHAINE — et
    // `save_working_context` la refusait, alors que c'est precisement la forme
    // qu'un client a en main lorsqu'il resoumet une `SourceReference` recue
    // d'un contexte compile.
    /// The memory backing this source, when it came from recall (US-002).
    /// Accepts a JSON number OR a decimal string on input, exactly like
    /// `fragment_id` just above.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::wire::deserialize_optional_id"
    )]
    pub memory_id: Option<u64>,
}

/// A not-emitted fragment the caller can fetch back on demand.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct RetrievalHandle {
    /// Recoverable address of the original content (`ctx://source/<id>`).
    pub handle: String,
    /// The fragment behind the handle.
    pub fragment_id: u64,
    /// Estimated token cost of re-injecting the full original.
    pub estimated_tokens: u64,
}

/// The auditable record of what happened to one fragment and why.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct ContextDecision {
    /// The fragment this decision is about (caller id, or content-derived).
    pub fragment_id: u64,
    /// Content hash of the *original* fragment text (FNV-1a 64, the crate's
    /// [`stable id`](super::fragment_id)) — lets an auditor prove which exact
    /// bytes the decision covered even when the caller supplied its own id.
    pub content_hash: u64,
    /// What was done.
    pub action: ContextAction,
    /// The stable id of the rule that decided (e.g. `"preserve.code_fence"`).
    pub rule_id: String,
    /// Lexical relevance of the fragment to the request query, in `[0, 1]`.
    pub relevance: f32,
    /// Fidelity risk this single decision contributes.
    pub risk: FidelityRisk,
    /// Human-readable explanation of the decision.
    pub reason: String,
    /// The memory backing this fragment, when it came from recall (US-002).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub memory_id: Option<u64>,
    /// Recoverable address of the original content, when not fully emitted.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub handle: Option<String>,
}

/// A mechanical heads-up over one decision, surfaced in
/// [`CompiledContext::warnings`] so a caller can check "was anything
/// relevant cut?" without scanning every entry of `decisions` by hand
/// (V2a-2 quick win). Only [`ContextAction::Retrieve`] decisions at or
/// above the relevance threshold qualify.
///
/// **An empty list is not a clean bill of health** (#1703 DC-4). Several
/// real losses never warn: a [`ContextAction::Preserve`] the packer could
/// only fit partially, an [`ContextAction::Abstract`], and two of
/// `dup_verdict`'s [`ContextAction::Drop`] shapes — a media duplicate whose
/// caption diverges from its twin's, and a duplicate whose twin was itself
/// not fully emitted. Both say so in their own reason strings. `decisions`
/// remains the exhaustive record; this list is a low-noise shortcut over it,
/// not a substitute for it.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct ContextWarning {
    /// The fragment this warning is about.
    pub fragment_id: u64,
    /// What happened to it (always [`ContextAction::Retrieve`] today).
    pub action: ContextAction,
    /// Lexical relevance to the request query, in `[0, 1]` — the same value
    /// as the matching `decisions` entry.
    pub relevance: f32,
    /// The matching `decisions` entry's `reason`, copied verbatim.
    pub reason: String,
}

/// The compiler's output: the assembled context plus its full audit trail.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct CompiledContext {
    /// The assembled context, ready to inject into a prompt.
    pub content: String,
    /// The output split into ordered blocks (cache prefix first). Emptied by
    /// [`CompilePolicy::slim_response`].
    pub sections: Vec<CompiledSection>,
    /// One decision per input fragment (duplicates included). Emptied by
    /// [`CompilePolicy::slim_response`].
    pub decisions: Vec<ContextDecision>,
    /// One source pointer per distinct fragment.
    pub sources: Vec<SourceReference>,
    /// Handles for the fragments that were externalized, not emitted.
    pub retrieval_handles: Vec<RetrievalHandle>,
    /// Token (and optional cost) savings of this compilation.
    pub insights: CompilationInsights,
    /// Overall fidelity risk (the max over all decisions).
    pub risk: FidelityRisk,
    /// Mechanical, low-noise heads-up over `decisions` (V2a-2 quick win):
    /// every externalized fragment relevant enough to the query that a
    /// caller should double-check it was not needed. `#[serde(default)]` so
    /// a pre-0.10.0 caller reading an older stored/replayed response still
    /// deserializes (defaults to empty).
    #[serde(default)]
    pub warnings: Vec<ContextWarning>,
}

/// One asserted fact inside a [`WorkingContext`].
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ContextFact {
    /// The fact text.
    pub text: String,
    /// Where the fact came from, when known.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<SourceReference>,
}

/// A lightweight pointer to a past [`ContextDecision`].
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct ContextDecisionRef {
    /// The fragment the decision was about. Accepts a JSON number OR a
    /// decimal string on input, for the same reason as
    /// [`SourceReference::fragment_id`].
    #[serde(deserialize_with = "crate::model::deserialize_id")]
    pub fragment_id: u64,
    /// The rule that decided.
    pub rule_id: String,
}

/// The distilled working state of an agent session — small enough to carry
/// across sessions, structured enough to resume from. Persisted and reloaded
/// by the memory bridge (US-002) under `type = working_context` metadata.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct WorkingContext {
    /// What the session is trying to achieve.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub goal: Option<String>,
    /// Constraints currently in force (never compressed away).
    #[serde(default)]
    pub active_constraints: Vec<ContextFact>,
    /// Facts that were verified, with their sources.
    #[serde(default)]
    pub verified_facts: Vec<ContextFact>,
    /// Hypotheses still open.
    #[serde(default)]
    pub open_hypotheses: Vec<ContextFact>,
    /// Decisions taken so far.
    #[serde(default)]
    pub decisions: Vec<ContextDecisionRef>,
    /// Exact evidence the session relies on (verbatim, addressable).
    #[serde(default)]
    pub exact_evidence: Vec<SourceReference>,
    /// Actions still to do.
    #[serde(default)]
    pub pending_actions: Vec<String>,
}

impl WorkingContext {
    /// Whether this working state records nothing at all: no goal (or a blank
    /// one, which says no more than an absent one) and every list empty.
    ///
    /// Saving is an idempotent upsert, so an empty state would *replace* —
    /// that is, destroy — whatever a previous save stored under the same
    /// project and session. [`crate::MemoryService::save_working_context`]
    /// refuses one for exactly that reason (issue #1654).
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.goal.as_ref().is_none_or(|goal| goal.trim().is_empty())
            && self.active_constraints.is_empty()
            && self.verified_facts.is_empty()
            && self.open_hypotheses.is_empty()
            && self.decisions.is_empty()
            && self.exact_evidence.is_empty()
            && self.pending_actions.is_empty()
    }
}

/// What a working-context lookup returns, on every surface: the MCP
/// `load_working_context` tool AND the Node/Python/WASM bindings.
///
/// An envelope (not a bare `Option<WorkingContext>`): the MCP spec requires
/// the output schema's root to be an object, so a nullable root is rejected
/// by rmcp.
///
/// It lives here — in the shared model — rather than in the `mcp` module
/// because the `mcp` module is a Cargo feature the bindings do not enable:
/// a type declared there is unreachable from them, and each binding would
/// have to re-declare the envelope AND re-derive its two policy rules ("list
/// on a hit too", "never re-emit the requested session"). Four copies of a
/// rule is four chances for it to diverge in silence. Built once, by
/// [`MemoryService::resume_working_context`](crate::MemoryService::resume_working_context).
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(transform = crate::schema::strip_int_formats)]
pub struct LoadedWorkingContext {
    /// `true` when a working context was found under this exact project +
    /// session. Wire-additive alongside `working` (added V2a-1): a client
    /// that only reads `working` sees no change.
    pub found: bool,
    /// The previously saved working context, or `null` when nothing was ever
    /// saved under that project + session (a fresh start, not an error).
    pub working: Option<WorkingContext>,
    /// The OTHER sessions saved under this SAME project (never the requested
    /// one) — helps recover from a typo in `session` instead of silently
    /// starting fresh (e.g. `"task-1234"` saved, `"task-1235"` requested by
    /// mistake). Populated on a hit as well as on a miss: a typo that lands
    /// on another real session is the case the caller can least detect on its
    /// own. Empty only when the project has no other session.
    #[serde(default)]
    pub other_sessions: Vec<String>,
}

/// One session recorded in a project's working-context index (V2a-1's
/// `list_working_contexts` quick win).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct WorkingContextSession {
    /// The session id, as passed to `save_working_context`.
    pub session: String,
    /// Unix seconds this session was last saved — updated on every
    /// `save_working_context` call under this project + session, not just
    /// the first (a resave never duplicates the entry).
    pub saved_at: u64,
}

/// The per-project index [`save_working_context`](crate::MemoryService::save_working_context)
/// maintains so [`list_working_contexts`](crate::MemoryService::list_working_contexts)
/// never has to scan the whole store: one system fact per project, appended
/// (or refreshed) on every save. The REJECTED alternative was an approximate
/// `query_filtered` scan over working-context facts (capped at
/// `MAX_RECALL_LIMIT`, imprecise) — this index is exact and O(1) to read.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct WorkingContextIndex {
    /// Every session ever saved under this project.
    #[serde(default)]
    pub sessions: Vec<WorkingContextSession>,
}