@sema-agent/server 1.205.0 → 1.207.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.
@@ -178,6 +178,7 @@ export function createHttpServer(deps) {
178
178
  const idemCache = new IdempotencyCache();
179
179
  const bakeSubmitLimiter = new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000);
180
180
  const inflightRuns = new Map();
181
+ const cancelledViaVerb = new Set();
181
182
  const preemptableRuns = new Map();
182
183
  const steerableRuns = new Map();
183
184
  const wakeParkMints = new Map();
@@ -742,7 +743,7 @@ export function createHttpServer(deps) {
742
743
  if (created.ok)
743
744
  deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective);
744
745
  if (!created.ok) {
745
- const conflict = { error: "session already has an active run", activeTaskId: created.activeTaskId };
746
+ const conflict = { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", activeTaskId: created.activeTaskId };
746
747
  res.write(`data: ${JSON.stringify({ type: "done", result: { status: "failed", errorMessage: conflict.error, activeTaskId: conflict.activeTaskId } })}\n\n`);
747
748
  return { status: 409, body: conflict };
748
749
  }
@@ -755,8 +756,21 @@ export function createHttpServer(deps) {
755
756
  const rs = deps.runStore;
756
757
  const tid = durableTaskId;
757
758
  const owner = principal ?? null;
758
- durableHeartbeat = setInterval(() => void rs.heartbeat(tid, owner).catch(() => undefined), HEARTBEAT_MS);
759
+ const abortFromVerb = () => {
760
+ cancelledViaVerb.add(tid);
761
+ ac.abort();
762
+ };
763
+ durableHeartbeat = setInterval(() => {
764
+ void rs.heartbeat(tid, owner).catch(() => undefined);
765
+ if (!ac.signal.aborted) {
766
+ void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c)
767
+ abortFromVerb(); }).catch(() => undefined);
768
+ }
769
+ }, HEARTBEAT_MS);
759
770
  durableHeartbeat.unref?.();
771
+ inflightRuns.set(tid, ac);
772
+ void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !ac.signal.aborted)
773
+ abortFromVerb(); }).catch(() => undefined);
760
774
  }
761
775
  const fleetPub = durableTaskId
762
776
  ? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: prepared.spec.sessionId, ...fleetRunLabels(prepared.spec.objective) })
@@ -841,6 +855,9 @@ export function createHttpServer(deps) {
841
855
  finalResult = stripCheckpointToken(ev.result);
842
856
  if (durableTaskId && finalResult)
843
857
  finalResult = { ...finalResult, taskId: durableTaskId };
858
+ if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
859
+ finalResult = { ...finalResult, errorCode: "cancelled" };
860
+ }
844
861
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
845
862
  putRewindAnchor();
846
863
  settleFleet(finalResult?.status ?? "completed");
@@ -947,6 +964,10 @@ export function createHttpServer(deps) {
947
964
  clearInterval(durableHeartbeat);
948
965
  if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
949
966
  steerableRuns.delete(durableTaskId);
967
+ if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
968
+ inflightRuns.delete(durableTaskId);
969
+ cancelledViaVerb.delete(durableTaskId);
970
+ }
950
971
  for (const evict of subagentHandleEvictions)
951
972
  evict();
952
973
  if (!runRowSettled && durableTaskId && deps.runStore) {
@@ -997,33 +1018,62 @@ export function createHttpServer(deps) {
997
1018
  const tid = uuidv7();
998
1019
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
999
1020
  if (!created.ok)
1000
- return { status: 409, body: { error: "session already has an active run", activeTaskId: created.activeTaskId } };
1021
+ return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", activeTaskId: created.activeTaskId } };
1001
1022
  deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective);
1002
1023
  durableTaskId = tid;
1003
1024
  }
1004
1025
  if (deps.checkpointStore && prepared.spec.sessionId) {
1005
1026
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
1006
1027
  }
1028
+ const syncCancelCtrl = new AbortController();
1029
+ if (prepared.spec.sessionId)
1030
+ markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, prepared.spec.sessionId, prepared.auth?.principal);
1007
1031
  if (durableTaskId && deps.runStore) {
1008
1032
  const rs = deps.runStore;
1009
1033
  const tid = durableTaskId;
1010
1034
  const owner = principal ?? null;
1011
- durableHeartbeat = setInterval(() => void rs.heartbeat(tid, owner).catch(() => undefined), HEARTBEAT_MS);
1035
+ const abortFromVerb = () => {
1036
+ cancelledViaVerb.add(tid);
1037
+ syncCancelCtrl.abort();
1038
+ };
1039
+ durableHeartbeat = setInterval(() => {
1040
+ void rs.heartbeat(tid, owner).catch(() => undefined);
1041
+ if (!syncCancelCtrl.signal.aborted) {
1042
+ void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c)
1043
+ abortFromVerb(); }).catch(() => undefined);
1044
+ }
1045
+ }, HEARTBEAT_MS);
1012
1046
  durableHeartbeat.unref?.();
1047
+ inflightRuns.set(tid, syncCancelCtrl);
1048
+ void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !syncCancelCtrl.signal.aborted)
1049
+ abortFromVerb(); }).catch(() => undefined);
1050
+ }
1051
+ else if (durableTaskId) {
1052
+ inflightRuns.set(durableTaskId, syncCancelCtrl);
1013
1053
  }
1054
+ const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal };
1014
1055
  let result;
1056
+ let cancelVerbLabel = false;
1015
1057
  uncountedBillableInflight++;
1016
1058
  try {
1017
1059
  result = await withPrincipal(principal, () => prepared.verify
1018
- ? runWithVerification(deps.runner, prepared.spec, { maxRounds: prepared.verify.maxRounds })
1060
+ ? runWithVerification(deps.runner, specWithSignal, { maxRounds: prepared.verify.maxRounds })
1019
1061
  : prepared.cascade
1020
- ? runCascade(deps.runner, prepared.spec, cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd))
1021
- : deps.runner.runTask(prepared.spec));
1062
+ ? runCascade(deps.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.maxCostUsd))
1063
+ : deps.runner.runTask(specWithSignal));
1022
1064
  }
1023
1065
  finally {
1024
1066
  uncountedBillableInflight--;
1025
1067
  if (durableHeartbeat)
1026
1068
  clearInterval(durableHeartbeat);
1069
+ if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
1070
+ inflightRuns.delete(durableTaskId);
1071
+ cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
1072
+ cancelledViaVerb.delete(durableTaskId);
1073
+ }
1074
+ }
1075
+ if (cancelVerbLabel && result.status === "failed" && !result.errorCode) {
1076
+ result = { ...result, errorCode: "cancelled" };
1027
1077
  }
1028
1078
  const resumeAtStatus = resumeAtHttpStatus(result);
1029
1079
  if (resumeAtStatus) {
@@ -1099,7 +1149,7 @@ export function createHttpServer(deps) {
1099
1149
  if (clientTaskId && created.activeTaskId === clientTaskId) {
1100
1150
  return { status: 202, body: { taskId: clientTaskId, sessionId, status: "running" } };
1101
1151
  }
1102
- return { status: 409, body: { error: "session already has an active run", activeTaskId: created.activeTaskId } };
1152
+ return { status: 409, body: { error: "session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)", activeTaskId: created.activeTaskId } };
1103
1153
  }
1104
1154
  if (deps.checkpointStore) {
1105
1155
  await deps.checkpointStore.putCtx(sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
@@ -1197,7 +1247,7 @@ export function createHttpServer(deps) {
1197
1247
  const taskId = cancelMatch[1];
1198
1248
  const run = await deps.runStore.getRun(taskId);
1199
1249
  if (!run) {
1200
- sendJson(res, 404, { error: "run not found" });
1250
+ sendJson(res, 404, { error: "run not found — the id belongs to no run in this deployment's run store (a run from another server process, or an in-memory store that did not survive a restart, is not visible here)" });
1201
1251
  return;
1202
1252
  }
1203
1253
  if (!runOwnerOk(req, res, run.owner))
@@ -1245,10 +1295,30 @@ export function createHttpServer(deps) {
1245
1295
  const err = note;
1246
1296
  try {
1247
1297
  const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
1248
- await rs.setTerminal(taskId, "failed", result, err);
1298
+ let lastErr;
1299
+ for (let attempt = 0;; attempt++) {
1300
+ try {
1301
+ await rs.setTerminal(taskId, "failed", result, err);
1302
+ lastErr = undefined;
1303
+ break;
1304
+ }
1305
+ catch (e) {
1306
+ lastErr = e;
1307
+ if (attempt >= 2)
1308
+ break;
1309
+ await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
1310
+ }
1311
+ }
1312
+ if (lastErr !== undefined)
1313
+ throw lastErr;
1249
1314
  }
1250
1315
  catch (e) {
1251
- sendJson(res, 500, { taskId, error: `cancel progressed (${note}) but could not terminalize the run row — retry cancel; a stuck row is reaped after the stale window (${e instanceof Error ? e.message : String(e)})` });
1316
+ sendJson(res, 500, { taskId, error: `cancel progressed (${note}) but could not terminalize the run row (retried) — retry cancel; a stuck row is reaped after the stale window (${e instanceof Error ? e.message : String(e)})` });
1317
+ return;
1318
+ }
1319
+ const finalRow = await rs.getRun(taskId).catch(() => undefined);
1320
+ if (finalRow && finalRow.errorCode !== "cancelled" && (finalRow.status === "failed" || finalRow.status === "completed" || finalRow.status === "blocked" || finalRow.status === "timeout")) {
1321
+ sendJson(res, 202, { taskId, status: finalRow.status, errorCode: finalRow.errorCode ?? null, note: `session unlocked; the run was terminalized concurrently (${finalRow.errorCode ?? finalRow.status}) before this cancel's write — reporting the actual ledger state` });
1252
1322
  return;
1253
1323
  }
1254
1324
  sendJson(res, 202, { taskId, status: "failed", errorCode: "cancelled", note });
@@ -1265,7 +1335,10 @@ export function createHttpServer(deps) {
1265
1335
  }
1266
1336
  return;
1267
1337
  }
1268
- inflightRuns.get(taskId)?.abort();
1338
+ if (inflightRuns.has(taskId)) {
1339
+ cancelledViaVerb.add(taskId);
1340
+ inflightRuns.get(taskId).abort();
1341
+ }
1269
1342
  sendJson(res, 202, { taskId, status: "cancelling" });
1270
1343
  }
1271
1344
  else if (run.status === "suspended" || run.status === "needs_review") {
@@ -4323,8 +4396,10 @@ export function createHttpServer(deps) {
4323
4396
  }
4324
4397
  finally {
4325
4398
  if (taskId) {
4326
- inflightRuns.delete(taskId);
4327
- preemptableRuns.delete(taskId);
4399
+ if (inflightRuns.get(taskId) === cancelCtrl)
4400
+ inflightRuns.delete(taskId);
4401
+ if (preemptableRuns.get(taskId) === preemptCtrl)
4402
+ preemptableRuns.delete(taskId);
4328
4403
  if (resumeStreamRef !== undefined && steerableRuns.get(taskId) === resumeStreamRef)
4329
4404
  steerableRuns.delete(taskId);
4330
4405
  for (const evict of subagentHandleEvictions)