@rulvar/core 1.59.3 → 1.59.4

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/index.d.ts CHANGED
@@ -2652,6 +2652,7 @@ declare class Replayer {
2652
2652
  private readonly runId;
2653
2653
  private readonly store;
2654
2654
  private readonly lease?;
2655
+ private readonly leaseOf?;
2655
2656
  private readonly now;
2656
2657
  private readonly priceUsd?;
2657
2658
  private readonly onWarn?;
@@ -2683,6 +2684,14 @@ declare class Replayer {
2683
2684
  * is asserted instead of fenced (the embedded default).
2684
2685
  */
2685
2686
  lease?: Lease;
2687
+ /**
2688
+ * Late-bound lease lookup (P0.2): consulted at EVERY append,
2689
+ * winning over the static `lease` when it returns one. The engine
2690
+ * passes its segment-lease holder here, because the
2691
+ * engine-acquired genesis lease exists only after the ownership
2692
+ * boot, which runs after this constructor.
2693
+ */
2694
+ leaseOf?: () => Lease | undefined;
2686
2695
  });
2687
2696
  /**
2688
2697
  * Forward-matches one live call against the prior journal. Fresh
@@ -5682,6 +5691,25 @@ interface CreateEngineOptions {
5682
5691
  security?: {
5683
5692
  argsHashSalt?: string;
5684
5693
  };
5694
+ /**
5695
+ * The genesis ownership protocol (P0.2): over a journal store with
5696
+ * the lease capability, a run or resume segment that was NOT handed
5697
+ * a lease acquires its own before its first durable mutation, renews
5698
+ * it at ttl/3 exactly like a queue worker, and releases it at
5699
+ * settle. Fresh start, in-process resume, and worker takeover then
5700
+ * share ONE owner/lease contract: at most one live driver per run
5701
+ * across processes, a second driver's acquire rejects with the typed
5702
+ * LeaseHeldError before any write or provider dispatch, and a
5703
+ * crashed owner's lease expires after the store ttl so a worker
5704
+ * sweep recovers the run. Default 'auto'. 'none' restores the
5705
+ * pre-1.59.4 behavior (no engine-acquired leases) for hosts that
5706
+ * coordinate ownership entirely outside the engine; a lease passed
5707
+ * via RunOptions.lease or ResumeOptions.lease always wins over both
5708
+ * modes (the caller owns acquire, renew, and release). Stores
5709
+ * without the lease capability are unaffected: the embedded
5710
+ * single-process default keeps the single-writer precondition.
5711
+ */
5712
+ ownership?: "auto" | "none";
5685
5713
  }
5686
5714
  interface RunOptions {
5687
5715
  /** Explicit id; otherwise the engine mints a ULID. */
@@ -5713,6 +5741,19 @@ interface RunOptions {
5713
5741
  tags?: string[];
5714
5742
  /** Host-initiated cancellation. */
5715
5743
  signal?: AbortSignal;
5744
+ /**
5745
+ * A lease the caller already holds for this run (the genesis side of
5746
+ * the ResumeOptions.lease contract): the engine carries it on EVERY
5747
+ * durable mutation of the fresh segment (every journal append, every
5748
+ * putMeta, every transcript blob write) and never acquires, renews,
5749
+ * or releases it itself; lifecycle stays with the caller. Passing it
5750
+ * disables the engine's own ownership acquisition for this run
5751
+ * regardless of the `ownership` mode. Hosts that admit runs through
5752
+ * an external queue acquire the lease at admission time and hand it
5753
+ * here, so admission and the first dispatch are covered by ONE
5754
+ * fencing epoch.
5755
+ */
5756
+ lease?: Lease;
5716
5757
  }
5717
5758
  /** Resume-time hit/miss/orphan accounting. */
5718
5759
  interface ResumePreview extends ResumeReport {
@@ -7334,8 +7375,12 @@ interface RunInternals {
7334
7375
  * worktree patches) exactly as the Replayer threads it into every
7335
7376
  * journal append, so a store declaring fencedWrites refuses a
7336
7377
  * superseded segment's blob overwrites (fenced run state RFC, F2).
7378
+ * The engine binds this as a live getter over its segment-lease
7379
+ * holder (P0.2), so the union with undefined is explicit: before
7380
+ * the ownership boot (and on non-leasable stores) it reads
7381
+ * undefined.
7337
7382
  */
7338
- lease?: Lease;
7383
+ lease?: Lease | undefined;
7339
7384
  adapters: ReadonlyMap<string, ProviderAdapter>;
7340
7385
  defaults: {
7341
7386
  routing?: Partial<Record<InvocationRole, ModelSpec>>;
package/dist/index.js CHANGED
@@ -420,6 +420,7 @@ function wrapJournalStore(inner, hook) {
420
420
  wrapped.acquire = (runId, owner) => inner.acquire(runId, owner);
421
421
  wrapped.renew = (l) => inner.renew(l);
422
422
  wrapped.release = (l) => inner.release(l);
423
+ if (typeof leasable.leaseTtlMs === "number") wrapped.leaseTtlMs = leasable.leaseTtlMs;
423
424
  }
424
425
  if (typeof inner.getMeta === "function") wrapped.getMeta = (runId) => inner.getMeta(runId);
425
426
  return wrapped;
@@ -6779,6 +6780,7 @@ var Replayer = class {
6779
6780
  runId;
6780
6781
  store;
6781
6782
  lease;
6783
+ leaseOf;
6782
6784
  now;
6783
6785
  priceUsd;
6784
6786
  onWarn;
@@ -6796,6 +6798,7 @@ var Replayer = class {
6796
6798
  this.runId = options.runId;
6797
6799
  this.store = options.store;
6798
6800
  if (options.lease !== void 0) this.lease = options.lease;
6801
+ if (options.leaseOf !== void 0) this.leaseOf = options.leaseOf;
6799
6802
  this.now = options.now ?? realNow;
6800
6803
  if (options.priceUsd !== void 0) this.priceUsd = options.priceUsd;
6801
6804
  if (options.onWarn !== void 0) this.onWarn = options.onWarn;
@@ -7083,7 +7086,7 @@ var Replayer = class {
7083
7086
  } });
7084
7087
  const shapeIssues = validateEntryShape(entry);
7085
7088
  if (shapeIssues.length > 0) throw new ConfigError(`journal entry shape violation (kind '${entry.kind}'): ` + shapeIssues.map((i) => i.message).join("; "));
7086
- await this.store.append(this.runId, entry, this.lease);
7089
+ await this.store.append(this.runId, entry, this.leaseOf?.() ?? this.lease);
7087
7090
  this.entries.push(entry);
7088
7091
  if (entry.status === "suspended") this.foldInternal.registerSuspended(entry);
7089
7092
  else if (entry.kind !== "resolution" && entry.kind !== "abandon") this.foldInternal.registerEntry(entry);
@@ -17119,6 +17122,28 @@ function hashRunOutput(value) {
17119
17122
  return;
17120
17123
  }
17121
17124
  }
17125
+ /**
17126
+ * Engine ownership identity: a process-local counter, not Math.random()
17127
+ * (the queue worker's identity convention): owner strings need
17128
+ * uniqueness within the store, and the dev-mode bare-randomness guard
17129
+ * stays armed while any run is live.
17130
+ */
17131
+ let engineOrdinal = 0;
17132
+ function engineIdentity() {
17133
+ engineOrdinal += 1;
17134
+ return `rulvar-engine:${process.pid}:${engineOrdinal}`;
17135
+ }
17136
+ /** Lease capability guard, mirroring createWorker's detection. */
17137
+ function leaseCapable(store) {
17138
+ const candidate = store;
17139
+ return typeof candidate.acquire === "function" && typeof candidate.renew === "function" && typeof candidate.release === "function";
17140
+ }
17141
+ /**
17142
+ * The renew-cadence fallback when a leasable store exposes no
17143
+ * leaseTtlMs: the Appendix A interim reference ttl the shipped stores
17144
+ * default to (60000 ms).
17145
+ */
17146
+ const ENGINE_DEFAULT_LEASE_TTL_MS = 6e4;
17122
17147
  function createEngine(options) {
17123
17148
  const adapters = buildAdapterRegistry(options.adapters);
17124
17149
  const rawJournal = options.stores?.journal ?? new InMemoryStore();
@@ -17128,6 +17153,17 @@ function createEngine(options) {
17128
17153
  const maskEvents = options.redaction?.maskEvents ?? true;
17129
17154
  const eventMasker = options.redaction?.patterns === void 0 ? void 0 : compileSecretMasker(options.redaction.patterns, "createEngine redaction.patterns");
17130
17155
  const defaults = options.defaults ?? {};
17156
+ const ownership = options.ownership ?? "auto";
17157
+ if (ownership !== "auto" && ownership !== "none") throw new ConfigError(`createEngine ownership must be 'auto' or 'none'; got '${String(ownership)}'`);
17158
+ const engineOwner = engineIdentity();
17159
+ let ownershipRenewMs = Math.max(1, Math.floor(ENGINE_DEFAULT_LEASE_TTL_MS / 3));
17160
+ if (ownership === "auto" && leaseCapable(journal)) {
17161
+ const storeTtlMs = journal.leaseTtlMs;
17162
+ if (storeTtlMs !== void 0) {
17163
+ if (!Number.isInteger(storeTtlMs) || storeTtlMs < 1 || storeTtlMs > 2147483647) throw new ConfigError(`the journal store's leaseTtlMs capability must report an integer between 1 and 2147483647 ms for the engine's ownership renew cadence; got ${String(storeTtlMs)}`);
17164
+ ownershipRenewMs = Math.max(1, Math.floor(storeTtlMs / 3));
17165
+ }
17166
+ }
17131
17167
  if (defaults.retry !== void 0) validateRetryPolicy(defaults.retry, "createEngine defaults.retry");
17132
17168
  if (options.concurrency?.perRun !== void 0) requirePositiveInteger(options.concurrency.perRun, "createEngine concurrency.perRun");
17133
17169
  for (const [adapterId, cap] of Object.entries(options.concurrency?.perProvider ?? {})) requirePositiveInteger(cap, `createEngine concurrency.perProvider['${adapterId}']`);
@@ -17182,6 +17218,10 @@ function createEngine(options) {
17182
17218
  if (compiled !== void 0 && options.runners?.sandbox === void 0) throw new ConfigError("running a CompiledWorkflow requires a sandbox runner: pass createEngine({ runners: { sandbox: new WorkerSandboxRunner() } }) from @rulvar/planner ");
17183
17219
  const runId = resumeCtx?.runId ?? opts?.runId ?? mintRunId();
17184
17220
  assertSafeRunId(runId, "engine.run");
17221
+ const suppliedLease = resumeCtx?.lease ?? opts?.lease;
17222
+ if (suppliedLease !== void 0 && suppliedLease.runId !== runId) throw new ConfigError(`the supplied lease is for run '${suppliedLease.runId}', not '${runId}'; a lease fences exactly the run it was acquired for`);
17223
+ const segmentLease = {};
17224
+ if (suppliedLease !== void 0) segmentLease.current = suppliedLease;
17185
17225
  const registry = buildDeriverRegistry(options.extraDerivers);
17186
17226
  const segmentsBefore = resumeCtx?.segmentsBefore ?? 0;
17187
17227
  const telemetryBase = segmentsBefore * EVENT_SEGMENT_STRIDE;
@@ -17218,7 +17258,7 @@ function createEngine(options) {
17218
17258
  }, rootSpanId),
17219
17259
  keyRing: registryKeyRing(registry),
17220
17260
  ...resumeCtx === void 0 ? {} : { priorEntries: resumeCtx.priorEntries },
17221
- ...resumeCtx?.lease === void 0 ? {} : { lease: resumeCtx.lease },
17261
+ leaseOf: () => segmentLease.current,
17222
17262
  strict: resumeCtx?.strict ?? false
17223
17263
  });
17224
17264
  for (const seqToInvalidate of invalidated) replayer.invalidate(seqToInvalidate);
@@ -17313,7 +17353,9 @@ function createEngine(options) {
17313
17353
  external,
17314
17354
  mintTranscriptRef: () => `${runId}/t${transcriptCounter++}`,
17315
17355
  now: realNow,
17316
- ...resumeCtx?.lease === void 0 ? {} : { lease: resumeCtx.lease }
17356
+ get lease() {
17357
+ return segmentLease.current;
17358
+ }
17317
17359
  };
17318
17360
  const argsBinding = {};
17319
17361
  if (resumeCtx === void 0) {
@@ -17341,15 +17383,60 @@ function createEngine(options) {
17341
17383
  workflowName: wf.name,
17342
17384
  workflowHash: compiled === void 0 ? hashWorkflowBody(wf) : hashWorkflowSource(compiled.source),
17343
17385
  ...compiled === void 0 ? {} : { workflowSourceRef: workflowSourceRef(runId) }
17344
- }, resumeCtx?.lease);
17386
+ }, segmentLease.current);
17345
17387
  if (activeSegments.has(runId)) throw new ConfigError(`run '${runId}' already has a live execution segment in this engine; await its settled result before starting another one (exactly one segment owns a run; https://docs.rulvar.com/guide/durability#resolving-a-settled-run)`);
17346
17388
  activeSegments.add(runId);
17389
+ let ownershipTeardown = () => Promise.resolve();
17347
17390
  const result = (async () => {
17391
+ let ownedLease;
17392
+ let renewTimer;
17393
+ const settleOwnership = async () => {
17394
+ if (renewTimer !== void 0) {
17395
+ clearInterval(renewTimer);
17396
+ renewTimer = void 0;
17397
+ }
17398
+ const held = ownedLease;
17399
+ ownedLease = void 0;
17400
+ if (held !== void 0) {
17401
+ segmentLease.current = void 0;
17402
+ try {
17403
+ await journal.release(held);
17404
+ } catch {}
17405
+ }
17406
+ };
17407
+ ownershipTeardown = settleOwnership;
17408
+ if (ownership === "auto" && segmentLease.current === void 0 && resumeCtx?.strict !== true && leaseCapable(journal)) {
17409
+ let acquired;
17410
+ try {
17411
+ acquired = await journal.acquire(runId, engineOwner);
17412
+ } catch (thrown) {
17413
+ if (deadlineTimer !== void 0) deadlineTimer.cancel();
17414
+ external.close();
17415
+ bus.end();
17416
+ throw thrown;
17417
+ }
17418
+ ownedLease = acquired;
17419
+ segmentLease.current = acquired;
17420
+ renewTimer = setInterval(() => {
17421
+ journal.renew(acquired).catch(() => {
17422
+ bus.emit({
17423
+ type: "log",
17424
+ level: "warn",
17425
+ msg: `run '${runId}' ownership lost: the lease could not be renewed and its fencing epoch may be superseded`
17426
+ }, rootSpanId);
17427
+ requestCancel("run ownership lost: lease fencing epoch superseded");
17428
+ if (renewTimer !== void 0) {
17429
+ clearInterval(renewTimer);
17430
+ renewTimer = void 0;
17431
+ }
17432
+ });
17433
+ }, ownershipRenewMs);
17434
+ }
17348
17435
  let status = "ok";
17349
17436
  let value;
17350
17437
  let wireError;
17351
17438
  let pending = [];
17352
- if (compiled !== void 0 && resumeCtx?.strict !== true) await transcripts.put(workflowSourceRef(runId), new TextEncoder().encode(compiled.source), resumeCtx?.lease);
17439
+ if (compiled !== void 0 && resumeCtx?.strict !== true) await transcripts.put(workflowSourceRef(runId), new TextEncoder().encode(compiled.source), segmentLease.current);
17353
17440
  await putMeta("running");
17354
17441
  bus.emit({
17355
17442
  type: "run:start",
@@ -17476,9 +17563,11 @@ function createEngine(options) {
17476
17563
  ...replayer.resumeReport(),
17477
17564
  invalidResolutions: replayer.fold.invalidResolutions()
17478
17565
  });
17566
+ await settleOwnership();
17479
17567
  return outcome;
17480
17568
  })();
17481
17569
  result.catch(() => void 0).finally(() => {
17570
+ ownershipTeardown();
17482
17571
  activeSegments.delete(runId);
17483
17572
  });
17484
17573
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.59.3",
3
+ "version": "1.59.4",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",