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:
weichen
2026-09-23 13:57:08 +00:00
69 changed files with 3512 additions and 514 deletions
+46 -1
View File
@@ -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
}
+46
View File
@@ -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)
}
}
+28 -3
View File
@@ -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
}