okengine 0.23.0 → 0.23.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/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +4 -4
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/signal/broadcast.mdx +4 -2
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/errors.mdx +31 -19
- package/site/content/docs/reference/fx.mdx +12 -4
- package/src/auth/tenants.ts +17 -0
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/compiler/aot.test.ts +3 -2
- package/src/compiler/aot.ts +45 -1
- package/src/compiler/dynamic.ts +1 -1
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +87 -16
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +11 -1
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/http-parse.test.ts +101 -0
- package/src/compiler/http-parse.ts +188 -9
- package/src/compiler/interpret.ts +25 -2
- package/src/console/index.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-DDKkhT9v.js → access-page-zBMhZnFm.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-62Xts8Xi.js → agent-disclosure-CBj9f7Ju.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-DyeoKHKA.js → cache-glyph-D3MY49h3.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DvfpLXsU.js → call-pii-button-L9JxdBhp.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BY03SeCg.js → collapsible-BvFaX-Zt.js} +1 -1
- package/src/console/ui-next/dist/assets/{decisions-page-CgkKlA56.js → decisions-page-DgB76m-X.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Y6HLjyJ5.js → duration-tone-l1DSJ2Vz.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DLPfNy-_.js → flows-page-Bb9t1lu6.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-D7nNzpgB.js → highlighted-json-DMvsi3Ti.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DdL19zzh.js → http-method-BwhJBTcQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C0lc_s9d.js → index-RhV2jT_7.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CRlcyLCC.js → observability-page-BRfUILBq.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Bb3FWUu9.js → replica-lag-BsceAzIG.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DYBMWhX8.js → request-meta-C6lTyCXp.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CcE-SXC8.js → store-page-YmE806bJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CKMSEQ4s.js → trace-detail-sheet-Di4irS49.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-QOfk0M0H.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DKC1CDDd.js → units-page-07agasRA.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-B54v4Yit.js → vault-page-CHBck4n_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/seed-parked-approval.ts +5 -1
- package/src/drivers/journal-postgres.test.ts +106 -2
- package/src/drivers/journal-postgres.ts +475 -14
- package/src/drivers/postgres.test.ts +60 -0
- package/src/drivers/postgres.ts +79 -5
- package/src/drivers/signal-compete.test.ts +238 -0
- package/src/drivers/signal-postgres.test.ts +170 -0
- package/src/drivers/signal-postgres.ts +486 -213
- package/src/drivers/signal-redis.test.ts +224 -0
- package/src/drivers/signal-redis.ts +934 -93
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/approval.test.ts +15 -3
- package/src/elements/ai/approval.ts +15 -9
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/runtime.ts +20 -19
- package/src/elements/channel/runtime.ts +42 -4
- package/src/elements/channel/sql-ledger.test.ts +404 -0
- package/src/elements/channel/sql-ledger.ts +395 -0
- package/src/elements/clock/chaos-child.ts +12 -2
- package/src/elements/clock/durable.ts +11 -0
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache-bus.test.ts +121 -0
- package/src/elements/store/cache-bus.ts +278 -0
- package/src/elements/store/cache.test.ts +179 -15
- package/src/elements/store/cache.ts +357 -51
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/runtime.ts +44 -2
- package/src/elements/store/sql-errors.test.ts +131 -0
- package/src/elements/store/sql-errors.ts +39 -16
- package/src/elements/store/sql-nested-tx.test.ts +344 -0
- package/src/elements/store/sql-session.test.ts +2 -0
- package/src/elements/store/sql-session.ts +275 -9
- package/src/elements/store.ts +1 -0
- package/src/kernel/app.ts +207 -43
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/auto-registry.test.ts +5 -1
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +72 -25
- package/src/kernel/boot-bind/store.ts +16 -1
- package/src/kernel/boot.test.ts +2 -2
- package/src/kernel/boot.ts +54 -10
- package/src/kernel/boundary-contract.ts +7 -0
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/call.test.ts +1 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +16 -0
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +11 -7
- package/src/kernel/fx-sql-handle.ts +52 -7
- package/src/kernel/fx.test.ts +1 -3
- package/src/kernel/fx.ts +27 -16
- package/src/kernel/horizontal-child.ts +54 -1
- package/src/kernel/horizontal.integration.test.ts +131 -7
- package/src/kernel/idempotency.test.ts +4 -4
- package/src/kernel/journal-boot.test.ts +83 -6
- package/src/kernel/journal.test.ts +158 -0
- package/src/kernel/journal.ts +515 -54
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/mcp/authorization.ts +5 -11
- package/src/mcp/confirmation.ts +203 -38
- package/src/mcp/docs-server.ts +26 -4
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp.test.ts +404 -33
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +31 -5
- package/src/mcp/session.ts +7 -0
- package/src/mcp/tools.ts +68 -19
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/runtime/bun.ts +5 -0
- package/src/runtime/types.ts +5 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Same concurrency physics as Signal's `once` delivery and Clock's postgres
|
|
5
5
|
* CronStore: claim with `FOR UPDATE SKIP LOCKED`; a crashed holder's lease is
|
|
6
|
-
* reclaimed lazily on the next claim attempt (no sweeper
|
|
7
|
-
*
|
|
6
|
+
* reclaimed lazily on the next claim attempt (no sweeper). Each acquire bumps
|
|
7
|
+
* a fencing token; writes from a previous holder are rejected. Entries are
|
|
8
|
+
* append-only rows, not a rewritten JSON blob.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
} from "../kernel/idempotency-store.ts";
|
|
23
24
|
import type { DecisionLabelStore } from "../kernel/decision-label-store.ts";
|
|
24
25
|
import type { AgentEventStore } from "../kernel/agent-event-store.ts";
|
|
26
|
+
import { throwOke } from "../kernel/errors.ts";
|
|
25
27
|
import { lazyRequire } from "../kernel/lazy-require.ts";
|
|
26
28
|
import {
|
|
27
29
|
JOURNAL_DEFAULT_LEASE_MS,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
type JournalLeaseStore,
|
|
30
32
|
type JournalRun,
|
|
31
33
|
type JournalStore,
|
|
34
|
+
type JournalWriteFence,
|
|
32
35
|
} from "../kernel/journal.ts";
|
|
33
36
|
import {
|
|
34
37
|
resolvePostgresUrl,
|
|
@@ -53,6 +56,19 @@ interface JournalDbRow {
|
|
|
53
56
|
created_at: number;
|
|
54
57
|
updated_at: number;
|
|
55
58
|
tenant: string | null;
|
|
59
|
+
lease_token: number | null;
|
|
60
|
+
code_version: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One append-only journal entry row. */
|
|
64
|
+
interface JournalEntryDbRow {
|
|
65
|
+
run_id: string;
|
|
66
|
+
seq: number;
|
|
67
|
+
kind: string;
|
|
68
|
+
name: string | null;
|
|
69
|
+
resource: string | null;
|
|
70
|
+
value: string | null;
|
|
71
|
+
at: number;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
/** Minimal SQL + transaction surface for the postgres journal store. */
|
|
@@ -85,7 +101,45 @@ const CLAIM_DUE_SQL = `SELECT * FROM oke_journal_runs WHERE status='sleeping' AN
|
|
|
85
101
|
/** Boot-time orphan discovery: running/sleeping runs with no live lease. */
|
|
86
102
|
const ORPHANS_SQL = `SELECT * FROM oke_journal_runs WHERE (status='running' OR status='sleeping' OR status='compensating') AND ((locked_by IS NULL) OR (lease_expires_at IS NOT NULL AND lease_expires_at<=?))`;
|
|
87
103
|
|
|
88
|
-
const UPDATE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = ?, lease_expires_at =
|
|
104
|
+
const UPDATE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = ?, lease_expires_at = ?, lease_token = COALESCE(lease_token, 0) + 1 WHERE id = ?`;
|
|
105
|
+
|
|
106
|
+
const HEARTBEAT_SQL = `UPDATE oke_journal_runs SET lease_expires_at = GREATEST(COALESCE(lease_expires_at, 0), ?) WHERE id = ? AND locked_by = ? AND lease_token = ? AND lease_expires_at > ?`;
|
|
107
|
+
|
|
108
|
+
const INSERT_ENTRY_SQL = `INSERT INTO oke_journal_entries (run_id, seq, kind, name, resource, value, at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (run_id, seq) DO NOTHING`;
|
|
109
|
+
|
|
110
|
+
/** One entry row, inserted only while the caller's fence still holds. */
|
|
111
|
+
const APPEND_ENTRY_FENCED_SQL = `INSERT INTO oke_journal_entries (run_id, seq, kind, name, resource, value, at) SELECT ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM oke_journal_runs WHERE id = ? AND locked_by = ? AND lease_token = ? AND lease_expires_at > ? FOR SHARE) ON CONFLICT (run_id, seq) DO NOTHING`;
|
|
112
|
+
|
|
113
|
+
const UPDATE_ENTRY_FENCED_SQL = `UPDATE oke_journal_entries SET kind = ?, name = ?, resource = ?, value = ?, at = ? WHERE run_id = ? AND seq = ? AND EXISTS (SELECT 1 FROM oke_journal_runs WHERE id = ? AND locked_by = ? AND lease_token = ? AND lease_expires_at > ? FOR SHARE)`;
|
|
114
|
+
|
|
115
|
+
const FENCED_HEADER_SQL = `UPDATE oke_journal_runs SET flow = ?, input = ?, status = ?, entries = ?, wake_at = ?, error = ?, output = ?, locked_by = ?, lease_expires_at = ?, updated_at = ?, tenant = ?, lease_token = GREATEST(COALESCE(lease_token, 0), ?), code_version = ? WHERE id = ? AND locked_by = ? AND lease_token = ? AND lease_expires_at > ?`;
|
|
116
|
+
|
|
117
|
+
/** Header write inside `cas`, which already holds the row `FOR UPDATE`. */
|
|
118
|
+
const CAS_HEADER_SQL = `UPDATE oke_journal_runs SET flow = ?, input = ?, status = ?, entries = ?, wake_at = ?, error = ?, output = ?, locked_by = ?, lease_expires_at = ?, updated_at = ?, tenant = ?, lease_token = GREATEST(COALESCE(lease_token, 0), ?), code_version = ? WHERE id = ? AND COALESCE(lease_token, 0) = ?`;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Every statement the journal driver sends. The in-memory fake must match
|
|
122
|
+
* each one; a new statement that misses every branch fails the driver tests.
|
|
123
|
+
*/
|
|
124
|
+
export function postgresJournalStatements(): readonly string[] {
|
|
125
|
+
return [
|
|
126
|
+
CLAIM_LEASE_SQL,
|
|
127
|
+
CLAIM_DUE_SQL,
|
|
128
|
+
ORPHANS_SQL,
|
|
129
|
+
UPDATE_LEASE_SQL,
|
|
130
|
+
HEARTBEAT_SQL,
|
|
131
|
+
INSERT_ENTRY_SQL,
|
|
132
|
+
APPEND_ENTRY_FENCED_SQL,
|
|
133
|
+
UPDATE_ENTRY_FENCED_SQL,
|
|
134
|
+
FENCED_HEADER_SQL,
|
|
135
|
+
CAS_HEADER_SQL,
|
|
136
|
+
SELECT_ENTRIES_SQL,
|
|
137
|
+
RELEASE_LEASE_SQL,
|
|
138
|
+
UPSERT_SQL,
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const SELECT_ENTRIES_SQL = `SELECT * FROM oke_journal_entries WHERE run_id = ? ORDER BY seq ASC`;
|
|
89
143
|
|
|
90
144
|
/** Guarded release — never clears another holder's lease. */
|
|
91
145
|
const RELEASE_LEASE_SQL = `UPDATE oke_journal_runs SET locked_by = NULL, lease_expires_at = NULL WHERE id = ? AND locked_by = ?`;
|
|
@@ -171,9 +225,118 @@ function rowToRun(row: Record<string, unknown>): JournalRun {
|
|
|
171
225
|
if (r.tenant !== null && r.tenant !== undefined && String(r.tenant).length > 0) {
|
|
172
226
|
run.tenant = String(r.tenant);
|
|
173
227
|
}
|
|
228
|
+
const leaseToken = numOrNull(r.lease_token);
|
|
229
|
+
if (leaseToken !== null) run.leaseToken = leaseToken;
|
|
230
|
+
if (
|
|
231
|
+
r.code_version !== null &&
|
|
232
|
+
r.code_version !== undefined &&
|
|
233
|
+
String(r.code_version).length > 0
|
|
234
|
+
) {
|
|
235
|
+
run.codeVersion = String(r.code_version);
|
|
236
|
+
}
|
|
174
237
|
return run;
|
|
175
238
|
}
|
|
176
239
|
|
|
240
|
+
function sameEntry(a: JournalEntry, b: JournalEntry): boolean {
|
|
241
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function fencedHeaderParams(run: JournalRun, fence: JournalWriteFence): unknown[] {
|
|
245
|
+
const base = runToParams(run);
|
|
246
|
+
return [
|
|
247
|
+
base[1],
|
|
248
|
+
base[2],
|
|
249
|
+
base[3],
|
|
250
|
+
JSON.stringify([]),
|
|
251
|
+
base[5],
|
|
252
|
+
base[6],
|
|
253
|
+
base[7],
|
|
254
|
+
base[8],
|
|
255
|
+
base[9],
|
|
256
|
+
base[11],
|
|
257
|
+
base[12],
|
|
258
|
+
fence.leaseToken,
|
|
259
|
+
base[14],
|
|
260
|
+
run.id,
|
|
261
|
+
fence.lockedBy,
|
|
262
|
+
fence.leaseToken,
|
|
263
|
+
fence.now,
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function casHeaderParams(run: JournalRun, heldToken: number): unknown[] {
|
|
268
|
+
const base = runToParams(run);
|
|
269
|
+
return [
|
|
270
|
+
base[1],
|
|
271
|
+
base[2],
|
|
272
|
+
base[3],
|
|
273
|
+
JSON.stringify([]),
|
|
274
|
+
base[5],
|
|
275
|
+
base[6],
|
|
276
|
+
base[7],
|
|
277
|
+
base[8],
|
|
278
|
+
base[9],
|
|
279
|
+
base[11],
|
|
280
|
+
base[12],
|
|
281
|
+
run.leaseToken ?? heldToken,
|
|
282
|
+
base[14],
|
|
283
|
+
run.id,
|
|
284
|
+
heldToken,
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function entryToParams(runId: string, seq: number, entry: JournalEntry): unknown[] {
|
|
289
|
+
if (entry.kind === "step") {
|
|
290
|
+
return [runId, seq, "step", entry.name, null, JSON.stringify(entry.value ?? null), entry.at];
|
|
291
|
+
}
|
|
292
|
+
if (entry.kind === "sleep") {
|
|
293
|
+
return [
|
|
294
|
+
runId,
|
|
295
|
+
seq,
|
|
296
|
+
"sleep",
|
|
297
|
+
entry.label,
|
|
298
|
+
entry.duration,
|
|
299
|
+
JSON.stringify(entry.wakeAt),
|
|
300
|
+
entry.at,
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
return [
|
|
304
|
+
runId,
|
|
305
|
+
seq,
|
|
306
|
+
"effect",
|
|
307
|
+
entry.effectKind,
|
|
308
|
+
entry.resource,
|
|
309
|
+
JSON.stringify(entry.value ?? null),
|
|
310
|
+
entry.at,
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function entryFromRow(row: Record<string, unknown>): JournalEntry {
|
|
315
|
+
const kind = String(row.kind);
|
|
316
|
+
const at = Number(row.at);
|
|
317
|
+
const value =
|
|
318
|
+
row.value === null || row.value === undefined ? null : JSON.parse(String(row.value));
|
|
319
|
+
if (kind === "step") {
|
|
320
|
+
return { kind: "step", name: String(row.name ?? ""), value, at };
|
|
321
|
+
}
|
|
322
|
+
if (kind === "sleep") {
|
|
323
|
+
return {
|
|
324
|
+
kind: "sleep",
|
|
325
|
+
label: String(row.name ?? ""),
|
|
326
|
+
duration: String(row.resource ?? ""),
|
|
327
|
+
wakeAt: Number(value),
|
|
328
|
+
at,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
kind: "effect",
|
|
333
|
+
effectKind: String(row.name ?? ""),
|
|
334
|
+
resource: String(row.resource ?? ""),
|
|
335
|
+
value,
|
|
336
|
+
at,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
177
340
|
function runToParams(run: JournalRun): unknown[] {
|
|
178
341
|
return [
|
|
179
342
|
run.id,
|
|
@@ -189,6 +352,8 @@ function runToParams(run: JournalRun): unknown[] {
|
|
|
189
352
|
run.createdAt,
|
|
190
353
|
run.updatedAt,
|
|
191
354
|
run.tenant ?? null,
|
|
355
|
+
run.leaseToken ?? null,
|
|
356
|
+
run.codeVersion ?? null,
|
|
192
357
|
];
|
|
193
358
|
}
|
|
194
359
|
|
|
@@ -209,6 +374,18 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
|
|
|
209
374
|
tenant TEXT
|
|
210
375
|
)`);
|
|
211
376
|
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS tenant TEXT`);
|
|
377
|
+
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS lease_token BIGINT`);
|
|
378
|
+
await sql.exec(`ALTER TABLE oke_journal_runs ADD COLUMN IF NOT EXISTS code_version TEXT`);
|
|
379
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_journal_entries (
|
|
380
|
+
run_id TEXT NOT NULL,
|
|
381
|
+
seq BIGINT NOT NULL,
|
|
382
|
+
kind TEXT NOT NULL,
|
|
383
|
+
name TEXT,
|
|
384
|
+
resource TEXT,
|
|
385
|
+
value TEXT,
|
|
386
|
+
at BIGINT NOT NULL,
|
|
387
|
+
PRIMARY KEY (run_id, seq)
|
|
388
|
+
)`);
|
|
212
389
|
await sql.exec(
|
|
213
390
|
`CREATE INDEX IF NOT EXISTS oke_journal_runs_wake ON oke_journal_runs (status, wake_at)`,
|
|
214
391
|
);
|
|
@@ -290,6 +467,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
290
467
|
event_type: string | null;
|
|
291
468
|
event_name: string | null;
|
|
292
469
|
}[];
|
|
470
|
+
entries: JournalEntryDbRow[];
|
|
293
471
|
};
|
|
294
472
|
|
|
295
473
|
let committed: State = {
|
|
@@ -300,6 +478,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
300
478
|
candidates: [],
|
|
301
479
|
agentRuns: [],
|
|
302
480
|
agentEvents: [],
|
|
481
|
+
entries: [],
|
|
303
482
|
};
|
|
304
483
|
let active: { state: State; locked: Set<string>; done: boolean } | null = null;
|
|
305
484
|
/** Run ids held by other active transactions (SKIP LOCKED). */
|
|
@@ -320,6 +499,7 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
320
499
|
candidates: s.candidates.map((r) => ({ ...r })),
|
|
321
500
|
agentRuns: s.agentRuns.map((r) => ({ ...r })),
|
|
322
501
|
agentEvents: s.agentEvents.map((r) => ({ ...r })),
|
|
502
|
+
entries: s.entries.map((r) => ({ ...r })),
|
|
323
503
|
};
|
|
324
504
|
}
|
|
325
505
|
|
|
@@ -342,6 +522,50 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
342
522
|
return r.locked_by === null || (r.lease_expires_at !== null && r.lease_expires_at <= cutoff);
|
|
343
523
|
}
|
|
344
524
|
|
|
525
|
+
function fenceHolds(
|
|
526
|
+
state: { rows: JournalDbRow[] },
|
|
527
|
+
id: string,
|
|
528
|
+
lockedBy: string,
|
|
529
|
+
token: number,
|
|
530
|
+
now: number,
|
|
531
|
+
): boolean {
|
|
532
|
+
const row = state.rows.find((r) => r.id === id);
|
|
533
|
+
if (!row || row.locked_by !== lockedBy) return false;
|
|
534
|
+
if ((row.lease_token ?? 0) !== token) return false;
|
|
535
|
+
return row.lease_expires_at !== null && row.lease_expires_at > now;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function applyFencedHeader(
|
|
539
|
+
state: { rows: JournalDbRow[] },
|
|
540
|
+
text: string,
|
|
541
|
+
params: readonly unknown[],
|
|
542
|
+
): { changes: number } {
|
|
543
|
+
const tokenOnly = /COALESCE\(lease_token,\s*0\)\s*=\s*\?/i.test(text);
|
|
544
|
+
const id = String(params[13]);
|
|
545
|
+
const row = state.rows.find((r) => r.id === id);
|
|
546
|
+
if (!row) return { changes: 0 };
|
|
547
|
+
if (tokenOnly) {
|
|
548
|
+
if ((row.lease_token ?? 0) !== Number(params[14])) return { changes: 0 };
|
|
549
|
+
} else if (!fenceHolds(state, id, String(params[14]), Number(params[15]), Number(params[16]))) {
|
|
550
|
+
return { changes: 0 };
|
|
551
|
+
}
|
|
552
|
+
row.flow = String(params[0]);
|
|
553
|
+
row.input = params[1] === null || params[1] === undefined ? null : String(params[1]);
|
|
554
|
+
row.status = String(params[2]);
|
|
555
|
+
row.entries = String(params[3] ?? "[]");
|
|
556
|
+
row.wake_at = params[4] === null || params[4] === undefined ? null : Number(params[4]);
|
|
557
|
+
row.error = params[5] === null || params[5] === undefined ? null : String(params[5]);
|
|
558
|
+
row.output = params[6] === null || params[6] === undefined ? null : String(params[6]);
|
|
559
|
+
row.locked_by = params[7] === null || params[7] === undefined ? null : String(params[7]);
|
|
560
|
+
row.lease_expires_at = params[8] === null || params[8] === undefined ? null : Number(params[8]);
|
|
561
|
+
row.updated_at = Number(params[9] ?? 0);
|
|
562
|
+
row.tenant = params[10] === null || params[10] === undefined ? null : String(params[10]);
|
|
563
|
+
row.lease_token = Math.max(row.lease_token ?? 0, Number(params[11] ?? 0));
|
|
564
|
+
row.code_version =
|
|
565
|
+
params[12] === null || params[12] === undefined ? row.code_version : String(params[12]);
|
|
566
|
+
return { changes: 1 };
|
|
567
|
+
}
|
|
568
|
+
|
|
345
569
|
function tryLock(id: string): boolean {
|
|
346
570
|
if (heldByTxn.has(id) && !(active?.locked.has(id) ?? false)) return false;
|
|
347
571
|
if (active) {
|
|
@@ -481,6 +705,15 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
481
705
|
.map((row) => ({ ...row }));
|
|
482
706
|
}
|
|
483
707
|
|
|
708
|
+
if (/FROM\s+oke_journal_entries/i.test(text)) {
|
|
709
|
+
const runId = String(params[0] ?? "");
|
|
710
|
+
const seq = /seq\s*=\s*\?/i.test(text) ? Number(params[1]) : undefined;
|
|
711
|
+
return state.entries
|
|
712
|
+
.filter((row) => row.run_id === runId && (seq === undefined || row.seq === seq))
|
|
713
|
+
.sort((a, b) => a.seq - b.seq)
|
|
714
|
+
.map((row) => ({ ...row }));
|
|
715
|
+
}
|
|
716
|
+
|
|
484
717
|
throw new Error(`postgres journal fake: unsupported query: ${sql}`);
|
|
485
718
|
},
|
|
486
719
|
async exec(sql, params = []) {
|
|
@@ -518,23 +751,110 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
518
751
|
created_at: Number(record.created_at ?? 0),
|
|
519
752
|
updated_at: Number(record.updated_at ?? 0),
|
|
520
753
|
tenant: (record.tenant as string | null) ?? null,
|
|
754
|
+
lease_token: (() => {
|
|
755
|
+
const prev = state.rows.find((r) => r.id === String(record.id))?.lease_token ?? 0;
|
|
756
|
+
const incoming =
|
|
757
|
+
record.lease_token === undefined || record.lease_token === null
|
|
758
|
+
? prev
|
|
759
|
+
: Number(record.lease_token);
|
|
760
|
+
return Math.max(prev, incoming);
|
|
761
|
+
})(),
|
|
762
|
+
code_version:
|
|
763
|
+
record.code_version === undefined || record.code_version === null
|
|
764
|
+
? (state.rows.find((r) => r.id === String(record.id))?.code_version ?? null)
|
|
765
|
+
: String(record.code_version),
|
|
521
766
|
};
|
|
522
767
|
const idx = state.rows.findIndex((r) => r.id === next.id);
|
|
523
|
-
if (idx >= 0)
|
|
524
|
-
|
|
768
|
+
if (idx >= 0) {
|
|
769
|
+
const prev = state.rows[idx]!;
|
|
770
|
+
next.created_at = prev.created_at;
|
|
771
|
+
state.rows[idx] = next;
|
|
772
|
+
} else state.rows.push(next);
|
|
773
|
+
return { changes: 1 };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (/^UPDATE\s+oke_journal_runs\s+SET\s+lease_expires_at\s*=\s*GREATEST/i.test(text)) {
|
|
777
|
+
const id = String(params[1]);
|
|
778
|
+
const row = state.rows.find((r) => r.id === id);
|
|
779
|
+
if (
|
|
780
|
+
!row ||
|
|
781
|
+
!fenceHolds(state, id, String(params[2]), Number(params[3]), Number(params[4]))
|
|
782
|
+
) {
|
|
783
|
+
return { changes: 0 };
|
|
784
|
+
}
|
|
785
|
+
const next = Number(params[0]);
|
|
786
|
+
row.lease_expires_at = Math.max(row.lease_expires_at ?? 0, next);
|
|
525
787
|
return { changes: 1 };
|
|
526
788
|
}
|
|
527
789
|
|
|
528
790
|
const updLease =
|
|
529
|
-
/^UPDATE\s+oke_journal_runs\s+SET\s+locked_by\s*=\s*\?,\s*lease_expires_at\s*=\s
|
|
791
|
+
/^UPDATE\s+oke_journal_runs\s+SET\s+locked_by\s*=\s*\?,\s*lease_expires_at\s*=\s*\?/i.exec(
|
|
530
792
|
text,
|
|
531
793
|
);
|
|
532
|
-
if (updLease) {
|
|
533
|
-
const
|
|
794
|
+
if (updLease && /WHERE\s+id\s*=\s*\?/i.test(text) && !/locked_by\s*=\s*NULL/i.test(text)) {
|
|
795
|
+
const id = String(params[params.length - 1]);
|
|
796
|
+
const row = state.rows.find((r) => r.id === id);
|
|
534
797
|
if (!row) return { changes: 0 };
|
|
535
798
|
row.locked_by = params[0] === null ? null : String(params[0]);
|
|
536
799
|
row.lease_expires_at =
|
|
537
800
|
params[1] === null || params[1] === undefined ? null : Number(params[1]);
|
|
801
|
+
if (/lease_token/i.test(text)) row.lease_token = (row.lease_token ?? 0) + 1;
|
|
802
|
+
return { changes: 1 };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (/^UPDATE\s+oke_journal_runs\s+SET\s+flow\s*=/i.test(text)) {
|
|
806
|
+
return applyFencedHeader(state, text, params);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (/^UPDATE\s+oke_journal_entries\s+SET\s+kind\s*=/i.test(text)) {
|
|
810
|
+
const runId = String(params[5]);
|
|
811
|
+
const seq = Number(params[6]);
|
|
812
|
+
const fenceRun = String(params[7]);
|
|
813
|
+
const lockedBy = String(params[8]);
|
|
814
|
+
const token = Number(params[9]);
|
|
815
|
+
const at = Number(params[10]);
|
|
816
|
+
if (!fenceHolds(state, fenceRun, lockedBy, token, at)) return { changes: 0 };
|
|
817
|
+
const idx = state.entries.findIndex((e) => e.run_id === runId && e.seq === seq);
|
|
818
|
+
if (idx < 0) return { changes: 0 };
|
|
819
|
+
const row = state.entries[idx]!;
|
|
820
|
+
row.kind = String(params[0]);
|
|
821
|
+
row.name = params[1] === null || params[1] === undefined ? null : String(params[1]);
|
|
822
|
+
row.resource = params[2] === null || params[2] === undefined ? null : String(params[2]);
|
|
823
|
+
row.value = params[3] === null || params[3] === undefined ? null : String(params[3]);
|
|
824
|
+
row.at = Number(params[4]);
|
|
825
|
+
return { changes: 1 };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (/^INSERT\s+INTO\s+oke_journal_entries\b/i.test(text)) {
|
|
829
|
+
const fenced = /WHERE\s+EXISTS/i.test(text);
|
|
830
|
+
const row: JournalEntryDbRow = {
|
|
831
|
+
run_id: String(params[0]),
|
|
832
|
+
seq: Number(params[1]),
|
|
833
|
+
kind: String(params[2]),
|
|
834
|
+
name: params[3] === null || params[3] === undefined ? null : String(params[3]),
|
|
835
|
+
resource: params[4] === null || params[4] === undefined ? null : String(params[4]),
|
|
836
|
+
value: params[5] === null || params[5] === undefined ? null : String(params[5]),
|
|
837
|
+
at: Number(params[6]),
|
|
838
|
+
};
|
|
839
|
+
if (
|
|
840
|
+
fenced &&
|
|
841
|
+
!fenceHolds(
|
|
842
|
+
state,
|
|
843
|
+
String(params[7]),
|
|
844
|
+
String(params[8]),
|
|
845
|
+
Number(params[9]),
|
|
846
|
+
Number(params[10]),
|
|
847
|
+
)
|
|
848
|
+
) {
|
|
849
|
+
return { changes: 0 };
|
|
850
|
+
}
|
|
851
|
+
const idx = state.entries.findIndex((e) => e.run_id === row.run_id && e.seq === row.seq);
|
|
852
|
+
if (idx >= 0) {
|
|
853
|
+
if (/DO\s+NOTHING/i.test(text)) return { changes: 0 };
|
|
854
|
+
state.entries[idx] = row;
|
|
855
|
+
return { changes: 1 };
|
|
856
|
+
}
|
|
857
|
+
state.entries.push(row);
|
|
538
858
|
return { changes: 1 };
|
|
539
859
|
}
|
|
540
860
|
|
|
@@ -807,12 +1127,14 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
|
|
|
807
1127
|
return api;
|
|
808
1128
|
}
|
|
809
1129
|
|
|
810
|
-
const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at =
|
|
1130
|
+
const UPSERT_SQL = `INSERT INTO oke_journal_runs (id, flow, input, status, entries, wake_at, error, output, locked_by, lease_expires_at, created_at, updated_at, tenant, lease_token, code_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET flow = EXCLUDED.flow, input = EXCLUDED.input, status = EXCLUDED.status, entries = EXCLUDED.entries, wake_at = EXCLUDED.wake_at, error = EXCLUDED.error, output = EXCLUDED.output, locked_by = EXCLUDED.locked_by, lease_expires_at = EXCLUDED.lease_expires_at, created_at = oke_journal_runs.created_at, updated_at = EXCLUDED.updated_at, tenant = EXCLUDED.tenant, lease_token = GREATEST(COALESCE(oke_journal_runs.lease_token, 0), COALESCE(EXCLUDED.lease_token, 0)), code_version = EXCLUDED.code_version`;
|
|
811
1131
|
|
|
812
1132
|
/** Postgres journal store with run-level lease coordination. */
|
|
813
1133
|
export type PostgresJournalStore = JournalStore &
|
|
814
1134
|
JournalLeaseStore & {
|
|
815
1135
|
readonly sql: PostgresJournalSql;
|
|
1136
|
+
/** Bytes written for entry rows plus header entry payloads. */
|
|
1137
|
+
readonly writeBytes: number;
|
|
816
1138
|
close(): Promise<void>;
|
|
817
1139
|
};
|
|
818
1140
|
|
|
@@ -888,22 +1210,100 @@ export async function createPostgresJournalStore(
|
|
|
888
1210
|
|
|
889
1211
|
await ensureSchema(sql);
|
|
890
1212
|
|
|
1213
|
+
let writeBytes = 0;
|
|
1214
|
+
|
|
1215
|
+
async function hydrate(row: Record<string, unknown>): Promise<JournalRun> {
|
|
1216
|
+
const run = rowToRun(row);
|
|
1217
|
+
const entryRows = await sql.query(SELECT_ENTRIES_SQL, [run.id]);
|
|
1218
|
+
if (entryRows.length > 0) {
|
|
1219
|
+
(run as { entries: JournalEntry[] }).entries = entryRows.map((entry) => entryFromRow(entry));
|
|
1220
|
+
return run;
|
|
1221
|
+
}
|
|
1222
|
+
if (run.entries.length > 0) {
|
|
1223
|
+
for (let seq = 0; seq < run.entries.length; seq++) {
|
|
1224
|
+
const entry = run.entries[seq];
|
|
1225
|
+
if (!entry) continue;
|
|
1226
|
+
const params = entryToParams(run.id, seq, entry);
|
|
1227
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1228
|
+
await sql.exec(INSERT_ENTRY_SQL, params);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return run;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
891
1234
|
const store: PostgresJournalStore = {
|
|
892
1235
|
sql,
|
|
1236
|
+
get writeBytes() {
|
|
1237
|
+
return writeBytes;
|
|
1238
|
+
},
|
|
893
1239
|
idempotency: createPostgresIdempotencyStore(sql),
|
|
894
1240
|
decisions: lazyDecisionLabels(sql),
|
|
895
1241
|
agentEvents: lazyAgentEvents(sql),
|
|
896
1242
|
async get(runId) {
|
|
897
1243
|
const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
|
|
898
1244
|
if (!rows[0]) return undefined;
|
|
899
|
-
return
|
|
1245
|
+
return hydrate(rows[0]);
|
|
900
1246
|
},
|
|
901
|
-
async put(run) {
|
|
1247
|
+
async put(run, fence) {
|
|
1248
|
+
writeBytes += JSON.stringify(run.entries).length;
|
|
1249
|
+
if (fence) {
|
|
1250
|
+
const wrote = await sql.exec(FENCED_HEADER_SQL, fencedHeaderParams(run, fence));
|
|
1251
|
+
if (wrote.changes === 0) throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
902
1254
|
await sql.exec(UPSERT_SQL, runToParams(run));
|
|
1255
|
+
for (let seq = 0; seq < run.entries.length; seq++) {
|
|
1256
|
+
const entry = run.entries[seq];
|
|
1257
|
+
if (!entry) continue;
|
|
1258
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1259
|
+
await sql.exec(INSERT_ENTRY_SQL, entryToParams(run.id, seq, entry));
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
async appendEntry(runId, seq, entry, fence) {
|
|
1263
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1264
|
+
if (!fence) {
|
|
1265
|
+
await sql.exec(INSERT_ENTRY_SQL, entryToParams(runId, seq, entry));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const wrote = await sql.exec(APPEND_ENTRY_FENCED_SQL, [
|
|
1269
|
+
...entryToParams(runId, seq, entry),
|
|
1270
|
+
runId,
|
|
1271
|
+
fence.lockedBy,
|
|
1272
|
+
fence.leaseToken,
|
|
1273
|
+
fence.now,
|
|
1274
|
+
]);
|
|
1275
|
+
if (wrote.changes === 1) return;
|
|
1276
|
+
const stored = await sql.query(
|
|
1277
|
+
`SELECT * FROM oke_journal_entries WHERE run_id = ? AND seq = ?`,
|
|
1278
|
+
[runId, seq],
|
|
1279
|
+
);
|
|
1280
|
+
const row = stored[0];
|
|
1281
|
+
if (row && sameEntry(entryFromRow(row), entry)) return;
|
|
1282
|
+
throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
1283
|
+
},
|
|
1284
|
+
async updateEntry(runId, seq, entry, fence) {
|
|
1285
|
+
writeBytes += JSON.stringify(entry).length;
|
|
1286
|
+
const params = entryToParams(runId, seq, entry);
|
|
1287
|
+
const wrote = await sql.exec(UPDATE_ENTRY_FENCED_SQL, [
|
|
1288
|
+
params[2],
|
|
1289
|
+
params[3],
|
|
1290
|
+
params[4],
|
|
1291
|
+
params[5],
|
|
1292
|
+
params[6],
|
|
1293
|
+
runId,
|
|
1294
|
+
seq,
|
|
1295
|
+
runId,
|
|
1296
|
+
fence.lockedBy,
|
|
1297
|
+
fence.leaseToken,
|
|
1298
|
+
fence.now,
|
|
1299
|
+
]);
|
|
1300
|
+
if (wrote.changes === 0) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
903
1301
|
},
|
|
904
1302
|
async list() {
|
|
905
1303
|
const rows = await sql.query(`SELECT * FROM oke_journal_runs`);
|
|
906
|
-
|
|
1304
|
+
const runs: JournalRun[] = [];
|
|
1305
|
+
for (const row of rows) runs.push(await hydrate(row));
|
|
1306
|
+
return runs;
|
|
907
1307
|
},
|
|
908
1308
|
async acquireLease(runId, instanceId, now, leaseMs) {
|
|
909
1309
|
return sql.begin(async (tx) => {
|
|
@@ -925,13 +1325,70 @@ export async function createPostgresJournalStore(
|
|
|
925
1325
|
return { lease: true, leaseExpiresAt: current.leaseExpiresAt };
|
|
926
1326
|
}
|
|
927
1327
|
const current = rowToRun(row);
|
|
1328
|
+
const entryRows = await tx.query(SELECT_ENTRIES_SQL, [runId]);
|
|
1329
|
+
if (entryRows.length > 0) {
|
|
1330
|
+
(current as { entries: JournalEntry[] }).entries = entryRows.map((entry) =>
|
|
1331
|
+
entryFromRow(entry),
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
928
1334
|
const next = update(current) ?? current;
|
|
1335
|
+
const heldToken = current.leaseToken ?? 0;
|
|
1336
|
+
const renew =
|
|
1337
|
+
current.lockedBy === instanceId &&
|
|
1338
|
+
current.leaseExpiresAt !== undefined &&
|
|
1339
|
+
current.leaseExpiresAt > now &&
|
|
1340
|
+
current.leaseToken !== undefined;
|
|
929
1341
|
next.lockedBy = instanceId;
|
|
930
1342
|
next.leaseExpiresAt = now + leaseMs;
|
|
931
|
-
|
|
1343
|
+
next.leaseToken = renew ? heldToken : heldToken + 1;
|
|
1344
|
+
const header = { ...next, entries: [] as JournalEntry[] };
|
|
1345
|
+
const wrote = await tx.exec(CAS_HEADER_SQL, casHeaderParams(header, heldToken));
|
|
1346
|
+
if (wrote.changes === 0) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
1347
|
+
const fence = { lockedBy: instanceId, leaseToken: next.leaseToken, now };
|
|
1348
|
+
for (let seq = 0; seq < next.entries.length; seq++) {
|
|
1349
|
+
const entry = next.entries[seq];
|
|
1350
|
+
if (!entry) continue;
|
|
1351
|
+
const prev = current.entries[seq];
|
|
1352
|
+
if (!prev) {
|
|
1353
|
+
const inserted = await tx.exec(APPEND_ENTRY_FENCED_SQL, [
|
|
1354
|
+
...entryToParams(runId, seq, entry),
|
|
1355
|
+
runId,
|
|
1356
|
+
fence.lockedBy,
|
|
1357
|
+
fence.leaseToken,
|
|
1358
|
+
fence.now,
|
|
1359
|
+
]);
|
|
1360
|
+
if (inserted.changes === 0) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
1361
|
+
} else if (!sameEntry(prev, entry)) {
|
|
1362
|
+
const params = entryToParams(runId, seq, entry);
|
|
1363
|
+
const updated = await tx.exec(UPDATE_ENTRY_FENCED_SQL, [
|
|
1364
|
+
params[2],
|
|
1365
|
+
params[3],
|
|
1366
|
+
params[4],
|
|
1367
|
+
params[5],
|
|
1368
|
+
params[6],
|
|
1369
|
+
runId,
|
|
1370
|
+
seq,
|
|
1371
|
+
runId,
|
|
1372
|
+
fence.lockedBy,
|
|
1373
|
+
fence.leaseToken,
|
|
1374
|
+
fence.now,
|
|
1375
|
+
]);
|
|
1376
|
+
if (updated.changes === 0) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
932
1379
|
return "ok";
|
|
933
1380
|
});
|
|
934
1381
|
},
|
|
1382
|
+
async renewLeaseExpiry(runId, expiresAt, fence) {
|
|
1383
|
+
const wrote = await sql.exec(HEARTBEAT_SQL, [
|
|
1384
|
+
expiresAt,
|
|
1385
|
+
runId,
|
|
1386
|
+
fence.lockedBy,
|
|
1387
|
+
fence.leaseToken,
|
|
1388
|
+
fence.now,
|
|
1389
|
+
]);
|
|
1390
|
+
return wrote.changes === 1;
|
|
1391
|
+
},
|
|
935
1392
|
async releaseLease(runId, instanceId) {
|
|
936
1393
|
await sql.exec(RELEASE_LEASE_SQL, [runId, instanceId]);
|
|
937
1394
|
},
|
|
@@ -941,7 +1398,11 @@ export async function createPostgresJournalStore(
|
|
|
941
1398
|
const row = rows[0];
|
|
942
1399
|
if (!row) return undefined;
|
|
943
1400
|
await tx.exec(UPDATE_LEASE_SQL, [instanceId, now + leaseMs, String(row.id)]);
|
|
944
|
-
|
|
1401
|
+
const run = rowToRun(row);
|
|
1402
|
+
run.lockedBy = instanceId;
|
|
1403
|
+
run.leaseExpiresAt = now + leaseMs;
|
|
1404
|
+
run.leaseToken = (run.leaseToken ?? 0) + 1;
|
|
1405
|
+
return run;
|
|
945
1406
|
});
|
|
946
1407
|
},
|
|
947
1408
|
async listOrphans(now) {
|