This commit is contained in:
Ian Gulliver
2026-10-14 19:50:19 -07:00
parent 1ea055682b
commit a07a697b17
10 changed files with 215 additions and 94 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
View File
@@ -190,6 +190,37 @@ func (s *Store) Respond(ctx context.Context, actor string, resolutions []Resolut
})
}
func dismissTaskInbox(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, actor string) error {
ids := []string{}
rows, err := tx.QueryContext(ctx, `SELECT id FROM inbox_items WHERE task_id = ? AND state = ?`, taskID, string(api.Open))
if err != nil {
return err
}
for rows.Next() {
id := ""
err = rows.Scan(&id)
if err != nil {
rows.Close()
return err
}
ids = append(ids, id)
}
rows.Close()
for _, id := range ids {
_, err = tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_by = ?, resolved_at = ?, snoozed_until = NULL WHERE id = ?`,
string(api.Dismissed), actor, now(), id)
if err != nil {
return err
}
err = emitItem(ctx, tx, emit, "inbox.updated", id, actor)
if err != nil {
return err
}
}
return nil
}
func (s *Store) AwaitingHuman(ctx context.Context, sessionID string) (int, error) {
n := 0
err := s.db.QueryRowContext(ctx, `
+80
View File
@@ -0,0 +1,80 @@
package store_test
import (
"context"
"path/filepath"
"testing"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/store"
)
func TestClosingTaskDismissesOpenInbox(t *testing.T) {
ctx := context.Background()
events := []api.Event{}
st, err := store.Open(filepath.Join(t.TempDir(), "byre.db"), func(ev api.Event) { events = append(events, ev) })
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() { st.Close() })
now := time.Now().UTC()
_, err = st.EnsureUser(ctx, "alice", "Alice")
if err != nil {
t.Fatalf("ensure user: %v", err)
}
err = st.CreateProject(ctx, api.Project{Id: "p1", Name: "demo", RepoPath: "/nonexistent", DefaultBranch: "main", Remote: "none", CreatedBy: "alice", CreatedAt: now})
if err != nil {
t.Fatalf("create project: %v", err)
}
for _, id := range []string{"t1", "t2"} {
err = st.CreateTask(ctx, api.Task{Id: id, ProjectId: "p1", Title: id, Branch: id, WorktreePath: "/nonexistent", BaseRef: "x", Status: api.TaskStatusActive, CreatedBy: "alice", CreatedAt: now})
if err != nil {
t.Fatalf("create task: %v", err)
}
err = st.CreateSession(ctx, api.AgentSession{Id: "s" + id, TaskId: id, Vendor: api.Claude, VendorSessionId: "s" + id, State: api.AgentSessionStateIdle, Role: api.Implementer, CreatedBy: "alice", CreatedAt: now}, "p1")
if err != nil {
t.Fatalf("create session: %v", err)
}
}
for _, item := range []struct{ id, task string }{{"q1", "t1"}, {"r1", "t1"}, {"q2", "t2"}} {
err = st.CreateInboxItem(ctx, api.InboxItem{Id: item.id, ProjectId: "p1", TaskId: item.task, AgentSessionId: "s" + item.task, ThreadId: item.id,
Kind: api.Question, Priority: api.Blocked, Blocking: true, Payload: map[string]any{}, State: api.Open, CreatedAt: now})
if err != nil {
t.Fatalf("create inbox item: %v", err)
}
}
events = events[:0]
_, err = st.DiscardTask(ctx, "t1", "alice")
if err != nil {
t.Fatalf("discard: %v", err)
}
for _, id := range []string{"q1", "r1"} {
item, err := st.GetInboxItem(ctx, id)
if err != nil {
t.Fatalf("get %s: %v", id, err)
}
if item.State != api.Dismissed || item.ResolvedBy == nil || *item.ResolvedBy != "alice" || item.ResolvedAt == nil {
t.Fatalf("%s not dismissed by alice: %+v", id, item)
}
}
other, err := st.GetInboxItem(ctx, "q2")
if err != nil {
t.Fatalf("get q2: %v", err)
}
if other.State != api.Open {
t.Fatalf("other task's item changed: %+v", other)
}
kinds := []string{}
for _, ev := range events {
kinds = append(kinds, ev.Kind)
}
if len(kinds) != 3 || kinds[0] != "task.discarded" || kinds[1] != "inbox.updated" || kinds[2] != "inbox.updated" {
t.Fatalf("unexpected events %v", kinds)
}
}
+8 -1
View File
@@ -131,6 +131,9 @@ UPDATE inbox_items SET payload = json_object('text', coalesce((SELECT group_conc
ALTER TABLE tasks RENAME COLUMN completed_at TO closed_at;
ALTER TABLE tasks ADD COLUMN merge_commit TEXT NOT NULL DEFAULT '';
UPDATE tasks SET status = 'discarded' WHERE status = 'completed';
`, `
UPDATE inbox_items SET state = 'dismissed', resolved_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), snoozed_until = NULL
WHERE state = 'open' AND task_id IN (SELECT id FROM tasks WHERE status != 'active');
`}
type Store struct {
@@ -345,7 +348,11 @@ func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus,
if err != nil {
return err
}
return emit(api.Event{Kind: kind, ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: payload})
err = emit(api.Event{Kind: kind, ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: payload})
if err != nil {
return err
}
return dismissTaskInbox(ctx, tx, emit, id, actor)
})
if err != nil {
return api.Task{}, err