Class: Collavre::Api::V1::Mobile::AgentEventsController

Inherits:
BaseController show all
Defined in:
app/controllers/collavre/api/v1/mobile/agent_events_controller.rb

Overview

The heart of the loop. GET surfaces pending approvals (+ recent agent replies) with a stable spoken ref number and a decision-ready summary. POST :id/respond branches on the referenced event KIND:

(A) approval/permission comment → the spoken response is a BUTTON
    press (allow|deny) → decide_claude_channel_permission! + relay.
(B) ordinary agent message → the spoken response is FREE TEXT passed
    verbatim back to the agent as a reply comment (no server parsing).

Constant Summary collapse

APPROVE =

Spoken decision verbs. The app no longer addresses approvals by ordinal, so a reply to an approval event is just an allow/deny verb; anything else asks for clarification rather than firing a decision.

/(승인|허용|적용|approve|allow|confirm|네|좋아|오케이|\bok\b|\byes\b)/i
DENY =
/(거절|거부|반려|deny|reject|취소|아니|\bno\b)/i

Instance Method Summary collapse

Instance Method Details

#indexObject



21
22
23
24
25
26
27
28
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
55
56
57
# File 'app/controllers/collavre/api/v1/mobile/agent_events_controller.rb', line 21

def index
  approvals = pending_approvals
  notices = system_inbox_messages

  # Every undecided approval is emitted on every poll — the server keeps
  # no per-client cursor. Re-speaking is prevented on the CLIENT (an
  # in-memory "already spoken" set), the same at-least-once shape as
  # notices: a pending approval keeps surfacing until it is decided
  # (which clears it from pending_approvals) or the app restarts (when a
  # still-pending approval SHOULD be re-surfaced). The old `since` cursor
  # filtered here, but a batch high-water-mark could burn past an approval
  # whose created_at trailed a newer notice, losing it forever.
  events = approvals.map do |c|
    {
      id: c.id, type: "approval_requested",
      title: title_for_topic(c.topic_id),
      summary: summarizer.approval_summary(comment: c, label: label_for_topic(c.topic_id)),
      speak: true, requires_response: true, topic_id: c.topic_id,
      created_at: c.created_at.iso8601(6)
    }
  end

  events += notices.map do |c|
    # The notice stands in for the origin comment it quotes; the app
    # lists/reads it against the ORIGIN thread and replies route there.
    origin_topic_id = c.quoted_comment&.topic_id || c.topic_id
    {
      id: c.id, type: "agent_reply",
      title: title_for_topic(origin_topic_id),
      summary: speakable(c.content),
      speak: true, requires_response: c.quoted_comment_id.present?,
      topic_id: origin_topic_id, created_at: c.created_at.iso8601(6)
    }
  end

  render json: events.sort_by { |e| e[:created_at] }
end

#readObject

The app calls this when it finishes reading a message ALOUD (whether or not the user then replies). Reading = hearing it, so the inbox read pointer advances and the notice stops surfacing — the SAME read-state the inbox badge uses. A crash before this call leaves the notice unread, so the next poll re-reads it (at-least-once, not the lossy old cursor).



81
82
83
84
85
86
87
88
89
# File 'app/controllers/collavre/api/v1/mobile/agent_events_controller.rb', line 81

def read
  comment = Collavre::Comment.find_by(id: params[:id])
  unless comment && authorized_comment?(comment)
    return render json: { error: "Event not found" }, status: :not_found
  end

  mark_inbox_read(comment)
  head :ok
end

#respondObject



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'app/controllers/collavre/api/v1/mobile/agent_events_controller.rb', line 59

def respond
  comment = Collavre::Comment.find_by(id: params[:id])
  unless comment && authorized_comment?(comment)
    return render json: { error: "Event not found" }, status: :not_found
  end

  # Acting on a notice means the user has heard it — clear it from the
  # unread inbox set so it isn't read again on the next poll.
  mark_inbox_read(comment)

  if comment.claude_channel_permission?
    decide_on(comment)
  else
    relay_free_text(comment)
  end
end