kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
896 行
25 KiB
Go
896 行
25 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type recallEvidenceValidationError struct {
|
|
message string
|
|
}
|
|
|
|
func (e *recallEvidenceValidationError) Error() string {
|
|
return e.message
|
|
}
|
|
|
|
func invalidRecallEvidencef(format string, args ...any) error {
|
|
return &recallEvidenceValidationError{
|
|
message: fmt.Sprintf(format, args...),
|
|
}
|
|
}
|
|
|
|
func isRecallEvidenceValidationError(err error) bool {
|
|
var validationErr *recallEvidenceValidationError
|
|
return errors.As(err, &validationErr)
|
|
}
|
|
|
|
const (
|
|
recallEvidenceWindowDigestVersion = "recall-evidence-window/v1"
|
|
recallEvidenceContentDigestVersion = "recall-evidence-content/v1"
|
|
recallEvidenceOrdinalBySourceUUIDSQL = `
|
|
SELECT COUNT(*), MIN(ordinal)
|
|
FROM messages
|
|
WHERE session_id = ? AND source_uuid = ?
|
|
AND source_uuid != ''`
|
|
)
|
|
|
|
type recallEvidenceRevocationReason string
|
|
|
|
const (
|
|
recallEvidenceRevocationStartEndpointUnresolved recallEvidenceRevocationReason = "start_endpoint_unresolved"
|
|
recallEvidenceRevocationEndEndpointUnresolved recallEvidenceRevocationReason = "end_endpoint_unresolved"
|
|
recallEvidenceRevocationInvalidResolvedRange recallEvidenceRevocationReason = "invalid_resolved_range"
|
|
recallEvidenceRevocationWindowInvalid recallEvidenceRevocationReason = "window_invalid"
|
|
recallEvidenceRevocationSelectionInvalid recallEvidenceRevocationReason = "selection_invalid"
|
|
recallEvidenceRevocationMissingDigest recallEvidenceRevocationReason = "missing_digest"
|
|
recallEvidenceRevocationContentDigestMismatch recallEvidenceRevocationReason = "content_digest_mismatch"
|
|
recallEvidenceRevocationEvidenceDroppedDuringResync recallEvidenceRevocationReason = "evidence_dropped_during_resync"
|
|
)
|
|
|
|
type recallEvidenceRevocationEvent struct {
|
|
entryID string
|
|
sessionID string
|
|
reason recallEvidenceRevocationReason
|
|
}
|
|
|
|
// recallEvidenceRevocationEvents buffers diagnostics until the transaction
|
|
// owner confirms its outermost commit succeeded.
|
|
type recallEvidenceRevocationEvents []recallEvidenceRevocationEvent
|
|
|
|
func (events recallEvidenceRevocationEvents) flush() {
|
|
for _, event := range events {
|
|
log.Printf(
|
|
"recall: revoked provenance entry=%s session=%s reason=%s",
|
|
event.entryID,
|
|
event.sessionID,
|
|
event.reason,
|
|
)
|
|
}
|
|
}
|
|
|
|
// RecallEvidenceWindow is the host-authorized transcript region supplied to an
|
|
// extractor. Its authorization digest binds the source session, exact ordinal
|
|
// coordinates, stable message identities, visible content, and tool calls.
|
|
type RecallEvidenceWindow struct {
|
|
SessionID string `json:"session_id"`
|
|
MessageStartOrdinal int `json:"message_start_ordinal"`
|
|
MessageEndOrdinal int `json:"message_end_ordinal"`
|
|
Messages []RecallEvidenceWindowMessage `json:"messages"`
|
|
AllowedToolUseIDs []string `json:"allowed_tool_use_ids"`
|
|
AuthorizationDigest string `json:"authorization_digest"`
|
|
}
|
|
|
|
// RecallEvidenceWindowMessage is the model-visible portion of one stored
|
|
// message plus the host-owned coordinates used to authorize a later selection.
|
|
type RecallEvidenceWindowMessage struct {
|
|
Ordinal int `json:"ordinal"`
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
SourceUUID string `json:"source_uuid,omitempty"`
|
|
ToolCalls []RecallEvidenceWindowToolCall `json:"tool_calls,omitempty"`
|
|
}
|
|
|
|
// RecallEvidenceWindowToolCall contains the stored tool-call fields supplied
|
|
// with an evidence window. Database row IDs and derived indexing fields are not
|
|
// part of the model-visible or fingerprinted representation.
|
|
type RecallEvidenceWindowToolCall struct {
|
|
ToolName string `json:"tool_name"`
|
|
Category string `json:"category"`
|
|
ToolUseID string `json:"tool_use_id,omitempty"`
|
|
InputJSON string `json:"input_json,omitempty"`
|
|
SkillName string `json:"skill_name,omitempty"`
|
|
ResultContentLength int `json:"result_content_length,omitempty"`
|
|
ResultContent string `json:"result_content,omitempty"`
|
|
SubagentSessionID string `json:"subagent_session_id,omitempty"`
|
|
}
|
|
|
|
// RecallEvidenceSelection is the only provenance choice an extractor may
|
|
// make: a narrowed ordinal range and optional tool calls inside the authorized
|
|
// host window. The source session cannot be replaced by model output.
|
|
type RecallEvidenceSelection struct {
|
|
MessageStartOrdinal int `json:"message_start_ordinal"`
|
|
MessageEndOrdinal int `json:"message_end_ordinal"`
|
|
ToolUseIDs []string `json:"tool_use_ids,omitempty"`
|
|
}
|
|
|
|
// RecallEvidenceSelectionMetadata is derived exclusively from host state and
|
|
// persisted with durable evidence. Its content digest intentionally omits
|
|
// storage IDs, timestamps, absolute ordinals, and source UUIDs so unchanged
|
|
// evidence remains recognizable after transcript coordinates shift.
|
|
type RecallEvidenceSelectionMetadata struct {
|
|
MessageStartSourceUUID string `json:"message_start_source_uuid,omitempty"`
|
|
MessageEndSourceUUID string `json:"message_end_source_uuid,omitempty"`
|
|
ContentDigest string `json:"content_digest"`
|
|
ToolUseIDs []string `json:"tool_use_ids,omitempty"`
|
|
}
|
|
|
|
type recallEvidenceQueryer interface {
|
|
QueryContext(
|
|
context.Context, string, ...any,
|
|
) (*sql.Rows, error)
|
|
}
|
|
|
|
// BuildRecallEvidenceWindow loads an exact, gap-free transcript range and
|
|
// fingerprints the representation the host may supply to an extractor.
|
|
func (db *DB) BuildRecallEvidenceWindow(
|
|
ctx context.Context,
|
|
sessionID string,
|
|
messageStartOrdinal int,
|
|
messageEndOrdinal int,
|
|
) (RecallEvidenceWindow, error) {
|
|
tx, err := db.getReader().BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
|
if err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"beginning recall evidence snapshot: %w", err,
|
|
)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
window, err := buildRecallEvidenceWindow(
|
|
ctx,
|
|
tx,
|
|
sessionID,
|
|
messageStartOrdinal,
|
|
messageEndOrdinal,
|
|
)
|
|
if err != nil {
|
|
return RecallEvidenceWindow{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"committing recall evidence snapshot: %w", err,
|
|
)
|
|
}
|
|
return window, nil
|
|
}
|
|
|
|
func buildRecallEvidenceWindow(
|
|
ctx context.Context,
|
|
queryer recallEvidenceQueryer,
|
|
sessionID string,
|
|
messageStartOrdinal int,
|
|
messageEndOrdinal int,
|
|
) (RecallEvidenceWindow, error) {
|
|
sessionID = strings.TrimSpace(sessionID)
|
|
if sessionID == "" {
|
|
return RecallEvidenceWindow{}, invalidRecallEvidencef(
|
|
"evidence window session id is required",
|
|
)
|
|
}
|
|
if messageStartOrdinal < 0 || messageEndOrdinal < messageStartOrdinal {
|
|
return RecallEvidenceWindow{}, invalidRecallEvidencef(
|
|
"invalid evidence window range %d-%d",
|
|
messageStartOrdinal,
|
|
messageEndOrdinal,
|
|
)
|
|
}
|
|
|
|
window := RecallEvidenceWindow{
|
|
SessionID: sessionID,
|
|
MessageStartOrdinal: messageStartOrdinal,
|
|
MessageEndOrdinal: messageEndOrdinal,
|
|
}
|
|
rows, err := queryer.QueryContext(ctx, `
|
|
SELECT ordinal, role, content, source_uuid
|
|
FROM messages
|
|
WHERE session_id = ?
|
|
AND ordinal BETWEEN ? AND ?
|
|
ORDER BY ordinal ASC`,
|
|
sessionID,
|
|
messageStartOrdinal,
|
|
messageEndOrdinal,
|
|
)
|
|
if err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"querying recall evidence messages: %w",
|
|
err,
|
|
)
|
|
}
|
|
for rows.Next() {
|
|
var message RecallEvidenceWindowMessage
|
|
if err := rows.Scan(
|
|
&message.Ordinal,
|
|
&message.Role,
|
|
&message.Content,
|
|
&message.SourceUUID,
|
|
); err != nil {
|
|
rows.Close()
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"scanning recall evidence message: %w",
|
|
err,
|
|
)
|
|
}
|
|
window.Messages = append(window.Messages, message)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"closing recall evidence messages: %w",
|
|
err,
|
|
)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"reading recall evidence messages: %w",
|
|
err,
|
|
)
|
|
}
|
|
for offset := 0; offset <= messageEndOrdinal-messageStartOrdinal; offset++ {
|
|
want := messageStartOrdinal + offset
|
|
if offset >= len(window.Messages) || window.Messages[offset].Ordinal != want {
|
|
return RecallEvidenceWindow{}, invalidRecallEvidencef(
|
|
"recall evidence window %s:%d-%d is missing ordinal %d",
|
|
sessionID,
|
|
messageStartOrdinal,
|
|
messageEndOrdinal,
|
|
want,
|
|
)
|
|
}
|
|
}
|
|
|
|
messageIndexes := make(map[int]int, len(window.Messages))
|
|
for i := range window.Messages {
|
|
messageIndexes[window.Messages[i].Ordinal] = i
|
|
}
|
|
toolRows, err := queryer.QueryContext(ctx, `
|
|
SELECT m.ordinal, tc.tool_name, tc.category,
|
|
COALESCE(tc.tool_use_id, ''),
|
|
COALESCE(tc.input_json, ''),
|
|
COALESCE(tc.skill_name, ''),
|
|
COALESCE(tc.result_content_length, 0),
|
|
COALESCE(tc.result_content, ''),
|
|
COALESCE(tc.subagent_session_id, '')
|
|
FROM tool_calls tc
|
|
JOIN messages m ON m.id = tc.message_id
|
|
WHERE m.session_id = ?
|
|
AND m.ordinal BETWEEN ? AND ?
|
|
ORDER BY m.ordinal ASC,
|
|
COALESCE(tc.call_index, 2147483647) ASC,
|
|
tc.id ASC`,
|
|
sessionID,
|
|
messageStartOrdinal,
|
|
messageEndOrdinal,
|
|
)
|
|
if err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"querying recall evidence tool calls: %w",
|
|
err,
|
|
)
|
|
}
|
|
allowedToolUseIDs := make(map[string]struct{})
|
|
for toolRows.Next() {
|
|
var ordinal int
|
|
var toolCall RecallEvidenceWindowToolCall
|
|
if err := toolRows.Scan(
|
|
&ordinal,
|
|
&toolCall.ToolName,
|
|
&toolCall.Category,
|
|
&toolCall.ToolUseID,
|
|
&toolCall.InputJSON,
|
|
&toolCall.SkillName,
|
|
&toolCall.ResultContentLength,
|
|
&toolCall.ResultContent,
|
|
&toolCall.SubagentSessionID,
|
|
); err != nil {
|
|
toolRows.Close()
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"scanning recall evidence tool call: %w",
|
|
err,
|
|
)
|
|
}
|
|
messageIndex, ok := messageIndexes[ordinal]
|
|
if !ok {
|
|
toolRows.Close()
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"tool call references missing ordinal %d",
|
|
ordinal,
|
|
)
|
|
}
|
|
window.Messages[messageIndex].ToolCalls = append(
|
|
window.Messages[messageIndex].ToolCalls,
|
|
toolCall,
|
|
)
|
|
if toolCall.ToolUseID != "" {
|
|
allowedToolUseIDs[toolCall.ToolUseID] = struct{}{}
|
|
}
|
|
}
|
|
if err := toolRows.Close(); err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"closing recall evidence tool calls: %w",
|
|
err,
|
|
)
|
|
}
|
|
if err := toolRows.Err(); err != nil {
|
|
return RecallEvidenceWindow{}, fmt.Errorf(
|
|
"reading recall evidence tool calls: %w",
|
|
err,
|
|
)
|
|
}
|
|
window.AllowedToolUseIDs = make(
|
|
[]string,
|
|
0,
|
|
len(allowedToolUseIDs),
|
|
)
|
|
for toolUseID := range allowedToolUseIDs {
|
|
window.AllowedToolUseIDs = append(
|
|
window.AllowedToolUseIDs,
|
|
toolUseID,
|
|
)
|
|
}
|
|
sort.Strings(window.AllowedToolUseIDs)
|
|
digest, err := recallEvidenceAuthorizationDigest(window)
|
|
if err != nil {
|
|
return RecallEvidenceWindow{}, err
|
|
}
|
|
window.AuthorizationDigest = digest
|
|
return window, nil
|
|
}
|
|
|
|
// BindSelection validates that an extractor selection is contained within the
|
|
// host authorization and returns durable metadata derived from that host-owned
|
|
// window. Duplicate tool IDs are normalized to a sorted unique list.
|
|
func (window RecallEvidenceWindow) BindSelection(
|
|
selection RecallEvidenceSelection,
|
|
) (RecallEvidenceSelectionMetadata, error) {
|
|
wantAuthorizationDigest, err := recallEvidenceAuthorizationDigest(window)
|
|
if err != nil {
|
|
return RecallEvidenceSelectionMetadata{}, err
|
|
}
|
|
if window.AuthorizationDigest == "" ||
|
|
window.AuthorizationDigest != wantAuthorizationDigest {
|
|
return RecallEvidenceSelectionMetadata{}, invalidRecallEvidencef(
|
|
"recall evidence window authorization digest is invalid",
|
|
)
|
|
}
|
|
if selection.MessageStartOrdinal < 0 ||
|
|
selection.MessageEndOrdinal < selection.MessageStartOrdinal {
|
|
return RecallEvidenceSelectionMetadata{}, invalidRecallEvidencef(
|
|
"invalid evidence selection range %d-%d",
|
|
selection.MessageStartOrdinal,
|
|
selection.MessageEndOrdinal,
|
|
)
|
|
}
|
|
if selection.MessageStartOrdinal < window.MessageStartOrdinal ||
|
|
selection.MessageEndOrdinal > window.MessageEndOrdinal {
|
|
return RecallEvidenceSelectionMetadata{}, invalidRecallEvidencef(
|
|
"evidence selection %d-%d is outside authorized window %d-%d",
|
|
selection.MessageStartOrdinal,
|
|
selection.MessageEndOrdinal,
|
|
window.MessageStartOrdinal,
|
|
window.MessageEndOrdinal,
|
|
)
|
|
}
|
|
|
|
toolUseIDs, err := normalizeRecallEvidenceToolUseIDs(selection.ToolUseIDs)
|
|
if err != nil {
|
|
return RecallEvidenceSelectionMetadata{}, err
|
|
}
|
|
allowedToolUseIDs := make(map[string]struct{}, len(window.AllowedToolUseIDs))
|
|
for _, toolUseID := range window.AllowedToolUseIDs {
|
|
allowedToolUseIDs[toolUseID] = struct{}{}
|
|
}
|
|
for _, toolUseID := range toolUseIDs {
|
|
if _, ok := allowedToolUseIDs[toolUseID]; !ok {
|
|
return RecallEvidenceSelectionMetadata{}, invalidRecallEvidencef(
|
|
"tool use %s is outside authorized window",
|
|
toolUseID,
|
|
)
|
|
}
|
|
}
|
|
|
|
requestedToolUseIDs := make(map[string]struct{}, len(toolUseIDs))
|
|
for _, toolUseID := range toolUseIDs {
|
|
requestedToolUseIDs[toolUseID] = struct{}{}
|
|
}
|
|
seenToolUseIDs := make(map[string]struct{}, len(toolUseIDs))
|
|
selectedMessages := make([]canonicalRecallEvidenceMessage, 0)
|
|
var startSourceUUID string
|
|
var endSourceUUID string
|
|
for _, message := range window.Messages {
|
|
if message.Ordinal < selection.MessageStartOrdinal ||
|
|
message.Ordinal > selection.MessageEndOrdinal {
|
|
continue
|
|
}
|
|
if message.Ordinal == selection.MessageStartOrdinal {
|
|
startSourceUUID = message.SourceUUID
|
|
}
|
|
if message.Ordinal == selection.MessageEndOrdinal {
|
|
endSourceUUID = message.SourceUUID
|
|
}
|
|
canonical := canonicalRecallEvidenceMessage{
|
|
Role: message.Role,
|
|
Content: message.Content,
|
|
}
|
|
for _, toolCall := range message.ToolCalls {
|
|
if _, ok := requestedToolUseIDs[toolCall.ToolUseID]; !ok {
|
|
continue
|
|
}
|
|
canonical.ToolCalls = append(canonical.ToolCalls, toolCall)
|
|
seenToolUseIDs[toolCall.ToolUseID] = struct{}{}
|
|
}
|
|
selectedMessages = append(selectedMessages, canonical)
|
|
}
|
|
wantMessageCount := selection.MessageEndOrdinal -
|
|
selection.MessageStartOrdinal + 1
|
|
if len(selectedMessages) != wantMessageCount {
|
|
return RecallEvidenceSelectionMetadata{}, invalidRecallEvidencef(
|
|
"evidence selection %d-%d contains a missing ordinal",
|
|
selection.MessageStartOrdinal,
|
|
selection.MessageEndOrdinal,
|
|
)
|
|
}
|
|
for _, toolUseID := range toolUseIDs {
|
|
if _, ok := seenToolUseIDs[toolUseID]; !ok {
|
|
return RecallEvidenceSelectionMetadata{}, invalidRecallEvidencef(
|
|
"tool use %s is outside selected messages",
|
|
toolUseID,
|
|
)
|
|
}
|
|
}
|
|
contentDigest, err := digestRecallEvidenceCanonical(
|
|
canonicalRecallEvidenceSelection{
|
|
Version: recallEvidenceContentDigestVersion,
|
|
Messages: selectedMessages,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return RecallEvidenceSelectionMetadata{}, err
|
|
}
|
|
return RecallEvidenceSelectionMetadata{
|
|
MessageStartSourceUUID: startSourceUUID,
|
|
MessageEndSourceUUID: endSourceUUID,
|
|
ContentDigest: contentDigest,
|
|
ToolUseIDs: toolUseIDs,
|
|
}, nil
|
|
}
|
|
|
|
type canonicalRecallEvidenceWindow struct {
|
|
Version string `json:"version"`
|
|
SessionID string `json:"session_id"`
|
|
MessageStartOrdinal int `json:"message_start_ordinal"`
|
|
MessageEndOrdinal int `json:"message_end_ordinal"`
|
|
Messages []RecallEvidenceWindowMessage `json:"messages"`
|
|
AllowedToolUseIDs []string `json:"allowed_tool_use_ids"`
|
|
}
|
|
|
|
type canonicalRecallEvidenceSelection struct {
|
|
Version string `json:"version"`
|
|
Messages []canonicalRecallEvidenceMessage `json:"messages"`
|
|
}
|
|
|
|
type canonicalRecallEvidenceMessage struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
ToolCalls []RecallEvidenceWindowToolCall `json:"tool_calls,omitempty"`
|
|
}
|
|
|
|
func recallEvidenceAuthorizationDigest(
|
|
window RecallEvidenceWindow,
|
|
) (string, error) {
|
|
return digestRecallEvidenceCanonical(canonicalRecallEvidenceWindow{
|
|
Version: recallEvidenceWindowDigestVersion,
|
|
SessionID: window.SessionID,
|
|
MessageStartOrdinal: window.MessageStartOrdinal,
|
|
MessageEndOrdinal: window.MessageEndOrdinal,
|
|
Messages: window.Messages,
|
|
AllowedToolUseIDs: window.AllowedToolUseIDs,
|
|
})
|
|
}
|
|
|
|
func digestRecallEvidenceCanonical(value any) (string, error) {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("encoding canonical recall evidence: %w", err)
|
|
}
|
|
digest := sha256.Sum256(encoded)
|
|
return hex.EncodeToString(digest[:]), nil
|
|
}
|
|
|
|
func normalizeRecallEvidenceToolUseIDs(values []string) ([]string, error) {
|
|
unique := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil, invalidRecallEvidencef(
|
|
"evidence selection contains an empty tool use id",
|
|
)
|
|
}
|
|
unique[value] = struct{}{}
|
|
}
|
|
result := make([]string, 0, len(unique))
|
|
for value := range unique {
|
|
result = append(result, value)
|
|
}
|
|
sort.Strings(result)
|
|
return result, nil
|
|
}
|
|
|
|
type recallEvidenceGroupKey struct {
|
|
entryID string
|
|
messageStartOrdinal int
|
|
messageEndOrdinal int
|
|
messageStartSourceID string
|
|
messageEndSourceID string
|
|
contentDigest string
|
|
}
|
|
|
|
type recallEvidenceGroup struct {
|
|
key recallEvidenceGroupKey
|
|
evidenceIDs []int64
|
|
toolUseIDs []string
|
|
}
|
|
|
|
// reconcileRecallEvidenceForSessionTx verifies every currently trusted
|
|
// evidence selection for sessionID against messages and tool calls visible in
|
|
// tx. Invalid selections revoke the parent entry without deleting historical
|
|
// evidence. Structural database failures are returned so the caller's
|
|
// transcript mutation rolls back atomically.
|
|
func reconcileRecallEvidenceForSessionTx(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
sessionID string,
|
|
pending *recallEvidenceRevocationEvents,
|
|
) error {
|
|
groups, err := loadTrustedRecallEvidenceGroupsTx(ctx, tx, sessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, group := range groups {
|
|
startOrdinal := group.key.messageStartOrdinal
|
|
endOrdinal := group.key.messageEndOrdinal
|
|
if group.key.messageStartSourceID != "" {
|
|
startOrdinal, err = uniqueRecallEvidenceOrdinalTx(
|
|
ctx,
|
|
tx,
|
|
sessionID,
|
|
group.key.messageStartSourceID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if startOrdinal < 0 {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationStartEndpointUnresolved,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if group.key.messageEndSourceID != "" {
|
|
endOrdinal, err = uniqueRecallEvidenceOrdinalTx(
|
|
ctx,
|
|
tx,
|
|
sessionID,
|
|
group.key.messageEndSourceID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if endOrdinal < 0 {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationEndEndpointUnresolved,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if startOrdinal < 0 || endOrdinal < 0 || endOrdinal < startOrdinal {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationInvalidResolvedRange,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
window, err := buildRecallEvidenceWindow(
|
|
ctx,
|
|
tx,
|
|
sessionID,
|
|
startOrdinal,
|
|
endOrdinal,
|
|
)
|
|
if err != nil {
|
|
if isRecallEvidenceValidationError(err) {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationWindowInvalid,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
metadata, err := window.BindSelection(RecallEvidenceSelection{
|
|
MessageStartOrdinal: startOrdinal,
|
|
MessageEndOrdinal: endOrdinal,
|
|
ToolUseIDs: group.toolUseIDs,
|
|
})
|
|
if err != nil {
|
|
if isRecallEvidenceValidationError(err) {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationSelectionInvalid,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if group.key.contentDigest == "" {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationMissingDigest,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if group.key.contentDigest != metadata.ContentDigest {
|
|
if err := revokeRecallEvidenceEntryTx(
|
|
ctx,
|
|
tx,
|
|
group.key.entryID,
|
|
sessionID,
|
|
recallEvidenceRevocationContentDigestMismatch,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
for _, evidenceID := range group.evidenceIDs {
|
|
if _, err := tx.ExecContext(ctx, `
|
|
UPDATE recall_evidence
|
|
SET message_start_ordinal = ?,
|
|
message_end_ordinal = ?,
|
|
message_start_source_uuid = ?,
|
|
message_end_source_uuid = ?,
|
|
content_digest = ?
|
|
WHERE id = ?`,
|
|
startOrdinal,
|
|
endOrdinal,
|
|
metadata.MessageStartSourceUUID,
|
|
metadata.MessageEndSourceUUID,
|
|
metadata.ContentDigest,
|
|
evidenceID,
|
|
); err != nil {
|
|
return fmt.Errorf(
|
|
"updating reconciled recall evidence %d: %w",
|
|
evidenceID,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadTrustedRecallEvidenceGroupsTx(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
sessionID string,
|
|
) ([]recallEvidenceGroup, error) {
|
|
rows, err := tx.QueryContext(ctx, `
|
|
SELECT e.id, e.entry_id,
|
|
e.message_start_ordinal, e.message_end_ordinal,
|
|
e.message_start_source_uuid, e.message_end_source_uuid,
|
|
e.content_digest, e.tool_use_id
|
|
FROM recall_evidence e
|
|
JOIN recall_entries r ON r.id = e.entry_id
|
|
WHERE e.session_id = ?
|
|
AND r.provenance_ok = 1
|
|
ORDER BY e.entry_id ASC, e.id ASC`,
|
|
sessionID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying recall evidence to reconcile: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
groups := make([]recallEvidenceGroup, 0)
|
|
indexes := make(map[recallEvidenceGroupKey]int)
|
|
for rows.Next() {
|
|
var evidenceID int64
|
|
var key recallEvidenceGroupKey
|
|
var toolUseID string
|
|
if err := rows.Scan(
|
|
&evidenceID,
|
|
&key.entryID,
|
|
&key.messageStartOrdinal,
|
|
&key.messageEndOrdinal,
|
|
&key.messageStartSourceID,
|
|
&key.messageEndSourceID,
|
|
&key.contentDigest,
|
|
&toolUseID,
|
|
); err != nil {
|
|
return nil, fmt.Errorf(
|
|
"scanning recall evidence to reconcile: %w",
|
|
err,
|
|
)
|
|
}
|
|
index, ok := indexes[key]
|
|
if !ok {
|
|
index = len(groups)
|
|
indexes[key] = index
|
|
groups = append(groups, recallEvidenceGroup{key: key})
|
|
}
|
|
groups[index].evidenceIDs = append(
|
|
groups[index].evidenceIDs,
|
|
evidenceID,
|
|
)
|
|
if toolUseID != "" {
|
|
groups[index].toolUseIDs = append(
|
|
groups[index].toolUseIDs,
|
|
toolUseID,
|
|
)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("reading recall evidence to reconcile: %w", err)
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
func uniqueRecallEvidenceOrdinalTx(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
sessionID string,
|
|
sourceUUID string,
|
|
) (int, error) {
|
|
var count int
|
|
var ordinal sql.NullInt64
|
|
if err := tx.QueryRowContext(ctx, recallEvidenceOrdinalBySourceUUIDSQL,
|
|
sessionID,
|
|
sourceUUID,
|
|
).Scan(&count, &ordinal); err != nil {
|
|
return -1, fmt.Errorf(
|
|
"resolving recall evidence source UUID %s: %w",
|
|
sourceUUID,
|
|
err,
|
|
)
|
|
}
|
|
if count != 1 || !ordinal.Valid {
|
|
return -1, nil
|
|
}
|
|
return int(ordinal.Int64), nil
|
|
}
|
|
|
|
func revokeRecallEvidenceEntryTx(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
entryID string,
|
|
sessionID string,
|
|
reason recallEvidenceRevocationReason,
|
|
pending *recallEvidenceRevocationEvents,
|
|
) error {
|
|
result, err := tx.ExecContext(ctx, `
|
|
UPDATE recall_entries
|
|
SET provenance_ok = 0,
|
|
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
WHERE id = ? AND provenance_ok = 1`,
|
|
entryID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"revoking recall evidence provenance for %s: %w",
|
|
entryID,
|
|
err,
|
|
)
|
|
}
|
|
affected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"checking recall evidence provenance revocation for %s: %w",
|
|
entryID,
|
|
err,
|
|
)
|
|
}
|
|
if affected == 1 {
|
|
*pending = append(*pending, recallEvidenceRevocationEvent{
|
|
entryID: entryID,
|
|
sessionID: sessionID,
|
|
reason: reason,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func reconcileAllRecallEvidenceTx(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
pending *recallEvidenceRevocationEvents,
|
|
) error {
|
|
rows, err := tx.QueryContext(ctx, `
|
|
SELECT DISTINCT e.session_id
|
|
FROM recall_evidence e
|
|
JOIN recall_entries r ON r.id = e.entry_id
|
|
WHERE r.provenance_ok = 1
|
|
ORDER BY e.session_id ASC`)
|
|
if err != nil {
|
|
return fmt.Errorf("querying recall evidence sessions: %w", err)
|
|
}
|
|
var sessionIDs []string
|
|
for rows.Next() {
|
|
var sessionID string
|
|
if err := rows.Scan(&sessionID); err != nil {
|
|
rows.Close()
|
|
return fmt.Errorf("scanning recall evidence session: %w", err)
|
|
}
|
|
sessionIDs = append(sessionIDs, sessionID)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return fmt.Errorf("closing recall evidence sessions: %w", err)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return fmt.Errorf("reading recall evidence sessions: %w", err)
|
|
}
|
|
for _, sessionID := range sessionIDs {
|
|
if err := reconcileRecallEvidenceForSessionTx(
|
|
ctx,
|
|
tx,
|
|
sessionID,
|
|
pending,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|