Skip to content

Commit 3427c95

Browse files
committed
feat: persist provider session refs to disk for resume across restarts
Save providerSessions and sessionWorkspaces to ~/.codewithphone/sessions.json so daemon restarts can resume CLI sessions (--resume / thread/resume) instead of starting fresh conversations.
1 parent 5b07a6b commit 3427c95

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

internal/app/service.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ func (s *Service) Run(ctx context.Context) error {
167167
return fmt.Errorf("prepare daemon data directory: %w", err)
168168
}
169169

170+
// Restore provider session refs from disk so daemon restarts can
171+
// resume CLI sessions instead of starting fresh conversations.
172+
s.loadSessionState()
173+
170174
// Log CLI tool versions at startup so operators can see at a glance
171175
// whether any runner is out of the tested range.
172176
CheckRunnerVersions(map[string]string{
@@ -367,6 +371,7 @@ func (s *Service) setProviderSession(sessionID, providerSessionRef string) {
367371
s.mu.Lock()
368372
defer s.mu.Unlock()
369373
s.providerSessions[sessionID] = providerSessionRef
374+
s.persistSessionState()
370375
}
371376

372377
// pinSessionWorkspace records the first workspaceRoot used for a session and
@@ -378,9 +383,67 @@ func (s *Service) pinSessionWorkspace(sessionID, workspaceRoot string) string {
378383
return existing
379384
}
380385
s.sessionWorkspaces[sessionID] = workspaceRoot
386+
s.persistSessionState()
381387
return workspaceRoot
382388
}
383389

390+
// ── Session state persistence ──────────────────────────────────────
391+
//
392+
// Persists providerSessions and sessionWorkspaces to disk so that
393+
// daemon restarts can resume CLI sessions (--resume / thread/resume)
394+
// instead of starting fresh conversations.
395+
396+
type sessionStateFile struct {
397+
ProviderSessions map[string]string `json:"providerSessions"`
398+
SessionWorkspaces map[string]string `json:"sessionWorkspaces"`
399+
}
400+
401+
func sessionStatePath() string {
402+
return filepath.Join(config.HomeDir(), "sessions.json")
403+
}
404+
405+
// persistSessionState writes the current session mappings to disk.
406+
// Caller must hold s.mu.
407+
func (s *Service) persistSessionState() {
408+
data := sessionStateFile{
409+
ProviderSessions: s.providerSessions,
410+
SessionWorkspaces: s.sessionWorkspaces,
411+
}
412+
body, err := json.Marshal(data)
413+
if err != nil {
414+
log.Printf("failed to marshal session state: %v", err)
415+
return
416+
}
417+
if err := os.WriteFile(sessionStatePath(), body, 0o600); err != nil {
418+
log.Printf("failed to persist session state: %v", err)
419+
}
420+
}
421+
422+
// loadSessionState restores provider session refs and workspace mappings
423+
// from disk. Called once at startup.
424+
func (s *Service) loadSessionState() {
425+
body, err := os.ReadFile(sessionStatePath())
426+
if err != nil {
427+
return // file doesn't exist yet — first run
428+
}
429+
var data sessionStateFile
430+
if err := json.Unmarshal(body, &data); err != nil {
431+
log.Printf("failed to parse session state: %v", err)
432+
return
433+
}
434+
if data.ProviderSessions != nil {
435+
for k, v := range data.ProviderSessions {
436+
s.providerSessions[k] = v
437+
}
438+
}
439+
if data.SessionWorkspaces != nil {
440+
for k, v := range data.SessionWorkspaces {
441+
s.sessionWorkspaces[k] = v
442+
}
443+
}
444+
log.Printf("restored %d provider sessions from disk", len(data.ProviderSessions))
445+
}
446+
384447
func (s *Service) setTaskWorkspace(taskRunID, workspaceRoot string) {
385448
taskRunID = strings.TrimSpace(taskRunID)
386449
workspaceRoot = strings.TrimSpace(workspaceRoot)

0 commit comments

Comments
 (0)