trusty-common 0.53.4

Shared utilities and provider-agnostic streaming chat (ChatProvider, OllamaProvider, OpenRouter, tool-use) for trusty-* projects
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
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
//! Which paused snapshot a resuming caller should reload, and by which route.
//!
//! Why: relaunching Claude Code inside the same tmux window mints a NEW harness
//! session id, so an exact-id lookup misses every snapshot the previous
//! incarnation wrote and `/tm-session-resume` degrades to a human reading prose
//! summaries to guess which of N snapshots is theirs. The tmux WINDOW ID is the
//! one identifier that survives the relaunch, and the pause path already records
//! it.
//! What: [`resolve_snapshot_for_caller`] tries the exact `session_id` first and,
//! only on a miss, the newest paused snapshot in the same project whose recorded
//! `tmux_window` carries the caller's window id, then (#8408) the newest one
//! recorded in the caller's tmux session since that session was created,
//! because a relaunch recreates the window. [`ResolutionPath`] names which
//! route answered so a caller never reads a fallback as an exact match.
//! [`redact_sessions_not_owned_by`] applies the same ownership test to the
//! digest, so the response cannot hand out the material for a claim the caller
//! could not otherwise make (#5386).
//! Test: inline `#[cfg(test)]` module.

use std::path::{Path, PathBuf};

use chrono::{DateTime, Utc};

use crate::catchup::json::PausedSessionJson;
use crate::catchup::session_finder::{PausedSession, find_paused_sessions};

/// How a snapshot was arrived at.
///
/// Why: a fallback and an exact match are different claims about ownership, and
/// a caller that cannot tell them apart will present a guess as a certainty.
/// What: [`ResolutionPath::as_str`] gives the wire value used in the
/// `session_context_catchup` response's `resolved_via` field.
/// Test: `window_fallback_reports_its_resolution_path`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResolutionPath {
    /// The caller's `session_id` owns this snapshot outright.
    SessionId,
    /// The caller runs in the tmux window that wrote this snapshot.
    TmuxWindow,
    /// The caller runs in the named tmux session that wrote this snapshot, in a
    /// window created since — every relaunch recreates the window (#8408). The
    /// snapshot was paused after that tmux session was created, so a later
    /// session reusing the name never matches it.
    TmuxSession,
}

impl ResolutionPath {
    /// The wire value for the `resolved_via` response field.
    pub fn as_str(self) -> &'static str {
        match self {
            ResolutionPath::SessionId => "session_id",
            ResolutionPath::TmuxWindow => "tmux_window",
            ResolutionPath::TmuxSession => "tmux_session",
        }
    }
}

/// A snapshot plus the route that found it.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ResolvedSnapshot {
    /// Absolute path to the snapshot file.
    pub path: PathBuf,
    /// Which lookup answered.
    pub via: ResolutionPath,
}

impl ResolvedSnapshot {
    /// Pair a snapshot path with the route that found it.
    pub fn new(path: PathBuf, via: ResolutionPath) -> Self {
        Self { path, via }
    }
}

/// Resolve the snapshot a caller should resume from.
///
/// Why: #5272 removed the "latest overall" fallback because an unidentified
/// caller in a shared store would silently inherit an arbitrary session's state.
/// A tmux window id is not that guess — the caller is demonstrably running in
/// the window that wrote the snapshot, which is an ownership claim, not a
/// coin flip. Without it a relaunch in the same window resolves nothing: the
/// harness mints a new session id per launch, so every relaunch orphans another
/// session directory and the next resume matches nothing again.
///
/// Window ids CAN be reused: kill a window and tmux may hand `@230` to a new
/// one, which would then match the dead window's snapshot. That is an accepted,
/// bounded risk — the project-path scope below is the second gate, since the
/// scan only ever reads `project_dir`'s own store. There is deliberately no
/// liveness check on the recorded window.
///
/// #8408: a relaunch recreates the window, so the window id changes too
/// (`@258` to `@262` within two minutes, live). The tmux SESSION name is what
/// a relaunch keeps, so it is a third route, reached only when the window
/// route misses. A session name is NOT bounded like a window id: trusty-mpm
/// names every session `tm-<folder>`, so each later session for the project
/// reuses it. The route therefore also requires the snapshot to have been
/// paused after the caller's tmux session was created (`#{session_created}`),
/// which a predecessor's snapshot never was. With no creation time the route
/// resolves nothing. A digits-only name is tmux's own default numbering, not
/// an identity, and never matches.
/// What: delegates to [`resolve_snapshot_for_identity`] with no session
/// creation time, so the tmux-session route is closed for this caller.
/// Test: `exact_session_id_match_wins_over_window_match`,
/// `window_fallback_resolves_when_session_id_never_paused`,
/// `window_fallback_is_scoped_to_the_project_dir`,
/// `malformed_window_fields_never_match`,
/// `the_session_route_without_a_creation_time_resolves_nothing`.
pub fn resolve_snapshot_for_caller(
    project_dir: &Path,
    session_id: Option<&str>,
    tmux_window: Option<&str>,
) -> Option<ResolvedSnapshot> {
    resolve_snapshot_for_identity(project_dir, &CallerIdentity::new(session_id, tmux_window))
}

/// Resolve the snapshot a caller should resume from, by every route its
/// identity supports.
///
/// Why: see [`resolve_snapshot_for_caller`]; this form also carries the tmux
/// session's creation time, which the tmux-session route needs (#8408).
/// What: (1) the exact `session_id` match via
/// [`latest_trusty_mpm_snapshot`](crate::catchup::session_finder::latest_trusty_mpm_snapshot),
/// which always wins; (2) failing that, the newest paused snapshot under
/// `project_dir` whose `## Tmux Window` section carries the caller's window id;
/// (3) failing that, the newest one recorded in the caller's tmux session name
/// ([`session_name_of`]) and paused strictly after
/// `caller.tmux_session_created`; (4) otherwise `None`. A snapshot with no
/// recorded window, a window field that does not parse, or no pause time never
/// matches route 3.
/// Test: `a_relaunched_window_resolves_through_its_tmux_session`,
/// `a_reused_session_name_does_not_claim_its_predecessors_snapshot`,
/// `a_window_match_outranks_a_session_match`,
/// `a_numbered_tmux_session_never_matches`.
pub fn resolve_snapshot_for_identity(
    project_dir: &Path,
    caller: &CallerIdentity<'_>,
) -> Option<ResolvedSnapshot> {
    if let Some(path) =
        crate::catchup::session_finder::latest_trusty_mpm_snapshot(project_dir, caller.session_id)
    {
        return Some(ResolvedSnapshot::new(path, ResolutionPath::SessionId));
    }
    // #5272: this is NOT the "latest overall" fallback that issue removed. That
    // one answered an unidentified caller with an arbitrary session's file; this
    // one requires the caller to be in the window that wrote the snapshot.
    let tmux_window = caller.tmux_window?;
    let caller_window = window_id_of(tmux_window)?;
    if let Some(path) =
        newest_snapshot_where(project_dir, |w, _| window_id_of(w) == Some(caller_window))
    {
        return Some(ResolvedSnapshot::new(path, ResolutionPath::TmuxWindow));
    }
    // #8408: the relaunched window has a new id; its tmux session kept the name.
    // The name is reused by every later `tm-<folder>` session, so only a pause
    // inside THIS session's lifetime counts; an unknown lifetime matches nothing.
    let caller_session = session_name_of(tmux_window)?;
    let created = caller.tmux_session_created?;
    newest_snapshot_where(project_dir, |w, paused_at| {
        session_name_of(w) == Some(caller_session)
            && paused_at.is_some_and(|t| t.timestamp() > created)
    })
    .map(|path| ResolvedSnapshot::new(path, ResolutionPath::TmuxSession))
}

/// The newest paused snapshot under `project_dir` whose recorded window field
/// and pause time satisfy `matches`.
///
/// Why: [`find_paused_sessions`] already sorts newest-first and already scopes
/// itself to one project's store, so "newest in this window, in this project" is
/// the first match over that list.
/// What: skips the legacy claude-mpm arm (it records no window) and every
/// snapshot whose window field is absent.
/// Test: `window_fallback_resolves_when_session_id_never_paused`,
/// `snapshot_without_a_recorded_window_is_skipped`.
fn newest_snapshot_where(
    project_dir: &Path,
    matches: impl Fn(&str, Option<DateTime<Utc>>) -> bool,
) -> Option<PathBuf> {
    find_paused_sessions(project_dir)
        .ok()?
        .into_iter()
        .find_map(|s| match s {
            PausedSession::TrustyMpm {
                path,
                tmux_window: Some(w),
                paused_at,
                ..
            } if matches(&w, paused_at) => Some(path),
            _ => None,
        })
}

/// Extract the tmux session name from a `session_name:window_index:window_id`
/// field (#8408).
///
/// Why: a relaunch recreates the window, so the window id the caller holds
/// afterwards never matches the one its last pause recorded. The session name
/// survives the relaunch.
/// What: everything before the last two components, accepted only when the
/// last is a window id ([`window_id_of`]), the middle is a window index (ASCII
/// digits), and the name is non-empty and not digits-only — tmux numbers
/// unnamed sessions `0`, `1`, … and reuses those numbers, so a number names no
/// one. A name may contain `:` itself.
/// Test: `session_name_of_reads_everything_before_index_and_id`,
/// `a_numbered_tmux_session_never_matches`.
pub fn session_name_of(field: &str) -> Option<&str> {
    let field = field.trim();
    window_id_of(field)?;
    let mut parts = field.rsplitn(3, ':');
    let (_id, index, name) = (parts.next()?, parts.next()?, parts.next()?);
    let is_number = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
    (is_number(index) && !name.is_empty() && !is_number(name)).then_some(name)
}

/// Extract the stable window id from a `session_name:window_index:window_id`
/// field.
///
/// Why: matching on `session_name:window_index` would be wrong — session names
/// get renamed and window indexes renumber when a window is closed. The `@N`
/// window id is stable for the window's lifetime, so it is the component the
/// window route compares. The session name is only a later route, for a window
/// that no longer exists (#8408, [`session_name_of`]).
/// What: the last `:`-delimited component, accepted only when it looks like a
/// tmux window id — a leading `@` followed by at least one character. Taking
/// the LAST component rather than requiring exactly three is what lets a tmux
/// session name contain its own `:` (`my:proj:0:@7`); requiring the `@` is what
/// keeps that from degenerating into "whatever follows the final colon", so
/// `a:b:c:d` still parses to nothing. Empty, one-component and
/// empty-last-component inputs yield `None`, so they match nothing.
/// Test: `window_id_of_reads_the_third_component`,
/// `window_id_of_tolerates_colons_in_the_session_name`,
/// `malformed_window_fields_never_match`.
pub fn window_id_of(field: &str) -> Option<&str> {
    let id = field.trim().rsplit(':').next()?;
    (id.len() > 1 && id.starts_with('@')).then_some(id)
}

/// Who a catch-up caller claims to be.
///
/// Why: both ownership questions this module answers — "which snapshot do I
/// resume from" and "which listed sessions may I see in full" — take the same
/// two self-reported identifiers, and passing them as a pair keeps the two
/// answers from drifting apart.
/// What: `session_id` is the harness session id; `tmux_window` is the caller's
/// own `session_name:window_index:window_id`. Both are self-reported and
/// neither is verified server-side — which is precisely why
/// [`redact_sessions_not_owned_by`] exists: the tool must not also SUPPLY the
/// values needed to make a claim.
/// Test: `redaction_withholds_handles_and_restorable_state`.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct CallerIdentity<'a> {
    /// The caller's own session id, when it identified itself.
    pub session_id: Option<&'a str>,
    /// The caller's own tmux window field, when it is running inside tmux.
    pub tmux_window: Option<&'a str>,
    /// The caller's tmux session creation time, tmux's `#{session_created}`
    /// (Unix seconds). The tmux-session route needs it (#8408).
    pub tmux_session_created: Option<i64>,
}

impl<'a> CallerIdentity<'a> {
    /// Build an identity from the two MCP arguments, with no session creation
    /// time.
    pub fn new(session_id: Option<&'a str>, tmux_window: Option<&'a str>) -> Self {
        Self {
            session_id,
            tmux_window,
            tmux_session_created: None,
        }
    }

    /// Add the caller's tmux `#{session_created}` epoch seconds (#8408).
    pub fn with_tmux_session_created(mut self, created: Option<i64>) -> Self {
        self.tmux_session_created = created;
        self
    }
}

/// Withhold, from every session the caller does not own, the fields that would
/// let it adopt that session.
///
/// Why: #5272 removed the "latest overall" fallback so an unidentified caller
/// could not inherit an arbitrary session's state. It left the DATA that
/// reconstructs the same result by hand: the digest returned every paused
/// session's `source_file` and `tmux_window` to any caller, so a caller could
/// read another session's window out of one response, hand it back as its own,
/// and resolve that session's snapshot deterministically — or skip the tool and
/// read `source_file` off disk. Removing one code path while still publishing
/// the means to re-create it is not the invariant #5272 was defending, so the
/// response now honors it directly.
///
/// What: ownership is the same claim [`resolve_snapshot_for_identity`] accepts —
/// the session is attributed to `caller.session_id` in `sessions-log.jsonl` (or
/// sits in that id's directory), OR the caller is in the tmux window that paused
/// it, OR (#8408) it is the ONE snapshot the resolver answered by tmux session
/// name. A shared session name alone owns nothing: the name is reused by every
/// later session for the project, and other windows of the same session are
/// not the caller. For everything else the entry keeps `format`, `paused_at` and `summary`
/// and loses the rest, with `owned: false` saying so. The line is drawn at what
/// a resuming PM would ACT on: `source_file`/`tmux_window` are the handles that
/// load a snapshot, and `in_progress`/`next_steps`/`git_context` are the state
/// `/tm-session-resume` restores as its own todos. `summary` stays because
/// "something else paused here, and it was about X" is the digest's purpose and
/// nothing loads from it.
///
/// A legacy claude-mpm session carries no id attribution and no window, so it is
/// never owned. It also carries no `source_file`, so nothing actionable is
/// withheld — only its `in_progress`/`next_steps` fold.
/// Test: `redaction_withholds_handles_and_restorable_state`,
/// `owner_sees_every_field`, `window_owner_sees_every_field`,
/// `redaction_leaves_nothing_to_reconstruct_a_window_claim_from`,
/// `a_session_name_match_owns_only_the_resolved_snapshot`.
pub fn redact_sessions_not_owned_by(
    project_dir: &Path,
    caller: &CallerIdentity<'_>,
    sessions: &mut [PausedSessionJson],
) {
    let mut owned_paths = caller
        .session_id
        .map(|id| {
            let sessions_dir = project_dir.join(".trusty-mpm").join("sessions");
            crate::catchup::session_log::snapshots_attributed_to(&sessions_dir, id, "md")
                .iter()
                .map(|p| canonical(p))
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    // #8408: the session-name route grants exactly the snapshot it resolved —
    // never every entry that shares the reused `tm-<folder>` name.
    if let Some(resolved) = resolve_snapshot_for_identity(project_dir, caller)
        && resolved.via == ResolutionPath::TmuxSession
    {
        owned_paths.push(canonical(&resolved.path));
    }
    let caller_window = caller.tmux_window.and_then(window_id_of);

    for s in sessions.iter_mut() {
        if !is_owned_by(s, &owned_paths, caller_window) {
            withhold(s);
        }
    }
}

/// Whether one digest entry is attributable to the caller.
///
/// Why: the ownership routes have to agree with
/// [`resolve_snapshot_for_identity`], or a caller could resolve a snapshot
/// whose own digest entry it is not allowed to read.
/// What: true when the entry's `source_file` is in `owned_paths` (the caller's
/// attributed snapshots plus any snapshot resolved by tmux session name), or
/// its recorded window id equals the caller's.
/// Test: `owner_sees_every_field`, `window_owner_sees_every_field`,
/// `a_session_name_match_owns_only_the_resolved_snapshot`.
fn is_owned_by(
    session: &PausedSessionJson,
    owned_paths: &[PathBuf],
    caller_window: Option<&str>,
) -> bool {
    if let Some(file) = session.source_file.as_deref() {
        let path = canonical(Path::new(file));
        if owned_paths.contains(&path) {
            return true;
        }
    }
    let recorded = session.tmux_window.as_deref().and_then(window_id_of);
    matches!((recorded, caller_window), (Some(x), Some(y)) if x == y)
}

/// Strip a digest entry down to what a non-owning caller may see.
///
/// Why: kept separate from the predicate so the disclosure boundary is one
/// readable list rather than five scattered assignments.
/// What: clears the two handles and the three restorable-state fields, and
/// marks the entry unowned. `format`, `paused_at` and `summary` survive.
/// Test: `redaction_withholds_handles_and_restorable_state`.
fn withhold(session: &mut PausedSessionJson) {
    session.source_file = None;
    session.tmux_window = None;
    session.in_progress = None;
    session.next_steps = None;
    session.git_context = None;
    session.owned = false;
}

/// Resolve a path for comparison, falling back to the path itself.
///
/// Why: the digest builds snapshot paths by joining `project_dir`, while the
/// attribution index joins the store root; a symlinked or non-normalised
/// `project_dir` would make two spellings of one file compare unequal and
/// redact a caller's own session.
/// What: [`std::fs::canonicalize`], or the input unchanged when it cannot be
/// resolved (a path that no longer exists compares by spelling, as before).
/// Test: covered by `owner_sees_every_field`.
fn canonical(path: &Path) -> PathBuf {
    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::catchup::pause::{PauseSnapshotInput, write_pause_snapshot};

    fn pause(dir: &Path, session_id: &str, window: Option<&str>) -> PathBuf {
        let input = PauseSnapshotInput {
            session_id,
            summary: "work",
            completed: &[],
            in_progress: &[],
            next_steps: &[],
            tmux_window: window,
        };
        write_pause_snapshot(dir, &input).unwrap().snapshot_path
    }

    #[test]
    fn window_id_of_reads_the_third_component() {
        assert_eq!(window_id_of("tm-dogfood:0:@230"), Some("@230"));
        assert_eq!(window_id_of("  main:12:@7  "), Some("@7"));
        assert_eq!(window_id_of("@230"), Some("@230"));
    }

    /// Why: tmux permits a literal `:` in a session name, which pushes the
    /// field past three components. Requiring exactly three made a caller's own
    /// snapshot unmatchable — it failed closed, so it was a functionality gap
    /// rather than a leak, but it is the caller's OWN window that stops working.
    /// What: the window id is read from the last component regardless of how
    /// many precede it, while a last component that is not an `@id` still
    /// parses to nothing.
    /// Test: itself.
    #[test]
    fn window_id_of_tolerates_colons_in_the_session_name() {
        assert_eq!(window_id_of("my:proj:0:@7"), Some("@7"));
        assert_eq!(window_id_of("a:b:c:d:e:12:@230"), Some("@230"));
        assert_eq!(
            window_id_of("a:b:c:d"),
            None,
            "a non-@ tail is not a window"
        );

        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "writer", Some("my:proj:0:@7"));
        let got = resolve_snapshot_for_caller(tmp.path(), None, Some("my:proj:0:@7"))
            .expect("a colon in the session name must not break the caller's own match");
        assert_eq!(got.via, ResolutionPath::TmuxWindow);
    }

    /// Build the digest entry `generate_catchup_json` would produce for a real
    /// snapshot file.
    fn entry(path: &Path, window: Option<&str>) -> PausedSessionJson {
        PausedSessionJson {
            format: "trusty-mpm".to_string(),
            paused_at: None,
            summary: "work".to_string(),
            in_progress: Some("halfway through X".to_string()),
            next_steps: Some("finish X".to_string()),
            git_context: Some("branch: main".to_string()),
            tmux_window: window.map(str::to_string),
            source_file: Some(path.display().to_string()),
            owned: true,
        }
    }

    /// Why: #5386 — the digest returned every paused session's `source_file`
    /// and `tmux_window` to any caller, so a caller could read another
    /// session's window out of the response, hand it back as its own, and
    /// resolve that session's snapshot deterministically. #5272 removed the
    /// code path that did this automatically; leaving the material to redo it
    /// by hand is not the invariant it was defending.
    /// What: a caller owning nothing sees the session exists and what it was
    /// about, and loses both handles plus the state a resume would restore.
    /// Test: itself.
    #[test]
    fn redaction_withholds_handles_and_restorable_state() {
        let tmp = tempfile::TempDir::new().unwrap();
        let theirs = pause(tmp.path(), "theirs", Some("tm-dogfood:0:@230"));
        let mut sessions = vec![entry(&theirs, Some("tm-dogfood:0:@230"))];

        redact_sessions_not_owned_by(
            tmp.path(),
            &CallerIdentity::new(Some("nobody"), Some("other:1:@999")),
            &mut sessions,
        );

        let s = &sessions[0];
        assert!(!s.owned, "a session the caller does not own must say so");
        assert_eq!(s.source_file, None, "the snapshot path is a handle");
        assert_eq!(s.tmux_window, None, "the window is a handle");
        assert_eq!(s.in_progress, None);
        assert_eq!(s.next_steps, None);
        assert_eq!(s.git_context, None);
        // Still enough to answer "something else paused here".
        assert_eq!(s.summary, "work");
        assert_eq!(s.format, "trusty-mpm");
    }

    /// Why: the exploit the redaction closes is specifically "read the window
    /// out of this response, then send it back" — so the response must carry no
    /// spelling of another session's window at all.
    /// What: nothing in the redacted entry, serialized, contains the window id
    /// that would resolve the snapshot.
    /// Test: itself.
    #[test]
    fn redaction_leaves_nothing_to_reconstruct_a_window_claim_from() {
        let tmp = tempfile::TempDir::new().unwrap();
        let theirs = pause(tmp.path(), "theirs", Some("tm-dogfood:0:@230"));
        let mut sessions = vec![entry(&theirs, Some("tm-dogfood:0:@230"))];

        redact_sessions_not_owned_by(tmp.path(), &CallerIdentity::default(), &mut sessions);

        let wire = serde_json::to_string(&sessions[0]).unwrap();
        assert!(
            !wire.contains("@230"),
            "the window id must not survive anywhere on the wire: {wire}"
        );
        assert!(
            !wire.contains(theirs.to_str().unwrap()),
            "the snapshot path must not survive anywhere on the wire: {wire}"
        );
    }

    /// Why: redaction that also hid the caller's OWN session would break resume
    /// outright — the digest is what `/tm-session-resume` renders.
    /// What: the session id that paused the snapshot keeps every field.
    /// Test: itself.
    #[test]
    fn owner_sees_every_field() {
        let tmp = tempfile::TempDir::new().unwrap();
        let mine = pause(tmp.path(), "mine", Some("tm-dogfood:0:@230"));
        let mut sessions = vec![entry(&mine, Some("tm-dogfood:0:@230"))];

        redact_sessions_not_owned_by(
            tmp.path(),
            &CallerIdentity::new(Some("mine"), None),
            &mut sessions,
        );

        let s = &sessions[0];
        assert!(s.owned);
        assert_eq!(s.source_file.as_deref(), Some(mine.to_str().unwrap()));
        assert_eq!(s.tmux_window.as_deref(), Some("tm-dogfood:0:@230"));
        assert_eq!(s.next_steps.as_deref(), Some("finish X"));
    }

    /// Why: the window fallback and the digest must agree. A caller that can
    /// RESOLVE a snapshot by window but cannot READ its digest entry would see
    /// `resolved_snapshot` pointing at a row it was told it does not own.
    /// What: the window that paused the snapshot keeps every field, with no
    /// session id at all.
    /// Test: itself.
    #[test]
    fn window_owner_sees_every_field() {
        let tmp = tempfile::TempDir::new().unwrap();
        let theirs = pause(
            tmp.path(),
            "some-earlier-incarnation",
            Some("tm-dogfood:0:@230"),
        );
        let mut sessions = vec![entry(&theirs, Some("tm-dogfood:0:@230"))];

        redact_sessions_not_owned_by(
            tmp.path(),
            &CallerIdentity::new(Some("relaunched-id"), Some("renamed:7:@230")),
            &mut sessions,
        );

        assert!(sessions[0].owned, "the window that paused it owns it");
        assert!(sessions[0].source_file.is_some());
    }

    /// Why: a snapshot with no `source_file` and no window — every legacy
    /// claude-mpm entry — is attributable to nobody, so it must not be owned by
    /// default. Defaulting it to owned would exempt the whole legacy arm.
    /// What: an entry with neither handle is redacted for every caller.
    /// Test: itself.
    #[test]
    fn an_unattributable_session_is_owned_by_nobody() {
        let tmp = tempfile::TempDir::new().unwrap();
        let mut sessions = vec![PausedSessionJson {
            format: "claude-mpm".to_string(),
            paused_at: None,
            summary: "legacy work".to_string(),
            in_progress: Some("todo 1".to_string()),
            next_steps: None,
            git_context: None,
            tmux_window: None,
            source_file: None,
            owned: true,
        }];

        redact_sessions_not_owned_by(
            tmp.path(),
            &CallerIdentity::new(Some("anyone"), Some("tm-dogfood:0:@230")),
            &mut sessions,
        );

        assert!(!sessions[0].owned);
        assert_eq!(sessions[0].in_progress, None);
        assert_eq!(sessions[0].summary, "legacy work");
    }

    /// Why: the field is parsed from a file any process can write, and older
    /// snapshots carry shapes this code never produced. A panic or a loose
    /// match here would either crash resume or hand over someone else's state.
    /// What: every degenerate shape resolves to `None`, and a caller holding
    /// one of them resolves no snapshot at all.
    /// Test: itself.
    #[test]
    fn malformed_window_fields_never_match() {
        for bad in ["", "   ", "main", "main:0", "a:b:", "a:b:c:d", ":", "::"] {
            assert_eq!(window_id_of(bad), None, "{bad:?} must not parse");
        }

        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "writer", Some("tm-dogfood:0:@230"));
        for bad in ["", "main", "main:0", "a:b:c:d"] {
            assert!(
                resolve_snapshot_for_caller(tmp.path(), Some("nobody"), Some(bad)).is_none(),
                "caller window {bad:?} must resolve nothing"
            );
        }
    }

    /// Why: the reported defect — a relaunch in the same tmux window mints a
    /// new harness session id, so the exact-id lookup misses and resume
    /// resolves nothing.
    /// What: an id that never paused plus the window that did resolves the
    /// window's newest snapshot, and says it came from the window.
    /// Test: itself.
    #[test]
    fn window_fallback_resolves_when_session_id_never_paused() {
        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "old-incarnation", Some("tm-dogfood:0:@230"));

        let got = resolve_snapshot_for_caller(
            tmp.path(),
            Some("69895d04-149d-4c31-a640-29048831f9a5"),
            Some("tm-dogfood:0:@230"),
        )
        .expect("the window that paused must resolve its own snapshot");
        assert_eq!(got.via, ResolutionPath::TmuxWindow);
        assert!(got.path.is_file());
    }

    /// Why: matching on `session:index` would break the moment a window is
    /// renamed or renumbered; matching on `@id` must survive both.
    /// What: a caller whose session name and window index both differ still
    /// resolves, because the window id is the same.
    /// Test: itself.
    #[test]
    fn window_match_ignores_session_name_and_index() {
        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "writer", Some("tm-dogfood:0:@230"));
        let got = resolve_snapshot_for_caller(tmp.path(), None, Some("renamed:7:@230"))
            .expect("the window id is what identifies the window");
        assert_eq!(got.via, ResolutionPath::TmuxWindow);
    }

    #[test]
    fn window_fallback_reports_its_resolution_path() {
        assert_eq!(ResolutionPath::SessionId.as_str(), "session_id");
        assert_eq!(ResolutionPath::TmuxWindow.as_str(), "tmux_window");
        assert_eq!(ResolutionPath::TmuxSession.as_str(), "tmux_session");
    }

    /// Why (#8408): a session name may itself contain `:`, and only a field
    /// that also names a window index and a window id is a tmux window field.
    /// What: the name is everything before the last two components; any other
    /// shape yields `None`.
    /// Test: itself.
    #[test]
    fn session_name_of_reads_everything_before_index_and_id() {
        assert_eq!(session_name_of("tm-apex:0:@263"), Some("tm-apex"));
        assert_eq!(session_name_of(" my:proj:2:@7 "), Some("my:proj"));
        for bad in [
            "",
            "@7",
            "0:@7",
            ":0:@7",
            "tm-apex:x:@7",
            "tm-apex:0:7",
            "a:b:c:d",
        ] {
            assert_eq!(session_name_of(bad), None, "{bad:?} must not parse");
        }
    }

    /// Why (#8408): every relaunch recreates the window, so the caller's window
    /// id is new (`@258` to `@262`, live) and the window route resolves
    /// nothing. The tmux session name is what the relaunch kept.
    /// What: a caller in the same named session, in a new window and with no
    /// session id, resolves the newest snapshot that session paused, reports
    /// `tmux_session`, and owns that snapshot's digest entry.
    /// Test: itself.
    #[test]
    fn a_relaunched_window_resolves_through_its_tmux_session() {
        let tmp = tempfile::TempDir::new().unwrap();
        let mine = pause(tmp.path(), "tmux-window-258", Some("tm-supervisor:0:@258"));
        pause(tmp.path(), "someone-else", Some("tm-other:0:@300"));
        let caller = CallerIdentity::new(None, Some("tm-supervisor:0:@262"))
            .with_tmux_session_created(Some(SESSION_CREATED));

        let got = resolve_snapshot_for_identity(tmp.path(), &caller)
            .expect("a relaunched window must resolve its session's own snapshot");
        assert_eq!(got.path, mine);
        assert_eq!(got.via, ResolutionPath::TmuxSession);

        let mut sessions = vec![entry(&mine, Some("tm-supervisor:0:@258"))];
        let caller = CallerIdentity::new(Some("tmux-window-262"), Some("tm-supervisor:0:@262"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        redact_sessions_not_owned_by(tmp.path(), &caller, &mut sessions);
        assert!(
            sessions[0].owned,
            "the resolved snapshot's entry must be readable"
        );
    }

    /// 2020-09-13T12:26:40Z: when the caller's tmux session was created in
    /// these tests — after [`PREDECESSOR_STAMP`], before any live pause.
    const SESSION_CREATED: i64 = 1_600_000_000;

    /// 2020-01-01T00:00:00Z: a pause by an earlier session of the same name.
    const PREDECESSOR_STAMP: &str = "20200101-000000";

    /// Pause, then re-date the snapshot to [`PREDECESSOR_STAMP`] by renaming
    /// it — the reader dates a snapshot from its filename.
    fn pause_as_predecessor(dir: &Path, session_id: &str, window: &str) -> PathBuf {
        let path = pause(dir, session_id, Some(window));
        let old = path.with_file_name(format!("session-{PREDECESSOR_STAMP}.md"));
        std::fs::rename(&path, &old).unwrap();
        old
    }

    /// Why (#8408, MEDIUM-1): trusty-mpm names every tmux session
    /// `tm-<folder>`, so a FRESH session for the project reuses its
    /// predecessor's name. A name match alone handed the new session the old
    /// one's snapshot.
    /// What: a snapshot paused before the caller's session was created does
    /// not resolve, and a relaunch inside the same session (paused after it
    /// was created) does.
    /// Test: itself.
    #[test]
    fn a_reused_session_name_does_not_claim_its_predecessors_snapshot() {
        let tmp = tempfile::TempDir::new().unwrap();
        let old = pause_as_predecessor(tmp.path(), "predecessor", "tm-apex:0:@263");
        let fresh = CallerIdentity::new(None, Some("tm-apex:0:@5"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        assert!(
            resolve_snapshot_for_identity(tmp.path(), &fresh).is_none(),
            "a predecessor's snapshot must not resolve under a reused name"
        );

        let mine = pause(tmp.path(), "this-session", Some("tm-apex:0:@6"));
        let relaunched = CallerIdentity::new(None, Some("tm-apex:0:@7"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        let got = resolve_snapshot_for_identity(tmp.path(), &relaunched)
            .expect("a relaunch inside the same tmux session must resolve");
        assert_eq!(got.path, mine);
        assert_eq!(got.via, ResolutionPath::TmuxSession);
        assert_ne!(got.path, old);
    }

    /// Why (#8408): with no creation time the resolver cannot tell this
    /// session from a predecessor of the same name, so it must fail closed.
    /// What: the same caller resolves through the session route with a
    /// creation time and resolves nothing without one.
    /// Test: itself.
    #[test]
    fn the_session_route_without_a_creation_time_resolves_nothing() {
        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "writer", Some("tm-apex:0:@6"));
        assert!(resolve_snapshot_for_caller(tmp.path(), None, Some("tm-apex:0:@7")).is_none());
        let caller = CallerIdentity::new(None, Some("tm-apex:0:@7"));
        assert!(resolve_snapshot_for_identity(tmp.path(), &caller).is_none());
        let caller = caller.with_tmux_session_created(Some(SESSION_CREATED));
        assert!(resolve_snapshot_for_identity(tmp.path(), &caller).is_some());
    }

    /// Why (#8408, HIGH-1): the digest marked every entry sharing the caller's
    /// tmux session name `owned`, so a caller in `@263` read the restorable
    /// state of `@270` and of every earlier `tm-<folder>` session.
    /// What: a window-route caller owns only its window's entry; a
    /// session-route caller owns exactly the one snapshot the resolver
    /// answered; neither owns the predecessor's.
    /// Test: itself.
    #[test]
    fn a_session_name_match_owns_only_the_resolved_snapshot() {
        let tmp = tempfile::TempDir::new().unwrap();
        let old = pause_as_predecessor(tmp.path(), "predecessor", "tm-apex:0:@200");
        let w263 = pause(tmp.path(), "in-263", Some("tm-apex:0:@263"));
        let w270 = pause(tmp.path(), "in-270", Some("tm-apex:1:@270"));
        let digest = || {
            vec![
                entry(&old, Some("tm-apex:0:@200")),
                entry(&w263, Some("tm-apex:0:@263")),
                entry(&w270, Some("tm-apex:1:@270")),
            ]
        };

        let mut sessions = digest();
        let in_263 = CallerIdentity::new(Some("relaunched"), Some("tm-apex:0:@263"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        redact_sessions_not_owned_by(tmp.path(), &in_263, &mut sessions);
        let owned: Vec<bool> = sessions.iter().map(|s| s.owned).collect();
        assert_eq!(owned, [false, true, false], "only @263's own entry");

        let mut sessions = digest();
        let in_280 = CallerIdentity::new(Some("relaunched"), Some("tm-apex:2:@280"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        let resolved = resolve_snapshot_for_identity(tmp.path(), &in_280).unwrap();
        assert_eq!(resolved.via, ResolutionPath::TmuxSession);
        redact_sessions_not_owned_by(tmp.path(), &in_280, &mut sessions);
        let owned: Vec<&str> = sessions
            .iter()
            .filter(|s| s.owned)
            .filter_map(|s| s.source_file.as_deref())
            .collect();
        assert_eq!(owned, [resolved.path.to_str().unwrap()]);
        assert!(!sessions[0].owned, "the predecessor's entry is never owned");
    }

    /// Why (#8408): the session route is a later fallback, so it must never
    /// displace a window that still exists.
    /// What: with a window match and a newer same-session match, the window's
    /// snapshot wins and the route says `tmux_window`.
    /// Test: itself.
    #[test]
    fn a_window_match_outranks_a_session_match() {
        let tmp = tempfile::TempDir::new().unwrap();
        let by_window = pause(tmp.path(), "a", Some("tm-apex:0:@263"));
        pause(tmp.path(), "b", Some("tm-apex:1:@270"));

        let caller = CallerIdentity::new(None, Some("tm-apex:0:@263"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        let got = resolve_snapshot_for_identity(tmp.path(), &caller).unwrap();
        assert_eq!(got.path, by_window);
        assert_eq!(got.via, ResolutionPath::TmuxWindow);
    }

    /// Why (#8408): tmux names an unnamed session `0`, `1`, … and reuses the
    /// numbers, so two unrelated sessions share them over time.
    /// What: a digits-only session name neither resolves a snapshot nor makes
    /// a caller its owner.
    /// Test: itself.
    #[test]
    fn a_numbered_tmux_session_never_matches() {
        let tmp = tempfile::TempDir::new().unwrap();
        let theirs = pause(tmp.path(), "writer", Some("0:0:@5"));
        assert_eq!(session_name_of("0:0:@5"), None);
        let caller = CallerIdentity::new(None, Some("0:0:@9"))
            .with_tmux_session_created(Some(SESSION_CREATED));
        assert!(resolve_snapshot_for_identity(tmp.path(), &caller).is_none());

        let mut sessions = vec![entry(&theirs, Some("0:0:@5"))];
        redact_sessions_not_owned_by(tmp.path(), &caller, &mut sessions);
        assert!(!sessions[0].owned);
    }

    /// Why: #5272's rule is unchanged — an id that owns a snapshot gets that
    /// snapshot. The fallback is only reachable on a miss, so a window match
    /// can never displace an exact one.
    /// What: with both available, the exact id's own file wins and the route
    /// says `session_id`.
    /// Test: itself.
    #[test]
    fn exact_session_id_match_wins_over_window_match() {
        let tmp = tempfile::TempDir::new().unwrap();
        // A second session paused in the same window, under its own id.
        let mine = pause(tmp.path(), "mine", Some("tm-dogfood:0:@230"));
        let theirs = pause(tmp.path(), "theirs", Some("tm-dogfood:0:@230"));
        assert_ne!(mine, theirs);

        let got = resolve_snapshot_for_caller(tmp.path(), Some("mine"), Some("tm-dogfood:0:@230"))
            .expect("an owning id always resolves");
        assert_eq!(got.path, mine, "the exact id must not be overridden");
        assert_eq!(got.via, ResolutionPath::SessionId);
    }

    /// Why: window ids are reusable, so the project scope is the second gate.
    /// A snapshot in another project's store must stay invisible even when the
    /// window id matches exactly.
    /// What: the same window id resolves in the project that wrote it and
    /// nowhere else.
    /// Test: itself.
    #[test]
    fn window_fallback_is_scoped_to_the_project_dir() {
        let a = tempfile::TempDir::new().unwrap();
        let b = tempfile::TempDir::new().unwrap();
        pause(a.path(), "writer", Some("tm-dogfood:0:@230"));

        assert!(resolve_snapshot_for_caller(a.path(), None, Some("tm-dogfood:0:@230")).is_some());
        assert!(
            resolve_snapshot_for_caller(b.path(), None, Some("tm-dogfood:0:@230")).is_none(),
            "another project's store must not answer"
        );
    }

    /// Why: snapshots written before the window was captured have no
    /// `## Tmux Window` section at all; they must be skipped rather than
    /// matched or panicked on.
    /// What: a windowless snapshot is invisible to the fallback, and a newer
    /// windowed one still resolves past it.
    /// Test: itself.
    #[test]
    fn snapshot_without_a_recorded_window_is_skipped() {
        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "legacy", None);
        assert!(resolve_snapshot_for_caller(tmp.path(), None, Some("tm-dogfood:0:@230")).is_none());

        let windowed = pause(tmp.path(), "modern", Some("tm-dogfood:0:@230"));
        let got = resolve_snapshot_for_caller(tmp.path(), None, Some("tm-dogfood:0:@230")).unwrap();
        assert_eq!(got.path, windowed);
    }

    #[test]
    fn no_session_id_and_no_window_resolves_nothing() {
        let tmp = tempfile::TempDir::new().unwrap();
        pause(tmp.path(), "writer", Some("tm-dogfood:0:@230"));
        assert!(resolve_snapshot_for_caller(tmp.path(), None, None).is_none());
        assert!(resolve_snapshot_for_caller(tmp.path(), Some("nobody"), None).is_none());
    }
}