Class: Collavre::AiAgent::SessionProvisioner

Inherits:
Object
  • Object
show all
Defined in:
app/services/collavre/ai_agent/session_provisioner.rb

Overview

Provisions the identities a Claude Code registration needs:

- one shared ai_user per (human, agent_name) — the Agent identity
- one Topic per (agent, session_id) — the Session identity

Extracted from Api::V1::AgentsController#register so the action stays a thin orchestration of provisioning + inbox share. Behavior is identical to the inlined controller methods it replaces.

Instance Method Summary collapse

Constructor Details

#initialize(user) ⇒ SessionProvisioner

Returns a new instance of SessionProvisioner.



13
14
15
# File 'app/services/collavre/ai_agent/session_provisioner.rb', line 13

def initialize(user)
  @user = user
end

Instance Method Details

#find_or_create_agent(agent_name) ⇒ Object

One ai_user per (user, agent_name) so a human's sessions share one Agent identity. Same agent_name re-registering reuses the existing row — idempotent retries (and every additional session) don't proliferate agents.

Returns nil when a row with the deterministic email already exists but is owned by someone else or isn't a Claude Channel ai_user. The email format is human-derivable (user.id + slug + digest), so a foreign row could be planted by signup/import; silently reusing it would attach the caller's inbox feedback share to that foreign User and leave the plugin's AgentChannel subscription rejected on ownership mismatch. Caller renders 409 in that case.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/services/collavre/ai_agent/session_provisioner.rb', line 29

def find_or_create_agent(agent_name)
  email = session_agent_email(agent_name)

  existing = User.find_by(email: email)
  return verified_agent(existing) if existing

  # routing_expression: nil so the new ai_user is not matchable until the
  # client claims the per-agent stream via AgentChannel. See the comment on
  # verified_agent.
  User.create!(
    email: email,
    name: "Claude Channel (#{agent_name})",
    password: SecureRandom.hex(32),
    llm_vendor: "anthropic",
    llm_model: "claude-code",
    created_by_id: @user.id,
    searchable: false,
    routing_expression: nil
  )
rescue ActiveRecord::RecordNotUnique
  # A concurrent registration for the same (user, agent_name) won the
  # users.email unique race. The desired row now exists — re-find and
  # re-verify ownership instead of surfacing a 500 that aborts one of the
  # two simultaneously launching plugin instances.
  verified_agent(User.find_by(email: email))
end

#find_or_create_topic(inbox, ai_user, session_id, session_label) ⇒ Object

One Topic per (agent, session_id). On re-register (including --resume from the same cwd, which yields the same session_id) the existing topic is reused — even if archived — so the conversation persists instead of orphaning. A fresh session gets a new topic under the same shared agent, which is how one agent fans out to many sessions.



61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/services/collavre/ai_agent/session_provisioner.rb', line 61

def find_or_create_topic(inbox, ai_user, session_id, session_label)
  existing = inbox.topics.find_by(primary_agent_id: ai_user.id, session_id: session_id)
  return existing if existing

  label = session_label.to_s.strip.presence || session_id
  inbox.topics.create!(
    name: unique_topic_name(inbox, "Claude #{label}"),
    user: @user,
    primary_agent_id: ai_user.id,
    session_id: session_id
  )
end