@consiliency/agent-board-schema 1.68.0 → 1.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/already-applied.json +28 -4
- package/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
- package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
- package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
- package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
- package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
- package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
- package/package.json +1 -1
- package/schema-manifest.json +57 -3
- package/src/tooling.test.mjs +8 -2
- package/tests/agent_board_661_reply_delivery.test.sql +185 -0
- package/tests/agent_board_667_boundary.test.sql +162 -0
- package/tests/agent_board_667_conversion_branches.test.sql +91 -0
- package/tests/agent_board_667_conversion_profile.test.sql +112 -0
- package/tests/agent_board_667_history_class.test.sql +785 -0
- package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
- package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
- package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
- package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
- package/tests/agent_board_667_retention.test.sql +201 -0
- package/tests/agent_board_667_routing.test.sql +294 -0
- package/tests/agent_board_667_sender_effects.test.sql +100 -0
- package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
- package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
- package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
- package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
- package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
- package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
- package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
- package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
- package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
- package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
- package/tests/agent_board_667_writers.test.sql +281 -0
- package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
- package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
- package/tests/agent_board_addressing.test.sql +8 -5
- package/tests/agent_board_claim_subject_gate.test.sql +7 -3
- package/tests/agent_board_claimpolicy_race.test.sql +39 -11
- package/tests/agent_board_delegate_role_class.test.sql +33 -15
- package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
- package/tests/agent_board_fk_lock_edges.test.sql +3 -0
- package/tests/agent_board_hosted_delegates.test.sql +5 -2
- package/tests/agent_board_inbox_rpcs.test.sql +5 -3
- package/tests/agent_board_obsgate.test.sql +8 -4
- package/tests/agent_board_obsgate4.test.sql +2 -2
- package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
- package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
- package/tests/agent_board_session_identity_behavior_classes.test.sql +2 -2
- package/tests/agent_board_session_identity_limits.test.sql +1 -1
- package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
- package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
- package/tests/agent_board_sidserver_authority.test.sql +69 -0
- package/tests/agent_board_sidserver_catalog.test.sql +47 -0
- package/tests/agent_board_signed_write_gateway.test.sql +4 -2
- package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
- package/tests/agent_board_v2_invariants.test.sql +5 -2
- package/tests/agent_board_write_enforcement_class.test.sql +3 -3
- package/tests/catalog/function_execute_privileges.json +41 -5
- package/tests/catalog/p_metadata_parameters.json +16 -0
- package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
- package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
- package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
- package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
- package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
- package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
- package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
- package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
- package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
- package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
- package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
- package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
- package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
- package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
- package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
- package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
- package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
- package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
- package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
- package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
- package/tests/isolation/expected/association_board_vs_board.out +71 -0
- package/tests/isolation/expected/association_post_wait_authority.out +77 -0
- package/tests/isolation/expected/association_strong_history_writers.out +203 -0
- package/tests/isolation/expected/association_strong_isolation.out +185 -0
- package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
- package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
- package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
- package/tests/isolation/expected/association_vs_private_message.out +67 -0
- package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
- package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
- package/tests/isolation/expected/message_replay_scope.out +209 -0
- package/tests/isolation/expected/tasking_replay_scope.out +125 -0
- package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
- package/tests/isolation/specs/association_board_vs_board.spec +37 -0
- package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
- package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
- package/tests/isolation/specs/association_strong_isolation.spec +50 -0
- package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
- package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
- package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
- package/tests/isolation/specs/association_vs_private_message.spec +33 -0
- package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
- package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
- package/tests/isolation/specs/message_replay_scope.spec +77 -0
- package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
- package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
- package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
- package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
- package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
- package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
- package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
- package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
- package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
- package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
- package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
- package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
- package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
- package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
- package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
- package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
- package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
- package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
- package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
- package/tests/mutations/717-f1/run.sh +17 -0
- package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
- package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
- package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
- package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
- package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
- package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
- package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
- package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
- package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
- package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
- package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
- package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
- package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
- package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
- package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
- package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
- package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
- package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
- package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
- package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
- package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
- package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
- package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
- package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
- package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
- package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
- package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
- package/tests/mutations/717-f2b/run.sh +82 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
-- #661: separate authorized insertion from public reply delivery.
|
|
2
|
+
-- Current-tree definitions replayed in an owned disposable database; no live catalog source.
|
|
3
|
+
CREATE OR REPLACE FUNCTION agent_board._insert_thread_message(p_thread_id uuid, p_author_agent_id uuid, p_body text, p_message_type text DEFAULT 'comment'::text, p_task_id uuid DEFAULT NULL::uuid, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
4
|
+
RETURNS TABLE(message_id uuid, thread_id uuid, task_id uuid, message_sequence bigint, created_at timestamp with time zone)
|
|
5
|
+
LANGUAGE plpgsql
|
|
6
|
+
SECURITY DEFINER
|
|
7
|
+
SET search_path TO 'agent_board', 'public'
|
|
8
|
+
AS $function$
|
|
9
|
+
declare
|
|
10
|
+
v_sequence bigint;
|
|
11
|
+
v_actor_agent_id uuid;
|
|
12
|
+
v_metadata jsonb;
|
|
13
|
+
v_instance agent_board.delegate_instances%rowtype;
|
|
14
|
+
begin
|
|
15
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
16
|
+
|
|
17
|
+
if nullif(trim(p_body), '') is null then
|
|
18
|
+
raise exception 'message body is required' using errcode = '22023';
|
|
19
|
+
end if;
|
|
20
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
21
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
22
|
+
end if;
|
|
23
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
24
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
25
|
+
end if;
|
|
26
|
+
-- OBSGATE (#78): a board-linked thread under 'inherit' requires a non-observer board role.
|
|
27
|
+
-- All message entrypoints use this authorized insertion primitive; the gate cannot be bypassed
|
|
28
|
+
-- by any send path. Board-less threads and service_role fall through (access check above).
|
|
29
|
+
if not agent_board._current_agent_can_post_to_thread(p_thread_id) then
|
|
30
|
+
raise exception 'thread posting requires a non-observer board role' using errcode = '42501';
|
|
31
|
+
end if;
|
|
32
|
+
if p_task_id is not null and not exists (select 1 from agent_board.tasks where id = p_task_id and tasks.thread_id = p_thread_id) then
|
|
33
|
+
raise exception 'task not found in thread' using errcode = '22023';
|
|
34
|
+
end if;
|
|
35
|
+
|
|
36
|
+
-- HOSTED attribution: the `delegate` key is written by the server, never by a caller. A
|
|
37
|
+
-- hosted delegate's post carries it (overwriting anything supplied); every other author's
|
|
38
|
+
-- caller-supplied `delegate` key is removed, so no keyed agent can forge the mark.
|
|
39
|
+
-- client_verified is always false: under dynamic client registration anyone can name a client.
|
|
40
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb);
|
|
41
|
+
if jsonb_typeof(v_metadata) = 'object' then
|
|
42
|
+
v_metadata := v_metadata - 'delegate';
|
|
43
|
+
end if;
|
|
44
|
+
select di.* into v_instance
|
|
45
|
+
from agent_board.agents a
|
|
46
|
+
join agent_board.delegate_instances di on di.agent_id = a.id
|
|
47
|
+
where a.id = v_actor_agent_id
|
|
48
|
+
and a.is_delegate;
|
|
49
|
+
if found then
|
|
50
|
+
if jsonb_typeof(v_metadata) <> 'object' then
|
|
51
|
+
raise exception 'a hosted delegate''s message metadata must be a JSON object' using errcode = '22023';
|
|
52
|
+
end if;
|
|
53
|
+
v_metadata := v_metadata || jsonb_build_object('delegate', jsonb_build_object(
|
|
54
|
+
'delegate', true,
|
|
55
|
+
'instance_id', v_instance.instance_id,
|
|
56
|
+
'owner_agent_id', agent_board._delegate_principal_agent_id(v_actor_agent_id),
|
|
57
|
+
'client_id', v_instance.client_id,
|
|
58
|
+
'client_verified', false));
|
|
59
|
+
end if;
|
|
60
|
+
|
|
61
|
+
v_sequence := agent_board._next_thread_message_sequence(p_thread_id);
|
|
62
|
+
-- The sequence update may have waited for an association; re-read after that wait.
|
|
63
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
64
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
65
|
+
end if;
|
|
66
|
+
if not agent_board._current_agent_can_post_to_thread(p_thread_id) then
|
|
67
|
+
raise exception 'thread posting requires a non-observer board role' using errcode = '42501';
|
|
68
|
+
end if;
|
|
69
|
+
|
|
70
|
+
return query
|
|
71
|
+
insert into agent_board.messages(thread_id, author_agent_id, task_id, message_sequence, message_type, body, metadata)
|
|
72
|
+
values (p_thread_id, v_actor_agent_id, p_task_id, v_sequence, coalesce(p_message_type, 'comment'), p_body, v_metadata)
|
|
73
|
+
returning id, messages.thread_id, messages.task_id, messages.message_sequence, messages.created_at;
|
|
74
|
+
end;
|
|
75
|
+
$function$;
|
|
76
|
+
revoke all on function agent_board._insert_thread_message(uuid,uuid,text,text,uuid,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
|
|
77
|
+
comment on function agent_board._insert_thread_message(uuid,uuid,text,text,uuid,jsonb) is 'Internal authorized message insertion; preserves sequence locking, session author and hosted attribution. Delivery is the caller responsibility.';
|
|
78
|
+
|
|
79
|
+
create or replace function agent_board._deliver_board_message(p_message_id uuid, p_subject text, p_body text, p_priority text)
|
|
80
|
+
returns table(delivered_recipient_count integer, callback_outbox_count integer)
|
|
81
|
+
language plpgsql security definer set search_path=agent_board,public as $function$
|
|
82
|
+
declare
|
|
83
|
+
v_message agent_board.messages%rowtype;
|
|
84
|
+
v_membership record;
|
|
85
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
86
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
87
|
+
v_delivered_count integer := 0;
|
|
88
|
+
v_callback_count integer := 0;
|
|
89
|
+
v_preview text;
|
|
90
|
+
v_fetch_descriptor jsonb;
|
|
91
|
+
v_actor_agent_id uuid;
|
|
92
|
+
v_is_author boolean;
|
|
93
|
+
p_board_id uuid;
|
|
94
|
+
p_thread_id uuid;
|
|
95
|
+
begin
|
|
96
|
+
select * into strict v_message from agent_board.messages where id=p_message_id;
|
|
97
|
+
p_thread_id := v_message.thread_id;
|
|
98
|
+
v_actor_agent_id := v_message.author_agent_id;
|
|
99
|
+
select board_id into p_board_id from agent_board.threads where id=p_thread_id;
|
|
100
|
+
if p_board_id is null then
|
|
101
|
+
raise exception 'board delivery requires a board thread' using errcode='22023';
|
|
102
|
+
end if;
|
|
103
|
+
-- Keep the canonical early board/thread boundary before any recipient sink,
|
|
104
|
+
-- including boards whose current membership set is empty.
|
|
105
|
+
perform agent_board._associate_empty_thread(p_thread_id, p_board_id);
|
|
106
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
107
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
108
|
+
'board',
|
|
109
|
+
p_board_id,
|
|
110
|
+
'agent_board.v_board_feed',
|
|
111
|
+
'board delivery',
|
|
112
|
+
jsonb_build_object('messageId', v_message.id::text)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
for v_membership in
|
|
116
|
+
select bm.id, bm.agent_id
|
|
117
|
+
from agent_board.board_memberships bm
|
|
118
|
+
where bm.board_id = p_board_id
|
|
119
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
120
|
+
order by bm.created_at
|
|
121
|
+
loop
|
|
122
|
+
-- #608: the author is not a recipient of its own post. Its row stays, FEED-ONLY
|
|
123
|
+
-- (visible_in_inbox = false), because v_board_feed is built from these rows and a post on a
|
|
124
|
+
-- board whose only member is its author would otherwise vanish from the feed. A feed-only
|
|
125
|
+
-- row is outside v_agent_inbox, v_agent_wake_recovery and v_agent_inbox_counts by their own
|
|
126
|
+
-- `visible_in_inbox` filter, and _queue_inbox_callbacks queues nothing for it.
|
|
127
|
+
v_is_author := v_membership.agent_id is not distinct from v_actor_agent_id;
|
|
128
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
129
|
+
'board',
|
|
130
|
+
v_membership.agent_id,
|
|
131
|
+
'message',
|
|
132
|
+
v_message.id,
|
|
133
|
+
null,
|
|
134
|
+
null,
|
|
135
|
+
p_board_id,
|
|
136
|
+
v_membership.id,
|
|
137
|
+
jsonb_build_object('priority', p_priority)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
141
|
+
v_recipient.id,
|
|
142
|
+
v_membership.agent_id,
|
|
143
|
+
p_board_id,
|
|
144
|
+
v_recipient.delivery_mode,
|
|
145
|
+
'message',
|
|
146
|
+
v_message.id,
|
|
147
|
+
null,
|
|
148
|
+
null,
|
|
149
|
+
trim(p_subject),
|
|
150
|
+
v_preview,
|
|
151
|
+
p_priority,
|
|
152
|
+
not v_is_author,
|
|
153
|
+
true,
|
|
154
|
+
v_fetch_descriptor,
|
|
155
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
-- delivered_recipient_count counts RECIPIENTS; the author's feed-only row is not one.
|
|
159
|
+
if not v_is_author then
|
|
160
|
+
v_delivered_count := v_delivered_count + 1;
|
|
161
|
+
end if;
|
|
162
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
163
|
+
end loop;
|
|
164
|
+
|
|
165
|
+
v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
|
|
166
|
+
p_board_id,
|
|
167
|
+
v_message.id,
|
|
168
|
+
trim(p_subject),
|
|
169
|
+
v_preview,
|
|
170
|
+
p_priority,
|
|
171
|
+
v_fetch_descriptor
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return query select v_delivered_count,v_callback_count;
|
|
175
|
+
end;
|
|
176
|
+
$function$;
|
|
177
|
+
revoke all on function agent_board._deliver_board_message(uuid,text,text,text) from public,anon,authenticated,service_role,agent_board_delegate;
|
|
178
|
+
comment on function agent_board._deliver_board_message(uuid,text,text,text) is 'Internal canonical board fan-out for an authorized persisted message; board and author derive from the source, with feed-only self delivery and existing callbacks.';
|
|
179
|
+
|
|
180
|
+
CREATE OR REPLACE FUNCTION agent_board.post_board_message(p_thread_id uuid, p_author_agent_id uuid, p_board_id uuid, p_subject text, p_body text, p_message_type text DEFAULT 'comment'::text, p_priority text DEFAULT 'medium'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
181
|
+
RETURNS TABLE(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamp with time zone)
|
|
182
|
+
LANGUAGE plpgsql
|
|
183
|
+
SECURITY DEFINER
|
|
184
|
+
SET search_path TO 'agent_board', 'public'
|
|
185
|
+
AS $function$
|
|
186
|
+
declare
|
|
187
|
+
v_message agent_board.messages%rowtype;
|
|
188
|
+
v_created_message record;
|
|
189
|
+
v_membership record;
|
|
190
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
191
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
192
|
+
v_delivered_count integer := 0;
|
|
193
|
+
v_callback_count integer := 0;
|
|
194
|
+
v_metadata jsonb;
|
|
195
|
+
v_preview text;
|
|
196
|
+
v_fetch_descriptor jsonb;
|
|
197
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
198
|
+
v_actor_agent_id uuid;
|
|
199
|
+
v_is_author boolean;
|
|
200
|
+
begin
|
|
201
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
202
|
+
|
|
203
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
204
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
205
|
+
end if;
|
|
206
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
207
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
208
|
+
end if;
|
|
209
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
210
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
211
|
+
end if;
|
|
212
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
213
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
214
|
+
end if;
|
|
215
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
216
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
217
|
+
end if;
|
|
218
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
219
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
220
|
+
end if;
|
|
221
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
222
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
223
|
+
end if;
|
|
224
|
+
|
|
225
|
+
if agent_board._is_service_role() then
|
|
226
|
+
perform agent_board._record_send_authz_telemetry(
|
|
227
|
+
'service_role_bypass',
|
|
228
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
229
|
+
v_actor_agent_id,
|
|
230
|
+
p_board_id,
|
|
231
|
+
'post_board_message',
|
|
232
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
233
|
+
null,
|
|
234
|
+
jsonb_build_object('redacted', true)
|
|
235
|
+
);
|
|
236
|
+
end if;
|
|
237
|
+
|
|
238
|
+
perform agent_board._lock_idempotency_key(
|
|
239
|
+
'post_board_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || p_board_id::text,
|
|
240
|
+
v_idempotency_key
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if p_board_id is not null then
|
|
244
|
+
perform agent_board._associate_empty_thread(p_thread_id, p_board_id);
|
|
245
|
+
-- Both checks are new statements after the thread-lock wait.
|
|
246
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
247
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
248
|
+
end if;
|
|
249
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
250
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
251
|
+
end if;
|
|
252
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
253
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
254
|
+
end if;
|
|
255
|
+
end if;
|
|
256
|
+
|
|
257
|
+
if v_idempotency_key is not null then
|
|
258
|
+
select *
|
|
259
|
+
into v_message
|
|
260
|
+
from agent_board.messages
|
|
261
|
+
where thread_id = p_thread_id
|
|
262
|
+
and author_agent_id = v_actor_agent_id
|
|
263
|
+
and metadata ->> 'rpcName' = 'post_board_message'
|
|
264
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
265
|
+
order by created_at desc, id desc
|
|
266
|
+
limit 1;
|
|
267
|
+
end if;
|
|
268
|
+
|
|
269
|
+
if v_message.id is null then
|
|
270
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
271
|
+
|| jsonb_build_object(
|
|
272
|
+
'rpcName', 'post_board_message',
|
|
273
|
+
'subject', trim(p_subject),
|
|
274
|
+
'priority', p_priority,
|
|
275
|
+
'boardId', p_board_id::text
|
|
276
|
+
);
|
|
277
|
+
if v_idempotency_key is not null then
|
|
278
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
279
|
+
end if;
|
|
280
|
+
|
|
281
|
+
select *
|
|
282
|
+
into v_created_message
|
|
283
|
+
from agent_board._insert_thread_message(
|
|
284
|
+
p_thread_id,
|
|
285
|
+
v_actor_agent_id,
|
|
286
|
+
p_body,
|
|
287
|
+
p_message_type,
|
|
288
|
+
null,
|
|
289
|
+
v_metadata
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
select *
|
|
293
|
+
into v_message
|
|
294
|
+
from agent_board.messages
|
|
295
|
+
where id = v_created_message.message_id;
|
|
296
|
+
end if;
|
|
297
|
+
|
|
298
|
+
select d.delivered_recipient_count, d.callback_outbox_count
|
|
299
|
+
into v_delivered_count, v_callback_count
|
|
300
|
+
from agent_board._deliver_board_message(v_message.id, trim(p_subject), p_body, p_priority) d;
|
|
301
|
+
|
|
302
|
+
return query
|
|
303
|
+
select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
|
|
304
|
+
end;
|
|
305
|
+
$function$;
|
|
306
|
+
|
|
307
|
+
CREATE OR REPLACE FUNCTION agent_board.post_message(p_thread_id uuid, p_author_agent_id uuid, p_body text, p_message_type text DEFAULT 'comment'::text, p_task_id uuid DEFAULT NULL::uuid, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
308
|
+
RETURNS TABLE(message_id uuid, thread_id uuid, task_id uuid, message_sequence bigint, created_at timestamp with time zone)
|
|
309
|
+
LANGUAGE plpgsql
|
|
310
|
+
SECURITY DEFINER
|
|
311
|
+
SET search_path TO 'agent_board', 'public'
|
|
312
|
+
AS $function$
|
|
313
|
+
declare
|
|
314
|
+
v_message record;
|
|
315
|
+
v_board_id uuid;
|
|
316
|
+
v_subject text;
|
|
317
|
+
v_access_mode text;
|
|
318
|
+
begin
|
|
319
|
+
select * into v_message from agent_board._insert_thread_message(
|
|
320
|
+
p_thread_id,p_author_agent_id,p_body,p_message_type,p_task_id,p_metadata);
|
|
321
|
+
-- Insertion holds the sequence/thread lock. Read the association after that
|
|
322
|
+
-- lock, so an initially unstamped thread cannot race past board delivery.
|
|
323
|
+
select t.board_id,coalesce(nullif(trim(t.subject),''),'Thread reply'),t.access_mode
|
|
324
|
+
into v_board_id,v_subject,v_access_mode from agent_board.threads t where t.id=p_thread_id;
|
|
325
|
+
-- Restricted threads keep their explicit audience; board association alone
|
|
326
|
+
-- does not authorize broadcasting an ordinary reply to all board members.
|
|
327
|
+
if v_board_id is not null and v_access_mode = 'inherit' then
|
|
328
|
+
if not agent_board._current_agent_has_board_access(v_board_id) then
|
|
329
|
+
raise exception 'board not accessible to current agent' using errcode='42501';
|
|
330
|
+
end if;
|
|
331
|
+
if not agent_board._current_agent_can_send_to_board(v_board_id) then
|
|
332
|
+
raise exception 'board send requires a non-observer board role' using errcode='42501';
|
|
333
|
+
end if;
|
|
334
|
+
perform * from agent_board._deliver_board_message(v_message.message_id,v_subject,p_body,'medium');
|
|
335
|
+
end if;
|
|
336
|
+
return query select v_message.message_id,v_message.thread_id,v_message.task_id,
|
|
337
|
+
v_message.message_sequence,v_message.created_at;
|
|
338
|
+
end;
|
|
339
|
+
$function$;
|
|
340
|
+
|
|
341
|
+
CREATE OR REPLACE FUNCTION agent_board.send_direct_message(p_thread_id uuid, p_author_agent_id uuid, p_recipient_agent_id uuid, p_subject text, p_body text, p_message_type text DEFAULT 'comment'::text, p_priority text DEFAULT 'medium'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
342
|
+
RETURNS TABLE(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamp with time zone)
|
|
343
|
+
LANGUAGE plpgsql
|
|
344
|
+
SECURITY DEFINER
|
|
345
|
+
SET search_path TO 'agent_board', 'public'
|
|
346
|
+
AS $function$
|
|
347
|
+
declare
|
|
348
|
+
v_message agent_board.messages%rowtype;
|
|
349
|
+
v_created_message record;
|
|
350
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
351
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
352
|
+
v_callback_count integer := 0;
|
|
353
|
+
v_metadata jsonb;
|
|
354
|
+
v_preview text;
|
|
355
|
+
v_fetch_descriptor jsonb;
|
|
356
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
357
|
+
v_actor_agent_id uuid;
|
|
358
|
+
begin
|
|
359
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
360
|
+
|
|
361
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
362
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
363
|
+
end if;
|
|
364
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
365
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
366
|
+
end if;
|
|
367
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
368
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
369
|
+
end if;
|
|
370
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
371
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
372
|
+
end if;
|
|
373
|
+
if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
|
|
374
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
375
|
+
end if;
|
|
376
|
+
|
|
377
|
+
if agent_board._is_service_role() then
|
|
378
|
+
perform agent_board._record_send_authz_telemetry(
|
|
379
|
+
'service_role_bypass',
|
|
380
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
381
|
+
v_actor_agent_id,
|
|
382
|
+
null,
|
|
383
|
+
'send_direct_message',
|
|
384
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
385
|
+
null,
|
|
386
|
+
jsonb_build_object('redacted', true)
|
|
387
|
+
);
|
|
388
|
+
end if;
|
|
389
|
+
|
|
390
|
+
perform agent_board._lock_idempotency_key(
|
|
391
|
+
'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
392
|
+
v_idempotency_key
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if v_idempotency_key is not null then
|
|
396
|
+
select *
|
|
397
|
+
into v_message
|
|
398
|
+
from agent_board.messages
|
|
399
|
+
where thread_id = p_thread_id
|
|
400
|
+
and author_agent_id = v_actor_agent_id
|
|
401
|
+
and metadata ->> 'rpcName' = 'send_direct_message'
|
|
402
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
403
|
+
order by created_at desc
|
|
404
|
+
limit 1;
|
|
405
|
+
end if;
|
|
406
|
+
|
|
407
|
+
if v_message.id is null then
|
|
408
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
409
|
+
|| jsonb_build_object(
|
|
410
|
+
'rpcName', 'send_direct_message',
|
|
411
|
+
'subject', trim(p_subject),
|
|
412
|
+
'priority', p_priority
|
|
413
|
+
);
|
|
414
|
+
if v_idempotency_key is not null then
|
|
415
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
416
|
+
end if;
|
|
417
|
+
|
|
418
|
+
select *
|
|
419
|
+
into v_created_message
|
|
420
|
+
from agent_board._insert_thread_message(
|
|
421
|
+
p_thread_id,
|
|
422
|
+
v_actor_agent_id,
|
|
423
|
+
p_body,
|
|
424
|
+
p_message_type,
|
|
425
|
+
null,
|
|
426
|
+
v_metadata
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
select *
|
|
430
|
+
into v_message
|
|
431
|
+
from agent_board.messages
|
|
432
|
+
where id = v_created_message.message_id;
|
|
433
|
+
end if;
|
|
434
|
+
|
|
435
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
436
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
437
|
+
'thread',
|
|
438
|
+
p_thread_id,
|
|
439
|
+
'agent_board.v_thread_feed',
|
|
440
|
+
'direct delivery',
|
|
441
|
+
jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
445
|
+
'direct',
|
|
446
|
+
p_recipient_agent_id,
|
|
447
|
+
'message',
|
|
448
|
+
v_message.id,
|
|
449
|
+
null,
|
|
450
|
+
null,
|
|
451
|
+
null,
|
|
452
|
+
null,
|
|
453
|
+
jsonb_build_object('priority', p_priority)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
457
|
+
v_recipient.id,
|
|
458
|
+
p_recipient_agent_id,
|
|
459
|
+
null,
|
|
460
|
+
v_recipient.delivery_mode,
|
|
461
|
+
'message',
|
|
462
|
+
v_message.id,
|
|
463
|
+
null,
|
|
464
|
+
null,
|
|
465
|
+
trim(p_subject),
|
|
466
|
+
v_preview,
|
|
467
|
+
p_priority,
|
|
468
|
+
true,
|
|
469
|
+
false,
|
|
470
|
+
v_fetch_descriptor,
|
|
471
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
475
|
+
|
|
476
|
+
return query
|
|
477
|
+
select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
|
|
478
|
+
end;
|
|
479
|
+
$function$;
|
|
480
|
+
|
|
481
|
+
CREATE OR REPLACE FUNCTION agent_board.send_multicast_message(p_thread_id uuid, p_author_agent_id uuid, p_recipient_agent_ids uuid[], p_subject text, p_body text, p_message_type text DEFAULT 'comment'::text, p_priority text DEFAULT 'medium'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
482
|
+
RETURNS TABLE(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamp with time zone)
|
|
483
|
+
LANGUAGE plpgsql
|
|
484
|
+
SECURITY DEFINER
|
|
485
|
+
SET search_path TO 'agent_board', 'public'
|
|
486
|
+
AS $function$
|
|
487
|
+
declare
|
|
488
|
+
v_message agent_board.messages%rowtype;
|
|
489
|
+
v_created_message record;
|
|
490
|
+
v_recipient_ids uuid[];
|
|
491
|
+
v_recipient_id uuid;
|
|
492
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
493
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
494
|
+
v_delivered_count integer := 0;
|
|
495
|
+
v_inbox_count integer := 0;
|
|
496
|
+
v_callback_count integer := 0;
|
|
497
|
+
v_metadata jsonb;
|
|
498
|
+
v_preview text;
|
|
499
|
+
v_fetch_descriptor jsonb;
|
|
500
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
501
|
+
v_actor_agent_id uuid;
|
|
502
|
+
begin
|
|
503
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
504
|
+
|
|
505
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
506
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
507
|
+
end if;
|
|
508
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
509
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
510
|
+
end if;
|
|
511
|
+
if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
|
|
512
|
+
raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
|
|
513
|
+
end if;
|
|
514
|
+
if array_position(p_recipient_agent_ids, null) is not null then
|
|
515
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
516
|
+
end if;
|
|
517
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
518
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
519
|
+
end if;
|
|
520
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
521
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
522
|
+
end if;
|
|
523
|
+
|
|
524
|
+
select array_agg(distinct recipient_id)
|
|
525
|
+
into v_recipient_ids
|
|
526
|
+
from unnest(p_recipient_agent_ids) as recipient_id;
|
|
527
|
+
|
|
528
|
+
if (
|
|
529
|
+
select count(*)
|
|
530
|
+
from agent_board.agents
|
|
531
|
+
where id = any(v_recipient_ids)
|
|
532
|
+
) <> cardinality(v_recipient_ids) then
|
|
533
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
534
|
+
end if;
|
|
535
|
+
|
|
536
|
+
if agent_board._is_service_role() then
|
|
537
|
+
perform agent_board._record_send_authz_telemetry(
|
|
538
|
+
'service_role_bypass',
|
|
539
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
540
|
+
v_actor_agent_id,
|
|
541
|
+
null,
|
|
542
|
+
'send_multicast_message',
|
|
543
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
544
|
+
null,
|
|
545
|
+
jsonb_build_object('redacted', true)
|
|
546
|
+
);
|
|
547
|
+
end if;
|
|
548
|
+
|
|
549
|
+
perform agent_board._lock_idempotency_key(
|
|
550
|
+
'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
551
|
+
v_idempotency_key
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
if v_idempotency_key is not null then
|
|
555
|
+
select *
|
|
556
|
+
into v_message
|
|
557
|
+
from agent_board.messages
|
|
558
|
+
where thread_id = p_thread_id
|
|
559
|
+
and author_agent_id = v_actor_agent_id
|
|
560
|
+
and metadata ->> 'rpcName' = 'send_multicast_message'
|
|
561
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
562
|
+
order by created_at desc
|
|
563
|
+
limit 1;
|
|
564
|
+
end if;
|
|
565
|
+
|
|
566
|
+
if v_message.id is null then
|
|
567
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
568
|
+
|| jsonb_build_object(
|
|
569
|
+
'rpcName', 'send_multicast_message',
|
|
570
|
+
'subject', trim(p_subject),
|
|
571
|
+
'priority', p_priority
|
|
572
|
+
);
|
|
573
|
+
if v_idempotency_key is not null then
|
|
574
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
575
|
+
end if;
|
|
576
|
+
|
|
577
|
+
select *
|
|
578
|
+
into v_created_message
|
|
579
|
+
from agent_board._insert_thread_message(
|
|
580
|
+
p_thread_id,
|
|
581
|
+
v_actor_agent_id,
|
|
582
|
+
p_body,
|
|
583
|
+
p_message_type,
|
|
584
|
+
null,
|
|
585
|
+
v_metadata
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
select *
|
|
589
|
+
into v_message
|
|
590
|
+
from agent_board.messages
|
|
591
|
+
where id = v_created_message.message_id;
|
|
592
|
+
end if;
|
|
593
|
+
|
|
594
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
595
|
+
|
|
596
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
597
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
598
|
+
'thread',
|
|
599
|
+
p_thread_id,
|
|
600
|
+
'agent_board.v_thread_feed',
|
|
601
|
+
'multicast delivery',
|
|
602
|
+
jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
606
|
+
'multicast',
|
|
607
|
+
v_recipient_id,
|
|
608
|
+
'message',
|
|
609
|
+
v_message.id,
|
|
610
|
+
null,
|
|
611
|
+
null,
|
|
612
|
+
null,
|
|
613
|
+
null,
|
|
614
|
+
jsonb_build_object('priority', p_priority)
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
618
|
+
v_recipient.id,
|
|
619
|
+
v_recipient_id,
|
|
620
|
+
null,
|
|
621
|
+
v_recipient.delivery_mode,
|
|
622
|
+
'message',
|
|
623
|
+
v_message.id,
|
|
624
|
+
null,
|
|
625
|
+
null,
|
|
626
|
+
trim(p_subject),
|
|
627
|
+
v_preview,
|
|
628
|
+
p_priority,
|
|
629
|
+
true,
|
|
630
|
+
false,
|
|
631
|
+
v_fetch_descriptor,
|
|
632
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
v_delivered_count := v_delivered_count + 1;
|
|
636
|
+
v_inbox_count := v_inbox_count + 1;
|
|
637
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
638
|
+
end loop;
|
|
639
|
+
|
|
640
|
+
return query
|
|
641
|
+
select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
|
|
642
|
+
end;
|
|
643
|
+
$function$;
|
|
644
|
+
|
|
645
|
+
revoke all on function agent_board.post_message(uuid,uuid,text,text,uuid,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
|
|
646
|
+
grant execute on function agent_board.post_message(uuid,uuid,text,text,uuid,jsonb) to authenticated,service_role,agent_board_delegate;
|
|
647
|
+
comment on function agent_board.post_message(uuid,uuid,text,text,uuid,jsonb) is 'Posts a thread reply; inherited board threads use canonical board delivery; non-board and restricted threads retain insertion-only behavior. Session-derived author and return shape are unchanged.';
|
|
648
|
+
|
|
649
|
+
revoke all on function agent_board.post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
|
|
650
|
+
grant execute on function agent_board.post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) to authenticated,service_role,agent_board_delegate;
|
|
651
|
+
comment on function agent_board.post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) is 'Creates or reuses a canonical board post after validating the session actor, board authority and locked thread association; delivery fan-out and downstream effects are transactional.';
|
|
652
|
+
|
|
653
|
+
revoke all on function agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
|
|
654
|
+
grant execute on function agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) to authenticated,service_role;
|
|
655
|
+
comment on function agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) is 'Sends privately to the explicit recipient, including on board threads; uses the common authorized insertion primitive.';
|
|
656
|
+
|
|
657
|
+
revoke all on function agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
|
|
658
|
+
grant execute on function agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb) to authenticated,service_role;
|
|
659
|
+
comment on function agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb) is 'Sends privately to explicit recipients, including on board threads; uses the common authorized insertion primitive.';
|