Files
Agent-System/internal/ctxmgr/message_update.go
weichenandCursor 7be873073c feat(taskrun): finalize RunManifest in one unit of work
CAS the unique terminal status with the assistant turn, frozen manifest, envelope and FAV staging, and join ctxmgr writes through the same UoW instead of a process mutex.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 02:05:58 +00:00

129 lines
3.1 KiB
Go

package ctxmgr
import (
"context"
"encoding/json"
"errors"
"fmt"
"chat-agent-system/internal/app/uow"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
// UpdateMessageMetadata 更新已持久化助手消息的 metadata 与任务状态。
func (m *ContextManager) UpdateMessageMetadata(ctx context.Context, messageID int64, meta map[string]any, taskStatus string) error {
if messageID <= 0 {
return errors.New("invalid message_id")
}
if meta == nil {
meta = map[string]any{}
}
if taskStatus != "" {
meta["task_status"] = taskStatus
}
if m.pool != nil || hasUoW(ctx) {
metaJSON, _ := json.Marshal(meta)
_, err := execChat(ctx, m.pool, `
UPDATE chat_messages
SET metadata = COALESCE(metadata, '{}'::jsonb) || $1::jsonb
WHERE id = $2
`, metaJSON, messageID)
if err != nil {
return fmt.Errorf("update message metadata failed: %w", err)
}
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
for sid, msgs := range m.messages {
for i := range msgs {
if msgs[i].ID == messageID {
if msgs[i].Metadata == nil {
msgs[i].Metadata = map[string]any{}
}
for k, v := range meta {
msgs[i].Metadata[k] = v
}
m.messages[sid][i] = msgs[i]
return nil
}
}
}
return errors.New("message not found")
}
// FinalizeAssistantTaskMessage 替换助手任务占位正文并写入完整 metadata(含 task_status)。
func (m *ContextManager) FinalizeAssistantTaskMessage(ctx context.Context, messageID int64, content string, meta map[string]any, taskStatus string) error {
if messageID <= 0 {
return errors.New("invalid message_id")
}
if meta == nil {
meta = map[string]any{}
}
if taskStatus != "" {
meta["task_status"] = taskStatus
}
if m.pool != nil || hasUoW(ctx) {
metaJSON, err := json.Marshal(meta)
if err != nil {
return fmt.Errorf("marshal metadata failed: %w", err)
}
tag, err := execChat(ctx, m.pool, `
UPDATE chat_messages
SET content = $1,
metadata = COALESCE(metadata, '{}'::jsonb) || $2::jsonb
WHERE id = $3
`, content, metaJSON, messageID)
if err != nil {
return fmt.Errorf("finalize assistant task message failed: %w", err)
}
if tag.RowsAffected() == 0 {
return errors.New("message not found")
}
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
for sid, msgs := range m.messages {
for i := range msgs {
if msgs[i].ID == messageID {
msgs[i].Content = content
if msgs[i].Metadata == nil {
msgs[i].Metadata = map[string]any{}
}
for k, v := range meta {
msgs[i].Metadata[k] = v
}
m.messages[sid][i] = msgs[i]
return nil
}
}
}
return errors.New("message not found")
}
func hasUoW(ctx context.Context) bool {
_, ok := uow.ScopeFrom(ctx)
return ok
}
func execChat(ctx context.Context, pool *pgxpool.Pool, q string, args ...any) (pgconn.CommandTag, error) {
if scope, ok := uow.ScopeFrom(ctx); ok {
tx, err := uow.AsPG(scope)
if err != nil {
return pgconn.CommandTag{}, err
}
return tx.Exec(ctx, q, args...)
}
if pool == nil {
return pgconn.CommandTag{}, errors.New("ctxmgr: database required")
}
return pool.Exec(ctx, q, args...)
}