okengine 0.23.1 → 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 +3 -3
- package/site/content/docs/elements/signal/broadcast.mdx +4 -2
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/errors.mdx +31 -19
- 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/http-parse.test.ts +101 -0
- package/src/compiler/http-parse.ts +188 -9
- package/src/compiler/interpret.ts +25 -2
- package/src/console/ui-next/dist/assets/{access-page-C_N4qvai.js → access-page-zBMhZnFm.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-Cq1TxniW.js → agent-disclosure-CBj9f7Ju.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BaI4uxZz.js → cache-glyph-D3MY49h3.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DNyK7IIY.js → call-pii-button-L9JxdBhp.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CRhBJMZR.js → collapsible-BvFaX-Zt.js} +1 -1
- package/src/console/ui-next/dist/assets/{decisions-page-D9twj8Xy.js → decisions-page-DgB76m-X.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-CsHWlVPU.js → duration-tone-l1DSJ2Vz.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-ZaTJAhR9.js → flows-page-Bb9t1lu6.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-DQ-EMAfz.js → highlighted-json-DMvsi3Ti.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-Cn_Fl79s.js → http-method-BwhJBTcQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-CmRFeWdT.js → index-RhV2jT_7.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-Cn6R_dXz.js → observability-page-BRfUILBq.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Bx6EZhSe.js → replica-lag-BsceAzIG.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DmdiUrfh.js → request-meta-C6lTyCXp.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-8P6vGXnn.js → store-page-YmE806bJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-BfGvpFSv.js → trace-detail-sheet-Di4irS49.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWdUHbc0.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-CZOFZzay.js → units-page-07agasRA.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-ki_ishmX.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 +84 -1
- package/src/drivers/journal-postgres.ts +282 -32
- package/src/drivers/postgres.test.ts +60 -0
- package/src/drivers/postgres.ts +79 -5
- package/src/drivers/signal-compete.test.ts +6 -2
- package/src/drivers/signal-postgres.test.ts +170 -0
- package/src/drivers/signal-postgres.ts +449 -196
- package/src/drivers/signal-redis.test.ts +224 -0
- package/src/drivers/signal-redis.ts +835 -150
- package/src/elements/ai/approval.test.ts +15 -3
- package/src/elements/ai/approval.ts +15 -9
- package/src/elements/channel/runtime.ts +31 -4
- package/src/elements/channel/sql-ledger.test.ts +404 -0
- package/src/elements/channel/sql-ledger.ts +177 -33
- package/src/elements/clock/chaos-child.ts +12 -2
- package/src/elements/clock/durable.ts +11 -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 +59 -0
- package/src/elements/store/cache.ts +157 -12
- 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.ts +183 -3
- package/src/kernel/app.ts +176 -28
- package/src/kernel/auto-registry.test.ts +5 -1
- package/src/kernel/boot-bind/signal.ts +13 -2
- package/src/kernel/boot-bind/store.ts +16 -1
- package/src/kernel/boot.test.ts +2 -2
- package/src/kernel/boot.ts +41 -11
- 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/fx-sql-handle.ts +7 -1
- package/src/kernel/horizontal-child.ts +54 -1
- package/src/kernel/horizontal.integration.test.ts +131 -7
- package/src/kernel/idempotency.test.ts +5 -6
- package/src/kernel/journal-boot.test.ts +83 -6
- package/src/kernel/journal.test.ts +44 -0
- package/src/kernel/journal.ts +171 -26
- package/src/mcp/authorization.ts +5 -11
- package/src/mcp/confirmation.ts +203 -38
- package/src/mcp/docs-server.ts +7 -2
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp.test.ts +321 -33
- package/src/mcp/server.ts +8 -3
- package/src/mcp/session.ts +7 -0
- package/src/mcp/tools.ts +68 -19
- package/src/runtime/bun.ts +5 -0
- package/src/runtime/types.ts +5 -0
|
@@ -8,6 +8,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
8
8
|
import { runDurable } from "../elements/clock/durable.ts";
|
|
9
9
|
import { flow } from "./flow.ts";
|
|
10
10
|
import {
|
|
11
|
+
clearJournalHeartbeats,
|
|
11
12
|
createJournal,
|
|
12
13
|
createMemoryJournalStore,
|
|
13
14
|
hasJournalLease,
|
|
@@ -171,6 +172,34 @@ describe("journal lease (memory store)", () => {
|
|
|
171
172
|
}
|
|
172
173
|
});
|
|
173
174
|
|
|
175
|
+
test("heartbeat extends the lease, and a lost lease does not persist the step", async () => {
|
|
176
|
+
let t = 1_000;
|
|
177
|
+
const store = createMemoryJournalStore();
|
|
178
|
+
const a = createJournal({
|
|
179
|
+
store,
|
|
180
|
+
now: () => t,
|
|
181
|
+
lease: { instanceId: "a", leaseMs: 60 },
|
|
182
|
+
codeVersion: "v1",
|
|
183
|
+
});
|
|
184
|
+
const session = await a.start("charge");
|
|
185
|
+
t = 1_030;
|
|
186
|
+
await Bun.sleep(40);
|
|
187
|
+
expect((await store.get(session.runId))?.leaseExpiresAt).toBe(1_090);
|
|
188
|
+
t = 2_000;
|
|
189
|
+
const b = createJournal({
|
|
190
|
+
store,
|
|
191
|
+
now: () => t,
|
|
192
|
+
lease: { instanceId: "b", leaseMs: 60 },
|
|
193
|
+
codeVersion: "v1",
|
|
194
|
+
});
|
|
195
|
+
await b.resume(session.runId);
|
|
196
|
+
await Bun.sleep(40);
|
|
197
|
+
await expect(session.step("capture", () => 1)).rejects.toBeInstanceOf(OkeError);
|
|
198
|
+
expect((await store.get(session.runId))?.entries ?? []).toHaveLength(0);
|
|
199
|
+
expect((await store.get(session.runId))?.lockedBy).toBe("b");
|
|
200
|
+
clearJournalHeartbeats();
|
|
201
|
+
});
|
|
202
|
+
|
|
174
203
|
test("replay throws when the next call does not match the journal", async () => {
|
|
175
204
|
const store = createMemoryJournalStore();
|
|
176
205
|
const journal = createJournal({ store, now: () => 1, codeVersion: "v1" });
|
|
@@ -198,6 +227,21 @@ describe("journal lease (memory store)", () => {
|
|
|
198
227
|
}
|
|
199
228
|
});
|
|
200
229
|
|
|
230
|
+
test("a 0.23.1 stamp resumes and is restamped when codeVersion is set", async () => {
|
|
231
|
+
const store = createMemoryJournalStore();
|
|
232
|
+
const started = await createJournal({ store, now: () => 1, codeVersion: "v1" }).start("charge");
|
|
233
|
+
const row = await store.get(started.runId);
|
|
234
|
+
if (row) {
|
|
235
|
+
row.codeVersion = "0.23.1";
|
|
236
|
+
await store.put(row);
|
|
237
|
+
}
|
|
238
|
+
const resumed = await createJournal({ store, now: () => 2, codeVersion: "v9" }).resume(
|
|
239
|
+
started.runId,
|
|
240
|
+
);
|
|
241
|
+
expect(resumed.run.codeVersion).toBe("app:v9");
|
|
242
|
+
expect((await store.get(started.runId))?.status).not.toBe("failed");
|
|
243
|
+
});
|
|
244
|
+
|
|
201
245
|
test("resume rejects a run stamped with another code version", async () => {
|
|
202
246
|
const store = createMemoryJournalStore();
|
|
203
247
|
const started = await createJournal({ store, now: () => 1, codeVersion: "v1" }).start("charge");
|
package/src/kernel/journal.ts
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
* (four-applications · Provisions).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFileSync } from "node:fs";
|
|
11
10
|
import { mkdir } from "node:fs/promises";
|
|
12
11
|
import { dirname } from "node:path";
|
|
13
12
|
import { okid } from "../okid.ts";
|
|
14
|
-
import { throwOke } from "./errors.ts";
|
|
13
|
+
import { OkeError, throwOke } from "./errors.ts";
|
|
15
14
|
import type { DecisionLabelStore } from "./decision-label-store.ts";
|
|
16
15
|
import type { IdempotencyStore } from "./idempotency-store.ts";
|
|
17
16
|
import { JournalSuspend } from "./journal-suspend.ts";
|
|
@@ -145,6 +144,15 @@ export interface JournalLeaseStore {
|
|
|
145
144
|
* @param leaseMs - Lease duration
|
|
146
145
|
*/
|
|
147
146
|
acquireLease(runId: string, instanceId: string, now: number, leaseMs: number): Promise<boolean>;
|
|
147
|
+
/**
|
|
148
|
+
* Extend `lease_expires_at` without changing the fencing token.
|
|
149
|
+
* Returns false when this holder no longer owns the run.
|
|
150
|
+
*
|
|
151
|
+
* @param runId - Run id
|
|
152
|
+
* @param expiresAt - New expiry, epoch-ms
|
|
153
|
+
* @param fence - Caller's lease fence
|
|
154
|
+
*/
|
|
155
|
+
renewLeaseExpiry?(runId: string, expiresAt: number, fence: JournalWriteFence): Promise<boolean>;
|
|
148
156
|
/**
|
|
149
157
|
* Take the lease and write `update(run)` in that same hold.
|
|
150
158
|
* `undefined` from `update` keeps the previous entries and still holds the lease.
|
|
@@ -259,6 +267,22 @@ export interface JournalStore extends Partial<JournalLeaseStore> {
|
|
|
259
267
|
entry: JournalEntry,
|
|
260
268
|
fence?: JournalWriteFence,
|
|
261
269
|
): Promise<void>;
|
|
270
|
+
/**
|
|
271
|
+
* Replace one existing entry under the caller's fence. Built-in stores
|
|
272
|
+
* implement this so approval and decide can correct a step without
|
|
273
|
+
* rewriting the run. Absent on a custom store — callers keep `put`.
|
|
274
|
+
*
|
|
275
|
+
* @param runId - Run id
|
|
276
|
+
* @param seq - Zero-based position
|
|
277
|
+
* @param entry - Replacement entry
|
|
278
|
+
* @param fence - Lease fence
|
|
279
|
+
*/
|
|
280
|
+
updateEntry?(
|
|
281
|
+
runId: string,
|
|
282
|
+
seq: number,
|
|
283
|
+
entry: JournalEntry,
|
|
284
|
+
fence: JournalWriteFence,
|
|
285
|
+
): Promise<void>;
|
|
262
286
|
/** List all runs (test / console helper). */
|
|
263
287
|
list(): Promise<readonly JournalRun[]>;
|
|
264
288
|
}
|
|
@@ -296,6 +320,27 @@ export function isJournalLeaseBusy(err: unknown): err is JournalLeaseBusy {
|
|
|
296
320
|
return err instanceof JournalLeaseBusy;
|
|
297
321
|
}
|
|
298
322
|
|
|
323
|
+
/**
|
|
324
|
+
* A fenced write lost the lease (OKE1074). The holder must not compensate
|
|
325
|
+
* or record the step — another instance may already own the run.
|
|
326
|
+
*
|
|
327
|
+
* @param err - Caught error
|
|
328
|
+
*/
|
|
329
|
+
export function isLostJournalLease(err: unknown): boolean {
|
|
330
|
+
return err instanceof OkeError && err.code === 1074;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const heartbeatTimers = new Set<ReturnType<typeof setInterval>>();
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Stop every session heartbeat. `stop` and `close` call this so a shut
|
|
337
|
+
* down process does not keep renewing leases it no longer owns.
|
|
338
|
+
*/
|
|
339
|
+
export function clearJournalHeartbeats(): void {
|
|
340
|
+
for (const timer of heartbeatTimers) clearInterval(timer);
|
|
341
|
+
heartbeatTimers.clear();
|
|
342
|
+
}
|
|
343
|
+
|
|
299
344
|
/** Live lease = a holder with an unexpired expiry. */
|
|
300
345
|
function hasLiveLease(run: JournalRun, now: number): boolean {
|
|
301
346
|
return run.lockedBy !== undefined && run.leaseExpiresAt !== undefined && run.leaseExpiresAt > now;
|
|
@@ -350,6 +395,20 @@ function leaseMethods(
|
|
|
350
395
|
release();
|
|
351
396
|
}
|
|
352
397
|
},
|
|
398
|
+
async renewLeaseExpiry(runId, expiresAt, fence) {
|
|
399
|
+
const map = await load();
|
|
400
|
+
const run = map.get(runId);
|
|
401
|
+
if (!run) return false;
|
|
402
|
+
const expired = run.leaseExpiresAt !== undefined && run.leaseExpiresAt <= fence.now;
|
|
403
|
+
if (run.lockedBy !== fence.lockedBy || run.leaseToken !== fence.leaseToken || expired) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (run.leaseExpiresAt === undefined || run.leaseExpiresAt < expiresAt) {
|
|
407
|
+
run.leaseExpiresAt = expiresAt;
|
|
408
|
+
}
|
|
409
|
+
await flush?.(map);
|
|
410
|
+
return true;
|
|
411
|
+
},
|
|
353
412
|
async releaseLease(runId, instanceId) {
|
|
354
413
|
const map = await load();
|
|
355
414
|
const run = map.get(runId);
|
|
@@ -410,6 +469,14 @@ export function createMemoryJournalStore(seed?: readonly JournalRun[]): JournalS
|
|
|
410
469
|
assertJournalFence(existing, fence);
|
|
411
470
|
appendStoredEntry(existing, seq, entry);
|
|
412
471
|
},
|
|
472
|
+
async updateEntry(runId, seq, entry, fence) {
|
|
473
|
+
const existing = runs.get(runId);
|
|
474
|
+
if (!existing) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
475
|
+
assertJournalFence(existing, fence);
|
|
476
|
+
const entries = existing.entries as JournalEntry[];
|
|
477
|
+
if (!entries[seq]) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
478
|
+
entries[seq] = structuredClone(entry);
|
|
479
|
+
},
|
|
413
480
|
async list() {
|
|
414
481
|
return [...runs.values()].map(cloneRun);
|
|
415
482
|
},
|
|
@@ -479,6 +546,16 @@ export function createFileJournalStore(path: string): JournalStore {
|
|
|
479
546
|
appendStoredEntry(existing, seq, entry);
|
|
480
547
|
await flush(map);
|
|
481
548
|
},
|
|
549
|
+
async updateEntry(runId, seq, entry, fence) {
|
|
550
|
+
const map = await load();
|
|
551
|
+
const existing = map.get(runId);
|
|
552
|
+
if (!existing) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
553
|
+
assertJournalFence(existing, fence);
|
|
554
|
+
const entries = existing.entries as JournalEntry[];
|
|
555
|
+
if (!entries[seq]) throwOke("JOURNAL_STALE_LEASE", { runId });
|
|
556
|
+
entries[seq] = structuredClone(entry);
|
|
557
|
+
await flush(map);
|
|
558
|
+
},
|
|
482
559
|
async list() {
|
|
483
560
|
const map = await load();
|
|
484
561
|
return [...map.values()].map(cloneRun);
|
|
@@ -674,7 +751,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
674
751
|
const now = options.now ?? (() => Date.now());
|
|
675
752
|
const newId = options.id ?? (() => okid());
|
|
676
753
|
const lease = options.lease;
|
|
677
|
-
const codeVersion = options.codeVersion
|
|
754
|
+
const codeVersion = stampAppVersion(options.codeVersion);
|
|
678
755
|
const coordinated = lease !== undefined && hasJournalLease(options.store);
|
|
679
756
|
|
|
680
757
|
function openSession(run: JournalRun, leased: boolean): JournalSession {
|
|
@@ -697,6 +774,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
697
774
|
}
|
|
698
775
|
|
|
699
776
|
async function persist(entry?: JournalEntry): Promise<void> {
|
|
777
|
+
if (lostLease) throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
|
|
700
778
|
run.updatedAt = now();
|
|
701
779
|
// Natural heartbeat — a live holder renews on every journal write.
|
|
702
780
|
if (leaseHeld && lease) {
|
|
@@ -742,8 +820,52 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
742
820
|
}
|
|
743
821
|
|
|
744
822
|
/** Parking / terminal states must not hold a short lease across days. */
|
|
823
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
824
|
+
let lostLease = false;
|
|
825
|
+
let activeStep = "";
|
|
826
|
+
|
|
827
|
+
function stopHeartbeat(): void {
|
|
828
|
+
if (!heartbeat) return;
|
|
829
|
+
clearInterval(heartbeat);
|
|
830
|
+
heartbeatTimers.delete(heartbeat);
|
|
831
|
+
heartbeat = undefined;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function startHeartbeat(): void {
|
|
835
|
+
if (!leased || !lease || heartbeat) return;
|
|
836
|
+
const every = Math.max(1, Math.floor((lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS) / 3));
|
|
837
|
+
heartbeat = setInterval(() => {
|
|
838
|
+
void (async () => {
|
|
839
|
+
if (!leaseHeld || lostLease || !options.store.renewLeaseExpiry) return;
|
|
840
|
+
const writeFence = fence();
|
|
841
|
+
if (!writeFence) return;
|
|
842
|
+
const expiresAt = now() + (lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS);
|
|
843
|
+
const ok = await options.store.renewLeaseExpiry(run.id, expiresAt, writeFence);
|
|
844
|
+
if (!ok) {
|
|
845
|
+
lostLease = true;
|
|
846
|
+
console.warn(
|
|
847
|
+
JSON.stringify({
|
|
848
|
+
event: "journal.lease.lost",
|
|
849
|
+
runId: run.id,
|
|
850
|
+
step: activeStep,
|
|
851
|
+
instanceId: lease.instanceId,
|
|
852
|
+
}),
|
|
853
|
+
);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (run.leaseExpiresAt === undefined || run.leaseExpiresAt < expiresAt) {
|
|
857
|
+
run.leaseExpiresAt = expiresAt;
|
|
858
|
+
}
|
|
859
|
+
})();
|
|
860
|
+
}, every);
|
|
861
|
+
heartbeat.unref?.();
|
|
862
|
+
heartbeatTimers.add(heartbeat);
|
|
863
|
+
}
|
|
864
|
+
|
|
745
865
|
function releaseLeaseLocally(): void {
|
|
746
866
|
leaseHeld = false;
|
|
867
|
+
lostLease = false;
|
|
868
|
+
stopHeartbeat();
|
|
747
869
|
delete run.lockedBy;
|
|
748
870
|
delete run.leaseExpiresAt;
|
|
749
871
|
}
|
|
@@ -772,6 +894,8 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
772
894
|
}
|
|
773
895
|
}
|
|
774
896
|
|
|
897
|
+
if (leased) startHeartbeat();
|
|
898
|
+
|
|
775
899
|
return {
|
|
776
900
|
runId: run.id,
|
|
777
901
|
run,
|
|
@@ -792,6 +916,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
792
916
|
return value;
|
|
793
917
|
}
|
|
794
918
|
assertCanAppendStep(name);
|
|
919
|
+
activeStep = name;
|
|
795
920
|
stepDepth += 1;
|
|
796
921
|
let stored: unknown;
|
|
797
922
|
try {
|
|
@@ -799,6 +924,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
799
924
|
} finally {
|
|
800
925
|
stepDepth -= 1;
|
|
801
926
|
}
|
|
927
|
+
if (lostLease) throwOke("JOURNAL_STALE_LEASE", { runId: run.id });
|
|
802
928
|
const value = reviveJournalValue(stored) as T;
|
|
803
929
|
const entry: JournalStepEntry = {
|
|
804
930
|
kind: "step",
|
|
@@ -808,7 +934,13 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
808
934
|
};
|
|
809
935
|
run.entries.push(entry);
|
|
810
936
|
cursor = run.entries.length;
|
|
811
|
-
|
|
937
|
+
try {
|
|
938
|
+
await persist(entry);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
run.entries.pop();
|
|
941
|
+
cursor = run.entries.length;
|
|
942
|
+
throw err;
|
|
943
|
+
}
|
|
812
944
|
if (!name.startsWith(JOURNAL_UNDO_PREFIX)) {
|
|
813
945
|
registerUndo(name, value, opts);
|
|
814
946
|
}
|
|
@@ -998,15 +1130,30 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
998
1130
|
}
|
|
999
1131
|
throw new Error(`journal: run "${runId}" not found`);
|
|
1000
1132
|
}
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1003
|
-
|
|
1133
|
+
if (
|
|
1134
|
+
codeVersion !== undefined &&
|
|
1135
|
+
run.codeVersion !== undefined &&
|
|
1136
|
+
run.codeVersion !== codeVersion
|
|
1137
|
+
) {
|
|
1138
|
+
if (run.codeVersion.startsWith("app:")) {
|
|
1139
|
+
run.status = "failed";
|
|
1140
|
+
run.error = "OKE1076";
|
|
1141
|
+
run.updatedAt = now();
|
|
1142
|
+
const skewFence =
|
|
1143
|
+
coordinated && lease && run.leaseToken !== undefined && run.lockedBy !== undefined
|
|
1144
|
+
? { lockedBy: run.lockedBy, leaseToken: run.leaseToken, now: now() }
|
|
1145
|
+
: undefined;
|
|
1146
|
+
await options.store.put(cloneRun(run), skewFence);
|
|
1147
|
+
if (coordinated && lease) {
|
|
1148
|
+
await options.store.releaseLease!(runId, lease.instanceId);
|
|
1149
|
+
}
|
|
1150
|
+
throwOke("JOURNAL_CODE_VERSION", {
|
|
1151
|
+
runId,
|
|
1152
|
+
expected: run.codeVersion,
|
|
1153
|
+
actual: codeVersion,
|
|
1154
|
+
});
|
|
1004
1155
|
}
|
|
1005
|
-
|
|
1006
|
-
runId,
|
|
1007
|
-
expected: run.codeVersion,
|
|
1008
|
-
actual: codeVersion,
|
|
1009
|
-
});
|
|
1156
|
+
run.codeVersion = codeVersion;
|
|
1010
1157
|
}
|
|
1011
1158
|
// Leave status intact — the durable runner parks or continues.
|
|
1012
1159
|
run.updatedAt = now();
|
|
@@ -1014,7 +1161,11 @@ export function createJournal(options: CreateJournalOptions): Journal {
|
|
|
1014
1161
|
run.lockedBy = lease.instanceId;
|
|
1015
1162
|
run.leaseExpiresAt = now() + (lease.leaseMs ?? JOURNAL_DEFAULT_LEASE_MS);
|
|
1016
1163
|
}
|
|
1017
|
-
|
|
1164
|
+
const resumeFence =
|
|
1165
|
+
coordinated && lease && run.leaseToken !== undefined && run.lockedBy !== undefined
|
|
1166
|
+
? { lockedBy: run.lockedBy, leaseToken: run.leaseToken, now: now() }
|
|
1167
|
+
: undefined;
|
|
1168
|
+
await options.store.put(cloneRun(run), resumeFence);
|
|
1018
1169
|
return openSession(run, coordinated);
|
|
1019
1170
|
},
|
|
1020
1171
|
};
|
|
@@ -1159,19 +1310,13 @@ function entryLabel(entry: JournalEntry): string {
|
|
|
1159
1310
|
return `effect ${entry.effectKind} ${entry.resource}`;
|
|
1160
1311
|
}
|
|
1161
1312
|
|
|
1162
|
-
let cachedPackageVersion: string | undefined;
|
|
1163
|
-
|
|
1164
1313
|
/**
|
|
1165
|
-
*
|
|
1314
|
+
* Namespace an opt-in code version so an okengine package version stored by
|
|
1315
|
+
* 0.23.1 (`"0.23.1"`) is not compared with an app stamp (`"app:1"`).
|
|
1316
|
+
*
|
|
1317
|
+
* @param version - Caller version, or unset to skip the check
|
|
1166
1318
|
*/
|
|
1167
|
-
function
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1170
|
-
const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
|
|
1171
|
-
const parsed = JSON.parse(raw) as { version?: string };
|
|
1172
|
-
cachedPackageVersion = parsed.version && parsed.version.length > 0 ? parsed.version : "0";
|
|
1173
|
-
} catch {
|
|
1174
|
-
cachedPackageVersion = "0";
|
|
1175
|
-
}
|
|
1176
|
-
return cachedPackageVersion;
|
|
1319
|
+
function stampAppVersion(version: string | undefined): string | undefined {
|
|
1320
|
+
if (version === undefined || version.length === 0) return undefined;
|
|
1321
|
+
return version.startsWith("app:") ? version : `app:${version}`;
|
|
1177
1322
|
}
|
package/src/mcp/authorization.ts
CHANGED
|
@@ -225,25 +225,19 @@ export const MCP_TOOL_POLICIES: readonly McpToolPolicy[] = [
|
|
|
225
225
|
name: "oke.action.confirm",
|
|
226
226
|
scopes: ["mcp:action:invoke", "console:flows:invoke"],
|
|
227
227
|
mutability: "read",
|
|
228
|
-
description:
|
|
228
|
+
description:
|
|
229
|
+
"Issue a single-use confirmation token for a pending write. The caller must be a different auth session than the requester (no caching).",
|
|
229
230
|
inputSchema: {
|
|
230
231
|
type: "object",
|
|
231
232
|
properties: {
|
|
232
|
-
|
|
233
|
-
type: "string",
|
|
234
|
-
enum: ["oke.action.invoke", "oke.action.structural_propose"],
|
|
235
|
-
},
|
|
236
|
-
args: { type: "object" },
|
|
233
|
+
confirmationId: { type: "string", minLength: 1 },
|
|
237
234
|
reason: { type: "string", minLength: 3 },
|
|
238
235
|
},
|
|
239
|
-
required: ["
|
|
236
|
+
required: ["confirmationId", "reason"],
|
|
240
237
|
additionalProperties: false,
|
|
241
238
|
},
|
|
242
239
|
params: [
|
|
243
|
-
{
|
|
244
|
-
name: "tool",
|
|
245
|
-
enum: ["oke.action.invoke", "oke.action.structural_propose"],
|
|
246
|
-
},
|
|
240
|
+
{ name: "confirmationId", maxLength: 200 },
|
|
247
241
|
{ name: "reason", maxLength: 2000 },
|
|
248
242
|
],
|
|
249
243
|
},
|