1use std::{
6 collections::BTreeMap,
7 fs::File,
8 path::{Path, PathBuf},
9};
10
11use anyhow::Context as _;
12use kcode_session_control_journal::{Journal, Record};
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16const LIFECYCLE_SIDEBAND: &str = "session_lifecycle";
17const COMMAND_SIDEBAND: &str = "session_command";
18const STOP_SIDEBAND: &str = "session_stop";
19const CONTROL_EXTENSION: &str = "session-control";
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum OpenMode {
23 CreateNew,
24 OpenOrCreate,
25 ExistingOnly,
26}
27
28#[derive(Clone, Debug, Default)]
29pub struct ControlProjection {
30 pub lifecycle: Option<SessionRecord>,
31 pub commands: BTreeMap<String, SessionCommand>,
32 pub stop_requests: BTreeMap<String, SessionStopRequest>,
33}
34
35#[derive(Clone, Debug)]
36pub enum ControlUpdate {
37 Lifecycle(SessionRecord),
38 Command(SessionCommand),
39 StopRequest(SessionStopRequest),
40}
41
42impl ControlUpdate {
43 pub fn projected(mut self) -> Self {
47 if let Self::Lifecycle(record) = &mut self {
48 record.state = control_state(&record.state);
49 }
50 self
51 }
52}
53
54#[derive(Clone, Debug, Deserialize, Serialize)]
55pub struct SessionRecord {
56 pub id: String,
57 pub phase: String,
58 pub started_at: String,
59 pub updated_at: String,
60 pub state: Value,
61 pub provenance_id: Option<String>,
62 pub version: i64,
63 pub last_user_message_at: Option<String>,
64 pub ended_at: Option<String>,
65 pub ingress_failure_count: i64,
66 pub ingress_failures: Value,
67 pub ingress_next_attempt_at: Option<String>,
68 #[serde(default, skip_serializing_if = "is_false")]
69 pub summary: bool,
70}
71
72#[derive(Clone, Debug, Deserialize, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct SessionCommand {
75 pub id: String,
76 pub conversation_id: String,
77 pub sequence: i64,
78 pub kind: String,
79 pub payload: Value,
80 pub status: String,
81 pub cancel_requested: bool,
82 pub outcome: Option<Value>,
83 pub created_at: String,
84 pub processing_started_at: Option<String>,
85 pub completed_at: Option<String>,
86 pub idempotency_id: String,
87}
88
89#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
90#[serde(rename_all = "camelCase")]
91pub struct SessionStopRequest {
92 pub id: String,
93 pub session_id: String,
94 pub scope: String,
95 pub status: String,
96 pub outcome: Option<Value>,
97 pub requested_at: String,
98 pub completed_at: Option<String>,
99 pub idempotency_id: String,
100}
101
102pub struct SessionControl {
103 directory: PathBuf,
104 path: PathBuf,
105 journal: Journal,
106}
107
108impl SessionControl {
109 pub fn open(
110 directory: impl AsRef<Path>,
111 session_id: &str,
112 mode: OpenMode,
113 ) -> anyhow::Result<Option<Self>> {
114 let directory = directory.as_ref().to_path_buf();
115 let path = control_path(&directory, session_id);
116 let journal = match mode {
117 OpenMode::CreateNew => Journal::create(path.clone())?,
118 OpenMode::OpenOrCreate => match Journal::open(path.clone())? {
119 Some(journal) => journal,
120 None => Journal::create(path.clone())?,
121 },
122 OpenMode::ExistingOnly => {
123 let Some(journal) = Journal::open(path.clone())? else {
124 return Ok(None);
125 };
126 journal
127 }
128 };
129 Ok(Some(Self {
130 directory,
131 path,
132 journal,
133 }))
134 }
135
136 pub fn projection(&self) -> ControlProjection {
137 project_records(self.journal.records())
138 }
139
140 pub fn append(
141 &mut self,
142 recorded_at: impl Into<String>,
143 update: ControlUpdate,
144 ) -> anyhow::Result<ControlUpdate> {
145 let update = update.projected();
146 let (kind, value) = match &update {
147 ControlUpdate::Lifecycle(record) => (
148 LIFECYCLE_SIDEBAND,
149 serde_json::to_value(record).context("encoding session lifecycle record")?,
150 ),
151 ControlUpdate::Command(command) => (
152 COMMAND_SIDEBAND,
153 serde_json::to_value(command).context("encoding session command record")?,
154 ),
155 ControlUpdate::StopRequest(request) => (
156 STOP_SIDEBAND,
157 serde_json::to_value(request).context("encoding session stop record")?,
158 ),
159 };
160 self.journal.append(kind, recorded_at, value)?;
161 Ok(update)
162 }
163
164 pub fn delete(self) -> anyhow::Result<()> {
165 let Self {
166 directory,
167 path,
168 journal,
169 } = self;
170 drop(journal);
171 if path.exists() {
172 std::fs::remove_file(&path).with_context(|| format!("removing {}", path.display()))?;
173 sync_directory(&directory)?;
174 }
175 Ok(())
176 }
177
178 pub fn compact_directory(directory: impl AsRef<Path>) -> anyhow::Result<()> {
179 let directory = directory.as_ref();
180 let mut paths = std::fs::read_dir(directory)?
181 .filter_map(Result::ok)
182 .map(|entry| entry.path())
183 .filter(|path| {
184 path.extension().and_then(|value| value.to_str()) == Some(CONTROL_EXTENSION)
185 })
186 .collect::<Vec<_>>();
187 paths.sort();
188 for path in paths {
189 compact_journal(&path)?;
190 }
191 Ok(())
192 }
193}
194
195fn is_false(value: &bool) -> bool {
196 !*value
197}
198
199fn control_path(directory: &Path, session_id: &str) -> PathBuf {
200 directory.join(format!("{session_id}.{CONTROL_EXTENSION}"))
201}
202
203fn project_records(records: &[Record]) -> ControlProjection {
204 let latest_lifecycle = records
205 .iter()
206 .rev()
207 .find(|record| record.kind == LIFECYCLE_SIDEBAND)
208 .and_then(|record| serde_json::from_value(record.value.clone()).ok());
209 let mut commands = BTreeMap::new();
210 let mut stop_requests = BTreeMap::new();
211 for record in records {
212 match record.kind.as_str() {
213 COMMAND_SIDEBAND => {
214 if let Ok(command) = serde_json::from_value::<SessionCommand>(record.value.clone())
215 {
216 commands.insert(command.id.clone(), command);
217 }
218 }
219 STOP_SIDEBAND => {
220 if let Ok(request) =
221 serde_json::from_value::<SessionStopRequest>(record.value.clone())
222 {
223 stop_requests.insert(request.id.clone(), request);
224 }
225 }
226 _ => {}
227 }
228 }
229 ControlProjection {
230 lifecycle: latest_lifecycle,
231 commands,
232 stop_requests,
233 }
234}
235
236fn control_state(value: &Value) -> Value {
237 const KEYS: &[&str] = &[
238 "format",
239 "version",
240 "stateVersion",
241 "sessionId",
242 "sessionType",
243 "sourceSessionType",
244 "channel",
245 "freeTime",
246 "selfTimeIntent",
247 "orchestration",
248 "provenanceId",
249 "rustLibSessionId",
250 "rootNodeIds",
251 "referenceRootNodeIds",
252 "startedAt",
253 "pendingTurn",
254 "pendingExternalEventId",
255 "roundsUsed",
256 "completed",
257 "sessionObjectId",
258 "commitReceipt",
259 "commitAuthor",
260 "providerModel",
261 "kwebPlan",
262 "startIdempotencyId",
263 "ingressSource",
264 "firstUserMessage",
265 "boxCount",
266 "eventCount",
267 "chatendMetadata",
268 "sessionStatus",
269 "historyIngress",
270 ];
271 let mut output = Map::new();
272 for key in KEYS {
273 if let Some(item) = value.get(*key) {
274 if *key == "commitReceipt" && item.is_null() {
275 continue;
276 }
277 let item = if *key == "historyIngress" {
278 control_state(item)
279 } else {
280 item.clone()
281 };
282 output.insert((*key).into(), item);
283 }
284 }
285 Value::Object(output)
286}
287
288fn compact_journal(path: &Path) -> anyhow::Result<()> {
289 let original_bytes = std::fs::metadata(path)
290 .with_context(|| format!("reading metadata for {}", path.display()))?
291 .len();
292 if original_bytes >= 16 * 1024 * 1024 {
293 tracing::info!(
294 path = %path.display(),
295 original_bytes,
296 "Compacting legacy Session History control journal"
297 );
298 }
299
300 let mut journal = Journal::open(path.to_path_buf())?
301 .with_context(|| format!("session-control journal {} disappeared", path.display()))?;
302 let repaired_bytes = std::fs::metadata(path)?.len();
303 let tail_repaired = repaired_bytes != original_bytes;
304 let mut latest_lifecycle = None;
305 let mut latest_commands = BTreeMap::<String, (u64, Record)>::new();
306 let mut latest_stop_requests = BTreeMap::<String, (u64, Record)>::new();
307 let mut retained_other = Vec::new();
308 let mut needs_rewrite = false;
309
310 for (sequence, mut record) in journal.records().iter().cloned().enumerate() {
311 let sequence = sequence as u64;
312 match record.kind.as_str() {
313 LIFECYCLE_SIDEBAND => {
314 if let Some(state) = record.value.get_mut("state") {
315 let projected = control_state(state);
316 if *state != projected {
317 *state = projected;
318 needs_rewrite = true;
319 }
320 }
321 if latest_lifecycle.replace((sequence, record)).is_some() {
322 needs_rewrite = true;
323 }
324 }
325 COMMAND_SIDEBAND => {
326 let id = record
327 .value
328 .get("id")
329 .and_then(Value::as_str)
330 .context("session command record has no ID")?
331 .to_owned();
332 if latest_commands.insert(id, (sequence, record)).is_some() {
333 needs_rewrite = true;
334 }
335 }
336 STOP_SIDEBAND => {
337 let id = record
338 .value
339 .get("id")
340 .and_then(Value::as_str)
341 .context("session stop record has no ID")?
342 .to_owned();
343 if latest_stop_requests
344 .insert(id, (sequence, record))
345 .is_some()
346 {
347 needs_rewrite = true;
348 }
349 }
350 _ => retained_other.push((sequence, record)),
351 }
352 }
353
354 if needs_rewrite {
355 let mut retained = retained_other;
356 retained.extend(latest_lifecycle);
357 retained.extend(latest_commands.into_values());
358 retained.extend(latest_stop_requests.into_values());
359 retained.sort_by_key(|(sequence, _)| *sequence);
360 journal.replace(retained.into_iter().map(|(_, record)| record))?;
361 }
362
363 if needs_rewrite || tail_repaired {
364 tracing::info!(
365 path = %path.display(),
366 original_bytes,
367 compacted_bytes = std::fs::metadata(path)?.len(),
368 "Compacted Session History control journal"
369 );
370 }
371 Ok(())
372}
373
374fn sync_directory(path: &Path) -> anyhow::Result<()> {
375 File::open(path)
376 .with_context(|| format!("opening directory {} for sync", path.display()))?
377 .sync_all()
378 .with_context(|| format!("syncing directory {}", path.display()))
379}
380
381#[cfg(test)]
382mod tests {
383 use std::{
384 fs::{self, OpenOptions},
385 io::Write as _,
386 sync::atomic::{AtomicU64, Ordering},
387 time::{SystemTime, UNIX_EPOCH},
388 };
389
390 use serde_json::json;
391
392 use super::*;
393
394 static NEXT_ROOT: AtomicU64 = AtomicU64::new(0);
395
396 fn root(label: &str) -> PathBuf {
397 let path = std::env::temp_dir().join(format!(
398 "kcode-session-control-state-{label}-{}-{}-{}",
399 std::process::id(),
400 SystemTime::now()
401 .duration_since(UNIX_EPOCH)
402 .unwrap()
403 .as_nanos(),
404 NEXT_ROOT.fetch_add(1, Ordering::Relaxed),
405 ));
406 fs::create_dir(&path).unwrap();
407 path
408 }
409
410 fn lifecycle(id: &str, version: i64, state: Value) -> SessionRecord {
411 SessionRecord {
412 id: id.into(),
413 phase: "active".into(),
414 started_at: "2026-08-02T00:00:00Z".into(),
415 updated_at: format!("2026-08-02T00:00:0{version}Z"),
416 state,
417 provenance_id: None,
418 version,
419 last_user_message_at: None,
420 ended_at: None,
421 ingress_failure_count: 0,
422 ingress_failures: json!([]),
423 ingress_next_attempt_at: None,
424 summary: false,
425 }
426 }
427
428 fn command(id: &str, status: &str, sequence: i64) -> SessionCommand {
429 SessionCommand {
430 id: id.into(),
431 conversation_id: "session-1".into(),
432 sequence,
433 kind: "message".into(),
434 payload: json!({"text":"hello"}),
435 status: status.into(),
436 cancel_requested: false,
437 outcome: None,
438 created_at: "2026-08-02T00:00:00Z".into(),
439 processing_started_at: None,
440 completed_at: None,
441 idempotency_id: format!("command-{id}"),
442 }
443 }
444
445 fn stop(id: &str, status: &str) -> SessionStopRequest {
446 SessionStopRequest {
447 id: id.into(),
448 session_id: "session-1".into(),
449 scope: "turn".into(),
450 status: status.into(),
451 outcome: None,
452 requested_at: "2026-08-02T00:00:00Z".into(),
453 completed_at: None,
454 idempotency_id: format!("stop-{id}"),
455 }
456 }
457
458 #[test]
459 fn opening_modes_preserve_create_and_absence_distinctions() {
460 let root = root("open-modes");
461 assert!(
462 SessionControl::open(&root, "missing", OpenMode::ExistingOnly)
463 .unwrap()
464 .is_none()
465 );
466
467 let created = SessionControl::open(&root, "new", OpenMode::CreateNew)
468 .unwrap()
469 .unwrap();
470 assert!(control_path(&root, "new").is_file());
471 assert!(SessionControl::open(&root, "new", OpenMode::CreateNew).is_err());
472 drop(created);
473
474 let opened = SessionControl::open(&root, "new", OpenMode::OpenOrCreate)
475 .unwrap()
476 .unwrap();
477 drop(opened);
478 let created_on_absence = SessionControl::open(&root, "other", OpenMode::OpenOrCreate)
479 .unwrap()
480 .unwrap();
481 assert!(control_path(&root, "other").is_file());
482 drop(created_on_absence);
483 fs::remove_dir_all(root).unwrap();
484 }
485
486 #[test]
487 fn typed_append_projects_recursive_lifecycle_state_and_latest_values() {
488 let root = root("typed-projection");
489 let mut control = SessionControl::open(&root, "session-1", OpenMode::CreateNew)
490 .unwrap()
491 .unwrap();
492 let persisted = control
493 .append(
494 "t1",
495 ControlUpdate::Lifecycle(lifecycle(
496 "session-1",
497 1,
498 json!({
499 "sessionType":"conversation",
500 "firstUserMessage":"hello",
501 "boxCount":2,
502 "eventCount":3,
503 "chatendText":"discard",
504 "commitReceipt":null,
505 "historyIngress":{
506 "completed":true,
507 "commitReceipt":{"sessionObjectId":"A1234567"},
508 "boxes":{"1":{"text":"discard"}},
509 "chatendText":"discard"
510 }
511 }),
512 )),
513 )
514 .unwrap();
515 let ControlUpdate::Lifecycle(persisted) = persisted else {
516 panic!("append changed update kind");
517 };
518 assert_eq!(persisted.state["sessionType"], "conversation");
519 assert_eq!(persisted.state["firstUserMessage"], "hello");
520 assert_eq!(persisted.state["boxCount"], 2);
521 assert_eq!(persisted.state["eventCount"], 3);
522 assert!(persisted.state.get("chatendText").is_none());
523 assert!(persisted.state.get("commitReceipt").is_none());
524 assert_eq!(
525 persisted.state["historyIngress"]["commitReceipt"]["sessionObjectId"],
526 "A1234567"
527 );
528 assert!(
529 persisted.state["historyIngress"]
530 .get("chatendText")
531 .is_none()
532 );
533 assert!(persisted.state["historyIngress"].get("boxes").is_none());
534
535 control
536 .append(
537 "t2",
538 ControlUpdate::Lifecycle(lifecycle(
539 "session-1",
540 2,
541 json!({"sessionType":"conversation","pendingTurn":true}),
542 )),
543 )
544 .unwrap();
545 control
546 .append("t3", ControlUpdate::Command(command("a", "pending", 1)))
547 .unwrap();
548 control
549 .append("t4", ControlUpdate::Command(command("a", "complete", 1)))
550 .unwrap();
551 control
552 .append("t5", ControlUpdate::Command(command("b", "pending", 2)))
553 .unwrap();
554 control
555 .append("t6", ControlUpdate::StopRequest(stop("s", "pending")))
556 .unwrap();
557 control
558 .append("t7", ControlUpdate::StopRequest(stop("s", "complete")))
559 .unwrap();
560
561 let projection = control.projection();
562 assert_eq!(projection.lifecycle.unwrap().version, 2);
563 assert_eq!(projection.commands.len(), 2);
564 assert_eq!(projection.commands["a"].status, "complete");
565 assert_eq!(projection.commands["b"].status, "pending");
566 assert_eq!(projection.stop_requests.len(), 1);
567 assert_eq!(projection.stop_requests["s"].status, "complete");
568 drop(control);
569
570 let reopened = SessionControl::open(&root, "session-1", OpenMode::ExistingOnly)
571 .unwrap()
572 .unwrap();
573 assert_eq!(reopened.projection().lifecycle.unwrap().version, 2);
574 drop(reopened);
575 fs::remove_dir_all(root).unwrap();
576 }
577
578 #[test]
579 fn startup_compaction_retains_unknown_kinds_and_survivor_order() {
580 let root = root("compaction-order");
581 let path = control_path(&root, "session-1");
582 let mut journal = Journal::create(path.clone()).unwrap();
583 journal
584 .append("unknown-first", "t0", json!({"value":0}))
585 .unwrap();
586 journal
587 .append(
588 LIFECYCLE_SIDEBAND,
589 "t1",
590 serde_json::to_value(lifecycle(
591 "session-1",
592 1,
593 json!({"sessionType":"conversation","chatendText":"old"}),
594 ))
595 .unwrap(),
596 )
597 .unwrap();
598 journal
599 .append(
600 COMMAND_SIDEBAND,
601 "t2",
602 serde_json::to_value(command("a", "pending", 1)).unwrap(),
603 )
604 .unwrap();
605 journal
606 .append("unknown-middle", "t3", json!({"value":3}))
607 .unwrap();
608 journal
609 .append(
610 LIFECYCLE_SIDEBAND,
611 "t4",
612 serde_json::to_value(lifecycle(
613 "session-1",
614 2,
615 json!({
616 "sessionType":"conversation",
617 "pendingTurn":true,
618 "historyIngress":{"chatendText":"discard","completed":true}
619 }),
620 ))
621 .unwrap(),
622 )
623 .unwrap();
624 journal
625 .append(
626 STOP_SIDEBAND,
627 "t5",
628 serde_json::to_value(stop("s", "pending")).unwrap(),
629 )
630 .unwrap();
631 journal
632 .append(
633 COMMAND_SIDEBAND,
634 "t6",
635 serde_json::to_value(command("a", "complete", 1)).unwrap(),
636 )
637 .unwrap();
638 journal
639 .append("unknown-last", "t7", json!({"value":7}))
640 .unwrap();
641 journal
642 .append(
643 STOP_SIDEBAND,
644 "t8",
645 serde_json::to_value(stop("s", "complete")).unwrap(),
646 )
647 .unwrap();
648 drop(journal);
649
650 SessionControl::compact_directory(&root).unwrap();
651
652 let compacted = Journal::open(path).unwrap().unwrap();
653 let kinds = compacted
654 .records()
655 .iter()
656 .map(|record| record.kind.as_str())
657 .collect::<Vec<_>>();
658 assert_eq!(
659 kinds,
660 [
661 "unknown-first",
662 "unknown-middle",
663 LIFECYCLE_SIDEBAND,
664 COMMAND_SIDEBAND,
665 "unknown-last",
666 STOP_SIDEBAND,
667 ]
668 );
669 let projected = project_records(compacted.records());
670 assert_eq!(projected.lifecycle.as_ref().unwrap().version, 2);
671 assert_eq!(
672 projected.lifecycle.unwrap().state["historyIngress"]["completed"],
673 true
674 );
675 assert_eq!(projected.commands["a"].status, "complete");
676 assert_eq!(projected.stop_requests["s"].status, "complete");
677 drop(compacted);
678 fs::remove_dir_all(root).unwrap();
679 }
680
681 #[test]
682 fn malformed_typed_records_keep_the_existing_tolerance_and_errors() {
683 let root = root("malformed");
684 let path = control_path(&root, "session-1");
685 let mut journal = Journal::create(path.clone()).unwrap();
686 journal
687 .append(
688 LIFECYCLE_SIDEBAND,
689 "t1",
690 serde_json::to_value(lifecycle("session-1", 1, json!({}))).unwrap(),
691 )
692 .unwrap();
693 journal
694 .append(LIFECYCLE_SIDEBAND, "t2", json!({"not":"a lifecycle"}))
695 .unwrap();
696 journal
697 .append(COMMAND_SIDEBAND, "t3", json!({"id":"partial"}))
698 .unwrap();
699 drop(journal);
700
701 let control = SessionControl::open(&root, "session-1", OpenMode::ExistingOnly)
702 .unwrap()
703 .unwrap();
704 let projection = control.projection();
705 assert!(projection.lifecycle.is_none());
706 assert!(projection.commands.is_empty());
707 drop(control);
708
709 assert!(SessionControl::compact_directory(&root).is_ok());
710
711 let malformed_path = control_path(&root, "missing-id");
712 let mut malformed = Journal::create(malformed_path).unwrap();
713 malformed
714 .append(COMMAND_SIDEBAND, "t1", json!({"status":"pending"}))
715 .unwrap();
716 drop(malformed);
717 assert!(
718 SessionControl::compact_directory(&root)
719 .unwrap_err()
720 .to_string()
721 .contains("session command record has no ID")
722 );
723 fs::remove_dir_all(root).unwrap();
724 }
725
726 #[test]
727 fn opening_repairs_incomplete_tail_but_rejects_complete_corruption() {
728 let root = root("integrity");
729 let path = control_path(&root, "tail");
730 let mut control = SessionControl::open(&root, "tail", OpenMode::CreateNew)
731 .unwrap()
732 .unwrap();
733 control
734 .append(
735 "t1",
736 ControlUpdate::Lifecycle(lifecycle("tail", 1, json!({}))),
737 )
738 .unwrap();
739 drop(control);
740 let complete = fs::read(&path).unwrap();
741 let mut file = OpenOptions::new().append(true).open(&path).unwrap();
742 file.write_all(b"incomplete tail").unwrap();
743 file.sync_all().unwrap();
744 drop(file);
745 let repaired = SessionControl::open(&root, "tail", OpenMode::ExistingOnly)
746 .unwrap()
747 .unwrap();
748 assert_eq!(repaired.projection().lifecycle.unwrap().version, 1);
749 drop(repaired);
750 assert_eq!(fs::read(&path).unwrap(), complete);
751
752 let corrupt_path = control_path(&root, "corrupt");
753 let mut corrupt = SessionControl::open(&root, "corrupt", OpenMode::CreateNew)
754 .unwrap()
755 .unwrap();
756 corrupt
757 .append(
758 "t1",
759 ControlUpdate::Lifecycle(lifecycle("corrupt", 1, json!({}))),
760 )
761 .unwrap();
762 drop(corrupt);
763 let mut bytes = fs::read(&corrupt_path).unwrap();
764 bytes[0] = if bytes[0] == b'0' { b'1' } else { b'0' };
765 fs::write(&corrupt_path, bytes).unwrap();
766 assert!(SessionControl::open(&root, "corrupt", OpenMode::ExistingOnly).is_err());
767 fs::remove_dir_all(root).unwrap();
768 }
769
770 #[test]
771 fn delete_removes_only_the_control_file() {
772 let root = root("delete");
773 let unrelated = root.join("keep.session-log");
774 fs::write(&unrelated, b"log").unwrap();
775 let control = SessionControl::open(&root, "session-1", OpenMode::CreateNew)
776 .unwrap()
777 .unwrap();
778 let path = control_path(&root, "session-1");
779 control.delete().unwrap();
780 assert!(!path.exists());
781 assert_eq!(fs::read(unrelated).unwrap(), b"log");
782 fs::remove_dir_all(root).unwrap();
783 }
784}