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.
- package/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- 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
|
+
}
|