@inflexa-ai/harness 0.41.1 → 0.41.2
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/dist/contracts/report-blocks.d.ts +10 -10
- package/dist/contracts/report-reference.d.ts +4 -4
- package/dist/memory/message-backfill.d.ts +8 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +12 -0
- package/dist/report-model/authoring-grammar.d.ts +4 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +3 -3
- package/dist/state/init.d.ts +7 -6
- package/dist/state/init.js +51 -672
- package/dist/state/migrations/20260927120000_baseline.d.ts +11 -0
- package/dist/state/migrations/20260927120000_baseline.js +675 -0
- package/dist/state/migrations/index.d.ts +10 -0
- package/dist/state/migrations/index.js +13 -0
- package/dist/tools/display/show-user.js +6 -4
- package/dist/tools/workspace/show-file.js +4 -3
- package/package.json +2 -1
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema of the Cortex state tables when the harness adopted versioned
|
|
3
|
+
* migrations.
|
|
4
|
+
*
|
|
5
|
+
* Each statement is idempotent. Thus the baseline also applies to a database
|
|
6
|
+
* that an earlier harness created with its boot-time DDL: it brings that
|
|
7
|
+
* database to this schema, and the migrator records it.
|
|
8
|
+
*/
|
|
9
|
+
import { CompiledQuery, sql } from "kysely";
|
|
10
|
+
import { backfillAiSdkMessageEnvelopes } from "../../memory/message-backfill.js";
|
|
11
|
+
// No ';' may appear anywhere in this text, comments included: it is executed by
|
|
12
|
+
// splitting on ';' (see `DDL.split` below), so a semicolon inside a comment cuts
|
|
13
|
+
// the statement it precedes in half and Postgres rejects the fragment.
|
|
14
|
+
const DDL = `
|
|
15
|
+
-- Analysis-level state (status, context, data-profile tracking).
|
|
16
|
+
--
|
|
17
|
+
-- User identity is derived from the ambient credential's JWT sub claim
|
|
18
|
+
-- at request time — not persisted in this table.
|
|
19
|
+
CREATE TABLE IF NOT EXISTS cortex_analysis_state (
|
|
20
|
+
analysis_id TEXT PRIMARY KEY,
|
|
21
|
+
status TEXT NOT NULL,
|
|
22
|
+
context TEXT,
|
|
23
|
+
-- Nullable: NULL is the honest "no profile" state, which loadDataProfileStatus
|
|
24
|
+
-- collapses to null so a cleared profile is indistinguishable from one that never
|
|
25
|
+
-- existed. The 'pending' default still applies to a row inserted without a status --
|
|
26
|
+
-- a freshly-seeded analysis awaiting its first profile.
|
|
27
|
+
data_profile_status TEXT DEFAULT 'pending',
|
|
28
|
+
data_profile_error TEXT,
|
|
29
|
+
data_profile_started_at TEXT,
|
|
30
|
+
data_profile_completed_at TEXT,
|
|
31
|
+
data_profile_result JSONB,
|
|
32
|
+
data_profile_workflow_id TEXT,
|
|
33
|
+
created_at TEXT NOT NULL,
|
|
34
|
+
updated_at TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
-- DBOS workflow id of the profile attempt that owns the row — what a consumer
|
|
38
|
+
-- subscribes to in order to observe that profile's activity. Written by the
|
|
39
|
+
-- workflow body, not the trigger: the ledger claim happens before the workflow id
|
|
40
|
+
-- is minted, so only the body knows the id of the attempt that actually started.
|
|
41
|
+
-- Nullable, and absence is a normal state rather than a defect, with two ordinary
|
|
42
|
+
-- causes: a row claimed whose body has not yet recorded its id, and a row written
|
|
43
|
+
-- before this column existed. Both mean "this profile's stream is not
|
|
44
|
+
-- addressable", which is true in each case — hence no backfill.
|
|
45
|
+
ALTER TABLE cortex_analysis_state
|
|
46
|
+
ADD COLUMN IF NOT EXISTS data_profile_workflow_id TEXT;
|
|
47
|
+
|
|
48
|
+
-- File registry — artifact_id (provenance) + file_id (S3), cross-run.
|
|
49
|
+
CREATE TABLE IF NOT EXISTS cortex_artifacts (
|
|
50
|
+
analysis_id TEXT NOT NULL,
|
|
51
|
+
path TEXT NOT NULL,
|
|
52
|
+
hash TEXT NOT NULL,
|
|
53
|
+
size BIGINT NOT NULL,
|
|
54
|
+
role TEXT NOT NULL,
|
|
55
|
+
source_step TEXT,
|
|
56
|
+
source_run TEXT,
|
|
57
|
+
artifact_id TEXT,
|
|
58
|
+
file_id TEXT,
|
|
59
|
+
created_at TEXT NOT NULL,
|
|
60
|
+
PRIMARY KEY (analysis_id, path)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_artifacts_run
|
|
64
|
+
ON cortex_artifacts(analysis_id, source_run);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_artifacts_artifact
|
|
66
|
+
ON cortex_artifacts(analysis_id, artifact_id);
|
|
67
|
+
|
|
68
|
+
-- Run ledger — one row per workflow execution.
|
|
69
|
+
CREATE TABLE IF NOT EXISTS cortex_runs (
|
|
70
|
+
run_id TEXT PRIMARY KEY,
|
|
71
|
+
analysis_id TEXT NOT NULL,
|
|
72
|
+
thread_id TEXT,
|
|
73
|
+
workflow_name TEXT NOT NULL,
|
|
74
|
+
status TEXT NOT NULL,
|
|
75
|
+
started_at TEXT NOT NULL,
|
|
76
|
+
completed_at TEXT,
|
|
77
|
+
error TEXT,
|
|
78
|
+
-- Run-level synthesis outcome. NULL means "unknown": a legacy row written
|
|
79
|
+
-- before these columns existed, or a run whose synthesis never recorded a result.
|
|
80
|
+
synthesis_status TEXT,
|
|
81
|
+
synthesis_reason TEXT,
|
|
82
|
+
parts JSONB,
|
|
83
|
+
mandate_jti TEXT, -- oss-core-managed-ok run-mandate ledger, nullable, OSS leaves null
|
|
84
|
+
mandate_expires_at TEXT -- oss-core-managed-ok run-mandate ledger, nullable, OSS leaves null
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_runs_analysis
|
|
88
|
+
ON cortex_runs(analysis_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_runs_thread
|
|
90
|
+
ON cortex_runs(thread_id);
|
|
91
|
+
|
|
92
|
+
-- Step execution ledger — one row per step execution.
|
|
93
|
+
CREATE TABLE IF NOT EXISTS cortex_step_executions (
|
|
94
|
+
run_id TEXT NOT NULL,
|
|
95
|
+
step_id TEXT NOT NULL,
|
|
96
|
+
analysis_id TEXT NOT NULL,
|
|
97
|
+
wave INTEGER NOT NULL,
|
|
98
|
+
agent_id TEXT NOT NULL,
|
|
99
|
+
status TEXT NOT NULL,
|
|
100
|
+
started_at TEXT,
|
|
101
|
+
completed_at TEXT,
|
|
102
|
+
duration_ms BIGINT,
|
|
103
|
+
error TEXT,
|
|
104
|
+
attempts INTEGER NOT NULL DEFAULT 1,
|
|
105
|
+
last_error_class TEXT,
|
|
106
|
+
finish_reason TEXT,
|
|
107
|
+
hit_max_steps INTEGER NOT NULL DEFAULT 0,
|
|
108
|
+
blocked_reason TEXT,
|
|
109
|
+
PRIMARY KEY (run_id, step_id)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_step_exec_analysis
|
|
113
|
+
ON cortex_step_executions(analysis_id);
|
|
114
|
+
|
|
115
|
+
-- Analysis plans — persisted by generatePlan, consumed by showUser and executePlan.
|
|
116
|
+
-- Immutable, append-only. Iteration produces a new row linked via parent_plan_id.
|
|
117
|
+
CREATE TABLE IF NOT EXISTS cortex_plans (
|
|
118
|
+
plan_id TEXT PRIMARY KEY,
|
|
119
|
+
analysis_id TEXT NOT NULL
|
|
120
|
+
REFERENCES cortex_analysis_state(analysis_id)
|
|
121
|
+
ON DELETE CASCADE,
|
|
122
|
+
plan JSONB NOT NULL,
|
|
123
|
+
parent_plan_id TEXT
|
|
124
|
+
REFERENCES cortex_plans(plan_id)
|
|
125
|
+
ON DELETE SET NULL,
|
|
126
|
+
created_at TEXT NOT NULL
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_plans_analysis
|
|
130
|
+
ON cortex_plans(analysis_id, created_at DESC);
|
|
131
|
+
|
|
132
|
+
-- Report versions — one row for each recorded report version. A row holds the
|
|
133
|
+
-- block document, the pinned snapshot, and the anchor. A thread holds at most one
|
|
134
|
+
-- version. The named unique constraint on thread_id enforces the rule, because
|
|
135
|
+
-- the report policy binds one version to each report session. A record on a
|
|
136
|
+
-- thread that holds a version replaces that row whole, under the same version_id
|
|
137
|
+
-- and the same created_at. The row outlives its thread. Thus the table has no
|
|
138
|
+
-- foreign key to cortex_analysis_threads, and the row carries the anchor columns
|
|
139
|
+
-- (thread_id, parent_thread_id, parent_seq) directly. A purge of the analysis
|
|
140
|
+
-- removes the row through the analysis_id cascade.
|
|
141
|
+
CREATE TABLE IF NOT EXISTS cortex_report_versions (
|
|
142
|
+
version_id TEXT PRIMARY KEY,
|
|
143
|
+
analysis_id TEXT NOT NULL
|
|
144
|
+
REFERENCES cortex_analysis_state(analysis_id)
|
|
145
|
+
ON DELETE CASCADE,
|
|
146
|
+
thread_id TEXT NOT NULL,
|
|
147
|
+
-- Nullable pair: a root conversation anchor can be absent. A version with no
|
|
148
|
+
-- parent thread carries no parent_seq either.
|
|
149
|
+
parent_thread_id TEXT,
|
|
150
|
+
parent_seq BIGINT,
|
|
151
|
+
parent_version_id TEXT
|
|
152
|
+
REFERENCES cortex_report_versions(version_id)
|
|
153
|
+
ON DELETE SET NULL,
|
|
154
|
+
document JSONB NOT NULL,
|
|
155
|
+
snapshot JSONB NOT NULL,
|
|
156
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
157
|
+
CONSTRAINT cortex_report_versions_one_per_thread UNIQUE (thread_id)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_report_versions_analysis
|
|
161
|
+
ON cortex_report_versions(analysis_id);
|
|
162
|
+
|
|
163
|
+
-- Report session state — one mutable row for each report session thread. The row
|
|
164
|
+
-- holds the in-progress draft document and the pinned snapshot of the session. The
|
|
165
|
+
-- thread id is the primary key, thus one session binds to one thread. The document
|
|
166
|
+
-- column and the snapshot column are each nullable, because the pin writes the
|
|
167
|
+
-- snapshot first and the document lands later. A purge of the analysis removes the
|
|
168
|
+
-- row through the analysis_id cascade. The explicit purge delete stays a backstop,
|
|
169
|
+
-- the same as the plans delete, because a database whose table predates the key
|
|
170
|
+
-- never acquires one. The thread id carries no foreign key, the same as the
|
|
171
|
+
-- messages rows, thus a purge of a thread cannot reach this row through a cascade
|
|
172
|
+
-- and purgeThread removes it by name.
|
|
173
|
+
--
|
|
174
|
+
-- The two hash columns are the look-before-record markers. rendered_document_hash
|
|
175
|
+
-- holds the hash of the draft that the last preview rendered. seen_document_hash
|
|
176
|
+
-- holds the hash that the last eyes capture saw, copied from the rendered hash.
|
|
177
|
+
-- Each is nullable, because a fresh session has run no preview and no eyes. The
|
|
178
|
+
-- record refuses unless the seen hash equals the hash of the current draft.
|
|
179
|
+
--
|
|
180
|
+
-- The derivations column holds the derivation list of the session. One record
|
|
181
|
+
-- carries the derived output path, the output hash, the source paths with their
|
|
182
|
+
-- hashes, the script hash, and the script text. The column is nullable, because a
|
|
183
|
+
-- session that derived nothing holds no list, and the read gives an empty list for
|
|
184
|
+
-- such a row. The served membership merges the records onto the pin, and the pin
|
|
185
|
+
-- itself never changes.
|
|
186
|
+
CREATE TABLE IF NOT EXISTS cortex_report_session_state (
|
|
187
|
+
thread_id TEXT PRIMARY KEY,
|
|
188
|
+
analysis_id TEXT NOT NULL
|
|
189
|
+
REFERENCES cortex_analysis_state(analysis_id)
|
|
190
|
+
ON DELETE CASCADE,
|
|
191
|
+
document JSONB,
|
|
192
|
+
snapshot JSONB,
|
|
193
|
+
derivations JSONB,
|
|
194
|
+
rendered_document_hash TEXT,
|
|
195
|
+
seen_document_hash TEXT,
|
|
196
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_report_session_state_analysis
|
|
200
|
+
ON cortex_report_session_state(analysis_id);
|
|
201
|
+
|
|
202
|
+
-- Regulatory guidance corpus — pre-indexed FDA CDER, FDA CBER, and ICH
|
|
203
|
+
-- guidance documents. Written/refreshed via tasks/refresh-regulatory-corpus.ts
|
|
204
|
+
-- and searched (top-K cosine) via lib/regulatory-corpus.ts. No synthesis tool
|
|
205
|
+
-- consumes it yet — a search_regulatory_guidance tool is not wired.
|
|
206
|
+
CREATE TABLE IF NOT EXISTS cortex_regulatory_chunks (
|
|
207
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
208
|
+
source TEXT NOT NULL,
|
|
209
|
+
doc_id TEXT NOT NULL,
|
|
210
|
+
doc_title TEXT NOT NULL,
|
|
211
|
+
doc_url TEXT NOT NULL,
|
|
212
|
+
section TEXT,
|
|
213
|
+
chunk_index INTEGER NOT NULL,
|
|
214
|
+
chunk_text TEXT NOT NULL,
|
|
215
|
+
embedding vector(1536),
|
|
216
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
217
|
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_source_doc
|
|
221
|
+
ON cortex_regulatory_chunks(source, doc_id);
|
|
222
|
+
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
|
|
224
|
+
ON cortex_regulatory_chunks USING gin (metadata);
|
|
225
|
+
|
|
226
|
+
-- Conversation message store — the harness ThreadHistory module's table.
|
|
227
|
+
-- One row per message, with seq monotonic per thread. message_envelope holds
|
|
228
|
+
-- AI SDK ModelMessage envelopes and tokens is counted at write time, so the
|
|
229
|
+
-- read path never tokenizes. Legacy role/content_jsonb columns are nullable
|
|
230
|
+
-- and write-frozen — they exist only for startup backfill/inspection and
|
|
231
|
+
-- should be dropped after the migration window. Conversation-scoped only —
|
|
232
|
+
-- workflow and sandbox agent loops use the DBOS step cache, never this
|
|
233
|
+
-- table (see the harness-thread-store spec). The (thread_id, seq) primary key already serves
|
|
234
|
+
-- (thread_id, seq DESC) reads, so no separate index is needed.
|
|
235
|
+
--
|
|
236
|
+
-- message_envelope is JSON, not JSONB, and must stay that way. JSONB stores a
|
|
237
|
+
-- re-sorted parse tree rather than the document it was given, so a tool call's
|
|
238
|
+
-- input — and every other free-form payload, which has no schema to be
|
|
239
|
+
-- normalized back against on read — comes back key-reordered. Those bytes are
|
|
240
|
+
-- re-sent to the provider on the next turn, where a reordered prefix misses the
|
|
241
|
+
-- prompt cache. JSON stores the text verbatim. Nothing indexes this column, and
|
|
242
|
+
-- the JSON operators retractLastTurn uses work the same on either type.
|
|
243
|
+
--
|
|
244
|
+
-- reported_usage holds a whole turn's TokenUsageRollup — what providers reported
|
|
245
|
+
-- for the turn this row completed — on the LAST assistant row of an older turn
|
|
246
|
+
-- only. cortex_thread_turns holds the figures of each later turn.
|
|
247
|
+
-- It is NOT the tokens column beside it, and the two must never be read for
|
|
248
|
+
-- each other: tokens is an offline js-tiktoken estimate stamped on EVERY row so
|
|
249
|
+
-- the read path can window by budget without a provider round trip, and it exists
|
|
250
|
+
-- for rows no provider ever saw. reported_usage is a provider's own report, and
|
|
251
|
+
-- covers a turn rather than a row. Both are plausible-looking integers on one row,
|
|
252
|
+
-- which is exactly why the column names carry the distinction.
|
|
253
|
+
--
|
|
254
|
+
-- Nullable with no default: absent means nothing was reported, and a zero would
|
|
255
|
+
-- claim a turn cost nothing. A row written before this column existed is absent
|
|
256
|
+
-- for the same reason, so the migration needs no backfill.
|
|
257
|
+
--
|
|
258
|
+
-- JSONB here, unlike message_envelope above. The verbatim-bytes requirement that
|
|
259
|
+
-- forces JSON on the envelope is about model content that is re-sent to the
|
|
260
|
+
-- provider. This record is harness-generated, has a fixed field set, and never
|
|
261
|
+
-- reaches a prompt, so it does not share that requirement.
|
|
262
|
+
--
|
|
263
|
+
-- turn_duration_ms holds the time that a whole turn took, in milliseconds, on
|
|
264
|
+
-- the same row as reported_usage. It is a column of its own, and not a member of
|
|
265
|
+
-- the rollup. The rollup is absent when a provider reported no quantity, and the
|
|
266
|
+
-- host measures the duration apart from the token counts. Thus a member of the
|
|
267
|
+
-- rollup would disappear with it. BIGINT, as cortex_step_executions.duration_ms
|
|
268
|
+
-- is, because an int4 duration overflowed there.
|
|
269
|
+
--
|
|
270
|
+
-- Nullable with no default: absent means that nobody measured the turn, and a
|
|
271
|
+
-- zero would claim that a turn took no time. A row written before this column
|
|
272
|
+
-- existed is absent for the same reason, thus the migration needs no backfill.
|
|
273
|
+
--
|
|
274
|
+
-- author names the identity that sent the user message of a turn, as the host
|
|
275
|
+
-- knew it at the append site. It is written on the genuine-user-start row of an
|
|
276
|
+
-- append only -- the row that carries display_envelope -- and never on an
|
|
277
|
+
-- assistant, tool, or synthetic row. Nullable with no default and no backfill:
|
|
278
|
+
-- absent means that no sender was recorded, which is the honest value both for a
|
|
279
|
+
-- row that predates the column and for a turn appended without one.
|
|
280
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
281
|
+
thread_id TEXT NOT NULL,
|
|
282
|
+
seq BIGINT NOT NULL,
|
|
283
|
+
role TEXT,
|
|
284
|
+
content_jsonb JSONB,
|
|
285
|
+
message_envelope JSON,
|
|
286
|
+
-- Versioned AI SDK UIMessage projection of one written group, on the first
|
|
287
|
+
-- row of that group. Model-history reads never select it.
|
|
288
|
+
display_envelope JSONB,
|
|
289
|
+
tokens INTEGER NOT NULL,
|
|
290
|
+
reported_usage JSONB,
|
|
291
|
+
turn_duration_ms BIGINT,
|
|
292
|
+
author TEXT,
|
|
293
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
294
|
+
PRIMARY KEY (thread_id, seq)
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
-- The state of one chat turn, and no message: a close never changes a row of
|
|
298
|
+
-- messages. messages.reported_usage and messages.turn_duration_ms hold the
|
|
299
|
+
-- figures of the older turns, which have no record here.
|
|
300
|
+
CREATE TABLE IF NOT EXISTS cortex_thread_turns (
|
|
301
|
+
thread_id TEXT NOT NULL,
|
|
302
|
+
-- The seq of the user row that opens the turn.
|
|
303
|
+
start_seq BIGINT NOT NULL,
|
|
304
|
+
status TEXT NOT NULL CHECK (status IN ('open', 'done', 'aborted', 'failed')),
|
|
305
|
+
-- A short text from the class of the error, never its message, because each later turn sends the note.
|
|
306
|
+
reason TEXT,
|
|
307
|
+
-- Absent when no provider reported a quantity, the same as messages.reported_usage.
|
|
308
|
+
reported_usage JSONB,
|
|
309
|
+
turn_duration_ms BIGINT,
|
|
310
|
+
opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
311
|
+
closed_at TIMESTAMPTZ,
|
|
312
|
+
PRIMARY KEY (thread_id, start_seq)
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
-- Conversation thread metadata — the harness ThreadStore module's table,
|
|
316
|
+
-- one row per conversation thread. Holds the analysis-scope, title, and
|
|
317
|
+
-- soft-delete tombstone the harness messages table deliberately lacks.
|
|
318
|
+
-- No free-form metadata column: working memory lives in cortex_working_memory;
|
|
319
|
+
-- nothing else reads thread metadata. A NULL deleted_at means the thread is live.
|
|
320
|
+
CREATE TABLE IF NOT EXISTS cortex_analysis_threads (
|
|
321
|
+
thread_id TEXT PRIMARY KEY,
|
|
322
|
+
analysis_id TEXT NOT NULL,
|
|
323
|
+
title TEXT,
|
|
324
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
325
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
326
|
+
deleted_at TIMESTAMPTZ,
|
|
327
|
+
thread_type TEXT NOT NULL DEFAULT 'conversation',
|
|
328
|
+
-- The cascade on this self-reference is a BACKSTOP, never the deletion path.
|
|
329
|
+
-- messages carries no foreign key to this table, so a cascade firing on its
|
|
330
|
+
-- own drops a descendant's metadata row while leaving that descendant's
|
|
331
|
+
-- messages rows behind with nothing naming them, beyond the reach of any
|
|
332
|
+
-- later thread-scoped or analysis-scoped reclamation. purgeThread therefore
|
|
333
|
+
-- walks the subtree and deletes its messages rows explicitly, to the same
|
|
334
|
+
-- depth the cascade reaches.
|
|
335
|
+
parent_thread_id TEXT REFERENCES cortex_analysis_threads(thread_id) ON DELETE CASCADE,
|
|
336
|
+
parent_seq BIGINT
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
-- These three sit here, and not in the additive block further down, because the
|
|
340
|
+
-- index below reads one of them. CREATE TABLE IF NOT EXISTS adds no column to a
|
|
341
|
+
-- table that already exists, so on every database but a brand new one the column
|
|
342
|
+
-- arrives from an ALTER -- and the whole additive block runs after this text. An
|
|
343
|
+
-- index declared here over a column added there fails with 42703 on exactly the
|
|
344
|
+
-- databases a migration exists to serve. Additive with no backfill either way: an
|
|
345
|
+
-- existing row reads back as a root conversation thread with no anchor, which is
|
|
346
|
+
-- what it is.
|
|
347
|
+
ALTER TABLE cortex_analysis_threads
|
|
348
|
+
ADD COLUMN IF NOT EXISTS thread_type TEXT NOT NULL DEFAULT 'conversation';
|
|
349
|
+
|
|
350
|
+
ALTER TABLE cortex_analysis_threads
|
|
351
|
+
ADD COLUMN IF NOT EXISTS parent_thread_id TEXT REFERENCES cortex_analysis_threads(thread_id) ON DELETE CASCADE;
|
|
352
|
+
|
|
353
|
+
ALTER TABLE cortex_analysis_threads
|
|
354
|
+
ADD COLUMN IF NOT EXISTS parent_seq BIGINT;
|
|
355
|
+
|
|
356
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_analysis_threads_analysis
|
|
357
|
+
ON cortex_analysis_threads(analysis_id) WHERE deleted_at IS NULL;
|
|
358
|
+
|
|
359
|
+
-- Not partial, unlike the analysis index above. Two of the three readers of
|
|
360
|
+
-- parent_thread_id carry no deleted_at predicate and cannot: the recursive
|
|
361
|
+
-- subtree walk in thread-store.ts must reach archived descendants, and the
|
|
362
|
+
-- referential trigger behind ON DELETE CASCADE is the database's own query.
|
|
363
|
+
-- Postgres uses a partial index only where it can prove the predicate holds, so
|
|
364
|
+
-- a partial index here leaves both to a sequential scan of the whole table.
|
|
365
|
+
-- Over 150k rows that measured 28 ms per walk and 20 ms per cascade, against
|
|
366
|
+
-- 0.06 ms and 0.10 ms with this one. The third reader, a listing filtered by
|
|
367
|
+
-- parent, keeps an index scan either way.
|
|
368
|
+
--
|
|
369
|
+
-- The old partial index goes by name: CREATE INDEX IF NOT EXISTS keeps whatever
|
|
370
|
+
-- definition already holds the name, so a database that built the partial one
|
|
371
|
+
-- would never see this.
|
|
372
|
+
DROP INDEX IF EXISTS idx_cortex_analysis_threads_parent;
|
|
373
|
+
|
|
374
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_analysis_threads_parent_fk
|
|
375
|
+
ON cortex_analysis_threads(parent_thread_id);
|
|
376
|
+
|
|
377
|
+
-- Working memory — the harness WorkingMemory module's table, one row per
|
|
378
|
+
-- analysis. The data column holds a JSONB object with four sections (goal,
|
|
379
|
+
-- constraints, hypotheses, findings). Section updates are a read-modify-write
|
|
380
|
+
-- in harness/memory/working-memory.ts, not a whole-blob rewrite.
|
|
381
|
+
-- Analysis-scoped and agent-maintained.
|
|
382
|
+
CREATE TABLE IF NOT EXISTS cortex_working_memory (
|
|
383
|
+
analysis_id TEXT PRIMARY KEY,
|
|
384
|
+
data JSONB NOT NULL,
|
|
385
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
-- Tool-approval ask ledger — the single source of truth for approval state, one
|
|
389
|
+
-- row per ctx.ask invocation. ctx.ask inserts a 'pending' row and polls it to a
|
|
390
|
+
-- terminal status. The outward answer() writes the decision. reply holds the
|
|
391
|
+
-- recorded AskReply (NULL while pending). Analysis-scoped audit of every
|
|
392
|
+
-- approval-gated action, including grant auto-approvals (recorded 'resolved'
|
|
393
|
+
-- with no prompt shown). A pending row a dead process orphans is swept to
|
|
394
|
+
-- 'expired' at boot.
|
|
395
|
+
CREATE TABLE IF NOT EXISTS cortex_asks (
|
|
396
|
+
id TEXT PRIMARY KEY,
|
|
397
|
+
analysis_id TEXT NOT NULL,
|
|
398
|
+
user_id TEXT,
|
|
399
|
+
thread_id TEXT,
|
|
400
|
+
title TEXT NOT NULL,
|
|
401
|
+
command TEXT NOT NULL,
|
|
402
|
+
detail TEXT,
|
|
403
|
+
-- The key the standing grant behind an 'always' is recorded under: the displayed
|
|
404
|
+
-- command unless the tool supplied a broader grant key. Written on every insert,
|
|
405
|
+
-- defaulting to the command when absent. Nullable only so the additive migration
|
|
406
|
+
-- can add it to an already-created table without a per-row backfill.
|
|
407
|
+
grant_key TEXT,
|
|
408
|
+
status TEXT NOT NULL,
|
|
409
|
+
reply JSONB,
|
|
410
|
+
created_at TEXT NOT NULL,
|
|
411
|
+
resolved_at TEXT
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_asks_analysis
|
|
415
|
+
ON cortex_asks(analysis_id);
|
|
416
|
+
|
|
417
|
+
-- Backs the pending() enumeration and the boot sweep. Partial so it stays
|
|
418
|
+
-- compact against the far larger terminal-row mass.
|
|
419
|
+
CREATE INDEX IF NOT EXISTS idx_cortex_asks_pending
|
|
420
|
+
ON cortex_asks(status) WHERE status = 'pending';
|
|
421
|
+
|
|
422
|
+
-- Standing approval grants behind an 'always' reply — keyed by the analysis, the
|
|
423
|
+
-- user, and the ask's grant key (the displayed command when the tool supplied no
|
|
424
|
+
-- broader key), so a later matching ask auto-approves without pausing. Lives for
|
|
425
|
+
-- the analysis lifecycle, surviving process restarts, and never applies to
|
|
426
|
+
-- another analysis or another user.
|
|
427
|
+
CREATE TABLE IF NOT EXISTS cortex_ask_grants (
|
|
428
|
+
analysis_id TEXT NOT NULL,
|
|
429
|
+
user_id TEXT NOT NULL,
|
|
430
|
+
grant_key TEXT NOT NULL,
|
|
431
|
+
created_at TEXT NOT NULL,
|
|
432
|
+
PRIMARY KEY (analysis_id, user_id, grant_key)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
-- The kept text of each cut tool result. thread_id is null for a text of a run, thus purgeThread removes
|
|
436
|
+
-- only the texts of chat turns, and purgeAnalysis removes each row by analysis_id.
|
|
437
|
+
CREATE TABLE IF NOT EXISTS cortex_tool_outputs (
|
|
438
|
+
analysis_id TEXT NOT NULL,
|
|
439
|
+
ref TEXT NOT NULL,
|
|
440
|
+
tool_name TEXT NOT NULL,
|
|
441
|
+
tool_call_id TEXT NOT NULL,
|
|
442
|
+
thread_id TEXT,
|
|
443
|
+
content TEXT NOT NULL,
|
|
444
|
+
total_length INTEGER NOT NULL,
|
|
445
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
446
|
+
PRIMARY KEY (analysis_id, ref)
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
CREATE INDEX IF NOT EXISTS cortex_tool_outputs_thread_idx
|
|
450
|
+
ON cortex_tool_outputs (thread_id) WHERE thread_id IS NOT NULL;
|
|
451
|
+
`;
|
|
452
|
+
/**
|
|
453
|
+
* Drop vestigial columns/tables from pre-v2 databases. Idempotent: each
|
|
454
|
+
* statement targets a specific column that may or may not exist;
|
|
455
|
+
* `DROP COLUMN IF EXISTS` is a no-op when absent.
|
|
456
|
+
*/
|
|
457
|
+
const DROP_MIGRATIONS = [
|
|
458
|
+
"DROP TABLE IF EXISTS cortex_run_events",
|
|
459
|
+
"ALTER TABLE cortex_step_executions DROP COLUMN IF EXISTS thread_id",
|
|
460
|
+
"ALTER TABLE cortex_step_executions DROP COLUMN IF EXISTS execution_id",
|
|
461
|
+
"ALTER TABLE cortex_step_executions DROP COLUMN IF EXISTS resources",
|
|
462
|
+
"ALTER TABLE cortex_step_executions DROP COLUMN IF EXISTS summary",
|
|
463
|
+
"ALTER TABLE cortex_artifacts DROP COLUMN IF EXISTS description",
|
|
464
|
+
"ALTER TABLE cortex_artifacts DROP COLUMN IF EXISTS metadata",
|
|
465
|
+
"ALTER TABLE cortex_analysis_state DROP COLUMN IF EXISTS profile",
|
|
466
|
+
"ALTER TABLE cortex_analysis_state DROP COLUMN IF EXISTS input_files",
|
|
467
|
+
"ALTER TABLE cortex_analysis_state DROP COLUMN IF EXISTS user_id",
|
|
468
|
+
// Billing identity is resolved lazily at charge-open time, never persisted.
|
|
469
|
+
"ALTER TABLE cortex_analysis_state DROP COLUMN IF EXISTS billing_context",
|
|
470
|
+
// v2 vestiges on cortex_runs.
|
|
471
|
+
"ALTER TABLE cortex_runs DROP COLUMN IF EXISTS plan",
|
|
472
|
+
"ALTER TABLE cortex_runs DROP COLUMN IF EXISTS plan_version",
|
|
473
|
+
"ALTER TABLE cortex_runs DROP COLUMN IF EXISTS current_wave",
|
|
474
|
+
"ALTER TABLE cortex_runs DROP COLUMN IF EXISTS suspension",
|
|
475
|
+
// The mandate JWT lives in DBOS workflow input (never reconstructed -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
476
|
+
// from the DB), and `run_id` is the DBOS workflowID directly — no
|
|
477
|
+
// separate `workflow_id` column.
|
|
478
|
+
"ALTER TABLE cortex_runs DROP COLUMN IF EXISTS mandate_token", // oss-core-managed-ok: drop-migration for removed branded column
|
|
479
|
+
"ALTER TABLE cortex_runs DROP COLUMN IF EXISTS workflow_id",
|
|
480
|
+
"DROP INDEX IF EXISTS idx_cortex_runs_workflow",
|
|
481
|
+
];
|
|
482
|
+
const RELAX_MIGRATIONS = [
|
|
483
|
+
// If an older DB had `cortex_runs.thread_id NOT NULL`, relax it now.
|
|
484
|
+
// Safe no-op when already nullable.
|
|
485
|
+
"ALTER TABLE cortex_runs ALTER COLUMN thread_id DROP NOT NULL",
|
|
486
|
+
// Legacy Anthropic message columns are write-frozen: runtime writes
|
|
487
|
+
// only message_envelope, so new rows must not require them. The
|
|
488
|
+
// columns should be dropped entirely after the migration window.
|
|
489
|
+
"ALTER TABLE messages ALTER COLUMN role DROP NOT NULL",
|
|
490
|
+
"ALTER TABLE messages ALTER COLUMN content_jsonb DROP NOT NULL",
|
|
491
|
+
];
|
|
492
|
+
/** Additive migrations — columns that may not exist on older DBs. */
|
|
493
|
+
const ADD_MIGRATIONS = [
|
|
494
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS thread_id TEXT",
|
|
495
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS parts JSONB",
|
|
496
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS mandate_jti TEXT", // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
497
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS mandate_expires_at TEXT", // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
498
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS synthesis_status TEXT",
|
|
499
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS synthesis_reason TEXT",
|
|
500
|
+
"ALTER TABLE cortex_analysis_state ADD COLUMN IF NOT EXISTS data_profile_result JSONB",
|
|
501
|
+
// Sandbox reliability telemetry (from sandbox-reliability change on main).
|
|
502
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS attempts INTEGER NOT NULL DEFAULT 1",
|
|
503
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS last_error_class TEXT",
|
|
504
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS finish_reason TEXT",
|
|
505
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS hit_max_steps INTEGER NOT NULL DEFAULT 0",
|
|
506
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS blocked_reason TEXT",
|
|
507
|
+
"ALTER TABLE cortex_runs ADD COLUMN IF NOT EXISTS plan_id TEXT REFERENCES cortex_plans(plan_id) ON DELETE SET NULL",
|
|
508
|
+
// Promote overflow-prone numeric columns from int4 to int8.
|
|
509
|
+
// File sizes >2 GB and durations >~24.8 days previously failed INSERT
|
|
510
|
+
// with "value ... is out of range of type integer".
|
|
511
|
+
"ALTER TABLE cortex_artifacts ALTER COLUMN size TYPE BIGINT",
|
|
512
|
+
"ALTER TABLE cortex_step_executions ALTER COLUMN duration_ms TYPE BIGINT",
|
|
513
|
+
"ALTER TABLE cortex_artifacts ADD COLUMN IF NOT EXISTS unrecoverable_at TEXT",
|
|
514
|
+
"ALTER TABLE cortex_artifacts ADD COLUMN IF NOT EXISTS file_type TEXT",
|
|
515
|
+
// Active-sandbox registry — sandbox_ref carries {sandboxId, host, port,
|
|
516
|
+
// backend} (no callbackSecret — that lives only in the DBOS step-output
|
|
517
|
+
// cache). exec_id carries the currently in-flight workflowId:stepId:fnId.
|
|
518
|
+
// Partial index supports the watchdog's registry query
|
|
519
|
+
// (status='running' AND sandbox_ref IS NOT NULL); kept partial so it
|
|
520
|
+
// stays compact against the much larger terminal-row mass.
|
|
521
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS sandbox_ref JSONB",
|
|
522
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS exec_id TEXT",
|
|
523
|
+
"CREATE INDEX IF NOT EXISTS idx_cortex_step_exec_active_sandbox " + "ON cortex_step_executions(status) WHERE sandbox_ref IS NOT NULL",
|
|
524
|
+
// DBOS workflow addressing. `cortex_runs.run_id` IS the parent DBOS
|
|
525
|
+
// workflowID — both are the same bare UUID. `child_workflow_id` maps
|
|
526
|
+
// each step-execution row to its DBOS child workflow id.
|
|
527
|
+
// Partial-unique index — at most one active run per (analysis_id, plan_id).
|
|
528
|
+
// Terminal rows are excluded so a deliberate re-run after completion
|
|
529
|
+
// succeeds; a concurrent double-trigger raises unique-violation and the
|
|
530
|
+
// caller resolves to the existing active runId via queryActiveRun.
|
|
531
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_cortex_runs_active_plan " +
|
|
532
|
+
"ON cortex_runs(analysis_id, plan_id) " +
|
|
533
|
+
"WHERE status IN ('running','suspended_insufficient_funds')",
|
|
534
|
+
"ALTER TABLE cortex_step_executions ADD COLUMN IF NOT EXISTS child_workflow_id TEXT",
|
|
535
|
+
"CREATE INDEX IF NOT EXISTS idx_cortex_step_exec_child_workflow " + "ON cortex_step_executions(child_workflow_id)",
|
|
536
|
+
"ALTER TABLE cortex_analysis_state ADD COLUMN IF NOT EXISTS seed_input_file_ids JSONB",
|
|
537
|
+
// The look-before-record markers of a report session. Nullable and
|
|
538
|
+
// unbackfilled: a session with no preview and no eyes carries neither hash,
|
|
539
|
+
// which is the honest state, thus an existing row reads back as never-seen.
|
|
540
|
+
"ALTER TABLE cortex_report_session_state ADD COLUMN IF NOT EXISTS rendered_document_hash TEXT",
|
|
541
|
+
"ALTER TABLE cortex_report_session_state ADD COLUMN IF NOT EXISTS seen_document_hash TEXT",
|
|
542
|
+
// The derivation list of a report session. Nullable and unbackfilled: a
|
|
543
|
+
// session that derived nothing holds no list, and the read gives an empty
|
|
544
|
+
// list for such a row.
|
|
545
|
+
"ALTER TABLE cortex_report_session_state ADD COLUMN IF NOT EXISTS derivations JSONB",
|
|
546
|
+
// Databases created before `data_profile_status` became nullable still
|
|
547
|
+
// carry the NOT NULL floor, which would reject a clear. Idempotent:
|
|
548
|
+
// dropping an absent NOT NULL no-ops, so this is a no-op on fresh DBs.
|
|
549
|
+
"ALTER TABLE cortex_analysis_state ALTER COLUMN data_profile_status DROP NOT NULL",
|
|
550
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSON",
|
|
551
|
+
// The turn's reported usage rollup (see the CREATE TABLE comment).
|
|
552
|
+
// Purely additive: nullable with no default, so adding it rewrites no
|
|
553
|
+
// row and every pre-existing message reads back with no rollup — which
|
|
554
|
+
// is the honest value, the figures having never been recorded.
|
|
555
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS reported_usage JSONB",
|
|
556
|
+
// The turn's measured duration (see the CREATE TABLE comment). Purely
|
|
557
|
+
// additive: nullable with no default, thus the column rewrites no row,
|
|
558
|
+
// and every earlier message reads back with no duration — which is the
|
|
559
|
+
// honest value, because nobody measured those turns.
|
|
560
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS turn_duration_ms BIGINT",
|
|
561
|
+
// Who sent the user message of the turn (see the CREATE TABLE comment).
|
|
562
|
+
// Purely additive: nullable with no default, thus the column rewrites no row.
|
|
563
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS author TEXT",
|
|
564
|
+
// Demote an existing JSONB envelope column to JSON (see the CREATE
|
|
565
|
+
// TABLE comment). Type-guarded because ALTER ... TYPE rewrites the
|
|
566
|
+
// whole table; scoped to current_schema() so parallel test schemas
|
|
567
|
+
// never see each other.
|
|
568
|
+
// Rows already written as JSONB carry over as they stand — their
|
|
569
|
+
// original key order is not recoverable.
|
|
570
|
+
`DO $$ BEGIN
|
|
571
|
+
IF EXISTS (
|
|
572
|
+
SELECT 1 FROM information_schema.columns
|
|
573
|
+
WHERE table_schema = current_schema() AND table_name = 'messages'
|
|
574
|
+
AND column_name = 'message_envelope' AND data_type = 'jsonb'
|
|
575
|
+
) THEN
|
|
576
|
+
ALTER TABLE messages ALTER COLUMN message_envelope TYPE JSON USING message_envelope::text::json;
|
|
577
|
+
END IF;
|
|
578
|
+
END $$`,
|
|
579
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS display_envelope JSONB",
|
|
580
|
+
// Grant key an 'always' records the standing grant under. Nullable
|
|
581
|
+
// and unbackfilled: every new row writes it (command when absent), and
|
|
582
|
+
// orphaned legacy pending rows are swept to 'expired' before any answer
|
|
583
|
+
// reads it, so no existing row is left needing a value.
|
|
584
|
+
"ALTER TABLE cortex_asks ADD COLUMN IF NOT EXISTS grant_key TEXT",
|
|
585
|
+
// The grants table's key column is named for what it stores. A table
|
|
586
|
+
// created when the column was `command` is renamed in place — its
|
|
587
|
+
// values carry over unchanged, because a grant recorded without a
|
|
588
|
+
// grant key is keyed on its exact command. Guarded via
|
|
589
|
+
// information_schema (Postgres has no RENAME COLUMN IF EXISTS) and
|
|
590
|
+
// scoped to current_schema() so parallel test schemas in the same
|
|
591
|
+
// database never trigger each other's rename.
|
|
592
|
+
`DO $$ BEGIN
|
|
593
|
+
IF EXISTS (
|
|
594
|
+
SELECT 1 FROM information_schema.columns
|
|
595
|
+
WHERE table_schema = current_schema() AND table_name = 'cortex_ask_grants' AND column_name = 'command'
|
|
596
|
+
) THEN
|
|
597
|
+
ALTER TABLE cortex_ask_grants RENAME COLUMN command TO grant_key;
|
|
598
|
+
END IF;
|
|
599
|
+
END $$`,
|
|
600
|
+
"ALTER TABLE cortex_asks ADD COLUMN IF NOT EXISTS user_id TEXT",
|
|
601
|
+
// The user joins the grant key. Guarded and scoped like the rename
|
|
602
|
+
// above. An existing row keeps the empty default, which matches no real
|
|
603
|
+
// identity, so it is inert rather than deleted.
|
|
604
|
+
`DO $$ BEGIN
|
|
605
|
+
IF NOT EXISTS (
|
|
606
|
+
SELECT 1 FROM information_schema.columns
|
|
607
|
+
WHERE table_schema = current_schema() AND table_name = 'cortex_ask_grants' AND column_name = 'user_id'
|
|
608
|
+
) THEN
|
|
609
|
+
ALTER TABLE cortex_ask_grants ADD COLUMN user_id TEXT NOT NULL DEFAULT '';
|
|
610
|
+
ALTER TABLE cortex_ask_grants ALTER COLUMN user_id DROP DEFAULT;
|
|
611
|
+
ALTER TABLE cortex_ask_grants DROP CONSTRAINT IF EXISTS cortex_ask_grants_pkey;
|
|
612
|
+
ALTER TABLE cortex_ask_grants ADD PRIMARY KEY (analysis_id, user_id, grant_key);
|
|
613
|
+
END IF;
|
|
614
|
+
END $$`,
|
|
615
|
+
];
|
|
616
|
+
// HNSW ANN index on cortex_regulatory_chunks.embedding. HNSW gives
|
|
617
|
+
// higher recall than ivfflat at the same latency and needs no
|
|
618
|
+
// training step — the right pick for a write-occasionally /
|
|
619
|
+
// read-many corpus (quarterly refresh, multiple search calls per
|
|
620
|
+
// synthesis step).
|
|
621
|
+
//
|
|
622
|
+
// Requires pgvector >= 0.5.0. Tolerated gracefully on older
|
|
623
|
+
// versions — the table and B-tree/GIN indexes still work; the
|
|
624
|
+
// operator can build this index manually once they upgrade pgvector.
|
|
625
|
+
// m and ef_construction are conservative defaults; tune after
|
|
626
|
+
// measuring recall vs. latency on the production corpus size.
|
|
627
|
+
const HNSW_INDEX = `CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_embedding
|
|
628
|
+
ON cortex_regulatory_chunks USING hnsw (embedding vector_cosine_ops)
|
|
629
|
+
WITH (m = 16, ef_construction = 64)`;
|
|
630
|
+
export function baseline(logger) {
|
|
631
|
+
return {
|
|
632
|
+
async up(db) {
|
|
633
|
+
// pgvector must be present for the per-analysis search indexes.
|
|
634
|
+
try {
|
|
635
|
+
await sql.raw("CREATE EXTENSION IF NOT EXISTS vector").execute(db);
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
639
|
+
throw new Error(`Cortex requires the pgvector extension. ` +
|
|
640
|
+
`CREATE EXTENSION IF NOT EXISTS vector failed: ${msg}. ` +
|
|
641
|
+
`Ask platform-infra to enable pgvector on the RDS parameter group, ` +
|
|
642
|
+
`or grant CREATE on the database to the Cortex role.`, { cause: err });
|
|
643
|
+
}
|
|
644
|
+
// Base DDL — idempotent via CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS.
|
|
645
|
+
const statements = DDL.split(";")
|
|
646
|
+
.map((s) => s.trim())
|
|
647
|
+
.filter((s) => s.length > 0);
|
|
648
|
+
for (const statement of [...statements, ...DROP_MIGRATIONS, ...RELAX_MIGRATIONS, ...ADD_MIGRATIONS]) {
|
|
649
|
+
await sql.raw(statement).execute(db);
|
|
650
|
+
}
|
|
651
|
+
await backfillAiSdkMessageEnvelopes({
|
|
652
|
+
query: (text, values) => db.executeQuery(CompiledQuery.raw(text, values)),
|
|
653
|
+
});
|
|
654
|
+
// The migration runs in one transaction, thus a failed statement must roll
|
|
655
|
+
// back to a savepoint before the next statement can run.
|
|
656
|
+
await sql `SAVEPOINT hnsw_index`.execute(db);
|
|
657
|
+
try {
|
|
658
|
+
await sql.raw(HNSW_INDEX).execute(db);
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
await sql `ROLLBACK TO SAVEPOINT hnsw_index`.execute(db);
|
|
662
|
+
logger.warn("could not create HNSW index on cortex_regulatory_chunks.embedding (pgvector < 0.5.0 or operator class unavailable) — " +
|
|
663
|
+
"cosine similarity queries fall back to sequential scan; build the index by hand after you upgrade pgvector", logger.errorFields(err));
|
|
664
|
+
}
|
|
665
|
+
// No boot-time orphan sweep on cortex_runs / cortex_step_executions.
|
|
666
|
+
// Every running row is backed by a DBOS workflow (executeAnalysis,
|
|
667
|
+
// resume entry); DBOS recovery is the
|
|
668
|
+
// substitute — each pod has a stable executorID (StatefulSet pod name)
|
|
669
|
+
// and runs `recoverPendingWorkflows` for its own ID at launch (ADR
|
|
670
|
+
// 0012). The workflow body owns the terminal transition on cortex_runs;
|
|
671
|
+
// a bulk fail here would race recovery and mark healthy sibling-replica
|
|
672
|
+
// runs as failed.
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|