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
//! Stale duplicate-record dedup for `reconcile_on_boot` (#2306).
//!
//! Why: a project can accrue two conflicting non-`Decommissioned` records — one
//! canonical (resolved `workspace_path`) and one adopted record permanently
//! stuck at `cwd`/`workspace_path = /unknown` (adopted before #2163's
//! `get_pane_cwd` resolution existed). `reconcile_on_boot` only resolves
//! `/unknown` when a LIVE pane is discovered; it never revisits a `/unknown`
//! record once its pane is gone, and nothing collapses N records per project,
//! so `tm` keeps re-attaching to the wrong/stale one. #2001/#2004 (zombie
//! reconcile) and #2148 (non-destructive resume) are single-record and do not
//! cover this.
//!
//! **Grouping caveat (post-review, #2306):** `source_id` is derived from the
//! git remote and is per-REPOSITORY
//! ([`super::adopt::derive_source_id_for_record`]), not per-workspace — every
//! concurrent worktree session of one repo shares the same `source_id`. A
//! naive "keep only the newest per group" policy would therefore collapse
//! *legitimate concurrent sessions*, and worse: an in-project worktree record
//! is `workspace_owned = false` BY DESIGN, yet `decommission` still runs
//! `git worktree remove --force` (+ branch delete) for such a path via
//! [`super::decommission::is_session_worktree`] — so a naive policy would
//! destroy real, possibly-uncommitted work. See the eligibility rule below,
//! which is the actual fix for that danger.
//!
//! What: groups non-`Decommissioned` records by `source_id` (fallback: resolved
//! `workspace_path`). Within a quiesced group (no member has a live tmux
//! session — see the live-group guard), a record is only ever a dedup
//! **loser** when it is unresolved-or-dead: [`is_resolved_existing`] is
//! `false` for it (no `workspace_path`, the `/unknown` sentinel, or a path
//! that no longer exists on disk). A record with a resolved, still-existing
//! `workspace_path` is NEVER a loser — regardless of group size or recency —
//! because such records are the concurrent-session case the naive policy got
//! wrong. Consequently:
//! - a group with 2+ resolved-existing members collapses nothing among them;
//! only its unresolved/dead members (if any) are decommissioned, and only
//! because a resolved-existing (or live) member supersedes them;
//! - a group with ZERO resolved-existing members (e.g. `/unknown`-only or
//! all-dead) keeps the most-recently-active/created member and
//! decommissions the rest — there is nothing on disk to lose in that case.
//!
//! An unresolved/dead loser is additionally rechecked against the SAME
//! [`is_resolved_existing`] predicate immediately before decommission runs
//! (belt-and-suspenders against a TOCTOU race); a loser that fails the
//! recheck is skipped with a `warn!`, never decommissioned.
//!
//! **Liveness is evidence, and absence of it is not.** Every rule above reads
//! "this record's `tmux_name` is not in the live set" as proof the session is
//! gone, and acts on that proof with `decommission` — which is irreversible:
//! it tombstones the record, clears `workspace_path`, and hides the row from
//! every picker view. The live set must therefore come from tmux actually
//! having been asked and answered. See
//! [`SessionManager::observed_live_managed_names`], which refuses rather than
//! reporting an empty set it cannot stand behind.
//!
//! Test: `plan_dedup_*`, `is_resolved_existing_*`, and `reconcile_dedup_*` in
//! `dedup_tests.rs`; the refusal contract in `dedup_refuses_*`.
use Ordering;
use ;
use ;
use ;
use ;
use ;
/// [`SessionManager::observed_live_managed_names`] against a bare driver.
///
/// Why it takes the driver rather than `&self` (#7965): the probe is two
/// synchronous tmux subprocesses, and
/// [`SessionManager::workspace_claims`](super::SessionManager::workspace_claims)
/// must run it on the blocking pool under a timeout instead of on whatever thread
/// called it. That needs an owned `Arc<dyn ManagedTmuxDriver>` and no borrow of
/// the manager.
/// What: the same body, unchanged — the `ensure_server_up` guard, the propagated
/// `list_sessions` error, the managed-name filter.
/// Test: as [`SessionManager::observed_live_managed_names`], plus
/// `a_slow_tmux_probe_is_bounded_and_leaves_every_claim_live`.
pub
/// Grouping key for a record, or `None` if it cannot be grouped.
///
/// Why: two records for the SAME project must land in the same group so a
/// quiesced duplicate can be collapsed; a record with neither a `source_id`
/// nor a `workspace_path` (a bare `/unknown` with no provenance) is not safely
/// attributable to any project and is therefore left alone. NOTE: `source_id`
/// is per-repository, not per-workspace (see module docs) — the safety
/// boundary against over-grouping is [`is_resolved_existing`] at selection
/// time, not this key.
/// What: prefers `source_id`; falls back to the `workspace_path` string. The
/// key is namespaced (`src:` / `ws:`) so a source id can never collide with a
/// path.
/// Test: covered indirectly by `plan_dedup_distinct_source_ids_untouched` and
/// `plan_dedup_unknown_only_group_keeps_most_recent`.
/// True when `record.workspace_path` is a resolved, real, currently-existing
/// directory on disk.
///
/// Why (#2306 review fix): this is THE safety boundary for dedup. A record
/// that passes this check represents a legitimate on-disk workspace — which
/// may be one of several concurrent sessions of the SAME repo, since
/// `source_id` groups by repository, not by workspace. Such a record must
/// never be treated as a stale duplicate, no matter the group size or its
/// recency relative to siblings — an in-project worktree record is
/// `workspace_owned = false` by design, so the `workspace_owned` flag alone
/// cannot distinguish "safe to decommission" from "real work that would be
/// `git worktree remove --force`'d". Only a record that FAILS this check (no
/// `workspace_path`, the literal `/unknown` sentinel, or a path that no
/// longer exists — e.g. already removed by other means) is eligible to be a
/// dedup loser.
/// What: `true` iff `workspace_path` is `Some(p)`, `p != Path::new("/unknown")`,
/// and `p.exists()`.
/// Test: `is_resolved_existing_true_for_real_dir`,
/// `is_resolved_existing_false_for_missing_dir`,
/// `is_resolved_existing_false_for_none_and_unknown_sentinel`.
pub
/// Order two records by recency only (greater = more recently active/created).
///
/// Why: used ONLY to break a tie among unresolved/dead candidates in a group
/// with zero resolved-existing members — there is nothing on disk to lose in
/// that case, so "most recently touched" is a reasonable survivor heuristic.
/// What: ranks by `last_activity_at` (`None` sorts earliest), then
/// `created_at`.
/// Test: `plan_dedup_unknown_only_group_keeps_most_recent`.
/// Plan which records to decommission to dedup stale duplicates (#2306).
///
/// Why: the decision must be a pure function of the current record set, disk
/// state (via [`is_resolved_existing`]'s `exists()` check), and the live-tmux
/// name set so it is trivially unit-testable without a real store or tmux
/// driver (the #1790 no-real-tmux guard).
/// What: groups non-`Decommissioned` records via [`group_key`]; skips any
/// group of size <2 or with a live-tmux member. For the remaining (quiesced,
/// size>1) groups, partitions members into resolved-existing vs.
/// unresolved/dead via [`is_resolved_existing`]. If ANY member is
/// resolved-existing, every unresolved/dead member is a loser (the
/// resolved-existing members are NEVER touched, even if there are several).
/// If NONE are resolved-existing, the most-recent member (by [`by_recency`])
/// survives and every other member is a loser.
/// Test: the `plan_dedup_*` tests in `dedup_tests.rs`.
pub
/// Plan losers among records that share the IDENTICAL, resolved-existing
/// `workspace_path` (#3396).
///
/// Why: [`plan_dedup`]'s `source_id` grouping is per-REPOSITORY by design
/// (see the module docs) — it deliberately never touches two
/// resolved-existing records with DIFFERENT `workspace_path`s, because that
/// shape is the legitimate concurrent-worktree-sessions case, and it treats
/// ANY resolved-existing member as categorically immune regardless of group
/// size. But two records pointing at the exact SAME `workspace_path` are
/// never that: a single worktree directory hosts at most one live managed
/// identity, so a group of 2+ non-`Decommissioned` records sharing one
/// resolved-existing path is ALWAYS a duplicate-identity bug (#3396 — e.g. a
/// live tmux session renamed/replaced out from under its record, then
/// re-discovered and adopted under a different name against the same cwd by
/// [`super::reconcile::SessionManager::reconcile_on_boot`]'s external-adopt
/// loop, before that loop's own #3396 duplicate-prevention check existed).
/// `plan_dedup`'s live-group guard ("skip the WHOLE group if ANY member is
/// live") and its resolved-existing categorical immunity both, correctly,
/// refuse to touch this shape — which is exactly why a #3396 stale duplicate
/// persists across every `plan_dedup`-only pass. This function is the
/// narrowly-scoped exact-path counterpart: it groups by canonicalized
/// `workspace_path` alone (ignoring `source_id`), and — because "same
/// directory" is unambiguous where "same repository" is not — is allowed to
/// pick a survivor even when one member is currently live.
/// What: groups non-`Decommissioned`, resolved-existing (per
/// [`is_resolved_existing`]) records by canonicalized `workspace_path`, then
/// for each group of 2+:
/// - A `workspace_owned = true` member (the SM provisioned this directory
/// itself, e.g. via clone) is NEVER a loser — categorical protection
/// against ever risking a real, SM-owned repository, mirroring
/// [`is_resolved_existing`]'s role in `plan_dedup`. Two-or-more owned
/// members sharing one path should structurally never happen; rather than
/// guess, that shape leaves the WHOLE group untouched.
/// - Exactly one owned member: it is the automatic survivor, and every
/// `workspace_owned = false` sibling whose `tmux_name` is currently DEAD is
/// a loser (a live one is left alone too — never decommission a currently
/// active session).
/// - Zero owned members: the survivor is chosen among the unowned members —
/// if exactly one has a live `tmux_name` it survives; if none are live, the
/// most recently active/created one survives (mirrors `plan_dedup`'s
/// /unknown tie-break); if two or more are simultaneously live, the group
/// is left untouched — two genuinely-live panes pointed at the same
/// directory is ambiguous enough that guessing a survivor risks tearing
/// down a real session, so this defers to the operator rather than picking
/// one.
///
/// A loser selected here therefore always has both `workspace_owned = false`
/// (decommission never deletes the directory) and a currently-DEAD
/// `tmux_name` (decommission never kills a live tmux session).
/// Test: the `plan_workspace_duplicates_*` tests in `dedup_tests.rs`.
pub