@consiliency/agent-board-schema 1.59.0 → 1.60.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 +4 -0
- package/migrations/20261010000100_agent_board_own_post_not_delivered.sql +637 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +5 -2
- package/tests/agent_board_addressing.test.sql +3 -1
- package/tests/agent_board_inbox_rpcs.test.sql +5 -2
- package/tests/agent_board_own_post_not_delivered.test.sql +490 -0
- package/tests/agent_board_v2_invariants.test.sql +4 -4
|
@@ -628,6 +628,10 @@
|
|
|
628
628
|
{
|
|
629
629
|
"id": "20261009000100_agent_board_hosted_followups",
|
|
630
630
|
"sha256": "4c2c4041bb41126b3e1bce940bc8a52ac78b69389a0c866556ba0c0e4adc3564"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
"id": "20261010000100_agent_board_own_post_not_delivered",
|
|
634
|
+
"sha256": "6cbbe0bb9ccf7ab65402a58de145ee21bc2158ddc7502671ea13a98415ca6421"
|
|
631
635
|
}
|
|
632
636
|
]
|
|
633
637
|
}
|
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
-- OWN POST NOT DELIVERED (Consiliency/message-board#608): an agent does not receive its own room
|
|
2
|
+
-- post, or its own task or multicast fanned out to a set it belongs to, as an inbox item. The owner's
|
|
3
|
+
-- rule (2026-10-03): an agent's own writes must not reach its inbox and must not wake it. Measured
|
|
4
|
+
-- before this migration: dev0-claude posted to mb-test-board, its own post appeared in its own
|
|
5
|
+
-- v_agent_wake_recovery, and its wake hook fired for it. schema_version stays 8.
|
|
6
|
+
--
|
|
7
|
+
-- THE CLASS, FROM THE CATALOG. agent_board.inbox_items has exactly one writer,
|
|
8
|
+
-- _upsert_inbox_item, and four callers: send_direct_message, send_multicast_message,
|
|
9
|
+
-- post_board_message and create_tasking_delivery (pg_proc). A recipient set reaches the actor
|
|
10
|
+
-- without the actor naming itself in two places, both fixed here:
|
|
11
|
+
-- * a BOARD expansion: post_board_message and create_tasking_delivery's board loop (the actor's
|
|
12
|
+
-- row is kept, feed-only, in both -- see below);
|
|
13
|
+
-- * a DESCRIPTOR expansion: group_alias and role_selector, resolved by
|
|
14
|
+
-- _addressing_agent_ids_or_raise for every descriptor overload and signed path (section 4).
|
|
15
|
+
-- Naming yourself -- a uuid list, or an agent_id / agent_handle / human_handle descriptor -- is an
|
|
16
|
+
-- explicit act and still delivers. agent_board_own_post_not_delivered.test.sql derives both sets
|
|
17
|
+
-- from pg_proc and asserts every member behaviourally, so a new site fails it by name.
|
|
18
|
+
--
|
|
19
|
+
-- WHY THE AUTHOR'S BOARD ROW IS KEPT, FEED-ONLY. v_board_feed is built from inbox_items rows with
|
|
20
|
+
-- visible_in_board_feed = true (any member's). Dropping the author's row would drop a post on a
|
|
21
|
+
-- board whose only member is its author from that board's feed. inbox_items_visibility_shape
|
|
22
|
+
-- already admits visible_in_inbox = false with visible_in_board_feed = true, and every inbox
|
|
23
|
+
-- surface -- v_agent_inbox, v_agent_wake_recovery (through v_agent_inbox), v_agent_inbox_counts,
|
|
24
|
+
-- v_agent_dashboard -- filters on visible_in_inbox, and v_fleet_delivery_posture reports such a row
|
|
25
|
+
-- as not actionable. v_board_directory's activity reads visible_in_board_feed, which is unchanged.
|
|
26
|
+
-- v_thread_feed reads neither inbox_items nor delivery_recipients. Read receipts (sent / delivered / read per
|
|
27
|
+
-- recipient, the owner's stated next goal) need RECIPIENT rows; the author's feed-only row is
|
|
28
|
+
-- distinguishable from them by visible_in_inbox and is not counted in delivered_recipient_count.
|
|
29
|
+
--
|
|
30
|
+
-- CALLBACKS. _queue_inbox_callbacks now queues nothing for a feed-only row, for every caller: an
|
|
31
|
+
-- own_inbox callback is about the inbox. The board-level board.post.delivered callbacks
|
|
32
|
+
-- (_queue_board_post_callbacks) are a board subscription, not an inbox delivery, and are unchanged.
|
|
33
|
+
--
|
|
34
|
+
-- NO BACKFILL. Author rows written before this migration keep visible_in_inbox = true. An author
|
|
35
|
+
-- that has acknowledged them is unaffected; a standing one is handled like any other item.
|
|
36
|
+
--
|
|
37
|
+
-- Sources, each at its LATEST definition, read from the catalog of a replay of this tree
|
|
38
|
+
-- (pg_get_functiondef):
|
|
39
|
+
-- * post_board_message -- 20260803000100
|
|
40
|
+
-- * create_tasking_delivery -- 20260803000100
|
|
41
|
+
-- * _queue_inbox_callbacks -- 20260506000000
|
|
42
|
+
-- * _addressing_agent_ids_or_raise -- 20261008000100
|
|
43
|
+
-- Each is redefined below VERBATIM plus the lines marked `#608`. Same signatures, so create or
|
|
44
|
+
-- replace keeps each function's ACL; none carries a COMMENT.
|
|
45
|
+
|
|
46
|
+
-- =============================================================================
|
|
47
|
+
-- 1. post_board_message: the author row is feed-only and not counted
|
|
48
|
+
-- =============================================================================
|
|
49
|
+
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)
|
|
50
|
+
RETURNS TABLE(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamp with time zone)
|
|
51
|
+
LANGUAGE plpgsql
|
|
52
|
+
SECURITY DEFINER
|
|
53
|
+
SET search_path TO 'agent_board', 'public'
|
|
54
|
+
AS $function$
|
|
55
|
+
declare
|
|
56
|
+
v_message agent_board.messages%rowtype;
|
|
57
|
+
v_created_message record;
|
|
58
|
+
v_membership record;
|
|
59
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
60
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
61
|
+
v_delivered_count integer := 0;
|
|
62
|
+
v_callback_count integer := 0;
|
|
63
|
+
v_metadata jsonb;
|
|
64
|
+
v_preview text;
|
|
65
|
+
v_fetch_descriptor jsonb;
|
|
66
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
67
|
+
v_actor_agent_id uuid;
|
|
68
|
+
v_is_author boolean;
|
|
69
|
+
begin
|
|
70
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
71
|
+
|
|
72
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
73
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
74
|
+
end if;
|
|
75
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
76
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
77
|
+
end if;
|
|
78
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
79
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
80
|
+
end if;
|
|
81
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
82
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
83
|
+
end if;
|
|
84
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
85
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
86
|
+
end if;
|
|
87
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
88
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
89
|
+
end if;
|
|
90
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
91
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
92
|
+
end if;
|
|
93
|
+
|
|
94
|
+
if agent_board._is_service_role() then
|
|
95
|
+
perform agent_board._record_send_authz_telemetry(
|
|
96
|
+
'service_role_bypass',
|
|
97
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
98
|
+
v_actor_agent_id,
|
|
99
|
+
p_board_id,
|
|
100
|
+
'post_board_message',
|
|
101
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
102
|
+
null,
|
|
103
|
+
jsonb_build_object('redacted', true)
|
|
104
|
+
);
|
|
105
|
+
end if;
|
|
106
|
+
|
|
107
|
+
perform agent_board._lock_idempotency_key(
|
|
108
|
+
'post_board_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || p_board_id::text,
|
|
109
|
+
v_idempotency_key
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if v_idempotency_key is not null then
|
|
113
|
+
select *
|
|
114
|
+
into v_message
|
|
115
|
+
from agent_board.messages
|
|
116
|
+
where thread_id = p_thread_id
|
|
117
|
+
and author_agent_id = v_actor_agent_id
|
|
118
|
+
and metadata ->> 'rpcName' = 'post_board_message'
|
|
119
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
120
|
+
order by created_at desc, id desc
|
|
121
|
+
limit 1;
|
|
122
|
+
end if;
|
|
123
|
+
|
|
124
|
+
if v_message.id is null then
|
|
125
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
126
|
+
|| jsonb_build_object(
|
|
127
|
+
'rpcName', 'post_board_message',
|
|
128
|
+
'subject', trim(p_subject),
|
|
129
|
+
'priority', p_priority,
|
|
130
|
+
'boardId', p_board_id::text
|
|
131
|
+
);
|
|
132
|
+
if v_idempotency_key is not null then
|
|
133
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
134
|
+
end if;
|
|
135
|
+
|
|
136
|
+
select *
|
|
137
|
+
into v_created_message
|
|
138
|
+
from agent_board.post_message(
|
|
139
|
+
p_thread_id,
|
|
140
|
+
v_actor_agent_id,
|
|
141
|
+
p_body,
|
|
142
|
+
p_message_type,
|
|
143
|
+
null,
|
|
144
|
+
v_metadata
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
select *
|
|
148
|
+
into v_message
|
|
149
|
+
from agent_board.messages
|
|
150
|
+
where id = v_created_message.message_id;
|
|
151
|
+
end if;
|
|
152
|
+
|
|
153
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
154
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
155
|
+
'board',
|
|
156
|
+
p_board_id,
|
|
157
|
+
'agent_board.v_board_feed',
|
|
158
|
+
'board delivery',
|
|
159
|
+
jsonb_build_object('messageId', v_message.id::text)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
for v_membership in
|
|
163
|
+
select bm.id, bm.agent_id
|
|
164
|
+
from agent_board.board_memberships bm
|
|
165
|
+
where bm.board_id = p_board_id
|
|
166
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
167
|
+
order by bm.created_at
|
|
168
|
+
loop
|
|
169
|
+
-- #608: the author is not a recipient of its own post. Its row stays, FEED-ONLY
|
|
170
|
+
-- (visible_in_inbox = false), because v_board_feed is built from these rows and a post on a
|
|
171
|
+
-- board whose only member is its author would otherwise vanish from the feed. A feed-only
|
|
172
|
+
-- row is outside v_agent_inbox, v_agent_wake_recovery and v_agent_inbox_counts by their own
|
|
173
|
+
-- `visible_in_inbox` filter, and _queue_inbox_callbacks queues nothing for it.
|
|
174
|
+
v_is_author := v_membership.agent_id is not distinct from v_actor_agent_id;
|
|
175
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
176
|
+
'board',
|
|
177
|
+
v_membership.agent_id,
|
|
178
|
+
'message',
|
|
179
|
+
v_message.id,
|
|
180
|
+
null,
|
|
181
|
+
null,
|
|
182
|
+
p_board_id,
|
|
183
|
+
v_membership.id,
|
|
184
|
+
jsonb_build_object('priority', p_priority)
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
188
|
+
v_recipient.id,
|
|
189
|
+
v_membership.agent_id,
|
|
190
|
+
p_board_id,
|
|
191
|
+
v_recipient.delivery_mode,
|
|
192
|
+
'message',
|
|
193
|
+
v_message.id,
|
|
194
|
+
null,
|
|
195
|
+
null,
|
|
196
|
+
trim(p_subject),
|
|
197
|
+
v_preview,
|
|
198
|
+
p_priority,
|
|
199
|
+
not v_is_author,
|
|
200
|
+
true,
|
|
201
|
+
v_fetch_descriptor,
|
|
202
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
-- delivered_recipient_count counts RECIPIENTS; the author's feed-only row is not one.
|
|
206
|
+
if not v_is_author then
|
|
207
|
+
v_delivered_count := v_delivered_count + 1;
|
|
208
|
+
end if;
|
|
209
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
210
|
+
end loop;
|
|
211
|
+
|
|
212
|
+
v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
|
|
213
|
+
p_board_id,
|
|
214
|
+
v_message.id,
|
|
215
|
+
trim(p_subject),
|
|
216
|
+
v_preview,
|
|
217
|
+
p_priority,
|
|
218
|
+
v_fetch_descriptor
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return query
|
|
222
|
+
select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
|
|
223
|
+
end;
|
|
224
|
+
$function$;
|
|
225
|
+
|
|
226
|
+
-- =============================================================================
|
|
227
|
+
-- 2. create_tasking_delivery: the creator's board fan-out row is feed-only and not counted
|
|
228
|
+
-- =============================================================================
|
|
229
|
+
create or replace function agent_board.create_tasking_delivery(p_thread_id uuid, p_created_by_agent_id uuid, p_title text, p_details text DEFAULT NULL::text, p_recipient_agent_ids uuid[] DEFAULT NULL::uuid[], p_board_id uuid DEFAULT NULL::uuid, p_priority text DEFAULT 'medium'::text, p_orchestration_mode text DEFAULT 'distributed'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
230
|
+
RETURNS TABLE(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
231
|
+
LANGUAGE plpgsql
|
|
232
|
+
SECURITY DEFINER
|
|
233
|
+
SET search_path TO 'agent_board', 'public'
|
|
234
|
+
AS $function$
|
|
235
|
+
declare
|
|
236
|
+
v_task agent_board.tasks%rowtype;
|
|
237
|
+
v_created_event_id uuid;
|
|
238
|
+
v_created_task record;
|
|
239
|
+
v_recipient_ids uuid[];
|
|
240
|
+
v_recipient_id uuid;
|
|
241
|
+
v_membership record;
|
|
242
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
243
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
244
|
+
v_delivered_count integer := 0;
|
|
245
|
+
v_inbox_count integer := 0;
|
|
246
|
+
v_callback_count integer := 0;
|
|
247
|
+
v_metadata jsonb;
|
|
248
|
+
v_preview text;
|
|
249
|
+
v_fetch_descriptor jsonb;
|
|
250
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
251
|
+
v_actor_agent_id uuid;
|
|
252
|
+
v_is_creator boolean;
|
|
253
|
+
begin
|
|
254
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
|
|
255
|
+
|
|
256
|
+
if nullif(trim(p_title), '') is null then
|
|
257
|
+
raise exception 'task title is required' using errcode = '22023';
|
|
258
|
+
end if;
|
|
259
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
260
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
261
|
+
end if;
|
|
262
|
+
if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
|
|
263
|
+
raise exception 'invalid orchestration mode' using errcode = '22023';
|
|
264
|
+
end if;
|
|
265
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
266
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
267
|
+
end if;
|
|
268
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
269
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
270
|
+
end if;
|
|
271
|
+
if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
|
|
272
|
+
raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
|
|
273
|
+
end if;
|
|
274
|
+
if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
|
|
275
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
276
|
+
end if;
|
|
277
|
+
if p_board_id is not null then
|
|
278
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
279
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
280
|
+
end if;
|
|
281
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
282
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
283
|
+
end if;
|
|
284
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
285
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
286
|
+
end if;
|
|
287
|
+
end if;
|
|
288
|
+
|
|
289
|
+
select array_agg(distinct recipient_id)
|
|
290
|
+
into v_recipient_ids
|
|
291
|
+
from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
|
|
292
|
+
|
|
293
|
+
if v_recipient_ids is not null and (
|
|
294
|
+
select count(*)
|
|
295
|
+
from agent_board.agents
|
|
296
|
+
where id = any(v_recipient_ids)
|
|
297
|
+
) <> cardinality(v_recipient_ids) then
|
|
298
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
299
|
+
end if;
|
|
300
|
+
|
|
301
|
+
if agent_board._is_service_role() then
|
|
302
|
+
perform agent_board._record_send_authz_telemetry(
|
|
303
|
+
'service_role_bypass',
|
|
304
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
305
|
+
v_actor_agent_id,
|
|
306
|
+
p_board_id,
|
|
307
|
+
'create_tasking_delivery',
|
|
308
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
309
|
+
null,
|
|
310
|
+
jsonb_build_object('redacted', true)
|
|
311
|
+
);
|
|
312
|
+
end if;
|
|
313
|
+
|
|
314
|
+
perform agent_board._lock_idempotency_key(
|
|
315
|
+
'create_tasking_delivery:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
316
|
+
v_idempotency_key
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if v_idempotency_key is not null then
|
|
320
|
+
select *
|
|
321
|
+
into v_task
|
|
322
|
+
from agent_board.tasks
|
|
323
|
+
where thread_id = p_thread_id
|
|
324
|
+
and created_by_agent_id = v_actor_agent_id
|
|
325
|
+
and metadata ->> 'rpcName' = 'create_tasking_delivery'
|
|
326
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
327
|
+
order by created_at desc, id desc
|
|
328
|
+
limit 1;
|
|
329
|
+
end if;
|
|
330
|
+
|
|
331
|
+
if v_task.id is null then
|
|
332
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
333
|
+
|| jsonb_build_object(
|
|
334
|
+
'rpcName', 'create_tasking_delivery',
|
|
335
|
+
'priority', p_priority
|
|
336
|
+
);
|
|
337
|
+
if p_board_id is not null then
|
|
338
|
+
v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
|
|
339
|
+
end if;
|
|
340
|
+
if v_idempotency_key is not null then
|
|
341
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
342
|
+
end if;
|
|
343
|
+
|
|
344
|
+
select *
|
|
345
|
+
into v_created_task
|
|
346
|
+
from agent_board.create_task(
|
|
347
|
+
p_thread_id,
|
|
348
|
+
v_actor_agent_id,
|
|
349
|
+
trim(p_title),
|
|
350
|
+
p_details,
|
|
351
|
+
null,
|
|
352
|
+
null,
|
|
353
|
+
p_priority,
|
|
354
|
+
p_orchestration_mode,
|
|
355
|
+
v_metadata
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
v_created_event_id := v_created_task.created_event_id;
|
|
359
|
+
|
|
360
|
+
select *
|
|
361
|
+
into v_task
|
|
362
|
+
from agent_board.tasks
|
|
363
|
+
where id = v_created_task.task_id;
|
|
364
|
+
else
|
|
365
|
+
select te.id
|
|
366
|
+
into v_created_event_id
|
|
367
|
+
from agent_board.task_events te
|
|
368
|
+
where te.task_id = v_task.id
|
|
369
|
+
and te.event_sequence = 1;
|
|
370
|
+
end if;
|
|
371
|
+
|
|
372
|
+
v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
|
|
373
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
374
|
+
'task',
|
|
375
|
+
v_task.id,
|
|
376
|
+
'agent_board.v_task_tree',
|
|
377
|
+
'tasking delivery',
|
|
378
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if v_recipient_ids is not null then
|
|
382
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
383
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
384
|
+
'tasking',
|
|
385
|
+
v_recipient_id,
|
|
386
|
+
'task',
|
|
387
|
+
null,
|
|
388
|
+
v_task.id,
|
|
389
|
+
null,
|
|
390
|
+
null,
|
|
391
|
+
null,
|
|
392
|
+
jsonb_build_object('priority', p_priority)
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
396
|
+
v_recipient.id,
|
|
397
|
+
v_recipient_id,
|
|
398
|
+
null,
|
|
399
|
+
v_recipient.delivery_mode,
|
|
400
|
+
'task',
|
|
401
|
+
null,
|
|
402
|
+
v_task.id,
|
|
403
|
+
null,
|
|
404
|
+
trim(p_title),
|
|
405
|
+
v_preview,
|
|
406
|
+
p_priority,
|
|
407
|
+
true,
|
|
408
|
+
false,
|
|
409
|
+
v_fetch_descriptor,
|
|
410
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
v_delivered_count := v_delivered_count + 1;
|
|
414
|
+
v_inbox_count := v_inbox_count + 1;
|
|
415
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
416
|
+
end loop;
|
|
417
|
+
end if;
|
|
418
|
+
|
|
419
|
+
if p_board_id is not null then
|
|
420
|
+
for v_membership in
|
|
421
|
+
select bm.id, bm.agent_id
|
|
422
|
+
from agent_board.board_memberships bm
|
|
423
|
+
where bm.board_id = p_board_id
|
|
424
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
425
|
+
order by bm.created_at
|
|
426
|
+
loop
|
|
427
|
+
-- #608: the board fan-out does not deliver a task to the agent that created it. The
|
|
428
|
+
-- creator's row is KEPT, feed-only, exactly as a board post's author row is: its
|
|
429
|
+
-- _upsert_delivery_recipient is what stamps threads.board_id (OBSGATE #78), so skipping it
|
|
430
|
+
-- left a creator-only board's thread unlinked and post_message on the access-only branch of
|
|
431
|
+
-- the observer gate (codex and grok seats, PR #615 round 1). It also keeps the board
|
|
432
|
+
-- directory's activity and delegate visibility as they were. An explicit recipient list
|
|
433
|
+
-- naming the creator still delivers to it: that is the caller addressing itself, and the
|
|
434
|
+
-- board loop must not then demote the row the explicit loop wrote. It shares that row (the
|
|
435
|
+
-- delivery upsert keeps the explicit row's null board_id), so demoting it wrote both
|
|
436
|
+
-- visibility flags false and violated inbox_items_visibility_shape, aborting the whole task
|
|
437
|
+
-- (every seat on PR #615 round 2).
|
|
438
|
+
v_is_creator := v_membership.agent_id is not distinct from v_actor_agent_id
|
|
439
|
+
and not (v_membership.agent_id = any(coalesce(v_recipient_ids, '{}'::uuid[])));
|
|
440
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
441
|
+
'tasking',
|
|
442
|
+
v_membership.agent_id,
|
|
443
|
+
'task',
|
|
444
|
+
null,
|
|
445
|
+
v_task.id,
|
|
446
|
+
null,
|
|
447
|
+
p_board_id,
|
|
448
|
+
v_membership.id,
|
|
449
|
+
jsonb_build_object('priority', p_priority)
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
453
|
+
v_recipient.id,
|
|
454
|
+
v_membership.agent_id,
|
|
455
|
+
v_recipient.board_id,
|
|
456
|
+
v_recipient.delivery_mode,
|
|
457
|
+
'task',
|
|
458
|
+
null,
|
|
459
|
+
v_task.id,
|
|
460
|
+
null,
|
|
461
|
+
trim(p_title),
|
|
462
|
+
v_preview,
|
|
463
|
+
p_priority,
|
|
464
|
+
not v_is_creator,
|
|
465
|
+
v_recipient.board_id is not null,
|
|
466
|
+
v_fetch_descriptor,
|
|
467
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
-- The counts are of RECIPIENTS; the creator's feed-only row is not one.
|
|
471
|
+
if not v_is_creator then
|
|
472
|
+
v_delivered_count := v_delivered_count + 1;
|
|
473
|
+
v_inbox_count := v_inbox_count + 1;
|
|
474
|
+
end if;
|
|
475
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
476
|
+
end loop;
|
|
477
|
+
end if;
|
|
478
|
+
|
|
479
|
+
return query
|
|
480
|
+
select v_task.id, v_created_event_id, v_delivered_count, v_inbox_count, v_callback_count, v_task.created_at, v_task.updated_at;
|
|
481
|
+
end;
|
|
482
|
+
$function$;
|
|
483
|
+
|
|
484
|
+
-- =============================================================================
|
|
485
|
+
-- 3. _queue_inbox_callbacks: nothing for a feed-only row
|
|
486
|
+
-- =============================================================================
|
|
487
|
+
create or replace function agent_board._queue_inbox_callbacks(p_inbox_item_id uuid, p_event_category text)
|
|
488
|
+
RETURNS integer
|
|
489
|
+
LANGUAGE plpgsql
|
|
490
|
+
SECURITY DEFINER
|
|
491
|
+
SET search_path TO 'agent_board', 'public'
|
|
492
|
+
AS $function$
|
|
493
|
+
declare
|
|
494
|
+
v_inserted_count integer := 0;
|
|
495
|
+
begin
|
|
496
|
+
if p_event_category not in ('inbox.item.delivered', 'inbox.item.updated') then
|
|
497
|
+
raise exception 'unsupported inbox callback event category' using errcode = '22023';
|
|
498
|
+
end if;
|
|
499
|
+
|
|
500
|
+
with inserted as (
|
|
501
|
+
insert into agent_board.callback_outbox_events(
|
|
502
|
+
endpoint_id,
|
|
503
|
+
subscription_id,
|
|
504
|
+
recipient_agent_id,
|
|
505
|
+
inbox_item_id,
|
|
506
|
+
board_id,
|
|
507
|
+
event_category,
|
|
508
|
+
delivery_mode,
|
|
509
|
+
source_kind,
|
|
510
|
+
source_message_id,
|
|
511
|
+
source_task_id,
|
|
512
|
+
source_delegation_id,
|
|
513
|
+
idempotency_key,
|
|
514
|
+
subject,
|
|
515
|
+
preview,
|
|
516
|
+
priority,
|
|
517
|
+
fetch_descriptor,
|
|
518
|
+
metadata
|
|
519
|
+
)
|
|
520
|
+
select
|
|
521
|
+
cs.endpoint_id,
|
|
522
|
+
cs.id,
|
|
523
|
+
ii.recipient_agent_id,
|
|
524
|
+
ii.id,
|
|
525
|
+
null,
|
|
526
|
+
p_event_category,
|
|
527
|
+
ii.delivery_mode,
|
|
528
|
+
ii.source_kind,
|
|
529
|
+
ii.source_message_id,
|
|
530
|
+
ii.source_task_id,
|
|
531
|
+
ii.source_delegation_id,
|
|
532
|
+
case
|
|
533
|
+
when p_event_category = 'inbox.item.delivered' then
|
|
534
|
+
p_event_category || ':' || ii.id::text || ':' || cs.id::text
|
|
535
|
+
else
|
|
536
|
+
p_event_category || ':' || ii.id::text || ':' || cs.id::text || ':' || replace(ii.updated_at::text, ' ', 'T') || ':' || ii.action_state
|
|
537
|
+
end,
|
|
538
|
+
ii.subject,
|
|
539
|
+
ii.preview,
|
|
540
|
+
ii.priority,
|
|
541
|
+
ii.fetch_descriptor,
|
|
542
|
+
jsonb_build_object(
|
|
543
|
+
'subscriptionScope', cs.subscription_scope,
|
|
544
|
+
'subscriptionStatus', cs.subscription_status,
|
|
545
|
+
'actionState', ii.action_state
|
|
546
|
+
)
|
|
547
|
+
from agent_board.inbox_items ii
|
|
548
|
+
join agent_board.callback_subscriptions cs
|
|
549
|
+
on cs.agent_id = ii.recipient_agent_id
|
|
550
|
+
and cs.subscription_scope = 'own_inbox'
|
|
551
|
+
and cs.subscription_status = 'active'
|
|
552
|
+
and cs.event_category = p_event_category
|
|
553
|
+
join agent_board.callback_endpoints ce
|
|
554
|
+
on ce.id = cs.endpoint_id
|
|
555
|
+
and ce.owner_agent_id = cs.agent_id
|
|
556
|
+
and ce.endpoint_status = 'verified'
|
|
557
|
+
where ii.id = p_inbox_item_id
|
|
558
|
+
-- #608: an own_inbox callback is about the inbox. A feed-only row (the author's own board
|
|
559
|
+
-- post) is not in it, so it queues nothing -- for every caller, not just post_board_message.
|
|
560
|
+
and ii.visible_in_inbox
|
|
561
|
+
on conflict (idempotency_key) do nothing
|
|
562
|
+
returning 1
|
|
563
|
+
)
|
|
564
|
+
select count(*) into v_inserted_count from inserted;
|
|
565
|
+
|
|
566
|
+
return v_inserted_count;
|
|
567
|
+
end;
|
|
568
|
+
$function$;
|
|
569
|
+
|
|
570
|
+
-- =============================================================================
|
|
571
|
+
-- 4. _addressing_agent_ids_or_raise: a set-naming descriptor does not reach the actor
|
|
572
|
+
-- =============================================================================
|
|
573
|
+
-- Found by the correctness seat on PR #615 round 1: group_alias and role_selector expand to every
|
|
574
|
+
-- matching active agent, the actor included, and the descriptor overloads of send_multicast_message
|
|
575
|
+
-- and create_tasking_delivery (and the signed paths) pass that set on as an explicit list. So the
|
|
576
|
+
-- board fan-out was not the only expansion: this is the other one, two calls before
|
|
577
|
+
-- _upsert_inbox_item, which is why a writer-caller scan did not see it.
|
|
578
|
+
create or replace function agent_board._addressing_agent_ids_or_raise(p_actor_agent_id uuid, p_recipient_descriptors jsonb)
|
|
579
|
+
RETURNS uuid[]
|
|
580
|
+
LANGUAGE plpgsql
|
|
581
|
+
SECURITY DEFINER
|
|
582
|
+
SET search_path TO 'agent_board', 'public'
|
|
583
|
+
AS $function$
|
|
584
|
+
declare
|
|
585
|
+
v_result record;
|
|
586
|
+
v_ids uuid[];
|
|
587
|
+
v_actor uuid;
|
|
588
|
+
begin
|
|
589
|
+
select * into v_result
|
|
590
|
+
from agent_board.resolve_recipients(p_actor_agent_id, p_recipient_descriptors);
|
|
591
|
+
|
|
592
|
+
-- HOSTED (D16, NEW-4): naming a hosted delegate refuses the whole send as an authorization
|
|
593
|
+
-- failure, before any delivery is written.
|
|
594
|
+
if exists (
|
|
595
|
+
select 1
|
|
596
|
+
from jsonb_array_elements(v_result."deniedRecipients") d
|
|
597
|
+
where d ->> 'reason' = 'recipient_is_hosted_delegate'
|
|
598
|
+
) then
|
|
599
|
+
raise exception 'a hosted delegate is reached only through a board it is on' using errcode = '42501', detail = v_result."deniedRecipients"::text;
|
|
600
|
+
end if;
|
|
601
|
+
|
|
602
|
+
if jsonb_array_length(v_result."deniedRecipients") > 0 then
|
|
603
|
+
raise exception 'recipient descriptor resolution failed' using errcode = '22023', detail = v_result."deniedRecipients"::text;
|
|
604
|
+
end if;
|
|
605
|
+
|
|
606
|
+
select array_agg((value ->> 'agentId')::uuid)
|
|
607
|
+
into v_ids
|
|
608
|
+
from jsonb_array_elements(v_result."resolvedRecipients") as value;
|
|
609
|
+
|
|
610
|
+
if v_ids is null or cardinality(v_ids) = 0 then
|
|
611
|
+
raise exception 'recipient descriptors resolved no recipients' using errcode = '22023';
|
|
612
|
+
end if;
|
|
613
|
+
|
|
614
|
+
-- #608: a descriptor that names a SET (group_alias, role_selector, and any kind added later)
|
|
615
|
+
-- does not deliver to the actor; only a descriptor that names the actor itself does (agent_id,
|
|
616
|
+
-- agent_handle, human_handle, directly or inside an explicit_set). The decision reads every
|
|
617
|
+
-- resolved row, not resolve_recipients' collapsed set, whose `distinct on` keeps the FIRST
|
|
618
|
+
-- descriptor for an agent: a group listed before an explicit self-mention would otherwise hide
|
|
619
|
+
-- the explicit one. The allowlist is of EXPLICIT kinds on purpose: a new set kind defaults to
|
|
620
|
+
-- not delivering an agent's own message to it. The actor is bound as resolve_recipients binds it.
|
|
621
|
+
v_actor := agent_board._resolved_actor_agent_id(p_actor_agent_id, 'p_actor_agent_id');
|
|
622
|
+
if v_actor = any(v_ids)
|
|
623
|
+
and not exists (
|
|
624
|
+
select 1
|
|
625
|
+
from agent_board._resolve_addressing_recipient_rows(v_actor, p_recipient_descriptors) r
|
|
626
|
+
where r.recipient_agent_id = v_actor
|
|
627
|
+
and r.descriptor ->> 'kind' in ('agent_id', 'agent_handle', 'human_handle')
|
|
628
|
+
) then
|
|
629
|
+
v_ids := array_remove(v_ids, v_actor);
|
|
630
|
+
if cardinality(v_ids) = 0 then
|
|
631
|
+
raise exception 'recipient descriptors resolved only the sender' using errcode = '22023';
|
|
632
|
+
end if;
|
|
633
|
+
end if;
|
|
634
|
+
|
|
635
|
+
return v_ids;
|
|
636
|
+
end;
|
|
637
|
+
$function$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.60.0",
|
|
4
4
|
"description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
|
|
5
5
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
6
6
|
"type": "module",
|
package/schema-manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": "agent_board_schema_manifest.v1",
|
|
3
3
|
"interface": "IF-0-MBPLANE-BUNDLE-1",
|
|
4
4
|
"package": "@consiliency/agent-board-schema",
|
|
5
|
-
"packageVersion": "1.
|
|
5
|
+
"packageVersion": "1.60.0",
|
|
6
6
|
"schemaVersion": 8,
|
|
7
7
|
"minClientVersion": "1.1.0",
|
|
8
8
|
"source": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"migrationCount":
|
|
50
|
-
"aggregateSha256": "
|
|
49
|
+
"migrationCount": 158,
|
|
50
|
+
"aggregateSha256": "e887208c42b9617a6a7cc2baae96c87d2d2a011320a8cd4799605d689159cf63",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1461,6 +1461,15 @@
|
|
|
1461
1461
|
"sourceRef": "message-board",
|
|
1462
1462
|
"bytes": 37430,
|
|
1463
1463
|
"sha256": "4c2c4041bb41126b3e1bce940bc8a52ac78b69389a0c866556ba0c0e4adc3564"
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
"order": 157,
|
|
1467
|
+
"id": "20261010000100_agent_board_own_post_not_delivered",
|
|
1468
|
+
"filename": "20261010000100_agent_board_own_post_not_delivered.sql",
|
|
1469
|
+
"origin": "message-board-forward",
|
|
1470
|
+
"sourceRef": "message-board",
|
|
1471
|
+
"bytes": 25831,
|
|
1472
|
+
"sha256": "6cbbe0bb9ccf7ab65402a58de145ee21bc2158ddc7502671ea13a98415ca6421"
|
|
1464
1473
|
}
|
|
1465
1474
|
],
|
|
1466
1475
|
"excludedBridge": {
|
package/src/tooling.test.mjs
CHANGED
|
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
|
|
|
38
38
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
39
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
40
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
41
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 158)', () => {
|
|
42
42
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
43
|
+
expect(manifest.migrationCount).toBe(158);
|
|
44
44
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
45
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
46
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -353,6 +353,9 @@ describe('manifest integrity', () => {
|
|
|
353
353
|
// revocation marker revoke_human_subtree derives from it; the tombstone's and the exchange's
|
|
354
354
|
// lock order against the subtree revokers (AB15).
|
|
355
355
|
'20261009000100_agent_board_hosted_followups.sql',
|
|
356
|
+
// OWNPOST (#608): an agent's own board post is a feed-only row for it, not an inbox item;
|
|
357
|
+
// a board-wide task fan-out skips its creator; own_inbox callbacks skip feed-only rows.
|
|
358
|
+
'20261010000100_agent_board_own_post_not_delivered.sql',
|
|
356
359
|
]) {
|
|
357
360
|
expect(names.has(f)).toBe(true);
|
|
358
361
|
}
|
|
@@ -29,7 +29,9 @@ begin
|
|
|
29
29
|
values
|
|
30
30
|
(v_actor, 'agent', 'agent', 'addressing-actor-' || v_suffix, 'ADDRESSING Actor', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
|
|
31
31
|
(v_worker, 'agent', 'agent', 'addressing-worker-' || v_suffix, 'ADDRESSING Worker', 'active', '{"role":"phase_worker","groupAliases":["ops"]}'::jsonb),
|
|
32
|
-
|
|
32
|
+
-- `role: phase_worker` on the reviewer too: since #608 a set descriptor never reaches its own
|
|
33
|
+
-- sender, so the direct-send ambiguity case below needs TWO matching agents besides the actor.
|
|
34
|
+
(v_reviewer, 'agent', 'agent', 'addressing-reviewer-' || v_suffix, 'ADDRESSING Reviewer', 'active', '{"role":"phase_worker","roles":["reviewer"],"groupAliases":["review"]}'::jsonb),
|
|
33
35
|
(v_human, 'human', 'human', 'addressing-human-' || v_suffix, 'ADDRESSING Human', 'active', '{}'::jsonb);
|
|
34
36
|
|
|
35
37
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
@@ -378,7 +378,9 @@ begin
|
|
|
378
378
|
'{}'::jsonb
|
|
379
379
|
);
|
|
380
380
|
v_board_message := v_result.message_id;
|
|
381
|
-
|
|
381
|
+
-- #608: the sender is the board's seeded owner-creator, and the author of a post is not one of
|
|
382
|
+
-- its recipients (its row is feed-only), so two of the three active members are counted.
|
|
383
|
+
insert into _tap(line) select is(v_result.delivered_recipient_count, 2, 'board delivery reaches joined and subscribed members only, not the author (#608)');
|
|
382
384
|
insert into _tap(line) select is(
|
|
383
385
|
(select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_board_message and recipient_agent_id = v_left_member),
|
|
384
386
|
0,
|
|
@@ -409,7 +411,8 @@ begin
|
|
|
409
411
|
'{}'::jsonb
|
|
410
412
|
);
|
|
411
413
|
v_task := v_result.task_id;
|
|
412
|
-
|
|
414
|
+
-- #608: the creator (the seeded owner-creator) keeps a feed-only row that is not counted: 1 explicit + 2 board.
|
|
415
|
+
insert into _tap(line) select is(v_result.delivered_recipient_count, 3, 'tasking counts explicit and board fan-out attempts, not the creator (#608)');
|
|
413
416
|
insert into _tap(line) select is(
|
|
414
417
|
(select count(*)::integer from agent_board.delivery_recipients where source_task_id = v_task),
|
|
415
418
|
3,
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
-- OWN POST NOT DELIVERED pgTAP (Consiliency/message-board#608, migration 20261010000100).
|
|
2
|
+
--
|
|
3
|
+
-- The owner's rule (2026-10-03): an agent does not receive its own room post as an inbox item,
|
|
4
|
+
-- and its own writes do not wake it. The wake side reads v_agent_wake_recovery, so "no inbox item
|
|
5
|
+
-- for the author" is asserted on that view, as the author, through a real authenticated session.
|
|
6
|
+
--
|
|
7
|
+
-- THE CLASS COMES FROM THE CATALOG (AGENTS.md rule 1). A recipient set can reach its own sender in
|
|
8
|
+
-- two ways, and each is derived from pg_proc so a new site fails here BY NAME:
|
|
9
|
+
-- * a BOARD expansion -- OPN-01 finds every inbox_items inserter; OPN-02 every caller of it that
|
|
10
|
+
-- reads board_memberships; OPN-03 the callers that do not (asserted positively, OPN-18..22, 29, 33);
|
|
11
|
+
-- * a DESCRIPTOR expansion -- OPN-34 finds every caller of the descriptor resolver; the only one that
|
|
12
|
+
-- delivers is _addressing_agent_ids_or_raise, asserted through both its overload callers (OPN-25..32).
|
|
13
|
+
-- Each member is asserted behaviourally, as an authenticated session; each exemption POSITIVELY.
|
|
14
|
+
--
|
|
15
|
+
-- Mutation controls (measured 2026-10-03, each run counting all 40 assertions):
|
|
16
|
+
-- * post_board_message writes the author's row visible_in_inbox = true -> OPN-06, 07, 09, 10, 14
|
|
17
|
+
-- * post_board_message skips the author's row entirely -> OPN-09, 13
|
|
18
|
+
-- * post_board_message counts the author in delivered_recipient_count -> OPN-05
|
|
19
|
+
-- * _queue_inbox_callbacks loses its visible_in_inbox filter -> OPN-10
|
|
20
|
+
-- * create_tasking_delivery writes the creator's row visible_in_inbox -> OPN-16
|
|
21
|
+
-- * create_tasking_delivery skips the creator (the round-1 shape) -> OPN-16, 24
|
|
22
|
+
-- * the board loop demotes an explicit self-listing (the round-2 shape) -> OPN-35, 39 (abort), 37, 40
|
|
23
|
+
-- * the board loop skips an explicitly listed creator (round-3 T1 shape) -> OPN-40
|
|
24
|
+
-- * _addressing_agent_ids_or_raise keeps the sender for a set descriptor -> OPN-28, 30, 32
|
|
25
|
+
-- * ... and drops it even when named explicitly -> OPN-31, 38
|
|
26
|
+
-- * a new function that calls _upsert_inbox_item over board_memberships -> OPN-02
|
|
27
|
+
-- * a new caller of _resolve_addressing_recipient_rows -> OPN-34
|
|
28
|
+
-- (create_tasking_delivery counting its creator is pinned by inbox_rpcs and v2_invariants.)
|
|
29
|
+
-- TAP numbers follow EMISSION order, not labels (OPN-34 is emitted last): cite by label.
|
|
30
|
+
--
|
|
31
|
+
-- HAND-COUNTED plan: OPN-01..OPN-40 = 40 assertions. The runner checks the emitted count.
|
|
32
|
+
create extension if not exists pgtap with schema extensions;
|
|
33
|
+
set search_path = extensions, public, agent_board;
|
|
34
|
+
|
|
35
|
+
begin;
|
|
36
|
+
|
|
37
|
+
select plan(40);
|
|
38
|
+
|
|
39
|
+
-- ---------------------------------------------------------------------------
|
|
40
|
+
-- The class, derived from pg_proc.
|
|
41
|
+
-- ---------------------------------------------------------------------------
|
|
42
|
+
select is(
|
|
43
|
+
(select array_agg(p.proname::text order by p.proname)
|
|
44
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
45
|
+
where n.nspname = 'agent_board'
|
|
46
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into|merge into', 'inbox_items')),
|
|
47
|
+
array['_upsert_inbox_item'],
|
|
48
|
+
'OPN-01: _upsert_inbox_item is the only function that inserts into inbox_items');
|
|
49
|
+
|
|
50
|
+
select is(
|
|
51
|
+
(select array_agg(p.proname::text order by p.proname)
|
|
52
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
53
|
+
where n.nspname = 'agent_board'
|
|
54
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_inbox_item')
|
|
55
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.qname_re('board_memberships')
|
|
56
|
+
and p.proname <> '_upsert_inbox_item'),
|
|
57
|
+
array['create_tasking_delivery', 'post_board_message'],
|
|
58
|
+
'OPN-02: the board-expanding delivery writers are exactly the two this file asserts behaviourally');
|
|
59
|
+
|
|
60
|
+
select is(
|
|
61
|
+
(select array_agg(p.proname::text order by p.proname)
|
|
62
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
63
|
+
where n.nspname = 'agent_board'
|
|
64
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_inbox_item')
|
|
65
|
+
and agent_board_test.stripped_body(p.oid) !~ agent_board_test.qname_re('board_memberships')
|
|
66
|
+
and p.proname <> '_upsert_inbox_item'),
|
|
67
|
+
array['send_direct_message', 'send_multicast_message'],
|
|
68
|
+
'OPN-03: the non-expanding delivery writers are exactly the two exemptions asserted below');
|
|
69
|
+
|
|
70
|
+
-- ---------------------------------------------------------------------------
|
|
71
|
+
-- Fixture: author A, member B; board AB (A owner, B joined) and board S (A alone).
|
|
72
|
+
-- ---------------------------------------------------------------------------
|
|
73
|
+
create temp table _opn (
|
|
74
|
+
a_user uuid, b_user uuid,
|
|
75
|
+
a_agent uuid, b_agent uuid,
|
|
76
|
+
board_ab uuid, board_s uuid, thread uuid, thread_unlinked uuid, thread_unlinked2 uuid
|
|
77
|
+
) on commit drop;
|
|
78
|
+
create temp table _opn_post (message_id uuid, board_id uuid, delivered_recipient_count integer,
|
|
79
|
+
callback_outbox_count integer, created_at timestamptz) on commit drop;
|
|
80
|
+
create temp table _opn_sole (like _opn_post) on commit drop;
|
|
81
|
+
create temp table _opn_task (task_id uuid, task_event_id uuid, delivered_recipient_count integer,
|
|
82
|
+
inbox_item_count integer, callback_outbox_count integer, created_at timestamptz,
|
|
83
|
+
updated_at timestamptz) on commit drop;
|
|
84
|
+
create temp table _opn_self_task (like _opn_task) on commit drop;
|
|
85
|
+
create temp table _opn_sole_task (like _opn_task) on commit drop;
|
|
86
|
+
create temp table _opn_desc_task (like _opn_task) on commit drop;
|
|
87
|
+
create temp table _opn_both_task (like _opn_task) on commit drop;
|
|
88
|
+
create temp table _opn_both_sole (like _opn_task) on commit drop;
|
|
89
|
+
create temp table _opn_multi (message_id uuid, delivered_recipient_count integer, inbox_item_count integer,
|
|
90
|
+
callback_outbox_count integer, created_at timestamptz) on commit drop;
|
|
91
|
+
create temp table _opn_group (like _opn_multi) on commit drop;
|
|
92
|
+
create temp table _opn_group_self (like _opn_multi) on commit drop;
|
|
93
|
+
create temp table _opn_nested (like _opn_multi) on commit drop;
|
|
94
|
+
create temp table _opn_direct (message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid,
|
|
95
|
+
callback_outbox_count integer, created_at timestamptz) on commit drop;
|
|
96
|
+
grant all on table _opn, _opn_post, _opn_sole, _opn_task, _opn_self_task, _opn_multi,
|
|
97
|
+
_opn_sole_task, _opn_desc_task, _opn_group, _opn_group_self, _opn_direct, _opn_both_task, _opn_nested, _opn_both_sole
|
|
98
|
+
to authenticated, service_role;
|
|
99
|
+
|
|
100
|
+
do $$
|
|
101
|
+
declare
|
|
102
|
+
v_a_user uuid := public.ensure_github_user('opn-author', 'opn-author@example.test', 'opn-author', 'OPN Author');
|
|
103
|
+
v_b_user uuid := public.ensure_github_user('opn-member', 'opn-member@example.test', 'opn-member', 'OPN Member');
|
|
104
|
+
v_a uuid := '76080000-0000-4000-8000-000000000001';
|
|
105
|
+
v_b uuid := '76080000-0000-4000-8000-000000000002';
|
|
106
|
+
v_thread uuid := '76080000-0000-4000-8000-000000000010';
|
|
107
|
+
v_thread_unlinked uuid := '76080000-0000-4000-8000-000000000011';
|
|
108
|
+
v_thread_unlinked2 uuid := '76080000-0000-4000-8000-000000000012';
|
|
109
|
+
v_board_ab uuid;
|
|
110
|
+
v_board_s uuid;
|
|
111
|
+
v_endpoint uuid;
|
|
112
|
+
begin
|
|
113
|
+
-- Both agents are in the group alias `opn-team` and carry the role `opn-role`, so a set-naming
|
|
114
|
+
-- descriptor resolves to BOTH of them -- the author included -- before the #608 rule applies.
|
|
115
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id, metadata)
|
|
116
|
+
values
|
|
117
|
+
(v_a, 'agent', 'opn-author', 'OPN Author', 'active', v_a_user, v_a_user, '{"groupAliases":["opn-team","opn-solo"],"roles":["opn-role"]}'::jsonb),
|
|
118
|
+
(v_b, 'agent', 'opn-member', 'OPN Member', 'active', v_b_user, v_b_user, '{"groupAliases":["opn-team"],"roles":["opn-role"]}'::jsonb);
|
|
119
|
+
|
|
120
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
121
|
+
values (v_thread, 'coordination', v_a, 'OPN thread'),
|
|
122
|
+
(v_thread_unlinked, 'coordination', v_a, 'OPN unlinked thread'),
|
|
123
|
+
(v_thread_unlinked2, 'coordination', v_a, 'OPN second unlinked thread');
|
|
124
|
+
|
|
125
|
+
select board_id into v_board_ab
|
|
126
|
+
from agent_board.create_board(v_a, 'opn-board-ab', 'OPN AB', 'two members', '{}'::jsonb, 'private');
|
|
127
|
+
perform agent_board.join_board(v_board_ab, v_b, 'joined', v_a, '{}'::jsonb);
|
|
128
|
+
select board_id into v_board_s
|
|
129
|
+
from agent_board.create_board(v_a, 'opn-board-s', 'OPN S', 'author alone', '{}'::jsonb, 'private');
|
|
130
|
+
|
|
131
|
+
-- Both agents subscribe their own inbox to delivered callbacks: B's is the positive control
|
|
132
|
+
-- for OPN-10, so a zero for A cannot come from a fixture that queues nothing for anyone.
|
|
133
|
+
select endpoint_id into v_endpoint from agent_board.register_callback_endpoint(v_a, 'https://example.test/opn-a', '{}'::jsonb);
|
|
134
|
+
perform agent_board.verify_callback_endpoint(v_endpoint, v_a, '{"controlProofRef":"opn/a"}'::jsonb);
|
|
135
|
+
perform agent_board.subscribe_callback_endpoint(v_endpoint, v_a, 'own_inbox', 'inbox.item.delivered', null, '{}'::jsonb);
|
|
136
|
+
select endpoint_id into v_endpoint from agent_board.register_callback_endpoint(v_b, 'https://example.test/opn-b', '{}'::jsonb);
|
|
137
|
+
perform agent_board.verify_callback_endpoint(v_endpoint, v_b, '{"controlProofRef":"opn/b"}'::jsonb);
|
|
138
|
+
perform agent_board.subscribe_callback_endpoint(v_endpoint, v_b, 'own_inbox', 'inbox.item.delivered', null, '{}'::jsonb);
|
|
139
|
+
|
|
140
|
+
insert into _opn values (v_a_user, v_b_user, v_a, v_b, v_board_ab, v_board_s, v_thread, v_thread_unlinked, v_thread_unlinked2);
|
|
141
|
+
end;
|
|
142
|
+
$$;
|
|
143
|
+
|
|
144
|
+
-- ---------------------------------------------------------------------------
|
|
145
|
+
-- A posts to AB, through its own authenticated session.
|
|
146
|
+
-- ---------------------------------------------------------------------------
|
|
147
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
148
|
+
select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
|
|
149
|
+
set local role authenticated;
|
|
150
|
+
|
|
151
|
+
select lives_ok(
|
|
152
|
+
$$ insert into _opn_post
|
|
153
|
+
select * from agent_board.post_board_message(
|
|
154
|
+
(select thread from _opn), null, (select board_ab from _opn),
|
|
155
|
+
'opn post', 'the author posts to a two-member board', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
156
|
+
'OPN-04: the author can post to a board it owns');
|
|
157
|
+
|
|
158
|
+
select is((select delivered_recipient_count from _opn_post), 1,
|
|
159
|
+
'OPN-05: delivered_recipient_count counts the other member only, not the author');
|
|
160
|
+
|
|
161
|
+
select is(
|
|
162
|
+
(select count(*)::int from agent_board.v_agent_wake_recovery
|
|
163
|
+
where source_message_id = (select message_id from _opn_post)),
|
|
164
|
+
0,
|
|
165
|
+
'OPN-06: the author''s own post is not in the author''s v_agent_wake_recovery');
|
|
166
|
+
|
|
167
|
+
select is(
|
|
168
|
+
(select count(*)::int from agent_board.v_agent_inbox
|
|
169
|
+
where source_message_id = (select message_id from _opn_post)
|
|
170
|
+
and recipient_agent_id = (select a_agent from _opn)),
|
|
171
|
+
0,
|
|
172
|
+
'OPN-07: the author''s own post is not in the author''s v_agent_inbox');
|
|
173
|
+
|
|
174
|
+
reset role;
|
|
175
|
+
select set_config('request.jwt.claim.sub', (select b_user::text from _opn), true);
|
|
176
|
+
set local role authenticated;
|
|
177
|
+
|
|
178
|
+
select is(
|
|
179
|
+
(select count(*)::int from agent_board.v_agent_wake_recovery
|
|
180
|
+
where source_message_id = (select message_id from _opn_post)),
|
|
181
|
+
1,
|
|
182
|
+
'OPN-08: the other member does receive the post as an unhandled item');
|
|
183
|
+
|
|
184
|
+
reset role;
|
|
185
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
186
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
187
|
+
|
|
188
|
+
select is(
|
|
189
|
+
(select array_agg(array[ii.visible_in_inbox, ii.visible_in_board_feed])
|
|
190
|
+
from agent_board.inbox_items ii
|
|
191
|
+
where ii.source_message_id = (select message_id from _opn_post)
|
|
192
|
+
and ii.recipient_agent_id = (select a_agent from _opn)),
|
|
193
|
+
array[array[false, true]],
|
|
194
|
+
'OPN-09: the author keeps exactly one row for its post, and it is feed-only');
|
|
195
|
+
|
|
196
|
+
select is(
|
|
197
|
+
(select count(*)::int from agent_board.callback_outbox_events
|
|
198
|
+
where source_message_id = (select message_id from _opn_post)
|
|
199
|
+
and event_category = 'inbox.item.delivered'
|
|
200
|
+
and recipient_agent_id = (select a_agent from _opn)),
|
|
201
|
+
0,
|
|
202
|
+
'OPN-10: no own_inbox delivered callback is queued to the author for its own post');
|
|
203
|
+
|
|
204
|
+
select is(
|
|
205
|
+
(select count(*)::int from agent_board.callback_outbox_events
|
|
206
|
+
where source_message_id = (select message_id from _opn_post)
|
|
207
|
+
and event_category = 'inbox.item.delivered'
|
|
208
|
+
and recipient_agent_id = (select b_agent from _opn)),
|
|
209
|
+
1,
|
|
210
|
+
'OPN-11: the other member''s own_inbox delivered callback is queued (control for OPN-10)');
|
|
211
|
+
|
|
212
|
+
-- ---------------------------------------------------------------------------
|
|
213
|
+
-- A posts to S, where it is the only member: the post must stay in the feed.
|
|
214
|
+
-- ---------------------------------------------------------------------------
|
|
215
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
216
|
+
select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
|
|
217
|
+
set local role authenticated;
|
|
218
|
+
|
|
219
|
+
select lives_ok(
|
|
220
|
+
$$ insert into _opn_sole
|
|
221
|
+
select * from agent_board.post_board_message(
|
|
222
|
+
(select thread from _opn), null, (select board_s from _opn),
|
|
223
|
+
'opn sole post', 'the author posts to a board it is alone on', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
224
|
+
'OPN-12: the author can post to a board it is the only member of');
|
|
225
|
+
|
|
226
|
+
select is(
|
|
227
|
+
(select count(*)::int from agent_board.v_board_feed
|
|
228
|
+
where message_id = (select message_id from _opn_sole)),
|
|
229
|
+
1,
|
|
230
|
+
'OPN-13: a post on a board whose only member is its author is still in that board''s feed');
|
|
231
|
+
|
|
232
|
+
select is(
|
|
233
|
+
(select count(*)::int from agent_board.v_agent_wake_recovery
|
|
234
|
+
where source_message_id = (select message_id from _opn_sole)),
|
|
235
|
+
0,
|
|
236
|
+
'OPN-14: the sole-member post is not in the author''s v_agent_wake_recovery either');
|
|
237
|
+
|
|
238
|
+
-- ---------------------------------------------------------------------------
|
|
239
|
+
-- A creates a board-wide task on AB.
|
|
240
|
+
-- ---------------------------------------------------------------------------
|
|
241
|
+
select lives_ok(
|
|
242
|
+
$$ insert into _opn_task
|
|
243
|
+
select * from agent_board.create_tasking_delivery(
|
|
244
|
+
(select thread from _opn), null, 'opn task', 'a board-wide task', null,
|
|
245
|
+
(select board_ab from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
|
|
246
|
+
'OPN-15: the creator can deliver a task to its board');
|
|
247
|
+
|
|
248
|
+
reset role;
|
|
249
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
250
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
251
|
+
|
|
252
|
+
select is(
|
|
253
|
+
(select array_agg(array[ii.visible_in_inbox, ii.visible_in_board_feed])
|
|
254
|
+
from agent_board.inbox_items ii
|
|
255
|
+
where ii.source_task_id = (select task_id from _opn_task)
|
|
256
|
+
and ii.recipient_agent_id = (select a_agent from _opn)),
|
|
257
|
+
array[array[false, true]],
|
|
258
|
+
'OPN-16: the board fan-out of a task gives its creator one feed-only row, not an inbox item');
|
|
259
|
+
|
|
260
|
+
select is(
|
|
261
|
+
(select count(*)::int from agent_board.inbox_items
|
|
262
|
+
where source_task_id = (select task_id from _opn_task)
|
|
263
|
+
and recipient_agent_id = (select b_agent from _opn)
|
|
264
|
+
and visible_in_inbox),
|
|
265
|
+
1,
|
|
266
|
+
'OPN-17: the board fan-out of a task delivers it to the other member');
|
|
267
|
+
|
|
268
|
+
-- ---------------------------------------------------------------------------
|
|
269
|
+
-- Exemptions, asserted positively: the explicit-recipient paths deliver what the caller lists.
|
|
270
|
+
-- ---------------------------------------------------------------------------
|
|
271
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
272
|
+
select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
|
|
273
|
+
set local role authenticated;
|
|
274
|
+
|
|
275
|
+
select lives_ok(
|
|
276
|
+
$$ insert into _opn_multi
|
|
277
|
+
select * from agent_board.send_multicast_message(
|
|
278
|
+
(select thread from _opn), null, array[(select b_agent from _opn)],
|
|
279
|
+
'opn multicast', 'listed recipients only', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
280
|
+
'OPN-18: the author can multicast to a listed recipient');
|
|
281
|
+
|
|
282
|
+
select lives_ok(
|
|
283
|
+
$$ insert into _opn_self_task
|
|
284
|
+
select * from agent_board.create_tasking_delivery(
|
|
285
|
+
(select thread from _opn), null, 'opn self task', 'the creator lists itself',
|
|
286
|
+
array[(select a_agent from _opn)], null, 'medium', 'distributed', null, '{}'::jsonb) $$,
|
|
287
|
+
'OPN-19: the creator can list itself as an explicit task recipient');
|
|
288
|
+
|
|
289
|
+
reset role;
|
|
290
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
291
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
292
|
+
|
|
293
|
+
select is(
|
|
294
|
+
(select array_agg(recipient_agent_id order by recipient_agent_id)
|
|
295
|
+
from agent_board.inbox_items
|
|
296
|
+
where source_message_id = (select message_id from _opn_multi)),
|
|
297
|
+
array[(select b_agent from _opn)],
|
|
298
|
+
'OPN-20: a multicast delivers to exactly the listed recipients, so not to an unlisted author');
|
|
299
|
+
|
|
300
|
+
select is(
|
|
301
|
+
(select count(*)::int from agent_board.inbox_items
|
|
302
|
+
where source_task_id = (select task_id from _opn_self_task)
|
|
303
|
+
and recipient_agent_id = (select a_agent from _opn)
|
|
304
|
+
and visible_in_inbox),
|
|
305
|
+
1,
|
|
306
|
+
'OPN-21: an explicit recipient list naming the creator still delivers to it');
|
|
307
|
+
|
|
308
|
+
select is(
|
|
309
|
+
(select delivered_recipient_count from _opn_self_task),
|
|
310
|
+
1,
|
|
311
|
+
'OPN-22: the explicit self-addressed task counts its one listed recipient');
|
|
312
|
+
|
|
313
|
+
-- ---------------------------------------------------------------------------
|
|
314
|
+
-- Round 1 of the PR #615 board: the creator-only task, the descriptor expansions, a direct send.
|
|
315
|
+
-- ---------------------------------------------------------------------------
|
|
316
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
317
|
+
select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
|
|
318
|
+
set local role authenticated;
|
|
319
|
+
|
|
320
|
+
-- A board-wide task on a board whose only member is its creator. Skipping the creator's delivery
|
|
321
|
+
-- would skip the only write that stamps threads.board_id (OBSGATE #78), leaving the thread on the
|
|
322
|
+
-- access-only branch of post_message's observer gate. Mutation: exclude the creator from the loop.
|
|
323
|
+
select lives_ok(
|
|
324
|
+
$$ insert into _opn_sole_task
|
|
325
|
+
select * from agent_board.create_tasking_delivery(
|
|
326
|
+
(select thread_unlinked from _opn), null, 'opn sole task', 'creator alone', null,
|
|
327
|
+
(select board_s from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
|
|
328
|
+
'OPN-23: the creator can deliver a task to a board it is alone on');
|
|
329
|
+
|
|
330
|
+
reset role;
|
|
331
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
332
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
333
|
+
|
|
334
|
+
select is(
|
|
335
|
+
(select board_id from agent_board.threads where id = (select thread_unlinked from _opn)),
|
|
336
|
+
(select board_s from _opn),
|
|
337
|
+
'OPN-24: a creator-only board task still links its thread to the board');
|
|
338
|
+
|
|
339
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
340
|
+
select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
|
|
341
|
+
set local role authenticated;
|
|
342
|
+
|
|
343
|
+
-- A multicast to a group alias the author belongs to.
|
|
344
|
+
select lives_ok(
|
|
345
|
+
$$ insert into _opn_group
|
|
346
|
+
select * from agent_board.send_multicast_message(
|
|
347
|
+
(select thread from _opn), null, '[{"kind":"group_alias","groupAlias":"opn-team"}]'::jsonb,
|
|
348
|
+
'addressing_v1', 'opn group', 'to a group the author is in', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
349
|
+
'OPN-25: the author can multicast to a group alias it belongs to');
|
|
350
|
+
|
|
351
|
+
-- The same group, plus an explicit agent_id naming the author AFTER it: the explicit mention wins.
|
|
352
|
+
select lives_ok(
|
|
353
|
+
$$ insert into _opn_group_self
|
|
354
|
+
select * from agent_board.send_multicast_message(
|
|
355
|
+
(select thread from _opn), null,
|
|
356
|
+
jsonb_build_array(
|
|
357
|
+
jsonb_build_object('kind', 'group_alias', 'groupAlias', 'opn-team'),
|
|
358
|
+
jsonb_build_object('kind', 'agent_id', 'agentId', (select a_agent from _opn)::text)),
|
|
359
|
+
'addressing_v1', 'opn group self', 'the group, and the author by id', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
360
|
+
'OPN-26: the author can multicast to a group and name itself explicitly');
|
|
361
|
+
|
|
362
|
+
-- A task to a role selector the creator carries.
|
|
363
|
+
select lives_ok(
|
|
364
|
+
$$ insert into _opn_desc_task
|
|
365
|
+
select * from agent_board.create_tasking_delivery(
|
|
366
|
+
(select thread from _opn), null, 'opn role task', 'to a role the creator has',
|
|
367
|
+
'[{"kind":"role_selector","role":"opn-role"}]'::jsonb, null, 'addressing_v1',
|
|
368
|
+
'medium', 'distributed', null, '{}'::jsonb) $$,
|
|
369
|
+
'OPN-27: the creator can deliver a task to a role selector it matches');
|
|
370
|
+
|
|
371
|
+
-- A group whose only member is the sender resolves to nobody else: refused, not self-delivered.
|
|
372
|
+
select throws_ok(
|
|
373
|
+
$$ select * from agent_board.send_multicast_message(
|
|
374
|
+
(select thread from _opn), null, '[{"kind":"group_alias","groupAlias":"opn-solo"}]'::jsonb,
|
|
375
|
+
'addressing_v1', 'opn solo', 'a group of one', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
376
|
+
'22023',
|
|
377
|
+
'recipient descriptors resolved only the sender',
|
|
378
|
+
'OPN-28: a set descriptor that resolves only to the sender is refused');
|
|
379
|
+
|
|
380
|
+
select lives_ok(
|
|
381
|
+
$$ insert into _opn_direct
|
|
382
|
+
select * from agent_board.send_direct_message(
|
|
383
|
+
(select thread from _opn), null, (select b_agent from _opn),
|
|
384
|
+
'opn direct', 'to the member', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
385
|
+
'OPN-29: the author can send a direct message');
|
|
386
|
+
|
|
387
|
+
reset role;
|
|
388
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
389
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
390
|
+
|
|
391
|
+
select is(
|
|
392
|
+
(select array_agg(recipient_agent_id order by recipient_agent_id)
|
|
393
|
+
from agent_board.inbox_items
|
|
394
|
+
where source_message_id = (select message_id from _opn_group)),
|
|
395
|
+
array[(select b_agent from _opn)],
|
|
396
|
+
'OPN-30: a group alias the author belongs to delivers to the other member only');
|
|
397
|
+
|
|
398
|
+
select is(
|
|
399
|
+
(select array_agg(recipient_agent_id order by recipient_agent_id)
|
|
400
|
+
from agent_board.inbox_items
|
|
401
|
+
where source_message_id = (select message_id from _opn_group_self)
|
|
402
|
+
and visible_in_inbox),
|
|
403
|
+
array[(select a_agent from _opn), (select b_agent from _opn)],
|
|
404
|
+
'OPN-31: an explicit agent_id naming the author delivers to it even beside a group that contains it');
|
|
405
|
+
|
|
406
|
+
select is(
|
|
407
|
+
(select array_agg(recipient_agent_id order by recipient_agent_id)
|
|
408
|
+
from agent_board.inbox_items
|
|
409
|
+
where source_task_id = (select task_id from _opn_desc_task)),
|
|
410
|
+
array[(select b_agent from _opn)],
|
|
411
|
+
'OPN-32: a role selector the creator matches delivers the task to the other member only');
|
|
412
|
+
|
|
413
|
+
select is(
|
|
414
|
+
(select array_agg(recipient_agent_id order by recipient_agent_id)
|
|
415
|
+
from agent_board.inbox_items
|
|
416
|
+
where source_message_id = (select message_id from _opn_direct)),
|
|
417
|
+
array[(select b_agent from _opn)],
|
|
418
|
+
'OPN-33: a direct message delivers to exactly its recipient');
|
|
419
|
+
|
|
420
|
+
-- Round 2 of the PR #615 board: the creator lists itself AND targets a board it is on. Both loops
|
|
421
|
+
-- reach the same delivery row; the board loop must not demote the explicit one (which aborted the
|
|
422
|
+
-- whole task on inbox_items_visibility_shape). And an explicit self-mention nested in an
|
|
423
|
+
-- explicit_set, by handle, beside a group that contains the author.
|
|
424
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
425
|
+
select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
|
|
426
|
+
set local role authenticated;
|
|
427
|
+
|
|
428
|
+
select lives_ok(
|
|
429
|
+
$$ insert into _opn_both_task
|
|
430
|
+
select * from agent_board.create_tasking_delivery(
|
|
431
|
+
(select thread from _opn), null, 'opn both task', 'the creator lists itself and its board',
|
|
432
|
+
array[(select a_agent from _opn)], (select board_ab from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
|
|
433
|
+
'OPN-35: the creator can list itself and target a board it is on, in one task');
|
|
434
|
+
|
|
435
|
+
select lives_ok(
|
|
436
|
+
$$ insert into _opn_nested
|
|
437
|
+
select * from agent_board.send_multicast_message(
|
|
438
|
+
(select thread from _opn), null,
|
|
439
|
+
'[{"kind":"group_alias","groupAlias":"opn-team"},{"kind":"explicit_set","recipients":[{"kind":"agent_handle","handle":"opn-author"}]}]'::jsonb,
|
|
440
|
+
'addressing_v1', 'opn nested', 'a group, and the author by handle inside an explicit set', 'comment', 'medium', null, '{}'::jsonb) $$,
|
|
441
|
+
'OPN-36: the author can multicast to a group and name itself by handle inside an explicit_set');
|
|
442
|
+
|
|
443
|
+
-- Round 3 (correctness T1): the creator lists itself AND targets a board it is ALONE on, from an
|
|
444
|
+
-- unlinked thread. A fix that skipped the creator in the board loop would pass every assertion above
|
|
445
|
+
-- and leave this thread unlinked.
|
|
446
|
+
select lives_ok(
|
|
447
|
+
$$ insert into _opn_both_sole
|
|
448
|
+
select * from agent_board.create_tasking_delivery(
|
|
449
|
+
(select thread_unlinked2 from _opn), null, 'opn both sole', 'the creator lists itself; board of one',
|
|
450
|
+
array[(select a_agent from _opn)], (select board_s from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
|
|
451
|
+
'OPN-39: the creator can list itself and target a board it is alone on');
|
|
452
|
+
|
|
453
|
+
reset role;
|
|
454
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
455
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
456
|
+
|
|
457
|
+
select is(
|
|
458
|
+
(select board_id from agent_board.threads where id = (select thread_unlinked2 from _opn)),
|
|
459
|
+
(select board_s from _opn),
|
|
460
|
+
'OPN-40: that task still links its thread to the board');
|
|
461
|
+
|
|
462
|
+
select is(
|
|
463
|
+
(select array_agg(array[ii.visible_in_inbox::text, (ii.recipient_agent_id = (select a_agent from _opn))::text] order by ii.recipient_agent_id)
|
|
464
|
+
from agent_board.inbox_items ii
|
|
465
|
+
where ii.source_task_id = (select task_id from _opn_both_task)),
|
|
466
|
+
array[array['true', 'true'], array['true', 'false']],
|
|
467
|
+
'OPN-37: an explicit self-listing survives a board fan-out the creator is in, and the other member is reached');
|
|
468
|
+
|
|
469
|
+
select is(
|
|
470
|
+
(select array_agg(recipient_agent_id order by recipient_agent_id)
|
|
471
|
+
from agent_board.inbox_items
|
|
472
|
+
where source_message_id = (select message_id from _opn_nested)
|
|
473
|
+
and visible_in_inbox),
|
|
474
|
+
array[(select a_agent from _opn), (select b_agent from _opn)],
|
|
475
|
+
'OPN-38: an agent_handle naming the author inside an explicit_set is an explicit self-mention');
|
|
476
|
+
|
|
477
|
+
-- The descriptor class, from the catalog: every function that resolves recipient descriptors does so
|
|
478
|
+
-- through _resolve_addressing_recipient_rows, and the only callers of it are the resolver itself
|
|
479
|
+
-- (explicit_set recursion), the read-only resolve_recipients, and _addressing_agent_ids_or_raise --
|
|
480
|
+
-- the one delivery path, where the #608 rule lives. A new caller that delivers fails this by name.
|
|
481
|
+
select is(
|
|
482
|
+
(select array_agg(p.proname::text order by p.proname)
|
|
483
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
484
|
+
where n.nspname = 'agent_board'
|
|
485
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_resolve_addressing_recipient_rows')),
|
|
486
|
+
array['_addressing_agent_ids_or_raise', '_resolve_addressing_recipient_rows', 'resolve_recipients'],
|
|
487
|
+
'OPN-34: descriptor expansion reaches a delivery only through _addressing_agent_ids_or_raise');
|
|
488
|
+
|
|
489
|
+
select * from finish();
|
|
490
|
+
rollback;
|
|
@@ -323,8 +323,8 @@ select *
|
|
|
323
323
|
|
|
324
324
|
select is(
|
|
325
325
|
(select delivered_recipient_count from v2_board_delivery),
|
|
326
|
-
|
|
327
|
-
'post_board_message only reaches active joined or subscribed board members (
|
|
326
|
+
2,
|
|
327
|
+
'post_board_message only reaches active joined or subscribed board members, not the author (#608: the author is the seeded owner-creator)'
|
|
328
328
|
);
|
|
329
329
|
|
|
330
330
|
select is(
|
|
@@ -369,8 +369,8 @@ select *
|
|
|
369
369
|
|
|
370
370
|
select is(
|
|
371
371
|
(select delivered_recipient_count from v2_tasking_delivery),
|
|
372
|
-
|
|
373
|
-
'tasking counts explicit and board fan-out attempts before duplicate collapse (
|
|
372
|
+
3,
|
|
373
|
+
'tasking counts explicit and board fan-out attempts before duplicate collapse, not the creator (#608: the creator is the seeded owner-creator)'
|
|
374
374
|
);
|
|
375
375
|
|
|
376
376
|
select is(
|