@sema-agent/core 1.293.0 → 1.294.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -213,6 +213,8 @@ export class Runner {
213
213
  sessionLocks = new Map();
214
214
  snapshotTooLargeRoots = new Map();
215
215
  pendingSessionNotifications = new PendingSessionNotifications();
216
+ parentConstraintRegistry = new Map();
217
+ static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
216
218
  constructor(deps) {
217
219
  this.deps = deps.memoryStore
218
220
  ? { ...deps, memoryStore: guardedMemoryStore(deps.memoryStore, deps.utilityGate) }
@@ -347,6 +349,18 @@ export class Runner {
347
349
  try {
348
350
  await this.runLocked(spec, queue, (r) => {
349
351
  resultValue = r;
352
+ const pcs = internals?.inheritedGate?.parentConstraints;
353
+ if (resume !== undefined && r.checkpointToken !== resume.cp.token) {
354
+ this.parentConstraintRegistry.delete(resume.cp.token);
355
+ }
356
+ if (r.status === "suspended" && r.checkpointToken !== undefined && pcs !== undefined && pcs.length > 0) {
357
+ if (this.parentConstraintRegistry.size >= Runner.PARENT_CONSTRAINT_REGISTRY_CAP) {
358
+ const oldest = this.parentConstraintRegistry.keys().next().value;
359
+ if (oldest !== undefined)
360
+ this.parentConstraintRegistry.delete(oldest);
361
+ }
362
+ this.parentConstraintRegistry.set(r.checkpointToken, pcs);
363
+ }
350
364
  }, (p) => {
351
365
  consolidationDone = p;
352
366
  }, (p) => {
@@ -365,7 +379,7 @@ export class Runner {
365
379
  }
366
380
  })();
367
381
  const settled = run.catch(async (err) => {
368
- const code = errorCodeOf(err);
382
+ let code = errorCodeOf(err);
369
383
  const reopenReason = code === "resume.env_failed"
370
384
  ? "env_failed"
371
385
  : code === "resume.tool_unavailable"
@@ -376,10 +390,20 @@ export class Runner {
376
390
  try {
377
391
  await resume.onEnvRestoreFailed(reopenReason);
378
392
  }
379
- catch {
393
+ catch (reopenErr) {
394
+ this.parentConstraintRegistry.delete(resume.cp.token);
395
+ if (errorCodeOf(reopenErr) === "checkpoint.reopen_failed") {
396
+ const original = err instanceof Error ? err : new Error(String(err));
397
+ const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
398
+ err = Object.assign(new Error(`${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), {
399
+ code: "checkpoint.reopen_failed",
400
+ });
401
+ code = "checkpoint.reopen_failed";
402
+ }
380
403
  }
381
404
  }
382
- else if (resume && !reopenable) {
405
+ else if (resume) {
406
+ this.parentConstraintRegistry.delete(resume.cp.token);
383
407
  try {
384
408
  await this.sessions.unpin?.(resume.cp.sessionId);
385
409
  }
@@ -445,6 +469,7 @@ export class Runner {
445
469
  }
446
470
  if (!won)
447
471
  return;
472
+ this.parentConstraintRegistry.delete(rh.token);
448
473
  if (rh.env && hasDestroy(rh.env)) {
449
474
  try {
450
475
  await rh.env.destroy();
@@ -2663,6 +2688,15 @@ export class Runner {
2663
2688
  if (!cp) {
2664
2689
  throw new CheckpointError("checkpoint.not_found", "no checkpoint found for the supplied token");
2665
2690
  }
2691
+ if ((internals?.inheritedGate?.parentConstraints?.length ?? 0) === 0) {
2692
+ const parked = this.parentConstraintRegistry.get(token);
2693
+ if (parked !== undefined) {
2694
+ internals = {
2695
+ ...(internals ?? {}),
2696
+ inheritedGate: { ...(internals?.inheritedGate ?? {}), parentConstraints: parked },
2697
+ };
2698
+ }
2699
+ }
2666
2700
  let wakeMessage;
2667
2701
  if (outcome.gate === "wake") {
2668
2702
  const gk = cp.gate.kind;
@@ -2756,12 +2790,28 @@ export class Runner {
2756
2790
  if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
2757
2791
  throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env");
2758
2792
  }
2793
+ if (cp.state.inheritedGate?.requiresParentConstraint === true) {
2794
+ const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
2795
+ if (supplied === 0) {
2796
+ throw new CheckpointError("resume.parent_constraint_missing", "this checkpoint's task ran under inherited parent-policy constraints (state.inheritedGate.requiresParentConstraint) " +
2797
+ "— the live policy/onAsk closures cannot be persisted, so the resume must re-supply them via " +
2798
+ "resumeStream(token, outcome, taskConfig, internals) with internals.inheritedGate.parentConstraints; " +
2799
+ "rejected pre-CAS (the checkpoint stays pending) rather than silently dropping the ancestors' gate");
2800
+ }
2801
+ const expected = cp.state.inheritedGate.parentConstraintCount;
2802
+ if (expected !== undefined && supplied !== expected) {
2803
+ throw new CheckpointError("resume.parent_constraint_mismatch", `re-supplied ${supplied} parent constraint(s) but the checkpoint recorded ${expected} — a partial/mismatched ` +
2804
+ "re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
2805
+ "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
2806
+ }
2807
+ }
2759
2808
  const won = await store.resolve(token, cp.scope, outcome, { rev: cp.rev ?? 0 });
2760
2809
  if (!won) {
2761
2810
  const live = await store.get(token);
2762
2811
  if (live?.status === "pending") {
2763
2812
  throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (its revision advanced via a resolve/reopen cycle since this resume validated) — not executed; re-resume against the current state");
2764
2813
  }
2814
+ this.parentConstraintRegistry.delete(token);
2765
2815
  throw new CheckpointError("checkpoint.already_resolved", "checkpoint already resolved or expired — not re-executed (idempotent)");
2766
2816
  }
2767
2817
  const spec = {
@@ -2770,9 +2820,13 @@ export class Runner {
2770
2820
  sessionId: cp.sessionId,
2771
2821
  preemptSignal: taskConfig.preemptSignal?.aborted ? undefined : taskConfig.preemptSignal,
2772
2822
  };
2773
- const onEnvRestoreFailed = store.reopen
2823
+ const reopenFn = store.reopen?.bind(store);
2824
+ const onEnvRestoreFailed = reopenFn
2774
2825
  ? async (reason) => {
2775
- await store.reopen?.(token, cp.scope, reason);
2826
+ const reopened = await reopenFn(token, cp.scope, reason);
2827
+ if (!reopened) {
2828
+ throw new CheckpointError("checkpoint.reopen_failed", "checkpoint could not be reopened for a retry (missing, expired, or changed concurrently) — the resume failure is terminal");
2829
+ }
2776
2830
  }
2777
2831
  : undefined;
2778
2832
  return this.runTaskStream(spec, { cp, outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);