agent-exec transport 9b5caca2968f510a4f81402ca7dfdb4c/f6d16260d971
governance / governance (pull_request) Successful in 20s
ci / vet-and-test (pull_request) Successful in 1m27s

This commit is contained in:
agent-exec-transport
2026-10-20 03:51:12 +00:00
parent 8050c6b289
commit 60f37adc04
8 changed files with 477 additions and 51 deletions
+119
View File
@@ -147,6 +147,28 @@ func TestReconcileInterruptedSealsOpenRuns(t *testing.T) {
if failed != 3 { if failed != 3 {
t.Fatalf("run.failed events: %d", failed) t.Fatalf("run.failed events: %d", failed)
} }
msgs, err := st.ListMessages(conv.ID)
if err != nil {
t.Fatal(err)
}
asstByRun := map[string]int{}
for _, m := range msgs {
if m.Role == core.RoleAssistant {
asstByRun[m.RunID]++
if len(m.Parts) != 1 || m.Parts[0].Type != core.PartError || m.Parts[0].Err == nil || m.Parts[0].Err.Code != "run.interrupted" || m.Parts[0].Err.Message != "进程重启或停机,运行已中断" {
t.Fatalf("unexpected assistant message parts for run %s: %+v", m.RunID, m.Parts)
}
}
}
for _, id := range []string{pending.ID, running.ID, waiting.ID} {
if asstByRun[id] != 1 {
t.Fatalf("run %s: expected 1 assistant message, got %d", id, asstByRun[id])
}
}
if asstByRun[done.ID] != 0 {
t.Fatalf("terminal run %s: expected 0 assistant messages, got %d", done.ID, asstByRun[done.ID])
}
if err := mgr.ReconcileInterrupted(); err != nil { if err := mgr.ReconcileInterrupted(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -157,6 +179,103 @@ func TestReconcileInterruptedSealsOpenRuns(t *testing.T) {
if len(again) != len(evs) { if len(again) != len(evs) {
t.Fatalf("second reconcile emitted more events: %d -> %d", len(evs), len(again)) t.Fatalf("second reconcile emitted more events: %d -> %d", len(evs), len(again))
} }
msgsAgain, err := st.ListMessages(conv.ID)
if err != nil {
t.Fatal(err)
}
if len(msgsAgain) != len(msgs) {
t.Fatalf("second reconcile added duplicate messages: %d -> %d", len(msgs), len(msgsAgain))
}
}
func TestReconcileInterruptedAssistantNotice(t *testing.T) {
st := openRecoveryStore(t)
st.SetInstanceID("this")
mgr := newRecoveryManager(t, st)
mgr.SetInstanceID("this")
// 1. Conversation with an open run that gets sealed
conv1 := recoveryConversation(t, st)
userMsg1 := core.Message{ConversationID: conv1.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "hello"}}}
if err := st.AppendMessage(&userMsg1); err != nil {
t.Fatal(err)
}
sealedRun := recoveryRun(t, st, conv1, core.RunRunning)
// 2. Conversation with an already terminal run
conv2 := recoveryConversation(t, st)
userMsg2 := core.Message{ConversationID: conv2.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "hi"}}}
if err := st.AppendMessage(&userMsg2); err != nil {
t.Fatal(err)
}
terminalRun := recoveryRun(t, st, conv2, core.RunCompleted)
// 3. Conversation with an open run that already has an assistant message
conv3 := recoveryConversation(t, st)
userMsg3 := core.Message{ConversationID: conv3.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "test"}}}
if err := st.AppendMessage(&userMsg3); err != nil {
t.Fatal(err)
}
openRunWithMsg := recoveryRun(t, st, conv3, core.RunRunning)
asstMsg3 := core.Message{ConversationID: conv3.ID, RunID: openRunWithMsg.ID, Role: core.RoleAssistant, Parts: []core.Part{{Type: core.PartText, Text: "already answered"}}}
if err := st.AppendMessage(&asstMsg3); err != nil {
t.Fatal(err)
}
if err := mgr.ReconcileInterrupted(); err != nil {
t.Fatal(err)
}
// conv1: history contains exactly one assistant message with run.interrupted
msgs1, err := st.ListMessages(conv1.ID)
if err != nil {
t.Fatal(err)
}
var asst1 []core.Message
for _, m := range msgs1 {
if m.Role == core.RoleAssistant {
asst1 = append(asst1, m)
}
}
if len(asst1) != 1 {
t.Fatalf("conv1: expected exactly 1 assistant message, got %d", len(asst1))
}
if asst1[0].RunID != sealedRun.ID {
t.Fatalf("conv1: expected assistant message for run %s, got %s", sealedRun.ID, asst1[0].RunID)
}
if len(asst1[0].Parts) != 1 || asst1[0].Parts[0].Type != core.PartError || asst1[0].Parts[0].Err == nil || asst1[0].Parts[0].Err.Code != "run.interrupted" || asst1[0].Parts[0].Err.Message != "进程重启或停机,运行已中断" {
t.Fatalf("conv1: unexpected parts: %+v", asst1[0].Parts)
}
// conv2: already-terminal run must have NO assistant message written
kept, err := st.GetRunByID(terminalRun.ID)
if err != nil || kept.Status != core.RunCompleted {
t.Fatalf("terminal run changed: %+v %v", kept, err)
}
msgs2, err := st.ListMessages(conv2.ID)
if err != nil {
t.Fatal(err)
}
for _, m := range msgs2 {
if m.Role == core.RoleAssistant {
t.Fatalf("conv2: unexpected assistant message for already-terminal run: %+v", m)
}
}
// conv3: run had an assistant message already, so no duplicate was created
msgs3, err := st.ListMessages(conv3.ID)
if err != nil {
t.Fatal(err)
}
var asst3 []core.Message
for _, m := range msgs3 {
if m.Role == core.RoleAssistant {
asst3 = append(asst3, m)
}
}
if len(asst3) != 1 || asst3[0].ID != asstMsg3.ID {
t.Fatalf("conv3: expected only original assistant message, got %+v", asst3)
}
} }
func TestReconcileLeavesOtherInstanceOnPostgres(t *testing.T) { func TestReconcileLeavesOtherInstanceOnPostgres(t *testing.T) {
+20 -4
View File
@@ -805,12 +805,20 @@ func (s *Store) searchKnowledgeFTSCandidates(pred knowledgePred, q core.Retrieva
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
out := []core.RetrievalHit{} var rawHits []core.RetrievalHit
for rows.Next() { for rows.Next() {
h, err := scanKnowledgeHit(rows) h, err := scanKnowledgeHit(rows)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rawHits = append(rawHits, h)
}
if err := rows.Err(); err != nil {
return nil, err
}
_ = rows.Close()
out := []core.RetrievalHit{}
for _, h := range rawHits {
if !pred(h.Doc) { if !pred(h.Doc) {
continue continue
} }
@@ -819,7 +827,7 @@ func (s *Store) searchKnowledgeFTSCandidates(pred knowledgePred, q core.Retrieva
break break
} }
} }
return out, rows.Err() return out, nil
} }
func (s *Store) searchKnowledgeLikeShared(requester string, q core.RetrievalQuery, pred knowledgePred) ([]core.RetrievalHit, error) { func (s *Store) searchKnowledgeLikeShared(requester string, q core.RetrievalQuery, pred knowledgePred) ([]core.RetrievalHit, error) {
@@ -842,7 +850,7 @@ func (s *Store) searchKnowledgeLikeSharedCandidates(requester string, q core.Ret
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
out := []core.RetrievalHit{} var rawDocs []core.KnowledgeDoc
for rows.Next() { for rows.Next() {
var d core.KnowledgeDoc var d core.KnowledgeDoc
var own, ca string var own, ca string
@@ -851,6 +859,14 @@ func (s *Store) searchKnowledgeLikeSharedCandidates(requester string, q core.Ret
} }
d.Owner = own d.Owner = own
d.CreatedAt, _ = parseTime(ca) d.CreatedAt, _ = parseTime(ca)
rawDocs = append(rawDocs, d)
}
if err := rows.Err(); err != nil {
return nil, err
}
_ = rows.Close()
out := []core.RetrievalHit{}
for _, d := range rawDocs {
if !pred(d) { if !pred(d) {
continue continue
} }
@@ -859,7 +875,7 @@ func (s *Store) searchKnowledgeLikeSharedCandidates(requester string, q core.Ret
break break
} }
} }
return out, rows.Err() return out, nil
} }
func (s *Store) searchKnowledgeLike(owner string, q core.RetrievalQuery, topK int) ([]core.RetrievalHit, error) { func (s *Store) searchKnowledgeLike(owner string, q core.RetrievalQuery, topK int) ([]core.RetrievalHit, error) {
+7 -2
View File
@@ -210,9 +210,14 @@ func (s *Store) CreateRun(r *core.Run) error {
if r.ID == "" { if r.ID == "" {
r.ID = newID("run") r.ID = newID("run")
} }
r.CreatedAt = r.CreatedAt.UTC() if r.CreatedAt.IsZero() {
r.CreatedAt = time.Now().UTC()
} else {
r.CreatedAt = r.CreatedAt.UTC()
}
ca := r.CreatedAt.Format(time.RFC3339Nano)
_, err := s.db.Exec(s.q(`INSERT INTO runs(id,parent_run_id,conversation_id,project_id,owner,kind,profile_id,status,phase,agent_name,created_at,instance_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`), _, err := s.db.Exec(s.q(`INSERT INTO runs(id,parent_run_id,conversation_id,project_id,owner,kind,profile_id,status,phase,agent_name,created_at,instance_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`),
r.ID, r.ParentRunID, r.ConversationID, r.ProjectID, r.Owner, string(r.Kind), r.ProfileID, string(r.Status), string(r.Phase), r.AgentName, nowISO(), s.instanceID) r.ID, r.ParentRunID, r.ConversationID, r.ProjectID, r.Owner, string(r.Kind), r.ProfileID, string(r.Status), string(r.Phase), r.AgentName, ca, s.instanceID)
return err return err
} }
+98 -30
View File
@@ -86,15 +86,70 @@ func (s *Store) ListRecoverableRuns(instanceID string, all bool) ([]core.Run, er
// SealInterrupted 把仍在进行的 run 收成 interrupted。 // SealInterrupted 把仍在进行的 run 收成 interrupted。
// 条件更新保证并发对账时只有一个赢家;已经是终态的行不受影响。 // 条件更新保证并发对账时只有一个赢家;已经是终态的行不受影响。
// 赢家并在同一事务中为该 run 写入一条助手中断错误消息(若尚无助手消息)。
func (s *Store) SealInterrupted(id string) (bool, error) { func (s *Store) SealInterrupted(id string) (bool, error) {
errPart := marshalJSON(&core.ErrorPart{Code: "run.interrupted", Message: "进程重启或停机,运行已中断"}) tx, err := s.db.Begin()
res, err := s.db.Exec(s.q(`UPDATE runs SET status=?, phase=?, error=?, ended_at=? WHERE id=? AND status IN ('pending','running','waiting_hitl')`), if err != nil {
string(core.RunInterrupted), string(core.PhaseDone), errPart, nowISO(), id) return false, err
}
defer tx.Rollback()
var convID string
err = tx.QueryRow(s.q(`SELECT conversation_id FROM runs WHERE id=?`), id).Scan(&convID)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
} else if err != nil {
return false, err
}
if convID != "" {
if err := s.dialect.lockConversation(tx, convID); err != nil {
return false, err
}
}
errPartObj := &core.ErrorPart{Code: "run.interrupted", Message: "进程重启或停机,运行已中断"}
errPartJSON := marshalJSON(errPartObj)
res, err := tx.Exec(s.q(`UPDATE runs SET status=?, phase=?, error=?, ended_at=? WHERE id=? AND status IN ('pending','running','waiting_hitl')`),
string(core.RunInterrupted), string(core.PhaseDone), errPartJSON, nowISO(), id)
if err != nil { if err != nil {
return false, err return false, err
} }
n, err := res.RowsAffected() n, err := res.RowsAffected()
return n == 1, err if err != nil {
return false, err
}
if n != 1 {
return false, nil
}
var count int
if err := tx.QueryRow(s.q(`SELECT COUNT(*) FROM messages WHERE run_id=? AND role=?`), id, string(core.RoleAssistant)).Scan(&count); err != nil {
return false, err
}
if count == 0 && convID != "" {
msg := &core.Message{
ID: newID("msg"),
ConversationID: convID,
RunID: id,
Role: core.RoleAssistant,
Parts: []core.Part{{Type: core.PartError, Err: errPartObj}},
CreatedAt: time.Now().UTC(),
}
var variantGroup string
var variantIndex int
if err := tx.QueryRow(s.q(`SELECT variant_group, variant_index FROM regen_pending WHERE conversation_id=?`), convID).Scan(&variantGroup, &variantIndex); err == nil && variantGroup != "" {
msg.VariantGroup = variantGroup
msg.VariantIndex = variantIndex
}
if err := s.insertSettledMessageTx(tx, msg); err != nil {
return false, err
}
}
if err := tx.Commit(); err != nil {
return false, err
}
return true, nil
} }
// ActiveHITLCardsByRun 返回某个 run 上尚未关闭的 HITL 卡片。 // ActiveHITLCardsByRun 返回某个 run 上尚未关闭的 HITL 卡片。
@@ -256,32 +311,7 @@ func (s *Store) settleRunOnce(r *core.Run, msg *core.Message, fence *fenceRef, a
return false, false, err return false, false, err
} }
if msg != nil { if msg != nil {
if msg.CreatedAt.IsZero() { if err := s.insertSettledMessageTx(tx, msg); err != nil {
msg.CreatedAt = time.Now().UTC()
}
insert := `INSERT INTO messages(id,conversation_id,run_id,role,parts,seq,created_at,request_id)
SELECT ?,?,?,?,?,COALESCE(MAX(seq),0)+1,?,? FROM messages WHERE conversation_id=?`
if _, err := tx.Exec(s.q(insert), msg.ID, msg.ConversationID, msg.RunID, string(msg.Role), marshalJSON(msg.Parts), msg.CreatedAt.UTC().Format(time.RFC3339Nano), "", msg.ConversationID); err != nil {
return false, false, err
}
if err := s.appendProjectMessageObservationsTx(tx, msg); err != nil {
return false, false, err
}
if msg.VariantGroup != "" || msg.Excluded {
ex := 0
if msg.Excluded {
ex = 1
}
if _, err := tx.Exec(s.q(`UPDATE messages SET excluded=?, variant_group=?, variant_index=? WHERE id=?`), ex, msg.VariantGroup, msg.VariantIndex, msg.ID); err != nil {
return false, false, err
}
}
if msg.VariantGroup != "" {
if _, err := tx.Exec(s.q(`DELETE FROM regen_pending WHERE conversation_id=? AND variant_group=? AND variant_index=?`), msg.ConversationID, msg.VariantGroup, msg.VariantIndex); err != nil {
return false, false, err
}
}
if _, err := tx.Exec(s.q(`UPDATE conversations SET updated_at=? WHERE id=?`), nowISO(), msg.ConversationID); err != nil {
return false, false, err return false, false, err
} }
} }
@@ -291,6 +321,44 @@ func (s *Store) settleRunOnce(r *core.Run, msg *core.Message, fence *fenceRef, a
return n == 1, false, nil return n == 1, false, nil
} }
func (s *Store) insertSettledMessageTx(tx *sql.Tx, msg *core.Message) error {
if msg == nil {
return nil
}
if msg.ID == "" {
msg.ID = newID("msg")
}
if msg.CreatedAt.IsZero() {
msg.CreatedAt = time.Now().UTC()
}
insert := `INSERT INTO messages(id,conversation_id,run_id,role,parts,seq,created_at,request_id)
SELECT ?,?,?,?,?,COALESCE(MAX(seq),0)+1,?,? FROM messages WHERE conversation_id=?`
if _, err := tx.Exec(s.q(insert), msg.ID, msg.ConversationID, msg.RunID, string(msg.Role), marshalJSON(msg.Parts), msg.CreatedAt.UTC().Format(time.RFC3339Nano), "", msg.ConversationID); err != nil {
return err
}
if err := s.appendProjectMessageObservationsTx(tx, msg); err != nil {
return err
}
if msg.VariantGroup != "" || msg.Excluded {
ex := 0
if msg.Excluded {
ex = 1
}
if _, err := tx.Exec(s.q(`UPDATE messages SET excluded=?, variant_group=?, variant_index=? WHERE id=?`), ex, msg.VariantGroup, msg.VariantIndex, msg.ID); err != nil {
return err
}
}
if msg.VariantGroup != "" {
if _, err := tx.Exec(s.q(`DELETE FROM regen_pending WHERE conversation_id=? AND variant_group=? AND variant_index=?`), msg.ConversationID, msg.VariantGroup, msg.VariantIndex); err != nil {
return err
}
}
if _, err := tx.Exec(s.q(`UPDATE conversations SET updated_at=? WHERE id=?`), nowISO(), msg.ConversationID); err != nil {
return err
}
return nil
}
// ListInterruptedSubagentRuns 列出被收成 interrupted 的子代理 run // ListInterruptedSubagentRuns 列出被收成 interrupted 的子代理 run
// (N2 WP5 重启续跑的候选集;幂等过滤由转录目录的 done/resumed 标记完成)。 // (N2 WP5 重启续跑的候选集;幂等过滤由转录目录的 done/resumed 标记完成)。
func (s *Store) ListInterruptedSubagentRuns() ([]core.Run, error) { func (s *Store) ListInterruptedSubagentRuns() ([]core.Run, error) {
+8 -2
View File
@@ -3,6 +3,7 @@ package store
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"time"
core "agent-system-core" core "agent-system-core"
) )
@@ -42,7 +43,12 @@ func (s *Store) CreateRunAccountChecked(r *core.Run, allowImplicitDefault bool)
if r.ID == "" { if r.ID == "" {
r.ID = newID("run") r.ID = newID("run")
} }
r.CreatedAt = r.CreatedAt.UTC() if r.CreatedAt.IsZero() {
r.CreatedAt = time.Now().UTC()
} else {
r.CreatedAt = r.CreatedAt.UTC()
}
ca := r.CreatedAt.Format(time.RFC3339Nano)
return s.withEffectiveAccountWrite(string(r.Owner), allowImplicitDefault, func(tx *sql.Tx, _ Account) error { return s.withEffectiveAccountWrite(string(r.Owner), allowImplicitDefault, func(tx *sql.Tx, _ Account) error {
var owner string var owner string
if err := tx.QueryRow(s.q(`SELECT owner FROM conversations WHERE id=?`), r.ConversationID).Scan(&owner); errors.Is(err, sql.ErrNoRows) { if err := tx.QueryRow(s.q(`SELECT owner FROM conversations WHERE id=?`), r.ConversationID).Scan(&owner); errors.Is(err, sql.ErrNoRows) {
@@ -54,7 +60,7 @@ func (s *Store) CreateRunAccountChecked(r *core.Run, allowImplicitDefault bool)
return ErrNotFound return ErrNotFound
} }
_, err := tx.Exec(s.q(`INSERT INTO runs(id,parent_run_id,conversation_id,project_id,owner,kind,profile_id,status,phase,agent_name,created_at,instance_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`), _, err := tx.Exec(s.q(`INSERT INTO runs(id,parent_run_id,conversation_id,project_id,owner,kind,profile_id,status,phase,agent_name,created_at,instance_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`),
r.ID, r.ParentRunID, r.ConversationID, r.ProjectID, r.Owner, string(r.Kind), r.ProfileID, string(r.Status), string(r.Phase), r.AgentName, nowISO(), s.instanceID) r.ID, r.ParentRunID, r.ConversationID, r.ProjectID, r.Owner, string(r.Kind), r.ProfileID, string(r.Status), string(r.Phase), r.AgentName, ca, s.instanceID)
return err return err
}) })
} }
@@ -0,0 +1,189 @@
package store
import (
"context"
"testing"
"time"
core "agent-system-core"
)
func TestSearchDeadlockRegression(t *testing.T) {
st := openTestStore(t)
ctx := context.Background()
// Seed account for allow/pred callback to query
if err := st.CreateAccount("alice", "", "", false); err != nil {
t.Fatal(err)
}
// Seed knowledge doc and vector
docVec := core.KnowledgeDoc{
ID: "doc_vec_deadlock",
Owner: "alice",
ProjectID: "proj_deadlock",
Source: "src_vec",
Title: "vector doc",
Chunk: "vector chunk content",
}
docFTS := core.KnowledgeDoc{
ID: "doc_fts_deadlock",
Owner: "alice",
ProjectID: "proj_deadlock",
Source: "src_fts",
Title: "Database Optimization Guide",
Chunk: "Performance tuning and indexing for relational databases",
}
docLike := core.KnowledgeDoc{
ID: "doc_like_deadlock",
Owner: "alice",
ProjectID: "proj_deadlock",
Source: "src_like",
Title: "微服务架构治理",
Chunk: "分布式系统与全链路追踪方案",
}
if err := st.AddKnowledge(docVec, docFTS, docLike); err != nil {
t.Fatal(err)
}
if err := st.UpsertKnowledgeVector(ctx, "alice", docVec.ID, "ref_vec", []float32{1.0, 0.0}); err != nil {
t.Fatal(err)
}
runBounded := func(name string, fn func()) {
t.Helper()
done := make(chan struct{})
go func() {
defer close(done)
fn()
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatalf("deadlock detected in %s: did not finish within 10s", name)
}
}
// 1. Vector scan: allow callback queries the same Store
runBounded("searchKnowledgeVectorsSharedAuthorizedScan", func() {
hits, err := st.SearchKnowledgeVectorsSharedAuthorized(ctx, "bob", "", []float32{1.0, 0.0}, 5, core.RetrievalQuery{}, func(d core.KnowledgeDoc) bool {
// Query the store inside the callback: on a single-connection SQLite store,
// this would deadlock if rows was not closed before invoking the callback.
_, qerr := st.GetAccountByOwner(d.Owner)
if qerr != nil {
t.Errorf("GetAccountByOwner failed in callback: %v", qerr)
}
return true
})
if err != nil {
t.Errorf("SearchKnowledgeVectorsSharedAuthorized failed: %v", err)
}
if len(hits) == 0 {
t.Errorf("expected at least 1 hit, got 0")
}
})
// 2. Content FTS scan: pred callback queries the same Store
runBounded("searchKnowledgeFTSCandidates", func() {
hits, err := st.searchKnowledgeFTSCandidates(func(d core.KnowledgeDoc) bool {
_, qerr := st.GetAccountByOwner(d.Owner)
if qerr != nil {
t.Errorf("GetAccountByOwner failed in callback: %v", qerr)
}
return true
}, core.RetrievalQuery{Text: "database optimization", TopK: 5}, true)
if err != nil {
t.Errorf("searchKnowledgeFTSCandidates failed: %v", err)
}
if len(hits) == 0 {
t.Errorf("expected at least 1 hit, got 0")
}
})
// 3. Content LIKE scan: pred callback queries the same Store
runBounded("searchKnowledgeLikeSharedCandidates", func() {
hits, err := st.searchKnowledgeLikeSharedCandidates("bob", core.RetrievalQuery{Text: "微服务", TopK: 5}, func(d core.KnowledgeDoc) bool {
_, qerr := st.GetAccountByOwner(d.Owner)
if qerr != nil {
t.Errorf("GetAccountByOwner failed in callback: %v", qerr)
}
return true
}, true)
if err != nil {
t.Errorf("searchKnowledgeLikeSharedCandidates failed: %v", err)
}
if len(hits) == 0 {
t.Errorf("expected at least 1 hit, got 0")
}
})
}
func TestRunCreatedAtNonZeroRecentAndMatchesReadBack(t *testing.T) {
st := openTestStore(t)
if err := st.CreateAccount("alice", "", "", false); err != nil {
t.Fatal(err)
}
proj, err := st.CreateProject("alice", "test_proj")
if err != nil {
t.Fatal(err)
}
conv, err := st.CreateConversation("alice", proj.ID, "test_conv", "")
if err != nil {
t.Fatal(err)
}
// 1. CreateRun with zero CreatedAt
before1 := time.Now().UTC().Add(-time.Second)
r1 := core.Run{
ConversationID: conv.ID,
ProjectID: conv.ProjectID,
Owner: conv.Owner,
Kind: core.RunConversation,
Status: core.RunRunning,
Phase: core.PhaseThinking,
}
if err := st.CreateRun(&r1); err != nil {
t.Fatalf("CreateRun failed: %v", err)
}
if r1.CreatedAt.IsZero() {
t.Fatalf("CreateRun: returned run has zero CreatedAt")
}
if r1.CreatedAt.Before(before1) || r1.CreatedAt.After(time.Now().UTC().Add(time.Second)) {
t.Fatalf("CreateRun: returned run CreatedAt %v not recent (before=%v)", r1.CreatedAt, before1)
}
readBack1, err := st.GetRunByID(r1.ID)
if err != nil {
t.Fatalf("GetRunByID failed: %v", err)
}
if !readBack1.CreatedAt.Equal(r1.CreatedAt) {
t.Fatalf("CreateRun: in-memory CreatedAt %v != stored CreatedAt %v", r1.CreatedAt, readBack1.CreatedAt)
}
// 2. CreateRunAccountChecked with zero CreatedAt
before2 := time.Now().UTC().Add(-time.Second)
r2 := core.Run{
ConversationID: conv.ID,
ProjectID: conv.ProjectID,
Owner: conv.Owner,
Kind: core.RunConversation,
Status: core.RunRunning,
Phase: core.PhaseThinking,
}
if err := st.CreateRunAccountChecked(&r2, false); err != nil {
t.Fatalf("CreateRunAccountChecked failed: %v", err)
}
if r2.CreatedAt.IsZero() {
t.Fatalf("CreateRunAccountChecked: returned run has zero CreatedAt")
}
if r2.CreatedAt.Before(before2) || r2.CreatedAt.After(time.Now().UTC().Add(time.Second)) {
t.Fatalf("CreateRunAccountChecked: returned run CreatedAt %v not recent (before=%v)", r2.CreatedAt, before2)
}
readBack2, err := st.GetRunByID(r2.ID)
if err != nil {
t.Fatalf("GetRunByID failed: %v", err)
}
if !readBack2.CreatedAt.Equal(r2.CreatedAt) {
t.Fatalf("CreateRunAccountChecked: in-memory CreatedAt %v != stored CreatedAt %v", r2.CreatedAt, readBack2.CreatedAt)
}
}
+17 -5
View File
@@ -79,7 +79,11 @@ func (s *Store) searchKnowledgePGSharedCandidates(requester string, q core.Retri
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
out := []core.RetrievalHit{} type rawPGCandidate struct {
doc core.KnowledgeDoc
rank float64
}
var rawCands []rawPGCandidate
for rows.Next() { for rows.Next() {
var d core.KnowledgeDoc var d core.KnowledgeDoc
var own, ca string var own, ca string
@@ -89,17 +93,25 @@ func (s *Store) searchKnowledgePGSharedCandidates(requester string, q core.Retri
} }
d.Owner = own d.Owner = own
d.CreatedAt, _ = parseTime(ca) d.CreatedAt, _ = parseTime(ca)
if !pred(d) { rawCands = append(rawCands, rawPGCandidate{doc: d, rank: rank})
}
if err := rows.Err(); err != nil {
return nil, err
}
_ = rows.Close()
out := []core.RetrievalHit{}
for _, c := range rawCands {
if !pred(c.doc) {
continue continue
} }
score := rank score := c.rank
if score == 0 { if score == 0 {
score = 0.5 score = 0.5
} }
out = append(out, core.RetrievalHit{Doc: d, Score: score, Kind: "fulltext"}) out = append(out, core.RetrievalHit{Doc: c.doc, Score: score, Kind: "fulltext"})
if len(out) >= topK { if len(out) >= topK {
break break
} }
} }
return out, rows.Err() return out, nil
} }
+19 -8
View File
@@ -279,11 +279,11 @@ func (s *Store) searchKnowledgeVectorsSharedAuthorizedScan(ctx context.Context,
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
type cand struct { type rawVectorCandidate struct {
hit core.RetrievalHit doc core.KnowledgeDoc
sim float64 blob []byte
} }
var cands []cand var rawCands []rawVectorCandidate
for rows.Next() { for rows.Next() {
var d core.KnowledgeDoc var d core.KnowledgeDoc
var own, ca string var own, ca string
@@ -293,6 +293,20 @@ func (s *Store) searchKnowledgeVectorsSharedAuthorizedScan(ctx context.Context,
} }
d.Owner = own d.Owner = own
d.CreatedAt, _ = parseTime(ca) d.CreatedAt, _ = parseTime(ca)
rawCands = append(rawCands, rawVectorCandidate{doc: d, blob: blob})
}
if err := rows.Err(); err != nil {
return nil, err
}
_ = rows.Close()
type cand struct {
hit core.RetrievalHit
sim float64
}
var cands []cand
for _, raw := range rawCands {
d := raw.doc
if d.ProjectID == "" || !allow(d) { if d.ProjectID == "" || !allow(d) {
continue continue
} }
@@ -302,12 +316,9 @@ func (s *Store) searchKnowledgeVectorsSharedAuthorizedScan(ctx context.Context,
if len(filter.Sources) > 0 && !containsStr(filter.Sources, d.Source) { if len(filter.Sources) > 0 && !containsStr(filter.Sources, d.Source) {
continue continue
} }
sim := cosine(vec, decodeVector(blob)) sim := cosine(vec, decodeVector(raw.blob))
cands = append(cands, cand{hit: core.RetrievalHit{Doc: d, Score: sim, Kind: "vector"}, sim: sim}) cands = append(cands, cand{hit: core.RetrievalHit{Doc: d, Score: sim, Kind: "vector"}, sim: sim})
} }
if err := rows.Err(); err != nil {
return nil, err
}
sort.SliceStable(cands, func(i, j int) bool { return cands[i].sim > cands[j].sim }) sort.SliceStable(cands, func(i, j int) bool { return cands[i].sim > cands[j].sim })
out := []core.RetrievalHit{} out := []core.RetrievalHit{}
for _, c := range cands { for _, c := range cands {