@sanoma-ai/workflows 0.11.15 → 0.11.16
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 +10 -3
- package/dist/app-steps.d.ts +17 -0
- package/dist/app-steps.js +43 -0
- package/dist/app-steps.js.map +1 -0
- package/dist/app-workflows.d.ts +170 -0
- package/dist/app-workflows.js +107 -0
- package/dist/app-workflows.js.map +1 -0
- package/dist/apply-steps.js +3 -9
- package/dist/apply-steps.js.map +1 -1
- package/dist/approvals.d.ts +1 -1
- package/dist/approvals.js +3 -0
- package/dist/approvals.js.map +1 -1
- package/dist/call.d.ts +2 -1
- package/dist/call.js +2 -1
- package/dist/call.js.map +1 -1
- package/dist/change-steps.js +11 -17
- package/dist/change-steps.js.map +1 -1
- package/dist/chat-steps.d.ts +5 -0
- package/dist/chat-steps.js +107 -0
- package/dist/chat-steps.js.map +1 -0
- package/dist/chat-workflow.d.ts +43 -0
- package/dist/chat-workflow.js +47 -0
- package/dist/chat-workflow.js.map +1 -0
- package/dist/chats.d.ts +23 -4
- package/dist/chats.js +54 -7
- package/dist/chats.js.map +1 -1
- package/dist/client.d.ts +42 -31
- package/dist/client.js +118 -105
- package/dist/client.js.map +1 -1
- package/dist/config.js +14 -1
- package/dist/config.js.map +1 -1
- package/dist/db/company.d.ts +259 -2
- package/dist/db/company.js +32 -1
- package/dist/db/company.js.map +1 -1
- package/dist/db.d.ts +15 -0
- package/dist/db.js +42 -2
- package/dist/db.js.map +1 -1
- package/dist/follow-steps.d.ts +8 -0
- package/dist/follow-steps.js +113 -0
- package/dist/follow-steps.js.map +1 -0
- package/dist/follow-workflow.d.ts +56 -0
- package/dist/follow-workflow.js +50 -0
- package/dist/follow-workflow.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/run.d.ts +39 -0
- package/dist/run.js +15 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +10 -0
- package/dist/sandbox.js +42 -4
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +19 -0
- package/dist/scenario.js.map +1 -1
- package/dist/seal.d.ts +7 -0
- package/dist/seal.js +14 -2
- package/dist/seal.js.map +1 -1
- package/dist/shared.d.ts +34 -2
- package/dist/shared.js +39 -2
- package/dist/shared.js.map +1 -1
- package/dist/worker.d.ts +8 -0
- package/dist/worker.js +116 -10
- package/dist/worker.js.map +1 -1
- package/drizzle/company/0002_runs_tell_the_app.sql +27 -0
- package/drizzle/company/meta/0002_snapshot.json +819 -0
- package/drizzle/company/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/app-steps.ts +67 -0
- package/src/app-workflows.ts +251 -0
- package/src/apply-steps.ts +3 -9
- package/src/approvals.ts +3 -1
- package/src/call.ts +2 -1
- package/src/change-steps.ts +10 -17
- package/src/chat-steps.ts +117 -0
- package/src/chat-workflow.ts +69 -0
- package/src/chats.ts +80 -14
- package/src/client.ts +154 -109
- package/src/config.ts +14 -0
- package/src/db/company.ts +49 -2
- package/src/db.ts +51 -3
- package/src/follow-steps.ts +129 -0
- package/src/follow-workflow.ts +88 -0
- package/src/index.ts +19 -1
- package/src/run.ts +41 -0
- package/src/sandbox.ts +41 -4
- package/src/scenario.ts +17 -0
- package/src/seal.ts +16 -2
- package/src/shared.ts +47 -2
- package/src/worker.ts +149 -10
package/src/shared.ts
CHANGED
|
@@ -308,13 +308,58 @@ export const APPS_LIST = "sanoma.apps.list";
|
|
|
308
308
|
*/
|
|
309
309
|
export const LOGIN_CODE_WORKFLOW = "login-code";
|
|
310
310
|
|
|
311
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* The app's own actions on the company's repository, each a built-in workflow the app starts and
|
|
313
|
+
* the worker runs with the app's steps (`WorkerOptions.app`, `app-workflows.ts`): Adopt (a drifted
|
|
314
|
+
* field's value at the vendor into its data file), a form's Save, going live (`infra.ts` written,
|
|
315
|
+
* then the push and the host's apply) and connecting an existing repository or database.
|
|
316
|
+
*/
|
|
317
|
+
export const ADOPT_WORKFLOW = "adopt";
|
|
318
|
+
export const PATCH_WORKFLOW = "patch";
|
|
319
|
+
export const GO_LIVE_WORKFLOW = "go-live";
|
|
320
|
+
export const GO_LIVE_HOST_WORKFLOW = "go-live-host";
|
|
321
|
+
export const CONNECT_EXISTING_WORKFLOW = "connect-existing";
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* What a git host's webhook says happened to the company's repository, done by the worker
|
|
325
|
+
* (`follow-workflow.ts`): started by the app as `host-check`, one run per event (`SanomaClient.follow`).
|
|
326
|
+
*/
|
|
327
|
+
export const FOLLOW_WORKFLOW = "follow";
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* A turn of a person's chat with the operator agent, run by the worker with the app's steps
|
|
331
|
+
* (`chat-workflow.ts`): started by the app as the chat's person (`SanomaClient.startTurn`), one run
|
|
332
|
+
* per message, `chat:<chat>:<message>`.
|
|
333
|
+
*/
|
|
334
|
+
export const CHAT_WORKFLOW = "chat";
|
|
335
|
+
|
|
336
|
+
/** What the run id of each of the chat `chat`'s turns starts with: exact, as a chat's id has no `:` (`DRAFT_ID`). */
|
|
337
|
+
export const chatRunPrefix = (chat: string) => `chat:${chat}:`;
|
|
338
|
+
|
|
339
|
+
/** The run id of the chat `chat`'s turn for the message `message`. */
|
|
340
|
+
export const chatRunId = (chat: string, message: string) => `${chatRunPrefix(chat)}${message}`;
|
|
341
|
+
|
|
342
|
+
/** The feature files that did not load (`loadScenarios`' errors), as a scenario not found names them after its name. */
|
|
343
|
+
export const notLoaded = (errors: readonly { message: string }[]) =>
|
|
344
|
+
errors.length ? `; these files did not load: ${errors.map((e) => e.message).join("; ")}` : "";
|
|
345
|
+
|
|
346
|
+
/** The `recv` topic a turn's run is told on (`ChatTell`): a stop, a sign-in code, its own turn's end. */
|
|
347
|
+
export const CHAT_TOPIC = "chat:tell";
|
|
348
|
+
|
|
349
|
+
/** The workflows the runtime adds itself, which a config's own may not be named: drift, apply, call, change, login-code, follow, chat and the app's actions. */
|
|
312
350
|
export const BUILT_IN_WORKFLOWS: ReadonlySet<string> = new Set([
|
|
313
351
|
DRIFT_WORKFLOW,
|
|
314
352
|
APPLY_WORKFLOW,
|
|
315
353
|
CALL_WORKFLOW,
|
|
316
354
|
CHANGE_WORKFLOW,
|
|
317
355
|
LOGIN_CODE_WORKFLOW,
|
|
356
|
+
FOLLOW_WORKFLOW,
|
|
357
|
+
CHAT_WORKFLOW,
|
|
358
|
+
ADOPT_WORKFLOW,
|
|
359
|
+
PATCH_WORKFLOW,
|
|
360
|
+
GO_LIVE_WORKFLOW,
|
|
361
|
+
GO_LIVE_HOST_WORKFLOW,
|
|
362
|
+
CONNECT_EXISTING_WORKFLOW,
|
|
318
363
|
]);
|
|
319
364
|
|
|
320
365
|
/**
|
|
@@ -422,7 +467,7 @@ export const ANONYMOUS_PRINCIPAL: Principal = Object.freeze({ id: ANONYMOUS, gro
|
|
|
422
467
|
export const WAKEUP_AGENT = "wakeup";
|
|
423
468
|
/** The runtime's agent a scheduled run is started by (`trigger: { schedule }`): the policy decides its starts. */
|
|
424
469
|
export const SCHEDULE_AGENT = "schedule";
|
|
425
|
-
/** The runtime's agent that checks and deploys the hosts: drift's, and
|
|
470
|
+
/** The runtime's agent that checks and deploys the hosts: drift's, and a host's webhook's (`SanomaClient.follow`), whose apply after a push to `main` is its too. */
|
|
426
471
|
export const HOST_CHECK_AGENT = "host-check";
|
|
427
472
|
/** The runtime's agent that sends a sign-in code (`LOGIN_CODE_WORKFLOW`): a company's policy exempts it from its holds on sending. */
|
|
428
473
|
export const LOGIN_AGENT = "login";
|
package/src/worker.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { DBOS, type ScheduledWorkflowFn } from "@dbos-inc/dbos-sdk";
|
|
1
|
+
import { DBOS, Error as DBOSErrors, type ScheduledWorkflowFn } from "@dbos-inc/dbos-sdk";
|
|
2
|
+
import type { WorkerApp } from "./app-steps.ts";
|
|
3
|
+
import { readApps } from "./apps.ts";
|
|
4
|
+
import type { AppSteps } from "./app-workflows.ts";
|
|
2
5
|
import { awaitApproval } from "./approvals.ts";
|
|
3
6
|
import { buildCtx, decideStart, isInfrastructureError, startHoldOf } from "./call.ts";
|
|
4
|
-
import {
|
|
7
|
+
import { followBase } from "./change.ts";
|
|
8
|
+
import { dbosStatusesOf, SanomaClient } from "./client.ts";
|
|
5
9
|
import { declaredWakeup, envWakeup, type ResolvedConfig, resolveConfig, type SanomaConfig } from "./config.ts";
|
|
6
10
|
import { configuredSecrets } from "./credentials.ts";
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
type AppBehind,
|
|
13
|
+
type Db,
|
|
14
|
+
DBOS_DATABASE_OPTIONS,
|
|
15
|
+
lazyDb,
|
|
16
|
+
prepareDatabase,
|
|
17
|
+
stampVersion,
|
|
18
|
+
writeAppsBehind,
|
|
19
|
+
} from "./db.ts";
|
|
8
20
|
import {
|
|
9
21
|
deadlineRegistry,
|
|
10
22
|
type Deadlines,
|
|
@@ -22,7 +34,14 @@ import { entry, ownedLedger, skipped, write, writeFailure } from "./ledger.ts";
|
|
|
22
34
|
import { dbosLogger, info, quietIfSandbox, setLogLevel, warn } from "./log.ts";
|
|
23
35
|
import { lineStartsOf, readSource } from "./ast.ts";
|
|
24
36
|
import { callsOf, outlineWithSource } from "./outline.ts";
|
|
25
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
currentRun,
|
|
39
|
+
recordSandboxChecks,
|
|
40
|
+
type Run,
|
|
41
|
+
type RunArgs,
|
|
42
|
+
type WorkerState,
|
|
43
|
+
type WorkflowOutline,
|
|
44
|
+
} from "./run.ts";
|
|
26
45
|
import type { DatabasePreparation } from "./op.ts";
|
|
27
46
|
import { applyScheduled, nextScheduledFire, type ScheduledStarter, scheduledStarter } from "./schedules.ts";
|
|
28
47
|
import type { Source } from "./source.ts";
|
|
@@ -84,6 +103,13 @@ export interface WorkerOptions {
|
|
|
84
103
|
promote?: boolean;
|
|
85
104
|
/** The company's repository, which apply commits the lockfile to; the launcher passes its checkout. */
|
|
86
105
|
source?: Source;
|
|
106
|
+
/**
|
|
107
|
+
* The app's own actions (`@sanoma-ai/app/steps`), which its built-in workflows run (Adopt, a
|
|
108
|
+
* form's Save, going live, connecting an existing repository, a chat's turn): the launcher passes
|
|
109
|
+
* them. Without them, a run of one fails, saying this worker runs none of them (`NO_APP_STEPS`,
|
|
110
|
+
* or for a chat `NO_CHAT_STEPS`).
|
|
111
|
+
*/
|
|
112
|
+
app?: AppSteps;
|
|
87
113
|
/**
|
|
88
114
|
* How soon a deadline keeps the worker up (`idle`): 60 s by default, so apply's short sleeps
|
|
89
115
|
* and other short waits never stop it.
|
|
@@ -141,6 +167,29 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
141
167
|
let stopRule: { close(): void } | undefined;
|
|
142
168
|
let stopping: Promise<void> | undefined;
|
|
143
169
|
const horizonMs = options.idleHorizonMs ?? 60_000;
|
|
170
|
+
// The worker's own client, connected by the first step that needs it, for the worker's version: what it starts runs on.
|
|
171
|
+
let client: Promise<SanomaClient> | undefined;
|
|
172
|
+
const appSteps: WorkerApp | undefined = options.app && { steps: options.app, config, resolved };
|
|
173
|
+
const shutdown = new AbortController();
|
|
174
|
+
// One check of the apps' sources at a time; one asked for meanwhile runs once it ends, so the
|
|
175
|
+
// table says what the latest `main` is behind.
|
|
176
|
+
let behind: Promise<void> | undefined;
|
|
177
|
+
let again = false;
|
|
178
|
+
const refreshAppsBehind = () => {
|
|
179
|
+
if (behind) {
|
|
180
|
+
again = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
behind = (async () => {
|
|
184
|
+
do {
|
|
185
|
+
again = false;
|
|
186
|
+
await recordAppsBehind(resolved.root, options.source, state.db, shutdown.signal);
|
|
187
|
+
} while (again && !shutdown.signal.aborted);
|
|
188
|
+
// In the same turn as the last look at `again`, so no ask falls between (it never throws).
|
|
189
|
+
behind = undefined;
|
|
190
|
+
})();
|
|
191
|
+
};
|
|
192
|
+
const serially = serialWrites();
|
|
144
193
|
const state: WorkerState = {
|
|
145
194
|
app: resolved.appName,
|
|
146
195
|
connections: resolved.connections,
|
|
@@ -149,6 +198,22 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
149
198
|
fakes: resolved.fakes,
|
|
150
199
|
...(resolved.scenarios && { scenarios: resolved.scenarios }),
|
|
151
200
|
...(options.source && { source: options.source }),
|
|
201
|
+
...(appSteps && { appSteps }),
|
|
202
|
+
client: () =>
|
|
203
|
+
(client ??= SanomaClient.connect(config, { appVersion: resolved.version }).catch((err: unknown) => {
|
|
204
|
+
// Tried again by the next step: a client that failed to connect is none.
|
|
205
|
+
client = undefined;
|
|
206
|
+
throw err;
|
|
207
|
+
})),
|
|
208
|
+
serially,
|
|
209
|
+
followMain: () =>
|
|
210
|
+
serially(async () => {
|
|
211
|
+
const moved = await followBase(resolved.remotes, storedSecrets, options.source);
|
|
212
|
+
if ("revision" in moved) refreshAppsBehind();
|
|
213
|
+
return moved;
|
|
214
|
+
}),
|
|
215
|
+
// The runtime's tables, on the secrets' pool: the same database.
|
|
216
|
+
db: () => secrets.db(),
|
|
152
217
|
types: resolved.types,
|
|
153
218
|
declaredObjects: resolved.declaredObjects,
|
|
154
219
|
collections: resolved.collections,
|
|
@@ -167,6 +232,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
167
232
|
},
|
|
168
233
|
stopped: false,
|
|
169
234
|
deadlines: deadlineRegistry(),
|
|
235
|
+
shutdown: shutdown.signal,
|
|
170
236
|
};
|
|
171
237
|
const worker: Worker = {
|
|
172
238
|
unconfigured: [...credentialsOf(resolved.driversByVendor, (name) => process.env[name])]
|
|
@@ -185,6 +251,11 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
185
251
|
let failed: { error: unknown } | undefined;
|
|
186
252
|
for (const close of [
|
|
187
253
|
() => DBOS.shutdown(),
|
|
254
|
+
// Once DBOS is down, so a turn's step cut short is recorded nowhere: the next worker settles it.
|
|
255
|
+
async () => shutdown.abort(),
|
|
256
|
+
async () => (await client?.catch(() => undefined))?.close(),
|
|
257
|
+
// Before the pool it writes to closes: the app it checks now, then none.
|
|
258
|
+
async () => behind,
|
|
188
259
|
() => storedSecrets.close(),
|
|
189
260
|
() => resolved.connections.close(),
|
|
190
261
|
() => resolved.identityProvisioning.close(),
|
|
@@ -204,7 +275,7 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
204
275
|
};
|
|
205
276
|
// Runs recovered during launch start before it returns, so they must already find this
|
|
206
277
|
// state. If starting fails, a worker still running in the process gets its own back, once
|
|
207
|
-
// DBOS has stopped: until then a run DBOS dispatches finds this state stopped, and is
|
|
278
|
+
// DBOS has stopped: until then a run DBOS dispatches finds this state stopped, and is handed back.
|
|
208
279
|
const previous = current;
|
|
209
280
|
let launched = false;
|
|
210
281
|
try {
|
|
@@ -245,7 +316,11 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
245
316
|
}
|
|
246
317
|
// Built-in runs move to the latest version only: a rollback's worker, unless promoted, leaves them be.
|
|
247
318
|
if (isLatest) await carryBuiltInRuns(resolved, () => secrets.db());
|
|
248
|
-
|
|
319
|
+
// How often it looks for queued runs: DBOS's default second, unless a test suite asks for less.
|
|
320
|
+
const pollMs = process.env.SANOMA_QUEUE_POLL_MS;
|
|
321
|
+
if (pollMs && !(Number(pollMs) > 0))
|
|
322
|
+
throw new Error(`SANOMA_QUEUE_POLL_MS is milliseconds above zero, not "${pollMs}"`);
|
|
323
|
+
await DBOS.registerQueue(resolved.queueName, pollMs ? { minPollingIntervalMs: Number(pollMs) } : undefined);
|
|
249
324
|
// The schedules are the latest version's: an older worker leaves them as the newer set them.
|
|
250
325
|
if (isLatest && options.schedules !== false) {
|
|
251
326
|
await applyScheduled(resolved, registered, () => secrets.db());
|
|
@@ -253,6 +328,8 @@ export async function startWorker(config: SanomaConfig, options: WorkerOptions =
|
|
|
253
328
|
state.deadlines.addSource(() => nextScheduledFire(resolved, new Date()));
|
|
254
329
|
}
|
|
255
330
|
await warnAboutStrandedRuns(resolved);
|
|
331
|
+
// How far each app taken from a source is behind it, fetched in the background: the app reads the table.
|
|
332
|
+
refreshAppsBehind();
|
|
256
333
|
const summary = unconfiguredSummary(worker.unconfigured, resolved.driversByVendor.size);
|
|
257
334
|
if (summary && !options.quiet) warn(summary);
|
|
258
335
|
if (resolved.policyProblems.length && !options.quiet) {
|
|
@@ -322,6 +399,54 @@ function outlineOf(wf: WorkflowDefinition<any, any>): WorkflowOutline {
|
|
|
322
399
|
return { file: outline.file, lineStarts: lineStartsOf(source!), calls: callsOf(outline.nodes) };
|
|
323
400
|
}
|
|
324
401
|
|
|
402
|
+
/**
|
|
403
|
+
* `WorkerState.serially`, over a chain of its own: each write waits for the one before it to
|
|
404
|
+
* settle, failed or not.
|
|
405
|
+
*/
|
|
406
|
+
function serialWrites(): WorkerState["serially"] {
|
|
407
|
+
let tail: Promise<unknown> = Promise.resolve();
|
|
408
|
+
return (next) => {
|
|
409
|
+
const done = tail.then(next);
|
|
410
|
+
tail = done.catch(() => undefined);
|
|
411
|
+
return done;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Tells how far each app under `root` taken from a source (its `app.ts` names one) is behind it,
|
|
417
|
+
* fetched now (`Source.behind`), or why that cannot be told: offline, no shared history, a
|
|
418
|
+
* repository that cannot fetch. Written to the runtime's table for the app (`writeAppsBehind`),
|
|
419
|
+
* which then lists no other app. Never throws: a failure is warned about. One at a time, since each
|
|
420
|
+
* fetches into the one checkout; nothing more once `stopped` is aborted, nor written.
|
|
421
|
+
*/
|
|
422
|
+
async function recordAppsBehind(
|
|
423
|
+
root: string | undefined,
|
|
424
|
+
source: Source | undefined,
|
|
425
|
+
db: () => Promise<Db>,
|
|
426
|
+
stopped: AbortSignal,
|
|
427
|
+
) {
|
|
428
|
+
try {
|
|
429
|
+
const told = new Map<string, AppBehind>();
|
|
430
|
+
for (const app of readApps(root).filter((a) => a.source !== undefined)) {
|
|
431
|
+
if (stopped.aborted) return;
|
|
432
|
+
if (!source?.behind) {
|
|
433
|
+
told.set(app.name, { error: "This instance's repository cannot fetch an app's source" });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
told.set(
|
|
437
|
+
app.name,
|
|
438
|
+
await source.behind(app.name).then(
|
|
439
|
+
(behind): AppBehind => ({ behind }),
|
|
440
|
+
(err: unknown) => ({ error: errorMessage(err) }),
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (!stopped.aborted) await writeAppsBehind(await db(), told);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
warn(`could not tell how far the apps are behind their sources: ${errorMessage(err)}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
325
450
|
/**
|
|
326
451
|
* Takes over the unfinished runs of the built-in workflows (apply, change, …) started on other
|
|
327
452
|
* versions whose code is this worker's (`builtInCodes`), keeping each run's id, history and unread
|
|
@@ -413,9 +538,18 @@ function register(name: string): ScheduledWorkflowFn {
|
|
|
413
538
|
*/
|
|
414
539
|
async function runWorkflow(name: string, start: RunArgs | Date): Promise<unknown> {
|
|
415
540
|
const state = current;
|
|
416
|
-
|
|
417
|
-
if (!state
|
|
418
|
-
|
|
541
|
+
const runId = DBOS.workflowID!;
|
|
542
|
+
if (!state) throw new Error(`Run of "${name}" started with no worker running`);
|
|
543
|
+
// A stop's failure is not the run's outcome, but DBOS would record it as one, for good, while
|
|
544
|
+
// its pool is still open. A run's own cancellation it records nothing for: the next worker recovers it.
|
|
545
|
+
// So does a run DBOS dispatches during the stop, as a stopped worker's state stays current until DBOS is down.
|
|
546
|
+
if (state.stopped) throw new DBOSErrors.DBOSWorkflowCancelledError(runId);
|
|
547
|
+
try {
|
|
548
|
+
return await state.deadlines.running(runId, () => runOn(state, name, start));
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (state.stopped && isInfrastructureError(err, state)) throw new DBOSErrors.DBOSWorkflowCancelledError(runId);
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
419
553
|
}
|
|
420
554
|
|
|
421
555
|
async function runOn(state: WorkerState, name: string, start: RunArgs | Date): Promise<unknown> {
|
|
@@ -494,9 +628,14 @@ async function runOn(state: WorkerState, name: string, start: RunArgs | Date): P
|
|
|
494
628
|
} catch (err) {
|
|
495
629
|
const record = entry(run, { type: "run.failed", error: errorInfo(err) });
|
|
496
630
|
if (isInfrastructureError(err, state)) skipped(record, `the run was interrupted (${errorMessage(err)})`);
|
|
497
|
-
else
|
|
631
|
+
else {
|
|
632
|
+
await writeFailure(run, record, err);
|
|
633
|
+
await recordSandboxChecks(run);
|
|
634
|
+
}
|
|
498
635
|
throw err;
|
|
499
636
|
}
|
|
500
637
|
await write(run, entry(run, { type: "run.finished", output }));
|
|
638
|
+
// Once its outcome is recorded, before DBOS records it, so a page that reads the run ended reads its last checks.
|
|
639
|
+
await recordSandboxChecks(run);
|
|
501
640
|
return output;
|
|
502
641
|
}
|