Feature/session persistence - #407
Conversation
…feature/session-persistence
…n accumulation test
initializ-mk
left a comment
There was a problem hiding this comment.
Review — session persistence + refresh recovery
Nice work on this — it's a real, well-scoped fix for the "refresh loses my message / kills the turn" problem, and the tests are a good regression guard. I traced it end to end; CI is green.
Verified:
- The fix reaches the real path. UI chat goes through
tasks/sendSubscribe→ExecuteStream, which delegates toExecute, so thepersistSessioncalls you added toExecutefire on the streaming flow (not just the direct-Executepath the tests drive). Good — this is the thing that could easily have been a gap. - Storage is safe.
persistSession→MemoryStore.Savesanitizes the task ID (separators can't traverse), writes atomically (temp+fsync+rename) under a mutex — so the new persist-on-cancel call sites can't corrupt or escape. - The detach is sound and mirrors the approach in #402:
context.WithoutCancel(r.Context())+ theclientGonedrain keep the agent turn alive and let it finish + persist after a refresh, bounded by the 10-min timeout. handleSessionStatusis authed (reusesloadAgentToken) and degrades gracefully to{state:"unknown"}.
Three non-blocking notes inline. Also — could you fill in the PR description (what/why + link the refresh bug)? It'll help the maintainers' review.
| } | ||
|
|
||
| // POST to the agent's A2A endpoint. | ||
| agentCtx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), agentCallTimeout) |
There was a problem hiding this comment.
MED–LOW — concurrent turns on the same session can clobber (last-write-wins). Because this detaches the agent call from the request, a refreshed-but-still-running turn keeps going. The resume flow polls status but doesn't lock out sending, so if the user sends again before the old turn settles, two Execute goroutines run on the same task ID and both persistSession to the same file. MemoryStore.Save is mutexed + atomic so the file won't corrupt — but the later Save overwrites the earlier turn's messages (logical loss), since each Execute persists its own mem.
Worth confirming the agent already serializes/rejects a second tasks/send for a task that's still working — if it doesn't, this is reachable. Cheapest UI-side guard: disable send while fetchSessionStatus reports working/submitted (you already expose exactly that signal via the new status endpoint).
| "github.com/initializ/forge/forge-core/auth" | ||
| ) | ||
|
|
||
| const agentCallTimeout = 10 * time.Minute |
There was a problem hiding this comment.
Minor (bound): with the detach, this 10-min timeout becomes the sole upper bound on a UI-initiated turn (a client disconnect no longer ends it), and the handler holds the agent connection/goroutine draining SSE until the agent finishes or this fires. That's bounded and intentional (same trade-off as #402), just worth being a conscious default — if agents legitimately run long tool chains past 10 min, the turn is cut (the persist-on-cancel you added does save the partial state, which is the right mitigation).
Type of Change
Description
General Checklist
go test ./...)gofmt -w)golangci-lint run)go vetreports no issuesSkill Contribution Checklist
forge skills validatepasses with no errorsforge skills auditreports no policy violationsegress_domainslists every domain the skill contacts## Tool:sections with input/output tablesRelated Issues