Files
goclaw/internal/store/sqlitestore/schema.sql
T
Viet Tran 8f56ddaa64 feat(v3): core architecture redesign — pipeline, memory, vault, evolution, providers, orchestration (#790)
* feat(v3): add core interface contracts and migration for v3 redesign

Foundation interfaces: TokenCounter, WorkspaceContext, DomainEventBus,
ProviderAdapter/Capabilities. Pipeline: Stage, RunState, MessageBuffer,
substates, Pipeline orchestrator. Memory: EpisodicStore, AutoInjector,
KG temporal extensions, consolidation workers. System integration:
PromptConfig, ToolCapability, Retriever. Orchestration: OrchestrationMode,
EvolutionMetrics/SuggestionStore. Migration 000037: episodic_summaries,
evolution tables, KG temporal columns. Schema version 36→37.

* refactor(plans): mark all v3 design phases complete with file references

* fix(v3): address code review findings on design contracts

- C1: add missing l0_abstract column to episodic_summaries migration
- C2: align EpisodicSummary ID/TenantID/AgentID to uuid.UUID
- H1: document tenant_id scoping requirement on EpisodicStore
- H2: add UNIQUE constraint on (agent_id, user_id, source_id) for dedup
- H4: clarify ProviderAdapter vs Provider relationship in doc
- M3: set state.ExitCode on BreakLoop/AbortRun in pipeline
- M6: store full PipelineConfig in Pipeline struct
- Edge: add WHERE embedding IS NOT NULL on HNSW index

* fix(v3): second-pass review fixes

- H1: use context.WithoutCancel for finalize + set ExitCode on ctx cancel
- H2: use utf8.RuneCountInString consistently in FallbackCounter
- H3: longest-prefix-match in ModelContextWindow (prevents wrong tokenizer)
- H4: return unsubscribe cleanup func from consolidation.Register

* feat(v3): implement DomainEventBus with worker pool, dedup, and retry

Worker pool processes events from buffered channel. SourceID-based dedup
prevents duplicate processing. Exponential backoff retry on handler error.
Panic recovery per handler. Graceful shutdown via Drain(). 8/8 tests pass
with race detector.

* feat(v3): implement ProviderAdapter for Anthropic, OpenAI, DashScope, Codex

Add CapabilitiesAware to all 6 providers. Create ProviderAdapter
implementations that delegate to existing buildRequestBody/parseResponse
for DRY. ClaudeCLI and ACP get capabilities only (subprocess transport).
DashScope wraps OpenAI adapter with StreamWithTools=false override.

* feat(v3): implement WorkspaceContext Resolver for 6 scenarios

Stateless resolver produces immutable WorkspaceContext at run start.
Handles personal/group/predefined/team-shared/team-isolated/delegation.
Wired into loop_context.go behind v3PipelineEnabled flag (additive,
v2 path unchanged). Includes delegation path boundary check,
master tenant bypass, and tenant slug path composition.

* feat(v3): implement tiktoken TokenCounter with BPE encoding + cache

Adds tiktoken-go for accurate cl100k_base/o200k_base token counting.
Per-message FNV-1a hash cache avoids re-encoding unchanged history.
Falls back to rune/3 heuristic for unknown models. NewTokenCounter
factory selects implementation at build time.

* feat(v3): promote 12 other_config JSONB fields to dedicated agent columns

Extract emoji, agent_description, thinking_level, max_tokens,
self_evolve, skill_evolve, skill_nudge_interval, reasoning_config,
workspace_sharing, chatgpt_oauth_routing, shell_deny_groups, and
kg_dedup_config from the catch-all other_config JSONB into proper
columns with DB-level types and defaults.

- Migration: PG (000037) + SQLite (schema v6→7) with backfill
- Go: AgentData struct + simplified Parse* methods
- Store: SELECT/INSERT/scan updated for both PG and SQLite
- Gateway: create/update handlers accept promoted fields
- HTTP: export/import with legacy backward compat
- Web UI: all 15 frontend files read/write from top level

* feat(v3): implement Knowledge Vault with unified search, wikilinks, and FS sync

Migration 000038 adds vault_documents (FTS+pgvector), vault_links, vault_versions
tables. VaultStore interface with PG implementation for document CRUD, hybrid
FTS+vector search, and bidirectional link management. All queries enforce
tenant_id isolation including JOIN-based scoping on link operations.

FS sync layer: SHA-256 content hashing, VaultInterceptor hooks into write_file/
read_file for auto-registration and lazy sync, fsnotify watcher with 500ms
debounce. Wikilink engine parses [[target]] syntax, resolves targets via
3-step strategy, and maintains vault_links on write.

VaultSearchService fans out queries across vault, episodic, and KG stores in
parallel with per-source score normalization and weighted merge. AutoInjector
and Retriever implementations for pipeline integration.

Three agent tools: vault_search (unified discovery), vault_link (explicit
linking), vault_backlinks (dependency tracing). Feature-flagged via
v3_vault_enabled agent setting.

* feat(v3): wire vault into gateway startup + add unit tests

Wire VaultStore embedding provider, VaultSearchService, VaultInterceptor
on read/write tools, and register vault_search/vault_link/vault_backlinks
tools in gateway_vault_wiring.go. All wiring gated by stores.Vault != nil.

Add 28 unit tests for ContentHash, ContentHashFile, and ExtractWikilinks
covering edge cases, unicode, display text, context windows, and offsets.

* feat(v3): implement stage-based pipeline loop with 8 pluggable stages

Decompose monolithic agent loop into internal/pipeline/ package:
- 6 stages: Context, Think, Prune+MemoryFlush, Tool, Observe+Checkpoint, Finalize
- Foundation types: Stage interface, RunState with 7 typed substates, MessageBuffer
- Pipeline orchestrator with setup/iteration/finalize 3-phase execution
- Callback-based PipelineDeps avoids circular import with agent package
- Feature-flagged via v3PipelineEnabled in Loop.Run()
- All 7 exit conditions preserved (no tools, max iter, truncation, loop kill,
  read-only streak, tool budget, ctx cancel)

* feat(v3): wire pipeline callbacks to Loop methods + add 71 unit tests

Wire 15 of 17 PipelineDeps callbacks from Loop methods via closures:
- Context: LoadContextFiles, BuildMessages, EnrichMedia, InjectReminders
- Think: BuildFilteredTools, CallLLM (stream/sync)
- Prune: PruneMessages, CompactMessages
- Memory: RunMemoryFlush
- Finalize: SanitizeContent, FlushMessages, UpdateMetadata, BootstrapCleanup, MaybeSummarize
- Remaining: ExecuteToolCall, CheckReadOnly (deep loop.go integration)

Add comprehensive test suite (71 tests, all passing with -race):
- MessageBuffer: 10 tests (append, flush, replace, counts)
- Pipeline.Run: 14 tests (3-phase flow, exit conditions, ctx cancel)
- Stage tests: 47 tests (ThinkStage nudges/truncation, PruneStage budget,
  ToolStage parallel/exit, ObserveStage content, CheckpointStage interval,
  FinalizeStage cleanup)

* feat(v3): wire remaining 2 callbacks (ExecuteToolCall, CheckReadOnly)

Complete callback wiring — 17/17 PipelineDeps callbacks now active:
- ExecuteToolCall: resolves tool name, executes via registry, processes
  result via existing processToolResult with loop detection bridge
- CheckReadOnly: delegates to checkReadOnlyStreak via bridge runState
- Bridge runState shares loop detection state between pipeline and agent

* fix(v3): eliminate data race in tool execution + capture injected messages

- Remove parallel tool execution path — serialize all tool calls to avoid
  data races on shared bridgeRS (loop detector, media results, deliverables)
- Loop kill checked after each tool (mid-batch early exit)
- BuildFilteredTools: capture and append injected tool-awareness messages
- Rename test to reflect sequential execution

* feat(v3): wire ResolveWorkspace, safe parallel tools, ContextStage tests

- Wire ResolveWorkspace callback via workspace.NewResolver() with
  ResolveParams from Loop fields (no longer a nil stub)
- Re-add safe parallel tool execution: split into ExecuteToolRaw
  (parallel I/O) + ProcessToolResult (sequential state mutation)
  with opaque rawData pass-through (no double execution)
- Add 12 unit tests for ContextStage (8) + MemoryFlushStage (3)
- Split tool callbacks to loop_pipeline_tool_callbacks.go (under 200 lines)
- Capture buildFilteredTools injected messages

* feat(v3): add episodic memory store + temporal KG columns

Phase 1 — Episodic Store:
- Migration 000039: episodic_summaries table with pgvector, FTS, L0 abstracts
- EpisodicStore PG impl: CRUD, hybrid FTS+vector search, ExistsBySourceID,
  PruneExpired. Idempotent via source_id UNIQUE constraint.

Phase 2 — Temporal KG:
- Migration 000040: valid_from/valid_until on kg_entities + kg_relations,
  partial indexes for current-facts queries, epoch→timestamptz backfill
- ListEntitiesTemporal: current-only, point-in-time, or include-expired modes
- SupersedeEntity: atomic expire-old + insert-new in single transaction

Schema version bumped to 40.

* fix(v3): review fixes for episodic store + temporal KG

- C1: Fix column name mismatch turn_count vs message_count in Go SQL
- C2: Remove redundant migration 000040 (000037 already adds temporal KG columns)
- H1: Use time.Time not int64 for TIMESTAMPTZ columns in SupersedeEntity
- H2: Add tenant_id scoping to Get/Delete for tenant isolation
- M2: Fix scanEntityTemporal to convert TIMESTAMPTZ→UnixMilli correctly
- L1: Remove unused uuid import from episodic_search.go
- Schema version corrected to 39 (only 000039 is new)

* feat(v3): implement consolidation pipeline with 3 event-driven workers

Event chain: session.completed → EpisodicWorker → episodic.created →
SemanticWorker → entity.upserted → DedupWorker

- EpisodicWorker: reuses compaction summary or calls LLM, generates L0
  abstract (extractive), idempotent via source_id check
- SemanticWorker: extracts KG facts from episodic summary via existing
  Extractor, sets temporal valid_from, publishes entity.upserted
- DedupWorker: runs DedupAfterExtraction on new entity IDs (terminal)
- L0 abstract: sentence-based extraction (~50 tokens), no LLM needed
- All workers registered via DomainEventBus.Subscribe()

* feat(v3): implement progressive loading with L0 auto-inject + unified search

- AutoInjector: searches episodic store, builds L0 prompt section (~200 tokens),
  skips trivial messages via stopword filter
- L1Cache: in-memory LRU (500 entries, 1h TTL) for structured overviews
- UnifiedSearch: cross-tier search merging episodic + document results by score
- ContextStage integration: AutoInject callback appends memory section to system prompt
- MemorySection field added to ContextState for observability

* feat(v3): add memory_expand tool for L2 episodic retrieval

New tool: memory_expand(id) returns full episodic summary with metadata.
Complements memory_search L0/L1 results with deep L2 access.
Nil-safe: returns error message when episodic store not available.

Gateway wiring + memory_search depth param + kg_search temporal param
deferred to runtime integration phase.

* feat(v3): complete Phase 5 — tool extensions + gateway wiring

- memory_search: add depth param + episodic tier search merged with docs
- kg_search: add as_of temporal param, use ListEntitiesTemporal
- memory_expand: registered in gateway startup
- Gateway: Episodic field in Stores, PGEpisodicStore in factory,
  embedding provider wired, tools connected to episodic store

* fix(v3): Phase 3 review fixes — tenant isolation + AutoInject args

- C1: Add tenant_id filter to ftsSearch, vectorSearch, List queries
  (prevents cross-tenant episodic memory leaks)
- C2: Fix AutoInject callback signature — agent/tenant captured by
  closure, only userMessage + userID passed explicitly
- H1: Add tenant_id to List query

* feat(v3): wire per-agent v3 flags from DB into dual-mode gate

Parse v3_pipeline_enabled, v3_memory_enabled, v3_retrieval_enabled from
agent other_config JSONB via ParseV3Flags(). Resolver now sets all flags
on LoopConfig so the existing gate in loop_run.go reads from DB.

- V3Flags struct + ParseV3Flags() + ValidateV3Flags() in store layer
- v3MemoryEnabled/v3RetrievalEnabled added to Loop, LoopConfig, PipelineConfig
- Auto-inject gated on V3RetrievalEnabled (was unconditional)
- Structured perf logging for v3 pipeline runs
- v3 flag validation on both WS agent.update and HTTP PUT endpoints

* feat(v3): wire AutoInjector into pipeline for L0 memory auto-inject

Create AutoInjector at gateway startup from episodic store, pass through
ResolverDeps → LoopConfig → Loop. Pipeline adapter builds AutoInject
callback capturing agent/tenant context via closure.

ContextStage already gates on V3RetrievalEnabled + AutoInject != nil.

* feat(v3): add tool metadata map + capability-based deny rules

Registry gains per-tool ToolMetadata map with RegisterWithMetadata()
and GetMetadata() (infers defaults from tool name when not explicit).
PolicyEngine gains DenyCapability() for RBAC integration — tools with
denied capabilities filtered at step 8 after existing 7-step pipeline.

* fix(v3): add RWMutex to PolicyEngine capability deny fields

DenyCapability() and SetRegistry() now guarded by sync.RWMutex.
FilterTools reads snapshot under RLock. Prevents data race when
capability rules are modified concurrently with tool filtering.

* feat(v3): implement delegate tool for inter-agent task delegation

New `delegate` tool wraps existing agent_links infrastructure
(CanDelegate, DelegateTargets). Supports async (fire-and-forget)
and sync (block with timeout) modes. Permission checked via
AgentLinkStore. Events emitted: delegate.sent/completed/failed.

DelegateRunFunc injected by gateway to avoid circular dependency.

* feat(v3): complete 3 deferred implementations

1. OrchestrationMode resolution: ResolveOrchestrationMode() checks
   team membership → delegate links → spawn (priority order).

2. PG EvolutionMetricsStore: RecordMetric, QueryMetrics, aggregate
   tool/retrieval metrics, TTL cleanup. All queries tenant-scoped.

3. BridgePromptBuilder: implements PromptBuilder interface by
   delegating to existing BuildSystemPrompt(). Appends v3 memory
   L0 section when enabled. Ready for template engine swap later.

* fix(v3): address code review findings on commits 5-6

- C1: CanDelegate now tenant-scoped (fail-closed on missing tenant)
- H1: Sync delegate timeout capped at 600s
- H2: Async goroutine gets 10min deadline (prevents leaks)
- H3: JSONB casts use COALESCE/NULLIF guards (handles missing fields)
- M1/M2: Remove dead code (formatVaultSection, memoryL0ToStrings)

* fix(teams): stop auto-creating agent_links for team members

Teams use agent_team_members table directly — agent_links caused
context confusion between team dispatch and delegation systems.

- Remove autoCreateTeamLinks() calls from team create + member add
- Remove link cleanup from member remove
- Remove dead autoCreateTeamLinks() function
- Append DELETE to migration 000039: clear team-created agent_links

* fix(v3): tenant isolation for all agent_links queries + PromptBuilder Instructions

- DelegateTargets, GetLinkBetween, SearchDelegateTargets,
  SearchDelegateTargetsByEmbedding, DeleteTeamLinksForAgent all now
  scoped by tenant_id (fail-closed on missing tenant)
- BridgePromptBuilder now maps Instructions/InstructionContent to
  AGENTS.md context file (was silently dropped)

* feat(v3): wire orchestration mode + evolution metrics into agent loop

- Orchestration mode: resolver resolves mode from team/links, tool filter
  hides delegate/team_tasks based on mode, prompt builder injects delegation
  targets section
- Evolution metrics: non-blocking goroutine records tool execution metrics
  (name, success, duration) via EvolutionMetricsStore in both v2 loop and
  v3 pipeline paths (sequential + parallel)
- Fix review findings: tenant ID propagated via store.WithTenantID in
  background goroutine, 5s timeout prevents goroutine leak

* feat(v3): implement suggestion engine with pluggable analysis rules

- PG EvolutionSuggestionStore: CRUD for agent_evolution_suggestions table
- SuggestionEngine: aggregates 7-day metrics, runs rules, deduplicates
  pending suggestions per type before creating new ones
- 3 initial rules: LowRetrievalUsage (usage_rate<0.2), ToolFailure
  (success_rate<0.1), RepeatedTool (>100 calls/week → suggest skill)
- EventSuggestionCreated event type added to eventbus
- Cron wiring deferred to gateway startup integration pass

* feat(v3): implement auto-adapt guardrails with apply/rollback

- AdaptationGuardrails: max delta per cycle, min data points, locked
  params, rollback-on-drop percentage
- ApplySuggestion: applies threshold suggestions to agent other_config
  JSONB, stores baseline for rollback
- RollbackSuggestion: restores baseline values from suggestion params
- EvaluateApplied: compares post-apply metrics to baseline, auto-rolls
  back when quality drops beyond threshold
- Scope limited to retrieval params only (never security settings)

* feat(v3): wire evolution stores + daily/weekly cron for suggestions

- Add EvolutionMetrics + EvolutionSuggestions to Stores struct + PG factory
- Wire EvolutionMetricsStore into ResolverDeps (cmd/gateway_managed.go)
- Add gateway_evolution_cron.go: daily suggestion analysis + weekly
  evaluation/rollback for applied suggestions
- Cron runs as background goroutine with 5-min timeout per cycle

* fix(v3): address code review findings on evolution engine

- C1: persist baseline parameters before marking suggestion as applied
  (was building map but never saving — rollback would always fail)
- H1: add tenant_id isolation to UpdateSuggestionStatus, GetSuggestion,
  and new UpdateSuggestionParameters method

* test(v3): add unit tests for orchestration, suggestions, guardrails, prompt

- orchestration_mode_test: orchModeDenyTools (4 modes) + ResolveOrchestrationMode
  (4 scenarios with mock stores)
- suggestion_rules_test: LowRetrievalUsage, ToolFailure, RepeatedTool with
  threshold boundary tests (at/below/above min data points)
- evolution_guardrails_test: DefaultGuardrails values + CheckGuardrails
  (insufficient data, locked params, zero-min fallback)
- prompt_builder_orchestration_test: BridgePromptBuilder orchestration section
  presence/absence across 4 scenarios + target content verification

* test(v3): add integration tests for evolution metrics + suggestions

- Test helper: shared PG connection with sync.Once migration, per-test
  tenant+agent seed with cleanup
- Evolution metrics: RecordMetric, AggregateToolMetrics (success rate),
  Cleanup (TTL deletion)
- Evolution suggestions: full CRUD, UpdateSuggestionParameters (baseline
  persist), tenant isolation (cross-tenant read blocked)
- Pipeline E2E: seed 25 failed tools + 55 low-usage retrievals, verify
  SuggestionEngine creates suggestions, verify dedup on second run
- Fix: migration 039 de-duped (episodic_summaries already in 037)
- Fix: NULL reviewed_by scan via sql.NullString

* feat(v3): add HTTP API handlers for evolution, vault, episodic, orchestration, v3-flags

5 new handler files exposing v3 backend stores as REST endpoints:
- evolution_handlers.go: metrics query/aggregate + suggestions CRUD
- vault_handlers.go: cross-agent document listing + search + links
- episodic_handlers.go: episodic summaries list + hybrid search
- orchestration_handlers.go: computed mode + delegate targets (read-only)
- v3_flags_handlers.go: per-agent v3 feature flag get/toggle

Store fixes from code review:
- episodic FTS: use inline to_tsvector (no stored tsv column)
- episodic: conditional user_id filter in List + Search (admin view)
- episodic: add tenant_id to ExistsBySourceID + PruneExpired
- evolution: require tenant_id in context (no struct fallback)
- evolution: check RowsAffected on suggestion updates
- vault: optional agent_id filter in ListDocuments (cross-agent)

* feat(v3): add web UI for evolution tab, v3 settings, vault page, episodic memory

Agent Detail enhancements:
- V3 Settings section: pipeline/memory/retrieval flag toggles
- Orchestration section: mode badge + delegate targets display
- Evolution section: added metrics + suggestions v3 flag toggles
- Evolution tab: Recharts metrics charts + suggestion review table
  with approve/reject/rollback actions + guardrails card

New pages:
- /vault: Knowledge Vault document registry with cross-agent listing,
  hybrid search dialog, document detail with wikilinks
- Memory page: added Episodic Memory tab with summary cards,
  expandable details, key topic badges, and hybrid search

Infrastructure:
- HttpClient: added patch() method
- Query keys: v3Flags, orchestration, evolution namespaces
- 4 new hooks: use-v3-flags, use-orchestration, use-evolution-metrics,
  use-evolution-suggestions, use-vault, use-episodic
- i18n: vault namespace (en/vi/zh), agents + memory keys updated
- Reused formatRelativeTime from lib/format.ts (eliminated 3 duplicates)

* refactor(http): add bindJSON helper and migrate all decode call sites

Replace 36 json.NewDecoder(r.Body).Decode + error blocks with bindJSON
across 20 HTTP handler files. Standardizes decode error responses to
structured writeError format. Fixes unchecked decode in handleIndexAll.

* refactor(store): adopt sqlx for PG scan operations (Phase 1+2)

Add jmoiron/sqlx v1.4.0 with camelToSnake json tag mapper.
Migrate scan-heavy PG store methods to sqlx Get/Select:
- tracing.go: GetTrace, ListTraces, ListChildTraces, GetTraceSpans, GetCostSummary
- heartbeat.go: Get, ListDue, ListLogs
- providers.go: GetProvider, GetProviderByName, ListProviders, ListAllProviders
- mcp_servers.go: GetServer, GetServerByName, ListServers
- pairing.go: ListPending, ListPaired
- agents_export_queries.go: 5 export functions
- agents_export_team_queries.go: exportTeamMembers, ExportAgentLinks

All writes (INSERT/UPDATE/DELETE), execMapUpdate, and dynamic WHERE
builders remain raw SQL. Zero behavior change.

* refactor(store): adopt sqlx for SQLite scan operations (Phase 3)

Migrate SQLite store scan methods to sqlx Get/Select:
- providers.go: GetProvider, GetProviderByName, ListProviders, ListAllProviders
- tenants.go: GetTenant, GetTenantBySlug, ListTenants, GetTenantUser, ListUsers, ListUserTenants
- mcp_servers.go: GetServer, GetServerByName, ListServers

Create sqlx_scan_structs.go with sqliteTime-aware scan structs
(providerRow, tenantRow, tenantUserRow, mcpServerRow) to handle
SQLite TEXT timestamp parsing via StructScan.

* refactor(store): migrate PG bulk scan operations to sqlx (Phase 4)

Migrate scan-heavy methods across 6 PG store files:
- tenant_store.go: GetTenant, GetTenantBySlug, ListTenants, GetTenantUser,
  ListUsers, ListUserTenants — removed 3 scan helpers
- teams.go: ListTeams, GetTeam, ListMembers, ListMembersByTenant
- teams_tasks_activity.go: ListComments, ListEvents, ListFollowUps
- pending_message_store.go: ListPending, ListByHistoryKey
- skills_grants.go: ListAgentGrants
- config_permissions.go: CheckPermission

~20 scan ops converted. Files with encryption post-processing,
pq.Array, pgvector, or dynamic SQL kept raw.

* refactor(store): extract shared CamelToSnake mapper, add UUIDArray usage note

- Move camelToSnake to internal/store/column_mapper.go (DRY)
- Both pg and sqlitestore packages now import shared CamelToSnake
- Add planned-use comment on UUIDArray type

* refactor(cli): migrate commands from config.json to HTTP API, add providers/setup/TUI

- Add unified HTTP client (gateway_http_client.go) with auth, error parsing, typed generics
- Rewrite agent list/add/delete to use gateway HTTP API instead of config.json
- Rewrite channels list to HTTP API, add channels add/delete subcommands
- Replace models command with full providers CRUD (list/add/update/delete/verify)
- Add setup wizard command (provider → agent → channel post-onboard flow)
- Add Bubble Tea TUI behind build tag (tui/!tui with noop fallback)
- Update onboard next-steps to mention goclaw setup
- Add build-tui Makefile target
- Fix URL path injection (url.PathEscape on all user-supplied path segments)
- Fix UTF-8 truncation in skills description display

* refactor(store): add explicit db struct tags, fix sqlx mapper for heartbeat scan error

Switch sqlx mapper from NewMapperFunc (which only applies CamelToSnake to
field names, not tag values) to NewMapperFunc("db", CamelToSnake) with
explicit db:"column_name" tags on all store structs.

Root cause: NewMapperFunc("json", fn) sets mapFunc but not tagMapFunc,
so camelCase json tags like "agentId" were used as-is instead of being
converted to "agent_id", causing "missing destination name" scan errors.

Fix: use db struct tags as the source of truth for column mapping.
Every DB entity field gets db:"column_name", nested JSON configs and
runtime-only structs get db:"-".

* test(store): add integration tests for 13 store interfaces (70 tests)

Cover Tier 1 (critical) + Tier 2 (security) stores with integration tests
running against pgvector pg18. Coverage from 2.4% to ~54%.

Stores tested: Session, Agent, Team/Task, Memory, KnowledgeGraph, Vault,
MCP Server, API Key, ConfigPermission, Contact.

Infrastructure: fixture builders (seedTeam, seedMCPServer, etc.),
mock EmbeddingProvider, multi-tenant helpers, expanded cleanup.

* fix(store): resolve NULL scan bugs in MCP server and task metadata

- mcp_servers: COALESCE nullable TEXT columns (display_name, command,
  url, api_key, tool_prefix) to prevent sqlx scan failures
- mcp_servers_access: COALESCE nullable JSONB columns in ListAgentGrants
  (tool_allow, tool_deny, config_overrides) to prevent silent row drops
- teams_tasks: default task metadata to '{}' instead of nil to satisfy
  NOT NULL constraint on CreateTask
- sqlx_helpers: export InitSqlx for integration test setup

* feat(pipeline): fix v3 pipeline context injection, tracing, KG temporal filters

- Pipeline context: add InjectContext + LoadSessionHistory callbacks to
  ContextStage, propagate enriched ctx via state.Ctx for iteration stages
- Pipeline tracing: wrap makeCallLLM with emitLLMSpanStart/End, wrap
  makeExecuteToolCall/Raw with emitToolSpanStart/End
- Token counter: switch pipeline from FallbackCounter to TiktokenCounter
- KG temporal: add valid_until IS NULL filter to all entity/relation
  queries (list, search, vector, FTS, traversal CTE, stats)
- Skills: add SkillEmbedder interface for future hybrid BM25+vector search
- Cache: remove unused tenantResolve dead code from PermissionCache
- Store: fix NULL scan bugs in tracing metadata and agent skill_nudge
- Test: add TestStoreKG_TemporalFilter integration test
- UI: add v3 version badge, evolution section, memory/traces improvements

* refactor(store): migrate KG store from raw sql.Rows to sqlx StructScan

Migrate 6 knowledge graph store files from manual rows.Scan() to
pkgSqlxDB.GetContext/SelectContext with intermediate scan row structs.

- Add entityRow, relationRow, traversalRow, dedupCandidateRow structs
  with json.RawMessage for jsonb and time.Time for timestamptz columns
- Add toEntity()/toRelation() converters (UnixMilli + json.Unmarshal)
- Add sqlxTx() helper for wrapping *sql.Tx with sqlx mapper
- Fix ScanDuplicates passing time.Now().Unix() to TIMESTAMPTZ column
- Fix ListEntitiesTemporal missing tenant scope (scopeClause)
- Fix SupersedeEntity missing tenant scope and tenant_id on INSERT
- Fix DedupCandidate.CreatedAt using Unix() instead of UnixMilli()
- Update agents_export_queries.go to reuse new scan row structs
- Net -160 lines of manual scan boilerplate removed

* refactor(store): migrate memory, skills, agents, sessions, mcp, cron, vault stores to sqlx

Batch migration of 19 store files from raw rows.Scan() to
pkgSqlxDB.GetContext/SelectContext with intermediate scan row structs.

Groups migrated:
- Memory: memory_docs, memory_admin, memory_search, memory_embedding_cache
- Episodic: episodic_search, episodic_summaries
- Skills: skills, skills_admin, skills_embedding, skills_export_queries
- Agents: agents (backfill+shares), agents_context, agents_export_team_standalone
- Sessions: sessions_list (List, ListPaged, ListPagedRich)
- MCP: mcp_servers_access, mcp_export_queries
- Cron: cron_exec (GetRunLog)
- Vault: vault_documents (ListDocuments, ftsSearch, vectorSearch)
- Tenant: tenant_configs (ListDisabled, ListAll)

7 new scan row files created. Net -510 lines of manual scan boilerplate.
INSERT/UPDATE/DELETE and scalar COUNT queries kept as raw SQL.

* fix(store): fix 3 sqlx scan struct db tag issues found by audit

- Fix vault FTS alias mismatch: `AS rank` → `AS score` (critical: runtime scan error)
- Fix episodic key_topics type: json.RawMessage → pq.StringArray (TEXT[] column)
- Fix agentShareRow.CreatedAt: string → time.Time, wire to output struct

* feat(providers): implement Wave 2 provider resilience and intelligence

9-phase implementation covering:
- Request middleware chain with composable body transformers
- OpenAI prompt caching, service tier, and fast mode middlewares
- Error classification (9 categories) with two-tier failover
- Model registry with forward-compat resolvers (Anthropic + OpenAI)
- Embedding providers (OpenAI + Voyage) with 1536-dim validation
- Cooldown/probe system with per-provider:model state tracking
- Markdown-aware chunking shared across 5 channels
- Session recall via FTS + pgvector on episodic summaries
- Dreaming/promotion pipeline for long-term memory consolidation

Migrations: 000040 (episodic search index), 000041 (promoted_at column)
Schema version: 39 → 41

* feat(providers): wire model registry into gateway provider construction

Create InMemoryRegistry with Anthropic + OpenAI forward-compat resolvers
at gateway startup. Pass to all Anthropic and OpenAI providers created
from both config and DB sources.

* feat(consolidation): wire DomainEventBus and consolidation pipeline

Create DomainEventBus at gateway startup, thread through resolver →
LoopConfig → Loop → PipelineDeps. Emit session.completed event after
each run finalization. Register consolidation pipeline (episodic →
semantic → KG dedup → dreaming) with event bus subscriptions.

* fix(store): fix episodic key_topics pq.Array, ON CONFLICT, and migration 040 immutability

- episodic_summaries.go Create: json.Marshal(KeyTopics) → pq.Array (text[] column)
- episodic_search.go scanEpisodic/scanEpisodicRow: json.RawMessage → pq.StringArray
- episodic_summaries.go Create: ON CONFLICT add WHERE source_id IS NOT NULL for partial index
- migration 040: add immutable_array_to_string wrapper (array_to_string is STABLE in PG)

* test(store): add 17 integration tests for skills, cron, episodic, tenant configs

- Skills store: 6 tests (CRUD, grants, tenant isolation)
- Cron store: 4 tests (job CRUD, run log sqlx scan, pagination, tenant isolation)
- Episodic store: 4 tests (summary CRUD, list, FTS search, tenant isolation)
- Tenant configs: 3 tests (tool/skill disable, list, tenant isolation)
- Test helper: add cleanup for skills, cron, episodic tables

* fix(permissions): use cron-specific permission check for cron tool (#725)

* fix(security): harden exec path exemption matching (#721)

- Add absolute path exemption for dataDir/skills-store/ (fixes skill
  scripts using absolute paths like /app/data/skills-store/ being denied)
- Strip surrounding quotes before prefix matching (LLMs often quote paths)
- Reject path traversal ("..") in exempt fields to prevent escape
- Switch from "any field exempt → skip" to per-field matching: only exempt
  if ALL fields that match the deny pattern are individually exempt
- Closes pipe/comment bypass vectors where an exempt path in one argument
  would exempt the entire command including non-exempt paths

Includes 27 test cases covering: legitimate access, quoted paths,
path traversal, unicode bypass, pipe/comment bypass, mixed args.

* fix(permissions): use cron-specific permission check for cron tool

Cron tool was hardcoded to check `file_writer` configType via
CheckFileWriterPermission(), ignoring the `cron` configType that
the UI actually saves when granting cron permissions. This caused
agents in group chats to be denied cron access even with correct
permission configured.

Add ConfigTypeCron constant and CheckCronPermission() that checks
`cron` configType first, falling back to `file_writer`.

---------

Co-authored-by: Viet Tran <viettranx@gmail.com>

* fix(chat): load message history on first conversation click (#730)

* fix(chat): load message history when selecting existing conversation from clean state

The skipNextHistoryRef was unconditionally set when sessionKey transitioned
from empty to non-empty. This prevented loadHistory() from running when
clicking an existing conversation from the initial /chat page. The skip
was only intended for the new-chat send flow where the optimistic message
is already displayed.

Guard the skip with expectingRunRef so it only activates when a message
send is in flight.

Closes #729

* docs: add UI diff evidence for PR #730

Before/after screenshots and HTML comparison report showing
first conversation click behavior fix.

* feat(whatsapp): port native WhatsApp channel with whatsmeow from dev

Cherry-pick 0db1e93a with manual conflict resolution:
- cmd/channels_cmd.go: kept dev-v3 HTTP API approach (not config-based)
- go.mod/go.sum: merged deps, ran go mod tidy

* feat(ui): v3 web UI enhancement — branded loading, rich markdown, vault graph, sidebar polish

- Branded loading: HTML pre-loader with logo pulse/shimmer, fade-out on app ready, PageLoader logo swap
- Rich markdown: wikilinks, mermaid (lazy-loaded), math/KaTeX, callouts/admonitions plugins
- Vault graph: force-directed document graph view with table/graph toggle
- Agent filters: type filter (open/predefined) on agents page
- Sidebar: tenant name + role badge in footer
- Query keys: add vault + episodic entries

* fix(ui): address code review — mermaid XSS, Safari compat, cache key

- Change mermaid securityLevel from "loose" to "strict" (XSS prevention)
- Add requestIdleCallback fallback for Safari < 17
- Fix vault all-links query key to use sorted doc IDs (stale cache fix)
- Remove dead abortRef from MermaidBlock
- Add prefers-reduced-motion to loader animation

* docs: update CLAUDE.md with v3 architecture + complete changelog

- Add 10 new v3 internal modules to project structure (pipeline, eventbus, consolidation, tokencount, vault, workspace, etc.)
- Add native WhatsApp channel, edition system, providerresolve, updater to structure
- Update Key Patterns with 8 v3-specific patterns (pipeline, eventbus, 3-tier memory, vault, evolution, orchestration, middleware)
- Add comprehensive V3 Redesign section to changelog covering:
  - 8-stage pipeline with dual-mode gate
  - DomainEventBus + consolidation workers
  - 3-tier memory (working/episodic/semantic)
  - Knowledge Vault with wikilinks
  - Self-evolution engine
  - Orchestration + delegate tool
  - WorkspaceContext resolver
  - ModelRegistry + provider adapter
  - Feature flags
  - Request middleware
  - sqlx migration + 70+ integration tests

* fix(security): harden file path validation, tenant isolation, and tool access

- handleSign: validate path within workspace/dataDir before signing HMAC token
- handleSign: enforce tenant-scoped restriction for RBAC-enabled editions
- handleServe: add workspace/dataDir boundary check for ft= signed requests
- handleServe: remove cross-tenant findInWorkspace fallback for ft= requests
- TenantDataDir/TenantWorkspace: guard against empty slug resolving to parent
- exec tool: add tenants/ to AllowPathExemptions for tenant skill execution
- list_files: add AllowPaths support and wire skills directory access

* docs(v3): update 16 docs + add 2 new docs for v3 architecture

Update all docs/ to reflect v3 implementation (64 commits, 31K insertions):
- 00: architecture overview with 7 new packages
- 01: agent loop with pipeline, orchestration, evolution
- 02: providers with Wave 2 resilience (middleware, failover, registry)
- 03: tools with delegate, vault_search, vault_link, memory_expand
- 04,08,10,11,14,23: targeted v3 additions
- 06: store with 6 new tables, promoted columns, sqlx
- 07: memory with 3-tier architecture, consolidation pipeline
- 18,19: HTTP/WS API with v3 endpoints and methods
- 21: evolution system (metrics, suggestions, auto-adapt)
- model-steering: model registry relationship

New docs:
- 22-v3-http-endpoints.md: 12 v3 HTTP endpoints
- 24-knowledge-vault.md: vault architecture, wikilinks, search

* feat(v3): wire delegate tool, fix pipeline callbacks, clean dead code

Wire delegate tool end-to-end:
- Register DelegateTool in gateway with DelegateRunFunc
- Implement runFn: resolve target agent, build session key, propagate tracing
- Add async announce via msgBus (reuses subagent announce handler)
- Populate context fields (TenantID, Channel, ChatID) for routing
- Set DelegationID + ParentAgentID on RunRequest for event correlation

Fix pipeline callbacks:
- BreakLoop now completes remaining stages in current iteration
- EnrichMedia signature updated to use RunState for message buffer access
- Add non-streaming event emission for channel compatibility
- Fix user message flush tracking in v3 pipeline

Clean dead code (510 LOC removed):
- Delete memory/l1_cache.go, unified_search.go (superseded by vault search)
- Delete vault/auto_injector_impl.go, retriever_impl.go (never wired)
- Delete vault/sync_worker.go (never started)
- Remove orphaned EventMemoryLint, EventSuggestionCreated constants

* feat(v3): finalize stage — emit session.completed, NO_REPLY, strip directives

- Emit session.completed event for consolidation pipeline (episodic → semantic → dreaming)
- Detect NO_REPLY before flush so silent content is persisted for context
- Strip [[...]] message directives from user-facing content (v2 parity)
- Wire StripMessageDirectives, IsSilentReply, EmitSessionCompleted callbacks

* feat(vault): full CRUD — backend endpoints, UI dialogs, content preview

Backend (5 new HTTP endpoints):
- POST/PUT/DELETE /v1/agents/{id}/vault/documents — create, update, delete
- POST/DELETE /v1/agents/{id}/vault/links — create, delete
- Server-side validation for doc_type and scope enums
- Agent ownership verification on link creation
- FK cascade handles link cleanup on document delete

Frontend (React):
- Create document dialog (title, path, type, scope)
- Edit mode in detail dialog (inline title/type/scope editing)
- Delete document with confirmation
- Create link dialog (from/to doc, link type, context)
- Delete link with inline confirmation on badges
- Content preview (collapsible, lazy-loads via /v1/storage/files/)
- Mutation hooks with query invalidation
- i18n keys for en/vi/zh

* fix(v3): critical pipeline parity fixes — ChatRequest, reasoning, passback, media

- Enrich ChatRequest with all provider options (temperature, sessionKey,
  agentID, userID, channel, workspace, tenantID) matching v2
- Add ResolveReasoningDecision for thinking models (o3, DeepSeek-R1, Kimi)
- Wire uniquifyToolCallIDs to prevent OpenAI 400 on duplicate IDs
- Add assistant message passback (Phase, RawAssistantContent) for Anthropic
- Emit block.reply for intermediate content (non-streaming channels)
- Add ContentSuffix append + ForwardMedia merge in FinalizeStage
- Build final assistant message with MediaRefs for session persistence
- Use effectiveMaxTokens() + OptMaxTokens constant
- Guard truncation retry on len(ToolCalls) > 0 + parseError check
- Accumulate ThinkingTokens in usage
- Deduplicate emitRun closure (shared from callbackSet)
- Fix EnrichMedia to receive RunState with actual messages
- Persist user message in makeFlushMessages (matching v2)

* fix(store): coerce NOT NULL JSONB columns to empty object on agent update

When switching provider away from ChatGPT OAuth, the UI sends
chatgpt_oauth_routing: null which violates the NOT NULL constraint.
Coerce null → '{}' for all NOT NULL JSONB promoted columns:
chatgpt_oauth_routing, reasoning_config, workspace_sharing,
shell_deny_groups, kg_dedup_config.

* feat(v3): v3 info modal redesign, agent links CRUD tab, sidebar rename

- Rewrite v3 info modal with 8 feature cards (pipeline, memory,
  retrieval, vault, evolution, orchestration, resilience, registry)
  with v2→v3 comparisons, stat badges, full i18n (en/vi/zh)
- Add tabbed layout to teams page: Agent Teams | Agent Links tabs
- Agent Links tab with full CRUD via existing WS RPC methods
  (list/create/edit/delete) with Radix Select, Combobox, mutual
  agent exclusion in create dialog
- Sidebar menu renamed to "Agent Link & Team"
- Backend: add source_display_name, source_emoji, target_emoji
  to agent link joined queries for consistent display

* fix(store): include personal chats in cron delivery targets

Add 'user' contact_type to ListDeliveryTargets SQL filter in both
PG and SQLite stores. Previously only group/topic contacts appeared
in cron channel/chat dropdowns.

* fix(v3): duplicate messages, missing thinking, span numbering

- ThinkStage: skip AppendPending for final answer (no tool calls),
  let FinalizeStage build the definitive message with sanitization
  and MediaRefs — fixes duplicate assistant messages in session history
- RunResult: add Thinking field, propagate through v2 finalizeRun,
  v3 convertRunResult, run.completed event, and chat.send response
- UI: capture thinkingRef before clearing on run.completed, include
  in final message object so thinking renders without page refresh
- Span numbering: pass Iteration+1 in v3 callback to match v2's
  1-based iteration display in trace span names

* fix(v3): wire delegation targets into BuildSystemPrompt

buildOrchestrationSection() was implemented and tested but never
wired into the actual BuildSystemPrompt() flow. Only the unused
BridgePromptBuilder had it. Add DelegateTargets + OrchMode fields
to SystemPromptConfig and inject "## Delegation Targets" section
so agents with agent_links see their delegation targets in prompt.

* fix(v3): sync mediaResults from bridgeRS to pipeline state

syncBridgeToState copied loopKilled, asyncToolCalls, deliverables
from the v2 bridgeRS but missed mediaResults. Tool results with
MEDIA: prefix were extracted by processToolResult into bridgeRS
but never propagated to state.Tool.MediaResults — causing
FinalizeStage to produce empty MediaRefs and RunResult.Media.

* fix(v3): populate SessionCompletedPayload in session.completed events

Both v2 loop and v3 pipeline emitted session.completed events with
nil Payload, causing episodicWorker type assertion to fail silently.
Episodic summaries were never created.

- Expand EmitSessionCompleted callback to pass msgCount, tokensUsed,
  compactionCount from pipeline state
- V3 path: build payload from state.Messages.TotalLen(),
  state.Think.TotalUsage, state.Compact.CompactionCount
- V2 path: build payload from history + rs.totalUsage +
  sessions.GetCompactionCount()

* fix(v3): remaining pipeline parity gaps — skill postscript, team task count

- Add SkillPostscript callback to PipelineDeps + FinalizeStage,
  matching v2's skill evolution nudge after complex tool runs
- Wire makeSkillPostscript() in adapter with same logic as v2
  (skillEvolve + skillNudgeInterval + totalToolCalls threshold)
- Sync teamTaskCreates from bridgeRS to pipeline EvolutionState
  (was already syncing teamTaskSpawns but missed creates)

* fix(evolution): add JSON struct tags to metric aggregates

ToolAggregate and RetrievalAggregate had no JSON tags, causing
Go to marshal field names as PascalCase (ToolName, CallCount)
while the UI expects snake_case (tool_name, call_count). Charts
rendered empty containers with no data bars.

Also change AvgDuration (time.Duration) to AvgDurationMs (float64)
for JSON-friendly serialization — time.Duration marshals as
nanoseconds which is unusable in frontend.

* fix(v3): add debug logging to episodic worker for consolidation pipeline

Add INFO/WARN logs at entry, summary decision, and creation points
to diagnose why episodic_summaries table stays empty in production.

* chore: silence noisy tenant_cache debug logs

* refactor(ui): replace v3 settings section with engine version picker + tabbed info modal

Replace flat toggle list with radio-style version cards (v2/v3) and
feature mini-cards. Redesign v3 info modal from single scroll to 3-tab
layout (Core Engine, Memory & Knowledge, Orchestration) with Lucide
icons and v2→v3 comparison cards.

- Add batchUpdate to use-v3-flags hook for atomic v2 switch
- Create engine-version-section with VersionCard + FeatureMiniCard
- Create v3-info-modal/ with 5 modular components (<45 LOC each)
- Add i18n keys (detail.engine + v3Info.tabs) for en/vi/zh
- Wire into agent-overview-tab, agent-header, agent-card
- Delete v3-settings-section.tsx + agent-v3-info-modal.tsx

* feat(v3): pass media files through delegate tool results

Delegate tool now carries media files (images, audio, etc.) produced
by the delegatee back to the parent agent. DelegateRunFunc returns
DelegateResult{Content, Media} instead of plain string.

- Add DelegateResult struct with Content + Media fields
- Convert agent.MediaResult to bus.MediaFile in gateway wire
- Attach media to sync result and async announce message
- Add MediaCount to DelegateCompletedPayload for observability
- Set MetaParentAgent in async announce metadata

* merge: bring main bug fixes into dev-v3

Cherry-pick 8 commits from origin/main:
- fix(telegram): handle group-to-supergroup migration (#698)
- feat(providers): add OpenRouter identification headers (#705)
- fix: deterministic prompt ordering for LLM cache hit (#719)
- fix(security): harden exec path exemption matching (#721)
- fix: invalidate storage size cache on delete and move (#726)
- fix: use errors.Is() for sentinel comparisons (#727)
- fix(desktop): add defaultValues to form dialogs (#737)
- Release: credential resolver, WhatsApp native, exec hardening (#754)

Conflict resolution:
- store/pg exports: accept main's errors.Is() additions
- shell.go: accept main's extracted matchesAnyPathExemption helper
- gateway_providers: merge both WithAnthropicName + WithAnthropicRegistry
- loop_types + resolver: merge v3 fields (OrchMode, DelegateTargets,
  EvolutionMetricsStore) with main's new UserResolver/ContactStore
- gateway_setup: keep dev-v3's tenant-scoped path exemptions
- channels_cmd: keep dev-v3's HTTP API approach

* feat(v3): add foundation packages for architecture refactor (Phase 1)

Purely additive — zero changes to existing files. Creates shared types
and helpers that Phase 2-4 will migrate callers to:

- internal/store/base/: Dialect interface, BuildMapUpdate, nullable/JSON
  helpers, scope clause builder, table metadata (44 tests)
- internal/orchestration/: ChildResult capture from v2/v3, media type
  conversion with round-trip tests (10 tests)
- internal/providers/sse_reader.go: Shared SSE scanner replacing inline
  bufio.Scanner boilerplate in 3 providers (8 tests)

* refactor(store): unify pg/ and sqlitestore/ helpers via base/ package (Phase 2)

- Create PG and SQLite dialect implementations (base.Dialect interface)
- Replace 15 duplicate helpers in pg/helpers.go with aliases to base.*
- Replace 13 duplicate helpers in sqlitestore/helpers.go with aliases
- Rewrite execMapUpdate and execMapUpdateWhereTenant to use
  base.BuildMapUpdate with dialect-specific placeholders
- Rewrite scopeClause/scopeClauseAlias as thin wrappers around
  base.BuildScopeClause
- Remove duplicate execMapUpdateWhereTenant from pg/agents.go

pg/helpers.go: 267→175 LOC, sqlitestore/helpers.go: 226→130 LOC

* refactor(orch): extract BatchQueue[T] generic for announce queues (Phase 3)

- Create internal/orchestration/batch_queue.go: generic producer-consumer
  queue replacing duplicated sync.Map+mutex pattern (10 tests, race-safe)
- Simplify cmd/gateway_announce_queue.go: team queue uses BatchQueue,
  removes announceQueueState/getOrCreate/drain/tryFinish (~40 LOC saved)
- Simplify cmd/gateway_subagent_announce_queue.go: subagent queue uses
  BatchQueue, same pattern reduction (~40 LOC saved)
- Update cmd/gateway_consumer_handlers.go: callers use new signatures

* fix: remove defer accumulation in announce loop, clean comment tombstone

- Remove `defer ptd.ReleaseTeamLock()` inside for loop that accumulated
  deferred calls per iteration (explicit ReleaseTeamLock already called)
- Remove dead comment tombstone in pg/agents.go

* fix: scope defer in announce loop via closure for panic safety

Wrap announce processing in closure so defer ptd.ReleaseTeamLock()
runs once per iteration instead of accumulating. Explicit release
still called for normal path; defer catches panics.

* refactor(providers): wire shared SSEScanner into 3 providers (Phase 4b)

Replace inline bufio.Scanner+SSE parsing boilerplate with shared
SSEScanner from sse_reader.go in:
- openai.go: data-only SSE (OpenAI, DashScope, Kimi)
- codex.go: data-only SSE (OpenAI Codex)
- anthropic_stream.go: event+data SSE (uses EventType() for switch)

Removes ~24 LOC of duplicated scanner setup + manual line parsing.

* refactor(agent): force v3 pipeline, remove v2 runLoop (Phase 4A)

- Delete runLoop() from loop.go (~745 LOC removed), keep shared helpers
  (resolveToolCallName, hasParseErrors, truncateToolArgs)
- Remove v2/v3 gate in loop_run.go: always call runViaPipeline()
- Remove v3PipelineEnabled field from Loop struct + LoopConfig + resolver
- Always resolve workspace in loop_context.go (was behind v3 gate)
- Deprecate PipelineEnabled in V3Flags (kept for backward compat parsing)

All agents now always use v3 pipeline. No behavioral change for agents
that already had v3_pipeline_enabled=true (which was all production agents).

* refactor(gateway): decompose gateway.go from 1295 to 476 LOC (Phase 4B)

Extract sections of runGateway() into focused files:
- gateway_deps.go: gatewayDeps struct for shared state
- gateway_http_wiring.go: wireHTTPHandlersOnServer (~207 LOC)
- gateway_events.go: event subscribers + teamTaskEventType (~367 LOC)
- gateway_lifecycle.go: signal handling, shutdown, server start (~222 LOC)
- gateway_tools_wiring.go: cron/heartbeat/session tool wiring (~116 LOC)

Also extracted: startCronAndHeartbeat, makeDelegateAnnounceCallback.
Pure structural refactoring — no behavior change.

* test(agent): add v3 force migration guard tests

Verify v2 runLoop is deleted, V3PipelineEnabled field removed from
LoopConfig, and V3Flags backward compat parsing still works.
Compile-time guards prevent accidental re-introduction of v2 code.

* feat(delegate): wire ChildResult + fix media passthrough (Phase 3 gap)

- Use orchestration.CaptureFromRunResult in delegate run callback
  to standardize result capture via ChildResult
- Use MediaResultToBusFiles in CaptureFromRunResult (DRY)
- Fix delegate_tool.go metadata key: delegate_id → delegation_id

* refactor(ui): remove v2/v3 pipeline toggle, always show V3 (Phase 4E)

- engine-version-section.tsx: remove pipeline toggle, show V3 read-only
- use-agent-version.ts: always return "v3" (no flag check)
- use-v3-flags.ts: remove v3_pipeline_enabled from toggleable flags
- Update i18n strings (en/vi/zh): remove v2-specific tooltip text

* test: add Phase 5 test infrastructure for v3 architecture refactor

- pg/pg_dialect_test.go: 4 tests for PG Dialect (placeholder, transform,
  returning, interface compliance)
- sse_reader_test.go: 4 edge case tests (empty data, scanner error,
  event type persistence, no data after [DONE])
- gateway_announce_format_test.go: 10 tests for team + subagent
  announce formatting (single/batch, success/failure, snapshot)

Coverage: base/ 96%, orchestration/ 100%, providers/ 57%

* chore: remove stale runLoop references + apply go fix (Phase 6)

- Update comments referencing deleted runLoop in pipeline callbacks,
  loop_types, stage.go
- go fix: reflect.TypeFor, range over int, strings.Builder

* docs: update architecture docs for v3 refactor completion (Phase 6)

- CLAUDE.md: add store/base/, orchestration/ to project structure;
  remove v2 runLoop and dual-mode gate references; rename Pipeline (v3)
  to Pipeline; add SSEScanner and BatchQueue to key patterns
- docs/00-architecture-overview.md: update module map with new packages,
  remove [V3] markers (now standard)
- docs/17-changelog.md: add V3 Architecture Refactor entry (6 phases)

* feat(evolution): skill draft auto-generation + go fix cleanup

- Add skill draft template generation from evolution suggestions
- Wire skill apply endpoint in evolution HTTP handlers
- Apply go fix across codebase (range-over-int, reflect.TypeFor, etc.)
- Minor refactors: simplify switch/case, reduce string builder allocs
- Gateway deps: add skills loader field

* perf(prompt): deterministic tool order + Anthropic cache boundary split

Sort tool names in buildToolingSection for cache-stable output.
Insert GOCLAW_CACHE_BOUNDARY marker before Time section; Anthropic
provider splits system prompt into 2 blocks (stable cached, dynamic not).
Backward compat: no marker → single cached block.

* perf(prompt): optimize cache boundary position + add Execution Bias

Move Memory Recall and stable context files (AGENTS.md, TOOLS.md,
USER_PREDEFINED.md) above cache boundary. Dynamic per-user files
(USER.md, BOOTSTRAP.md) stay below. Add Execution Bias section
(full mode only) forcing action-oriented tool use. Fix duplicate
header when Project Context split across boundary.

* feat(prompt): add PromptMode task/none + 3-layer resolution

Expand PromptMode from full|minimal to full|task|minimal|none.
Task mode = enterprise automation: keeps Tooling, Execution Bias,
Safety-slim, Persona-slim, Skills-search, MCP-search, Memory-slim,
Workspace, Runtime, Delegation. Drops verbose sections (Tool Call
Style, Self-Evolution, Spawning, Recency, etc.).

None mode returns identity line only. 3-layer mode resolution:
runtime override > auto-detect (subagent/cron) > agent config
(other_config.prompt_mode) > default (full).

* feat(prompt): provider prompt contributions (stable/dynamic/overrides)

Add PromptContribution struct + PromptContributor interface for
provider-specific prompt customizations. Providers can inject
StablePrefix (before cache boundary), DynamicSuffix (after boundary),
or override sections by ID (e.g. execution_bias). Nil-safe: providers
that don't implement the interface get default behavior.

* feat(prompt): pinned skills with hybrid inline+search mode

Add per-agent pinned_skills config (max 10, from other_config JSONB).
Pinned skills always inline in prompt via BuildPinnedSummary.
Non-pinned discovered via skill_search. Hybrid section shows both
pinned XML and search instructions. Works in full and task modes.

* fix(prompt): validate prompt_mode from DB before cast

Reject unknown prompt_mode values from OtherConfig JSONB
(e.g. typos like "taks") by checking against validPromptModes set.
Invalid values default to "" (full mode). Prevents broken prompts
where no mode flags match.

* fix(prompt): wire SectionIDToolCallStyle for provider override

Tool Call Style section now uses sectionContent() like Execution Bias,
allowing providers to override it via PromptContribution.SectionOverrides.

* feat(store): implement 9 SQLite store backends for v3 parity

Close feature gap between PostgreSQL and SQLite (desktop/lite) editions.
96 methods across 9 stores: AgentLinks, SubagentTasks, SecureCLI,
SecureCLIGrants, EvolutionMetrics, EvolutionSuggestions, Episodic,
KnowledgeGraph, Vault. Schema v8→v9 adds 4 tables.

Key design decisions:
- LIKE-based search replaces tsvector/FTS5 (unavailable in modernc.org/sqlite)
- Go-side StringSimilarity for KG dedup (replaces pgvector cosine)
- Recursive CTE traversal with comma-delimited path cycle detection
- AES-256-GCM encryption on all SecureCLI read/write paths
- F15: SecureCLI disabled when EncryptionKey empty
- ON CONFLICT DO UPDATE (never INSERT OR REPLACE) to preserve FK cascades

* docs(store): add SQLite parity section to store data model docs

Document 9 new SQLite store implementations, schema v9, and
feature parity gaps (LIKE vs FTS, Jaro-Winkler vs vector dedup).

* fix(docker): skip web-builder stage when ENABLE_EMBEDUI=false

Use BuildKit conditional stage pattern so web-builder is not executed
when embedding is disabled. Also update pnpm-lock.yaml for 6 new deps
that were missing from the lockfile (markdown/math/mermaid packages).

* feat(vault): embed metadata.summary for richer vector search

Include summary from metadata JSONB in embedding text (title + path +
summary) for better semantic search. Update tsvector generated column
to include summary in FTS index. Zero new columns — summary stored in
existing metadata JSONB field. Backward compat: docs without summary
still embed title+path only.

* fix(vault-ui): wider detail dialog, markdown rendering, create tooltip

- Detail dialog: sm:max-w-lg → sm:max-w-2xl, content area 200→300px
- Content preview: render markdown via MarkdownRenderer instead of <pre>
- Create button: add tooltip explaining "select agent first" when disabled

* feat(ui): prompt_mode dropdown + pinned_skills multi-select

Add PromptSettingsSection to agent overview tab:
- prompt_mode: Select dropdown (full/task/minimal/none)
- pinned_skills: Tag input with max 10, click to remove
Both save to agent other_config JSONB via existing onUpdate flow.
Self-contained save button appears only when values change.

* fix(vault-ui): enlarge dialog to max-w-4xl, constrain markdown heading sizes

Dialog sm:max-w-2xl → sm:max-w-4xl. Markdown headings capped at
text-base/text-sm via Tailwind child selectors to prevent oversized
h1/h2 in content preview. Content area raised to 400px.

* refactor(ui): move pinned skills to dedicated section with skill select

Split pinned_skills out of PromptSettingsSection into PinnedSkillsSection.
Uses useAgentSkills hook for proper dropdown with granted skills list.
Placed right below SkillsSection in agent overview tab. Badge chips
with X to remove, Select dropdown to add. Max 10 enforced.

* fix(ui): add border wrap to capabilities section for visual consistency

* feat(ui): redesign prompt mode as compact cards, replace v3 badge

- Rewrite prompt settings from select dropdown to 2×2 compact cards
  with lucide icons (Zap/Wrench/Package/CircleOff) and ring selection
- Move prompt settings to top of agent overview tab
- Replace v3 badge with prompt mode badge in header, card, and list row
- Add i18n keys for prompt mode labels/descriptions (en/vi/zh)
- Extract readPromptMode() to shared agent-display-utils
- Remove dead useAgentVersion hook and v3Tooltip/v2Tooltip i18n keys
- Remove v3 badge from EngineVersionSection (keep feature toggles)
- Use cn() for badge class composition with twMerge safety

* feat(ui): add section tags and token estimates to prompt mode cards

Each card now shows which system prompt sections are included
(Persona, Tools, Safety, Skills, MCP, Memory, etc.) as compact
tags, plus estimated base token range (~2-4K for full, ~10 for none).
Helps users understand the impact of each mode at a glance.

* fix(ui): correct token estimates with actual tiktoken measurements

Measured via tiktoken (cl100k) on realistic config:
full=~1.7K+, task=~1.1K+, minimal=~660, none=~6 base tokens.
The "+" suffix indicates context files/skills/MCP add more.

* fix(ui): use tiktoken-measured token ranges for prompt mode cards

Measured across 3 scenarios (bare/typical/heavy) via tiktoken cl100k:
full=~500-2.9K, task=~350-1.2K, minimal=~350-820, none=~6 tokens.
Ranges reflect real configs from minimal (3 tools) to heavy
(10 tools, long persona, MCP, sandbox, pinned skills).

* fix(ui): use production-measured token counts for prompt mode cards

Measured via tiktoken on real production agent (tieu-ho) with
4 context files (AGENTS.md, SOUL.md, IDENTITY.md, USER_PREDEFINED.md),
14 tools, memory, KG, skills, Telegram channel:
full=~3.1K, task=~2.2K, minimal=~1.9K, none=~6 tokens.

* refactor(ui): accurate section tags per prompt mode from systemprompt.go

Section tags now match exact gating logic in BuildSystemPrompt():
- full: persona, tools, exec bias, call style, safety, skills,
  MCP, memory, sandbox, evolution, channel hints (11 sections)
- task: style echo, tools, exec bias, safety (slim), skills (search),
  MCP (search), memory (slim) (7 sections)
- minimal: tools, safety (2 sections + shared context files)
- none: no tags shown, no token count (trivially ~6 tokens)

Removed workspace/identityOnly tags (shared across all modes).

* feat(ui): replace engine version toggles with V3 capabilities modal

- Remove 3 toggle switches (Pipeline, Memory, Retrieval) from agent
  detail — all features are now always-on for v3 agents
- Replace with compact static badges layout
- New V3 Capabilities modal with 4 tabs:
  Pipeline (8-stage flow), Memory (L0/L1/L2 tiers),
  Knowledge (KG, Vault, Dreaming), Orchestration (Delegate, Evolution)
- Each tab has Lucide icons and technical descriptions of how
  features actually work
- Separate i18n namespace v3-capabilities in en/vi/zh locales

* refactor(ui): move V3 badge to agent header, remove engine version section

- Add clickable V3 badge next to agent name in header
- Clicking opens V3 Capabilities modal
- Remove Engine Version section from overview tab entirely
- Remove unused EngineVersionSection import

* feat: v3 prompt engine overhaul — 7-phase restructuring

Phase 1: Fix mode resolution — subagent/cron cap at task (not minimal),
heartbeat stays minimal, pinned skills injected in all modes, USER_PREDEFINED
added to minimal allowlist.

Phase 2: Context file restructuring — new CAPABILITIES.md (domain expertise,
separated from SOUL.md), AGENTS_MINIMAL.md for heartbeat sessions, both added
to stable context files and minimal allowlist.

Phase 3: Summoner update — all 4 prompt builders generate CAPABILITIES.md,
fallback 2-call stores capabilities alongside SOUL.md.

Phase 4: Open agent deprecation — creation silently upgrades open→predefined
in both HTTP and WS endpoints.

Phase 5: Bootstrap auto-contact — sender name from channel metadata injected
into bootstrap context for 1-turn onboarding (DM only).

Phase 6: System prompt preview — GET /v1/agents/{id}/system-prompt-preview
endpoint with mode param, token counting, section parsing.

Phase 7: Agent creation UX — removed open agent type toggle, schema simplified,
description always required, prompt mode cards updated with v3 token estimates.

* fix(vault-ui): redesign detail dialog and link dialog UX

- Vault detail: show content preview by default (not collapsed),
  move type/scope to header badges, hash to subtle footer
- Link dialog: replace native <select> with searchable Combobox
  for target document and link type (5 presets + custom)
- Fix Vietnamese i18n: add proper diacritics to v3-capabilities

* fix(i18n): keep prompt section badges in English across all locales

Technical terms like Persona, Tools, Safety, Memory, Skills, MCP,
Sandbox, Evolution should not be translated — they are UI labels
matching system prompt section names.

* feat(ui): system prompt preview in Files tab + README restructure

Files tab: add "System Prompt" item in sidebar below context files.
When selected, shows readonly preview with mode selector (full/task/
minimal/none), token count badge, and cache boundary highlighting.
Fetches from GET /v1/agents/{id}/system-prompt-preview.

README: remove Claw Ecosystem comparison tables, remove OpenClaw port
reference, rename "What Makes It Different" to "Core Features" with v3
additions (8-stage pipeline, 4-mode prompt, 3-tier memory, knowledge
vault, self-evolution), slim built-in tools to category summary table.

* feat: replace architecture images with v3 sketchnotes

Add 9 new architecture sketchnote images generated for v3:
- 8-Stage Agent Pipeline
- 4-Mode Prompt System
- 3-Tier Memory Architecture
- Multi-Tenant Architecture
- Agent Orchestration
- Knowledge Vault
- Provider Adapter System
- Self-Evolution System
- DomainEventBus

Remove old architecture images (architecture.jpg, goclaw_multi_tenant.png,
agent-delegation.jpg, agent-teams.jpg).

Update README to reference new images in Architecture and Orchestration
sections. Consolidate orchestration section (remove separate delegation
and teams subsections).

* feat(readme): add remaining 4 architecture sketchnotes with sections

Add Knowledge Vault, Self-Evolution, Provider Adapters, and Event-Driven
Architecture sections with corresponding sketchnote images and concise
descriptions. All 9 v3 architecture diagrams now in README.

* feat: reorder README architecture images, evolution guardrails fix, memory tools enhancement

README: reorder architecture sketchnotes (Multi-Tenant first), remove
pinnedSkills from task mode sections badge.

Backend: evolution guardrails fix, memory auto-injector and tools
enhancement, gateway HTTP wiring update.

* feat(desktop): add 12 v3 feature sections to agent detail panel

Add evolution expansion (skill learning, v3 flags), prompt mode selector,
evolution dashboard tab (CSS bar charts, suggestions, guardrails),
thinking/reasoning config, orchestration display, context pruning,
compaction, subagents (lite-limited), tool policy, sandbox config,
and pinned skills management.

Extract agent detail state into use-agent-detail-state hook.
Add getWithParams to ApiClient. Add i18n keys to en/vi/zh locales.

* fix(vault): team-scope security — prevent cross-team data corruption and leaks

- Add team_id UUID + custom_scope to vault_documents (PG migration 043, SQLite migration 10)
- COALESCE-based UNIQUE prevents silent cross-team data overwrite on ON CONFLICT
- PG trigger auto-corrects scope to 'personal' on team deletion (ON DELETE SET NULL)
- Store layer: TeamID filter on all query methods, RunContext-based team scoping
- CreateLink validates same-tenant + same-team boundary (defense in depth)
- VaultInterceptor: infer scope from RunContext, add AfterWriteMedia for binary files
- Wire VaultInterceptor into 5 tools (create_image, create_video, create_audio, tts, edit)
- HTTP handlers: team membership validation via HasTeamAccess, non-owner defaults to personal
- GetBacklinks: single JOIN + LIMIT 100 replaces N+1, VaultBacklink struct with team_id
- vault_search/vault_link/vault_backlinks tools read TeamID from RunContext
- Backlinks filtered by team boundary to prevent title exfiltration

Addresses 7 original + 13 red-team findings (1 CRITICAL, 5 HIGH, 3 MEDIUM).

* feat(evolution): allow CAPABILITIES.md self-evolution, backfill existing agents, cleanup v3 dead flags

- Allow self-evolving predefined agents to read/write CAPABILITIES.md
  (domain expertise) in addition to SOUL.md (style/tone)
- Add CAPABILITIES.md to contextFileSet (DB routing) and protectedFileSet
  (group chat permission check)
- Update buildSelfEvolveSection() system prompt to mention CAPABILITIES.md
- Merge ensureUserPredefined + ensureCapabilities into single
  ensureBackfillFiles() — one DB query instead of two, with error logging
- Remove dead V3MemoryEnabled/V3RetrievalEnabled from PipelineConfig,
  Loop, LoopConfig, resolver, and adapter (always true at runtime)
- Keep fields in V3Flags struct for JSONB backward compat with
  deprecation comments
- Add 10 new tests covering interceptor read/write, prompt section,
  and backfill logic

* feat(export): add v3 sections to agent import/export pipeline

- Phase 1: KG temporal fields (valid_from/valid_until) in export/import
- Phase 2: Episodic summaries section (episodic/summaries.jsonl)
- Phase 3: Evolution metrics + suggestions (evolution/*.jsonl)
- Phase 4: Vault documents + links (vault/*.jsonl, two-pass link resolution)
- Phase 5: ImportSummary expanded to 17 fields, cron/overrides UPSERT dedup
- Add backup/pgpass.go: secure .pgpass credential handling for pg_dump

* feat(backup): system and tenant backup/restore with S3 support

Phase 6: System backup — pg_dump + filesystem tar.gz, .pgpass security,
  preflight check, CLI (goclaw backup) + HTTP API with SSE progress
Phase 7: System restore — psql restore, path traversal protection,
  active connection check, --force/--dry-run/--skip-db/--skip-files
Phase 8: S3 integration — upload/download via AWS SDK v2, credentials
  encrypted in config_secrets (AES-256-GCM), custom endpoint support
Phase 9: Tenant backup/restore — per-table JSONL export (43 tables,
  5-tier FK ordering), 3 restore modes (upsert/replace/new-tenant),
  tenant admin permission checks, CLI + HTTP endpoints

* feat(backup): add table registry validation + gate tenant backup for PG-only

- DiscoverTenantTables() queries information_schema for tables with tenant_id
- ValidateTableRegistry() cross-checks hardcoded registry vs actual schema,
  warns about unregistered tables to prevent silent data loss
- TenantBackup() runs validation before export
- Tenant backup/restore gated for PG-only — SQLite Lite edition has only
  master tenant, returns clear error directing to system backup instead

* fix(backup): address code review security + correctness findings

- C1: Remove backup_path from S3 upload — prevent file exfiltration,
  require backup_token only
- H1: createNewTenant fails explicitly on slug conflict instead of
  silent NOOP that orphans imported data
- H2: Validate JSONL column names against safe regex before SQL
  interpolation — prevent SQL injection from crafted archives
- H3: Replace EOF string comparison with io.Copy in addFileToTar
- H6: Add atomic concurrency guard — reject concurrent backup/restore

* feat(web): add backup & restore admin page with 4 tabs

New admin-only page at /backup-restore with System Backup, System
Restore, S3 Config, and Tenant Backup tabs. Reuses existing
useSseProgress hook and OperationProgress component for real-time
SSE streaming. Includes full i18n support (en/vi/zh).

* feat(web): system prompt preview modal + CAPABILITIES.md backfill + pipeline parity

- Add Eye button in agent header → opens wide modal with markdown-rendered system prompt preview
- CAPABILITIES.md one-time startup backfill for pre-v3 agents (single SQL INSERT WHERE NOT EXISTS)
- Add CAPABILITIES.md to allowedAgentFiles so it shows in Files tab
- Refactor preview API to reuse pipeline's BuildSystemPrompt via BuildPreviewPrompt()
- Resolve actual tool names from registry, provider contributions, sandbox config, shell deny groups
- Resolve team context (TEAM.md virtual file, members, workspace, delegation targets) from DB
- Add IsMinimalAllowed() for mode-aware context file filtering
- Support ?user_id= query param for per-user context file preview

* feat(prompt): redesign 4 system prompt modes with tiered context files

- Add AGENTS_CORE.md (minimal) and AGENTS_TASK.md (task) templates
- Implement ModeAllowlist() for per-mode context file filtering
- Wire filtering into pipeline (loop_history) and preview API
- Upgrade none mode from single-line to functional tool-call prompt
  with slim safety, pinned skills, MCP search, workspace, runtime
- Task mode now gets full persona (SOUL.md + IDENTITY.md)
- Add prompt_mode selector to agent creation dialog
- Mode upgrade warning toast on save
- Skip summoning modal for none/minimal mode agents
- SQL migration 000044: seed AGENTS_CORE/TASK, remove AGENTS_MINIMAL
- SQLite: fix migration v9 duplicate column, v10 COALESCE in UNIQUE
- Add ModeAllowlist tests, none mode tests, SQLite schema tests
- Update i18n (en/vi/zh) with new section keys and mode descriptions

Token targets: full ~4.8K, task ~1.3K, minimal ~570, none ~640

* refactor(web): extract PromptModeCards shared component

Reuse same card layout (icon + name + desc + tokens + section tags)
in both agent creation dialog and agent settings page.
Create dialog uses compact=true to hide section tags.

* fix(backup): align preflight API contract + add package guidance UX

Backend preflight endpoint now returns flat JSON matching frontend
interface (pg_dump_available, disk_space_ok, size metrics) instead of
internal checks array. Adds DirSize/FormatBytes helpers.

Frontend: PageHeader component for consistency, amber alert banner
with link to /packages when pg_dump missing, mobile grid fix.

* feat(web): system prompt preview modal + CAPABILITIES.md backfill + pipeline parity

* feat(vault): async enrich worker for auto summary + semantic linking

Add EventBus-driven worker that generates LLM summaries for vault
documents, embeds them via pgvector, and auto-creates semantic links
between related docs using cosine similarity search.

- Skip embedding in UpsertDocument when summary is empty
- Add UpdateSummaryAndReembed + FindSimilarDocs to VaultStore interface
- Wire enrichment events in AfterWrite/AfterWriteMedia interceptors
- BatchQueue batching for burst writes, bounded dedup (10K cap)
- 5-minute LLM timeout, 0.7 similarity threshold, top-5 neighbors
- SQLite: summary-only (no vector ops, graceful noop)

* refactor(web): phase 1 quick wins — grid breakpoints, silent catches, useQuery migration

- Fix 6 grid-cols-2 without mobile breakpoint → grid-cols-1 sm:grid-cols-2
- Replace 9 silent .catch(() => {}) with console.error for debuggability
- Migrate system-prompt-preview.tsx from useEffect+useState to useQuery
- Add agent column + path tail truncation to vault documents table

* refactor(web): phase 2 split god components into sub-components

- cron-overview-tab: extract schedule, delivery, lifecycle sections
- channel-detail: extract timeline hook + dialogs component
- agent-advanced: extract state utils (deriveState, buildPayload)
- heartbeat-config: add deriveFormDefaults helper
- tenant-backup: split into backup + restore sections
- board-container: extract useBoardTasks hook
- provider-form: extract standard form fields
- contacts: extract contacts table component

* refactor(web): phase 3 form standardization — RHF+Zod migration + field errors

- Migrate 6 forms from useState to React Hook Form + Zod validation
- Create 4 new schema files (api-key, vault, login, s3-config)
- Add inline field error display to memory, mcp, heartbeat, agent-create forms
- Replace raw <input> with <Input> in login forms for consistent styling

* refactor(web): phase 4 lazy-load 20 dialog components + memory co-location

- Convert 20 heavy dialogs from eager to React.lazy + Suspense
- All dialogs use named exports with .then(m => ({ default: m.X })) wrapper
- Suspense fallback={null} per-dialog (invisible during chunk load)
- Move memory page components into documents/, knowledge-graph/, episodic/ subdirs

* refactor(web): phase 5 data fetching polish — staleTime tiers + optimistic updates

- Apply 3-tier staleTime policy: static 5min, standard 60s, realtime 5-15s
- Update 31 hooks with explicit staleTime (37 useQuery call sites)
- Add optimistic updates to builtin-tools, v3-flags, cron, mcp toggles

* refactor(web): phase 6 styling standardization — CVA, design tokens, font utility

- Convert input, textarea, select trigger to CVA with size variants
- Add font-mono-code utility class, replace 4 inline fontFamily styles
- Add text-2xs (10px) and text-xs-plus (11px) design tokens
- Replace 241 arbitrary text-[10px]/text-[11px] with token classes
- Co-locate memory page knowledge-graph files into subdirectory

* feat(vault): pagination, team filter, graph upgrade, and link_type param

- Add CountDocuments to VaultStore interface (PG + SQLite)
- Wrap vault list response as {documents, total} for pagination
- Add optional link_type param to vault_link tool (wikilink/reference)
- Fix resolveOrRegister to use inferVaultDocType instead of hardcoded "note"
- Add team filter dropdown and pagination UI (100/page) to vault page
- Rewrite vault graph with KG-level features: zoom controls, node limit
  selector, click highlight/dim, double-click detail, link labels, stats bar
- Decouple graph data fetch from table (independent limit 500)
- Update VaultDocument type with team_id, summary, custom_scope, media

* fix(vault): graph not rendering due to containerRef timing issue

The early return for loading state prevented containerRef from mounting,
so useLayoutEffect and ResizeObserver never captured dimensions. Moved
loading/empty states inside the container div so the ref always exists.

* fix(vault): eliminate graph flicker on zoom by using ref instead of state

onZoom fired setZoomLevel on every frame → React re-render → new inline
callback references → ForceGraph2D flickered. Now zoom level is stored
in a ref and the display is updated via direct DOM mutation, avoiding
re-renders during continuous zoom interactions.

* refactor(backend): comprehensive audit — safety, god files, interfaces, BaseChannel, tests, benchmarks

Phase 1: ExportTokenStore with lifecycle management (replaces leaked globals)
Phase 2: Split 4 god files (agents_export, agents_import, openai, loop_history) into 17 focused files
Phase 3: Add testability interfaces (consolidation EntityExtractor, heartbeat ProviderResolver/EventPublisher/ActiveSessionChecker)
Phase 4: Consolidate 6 duplicated fields + policy/pairing logic into BaseChannel, migrate all 8 channels
Phase 5: 57 new unit tests (i18n, edition, channels/policy, consolidation, heartbeat) + CI coverage
Phase 6: 30 benchmark tests (tokencount, skills BM25, tool registry, agent loop)
Phase 7: Context propagation fix + 6 metadata key constants

69 files changed, +7361/-3828 (net -3050 lines removed)

* fix(vault): fetch graph links per-agent so all-agents mode shows links

useVaultAllLinks required a single agentId, so links never loaded in
all-agents mode (agentId=""). Replaced with inline per-agent fetching
that groups documents by agent_id and fetches links for each agent.

* docs: add vault enhancement changelog entry

* feat(agent): add displayName to Loop and SystemPrompt for runtime context

Pass agent display name through LoopConfig and SystemPromptConfig so
the runtime section can show a human-readable agent name.

* fix(vault): truncate path from head and hash from middle in detail dialog

Path now uses dir=rtl so ellipsis appears at the start, keeping the
meaningful filename visible. SHA-256 hash shows first 8 + last 8 chars
with ellipsis in the middle instead of truncating the tail.

---------

Co-authored-by: Plateau Nguyen <nguyennlt.ncc@gmail.com>
Co-authored-by: Kai (Tam Nhu) Tran <61256810+kaitranntt@users.noreply.github.com>
2026-04-09 21:15:19 +07:00

1551 lines
71 KiB
SQL

-- GoClaw SQLite Schema (auto-translated from PG migrations 000001-000029)
--
-- Translation rules applied:
-- UUID → TEXT (36-char string)
-- TIMESTAMPTZ → TEXT (RFC3339)
-- JSONB → TEXT (JSON string)
-- BYTEA → BLOB
-- SERIAL/BIGSERIAL → INTEGER PRIMARY KEY AUTOINCREMENT
-- DEFAULT uuid_generate_v7() / gen_random_uuid() → removed (generated in Go)
-- DEFAULT NOW() / CURRENT_TIMESTAMP → DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
-- text[] / uuid[] → TEXT (JSON array)
-- vector(N) → OMITTED (sqlite-vec virtual tables added separately)
-- tsvector → OMITTED (FTS5 virtual tables added separately)
-- CREATE EXTENSION / CREATE OR REPLACE FUNCTION → OMITTED
-- CREATE INDEX USING gin / hnsw → OMITTED
-- USING btree → removed from CREATE INDEX
--
-- Tables dropped across migrations (not included):
-- group_file_writers (migration 23: merged into agent_config_permissions)
-- handoff_routes (migration 26: dropped)
-- delegation_history (migration 26: dropped)
-- team_messages (migration 24: dropped)
-- team_workspace_files (migration 24: dropped)
-- team_workspace_comments (migration 24: dropped)
-- team_workspace_file_versions (migration 24: dropped)
-- team_task_attachments (migration 24: old version dropped, new path-based version created)
-- custom_tools (migration 27: dropped)
--
-- FK cascade constraints reflect final state after migration 23 alterations.
PRAGMA foreign_keys = ON;
-- ============================================================
-- Table: tenants
-- (created first — referenced by almost all other tables)
-- ============================================================
CREATE TABLE IF NOT EXISTS tenants (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
settings TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_tenants_slug ON tenants(slug);
CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status) WHERE status = 'active';
-- Seed master tenant (required by all FK references)
INSERT OR IGNORE INTO tenants (id, name, slug, status)
VALUES ('0193a5b0-7000-7000-8000-000000000001', 'Master', 'master', 'active');
-- ============================================================
-- Table: tenant_users
-- ============================================================
CREATE TABLE IF NOT EXISTS tenant_users (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
role VARCHAR(20) NOT NULL DEFAULT 'member',
metadata TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(tenant_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_tenant_users_user ON tenant_users(user_id);
CREATE INDEX IF NOT EXISTS idx_tenant_users_tenant ON tenant_users(tenant_id);
-- ============================================================
-- Table: llm_providers
-- ============================================================
CREATE TABLE IF NOT EXISTS llm_providers (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
display_name VARCHAR(255),
provider_type VARCHAR(30) NOT NULL DEFAULT 'openai_compat',
api_base TEXT,
api_key TEXT,
enabled BOOLEAN NOT NULL DEFAULT 1,
settings TEXT NOT NULL DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique name (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_llm_providers_tenant_name ON llm_providers(tenant_id, name);
CREATE INDEX IF NOT EXISTS idx_llm_providers_tenant ON llm_providers(tenant_id);
-- ============================================================
-- Table: agents
-- Note: tsv (tsvector) and embedding (vector) columns omitted
-- ============================================================
CREATE TABLE IF NOT EXISTS agents (
id TEXT NOT NULL PRIMARY KEY,
agent_key VARCHAR(100) NOT NULL,
display_name VARCHAR(255),
owner_id VARCHAR(255) NOT NULL,
provider VARCHAR(50) NOT NULL DEFAULT 'openrouter',
model VARCHAR(200) NOT NULL,
context_window INT NOT NULL DEFAULT 200000,
max_tool_iterations INT NOT NULL DEFAULT 20,
workspace TEXT NOT NULL DEFAULT '.',
restrict_to_workspace BOOLEAN NOT NULL DEFAULT 1,
tools_config TEXT NOT NULL DEFAULT '{}',
sandbox_config TEXT,
subagents_config TEXT,
memory_config TEXT,
compaction_config TEXT,
context_pruning TEXT,
other_config TEXT NOT NULL DEFAULT '{}',
emoji TEXT NOT NULL DEFAULT '',
agent_description TEXT NOT NULL DEFAULT '',
thinking_level TEXT NOT NULL DEFAULT '',
max_tokens INT NOT NULL DEFAULT 0,
self_evolve BOOLEAN NOT NULL DEFAULT 0,
skill_evolve BOOLEAN NOT NULL DEFAULT 0,
skill_nudge_interval INT NOT NULL DEFAULT 0,
reasoning_config TEXT NOT NULL DEFAULT '{}',
workspace_sharing TEXT NOT NULL DEFAULT '{}',
chatgpt_oauth_routing TEXT NOT NULL DEFAULT '{}',
shell_deny_groups TEXT NOT NULL DEFAULT '{}',
kg_dedup_config TEXT NOT NULL DEFAULT '{}',
is_default BOOLEAN NOT NULL DEFAULT 0,
agent_type VARCHAR(20) NOT NULL DEFAULT 'open',
status VARCHAR(20) DEFAULT 'active',
frontmatter TEXT,
budget_monthly_cents INTEGER,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
deleted_at TEXT
);
-- Final unique constraint: tenant-scoped agent_key for active agents (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_tenant_agent_key_active ON agents(tenant_id, agent_key) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_agents_owner ON agents(owner_id) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_agents_tenant ON agents(tenant_id);
CREATE INDEX IF NOT EXISTS idx_agents_tenant_active ON agents(tenant_id) WHERE deleted_at IS NULL;
-- ============================================================
-- Table: agent_shares
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_shares (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'user',
granted_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(agent_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_agent_shares_user ON agent_shares(user_id);
CREATE INDEX IF NOT EXISTS idx_agent_shares_tenant ON agent_shares(tenant_id);
-- ============================================================
-- Table: agent_context_files
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_context_files (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
file_name VARCHAR(255) NOT NULL,
content TEXT NOT NULL DEFAULT '',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(agent_id, file_name)
);
CREATE INDEX IF NOT EXISTS idx_agent_context_files_tenant ON agent_context_files(tenant_id);
-- ============================================================
-- Table: user_context_files
-- ============================================================
CREATE TABLE IF NOT EXISTS user_context_files (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
file_name VARCHAR(255) NOT NULL,
content TEXT NOT NULL DEFAULT '',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(agent_id, user_id, file_name)
);
CREATE INDEX IF NOT EXISTS idx_user_context_files_tenant ON user_context_files(tenant_id);
-- ============================================================
-- Table: user_agent_profiles
-- ============================================================
CREATE TABLE IF NOT EXISTS user_agent_profiles (
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
workspace TEXT,
metadata TEXT DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
first_seen_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_seen_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (agent_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_user_agent_profiles_tenant ON user_agent_profiles(tenant_id);
-- ============================================================
-- Table: user_agent_overrides
-- ============================================================
CREATE TABLE IF NOT EXISTS user_agent_overrides (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
provider VARCHAR(50),
model VARCHAR(200),
settings TEXT NOT NULL DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(agent_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_user_agent_overrides_tenant ON user_agent_overrides(tenant_id);
-- ============================================================
-- Table: agent_teams
-- (defined before sessions/memory since they reference it)
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_teams (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
lead_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
settings TEXT NOT NULL DEFAULT '{}',
created_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_agent_teams_tenant ON agent_teams(tenant_id);
-- ============================================================
-- Table: sessions
-- ============================================================
CREATE TABLE IF NOT EXISTS sessions (
id TEXT NOT NULL PRIMARY KEY,
session_key VARCHAR(500) NOT NULL,
agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255),
messages TEXT NOT NULL DEFAULT '[]',
summary TEXT,
model VARCHAR(200),
provider VARCHAR(50),
channel VARCHAR(50),
input_tokens BIGINT NOT NULL DEFAULT 0,
output_tokens BIGINT NOT NULL DEFAULT 0,
compaction_count INT NOT NULL DEFAULT 0,
memory_flush_compaction_count INT NOT NULL DEFAULT 0,
memory_flush_at BIGINT DEFAULT 0,
label VARCHAR(500),
spawned_by VARCHAR(200),
spawn_depth INT NOT NULL DEFAULT 0,
metadata TEXT DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique session_key (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_tenant_session_key ON sessions(tenant_id, session_key);
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON sessions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_sessions_tenant_user ON sessions(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_team ON sessions(team_id) WHERE team_id IS NOT NULL;
-- ============================================================
-- Table: memory_documents
-- ============================================================
CREATE TABLE IF NOT EXISTS memory_documents (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255),
path VARCHAR(500) NOT NULL,
content TEXT NOT NULL DEFAULT '',
hash VARCHAR(64) NOT NULL,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
custom_scope TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_memdoc_unique ON memory_documents(agent_id, COALESCE(user_id, ''), path);
CREATE INDEX IF NOT EXISTS idx_memdoc_agent_user ON memory_documents(agent_id, user_id);
CREATE INDEX IF NOT EXISTS idx_memdoc_team ON memory_documents(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_memory_documents_tenant ON memory_documents(tenant_id);
-- ============================================================
-- Table: memory_chunks
-- Note: embedding (vector) and tsv (tsvector) columns omitted
-- ============================================================
CREATE TABLE IF NOT EXISTS memory_chunks (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
document_id TEXT REFERENCES memory_documents(id) ON DELETE CASCADE,
user_id VARCHAR(255),
path TEXT NOT NULL,
start_line INT NOT NULL DEFAULT 0,
end_line INT NOT NULL DEFAULT 0,
hash VARCHAR(64) NOT NULL,
text TEXT NOT NULL,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
custom_scope TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_mem_agent_user ON memory_chunks(agent_id, user_id);
CREATE INDEX IF NOT EXISTS idx_mem_global ON memory_chunks(agent_id) WHERE user_id IS NULL;
CREATE INDEX IF NOT EXISTS idx_mem_document ON memory_chunks(document_id);
CREATE INDEX IF NOT EXISTS idx_memchunk_team ON memory_chunks(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_memory_chunks_tenant ON memory_chunks(tenant_id);
-- ============================================================
-- Table: embedding_cache
-- Note: embedding (vector) column omitted
-- ============================================================
CREATE TABLE IF NOT EXISTS embedding_cache (
hash VARCHAR(64) NOT NULL,
provider VARCHAR(50) NOT NULL,
model VARCHAR(200) NOT NULL,
dims INT NOT NULL DEFAULT 0,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (hash, provider, model)
);
CREATE INDEX IF NOT EXISTS idx_embedding_cache_tenant ON embedding_cache(tenant_id);
-- ============================================================
-- Table: skills
-- Note: embedding (vector) column omitted
-- ============================================================
CREATE TABLE IF NOT EXISTS skills (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL,
description TEXT,
owner_id VARCHAR(255) NOT NULL,
visibility VARCHAR(10) NOT NULL DEFAULT 'private',
version INT NOT NULL DEFAULT 1,
status VARCHAR(20) NOT NULL DEFAULT 'active',
frontmatter TEXT NOT NULL DEFAULT '{}',
file_path TEXT NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
file_hash VARCHAR(64),
tags TEXT,
is_system BOOLEAN NOT NULL DEFAULT 0,
deps TEXT NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT 1,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique slug (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_skills_tenant_slug ON skills(tenant_id, slug);
CREATE INDEX IF NOT EXISTS idx_skills_owner ON skills(owner_id);
CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(visibility) WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_skills_system ON skills(is_system) WHERE is_system = 1;
CREATE INDEX IF NOT EXISTS idx_skills_enabled ON skills(enabled) WHERE enabled = 0;
CREATE INDEX IF NOT EXISTS idx_skills_tenant ON skills(tenant_id);
-- ============================================================
-- Table: skill_agent_grants
-- ============================================================
CREATE TABLE IF NOT EXISTS skill_agent_grants (
id TEXT NOT NULL PRIMARY KEY,
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
pinned_version INT NOT NULL,
granted_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(skill_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_skill_agent_grants_agent ON skill_agent_grants(agent_id);
CREATE INDEX IF NOT EXISTS idx_skill_agent_grants_tenant ON skill_agent_grants(tenant_id);
-- ============================================================
-- Table: skill_user_grants
-- ============================================================
CREATE TABLE IF NOT EXISTS skill_user_grants (
id TEXT NOT NULL PRIMARY KEY,
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
granted_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(skill_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_skill_user_grants_user ON skill_user_grants(user_id);
CREATE INDEX IF NOT EXISTS idx_skill_user_grants_tenant ON skill_user_grants(tenant_id);
-- ============================================================
-- Table: skill_tenant_configs
-- ============================================================
CREATE TABLE IF NOT EXISTS skill_tenant_configs (
skill_id TEXT NOT NULL REFERENCES skills(id) ON DELETE CASCADE,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (skill_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_skill_tenant_configs_tenant ON skill_tenant_configs(tenant_id);
-- ============================================================
-- Table: cron_jobs
-- ============================================================
CREATE TABLE IF NOT EXISTS cron_jobs (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE,
user_id TEXT,
name VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT 1,
schedule_kind VARCHAR(10) NOT NULL,
cron_expression VARCHAR(100),
interval_ms BIGINT,
run_at TEXT,
timezone VARCHAR(50),
payload TEXT NOT NULL,
delete_after_run BOOLEAN NOT NULL DEFAULT 0,
stateless INTEGER NOT NULL DEFAULT 0,
deliver INTEGER NOT NULL DEFAULT 0,
deliver_channel TEXT NOT NULL DEFAULT '',
deliver_to TEXT NOT NULL DEFAULT '',
wake_heartbeat INTEGER NOT NULL DEFAULT 0,
next_run_at TEXT,
last_run_at TEXT,
last_status VARCHAR(20),
last_error TEXT,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_cron_jobs_user_id ON cron_jobs(user_id);
CREATE INDEX IF NOT EXISTS idx_cron_jobs_agent_user ON cron_jobs(agent_id, user_id);
CREATE INDEX IF NOT EXISTS idx_cron_jobs_team ON cron_jobs(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_cron_jobs_tenant ON cron_jobs(tenant_id);
-- ============================================================
-- Table: cron_run_logs
-- ============================================================
CREATE TABLE IF NOT EXISTS cron_run_logs (
id TEXT NOT NULL PRIMARY KEY,
job_id TEXT NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
status VARCHAR(20) NOT NULL,
summary TEXT,
error TEXT,
duration_ms INT,
input_tokens INT DEFAULT 0,
output_tokens INT DEFAULT 0,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
ran_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_cron_run_logs_job ON cron_run_logs(job_id, ran_at DESC);
CREATE INDEX IF NOT EXISTS idx_cron_run_logs_team ON cron_run_logs(team_id) WHERE team_id IS NOT NULL;
-- ============================================================
-- Table: pairing_requests
-- ============================================================
CREATE TABLE IF NOT EXISTS pairing_requests (
id TEXT NOT NULL PRIMARY KEY,
code VARCHAR(8) NOT NULL UNIQUE,
sender_id VARCHAR(200) NOT NULL,
channel VARCHAR(255) NOT NULL,
chat_id VARCHAR(200) NOT NULL,
account_id VARCHAR(100) NOT NULL DEFAULT 'default',
metadata TEXT DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
expires_at TEXT NOT NULL,
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_pairing_requests_tenant ON pairing_requests(tenant_id);
-- ============================================================
-- Table: paired_devices
-- ============================================================
CREATE TABLE IF NOT EXISTS paired_devices (
id TEXT NOT NULL PRIMARY KEY,
sender_id VARCHAR(200) NOT NULL,
channel VARCHAR(255) NOT NULL,
chat_id VARCHAR(200) NOT NULL,
paired_by VARCHAR(100) NOT NULL DEFAULT 'operator',
metadata TEXT DEFAULT '{}',
expires_at TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
paired_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_paired_devices_tenant_sender_channel ON paired_devices(tenant_id, sender_id, channel);
CREATE INDEX IF NOT EXISTS idx_paired_devices_tenant ON paired_devices(tenant_id);
-- ============================================================
-- Table: traces
-- ============================================================
CREATE TABLE IF NOT EXISTS traces (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT,
user_id VARCHAR(255),
session_key TEXT,
run_id TEXT,
start_time TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
end_time TEXT,
duration_ms INT,
name TEXT,
channel VARCHAR(50),
input_preview TEXT,
output_preview TEXT,
total_input_tokens INT DEFAULT 0,
total_output_tokens INT DEFAULT 0,
total_cost NUMERIC(12,6) DEFAULT 0,
span_count INT DEFAULT 0,
llm_call_count INT DEFAULT 0,
tool_call_count INT DEFAULT 0,
status VARCHAR(20) DEFAULT 'running',
error TEXT,
metadata TEXT,
tags TEXT,
parent_trace_id TEXT,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_traces_agent_time ON traces(agent_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_traces_user_time ON traces(user_id, created_at DESC) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_traces_session ON traces(session_key, created_at DESC) WHERE session_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_traces_status ON traces(status) WHERE status = 'error';
CREATE INDEX IF NOT EXISTS idx_traces_parent ON traces(parent_trace_id) WHERE parent_trace_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_traces_quota ON traces(user_id, created_at DESC) WHERE parent_trace_id IS NULL AND user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_traces_start_root ON traces(start_time DESC) WHERE parent_trace_id IS NULL;
CREATE INDEX IF NOT EXISTS idx_traces_team ON traces(team_id, created_at DESC) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_traces_tenant ON traces(tenant_id);
CREATE INDEX IF NOT EXISTS idx_traces_tenant_time ON traces(tenant_id, created_at DESC);
-- ============================================================
-- Table: spans
-- ============================================================
CREATE TABLE IF NOT EXISTS spans (
id TEXT NOT NULL PRIMARY KEY,
trace_id TEXT NOT NULL,
parent_span_id TEXT,
agent_id TEXT,
span_type VARCHAR(20) NOT NULL,
name TEXT,
start_time TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
end_time TEXT,
duration_ms INT,
status VARCHAR(20) DEFAULT 'running',
error TEXT,
level VARCHAR(10) DEFAULT 'DEFAULT',
model VARCHAR(200),
provider VARCHAR(50),
input_tokens INT,
output_tokens INT,
total_cost NUMERIC(12,8),
finish_reason VARCHAR(50),
model_params TEXT,
tool_name VARCHAR(200),
tool_call_id VARCHAR(100),
input_preview TEXT,
output_preview TEXT,
metadata TEXT,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_spans_trace ON spans(trace_id, start_time);
CREATE INDEX IF NOT EXISTS idx_spans_trace_type ON spans(trace_id, span_type);
CREATE INDEX IF NOT EXISTS idx_spans_parent ON spans(parent_span_id) WHERE parent_span_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_spans_agent_time ON spans(agent_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_spans_type ON spans(span_type, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_spans_model ON spans(model, created_at DESC) WHERE model IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_spans_error ON spans(status) WHERE status = 'error';
CREATE INDEX IF NOT EXISTS idx_spans_team ON spans(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_spans_tenant ON spans(tenant_id);
-- ============================================================
-- Table: mcp_servers
-- ============================================================
CREATE TABLE IF NOT EXISTS mcp_servers (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
transport VARCHAR(50) NOT NULL,
command TEXT,
args TEXT DEFAULT '[]',
url TEXT,
headers TEXT DEFAULT '{}',
env TEXT DEFAULT '{}',
api_key TEXT,
tool_prefix VARCHAR(50),
timeout_sec INT DEFAULT 60,
settings TEXT NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT 1,
created_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique name (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_tenant_name ON mcp_servers(tenant_id, name);
CREATE INDEX IF NOT EXISTS idx_mcp_servers_tenant ON mcp_servers(tenant_id);
-- ============================================================
-- Table: mcp_agent_grants
-- ============================================================
CREATE TABLE IF NOT EXISTS mcp_agent_grants (
id TEXT NOT NULL PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT 1,
tool_allow TEXT,
tool_deny TEXT,
config_overrides TEXT,
granted_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(server_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_mcp_agent_grants_agent ON mcp_agent_grants(agent_id);
CREATE INDEX IF NOT EXISTS idx_mcp_agent_grants_tenant ON mcp_agent_grants(tenant_id);
-- ============================================================
-- Table: mcp_user_grants
-- ============================================================
CREATE TABLE IF NOT EXISTS mcp_user_grants (
id TEXT NOT NULL PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT 1,
tool_allow TEXT,
tool_deny TEXT,
granted_by VARCHAR(255) NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(server_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_mcp_user_grants_user ON mcp_user_grants(user_id);
CREATE INDEX IF NOT EXISTS idx_mcp_user_grants_tenant ON mcp_user_grants(tenant_id);
-- ============================================================
-- Table: mcp_access_requests
-- ============================================================
CREATE TABLE IF NOT EXISTS mcp_access_requests (
id TEXT NOT NULL PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
agent_id TEXT REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255),
scope VARCHAR(10) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
reason TEXT,
tool_allow TEXT,
requested_by VARCHAR(255) NOT NULL,
reviewed_by VARCHAR(255),
reviewed_at TEXT,
review_note TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_mcp_requests_status ON mcp_access_requests(status) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_mcp_requests_server ON mcp_access_requests(server_id);
CREATE INDEX IF NOT EXISTS idx_mcp_access_requests_tenant ON mcp_access_requests(tenant_id);
-- ============================================================
-- Table: mcp_user_credentials
-- ============================================================
CREATE TABLE IF NOT EXISTS mcp_user_credentials (
id TEXT NOT NULL PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
api_key TEXT,
headers BLOB,
env BLOB,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(server_id, user_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_mcp_user_credentials_tenant ON mcp_user_credentials(tenant_id);
CREATE INDEX IF NOT EXISTS idx_mcp_user_credentials_server ON mcp_user_credentials(server_id);
-- ============================================================
-- Table: channel_instances
-- ============================================================
CREATE TABLE IF NOT EXISTS channel_instances (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
display_name VARCHAR(255) DEFAULT '',
channel_type VARCHAR(50) NOT NULL,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
credentials BLOB,
config TEXT DEFAULT '{}',
enabled BOOLEAN DEFAULT 1,
created_by VARCHAR(255) DEFAULT '',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique name (migration 27 Phase I)
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_instances_tenant_name ON channel_instances(tenant_id, name);
CREATE INDEX IF NOT EXISTS idx_channel_instances_type ON channel_instances(channel_type);
CREATE INDEX IF NOT EXISTS idx_channel_instances_agent ON channel_instances(agent_id);
CREATE INDEX IF NOT EXISTS idx_channel_instances_tenant ON channel_instances(tenant_id);
-- ============================================================
-- Table: config_secrets
-- PK changed to (key, tenant_id) in migration 27 Phase I
-- ============================================================
CREATE TABLE IF NOT EXISTS config_secrets (
key VARCHAR(100) NOT NULL,
value BLOB NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (key, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_config_secrets_tenant ON config_secrets(tenant_id);
-- ============================================================
-- Table: agent_links
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_links (
id TEXT NOT NULL PRIMARY KEY,
source_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
target_agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
direction VARCHAR(20) NOT NULL DEFAULT 'outbound',
description TEXT,
max_concurrent INT NOT NULL DEFAULT 3,
settings TEXT NOT NULL DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_by VARCHAR(255) NOT NULL,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(source_agent_id, target_agent_id),
CHECK (source_agent_id != target_agent_id)
);
CREATE INDEX IF NOT EXISTS idx_agent_links_source ON agent_links(source_agent_id) WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_agent_links_target ON agent_links(target_agent_id) WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_agent_links_tenant ON agent_links(tenant_id);
-- ============================================================
-- Table: agent_team_members
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_team_members (
team_id TEXT NOT NULL REFERENCES agent_teams(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL DEFAULT 'member',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
joined_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (team_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_agent_team_members_tenant ON agent_team_members(tenant_id);
-- ============================================================
-- Table: team_tasks
-- Note: tsv (tsvector) and embedding (vector) columns omitted
-- blocked_by stored as TEXT (JSON array of UUIDs)
-- ============================================================
CREATE TABLE IF NOT EXISTS team_tasks (
id TEXT NOT NULL PRIMARY KEY,
team_id TEXT NOT NULL REFERENCES agent_teams(id) ON DELETE CASCADE,
subject VARCHAR(500) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
owner_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
blocked_by TEXT NOT NULL DEFAULT '[]',
priority INT NOT NULL DEFAULT 0,
result TEXT,
metadata TEXT NOT NULL DEFAULT '{}',
user_id VARCHAR(255),
channel VARCHAR(50),
task_type VARCHAR(30) NOT NULL DEFAULT 'general',
task_number INT NOT NULL DEFAULT 0,
identifier VARCHAR(20),
created_by_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
assignee_user_id VARCHAR(255),
parent_id TEXT REFERENCES team_tasks(id) ON DELETE SET NULL,
chat_id VARCHAR(255) DEFAULT '',
locked_at TEXT,
lock_expires_at TEXT,
progress_percent INT DEFAULT 0 CHECK (progress_percent BETWEEN 0 AND 100),
progress_step TEXT,
followup_at TEXT,
followup_count INT NOT NULL DEFAULT 0,
followup_max INT NOT NULL DEFAULT 0,
followup_message TEXT,
followup_channel VARCHAR(60),
followup_chat_id VARCHAR(255),
confidence_score REAL,
comment_count INT NOT NULL DEFAULT 0,
attachment_count INT NOT NULL DEFAULT 0,
custom_scope TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_team_tasks_team ON team_tasks(team_id);
CREATE INDEX IF NOT EXISTS idx_team_tasks_status ON team_tasks(team_id, status);
CREATE INDEX IF NOT EXISTS idx_team_tasks_user_scope ON team_tasks(team_id, user_id) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tt_parent ON team_tasks(parent_id) WHERE parent_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tt_scope ON team_tasks(team_id, channel, chat_id);
CREATE INDEX IF NOT EXISTS idx_tt_type ON team_tasks(team_id, task_type);
CREATE INDEX IF NOT EXISTS idx_tt_lock ON team_tasks(lock_expires_at) WHERE lock_expires_at IS NOT NULL AND status = 'in_progress';
CREATE UNIQUE INDEX IF NOT EXISTS idx_tt_identifier ON team_tasks(team_id, identifier) WHERE identifier IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tt_followup ON team_tasks(followup_at) WHERE followup_at IS NOT NULL AND status = 'in_progress';
-- idx_tt_blocked_by (GIN on array) omitted: Go code handles JSON array filtering
CREATE INDEX IF NOT EXISTS idx_tt_owner_status ON team_tasks(team_id, owner_agent_id, status);
CREATE INDEX IF NOT EXISTS idx_team_tasks_tenant ON team_tasks(tenant_id);
-- ============================================================
-- Table: team_task_comments
-- ============================================================
CREATE TABLE IF NOT EXISTS team_task_comments (
id TEXT NOT NULL PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES team_tasks(id) ON DELETE CASCADE,
agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
user_id VARCHAR(255),
content TEXT NOT NULL,
metadata TEXT DEFAULT '{}',
comment_type VARCHAR(20) NOT NULL DEFAULT 'note',
confidence_score REAL,
custom_scope TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_ttc_task ON team_task_comments(task_id);
CREATE INDEX IF NOT EXISTS idx_team_task_comments_tenant ON team_task_comments(tenant_id);
-- ============================================================
-- Table: team_task_events
-- ============================================================
CREATE TABLE IF NOT EXISTS team_task_events (
id TEXT NOT NULL PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES team_tasks(id) ON DELETE CASCADE,
event_type VARCHAR(30) NOT NULL,
actor_type VARCHAR(10) NOT NULL,
actor_id VARCHAR(255) NOT NULL,
data TEXT,
custom_scope TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_tte_task ON team_task_events(task_id);
CREATE INDEX IF NOT EXISTS idx_team_task_events_tenant ON team_task_events(tenant_id);
-- ============================================================
-- Table: team_task_attachments
-- (new path-based version from migration 24)
-- ============================================================
CREATE TABLE IF NOT EXISTS team_task_attachments (
id TEXT NOT NULL PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES team_tasks(id) ON DELETE CASCADE,
team_id TEXT NOT NULL REFERENCES agent_teams(id) ON DELETE CASCADE,
chat_id VARCHAR(255) NOT NULL DEFAULT '',
path TEXT NOT NULL,
file_size BIGINT NOT NULL DEFAULT 0,
mime_type VARCHAR(100) DEFAULT '',
created_by_agent_id TEXT REFERENCES agents(id),
created_by_sender_id VARCHAR(255) DEFAULT '',
metadata TEXT NOT NULL DEFAULT '{}',
custom_scope TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(task_id, path)
);
CREATE INDEX IF NOT EXISTS idx_tta_task ON team_task_attachments(task_id);
CREATE INDEX IF NOT EXISTS idx_tta_team ON team_task_attachments(team_id);
CREATE INDEX IF NOT EXISTS idx_team_task_attachments_tenant ON team_task_attachments(tenant_id);
-- ============================================================
-- Table: team_user_grants
-- ============================================================
CREATE TABLE IF NOT EXISTS team_user_grants (
id TEXT NOT NULL PRIMARY KEY,
team_id TEXT NOT NULL REFERENCES agent_teams(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'viewer',
granted_by VARCHAR(255),
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(team_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_team_user_grants_user ON team_user_grants(user_id);
CREATE INDEX IF NOT EXISTS idx_team_user_grants_team ON team_user_grants(team_id);
CREATE INDEX IF NOT EXISTS idx_team_user_grants_tenant ON team_user_grants(tenant_id);
-- ============================================================
-- Table: kg_entities
-- Note: embedding (vector) column omitted
-- ============================================================
CREATE TABLE IF NOT EXISTS kg_entities (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL DEFAULT '',
external_id VARCHAR(255) NOT NULL,
name TEXT NOT NULL,
entity_type VARCHAR(100) NOT NULL,
description TEXT DEFAULT '',
properties TEXT DEFAULT '{}',
source_id VARCHAR(255) DEFAULT '',
confidence REAL NOT NULL DEFAULT 1.0,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
valid_from TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
valid_until TEXT,
UNIQUE(agent_id, user_id, external_id)
);
CREATE INDEX IF NOT EXISTS idx_kg_entities_scope ON kg_entities(agent_id, user_id);
CREATE INDEX IF NOT EXISTS idx_kg_entities_type ON kg_entities(agent_id, user_id, entity_type);
CREATE INDEX IF NOT EXISTS idx_kg_entities_current ON kg_entities(agent_id, user_id) WHERE valid_until IS NULL;
CREATE INDEX IF NOT EXISTS idx_kg_entities_team ON kg_entities(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_kg_entities_tenant ON kg_entities(tenant_id);
-- ============================================================
-- Table: kg_relations
-- ============================================================
CREATE TABLE IF NOT EXISTS kg_relations (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL DEFAULT '',
source_entity_id TEXT NOT NULL REFERENCES kg_entities(id) ON DELETE CASCADE,
relation_type VARCHAR(200) NOT NULL,
target_entity_id TEXT NOT NULL REFERENCES kg_entities(id) ON DELETE CASCADE,
confidence REAL NOT NULL DEFAULT 1.0,
properties TEXT DEFAULT '{}',
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
valid_from TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
valid_until TEXT,
UNIQUE(agent_id, user_id, source_entity_id, relation_type, target_entity_id)
);
CREATE INDEX IF NOT EXISTS idx_kg_relations_source ON kg_relations(source_entity_id, relation_type);
CREATE INDEX IF NOT EXISTS idx_kg_relations_target ON kg_relations(target_entity_id);
CREATE INDEX IF NOT EXISTS idx_kg_relations_current ON kg_relations(agent_id, user_id) WHERE valid_until IS NULL;
CREATE INDEX IF NOT EXISTS idx_kg_relations_team ON kg_relations(team_id) WHERE team_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_kg_relations_tenant ON kg_relations(tenant_id);
-- ============================================================
-- Table: channel_pending_messages
-- ============================================================
CREATE TABLE IF NOT EXISTS channel_pending_messages (
id TEXT NOT NULL PRIMARY KEY,
channel_name VARCHAR(100) NOT NULL,
history_key VARCHAR(200) NOT NULL,
sender VARCHAR(255) NOT NULL,
sender_id VARCHAR(255) NOT NULL DEFAULT '',
body TEXT NOT NULL,
platform_msg_id VARCHAR(100) NOT NULL DEFAULT '',
is_summary BOOLEAN NOT NULL DEFAULT 0,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_channel_pending_messages_lookup ON channel_pending_messages(channel_name, history_key, created_at);
CREATE INDEX IF NOT EXISTS idx_channel_pending_messages_tenant ON channel_pending_messages(tenant_id);
-- ============================================================
-- Table: channel_contacts
-- ============================================================
CREATE TABLE IF NOT EXISTS channel_contacts (
id TEXT NOT NULL PRIMARY KEY,
channel_type VARCHAR(50) NOT NULL,
channel_instance VARCHAR(255),
sender_id VARCHAR(255) NOT NULL,
user_id VARCHAR(255),
display_name VARCHAR(255),
username VARCHAR(255),
avatar_url TEXT,
peer_kind VARCHAR(20),
contact_type VARCHAR(20) NOT NULL DEFAULT 'user',
thread_id VARCHAR(100),
thread_type VARCHAR(20),
metadata TEXT DEFAULT '{}',
merged_id TEXT,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
first_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- tenant-scoped unique including thread_id for topic contacts (migration 35)
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_contacts_tenant_type_sender ON channel_contacts(tenant_id, channel_type, sender_id, COALESCE(thread_id, ''));
CREATE INDEX IF NOT EXISTS idx_channel_contacts_instance ON channel_contacts(channel_instance) WHERE channel_instance IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channel_contacts_merged ON channel_contacts(merged_id) WHERE merged_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_channel_contacts_search ON channel_contacts(display_name, username);
CREATE INDEX IF NOT EXISTS idx_channel_contacts_tenant ON channel_contacts(tenant_id);
-- ============================================================
-- Table: activity_logs
-- ============================================================
CREATE TABLE IF NOT EXISTS activity_logs (
id TEXT NOT NULL PRIMARY KEY,
actor_type VARCHAR(20) NOT NULL,
actor_id VARCHAR(255) NOT NULL,
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(50),
entity_id VARCHAR(255),
details TEXT,
ip_address VARCHAR(45),
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_activity_logs_actor ON activity_logs(actor_type, actor_id);
CREATE INDEX IF NOT EXISTS idx_activity_logs_action ON activity_logs(action);
CREATE INDEX IF NOT EXISTS idx_activity_logs_entity ON activity_logs(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_activity_logs_created ON activity_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_activity_logs_tenant ON activity_logs(tenant_id);
-- ============================================================
-- Table: usage_snapshots
-- ============================================================
CREATE TABLE IF NOT EXISTS usage_snapshots (
id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
bucket_hour TEXT NOT NULL,
agent_id TEXT,
provider VARCHAR(50) NOT NULL DEFAULT '',
model VARCHAR(200) NOT NULL DEFAULT '',
channel VARCHAR(50) NOT NULL DEFAULT '',
input_tokens BIGINT NOT NULL DEFAULT 0,
output_tokens BIGINT NOT NULL DEFAULT 0,
cache_read_tokens BIGINT NOT NULL DEFAULT 0,
cache_create_tokens BIGINT NOT NULL DEFAULT 0,
thinking_tokens BIGINT NOT NULL DEFAULT 0,
total_cost NUMERIC(12,6) NOT NULL DEFAULT 0,
request_count INTEGER NOT NULL DEFAULT 0,
llm_call_count INTEGER NOT NULL DEFAULT 0,
tool_call_count INTEGER NOT NULL DEFAULT 0,
error_count INTEGER NOT NULL DEFAULT 0,
unique_users INTEGER NOT NULL DEFAULT 0,
avg_duration_ms INTEGER NOT NULL DEFAULT 0,
memory_docs INTEGER NOT NULL DEFAULT 0,
memory_chunks INTEGER NOT NULL DEFAULT 0,
kg_entities INTEGER NOT NULL DEFAULT 0,
kg_relations INTEGER NOT NULL DEFAULT 0,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_bucket ON usage_snapshots(bucket_hour DESC);
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_agent_bucket ON usage_snapshots(agent_id, bucket_hour DESC);
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_provider_bucket ON usage_snapshots(provider, bucket_hour DESC) WHERE provider != '';
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_channel_bucket ON usage_snapshots(channel, bucket_hour DESC) WHERE channel != '';
-- COALESCE NULLs to sentinel so upsert dedup works (SQLite treats NULL != NULL in unique indexes).
CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_snapshots_unique ON usage_snapshots(
bucket_hour,
COALESCE(agent_id, '00000000-0000-0000-0000-000000000000'),
COALESCE(provider, ''),
COALESCE(model, ''),
COALESCE(channel, ''),
tenant_id
);
CREATE INDEX IF NOT EXISTS idx_usage_snapshots_tenant ON usage_snapshots(tenant_id);
-- ============================================================
-- Table: builtin_tools
-- ============================================================
CREATE TABLE IF NOT EXISTS builtin_tools (
name VARCHAR(100) NOT NULL PRIMARY KEY,
display_name VARCHAR(255) NOT NULL,
description TEXT NOT NULL DEFAULT '',
category VARCHAR(50) NOT NULL DEFAULT 'general',
enabled BOOLEAN NOT NULL DEFAULT 1,
settings TEXT NOT NULL DEFAULT '{}',
requires TEXT DEFAULT '[]',
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_builtin_tools_category ON builtin_tools(category);
-- ============================================================
-- Table: builtin_tool_tenant_configs
-- ============================================================
CREATE TABLE IF NOT EXISTS builtin_tool_tenant_configs (
tool_name VARCHAR(100) NOT NULL REFERENCES builtin_tools(name) ON DELETE CASCADE,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
enabled BOOLEAN,
settings TEXT,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (tool_name, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_builtin_tool_tenant_configs_tenant ON builtin_tool_tenant_configs(tenant_id);
-- ============================================================
-- Table: secure_cli_binaries
-- ============================================================
CREATE TABLE IF NOT EXISTS secure_cli_binaries (
id TEXT NOT NULL PRIMARY KEY,
binary_name TEXT NOT NULL,
binary_path TEXT,
description TEXT NOT NULL DEFAULT '',
encrypted_env BLOB NOT NULL,
deny_args TEXT NOT NULL DEFAULT '[]',
deny_verbose TEXT NOT NULL DEFAULT '[]',
timeout_seconds INTEGER NOT NULL DEFAULT 30,
tips TEXT NOT NULL DEFAULT '',
is_global BOOLEAN NOT NULL DEFAULT 1,
enabled BOOLEAN NOT NULL DEFAULT 1,
created_by TEXT NOT NULL DEFAULT '',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_secure_cli_binary_name ON secure_cli_binaries(binary_name);
CREATE UNIQUE INDEX IF NOT EXISTS idx_secure_cli_unique_binary_tenant ON secure_cli_binaries(binary_name, tenant_id);
CREATE INDEX IF NOT EXISTS idx_secure_cli_binaries_tenant ON secure_cli_binaries(tenant_id);
-- ============================================================
-- Table: secure_cli_agent_grants
-- ============================================================
CREATE TABLE IF NOT EXISTS secure_cli_agent_grants (
id TEXT NOT NULL PRIMARY KEY,
binary_id TEXT NOT NULL REFERENCES secure_cli_binaries(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
deny_args TEXT,
deny_verbose TEXT,
timeout_seconds INTEGER,
tips TEXT,
enabled BOOLEAN NOT NULL DEFAULT 1,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(binary_id, agent_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_scag_binary ON secure_cli_agent_grants(binary_id);
CREATE INDEX IF NOT EXISTS idx_scag_agent ON secure_cli_agent_grants(agent_id);
CREATE INDEX IF NOT EXISTS idx_scag_tenant ON secure_cli_agent_grants(tenant_id);
-- ============================================================
-- Table: api_keys
-- ============================================================
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT NOT NULL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
prefix VARCHAR(8) NOT NULL,
key_hash VARCHAR(64) NOT NULL UNIQUE,
scopes TEXT NOT NULL DEFAULT '[]',
expires_at TEXT,
last_used_at TEXT,
revoked BOOLEAN NOT NULL DEFAULT 0,
created_by VARCHAR(255),
owner_id VARCHAR(255),
tenant_id TEXT REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash) WHERE NOT revoked;
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix);
CREATE INDEX IF NOT EXISTS idx_api_keys_owner_id ON api_keys(owner_id) WHERE owner_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant ON api_keys(tenant_id) WHERE tenant_id IS NOT NULL;
-- ============================================================
-- Table: agent_heartbeats
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_heartbeats (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL UNIQUE REFERENCES agents(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT 0,
interval_sec INT NOT NULL DEFAULT 1800,
prompt TEXT,
provider_id TEXT REFERENCES llm_providers(id),
model VARCHAR(200),
isolated_session BOOLEAN NOT NULL DEFAULT 1,
light_context BOOLEAN NOT NULL DEFAULT 0,
ack_max_chars INT NOT NULL DEFAULT 300,
max_retries INT NOT NULL DEFAULT 2,
active_hours_start VARCHAR(5),
active_hours_end VARCHAR(5),
timezone TEXT,
channel VARCHAR(50),
chat_id TEXT,
next_run_at TEXT,
last_run_at TEXT,
last_status VARCHAR(20),
last_error TEXT,
run_count INT NOT NULL DEFAULT 0,
suppress_count INT NOT NULL DEFAULT 0,
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_heartbeats_due ON agent_heartbeats(next_run_at) WHERE enabled = 1 AND next_run_at IS NOT NULL;
-- ============================================================
-- Table: heartbeat_run_logs
-- ============================================================
CREATE TABLE IF NOT EXISTS heartbeat_run_logs (
id TEXT NOT NULL PRIMARY KEY,
heartbeat_id TEXT NOT NULL REFERENCES agent_heartbeats(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL,
summary TEXT,
error TEXT,
duration_ms INT,
input_tokens INT DEFAULT 0,
output_tokens INT DEFAULT 0,
skip_reason VARCHAR(50),
metadata TEXT DEFAULT '{}',
ran_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_hb_logs_heartbeat ON heartbeat_run_logs(heartbeat_id, ran_at DESC);
CREATE INDEX IF NOT EXISTS idx_hb_logs_agent ON heartbeat_run_logs(agent_id, ran_at DESC);
-- ============================================================
-- Table: agent_config_permissions
-- (scope widened to VARCHAR(255) in migration 23;
-- includes migrated group_file_writers rows)
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_config_permissions (
id TEXT NOT NULL PRIMARY KEY,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
scope VARCHAR(255) NOT NULL,
config_type VARCHAR(50) NOT NULL,
user_id VARCHAR(255) NOT NULL,
permission VARCHAR(10) NOT NULL,
granted_by VARCHAR(255),
metadata TEXT DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(agent_id, scope, config_type, user_id)
);
CREATE INDEX IF NOT EXISTS idx_acp_lookup ON agent_config_permissions(agent_id, scope, config_type);
CREATE INDEX IF NOT EXISTS idx_agent_config_permissions_tenant ON agent_config_permissions(tenant_id);
-- ============================================================
-- Table: skill_tenant_configs (already defined above)
-- Table: builtin_tool_tenant_configs (already defined above)
-- ============================================================
-- ============================================================
-- Table: system_configs
-- ============================================================
CREATE TABLE IF NOT EXISTS system_configs (
key VARCHAR(100) NOT NULL,
value TEXT NOT NULL,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
PRIMARY KEY (key, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_system_configs_tenant ON system_configs(tenant_id);
-- ============================================================
-- Table: subagent_tasks
-- ============================================================
CREATE TABLE IF NOT EXISTS subagent_tasks (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
parent_agent_key VARCHAR(255) NOT NULL,
session_key VARCHAR(500),
subject VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'running',
result TEXT,
depth INTEGER NOT NULL DEFAULT 1,
model VARCHAR(255),
provider VARCHAR(255),
iterations INTEGER NOT NULL DEFAULT 0,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
origin_channel VARCHAR(50),
origin_chat_id VARCHAR(255),
origin_peer_kind VARCHAR(20),
origin_user_id VARCHAR(255),
spawned_by TEXT,
completed_at TEXT,
archived_at TEXT,
metadata TEXT NOT NULL DEFAULT '{}',
custom_scope TEXT,
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_subagent_tasks_parent_status ON subagent_tasks(tenant_id, parent_agent_key, status);
CREATE INDEX IF NOT EXISTS idx_subagent_tasks_session ON subagent_tasks(session_key);
CREATE INDEX IF NOT EXISTS idx_subagent_tasks_created ON subagent_tasks(tenant_id, created_at);
-- ============================================================
-- Table: episodic_summaries (V3 Tier 2 memory)
-- ============================================================
CREATE TABLE IF NOT EXISTS episodic_summaries (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL DEFAULT '',
session_key TEXT NOT NULL,
summary TEXT NOT NULL,
l0_abstract TEXT NOT NULL DEFAULT '',
key_topics TEXT NOT NULL DEFAULT '[]',
source_type TEXT NOT NULL DEFAULT 'session',
source_id TEXT,
turn_count INTEGER NOT NULL DEFAULT 0,
token_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
expires_at TEXT,
promoted_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_episodic_agent_user ON episodic_summaries(agent_id, user_id);
CREATE INDEX IF NOT EXISTS idx_episodic_tenant ON episodic_summaries(tenant_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_episodic_source_dedup ON episodic_summaries(agent_id, user_id, source_id)
WHERE source_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_episodic_unpromoted ON episodic_summaries(agent_id, user_id, created_at)
WHERE promoted_at IS NULL;
-- ============================================================
-- Table: agent_evolution_metrics (V3 self-evolution Stage 1)
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_evolution_metrics (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
session_key TEXT NOT NULL,
metric_type TEXT NOT NULL,
metric_key TEXT NOT NULL,
value TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_evo_metrics_agent_type ON agent_evolution_metrics(agent_id, metric_type);
CREATE INDEX IF NOT EXISTS idx_evo_metrics_created ON agent_evolution_metrics(created_at);
CREATE INDEX IF NOT EXISTS idx_evo_metrics_tenant ON agent_evolution_metrics(tenant_id);
-- ============================================================
-- Table: agent_evolution_suggestions (V3 self-evolution Stage 2)
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_evolution_suggestions (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id),
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
suggestion_type TEXT NOT NULL,
suggestion TEXT NOT NULL,
rationale TEXT NOT NULL,
parameters TEXT,
status TEXT NOT NULL DEFAULT 'pending',
reviewed_by TEXT,
reviewed_at TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_evo_suggestions_agent ON agent_evolution_suggestions(agent_id, status);
CREATE INDEX IF NOT EXISTS idx_evo_suggestions_tenant ON agent_evolution_suggestions(tenant_id);
-- ============================================================
-- Table: kg_dedup_candidates (V3 dedup review queue)
-- ============================================================
CREATE TABLE IF NOT EXISTS kg_dedup_candidates (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT REFERENCES tenants(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL DEFAULT '',
entity_a_id TEXT NOT NULL REFERENCES kg_entities(id) ON DELETE CASCADE,
entity_b_id TEXT NOT NULL REFERENCES kg_entities(id) ON DELETE CASCADE,
similarity REAL NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(entity_a_id, entity_b_id)
);
CREATE INDEX IF NOT EXISTS idx_kg_dedup_agent ON kg_dedup_candidates(agent_id, status);
-- ============================================================
-- Table: secure_cli_user_credentials (per-user encrypted env)
-- ============================================================
CREATE TABLE IF NOT EXISTS secure_cli_user_credentials (
id TEXT NOT NULL PRIMARY KEY,
binary_id TEXT NOT NULL REFERENCES secure_cli_binaries(id) ON DELETE CASCADE,
user_id VARCHAR(255) NOT NULL,
encrypted_env BLOB NOT NULL,
metadata TEXT NOT NULL DEFAULT '{}',
tenant_id TEXT NOT NULL REFERENCES tenants(id),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(binary_id, user_id, tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_scuc_tenant ON secure_cli_user_credentials(tenant_id);
CREATE INDEX IF NOT EXISTS idx_scuc_binary ON secure_cli_user_credentials(binary_id);
-- ============================================================
-- Table: vault_documents (V3 Knowledge Vault registry)
-- ============================================================
CREATE TABLE IF NOT EXISTS vault_documents (
id TEXT NOT NULL PRIMARY KEY,
tenant_id TEXT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
team_id TEXT REFERENCES agent_teams(id) ON DELETE SET NULL,
scope TEXT NOT NULL DEFAULT 'personal',
custom_scope TEXT,
path TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
doc_type TEXT NOT NULL DEFAULT 'note',
content_hash TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
metadata TEXT DEFAULT '{}',
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
-- SQLite prohibits expressions in inline UNIQUE constraints; use a unique index instead.
CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_docs_unique_path
ON vault_documents(agent_id, COALESCE(team_id, ''), scope, path);
CREATE INDEX IF NOT EXISTS idx_vault_docs_tenant ON vault_documents(tenant_id);
CREATE INDEX IF NOT EXISTS idx_vault_docs_agent_scope ON vault_documents(agent_id, scope);
CREATE INDEX IF NOT EXISTS idx_vault_docs_type ON vault_documents(agent_id, doc_type);
CREATE INDEX IF NOT EXISTS idx_vault_docs_hash ON vault_documents(content_hash);
CREATE INDEX IF NOT EXISTS idx_vault_docs_team ON vault_documents(team_id);
-- ============================================================
-- Table: vault_links (V3 wikilink edges)
-- ============================================================
CREATE TABLE IF NOT EXISTS vault_links (
id TEXT NOT NULL PRIMARY KEY,
from_doc_id TEXT NOT NULL REFERENCES vault_documents(id) ON DELETE CASCADE,
to_doc_id TEXT NOT NULL REFERENCES vault_documents(id) ON DELETE CASCADE,
link_type TEXT NOT NULL DEFAULT 'wikilink',
context TEXT NOT NULL DEFAULT '',
custom_scope TEXT,
created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
UNIQUE(from_doc_id, to_doc_id, link_type)
);
CREATE INDEX IF NOT EXISTS idx_vault_links_from ON vault_links(from_doc_id);
CREATE INDEX IF NOT EXISTS idx_vault_links_to ON vault_links(to_doc_id);