@@ -7,10 +7,12 @@ import (
77 "log/slog"
88 "os"
99 "path/filepath"
10+ "sync"
1011 "time"
1112
1213 "github.com/marmutapp/superbased-observer/internal/attachsock"
1314 "github.com/marmutapp/superbased-observer/internal/config"
15+ "github.com/marmutapp/superbased-observer/internal/git"
1416 "github.com/marmutapp/superbased-observer/internal/intelligence/dashboard"
1517 "github.com/marmutapp/superbased-observer/internal/remotenotify"
1618 "github.com/marmutapp/superbased-observer/internal/store"
@@ -230,6 +232,52 @@ func buildTerminalStack(cfg config.Config, database *sql.DB, logger *slog.Logger
230232 // mutually referential — the same shape as the OnExit/scanHub closures).
231233 launcher .correlate = svc .Correlate
232234
235+ // Generic terminal-run discovery sweep (Session Cockpit part C). A
236+ // daemon-resident, tool-AGNOSTIC pass that periodically links a LIVE
237+ // uncorrelated run to a UNIQUE candidate observer session — agreeing on tool +
238+ // git root + launch time, unique-or-abstain, held across a dwell — at
239+ // termrun.SourceDiscovered (0.75). It closes the correlation gap for every
240+ // launcher that has no OOB id echo (only claude-code and codex correlate
241+ // today; every other launcher's Session Cockpit otherwise waits forever). It
242+ // reads the SAME svc seams the OOB path uses (HandleForRun = liveness truth;
243+ // ProjectRoot = the validated launch dir; Correlate = the one scored-link
244+ // seam) so a stale crash-orphan run from a previous boot simply misses the
245+ // live-handle lookup and is skipped. Gated on a wired DB, and run on its OWN
246+ // context so the stack's close func can stop it FIRST — before the H2 shutdown
247+ // stamps and mgr.Shutdown() — so no in-flight tick write can race the
248+ // defer-LIFO DB close (the same ordering discipline the H2 stamps rely on).
249+ stopDiscover := func () {}
250+ if database != nil {
251+ disc := newTerminalDiscoverer (
252+ store .New (database ),
253+ svc .HandleForRun ,
254+ svc .SessionLinkForRun ,
255+ svc .ProjectRoot ,
256+ svc .Correlate ,
257+ func (dir string ) string {
258+ info , gerr := git .Resolve (dir )
259+ if gerr != nil {
260+ return dir // unreadable dir: fall back to the raw path (store filter still applies)
261+ }
262+ return info .Root
263+ },
264+ nil , // now → time.Now (the link timestamp is now().UTC())
265+ logger ,
266+ defaultTerminalDiscoverConfig (),
267+ )
268+ dctx , dcancel := context .WithCancel (context .Background ())
269+ var dwg sync.WaitGroup
270+ dwg .Add (1 )
271+ go func () {
272+ defer dwg .Done ()
273+ disc .run (dctx )
274+ }()
275+ stopDiscover = func () {
276+ dcancel ()
277+ dwg .Wait ()
278+ }
279+ }
280+
233281 // F4 agent-status hub: fuses the feed (OSC hints + OOB/lifecycle) with
234282 // termsession output-recency/exit into a per-run status. Gated by
235283 // [terminal.status].enabled; when off, no provider is wired (endpoints 503).
@@ -320,6 +368,11 @@ func buildTerminalStack(cfg config.Config, database *sql.DB, logger *slog.Logger
320368 resumeAuthority : resumeAuthority ,
321369 reclaimOnInput : cfg .Terminal .Attach .ReclaimOnInput ,
322370 close : func () {
371+ // Stop the discovery sweep FIRST — before ANY DB write below and before
372+ // mgr.Shutdown() — and WAIT for its goroutine to drain, so no in-flight
373+ // tick's Correlate write can race the defer-LIFO DB close that start.go
374+ // runs after this. No-op when no DB was wired.
375+ stopDiscover ()
323376 // H2: stamp every LIVE attach run 'daemon_shutdown' SYNCHRONOUSLY,
324377 // BEFORE mgr.Shutdown() kills the PTYs (whose async OnExit would
325378 // otherwise race the DB close) and before start.go's defer-LIFO closes
0 commit comments