feat(agent): event-driven uplink — sync on every state transition

The agent reported its state only on the adaptive sync tick (3s watching /
10s idle), so a resolving→downloading→verifying→organizing→completed
transition could lag up to a full interval before the server (and the web
UI) saw it. Now every successful Task.Transition fires an onChange hook
wired to TriggerSync, pushing the new state immediately. Bursts are safe:
TriggerSync is a buffered-1 send, so clustered transitions coalesce into
one sync.

- Task gains an onChange hook fired AFTER the status mutex is released
  (so a future heavier hook can't deadlock on task.mu); nil is a no-op.
- Manager.OnStateChange is set on each task at Submit; the daemon wires it
  to TriggerSync alongside the existing OnTaskDone.
- Stream tasks transition outside the Manager, so handleStreamTask wires
  the same hook explicitly (gap found in review) — resolving/downloading/
  completed/failed on the stream path now push too.

The adaptive ticker stays as a reconciliation heartbeat; it's just no
longer the latency floor for state changes.
This commit is contained in:
Deivid Soto 2026-06-01 19:09:44 +02:00
parent 1052529ca2
commit 864b6ea832
5 changed files with 83 additions and 4 deletions

View file

@ -87,7 +87,7 @@ func cancelStreamTask(taskID string) {
// handleStreamTask manages a streaming task lifecycle for active torrent downloads.
// It creates a StreamEngine, buffers, sets the file on the persistent server,
// and reports progress until the task is cancelled or the download completes.
func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine.ProgressReporter, cfg config.Config, agentClient *agent.Client, srv *engine.StreamServer) {
func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine.ProgressReporter, cfg config.Config, agentClient *agent.Client, srv *engine.StreamServer, onStateChange func()) {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
@ -106,6 +106,10 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
}()
task := engine.NewTaskFromAgent(at)
// Event-driven uplink: stream tasks transition outside the Manager (which
// wires this for downloads), so set it here too — resolving/downloading/
// completed/failed get pushed to the server immediately.
task.SetOnChange(onStateChange)
task.ResolvedMethod = engine.MethodTorrent
reporter.Track(task)
defer reporter.ReportFinal(context.Background(), task)