Let a queued message be taken back into the composer

This commit is contained in:
Ian Gulliver
2026-10-17 15:45:27 -07:00
parent e67576ac53
commit 658f78934c
12 changed files with 218 additions and 37 deletions
+38
View File
@@ -75,7 +75,42 @@ func (m *Manager) Message(ctx context.Context, sessionID, actor, text, model str
return sess, nil
}
func (m *Manager) Withdraw(ctx context.Context, taskID string, inputID int64, actor string) error {
task, err := m.store.GetTask(ctx, taskID)
if err != nil {
return err
}
sessions, err := m.store.ListSessions(ctx, taskID)
if err != nil {
return err
}
if len(sessions) == 0 {
return fmt.Errorf("%w: message %d is not queued", ErrConflict, inputID)
}
sess := sessions[len(sessions)-1]
unlock := m.lockDelivery(queueLockKey(taskID))
defer unlock()
err = m.store.WithdrawTaskInput(ctx, taskID, inputID, task.ProjectId, sess.Id, actor)
if errors.Is(err, store.ErrNotFound) {
return fmt.Errorf("%w: message %d is not queued", ErrConflict, inputID)
}
if err != nil {
return err
}
slog.Info("queued message withdrawn", "session", sess.Id, "task", taskID, "input", inputID, "actor", actor)
return nil
}
func queueLockKey(taskID string) string {
return "task:" + taskID
}
func (m *Manager) deliverQueued(ctx context.Context, sess api.AgentSession) (bool, error) {
unlock := m.lockDelivery(queueLockKey(sess.TaskId))
defer unlock()
inputs, err := m.store.TaskInputs(ctx, sess.TaskId)
if err != nil || len(inputs) == 0 {
return false, err
@@ -131,6 +166,9 @@ func (m *Manager) takeQueuedMidTurn(ctx context.Context, l *live) []store.TaskIn
return nil
}
unlock := m.lockDelivery(queueLockKey(l.taskID))
defer unlock()
inputs, err := m.store.TaskInputs(ctx, l.taskID)
if err != nil {
slog.Error("list queued messages", "session", l.id, "err", err)
+60 -19
View File
@@ -1624,6 +1624,9 @@ type ServerInterface interface {
// (GET /api/tasks/{taskId}/previews)
ListPreviews(w http.ResponseWriter, r *http.Request, taskId string)
// (DELETE /api/tasks/{taskId}/queue/{inputId})
WithdrawMessage(w http.ResponseWriter, r *http.Request, taskId string, inputId int64)
// (GET /api/tasks/{taskId}/reviews)
ListReviews(w http.ResponseWriter, r *http.Request, taskId string)
@@ -3008,6 +3011,41 @@ func (siw *ServerInterfaceWrapper) ListPreviews(w http.ResponseWriter, r *http.R
handler.ServeHTTP(w, r)
}
// WithdrawMessage operation middleware
func (siw *ServerInterfaceWrapper) WithdrawMessage(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "taskId" -------------
var taskId string
err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err})
return
}
// ------------- Path parameter "inputId" -------------
var inputId int64
err = runtime.BindStyledParameterWithOptions("simple", "inputId", r.PathValue("inputId"), &inputId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "integer", Format: "int64", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "inputId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.WithdrawMessage(w, r, taskId, inputId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// ListReviews operation middleware
func (siw *ServerInterfaceWrapper) ListReviews(w http.ResponseWriter, r *http.Request) {
@@ -3562,6 +3600,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/snapshots/{n}/fork", wrapper.ForkSnapshot)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/sessions", wrapper.ListSessions)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/sessions", wrapper.CreateSession)
m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/queue/{inputId}", wrapper.WithdrawMessage)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/sessions/{sessionId}", wrapper.GetSession)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/sessions/{sessionId}/messages", wrapper.SendMessage)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/sessions/{sessionId}/compact", wrapper.CompactSession)
@@ -3798,25 +3837,27 @@ var swaggerSpec = []string{
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+9
View File
@@ -779,6 +779,15 @@ func (s *Server) SendMessage(w http.ResponseWriter, r *http.Request, sessionID s
writeJSON(w, http.StatusAccepted, sess)
}
func (s *Server) WithdrawMessage(w http.ResponseWriter, r *http.Request, taskID string, inputID int64) {
err := s.agents.Withdraw(r.Context(), taskID, inputID, actor(r))
if err != nil {
writeAgentError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) CompactSession(w http.ResponseWriter, r *http.Request, sessionID string) {
sess, err := s.agents.Compact(r.Context(), sessionID)
if err != nil {
+22 -1
View File
@@ -26,15 +26,36 @@ func (s *Store) AddTaskInput(ctx context.Context, in TaskInput, projectID, sessi
payload["images"] = in.Images
}
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
_, err := tx.ExecContext(ctx, `INSERT INTO task_inputs (task_id, actor, text, images, created_at) VALUES (?, ?, ?, ?, ?)`,
res, err := tx.ExecContext(ctx, `INSERT INTO task_inputs (task_id, actor, text, images, created_at) VALUES (?, ?, ?, ?, ?)`,
in.TaskID, in.Actor, in.Text, string(images), now())
if err != nil {
return err
}
payload["id"], err = res.LastInsertId()
if err != nil {
return err
}
return emit(sessionEvent("user.queued", projectID, in.TaskID, "", sessionID, in.Actor, payload))
})
}
func (s *Store) WithdrawTaskInput(ctx context.Context, taskID string, id int64, projectID, sessionID, actor string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
res, err := tx.ExecContext(ctx, `DELETE FROM task_inputs WHERE id = ? AND task_id = ?`, id, taskID)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return ErrNotFound
}
return emit(sessionEvent("user.unqueued", projectID, taskID, "", sessionID, actor, map[string]any{"id": id}))
})
}
func (s *Store) TaskInputs(ctx context.Context, taskID string) ([]TaskInput, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id, task_id, actor, text, images, created_at FROM task_inputs WHERE task_id = ? ORDER BY id`, taskID)
if err != nil {