Files
Algis DumbrisandClaude Opus 4.7 da827c03b3 feat(020): foundational — migration 028, dispatch tokens, owner resolver
Adds the SQL substrate (6 tables + memory_status view) and the helpers
every user story depends on:
- migration 028_memory_consolidation.sql + smoke test
- internal/messaging/memory_config.go (env-flag plumbing)
- internal/messaging/dispatch_tokens.go (32-byte rand, 15m TTL, single-job-bound)
- internal/messaging/memory_channels.go (open-brain / reflections-* / is_memory flag)
- internal/agents/owner.go (OwnerFor with sentinel errors)

Deviations from spec, all documented in code:
- owner_id is stored as INTEGER FK to users; OwnerFor converts to the
  string scope-key the new tables use.
- MemoryChannel is a local struct to avoid an import cycle between
  internal/channels and internal/messaging.
- channels.metadata column does not exist yet; IsMemoryChannel honors
  it conditionally so MemoryChannelIDs can extend trivially when added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:55:33 +03:00

54 lines
1.7 KiB
Go

package agents
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
)
// Sentinel errors returned by OwnerFor.
//
// Callers in the proactive-memory / dream-worker path use these to
// distinguish "no such agent" from "agent exists but has no owner"
// without parsing error strings.
var (
// ErrAgentNotFound is returned when no row exists in `agents` for
// the requested name.
ErrAgentNotFound = errors.New("agent not found")
// ErrAgentUnowned is returned when an agent row exists but its
// owner_id is zero / empty. In the current schema owner_id is
// declared NOT NULL, so this is effectively an integrity guard for
// rows backfilled with 0.
ErrAgentUnowned = errors.New("agent has no owner")
)
// OwnerFor returns the string-encoded owner_id of the named agent.
//
// The schema stores `agents.owner_id` as INTEGER (FK to `users.id`), but
// the proactive-memory tables and the request-context `owner_id`
// (populated by auth middleware via `trace.ContextWithOwnerID`) carry it
// as a string. OwnerFor canonicalizes to that string form so call sites
// can compare without re-converting.
//
// Returns ("", ErrAgentNotFound) when no row matches; ("",
// ErrAgentUnowned) when a row exists but owner_id is 0.
func OwnerFor(ctx context.Context, db *sql.DB, agentName string) (string, error) {
var ownerID int64
err := db.QueryRowContext(ctx,
`SELECT owner_id FROM agents WHERE name = ?`, agentName,
).Scan(&ownerID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", ErrAgentNotFound
}
return "", fmt.Errorf("query owner for agent %q: %w", agentName, err)
}
if ownerID == 0 {
return "", ErrAgentUnowned
}
return strconv.FormatInt(ownerID, 10), nil
}