@isomorph.ai/app-sdk 1.36.1 → 1.36.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.
Files changed (2) hide show
  1. package/dist/runtime.js +30 -9
  2. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -237,18 +237,31 @@ export class Runs {
237
237
  const interrupted = expired.filter(run => !active.has(run.id));
238
238
  for (const run of interrupted) {
239
239
  const exhausted = run.attempt >= run.max_attempts;
240
- await this.table().update({ status: exhausted ? 'failed' : 'queued', error: exhausted ? 'the worker was interrupted and no attempts remain' : run.error, lease_until: null, updated_at: now.toISOString() }).eq('id', run.id).eq('status', 'running');
240
+ const status = run.cancel_requested ? 'cancelled' : exhausted ? 'failed' : 'queued';
241
+ const { data } = await this.table().update({ status, error: status === 'failed' ? 'the worker was interrupted and no attempts remain' : run.error, lease_until: null, updated_at: now.toISOString() }).eq('id', run.id).eq('status', 'running').eq('cancel_requested', run.cancel_requested).select('id').maybeSingle();
242
+ // Cancellation may have arrived between reading the expired lease and
243
+ // writing its retry. It wins that race without leaving another attempt.
244
+ if (!data && !run.cancel_requested)
245
+ await this.table().update({ status: 'cancelled', lease_until: null, updated_at: now.toISOString() }).eq('id', run.id).eq('status', 'running').eq('cancel_requested', true);
241
246
  }
242
247
  return interrupted.length;
243
248
  }
244
249
  /** Claims the oldest queued run by a conditional update; null when the queue is empty or another worker won. */
245
250
  async claim(now) {
246
- const { data: queued } = await this.table().select('id, attempt').eq('status', 'queued').order('created_at').limit(1);
247
- const next = queued[0];
248
- if (!next)
249
- return null;
250
- const { data } = await this.table().update({ status: 'running', attempt: next.attempt + 1, lease_until: new Date(now.getTime() + LEASE_MS).toISOString(), updated_at: now.toISOString() }).eq('id', next.id).eq('status', 'queued').select('*').maybeSingle();
251
- return data;
251
+ while (true) {
252
+ const { data: queued } = await this.table().select('id, attempt, cancel_requested').eq('status', 'queued').order('created_at').limit(1);
253
+ const next = queued[0];
254
+ if (!next)
255
+ return null;
256
+ if (next.cancel_requested) {
257
+ const { data } = await this.table().update({ status: 'cancelled', lease_until: null, updated_at: now.toISOString() }).eq('id', next.id).eq('status', 'queued').eq('cancel_requested', true).select('id').maybeSingle();
258
+ if (!data)
259
+ return null;
260
+ continue;
261
+ }
262
+ const { data } = await this.table().update({ status: 'running', attempt: next.attempt + 1, lease_until: new Date(now.getTime() + LEASE_MS).toISOString(), updated_at: now.toISOString() }).eq('id', next.id).eq('status', 'queued').eq('cancel_requested', false).select('*').maybeSingle();
263
+ return data;
264
+ }
252
265
  }
253
266
  async progress(id, percent, note) {
254
267
  await this.table().update({ progress: Math.max(0, Math.min(100, Math.round(percent))), ...(note === undefined ? {} : { note }), lease_until: new Date(Date.now() + LEASE_MS).toISOString(), updated_at: new Date().toISOString() }).eq('id', id);
@@ -261,8 +274,16 @@ export class Runs {
261
274
  async finish(run, outcome) {
262
275
  const retry = outcome.error !== undefined && !outcome.cancelled && run.attempt < run.max_attempts;
263
276
  const status = outcome.cancelled ? 'cancelled' : outcome.error === undefined ? 'succeeded' : retry ? 'queued' : 'failed';
264
- await this.table().update({ status, result: outcome.result ?? null, error: outcome.error ?? null, progress: status === 'succeeded' ? 100 : run.progress, lease_until: null, updated_at: new Date().toISOString() }).eq('id', run.id).eq('status', 'running');
265
- return status;
277
+ const now = new Date().toISOString();
278
+ if (!outcome.cancelled) {
279
+ const { data } = await this.table().update({ status, result: outcome.result ?? null, error: outcome.error ?? null, progress: status === 'succeeded' ? 100 : run.progress, lease_until: null, updated_at: now }).eq('id', run.id).eq('status', 'running').eq('cancel_requested', false).select('id').maybeSingle();
280
+ if (data)
281
+ return status;
282
+ }
283
+ // The handler may throw after the user cancels. A cancelled run is
284
+ // terminal even when that failure would otherwise be retried.
285
+ await this.table().update({ status: 'cancelled', result: null, error: null, lease_until: null, updated_at: now }).eq('id', run.id).eq('status', 'running').eq('cancel_requested', true);
286
+ return 'cancelled';
266
287
  }
267
288
  }
268
289
  export class Runtime {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.36.1",
3
+ "version": "1.36.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",