merge: integrate final-artifact-vault into local_dev (authorized exception)
User authorized lifting CURRENT FAV whole-branch merge protection on 2026-09-23. Keep FAV_DEGRADED_READ/FAV_PUBLISHER_ENABLED default false. Drop duplicate goose 00034 (local_dev already has 00036). Preserve agentx workspace admin routes and pin-export wiring.
This commit is contained in:
@@ -164,7 +164,6 @@ func (m *ContextManager) acquireChatTurnDB(
|
||||
return &AcquireTurnResult{Record: rec, WonLease: true}, nil
|
||||
}
|
||||
|
||||
|
||||
const maxMemTurns = 2000
|
||||
const memTurnTTL = 2 * time.Hour
|
||||
|
||||
@@ -352,3 +351,49 @@ func (m *ContextManager) GetChatTurn(ctx context.Context, sessionID, clientTurnI
|
||||
cp := *rec
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
// GetChatTurnByAssistantMessageID 按会话 + 助手消息定位回合;用于 copy_only 显式 pin。
|
||||
func (m *ContextManager) GetChatTurnByAssistantMessageID(ctx context.Context, sessionID string, assistantMessageID int64) (*ChatTurnRecord, error) {
|
||||
if sessionID == "" || assistantMessageID <= 0 {
|
||||
return nil, ErrChatTurnNotFound
|
||||
}
|
||||
if m.pool != nil {
|
||||
var rec ChatTurnRecord
|
||||
var completedAt *time.Time
|
||||
var leaseExpiresAt *time.Time
|
||||
err := m.pool.QueryRow(ctx, `
|
||||
SELECT id, session_id, client_turn_id,
|
||||
COALESCE(user_id, 0), COALESCE(project_id, 0),
|
||||
COALESCE(user_message_id, 0), COALESCE(assistant_message_id, 0),
|
||||
request_body_sha256, status,
|
||||
COALESCE(first_request_id, ''), COALESCE(last_request_id, ''),
|
||||
attempt_count, created_at, updated_at, completed_at, lease_expires_at
|
||||
FROM chat_turns
|
||||
WHERE session_id = $1 AND assistant_message_id = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
`, sessionID, assistantMessageID).Scan(
|
||||
&rec.ID, &rec.SessionID, &rec.ClientTurnID,
|
||||
&rec.UserID, &rec.ProjectID,
|
||||
&rec.UserMessageID, &rec.AssistantMessageID,
|
||||
&rec.RequestBodySHA256, &rec.Status,
|
||||
&rec.FirstRequestID, &rec.LastRequestID,
|
||||
&rec.AttemptCount, &rec.CreatedAt, &rec.UpdatedAt, &completedAt, &leaseExpiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, ErrChatTurnNotFound
|
||||
}
|
||||
rec.CompletedAt = completedAt
|
||||
rec.LeaseExpiresAt = leaseExpiresAt
|
||||
return &rec, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, rec := range m.turns {
|
||||
if rec != nil && rec.SessionID == sessionID && rec.AssistantMessageID == assistantMessageID {
|
||||
cp := *rec
|
||||
return &cp, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrChatTurnNotFound
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package ctxmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/replyexport"
|
||||
)
|
||||
|
||||
func TestFinalizeAssistantTaskMessageRecalculatesExportColumns(t *testing.T) {
|
||||
m := NewContextManager(nil, nil, TimeDecayConfig{})
|
||||
ctx := context.Background()
|
||||
id, err := m.RecordUserMessageWithID(ctx, "sess-fav", 1, "assistant", "占位", "", "", map[string]any{"task_status": "pending"})
|
||||
if err != nil || id <= 0 {
|
||||
t.Fatalf("record: %v %d", err, id)
|
||||
}
|
||||
body := "# 完整助手终态正文\n\n这是一段足够长的结构化说明,用于重算 export_policy。"
|
||||
if err := m.FinalizeAssistantTaskMessage(ctx, id, body, map[string]any{}, "completed"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msg, err := m.GetMessageByID(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.ExportPolicy != string(replyexport.PolicyFull) {
|
||||
t.Fatalf("export_policy=%q", msg.ExportPolicy)
|
||||
}
|
||||
if msg.ContentSha256 == "" || msg.Content != body {
|
||||
t.Fatalf("sha/body missing: %+v", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteChatTurnAfterFinalize(t *testing.T) {
|
||||
m := NewContextManager(nil, nil, TimeDecayConfig{})
|
||||
ctx := context.Background()
|
||||
if _, err := m.AcquireChatTurn(ctx, "sess-fav", "turn-1", "abc", "req-1", 1, 9); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.CompleteChatTurn(ctx, "sess-fav", "turn-1", 42); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec, err := m.GetChatTurn(ctx, "sess-fav", "turn-1")
|
||||
if err != nil || rec.Status != TurnStatusCompleted || rec.AssistantMessageID != 42 || rec.CompletedAt == nil {
|
||||
t.Fatalf("turn not completed: %+v %v", rec, err)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"chat-agent-system/internal/app/uow"
|
||||
"chat-agent-system/internal/replyexport"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -68,6 +69,16 @@ func (m *ContextManager) FinalizeAssistantTaskMessage(ctx context.Context, messa
|
||||
meta["task_status"] = taskStatus
|
||||
}
|
||||
|
||||
kind, cap := replyexport.EvaluatePolicy(replyexport.PolicyOptions{
|
||||
Role: "assistant",
|
||||
Status: nzStatus(taskStatus),
|
||||
Content: content,
|
||||
})
|
||||
meta["content_kind"] = string(kind)
|
||||
meta["export_policy"] = string(cap.Policy)
|
||||
meta["content_revision"] = cap.ContentRevision
|
||||
meta["content_sha256"] = cap.ContentSha256
|
||||
|
||||
if m.pool != nil || hasUoW(ctx) {
|
||||
metaJSON, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
@@ -76,9 +87,12 @@ func (m *ContextManager) FinalizeAssistantTaskMessage(ctx context.Context, messa
|
||||
tag, err := execChat(ctx, m.pool, `
|
||||
UPDATE chat_messages
|
||||
SET content = $1,
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || $2::jsonb
|
||||
WHERE id = $3
|
||||
`, content, metaJSON, messageID)
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || $2::jsonb,
|
||||
export_policy = $3,
|
||||
content_sha256 = $4,
|
||||
content_kind = $5
|
||||
WHERE id = $6
|
||||
`, content, metaJSON, string(cap.Policy), cap.ContentSha256, string(kind), messageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finalize assistant task message failed: %w", err)
|
||||
}
|
||||
@@ -100,6 +114,10 @@ func (m *ContextManager) FinalizeAssistantTaskMessage(ctx context.Context, messa
|
||||
for k, v := range meta {
|
||||
msgs[i].Metadata[k] = v
|
||||
}
|
||||
msgs[i].ExportPolicy = string(cap.Policy)
|
||||
msgs[i].ContentSha256 = cap.ContentSha256
|
||||
msgs[i].ContentKind = string(kind)
|
||||
msgs[i].ContentRevision = cap.ContentRevision
|
||||
m.messages[sid][i] = msgs[i]
|
||||
return nil
|
||||
}
|
||||
@@ -168,3 +186,10 @@ func execChat(ctx context.Context, pool *pgxpool.Pool, q string, args ...any) (p
|
||||
}
|
||||
return pool.Exec(ctx, q, args...)
|
||||
}
|
||||
|
||||
func nzStatus(taskStatus string) string {
|
||||
if taskStatus == "" {
|
||||
return "completed"
|
||||
}
|
||||
return taskStatus
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user