Skip to content

Commit 894ec3b

Browse files
fix: restore closed windows using path+timestamp matching
EnrichFromCopilotSessionStore now scores each candidate copilot session by: - Path/CWD match (100 exact, 50 prefix) — strongest signal - Title/summary match (80 exact, 40 contains) - Timestamp proximity to window's last_seen/closed_at (+0..20) Pass referenceTime (closed_at or last_seen) through GetLastKnownTabs and GetClosedWindowsWithTabs so old windows captured before reliable session ID tracking can still recover their copilot sessions and correct working directories on restore. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 410b5b8 commit 894ec3b

2 files changed

Lines changed: 93 additions & 39 deletions

File tree

src/TSG/TSG.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<PackAsTool>true</PackAsTool>
2525
<ToolCommandName>tsg</ToolCommandName>
2626
<PackageId>TerminalStateGuard</PackageId>
27-
<Version>2.0.3</Version>
27+
<Version>2.0.4</Version>
2828
<Authors>sbay-dev</Authors>
2929
<Company>sbay-dev</Company>
3030
<Description>TSG — Terminal State Guard: Complete terminal intelligence platform with real-time window tracking (COM IUIAutomation), interactive process manager, SQLite state database, session recovery, and Copilot performance optimization.</Description>

src/TSG/TerminalDatabase.cs

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ public List<EventRecord> GetRecentEvents(int limit = 50)
366366
/// Enriches tabs with copilot session IDs from earlier captures if the last capture lost them
367367
/// (copilot process may have ended before window closed).
368368
/// </summary>
369-
public List<CaptureTab> GetLastKnownTabs(string windowId)
369+
public List<CaptureTab> GetLastKnownTabs(string windowId, string? referenceTime = null)
370370
{
371371
using var cmd = _conn.CreateCommand();
372372
cmd.CommandText = """
@@ -418,7 +418,7 @@ ORDER BY cw.capture_id DESC LIMIT 1
418418

419419
// Enrich: if any tab lost its copilot session ID (copilot ended before window closed),
420420
// recover it from earlier captures for the same window
421-
EnrichTabsWithHistoricalSessionIds(windowId, tabs);
421+
EnrichTabsWithHistoricalSessionIds(windowId, tabs, referenceTime);
422422

423423
return tabs;
424424
}
@@ -427,7 +427,7 @@ ORDER BY cw.capture_id DESC LIMIT 1
427427
/// Recover copilot session IDs from earlier captures when the last capture lost them.
428428
/// Matches tabs by ordinal position within the same window.
429429
/// </summary>
430-
void EnrichTabsWithHistoricalSessionIds(string windowId, List<CaptureTab> tabs)
430+
void EnrichTabsWithHistoricalSessionIds(string windowId, List<CaptureTab> tabs, string? referenceTime = null)
431431
{
432432
// Find all distinct copilot session IDs ever seen for this window, with their ordinal and path
433433
using var cmd = _conn.CreateCommand();
@@ -465,7 +465,7 @@ AND ct.parent_ordinal IS NULL
465465
if (sessionByOrdinal.Count == 0)
466466
{
467467
// No history for this window — fall back to scanning all copilot sessions on disk
468-
EnrichFromCopilotSessionStore(tabs);
468+
EnrichFromCopilotSessionStore(tabs, referenceTime);
469469
return;
470470
}
471471

@@ -514,36 +514,50 @@ AND ct.parent_ordinal IS NULL
514514
}
515515

516516
// Final fallback: scan disk for any unmatched tabs
517-
EnrichFromCopilotSessionStore(tabs);
517+
EnrichFromCopilotSessionStore(tabs, referenceTime);
518518
}
519519

520520
/// <summary>
521521
/// Last-resort enrichment: scan all copilot session-state directories on disk
522-
/// and match tab titles to session summaries. Useful for OLD windows captured
523-
/// before session ID tracking was reliable.
522+
/// and match unmatched tabs by PATH (CWD) and TITLE, using nearest TIMESTAMP
523+
/// to the window's reference time as tie-breaker. Restores session continuity
524+
/// for closed windows captured before reliable session ID tracking.
524525
/// </summary>
525-
static void EnrichFromCopilotSessionStore(List<CaptureTab> tabs)
526+
static void EnrichFromCopilotSessionStore(List<CaptureTab> tabs, string? referenceTime = null)
526527
{
527528
var sessionsDir = Path.Combine(
528529
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
529530
".copilot", "session-state");
530531
if (!Directory.Exists(sessionsDir)) return;
531532

532533
// Check if any tabs need enrichment
533-
if (!tabs.Any(t => t.CopilotSessionId == null && !string.IsNullOrEmpty(t.Title))) return;
534+
if (!tabs.Any(t => t.CopilotSessionId == null)) return;
534535

535-
// Build summary → (sessionId, cwd, updated) lookup from disk
536+
DateTime? refTime = null;
537+
if (!string.IsNullOrEmpty(referenceTime)
538+
&& DateTime.TryParse(referenceTime, System.Globalization.CultureInfo.InvariantCulture,
539+
System.Globalization.DateTimeStyles.AssumeLocal, out var parsedRef))
540+
{
541+
refTime = parsedRef;
542+
}
543+
544+
// Collect already-claimed session IDs to avoid double-assignment
545+
var claimed = new HashSet<string>(
546+
tabs.Where(t => t.CopilotSessionId != null).Select(t => t.CopilotSessionId!),
547+
StringComparer.OrdinalIgnoreCase);
548+
549+
// Build sessions catalog from disk
536550
var diskSessions = new List<(string SessionId, string Summary, string Cwd, DateTime Updated)>();
537551
foreach (var dir in Directory.EnumerateDirectories(sessionsDir))
538552
{
539553
var ws = Path.Combine(dir, "workspace.yaml");
540554
if (!File.Exists(ws)) continue;
541555
try
542556
{
543-
string? id = Path.GetFileName(dir);
557+
var id = Path.GetFileName(dir);
544558
string? summary = null;
545559
string? cwd = null;
546-
DateTime updated = File.GetLastWriteTime(ws);
560+
var updated = File.GetLastWriteTime(ws);
547561
foreach (var line in File.ReadLines(ws))
548562
{
549563
var t = line.TrimStart();
@@ -552,47 +566,87 @@ static void EnrichFromCopilotSessionStore(List<CaptureTab> tabs)
552566
else if (t.StartsWith("cwd: ", StringComparison.Ordinal))
553567
cwd = t["cwd: ".Length..].Trim();
554568
}
555-
if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(summary) && summary.Length >= 4)
556-
diskSessions.Add((id, summary, cwd ?? "", updated));
569+
if (!string.IsNullOrEmpty(id))
570+
diskSessions.Add((id, summary ?? "", cwd ?? "", updated));
557571
}
558572
catch (IOException) { }
559573
}
560574

561575
if (diskSessions.Count == 0) return;
562576

563-
// Sort by most recent for tie-breaking
564-
diskSessions = [.. diskSessions.OrderByDescending(s => s.Updated)];
565-
566577
foreach (var tab in tabs)
567578
{
568579
if (tab.CopilotSessionId != null) continue;
569-
if (string.IsNullOrEmpty(tab.Title)) continue;
570580

571-
var cleanTitle = tab.Title.Replace("🤖", "", StringComparison.Ordinal).Trim();
572-
if (cleanTitle.Length < 4) continue;
581+
var cleanTitle = (tab.Title ?? "")
582+
.Replace("🤖", "", StringComparison.Ordinal).Trim();
583+
var tabPath = tab.Path ?? "";
584+
var hasUsefulTitle = cleanTitle.Length >= 4;
585+
var hasUsefulPath = !string.IsNullOrEmpty(tabPath);
573586

574-
// Try exact summary match first, then bidirectional contains
575-
var match = diskSessions.FirstOrDefault(s =>
576-
s.Summary.Equals(cleanTitle, StringComparison.OrdinalIgnoreCase));
577-
if (match.SessionId == null)
578-
{
579-
match = diskSessions.FirstOrDefault(s =>
580-
cleanTitle.Contains(s.Summary, StringComparison.OrdinalIgnoreCase)
581-
|| s.Summary.Contains(cleanTitle, StringComparison.OrdinalIgnoreCase));
582-
}
587+
if (!hasUsefulTitle && !hasUsefulPath) continue;
583588

584-
if (match.SessionId != null)
589+
// Score every candidate session
590+
(string SessionId, string Summary, string Cwd, DateTime Updated, double Score)? best = null;
591+
foreach (var s in diskSessions)
585592
{
586-
tab.HasCopilot = true;
587-
tab.CopilotSessionId = match.SessionId;
588-
tab.CopilotSummary = match.Summary;
589-
if (string.IsNullOrEmpty(tab.Path) && !string.IsNullOrEmpty(match.Cwd))
593+
if (claimed.Contains(s.SessionId)) continue;
594+
595+
double score = 0;
596+
597+
// Path match (strongest signal)
598+
if (hasUsefulPath && !string.IsNullOrEmpty(s.Cwd))
599+
{
600+
if (tabPath.Equals(s.Cwd, StringComparison.OrdinalIgnoreCase))
601+
score += 100;
602+
else if (tabPath.StartsWith(s.Cwd, StringComparison.OrdinalIgnoreCase)
603+
|| s.Cwd.StartsWith(tabPath, StringComparison.OrdinalIgnoreCase))
604+
score += 50;
605+
}
606+
607+
// Title/summary match
608+
if (hasUsefulTitle && !string.IsNullOrEmpty(s.Summary))
590609
{
591-
tab.Path = match.Cwd;
592-
tab.Folder = Path.GetFileName(match.Cwd);
593-
tab.DirExists = Directory.Exists(match.Cwd);
610+
if (cleanTitle.Equals(s.Summary, StringComparison.OrdinalIgnoreCase))
611+
score += 80;
612+
else if (cleanTitle.Contains(s.Summary, StringComparison.OrdinalIgnoreCase)
613+
|| s.Summary.Contains(cleanTitle, StringComparison.OrdinalIgnoreCase))
614+
score += 40;
594615
}
616+
617+
if (score == 0) continue;
618+
619+
// Timestamp proximity bonus (closer to refTime = higher bonus, max +20)
620+
if (refTime.HasValue)
621+
{
622+
var deltaHours = Math.Abs((s.Updated - refTime.Value).TotalHours);
623+
// 0h delta → +20, 24h → +10, 1week → 0
624+
score += Math.Max(0, 20 - (deltaHours * 20.0 / 168.0));
625+
}
626+
else
627+
{
628+
// No reference time — prefer most recently updated
629+
score += Math.Max(0, 10 - (DateTime.Now - s.Updated).TotalDays * 0.1);
630+
}
631+
632+
if (best == null || score > best.Value.Score)
633+
best = (s.SessionId, s.Summary, s.Cwd, s.Updated, score);
634+
}
635+
636+
// Require minimum score to avoid garbage matches
637+
if (best == null || best.Value.Score < 40) continue;
638+
639+
tab.HasCopilot = true;
640+
tab.CopilotSessionId = best.Value.SessionId;
641+
if (!string.IsNullOrEmpty(best.Value.Summary))
642+
tab.CopilotSummary = best.Value.Summary;
643+
if (string.IsNullOrEmpty(tab.Path) && !string.IsNullOrEmpty(best.Value.Cwd))
644+
{
645+
tab.Path = best.Value.Cwd;
646+
tab.Folder = Path.GetFileName(best.Value.Cwd);
647+
tab.DirExists = Directory.Exists(best.Value.Cwd);
595648
}
649+
claimed.Add(best.Value.SessionId);
596650
}
597651
}
598652

@@ -618,7 +672,7 @@ ORDER BY w.closed_at DESC
618672
var firstSeen = reader.GetString(1);
619673
var lastSeen = reader.GetString(2);
620674
var closedAt = reader.IsDBNull(3) ? null : reader.GetString(3);
621-
var tabs = GetLastKnownTabs(id);
675+
var tabs = GetLastKnownTabs(id, closedAt ?? lastSeen);
622676
if (tabs.Count > 0)
623677
result.Add(new ClosedWindowRecord(id, firstSeen, lastSeen, closedAt, tabs));
624678
}

0 commit comments

Comments
 (0)