zeph_config/memory/fidelity.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Compression, forgetting, and fidelity configuration.
5//!
6//! ACON budget compaction, ARC compaction, typed pages, optical forgetting,
7//! eviction policy, pruning strategy, and compression guidelines.
8
9use crate::providers::ProviderName;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12
13use super::CompactionProbeConfig;
14
15// ── ScrapMem optical forgetting config (issue #3713) ───────────────────────────
16
17/// `ScrapMem` optical forgetting configuration.
18///
19/// Controls progressive content-fidelity decay: `Full` → `Compressed` → `SummaryOnly`.
20/// The sweep is orthogonal to `SleepGate` (which decays importance scores); optical
21/// forgetting compresses content in place based on age.
22///
23/// # Example (TOML)
24///
25/// ```toml
26/// [memory.optical_forgetting]
27/// enabled = false
28/// compress_provider = ""
29/// compress_after_turns = 100
30/// summarize_after_turns = 500
31/// sweep_interval_secs = 3600
32/// sweep_batch_size = 50
33/// ```
34#[derive(Debug, Clone, Deserialize, Serialize)]
35#[serde(default)]
36pub struct OpticalForgettingConfig {
37 /// Enable optical forgetting sweep. Default: `false`.
38 pub enabled: bool,
39 /// Provider name from `[[llm.providers]]` for LLM-based content compression.
40 /// Falls back to the primary provider when empty.
41 pub compress_provider: ProviderName,
42 /// Number of conversation turns after which `Full` messages are compressed. Default: `100`.
43 pub compress_after_turns: u32,
44 /// Number of conversation turns after which `Compressed` messages become `SummaryOnly`. Default: `500`.
45 pub summarize_after_turns: u32,
46 /// How often the sweep runs, in seconds. Default: `3600`.
47 pub sweep_interval_secs: u64,
48 /// Maximum messages to compress per sweep iteration. Default: `50`.
49 pub sweep_batch_size: usize,
50}
51
52impl Default for OpticalForgettingConfig {
53 fn default() -> Self {
54 Self {
55 enabled: false,
56 compress_provider: ProviderName::default(),
57 compress_after_turns: 100,
58 summarize_after_turns: 500,
59 sweep_interval_secs: 3600,
60 sweep_batch_size: 50,
61 }
62 }
63}
64
65/// Session digest configuration (#2289).
66#[derive(Debug, Clone, Deserialize, Serialize)]
67#[serde(default)]
68pub struct DigestConfig {
69 /// Enable session digest generation at session end. Default: `false`.
70 pub enabled: bool,
71 /// Provider name from `[[llm.providers]]` for digest generation.
72 /// Falls back to the primary provider when `None`.
73 #[serde(default)]
74 pub provider: Option<ProviderName>,
75 /// Maximum tokens for the digest text. Default: `500`.
76 pub max_tokens: usize,
77 /// Maximum messages to feed into the digest prompt. Default: `50`.
78 pub max_input_messages: usize,
79}
80
81impl Default for DigestConfig {
82 fn default() -> Self {
83 Self {
84 enabled: false,
85 provider: None,
86 max_tokens: 500,
87 max_input_messages: 50,
88 }
89 }
90}
91
92/// Compression strategy for active context compression (#1161).
93#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
94#[serde(tag = "strategy", rename_all = "snake_case")]
95#[non_exhaustive]
96pub enum CompressionStrategy {
97 /// Compress only when reactive compaction fires (current behavior).
98 #[default]
99 Reactive,
100 /// Compress proactively when context exceeds `threshold_tokens`.
101 Proactive {
102 /// Token count that triggers proactive compression.
103 threshold_tokens: usize,
104 /// Maximum tokens for the compressed summary (passed to LLM as `max_tokens`).
105 max_summary_tokens: usize,
106 },
107 /// Agent calls `compress_context` tool explicitly. Reactive compaction still fires as a
108 /// safety net. The `compress_context` tool is also available in all other strategies.
109 Autonomous,
110 /// Knowledge-block-aware compression strategy (#2510).
111 ///
112 /// Low-relevance context segments are automatically consolidated into `AutoConsolidated`
113 /// knowledge blocks. LLM-curated blocks are never evicted before auto-consolidated ones.
114 Focus,
115}
116
117/// Pruning strategy for tool-output eviction inside the compaction pipeline (#1851, #2022).
118///
119/// When `context-compression` feature is enabled, this replaces the default oldest-first
120/// heuristic with scored eviction.
121#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
122#[serde(rename_all = "snake_case")]
123#[non_exhaustive]
124pub enum PruningStrategy {
125 /// Oldest-first eviction — current default behavior.
126 #[default]
127 Reactive,
128 /// Short LLM call extracts a task goal; blocks are scored by keyword overlap and pruned
129 /// lowest-first. Requires `context-compression` feature.
130 TaskAware,
131 /// Coarse-to-fine MIG scoring: relevance − redundancy with temporal partitioning.
132 /// Requires `context-compression` feature.
133 Mig,
134 /// Subgoal-aware pruning: tracks the agent's current subgoal via fire-and-forget LLM
135 /// extraction and partitions tool outputs into Active/Completed/Outdated tiers (#2022).
136 /// Requires `context-compression` feature.
137 Subgoal,
138 /// Subgoal-aware pruning combined with MIG redundancy scoring (#2022).
139 /// Requires `context-compression` feature.
140 SubgoalMig,
141}
142
143impl PruningStrategy {
144 /// Returns `true` when the strategy is subgoal-aware (`Subgoal` or `SubgoalMig`).
145 #[must_use]
146 pub fn is_subgoal(self) -> bool {
147 matches!(self, Self::Subgoal | Self::SubgoalMig)
148 }
149}
150
151// Route serde deserialization through FromStr so that removed variants (e.g. task_aware_mig)
152// emit a warning and fall back to Reactive instead of hard-erroring when found in TOML configs.
153impl<'de> serde::Deserialize<'de> for PruningStrategy {
154 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
155 let s = String::deserialize(deserializer)?;
156 s.parse().map_err(serde::de::Error::custom)
157 }
158}
159
160impl std::str::FromStr for PruningStrategy {
161 type Err = String;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 match s {
165 "reactive" => Ok(Self::Reactive),
166 "task_aware" | "task-aware" => Ok(Self::TaskAware),
167 "mig" => Ok(Self::Mig),
168 // task_aware_mig was removed (dead code — was routed to scored path only).
169 // Fall back to Reactive so existing TOML configs do not hard-error on startup.
170 "task_aware_mig" | "task-aware-mig" => {
171 tracing::warn!(
172 "pruning strategy `task_aware_mig` has been removed; \
173 falling back to `reactive`. Use `task_aware` or `mig` instead."
174 );
175 Ok(Self::Reactive)
176 }
177 "subgoal" => Ok(Self::Subgoal),
178 "subgoal_mig" | "subgoal-mig" => Ok(Self::SubgoalMig),
179 other => Err(format!(
180 "unknown pruning strategy `{other}`, expected \
181 reactive|task_aware|mig|subgoal|subgoal_mig"
182 )),
183 }
184 }
185}
186
187fn default_high_density_budget() -> f32 {
188 0.7
189}
190
191fn default_low_density_budget() -> f32 {
192 0.3
193}
194
195/// Configuration for the `SleepGate` forgetting sweep (#2397).
196///
197/// When `enabled = true`, a background loop periodically decays importance scores
198/// (synaptic downscaling), restores recently-accessed memories (selective replay),
199/// and prunes memories below `forgetting_floor` (targeted forgetting).
200#[derive(Debug, Clone, Deserialize, Serialize)]
201#[serde(default)]
202pub struct ForgettingConfig {
203 /// Enable the `SleepGate` forgetting sweep. Default: `false`.
204 pub enabled: bool,
205 /// Per-sweep decay rate applied to importance scores. Range: (0.0, 1.0). Default: `0.1`.
206 pub decay_rate: f32,
207 /// Importance floor below which memories are pruned. Range: [0.0, 1.0]. Default: `0.05`.
208 pub forgetting_floor: f32,
209 /// How often the forgetting sweep runs, in seconds. Default: `7200`.
210 pub sweep_interval_secs: u64,
211 /// Maximum messages to process per sweep. Default: `500`.
212 pub sweep_batch_size: usize,
213 /// Hours: messages accessed within this window get replay protection. Default: `24`.
214 pub replay_window_hours: u32,
215 /// Messages with `access_count` >= this get replay protection. Default: `3`.
216 pub replay_min_access_count: u32,
217 /// Hours: never prune messages accessed within this window. Default: `24`.
218 pub protect_recent_hours: u32,
219 /// Never prune messages with `access_count` >= this. Default: `3`.
220 pub protect_min_access_count: u32,
221}
222
223impl Default for ForgettingConfig {
224 fn default() -> Self {
225 Self {
226 enabled: false,
227 decay_rate: 0.1,
228 forgetting_floor: 0.05,
229 sweep_interval_secs: 7200,
230 sweep_batch_size: 500,
231 replay_window_hours: 24,
232 replay_min_access_count: 3,
233 protect_recent_hours: 24,
234 protect_min_access_count: 3,
235 }
236 }
237}
238
239/// Configuration for active context compression (#1161).
240#[derive(Debug, Clone, Default, Deserialize, Serialize)]
241#[serde(default)]
242pub struct CompressionConfig {
243 /// Compression strategy.
244 #[serde(flatten)]
245 pub strategy: CompressionStrategy,
246 /// Tool-output pruning strategy (requires `context-compression` feature).
247 pub pruning_strategy: PruningStrategy,
248 /// Model to use for compression summaries.
249 ///
250 /// Currently unused — the primary summary provider is used regardless of this value.
251 /// Reserved for future per-compression model selection. Setting this field has no effect.
252 pub model: String,
253 /// Provider name from `[[llm.providers]]` for `compress_context` summaries.
254 /// Falls back to the primary provider when empty. Default: `""`.
255 pub compress_provider: ProviderName,
256 /// Compaction probe: validates summary quality before committing it (#1609).
257 #[serde(default)]
258 pub probe: CompactionProbeConfig,
259 /// Archive tool output bodies to `SQLite` before compaction (Memex #2432).
260 ///
261 /// When enabled, tool output bodies in the compaction range are saved to
262 /// `tool_overflow` with `archive_type = 'archive'` before summarization.
263 /// The LLM summarizes placeholder messages; archived content is appended as
264 /// a postfix after summarization so references survive compaction.
265 /// Default: `false`.
266 #[serde(default)]
267 pub archive_tool_outputs: bool,
268 /// Provider for Focus strategy segment scoring and the auto-consolidation extraction
269 /// LLM call (#2510, #3313). Both are cheap/mid-tier tasks, so one provider suffices.
270 /// Falls back to the primary provider when empty. Default: `""`.
271 pub focus_scorer_provider: ProviderName,
272 /// Token-budget fraction for high-density content in density-aware compression (#2481).
273 /// Must sum to 1.0 with `low_density_budget`. Default: `0.7`.
274 #[serde(default = "default_high_density_budget")]
275 pub high_density_budget: f32,
276 /// Token-budget fraction for low-density content in density-aware compression (#2481).
277 /// Must sum to 1.0 with `high_density_budget`. Default: `0.3`.
278 #[serde(default = "default_low_density_budget")]
279 pub low_density_budget: f32,
280 /// Typed-page classification and batch-level assertion checking (#3630).
281 #[serde(default)]
282 pub typed_pages: TypedPagesConfig,
283 /// Acon tool-result compression settings (#4021).
284 ///
285 /// Controls per-result and batch-level token budgets for tool outputs before they enter
286 /// message history. Distinct from `[tools.compression]` (TACO), which applies regex-based
287 /// rule compression at the executor level.
288 #[serde(default)]
289 pub acon: AconConfig,
290 /// ARC agent-initiated compaction settings (#4020).
291 ///
292 /// When `allow_agent_compaction = true`, the agent can call the `request_compaction`
293 /// internal tool to trigger context summarization on demand.
294 #[serde(default)]
295 pub arc: ArcCompactionConfig,
296}
297
298fn default_acon_passthrough_threshold() -> usize {
299 2000
300}
301
302fn default_acon_summarize_threshold() -> usize {
303 4000
304}
305
306fn default_acon_total_budget() -> usize {
307 8000
308}
309
310fn validate_acon_passthrough_threshold<'de, D>(deserializer: D) -> Result<usize, D::Error>
311where
312 D: serde::Deserializer<'de>,
313{
314 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
315 if value == 0 {
316 return Err(serde::de::Error::custom(
317 "acon.passthrough_threshold must be >= 1",
318 ));
319 }
320 Ok(value)
321}
322
323fn validate_acon_summarize_threshold<'de, D>(deserializer: D) -> Result<usize, D::Error>
324where
325 D: serde::Deserializer<'de>,
326{
327 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
328 if value == 0 {
329 return Err(serde::de::Error::custom(
330 "acon.summarize_threshold must be >= 1",
331 ));
332 }
333 Ok(value)
334}
335
336fn validate_acon_total_budget<'de, D>(deserializer: D) -> Result<usize, D::Error>
337where
338 D: serde::Deserializer<'de>,
339{
340 let value = <usize as serde::Deserialize>::deserialize(deserializer)?;
341 if value == 0 {
342 return Err(serde::de::Error::custom("acon.total_budget must be >= 1"));
343 }
344 Ok(value)
345}
346
347/// Token budget configuration for Acon tool-result compression (#4021).
348///
349/// Controls per-result and batch-level token budgets for tool outputs injected into context.
350/// Distinct from `[tools.compression]` (TACO), which applies regex-based rule compression
351/// at the executor level.
352///
353/// # Invariants
354///
355/// The following ordering must hold: `passthrough_threshold < summarize_threshold <= total_budget`.
356/// A config where `passthrough_threshold >= summarize_threshold` would make the summarization path
357/// unreachable, silently producing incorrect compression behavior.
358///
359/// # Example (TOML)
360///
361/// ```toml
362/// [memory.compression.acon]
363/// enabled = true
364/// passthrough_threshold = 2000
365/// summarize_threshold = 4000
366/// total_budget = 8000
367/// ```
368#[derive(Debug, Clone, Deserialize, Serialize)]
369#[serde(default)]
370pub struct AconConfig {
371 /// Enable Acon tool-result compression. Default: `true`.
372 pub enabled: bool,
373 /// Token count below which results pass through unchanged.
374 /// Also the truncation target: results above this get char-truncated to this size.
375 /// Must be < `summarize_threshold`. Default: `2000`.
376 #[serde(default = "default_acon_passthrough_threshold")]
377 #[serde(deserialize_with = "validate_acon_passthrough_threshold")]
378 pub passthrough_threshold: usize,
379 /// Token count above which LLM summarization should be attempted before truncation.
380 /// Must be > `passthrough_threshold` and <= `total_budget`. Default: `4000`.
381 #[serde(default = "default_acon_summarize_threshold")]
382 #[serde(deserialize_with = "validate_acon_summarize_threshold")]
383 pub summarize_threshold: usize,
384 /// Maximum total tokens for all tool results in a single turn.
385 /// Must be >= `summarize_threshold`. Default: `8000`.
386 #[serde(default = "default_acon_total_budget")]
387 #[serde(deserialize_with = "validate_acon_total_budget")]
388 pub total_budget: usize,
389 /// Provider name from `[[llm.providers]]` for LLM summarization of large results.
390 /// Falls back to the primary provider when empty. Default: `""`.
391 #[serde(default)]
392 pub summarize_provider: ProviderName,
393}
394
395impl AconConfig {
396 /// Validate threshold ordering invariants after deserialization.
397 ///
398 /// Returns an error string if `passthrough_threshold >= summarize_threshold` or
399 /// `summarize_threshold > total_budget`.
400 ///
401 /// # Errors
402 ///
403 /// Returns a descriptive error string when any threshold invariant is violated.
404 #[must_use = "validation result must be checked"]
405 pub fn validate(&self) -> Result<(), String> {
406 if self.passthrough_threshold >= self.summarize_threshold {
407 return Err(format!(
408 "acon: passthrough_threshold ({}) must be < summarize_threshold ({})",
409 self.passthrough_threshold, self.summarize_threshold
410 ));
411 }
412 if self.summarize_threshold > self.total_budget {
413 return Err(format!(
414 "acon: summarize_threshold ({}) must be <= total_budget ({})",
415 self.summarize_threshold, self.total_budget
416 ));
417 }
418 Ok(())
419 }
420}
421
422impl Default for AconConfig {
423 fn default() -> Self {
424 Self {
425 enabled: true,
426 passthrough_threshold: default_acon_passthrough_threshold(),
427 summarize_threshold: default_acon_summarize_threshold(),
428 total_budget: default_acon_total_budget(),
429 summarize_provider: ProviderName::default(),
430 }
431 }
432}
433
434/// Configuration for ARC agent-initiated compaction (#4020).
435///
436/// When `allow_agent_compaction = true`, the `request_compaction` internal tool is
437/// registered and the agent can call it to trigger context summarization on demand.
438/// Rate limiting is handled by `CompactionState` — only one compaction fires per turn.
439///
440/// # Example (TOML)
441///
442/// ```toml
443/// [memory.compression.arc]
444/// allow_agent_compaction = true
445/// ```
446#[derive(Debug, Clone, Deserialize, Serialize)]
447#[serde(default)]
448pub struct ArcCompactionConfig {
449 /// Allow the agent to request compaction via the `request_compaction` tool call.
450 /// Default: `true`.
451 pub allow_agent_compaction: bool,
452}
453
454impl Default for ArcCompactionConfig {
455 fn default() -> Self {
456 Self {
457 allow_agent_compaction: true,
458 }
459 }
460}
461
462/// Configuration for typed-page compaction invariants (#3630).
463///
464/// Controls classification, batch-level assertion checking, and audit logging.
465/// All behavior is disabled by default; set `enabled = true` to activate.
466///
467/// # Example (TOML)
468///
469/// ```toml
470/// [memory.compression.typed_pages]
471/// enabled = true
472/// enforcement = "active"
473/// audit_path = ""
474/// audit_channel_capacity = 256
475/// ```
476#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
477#[serde(default)]
478pub struct TypedPagesConfig {
479 /// Enable typed-page classification and batch-level assertion checking.
480 /// Default: `false`.
481 pub enabled: bool,
482 /// Enforcement mode:
483 ///
484 /// - `observe`: classify and emit audit records only; no behavioral change.
485 /// - `active`: classify + `SystemContext` pointer-replace + batch assertions + audit.
486 ///
487 /// Default: `"observe"`.
488 pub enforcement: TypedPagesEnforcement,
489 /// Path for JSONL audit log. Empty string resolves to `{data_dir}/audit/compaction.jsonl`.
490 /// Default: `""`.
491 ///
492 /// # Security
493 ///
494 /// This field is **operator-only trusted input** read from the agent's configuration file.
495 /// Write access to the config file implies file-system write access, so no additional
496 /// canonicalization is enforced here. Do not expose this field to end-users or untrusted
497 /// configuration sources.
498 pub audit_path: String,
499 /// Bounded channel capacity for the async audit writer. Default: `256`.
500 pub audit_channel_capacity: usize,
501}
502
503impl Default for TypedPagesConfig {
504 fn default() -> Self {
505 Self {
506 enabled: false,
507 enforcement: TypedPagesEnforcement::Observe,
508 audit_path: String::new(),
509 audit_channel_capacity: 256,
510 }
511 }
512}
513
514/// Enforcement mode for typed-page compaction (#3630).
515#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize, JsonSchema)]
516#[serde(rename_all = "snake_case")]
517#[non_exhaustive]
518pub enum TypedPagesEnforcement {
519 /// Classify and audit only. Zero behavioral change relative to the untyped path.
520 #[default]
521 Observe,
522 /// Classify + pointer-replace `SystemContext` pages + batch assertions + audit.
523 Active,
524}
525
526/// Time-based microcompact configuration (#2699).
527///
528/// When `enabled = true`, low-value tool outputs are cleared from context
529/// (replaced with a sentinel string) when the session gap exceeds `gap_threshold_minutes`.
530/// The most recent `keep_recent` tool messages are preserved unconditionally.
531#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
532#[serde(default)]
533pub struct MicrocompactConfig {
534 /// Enable time-based microcompaction. Default: `false`.
535 pub enabled: bool,
536 /// Minimum idle gap in minutes before stale tool outputs are cleared. Default: `60`.
537 pub gap_threshold_minutes: u32,
538 /// Number of most recent compactable tool messages to preserve. Default: `3`.
539 pub keep_recent: usize,
540}
541
542impl Default for MicrocompactConfig {
543 fn default() -> Self {
544 Self {
545 enabled: false,
546 gap_threshold_minutes: 60,
547 keep_recent: 3,
548 }
549 }
550}
551
552// ── Eviction config (moved from zeph-memory) ─────────────────────────────────
553
554/// Eviction policy variant.
555///
556/// Serialises as `"ebbinghaus"` in TOML/JSON so existing configs remain valid.
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
558#[serde(rename_all = "lowercase")]
559#[non_exhaustive]
560pub enum EvictionPolicy {
561 /// Ebbinghaus forgetting-curve eviction.
562 #[default]
563 Ebbinghaus,
564}
565
566/// Configuration for the memory eviction policy.
567///
568/// Controls which policy runs during the periodic sweep and how many entries
569/// are retained. `zeph-memory` re-exports this type from here.
570#[derive(Debug, Clone, Deserialize, Serialize)]
571pub struct EvictionConfig {
572 /// Eviction policy. Currently only [`EvictionPolicy::Ebbinghaus`] is supported.
573 pub policy: EvictionPolicy,
574 /// Maximum number of entries to retain. `0` means unlimited (eviction disabled).
575 pub max_entries: usize,
576 /// How often to run the eviction sweep, in seconds.
577 pub sweep_interval_secs: u64,
578}
579
580impl Default for EvictionConfig {
581 fn default() -> Self {
582 Self {
583 policy: EvictionPolicy::Ebbinghaus,
584 max_entries: 0,
585 sweep_interval_secs: 3600,
586 }
587 }
588}
589
590// ── Compression guidelines config (moved from zeph-memory) ───────────────────
591
592/// Configuration for ACON failure-driven compression guidelines.
593///
594/// `zeph-memory` re-exports this type from here.
595#[derive(Debug, Clone, Deserialize, Serialize)]
596#[serde(default)]
597pub struct CompressionGuidelinesConfig {
598 /// Enable the feature. Default: `false`.
599 pub enabled: bool,
600 /// Minimum unused failure pairs before triggering a guidelines update. Default: `5`.
601 pub update_threshold: u16,
602 /// Maximum token budget for the guidelines document. Default: `500`.
603 pub max_guidelines_tokens: usize,
604 /// Maximum failure pairs consumed per update cycle. Default: `10`.
605 pub max_pairs_per_update: usize,
606 /// Number of turns after hard compaction to watch for context loss. Default: `10`.
607 pub detection_window_turns: u64,
608 /// Interval in seconds between background updater checks. Default: `300`.
609 pub update_interval_secs: u64,
610 /// Maximum unused failure pairs to retain (cleanup policy). Default: `100`.
611 pub max_stored_pairs: usize,
612 /// Provider name from `[[llm.providers]]` for guidelines update LLM calls.
613 /// `None` (or `Some("")`) falls back to the primary provider.
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub guidelines_provider: Option<ProviderName>,
616 /// Maintain separate guideline documents per content category.
617 #[serde(default)]
618 pub categorized_guidelines: bool,
619}
620
621impl Default for CompressionGuidelinesConfig {
622 fn default() -> Self {
623 Self {
624 enabled: false,
625 update_threshold: 5,
626 max_guidelines_tokens: 500,
627 max_pairs_per_update: 10,
628 detection_window_turns: 10,
629 update_interval_secs: 300,
630 max_stored_pairs: 100,
631 guidelines_provider: None,
632 categorized_guidelines: false,
633 }
634 }
635}