@isomorph.ai/app-sdk 1.31.0 → 1.32.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.
package/README.md CHANGED
@@ -485,7 +485,7 @@ On a serverless cell Harbour runs an app's server code as Lambda functions, from
485
485
 
486
486
  ### Email-arrival jobs
487
487
 
488
- On managed serverless hosting, an ordinary job can declare an inbox trigger instead of a schedule:
488
+ On managed same-account serverless or Kubernetes hosting, an ordinary job can declare an inbox trigger instead of a schedule:
489
489
 
490
490
  ```ts
491
491
  export const trigger = {
@@ -506,8 +506,29 @@ may additionally require an exact sender. Deployment registers the job; activati
506
506
  the app owner's approved read access and background consent for that environment.
507
507
  Multiple apps share one provider watch per mailbox. Notifications enqueue the existing durable
508
508
  job runtime; duplicate deliveries retain the same run instead of rerunning a completed job.
509
- This does not add a per-app polling timer. Cluster hosting and cross-account delivery are not
510
- supported by this first release. Local manual job execution tests the handler, not the provider
509
+ This does not add a per-app polling timer. Cross-account delivery is not supported. Local manual job execution tests the handler, not the provider
511
510
  subscription; verify a real incoming email after deployment before calling the trigger live.
512
511
 
513
512
  For PO-linked Zoho bills, first read the PO and send each returned `lineItemId` as the bill line’s `purchaseOrderItemId`. `itemId` identifies the catalog item; it does not identify the PO line. Purchase-order list results are summaries; read the document before comparing lines.
513
+
514
+
515
+ ### Folder-arrival jobs
516
+
517
+ ```ts
518
+ export const trigger = {
519
+ type: 'file.created', connection: 'company-drive', resource: 'my-drive',
520
+ folderId: 'provider-folder-id', mimeTypes: ['application/pdf'],
521
+ };
522
+ export default async (ctx) => {
523
+ // ctx.input is FileArrivalInput: provider/sourceId/fileId/folderId/driveId?,
524
+ // name/mimeType/size/createdAt/observedAt. Read bytes through the existing integration.
525
+ };
526
+ ```
527
+
528
+ SharePoint uses its configured site alias as `resource` and also requires `driveId`.
529
+ The trigger covers newly created direct-child PDF/XLSX files observed after activation;
530
+ omit `mimeTypes` for both formats. Existing files moved into the folder, edits, renames,
531
+ subfolders, shortcuts and native Google documents are excluded. No historical backfill.
532
+ Background read consent is required. Existing file-read size limits still apply. Provider
533
+ subscriptions and real upload acceptance must be verified after deployment. Retries retain
534
+ one durable job run; accounting/email side effects still need their own idempotency keys.
package/dist/index.d.ts CHANGED
@@ -1962,3 +1962,17 @@ export type HarbourResult<T> = IsomorphResult<T>;
1962
1962
  export type HarbourClient = IsomorphClient;
1963
1963
  /** @deprecated Internal transformation lane only; a kit app uses IsomorphCompatSource. */
1964
1964
  export type HarbourCompatSource = import('./supabase-compat.js').IsomorphCompatSource;
1965
+ /** Provider file metadata delivered to a file.created job; bytes are read through its connection. */
1966
+ export interface FileArrivalInput {
1967
+ type: 'file.created';
1968
+ provider: 'drive' | 'sharepoint';
1969
+ sourceId: string;
1970
+ fileId: string;
1971
+ folderId: string;
1972
+ driveId?: string;
1973
+ name: string;
1974
+ mimeType: string;
1975
+ size: number;
1976
+ createdAt: string;
1977
+ observedAt: string;
1978
+ }
package/dist/jobs.d.ts CHANGED
@@ -8,12 +8,23 @@ export interface EmailArrivalTrigger {
8
8
  subjectContains?: string;
9
9
  };
10
10
  }
11
+ export declare const FILE_MIME_TYPES: readonly ["application/pdf", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"];
12
+ export declare const providerFileId: (value: unknown) => value is string;
13
+ export interface FileCreatedTrigger {
14
+ type: 'file.created';
15
+ connection: string;
16
+ resource: string;
17
+ folderId: string;
18
+ driveId?: string;
19
+ mimeTypes?: string[];
20
+ }
21
+ export type EventTrigger = EmailArrivalTrigger | FileCreatedTrigger;
11
22
  export interface JobDefinition {
12
23
  name: string;
13
24
  schedule?: string;
14
- trigger?: EmailArrivalTrigger;
25
+ trigger?: EventTrigger;
15
26
  }
16
- export declare function emailArrivalTrigger(value: unknown): EmailArrivalTrigger;
27
+ export declare function eventTrigger(value: unknown): EventTrigger;
17
28
  export interface JobContext {
18
29
  scheduledAt: string;
19
30
  runId?: string;
package/dist/jobs.js CHANGED
@@ -6,10 +6,14 @@ import { stripTypeScriptTypes } from 'node:module';
6
6
  import { parse } from 'acorn';
7
7
  export const JOBS_DIR = 'jobs';
8
8
  const LOGICAL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
9
+ export const FILE_MIME_TYPES = ['application/pdf', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
10
+ export const providerFileId = (value) => typeof value === 'string' && /^[A-Za-z0-9!._,=-]{1,256}$/.test(value);
9
11
  function literalObject(node) {
10
12
  const value = node;
11
13
  if (value?.type === 'Literal')
12
14
  return value.value;
15
+ if (value?.type === 'ArrayExpression')
16
+ return value.elements.map(literalObject);
13
17
  if (value?.type !== 'ObjectExpression')
14
18
  throw new Error('trigger must be a literal object');
15
19
  const result = {};
@@ -23,7 +27,17 @@ function literalObject(node) {
23
27
  }
24
28
  return result;
25
29
  }
26
- export function emailArrivalTrigger(value) {
30
+ export function eventTrigger(value) {
31
+ if (value?.type === 'file.created') {
32
+ const t = value;
33
+ if (Object.keys(t).some(k => !['type', 'connection', 'resource', 'folderId', 'driveId', 'mimeTypes'].includes(k))
34
+ || typeof t.connection !== 'string' || !LOGICAL_NAME.test(t.connection)
35
+ || typeof t.resource !== 'string' || !t.resource.trim() || t.resource.length > 320
36
+ || !providerFileId(t.folderId) || (t.driveId !== undefined && !providerFileId(t.driveId))
37
+ || (t.mimeTypes !== undefined && (!Array.isArray(t.mimeTypes) || !t.mimeTypes.length || t.mimeTypes.length > 2 || new Set(t.mimeTypes).size !== t.mimeTypes.length || t.mimeTypes.some(m => !FILE_MIME_TYPES.includes(m)))))
38
+ throw new Error('file.created requires a connection, resource, folder ID and optional PDF/XLSX MIME types');
39
+ return t;
40
+ }
27
41
  const trigger = value;
28
42
  if (!trigger || typeof trigger !== 'object' || Object.keys(trigger).some(key => !['type', 'connection', 'resource', 'filter'].includes(key))
29
43
  || trigger.type !== 'email.arrived' || trigger.resource !== 'inbox' || typeof trigger.connection !== 'string' || !LOGICAL_NAME.test(trigger.connection))
@@ -102,7 +116,7 @@ export async function discoverJobs(root) {
102
116
  if (statement.declaration.kind !== 'const')
103
117
  fail('export one literal trigger with export const trigger.');
104
118
  try {
105
- trigger = emailArrivalTrigger(literalObject(declaration.init));
119
+ trigger = eventTrigger(literalObject(declaration.init));
106
120
  }
107
121
  catch (error) {
108
122
  fail(String(error));
@@ -18,9 +18,11 @@ function eventRuntime() {
18
18
  return runtime;
19
19
  }
20
20
  export async function handler(event) {
21
- const { job, Records: records, mailArrival } = (event && typeof event === 'object' ? event : {});
22
- if (mailArrival !== undefined)
23
- return eventRuntime().acceptMailArrival(mailArrival);
21
+ const { job, Records: records, mailArrival, eventArrival } = (event && typeof event === 'object' ? event : {});
22
+ if (eventArrival !== undefined && mailArrival !== undefined)
23
+ throw new Error('ambiguous arrival envelope');
24
+ if (eventArrival !== undefined || mailArrival !== undefined)
25
+ return eventRuntime().acceptEventArrival(eventArrival ?? mailArrival);
24
26
  if (typeof job === 'string' && job) {
25
27
  if (!(await eventRuntime().runJobOnce(job)))
26
28
  throw new Error(`jobs/${job}.ts did not run to completion; the log above names why`);
package/dist/runtime.d.ts CHANGED
@@ -50,7 +50,7 @@ export declare class Runs {
50
50
  enqueue(job: string, input: unknown, owner: string): Promise<{
51
51
  id: string;
52
52
  }>;
53
- enqueueMail(delivery: {
53
+ enqueueEvent(delivery: {
54
54
  deliveryId: string;
55
55
  job: string;
56
56
  input: unknown;
@@ -61,7 +61,7 @@ export declare class Runs {
61
61
  get(id: string, owner: string): Promise<JobRun | null>;
62
62
  cancel(id: string, owner: string): Promise<JobRun | null>;
63
63
  /** Rows whose lease ran out go back to the queue while attempts remain, else fail; how many there were. */
64
- reclaim(now: Date): Promise<number>;
64
+ reclaim(now: Date, active?: ReadonlySet<string>): Promise<number>;
65
65
  /** Claims the oldest queued run by a conditional update; null when the queue is empty or another worker won. */
66
66
  claim(now: Date): Promise<JobRun | null>;
67
67
  progress(id: string, percent: number, note?: string): Promise<void>;
@@ -106,8 +106,10 @@ export declare class Runtime {
106
106
  private readonly timers;
107
107
  private stopped;
108
108
  private aiBinding;
109
- private queueLive;
109
+ private queueWorkers;
110
110
  private queueWake;
111
+ private claimTail;
112
+ private readonly activeRuns;
111
113
  private readonly runs;
112
114
  private session;
113
115
  private readonly log;
@@ -127,8 +129,8 @@ export declare class Runtime {
127
129
  */
128
130
  drainQueue(): Promise<void>;
129
131
  /**
130
- * Starts the queue loop unless it is already live. The first pass runs at
131
- * once; each further pass follows QUEUE_POLL_MS after one that found work
132
+ * Starts one free execution slot, up to the fixed local bound. Its first
133
+ * pass runs at once; each further pass follows QUEUE_POLL_MS after one that found work
132
134
  * (or threw: a gateway that is waking answers 5xx, and the row that woke
133
135
  * the loop is still there). A pass that finds nothing ends the loop with no
134
136
  * timer armed, so an idle runtime makes no data calls at all.
@@ -141,7 +143,7 @@ export declare class Runtime {
141
143
  * would have been. False when the app has no such job, the gateway's data
142
144
  * path never answered, or the handler threw; the log names which.
143
145
  */
144
- acceptMailArrival(value: unknown): Promise<void>;
146
+ acceptEventArrival(value: unknown): Promise<void>;
145
147
  runJobOnce(name: string, now?: Date): Promise<boolean>;
146
148
  private awaitDataPath;
147
149
  stop(): Promise<void>;
@@ -152,10 +154,11 @@ export declare class Runtime {
152
154
  /** One scheduled run; true when the handler returned, false when it threw or the last tick's run is still going. */
153
155
  private runScheduled;
154
156
  /**
155
- * One pass of the queue loop: reclaim expired leases, claim one run, run it.
157
+ * One worker pass: reclaim interrupted leases, claim one run, then execute
158
+ * outside the claim lock so another slot can handle unrelated work.
156
159
  * True when the pass found work — a lease reclaimed, a run claimed, or the
157
160
  * run it finished left a retry queued — so the loop should look again; false
158
- * when the queue is empty (or another pass is already going).
161
+ * when the queue is empty or both execution slots are occupied.
159
162
  */
160
163
  work(): Promise<boolean>;
161
164
  private serve;
package/dist/runtime.js CHANGED
@@ -20,7 +20,8 @@
20
20
  // database calls: the runtime is the process that serves the enqueue action
21
21
  // and the one that finishes every run, so it knows when work exists. It runs
22
22
  // one pass at start (reclaiming whatever a crashed predecessor left running
23
- // and draining any backlog), then loops every QUEUE_POLL_MS only while a pass
23
+ // and draining any backlog), filling at most two local execution slots. Each
24
+ // slot loops every QUEUE_POLL_MS only while a pass
24
25
  // found something — a claimed run, a reclaimed lease, a run that finished with
25
26
  // retries queued — and stops with no timer armed once the queue is empty. An
26
27
  // enqueue starts the loop again. This holds because kit-runtime is pinned to
@@ -67,7 +68,7 @@ import { pathToFileURL } from 'node:url';
67
68
  import { createClient, gatewayRequest, IsomorphError } from './index.js';
68
69
  import { userGroupsFromHeader } from './user-groups.js';
69
70
  import { userRole } from './user-role.js';
70
- import { discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
71
+ import { FILE_MIME_TYPES, providerFileId, discoverJobs, exportsTrue, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
71
72
  import { LocalGateway } from './local-gateway.js';
72
73
  export { JOBS_DIR } from './jobs.js';
73
74
  const actionIdentity = new AsyncLocalStorage();
@@ -83,6 +84,7 @@ export const CANCEL_RUN_ACTION = 'isomorph-jobs-cancel';
83
84
  const LOGICAL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
84
85
  const MAX_BODY_BYTES = 1 << 20;
85
86
  const LEASE_MS = 5 * 60_000;
87
+ const QUEUE_WORKERS = 2;
86
88
  const QUEUE_POLL_MS = 2_000;
87
89
  const TOKEN_REFRESH_MS = 60_000;
88
90
  // A gateway session lasts an hour. An action runs at most 300 s, so a request
@@ -204,7 +206,7 @@ export class Runs {
204
206
  const { data } = await this.table().insert({ job, input: input ?? null, status: 'queued', attempt: 0, max_attempts: DEFAULT_MAX_ATTEMPTS, progress: 0, owner_subject: owner, cancel_requested: false }).select('id').single();
205
207
  return { id: data.id };
206
208
  }
207
- async enqueueMail(delivery) {
209
+ async enqueueEvent(delivery) {
208
210
  const hex = createHash('sha256').update(`mail:${delivery.deliveryId}`).digest('hex');
209
211
  const id = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
210
212
  try {
@@ -230,13 +232,14 @@ export class Runs {
230
232
  return data;
231
233
  }
232
234
  /** Rows whose lease ran out go back to the queue while attempts remain, else fail; how many there were. */
233
- async reclaim(now) {
235
+ async reclaim(now, active = new Set()) {
234
236
  const { data: expired } = await this.table().select('*').eq('status', 'running').lt('lease_until', now.toISOString()).limit(20);
235
- for (const run of expired) {
237
+ const interrupted = expired.filter(run => !active.has(run.id));
238
+ for (const run of interrupted) {
236
239
  const exhausted = run.attempt >= run.max_attempts;
237
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');
238
241
  }
239
- return expired.length;
242
+ return interrupted.length;
240
243
  }
241
244
  /** Claims the oldest queued run by a conditional update; null when the queue is empty or another worker won. */
242
245
  async claim(now) {
@@ -270,10 +273,10 @@ export class Runtime {
270
273
  timers = [];
271
274
  stopped = false;
272
275
  aiBinding;
273
- // The queue loop is live while a pass runs or its next pass is timed; a
274
- // wake that arrives mid-pass is remembered so the pass after it looks again.
275
- queueLive = false;
276
- queueWake = false;
276
+ queueWorkers = 0;
277
+ queueWake = 0;
278
+ claimTail = Promise.resolve();
279
+ activeRuns = new Set();
277
280
  runs;
278
281
  // Built on first use: the AWS signing modules load only on a serverless
279
282
  // cell, so a cluster runtime never imports them.
@@ -339,8 +342,8 @@ export class Runtime {
339
342
  await new Promise((wake) => setTimeout(wake, this.options.queuePollMs ?? QUEUE_POLL_MS));
340
343
  }
341
344
  /**
342
- * Starts the queue loop unless it is already live. The first pass runs at
343
- * once; each further pass follows QUEUE_POLL_MS after one that found work
345
+ * Starts one free execution slot, up to the fixed local bound. Its first
346
+ * pass runs at once; each further pass follows QUEUE_POLL_MS after one that found work
344
347
  * (or threw: a gateway that is waking answers 5xx, and the row that woke
345
348
  * the loop is still there). A pass that finds nothing ends the loop with no
346
349
  * timer armed, so an idle runtime makes no data calls at all.
@@ -348,17 +351,16 @@ export class Runtime {
348
351
  wakeQueue() {
349
352
  if (this.stopped || !this.worksQueue())
350
353
  return;
351
- if (this.queueLive) {
352
- this.queueWake = true;
354
+ this.queueWake += 1;
355
+ if (this.queueWorkers >= QUEUE_WORKERS)
353
356
  return;
354
- }
355
- this.queueLive = true;
357
+ this.queueWorkers += 1;
356
358
  const pass = async () => {
357
359
  if (this.stopped) {
358
- this.queueLive = false;
360
+ this.queueWorkers -= 1;
359
361
  return;
360
362
  }
361
- this.queueWake = false;
363
+ const wake = this.queueWake;
362
364
  let again = true;
363
365
  try {
364
366
  again = await this.work();
@@ -366,14 +368,10 @@ export class Runtime {
366
368
  catch (error) {
367
369
  this.log(`queue pass failed: ${error instanceof Error ? error.message : String(error)}`);
368
370
  }
369
- if (this.stopped) {
370
- this.queueLive = false;
371
- return;
372
- }
373
- if (again || this.queueWake)
371
+ if (!this.stopped && (again || wake !== this.queueWake))
374
372
  this.timers.push(setTimeout(() => void pass(), this.options.queuePollMs ?? QUEUE_POLL_MS));
375
373
  else
376
- this.queueLive = false;
374
+ this.queueWorkers -= 1;
377
375
  };
378
376
  void pass();
379
377
  }
@@ -384,23 +382,36 @@ export class Runtime {
384
382
  * would have been. False when the app has no such job, the gateway's data
385
383
  * path never answered, or the handler threw; the log names which.
386
384
  */
387
- async acceptMailArrival(value) {
385
+ async acceptEventArrival(value) {
388
386
  const delivery = value;
389
387
  if (!delivery || typeof delivery.deliveryId !== 'string' || !/^[a-f0-9]{64}$/.test(delivery.deliveryId)
390
- || typeof delivery.owner !== 'string' || !delivery.owner || delivery.owner.length > 320
391
- || !delivery.input || !['gmail', 'outlook'].includes(delivery.input.provider)
392
- || typeof delivery.input.messageId !== 'string' || !delivery.input.messageId || delivery.input.messageId.length > 2048
393
- || typeof delivery.input.threadId !== 'string' || delivery.input.threadId.length > 2048
394
- || typeof delivery.input.receivedAt !== 'string' || !Number.isFinite(Date.parse(delivery.input.receivedAt)))
395
- throw new Error('invalid mail arrival');
388
+ || typeof delivery.owner !== 'string' || !delivery.owner || delivery.owner.length > 320 || !delivery.input || typeof delivery.input !== 'object')
389
+ throw new Error('invalid event arrival');
396
390
  this.inventory = await discover(this.options.root);
397
391
  const job = this.inventory.jobs.find(job => job.name === delivery.job);
398
- if (!job?.trigger || job.trigger.connection !== delivery.input.sourceId)
399
- throw new Error('mail trigger is not declared by this deployed app');
392
+ const input = delivery.input;
393
+ if (!job?.trigger || job.trigger.connection !== input.sourceId)
394
+ throw new Error('event trigger is not declared by this deployed app');
395
+ if (job.trigger.type === 'file.created') {
396
+ const instant = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(v) && Number.isFinite(Date.parse(v));
397
+ if (Object.keys(input).some(k => !['type', 'provider', 'sourceId', 'fileId', 'folderId', 'driveId', 'name', 'mimeType', 'size', 'createdAt', 'observedAt'].includes(k))
398
+ || input.type !== 'file.created' || !['drive', 'sharepoint'].includes(String(input.provider)) || !providerFileId(input.fileId) || input.folderId !== job.trigger.folderId
399
+ || (input.driveId !== undefined && !providerFileId(input.driveId)) || (input.provider === 'sharepoint' && !input.driveId)
400
+ || (job.trigger.driveId !== undefined && input.driveId !== job.trigger.driveId)
401
+ || typeof input.name !== 'string' || !input.name || input.name.length > 1024 || !Number.isSafeInteger(input.size) || Number(input.size) < 0
402
+ || !(job.trigger.mimeTypes ?? [...FILE_MIME_TYPES]).includes(String(input.mimeType))
403
+ || !instant(input.createdAt) || !instant(input.observedAt))
404
+ throw new Error('invalid file arrival');
405
+ }
406
+ else if (input.type !== undefined || !['gmail', 'outlook'].includes(String(input.provider))
407
+ || typeof input.messageId !== 'string' || !input.messageId || input.messageId.length > 2048
408
+ || typeof input.threadId !== 'string' || input.threadId.length > 2048
409
+ || typeof input.receivedAt !== 'string' || !Number.isFinite(Date.parse(input.receivedAt)))
410
+ throw new Error('invalid mail arrival');
400
411
  await this.refreshToken(JOB_SESSION_MIN_VALIDITY_MS);
401
412
  if (!this.options.gatewayUrl || this.options.mode === 'gate' || this.options.jobRuns === false || !(await this.awaitDataPath()))
402
- throw new Error('mail arrival requires durable job runs');
403
- const queued = await this.runs.enqueueMail(delivery);
413
+ throw new Error('event arrival requires durable job runs');
414
+ const queued = await this.runs.enqueueEvent(delivery);
404
415
  await this.signalQueuedRun(queued.id);
405
416
  }
406
417
  async runJobOnce(name, now = new Date()) {
@@ -539,21 +550,38 @@ export class Runtime {
539
550
  }
540
551
  }
541
552
  /**
542
- * One pass of the queue loop: reclaim expired leases, claim one run, run it.
553
+ * One worker pass: reclaim interrupted leases, claim one run, then execute
554
+ * outside the claim lock so another slot can handle unrelated work.
543
555
  * True when the pass found work — a lease reclaimed, a run claimed, or the
544
556
  * run it finished left a retry queued — so the loop should look again; false
545
- * when the queue is empty (or another pass is already going).
557
+ * when the queue is empty or both execution slots are occupied.
546
558
  */
547
559
  async work() {
548
- if (this.running.has('queue'))
549
- return false;
550
- this.running.add('queue');
560
+ // Serialize only the claim, not the handler. A second slot must not race
561
+ // the same oldest row or reclaim a long handler still running here.
562
+ const previous = this.claimTail;
563
+ let release;
564
+ this.claimTail = new Promise(resolve => { release = resolve; });
565
+ await previous;
566
+ let run;
567
+ let reclaimed = 0;
568
+ try {
569
+ if (this.activeRuns.size >= QUEUE_WORKERS)
570
+ return false;
571
+ const now = new Date();
572
+ reclaimed = await this.runs.reclaim(now, this.activeRuns);
573
+ run = await this.runs.claim(now);
574
+ if (run)
575
+ this.activeRuns.add(run.id);
576
+ }
577
+ finally {
578
+ release();
579
+ }
580
+ if (!run)
581
+ return reclaimed > 0;
582
+ this.wakeQueue(); // Fill the spare slot when startup discovers a backlog.
551
583
  try {
552
584
  const now = new Date();
553
- const reclaimed = await this.runs.reclaim(now);
554
- const run = await this.runs.claim(now);
555
- if (!run)
556
- return reclaimed > 0;
557
585
  if (!this.inventory.jobs.some((job) => job.name === run.job)) {
558
586
  await this.runs.finish({ ...run, attempt: run.max_attempts }, { error: `no jobs/${run.job}.ts in this app` });
559
587
  return true;
@@ -574,7 +602,7 @@ export class Runtime {
574
602
  return true;
575
603
  }
576
604
  finally {
577
- this.running.delete('queue');
605
+ this.activeRuns.delete(run.id);
578
606
  }
579
607
  }
580
608
  async serve(request, response) {
@@ -645,7 +673,7 @@ export class Runtime {
645
673
  if (name === 'isomorph-mail-arrival') {
646
674
  if (header(request, 'X-Harbour-Verified-Mail-Arrival') !== 'true')
647
675
  return refuse(response, 403, 'FORBIDDEN', 'governed mail delivery is required', requestId);
648
- await this.acceptMailArrival(input);
676
+ await this.acceptEventArrival(input);
649
677
  return answer(response, 200, { data: { accepted: true } }, requestId);
650
678
  }
651
679
  if (name.startsWith(RESERVED_PREFIX))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",