Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

601 lines
17 KiB
Go

package storesqlite
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
)
type sessionForkInteractionRef struct {
TurnID string
RequestID string
}
type sessionForkCanonicalIdentityMap struct {
SourceSessionID string
TargetSessionID string
TurnIDs map[string]string
MessageIDs map[string]string
InteractionIDs map[sessionForkInteractionRef]string
AttachmentIDs map[string]string
}
func buildSessionForkCanonicalIdentityMap(
operation SessionForkOperation,
snapshot sessionForkSnapshot,
) (sessionForkCanonicalIdentityMap, error) {
identityMap := sessionForkCanonicalIdentityMap{
SourceSessionID: strings.TrimSpace(operation.SourceAgentSessionID),
TargetSessionID: strings.TrimSpace(operation.TargetAgentSessionID),
TurnIDs: make(map[string]string, len(snapshot.Turns)),
MessageIDs: make(map[string]string, len(snapshot.Messages)),
InteractionIDs: make(map[sessionForkInteractionRef]string, len(snapshot.Interactions)),
AttachmentIDs: make(map[string]string),
}
if strings.TrimSpace(operation.WorkspaceID) == "" ||
strings.TrimSpace(operation.OperationID) == "" ||
identityMap.SourceSessionID == "" ||
identityMap.TargetSessionID == "" ||
identityMap.SourceSessionID == identityMap.TargetSessionID {
return sessionForkCanonicalIdentityMap{}, errors.New(
"session fork canonical identity requires distinct source and target sessions",
)
}
targetTurnIDs := make(map[string]string, len(snapshot.Turns))
for _, item := range snapshot.Turns {
sourceID := strings.TrimSpace(item.Turn.TurnID)
if sourceID == "" {
return sessionForkCanonicalIdentityMap{}, errors.New(
"session fork snapshot contains an empty turn identity",
)
}
if _, exists := identityMap.TurnIDs[sourceID]; exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork snapshot contains duplicate turn identity %q",
sourceID,
)
}
targetID := deterministicSessionForkCanonicalID(operation, "turn", sourceID)
if prior, exists := targetTurnIDs[targetID]; exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork turn identity collision between %q and %q",
prior,
sourceID,
)
}
identityMap.TurnIDs[sourceID] = targetID
targetTurnIDs[targetID] = sourceID
}
targetMessageIDs := make(map[string]string, len(snapshot.Messages))
for _, message := range snapshot.Messages {
collectSessionForkAttachmentIDs(message.Payload, identityMap.AttachmentIDs)
sourceID := strings.TrimSpace(message.MessageID)
if sourceID == "" {
return sessionForkCanonicalIdentityMap{}, errors.New(
"session fork snapshot contains an empty message identity",
)
}
if _, exists := identityMap.MessageIDs[sourceID]; exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork snapshot contains duplicate message identity %q",
sourceID,
)
}
if sourceTurnID := strings.TrimSpace(message.TurnID); sourceTurnID != "" {
if _, exists := identityMap.TurnIDs[sourceTurnID]; !exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork message %q references unmapped turn %q",
sourceID,
sourceTurnID,
)
}
}
targetID := deterministicSessionForkCanonicalID(operation, "message", sourceID)
if prior, exists := targetMessageIDs[targetID]; exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork message identity collision between %q and %q",
prior,
sourceID,
)
}
identityMap.MessageIDs[sourceID] = targetID
targetMessageIDs[targetID] = sourceID
}
for sourceID := range identityMap.AttachmentIDs {
identityMap.AttachmentIDs[sourceID] = deterministicSessionForkCanonicalID(
operation,
"attachment",
sourceID,
)
}
targetInteractionIDs := make(map[sessionForkInteractionRef]sessionForkInteractionRef, len(snapshot.Interactions))
for _, interaction := range snapshot.Interactions {
sourceRef := sessionForkInteractionRef{
TurnID: strings.TrimSpace(interaction.TurnID),
RequestID: strings.TrimSpace(interaction.RequestID),
}
if sourceRef.TurnID == "" || sourceRef.RequestID == "" {
return sessionForkCanonicalIdentityMap{}, errors.New(
"session fork snapshot contains an incomplete interaction identity",
)
}
targetTurnID, exists := identityMap.TurnIDs[sourceRef.TurnID]
if !exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork interaction %q references unmapped turn %q",
sourceRef.RequestID,
sourceRef.TurnID,
)
}
if _, exists := identityMap.InteractionIDs[sourceRef]; exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork snapshot contains duplicate interaction identity %q in turn %q",
sourceRef.RequestID,
sourceRef.TurnID,
)
}
targetRequestID := deterministicSessionForkCanonicalID(
operation,
"interaction",
sourceRef.TurnID+"\x00"+sourceRef.RequestID,
)
targetRef := sessionForkInteractionRef{
TurnID: targetTurnID,
RequestID: targetRequestID,
}
if prior, exists := targetInteractionIDs[targetRef]; exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork interaction identity collision between %#v and %#v",
prior,
sourceRef,
)
}
identityMap.InteractionIDs[sourceRef] = targetRequestID
targetInteractionIDs[targetRef] = sourceRef
}
for _, item := range snapshot.Turns {
finalMessageID := strings.TrimSpace(item.Turn.FinalAssistantMessageID)
if finalMessageID == "" {
continue
}
if _, exists := identityMap.MessageIDs[finalMessageID]; !exists {
return sessionForkCanonicalIdentityMap{}, fmt.Errorf(
"session fork turn %q references unmapped final assistant message %q",
item.Turn.TurnID,
finalMessageID,
)
}
}
return identityMap, nil
}
func deterministicSessionForkCanonicalID(
operation SessionForkOperation,
entityKind string,
sourceIdentity string,
) string {
sum := sha256.Sum256([]byte(strings.Join([]string{
"tutti-session-fork-canonical-id-v1",
strings.TrimSpace(operation.WorkspaceID),
strings.TrimSpace(operation.OperationID),
strings.TrimSpace(operation.SourceAgentSessionID),
strings.TrimSpace(operation.TargetAgentSessionID),
strings.TrimSpace(entityKind),
sourceIdentity,
}, "\x00")))
return "fork-" + strings.TrimSpace(entityKind) + "-" + hex.EncodeToString(sum[:])
}
func remapSessionForkTurn(
turn Turn,
identityMap sessionForkCanonicalIdentityMap,
) (Turn, error) {
sourceTurnID := strings.TrimSpace(turn.TurnID)
targetTurnID, exists := identityMap.TurnIDs[sourceTurnID]
if !exists {
return Turn{}, fmt.Errorf("session fork turn %q has no canonical mapping", sourceTurnID)
}
turn.AgentSessionID = identityMap.TargetSessionID
turn.TurnID = targetTurnID
if sourceAnchorTurnID := strings.TrimSpace(turn.IdentityAnchorTurnID); sourceAnchorTurnID != "" {
targetAnchorTurnID, exists := identityMap.TurnIDs[sourceAnchorTurnID]
if !exists {
return Turn{}, fmt.Errorf(
"session fork turn identity anchor %q has no canonical mapping",
sourceAnchorTurnID,
)
}
turn.IdentityAnchorTurnID = targetAnchorTurnID
}
if sourceMessageID := strings.TrimSpace(turn.FinalAssistantMessageID); sourceMessageID != "" {
targetMessageID, exists := identityMap.MessageIDs[sourceMessageID]
if !exists {
return Turn{}, fmt.Errorf(
"session fork final assistant message %q has no canonical mapping",
sourceMessageID,
)
}
turn.FinalAssistantMessageID = targetMessageID
}
return turn, nil
}
func remapSessionForkMessage(
message Message,
identityMap sessionForkCanonicalIdentityMap,
) (Message, error) {
sourceMessageID := strings.TrimSpace(message.MessageID)
targetMessageID, exists := identityMap.MessageIDs[sourceMessageID]
if !exists {
return Message{}, fmt.Errorf(
"session fork message %q has no canonical mapping",
sourceMessageID,
)
}
sourceTurnID := strings.TrimSpace(message.TurnID)
targetTurnID := ""
if sourceTurnID != "" {
targetTurnID, exists = identityMap.TurnIDs[sourceTurnID]
if !exists {
return Message{}, fmt.Errorf(
"session fork message %q turn %q has no canonical mapping",
sourceMessageID,
sourceTurnID,
)
}
}
payload, err := rewriteSessionForkMessagePayload(message, sourceTurnID, identityMap)
if err != nil {
return Message{}, fmt.Errorf(
"rewrite session fork message %q payload: %w",
sourceMessageID,
err,
)
}
message.AgentSessionID = identityMap.TargetSessionID
message.MessageID = targetMessageID
message.TurnID = targetTurnID
message.Payload = payload
return message, nil
}
func remapSessionForkInteraction(
interaction Interaction,
identityMap sessionForkCanonicalIdentityMap,
) (Interaction, error) {
sourceRef := sessionForkInteractionRef{
TurnID: strings.TrimSpace(interaction.TurnID),
RequestID: strings.TrimSpace(interaction.RequestID),
}
targetTurnID, turnExists := identityMap.TurnIDs[sourceRef.TurnID]
targetRequestID, requestExists := identityMap.InteractionIDs[sourceRef]
if !turnExists || !requestExists {
return Interaction{}, fmt.Errorf(
"session fork interaction %#v has no complete canonical mapping",
sourceRef,
)
}
input, output, metadata, err := rewriteSessionForkInteractionPayload(
interaction,
sourceRef.TurnID,
identityMap,
)
if err != nil {
return Interaction{}, fmt.Errorf("rewrite session fork interaction payload: %w", err)
}
interaction.AgentSessionID = identityMap.TargetSessionID
interaction.TurnID = targetTurnID
interaction.RequestID = targetRequestID
interaction.Input = input
interaction.Output = output
interaction.Metadata = metadata
return interaction, nil
}
type sessionForkCanonicalRefKind string
const (
sessionForkCanonicalRefTurn sessionForkCanonicalRefKind = "turn"
sessionForkCanonicalRefMessage sessionForkCanonicalRefKind = "message"
sessionForkCanonicalRefInteraction sessionForkCanonicalRefKind = "interaction"
)
type sessionForkCanonicalRefPath struct {
Path []string
Kind sessionForkCanonicalRefKind
}
// rewriteSessionForkMessagePayload uses a closed schema/path catalog. Message
// bodies are intentionally opaque: content, input, output, error, Markdown,
// code, and arbitrary user/tool JSON are never recursively inspected.
func rewriteSessionForkMessagePayload(
message Message,
sourceTurnID string,
identityMap sessionForkCanonicalIdentityMap,
) (map[string]any, error) {
payload := message.Payload
if payload == nil {
return nil, nil
}
paths := make([]sessionForkCanonicalRefPath, 0, 3)
switch strings.ToLower(strings.TrimSpace(message.Kind)) {
case "session_audit":
// goal-control audits use auditId as the canonical Message identity.
// Their optional messageId is submit provenance and is not necessarily
// a canonical workspace_agent_messages identity.
paths = append(paths, sessionForkCanonicalRefPath{
Path: []string{"auditId"},
Kind: sessionForkCanonicalRefMessage,
})
case "tool_call":
if sessionForkInteractiveCallPayload(payload) {
paths = append(paths,
sessionForkCanonicalRefPath{
Path: []string{"requestId"},
Kind: sessionForkCanonicalRefInteraction,
},
sessionForkCanonicalRefPath{
Path: []string{"output", "requestId"},
Kind: sessionForkCanonicalRefInteraction,
},
sessionForkCanonicalRefPath{
Path: []string{"error", "requestId"},
Kind: sessionForkCanonicalRefInteraction,
},
)
}
}
if strings.EqualFold(strings.TrimSpace(payloadString(payload, "kind")), "agent_system_notice") {
paths = append(paths,
sessionForkCanonicalRefPath{
Path: []string{"planTurnId"},
Kind: sessionForkCanonicalRefTurn,
},
sessionForkCanonicalRefPath{
Path: []string{"confirmedTurnId"},
Kind: sessionForkCanonicalRefTurn,
},
)
}
rewritten, err := rewriteSessionForkCanonicalPaths(
payload,
sourceTurnID,
identityMap,
paths,
)
if err != nil {
return nil, err
}
return rewriteSessionForkAttachmentIDs(rewritten, identityMap.AttachmentIDs), nil
}
func collectSessionForkAttachmentIDs(
value any,
ids map[string]string,
) {
switch typed := value.(type) {
case map[string]any:
if attachmentID, ok := typed["attachmentId"].(string); ok {
if attachmentID = strings.TrimSpace(attachmentID); attachmentID != "" {
ids[attachmentID] = ""
}
}
for _, nested := range typed {
collectSessionForkAttachmentIDs(nested, ids)
}
case []any:
for _, nested := range typed {
collectSessionForkAttachmentIDs(nested, ids)
}
}
}
func rewriteSessionForkAttachmentIDs(
value any,
ids map[string]string,
) map[string]any {
payload, _ := cloneSessionForkCanonicalValue(value).(map[string]any)
var rewrite func(any)
rewrite = func(candidate any) {
switch typed := candidate.(type) {
case map[string]any:
if sourceID, ok := typed["attachmentId"].(string); ok {
if targetID := ids[strings.TrimSpace(sourceID)]; targetID != "" {
typed["attachmentId"] = targetID
}
}
for _, nested := range typed {
rewrite(nested)
}
case []any:
for _, nested := range typed {
rewrite(nested)
}
}
}
rewrite(payload)
return payload
}
func sessionForkInteractiveCallPayload(payload map[string]any) bool {
switch strings.ToLower(strings.TrimSpace(payloadString(payload, "callType"))) {
case "approval", "interactive":
return true
default:
return false
}
}
// rewriteSessionForkInteractionPayload applies only the paths owned by the
// closed Interaction kind. Provider prompt metadata and arbitrary answer/tool
// payloads remain opaque.
func rewriteSessionForkInteractionPayload(
interaction Interaction,
sourceTurnID string,
identityMap sessionForkCanonicalIdentityMap,
) (map[string]any, map[string]any, map[string]any, error) {
var inputPaths []sessionForkCanonicalRefPath
switch strings.ToLower(strings.TrimSpace(interaction.Kind)) {
case InteractionKindApproval:
inputPaths = []sessionForkCanonicalRefPath{{
Path: []string{"requestId"},
Kind: sessionForkCanonicalRefInteraction,
}}
case InteractionKindQuestion, InteractionKindPlan:
default:
return nil, nil, nil, fmt.Errorf(
"session fork interaction kind %q has no canonical payload schema",
interaction.Kind,
)
}
input, err := rewriteSessionForkCanonicalPaths(
interaction.Input,
sourceTurnID,
identityMap,
inputPaths,
)
if err != nil {
return nil, nil, nil, fmt.Errorf("input: %w", err)
}
output, err := rewriteSessionForkCanonicalPaths(
interaction.Output,
sourceTurnID,
identityMap,
[]sessionForkCanonicalRefPath{{
Path: []string{"requestId"},
Kind: sessionForkCanonicalRefInteraction,
}},
)
if err != nil {
return nil, nil, nil, fmt.Errorf("output: %w", err)
}
metadata, err := rewriteSessionForkCanonicalPaths(
interaction.Metadata,
sourceTurnID,
identityMap,
nil,
)
if err != nil {
return nil, nil, nil, fmt.Errorf("metadata: %w", err)
}
return input, output, metadata, nil
}
func rewriteSessionForkCanonicalPaths(
value map[string]any,
sourceTurnID string,
identityMap sessionForkCanonicalIdentityMap,
paths []sessionForkCanonicalRefPath,
) (map[string]any, error) {
if value == nil {
return nil, nil
}
result, ok := cloneSessionForkCanonicalValue(value).(map[string]any)
if !ok {
return nil, errors.New("session fork canonical payload root is not an object")
}
for _, refPath := range paths {
if err := rewriteSessionForkCanonicalPath(
result,
strings.TrimSpace(sourceTurnID),
identityMap,
refPath,
); err != nil {
return nil, err
}
}
return result, nil
}
func cloneSessionForkCanonicalValue(value any) any {
switch typed := value.(type) {
case map[string]any:
result := make(map[string]any, len(typed))
for key, nested := range typed {
result[key] = cloneSessionForkCanonicalValue(nested)
}
return result
case []any:
result := make([]any, len(typed))
for index, nested := range typed {
result[index] = cloneSessionForkCanonicalValue(nested)
}
return result
default:
return typed
}
}
func rewriteSessionForkCanonicalPath(
payload map[string]any,
sourceTurnID string,
identityMap sessionForkCanonicalIdentityMap,
refPath sessionForkCanonicalRefPath,
) error {
if len(refPath.Path) == 0 {
return errors.New("session fork canonical reference path is empty")
}
parent := payload
for index, key := range refPath.Path {
value, exists := parent[key]
if !exists {
return nil
}
if index < len(refPath.Path)-1 {
nested, ok := value.(map[string]any)
if !ok {
return fmt.Errorf(
"session fork canonical reference path %q is not an object",
strings.Join(refPath.Path[:index+1], "."),
)
}
parent = nested
continue
}
identity, ok := value.(string)
identity = strings.TrimSpace(identity)
if !ok || identity == "" {
return fmt.Errorf(
"session fork canonical reference %q is not a non-empty string",
strings.Join(refPath.Path, "."),
)
}
var target string
var mapped bool
switch refPath.Kind {
case sessionForkCanonicalRefTurn:
target, mapped = identityMap.TurnIDs[identity]
case sessionForkCanonicalRefMessage:
target, mapped = identityMap.MessageIDs[identity]
case sessionForkCanonicalRefInteraction:
sourceRef := sessionForkInteractionRef{
TurnID: sourceTurnID,
RequestID: identity,
}
target, mapped = identityMap.InteractionIDs[sourceRef]
default:
return fmt.Errorf(
"session fork canonical reference %q has unknown kind %q",
strings.Join(refPath.Path, "."),
refPath.Kind,
)
}
if !mapped {
return fmt.Errorf(
"session fork canonical reference %q=%q has no %s mapping",
strings.Join(refPath.Path, "."),
identity,
refPath.Kind,
)
}
parent[key] = target
}
return nil
}