Skip to content

Commit e84ddee

Browse files
committed
Merge remote-tracking branch 'origin/main' into hampton/comment-resolve-delete-494f8c
* origin/main: Live agent collaboration: the event inbox, honest presence, and every way to wake an agent (#197) # Conflicts: # engine/app/controllers/coplan/api/v1/comments_controller.rb # engine/app/views/coplan/agent_instructions/show.text.erb # engine/config/routes.rb
2 parents 3fc3e6e + 3c415e6 commit e84ddee

63 files changed

Lines changed: 4721 additions & 39 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/admin/agent_events.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
ActiveAdmin.register CoPlan::AgentEvent, as: "AgentEvent" do
2+
# Inbox rows are written by the platform and acked by agents; admin is
3+
# for inspection only (a stuck inbox, a delivery question).
4+
actions :index, :show
5+
6+
index do
7+
id_column
8+
column :event_type
9+
column :plan
10+
column :api_token
11+
column :acked_at
12+
column :created_at
13+
end
14+
15+
show do
16+
attributes_table do
17+
row :id
18+
row :event_type
19+
row :plan
20+
row :api_token
21+
row :comment_thread_id
22+
row :comment_id
23+
row :payload
24+
row :acked_at
25+
row :created_at
26+
end
27+
end
28+
end

app/admin/agent_sessions.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
ActiveAdmin.register CoPlan::AgentSession, as: "AgentSession" do
2+
# Read-mostly: sessions are claimed and updated over the API. Destroy
3+
# stays available so an operator can clear a stuck row.
4+
actions :index, :show, :destroy
5+
6+
index do
7+
selectable_column
8+
id_column
9+
column :agent_name
10+
column :plan
11+
column :api_token
12+
column :state
13+
column :state_detail
14+
column :last_activity_at
15+
column :wakes_answered_count
16+
column :wake_failures_count
17+
actions
18+
end
19+
20+
show do
21+
attributes_table do
22+
row :id
23+
row :agent_name
24+
row :plan
25+
row :api_token
26+
row :state
27+
row :state_detail
28+
row :last_activity_at
29+
row :last_transport_at
30+
# wake_secret is deliberately not rendered — it's a credential.
31+
row :wake_url
32+
row :wakes_answered_count
33+
row :wake_failures_count
34+
row :created_at
35+
row :updated_at
36+
end
37+
end
38+
end

config/initializers/coplan.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
# }
4141
# }
4242

43+
# Wake webhook egress: the engine default refuses URLs whose hosts
44+
# resolve to private/loopback/link-local space (SSRF). Locally that's
45+
# exactly where agents live, and specs use non-resolving example hosts,
46+
# so dev and test allow any well-formed http(s) URL.
47+
if Rails.env.development? || Rails.env.test?
48+
config.wake_url_policy = ->(uri) { true }
49+
end
50+
4351
config.notification_handler = ->(event, payload) {
4452
case event
4553
when :comment_created

db/cable_schema.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
t.binary "payload", limit: 536870912, null: false
55
t.datetime "created_at", null: false
66
t.integer "channel_hash", limit: 8, null: false
7-
t.index ["channel"], name: "index_solid_cable_messages_on_channel"
8-
t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
9-
t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
7+
t.index [ "channel" ], name: "index_solid_cable_messages_on_channel"
8+
t.index [ "channel_hash" ], name: "index_solid_cable_messages_on_channel_hash"
9+
t.index [ "created_at" ], name: "index_solid_cable_messages_on_created_at"
1010
end
1111
end

db/cache_schema.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
t.datetime "created_at", null: false
66
t.integer "key_hash", limit: 8, null: false
77
t.integer "byte_size", limit: 4, null: false
8-
t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
9-
t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
10-
t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
8+
t.index [ "byte_size" ], name: "index_solid_cache_entries_on_byte_size"
9+
t.index [ "key_hash", "byte_size" ], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
10+
t.index [ "key_hash" ], name: "index_solid_cache_entries_on_key_hash", unique: true
1111
end
1212
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# This migration comes from co_plan (originally 20260807000000)
2+
class CreateAgentCollaborationTables < ActiveRecord::Migration[8.1]
3+
# Guarded because these objects may already exist on both sides of the
4+
# split this migration heals: databases loaded from schema.rb carry the
5+
# tables (they leaked into the schema via #175's regeneration against a
6+
# dev database), and api_tokens.agent_name ships with the identity
7+
# migration (20260815000000), which runs first on hosts that install
8+
# this one later.
9+
def change
10+
# Durable per-agent event inbox. IDs are UUIDv7 (time-ordered), so the
11+
# id doubles as the pagination cursor: "give me events after <id>".
12+
unless table_exists?(:coplan_agent_events)
13+
create_table :coplan_agent_events, id: { type: :string, limit: 36 } do |t|
14+
t.string :api_token_id, limit: 36, null: false
15+
t.string :plan_id, limit: 36, null: false
16+
t.string :comment_thread_id, limit: 36
17+
t.string :comment_id, limit: 36
18+
t.string :event_type, null: false
19+
t.json :payload
20+
t.datetime :acked_at
21+
t.datetime :created_at, null: false
22+
23+
t.index [ :api_token_id, :id ]
24+
t.index [ :api_token_id, :acked_at ]
25+
t.index :plan_id
26+
end
27+
28+
add_foreign_key :coplan_agent_events, :coplan_api_tokens, column: :api_token_id
29+
add_foreign_key :coplan_agent_events, :coplan_plans, column: :plan_id
30+
end
31+
32+
# One session per (plan, agent token) — Linear-style delegation state
33+
# machine driving the presence pill: pending / active / awaiting_input /
34+
# complete / stale.
35+
unless table_exists?(:coplan_agent_sessions)
36+
create_table :coplan_agent_sessions, id: { type: :string, limit: 36 } do |t|
37+
t.string :plan_id, limit: 36, null: false
38+
t.string :api_token_id, limit: 36, null: false
39+
t.string :agent_name, null: false
40+
t.string :state, null: false, default: "pending"
41+
t.string :state_detail
42+
t.datetime :last_activity_at
43+
t.timestamps
44+
45+
t.index [ :plan_id, :api_token_id ], unique: true
46+
t.index :api_token_id
47+
end
48+
49+
add_foreign_key :coplan_agent_sessions, :coplan_api_tokens, column: :api_token_id
50+
add_foreign_key :coplan_agent_sessions, :coplan_plans, column: :plan_id
51+
end
52+
53+
# Stable display identity for an agent token, instead of the free-text
54+
# per-comment agent_name. Normally added by 20260815000000 already.
55+
unless column_exists?(:coplan_api_tokens, :agent_name)
56+
add_column :coplan_api_tokens, :agent_name, :string
57+
end
58+
end
59+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# This migration comes from co_plan (originally 20260820000000)
2+
class AddWakePlumbingToAgentSessions < ActiveRecord::Migration[8.0]
3+
# Guarded like 20260807000000: main's schema.rb has carried leaked
4+
# agent-collab structure before, so never assume a clean slate.
5+
def change
6+
return unless table_exists?(:coplan_agent_sessions)
7+
8+
# Evidence a connection is actually parked on this session's token —
9+
# SSE heartbeats and long-poll parks touch it. Distinct from
10+
# last_activity_at, which tracks the agent/state machine: a held
11+
# socket must not keep a `pending` promise alive forever.
12+
unless column_exists?(:coplan_agent_sessions, :last_transport_at)
13+
add_column :coplan_agent_sessions, :last_transport_at, :datetime
14+
end
15+
16+
# How many wakes this session has demonstrably answered (pending →
17+
# an agent-driven state). Zero means the loop is unproven and the
18+
# pill makes no wake promise.
19+
unless column_exists?(:coplan_agent_sessions, :wakes_answered_count)
20+
add_column :coplan_agent_sessions, :wakes_answered_count, :integer, default: 0, null: false
21+
end
22+
23+
# Webhook wake: a session may register a URL CoPlan POSTs a signed
24+
# "you have inbox items" ping to — the wake path for hosted agents
25+
# that can receive HTTP but can't hold a connection or be resumed.
26+
unless column_exists?(:coplan_agent_sessions, :wake_url)
27+
add_column :coplan_agent_sessions, :wake_url, :string
28+
end
29+
30+
unless column_exists?(:coplan_agent_sessions, :wake_secret)
31+
add_column :coplan_agent_sessions, :wake_secret, :string
32+
end
33+
end
34+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This migration comes from co_plan (originally 20260821000000)
2+
class AddWakeFailuresCountToAgentSessions < ActiveRecord::Migration[8.0]
3+
def change
4+
return unless table_exists?(:coplan_agent_sessions)
5+
return if column_exists?(:coplan_agent_sessions, :wake_failures_count)
6+
7+
# Exhausted wake-webhook delivery runs since the last success; the
8+
# URL is unregistered once this crosses WakeWebhookJob::MAX_EXHAUSTIONS.
9+
add_column :coplan_agent_sessions, :wake_failures_count, :integer, default: 0, null: false
10+
end
11+
end

db/schema.rb

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)