Files
viettranx 4cf66eb379 feat(ts-port): reasoning strip, dreaming config + weighted scoring
Phase 6 — Reasoning token stripping:
- ReasoningDecision.StripThinking auto-flags Kimi + DeepSeek-Reasoner
- Guard clauses in Anthropic/OpenAI/Codex stream handlers
- Usage.ThinkingTokens + RawAssistantContent preserved (billing + tool passback safe)

Phase 8 — Per-agent dreaming config:
- MemoryConfig.Dreaming JSONB (no migration), resolver callback pattern
- Enabled/DebounceMs/Threshold/VerboseLog fields with partial-override merge
- ConsolidationDeps gains optional AgentStore

Phase 10 — Dreaming weighted scoring:
- Migration 000045 adds recall_count/recall_score/last_recalled_at on episodic_summaries
- ComputeRecallScore 4-component formula (freq/rel/recency/freshness, 14d half-life)
- memory_search fire-and-forget RecordRecall; ListUnpromotedScored in DreamingWorker
- Bootstrap-friendly filter: unrecalled entries bypass thresholds
- Debounce stamped on filter-empty skip to prevent starvation loop

Phase 5 follow-up — last_compaction_at in sessions.metadata JSONB:
- v3 PruneStage.CompactMessages and v2 maybeSummarize both stamp timestamp
- Zero migration; exported const SessionMetaKeyLastCompactionAt

RequiredSchemaVersion: 44 → 45 (PG), SchemaVersion: 12 → 13 (SQLite).
27 new tests; builds pass under PG and sqliteonly tags.
2026-04-10 13:32:02 +07:00

165 lines
4.8 KiB
Go

package pg
import (
"time"
"github.com/lib/pq"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// documentInfoRow is an sqlx scan struct for memory_documents SELECT queries.
// Handles TIMESTAMPTZ→int64 (UnixMilli) and nullable user_id conversion.
type documentInfoRow struct {
AgentID string `db:"agent_id"`
Path string `db:"path"`
Hash string `db:"hash"`
UserID *string `db:"user_id"`
UpdatedAt time.Time `db:"updated_at"`
}
func (r *documentInfoRow) toDocumentInfo() store.DocumentInfo {
info := store.DocumentInfo{
AgentID: r.AgentID,
Path: r.Path,
Hash: r.Hash,
UpdatedAt: r.UpdatedAt.UnixMilli(),
}
if r.UserID != nil {
info.UserID = *r.UserID
}
return info
}
// documentDetailRow is an sqlx scan struct for the GetDocumentDetail query.
type documentDetailRow struct {
Path string `db:"path"`
Content string `db:"content"`
Hash string `db:"hash"`
UserID *string `db:"user_id"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ChunkCount int `db:"chunk_count"`
EmbeddedCount int `db:"embedded_count"`
}
func (r *documentDetailRow) toDocumentDetail() store.DocumentDetail {
d := store.DocumentDetail{
Path: r.Path,
Content: r.Content,
Hash: r.Hash,
ChunkCount: r.ChunkCount,
EmbeddedCount: r.EmbeddedCount,
CreatedAt: r.CreatedAt.UnixMilli(),
UpdatedAt: r.UpdatedAt.UnixMilli(),
}
if r.UserID != nil {
d.UserID = *r.UserID
}
return d
}
// chunkInfoRow is an sqlx scan struct for memory_chunks SELECT queries.
// All fields are directly compatible with store.ChunkInfo.
type chunkInfoRow struct {
ID string `db:"id"`
StartLine int `db:"start_line"`
EndLine int `db:"end_line"`
TextPreview string `db:"text_preview"`
HasEmbedding bool `db:"has_embedding"`
}
func (r *chunkInfoRow) toChunkInfo() store.ChunkInfo {
return store.ChunkInfo{
ID: r.ID,
StartLine: r.StartLine,
EndLine: r.EndLine,
TextPreview: r.TextPreview,
HasEmbedding: r.HasEmbedding,
}
}
// scoredChunkRow is an sqlx scan struct for ftsSearch/vectorSearch queries in memory_search.go.
type scoredChunkRow struct {
Path string `db:"path"`
StartLine int `db:"start_line"`
EndLine int `db:"end_line"`
Text string `db:"text"`
UserID *string `db:"user_id"`
Score float64 `db:"score"`
}
func (r *scoredChunkRow) toScoredChunk() scoredChunk {
return scoredChunk{
Path: r.Path,
StartLine: r.StartLine,
EndLine: r.EndLine,
Text: r.Text,
Score: r.Score,
UserID: r.UserID,
}
}
// episodicSummaryRow is an sqlx scan struct for episodic_summaries SELECT queries.
// Handles TEXT[] key_topics via pq.StringArray.
type episodicSummaryRow struct {
ID string `db:"id"`
TenantID string `db:"tenant_id"`
AgentID string `db:"agent_id"`
UserID string `db:"user_id"`
SessionKey string `db:"session_key"`
Summary string `db:"summary"`
KeyTopics pq.StringArray `db:"key_topics"`
TurnCount int `db:"turn_count"`
TokenCount int `db:"token_count"`
L0Abstract string `db:"l0_abstract"`
SourceID string `db:"source_id"`
SourceType string `db:"source_type"`
CreatedAt time.Time `db:"created_at"`
ExpiresAt *time.Time `db:"expires_at"`
RecallCount int `db:"recall_count"`
RecallScore float64 `db:"recall_score"`
LastRecalledAt *time.Time `db:"last_recalled_at"`
}
func (r *episodicSummaryRow) toEpisodicSummary() store.EpisodicSummary {
ep := store.EpisodicSummary{
UserID: r.UserID,
SessionKey: r.SessionKey,
Summary: r.Summary,
TurnCount: r.TurnCount,
TokenCount: r.TokenCount,
L0Abstract: r.L0Abstract,
SourceID: r.SourceID,
SourceType: r.SourceType,
CreatedAt: r.CreatedAt,
ExpiresAt: r.ExpiresAt,
RecallCount: r.RecallCount,
RecallScore: r.RecallScore,
LastRecalledAt: r.LastRecalledAt,
}
_ = ep.ID.Scan(r.ID)
_ = ep.TenantID.Scan(r.TenantID)
_ = ep.AgentID.Scan(r.AgentID)
ep.KeyTopics = []string(r.KeyTopics)
return ep
}
// episodicScoredRow is an sqlx scan struct for ftsSearch/vectorSearch in episodic_search.go.
type episodicScoredRow struct {
ID string `db:"id"`
SessionKey string `db:"session_key"`
L0Abstract string `db:"l0_abstract"`
Score float64 `db:"score"`
CreatedAt time.Time `db:"created_at"`
}
func (r *episodicScoredRow) toEpisodicScored() episodicScored {
return episodicScored{
id: r.ID,
sessionKey: r.SessionKey,
l0: r.L0Abstract,
score: r.Score,
createdAt: r.CreatedAt,
}
}