Changeset 0.23.0 (#153)

This commit is contained in:
2026-03-05 22:40:26 +00:00
parent 40d9834f64
commit 2fc620e1ac
62 changed files with 6214 additions and 362 deletions

View File

@@ -188,20 +188,39 @@ func (s *ChannelStore) SetCursor(ctx context.Context, channelID, userID, leafID
}
func (s *ChannelStore) SetModel(ctx context.Context, cm *models.ChannelModel) error {
// For persona entries, use the persona-aware path
if cm.PersonaID != nil && *cm.PersonaID != "" {
return s.SetPersonaModel(ctx, cm)
}
// Raw model upsert (no persona) — matches idx_channel_models_raw partial index
_, err := DB.ExecContext(ctx, `
INSERT INTO channel_models (id, channel_id, model_id, provider_config_id, display_name, system_prompt, settings, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (channel_id, model_id) DO UPDATE SET
provider_config_id = excluded.provider_config_id, display_name = excluded.display_name, system_prompt = excluded.system_prompt, settings = excluded.settings, is_default = excluded.is_default`,
ON CONFLICT (channel_id, model_id, provider_config_id) WHERE persona_id IS NULL DO UPDATE SET
display_name = excluded.display_name, system_prompt = excluded.system_prompt, settings = excluded.settings, is_default = excluded.is_default`,
store.NewID(), cm.ChannelID, cm.ModelID, cm.ProviderConfigID, cm.DisplayName, cm.SystemPrompt, "{}", cm.IsDefault)
return err
}
// SetPersonaModel inserts or updates a persona's channel model roster entry.
func (s *ChannelStore) SetPersonaModel(ctx context.Context, cm *models.ChannelModel) error {
_, err := DB.ExecContext(ctx, `
INSERT INTO channel_models (id, channel_id, model_id, provider_config_id, persona_id, display_name, system_prompt, settings, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (channel_id, persona_id) WHERE persona_id IS NOT NULL DO UPDATE SET
model_id = excluded.model_id, provider_config_id = excluded.provider_config_id, display_name = excluded.display_name, system_prompt = excluded.system_prompt, settings = excluded.settings, is_default = excluded.is_default`,
store.NewID(), cm.ChannelID, cm.ModelID, cm.ProviderConfigID, cm.PersonaID, cm.DisplayName, cm.SystemPrompt, "{}", cm.IsDefault)
return err
}
func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]models.ChannelModel, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, channel_id, model_id, COALESCE(provider_config_id, ''),
COALESCE(display_name, ''), COALESCE(system_prompt, ''), is_default
FROM channel_models WHERE channel_id = ?`, channelID)
SELECT cm.id, cm.channel_id, cm.model_id, COALESCE(cm.provider_config_id, ''),
COALESCE(cm.persona_id, ''), COALESCE(p.handle, ''), COALESCE(cm.display_name, ''),
COALESCE(cm.system_prompt, ''), cm.is_default
FROM channel_models cm
LEFT JOIN personas p ON p.id = cm.persona_id
WHERE cm.channel_id = ?`, channelID)
if err != nil {
return nil, err
}
@@ -210,10 +229,14 @@ func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]model
var result []models.ChannelModel
for rows.Next() {
var cm models.ChannelModel
var personaID string
if err := rows.Scan(&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID,
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault); err != nil {
&personaID, &cm.Handle, &cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault); err != nil {
return nil, err
}
if personaID != "" {
cm.PersonaID = &personaID
}
result = append(result, cm)
}
return result, rows.Err()
@@ -221,15 +244,22 @@ func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]model
func (s *ChannelStore) GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error) {
var cm models.ChannelModel
var personaID string
err := DB.QueryRowContext(ctx, `
SELECT id, channel_id, model_id, COALESCE(provider_config_id, ''),
COALESCE(display_name, ''), COALESCE(system_prompt, ''), is_default
FROM channel_models WHERE id = ?`, id).Scan(
SELECT cm.id, cm.channel_id, cm.model_id, COALESCE(cm.provider_config_id, ''),
COALESCE(cm.persona_id, ''), COALESCE(p.handle, ''), COALESCE(cm.display_name, ''),
COALESCE(cm.system_prompt, ''), cm.is_default
FROM channel_models cm
LEFT JOIN personas p ON p.id = cm.persona_id
WHERE cm.id = ?`, id).Scan(
&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID,
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault)
&personaID, &cm.Handle, &cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault)
if err != nil {
return nil, err
}
if personaID != "" {
cm.PersonaID = &personaID
}
return &cm, nil
}
@@ -289,3 +319,103 @@ func (s *ChannelStore) ResolveWorkspaceID(ctx context.Context, channelID string)
}
return NullableString(wsID), nil
}
// ── Channel Participants (ICD §3.7) ──────────
func (s *ChannelStore) AddParticipant(ctx context.Context, p *models.ChannelParticipant) error {
_, err := DB.ExecContext(ctx, `
INSERT INTO channel_participants (id, channel_id, participant_type, participant_id, role, display_name, avatar_url)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
store.NewID(), p.ChannelID, p.ParticipantType, p.ParticipantID, p.Role, p.DisplayName, p.AvatarURL)
return err
}
func (s *ChannelStore) ListParticipants(ctx context.Context, channelID string) ([]models.ChannelParticipant, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, channel_id, participant_type, participant_id, role,
display_name, avatar_url, joined_at
FROM channel_participants WHERE channel_id = ? ORDER BY joined_at`, channelID)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.ChannelParticipant
for rows.Next() {
var p models.ChannelParticipant
var joinedStr string
if err := rows.Scan(&p.ID, &p.ChannelID, &p.ParticipantType, &p.ParticipantID,
&p.Role, &p.DisplayName, &p.AvatarURL, &joinedStr); err != nil {
return nil, err
}
p.JoinedAt, _ = time.Parse("2006-01-02 15:04:05", joinedStr)
result = append(result, p)
}
return result, rows.Err()
}
func (s *ChannelStore) GetParticipantByID(ctx context.Context, id string) (*models.ChannelParticipant, error) {
var p models.ChannelParticipant
var joinedStr string
err := DB.QueryRowContext(ctx, `
SELECT id, channel_id, participant_type, participant_id, role,
display_name, avatar_url, joined_at
FROM channel_participants WHERE id = ?`, id).Scan(
&p.ID, &p.ChannelID, &p.ParticipantType, &p.ParticipantID,
&p.Role, &p.DisplayName, &p.AvatarURL, &joinedStr)
if err != nil {
return nil, err
}
p.JoinedAt, _ = time.Parse("2006-01-02 15:04:05", joinedStr)
return &p, nil
}
func (s *ChannelStore) UpdateParticipantRole(ctx context.Context, id, role string) error {
_, err := DB.ExecContext(ctx, `UPDATE channel_participants SET role = ? WHERE id = ?`, role, id)
return err
}
func (s *ChannelStore) RemoveParticipant(ctx context.Context, id string) error {
res, err := DB.ExecContext(ctx, `DELETE FROM channel_participants WHERE id = ?`, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return sql.ErrNoRows
}
return nil
}
func (s *ChannelStore) IsParticipant(ctx context.Context, channelID, pType, pID string) (bool, error) {
var count int
err := DB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM channel_participants
WHERE channel_id = ? AND participant_type = ? AND participant_id = ?`,
channelID, pType, pID).Scan(&count)
return count > 0, err
}
func (s *ChannelStore) GetParticipantRole(ctx context.Context, channelID, pType, pID string) (string, error) {
var role string
err := DB.QueryRowContext(ctx, `
SELECT role FROM channel_participants
WHERE channel_id = ? AND participant_type = ? AND participant_id = ?`,
channelID, pType, pID).Scan(&role)
return role, err
}
func (s *ChannelStore) CountParticipantsByRole(ctx context.Context, channelID, role string) (int, error) {
var count int
err := DB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM channel_participants WHERE channel_id = ? AND role = ?`,
channelID, role).Scan(&count)
return count, err
}
func (s *ChannelStore) DeleteModelByPersona(ctx context.Context, channelID, personaID string) error {
_, err := DB.ExecContext(ctx,
`DELETE FROM channel_models WHERE channel_id = ? AND persona_id = ?`,
channelID, personaID)
return err
}

View File

@@ -15,7 +15,7 @@ type PersonaStore struct{}
func NewPersonaStore() *PersonaStore { return &PersonaStore{} }
const personaCols = `id, name, description, icon, avatar, base_model_id, provider_config_id,
const personaCols = `id, name, handle, description, icon, avatar, base_model_id, provider_config_id,
system_prompt, temperature, max_tokens, thinking_budget, top_p,
scope, owner_id, created_by, is_active, is_shared, memory_enabled, memory_extraction_prompt,
created_at, updated_at`
@@ -25,13 +25,17 @@ func (s *PersonaStore) Create(ctx context.Context, p *models.Persona) error {
now := time.Now().UTC()
p.CreatedAt = now
p.UpdatedAt = now
// Auto-generate handle from name if not set
if p.Handle == "" {
p.Handle = models.HandleFromName(p.Name)
}
_, err := DB.ExecContext(ctx, `
INSERT INTO personas (id, name, description, icon, avatar, base_model_id, provider_config_id,
INSERT INTO personas (id, name, handle, description, icon, avatar, base_model_id, provider_config_id,
system_prompt, temperature, max_tokens, thinking_budget, top_p,
scope, owner_id, created_by, is_active, is_shared, memory_enabled, memory_extraction_prompt,
created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
p.ID, p.Name, p.Description, p.Icon, p.Avatar, p.BaseModelID,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
p.ID, p.Name, p.Handle, p.Description, p.Icon, p.Avatar, p.BaseModelID,
models.NullString(p.ProviderConfigID),
p.SystemPrompt, models.NullFloat(p.Temperature), models.NullInt(p.MaxTokens),
models.NullInt(p.ThinkingBudget), models.NullFloat(p.TopP),
@@ -60,6 +64,9 @@ func (s *PersonaStore) Update(ctx context.Context, id string, patch models.Perso
if patch.Name != nil {
b.Set("name", *patch.Name)
}
if patch.Handle != nil {
b.Set("handle", *patch.Handle)
}
if patch.Description != nil {
b.Set("description", *patch.Description)
}
@@ -287,11 +294,11 @@ func (s *PersonaStore) UserCanAccess(ctx context.Context, userID, personaID stri
func scanPersona(row *sql.Row) (*models.Persona, error) {
var p models.Persona
var providerConfigID, ownerID sql.NullString
var providerConfigID, ownerID, handle sql.NullString
var temp, topP sql.NullFloat64
var maxTokens, thinkingBudget sql.NullInt64
err := row.Scan(
&p.ID, &p.Name, &p.Description, &p.Icon, &p.Avatar,
&p.ID, &p.Name, &handle, &p.Description, &p.Icon, &p.Avatar,
&p.BaseModelID, &providerConfigID,
&p.SystemPrompt, &temp, &maxTokens, &thinkingBudget, &topP,
&p.Scope, &ownerID, &p.CreatedBy, &p.IsActive, &p.IsShared,
@@ -301,6 +308,9 @@ func scanPersona(row *sql.Row) (*models.Persona, error) {
if err != nil {
return nil, err
}
if handle.Valid {
p.Handle = handle.String
}
p.ProviderConfigID = NullableStringPtr(providerConfigID)
p.OwnerID = NullableStringPtr(ownerID)
p.Temperature = NullableFloat64Ptr(temp)
@@ -314,11 +324,11 @@ func scanPersonas(rows *sql.Rows) ([]models.Persona, error) {
var result []models.Persona
for rows.Next() {
var p models.Persona
var providerConfigID, ownerID sql.NullString
var providerConfigID, ownerID, handle sql.NullString
var temp, topP sql.NullFloat64
var maxTokens, thinkingBudget sql.NullInt64
err := rows.Scan(
&p.ID, &p.Name, &p.Description, &p.Icon, &p.Avatar,
&p.ID, &p.Name, &handle, &p.Description, &p.Icon, &p.Avatar,
&p.BaseModelID, &providerConfigID,
&p.SystemPrompt, &temp, &maxTokens, &thinkingBudget, &topP,
&p.Scope, &ownerID, &p.CreatedBy, &p.IsActive, &p.IsShared,
@@ -328,6 +338,9 @@ func scanPersonas(rows *sql.Rows) ([]models.Persona, error) {
if err != nil {
return nil, err
}
if handle.Valid {
p.Handle = handle.String
}
p.ProviderConfigID = NullableStringPtr(providerConfigID)
p.OwnerID = NullableStringPtr(ownerID)
p.Temperature = NullableFloat64Ptr(temp)

View File

@@ -15,7 +15,7 @@ func NewUserModelSettingsStore() *UserModelSettingsStore { return &UserModelSett
func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, model_id, COALESCE(hidden, 0), preferred_temperature, preferred_max_tokens,
SELECT id, user_id, model_id, provider_config_id, COALESCE(hidden, 0), preferred_temperature, preferred_max_tokens,
COALESCE(sort_order, 0), created_at, updated_at
FROM user_model_settings WHERE user_id = ? ORDER BY sort_order, model_id`, userID)
if err != nil {
@@ -28,7 +28,7 @@ func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string)
var s models.UserModelSetting
var prefTemp, prefMaxTokens interface{}
err := rows.Scan(
&s.ID, &s.UserID, &s.ModelID, &s.Hidden,
&s.ID, &s.UserID, &s.ModelID, &s.ProviderConfigID, &s.Hidden,
&prefTemp, &prefMaxTokens,
&s.SortOrder, st(&s.CreatedAt), st(&s.UpdatedAt),
)
@@ -47,10 +47,10 @@ func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string)
return result, rows.Err()
}
// GetHiddenModelIDs returns a map of model_id → true for all hidden models.
// GetHiddenModelIDs returns a map of provider_config_id:model_id → true for all hidden models.
func (s *UserModelSettingsStore) GetHiddenModelIDs(ctx context.Context, userID string) (map[string]bool, error) {
rows, err := DB.QueryContext(ctx,
"SELECT model_id FROM user_model_settings WHERE user_id = ? AND hidden = 1",
"SELECT model_id, COALESCE(provider_config_id, '') FROM user_model_settings WHERE user_id = ? AND hidden = 1",
userID)
if err != nil {
return nil, err
@@ -59,17 +59,17 @@ func (s *UserModelSettingsStore) GetHiddenModelIDs(ctx context.Context, userID s
result := make(map[string]bool)
for rows.Next() {
var modelID string
if err := rows.Scan(&modelID); err != nil {
var modelID, provCfgID string
if err := rows.Scan(&modelID, &provCfgID); err != nil {
return nil, err
}
result[modelID] = true
result[models.CompositeModelKey(provCfgID, modelID)] = true
}
return result, rows.Err()
}
// Set upserts a single user model setting.
func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string, patch models.UserModelSettingPatch) error {
func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string, providerConfigID *string, patch models.UserModelSettingPatch) error {
b := NewUpdate("user_model_settings")
if patch.Hidden != nil {
b.Set("hidden", *patch.Hidden)
@@ -89,17 +89,16 @@ func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string
}
// Use upsert: insert if not exists, update if exists
// Build a custom upsert since the update builder doesn't handle INSERT ON CONFLICT
_, err := DB.ExecContext(ctx, `
INSERT INTO user_model_settings (id, user_id, model_id, hidden, preferred_temperature, preferred_max_tokens, sort_order)
VALUES (?, ?, ?, COALESCE(?, 0), ?, ?, COALESCE(?, 0))
ON CONFLICT (user_id, model_id)
INSERT INTO user_model_settings (id, user_id, model_id, provider_config_id, hidden, preferred_temperature, preferred_max_tokens, sort_order)
VALUES (?, ?, ?, ?, COALESCE(?, 0), ?, ?, COALESCE(?, 0))
ON CONFLICT (user_id, model_id, provider_config_id)
DO UPDATE SET
hidden = COALESCE(excluded.hidden, user_model_settings.hidden),
preferred_temperature = COALESCE(excluded.preferred_temperature, user_model_settings.preferred_temperature),
preferred_max_tokens = COALESCE(excluded.preferred_max_tokens, user_model_settings.preferred_max_tokens),
sort_order = COALESCE(excluded.sort_order, user_model_settings.sort_order)`,
store.NewID(), userID, modelID,
store.NewID(), userID, modelID, providerConfigID,
patchBoolOrNil(patch.Hidden),
patchFloat64OrNil(patch.PreferredTemperature),
patchIntOrNil(patch.PreferredMaxTokens),
@@ -108,21 +107,20 @@ func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string
return err
}
// BulkSetHidden sets the hidden state for multiple models at once.
func (s *UserModelSettingsStore) BulkSetHidden(ctx context.Context, userID string, modelIDs []string, hidden bool) error {
if len(modelIDs) == 0 {
// BulkSetHidden sets the hidden state for multiple model+provider pairs at once.
func (s *UserModelSettingsStore) BulkSetHidden(ctx context.Context, userID string, entries []models.HiddenEntry, hidden bool) error {
if len(entries) == 0 {
return nil
}
// Upsert each model individually
for _, modelID := range modelIDs {
for _, entry := range entries {
_, err := DB.ExecContext(ctx, `
INSERT INTO user_model_settings (id, user_id, model_id, hidden)
VALUES (?, ?, ?, ?)
ON CONFLICT (user_id, model_id) DO UPDATE SET hidden = excluded.hidden`,
store.NewID(), userID, modelID, hidden)
INSERT INTO user_model_settings (id, user_id, model_id, provider_config_id, hidden)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (user_id, model_id, provider_config_id) DO UPDATE SET hidden = excluded.hidden`,
store.NewID(), userID, entry.ModelID, entry.ProviderConfigID, hidden)
if err != nil {
return fmt.Errorf("set hidden for %s: %w", modelID, err)
return fmt.Errorf("set hidden for %s:%s: %w", entry.ProviderConfigID, entry.ModelID, err)
}
}
return nil