@contractspec/integration.runtime 2.10.0 → 3.0.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 (62) hide show
  1. package/dist/channel/dispatcher.d.ts +37 -0
  2. package/dist/channel/dispatcher.js +130 -0
  3. package/dist/channel/dispatcher.test.d.ts +1 -0
  4. package/dist/channel/github.d.ts +47 -0
  5. package/dist/channel/github.js +58 -0
  6. package/dist/channel/github.test.d.ts +1 -0
  7. package/dist/channel/index.d.ts +14 -0
  8. package/dist/channel/index.js +1420 -0
  9. package/dist/channel/memory-store.d.ts +28 -0
  10. package/dist/channel/memory-store.js +223 -0
  11. package/dist/channel/policy.d.ts +19 -0
  12. package/dist/channel/policy.js +110 -0
  13. package/dist/channel/policy.test.d.ts +1 -0
  14. package/dist/channel/postgres-queries.d.ts +11 -0
  15. package/dist/channel/postgres-queries.js +222 -0
  16. package/dist/channel/postgres-schema.d.ts +1 -0
  17. package/dist/channel/postgres-schema.js +94 -0
  18. package/dist/channel/postgres-store.d.ts +21 -0
  19. package/dist/channel/postgres-store.js +498 -0
  20. package/dist/channel/postgres-store.test.d.ts +1 -0
  21. package/dist/channel/replay-fixtures.d.ts +8 -0
  22. package/dist/channel/replay-fixtures.js +31 -0
  23. package/dist/channel/replay.test.d.ts +1 -0
  24. package/dist/channel/service.d.ts +26 -0
  25. package/dist/channel/service.js +287 -0
  26. package/dist/channel/service.test.d.ts +1 -0
  27. package/dist/channel/slack.d.ts +42 -0
  28. package/dist/channel/slack.js +82 -0
  29. package/dist/channel/slack.test.d.ts +1 -0
  30. package/dist/channel/store.d.ts +83 -0
  31. package/dist/channel/store.js +1 -0
  32. package/dist/channel/telemetry.d.ts +17 -0
  33. package/dist/channel/telemetry.js +1 -0
  34. package/dist/channel/types.d.ts +111 -0
  35. package/dist/channel/types.js +1 -0
  36. package/dist/channel/whatsapp-meta.d.ts +55 -0
  37. package/dist/channel/whatsapp-meta.js +66 -0
  38. package/dist/channel/whatsapp-meta.test.d.ts +1 -0
  39. package/dist/channel/whatsapp-twilio.d.ts +20 -0
  40. package/dist/channel/whatsapp-twilio.js +61 -0
  41. package/dist/channel/whatsapp-twilio.test.d.ts +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1418 -1
  44. package/dist/node/channel/dispatcher.js +129 -0
  45. package/dist/node/channel/github.js +57 -0
  46. package/dist/node/channel/index.js +1419 -0
  47. package/dist/node/channel/memory-store.js +222 -0
  48. package/dist/node/channel/policy.js +109 -0
  49. package/dist/node/channel/postgres-queries.js +221 -0
  50. package/dist/node/channel/postgres-schema.js +93 -0
  51. package/dist/node/channel/postgres-store.js +497 -0
  52. package/dist/node/channel/replay-fixtures.js +30 -0
  53. package/dist/node/channel/service.js +286 -0
  54. package/dist/node/channel/slack.js +81 -0
  55. package/dist/node/channel/store.js +0 -0
  56. package/dist/node/channel/telemetry.js +0 -0
  57. package/dist/node/channel/types.js +0 -0
  58. package/dist/node/channel/whatsapp-meta.js +65 -0
  59. package/dist/node/channel/whatsapp-twilio.js +60 -0
  60. package/dist/node/index.js +1418 -1
  61. package/dist/runtime.health.test.d.ts +1 -0
  62. package/package.json +213 -6
@@ -0,0 +1,222 @@
1
+ // src/channel/memory-store.ts
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ class InMemoryChannelRuntimeStore {
5
+ receipts = new Map;
6
+ threads = new Map;
7
+ decisions = new Map;
8
+ outbox = new Map;
9
+ deliveryAttempts = new Map;
10
+ receiptKeyToId = new Map;
11
+ threadKeyToId = new Map;
12
+ outboxKeyToId = new Map;
13
+ deliveryAttemptSequence = 0;
14
+ async claimEventReceipt(input) {
15
+ const key = this.receiptKey(input);
16
+ const existingId = this.receiptKeyToId.get(key);
17
+ if (existingId) {
18
+ const existing = this.receipts.get(existingId);
19
+ if (existing) {
20
+ existing.lastSeenAt = new Date;
21
+ this.receipts.set(existing.id, existing);
22
+ }
23
+ return {
24
+ receiptId: existingId,
25
+ duplicate: true
26
+ };
27
+ }
28
+ const id = randomUUID();
29
+ const now = new Date;
30
+ this.receipts.set(id, {
31
+ id,
32
+ workspaceId: input.workspaceId,
33
+ providerKey: input.providerKey,
34
+ externalEventId: input.externalEventId,
35
+ eventType: input.eventType,
36
+ status: "accepted",
37
+ signatureValid: input.signatureValid,
38
+ payloadHash: input.payloadHash,
39
+ traceId: input.traceId,
40
+ firstSeenAt: now,
41
+ lastSeenAt: now
42
+ });
43
+ this.receiptKeyToId.set(key, id);
44
+ return { receiptId: id, duplicate: false };
45
+ }
46
+ async updateReceiptStatus(receiptId, status, error) {
47
+ const receipt = this.receipts.get(receiptId);
48
+ if (!receipt) {
49
+ return;
50
+ }
51
+ receipt.status = status;
52
+ receipt.lastSeenAt = new Date;
53
+ if (status === "processed") {
54
+ receipt.processedAt = new Date;
55
+ }
56
+ receipt.errorCode = error?.code;
57
+ receipt.errorMessage = error?.message;
58
+ this.receipts.set(receiptId, receipt);
59
+ }
60
+ async upsertThread(input) {
61
+ const key = this.threadKey(input);
62
+ const existingId = this.threadKeyToId.get(key);
63
+ if (existingId) {
64
+ const existing = this.threads.get(existingId);
65
+ if (!existing) {
66
+ throw new Error("Corrupted thread state");
67
+ }
68
+ existing.externalChannelId = input.externalChannelId ?? existing.externalChannelId;
69
+ existing.externalUserId = input.externalUserId ?? existing.externalUserId;
70
+ existing.lastProviderEventAt = input.occurredAt ?? existing.lastProviderEventAt;
71
+ existing.updatedAt = new Date;
72
+ if (input.state) {
73
+ existing.state = {
74
+ ...existing.state,
75
+ ...input.state
76
+ };
77
+ }
78
+ this.threads.set(existing.id, existing);
79
+ return existing;
80
+ }
81
+ const id = randomUUID();
82
+ const now = new Date;
83
+ const record = {
84
+ id,
85
+ workspaceId: input.workspaceId,
86
+ providerKey: input.providerKey,
87
+ externalThreadId: input.externalThreadId,
88
+ externalChannelId: input.externalChannelId,
89
+ externalUserId: input.externalUserId,
90
+ state: input.state ?? {},
91
+ lastProviderEventAt: input.occurredAt,
92
+ createdAt: now,
93
+ updatedAt: now
94
+ };
95
+ this.threads.set(id, record);
96
+ this.threadKeyToId.set(key, id);
97
+ return record;
98
+ }
99
+ async saveDecision(input) {
100
+ const id = randomUUID();
101
+ const record = {
102
+ id,
103
+ receiptId: input.receiptId,
104
+ threadId: input.threadId,
105
+ policyMode: input.policyMode,
106
+ riskTier: input.riskTier,
107
+ confidence: input.confidence,
108
+ modelName: input.modelName,
109
+ promptVersion: input.promptVersion,
110
+ policyVersion: input.policyVersion,
111
+ toolTrace: input.toolTrace ?? [],
112
+ actionPlan: input.actionPlan,
113
+ requiresApproval: input.requiresApproval,
114
+ createdAt: new Date
115
+ };
116
+ this.decisions.set(id, record);
117
+ return record;
118
+ }
119
+ async enqueueOutboxAction(input) {
120
+ const existingId = this.outboxKeyToId.get(input.idempotencyKey);
121
+ if (existingId) {
122
+ return {
123
+ actionId: existingId,
124
+ duplicate: true
125
+ };
126
+ }
127
+ const id = randomUUID();
128
+ const now = new Date;
129
+ this.outbox.set(id, {
130
+ id,
131
+ workspaceId: input.workspaceId,
132
+ providerKey: input.providerKey,
133
+ decisionId: input.decisionId,
134
+ threadId: input.threadId,
135
+ actionType: input.actionType,
136
+ idempotencyKey: input.idempotencyKey,
137
+ target: input.target,
138
+ payload: input.payload,
139
+ status: "pending",
140
+ attemptCount: 0,
141
+ nextAttemptAt: now,
142
+ createdAt: now,
143
+ updatedAt: now
144
+ });
145
+ this.outboxKeyToId.set(input.idempotencyKey, id);
146
+ return {
147
+ actionId: id,
148
+ duplicate: false
149
+ };
150
+ }
151
+ async claimPendingOutboxActions(limit, now = new Date) {
152
+ const items = Array.from(this.outbox.values()).filter((item) => (item.status === "pending" || item.status === "retryable") && item.nextAttemptAt.getTime() <= now.getTime()).sort((a, b) => a.nextAttemptAt.getTime() - b.nextAttemptAt.getTime()).slice(0, Math.max(1, limit));
153
+ const claimed = [];
154
+ for (const item of items) {
155
+ const updated = {
156
+ ...item,
157
+ status: "sending",
158
+ attemptCount: item.attemptCount + 1,
159
+ updatedAt: new Date
160
+ };
161
+ this.outbox.set(updated.id, updated);
162
+ claimed.push(updated);
163
+ }
164
+ return claimed;
165
+ }
166
+ async recordDeliveryAttempt(input) {
167
+ this.deliveryAttemptSequence += 1;
168
+ const record = {
169
+ id: this.deliveryAttemptSequence,
170
+ actionId: input.actionId,
171
+ attempt: input.attempt,
172
+ responseStatus: input.responseStatus,
173
+ responseBody: input.responseBody,
174
+ latencyMs: input.latencyMs,
175
+ createdAt: new Date
176
+ };
177
+ this.deliveryAttempts.set(`${input.actionId}:${input.attempt}`, record);
178
+ return record;
179
+ }
180
+ async markOutboxSent(actionId, providerMessageId) {
181
+ const item = this.outbox.get(actionId);
182
+ if (!item)
183
+ return;
184
+ item.status = "sent";
185
+ item.providerMessageId = providerMessageId;
186
+ item.sentAt = new Date;
187
+ item.lastErrorCode = undefined;
188
+ item.lastErrorMessage = undefined;
189
+ item.updatedAt = new Date;
190
+ this.outbox.set(actionId, item);
191
+ }
192
+ async markOutboxRetry(input) {
193
+ const item = this.outbox.get(input.actionId);
194
+ if (!item)
195
+ return;
196
+ item.status = "retryable";
197
+ item.nextAttemptAt = input.nextAttemptAt;
198
+ item.lastErrorCode = input.lastErrorCode;
199
+ item.lastErrorMessage = input.lastErrorMessage;
200
+ item.updatedAt = new Date;
201
+ this.outbox.set(input.actionId, item);
202
+ }
203
+ async markOutboxDeadLetter(input) {
204
+ const item = this.outbox.get(input.actionId);
205
+ if (!item)
206
+ return;
207
+ item.status = "dead_letter";
208
+ item.lastErrorCode = input.lastErrorCode;
209
+ item.lastErrorMessage = input.lastErrorMessage;
210
+ item.updatedAt = new Date;
211
+ this.outbox.set(input.actionId, item);
212
+ }
213
+ receiptKey(input) {
214
+ return `${input.workspaceId}:${input.providerKey}:${input.externalEventId}`;
215
+ }
216
+ threadKey(input) {
217
+ return `${input.workspaceId}:${input.providerKey}:${input.externalThreadId}`;
218
+ }
219
+ }
220
+ export {
221
+ InMemoryChannelRuntimeStore
222
+ };
@@ -0,0 +1,109 @@
1
+ // src/channel/policy.ts
2
+ var DEFAULT_MESSAGING_POLICY_CONFIG = {
3
+ autoResolveMinConfidence: 0.85,
4
+ assistMinConfidence: 0.65,
5
+ blockedSignals: [
6
+ "ignore previous instructions",
7
+ "reveal secret",
8
+ "api key",
9
+ "password",
10
+ "token",
11
+ "drop table",
12
+ "delete repository"
13
+ ],
14
+ highRiskSignals: [
15
+ "refund",
16
+ "delete account",
17
+ "cancel subscription",
18
+ "permission",
19
+ "admin access",
20
+ "wire transfer",
21
+ "bank account"
22
+ ],
23
+ mediumRiskSignals: [
24
+ "urgent",
25
+ "legal",
26
+ "compliance",
27
+ "frustrated",
28
+ "escalate",
29
+ "outage"
30
+ ],
31
+ safeAckTemplate: "Thanks for your message. We received it and are preparing the next step."
32
+ };
33
+
34
+ class MessagingPolicyEngine {
35
+ config;
36
+ constructor(config) {
37
+ this.config = {
38
+ ...DEFAULT_MESSAGING_POLICY_CONFIG,
39
+ ...config ?? {}
40
+ };
41
+ }
42
+ evaluate(input) {
43
+ const text = (input.event.message?.text ?? "").toLowerCase();
44
+ if (containsAny(text, this.config.blockedSignals)) {
45
+ return {
46
+ confidence: 0.2,
47
+ riskTier: "blocked",
48
+ verdict: "blocked",
49
+ reasons: ["blocked_signal_detected"],
50
+ responseText: this.config.safeAckTemplate,
51
+ requiresApproval: true
52
+ };
53
+ }
54
+ if (containsAny(text, this.config.highRiskSignals)) {
55
+ return {
56
+ confidence: 0.55,
57
+ riskTier: "high",
58
+ verdict: "assist",
59
+ reasons: ["high_risk_topic_detected"],
60
+ responseText: this.config.safeAckTemplate,
61
+ requiresApproval: true
62
+ };
63
+ }
64
+ const mediumRiskDetected = containsAny(text, this.config.mediumRiskSignals);
65
+ const confidence = mediumRiskDetected ? 0.74 : 0.92;
66
+ const riskTier = mediumRiskDetected ? "medium" : "low";
67
+ if (confidence >= this.config.autoResolveMinConfidence && riskTier === "low") {
68
+ return {
69
+ confidence,
70
+ riskTier,
71
+ verdict: "autonomous",
72
+ reasons: ["low_risk_high_confidence"],
73
+ responseText: this.defaultResponseText(input.event),
74
+ requiresApproval: false
75
+ };
76
+ }
77
+ if (confidence >= this.config.assistMinConfidence) {
78
+ return {
79
+ confidence,
80
+ riskTier,
81
+ verdict: "assist",
82
+ reasons: ["needs_human_review"],
83
+ responseText: this.config.safeAckTemplate,
84
+ requiresApproval: true
85
+ };
86
+ }
87
+ return {
88
+ confidence,
89
+ riskTier: "blocked",
90
+ verdict: "blocked",
91
+ reasons: ["low_confidence"],
92
+ responseText: this.config.safeAckTemplate,
93
+ requiresApproval: true
94
+ };
95
+ }
96
+ defaultResponseText(event) {
97
+ if (!event.message?.text) {
98
+ return this.config.safeAckTemplate;
99
+ }
100
+ return `Acknowledged: ${event.message.text.slice(0, 240)}`;
101
+ }
102
+ }
103
+ function containsAny(text, candidates) {
104
+ return candidates.some((candidate) => text.includes(candidate));
105
+ }
106
+ export {
107
+ MessagingPolicyEngine,
108
+ DEFAULT_MESSAGING_POLICY_CONFIG
109
+ };
@@ -0,0 +1,221 @@
1
+ // src/channel/postgres-queries.ts
2
+ var CLAIM_EVENT_RECEIPT_SQL = `
3
+ with inserted as (
4
+ insert into channel_event_receipts (
5
+ id,
6
+ workspace_id,
7
+ provider_key,
8
+ external_event_id,
9
+ event_type,
10
+ status,
11
+ signature_valid,
12
+ payload_hash,
13
+ trace_id
14
+ )
15
+ values ($1, $2, $3, $4, $5, 'accepted', $6, $7, $8)
16
+ on conflict (workspace_id, provider_key, external_event_id)
17
+ do nothing
18
+ returning id
19
+ )
20
+ select id, true as inserted from inserted
21
+ union all
22
+ select id, false as inserted
23
+ from channel_event_receipts
24
+ where workspace_id = $2
25
+ and provider_key = $3
26
+ and external_event_id = $4
27
+ limit 1
28
+ `;
29
+ var MARK_RECEIPT_DUPLICATE_SQL = `
30
+ update channel_event_receipts
31
+ set last_seen_at = now(), status = 'duplicate'
32
+ where id = $1
33
+ `;
34
+ var UPDATE_RECEIPT_STATUS_SQL = `
35
+ update channel_event_receipts
36
+ set
37
+ status = $2,
38
+ error_code = $3,
39
+ error_message = $4,
40
+ last_seen_at = now(),
41
+ processed_at = case when $2 = 'processed' then now() else processed_at end
42
+ where id = $1
43
+ `;
44
+ var UPSERT_THREAD_SQL = `
45
+ insert into channel_threads (
46
+ id,
47
+ workspace_id,
48
+ provider_key,
49
+ external_thread_id,
50
+ external_channel_id,
51
+ external_user_id,
52
+ state,
53
+ last_provider_event_ts
54
+ )
55
+ values ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
56
+ on conflict (workspace_id, provider_key, external_thread_id)
57
+ do update set
58
+ external_channel_id = coalesce(excluded.external_channel_id, channel_threads.external_channel_id),
59
+ external_user_id = coalesce(excluded.external_user_id, channel_threads.external_user_id),
60
+ state = channel_threads.state || excluded.state,
61
+ last_provider_event_ts = coalesce(excluded.last_provider_event_ts, channel_threads.last_provider_event_ts),
62
+ updated_at = now()
63
+ returning
64
+ id,
65
+ workspace_id,
66
+ provider_key,
67
+ external_thread_id,
68
+ external_channel_id,
69
+ external_user_id,
70
+ state,
71
+ last_provider_event_ts,
72
+ created_at,
73
+ updated_at
74
+ `;
75
+ var INSERT_DECISION_SQL = `
76
+ insert into channel_ai_decisions (
77
+ id,
78
+ receipt_id,
79
+ thread_id,
80
+ policy_mode,
81
+ risk_tier,
82
+ confidence,
83
+ model_name,
84
+ prompt_version,
85
+ policy_version,
86
+ tool_trace,
87
+ action_plan,
88
+ requires_approval
89
+ )
90
+ values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)
91
+ `;
92
+ var ENQUEUE_OUTBOX_SQL = `
93
+ with inserted as (
94
+ insert into channel_outbox_actions (
95
+ id,
96
+ workspace_id,
97
+ provider_key,
98
+ decision_id,
99
+ thread_id,
100
+ action_type,
101
+ idempotency_key,
102
+ target,
103
+ payload,
104
+ status
105
+ )
106
+ values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, 'pending')
107
+ on conflict (idempotency_key)
108
+ do nothing
109
+ returning id
110
+ )
111
+ select id, true as inserted from inserted
112
+ union all
113
+ select id, false as inserted
114
+ from channel_outbox_actions
115
+ where idempotency_key = $7
116
+ limit 1
117
+ `;
118
+ var CLAIM_PENDING_OUTBOX_SQL = `
119
+ with candidates as (
120
+ select id
121
+ from channel_outbox_actions
122
+ where status in ('pending', 'retryable')
123
+ and next_attempt_at <= $2
124
+ order by next_attempt_at asc
125
+ limit $1
126
+ for update skip locked
127
+ )
128
+ update channel_outbox_actions as actions
129
+ set
130
+ status = 'sending',
131
+ attempt_count = actions.attempt_count + 1,
132
+ updated_at = now()
133
+ from candidates
134
+ where actions.id = candidates.id
135
+ returning
136
+ actions.id,
137
+ actions.workspace_id,
138
+ actions.provider_key,
139
+ actions.decision_id,
140
+ actions.thread_id,
141
+ actions.action_type,
142
+ actions.idempotency_key,
143
+ actions.target,
144
+ actions.payload,
145
+ actions.status,
146
+ actions.attempt_count,
147
+ actions.next_attempt_at,
148
+ actions.provider_message_id,
149
+ actions.last_error_code,
150
+ actions.last_error_message,
151
+ actions.created_at,
152
+ actions.updated_at,
153
+ actions.sent_at
154
+ `;
155
+ var INSERT_DELIVERY_ATTEMPT_SQL = `
156
+ insert into channel_delivery_attempts (
157
+ action_id,
158
+ attempt,
159
+ response_status,
160
+ response_body,
161
+ latency_ms
162
+ )
163
+ values ($1, $2, $3, $4, $5)
164
+ on conflict (action_id, attempt)
165
+ do update set
166
+ response_status = excluded.response_status,
167
+ response_body = excluded.response_body,
168
+ latency_ms = excluded.latency_ms,
169
+ created_at = now()
170
+ returning
171
+ id,
172
+ action_id,
173
+ attempt,
174
+ response_status,
175
+ response_body,
176
+ latency_ms,
177
+ created_at
178
+ `;
179
+ var MARK_OUTBOX_SENT_SQL = `
180
+ update channel_outbox_actions
181
+ set
182
+ status = 'sent',
183
+ provider_message_id = $2,
184
+ sent_at = now(),
185
+ updated_at = now(),
186
+ last_error_code = null,
187
+ last_error_message = null
188
+ where id = $1
189
+ `;
190
+ var MARK_OUTBOX_RETRY_SQL = `
191
+ update channel_outbox_actions
192
+ set
193
+ status = 'retryable',
194
+ next_attempt_at = $2,
195
+ last_error_code = $3,
196
+ last_error_message = $4,
197
+ updated_at = now()
198
+ where id = $1
199
+ `;
200
+ var MARK_OUTBOX_DEAD_LETTER_SQL = `
201
+ update channel_outbox_actions
202
+ set
203
+ status = 'dead_letter',
204
+ last_error_code = $2,
205
+ last_error_message = $3,
206
+ updated_at = now()
207
+ where id = $1
208
+ `;
209
+ export {
210
+ UPSERT_THREAD_SQL,
211
+ UPDATE_RECEIPT_STATUS_SQL,
212
+ MARK_RECEIPT_DUPLICATE_SQL,
213
+ MARK_OUTBOX_SENT_SQL,
214
+ MARK_OUTBOX_RETRY_SQL,
215
+ MARK_OUTBOX_DEAD_LETTER_SQL,
216
+ INSERT_DELIVERY_ATTEMPT_SQL,
217
+ INSERT_DECISION_SQL,
218
+ ENQUEUE_OUTBOX_SQL,
219
+ CLAIM_PENDING_OUTBOX_SQL,
220
+ CLAIM_EVENT_RECEIPT_SQL
221
+ };
@@ -0,0 +1,93 @@
1
+ // src/channel/postgres-schema.ts
2
+ var CHANNEL_RUNTIME_SCHEMA_STATEMENTS = [
3
+ `
4
+ create table if not exists channel_event_receipts (
5
+ id uuid primary key,
6
+ workspace_id text not null,
7
+ provider_key text not null,
8
+ external_event_id text not null,
9
+ event_type text not null,
10
+ status text not null,
11
+ signature_valid boolean not null default false,
12
+ payload_hash text,
13
+ trace_id text,
14
+ first_seen_at timestamptz not null default now(),
15
+ last_seen_at timestamptz not null default now(),
16
+ processed_at timestamptz,
17
+ error_code text,
18
+ error_message text,
19
+ unique (workspace_id, provider_key, external_event_id)
20
+ )
21
+ `,
22
+ `
23
+ create table if not exists channel_threads (
24
+ id uuid primary key,
25
+ workspace_id text not null,
26
+ provider_key text not null,
27
+ external_thread_id text not null,
28
+ external_channel_id text,
29
+ external_user_id text,
30
+ state jsonb not null default '{}'::jsonb,
31
+ last_provider_event_ts timestamptz,
32
+ created_at timestamptz not null default now(),
33
+ updated_at timestamptz not null default now(),
34
+ unique (workspace_id, provider_key, external_thread_id)
35
+ )
36
+ `,
37
+ `
38
+ create table if not exists channel_ai_decisions (
39
+ id uuid primary key,
40
+ receipt_id uuid not null references channel_event_receipts (id),
41
+ thread_id uuid not null references channel_threads (id),
42
+ policy_mode text not null,
43
+ risk_tier text not null,
44
+ confidence numeric(5,4) not null,
45
+ model_name text not null,
46
+ prompt_version text not null,
47
+ policy_version text not null,
48
+ tool_trace jsonb not null default '[]'::jsonb,
49
+ action_plan jsonb not null,
50
+ requires_approval boolean not null default false,
51
+ approved_by text,
52
+ approved_at timestamptz,
53
+ created_at timestamptz not null default now()
54
+ )
55
+ `,
56
+ `
57
+ create table if not exists channel_outbox_actions (
58
+ id uuid primary key,
59
+ workspace_id text not null,
60
+ provider_key text not null,
61
+ decision_id uuid not null references channel_ai_decisions (id),
62
+ thread_id uuid not null references channel_threads (id),
63
+ action_type text not null,
64
+ idempotency_key text not null unique,
65
+ target jsonb not null,
66
+ payload jsonb not null,
67
+ status text not null,
68
+ attempt_count integer not null default 0,
69
+ next_attempt_at timestamptz not null default now(),
70
+ provider_message_id text,
71
+ last_error_code text,
72
+ last_error_message text,
73
+ created_at timestamptz not null default now(),
74
+ updated_at timestamptz not null default now(),
75
+ sent_at timestamptz
76
+ )
77
+ `,
78
+ `
79
+ create table if not exists channel_delivery_attempts (
80
+ id bigserial primary key,
81
+ action_id uuid not null references channel_outbox_actions (id),
82
+ attempt integer not null,
83
+ response_status integer,
84
+ response_body text,
85
+ latency_ms integer,
86
+ created_at timestamptz not null default now(),
87
+ unique (action_id, attempt)
88
+ )
89
+ `
90
+ ];
91
+ export {
92
+ CHANNEL_RUNTIME_SCHEMA_STATEMENTS
93
+ };