Fetch today's tasks asynchronously on entering planning
The controller fetches the Marvin task list when entering planning, mirroring the planning coach: generation-guarded goroutine, status enum (idle/pending/ready/error), projected into State only while planning and only when a provider is set. nil provider degrades to no tasks view. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"antidrift/internal/evidence"
|
||||
"antidrift/internal/statemachine"
|
||||
"antidrift/internal/store"
|
||||
"antidrift/internal/tasks"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -37,6 +38,15 @@ const (
|
||||
coachError = "error"
|
||||
)
|
||||
|
||||
const tasksTimeout = 30 * time.Second
|
||||
|
||||
const (
|
||||
tasksIdle = "idle"
|
||||
tasksPending = "pending"
|
||||
tasksReady = "ready"
|
||||
tasksError = "error"
|
||||
)
|
||||
|
||||
const (
|
||||
driftDebounce = 10 * time.Second
|
||||
driftTimeout = 30 * time.Second
|
||||
@@ -92,6 +102,11 @@ type Controller struct {
|
||||
coachErr string
|
||||
coachGen int
|
||||
|
||||
tasksProvider tasks.Provider
|
||||
tasksStatus string
|
||||
tasksList []tasks.Task
|
||||
tasksGen int
|
||||
|
||||
allowedClasses []string // durable: the active session's allowed window classes
|
||||
judge ai.DriftJudge
|
||||
driftStatus string
|
||||
@@ -139,6 +154,19 @@ type CoachView struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TaskView is one to-do item in the planning tasks list.
|
||||
type TaskView struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Day string `json:"day,omitempty"`
|
||||
}
|
||||
|
||||
// TasksView projects the ephemeral planning tasks state (Marvin's today list).
|
||||
type TasksView struct {
|
||||
Status string `json:"status"`
|
||||
Tasks []TaskView `json:"tasks,omitempty"`
|
||||
}
|
||||
|
||||
// WindowView / BucketView / EvidenceView are the evidence projection.
|
||||
type WindowView struct {
|
||||
Class string `json:"class"`
|
||||
@@ -165,6 +193,7 @@ type State struct {
|
||||
Commitment *CommitmentView `json:"commitment"`
|
||||
Evidence *EvidenceView `json:"evidence"`
|
||||
Coach *CoachView `json:"coach,omitempty"`
|
||||
Tasks *TasksView `json:"tasks,omitempty"`
|
||||
Drift *DriftView `json:"drift,omitempty"`
|
||||
}
|
||||
|
||||
@@ -279,6 +308,17 @@ func (c *Controller) stateLocked() State {
|
||||
}
|
||||
}
|
||||
st.Coach = cv
|
||||
if c.tasksProvider != nil {
|
||||
tstatus := c.tasksStatus
|
||||
if tstatus == "" {
|
||||
tstatus = tasksIdle
|
||||
}
|
||||
tv := &TasksView{Status: tstatus}
|
||||
for _, t := range c.tasksList {
|
||||
tv.Tasks = append(tv.Tasks, TaskView{ID: t.ID, Title: t.Title, Day: t.Day})
|
||||
}
|
||||
st.Tasks = tv
|
||||
}
|
||||
}
|
||||
if c.runtimeState == domain.RuntimeActive {
|
||||
status := c.driftStatus
|
||||
@@ -325,6 +365,7 @@ func (c *Controller) EnterPlanning() error {
|
||||
}
|
||||
c.runtimeState = next
|
||||
c.resetCoachLocked()
|
||||
c.startTasksFetchLocked()
|
||||
return c.persistLocked()
|
||||
}
|
||||
|
||||
@@ -345,6 +386,49 @@ func (c *Controller) resetCoachLocked() {
|
||||
c.coachGen++
|
||||
}
|
||||
|
||||
// SetTasks injects the Tasks provider. Mirrors SetCoach. A nil provider keeps
|
||||
// the planning tasks list absent.
|
||||
func (c *Controller) SetTasks(p tasks.Provider) {
|
||||
c.mu.Lock()
|
||||
c.tasksProvider = p
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// startTasksFetchLocked kicks off an asynchronous Today() fetch when a provider
|
||||
// is set. Mirrors RequestCoach: generation-guarded, discards stale or
|
||||
// post-planning results, and notifies on completion. Caller holds mu.
|
||||
func (c *Controller) startTasksFetchLocked() {
|
||||
c.tasksList = nil
|
||||
if c.tasksProvider == nil {
|
||||
c.tasksStatus = tasksIdle
|
||||
return
|
||||
}
|
||||
c.tasksGen++
|
||||
gen := c.tasksGen
|
||||
c.tasksStatus = tasksPending
|
||||
provider := c.tasksProvider
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tasksTimeout)
|
||||
defer cancel()
|
||||
list, err := provider.Today(ctx)
|
||||
|
||||
c.mu.Lock()
|
||||
if gen != c.tasksGen || c.runtimeState != domain.RuntimePlanning {
|
||||
c.mu.Unlock()
|
||||
return // stale or left planning: discard
|
||||
}
|
||||
if err != nil {
|
||||
c.tasksStatus = tasksError
|
||||
c.tasksList = nil
|
||||
} else {
|
||||
c.tasksStatus = tasksReady
|
||||
c.tasksList = list
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.notify()
|
||||
}()
|
||||
}
|
||||
|
||||
// AllowedClassesForTest exposes the session allowed classes for tests.
|
||||
func (c *Controller) AllowedClassesForTest() []string {
|
||||
c.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user