fix: persist one-shot sessions when --save-session is enabled#142
Conversation
--save-session only flipped appConfig.AutoSave, but runOneShot (the -p and positional-arg dispatch path) never consulted it or called session.SaveSession, so scripted/CI one-shot runs were silently never persisted regardless of the flag.
Sigilix OverviewEffort: 3/5 (medium) Quality gates
Summary — latest pushFixes one-shot sessions (invoked via Important files
Confidence: 4/5The change has a narrow, well-scoped surface with explicit gate logic and round-trip tests, though the closure wiring in main.go lacks direct unit coverage for the save call itself.
Suggested labels:
|
✅ QualityMax Pipeline
Powered by QualityMax — AI-Powered Test Automation |
| oneShotSessionID := session.GenerateSessionID() | ||
| saveOneShotSession := func() { | ||
| if shouldSaveOneShotSession(appConfig.AutoSave, ag.History) { | ||
| _ = session.SaveSession(oneShotSessionID, ag.History, ag.Cfg.Context.ProjectID, ag.Usage, ag.Cfg.Model) |
There was a problem hiding this comment.
One-shot session save discards SaveSession error silently
The saveOneShotSession closure calls session.SaveSession and discards the error (_ = session.SaveSession(...)). The sibling handler for saving sessions (the /save command in repl.go) prints the error to the user with fmt.Sprintf("Failed to save: %v", err). If SaveSession fails for the one-shot path (e.g., disk full, permission denied), the user receives no indication that the session was not persisted, potentially losing the session without knowing. Fix: log the error to stderr or propagate it so the user can be made aware.
More Info
- Threat model: User runs
qmax-code -p ...with --save-session, expects session to be saved. If SaveSession fails, the session is lost silently and the user cannot resume later. - Specific code citations: Rule-establishing sibling:
repl.go/save handler (retrieval snapshot lines 301-315) callssession.SaveSessionand prints error on failure. Violating site:main.gonewsaveOneShotSessionclosure (line ~429) uses_ = session.SaveSession(...). - Existing protections: None — error is unconditionally discarded.
- Proposed mitigation: Replace
_ = session.SaveSession(...)with an error check that logs the failure to stderr, e.g.,if err := session.SaveSession(...); err != nil { fmt.Fprintf(os.Stderr, "Failed to save session: %v\n", err) }. - Alternative mitigations considered: Returning the error would change the calling signature and may not be appropriate after successful agent output; logging is sufficient.
- Severity calibration: Score 2: silent data loss that only triggers under disk/permission edge cases, but sibling handler shows the expected user-visible error contract.
Prompt To Fix With AI
This is a comment left during a code review.
Path: main.go
Line: 436
Comment:
**One-shot session save discards SaveSession error silently**
The `saveOneShotSession` closure calls `session.SaveSession` and discards the error (`_ = session.SaveSession(...)`). The sibling handler for saving sessions (the `/save` command in repl.go) prints the error to the user with `fmt.Sprintf("Failed to save: %v", err)`. If SaveSession fails for the one-shot path (e.g., disk full, permission denied), the user receives no indication that the session was not persisted, potentially losing the session without knowing. Fix: log the error to stderr or propagate it so the user can be made aware.
Threat model:
User runs `qmax-code -p ...` with --save-session, expects session to be saved. If SaveSession fails, the session is lost silently and the user cannot resume later.
Specific code citations:
Rule-establishing sibling: `repl.go` /save handler (retrieval snapshot lines 301-315) calls `session.SaveSession` and prints error on failure. Violating site: `main.go` new `saveOneShotSession` closure (line ~429) uses `_ = session.SaveSession(...)`.
Existing protections:
None — error is unconditionally discarded.
Proposed mitigation:
Replace `_ = session.SaveSession(...)` with an error check that logs the failure to stderr, e.g., `if err := session.SaveSession(...); err != nil { fmt.Fprintf(os.Stderr, "Failed to save session: %v\n", err) }`.
Alternative mitigations considered:
Returning the error would change the calling signature and may not be appropriate after successful agent output; logging is sufficient.
Severity calibration:
Score 2: silent data loss that only triggers under disk/permission edge cases, but sibling handler shows the expected user-visible error contract.
How can I resolve this? If you propose a fix, please make it concise.
There was a problem hiding this comment.
Fixed in c5d026f — now logs Failed to save session: %v to stderr instead of discarding the error, matching the /save REPL handler's contract.
There was a problem hiding this comment.
Confirmed — the fix in c5d026f addresses the finding correctly. The original code in saveOneShotSession used _ = session.SaveSession(...) which silently discarded errors. The /save REPL handler at internal/repl/repl.go:307-309 checks the error and surfaces it via term.PrintError(fmt.Sprintf("Failed to save: %v", err)). Logging Failed to save session: %v to stderr in the one-shot path now matches that contract. Note: the indexed main branch (at 1249932) still shows two other SaveSession calls in internal/repl/repl.go that discard errors — autoSave() at line 73 (_ = session.SaveSession(...)) and saveAndExit() at line 81 (_ = session.SaveSession(...)). Those are REPL paths where silent failure may be acceptable (auto-save on interrupt, save-and-exit), but they're worth a quick sanity check if you want full consistency.
Matches the /save REPL handler's contract of telling the user when SaveSession fails, per sigilix review on #142.
There was a problem hiding this comment.
Approved — no issues in your changes. 1 note about code outside this PR below.
1 finding outside the diff
| File | Scope | Finding |
|---|---|---|
| main_oneshot_test.go:1 | out-of-diff | TestOneShotSessionPersistsToDisk depends on an unreviewable SetHomeDir helper that may not be concurrency-safe |
Summary
--save-session(and a persistedauto_save=true) only setappConfig.AutoSave; the one-shot dispatch path (-p/ positional-arg mode) never read that flag or calledsession.SaveSession, so scripted/CI single-prompt runs were never persisted regardless of the setting.saveOneShotSession, called after the direct-API and Cerebras branches (the two paths that populateag.History), gated by a smallshouldSaveOneShotSessionhelper.cc/codexCLI backends are intentionally untouched — they manage their own native--resumesession state, so there's nothing for qmax-code's session store to persist there. Documented in README.Test plan
go build ./...go test ./...TestShouldSaveOneShotSession(gate logic) andTestOneShotSessionPersistsToDisk(real save/load round trip against a temp$HOME) inmain_oneshot_test.go