@consiliency/agent-board-schema 1.2.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.
Files changed (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +55 -0
  4. package/bin/agent-board-schema.js +97 -0
  5. package/fixtures/already-applied.json +181 -0
  6. package/fixtures/divergent-history.json +173 -0
  7. package/fixtures/production-history.json +173 -0
  8. package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
  9. package/migrations/20260502000000_agent_board_schema.sql +180 -0
  10. package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
  11. package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
  12. package/migrations/20260504000000_agent_board_read_models.sql +206 -0
  13. package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
  14. package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
  15. package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
  16. package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
  17. package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
  18. package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
  19. package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
  20. package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
  21. package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
  22. package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
  23. package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
  24. package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
  25. package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
  26. package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
  27. package/migrations/20260513000001_agent_board_retention.sql +160 -0
  28. package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
  29. package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
  30. package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
  31. package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
  32. package/migrations/20260517000000_agent_board_addressing.sql +542 -0
  33. package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
  34. package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
  35. package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
  36. package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
  37. package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
  38. package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
  39. package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
  40. package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
  41. package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
  42. package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
  43. package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
  44. package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
  45. package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
  46. package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
  47. package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
  48. package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
  49. package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
  50. package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
  51. package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
  52. package/package.json +42 -0
  53. package/provenance.json +580 -0
  54. package/schema-manifest.json +458 -0
  55. package/src/check.js +23 -0
  56. package/src/index.js +50 -0
  57. package/src/materialize.js +73 -0
  58. package/src/plan.js +44 -0
  59. package/src/tooling.test.mjs +97 -0
  60. package/tests/agent_board_addressing.test.sql +166 -0
  61. package/tests/agent_board_auth_rls.test.sql +243 -0
  62. package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
  63. package/tests/agent_board_auth_rpcs.test.sql +311 -0
  64. package/tests/agent_board_auth_schema.test.sql +411 -0
  65. package/tests/agent_board_authbound_boundary.test.sql +148 -0
  66. package/tests/agent_board_boardopt.test.sql +170 -0
  67. package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
  68. package/tests/agent_board_callback_runtime.test.sql +425 -0
  69. package/tests/agent_board_callbackcap.test.sql +222 -0
  70. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
  71. package/tests/agent_board_dblint_rpcs.test.sql +247 -0
  72. package/tests/agent_board_fleet_policy.test.sql +370 -0
  73. package/tests/agent_board_fleet_read_models.test.sql +158 -0
  74. package/tests/agent_board_fleet_registry.test.sql +149 -0
  75. package/tests/agent_board_inbox_reads.test.sql +471 -0
  76. package/tests/agent_board_inbox_rpcs.test.sql +452 -0
  77. package/tests/agent_board_inbox_schema.test.sql +355 -0
  78. package/tests/agent_board_nonfleet_identity.test.sql +26 -0
  79. package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
  80. package/tests/agent_board_schema_version.test.sql +24 -0
  81. package/tests/agent_board_schema_version_rpc.test.sql +40 -0
  82. package/tests/agent_board_signature_rls_reads.test.sql +133 -0
  83. package/tests/agent_board_signed_write_gateway.test.sql +219 -0
  84. package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
  85. package/tests/agent_board_signing_store.test.sql +183 -0
  86. package/tests/agent_board_smoke.test.sql +12 -0
  87. package/tests/agent_board_v1_invariants.test.sql +298 -0
  88. package/tests/agent_board_v2_invariants.test.sql +753 -0
  89. package/tests/agent_board_workload_providers.test.sql +15 -0
  90. package/tests/fixtures/adopter-base.sql +19 -0
  91. package/tests/fixtures/operator-hardening.sql +27 -0
  92. package/tests/fixtures/test-prereqs.sql +84 -0
@@ -0,0 +1,1200 @@
1
+ create or replace function agent_board._trim_inbox_text(p_value text, p_max_length integer)
2
+ returns text
3
+ language sql
4
+ immutable
5
+ as $$
6
+ select case
7
+ when p_value is null then null
8
+ else nullif(left(trim(p_value), p_max_length), '')
9
+ end
10
+ $$;
11
+
12
+ create or replace function agent_board._lock_idempotency_key(p_namespace text, p_idempotency_key text)
13
+ returns void
14
+ language plpgsql
15
+ security definer
16
+ set search_path = agent_board, public
17
+ as $$
18
+ begin
19
+ if nullif(trim(p_idempotency_key), '') is null then
20
+ return;
21
+ end if;
22
+
23
+ perform pg_advisory_xact_lock(hashtextextended(p_namespace || ':' || trim(p_idempotency_key), 0));
24
+ end;
25
+ $$;
26
+
27
+ create or replace function agent_board._build_fetch_descriptor(
28
+ p_surface text,
29
+ p_target_id uuid,
30
+ p_view text,
31
+ p_reason text,
32
+ p_filters jsonb default '{}'::jsonb
33
+ )
34
+ returns jsonb
35
+ language sql
36
+ immutable
37
+ as $$
38
+ select jsonb_build_object(
39
+ 'surface', p_surface,
40
+ 'targetId', p_target_id::text,
41
+ 'view', p_view,
42
+ 'filters', coalesce(p_filters, '{}'::jsonb),
43
+ 'reason', p_reason
44
+ )
45
+ $$;
46
+
47
+ create or replace function agent_board._active_board_membership_id(p_board_id uuid, p_agent_id uuid)
48
+ returns uuid
49
+ language sql
50
+ stable
51
+ as $$
52
+ select bm.id
53
+ from agent_board.board_memberships bm
54
+ where bm.board_id = p_board_id
55
+ and bm.agent_id = p_agent_id
56
+ and bm.membership_state in ('joined', 'subscribed')
57
+ order by bm.created_at desc
58
+ limit 1
59
+ $$;
60
+
61
+ create or replace function agent_board._upsert_delivery_recipient(
62
+ p_delivery_mode text,
63
+ p_recipient_agent_id uuid,
64
+ p_source_kind text,
65
+ p_source_message_id uuid default null,
66
+ p_source_task_id uuid default null,
67
+ p_source_delegation_id uuid default null,
68
+ p_board_id uuid default null,
69
+ p_board_membership_id uuid default null,
70
+ p_metadata jsonb default '{}'::jsonb
71
+ )
72
+ returns agent_board.delivery_recipients
73
+ language plpgsql
74
+ security definer
75
+ set search_path = agent_board, public
76
+ as $$
77
+ declare
78
+ v_row agent_board.delivery_recipients%rowtype;
79
+ begin
80
+ insert into agent_board.delivery_recipients(
81
+ delivery_mode,
82
+ recipient_agent_id,
83
+ source_kind,
84
+ source_message_id,
85
+ source_task_id,
86
+ source_delegation_id,
87
+ board_id,
88
+ board_membership_id,
89
+ metadata
90
+ )
91
+ values (
92
+ p_delivery_mode,
93
+ p_recipient_agent_id,
94
+ p_source_kind,
95
+ p_source_message_id,
96
+ p_source_task_id,
97
+ p_source_delegation_id,
98
+ p_board_id,
99
+ p_board_membership_id,
100
+ coalesce(p_metadata, '{}'::jsonb)
101
+ )
102
+ on conflict (recipient_agent_id, canonical_source_key) do update
103
+ set metadata = coalesce(agent_board.delivery_recipients.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb)
104
+ returning * into v_row;
105
+
106
+ return v_row;
107
+ end;
108
+ $$;
109
+
110
+ create or replace function agent_board._upsert_inbox_item(
111
+ p_delivery_recipient_id uuid,
112
+ p_recipient_agent_id uuid,
113
+ p_board_id uuid,
114
+ p_delivery_mode text,
115
+ p_source_kind text,
116
+ p_source_message_id uuid default null,
117
+ p_source_task_id uuid default null,
118
+ p_source_delegation_id uuid default null,
119
+ p_subject text default null,
120
+ p_preview text default null,
121
+ p_priority text default 'medium',
122
+ p_visible_in_inbox boolean default true,
123
+ p_visible_in_board_feed boolean default false,
124
+ p_fetch_descriptor jsonb default '{}'::jsonb,
125
+ p_metadata jsonb default '{}'::jsonb
126
+ )
127
+ returns agent_board.inbox_items
128
+ language plpgsql
129
+ security definer
130
+ set search_path = agent_board, public
131
+ as $$
132
+ declare
133
+ v_row agent_board.inbox_items%rowtype;
134
+ begin
135
+ insert into agent_board.inbox_items(
136
+ delivery_recipient_id,
137
+ recipient_agent_id,
138
+ board_id,
139
+ delivery_mode,
140
+ source_kind,
141
+ source_message_id,
142
+ source_task_id,
143
+ source_delegation_id,
144
+ subject,
145
+ preview,
146
+ priority,
147
+ visible_in_inbox,
148
+ visible_in_board_feed,
149
+ fetch_descriptor,
150
+ metadata
151
+ )
152
+ values (
153
+ p_delivery_recipient_id,
154
+ p_recipient_agent_id,
155
+ p_board_id,
156
+ p_delivery_mode,
157
+ p_source_kind,
158
+ p_source_message_id,
159
+ p_source_task_id,
160
+ p_source_delegation_id,
161
+ p_subject,
162
+ p_preview,
163
+ p_priority,
164
+ p_visible_in_inbox,
165
+ p_visible_in_board_feed,
166
+ coalesce(p_fetch_descriptor, '{}'::jsonb),
167
+ coalesce(p_metadata, '{}'::jsonb)
168
+ )
169
+ on conflict (delivery_recipient_id) do update
170
+ set board_id = excluded.board_id,
171
+ subject = excluded.subject,
172
+ preview = excluded.preview,
173
+ priority = excluded.priority,
174
+ visible_in_inbox = excluded.visible_in_inbox,
175
+ visible_in_board_feed = excluded.visible_in_board_feed,
176
+ fetch_descriptor = excluded.fetch_descriptor,
177
+ metadata = coalesce(agent_board.inbox_items.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb),
178
+ updated_at = now()
179
+ returning * into v_row;
180
+
181
+ return v_row;
182
+ end;
183
+ $$;
184
+
185
+ create or replace function agent_board._queue_inbox_callbacks(p_inbox_item_id uuid, p_event_category text)
186
+ returns integer
187
+ language plpgsql
188
+ security definer
189
+ set search_path = agent_board, public
190
+ as $$
191
+ declare
192
+ v_inserted_count integer := 0;
193
+ begin
194
+ if p_event_category not in ('inbox.item.delivered', 'inbox.item.updated') then
195
+ raise exception 'unsupported inbox callback event category' using errcode = '22023';
196
+ end if;
197
+
198
+ with inserted as (
199
+ insert into agent_board.callback_outbox_events(
200
+ endpoint_id,
201
+ subscription_id,
202
+ recipient_agent_id,
203
+ inbox_item_id,
204
+ board_id,
205
+ event_category,
206
+ delivery_mode,
207
+ source_kind,
208
+ source_message_id,
209
+ source_task_id,
210
+ source_delegation_id,
211
+ idempotency_key,
212
+ subject,
213
+ preview,
214
+ priority,
215
+ fetch_descriptor,
216
+ metadata
217
+ )
218
+ select
219
+ cs.endpoint_id,
220
+ cs.id,
221
+ ii.recipient_agent_id,
222
+ ii.id,
223
+ null,
224
+ p_event_category,
225
+ ii.delivery_mode,
226
+ ii.source_kind,
227
+ ii.source_message_id,
228
+ ii.source_task_id,
229
+ ii.source_delegation_id,
230
+ case
231
+ when p_event_category = 'inbox.item.delivered' then
232
+ p_event_category || ':' || ii.id::text || ':' || cs.id::text
233
+ else
234
+ p_event_category || ':' || ii.id::text || ':' || cs.id::text || ':' || replace(ii.updated_at::text, ' ', 'T') || ':' || ii.action_state
235
+ end,
236
+ ii.subject,
237
+ ii.preview,
238
+ ii.priority,
239
+ ii.fetch_descriptor,
240
+ jsonb_build_object(
241
+ 'subscriptionScope', cs.subscription_scope,
242
+ 'subscriptionStatus', cs.subscription_status,
243
+ 'actionState', ii.action_state
244
+ )
245
+ from agent_board.inbox_items ii
246
+ join agent_board.callback_subscriptions cs
247
+ on cs.agent_id = ii.recipient_agent_id
248
+ and cs.subscription_scope = 'own_inbox'
249
+ and cs.subscription_status = 'active'
250
+ and cs.event_category = p_event_category
251
+ join agent_board.callback_endpoints ce
252
+ on ce.id = cs.endpoint_id
253
+ and ce.owner_agent_id = cs.agent_id
254
+ and ce.endpoint_status = 'verified'
255
+ where ii.id = p_inbox_item_id
256
+ on conflict (idempotency_key) do nothing
257
+ returning 1
258
+ )
259
+ select count(*) into v_inserted_count from inserted;
260
+
261
+ return v_inserted_count;
262
+ end;
263
+ $$;
264
+
265
+ create or replace function agent_board._queue_board_post_callbacks(
266
+ p_board_id uuid,
267
+ p_message_id uuid,
268
+ p_subject text,
269
+ p_preview text,
270
+ p_priority text,
271
+ p_fetch_descriptor jsonb
272
+ )
273
+ returns integer
274
+ language plpgsql
275
+ security definer
276
+ set search_path = agent_board, public
277
+ as $$
278
+ declare
279
+ v_inserted_count integer := 0;
280
+ begin
281
+ with inserted as (
282
+ insert into agent_board.callback_outbox_events(
283
+ endpoint_id,
284
+ subscription_id,
285
+ recipient_agent_id,
286
+ inbox_item_id,
287
+ board_id,
288
+ event_category,
289
+ delivery_mode,
290
+ source_kind,
291
+ source_message_id,
292
+ idempotency_key,
293
+ subject,
294
+ preview,
295
+ priority,
296
+ fetch_descriptor,
297
+ metadata
298
+ )
299
+ select
300
+ cs.endpoint_id,
301
+ cs.id,
302
+ cs.agent_id,
303
+ null,
304
+ p_board_id,
305
+ 'board.post.delivered',
306
+ 'board',
307
+ 'message',
308
+ p_message_id,
309
+ 'board.post.delivered:' || p_message_id::text || ':' || cs.id::text,
310
+ p_subject,
311
+ p_preview,
312
+ p_priority,
313
+ p_fetch_descriptor,
314
+ jsonb_build_object(
315
+ 'subscriptionScope', cs.subscription_scope,
316
+ 'subscriptionStatus', cs.subscription_status
317
+ )
318
+ from agent_board.callback_subscriptions cs
319
+ join agent_board.callback_endpoints ce
320
+ on ce.id = cs.endpoint_id
321
+ and ce.owner_agent_id = cs.agent_id
322
+ and ce.endpoint_status = 'verified'
323
+ join agent_board.board_memberships bm
324
+ on bm.id = cs.board_membership_id
325
+ and bm.membership_state in ('joined', 'subscribed')
326
+ where cs.subscription_scope = 'board'
327
+ and cs.subscription_status = 'active'
328
+ and cs.event_category = 'board.post.delivered'
329
+ and cs.board_id = p_board_id
330
+ on conflict (idempotency_key) do nothing
331
+ returning 1
332
+ )
333
+ select count(*) into v_inserted_count from inserted;
334
+
335
+ return v_inserted_count;
336
+ end;
337
+ $$;
338
+
339
+ create or replace function agent_board.create_board(
340
+ p_created_by_agent_id uuid,
341
+ p_slug text,
342
+ p_name text,
343
+ p_description text default null,
344
+ p_metadata jsonb default '{}'::jsonb
345
+ )
346
+ returns table(board_id uuid, slug text, name text, status text, created_at timestamptz, updated_at timestamptz)
347
+ language plpgsql
348
+ security definer
349
+ set search_path = agent_board, public
350
+ as $$
351
+ begin
352
+ if nullif(trim(p_slug), '') is null or nullif(trim(p_name), '') is null then
353
+ raise exception 'board slug and name are required' using errcode = '22023';
354
+ end if;
355
+ if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
356
+ raise exception 'agent not found' using errcode = 'P0002';
357
+ end if;
358
+
359
+ return query
360
+ insert into agent_board.boards as b(slug, name, description, created_by_agent_id, metadata)
361
+ values (trim(p_slug), trim(p_name), nullif(trim(p_description), ''), p_created_by_agent_id, coalesce(p_metadata, '{}'::jsonb))
362
+ on conflict on constraint boards_slug_key do update
363
+ set name = excluded.name,
364
+ description = excluded.description,
365
+ status = 'active',
366
+ metadata = coalesce(b.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb),
367
+ updated_at = now()
368
+ returning b.id, b.slug, b.name, b.status, b.created_at, b.updated_at;
369
+ end;
370
+ $$;
371
+
372
+ create or replace function agent_board.join_board(
373
+ p_board_id uuid,
374
+ p_agent_id uuid,
375
+ p_membership_state text default 'joined',
376
+ p_created_by_agent_id uuid default null,
377
+ p_metadata jsonb default '{}'::jsonb
378
+ )
379
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamptz, updated_at timestamptz)
380
+ language plpgsql
381
+ security definer
382
+ set search_path = agent_board, public
383
+ as $$
384
+ declare
385
+ v_membership agent_board.board_memberships%rowtype;
386
+ begin
387
+ if p_membership_state not in ('joined', 'subscribed') then
388
+ raise exception 'join_board only accepts joined or subscribed' using errcode = '22023';
389
+ end if;
390
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
391
+ raise exception 'board not found' using errcode = 'P0002';
392
+ end if;
393
+ if not exists (select 1 from agent_board.agents where id = p_agent_id) then
394
+ raise exception 'agent not found' using errcode = 'P0002';
395
+ end if;
396
+ if p_created_by_agent_id is not null and not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
397
+ raise exception 'creator agent not found' using errcode = 'P0002';
398
+ end if;
399
+
400
+ select *
401
+ into v_membership
402
+ from agent_board.board_memberships bm
403
+ where bm.board_id = p_board_id
404
+ and bm.agent_id = p_agent_id
405
+ order by bm.created_at desc
406
+ limit 1
407
+ for update;
408
+
409
+ if v_membership.id is null then
410
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, metadata)
411
+ values (p_board_id, p_agent_id, p_membership_state, coalesce(p_created_by_agent_id, p_agent_id), coalesce(p_metadata, '{}'::jsonb))
412
+ returning * into v_membership;
413
+ else
414
+ update agent_board.board_memberships bm
415
+ set membership_state = p_membership_state,
416
+ left_at = null,
417
+ metadata = coalesce(bm.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
418
+ updated_at = now()
419
+ where bm.id = v_membership.id
420
+ returning * into v_membership;
421
+ end if;
422
+
423
+ return query
424
+ select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
425
+ end;
426
+ $$;
427
+
428
+ create or replace function agent_board.update_board_membership(
429
+ p_board_id uuid,
430
+ p_agent_id uuid,
431
+ p_membership_state text,
432
+ p_metadata jsonb default '{}'::jsonb
433
+ )
434
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
435
+ language plpgsql
436
+ security definer
437
+ set search_path = agent_board, public
438
+ as $$
439
+ declare
440
+ v_membership agent_board.board_memberships%rowtype;
441
+ begin
442
+ if p_membership_state not in ('joined', 'subscribed', 'left', 'removed') then
443
+ raise exception 'invalid board membership state' using errcode = '22023';
444
+ end if;
445
+
446
+ select *
447
+ into v_membership
448
+ from agent_board.board_memberships bm
449
+ where bm.board_id = p_board_id
450
+ and bm.agent_id = p_agent_id
451
+ order by bm.created_at desc
452
+ limit 1
453
+ for update;
454
+
455
+ if v_membership.id is null then
456
+ raise exception 'board membership not found' using errcode = 'P0002';
457
+ end if;
458
+
459
+ update agent_board.board_memberships bm
460
+ set membership_state = p_membership_state,
461
+ left_at = case when p_membership_state in ('left', 'removed') then now() else null end,
462
+ metadata = coalesce(bm.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
463
+ updated_at = now()
464
+ where bm.id = v_membership.id
465
+ returning * into v_membership;
466
+
467
+ return query
468
+ select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
469
+ end;
470
+ $$;
471
+
472
+ create or replace function agent_board.leave_board(
473
+ p_board_id uuid,
474
+ p_agent_id uuid,
475
+ p_metadata jsonb default '{}'::jsonb
476
+ )
477
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
478
+ language plpgsql
479
+ security definer
480
+ set search_path = agent_board, public
481
+ as $$
482
+ begin
483
+ return query
484
+ select *
485
+ from agent_board.update_board_membership(p_board_id, p_agent_id, 'left', p_metadata);
486
+ end;
487
+ $$;
488
+
489
+ create or replace function agent_board.send_direct_message(
490
+ p_thread_id uuid,
491
+ p_author_agent_id uuid,
492
+ p_recipient_agent_id uuid,
493
+ p_subject text,
494
+ p_body text,
495
+ p_message_type text default 'comment',
496
+ p_priority text default 'medium',
497
+ p_idempotency_key text default null,
498
+ p_metadata jsonb default '{}'::jsonb
499
+ )
500
+ returns table(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamptz)
501
+ language plpgsql
502
+ security definer
503
+ set search_path = agent_board, public
504
+ as $$
505
+ declare
506
+ v_message agent_board.messages%rowtype;
507
+ v_created_message record;
508
+ v_recipient agent_board.delivery_recipients%rowtype;
509
+ v_inbox agent_board.inbox_items%rowtype;
510
+ v_callback_count integer := 0;
511
+ v_metadata jsonb;
512
+ v_preview text;
513
+ v_fetch_descriptor jsonb;
514
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
515
+ begin
516
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
517
+ raise exception 'subject and body are required' using errcode = '22023';
518
+ end if;
519
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
520
+ raise exception 'invalid priority' using errcode = '22023';
521
+ end if;
522
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
523
+ raise exception 'thread not found' using errcode = 'P0002';
524
+ end if;
525
+ if not exists (select 1 from agent_board.agents where id = p_author_agent_id) then
526
+ raise exception 'author agent not found' using errcode = 'P0002';
527
+ end if;
528
+ if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
529
+ raise exception 'recipient agent not found' using errcode = 'P0002';
530
+ end if;
531
+
532
+ perform agent_board._lock_idempotency_key(
533
+ 'send_direct_message:' || p_thread_id::text || ':' || p_author_agent_id::text,
534
+ v_idempotency_key
535
+ );
536
+
537
+ if v_idempotency_key is not null then
538
+ select *
539
+ into v_message
540
+ from agent_board.messages
541
+ where thread_id = p_thread_id
542
+ and author_agent_id = p_author_agent_id
543
+ and metadata ->> 'rpcName' = 'send_direct_message'
544
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
545
+ order by created_at desc
546
+ limit 1;
547
+ end if;
548
+
549
+ if v_message.id is null then
550
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
551
+ || jsonb_build_object(
552
+ 'rpcName', 'send_direct_message',
553
+ 'subject', trim(p_subject),
554
+ 'priority', p_priority
555
+ );
556
+ if v_idempotency_key is not null then
557
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
558
+ end if;
559
+
560
+ select *
561
+ into v_created_message
562
+ from agent_board.post_message(
563
+ p_thread_id,
564
+ p_author_agent_id,
565
+ p_body,
566
+ p_message_type,
567
+ null,
568
+ v_metadata
569
+ );
570
+
571
+ select *
572
+ into v_message
573
+ from agent_board.messages
574
+ where id = v_created_message.message_id;
575
+ end if;
576
+
577
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
578
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
579
+ 'thread',
580
+ p_thread_id,
581
+ 'agent_board.v_thread_feed',
582
+ 'direct delivery',
583
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
584
+ );
585
+
586
+ v_recipient := agent_board._upsert_delivery_recipient(
587
+ 'direct',
588
+ p_recipient_agent_id,
589
+ 'message',
590
+ v_message.id,
591
+ null,
592
+ null,
593
+ null,
594
+ null,
595
+ jsonb_build_object('priority', p_priority)
596
+ );
597
+
598
+ v_inbox := agent_board._upsert_inbox_item(
599
+ v_recipient.id,
600
+ p_recipient_agent_id,
601
+ null,
602
+ v_recipient.delivery_mode,
603
+ 'message',
604
+ v_message.id,
605
+ null,
606
+ null,
607
+ trim(p_subject),
608
+ v_preview,
609
+ p_priority,
610
+ true,
611
+ false,
612
+ v_fetch_descriptor,
613
+ jsonb_build_object('threadId', p_thread_id::text)
614
+ );
615
+
616
+ v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
617
+
618
+ return query
619
+ select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
620
+ end;
621
+ $$;
622
+
623
+ create or replace function agent_board.send_multicast_message(
624
+ p_thread_id uuid,
625
+ p_author_agent_id uuid,
626
+ p_recipient_agent_ids uuid[],
627
+ p_subject text,
628
+ p_body text,
629
+ p_message_type text default 'comment',
630
+ p_priority text default 'medium',
631
+ p_idempotency_key text default null,
632
+ p_metadata jsonb default '{}'::jsonb
633
+ )
634
+ returns table(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz)
635
+ language plpgsql
636
+ security definer
637
+ set search_path = agent_board, public
638
+ as $$
639
+ declare
640
+ v_message agent_board.messages%rowtype;
641
+ v_created_message record;
642
+ v_recipient_ids uuid[];
643
+ v_recipient_id uuid;
644
+ v_recipient agent_board.delivery_recipients%rowtype;
645
+ v_inbox agent_board.inbox_items%rowtype;
646
+ v_delivered_count integer := 0;
647
+ v_inbox_count integer := 0;
648
+ v_callback_count integer := 0;
649
+ v_metadata jsonb;
650
+ v_preview text;
651
+ v_fetch_descriptor jsonb;
652
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
653
+ begin
654
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
655
+ raise exception 'subject and body are required' using errcode = '22023';
656
+ end if;
657
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
658
+ raise exception 'invalid priority' using errcode = '22023';
659
+ end if;
660
+ if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
661
+ raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
662
+ end if;
663
+ if array_position(p_recipient_agent_ids, null) is not null then
664
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
665
+ end if;
666
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
667
+ raise exception 'thread not found' using errcode = 'P0002';
668
+ end if;
669
+ if not exists (select 1 from agent_board.agents where id = p_author_agent_id) then
670
+ raise exception 'author agent not found' using errcode = 'P0002';
671
+ end if;
672
+
673
+ select array_agg(distinct recipient_id)
674
+ into v_recipient_ids
675
+ from unnest(p_recipient_agent_ids) as recipient_id;
676
+
677
+ if (
678
+ select count(*)
679
+ from agent_board.agents
680
+ where id = any(v_recipient_ids)
681
+ ) <> cardinality(v_recipient_ids) then
682
+ raise exception 'recipient agent not found' using errcode = 'P0002';
683
+ end if;
684
+
685
+ perform agent_board._lock_idempotency_key(
686
+ 'send_multicast_message:' || p_thread_id::text || ':' || p_author_agent_id::text,
687
+ v_idempotency_key
688
+ );
689
+
690
+ if v_idempotency_key is not null then
691
+ select *
692
+ into v_message
693
+ from agent_board.messages
694
+ where thread_id = p_thread_id
695
+ and author_agent_id = p_author_agent_id
696
+ and metadata ->> 'rpcName' = 'send_multicast_message'
697
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
698
+ order by created_at desc
699
+ limit 1;
700
+ end if;
701
+
702
+ if v_message.id is null then
703
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
704
+ || jsonb_build_object(
705
+ 'rpcName', 'send_multicast_message',
706
+ 'subject', trim(p_subject),
707
+ 'priority', p_priority
708
+ );
709
+ if v_idempotency_key is not null then
710
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
711
+ end if;
712
+
713
+ select *
714
+ into v_created_message
715
+ from agent_board.post_message(
716
+ p_thread_id,
717
+ p_author_agent_id,
718
+ p_body,
719
+ p_message_type,
720
+ null,
721
+ v_metadata
722
+ );
723
+
724
+ select *
725
+ into v_message
726
+ from agent_board.messages
727
+ where id = v_created_message.message_id;
728
+ end if;
729
+
730
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
731
+
732
+ foreach v_recipient_id in array v_recipient_ids loop
733
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
734
+ 'thread',
735
+ p_thread_id,
736
+ 'agent_board.v_thread_feed',
737
+ 'multicast delivery',
738
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
739
+ );
740
+
741
+ v_recipient := agent_board._upsert_delivery_recipient(
742
+ 'multicast',
743
+ v_recipient_id,
744
+ 'message',
745
+ v_message.id,
746
+ null,
747
+ null,
748
+ null,
749
+ null,
750
+ jsonb_build_object('priority', p_priority)
751
+ );
752
+
753
+ v_inbox := agent_board._upsert_inbox_item(
754
+ v_recipient.id,
755
+ v_recipient_id,
756
+ null,
757
+ v_recipient.delivery_mode,
758
+ 'message',
759
+ v_message.id,
760
+ null,
761
+ null,
762
+ trim(p_subject),
763
+ v_preview,
764
+ p_priority,
765
+ true,
766
+ false,
767
+ v_fetch_descriptor,
768
+ jsonb_build_object('threadId', p_thread_id::text)
769
+ );
770
+
771
+ v_delivered_count := v_delivered_count + 1;
772
+ v_inbox_count := v_inbox_count + 1;
773
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
774
+ end loop;
775
+
776
+ return query
777
+ select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
778
+ end;
779
+ $$;
780
+
781
+ create or replace function agent_board.post_board_message(
782
+ p_thread_id uuid,
783
+ p_author_agent_id uuid,
784
+ p_board_id uuid,
785
+ p_subject text,
786
+ p_body text,
787
+ p_message_type text default 'comment',
788
+ p_priority text default 'medium',
789
+ p_idempotency_key text default null,
790
+ p_metadata jsonb default '{}'::jsonb
791
+ )
792
+ returns table(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamptz)
793
+ language plpgsql
794
+ security definer
795
+ set search_path = agent_board, public
796
+ as $$
797
+ declare
798
+ v_message agent_board.messages%rowtype;
799
+ v_created_message record;
800
+ v_membership record;
801
+ v_recipient agent_board.delivery_recipients%rowtype;
802
+ v_inbox agent_board.inbox_items%rowtype;
803
+ v_delivered_count integer := 0;
804
+ v_callback_count integer := 0;
805
+ v_metadata jsonb;
806
+ v_preview text;
807
+ v_fetch_descriptor jsonb;
808
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
809
+ begin
810
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
811
+ raise exception 'subject and body are required' using errcode = '22023';
812
+ end if;
813
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
814
+ raise exception 'invalid priority' using errcode = '22023';
815
+ end if;
816
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
817
+ raise exception 'thread not found' using errcode = 'P0002';
818
+ end if;
819
+ if not exists (select 1 from agent_board.agents where id = p_author_agent_id) then
820
+ raise exception 'author agent not found' using errcode = 'P0002';
821
+ end if;
822
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
823
+ raise exception 'board not found' using errcode = 'P0002';
824
+ end if;
825
+
826
+ perform agent_board._lock_idempotency_key(
827
+ 'post_board_message:' || p_thread_id::text || ':' || p_author_agent_id::text || ':' || p_board_id::text,
828
+ v_idempotency_key
829
+ );
830
+
831
+ if v_idempotency_key is not null then
832
+ select *
833
+ into v_message
834
+ from agent_board.messages
835
+ where thread_id = p_thread_id
836
+ and author_agent_id = p_author_agent_id
837
+ and metadata ->> 'rpcName' = 'post_board_message'
838
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
839
+ order by created_at desc
840
+ limit 1;
841
+ end if;
842
+
843
+ if v_message.id is null then
844
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
845
+ || jsonb_build_object(
846
+ 'rpcName', 'post_board_message',
847
+ 'subject', trim(p_subject),
848
+ 'priority', p_priority,
849
+ 'boardId', p_board_id::text
850
+ );
851
+ if v_idempotency_key is not null then
852
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
853
+ end if;
854
+
855
+ select *
856
+ into v_created_message
857
+ from agent_board.post_message(
858
+ p_thread_id,
859
+ p_author_agent_id,
860
+ p_body,
861
+ p_message_type,
862
+ null,
863
+ v_metadata
864
+ );
865
+
866
+ select *
867
+ into v_message
868
+ from agent_board.messages
869
+ where id = v_created_message.message_id;
870
+ end if;
871
+
872
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
873
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
874
+ 'board',
875
+ p_board_id,
876
+ 'agent_board.v_board_feed',
877
+ 'board delivery',
878
+ jsonb_build_object('messageId', v_message.id::text)
879
+ );
880
+
881
+ for v_membership in
882
+ select bm.id, bm.agent_id
883
+ from agent_board.board_memberships bm
884
+ where bm.board_id = p_board_id
885
+ and bm.membership_state in ('joined', 'subscribed')
886
+ order by bm.created_at
887
+ loop
888
+ v_recipient := agent_board._upsert_delivery_recipient(
889
+ 'board',
890
+ v_membership.agent_id,
891
+ 'message',
892
+ v_message.id,
893
+ null,
894
+ null,
895
+ p_board_id,
896
+ v_membership.id,
897
+ jsonb_build_object('priority', p_priority)
898
+ );
899
+
900
+ v_inbox := agent_board._upsert_inbox_item(
901
+ v_recipient.id,
902
+ v_membership.agent_id,
903
+ p_board_id,
904
+ v_recipient.delivery_mode,
905
+ 'message',
906
+ v_message.id,
907
+ null,
908
+ null,
909
+ trim(p_subject),
910
+ v_preview,
911
+ p_priority,
912
+ true,
913
+ true,
914
+ v_fetch_descriptor,
915
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
916
+ );
917
+
918
+ v_delivered_count := v_delivered_count + 1;
919
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
920
+ end loop;
921
+
922
+ v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
923
+ p_board_id,
924
+ v_message.id,
925
+ trim(p_subject),
926
+ v_preview,
927
+ p_priority,
928
+ v_fetch_descriptor
929
+ );
930
+
931
+ return query
932
+ select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
933
+ end;
934
+ $$;
935
+
936
+ create or replace function agent_board.create_tasking_delivery(
937
+ p_thread_id uuid,
938
+ p_created_by_agent_id uuid,
939
+ p_title text,
940
+ p_details text default null,
941
+ p_recipient_agent_ids uuid[] default null,
942
+ p_board_id uuid default null,
943
+ p_priority text default 'medium',
944
+ p_orchestration_mode text default 'distributed',
945
+ p_idempotency_key text default null,
946
+ p_metadata jsonb default '{}'::jsonb
947
+ )
948
+ returns table(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz, updated_at timestamptz)
949
+ language plpgsql
950
+ security definer
951
+ set search_path = agent_board, public
952
+ as $$
953
+ declare
954
+ v_task agent_board.tasks%rowtype;
955
+ v_created_event_id uuid;
956
+ v_created_task record;
957
+ v_recipient_ids uuid[];
958
+ v_recipient_id uuid;
959
+ v_membership record;
960
+ v_recipient agent_board.delivery_recipients%rowtype;
961
+ v_inbox agent_board.inbox_items%rowtype;
962
+ v_delivered_count integer := 0;
963
+ v_inbox_count integer := 0;
964
+ v_callback_count integer := 0;
965
+ v_metadata jsonb;
966
+ v_preview text;
967
+ v_fetch_descriptor jsonb;
968
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
969
+ begin
970
+ if nullif(trim(p_title), '') is null then
971
+ raise exception 'task title is required' using errcode = '22023';
972
+ end if;
973
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
974
+ raise exception 'invalid priority' using errcode = '22023';
975
+ end if;
976
+ if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
977
+ raise exception 'invalid orchestration mode' using errcode = '22023';
978
+ end if;
979
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
980
+ raise exception 'thread not found' using errcode = 'P0002';
981
+ end if;
982
+ if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
983
+ raise exception 'creator agent not found' using errcode = 'P0002';
984
+ end if;
985
+ if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
986
+ raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
987
+ end if;
988
+ if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
989
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
990
+ end if;
991
+ if p_board_id is not null and not exists (select 1 from agent_board.boards where id = p_board_id) then
992
+ raise exception 'board not found' using errcode = 'P0002';
993
+ end if;
994
+
995
+ select array_agg(distinct recipient_id)
996
+ into v_recipient_ids
997
+ from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
998
+
999
+ if v_recipient_ids is not null and (
1000
+ select count(*)
1001
+ from agent_board.agents
1002
+ where id = any(v_recipient_ids)
1003
+ ) <> cardinality(v_recipient_ids) then
1004
+ raise exception 'recipient agent not found' using errcode = 'P0002';
1005
+ end if;
1006
+
1007
+ perform agent_board._lock_idempotency_key(
1008
+ 'create_tasking_delivery:' || p_thread_id::text || ':' || p_created_by_agent_id::text,
1009
+ v_idempotency_key
1010
+ );
1011
+
1012
+ if v_idempotency_key is not null then
1013
+ select *
1014
+ into v_task
1015
+ from agent_board.tasks
1016
+ where thread_id = p_thread_id
1017
+ and created_by_agent_id = p_created_by_agent_id
1018
+ and metadata ->> 'rpcName' = 'create_tasking_delivery'
1019
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
1020
+ order by created_at desc
1021
+ limit 1;
1022
+ end if;
1023
+
1024
+ if v_task.id is null then
1025
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
1026
+ || jsonb_build_object(
1027
+ 'rpcName', 'create_tasking_delivery',
1028
+ 'priority', p_priority
1029
+ );
1030
+ if p_board_id is not null then
1031
+ v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
1032
+ end if;
1033
+ if v_idempotency_key is not null then
1034
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
1035
+ end if;
1036
+
1037
+ select *
1038
+ into v_created_task
1039
+ from agent_board.create_task(
1040
+ p_thread_id,
1041
+ p_created_by_agent_id,
1042
+ trim(p_title),
1043
+ p_details,
1044
+ null,
1045
+ null,
1046
+ p_priority,
1047
+ p_orchestration_mode,
1048
+ v_metadata
1049
+ );
1050
+
1051
+ v_created_event_id := v_created_task.created_event_id;
1052
+
1053
+ select *
1054
+ into v_task
1055
+ from agent_board.tasks
1056
+ where id = v_created_task.task_id;
1057
+ else
1058
+ select id
1059
+ into v_created_event_id
1060
+ from agent_board.task_events
1061
+ where task_id = v_task.id
1062
+ and event_sequence = 1;
1063
+ end if;
1064
+
1065
+ v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
1066
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
1067
+ 'task',
1068
+ v_task.id,
1069
+ 'agent_board.v_task_tree',
1070
+ 'tasking delivery',
1071
+ jsonb_build_object('threadId', p_thread_id::text)
1072
+ );
1073
+
1074
+ if v_recipient_ids is not null then
1075
+ foreach v_recipient_id in array v_recipient_ids loop
1076
+ v_recipient := agent_board._upsert_delivery_recipient(
1077
+ 'tasking',
1078
+ v_recipient_id,
1079
+ 'task',
1080
+ null,
1081
+ v_task.id,
1082
+ null,
1083
+ null,
1084
+ null,
1085
+ jsonb_build_object('priority', p_priority)
1086
+ );
1087
+
1088
+ v_inbox := agent_board._upsert_inbox_item(
1089
+ v_recipient.id,
1090
+ v_recipient_id,
1091
+ null,
1092
+ v_recipient.delivery_mode,
1093
+ 'task',
1094
+ null,
1095
+ v_task.id,
1096
+ null,
1097
+ trim(p_title),
1098
+ v_preview,
1099
+ p_priority,
1100
+ true,
1101
+ false,
1102
+ v_fetch_descriptor,
1103
+ jsonb_build_object('threadId', p_thread_id::text)
1104
+ );
1105
+
1106
+ v_delivered_count := v_delivered_count + 1;
1107
+ v_inbox_count := v_inbox_count + 1;
1108
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
1109
+ end loop;
1110
+ end if;
1111
+
1112
+ if p_board_id is not null then
1113
+ for v_membership in
1114
+ select bm.id, bm.agent_id
1115
+ from agent_board.board_memberships bm
1116
+ where bm.board_id = p_board_id
1117
+ and bm.membership_state in ('joined', 'subscribed')
1118
+ order by bm.created_at
1119
+ loop
1120
+ v_recipient := agent_board._upsert_delivery_recipient(
1121
+ 'tasking',
1122
+ v_membership.agent_id,
1123
+ 'task',
1124
+ null,
1125
+ v_task.id,
1126
+ null,
1127
+ p_board_id,
1128
+ v_membership.id,
1129
+ jsonb_build_object('priority', p_priority)
1130
+ );
1131
+
1132
+ v_inbox := agent_board._upsert_inbox_item(
1133
+ v_recipient.id,
1134
+ v_membership.agent_id,
1135
+ v_recipient.board_id,
1136
+ v_recipient.delivery_mode,
1137
+ 'task',
1138
+ null,
1139
+ v_task.id,
1140
+ null,
1141
+ trim(p_title),
1142
+ v_preview,
1143
+ p_priority,
1144
+ true,
1145
+ v_recipient.board_id is not null,
1146
+ v_fetch_descriptor,
1147
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
1148
+ );
1149
+
1150
+ v_delivered_count := v_delivered_count + 1;
1151
+ v_inbox_count := v_inbox_count + 1;
1152
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
1153
+ end loop;
1154
+ end if;
1155
+
1156
+ return query
1157
+ 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;
1158
+ end;
1159
+ $$;
1160
+
1161
+ create or replace function agent_board.update_inbox_item_state(
1162
+ p_inbox_item_id uuid,
1163
+ p_recipient_agent_id uuid,
1164
+ p_action_state text,
1165
+ p_metadata jsonb default '{}'::jsonb
1166
+ )
1167
+ returns table(inbox_item_id uuid, recipient_agent_id uuid, action_state text, callback_outbox_count integer, updated_at timestamptz)
1168
+ language plpgsql
1169
+ security definer
1170
+ set search_path = agent_board, public
1171
+ as $$
1172
+ declare
1173
+ v_inbox agent_board.inbox_items%rowtype;
1174
+ v_callback_count integer := 0;
1175
+ begin
1176
+ if p_action_state not in ('new', 'read', 'acknowledged', 'deferred', 'dismissed', 'restored') then
1177
+ raise exception 'invalid inbox action state' using errcode = '22023';
1178
+ end if;
1179
+
1180
+ update agent_board.inbox_items ii
1181
+ set action_state = p_action_state,
1182
+ metadata = coalesce(ii.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
1183
+ updated_at = now()
1184
+ where ii.id = p_inbox_item_id
1185
+ and ii.recipient_agent_id = p_recipient_agent_id
1186
+ returning * into v_inbox;
1187
+
1188
+ if v_inbox.id is null then
1189
+ raise exception 'inbox item not found' using errcode = 'P0002';
1190
+ end if;
1191
+
1192
+ v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.updated');
1193
+
1194
+ return query
1195
+ select v_inbox.id, v_inbox.recipient_agent_id, v_inbox.action_state, v_callback_count, v_inbox.updated_at;
1196
+ end;
1197
+ $$;
1198
+
1199
+ revoke all on all functions in schema agent_board from public, anon, authenticated;
1200
+ grant execute on all functions in schema agent_board to service_role;