@portalshq/capability-interaction-orchestrator 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +593 -225
- package/package.json +2 -2
- package/sql/001_interaction_orchestrator_core.sql +0 -4
package/README.md
CHANGED
|
@@ -1,248 +1,616 @@
|
|
|
1
1
|
# @portalshq/capability-interaction-orchestrator
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
credit-backed billing, execution handoff and replayable events. PostgreSQL owns
|
|
5
|
-
admission, quotas, idempotency, credit allocations, resolutions and event ordering.
|
|
3
|
+
**Reusable interaction orchestration for Portals:** input windows, submission gateway, arbitration engine, moderation gateway, billing controller, and event publisher.
|
|
6
4
|
|
|
7
|
-
##
|
|
5
|
+
## Status: Phase 1 (Stub Package) - NOT Production-Ready
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
This is currently a **stub package** providing TypeScript interfaces and type definitions. Implementations throw "not implemented" errors. Full implementation coming in Phase 2.
|
|
8
|
+
|
|
9
|
+
**Production Readiness:**
|
|
10
|
+
- **Phase 1 (Current):** NOT production-ready - stubs only
|
|
11
|
+
- **Phase 2:** Production-ready for basic use cases (FIFO, E-rated, prepaid credits)
|
|
12
|
+
- **Phase 3:** Production-ready for all use cases (all policies, all ratings, pluggable moderation)
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
The Interaction Orchestrator provides reusable infrastructure for managing user interactions in AI-driven entertainment experiences. It handles:
|
|
17
|
+
|
|
18
|
+
- **Input Windows** - Time-bounded opportunities for user interaction
|
|
19
|
+
- **Submission Gateway** - Validation, idempotency, and admission
|
|
20
|
+
- **Arbitration Engine** - Configurable contention resolution policies
|
|
21
|
+
- **Moderation Gateway** - Rating-based and pluggable content moderation
|
|
22
|
+
- **Billing Controller** - Integration with `@portalshq/monetization`
|
|
23
|
+
- **Event Publisher** - SSE/outbox for frontend signaling
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @portalshq/monetization # Peer dependency
|
|
29
|
+
npm install @portalshq/capability-interaction-orchestrator
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Core Concepts
|
|
33
|
+
|
|
34
|
+
### Input Windows
|
|
35
|
+
|
|
36
|
+
An Input Window represents a time-bounded opportunity for user interaction. Examples:
|
|
37
|
+
- A 30-second audience vote on a plot twist
|
|
38
|
+
- A 1-minute challenge round in a game show
|
|
39
|
+
- An always-open paid prompt queue (set `closesAt` to null)
|
|
40
|
+
|
|
41
|
+
**Window Lifecycle:**
|
|
42
|
+
```
|
|
43
|
+
scheduled → open → qualifying → resolving → executing → committing → published
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Key Properties:**
|
|
47
|
+
- `worldId` - Channel/world identifier
|
|
48
|
+
- `conflictKey` - Contention scoping (entity, branch, or session)
|
|
49
|
+
- `arbitrationPolicy` - How to resolve contention (FIFO, voting, etc.)
|
|
50
|
+
- `billingPolicy` - How to charge (free, pay-to-submit, etc.)
|
|
51
|
+
- `contentRating` - Moderation threshold (E, T, T+, M)
|
|
52
|
+
- `closesAt` - When window closes (null = always-open)
|
|
53
|
+
|
|
54
|
+
### Submissions
|
|
55
|
+
|
|
56
|
+
A Submission is a user's proposed action against a specific target. Multiple submissions may compete for selection via arbitration.
|
|
57
|
+
|
|
58
|
+
**Key Properties:**
|
|
59
|
+
- `windowId` - Window this submission belongs to
|
|
60
|
+
- `actorId` - User/app identifier
|
|
61
|
+
- `idempotencyKey` - Retry safety
|
|
62
|
+
- `kind` - Type of interaction (vote, prompt, mutation, etc.)
|
|
63
|
+
- `payload` - Submission data
|
|
64
|
+
- `status` - Lifecycle state
|
|
65
|
+
|
|
66
|
+
### Resolutions
|
|
67
|
+
|
|
68
|
+
A Resolution represents the outcome of arbitration: which submissions were selected and how they were combined into execution.
|
|
69
|
+
|
|
70
|
+
**Key Properties:**
|
|
71
|
+
- `selectedSubmissionIds` - IDs of selected submissions
|
|
72
|
+
- `rejectedSubmissionIds` - IDs of rejected submissions
|
|
73
|
+
- `arbitrationPolicy` - Policy used
|
|
74
|
+
- `resultingCommitHash` - Resulting world state
|
|
75
|
+
|
|
76
|
+
### Arbitration Policies
|
|
77
|
+
|
|
78
|
+
The package supports 7 arbitration policies, configurable per window:
|
|
79
|
+
|
|
80
|
+
1. **first_accepted** - Earliest qualifying submission wins (exclusive actions)
|
|
81
|
+
2. **fifo** - Every qualifying input executes in order (paid story creation)
|
|
82
|
+
3. **plurality** - Most votes wins (game shows)
|
|
83
|
+
4. **ranked** - Rank by priority, then tie-break (featured submissions)
|
|
84
|
+
5. **batch** - Combine compatible inputs (live narrative twists)
|
|
85
|
+
6. **parallel_branches** - Every qualified input gets independent result (remix galleries)
|
|
86
|
+
7. **host_selection** - Host manually selects (curated events)
|
|
87
|
+
|
|
88
|
+
### Content Rating System
|
|
89
|
+
|
|
90
|
+
Based on DC Comics rating system:
|
|
91
|
+
|
|
92
|
+
- **E (Everyone)** - All ages, cartoonish action, no profanity/violence/sexual content
|
|
93
|
+
- **T (Teen)** - Ages 12+, mild violence/language/suggestive dialogue
|
|
94
|
+
- **T+ (Teen Plus)** - Ages 15+, moderate profanity, graphic action, mature storylines
|
|
95
|
+
- **M (Mature)** - Ages 17+, heavy sexual themes, nudity, extensive profanity, graphic violence
|
|
96
|
+
|
|
97
|
+
### Billing Policies
|
|
98
|
+
|
|
99
|
+
- **free** - No payment required
|
|
100
|
+
- **pay_to_generate** - Payment before generation
|
|
101
|
+
- **pay_to_publish** - Payment after valid preview, before publication
|
|
102
|
+
- **pay_for_guaranteed_slot** - Payment for guaranteed submission/placement
|
|
103
|
+
- **capture_if_selected** - Authorize payment, capture only if selected
|
|
104
|
+
- **prepaid_credits** - Consume prepaid credits
|
|
105
|
+
|
|
106
|
+
## Phase 1: Stub Package (Current)
|
|
107
|
+
|
|
108
|
+
**What exists:**
|
|
109
|
+
- TypeScript interfaces and types
|
|
110
|
+
- Stub implementations (throw "not implemented - Phase 2")
|
|
111
|
+
- Core database schema
|
|
112
|
+
- Basic documentation
|
|
113
|
+
|
|
114
|
+
**What's missing:**
|
|
115
|
+
- Actual implementations
|
|
116
|
+
- Database persistence layer
|
|
117
|
+
- Integration with `@portalshq/monetization`
|
|
118
|
+
- Integration with `@portalshq/capability-realtime-fanout-postgres`
|
|
119
|
+
- Tests
|
|
120
|
+
|
|
121
|
+
## Phase 2: Core Implementation (Next)
|
|
122
|
+
|
|
123
|
+
**Will include:**
|
|
124
|
+
- Window Manager (in-process window closer via setInterval)
|
|
125
|
+
- Submission Gateway (validation, idempotency, admission)
|
|
126
|
+
- Arbitration Engine (FIFO policy)
|
|
127
|
+
- Moderation Gateway (E-rated, rule-based validators)
|
|
128
|
+
- Billing Controller (monetization integration)
|
|
129
|
+
- Event Publisher (fanout-postgres integration)
|
|
130
|
+
- PostgreSQL persistence layer
|
|
131
|
+
- Tests for implemented components
|
|
132
|
+
- Migration guide for replacing existing guides.ts
|
|
133
|
+
|
|
134
|
+
**Production-ready for:**
|
|
135
|
+
- Always-open queues (like current guides.ts)
|
|
136
|
+
- FIFO arbitration
|
|
137
|
+
- E-rated content
|
|
138
|
+
- Prepaid credit billing
|
|
139
|
+
- Basic SSE event publishing
|
|
140
|
+
|
|
141
|
+
**Estimated effort:** 60 hours
|
|
142
|
+
|
|
143
|
+
## Phase 3: Full Feature Set
|
|
144
|
+
|
|
145
|
+
**Will add:**
|
|
146
|
+
- Remaining 6 arbitration policies (plurality, ranked, batch, etc.)
|
|
147
|
+
- T, T+, M rating validators
|
|
148
|
+
- Pluggable moderation interface for custom validators
|
|
149
|
+
- Advanced billing policies
|
|
150
|
+
- Complete documentation and examples
|
|
151
|
+
- 95% test coverage
|
|
152
|
+
|
|
153
|
+
**Estimated effort:** 44 hours
|
|
154
|
+
|
|
155
|
+
## Database Schema
|
|
156
|
+
|
|
157
|
+
### Core Schema (Package-Provided)
|
|
158
|
+
|
|
159
|
+
The package provides core tables in `sql/001_interaction_orchestrator_core.sql`:
|
|
160
|
+
|
|
161
|
+
- `input_windows` - Window definitions
|
|
162
|
+
- `submissions` - User submissions
|
|
163
|
+
- `resolutions` - Arbitration outcomes
|
|
164
|
+
- `moderation_decisions` - Moderation results
|
|
165
|
+
- `interaction_outbox` - Event queue
|
|
166
|
+
|
|
167
|
+
### App Extensions (App-Provided)
|
|
168
|
+
|
|
169
|
+
Applications can extend core tables via:
|
|
170
|
+
- `app_metadata` JSONB column on all core tables
|
|
171
|
+
- Additional extension tables with foreign keys to core tables
|
|
172
|
+
|
|
173
|
+
**Example extension:**
|
|
174
|
+
```sql
|
|
175
|
+
-- App-specific extension table
|
|
176
|
+
CREATE TABLE game_show_windows (
|
|
177
|
+
window_id text PRIMARY KEY REFERENCES input_windows(id),
|
|
178
|
+
round_number integer NOT NULL,
|
|
179
|
+
contestant_count integer NOT NULL
|
|
180
|
+
);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Architecture
|
|
11
184
|
|
|
12
|
-
```sh
|
|
13
|
-
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 --single-transaction \
|
|
14
|
-
-f node_modules/@portalshq/monetization/sql/001_monetization.sql \
|
|
15
|
-
-f node_modules/@portalshq/capability-interaction-orchestrator/sql/001_interaction_orchestrator_core.sql \
|
|
16
|
-
-f node_modules/@portalshq/capability-interaction-orchestrator/sql/002_durable_lifecycle.sql
|
|
17
185
|
```
|
|
186
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
187
|
+
│ Interaction Orchestrator Package │
|
|
188
|
+
│ (In-Process Only) │
|
|
189
|
+
├─────────────────────────────────────────────────────────────┤
|
|
190
|
+
│ │
|
|
191
|
+
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
192
|
+
│ │ Window Manager │ │Submission Gateway│ │
|
|
193
|
+
│ │ - Lifecycle │ │ - Validation │ │
|
|
194
|
+
│ │ - Triggers │ │ - Idempotency │ │
|
|
195
|
+
│ │ - State Machine │ │ - Admission │ │
|
|
196
|
+
│ │ - setInterval │ │ │ │
|
|
197
|
+
│ └────────┬─────────┘ └────────┬─────────┘ │
|
|
198
|
+
│ │ │ │
|
|
199
|
+
│ └────────────┬───────────────┘ │
|
|
200
|
+
│ │ │
|
|
201
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
202
|
+
│ │ Moderation Gateway │ │
|
|
203
|
+
│ │ - Admission validation (built-in) │ │
|
|
204
|
+
│ │ - Content safety (rating-based) │ │
|
|
205
|
+
│ │ - Custom validators (pluggable) │ │
|
|
206
|
+
│ │ - E/T/T+/M rating system │ │
|
|
207
|
+
│ └────────────────────┬────────────────────┘ │
|
|
208
|
+
│ │ │
|
|
209
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
210
|
+
│ │ Arbitration Engine │ │
|
|
211
|
+
│ │ - FIFO, voting, branches, etc. │ │
|
|
212
|
+
│ │ - Configurable per window │ │
|
|
213
|
+
│ └────────────────────┬────────────────────┘ │
|
|
214
|
+
│ │ │
|
|
215
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
216
|
+
│ │ Billing Controller │ │
|
|
217
|
+
│ │ - Quote, authorize, capture │ │
|
|
218
|
+
│ │ - Integration with @portalshq/monetization│
|
|
219
|
+
│ └────────────────────┬────────────────────┘ │
|
|
220
|
+
│ │ │
|
|
221
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
222
|
+
│ │ Event Publisher │ │
|
|
223
|
+
│ │ - SSE events, outbox │ │
|
|
224
|
+
│ │ - Integration with fanout-postgres │ │
|
|
225
|
+
│ └─────────────────────────────────────────┘ │
|
|
226
|
+
│ │
|
|
227
|
+
└─────────────────────────────────────────────────────────────┘
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Ownership Boundaries
|
|
231
|
+
|
|
232
|
+
**Package owns:**
|
|
233
|
+
- Window lifecycle
|
|
234
|
+
- Admission gates
|
|
235
|
+
- Arbitration policies
|
|
236
|
+
- Event signaling
|
|
237
|
+
- Core database schema
|
|
238
|
+
|
|
239
|
+
**Application owns:**
|
|
240
|
+
- Moderation rules (custom validators)
|
|
241
|
+
- World constraints (custom validators)
|
|
242
|
+
- Model execution (production layer)
|
|
243
|
+
- Pricing (monetization catalog)
|
|
244
|
+
- Schema extensions (app_metadata, extension tables)
|
|
245
|
+
- Production layer workers (unchanged)
|
|
246
|
+
|
|
247
|
+
## Peer Dependencies
|
|
248
|
+
|
|
249
|
+
- `@portalshq/monetization` - Billing, entitlements, ledger
|
|
250
|
+
|
|
251
|
+
## Integration with Portals Packages
|
|
252
|
+
|
|
253
|
+
- `@portalshq/monetization` - Billing Controller integration
|
|
254
|
+
- `@portalshq/capability-realtime-fanout-postgres` - Event Publisher integration
|
|
255
|
+
- `@portalshq/capability-identity` - Actor ID verification
|
|
256
|
+
- `@portalshq/capability-live-production` - Window triggers integration
|
|
257
|
+
- `@portalshq/queue-broadcast` - Media staging integration
|
|
258
|
+
|
|
259
|
+
## Migration Path for Bears Live App
|
|
260
|
+
|
|
261
|
+
**Phase 1:** Apply core schema, keep existing `guides.ts` unchanged
|
|
262
|
+
|
|
263
|
+
**Phase 2:**
|
|
264
|
+
- Rename `guides.ts` to `interaction-orchestrator-client.ts`
|
|
265
|
+
- Rewrite to use new package
|
|
266
|
+
- Configure as always-open window (no closesAt)
|
|
267
|
+
- Set content rating to "E"
|
|
268
|
+
- Test with existing payment flow
|
|
18
269
|
|
|
19
|
-
|
|
20
|
-
ID order for grant locks while consuming the oldest matching credits first.
|
|
21
|
-
The old `Stub*` exports have been removed. The executable classes now require their
|
|
22
|
-
actual dependencies; no implementation silently grants a payment or host approval.
|
|
270
|
+
**Phase 3:** Add advanced features (voting, branches, etc.)
|
|
23
271
|
|
|
24
|
-
##
|
|
272
|
+
## Development
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Build
|
|
276
|
+
npm run build
|
|
277
|
+
|
|
278
|
+
# Type check
|
|
279
|
+
npm run typecheck
|
|
280
|
+
|
|
281
|
+
# Test (Phase 2+)
|
|
282
|
+
npm test
|
|
283
|
+
|
|
284
|
+
# Test with coverage (Phase 2+)
|
|
285
|
+
npm run test:coverage
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Database Setup
|
|
289
|
+
|
|
290
|
+
Apply the core schema to your PostgreSQL database:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
psql $DATABASE_URL -f node_modules/@portalshq/capability-interaction-orchestrator/sql/001_interaction_orchestrator_core.sql
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
For Bears Live App migration, see the migration section below.
|
|
297
|
+
|
|
298
|
+
## Migration from guides.ts to Interaction Orchestrator
|
|
299
|
+
|
|
300
|
+
### Current State (guides.ts)
|
|
301
|
+
|
|
302
|
+
The existing `guides.ts` in Bears Live App provides:
|
|
303
|
+
- Simple FIFO queue for paid prompts
|
|
304
|
+
- 1 credit per prompt via `@portalshq/monetization`
|
|
305
|
+
- Basic validation and text model moderation
|
|
306
|
+
- Implicit always-open window (no explicit window management)
|
|
307
|
+
- Single arbitration policy (all eligible prompts used)
|
|
308
|
+
|
|
309
|
+
### Migration Path
|
|
310
|
+
|
|
311
|
+
**Phase 1 (Current):**
|
|
312
|
+
- Apply core schema to Bears database
|
|
313
|
+
- Keep existing `guides.ts` unchanged
|
|
314
|
+
- No functional changes
|
|
315
|
+
|
|
316
|
+
**Phase 2:**
|
|
317
|
+
1. Rename `guides.ts` to `interaction-orchestrator-client.ts`
|
|
318
|
+
2. Import `WindowManager`, `SubmissionGateway`, etc. from new package
|
|
319
|
+
3. Create an always-open window:
|
|
320
|
+
```typescript
|
|
321
|
+
const window = await windowManager.createWindow({
|
|
322
|
+
worldId: channelId,
|
|
323
|
+
branchId: "canonical",
|
|
324
|
+
baseCommitHash: currentCommit,
|
|
325
|
+
conflictKey: channelId,
|
|
326
|
+
allowedInputKinds: ["custom_prompt"],
|
|
327
|
+
arbitrationPolicy: "fifo",
|
|
328
|
+
billingPolicy: "prepaid_credits",
|
|
329
|
+
maxSubmissionsPerUser: 1,
|
|
330
|
+
maxSelectedInputs: 5,
|
|
331
|
+
opensAt: new Date().toISOString(),
|
|
332
|
+
closesAt: null, // Always-open
|
|
333
|
+
contentRating: "E",
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
4. Replace `submitGuide()` with `submissionGateway.submitIntent()`
|
|
337
|
+
5. Replace `takePendingGuides()` with window closure and arbitration
|
|
338
|
+
6. Integrate moderation gateway with existing text model moderation
|
|
339
|
+
7. Test with existing payment flow
|
|
340
|
+
|
|
341
|
+
**Phase 3:**
|
|
342
|
+
- Add voting windows for game-show modes
|
|
343
|
+
- Add host selection for curated events
|
|
344
|
+
- Add parallel branches for remixes
|
|
345
|
+
- Add billing variants (pay-if-selected, pay-to-publish)
|
|
346
|
+
|
|
347
|
+
## Example: Always-Open Paid Prompt Queue
|
|
348
|
+
|
|
349
|
+
This example shows how to replace the current `guides.ts` with the Interaction Orchestrator:
|
|
25
350
|
|
|
26
351
|
```typescript
|
|
27
|
-
import { Pool } from 'pg';
|
|
28
352
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
353
|
+
WindowManager,
|
|
354
|
+
SubmissionGateway,
|
|
355
|
+
ArbitrationEngine,
|
|
356
|
+
ModerationGateway,
|
|
357
|
+
BillingController,
|
|
358
|
+
EventPublisher,
|
|
359
|
+
type CreateWindowInput,
|
|
360
|
+
type SubmitIntentInput,
|
|
361
|
+
} from "@portalshq/capability-interaction-orchestrator";
|
|
362
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
363
|
+
import type pg from "pg";
|
|
364
|
+
|
|
365
|
+
// Initialize components (in Phase 2, these will be real implementations)
|
|
366
|
+
const windowManager = new WindowManager(pool);
|
|
367
|
+
const submissionGateway = new SubmissionGateway(pool);
|
|
368
|
+
const arbitrationEngine = new ArbitrationEngine();
|
|
369
|
+
const moderationGateway = new ModerationGateway();
|
|
370
|
+
const billingController = new BillingController(monetizationStore);
|
|
371
|
+
const eventPublisher = new EventPublisher(fanoutBus);
|
|
372
|
+
|
|
373
|
+
// Create always-open window for paid prompts
|
|
374
|
+
async function createPromptWindow(channelId: string): Promise<string> {
|
|
375
|
+
const windowId = randomUUID();
|
|
376
|
+
const window = await windowManager.createWindow({
|
|
377
|
+
worldId: channelId,
|
|
378
|
+
branchId: "canonical",
|
|
379
|
+
baseCommitHash: currentCommit,
|
|
380
|
+
conflictKey: channelId,
|
|
381
|
+
allowedInputKinds: ["custom_prompt"],
|
|
382
|
+
arbitrationPolicy: "fifo",
|
|
383
|
+
billingPolicy: "prepaid_credits",
|
|
384
|
+
maxSubmissionsPerUser: 1,
|
|
385
|
+
maxSelectedInputs: 5,
|
|
386
|
+
opensAt: new Date().toISOString(),
|
|
387
|
+
closesAt: null, // Always-open
|
|
388
|
+
contentRating: "E",
|
|
389
|
+
moderationPolicyVersion: "1.0",
|
|
390
|
+
rulesetVersion: "1.0",
|
|
391
|
+
});
|
|
392
|
+
return window.id;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Submit a paid prompt
|
|
396
|
+
async function submitPaidPrompt(
|
|
397
|
+
channelId: string,
|
|
398
|
+
userId: string,
|
|
399
|
+
prompt: string,
|
|
400
|
+
): Promise<Submission> {
|
|
401
|
+
const windowId = getOrCreateWindowId(channelId);
|
|
402
|
+
|
|
403
|
+
const submission = await submissionGateway.submitIntent({
|
|
404
|
+
windowId,
|
|
405
|
+
actorId: userId,
|
|
406
|
+
idempotencyKey: createHash(userId + prompt).digest('hex'),
|
|
407
|
+
kind: "custom_prompt",
|
|
408
|
+
payload: { prompt },
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return submission;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Take prompts for next generation (replaces takePendingGuides)
|
|
415
|
+
async function takePromptsForGeneration(
|
|
416
|
+
channelId: string,
|
|
417
|
+
limit: number = 5,
|
|
418
|
+
): Promise<Submission[]> {
|
|
419
|
+
const windowId = getWindowId(channelId);
|
|
420
|
+
const submissions = await submissionGateway.listSubmissions(windowId, "eligible");
|
|
421
|
+
|
|
422
|
+
// Resolve via FIFO arbitration
|
|
423
|
+
const resolution = await arbitrationEngine.resolve(
|
|
424
|
+
await windowManager.getWindow(windowId),
|
|
425
|
+
submissions.slice(0, limit),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Get selected submissions
|
|
429
|
+
const selected = await Promise.all(
|
|
430
|
+
resolution.selectedSubmissionIds.map(id => submissionGateway.getSubmission(id))
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
return selected.filter((s): s is Submission => s !== null);
|
|
71
434
|
}
|
|
72
435
|
```
|
|
73
436
|
|
|
74
|
-
|
|
75
|
-
against the window's world before exposing submission, cancellation, review,
|
|
76
|
-
host-selection, refund or event methods over a network. This package is an internal
|
|
77
|
-
module; it does not authenticate HTTP requests. Window configuration, billing terms,
|
|
78
|
-
policy configuration and moderation context come from trusted application code.
|
|
79
|
-
Submission payloads and app metadata cannot set prices, rank or host authority.
|
|
437
|
+
## Architecture Diagram
|
|
80
438
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
439
|
+
```
|
|
440
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
441
|
+
│ Interaction Orchestrator Package │
|
|
442
|
+
│ (In-Process Only) │
|
|
443
|
+
├─────────────────────────────────────────────────────────────┤
|
|
444
|
+
│ │
|
|
445
|
+
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
446
|
+
│ │ Window Manager │ │Submission Gateway│ │
|
|
447
|
+
│ │ - Lifecycle │ │ - Validation │ │
|
|
448
|
+
│ │ - Triggers │ │ - Idempotency │ │
|
|
449
|
+
│ │ - State Machine │ │ - Admission │ │
|
|
450
|
+
│ │ - setInterval │ │ │ │
|
|
451
|
+
│ └────────┬─────────┘ └────────┬─────────┘ │
|
|
452
|
+
│ │ │ │
|
|
453
|
+
│ └────────────┬───────────────┘ │
|
|
454
|
+
│ │ │
|
|
455
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
456
|
+
│ │ Moderation Gateway │ │
|
|
457
|
+
│ │ - Admission validation (built-in) │ │
|
|
458
|
+
│ │ - Content safety (rating-based) │ │
|
|
459
|
+
│ │ - Custom validators (pluggable) │ │
|
|
460
|
+
│ │ - E/T/T+/M rating system │ │
|
|
461
|
+
│ └────────────────────┬────────────────────┘ │
|
|
462
|
+
│ │ │
|
|
463
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
464
|
+
│ │ Arbitration Engine │ │
|
|
465
|
+
│ │ - FIFO │ │
|
|
466
|
+
│ │ - Plurality voting │ │
|
|
467
|
+
│ │ - First accepted │ │
|
|
468
|
+
│ │ - Ranked selection │ │
|
|
469
|
+
│ │ - Batch composition │ │
|
|
470
|
+
│ │ - Parallel branches │ │
|
|
471
|
+
│ │ - Host selection │ │
|
|
472
|
+
│ │ - Configurable per window │ │
|
|
473
|
+
│ └────────────────────┬────────────────────┘ │
|
|
474
|
+
│ │ │
|
|
475
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
476
|
+
│ │ Billing Controller │ │
|
|
477
|
+
│ │ - Quote generation │ │
|
|
478
|
+
│ │ - Credit reservation │ │
|
|
479
|
+
│ │ - Authorization/capture │ │
|
|
480
|
+
│ │ - Settlement │ │
|
|
481
|
+
│ │ - Refund handling │ │
|
|
482
|
+
│ │ - Integration with @portalshq/monetization│
|
|
483
|
+
│ └────────────────────┬────────────────────┘ │
|
|
484
|
+
│ │ │
|
|
485
|
+
│ ┌────────────────────▼────────────────────┐ │
|
|
486
|
+
│ │ Event Publisher │ │
|
|
487
|
+
│ │ - SSE events │ │
|
|
488
|
+
│ │ - Outbox pattern │ │
|
|
489
|
+
│ │ - Replay support │ │
|
|
490
|
+
│ │ - Integration with fanout-postgres │ │
|
|
491
|
+
│ └─────────────────────────────────────────┘ │
|
|
492
|
+
│ │
|
|
493
|
+
└─────────────────────────────────────────────────────────────┘
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Data Flow Diagram
|
|
497
|
+
|
|
498
|
+
```
|
|
499
|
+
User Input
|
|
500
|
+
│
|
|
501
|
+
▼
|
|
502
|
+
┌─────────────┐
|
|
503
|
+
│ HTTP POST │
|
|
504
|
+
└──────┬──────┘
|
|
505
|
+
│
|
|
506
|
+
▼
|
|
507
|
+
┌──────────────────┐
|
|
508
|
+
│ Submission │
|
|
509
|
+
│ Gateway │
|
|
510
|
+
│ - Validate │
|
|
511
|
+
│ - Idempotency │
|
|
512
|
+
│ - Check window │
|
|
513
|
+
└──────┬───────────┘
|
|
514
|
+
│
|
|
515
|
+
▼
|
|
516
|
+
┌──────────────────┐
|
|
517
|
+
│ Moderation │
|
|
518
|
+
│ Gateway │
|
|
519
|
+
│ - Admission │
|
|
520
|
+
│ - Content safety │
|
|
521
|
+
│ - World rules │
|
|
522
|
+
└──────┬───────────┘
|
|
523
|
+
│
|
|
524
|
+
▼
|
|
525
|
+
┌──────────────────┐
|
|
526
|
+
│ Billing │
|
|
527
|
+
│ Controller │
|
|
528
|
+
│ - Quote │
|
|
529
|
+
│ - Authorize │
|
|
530
|
+
└──────┬───────────┘
|
|
531
|
+
│
|
|
532
|
+
▼
|
|
533
|
+
┌──────────────────┐
|
|
534
|
+
│ Submission │
|
|
535
|
+
│ Stored (DB) │
|
|
536
|
+
└──────────────────┘
|
|
537
|
+
│
|
|
538
|
+
▼
|
|
539
|
+
┌──────────────────┐
|
|
540
|
+
│ Window Close │
|
|
541
|
+
│ (setInterval) │
|
|
542
|
+
└──────┬───────────┘
|
|
543
|
+
│
|
|
544
|
+
▼
|
|
545
|
+
┌──────────────────┐
|
|
546
|
+
│ Arbitration │
|
|
547
|
+
│ Engine │
|
|
548
|
+
│ - Select inputs │
|
|
549
|
+
│ - Apply policy │
|
|
550
|
+
└──────┬───────────┘
|
|
551
|
+
│
|
|
552
|
+
▼
|
|
553
|
+
┌──────────────────┐
|
|
554
|
+
│ Resolution │
|
|
555
|
+
│ Created (DB) │
|
|
556
|
+
└──────┬───────────┘
|
|
557
|
+
│
|
|
558
|
+
▼
|
|
559
|
+
┌──────────────────┐
|
|
560
|
+
│ Billing Capture │
|
|
561
|
+
│ (if selected) │
|
|
562
|
+
└──────┬───────────┘
|
|
563
|
+
│
|
|
564
|
+
▼
|
|
565
|
+
┌──────────────────┐
|
|
566
|
+
│ Model Execution │
|
|
567
|
+
│ (App-owned) │
|
|
568
|
+
└──────┬───────────┘
|
|
569
|
+
│
|
|
570
|
+
▼
|
|
571
|
+
┌──────────────────┐
|
|
572
|
+
│ Event Publisher │
|
|
573
|
+
│ - Outbox │
|
|
574
|
+
│ - SSE │
|
|
575
|
+
└──────────────────┘
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Error Handling
|
|
579
|
+
|
|
580
|
+
All components should implement comprehensive error handling:
|
|
86
581
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
582
|
+
- **Window Manager:** Handle invalid state transitions, missing windows
|
|
583
|
+
- **Submission Gateway:** Handle idempotency conflicts, closed windows, validation failures
|
|
584
|
+
- **Arbitration Engine:** Handle empty submissions, policy errors
|
|
585
|
+
- **Moderation Gateway:** Handle moderation service failures, rating violations
|
|
586
|
+
- **Billing Controller:** Handle authorization failures, payment errors
|
|
587
|
+
- **Event Publisher:** Handle fanout failures, retry with backoff
|
|
92
588
|
|
|
93
|
-
|
|
589
|
+
## Logging
|
|
590
|
+
|
|
591
|
+
Components should use structured logging with context:
|
|
94
592
|
|
|
95
593
|
```typescript
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
idempotencyKey: 'generation-request-id',
|
|
99
|
-
});
|
|
100
|
-
const executable = await interactions.prepareExecution(resolution.id);
|
|
101
|
-
// Pass executable to your execution adapter to generate a private preview/artifact.
|
|
102
|
-
// Keep proposed world changes private until output moderation and commit succeed.
|
|
103
|
-
// Then call commitResolution(resolution.id, proposedOutput, resultingCommitHash).
|
|
594
|
+
console.log(`[interaction-orchestrator] window=${windowId} state=${state} action=${action}`);
|
|
595
|
+
console.error(`[interaction-orchestrator] window=${windowId} error=${error.message}`);
|
|
104
596
|
```
|
|
105
597
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`failResolution(id, reason)` refunds selected work and records failure. Use it after
|
|
122
|
-
an execution failure or expired/revoked hold; retrying a failed resolution requires
|
|
123
|
-
a new submission. `cancelWindow(id)` cancels pending inputs and refunds their credits
|
|
124
|
-
in one transaction, refusing to interrupt an active resolution.
|
|
125
|
-
|
|
126
|
-
## Selection policies
|
|
127
|
-
|
|
128
|
-
| Policy | Behavior |
|
|
129
|
-
| --- | --- |
|
|
130
|
-
| `fifo` | Admission order, with ID as the final deterministic tie-break |
|
|
131
|
-
| `first_accepted` | The first eligible input |
|
|
132
|
-
| `plurality` | Distinct actors per option/target, descending vote count, then admission order; selects one representative per winning target |
|
|
133
|
-
| `ranked` | Trusted `arbitrationPolicyConfig.priorities[submissionId]`, then admission order; ignores payload priority |
|
|
134
|
-
| `batch` | Admission order, combining nonoverlapping `targetEntityIds` up to capacity |
|
|
135
|
-
| `parallel_branches` | Admission order up to capacity, one stable branch destination per selected input |
|
|
136
|
-
| `host_selection` | Explicit `selectedSubmissionIds` passed to `resolveWindow`; rejects unknown, duplicate or excessive IDs |
|
|
137
|
-
|
|
138
|
-
All policies respect `maxSelectedInputs`. Custom policies must partition the eligible
|
|
139
|
-
inputs exactly once into selected and rejected IDs. The arbitration engine is a
|
|
140
|
-
pure selection module; its `resolutionSequence: 0` is an unpersisted proposal.
|
|
141
|
-
The orchestrator allocates the actual monotonic sequence under the window lock.
|
|
142
|
-
|
|
143
|
-
For always-open FIFO/parallel queues, overflow stays eligible for the next generation
|
|
144
|
-
and is recorded in `appMetadata.deferredSubmissionIds`. Finite-window losers are
|
|
145
|
-
marked `not_selected` and refunded. Prompt/message/vote inputs can follow a moving
|
|
146
|
-
queue head; state-changing inputs with an old base must be cancelled and resubmitted
|
|
147
|
-
after application revalidation. Active per-user quota counts pending, eligible,
|
|
148
|
-
selected and executing inputs; completed/rejected/cancelled inputs free that quota.
|
|
149
|
-
Scheduled windows can coexist, but opening two windows with the same world, branch
|
|
150
|
-
and conflict key fails.
|
|
151
|
-
|
|
152
|
-
## Moderation
|
|
153
|
-
|
|
154
|
-
`ModerationGateway` checks every nested string in JSON input/output. Its defaults
|
|
155
|
-
provide configurable term rules, Unicode normalization, word edges, input length
|
|
156
|
-
200 and output length 32,000. E blocks mild/strong profanity, explicit terms and
|
|
157
|
-
graphic violence terms; T permits mild profanity; T+ permits strong profanity and
|
|
158
|
-
graphic violence; M permits these categories. Blocked/prohibited terms apply at
|
|
159
|
-
every rating. These are text rules, not semantic or image/video classification.
|
|
160
|
-
Install application/model validators for contextual content decisions and non-text
|
|
161
|
-
media inspection. Version your configuration with `moderationPolicyVersion`.
|
|
162
|
-
|
|
163
|
-
Custom validators cannot bypass the built-in rules. Missing `validateOutput` falls
|
|
164
|
-
back to `validate` with the complete output in a synthetic submission. Validator
|
|
165
|
-
exceptions and malformed decisions stop qualification. `review` and `transform`
|
|
166
|
-
remain in `moderating`; only an explicit trusted
|
|
167
|
-
`submissions.reviewSubmission(id, 'allow' | 'reject', reasons)` completes them.
|
|
168
|
-
Transforms are never silently applied. `contextFor(window)` injects application
|
|
169
|
-
world rules; each request uses its own rating context.
|
|
170
|
-
|
|
171
|
-
## Billing behavior
|
|
172
|
-
|
|
173
|
-
| Policy | Admission | Selection/execution | Commit |
|
|
174
|
-
| --- | --- | --- | --- |
|
|
175
|
-
| `free` | Zero-credit reservation | Verify lifecycle | No charge |
|
|
176
|
-
| `prepaid_credits` | Consume credits | Validate backing grants | Fulfill |
|
|
177
|
-
| `pay_to_generate` | Consume credits | Validate backing grants | Fulfill |
|
|
178
|
-
| `pay_for_guaranteed_slot` | Claim capacity and consume credits | Guaranteed FIFO/parallel selection | Fulfill |
|
|
179
|
-
| `capture_if_selected` | Deduct a credit hold from spendable balance | Capture selected holds; release losers | Fulfill |
|
|
180
|
-
| `pay_to_publish` | Deduct a credit hold | Generate a private preview | Capture only on allowed output |
|
|
181
|
-
|
|
182
|
-
Guaranteed slots require FIFO or parallel selection and cannot overbook capacity.
|
|
183
|
-
Reservations default to one hour; configure `reservationTtlMs` (1 second–24 hours).
|
|
184
|
-
A hold expires no later than its earliest backing grant. Captured prepaid credits
|
|
185
|
-
remain spent while queued. Cancellation, failed execution, non-allow output and
|
|
186
|
-
finite-window loss release/refund exactly once, restoring only surviving unexpired
|
|
187
|
-
original grants. A deleted/refunded purchase's grant is never recreated. Expired
|
|
188
|
-
credits cannot be made spendable by an interaction refund. Deferred captures check
|
|
189
|
-
expiry and revocation again at execution/publication.
|
|
190
|
-
|
|
191
|
-
## Events and workers
|
|
192
|
-
|
|
193
|
-
`EventPublisher` accepts `createPostgresFanoutBus(pool)` or `InMemoryFanoutBus` through
|
|
194
|
-
the fanout seam. Set up `fanout_messages` as documented by
|
|
195
|
-
[`capability-realtime-fanout-postgres`](../capability-realtime-fanout-postgres).
|
|
196
|
-
Lifecycle writes enqueue events; call `drainOutbox()` from a worker. Publishing to
|
|
197
|
-
the transport is at least once, with expiring claims and token-checked acknowledgment.
|
|
198
|
-
Failed deliveries stay durable for retry. Each event has a stable ID and a
|
|
199
|
-
transactionally allocated world sequence. Topics use `interaction:${worldId}`.
|
|
200
|
-
|
|
201
|
-
`subscribe(worldId, afterSequence?, { signal? })` returns an async iterator. With a
|
|
202
|
-
cursor it replays all later events in bounded pages; without one it starts at the
|
|
203
|
-
current head. Fanout messages wake the iterator; the database remains authoritative
|
|
204
|
-
for ordering and deduplication. Polling recovers missed hints. Abort, iterator return
|
|
205
|
-
and exceptions release subscriptions and timers, including a read that is waiting.
|
|
206
|
-
Use `String(event.sequence)` for SSE `id`; validate and authorize `Last-Event-ID`.
|
|
207
|
-
`getEvents` returns at most 100 events per page. The iterator handles pagination.
|
|
208
|
-
|
|
209
|
-
Run these bounded jobs with error reporting and nonoverlapping invocations:
|
|
210
|
-
|
|
211
|
-
- `windows.startWindowCloser(5000)` closes due finite windows; always-open queues remain open.
|
|
212
|
-
- `billing.expireReservations()` releases expired, unselected holds in batches of 100.
|
|
213
|
-
- `submissions.resumeSubmission(id)` recovers a durable `received` admission after an outage. Find these through application recovery jobs or `listSubmissions(windowId, 'received')`.
|
|
214
|
-
- `events.drainOutbox()` delivers a bounded outbox batch; repeat while work remains.
|
|
215
|
-
|
|
216
|
-
Open scheduled windows explicitly at their scheduled time. Finish the active
|
|
217
|
-
always-open queue resolution before explicitly closing its intake. Stop/await the closer,
|
|
218
|
-
abort/return all event iterators, finish in-flight worker jobs and then close the
|
|
219
|
-
caller-owned fanout bus and pool during shutdown. A selected/executing expired hold
|
|
220
|
-
requires `failResolution`; it is never silently reassigned. Use trusted operator
|
|
221
|
-
`billing.refund` for completed work; active resolutions must fail first.
|
|
222
|
-
|
|
223
|
-
Repository transactions serialize admission/selection per window, world event writes
|
|
224
|
-
per world, and credit mutations in grant-ID order. Keep network calls outside these
|
|
225
|
-
transactions. The exported repository and `*InTransaction` methods are low-level
|
|
226
|
-
integration seams; use `InteractionOrchestrator` for lifecycle operations. Retain
|
|
227
|
-
outbox history for the promised replay period and monitor unpublished age, pending
|
|
228
|
-
qualification age, expiring holds and failed resolutions. This package does not
|
|
229
|
-
silently prune audit/replay data.
|
|
230
|
-
|
|
231
|
-
## Validation and migration
|
|
232
|
-
|
|
233
|
-
```sh
|
|
234
|
-
npm run build --workspace=packages/monetization
|
|
235
|
-
npm run build --workspace=packages/capability-interaction-orchestrator
|
|
236
|
-
npm run typecheck --workspace=packages/capability-interaction-orchestrator
|
|
237
|
-
npm run test:coverage --workspace=packages/capability-interaction-orchestrator
|
|
238
|
-
```
|
|
598
|
+
## Testing
|
|
599
|
+
|
|
600
|
+
Phase 2 will include comprehensive test coverage:
|
|
601
|
+
|
|
602
|
+
- Unit tests for each component
|
|
603
|
+
- Integration tests with monetization package
|
|
604
|
+
- Integration tests with fanout-postgres package
|
|
605
|
+
- Database migration tests
|
|
606
|
+
- End-to-end workflow tests
|
|
607
|
+
|
|
608
|
+
Target: 95% code coverage
|
|
609
|
+
|
|
610
|
+
## License
|
|
611
|
+
|
|
612
|
+
MIT
|
|
239
613
|
|
|
240
|
-
|
|
241
|
-
multiple connections against a scratch PostgreSQL instance, set both
|
|
242
|
-
`INTERACTION_TEST_MODE=non-production` and `INTERACTION_TEST_DATABASE_URL`. Each suite
|
|
243
|
-
creates/drops only its randomly named schema. Coverage thresholds are 95% for lines,
|
|
244
|
-
statements, functions and branches and are enforced in the PostgreSQL CI job.
|
|
614
|
+
## License
|
|
245
615
|
|
|
246
|
-
|
|
247
|
-
[migration guide](https://github.com/portalshq/portals-cloud/blob/main/packages/capability-interaction-orchestrator/docs/bears-live-app-migration.md)
|
|
248
|
-
and [advanced usage](https://github.com/portalshq/portals-cloud/blob/main/packages/capability-interaction-orchestrator/docs/advanced-usage.md).
|
|
616
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portalshq/capability-interaction-orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Reusable interaction orchestration for Portals: input windows, submission gateway, arbitration engine, moderation gateway, billing controller, and event publisher",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"pg": "^8.16.3",
|
|
37
|
-
"@portalshq/monetization": "^0.2.
|
|
37
|
+
"@portalshq/monetization": "^0.2.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^20.0.0",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
CREATE TABLE IF NOT EXISTS input_windows (
|
|
13
13
|
id text PRIMARY KEY,
|
|
14
14
|
revision integer NOT NULL DEFAULT 1,
|
|
15
|
-
|
|
16
15
|
world_id text NOT NULL,
|
|
17
16
|
branch_id text NOT NULL,
|
|
18
17
|
base_commit_hash text NOT NULL,
|
|
@@ -114,12 +113,10 @@ CREATE INDEX IF NOT EXISTS resolutions_window_idx ON resolutions(window_id, stat
|
|
|
114
113
|
CREATE TABLE IF NOT EXISTS moderation_decisions (
|
|
115
114
|
id text PRIMARY KEY,
|
|
116
115
|
submission_id text NOT NULL REFERENCES submissions(id) ON DELETE CASCADE,
|
|
117
|
-
|
|
118
116
|
status text NOT NULL CHECK (status IN ('allow', 'reject', 'review', 'transform')),
|
|
119
117
|
reasons jsonb,
|
|
120
118
|
policy_version text NOT NULL,
|
|
121
119
|
reviewed_by text NOT NULL CHECK (reviewed_by IN ('rules', 'model', 'human')),
|
|
122
|
-
|
|
123
120
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
124
121
|
);
|
|
125
122
|
|
|
@@ -134,7 +131,6 @@ CREATE TABLE IF NOT EXISTS interaction_outbox (
|
|
|
134
131
|
event_type text NOT NULL,
|
|
135
132
|
sequence integer NOT NULL,
|
|
136
133
|
payload jsonb NOT NULL,
|
|
137
|
-
|
|
138
134
|
published_at timestamptz,
|
|
139
135
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
140
136
|
);
|