open-managed-agents 0.1.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 (197) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +248 -0
  3. package/bin/oma.mjs +499 -0
  4. package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
  5. package/dist/scripts/alpha-smoke-models.mjs +92 -0
  6. package/dist/scripts/alpha-smoke.mjs +581 -0
  7. package/dist/scripts/oma-doctor.js +230 -0
  8. package/dist/scripts/oma-models.js +299 -0
  9. package/dist/scripts/oma-workspaces.js +133 -0
  10. package/dist/src/control-plane/admin/auth.js +43 -0
  11. package/dist/src/control-plane/admin/routes.js +76 -0
  12. package/dist/src/control-plane/admin/service.js +102 -0
  13. package/dist/src/control-plane/admission.js +90 -0
  14. package/dist/src/control-plane/agents/routes.js +63 -0
  15. package/dist/src/control-plane/agents/service.js +687 -0
  16. package/dist/src/control-plane/agents/store.js +391 -0
  17. package/dist/src/control-plane/agents/types.js +1 -0
  18. package/dist/src/control-plane/api-constants.js +3 -0
  19. package/dist/src/control-plane/app.js +907 -0
  20. package/dist/src/control-plane/console/auth.js +68 -0
  21. package/dist/src/control-plane/console/static.js +112 -0
  22. package/dist/src/control-plane/deployment-runtime-config.js +328 -0
  23. package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
  24. package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
  25. package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
  26. package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
  27. package/dist/src/control-plane/deployment-storage.js +354 -0
  28. package/dist/src/control-plane/egress/image.js +48 -0
  29. package/dist/src/control-plane/egress/policy.js +571 -0
  30. package/dist/src/control-plane/egress/presets.js +77 -0
  31. package/dist/src/control-plane/egress/proxy.js +72 -0
  32. package/dist/src/control-plane/egress/ssrf.js +106 -0
  33. package/dist/src/control-plane/egress/utils/debug.js +14 -0
  34. package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
  35. package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
  36. package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
  37. package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
  38. package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
  39. package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
  40. package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
  41. package/dist/src/control-plane/environments/routes.js +54 -0
  42. package/dist/src/control-plane/environments/service.js +112 -0
  43. package/dist/src/control-plane/environments/store.js +132 -0
  44. package/dist/src/control-plane/environments/types.js +1 -0
  45. package/dist/src/control-plane/errors.js +81 -0
  46. package/dist/src/control-plane/events/broadcaster.js +217 -0
  47. package/dist/src/control-plane/events/constants.js +2 -0
  48. package/dist/src/control-plane/events/persist.js +39 -0
  49. package/dist/src/control-plane/events/request.js +190 -0
  50. package/dist/src/control-plane/events/routes.js +159 -0
  51. package/dist/src/control-plane/events/runtime-helpers.js +134 -0
  52. package/dist/src/control-plane/events/service.js +1985 -0
  53. package/dist/src/control-plane/events/session-guards.js +29 -0
  54. package/dist/src/control-plane/events/sse.js +6 -0
  55. package/dist/src/control-plane/events/store.js +772 -0
  56. package/dist/src/control-plane/events/tool-persistence.js +102 -0
  57. package/dist/src/control-plane/events/types.js +21 -0
  58. package/dist/src/control-plane/files/routes.js +121 -0
  59. package/dist/src/control-plane/files/service.js +65 -0
  60. package/dist/src/control-plane/files/store-common.js +87 -0
  61. package/dist/src/control-plane/files/store-local.js +488 -0
  62. package/dist/src/control-plane/files/store-memory.js +270 -0
  63. package/dist/src/control-plane/files/store.js +2 -0
  64. package/dist/src/control-plane/files/types.js +7 -0
  65. package/dist/src/control-plane/http.js +25 -0
  66. package/dist/src/control-plane/ids.js +43 -0
  67. package/dist/src/control-plane/logging.js +263 -0
  68. package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
  69. package/dist/src/control-plane/models/catalog.js +78 -0
  70. package/dist/src/control-plane/models/config-security.js +376 -0
  71. package/dist/src/control-plane/models/deployment-config.js +93 -0
  72. package/dist/src/control-plane/models/routes.js +32 -0
  73. package/dist/src/control-plane/models/service.js +138 -0
  74. package/dist/src/control-plane/observability/instruments.js +65 -0
  75. package/dist/src/control-plane/observability/metrics.js +125 -0
  76. package/dist/src/control-plane/observability/routes.js +95 -0
  77. package/dist/src/control-plane/openapi/document.js +282 -0
  78. package/dist/src/control-plane/openapi/routes.js +78 -0
  79. package/dist/src/control-plane/request-idempotency.js +85 -0
  80. package/dist/src/control-plane/secrets/envelope.js +132 -0
  81. package/dist/src/control-plane/secrets/master-key.js +44 -0
  82. package/dist/src/control-plane/secrets/routes.js +20 -0
  83. package/dist/src/control-plane/secrets/service.js +71 -0
  84. package/dist/src/control-plane/secrets/store.js +212 -0
  85. package/dist/src/control-plane/secrets/types.js +3 -0
  86. package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
  87. package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
  88. package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
  89. package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
  90. package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
  91. package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
  92. package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
  93. package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
  94. package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
  95. package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
  96. package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
  97. package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
  98. package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
  99. package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
  100. package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
  101. package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
  102. package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
  103. package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
  104. package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
  105. package/dist/src/control-plane/sessions/pi/translator.js +100 -0
  106. package/dist/src/control-plane/sessions/request.js +164 -0
  107. package/dist/src/control-plane/sessions/resources.js +63 -0
  108. package/dist/src/control-plane/sessions/routes.js +88 -0
  109. package/dist/src/control-plane/sessions/serialize.js +24 -0
  110. package/dist/src/control-plane/sessions/service.js +733 -0
  111. package/dist/src/control-plane/sessions/store.js +538 -0
  112. package/dist/src/control-plane/sessions/types.js +1 -0
  113. package/dist/src/control-plane/skills/archive.js +169 -0
  114. package/dist/src/control-plane/skills/routes.js +41 -0
  115. package/dist/src/control-plane/skills/service.js +29 -0
  116. package/dist/src/control-plane/skills/store.js +233 -0
  117. package/dist/src/control-plane/skills/types.js +5 -0
  118. package/dist/src/control-plane/sqlite-transaction.js +15 -0
  119. package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
  120. package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
  121. package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
  122. package/dist/src/control-plane/vaults/routes.js +66 -0
  123. package/dist/src/control-plane/vaults/service.js +553 -0
  124. package/dist/src/control-plane/vaults/store.js +691 -0
  125. package/dist/src/control-plane/vaults/types.js +1 -0
  126. package/dist/src/control-plane/wake-loop.js +65 -0
  127. package/dist/src/control-plane/wiring.js +92 -0
  128. package/dist/src/control-plane/workspace.js +4 -0
  129. package/dist/src/control-plane/workspaces/store.js +158 -0
  130. package/dist/src/egress-proxy-main.js +103 -0
  131. package/dist/src/main.js +124 -0
  132. package/dist/src/types/agents.js +1 -0
  133. package/dist/src/types/common.js +1 -0
  134. package/dist/src/types/environments.js +1 -0
  135. package/dist/src/types/events.js +121 -0
  136. package/dist/src/types/files.js +1 -0
  137. package/dist/src/types/json.js +19 -0
  138. package/dist/src/types/sessions.js +1 -0
  139. package/dist/ui/managed-agents-console/README.md +52 -0
  140. package/dist/ui/managed-agents-console/docs/agents.md +23 -0
  141. package/dist/ui/managed-agents-console/docs/console.md +23 -0
  142. package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
  143. package/dist/ui/managed-agents-console/docs/environments.md +27 -0
  144. package/dist/ui/managed-agents-console/docs/events.md +19 -0
  145. package/dist/ui/managed-agents-console/docs/files.md +17 -0
  146. package/dist/ui/managed-agents-console/docs/github.md +11 -0
  147. package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
  148. package/dist/ui/managed-agents-console/docs/memory.md +11 -0
  149. package/dist/ui/managed-agents-console/docs/migration.md +17 -0
  150. package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
  151. package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
  152. package/dist/ui/managed-agents-console/docs/overview.md +41 -0
  153. package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
  154. package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
  155. package/dist/ui/managed-agents-console/docs/reference.md +27 -0
  156. package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
  157. package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
  158. package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
  159. package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
  160. package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
  161. package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
  162. package/dist/ui/managed-agents-console/docs/skills.md +15 -0
  163. package/dist/ui/managed-agents-console/docs/tools.md +23 -0
  164. package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
  165. package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
  166. package/dist/ui/managed-agents-console/index.html +47 -0
  167. package/dist/ui/managed-agents-console/serve.mjs +116 -0
  168. package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
  169. package/dist/ui/managed-agents-console/src/api.js +1043 -0
  170. package/dist/ui/managed-agents-console/src/app.jsx +633 -0
  171. package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
  172. package/dist/ui/managed-agents-console/src/console.css +733 -0
  173. package/dist/ui/managed-agents-console/src/data.js +188 -0
  174. package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
  175. package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
  176. package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
  177. package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
  178. package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
  179. package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
  180. package/dist/ui/managed-agents-console/src/sse.js +185 -0
  181. package/dist/ui/managed-agents-console/src/states.jsx +90 -0
  182. package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
  183. package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
  184. package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
  185. package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
  186. package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
  187. package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
  188. package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
  189. package/dist/ui/openapi-docs/VENDOR.md +27 -0
  190. package/dist/ui/openapi-docs/index.html +16 -0
  191. package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
  192. package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
  193. package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
  194. package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
  195. package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
  196. package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
  197. package/package.json +53 -0
@@ -0,0 +1,772 @@
1
+ /**
2
+ * EventStore — durable append-only event log backed by SQLite.
3
+ *
4
+ * Persist-before-publish (ADR 0007 Pattern 2): every event hits this table
5
+ * before being broadcast to live subscribers. Subscribers that miss the live
6
+ * broadcast (drop, reconnect, never-connected) recover via `list()`.
7
+ *
8
+ * Uses `node:sqlite` (built-in to Node 22.5+) so we have no native dependency
9
+ * that would break under the `ignore-scripts=true` npm policy.
10
+ */
11
+ import { DatabaseSync } from "node:sqlite";
12
+ import { RuntimeTurnOwnershipLostError } from "./types.js";
13
+ import { withSqliteTransaction } from "../sqlite-transaction.js";
14
+ const SCHEMA = `
15
+ CREATE TABLE IF NOT EXISTS events (
16
+ id TEXT PRIMARY KEY,
17
+ workspace_id TEXT NOT NULL DEFAULT 'wrk_default',
18
+ session_id TEXT NOT NULL,
19
+ type TEXT NOT NULL,
20
+ processed_at TEXT,
21
+ payload TEXT NOT NULL,
22
+ created_at TEXT NOT NULL
23
+ );
24
+ CREATE TABLE IF NOT EXISTS pending_runtime_turns (
25
+ workspace_id TEXT NOT NULL,
26
+ session_id TEXT NOT NULL,
27
+ turn_id TEXT NOT NULL,
28
+ owner_id TEXT NOT NULL,
29
+ owner_generation INTEGER NOT NULL,
30
+ lease_expires_at TEXT NOT NULL,
31
+ state TEXT NOT NULL,
32
+ trigger_event_ids TEXT NOT NULL,
33
+ open_model_request_start_ids TEXT NOT NULL DEFAULT '[]',
34
+ created_at TEXT NOT NULL,
35
+ updated_at TEXT NOT NULL,
36
+ completed_at TEXT,
37
+ terminalized_at TEXT,
38
+ PRIMARY KEY (workspace_id, session_id, turn_id)
39
+ );
40
+ -- Closed turns are retained as history (UPDATE, not DELETE), so live-turn
41
+ -- counts need a partial index or every /metrics scrape and /health check
42
+ -- scans all history (0121 C2 review, Codex-adv HIGH). Serves both the
43
+ -- workspace-scoped and unscoped counts.
44
+ CREATE INDEX IF NOT EXISTS idx_runtime_turns_live
45
+ ON pending_runtime_turns (workspace_id)
46
+ WHERE state NOT IN ('completed', 'terminalized');
47
+ CREATE TABLE IF NOT EXISTS pending_runtime_actions (
48
+ workspace_id TEXT NOT NULL,
49
+ session_id TEXT NOT NULL,
50
+ turn_id TEXT NOT NULL,
51
+ action_id TEXT NOT NULL,
52
+ action_type TEXT NOT NULL,
53
+ state TEXT NOT NULL,
54
+ acknowledged_at TEXT,
55
+ closed_at TEXT,
56
+ close_reason TEXT,
57
+ created_at TEXT NOT NULL,
58
+ updated_at TEXT NOT NULL,
59
+ PRIMARY KEY (workspace_id, session_id, action_id),
60
+ FOREIGN KEY (workspace_id, session_id, turn_id)
61
+ REFERENCES pending_runtime_turns(workspace_id, session_id, turn_id)
62
+ );
63
+ CREATE TABLE IF NOT EXISTS idempotency_keys (
64
+ workspace_id TEXT NOT NULL,
65
+ method TEXT NOT NULL,
66
+ concrete_path TEXT NOT NULL,
67
+ idempotency_key TEXT NOT NULL,
68
+ route_label TEXT NOT NULL,
69
+ fingerprint_sha256 TEXT NOT NULL,
70
+ status TEXT NOT NULL,
71
+ response_status INTEGER,
72
+ response_body TEXT,
73
+ resource_type TEXT,
74
+ resource_id TEXT,
75
+ created_at TEXT NOT NULL,
76
+ updated_at TEXT NOT NULL,
77
+ expires_at TEXT NOT NULL,
78
+ PRIMARY KEY (workspace_id, method, concrete_path, idempotency_key)
79
+ );
80
+ `;
81
+ const INDEXES = `
82
+ CREATE INDEX IF NOT EXISTS events_by_workspace_session ON events (workspace_id, session_id, id);
83
+ CREATE INDEX IF NOT EXISTS events_by_workspace_session_type ON events (workspace_id, session_id, type, id);
84
+ CREATE INDEX IF NOT EXISTS pending_runtime_actions_by_turn
85
+ ON pending_runtime_actions (workspace_id, session_id, turn_id);
86
+ CREATE INDEX IF NOT EXISTS idempotency_keys_by_status_expiry
87
+ ON idempotency_keys (status, expires_at);
88
+ CREATE INDEX IF NOT EXISTS idempotency_keys_by_resource
89
+ ON idempotency_keys (workspace_id, resource_type, resource_id);
90
+ `;
91
+ export class EventStore {
92
+ db;
93
+ appendStmt;
94
+ deleteForSessionStmt;
95
+ deleteIdempotencyKeysForSessionStmt;
96
+ deleteRuntimeActionsForSessionStmt;
97
+ deleteRuntimeTurnsForSessionStmt;
98
+ retrieveStmt;
99
+ findRuntimeActionStmt;
100
+ listPendingRuntimeTurnsStmt;
101
+ listWorkspacesWithPendingRuntimeTurnsStmt;
102
+ countPendingRuntimeTurnsStmt;
103
+ countAllPendingRuntimeTurnsStmt;
104
+ runtimeChangesObserver;
105
+ pendingRuntimeChangeObservations = [];
106
+ txDepth = 0;
107
+ listRuntimeActionsForTurnStmt;
108
+ insertRuntimeTurnStmt;
109
+ insertRuntimeActionStmt;
110
+ acknowledgeRuntimeActionStmt;
111
+ closeRuntimeActionStmt;
112
+ updateRuntimeTurnStateStmt;
113
+ renewRuntimeTurnLeaseStmt;
114
+ updateRuntimeTurnOpenModelRequestStartsStmt;
115
+ closeRuntimeTurnStmt;
116
+ closeRuntimeActionsForTurnStmt;
117
+ claimAcceptedRuntimeTurnStmt;
118
+ claimTerminalizingRuntimeTurnStmt;
119
+ retrieveRuntimeTurnStmt;
120
+ purgeExpiredIdempotencyKeysStmt;
121
+ reserveIdempotencyKeyStmt;
122
+ retrieveIdempotencyKeyStmt;
123
+ acquireAbandonedIdempotencyKeyStmt;
124
+ completeIdempotencyKeyStmt;
125
+ releaseIdempotencyReservationStmt;
126
+ refreshIdempotencyReservationStmt;
127
+ deleteIdempotencyKeysForResourceStmt;
128
+ listStmts = new Map();
129
+ constructor(db) {
130
+ this.db = db;
131
+ this.db.exec(SCHEMA);
132
+ ensureWorkspaceIdColumn(this.db);
133
+ ensureOpenModelRequestStartIdsColumn(this.db);
134
+ ensureIdempotencyResourceColumns(this.db);
135
+ this.db.exec(INDEXES);
136
+ this.appendStmt = this.db.prepare(`INSERT INTO events (id, workspace_id, session_id, type, processed_at, payload, created_at)
137
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
138
+ this.deleteForSessionStmt = this.db.prepare(`DELETE FROM events WHERE workspace_id = ? AND session_id = ?`);
139
+ this.deleteIdempotencyKeysForSessionStmt = this.db.prepare(`DELETE FROM idempotency_keys
140
+ WHERE workspace_id = ? AND concrete_path = ?`);
141
+ this.deleteIdempotencyKeysForResourceStmt = this.db.prepare(`DELETE FROM idempotency_keys
142
+ WHERE workspace_id = ? AND resource_type = ? AND resource_id = ?`);
143
+ this.deleteRuntimeActionsForSessionStmt = this.db.prepare(`DELETE FROM pending_runtime_actions
144
+ WHERE workspace_id = ? AND session_id = ?`);
145
+ this.deleteRuntimeTurnsForSessionStmt = this.db.prepare(`DELETE FROM pending_runtime_turns
146
+ WHERE workspace_id = ? AND session_id = ?`);
147
+ this.retrieveStmt = this.db.prepare(`SELECT id, workspace_id, session_id, type, processed_at, payload, created_at
148
+ FROM events WHERE workspace_id = ? AND id = ?`);
149
+ this.findRuntimeActionStmt = this.db.prepare(runtimeActionSelectSql(`
150
+ WHERE a.workspace_id = ? AND a.session_id = ? AND a.action_id = ?
151
+ LIMIT 1
152
+ `));
153
+ this.listPendingRuntimeTurnsStmt = this.db.prepare(`SELECT workspace_id, session_id, turn_id, owner_id, owner_generation,
154
+ lease_expires_at, state, trigger_event_ids, created_at, updated_at,
155
+ completed_at, terminalized_at, open_model_request_start_ids
156
+ FROM pending_runtime_turns
157
+ WHERE workspace_id = ? AND state NOT IN ('completed', 'terminalized')
158
+ ORDER BY turn_id ASC`);
159
+ this.listWorkspacesWithPendingRuntimeTurnsStmt = this.db.prepare(`SELECT DISTINCT workspace_id
160
+ FROM pending_runtime_turns
161
+ WHERE state NOT IN ('completed', 'terminalized')
162
+ ORDER BY workspace_id ASC`);
163
+ this.countPendingRuntimeTurnsStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM pending_runtime_turns
164
+ WHERE workspace_id = ? AND state NOT IN ('completed', 'terminalized')`);
165
+ this.countAllPendingRuntimeTurnsStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM pending_runtime_turns
166
+ WHERE state NOT IN ('completed', 'terminalized')`);
167
+ this.listRuntimeActionsForTurnStmt = this.db.prepare(runtimeActionSelectSql(`
168
+ WHERE a.workspace_id = ? AND a.session_id = ? AND a.turn_id = ?
169
+ ORDER BY a.action_id ASC
170
+ `));
171
+ this.insertRuntimeTurnStmt = this.db.prepare(`INSERT INTO pending_runtime_turns
172
+ (workspace_id, session_id, turn_id, owner_id, owner_generation,
173
+ lease_expires_at, state, trigger_event_ids, open_model_request_start_ids,
174
+ created_at, updated_at)
175
+ VALUES (?, ?, ?, ?, ?, ?, 'accepted', ?, '[]', ?, ?)`);
176
+ this.insertRuntimeActionStmt = this.db.prepare(`INSERT INTO pending_runtime_actions
177
+ (workspace_id, session_id, turn_id, action_id, action_type, state,
178
+ created_at, updated_at)
179
+ VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`);
180
+ this.acknowledgeRuntimeActionStmt = this.db.prepare(`UPDATE pending_runtime_actions
181
+ SET state = 'acknowledged',
182
+ acknowledged_at = COALESCE(acknowledged_at, ?),
183
+ updated_at = ?
184
+ WHERE workspace_id = ? AND session_id = ? AND action_id = ?
185
+ AND state = 'pending'`);
186
+ this.closeRuntimeActionStmt = this.db.prepare(`UPDATE pending_runtime_actions
187
+ SET state = 'closed',
188
+ closed_at = COALESCE(closed_at, ?),
189
+ close_reason = COALESCE(close_reason, ?),
190
+ updated_at = ?
191
+ WHERE workspace_id = ? AND session_id = ? AND action_id = ?
192
+ AND state != 'closed'`);
193
+ this.updateRuntimeTurnStateStmt = this.db.prepare(`UPDATE pending_runtime_turns
194
+ SET state = ?,
195
+ lease_expires_at = COALESCE(?, lease_expires_at),
196
+ updated_at = ?
197
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
198
+ AND state NOT IN ('completed', 'terminalized')
199
+ AND (? IS NULL OR (owner_id = ? AND owner_generation = ?))`);
200
+ this.renewRuntimeTurnLeaseStmt = this.db.prepare(`UPDATE pending_runtime_turns
201
+ SET lease_expires_at = ?, updated_at = ?
202
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
203
+ AND state NOT IN ('completed', 'terminalized')
204
+ AND owner_id = ? AND owner_generation = ?`);
205
+ this.updateRuntimeTurnOpenModelRequestStartsStmt = this.db.prepare(`UPDATE pending_runtime_turns
206
+ SET open_model_request_start_ids = ?, updated_at = ?
207
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
208
+ AND state NOT IN ('completed', 'terminalized')
209
+ AND owner_id = ? AND owner_generation = ?`);
210
+ this.closeRuntimeTurnStmt = this.db.prepare(`UPDATE pending_runtime_turns
211
+ SET state = ?,
212
+ updated_at = ?,
213
+ open_model_request_start_ids = '[]',
214
+ completed_at = CASE WHEN ? = 'completed' THEN COALESCE(completed_at, ?) ELSE completed_at END,
215
+ terminalized_at = CASE WHEN ? = 'terminalized' THEN COALESCE(terminalized_at, ?) ELSE terminalized_at END
216
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
217
+ AND state NOT IN ('completed', 'terminalized')
218
+ AND (? IS NULL OR (owner_id = ? AND owner_generation = ?))`);
219
+ this.closeRuntimeActionsForTurnStmt = this.db.prepare(`UPDATE pending_runtime_actions
220
+ SET state = 'closed',
221
+ closed_at = COALESCE(closed_at, ?),
222
+ close_reason = COALESCE(close_reason, ?),
223
+ updated_at = ?
224
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
225
+ AND state != 'closed'`);
226
+ this.claimAcceptedRuntimeTurnStmt = this.db.prepare(`UPDATE pending_runtime_turns
227
+ SET owner_id = ?,
228
+ owner_generation = owner_generation + 1,
229
+ lease_expires_at = ?,
230
+ state = 'dispatching',
231
+ updated_at = ?
232
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
233
+ AND state = 'accepted'
234
+ AND (owner_id = ? OR lease_expires_at <= ?)`);
235
+ this.claimTerminalizingRuntimeTurnStmt = this.db.prepare(`UPDATE pending_runtime_turns
236
+ SET owner_id = ?,
237
+ owner_generation = owner_generation + 1,
238
+ lease_expires_at = ?,
239
+ state = 'terminalizing',
240
+ updated_at = ?
241
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?
242
+ AND state NOT IN ('completed', 'terminalized')
243
+ AND (owner_id = ? OR lease_expires_at <= ?)`);
244
+ this.retrieveRuntimeTurnStmt = this.db.prepare(`SELECT workspace_id, session_id, turn_id, owner_id, owner_generation,
245
+ lease_expires_at, state, trigger_event_ids, created_at, updated_at,
246
+ completed_at, terminalized_at, open_model_request_start_ids
247
+ FROM pending_runtime_turns
248
+ WHERE workspace_id = ? AND session_id = ? AND turn_id = ?`);
249
+ this.purgeExpiredIdempotencyKeysStmt = this.db.prepare(`DELETE FROM idempotency_keys
250
+ WHERE status IN ('completed', 'in_progress') AND expires_at <= ?`);
251
+ this.reserveIdempotencyKeyStmt = this.db.prepare(`INSERT INTO idempotency_keys
252
+ (workspace_id, method, concrete_path, idempotency_key, route_label,
253
+ fingerprint_sha256, status, created_at, updated_at, expires_at)
254
+ VALUES (?, ?, ?, ?, ?, ?, 'in_progress', ?, ?, ?)
255
+ ON CONFLICT(workspace_id, method, concrete_path, idempotency_key) DO NOTHING`);
256
+ this.retrieveIdempotencyKeyStmt = this.db.prepare(`SELECT workspace_id, method, concrete_path, idempotency_key, route_label,
257
+ fingerprint_sha256, status, response_status, response_body,
258
+ resource_type, resource_id, created_at, updated_at, expires_at
259
+ FROM idempotency_keys
260
+ WHERE workspace_id = ? AND method = ? AND concrete_path = ?
261
+ AND idempotency_key = ?`);
262
+ this.acquireAbandonedIdempotencyKeyStmt = this.db.prepare(`UPDATE idempotency_keys
263
+ SET updated_at = ?, expires_at = ?
264
+ WHERE workspace_id = ? AND method = ? AND concrete_path = ?
265
+ AND idempotency_key = ? AND fingerprint_sha256 = ?
266
+ AND status = 'in_progress' AND updated_at <= ?`);
267
+ this.completeIdempotencyKeyStmt = this.db.prepare(`UPDATE idempotency_keys
268
+ SET status = 'completed',
269
+ response_status = ?,
270
+ response_body = ?,
271
+ resource_type = ?,
272
+ resource_id = ?,
273
+ updated_at = ?,
274
+ expires_at = ?
275
+ WHERE workspace_id = ? AND method = ? AND concrete_path = ?
276
+ AND idempotency_key = ? AND fingerprint_sha256 = ?
277
+ AND status = 'in_progress'`);
278
+ this.releaseIdempotencyReservationStmt = this.db.prepare(`DELETE FROM idempotency_keys
279
+ WHERE workspace_id = ? AND method = ? AND concrete_path = ?
280
+ AND idempotency_key = ? AND fingerprint_sha256 = ?
281
+ AND status = 'in_progress'`);
282
+ this.refreshIdempotencyReservationStmt = this.db.prepare(`UPDATE idempotency_keys
283
+ SET updated_at = ?, expires_at = ?
284
+ WHERE workspace_id = ? AND method = ? AND concrete_path = ?
285
+ AND idempotency_key = ? AND fingerprint_sha256 = ?
286
+ AND status = 'in_progress'`);
287
+ }
288
+ /** Open a store backed by a SQLite file. Pass `:memory:` for in-memory. */
289
+ static open(path = ":memory:") {
290
+ return new EventStore(new DatabaseSync(path));
291
+ }
292
+ append(event) {
293
+ this.appendEvent(event);
294
+ }
295
+ appendBatch(events) {
296
+ this.appendBatchWithRuntimeChanges(events, {});
297
+ }
298
+ appendBatchWithRuntimeChanges(events, changes) {
299
+ this.withTransaction(() => {
300
+ this.appendBatchWithRuntimeChangesInTransaction(events, changes);
301
+ });
302
+ }
303
+ appendBatchWithRuntimeChangesAndCompleteIdempotency(events, changes, completion) {
304
+ this.withTransaction(() => {
305
+ this.appendBatchWithRuntimeChangesInTransaction(events, changes);
306
+ this.completeIdempotencyInTransaction(completion);
307
+ });
308
+ }
309
+ appendBatchWithRuntimeChangesInTransaction(events, changes) {
310
+ for (const event of events) {
311
+ this.appendEvent(event);
312
+ }
313
+ this.applyRuntimeChanges(changes);
314
+ }
315
+ completeIdempotencyInTransaction(completion) {
316
+ const result = this.completeIdempotencyKeyStmt.run(completion.responseStatus, JSON.stringify(completion.responseBody), completion.resourceType ?? null, completion.resourceId ?? null, completion.now, completion.expiresAt, completion.workspaceId, completion.method, completion.concretePath, completion.key, completion.fingerprintSha256);
317
+ if (result.changes === 0) {
318
+ throw new Error("Idempotency key reservation was not active at completion");
319
+ }
320
+ }
321
+ completeIdempotency(completion) {
322
+ this.withTransaction(() => {
323
+ this.completeIdempotencyInTransaction(completion);
324
+ });
325
+ }
326
+ releaseIdempotencyReservation(input) {
327
+ this.releaseIdempotencyReservationStmt.run(input.workspaceId, input.method, input.concretePath, input.key, input.fingerprintSha256);
328
+ }
329
+ refreshIdempotencyReservation(input) {
330
+ this.refreshIdempotencyReservationStmt.run(input.now, input.expiresAt, input.workspaceId, input.method, input.concretePath, input.key, input.fingerprintSha256);
331
+ }
332
+ reserveIdempotencyKey(input) {
333
+ // Policy lives here because this store owns the durable ledger rows. The
334
+ // service currently passes a conservative five-minute abandoned threshold.
335
+ // Reacquiring an abandoned in-progress row is safe because reservation
336
+ // commits before the domain transaction; if the completed response did not
337
+ // commit, the event/runtime side effect could not have committed either.
338
+ this.purgeExpiredIdempotencyKeysStmt.run(input.now);
339
+ const inserted = this.reserveIdempotencyKeyStmt.run(input.workspaceId, input.method, input.concretePath, input.key, input.routeLabel, input.fingerprintSha256, input.now, input.now, input.expiresAt);
340
+ if (inserted.changes === 1)
341
+ return { kind: "reserved" };
342
+ const row = this.retrieveIdempotencyKey(input);
343
+ if (!row)
344
+ return { kind: "reserved" };
345
+ if (row.fingerprint_sha256 !== input.fingerprintSha256) {
346
+ return { kind: "fingerprint_mismatch" };
347
+ }
348
+ if (row.status === "completed") {
349
+ if (row.response_status === null || row.response_body === null) {
350
+ throw new Error("Completed idempotency row is missing response data");
351
+ }
352
+ return {
353
+ kind: "replay",
354
+ responseStatus: row.response_status,
355
+ responseBody: JSON.parse(row.response_body),
356
+ };
357
+ }
358
+ if (row.status === "in_progress") {
359
+ const acquired = this.acquireAbandonedIdempotencyKeyStmt.run(input.now, input.expiresAt, input.workspaceId, input.method, input.concretePath, input.key, input.fingerprintSha256, input.abandonedBefore);
360
+ return acquired.changes === 1 ? { kind: "reserved" } : { kind: "in_progress" };
361
+ }
362
+ throw new Error(`Unknown idempotency key status: ${row.status}`);
363
+ }
364
+ deleteForSession(workspaceId, sessionId) {
365
+ this.withTransaction(() => {
366
+ this.deleteRuntimeActionsForSessionStmt.run(workspaceId, sessionId);
367
+ this.deleteRuntimeTurnsForSessionStmt.run(workspaceId, sessionId);
368
+ this.deleteForSessionStmt.run(workspaceId, sessionId);
369
+ this.deleteIdempotencyKeysForSessionStmt.run(workspaceId, `/v1/sessions/${sessionId}/events`);
370
+ this.deleteIdempotencyKeysForResourceStmt.run(workspaceId, "session", sessionId);
371
+ });
372
+ }
373
+ withTransaction(fn) {
374
+ this.txDepth += 1;
375
+ try {
376
+ return withSqliteTransaction(this.db, fn);
377
+ }
378
+ catch (error) {
379
+ // This savepoint rolled back; the recorded changes never committed.
380
+ // (Dropping ALL pending observations undercounts if a sibling nested
381
+ // transaction already succeeded — the safe direction for a counter.)
382
+ this.pendingRuntimeChangeObservations.length = 0;
383
+ throw error;
384
+ }
385
+ finally {
386
+ this.txDepth -= 1;
387
+ if (this.txDepth === 0)
388
+ this.flushRuntimeChangeObservations();
389
+ }
390
+ }
391
+ // Post-commit observation seam for /metrics (plan 0121 §2): fires AFTER
392
+ // the outermost store transaction releases, never inside it, so metrics
393
+ // can't alter commit semantics. One seam covers every apply path — the
394
+ // service's direct appends, persist.ts helpers, and both runtime-event
395
+ // coordinators. Caveat (documented, accepted): when an EventStore
396
+ // transaction is nested inside another store's savepoint on the same
397
+ // connection, the observer fires before that OUTER commit — a rare
398
+ // rollback there overcounts a turn; gauges self-correct from DB truth.
399
+ setRuntimeChangesObserver(observer) {
400
+ this.runtimeChangesObserver = observer;
401
+ }
402
+ flushRuntimeChangeObservations() {
403
+ if (this.pendingRuntimeChangeObservations.length === 0)
404
+ return;
405
+ const batches = this.pendingRuntimeChangeObservations.splice(0);
406
+ const observer = this.runtimeChangesObserver;
407
+ if (observer === undefined)
408
+ return;
409
+ for (const changes of batches) {
410
+ try {
411
+ observer(changes);
412
+ }
413
+ catch {
414
+ // Observers are telemetry; they must never fail a request.
415
+ }
416
+ }
417
+ }
418
+ list(workspaceId, sessionId, opts = {}) {
419
+ return this.listPage(workspaceId, sessionId, {
420
+ ...opts,
421
+ limit: opts.limit ?? 1000,
422
+ }).data;
423
+ }
424
+ listPage(workspaceId, sessionId, opts = {}) {
425
+ const cursor = resolveCursor(opts);
426
+ if (cursor === "") {
427
+ return { data: [], next_page: null };
428
+ }
429
+ const order = resolveOrder(opts);
430
+ const limit = normalizeLimit(opts.limit);
431
+ const stmt = this.listStmt({
432
+ hasCursor: cursor !== undefined,
433
+ order,
434
+ types: opts.types ?? [],
435
+ });
436
+ const rows = stmt.all(...selectListArgs(workspaceId, sessionId, cursor, limit + 1, opts.types));
437
+ const data = rows.slice(0, limit).map(deserialize);
438
+ return {
439
+ data,
440
+ next_page: rows.length > limit ? data[data.length - 1]?.id ?? null : null,
441
+ };
442
+ }
443
+ retrieve(workspaceId, id) {
444
+ const row = this.retrieveStmt.get(workspaceId, id);
445
+ return row ? deserialize(row) : undefined;
446
+ }
447
+ findRuntimeAction(workspaceId, sessionId, actionId) {
448
+ const row = this.findRuntimeActionStmt.get(workspaceId, sessionId, actionId);
449
+ return row ? deserializeRuntimeAction(row) : undefined;
450
+ }
451
+ listPendingRuntimeTurns(workspaceId) {
452
+ const rows = this.listPendingRuntimeTurnsStmt.all(workspaceId);
453
+ return rows.map(deserializeRuntimeTurn);
454
+ }
455
+ countPendingRuntimeTurns(workspaceId) {
456
+ return this.countPendingRuntimeTurnsStmt.get(workspaceId).n;
457
+ }
458
+ // Unscoped, for the /metrics gauge (0121 C2). Bounded by in-flight work,
459
+ // so no dedicated index is needed.
460
+ countAllPendingRuntimeTurns() {
461
+ return this.countAllPendingRuntimeTurnsStmt.get().n;
462
+ }
463
+ listWorkspaceIdsWithPendingRuntimeTurns() {
464
+ const rows = this.listWorkspacesWithPendingRuntimeTurnsStmt.all();
465
+ return rows.map((row) => row.workspace_id);
466
+ }
467
+ claimAcceptedRuntimeTurnForRecovery(claim) {
468
+ const result = this.claimAcceptedRuntimeTurnStmt.run(claim.ownerId, claim.leaseExpiresAt, claim.now, claim.workspaceId, claim.sessionId, claim.turnId, claim.ownerId, claim.now);
469
+ if (result.changes === 0)
470
+ return undefined;
471
+ return this.retrieveRuntimeTurn(claim.workspaceId, claim.sessionId, claim.turnId);
472
+ }
473
+ claimRuntimeTurnForTerminalization(claim) {
474
+ const result = this.claimTerminalizingRuntimeTurnStmt.run(claim.ownerId, claim.leaseExpiresAt, claim.now, claim.workspaceId, claim.sessionId, claim.turnId, claim.ownerId, claim.now);
475
+ if (result.changes === 0)
476
+ return undefined;
477
+ return this.retrieveRuntimeTurn(claim.workspaceId, claim.sessionId, claim.turnId);
478
+ }
479
+ listRuntimeActionsForTurn(workspaceId, sessionId, turnId) {
480
+ const rows = this.listRuntimeActionsForTurnStmt.all(workspaceId, sessionId, turnId);
481
+ return rows.map(deserializeRuntimeAction);
482
+ }
483
+ retrieveRuntimeTurn(workspaceId, sessionId, turnId) {
484
+ const row = this.retrieveRuntimeTurnStmt.get(workspaceId, sessionId, turnId);
485
+ return row ? deserializeRuntimeTurn(row) : undefined;
486
+ }
487
+ retrieveIdempotencyKey(input) {
488
+ return this.retrieveIdempotencyKeyStmt.get(input.workspaceId, input.method, input.concretePath, input.key);
489
+ }
490
+ close() {
491
+ this.db.close();
492
+ }
493
+ appendEvent(event) {
494
+ this.appendStmt.run(event.id, event.workspace_id, event.session_id, event.type, event.processed_at, JSON.stringify(event.payload), event.created_at);
495
+ }
496
+ applyRuntimeChanges(changes) {
497
+ if (this.runtimeChangesObserver !== undefined &&
498
+ ((changes.acceptedTurns?.length ?? 0) > 0 ||
499
+ (changes.closedTurns?.length ?? 0) > 0)) {
500
+ this.pendingRuntimeChangeObservations.push(changes);
501
+ }
502
+ for (const turn of changes.acceptedTurns ?? []) {
503
+ this.insertRuntimeTurnStmt.run(turn.workspaceId, turn.sessionId, turn.turnId, turn.ownerId, turn.ownerGeneration, turn.leaseExpiresAt, JSON.stringify([...turn.triggerEventIds]), turn.now, turn.now);
504
+ }
505
+ for (const action of changes.openedActions ?? []) {
506
+ this.insertRuntimeActionStmt.run(action.workspaceId, action.sessionId, action.turnId, action.actionId, action.actionType, action.now, action.now);
507
+ }
508
+ for (const action of changes.acknowledgedActions ?? []) {
509
+ this.acknowledgeRuntimeActionStmt.run(action.now, action.now, action.workspaceId, action.sessionId, action.actionId);
510
+ }
511
+ for (const action of changes.closedActions ?? []) {
512
+ this.closeRuntimeActionStmt.run(action.now, action.reason, action.now, action.workspaceId, action.sessionId, action.actionId);
513
+ }
514
+ for (const turn of changes.turnStates ?? []) {
515
+ const result = this.updateRuntimeTurnStateStmt.run(turn.state, turn.leaseExpiresAt ?? null, turn.now, turn.workspaceId, turn.sessionId, turn.turnId, turn.ownerId ?? null, turn.ownerId ?? null, turn.ownerGeneration ?? null);
516
+ if (turn.ownerId !== undefined && result.changes === 0) {
517
+ throw new RuntimeTurnOwnershipLostError(turn.turnId);
518
+ }
519
+ }
520
+ for (const turn of changes.leaseRenewals ?? []) {
521
+ const result = this.renewRuntimeTurnLeaseStmt.run(turn.leaseExpiresAt, turn.now, turn.workspaceId, turn.sessionId, turn.turnId, turn.ownerId, turn.ownerGeneration);
522
+ if (result.changes === 0) {
523
+ throw new RuntimeTurnOwnershipLostError(turn.turnId);
524
+ }
525
+ }
526
+ for (const turn of changes.openedModelRequestStarts ?? []) {
527
+ this.updateRuntimeTurnOpenModelRequestStarts(turn, (current) => [
528
+ ...current,
529
+ turn.startEventId,
530
+ ]);
531
+ }
532
+ for (const turn of changes.closedModelRequestStarts ?? []) {
533
+ this.updateRuntimeTurnOpenModelRequestStarts(turn, (current) => {
534
+ const index = current.lastIndexOf(turn.startEventId);
535
+ return index === -1
536
+ ? current
537
+ : [...current.slice(0, index), ...current.slice(index + 1)];
538
+ });
539
+ }
540
+ for (const turn of changes.closedTurns ?? []) {
541
+ const result = this.closeRuntimeTurnStmt.run(turn.state, turn.now, turn.state, turn.now, turn.state, turn.now, turn.workspaceId, turn.sessionId, turn.turnId, turn.ownerId ?? null, turn.ownerId ?? null, turn.ownerGeneration ?? null);
542
+ if (turn.ownerId !== undefined && result.changes === 0) {
543
+ throw new RuntimeTurnOwnershipLostError(turn.turnId);
544
+ }
545
+ this.closeRuntimeActionsForTurnStmt.run(turn.now, turn.reason, turn.now, turn.workspaceId, turn.sessionId, turn.turnId);
546
+ }
547
+ }
548
+ updateRuntimeTurnOpenModelRequestStarts(turn, update) {
549
+ const existing = this.retrieveRuntimeTurn(turn.workspaceId, turn.sessionId, turn.turnId);
550
+ const current = existing?.open_model_request_start_ids ?? [];
551
+ const next = update(current);
552
+ const result = this.updateRuntimeTurnOpenModelRequestStartsStmt.run(JSON.stringify(next), turn.now, turn.workspaceId, turn.sessionId, turn.turnId, turn.ownerId, turn.ownerGeneration);
553
+ if (result.changes === 0) {
554
+ throw new RuntimeTurnOwnershipLostError(turn.turnId);
555
+ }
556
+ }
557
+ listStmt(opts) {
558
+ const key = JSON.stringify({
559
+ hasCursor: opts.hasCursor,
560
+ order: opts.order,
561
+ typeCount: opts.types.length,
562
+ });
563
+ const existing = this.listStmts.get(key);
564
+ if (existing)
565
+ return existing;
566
+ const predicates = ["workspace_id = ?", "session_id = ?"];
567
+ if (opts.types.length > 0) {
568
+ predicates.push(`type IN (${opts.types.map(() => "?").join(", ")})`);
569
+ }
570
+ if (opts.hasCursor) {
571
+ predicates.push(opts.order === "asc" ? "id > ?" : "id < ?");
572
+ }
573
+ const stmt = this.db.prepare(`SELECT id, workspace_id, session_id, type, processed_at, payload, created_at
574
+ FROM events
575
+ WHERE ${predicates.join(" AND ")}
576
+ ORDER BY id ${opts.order.toUpperCase()}
577
+ LIMIT ?`);
578
+ this.listStmts.set(key, stmt);
579
+ return stmt;
580
+ }
581
+ }
582
+ function selectListArgs(workspaceId, sessionId, cursor, limit, types) {
583
+ const t = types ?? [];
584
+ if (cursor === undefined) {
585
+ return t.length === 0
586
+ ? [workspaceId, sessionId, limit]
587
+ : [workspaceId, sessionId, ...t, limit];
588
+ }
589
+ return t.length === 0
590
+ ? [workspaceId, sessionId, cursor, limit]
591
+ : [workspaceId, sessionId, ...t, cursor, limit];
592
+ }
593
+ function resolveCursor(opts) {
594
+ return opts.page ?? opts.afterId;
595
+ }
596
+ function resolveOrder(opts) {
597
+ if (opts.afterId !== undefined && opts.order === undefined) {
598
+ return "asc";
599
+ }
600
+ return opts.order ?? "asc";
601
+ }
602
+ function normalizeLimit(limit) {
603
+ if (limit === undefined)
604
+ return 20;
605
+ if (!Number.isSafeInteger(limit) || limit <= 0)
606
+ return 20;
607
+ return Math.min(limit, 1000);
608
+ }
609
+ function deserialize(row) {
610
+ return {
611
+ id: row.id,
612
+ workspace_id: row.workspace_id,
613
+ session_id: row.session_id,
614
+ type: row.type,
615
+ processed_at: row.processed_at,
616
+ payload: JSON.parse(row.payload),
617
+ created_at: row.created_at,
618
+ };
619
+ }
620
+ function runtimeActionSelectSql(whereClause) {
621
+ return `
622
+ SELECT
623
+ a.workspace_id,
624
+ a.session_id,
625
+ a.turn_id,
626
+ a.action_id,
627
+ a.action_type,
628
+ a.state AS action_state,
629
+ a.acknowledged_at,
630
+ a.closed_at,
631
+ a.close_reason,
632
+ a.created_at AS action_created_at,
633
+ a.updated_at AS action_updated_at,
634
+ t.owner_id,
635
+ t.owner_generation,
636
+ t.lease_expires_at,
637
+ t.state AS turn_state,
638
+ t.trigger_event_ids,
639
+ t.open_model_request_start_ids,
640
+ t.created_at AS turn_created_at,
641
+ t.updated_at AS turn_updated_at,
642
+ t.completed_at,
643
+ t.terminalized_at
644
+ FROM pending_runtime_actions a
645
+ JOIN pending_runtime_turns t
646
+ ON t.workspace_id = a.workspace_id
647
+ AND t.session_id = a.session_id
648
+ AND t.turn_id = a.turn_id
649
+ ${whereClause}
650
+ `;
651
+ }
652
+ function deserializeRuntimeAction(row) {
653
+ return {
654
+ workspace_id: row.workspace_id,
655
+ session_id: row.session_id,
656
+ turn_id: row.turn_id,
657
+ action_id: row.action_id,
658
+ action_type: row.action_type,
659
+ state: row.action_state,
660
+ acknowledged_at: row.acknowledged_at,
661
+ closed_at: row.closed_at,
662
+ close_reason: row.close_reason,
663
+ created_at: row.action_created_at,
664
+ updated_at: row.action_updated_at,
665
+ turn: {
666
+ workspace_id: row.workspace_id,
667
+ session_id: row.session_id,
668
+ turn_id: row.turn_id,
669
+ owner_id: row.owner_id,
670
+ owner_generation: row.owner_generation,
671
+ lease_expires_at: row.lease_expires_at,
672
+ state: row.turn_state,
673
+ trigger_event_ids: parseTriggerEventIds(row.trigger_event_ids),
674
+ open_model_request_start_ids: parseStringArray(row.open_model_request_start_ids),
675
+ created_at: row.turn_created_at,
676
+ updated_at: row.turn_updated_at,
677
+ completed_at: row.completed_at,
678
+ terminalized_at: row.terminalized_at,
679
+ },
680
+ };
681
+ }
682
+ function deserializeRuntimeTurn(row) {
683
+ return {
684
+ workspace_id: row.workspace_id,
685
+ session_id: row.session_id,
686
+ turn_id: row.turn_id,
687
+ owner_id: row.owner_id,
688
+ owner_generation: row.owner_generation,
689
+ lease_expires_at: row.lease_expires_at,
690
+ state: row.state,
691
+ trigger_event_ids: parseTriggerEventIds(row.trigger_event_ids),
692
+ open_model_request_start_ids: parseStringArray(row.open_model_request_start_ids),
693
+ created_at: row.created_at,
694
+ updated_at: row.updated_at,
695
+ completed_at: row.completed_at,
696
+ terminalized_at: row.terminalized_at,
697
+ };
698
+ }
699
+ function parseTriggerEventIds(value) {
700
+ return parseStringArray(value);
701
+ }
702
+ function parseStringArray(value) {
703
+ const parsed = JSON.parse(value);
704
+ return Array.isArray(parsed) && parsed.every((item) => typeof item === "string")
705
+ ? parsed
706
+ : [];
707
+ }
708
+ function ensureWorkspaceIdColumn(db) {
709
+ const columns = db.prepare("PRAGMA table_info(events)").all();
710
+ if (columns.some((column) => column.name === "workspace_id"))
711
+ return;
712
+ const existing = db.prepare("SELECT COUNT(*) AS count FROM events").get();
713
+ db.exec("BEGIN");
714
+ try {
715
+ db.exec("ALTER TABLE events ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'wrk_default'");
716
+ if (existing.count === 0) {
717
+ db.exec("COMMIT");
718
+ return;
719
+ }
720
+ if (hasTable(db, "sessions")) {
721
+ db.exec(`
722
+ UPDATE events
723
+ SET workspace_id = (
724
+ SELECT sessions.workspace_id
725
+ FROM sessions
726
+ WHERE sessions.id = events.session_id
727
+ )
728
+ WHERE EXISTS (
729
+ SELECT 1 FROM sessions WHERE sessions.id = events.session_id
730
+ )
731
+ `);
732
+ const unmapped = db.prepare(`
733
+ SELECT COUNT(*) AS count
734
+ FROM events
735
+ WHERE NOT EXISTS (
736
+ SELECT 1 FROM sessions WHERE sessions.id = events.session_id
737
+ )
738
+ `).get();
739
+ if (unmapped.count === 0) {
740
+ db.exec("COMMIT");
741
+ return;
742
+ }
743
+ }
744
+ throw new Error("Cannot automatically migrate legacy events without workspace_id; run an explicit workspace backfill first.");
745
+ }
746
+ catch (error) {
747
+ db.exec("ROLLBACK");
748
+ throw error;
749
+ }
750
+ }
751
+ function ensureOpenModelRequestStartIdsColumn(db) {
752
+ const columns = db.prepare("PRAGMA table_info(pending_runtime_turns)").all();
753
+ if (columns.some((column) => column.name === "open_model_request_start_ids")) {
754
+ return;
755
+ }
756
+ db.exec("ALTER TABLE pending_runtime_turns ADD COLUMN open_model_request_start_ids TEXT NOT NULL DEFAULT '[]'");
757
+ }
758
+ function ensureIdempotencyResourceColumns(db) {
759
+ const columns = db.prepare("PRAGMA table_info(idempotency_keys)").all();
760
+ if (!columns.some((column) => column.name === "resource_type")) {
761
+ db.exec("ALTER TABLE idempotency_keys ADD COLUMN resource_type TEXT");
762
+ }
763
+ if (!columns.some((column) => column.name === "resource_id")) {
764
+ db.exec("ALTER TABLE idempotency_keys ADD COLUMN resource_id TEXT");
765
+ }
766
+ }
767
+ function hasTable(db, name) {
768
+ const row = db
769
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
770
+ .get(name);
771
+ return row !== undefined;
772
+ }