patchr 0.1.0__py3-none-any.whl

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 (116) hide show
  1. apps/__init__.py +2 -0
  2. apps/api/__init__.py +2 -0
  3. apps/api/main.py +652 -0
  4. apps/benchmarks/__init__.py +1 -0
  5. apps/benchmarks/main.py +20 -0
  6. apps/sandbox/__init__.py +1 -0
  7. apps/sandbox/main.py +20 -0
  8. apps/worker/__init__.py +2 -0
  9. apps/worker/main.py +15 -0
  10. apps/worker/verify.py +14 -0
  11. patchr/__init__.py +12 -0
  12. patchr/sdk/__init__.py +20 -0
  13. patchr/sdk/client.py +12 -0
  14. patchr-0.1.0.dist-info/METADATA +137 -0
  15. patchr-0.1.0.dist-info/RECORD +116 -0
  16. patchr-0.1.0.dist-info/WHEEL +5 -0
  17. patchr-0.1.0.dist-info/entry_points.txt +5 -0
  18. patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
  19. patchr-0.1.0.dist-info/top_level.txt +3 -0
  20. picux/__init__.py +6 -0
  21. picux/agents/__init__.py +5 -0
  22. picux/agents/registry.py +204 -0
  23. picux/api/__init__.py +5 -0
  24. picux/api/service.py +5075 -0
  25. picux/audit/__init__.py +31 -0
  26. picux/audit/activity.py +97 -0
  27. picux/audit/observability.py +55 -0
  28. picux/audit/verification/__init__.py +21 -0
  29. picux/audit/verification/ledger.py +633 -0
  30. picux/benchmarks/__init__.py +5 -0
  31. picux/benchmarks/local.py +286 -0
  32. picux/config.py +140 -0
  33. picux/contracts/__init__.py +22 -0
  34. picux/contracts/handshake.py +122 -0
  35. picux/contracts/integration.py +385 -0
  36. picux/contracts/openapi.py +187 -0
  37. picux/contracts/protocol_map.py +152 -0
  38. picux/contracts/routes.py +980 -0
  39. picux/contracts/schema_catalog.py +125 -0
  40. picux/core/__init__.py +17 -0
  41. picux/core/models.py +148 -0
  42. picux/core/router.py +131 -0
  43. picux/core/runtime.py +42 -0
  44. picux/core/state_machine.py +38 -0
  45. picux/domains/__init__.py +2 -0
  46. picux/domains/bridge/HostRun.py +1104 -0
  47. picux/domains/bridge/__init__.py +6 -0
  48. picux/domains/bridge/engine.py +345 -0
  49. picux/domains/hunt/__init__.py +6 -0
  50. picux/domains/hunt/engine.py +307 -0
  51. picux/domains/hunt/models.py +88 -0
  52. picux/domains/pay/__init__.py +16 -0
  53. picux/domains/pay/adapters.py +607 -0
  54. picux/domains/pay/engine.py +950 -0
  55. picux/domains/pay/models.py +95 -0
  56. picux/domains/proxy/__init__.py +5 -0
  57. picux/domains/proxy/engine.py +466 -0
  58. picux/domains/resolve/__init__.py +5 -0
  59. picux/domains/resolve/engine.py +546 -0
  60. picux/orchestrator/__init__.py +3 -0
  61. picux/orchestrator/engine.py +2840 -0
  62. picux/portals/__init__.py +17 -0
  63. picux/portals/templates.py +272 -0
  64. picux/protocols/__init__.py +1 -0
  65. picux/protocols/a2a/__init__.py +6 -0
  66. picux/protocols/a2a/client.py +51 -0
  67. picux/protocols/a2a/envelope.py +132 -0
  68. picux/protocols/mcp/__init__.py +7 -0
  69. picux/protocols/mcp/client.py +69 -0
  70. picux/protocols/mcp/contract.py +67 -0
  71. picux/protocols/mcp/server.py +76 -0
  72. picux/sandbox/__init__.py +6 -0
  73. picux/sandbox/midnight_arbitrage.py +215 -0
  74. picux/sandbox/models.py +90 -0
  75. picux/sdk/__init__.py +13 -0
  76. picux/sdk/client.py +768 -0
  77. picux/sdk/external.py +245 -0
  78. picux/security/__init__.py +18 -0
  79. picux/security/auth.py +86 -0
  80. picux/security/config_validator.py +58 -0
  81. picux/security/policy.py +158 -0
  82. picux/security/secrets.py +144 -0
  83. picux/signals/__init__.py +1 -0
  84. picux/signals/community/__init__.py +24 -0
  85. picux/signals/community/adapters/__init__.py +7 -0
  86. picux/signals/community/adapters/reddit.py +37 -0
  87. picux/signals/community/adapters/shopify.py +23 -0
  88. picux/signals/community/adapters/web.py +23 -0
  89. picux/signals/community/disambiguation.py +51 -0
  90. picux/signals/community/intake.py +227 -0
  91. picux/signals/community/models.py +102 -0
  92. picux/signals/community/rules.py +91 -0
  93. picux/signals/community/scoring.py +64 -0
  94. picux/storage/__init__.py +41 -0
  95. picux/storage/agents.py +50 -0
  96. picux/storage/cases.py +440 -0
  97. picux/storage/channels.py +476 -0
  98. picux/storage/connectors.py +411 -0
  99. picux/storage/envelopes.py +137 -0
  100. picux/storage/escrows.py +168 -0
  101. picux/storage/events.py +989 -0
  102. picux/storage/keyspace.py +60 -0
  103. picux/storage/mandates.py +107 -0
  104. picux/storage/portals.py +222 -0
  105. picux/storage/postgres.py +2049 -0
  106. picux/storage/providers.py +148 -0
  107. picux/storage/proxy.py +231 -0
  108. picux/storage/receipts.py +131 -0
  109. picux/storage/signals.py +147 -0
  110. picux/storage/tasks.py +179 -0
  111. picux/tools/__init__.py +11 -0
  112. picux/tools/shared.py +2048 -0
  113. picux/verification/__init__.py +5 -0
  114. picux/verification/rollout.py +183 -0
  115. picux/workflows/__init__.py +5 -0
  116. picux/workflows/templates.py +74 -0
@@ -0,0 +1,2049 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from contextlib import contextmanager
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Iterator
8
+
9
+ from picux.config import PicuxSettings
10
+ from picux.core import Domain, ProtocolTask, ProtocolTaskStatus, ProtocolUser
11
+
12
+ logger = logging.getLogger("picux.storage.postgres")
13
+
14
+ try:
15
+ import psycopg
16
+ except Exception: # pragma: no cover - optional dependency
17
+ psycopg = None
18
+
19
+
20
+ class PicuxPostgresStore:
21
+ """Optional Postgres persistence for protocol-grade state and audit logs."""
22
+
23
+ def __init__(self, dsn: str = "", *, ensure: bool = True) -> None:
24
+ self.dsn = (dsn or "").strip()
25
+ self.enabled = bool(self.dsn and psycopg is not None)
26
+ if self.enabled and ensure:
27
+ self.ensureSchema()
28
+
29
+ @classmethod
30
+ def fromEnv(cls) -> "PicuxPostgresStore":
31
+ return cls(PicuxSettings.fromEnv().postgresDsn)
32
+
33
+ @contextmanager
34
+ def conn(self) -> Iterator[Any]:
35
+ if not self.enabled:
36
+ raise RuntimeError("postgresDisabled")
37
+ db = psycopg.connect(self.dsn)
38
+ try:
39
+ yield db
40
+ db.commit()
41
+ finally:
42
+ db.close()
43
+
44
+ def ddl(self) -> list[str]:
45
+ return [
46
+ """
47
+ CREATE TABLE IF NOT EXISTS picux_users (
48
+ user_id TEXT PRIMARY KEY,
49
+ channel TEXT NOT NULL DEFAULT 'api',
50
+ trust INTEGER NOT NULL DEFAULT 1,
51
+ refs JSONB NOT NULL DEFAULT '{}'::jsonb,
52
+ prefs JSONB NOT NULL DEFAULT '{}'::jsonb,
53
+ meta JSONB NOT NULL DEFAULT '{}'::jsonb,
54
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
55
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
56
+ )
57
+ """,
58
+ "CREATE INDEX IF NOT EXISTS idx_picux_users_channel ON picux_users (channel)",
59
+ """
60
+ CREATE TABLE IF NOT EXISTS picux_agents (
61
+ agent_id TEXT PRIMARY KEY,
62
+ name TEXT NOT NULL,
63
+ domains JSONB NOT NULL DEFAULT '[]'::jsonb,
64
+ caps JSONB NOT NULL DEFAULT '[]'::jsonb,
65
+ endpoint TEXT NOT NULL DEFAULT '',
66
+ trust INTEGER NOT NULL DEFAULT 1,
67
+ status TEXT NOT NULL DEFAULT 'active',
68
+ meta JSONB NOT NULL DEFAULT '{}'::jsonb,
69
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
70
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
71
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
72
+ )
73
+ """,
74
+ "CREATE INDEX IF NOT EXISTS idx_picux_agents_status ON picux_agents (status)",
75
+ "CREATE INDEX IF NOT EXISTS idx_picux_agents_domains ON picux_agents USING GIN (domains)",
76
+ "CREATE INDEX IF NOT EXISTS idx_picux_agents_caps ON picux_agents USING GIN (caps)",
77
+ """
78
+ CREATE TABLE IF NOT EXISTS picux_a2a_envelopes (
79
+ msg_id TEXT PRIMARY KEY,
80
+ status TEXT NOT NULL DEFAULT 'queued',
81
+ from_agent TEXT NOT NULL DEFAULT '',
82
+ to_agent TEXT NOT NULL DEFAULT '',
83
+ trace_id TEXT NOT NULL DEFAULT '',
84
+ task_id TEXT NOT NULL DEFAULT '',
85
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
86
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
87
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
88
+ )
89
+ """,
90
+ "CREATE INDEX IF NOT EXISTS idx_picux_a2a_envelopes_to_status ON picux_a2a_envelopes (to_agent, status)",
91
+ "CREATE INDEX IF NOT EXISTS idx_picux_a2a_envelopes_trace ON picux_a2a_envelopes (trace_id)",
92
+ """
93
+ CREATE TABLE IF NOT EXISTS picux_mandates (
94
+ mandate_id TEXT PRIMARY KEY,
95
+ status TEXT NOT NULL DEFAULT 'pendingVerification',
96
+ issuer_id TEXT NOT NULL DEFAULT '',
97
+ mandate JSONB NOT NULL DEFAULT '{}'::jsonb,
98
+ validation JSONB NOT NULL DEFAULT '{}'::jsonb,
99
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
100
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
101
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
102
+ )
103
+ """,
104
+ "CREATE INDEX IF NOT EXISTS idx_picux_mandates_status ON picux_mandates (status)",
105
+ "CREATE INDEX IF NOT EXISTS idx_picux_mandates_issuer ON picux_mandates (issuer_id)",
106
+ """
107
+ CREATE TABLE IF NOT EXISTS picux_tasks (
108
+ task_id TEXT PRIMARY KEY,
109
+ user_id TEXT NOT NULL,
110
+ domain TEXT NOT NULL,
111
+ status TEXT NOT NULL,
112
+ channel TEXT NOT NULL DEFAULT 'api',
113
+ mandate_id TEXT NOT NULL DEFAULT '',
114
+ in_data JSONB NOT NULL DEFAULT '{}'::jsonb,
115
+ out_data JSONB NOT NULL DEFAULT '{}'::jsonb,
116
+ needs_approval BOOLEAN NOT NULL DEFAULT FALSE,
117
+ ext_ref TEXT NOT NULL DEFAULT '',
118
+ error_msg TEXT NOT NULL DEFAULT '',
119
+ meta JSONB NOT NULL DEFAULT '{}'::jsonb,
120
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
121
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
122
+ approval_by BIGINT
123
+ )
124
+ """,
125
+ "ALTER TABLE picux_tasks ADD COLUMN IF NOT EXISTS needs_approval BOOLEAN NOT NULL DEFAULT FALSE",
126
+ "ALTER TABLE picux_tasks ADD COLUMN IF NOT EXISTS ext_ref TEXT NOT NULL DEFAULT ''",
127
+ "ALTER TABLE picux_tasks ADD COLUMN IF NOT EXISTS error_msg TEXT NOT NULL DEFAULT ''",
128
+ "ALTER TABLE picux_tasks ADD COLUMN IF NOT EXISTS approval_by BIGINT",
129
+ "CREATE INDEX IF NOT EXISTS idx_picux_tasks_user_created ON picux_tasks (user_id, created_at DESC)",
130
+ "CREATE INDEX IF NOT EXISTS idx_picux_tasks_domain_status ON picux_tasks (domain, status)",
131
+ """
132
+ CREATE TABLE IF NOT EXISTS picux_activity_logs (
133
+ id BIGSERIAL PRIMARY KEY,
134
+ task_id TEXT NOT NULL,
135
+ user_id TEXT NOT NULL,
136
+ level TEXT NOT NULL,
137
+ action TEXT NOT NULL,
138
+ detail TEXT NOT NULL,
139
+ meta JSONB NOT NULL DEFAULT '{}'::jsonb,
140
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
141
+ )
142
+ """,
143
+ "CREATE INDEX IF NOT EXISTS idx_picux_activity_task_created ON picux_activity_logs (task_id, created_at DESC)",
144
+ "CREATE INDEX IF NOT EXISTS idx_picux_activity_user_created ON picux_activity_logs (user_id, created_at DESC)",
145
+ """
146
+ CREATE TABLE IF NOT EXISTS picux_activity_events (
147
+ id BIGSERIAL PRIMARY KEY,
148
+ user_id TEXT NOT NULL,
149
+ task_id TEXT NOT NULL DEFAULT '',
150
+ event TEXT NOT NULL,
151
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
152
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
153
+ )
154
+ """,
155
+ "CREATE INDEX IF NOT EXISTS idx_picux_events_user_created ON picux_activity_events (user_id, created_at DESC)",
156
+ """
157
+ CREATE TABLE IF NOT EXISTS picux_event_subscriptions (
158
+ sub_id TEXT PRIMARY KEY,
159
+ client_id TEXT NOT NULL,
160
+ target TEXT NOT NULL DEFAULT '',
161
+ transport TEXT NOT NULL DEFAULT 'poll',
162
+ endpoint TEXT NOT NULL DEFAULT '',
163
+ status TEXT NOT NULL DEFAULT 'active',
164
+ event_types JSONB NOT NULL DEFAULT '[]'::jsonb,
165
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
166
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
167
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
168
+ )
169
+ """,
170
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_subscriptions_client ON picux_event_subscriptions (client_id)",
171
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_subscriptions_status_transport ON picux_event_subscriptions (status, transport)",
172
+ """
173
+ CREATE TABLE IF NOT EXISTS picux_integration_events (
174
+ event_id TEXT PRIMARY KEY,
175
+ type TEXT NOT NULL,
176
+ subject TEXT NOT NULL DEFAULT '',
177
+ source TEXT NOT NULL DEFAULT 'picux',
178
+ target TEXT NOT NULL DEFAULT '',
179
+ status TEXT NOT NULL DEFAULT 'queued',
180
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
181
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
182
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
183
+ ack_at TIMESTAMPTZ
184
+ )
185
+ """,
186
+ "CREATE INDEX IF NOT EXISTS idx_picux_integration_events_type_status ON picux_integration_events (type, status)",
187
+ "CREATE INDEX IF NOT EXISTS idx_picux_integration_events_target_created ON picux_integration_events (target, created_at DESC)",
188
+ """
189
+ CREATE TABLE IF NOT EXISTS picux_event_deliveries (
190
+ delivery_id TEXT PRIMARY KEY,
191
+ event_id TEXT NOT NULL,
192
+ sub_id TEXT NOT NULL,
193
+ client_id TEXT NOT NULL DEFAULT '',
194
+ transport TEXT NOT NULL DEFAULT 'poll',
195
+ endpoint TEXT NOT NULL DEFAULT '',
196
+ status TEXT NOT NULL DEFAULT 'queued',
197
+ attempt INTEGER NOT NULL DEFAULT 1,
198
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
199
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
200
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
201
+ next_attempt_at TIMESTAMPTZ,
202
+ claimed_at TIMESTAMPTZ,
203
+ lease_expires_at TIMESTAMPTZ,
204
+ delivered_at TIMESTAMPTZ
205
+ )
206
+ """,
207
+ "ALTER TABLE picux_event_deliveries ADD COLUMN IF NOT EXISTS next_attempt_at TIMESTAMPTZ",
208
+ "ALTER TABLE picux_event_deliveries ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ",
209
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_deliveries_event ON picux_event_deliveries (event_id)",
210
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_deliveries_sub_status ON picux_event_deliveries (sub_id, status)",
211
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_deliveries_client_status ON picux_event_deliveries (client_id, status)",
212
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_deliveries_status_lease ON picux_event_deliveries (status, lease_expires_at)",
213
+ "CREATE INDEX IF NOT EXISTS idx_picux_event_deliveries_status_next_attempt ON picux_event_deliveries (status, next_attempt_at)",
214
+ """
215
+ CREATE TABLE IF NOT EXISTS picux_bridge_connectors (
216
+ connector_id TEXT PRIMARY KEY,
217
+ status TEXT NOT NULL DEFAULT 'active',
218
+ domain TEXT NOT NULL DEFAULT 'bridge',
219
+ kind TEXT NOT NULL DEFAULT 'api',
220
+ endpoint TEXT NOT NULL DEFAULT '',
221
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
222
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
223
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
224
+ )
225
+ """,
226
+ "CREATE INDEX IF NOT EXISTS idx_picux_bridge_connectors_status ON picux_bridge_connectors (status)",
227
+ "CREATE INDEX IF NOT EXISTS idx_picux_bridge_connectors_domain_kind ON picux_bridge_connectors (domain, kind)",
228
+ """
229
+ CREATE TABLE IF NOT EXISTS picux_bridge_connection_sessions (
230
+ session_id TEXT PRIMARY KEY,
231
+ connector_id TEXT NOT NULL DEFAULT '',
232
+ status TEXT NOT NULL DEFAULT 'active',
233
+ subject_id TEXT NOT NULL DEFAULT '',
234
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
235
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
236
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
237
+ )
238
+ """,
239
+ "CREATE INDEX IF NOT EXISTS idx_picux_bridge_sessions_connector_status ON picux_bridge_connection_sessions (connector_id, status)",
240
+ """
241
+ CREATE TABLE IF NOT EXISTS picux_channel_threads (
242
+ thread_id TEXT PRIMARY KEY,
243
+ case_id TEXT NOT NULL DEFAULT '',
244
+ conversation_id TEXT NOT NULL DEFAULT '',
245
+ channel TEXT NOT NULL DEFAULT 'api',
246
+ provider TEXT NOT NULL DEFAULT '',
247
+ provider_thread_id TEXT NOT NULL DEFAULT '',
248
+ connector_id TEXT NOT NULL DEFAULT '',
249
+ session_id TEXT NOT NULL DEFAULT '',
250
+ status TEXT NOT NULL DEFAULT 'active',
251
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
252
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
253
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
254
+ )
255
+ """,
256
+ "CREATE INDEX IF NOT EXISTS idx_picux_channel_threads_case ON picux_channel_threads (case_id)",
257
+ "CREATE INDEX IF NOT EXISTS idx_picux_channel_threads_conversation ON picux_channel_threads (conversation_id)",
258
+ "CREATE INDEX IF NOT EXISTS idx_picux_channel_threads_provider_ref ON picux_channel_threads (provider, provider_thread_id)",
259
+ """
260
+ CREATE TABLE IF NOT EXISTS picux_touchpoints (
261
+ touchpoint_id TEXT PRIMARY KEY,
262
+ thread_id TEXT NOT NULL DEFAULT '',
263
+ case_id TEXT NOT NULL DEFAULT '',
264
+ conversation_id TEXT NOT NULL DEFAULT '',
265
+ channel TEXT NOT NULL DEFAULT 'api',
266
+ provider TEXT NOT NULL DEFAULT '',
267
+ connector_id TEXT NOT NULL DEFAULT '',
268
+ direction TEXT NOT NULL DEFAULT 'inbound',
269
+ status TEXT NOT NULL DEFAULT 'received',
270
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
271
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
272
+ )
273
+ """,
274
+ "CREATE INDEX IF NOT EXISTS idx_picux_touchpoints_thread_created ON picux_touchpoints (thread_id, created_at DESC)",
275
+ "CREATE INDEX IF NOT EXISTS idx_picux_touchpoints_case_created ON picux_touchpoints (case_id, created_at DESC)",
276
+ "CREATE INDEX IF NOT EXISTS idx_picux_touchpoints_conversation_created ON picux_touchpoints (conversation_id, created_at DESC)",
277
+ """
278
+ CREATE TABLE IF NOT EXISTS picux_provider_actions (
279
+ action_id TEXT PRIMARY KEY,
280
+ case_id TEXT NOT NULL DEFAULT '',
281
+ conversation_id TEXT NOT NULL DEFAULT '',
282
+ thread_id TEXT NOT NULL DEFAULT '',
283
+ connector_id TEXT NOT NULL DEFAULT '',
284
+ session_id TEXT NOT NULL DEFAULT '',
285
+ status TEXT NOT NULL DEFAULT 'queued',
286
+ action TEXT NOT NULL DEFAULT '',
287
+ resource TEXT NOT NULL DEFAULT '',
288
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
289
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
290
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
291
+ )
292
+ """,
293
+ "CREATE INDEX IF NOT EXISTS idx_picux_provider_actions_connector_status ON picux_provider_actions (connector_id, status)",
294
+ "CREATE INDEX IF NOT EXISTS idx_picux_provider_actions_case ON picux_provider_actions (case_id)",
295
+ """
296
+ CREATE TABLE IF NOT EXISTS picux_provider_callbacks (
297
+ callback_id TEXT PRIMARY KEY,
298
+ case_id TEXT NOT NULL DEFAULT '',
299
+ conversation_id TEXT NOT NULL DEFAULT '',
300
+ thread_id TEXT NOT NULL DEFAULT '',
301
+ connector_id TEXT NOT NULL DEFAULT '',
302
+ session_id TEXT NOT NULL DEFAULT '',
303
+ provider TEXT NOT NULL DEFAULT '',
304
+ provider_event_id TEXT NOT NULL DEFAULT '',
305
+ status TEXT NOT NULL DEFAULT 'received',
306
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
307
+ received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
308
+ )
309
+ """,
310
+ "CREATE INDEX IF NOT EXISTS idx_picux_provider_callbacks_provider_event ON picux_provider_callbacks (provider, provider_event_id)",
311
+ "CREATE INDEX IF NOT EXISTS idx_picux_provider_callbacks_conversation ON picux_provider_callbacks (conversation_id)",
312
+ """
313
+ CREATE TABLE IF NOT EXISTS picux_case_workspaces (
314
+ case_id TEXT PRIMARY KEY,
315
+ status TEXT NOT NULL DEFAULT 'open',
316
+ owner_id TEXT NOT NULL DEFAULT '',
317
+ conversation_id TEXT NOT NULL DEFAULT '',
318
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
319
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
320
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
321
+ )
322
+ """,
323
+ "CREATE INDEX IF NOT EXISTS idx_picux_case_workspaces_status ON picux_case_workspaces (status)",
324
+ "CREATE INDEX IF NOT EXISTS idx_picux_case_workspaces_owner ON picux_case_workspaces (owner_id)",
325
+ "CREATE INDEX IF NOT EXISTS idx_picux_case_workspaces_conversation ON picux_case_workspaces (conversation_id)",
326
+ """
327
+ CREATE TABLE IF NOT EXISTS picux_case_events (
328
+ event_id TEXT PRIMARY KEY,
329
+ case_id TEXT NOT NULL DEFAULT '',
330
+ type TEXT NOT NULL DEFAULT '',
331
+ actor TEXT NOT NULL DEFAULT '',
332
+ status TEXT NOT NULL DEFAULT '',
333
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
334
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
335
+ )
336
+ """,
337
+ "CREATE INDEX IF NOT EXISTS idx_picux_case_events_case_created ON picux_case_events (case_id, created_at DESC)",
338
+ """
339
+ CREATE TABLE IF NOT EXISTS picux_portal_sessions (
340
+ portal_session_id TEXT PRIMARY KEY,
341
+ case_id TEXT NOT NULL DEFAULT '',
342
+ conversation_id TEXT NOT NULL DEFAULT '',
343
+ connector_id TEXT NOT NULL DEFAULT '',
344
+ provider TEXT NOT NULL DEFAULT '',
345
+ status TEXT NOT NULL DEFAULT 'active',
346
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
347
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
348
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
349
+ )
350
+ """,
351
+ "CREATE INDEX IF NOT EXISTS idx_picux_portal_sessions_case ON picux_portal_sessions (case_id)",
352
+ "CREATE INDEX IF NOT EXISTS idx_picux_portal_sessions_connector ON picux_portal_sessions (connector_id, status)",
353
+ """
354
+ CREATE TABLE IF NOT EXISTS picux_portal_actions (
355
+ portal_action_id TEXT PRIMARY KEY,
356
+ portal_session_id TEXT NOT NULL DEFAULT '',
357
+ case_id TEXT NOT NULL DEFAULT '',
358
+ conversation_id TEXT NOT NULL DEFAULT '',
359
+ connector_id TEXT NOT NULL DEFAULT '',
360
+ provider TEXT NOT NULL DEFAULT '',
361
+ status TEXT NOT NULL DEFAULT 'queued',
362
+ kind TEXT NOT NULL DEFAULT '',
363
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
364
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
365
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
366
+ )
367
+ """,
368
+ "CREATE INDEX IF NOT EXISTS idx_picux_portal_actions_session ON picux_portal_actions (portal_session_id)",
369
+ "CREATE INDEX IF NOT EXISTS idx_picux_portal_actions_case_status ON picux_portal_actions (case_id, status)",
370
+ """
371
+ CREATE TABLE IF NOT EXISTS picux_pov_events (
372
+ event_id TEXT PRIMARY KEY,
373
+ user_id TEXT NOT NULL DEFAULT '',
374
+ task_id TEXT NOT NULL DEFAULT '',
375
+ domain TEXT NOT NULL,
376
+ value_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
377
+ currency TEXT NOT NULL DEFAULT 'USD',
378
+ summary TEXT NOT NULL DEFAULT '',
379
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
380
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
381
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
382
+ )
383
+ """,
384
+ "CREATE INDEX IF NOT EXISTS idx_picux_pov_user_occurred ON picux_pov_events (user_id, occurred_at DESC)",
385
+ """
386
+ CREATE TABLE IF NOT EXISTS picux_community_signals (
387
+ signal_id TEXT PRIMARY KEY,
388
+ entity TEXT NOT NULL DEFAULT 'unknown',
389
+ confidence NUMERIC(5,4) NOT NULL DEFAULT 0,
390
+ source_platform TEXT NOT NULL DEFAULT '',
391
+ query TEXT NOT NULL DEFAULT '',
392
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
393
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
394
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
395
+ )
396
+ """,
397
+ "CREATE INDEX IF NOT EXISTS idx_picux_community_signals_entity ON picux_community_signals (entity)",
398
+ "CREATE INDEX IF NOT EXISTS idx_picux_community_signals_source_query ON picux_community_signals (source_platform, query)",
399
+ """
400
+ CREATE TABLE IF NOT EXISTS picux_evidence_artifacts (
401
+ artifact_id TEXT PRIMARY KEY,
402
+ kind TEXT NOT NULL DEFAULT '',
403
+ source TEXT NOT NULL DEFAULT '',
404
+ payload_hash TEXT NOT NULL DEFAULT '',
405
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
406
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
407
+ )
408
+ """,
409
+ "CREATE INDEX IF NOT EXISTS idx_picux_evidence_kind_source ON picux_evidence_artifacts (kind, source)",
410
+ "CREATE INDEX IF NOT EXISTS idx_picux_evidence_payload_hash ON picux_evidence_artifacts (payload_hash)",
411
+ """
412
+ CREATE TABLE IF NOT EXISTS picux_escrows (
413
+ escrow_id TEXT PRIMARY KEY,
414
+ status TEXT NOT NULL DEFAULT 'pending',
415
+ mandate_id TEXT NOT NULL DEFAULT '',
416
+ task_id TEXT NOT NULL DEFAULT '',
417
+ amount NUMERIC(12,2) NOT NULL DEFAULT 0,
418
+ currency TEXT NOT NULL DEFAULT 'USD',
419
+ pov_ref TEXT NOT NULL DEFAULT '',
420
+ receipt_id TEXT NOT NULL DEFAULT '',
421
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
422
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
423
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
424
+ )
425
+ """,
426
+ "CREATE INDEX IF NOT EXISTS idx_picux_escrows_status ON picux_escrows (status)",
427
+ "CREATE INDEX IF NOT EXISTS idx_picux_escrows_mandate_task ON picux_escrows (mandate_id, task_id)",
428
+ """
429
+ CREATE TABLE IF NOT EXISTS picux_proxy_missions (
430
+ proxy_id TEXT PRIMARY KEY,
431
+ status TEXT NOT NULL DEFAULT 'awaitingProxy',
432
+ kind TEXT NOT NULL DEFAULT 'custom',
433
+ task_id TEXT NOT NULL DEFAULT '',
434
+ user_id TEXT NOT NULL DEFAULT '',
435
+ resolve_id TEXT NOT NULL DEFAULT '',
436
+ refs JSONB NOT NULL DEFAULT '{}'::jsonb,
437
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
438
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
439
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
440
+ )
441
+ """,
442
+ "CREATE INDEX IF NOT EXISTS idx_picux_proxy_missions_status ON picux_proxy_missions (status)",
443
+ "CREATE INDEX IF NOT EXISTS idx_picux_proxy_missions_task_status ON picux_proxy_missions (task_id, status)",
444
+ "CREATE INDEX IF NOT EXISTS idx_picux_proxy_missions_refs ON picux_proxy_missions USING GIN (refs)",
445
+ """
446
+ CREATE TABLE IF NOT EXISTS picux_receipts (
447
+ receipt_id TEXT PRIMARY KEY,
448
+ task_id TEXT NOT NULL DEFAULT '',
449
+ mandate_id TEXT NOT NULL DEFAULT '',
450
+ status TEXT NOT NULL,
451
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
452
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
453
+ )
454
+ """,
455
+ """
456
+ CREATE TABLE IF NOT EXISTS picux_schema_migrations (
457
+ version TEXT PRIMARY KEY,
458
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
459
+ )
460
+ """,
461
+ ]
462
+
463
+ def ensureSchema(self) -> None:
464
+ if not self.enabled:
465
+ return
466
+ with self.conn() as db:
467
+ with db.cursor() as cur:
468
+ for stmt in self.ddl():
469
+ cur.execute(stmt)
470
+ cur.execute(
471
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
472
+ ("2026-05-09-001",),
473
+ )
474
+ cur.execute(
475
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
476
+ ("2026-05-10-001",),
477
+ )
478
+ cur.execute(
479
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
480
+ ("2026-05-10-002",),
481
+ )
482
+ cur.execute(
483
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
484
+ ("2026-05-10-003",),
485
+ )
486
+ cur.execute(
487
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
488
+ ("2026-05-10-004",),
489
+ )
490
+ cur.execute(
491
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
492
+ ("2026-05-10-005",),
493
+ )
494
+ cur.execute(
495
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
496
+ ("2026-05-10-006",),
497
+ )
498
+ cur.execute(
499
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
500
+ ("2026-05-10-007",),
501
+ )
502
+ cur.execute(
503
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
504
+ ("2026-05-10-008",),
505
+ )
506
+ cur.execute(
507
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
508
+ ("2026-05-10-009",),
509
+ )
510
+ cur.execute(
511
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
512
+ ("2026-05-10-010",),
513
+ )
514
+ cur.execute(
515
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
516
+ ("2026-05-10-011",),
517
+ )
518
+ cur.execute(
519
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
520
+ ("2026-05-10-012",),
521
+ )
522
+ cur.execute(
523
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
524
+ ("2026-05-10-013",),
525
+ )
526
+ cur.execute(
527
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
528
+ ("2026-05-14-001",),
529
+ )
530
+ cur.execute(
531
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
532
+ ("2026-05-14-002",),
533
+ )
534
+ cur.execute(
535
+ "INSERT INTO picux_schema_migrations (version) VALUES (%s) ON CONFLICT (version) DO NOTHING",
536
+ ("2026-05-14-003",),
537
+ )
538
+
539
+ def upsertUser(self, user: ProtocolUser) -> None:
540
+ if not self.enabled:
541
+ return
542
+ sql = (
543
+ "INSERT INTO picux_users (user_id, channel, trust, refs, prefs, meta, created_at, updated_at) "
544
+ "VALUES (%s, %s, %s, %s::jsonb, %s::jsonb, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
545
+ "ON CONFLICT (user_id) DO UPDATE SET "
546
+ "channel = EXCLUDED.channel, trust = EXCLUDED.trust, refs = EXCLUDED.refs, "
547
+ "prefs = EXCLUDED.prefs, meta = EXCLUDED.meta, updated_at = EXCLUDED.updated_at"
548
+ )
549
+ with self.conn() as db:
550
+ with db.cursor() as cur:
551
+ cur.execute(
552
+ sql,
553
+ (
554
+ user.userId,
555
+ user.channel,
556
+ user.trust,
557
+ json.dumps(user.refs, ensure_ascii=True),
558
+ json.dumps(user.prefs, ensure_ascii=True),
559
+ json.dumps(user.meta, ensure_ascii=True),
560
+ int(user.createdAt or 0),
561
+ int(user.updatedAt or 0),
562
+ ),
563
+ )
564
+
565
+ def upsertTask(self, task: ProtocolTask) -> None:
566
+ if not self.enabled:
567
+ return
568
+ sql = (
569
+ "INSERT INTO picux_tasks "
570
+ "(task_id, user_id, domain, status, channel, mandate_id, in_data, out_data, needs_approval, "
571
+ "ext_ref, error_msg, meta, created_at, updated_at, approval_by) "
572
+ "VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, %s, %s, %s, %s::jsonb, "
573
+ "TO_TIMESTAMP(%s), TO_TIMESTAMP(%s), %s) "
574
+ "ON CONFLICT (task_id) DO UPDATE SET "
575
+ "user_id = EXCLUDED.user_id, domain = EXCLUDED.domain, status = EXCLUDED.status, "
576
+ "channel = EXCLUDED.channel, mandate_id = EXCLUDED.mandate_id, in_data = EXCLUDED.in_data, "
577
+ "out_data = EXCLUDED.out_data, needs_approval = EXCLUDED.needs_approval, ext_ref = EXCLUDED.ext_ref, "
578
+ "error_msg = EXCLUDED.error_msg, meta = EXCLUDED.meta, updated_at = EXCLUDED.updated_at, "
579
+ "approval_by = EXCLUDED.approval_by"
580
+ )
581
+ with self.conn() as db:
582
+ with db.cursor() as cur:
583
+ cur.execute(
584
+ sql,
585
+ (
586
+ task.taskId,
587
+ task.userId,
588
+ task.domain.value,
589
+ task.status.value,
590
+ task.channel,
591
+ task.mandateId,
592
+ json.dumps(task.inData, ensure_ascii=True),
593
+ json.dumps(task.outData, ensure_ascii=True),
594
+ task.needsApproval,
595
+ task.extRef,
596
+ task.errorMsg,
597
+ json.dumps(task.meta, ensure_ascii=True),
598
+ int(task.createdAt or 0),
599
+ int(task.updatedAt or 0),
600
+ task.approvalBy,
601
+ ),
602
+ )
603
+
604
+ def upsertAgent(self, agent: dict[str, Any]) -> None:
605
+ if not self.enabled:
606
+ return
607
+ agentId = str(agent.get("agentId", "") or "")
608
+ if not agentId:
609
+ return
610
+ domains = agent.get("domains", []) if isinstance(agent.get("domains"), list) else []
611
+ caps = agent.get("caps", []) if isinstance(agent.get("caps"), list) else []
612
+ meta = agent.get("meta", {}) if isinstance(agent.get("meta"), dict) else {}
613
+ sql = (
614
+ "INSERT INTO picux_agents "
615
+ "(agent_id, name, domains, caps, endpoint, trust, status, meta, payload, updated_at) "
616
+ "VALUES (%s, %s, %s::jsonb, %s::jsonb, %s, %s, %s, %s::jsonb, %s::jsonb, NOW()) "
617
+ "ON CONFLICT (agent_id) DO UPDATE SET "
618
+ "name = EXCLUDED.name, domains = EXCLUDED.domains, caps = EXCLUDED.caps, "
619
+ "endpoint = EXCLUDED.endpoint, trust = EXCLUDED.trust, status = EXCLUDED.status, "
620
+ "meta = EXCLUDED.meta, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
621
+ )
622
+ with self.conn() as db:
623
+ with db.cursor() as cur:
624
+ cur.execute(
625
+ sql,
626
+ (
627
+ agentId,
628
+ str(agent.get("name", agentId) or agentId),
629
+ json.dumps(domains, ensure_ascii=True),
630
+ json.dumps(caps, ensure_ascii=True),
631
+ str(agent.get("endpoint", "") or ""),
632
+ int(agent.get("trust", 1) or 1),
633
+ str(agent.get("status", "active") or "active"),
634
+ json.dumps(meta, ensure_ascii=True),
635
+ json.dumps(agent or {}, ensure_ascii=True),
636
+ ),
637
+ )
638
+
639
+ def fetchAgent(self, agentId: str) -> dict[str, Any] | None:
640
+ if not self.enabled:
641
+ return None
642
+ with self.conn() as db:
643
+ with db.cursor() as cur:
644
+ cur.execute("SELECT payload::text FROM picux_agents WHERE agent_id = %s", (agentId,))
645
+ row = cur.fetchone()
646
+ if not row:
647
+ return None
648
+ return json.loads(row[0] or "{}")
649
+
650
+ def listAgents(self) -> list[dict[str, Any]]:
651
+ if not self.enabled:
652
+ return []
653
+ with self.conn() as db:
654
+ with db.cursor() as cur:
655
+ cur.execute("SELECT payload::text FROM picux_agents ORDER BY agent_id")
656
+ rows = cur.fetchall()
657
+ return [json.loads(row[0] or "{}") for row in rows]
658
+
659
+ def upsertEnvelope(self, record: dict[str, Any]) -> None:
660
+ if not self.enabled:
661
+ return
662
+ msgId = str(record.get("msgId", "") or "")
663
+ if not msgId:
664
+ return
665
+ sql = (
666
+ "INSERT INTO picux_a2a_envelopes "
667
+ "(msg_id, status, from_agent, to_agent, trace_id, task_id, payload, created_at, updated_at) "
668
+ "VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
669
+ "ON CONFLICT (msg_id) DO UPDATE SET "
670
+ "status = EXCLUDED.status, from_agent = EXCLUDED.from_agent, to_agent = EXCLUDED.to_agent, "
671
+ "trace_id = EXCLUDED.trace_id, task_id = EXCLUDED.task_id, payload = EXCLUDED.payload, "
672
+ "updated_at = EXCLUDED.updated_at"
673
+ )
674
+ with self.conn() as db:
675
+ with db.cursor() as cur:
676
+ cur.execute(
677
+ sql,
678
+ (
679
+ msgId,
680
+ str(record.get("status", "queued") or "queued"),
681
+ str(record.get("fromAgent", "") or ""),
682
+ str(record.get("toAgent", "") or ""),
683
+ str(record.get("traceId", "") or ""),
684
+ str(record.get("taskId", "") or ""),
685
+ json.dumps(record or {}, ensure_ascii=True),
686
+ int(record.get("createdAt", 0) or 0),
687
+ int(record.get("updatedAt", 0) or 0),
688
+ ),
689
+ )
690
+
691
+ def fetchEnvelope(self, msgId: str) -> dict[str, Any] | None:
692
+ if not self.enabled:
693
+ return None
694
+ with self.conn() as db:
695
+ with db.cursor() as cur:
696
+ cur.execute("SELECT payload::text FROM picux_a2a_envelopes WHERE msg_id = %s", (msgId,))
697
+ row = cur.fetchone()
698
+ if not row:
699
+ return None
700
+ return json.loads(row[0] or "{}")
701
+
702
+ def listEnvelopes(self, *, limit: int = 500) -> list[dict[str, Any]]:
703
+ if not self.enabled:
704
+ return []
705
+ with self.conn() as db:
706
+ with db.cursor() as cur:
707
+ cur.execute(
708
+ "SELECT payload::text FROM picux_a2a_envelopes ORDER BY created_at DESC LIMIT %s",
709
+ (max(1, min(int(limit or 500), 500)),),
710
+ )
711
+ rows = cur.fetchall()
712
+ return [json.loads(row[0] or "{}") for row in rows]
713
+
714
+ def upsertMandate(self, record: dict[str, Any]) -> None:
715
+ if not self.enabled:
716
+ return
717
+ mandateId = str(record.get("mandateId", "") or "")
718
+ if not mandateId:
719
+ return
720
+ mandate = record.get("mandate", {}) if isinstance(record.get("mandate"), dict) else {}
721
+ issuer = mandate.get("issuer", {}) if isinstance(mandate.get("issuer"), dict) else {}
722
+ validation = record.get("validation", {}) if isinstance(record.get("validation"), dict) else {}
723
+ sql = (
724
+ "INSERT INTO picux_mandates "
725
+ "(mandate_id, status, issuer_id, mandate, validation, payload, created_at, updated_at) "
726
+ "VALUES (%s, %s, %s, %s::jsonb, %s::jsonb, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
727
+ "ON CONFLICT (mandate_id) DO UPDATE SET "
728
+ "status = EXCLUDED.status, issuer_id = EXCLUDED.issuer_id, mandate = EXCLUDED.mandate, "
729
+ "validation = EXCLUDED.validation, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
730
+ )
731
+ with self.conn() as db:
732
+ with db.cursor() as cur:
733
+ cur.execute(
734
+ sql,
735
+ (
736
+ mandateId,
737
+ str(record.get("status", "pendingVerification") or "pendingVerification"),
738
+ str(issuer.get("entityId", "") or ""),
739
+ json.dumps(mandate, ensure_ascii=True),
740
+ json.dumps(validation, ensure_ascii=True),
741
+ json.dumps(record or {}, ensure_ascii=True),
742
+ int(record.get("createdAt", 0) or 0),
743
+ int(record.get("updatedAt", 0) or 0),
744
+ ),
745
+ )
746
+
747
+ def fetchMandate(self, mandateId: str) -> dict[str, Any] | None:
748
+ if not self.enabled:
749
+ return None
750
+ with self.conn() as db:
751
+ with db.cursor() as cur:
752
+ cur.execute("SELECT payload::text FROM picux_mandates WHERE mandate_id = %s", (mandateId,))
753
+ row = cur.fetchone()
754
+ if not row:
755
+ return None
756
+ return json.loads(row[0] or "{}")
757
+
758
+ def listMandates(self, *, limit: int = 500) -> list[dict[str, Any]]:
759
+ if not self.enabled:
760
+ return []
761
+ with self.conn() as db:
762
+ with db.cursor() as cur:
763
+ cur.execute(
764
+ "SELECT payload::text FROM picux_mandates ORDER BY created_at DESC LIMIT %s",
765
+ (max(1, min(int(limit or 500), 500)),),
766
+ )
767
+ rows = cur.fetchall()
768
+ return [json.loads(row[0] or "{}") for row in rows]
769
+
770
+ def upsertEscrow(self, record: dict[str, Any]) -> None:
771
+ if not self.enabled:
772
+ return
773
+ escrowId = str(record.get("escrowId", "") or "")
774
+ if not escrowId:
775
+ return
776
+ amount = record.get("amount", {}) if isinstance(record.get("amount"), dict) else {}
777
+ sql = (
778
+ "INSERT INTO picux_escrows "
779
+ "(escrow_id, status, mandate_id, task_id, amount, currency, pov_ref, receipt_id, payload, created_at, updated_at) "
780
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
781
+ "ON CONFLICT (escrow_id) DO UPDATE SET "
782
+ "status = EXCLUDED.status, mandate_id = EXCLUDED.mandate_id, task_id = EXCLUDED.task_id, "
783
+ "amount = EXCLUDED.amount, currency = EXCLUDED.currency, pov_ref = EXCLUDED.pov_ref, "
784
+ "receipt_id = EXCLUDED.receipt_id, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
785
+ )
786
+ with self.conn() as db:
787
+ with db.cursor() as cur:
788
+ cur.execute(
789
+ sql,
790
+ (
791
+ escrowId,
792
+ str(record.get("status", "pending") or "pending"),
793
+ str(record.get("mandateId", "") or ""),
794
+ str(record.get("taskId", "") or ""),
795
+ self._amount(amount.get("amount", 0.0)),
796
+ str(amount.get("currency", "USD") or "USD"),
797
+ str(record.get("povRef", "") or ""),
798
+ str(record.get("receiptId", "") or ""),
799
+ json.dumps(record or {}, ensure_ascii=True),
800
+ int(record.get("createdAt", 0) or 0),
801
+ int(record.get("updatedAt", 0) or 0),
802
+ ),
803
+ )
804
+
805
+ def fetchEscrow(self, escrowId: str) -> dict[str, Any] | None:
806
+ if not self.enabled:
807
+ return None
808
+ with self.conn() as db:
809
+ with db.cursor() as cur:
810
+ cur.execute("SELECT payload::text FROM picux_escrows WHERE escrow_id = %s", (escrowId,))
811
+ row = cur.fetchone()
812
+ if not row:
813
+ return None
814
+ return json.loads(row[0] or "{}")
815
+
816
+ def listEscrows(self, *, limit: int = 500) -> list[dict[str, Any]]:
817
+ if not self.enabled:
818
+ return []
819
+ with self.conn() as db:
820
+ with db.cursor() as cur:
821
+ cur.execute(
822
+ "SELECT payload::text FROM picux_escrows ORDER BY created_at DESC LIMIT %s",
823
+ (max(1, min(int(limit or 500), 500)),),
824
+ )
825
+ rows = cur.fetchall()
826
+ return [json.loads(row[0] or "{}") for row in rows]
827
+
828
+ def upsertProxyMission(self, record: dict[str, Any]) -> None:
829
+ if not self.enabled:
830
+ return
831
+ proxyId = str(record.get("proxyId", "") or "")
832
+ if not proxyId:
833
+ return
834
+ refs = record.get("refs", {}) if isinstance(record.get("refs"), dict) else {}
835
+ sql = (
836
+ "INSERT INTO picux_proxy_missions "
837
+ "(proxy_id, status, kind, task_id, user_id, resolve_id, refs, payload, created_at, updated_at) "
838
+ "VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
839
+ "ON CONFLICT (proxy_id) DO UPDATE SET "
840
+ "status = EXCLUDED.status, kind = EXCLUDED.kind, task_id = EXCLUDED.task_id, "
841
+ "user_id = EXCLUDED.user_id, resolve_id = EXCLUDED.resolve_id, refs = EXCLUDED.refs, "
842
+ "payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
843
+ )
844
+ with self.conn() as db:
845
+ with db.cursor() as cur:
846
+ cur.execute(
847
+ sql,
848
+ (
849
+ proxyId,
850
+ str(record.get("status", "awaitingProxy") or "awaitingProxy"),
851
+ str(record.get("kind", "custom") or "custom"),
852
+ str(record.get("taskId", "") or ""),
853
+ str(record.get("userId", "") or ""),
854
+ str(record.get("resolveId", "") or ""),
855
+ json.dumps(refs, ensure_ascii=True),
856
+ json.dumps(record or {}, ensure_ascii=True),
857
+ int(record.get("createdAt", 0) or 0),
858
+ int(record.get("updatedAt", 0) or 0),
859
+ ),
860
+ )
861
+
862
+ def fetchProxyMission(self, proxyId: str) -> dict[str, Any] | None:
863
+ if not self.enabled:
864
+ return None
865
+ with self.conn() as db:
866
+ with db.cursor() as cur:
867
+ cur.execute("SELECT payload::text FROM picux_proxy_missions WHERE proxy_id = %s", (proxyId,))
868
+ row = cur.fetchone()
869
+ if not row:
870
+ return None
871
+ return json.loads(row[0] or "{}")
872
+
873
+ def fetchProxyMissionByRef(self, refName: str, refValue: str) -> dict[str, Any] | None:
874
+ if not self.enabled:
875
+ return None
876
+ refName = str(refName or "")
877
+ refValue = str(refValue or "")
878
+ if refName not in {"requestId", "providerTaskId", "externalRef", "connectorRunId"} or not refValue:
879
+ return None
880
+ with self.conn() as db:
881
+ with db.cursor() as cur:
882
+ cur.execute(
883
+ "SELECT payload::text FROM picux_proxy_missions WHERE refs @> %s::jsonb ORDER BY created_at DESC LIMIT 1",
884
+ (json.dumps({refName: refValue}, ensure_ascii=True),),
885
+ )
886
+ row = cur.fetchone()
887
+ if not row:
888
+ return None
889
+ return json.loads(row[0] or "{}")
890
+
891
+ def listProxyMissions(self, *, limit: int = 500) -> list[dict[str, Any]]:
892
+ if not self.enabled:
893
+ return []
894
+ with self.conn() as db:
895
+ with db.cursor() as cur:
896
+ cur.execute(
897
+ "SELECT payload::text FROM picux_proxy_missions ORDER BY created_at DESC LIMIT %s",
898
+ (max(1, min(int(limit or 500), 500)),),
899
+ )
900
+ rows = cur.fetchall()
901
+ return [json.loads(row[0] or "{}") for row in rows]
902
+
903
+ def fetchTask(self, taskId: str) -> ProtocolTask | None:
904
+ if not self.enabled:
905
+ return None
906
+ sql = (
907
+ "SELECT task_id, user_id, domain, status, channel, mandate_id, "
908
+ "in_data::text, out_data::text, needs_approval, ext_ref, error_msg, meta::text, "
909
+ "EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint, approval_by "
910
+ "FROM picux_tasks WHERE task_id = %s"
911
+ )
912
+ with self.conn() as db:
913
+ with db.cursor() as cur:
914
+ cur.execute(sql, (taskId,))
915
+ row = cur.fetchone()
916
+ if not row:
917
+ return None
918
+ return self._taskFromRow(row)
919
+
920
+ def listTasks(self, *, limit: int = 500) -> list[ProtocolTask]:
921
+ if not self.enabled:
922
+ return []
923
+ sql = (
924
+ "SELECT task_id, user_id, domain, status, channel, mandate_id, "
925
+ "in_data::text, out_data::text, needs_approval, ext_ref, error_msg, meta::text, "
926
+ "EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint, approval_by "
927
+ "FROM picux_tasks ORDER BY created_at DESC LIMIT %s"
928
+ )
929
+ with self.conn() as db:
930
+ with db.cursor() as cur:
931
+ cur.execute(sql, (max(1, min(int(limit or 500), 500)),))
932
+ rows = cur.fetchall()
933
+ tasks: list[ProtocolTask] = []
934
+ for row in rows:
935
+ tasks.append(self._taskFromRow(row))
936
+ return tasks
937
+
938
+ def durableWriteReadCheck(self, task: ProtocolTask) -> dict[str, Any]:
939
+ if not self.enabled:
940
+ return {"ok": False, "skipped": True, "reason": "postgresDisabled"}
941
+ self.upsertTask(task)
942
+ fetched = self.fetchTask(task.taskId)
943
+ return {
944
+ "ok": bool(fetched and fetched.taskId == task.taskId and fetched.domain == task.domain),
945
+ "skipped": False,
946
+ "taskId": task.taskId,
947
+ }
948
+
949
+ def insertActivity(self, *, taskId: str, userId: str, level: str, action: str, detail: str, meta: dict[str, Any]) -> None:
950
+ if not self.enabled:
951
+ return
952
+ sql = (
953
+ "INSERT INTO picux_activity_logs (task_id, user_id, level, action, detail, meta) "
954
+ "VALUES (%s, %s, %s, %s, %s, %s::jsonb)"
955
+ )
956
+ with self.conn() as db:
957
+ with db.cursor() as cur:
958
+ cur.execute(sql, (taskId, userId, level, action, detail, json.dumps(meta or {}, ensure_ascii=True)))
959
+
960
+ def insertEvent(self, *, userId: str, taskId: str, event: str, payload: dict[str, Any]) -> None:
961
+ if not self.enabled:
962
+ return
963
+ sql = "INSERT INTO picux_activity_events (user_id, task_id, event, payload) VALUES (%s, %s, %s, %s::jsonb)"
964
+ with self.conn() as db:
965
+ with db.cursor() as cur:
966
+ cur.execute(sql, (userId, taskId, event, json.dumps(payload or {}, ensure_ascii=True)))
967
+
968
+ def upsertIntegrationEvent(self, record: dict[str, Any]) -> None:
969
+ if not self.enabled:
970
+ return
971
+ eventId = str(record.get("eventId", "") or "")
972
+ if not eventId:
973
+ return
974
+ ackAt = int(record.get("ackAt", 0) or 0)
975
+ sql = (
976
+ "INSERT INTO picux_integration_events "
977
+ "(event_id, type, subject, source, target, status, payload, created_at, updated_at, ack_at) "
978
+ "VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s), "
979
+ "CASE WHEN %s > 0 THEN TO_TIMESTAMP(%s) ELSE NULL END) "
980
+ "ON CONFLICT (event_id) DO UPDATE SET "
981
+ "type = EXCLUDED.type, subject = EXCLUDED.subject, source = EXCLUDED.source, target = EXCLUDED.target, "
982
+ "status = EXCLUDED.status, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at, ack_at = EXCLUDED.ack_at"
983
+ )
984
+ with self.conn() as db:
985
+ with db.cursor() as cur:
986
+ cur.execute(
987
+ sql,
988
+ (
989
+ eventId,
990
+ str(record.get("type", "") or ""),
991
+ str(record.get("subject", "") or ""),
992
+ str(record.get("source", "picux") or "picux"),
993
+ str(record.get("target", "") or ""),
994
+ str(record.get("status", "queued") or "queued"),
995
+ json.dumps(record or {}, ensure_ascii=True),
996
+ int(record.get("createdAt", 0) or 0),
997
+ int(record.get("updatedAt", 0) or 0),
998
+ ackAt,
999
+ ackAt,
1000
+ ),
1001
+ )
1002
+
1003
+ def fetchIntegrationEvent(self, eventId: str) -> dict[str, Any] | None:
1004
+ if not self.enabled:
1005
+ return None
1006
+ with self.conn() as db:
1007
+ with db.cursor() as cur:
1008
+ cur.execute("SELECT payload::text FROM picux_integration_events WHERE event_id = %s", (eventId,))
1009
+ row = cur.fetchone()
1010
+ if not row:
1011
+ return None
1012
+ return json.loads(row[0] or "{}")
1013
+
1014
+ def listIntegrationEvents(self, *, limit: int = 500) -> list[dict[str, Any]]:
1015
+ if not self.enabled:
1016
+ return []
1017
+ with self.conn() as db:
1018
+ with db.cursor() as cur:
1019
+ cur.execute(
1020
+ "SELECT payload::text FROM picux_integration_events ORDER BY created_at DESC LIMIT %s",
1021
+ (max(1, min(int(limit or 500), 500)),),
1022
+ )
1023
+ rows = cur.fetchall()
1024
+ return [json.loads(row[0] or "{}") for row in rows]
1025
+
1026
+ def upsertEventSubscription(self, record: dict[str, Any]) -> None:
1027
+ if not self.enabled:
1028
+ return
1029
+ subId = str(record.get("subId", "") or "")
1030
+ if not subId:
1031
+ return
1032
+ eventTypes = record.get("eventTypes", []) if isinstance(record.get("eventTypes"), list) else []
1033
+ sql = (
1034
+ "INSERT INTO picux_event_subscriptions "
1035
+ "(sub_id, client_id, target, transport, endpoint, status, event_types, payload, created_at, updated_at) "
1036
+ "VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1037
+ "ON CONFLICT (sub_id) DO UPDATE SET "
1038
+ "client_id = EXCLUDED.client_id, target = EXCLUDED.target, transport = EXCLUDED.transport, "
1039
+ "endpoint = EXCLUDED.endpoint, status = EXCLUDED.status, event_types = EXCLUDED.event_types, "
1040
+ "payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1041
+ )
1042
+ with self.conn() as db:
1043
+ with db.cursor() as cur:
1044
+ cur.execute(
1045
+ sql,
1046
+ (
1047
+ subId,
1048
+ str(record.get("clientId", "") or ""),
1049
+ str(record.get("target", "") or ""),
1050
+ str(record.get("transport", "poll") or "poll"),
1051
+ str(record.get("endpoint", "") or ""),
1052
+ str(record.get("status", "active") or "active"),
1053
+ json.dumps(eventTypes, ensure_ascii=True),
1054
+ json.dumps(record or {}, ensure_ascii=True),
1055
+ int(record.get("createdAt", 0) or 0),
1056
+ int(record.get("updatedAt", 0) or 0),
1057
+ ),
1058
+ )
1059
+
1060
+ def fetchEventSubscription(self, subId: str) -> dict[str, Any] | None:
1061
+ if not self.enabled:
1062
+ return None
1063
+ with self.conn() as db:
1064
+ with db.cursor() as cur:
1065
+ cur.execute("SELECT payload::text FROM picux_event_subscriptions WHERE sub_id = %s", (subId,))
1066
+ row = cur.fetchone()
1067
+ if not row:
1068
+ return None
1069
+ return json.loads(row[0] or "{}")
1070
+
1071
+ def listEventSubscriptions(self, *, limit: int = 500) -> list[dict[str, Any]]:
1072
+ if not self.enabled:
1073
+ return []
1074
+ with self.conn() as db:
1075
+ with db.cursor() as cur:
1076
+ cur.execute(
1077
+ "SELECT payload::text FROM picux_event_subscriptions ORDER BY created_at DESC LIMIT %s",
1078
+ (max(1, min(int(limit or 500), 500)),),
1079
+ )
1080
+ rows = cur.fetchall()
1081
+ return [json.loads(row[0] or "{}") for row in rows]
1082
+
1083
+ def upsertEventDelivery(self, record: dict[str, Any]) -> None:
1084
+ if not self.enabled:
1085
+ return
1086
+ deliveryId = str(record.get("deliveryId", "") or "")
1087
+ if not deliveryId:
1088
+ return
1089
+ nextAttemptAt = int(record.get("nextAttemptAt", 0) or 0)
1090
+ claimedAt = int(record.get("claimedAt", 0) or 0)
1091
+ leaseExpiresAt = int(record.get("leaseExpiresAt", 0) or 0)
1092
+ deliveredAt = int(record.get("deliveredAt", 0) or 0)
1093
+ sql = (
1094
+ "INSERT INTO picux_event_deliveries "
1095
+ "(delivery_id, event_id, sub_id, client_id, transport, endpoint, status, attempt, payload, "
1096
+ "created_at, updated_at, next_attempt_at, claimed_at, lease_expires_at, delivered_at) "
1097
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s), "
1098
+ "CASE WHEN %s > 0 THEN TO_TIMESTAMP(%s) ELSE NULL END, "
1099
+ "CASE WHEN %s > 0 THEN TO_TIMESTAMP(%s) ELSE NULL END, "
1100
+ "CASE WHEN %s > 0 THEN TO_TIMESTAMP(%s) ELSE NULL END, "
1101
+ "CASE WHEN %s > 0 THEN TO_TIMESTAMP(%s) ELSE NULL END) "
1102
+ "ON CONFLICT (delivery_id) DO UPDATE SET "
1103
+ "event_id = EXCLUDED.event_id, sub_id = EXCLUDED.sub_id, client_id = EXCLUDED.client_id, "
1104
+ "transport = EXCLUDED.transport, endpoint = EXCLUDED.endpoint, status = EXCLUDED.status, "
1105
+ "attempt = EXCLUDED.attempt, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at, "
1106
+ "next_attempt_at = EXCLUDED.next_attempt_at, claimed_at = EXCLUDED.claimed_at, "
1107
+ "lease_expires_at = EXCLUDED.lease_expires_at, delivered_at = EXCLUDED.delivered_at"
1108
+ )
1109
+ with self.conn() as db:
1110
+ with db.cursor() as cur:
1111
+ cur.execute(
1112
+ sql,
1113
+ (
1114
+ deliveryId,
1115
+ str(record.get("eventId", "") or ""),
1116
+ str(record.get("subId", "") or ""),
1117
+ str(record.get("clientId", "") or ""),
1118
+ str(record.get("transport", "poll") or "poll"),
1119
+ str(record.get("endpoint", "") or ""),
1120
+ str(record.get("status", "queued") or "queued"),
1121
+ int(record.get("attempt", 1) or 1),
1122
+ json.dumps(record or {}, ensure_ascii=True),
1123
+ int(record.get("createdAt", 0) or 0),
1124
+ int(record.get("updatedAt", 0) or 0),
1125
+ nextAttemptAt,
1126
+ nextAttemptAt,
1127
+ claimedAt,
1128
+ claimedAt,
1129
+ leaseExpiresAt,
1130
+ leaseExpiresAt,
1131
+ deliveredAt,
1132
+ deliveredAt,
1133
+ ),
1134
+ )
1135
+
1136
+ def fetchEventDelivery(self, deliveryId: str) -> dict[str, Any] | None:
1137
+ if not self.enabled:
1138
+ return None
1139
+ with self.conn() as db:
1140
+ with db.cursor() as cur:
1141
+ cur.execute("SELECT payload::text FROM picux_event_deliveries WHERE delivery_id = %s", (deliveryId,))
1142
+ row = cur.fetchone()
1143
+ if not row:
1144
+ return None
1145
+ return json.loads(row[0] or "{}")
1146
+
1147
+ def listEventDeliveries(self, *, limit: int = 500) -> list[dict[str, Any]]:
1148
+ if not self.enabled:
1149
+ return []
1150
+ with self.conn() as db:
1151
+ with db.cursor() as cur:
1152
+ cur.execute(
1153
+ "SELECT payload::text FROM picux_event_deliveries ORDER BY created_at DESC LIMIT %s",
1154
+ (max(1, min(int(limit or 500), 500)),),
1155
+ )
1156
+ rows = cur.fetchall()
1157
+ return [json.loads(row[0] or "{}") for row in rows]
1158
+
1159
+ def upsertConnector(self, record: dict[str, Any]) -> None:
1160
+ if not self.enabled:
1161
+ return
1162
+ connectorId = str(record.get("connectorId", "") or "")
1163
+ if not connectorId:
1164
+ return
1165
+ sql = (
1166
+ "INSERT INTO picux_bridge_connectors "
1167
+ "(connector_id, status, domain, kind, endpoint, payload, created_at, updated_at) "
1168
+ "VALUES (%s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1169
+ "ON CONFLICT (connector_id) DO UPDATE SET "
1170
+ "status = EXCLUDED.status, domain = EXCLUDED.domain, kind = EXCLUDED.kind, "
1171
+ "endpoint = EXCLUDED.endpoint, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1172
+ )
1173
+ with self.conn() as db:
1174
+ with db.cursor() as cur:
1175
+ cur.execute(
1176
+ sql,
1177
+ (
1178
+ connectorId,
1179
+ str(record.get("status", "active") or "active"),
1180
+ str(record.get("domain", "bridge") or "bridge"),
1181
+ str(record.get("kind", "api") or "api"),
1182
+ str(record.get("endpoint", "") or ""),
1183
+ json.dumps(record or {}, ensure_ascii=True),
1184
+ int(record.get("createdAt", 0) or 0),
1185
+ int(record.get("updatedAt", 0) or 0),
1186
+ ),
1187
+ )
1188
+
1189
+ def fetchConnector(self, connectorId: str) -> dict[str, Any] | None:
1190
+ return self._fetchJsonPayload("picux_bridge_connectors", "connector_id", connectorId)
1191
+
1192
+ def listConnectors(self, *, limit: int = 500) -> list[dict[str, Any]]:
1193
+ return self._listJsonPayloads("picux_bridge_connectors", "created_at", limit=limit)
1194
+
1195
+ def upsertConnectionSession(self, record: dict[str, Any]) -> None:
1196
+ if not self.enabled:
1197
+ return
1198
+ sessionId = str(record.get("sessionId", "") or "")
1199
+ if not sessionId:
1200
+ return
1201
+ sql = (
1202
+ "INSERT INTO picux_bridge_connection_sessions "
1203
+ "(session_id, connector_id, status, subject_id, payload, created_at, updated_at) "
1204
+ "VALUES (%s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1205
+ "ON CONFLICT (session_id) DO UPDATE SET "
1206
+ "connector_id = EXCLUDED.connector_id, status = EXCLUDED.status, subject_id = EXCLUDED.subject_id, "
1207
+ "payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1208
+ )
1209
+ with self.conn() as db:
1210
+ with db.cursor() as cur:
1211
+ cur.execute(
1212
+ sql,
1213
+ (
1214
+ sessionId,
1215
+ str(record.get("connectorId", "") or ""),
1216
+ str(record.get("status", "active") or "active"),
1217
+ str(record.get("subjectId", "") or ""),
1218
+ json.dumps(record or {}, ensure_ascii=True),
1219
+ int(record.get("createdAt", 0) or 0),
1220
+ int(record.get("updatedAt", 0) or 0),
1221
+ ),
1222
+ )
1223
+
1224
+ def fetchConnectionSession(self, sessionId: str) -> dict[str, Any] | None:
1225
+ return self._fetchJsonPayload("picux_bridge_connection_sessions", "session_id", sessionId)
1226
+
1227
+ def listConnectionSessions(self, *, limit: int = 500) -> list[dict[str, Any]]:
1228
+ return self._listJsonPayloads("picux_bridge_connection_sessions", "created_at", limit=limit)
1229
+
1230
+ def upsertChannelThread(self, record: dict[str, Any]) -> None:
1231
+ if not self.enabled:
1232
+ return
1233
+ threadId = str(record.get("threadId", "") or "")
1234
+ if not threadId:
1235
+ return
1236
+ sql = (
1237
+ "INSERT INTO picux_channel_threads "
1238
+ "(thread_id, case_id, conversation_id, channel, provider, provider_thread_id, connector_id, session_id, status, payload, created_at, updated_at) "
1239
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1240
+ "ON CONFLICT (thread_id) DO UPDATE SET "
1241
+ "case_id = EXCLUDED.case_id, conversation_id = EXCLUDED.conversation_id, channel = EXCLUDED.channel, "
1242
+ "provider = EXCLUDED.provider, provider_thread_id = EXCLUDED.provider_thread_id, connector_id = EXCLUDED.connector_id, "
1243
+ "session_id = EXCLUDED.session_id, status = EXCLUDED.status, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1244
+ )
1245
+ with self.conn() as db:
1246
+ with db.cursor() as cur:
1247
+ cur.execute(
1248
+ sql,
1249
+ (
1250
+ threadId,
1251
+ str(record.get("caseId", "") or ""),
1252
+ str(record.get("conversationId", "") or ""),
1253
+ str(record.get("channel", "api") or "api"),
1254
+ str(record.get("provider", "") or ""),
1255
+ str(record.get("providerThreadId", "") or ""),
1256
+ str(record.get("connectorId", "") or ""),
1257
+ str(record.get("sessionId", "") or ""),
1258
+ str(record.get("status", "active") or "active"),
1259
+ json.dumps(record or {}, ensure_ascii=True),
1260
+ int(record.get("createdAt", 0) or 0),
1261
+ int(record.get("updatedAt", 0) or 0),
1262
+ ),
1263
+ )
1264
+
1265
+ def fetchChannelThread(self, threadId: str) -> dict[str, Any] | None:
1266
+ return self._fetchJsonPayload("picux_channel_threads", "thread_id", threadId)
1267
+
1268
+ def findChannelThread(self, refs: dict[str, Any]) -> dict[str, Any] | None:
1269
+ if not self.enabled:
1270
+ return None
1271
+ providerThreadId = str(refs.get("providerThreadId", "") or "")
1272
+ conversationId = str(refs.get("conversationId", "") or "")
1273
+ caseId = str(refs.get("caseId", "") or "")
1274
+ channel = str(refs.get("channel", "") or "")
1275
+ provider = str(refs.get("provider", "") or "")
1276
+ where = []
1277
+ params: list[Any] = []
1278
+ if providerThreadId:
1279
+ where.append("provider_thread_id = %s")
1280
+ params.append(providerThreadId)
1281
+ elif conversationId:
1282
+ where.append("conversation_id = %s")
1283
+ params.append(conversationId)
1284
+ if channel:
1285
+ where.append("channel = %s")
1286
+ params.append(channel)
1287
+ if provider:
1288
+ where.append("provider = %s")
1289
+ params.append(provider)
1290
+ elif caseId and channel:
1291
+ where.extend(["case_id = %s", "channel = %s"])
1292
+ params.extend([caseId, channel])
1293
+ if not where:
1294
+ return None
1295
+ with self.conn() as db:
1296
+ with db.cursor() as cur:
1297
+ cur.execute(
1298
+ f"SELECT payload::text FROM picux_channel_threads WHERE {' AND '.join(where)} ORDER BY updated_at DESC LIMIT 1",
1299
+ tuple(params),
1300
+ )
1301
+ row = cur.fetchone()
1302
+ return json.loads(row[0] or "{}") if row else None
1303
+
1304
+ def listChannelThreads(self, *, limit: int = 500) -> list[dict[str, Any]]:
1305
+ return self._listJsonPayloads("picux_channel_threads", "updated_at", limit=limit)
1306
+
1307
+ def upsertTouchpoint(self, record: dict[str, Any]) -> None:
1308
+ if not self.enabled:
1309
+ return
1310
+ touchpointId = str(record.get("touchpointId", "") or "")
1311
+ if not touchpointId:
1312
+ return
1313
+ sql = (
1314
+ "INSERT INTO picux_touchpoints "
1315
+ "(touchpoint_id, thread_id, case_id, conversation_id, channel, provider, connector_id, direction, status, payload, created_at) "
1316
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s)) "
1317
+ "ON CONFLICT (touchpoint_id) DO UPDATE SET "
1318
+ "thread_id = EXCLUDED.thread_id, case_id = EXCLUDED.case_id, conversation_id = EXCLUDED.conversation_id, "
1319
+ "channel = EXCLUDED.channel, provider = EXCLUDED.provider, connector_id = EXCLUDED.connector_id, "
1320
+ "direction = EXCLUDED.direction, status = EXCLUDED.status, payload = EXCLUDED.payload"
1321
+ )
1322
+ with self.conn() as db:
1323
+ with db.cursor() as cur:
1324
+ cur.execute(
1325
+ sql,
1326
+ (
1327
+ touchpointId,
1328
+ str(record.get("threadId", "") or ""),
1329
+ str(record.get("caseId", "") or ""),
1330
+ str(record.get("conversationId", "") or ""),
1331
+ str(record.get("channel", "api") or "api"),
1332
+ str(record.get("provider", "") or ""),
1333
+ str(record.get("connectorId", "") or ""),
1334
+ str(record.get("direction", "inbound") or "inbound"),
1335
+ str(record.get("status", "received") or "received"),
1336
+ json.dumps(record or {}, ensure_ascii=True),
1337
+ int(record.get("createdAt", 0) or 0),
1338
+ ),
1339
+ )
1340
+
1341
+ def fetchTouchpoint(self, touchpointId: str) -> dict[str, Any] | None:
1342
+ return self._fetchJsonPayload("picux_touchpoints", "touchpoint_id", touchpointId)
1343
+
1344
+ def listTouchpoints(self, *, threadId: str = "", caseId: str = "", conversationId: str = "", limit: int = 500) -> list[dict[str, Any]]:
1345
+ if not self.enabled:
1346
+ return []
1347
+ where = []
1348
+ params: list[Any] = []
1349
+ for value, column in ((threadId, "thread_id"), (caseId, "case_id"), (conversationId, "conversation_id")):
1350
+ clean = str(value or "")
1351
+ if clean:
1352
+ where.append(f"{column} = %s")
1353
+ params.append(clean)
1354
+ whereSql = f"WHERE {' AND '.join(where)} " if where else ""
1355
+ with self.conn() as db:
1356
+ with db.cursor() as cur:
1357
+ cur.execute(
1358
+ f"SELECT payload::text FROM picux_touchpoints {whereSql}ORDER BY created_at DESC LIMIT %s",
1359
+ (*params, max(1, min(int(limit or 500), 500))),
1360
+ )
1361
+ rows = cur.fetchall()
1362
+ return [json.loads(row[0] or "{}") for row in rows]
1363
+
1364
+ def upsertProviderAction(self, record: dict[str, Any]) -> None:
1365
+ if not self.enabled:
1366
+ return
1367
+ actionId = str(record.get("actionId", "") or "")
1368
+ if not actionId:
1369
+ return
1370
+ sql = (
1371
+ "INSERT INTO picux_provider_actions "
1372
+ "(action_id, case_id, conversation_id, thread_id, connector_id, session_id, status, action, resource, payload, created_at, updated_at) "
1373
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1374
+ "ON CONFLICT (action_id) DO UPDATE SET "
1375
+ "case_id = EXCLUDED.case_id, conversation_id = EXCLUDED.conversation_id, thread_id = EXCLUDED.thread_id, "
1376
+ "connector_id = EXCLUDED.connector_id, session_id = EXCLUDED.session_id, status = EXCLUDED.status, "
1377
+ "action = EXCLUDED.action, resource = EXCLUDED.resource, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1378
+ )
1379
+ with self.conn() as db:
1380
+ with db.cursor() as cur:
1381
+ cur.execute(
1382
+ sql,
1383
+ (
1384
+ actionId,
1385
+ str(record.get("caseId", "") or ""),
1386
+ str(record.get("conversationId", "") or ""),
1387
+ str(record.get("threadId", "") or ""),
1388
+ str(record.get("connectorId", "") or ""),
1389
+ str(record.get("sessionId", "") or ""),
1390
+ str(record.get("status", "queued") or "queued"),
1391
+ str(record.get("action", "") or ""),
1392
+ str(record.get("resource", "") or ""),
1393
+ json.dumps(record or {}, ensure_ascii=True),
1394
+ int(record.get("createdAt", 0) or 0),
1395
+ int(record.get("updatedAt", 0) or 0),
1396
+ ),
1397
+ )
1398
+
1399
+ def fetchProviderAction(self, actionId: str) -> dict[str, Any] | None:
1400
+ return self._fetchJsonPayload("picux_provider_actions", "action_id", actionId)
1401
+
1402
+ def listProviderActions(self, *, limit: int = 500) -> list[dict[str, Any]]:
1403
+ return self._listJsonPayloads("picux_provider_actions", "updated_at", limit=limit)
1404
+
1405
+ def upsertProviderCallback(self, record: dict[str, Any]) -> None:
1406
+ if not self.enabled:
1407
+ return
1408
+ callbackId = str(record.get("callbackId", "") or "")
1409
+ if not callbackId:
1410
+ return
1411
+ sql = (
1412
+ "INSERT INTO picux_provider_callbacks "
1413
+ "(callback_id, case_id, conversation_id, thread_id, connector_id, session_id, provider, provider_event_id, status, payload, received_at) "
1414
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s)) "
1415
+ "ON CONFLICT (callback_id) DO UPDATE SET "
1416
+ "case_id = EXCLUDED.case_id, conversation_id = EXCLUDED.conversation_id, thread_id = EXCLUDED.thread_id, "
1417
+ "connector_id = EXCLUDED.connector_id, session_id = EXCLUDED.session_id, provider = EXCLUDED.provider, "
1418
+ "provider_event_id = EXCLUDED.provider_event_id, status = EXCLUDED.status, payload = EXCLUDED.payload"
1419
+ )
1420
+ with self.conn() as db:
1421
+ with db.cursor() as cur:
1422
+ cur.execute(
1423
+ sql,
1424
+ (
1425
+ callbackId,
1426
+ str(record.get("caseId", "") or ""),
1427
+ str(record.get("conversationId", "") or ""),
1428
+ str(record.get("threadId", "") or ""),
1429
+ str(record.get("connectorId", "") or ""),
1430
+ str(record.get("sessionId", "") or ""),
1431
+ str(record.get("provider", "") or ""),
1432
+ str(record.get("providerEventId", "") or ""),
1433
+ str(record.get("status", "received") or "received"),
1434
+ json.dumps(record or {}, ensure_ascii=True),
1435
+ int(record.get("receivedAt", 0) or 0),
1436
+ ),
1437
+ )
1438
+
1439
+ def listProviderCallbacks(self, *, limit: int = 500) -> list[dict[str, Any]]:
1440
+ return self._listJsonPayloads("picux_provider_callbacks", "received_at", limit=limit)
1441
+
1442
+ def upsertCaseWorkspace(self, record: dict[str, Any]) -> None:
1443
+ if not self.enabled:
1444
+ return
1445
+ caseId = str(record.get("caseId", "") or "")
1446
+ if not caseId:
1447
+ return
1448
+ sql = (
1449
+ "INSERT INTO picux_case_workspaces "
1450
+ "(case_id, status, owner_id, conversation_id, payload, created_at, updated_at) "
1451
+ "VALUES (%s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1452
+ "ON CONFLICT (case_id) DO UPDATE SET "
1453
+ "status = EXCLUDED.status, owner_id = EXCLUDED.owner_id, conversation_id = EXCLUDED.conversation_id, "
1454
+ "payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1455
+ )
1456
+ with self.conn() as db:
1457
+ with db.cursor() as cur:
1458
+ cur.execute(
1459
+ sql,
1460
+ (
1461
+ caseId,
1462
+ str(record.get("status", "open") or "open"),
1463
+ str(record.get("ownerId", "") or ""),
1464
+ str(record.get("conversationId", "") or ""),
1465
+ json.dumps(record or {}, ensure_ascii=True),
1466
+ int(record.get("createdAt", 0) or 0),
1467
+ int(record.get("updatedAt", 0) or 0),
1468
+ ),
1469
+ )
1470
+
1471
+ def fetchCaseWorkspace(self, caseId: str) -> dict[str, Any] | None:
1472
+ return self._fetchJsonPayload("picux_case_workspaces", "case_id", caseId)
1473
+
1474
+ def listCaseWorkspaces(self, *, limit: int = 500) -> list[dict[str, Any]]:
1475
+ return self._listJsonPayloads("picux_case_workspaces", "updated_at", limit=limit)
1476
+
1477
+ def upsertCaseEvent(self, record: dict[str, Any]) -> None:
1478
+ if not self.enabled:
1479
+ return
1480
+ eventId = str(record.get("eventId", "") or "")
1481
+ if not eventId:
1482
+ return
1483
+ sql = (
1484
+ "INSERT INTO picux_case_events "
1485
+ "(event_id, case_id, type, actor, status, payload, created_at) "
1486
+ "VALUES (%s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s)) "
1487
+ "ON CONFLICT (event_id) DO UPDATE SET "
1488
+ "case_id = EXCLUDED.case_id, type = EXCLUDED.type, actor = EXCLUDED.actor, "
1489
+ "status = EXCLUDED.status, payload = EXCLUDED.payload"
1490
+ )
1491
+ with self.conn() as db:
1492
+ with db.cursor() as cur:
1493
+ cur.execute(
1494
+ sql,
1495
+ (
1496
+ eventId,
1497
+ str(record.get("caseId", "") or ""),
1498
+ str(record.get("type", "") or ""),
1499
+ str(record.get("actor", "") or ""),
1500
+ str(record.get("status", "") or ""),
1501
+ json.dumps(record or {}, ensure_ascii=True),
1502
+ int(record.get("createdAt", 0) or 0),
1503
+ ),
1504
+ )
1505
+
1506
+ def listCaseEvents(self, *, caseId: str = "", limit: int = 500) -> list[dict[str, Any]]:
1507
+ if not self.enabled:
1508
+ return []
1509
+ params: list[Any] = []
1510
+ where = ""
1511
+ if caseId:
1512
+ where = "WHERE case_id = %s "
1513
+ params.append(str(caseId))
1514
+ with self.conn() as db:
1515
+ with db.cursor() as cur:
1516
+ cur.execute(
1517
+ f"SELECT payload::text FROM picux_case_events {where}ORDER BY created_at ASC LIMIT %s",
1518
+ (*params, max(1, min(int(limit or 500), 500))),
1519
+ )
1520
+ rows = cur.fetchall()
1521
+ return [json.loads(row[0] or "{}") for row in rows]
1522
+
1523
+ def upsertPortalSession(self, record: dict[str, Any]) -> None:
1524
+ if not self.enabled:
1525
+ return
1526
+ sessionId = str(record.get("portalSessionId", "") or "")
1527
+ if not sessionId:
1528
+ return
1529
+ sql = (
1530
+ "INSERT INTO picux_portal_sessions "
1531
+ "(portal_session_id, case_id, conversation_id, connector_id, provider, status, payload, created_at, updated_at) "
1532
+ "VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1533
+ "ON CONFLICT (portal_session_id) DO UPDATE SET "
1534
+ "case_id = EXCLUDED.case_id, conversation_id = EXCLUDED.conversation_id, connector_id = EXCLUDED.connector_id, "
1535
+ "provider = EXCLUDED.provider, status = EXCLUDED.status, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1536
+ )
1537
+ with self.conn() as db:
1538
+ with db.cursor() as cur:
1539
+ cur.execute(
1540
+ sql,
1541
+ (
1542
+ sessionId,
1543
+ str(record.get("caseId", "") or ""),
1544
+ str(record.get("conversationId", "") or ""),
1545
+ str(record.get("connectorId", "") or ""),
1546
+ str(record.get("provider", "") or ""),
1547
+ str(record.get("status", "active") or "active"),
1548
+ json.dumps(record or {}, ensure_ascii=True),
1549
+ int(record.get("createdAt", 0) or 0),
1550
+ int(record.get("updatedAt", 0) or 0),
1551
+ ),
1552
+ )
1553
+
1554
+ def fetchPortalSession(self, sessionId: str) -> dict[str, Any] | None:
1555
+ return self._fetchJsonPayload("picux_portal_sessions", "portal_session_id", sessionId)
1556
+
1557
+ def listPortalSessions(self, *, limit: int = 500) -> list[dict[str, Any]]:
1558
+ return self._listJsonPayloads("picux_portal_sessions", "updated_at", limit=limit)
1559
+
1560
+ def upsertPortalAction(self, record: dict[str, Any]) -> None:
1561
+ if not self.enabled:
1562
+ return
1563
+ actionId = str(record.get("portalActionId", "") or "")
1564
+ if not actionId:
1565
+ return
1566
+ sql = (
1567
+ "INSERT INTO picux_portal_actions "
1568
+ "(portal_action_id, portal_session_id, case_id, conversation_id, connector_id, provider, status, kind, payload, created_at, updated_at) "
1569
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1570
+ "ON CONFLICT (portal_action_id) DO UPDATE SET "
1571
+ "portal_session_id = EXCLUDED.portal_session_id, case_id = EXCLUDED.case_id, conversation_id = EXCLUDED.conversation_id, "
1572
+ "connector_id = EXCLUDED.connector_id, provider = EXCLUDED.provider, status = EXCLUDED.status, kind = EXCLUDED.kind, "
1573
+ "payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1574
+ )
1575
+ with self.conn() as db:
1576
+ with db.cursor() as cur:
1577
+ cur.execute(
1578
+ sql,
1579
+ (
1580
+ actionId,
1581
+ str(record.get("portalSessionId", "") or ""),
1582
+ str(record.get("caseId", "") or ""),
1583
+ str(record.get("conversationId", "") or ""),
1584
+ str(record.get("connectorId", "") or ""),
1585
+ str(record.get("provider", "") or ""),
1586
+ str(record.get("status", "queued") or "queued"),
1587
+ str(record.get("kind", "") or ""),
1588
+ json.dumps(record or {}, ensure_ascii=True),
1589
+ int(record.get("createdAt", 0) or 0),
1590
+ int(record.get("updatedAt", 0) or 0),
1591
+ ),
1592
+ )
1593
+
1594
+ def fetchPortalAction(self, actionId: str) -> dict[str, Any] | None:
1595
+ return self._fetchJsonPayload("picux_portal_actions", "portal_action_id", actionId)
1596
+
1597
+ def listPortalActions(self, *, limit: int = 500) -> list[dict[str, Any]]:
1598
+ return self._listJsonPayloads("picux_portal_actions", "updated_at", limit=limit)
1599
+
1600
+ def claimEventDelivery(self, filters: dict[str, Any] | None = None) -> dict[str, Any] | None:
1601
+ if not self.enabled:
1602
+ return None
1603
+ filters = filters or {}
1604
+ where = [
1605
+ "((status = %s AND (next_attempt_at IS NULL OR next_attempt_at <= NOW())) "
1606
+ "OR (status = %s AND lease_expires_at IS NOT NULL AND lease_expires_at <= NOW()))"
1607
+ ]
1608
+ params: list[Any] = ["queued", "claimed"]
1609
+ for field, column in (
1610
+ ("eventId", "event_id"),
1611
+ ("subId", "sub_id"),
1612
+ ("clientId", "client_id"),
1613
+ ("transport", "transport"),
1614
+ ):
1615
+ value = str(filters.get(field, "") or "")
1616
+ if value:
1617
+ where.append(f"{column} = %s")
1618
+ params.append(value)
1619
+ whereSql = " AND ".join(where)
1620
+ claimedBy = str(filters.get("claimedBy", filters.get("workerId", "")) or "")
1621
+ now = int(datetime.now(tz=timezone.utc).timestamp())
1622
+ leaseExpiresAt = now + self._leaseSeconds(filters.get("leaseSeconds", 300))
1623
+ explicitLease = int(filters.get("leaseExpiresAt", 0) or 0)
1624
+ if explicitLease:
1625
+ leaseExpiresAt = explicitLease
1626
+ with self.conn() as db:
1627
+ with db.cursor() as cur:
1628
+ cur.execute(
1629
+ f"SELECT payload::text FROM picux_event_deliveries WHERE {whereSql} "
1630
+ "ORDER BY created_at ASC, delivery_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED",
1631
+ tuple(params),
1632
+ )
1633
+ row = cur.fetchone()
1634
+ if not row:
1635
+ return None
1636
+ record = json.loads(row[0] or "{}")
1637
+ record["status"] = "claimed"
1638
+ record["claimedBy"] = claimedBy
1639
+ record["claimedAt"] = now
1640
+ record["leaseExpiresAt"] = leaseExpiresAt
1641
+ record["nextAttemptAt"] = 0
1642
+ record["updatedAt"] = now
1643
+ cur.execute(
1644
+ "UPDATE picux_event_deliveries SET status = %s, payload = %s::jsonb, "
1645
+ "updated_at = TO_TIMESTAMP(%s), claimed_at = TO_TIMESTAMP(%s), "
1646
+ "lease_expires_at = TO_TIMESTAMP(%s), next_attempt_at = NULL WHERE delivery_id = %s",
1647
+ (
1648
+ "claimed",
1649
+ json.dumps(record, ensure_ascii=True),
1650
+ now,
1651
+ now,
1652
+ leaseExpiresAt,
1653
+ str(record.get("deliveryId", "") or ""),
1654
+ ),
1655
+ )
1656
+ return record
1657
+
1658
+ def sweepEventDeliveryLeases(self, filters: dict[str, Any] | None = None) -> list[dict[str, Any]]:
1659
+ if not self.enabled:
1660
+ return []
1661
+ filters = filters or {}
1662
+ where = ["status = %s", "lease_expires_at IS NOT NULL", "lease_expires_at <= NOW()"]
1663
+ params: list[Any] = ["claimed"]
1664
+ for field, column in (
1665
+ ("eventId", "event_id"),
1666
+ ("subId", "sub_id"),
1667
+ ("clientId", "client_id"),
1668
+ ("transport", "transport"),
1669
+ ):
1670
+ value = str(filters.get(field, "") or "")
1671
+ if value:
1672
+ where.append(f"{column} = %s")
1673
+ params.append(value)
1674
+ whereSql = " AND ".join(where)
1675
+ limit = max(1, min(int(filters.get("limit", 100) or 100), 500))
1676
+ lastError = str(filters.get("lastError", "") or "")
1677
+ now = int(datetime.now(tz=timezone.utc).timestamp())
1678
+ swept: list[dict[str, Any]] = []
1679
+ with self.conn() as db:
1680
+ with db.cursor() as cur:
1681
+ cur.execute(
1682
+ f"SELECT payload::text FROM picux_event_deliveries WHERE {whereSql} "
1683
+ "ORDER BY lease_expires_at ASC, delivery_id ASC LIMIT %s FOR UPDATE SKIP LOCKED",
1684
+ tuple(params + [limit]),
1685
+ )
1686
+ rows = cur.fetchall()
1687
+ for row in rows:
1688
+ record = json.loads(row[0] or "{}")
1689
+ record["status"] = "queued"
1690
+ record["claimedBy"] = ""
1691
+ record["claimedAt"] = 0
1692
+ record["leaseExpiresAt"] = 0
1693
+ record["updatedAt"] = now
1694
+ if lastError:
1695
+ record["lastError"] = lastError
1696
+ cur.execute(
1697
+ "UPDATE picux_event_deliveries SET status = %s, payload = %s::jsonb, "
1698
+ "updated_at = TO_TIMESTAMP(%s), claimed_at = NULL, lease_expires_at = NULL "
1699
+ "WHERE delivery_id = %s",
1700
+ (
1701
+ "queued",
1702
+ json.dumps(record, ensure_ascii=True),
1703
+ now,
1704
+ str(record.get("deliveryId", "") or ""),
1705
+ ),
1706
+ )
1707
+ swept.append(record)
1708
+ return swept
1709
+
1710
+ def insertEvidence(self, artifact: dict[str, Any]) -> None:
1711
+ if not self.enabled:
1712
+ return
1713
+ artifactId = str(artifact.get("artifactId", "") or "")
1714
+ if not artifactId:
1715
+ return
1716
+ sql = (
1717
+ "INSERT INTO picux_evidence_artifacts (artifact_id, kind, source, payload_hash, payload, created_at) "
1718
+ "VALUES (%s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s)) "
1719
+ "ON CONFLICT (artifact_id) DO UPDATE SET "
1720
+ "kind = EXCLUDED.kind, source = EXCLUDED.source, payload_hash = EXCLUDED.payload_hash, "
1721
+ "payload = EXCLUDED.payload"
1722
+ )
1723
+ with self.conn() as db:
1724
+ with db.cursor() as cur:
1725
+ cur.execute(
1726
+ sql,
1727
+ (
1728
+ artifactId,
1729
+ str(artifact.get("kind", "") or ""),
1730
+ str(artifact.get("source", "") or ""),
1731
+ str(artifact.get("payloadHash", "") or ""),
1732
+ json.dumps(artifact or {}, ensure_ascii=True),
1733
+ self._epoch(artifact.get("createdAt", "")),
1734
+ ),
1735
+ )
1736
+
1737
+ def fetchEvidence(self, artifactId: str) -> dict[str, Any] | None:
1738
+ if not self.enabled:
1739
+ return None
1740
+ with self.conn() as db:
1741
+ with db.cursor() as cur:
1742
+ cur.execute("SELECT payload::text FROM picux_evidence_artifacts WHERE artifact_id = %s", (artifactId,))
1743
+ row = cur.fetchone()
1744
+ if not row:
1745
+ return None
1746
+ return json.loads(row[0] or "{}")
1747
+
1748
+ def listEvidence(self, *, limit: int = 500) -> list[dict[str, Any]]:
1749
+ if not self.enabled:
1750
+ return []
1751
+ with self.conn() as db:
1752
+ with db.cursor() as cur:
1753
+ cur.execute(
1754
+ "SELECT payload::text FROM picux_evidence_artifacts ORDER BY created_at DESC LIMIT %s",
1755
+ (max(1, min(int(limit or 500), 500)),),
1756
+ )
1757
+ rows = cur.fetchall()
1758
+ return [json.loads(row[0] or "{}") for row in rows]
1759
+
1760
+ def upsertSignal(self, record: dict[str, Any]) -> None:
1761
+ if not self.enabled:
1762
+ return
1763
+ signalId = str(record.get("signalId", "") or "")
1764
+ if not signalId:
1765
+ return
1766
+ sql = (
1767
+ "INSERT INTO picux_community_signals "
1768
+ "(signal_id, entity, confidence, source_platform, query, payload, created_at, updated_at) "
1769
+ "VALUES (%s, %s, %s, %s, %s, %s::jsonb, TO_TIMESTAMP(%s), TO_TIMESTAMP(%s)) "
1770
+ "ON CONFLICT (signal_id) DO UPDATE SET "
1771
+ "entity = EXCLUDED.entity, confidence = EXCLUDED.confidence, source_platform = EXCLUDED.source_platform, "
1772
+ "query = EXCLUDED.query, payload = EXCLUDED.payload, updated_at = EXCLUDED.updated_at"
1773
+ )
1774
+ with self.conn() as db:
1775
+ with db.cursor() as cur:
1776
+ cur.execute(
1777
+ sql,
1778
+ (
1779
+ signalId,
1780
+ str(record.get("entity", "unknown") or "unknown"),
1781
+ self._confidence(record.get("confidence", 0.0)),
1782
+ str(record.get("sourcePlatform", "") or ""),
1783
+ str(record.get("query", "") or ""),
1784
+ json.dumps(record or {}, ensure_ascii=True),
1785
+ int(record.get("createdAt", 0) or 0),
1786
+ int(record.get("updatedAt", 0) or 0),
1787
+ ),
1788
+ )
1789
+
1790
+ def fetchSignal(self, signalId: str) -> dict[str, Any] | None:
1791
+ if not self.enabled:
1792
+ return None
1793
+ with self.conn() as db:
1794
+ with db.cursor() as cur:
1795
+ cur.execute("SELECT payload::text FROM picux_community_signals WHERE signal_id = %s", (signalId,))
1796
+ row = cur.fetchone()
1797
+ if not row:
1798
+ return None
1799
+ return json.loads(row[0] or "{}")
1800
+
1801
+ def listSignals(self, *, limit: int = 500) -> list[dict[str, Any]]:
1802
+ if not self.enabled:
1803
+ return []
1804
+ with self.conn() as db:
1805
+ with db.cursor() as cur:
1806
+ cur.execute(
1807
+ "SELECT payload::text FROM picux_community_signals ORDER BY created_at DESC LIMIT %s",
1808
+ (max(1, min(int(limit or 500), 500)),),
1809
+ )
1810
+ rows = cur.fetchall()
1811
+ return [json.loads(row[0] or "{}") for row in rows]
1812
+
1813
+ def insertReceipt(self, receipt: dict[str, Any]) -> None:
1814
+ if not self.enabled:
1815
+ return
1816
+ receiptId = str(receipt.get("receiptId", "") or "")
1817
+ if not receiptId:
1818
+ return
1819
+ sql = (
1820
+ "INSERT INTO picux_receipts (receipt_id, task_id, mandate_id, status, payload) "
1821
+ "VALUES (%s, %s, %s, %s, %s::jsonb) "
1822
+ "ON CONFLICT (receipt_id) DO UPDATE SET "
1823
+ "task_id = EXCLUDED.task_id, mandate_id = EXCLUDED.mandate_id, "
1824
+ "status = EXCLUDED.status, payload = EXCLUDED.payload"
1825
+ )
1826
+ with self.conn() as db:
1827
+ with db.cursor() as cur:
1828
+ cur.execute(
1829
+ sql,
1830
+ (
1831
+ receiptId,
1832
+ str(receipt.get("taskId", "") or ""),
1833
+ str(receipt.get("mandateId", "") or ""),
1834
+ str(receipt.get("status", "") or ""),
1835
+ json.dumps(receipt or {}, ensure_ascii=True),
1836
+ ),
1837
+ )
1838
+
1839
+ def fetchReceipt(self, receiptId: str) -> dict[str, Any] | None:
1840
+ if not self.enabled:
1841
+ return None
1842
+ with self.conn() as db:
1843
+ with db.cursor() as cur:
1844
+ cur.execute("SELECT payload::text FROM picux_receipts WHERE receipt_id = %s", (receiptId,))
1845
+ row = cur.fetchone()
1846
+ if not row:
1847
+ return None
1848
+ return json.loads(row[0] or "{}")
1849
+
1850
+ def listReceipts(self, *, limit: int = 500) -> list[dict[str, Any]]:
1851
+ if not self.enabled:
1852
+ return []
1853
+ with self.conn() as db:
1854
+ with db.cursor() as cur:
1855
+ cur.execute(
1856
+ "SELECT payload::text FROM picux_receipts ORDER BY created_at DESC LIMIT %s",
1857
+ (max(1, min(int(limit or 500), 500)),),
1858
+ )
1859
+ rows = cur.fetchall()
1860
+ return [json.loads(row[0] or "{}") for row in rows]
1861
+
1862
+ def insertPov(self, pov: dict[str, Any]) -> None:
1863
+ if not self.enabled:
1864
+ return
1865
+ povId = str(pov.get("povId", "") or "")
1866
+ if not povId:
1867
+ return
1868
+ value = pov.get("value", {}) if isinstance(pov.get("value"), dict) else {}
1869
+ meta = pov.get("meta", {}) if isinstance(pov.get("meta"), dict) else {}
1870
+ sql = (
1871
+ "INSERT INTO picux_pov_events "
1872
+ "(event_id, user_id, task_id, domain, value_amount, currency, summary, payload) "
1873
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb) "
1874
+ "ON CONFLICT (event_id) DO UPDATE SET "
1875
+ "task_id = EXCLUDED.task_id, domain = EXCLUDED.domain, value_amount = EXCLUDED.value_amount, "
1876
+ "currency = EXCLUDED.currency, summary = EXCLUDED.summary, payload = EXCLUDED.payload"
1877
+ )
1878
+ with self.conn() as db:
1879
+ with db.cursor() as cur:
1880
+ cur.execute(
1881
+ sql,
1882
+ (
1883
+ povId,
1884
+ str(pov.get("userId", meta.get("userId", "")) or ""),
1885
+ str(pov.get("taskId", "") or ""),
1886
+ str(pov.get("domain", "") or ""),
1887
+ self._amount(value.get("amount", 0.0)),
1888
+ str(value.get("currency", "USD") or "USD"),
1889
+ str(meta.get("summary", "") or ""),
1890
+ json.dumps(pov or {}, ensure_ascii=True),
1891
+ ),
1892
+ )
1893
+
1894
+ def fetchPov(self, povId: str) -> dict[str, Any] | None:
1895
+ if not self.enabled:
1896
+ return None
1897
+ with self.conn() as db:
1898
+ with db.cursor() as cur:
1899
+ cur.execute("SELECT payload::text FROM picux_pov_events WHERE event_id = %s", (povId,))
1900
+ row = cur.fetchone()
1901
+ if not row:
1902
+ return None
1903
+ return json.loads(row[0] or "{}")
1904
+
1905
+ def listPov(self, *, limit: int = 500) -> list[dict[str, Any]]:
1906
+ if not self.enabled:
1907
+ return []
1908
+ with self.conn() as db:
1909
+ with db.cursor() as cur:
1910
+ cur.execute(
1911
+ "SELECT payload::text FROM picux_pov_events ORDER BY created_at DESC LIMIT %s",
1912
+ (max(1, min(int(limit or 500), 500)),),
1913
+ )
1914
+ rows = cur.fetchall()
1915
+ return [json.loads(row[0] or "{}") for row in rows]
1916
+
1917
+ def _fetchJsonPayload(self, table: str, column: str, value: str) -> dict[str, Any] | None:
1918
+ if not self.enabled or not value:
1919
+ return None
1920
+ allowedTables = {
1921
+ "picux_bridge_connectors",
1922
+ "picux_bridge_connection_sessions",
1923
+ "picux_channel_threads",
1924
+ "picux_touchpoints",
1925
+ "picux_provider_actions",
1926
+ "picux_case_workspaces",
1927
+ "picux_portal_sessions",
1928
+ "picux_portal_actions",
1929
+ }
1930
+ allowedColumns = {"connector_id", "session_id", "thread_id", "touchpoint_id", "action_id", "case_id", "portal_session_id", "portal_action_id"}
1931
+ if table not in allowedTables or column not in allowedColumns:
1932
+ return None
1933
+ with self.conn() as db:
1934
+ with db.cursor() as cur:
1935
+ cur.execute(f"SELECT payload::text FROM {table} WHERE {column} = %s", (str(value),))
1936
+ row = cur.fetchone()
1937
+ if not row:
1938
+ return None
1939
+ return json.loads(row[0] or "{}")
1940
+
1941
+ def _listJsonPayloads(self, table: str, orderColumn: str, *, limit: int = 500) -> list[dict[str, Any]]:
1942
+ if not self.enabled:
1943
+ return []
1944
+ allowedTables = {
1945
+ "picux_bridge_connectors",
1946
+ "picux_bridge_connection_sessions",
1947
+ "picux_channel_threads",
1948
+ "picux_provider_actions",
1949
+ "picux_provider_callbacks",
1950
+ "picux_case_workspaces",
1951
+ "picux_portal_sessions",
1952
+ "picux_portal_actions",
1953
+ }
1954
+ allowedOrder = {"created_at", "updated_at", "received_at"}
1955
+ if table not in allowedTables or orderColumn not in allowedOrder:
1956
+ return []
1957
+ with self.conn() as db:
1958
+ with db.cursor() as cur:
1959
+ cur.execute(
1960
+ f"SELECT payload::text FROM {table} ORDER BY {orderColumn} DESC LIMIT %s",
1961
+ (max(1, min(int(limit or 500), 500)),),
1962
+ )
1963
+ rows = cur.fetchall()
1964
+ return [json.loads(row[0] or "{}") for row in rows]
1965
+
1966
+ @staticmethod
1967
+ def _amount(value: Any) -> float:
1968
+ try:
1969
+ return float(value or 0.0)
1970
+ except (TypeError, ValueError):
1971
+ return 0.0
1972
+
1973
+ @staticmethod
1974
+ def _confidence(value: Any) -> float:
1975
+ try:
1976
+ parsed = float(value or 0.0)
1977
+ except (TypeError, ValueError):
1978
+ return 0.0
1979
+ return max(0.0, min(parsed, 1.0))
1980
+
1981
+ @staticmethod
1982
+ def _epoch(value: Any) -> int:
1983
+ if isinstance(value, (int, float)):
1984
+ return int(value or 0)
1985
+ raw = str(value or "").strip()
1986
+ if raw.endswith("Z"):
1987
+ raw = raw[:-1] + "+00:00"
1988
+ try:
1989
+ parsed = datetime.fromisoformat(raw)
1990
+ except Exception:
1991
+ return 0
1992
+ if parsed.tzinfo is None:
1993
+ parsed = parsed.replace(tzinfo=timezone.utc)
1994
+ return int(parsed.timestamp())
1995
+
1996
+ @staticmethod
1997
+ def _taskFromRow(row: tuple[Any, ...]) -> ProtocolTask:
1998
+ domain = str(row[2])
1999
+ status = str(row[3])
2000
+ if len(row) >= 15:
2001
+ needsApproval = bool(row[8])
2002
+ extRef = str(row[9] or "")
2003
+ errorMsg = str(row[10] or "")
2004
+ meta = json.loads(row[11] or "{}")
2005
+ createdAt = int(row[12] or 0)
2006
+ updatedAt = int(row[13] or 0)
2007
+ approvalBy = int(row[14]) if row[14] else None
2008
+ else:
2009
+ needsApproval = False
2010
+ extRef = ""
2011
+ errorMsg = ""
2012
+ meta = json.loads(row[8] if isinstance(row[8], str) and row[8] else "{}")
2013
+ createdAt = PicuxPostgresStore._int(row[9])
2014
+ updatedAt = PicuxPostgresStore._int(row[10])
2015
+ approvalBy = None
2016
+ return ProtocolTask(
2017
+ taskId=str(row[0]),
2018
+ userId=str(row[1]),
2019
+ domain=Domain(domain) if domain in Domain._value2member_map_ else Domain.UNKNOWN,
2020
+ status=ProtocolTaskStatus(status)
2021
+ if status in ProtocolTaskStatus._value2member_map_
2022
+ else ProtocolTaskStatus.PENDING,
2023
+ channel=str(row[4]),
2024
+ mandateId=str(row[5]),
2025
+ inData=json.loads(row[6] or "{}"),
2026
+ outData=json.loads(row[7] or "{}"),
2027
+ needsApproval=needsApproval,
2028
+ extRef=extRef,
2029
+ errorMsg=errorMsg,
2030
+ meta=meta,
2031
+ createdAt=createdAt,
2032
+ updatedAt=updatedAt,
2033
+ approvalBy=approvalBy,
2034
+ )
2035
+
2036
+ @staticmethod
2037
+ def _int(value: Any) -> int:
2038
+ try:
2039
+ return int(value or 0)
2040
+ except (TypeError, ValueError):
2041
+ return 0
2042
+
2043
+ @staticmethod
2044
+ def _leaseSeconds(value: Any) -> int:
2045
+ try:
2046
+ parsed = int(value)
2047
+ except (TypeError, ValueError):
2048
+ return 300
2049
+ return max(1, min(parsed, 86400))