@inflexa-ai/harness 0.37.0 → 0.39.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 +1 -2
- package/dist/auth/local-run-authorizer.js +5 -4
- package/dist/auth/types.d.ts +28 -9
- package/dist/auth/types.js +5 -3
- package/dist/billing/cached-resolver.d.ts +11 -7
- package/dist/billing/cached-resolver.js +17 -11
- package/dist/billing/noop-run-charge.js +3 -2
- package/dist/billing/noop-usage-recorder.js +2 -1
- package/dist/billing/run-charge.d.ts +18 -5
- package/dist/billing/run-charge.js +5 -1
- package/dist/billing/usage-recorder.d.ts +11 -9
- package/dist/billing/usage-recorder.js +2 -3
- package/dist/contracts/chat-events.d.ts +4 -1
- package/dist/contracts/chat-parts.d.ts +1 -1
- package/dist/contracts/schemas/chat-parts.d.ts +4 -4
- package/dist/execution/artifact-registration.d.ts +6 -1
- package/dist/execution/artifact-registration.js +12 -4
- package/dist/execution/artifact-registry.d.ts +10 -3
- package/dist/execution/noop-artifact-registry.js +3 -4
- package/dist/execution/post-step-pipeline.d.ts +14 -3
- package/dist/execution/post-step-pipeline.js +21 -8
- package/dist/execution/run-authorizer.d.ts +5 -3
- package/dist/execution/run-canceler.js +4 -20
- package/dist/index.d.ts +14 -8
- package/dist/index.js +12 -6
- package/dist/lib/hooks.d.ts +49 -0
- package/dist/lib/hooks.js +39 -0
- package/dist/lib/result.d.ts +22 -11
- package/dist/lib/result.js +24 -14
- package/dist/loop/run-agent.d.ts +2 -2
- package/dist/loop/run-agent.js +5 -4
- package/dist/provenance/seam.d.ts +1 -1
- package/dist/providers/ai-sdk.d.ts +43 -5
- package/dist/providers/ai-sdk.js +55 -52
- package/dist/providers/anthropic.d.ts +2 -3
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/embedding.d.ts +13 -7
- package/dist/providers/embedding.js +26 -6
- package/dist/providers/errors.d.ts +67 -37
- package/dist/providers/errors.js +57 -58
- package/dist/providers/request-headers.d.ts +18 -0
- package/dist/providers/request-headers.js +22 -0
- package/dist/report-model/production-resolver.d.ts +5 -2
- package/dist/report-model/production-resolver.js +12 -2
- package/dist/runtime/assemble.d.ts +3 -2
- package/dist/runtime/dbos.d.ts +4 -3
- package/dist/runtime/dbos.js +18 -3
- package/dist/runtime/result-serialization.d.ts +29 -0
- package/dist/runtime/result-serialization.js +45 -0
- package/dist/sandbox/client.d.ts +11 -3
- package/dist/sandbox/create-sandbox.d.ts +5 -2
- package/dist/sandbox/create-sandbox.js +51 -25
- package/dist/sandbox/docker-client.d.ts +4 -3
- package/dist/sandbox/docker-client.js +23 -31
- package/dist/sandbox/k8s-client.d.ts +4 -13
- package/dist/sandbox/k8s-client.js +28 -61
- package/dist/sandbox/labels.d.ts +34 -0
- package/dist/sandbox/labels.js +47 -0
- package/dist/sandbox/mount-plan.d.ts +3 -1
- package/dist/sandbox/mount-plan.js +9 -0
- package/dist/sandbox/sandbox-error.d.ts +36 -13
- package/dist/sandbox/sandbox-error.js +32 -17
- package/dist/sandbox/types.d.ts +18 -10
- package/dist/schemas/validate-plan.js +13 -0
- package/dist/schemas/workflow-state.d.ts +4 -4
- package/dist/state/analyses.d.ts +4 -4
- package/dist/state/analyses.js +4 -4
- package/dist/state/schema.d.ts +5 -5
- package/dist/tasks/data-profile.d.ts +5 -8
- package/dist/tasks/data-profile.js +88 -43
- package/dist/tasks/derive-table-exec.d.ts +10 -6
- package/dist/tasks/derive-table-exec.js +17 -12
- package/dist/tasks/extract-values.d.ts +16 -10
- package/dist/tasks/extract-values.js +34 -18
- package/dist/tools/execute-analysis.js +29 -24
- package/dist/tools/report-session/derive-table.d.ts +9 -1
- package/dist/tools/report-session/derive-table.js +25 -22
- package/dist/tools/report-session/examine-page.d.ts +6 -3
- package/dist/tools/report-session/examine-page.js +16 -8
- package/dist/tools/report-session/index.d.ts +1 -1
- package/dist/tools/report-session/preview-report.js +11 -9
- package/dist/tools/report-session/session-page-publisher.d.ts +17 -21
- package/dist/tools/report-session/session-page-publisher.js +5 -7
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/sandbox/link-packages.d.ts +8 -0
- package/dist/tools/sandbox/link-packages.js +19 -9
- package/dist/workflows/execute-analysis.d.ts +25 -16
- package/dist/workflows/execute-analysis.js +223 -216
- package/dist/workflows/metrics.d.ts +4 -3
- package/dist/workflows/metrics.js +4 -3
- package/dist/workflows/sandbox-step.d.ts +20 -29
- package/dist/workflows/sandbox-step.js +71 -127
- package/dist/workflows/suspension.d.ts +32 -0
- package/dist/workflows/suspension.js +38 -0
- package/package.json +1 -1
- package/dist/billing/noop-resolver.d.ts +0 -6
- package/dist/billing/noop-resolver.js +0 -7
- package/dist/billing/resolver.d.ts +0 -64
- package/dist/billing/resolver.js +0 -24
- package/dist/loop/budget-exceeded.d.ts +0 -38
- package/dist/loop/budget-exceeded.js +0 -88
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
* propagate untouched. `await-exec.ts` is NOT converted; `trySandbox` only
|
|
26
26
|
* ever wraps the single SDK/HTTP call handed to it.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* untouched by this wave.
|
|
28
|
+
* `createSandbox` of the public `SandboxClient` gives each `SandboxError` as
|
|
29
|
+
* an `err` value, because a refusal of the label hook must reach the spawn path
|
|
30
|
+
* as a value. A spawn path splits the result with `keepSuspendingRefusal`: a
|
|
31
|
+
* refusal with the suspend flag stays a value, and each other failure throws
|
|
32
|
+
* as a `SandboxFailure`.
|
|
33
|
+
* Each other op keeps a `Promise<T>` signature. `create-sandbox.ts` is the
|
|
34
|
+
* composition seam: it builds the backend ops, maps each `err` of such an op to
|
|
35
|
+
* a `SandboxFailure`, and `unwrapOrThrow`s it, so the seam throws the described
|
|
36
|
+
* failure with the variant on `.cause` and on `.error`. A `catch` never sees
|
|
37
|
+
* the raw variant. (`submitExec` is the exception — it is already its own DBOS
|
|
38
|
+
* step and throws directly, so the seam forwards its `Promise<void>` as-is.)
|
|
40
39
|
*/
|
|
41
40
|
import { ResultAsync, err, ok } from "neverthrow";
|
|
42
41
|
import { ZodError } from "zod";
|
|
42
|
+
import { unwrapOrThrow } from "../lib/result.js";
|
|
43
43
|
const _assertDomainError = true;
|
|
44
44
|
/**
|
|
45
45
|
* The HTTP/driver status a backend SDK error carries. dockerode exposes it on
|
|
@@ -100,11 +100,11 @@ function causeLine(cause) {
|
|
|
100
100
|
* on the operator surfaces.
|
|
101
101
|
*/
|
|
102
102
|
export function describeSandboxError(e) {
|
|
103
|
-
// `farm_unavailable`
|
|
104
|
-
// reason, the text of the resolver
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
const line = e.type === "farm_unavailable" ? undefined : causeLine(e.cause);
|
|
103
|
+
// `farm_unavailable` and `labels_refused` are the two variants whose head
|
|
104
|
+
// already carries its reason, the text of the resolver or of the host. A
|
|
105
|
+
// cause line after it would put two dashes on one row. Every other variant
|
|
106
|
+
// names its reason only through the cause.
|
|
107
|
+
const line = e.type === "farm_unavailable" || e.type === "labels_refused" ? undefined : causeLine(e.cause);
|
|
108
108
|
const head = describeVariant(e);
|
|
109
109
|
return line === undefined ? head : `${head} — ${line}`;
|
|
110
110
|
}
|
|
@@ -123,6 +123,8 @@ function describeVariant(e) {
|
|
|
123
123
|
return `sandbox teardown failed (${e.op}${e.sandboxId ? `: ${e.sandboxId}` : ""})`;
|
|
124
124
|
case "liveness_failed":
|
|
125
125
|
return `sandbox liveness probe failed (${e.op}${e.sandboxId ? `: ${e.sandboxId}` : ""})`;
|
|
126
|
+
case "labels_refused":
|
|
127
|
+
return `the label hook refused the spawn (${e.op}) — ${e.reason}`;
|
|
126
128
|
case "farm_unavailable":
|
|
127
129
|
return `farm unavailable (${e.op}: ${e.analysisId}) — ${e.reason}`;
|
|
128
130
|
case "farm_unusable":
|
|
@@ -144,6 +146,19 @@ export class SandboxFailure extends Error {
|
|
|
144
146
|
this.name = "SandboxFailure";
|
|
145
147
|
}
|
|
146
148
|
}
|
|
149
|
+
function isSuspendingRefusal(e) {
|
|
150
|
+
return e.type === "labels_refused" && e.suspend;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Split the result of a spawn at a DBOS boundary. Call it inside the step of
|
|
154
|
+
* the spawn, before the checkpoint: the cause of a backend failure does not
|
|
155
|
+
* survive the serialization of a `Result`.
|
|
156
|
+
*/
|
|
157
|
+
export function keepSuspendingRefusal(result) {
|
|
158
|
+
if (result.isErr() && isSuspendingRefusal(result.error))
|
|
159
|
+
return err(result.error);
|
|
160
|
+
return ok(unwrapOrThrow(result.mapErr((e) => new SandboxFailure(e))));
|
|
161
|
+
}
|
|
147
162
|
/**
|
|
148
163
|
* Wrap a single sandbox backend call (Docker SDK op, K8s API op, or the
|
|
149
164
|
* `/exec` POST). A throw becomes `err(toError(status, cause))`; the caller
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* — that one OMITS the secret, which lives only in the cached
|
|
9
9
|
* `createSandbox` DBOS step output.
|
|
10
10
|
*/
|
|
11
|
+
import type { ResultAsync } from "neverthrow";
|
|
11
12
|
import { z } from "zod";
|
|
13
|
+
import type { RunSession } from "../auth/types.js";
|
|
12
14
|
import type { ResourceSpec } from "../config/resource-limits.js";
|
|
15
|
+
import type { GateFailure } from "../lib/hooks.js";
|
|
13
16
|
import type { PackageQuery } from "./package-identity.js";
|
|
14
17
|
export declare const SandboxBackend: z.ZodEnum<{
|
|
15
18
|
docker: "docker";
|
|
@@ -141,8 +144,12 @@ export type PackageRequestOutcome = {
|
|
|
141
144
|
*
|
|
142
145
|
* The seam speaks the query of the `package-identity` capability, thus the
|
|
143
146
|
* host resolves the identity and the harness never folds a name.
|
|
147
|
+
*
|
|
148
|
+
* A failure of one package is an outcome. An `err` means that the whole
|
|
149
|
+
* call gave no answer, and the harness reads it as `unavailable` for each
|
|
150
|
+
* query.
|
|
144
151
|
*/
|
|
145
|
-
export type ExtendAnalysisFarm = (analysisId: string, queries: readonly PackageQuery[]) =>
|
|
152
|
+
export type ExtendAnalysisFarm = (analysisId: string, queries: readonly PackageQuery[]) => ResultAsync<readonly PackageRequestOutcome[], GateFailure>;
|
|
146
153
|
/**
|
|
147
154
|
* Per-sandbox-machine liveness verdict. `oomKilled` is meaningful only when
|
|
148
155
|
* `alive` is false: true when the backend reports the machine was killed for
|
|
@@ -380,11 +387,8 @@ export interface SubmitExecBody {
|
|
|
380
387
|
stdoutByteCap?: number;
|
|
381
388
|
stderrByteCap?: number;
|
|
382
389
|
}
|
|
383
|
-
/**
|
|
384
|
-
export interface
|
|
385
|
-
runId: string;
|
|
386
|
-
stepId: string;
|
|
387
|
-
analysisId: string;
|
|
390
|
+
/** What a spawn needs beside its session. */
|
|
391
|
+
export interface SandboxSpec {
|
|
388
392
|
/** The first `execId` that will fire against this sandbox; nullable for
|
|
389
393
|
* early-create flows where the workflow mints the first execId later. */
|
|
390
394
|
execId?: string | null;
|
|
@@ -408,11 +412,15 @@ export interface CreateSandboxMeta {
|
|
|
408
412
|
* mount, thus a run provisions exactly as before. A tail beside `readOnly` is
|
|
409
413
|
* a contradiction, and the mount builders refuse it. */
|
|
410
414
|
writableTail?: string;
|
|
411
|
-
/** Host-supplied labels stamped onto the sandbox pod, verbatim. Opaque here:
|
|
412
|
-
* the harness reads no key and no value, it only sanitizes each value into a
|
|
413
|
-
* valid label. Absent ⇒ the pod carries the harness's own labels only. */
|
|
414
|
-
podLabels?: Record<string, string>;
|
|
415
415
|
}
|
|
416
|
+
/** The labels that the host adds to a sandbox. The harness stamps each one as the host gives it. */
|
|
417
|
+
export type SandboxLabels = Readonly<Record<string, string>>;
|
|
418
|
+
/**
|
|
419
|
+
* The sandbox label hook of the host: a gate (`lib/hooks.ts`). The sandbox
|
|
420
|
+
* client calls it at each spawn, on both backends, before it makes the step
|
|
421
|
+
* tree and before it calls a backend.
|
|
422
|
+
*/
|
|
423
|
+
export type ResolveSandboxLabels = (session: RunSession) => ResultAsync<SandboxLabels, GateFailure>;
|
|
416
424
|
/**
|
|
417
425
|
* The identity minted for a sandbox machine *before* it is spawned — the
|
|
418
426
|
* durable half of the two-step create (see the harness-sandbox-exec spec). Step 1 checkpoints this so a
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { KNOWN_AGENT_IDS } from "../agents/sandbox-catalog.js";
|
|
15
15
|
import { CycleError, DependencyError, topoSortIntoWaves } from "../execution/topo-sort.js";
|
|
16
16
|
import { parseQuery } from "../sandbox/package-identity.js";
|
|
17
|
+
import { isLabelValue } from "../sandbox/labels.js";
|
|
17
18
|
import { isSafeId, STEP_SUBDIRS, SYNTHESIS_STEP_ID } from "../workspace/paths.js";
|
|
18
19
|
const KNOWN_AGENTS = new Set(KNOWN_AGENT_IDS);
|
|
19
20
|
/**
|
|
@@ -157,5 +158,17 @@ export function validatePlan(plan, options) {
|
|
|
157
158
|
`and cannot be '.' or '..' (the id becomes a workspace directory and container mount segment)`);
|
|
158
159
|
}
|
|
159
160
|
}
|
|
161
|
+
// 8. Step-id label safety. The id also becomes the value of the sandbox
|
|
162
|
+
// label `cortex/step-id`, and the harness stamps a label value with no
|
|
163
|
+
// change (sandbox-labels spec). A safe id can be too long for a label
|
|
164
|
+
// value, or it can start or end with a separator. Refused here, the
|
|
165
|
+
// planner makes the plan again; left alone, the K8s API server refuses
|
|
166
|
+
// the spawn of the step.
|
|
167
|
+
for (const step of plan.steps) {
|
|
168
|
+
if (isSafeId(step.id) && !isLabelValue(step.id)) {
|
|
169
|
+
errors.push(`Step "${step.id}" is not a valid label value — a step id has 1 to 63 characters, and it starts and ends ` +
|
|
170
|
+
`with a letter or a digit (the id becomes the value of the sandbox label cortex/step-id)`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
160
173
|
return { valid: errors.length === 0, errors };
|
|
161
174
|
}
|
|
@@ -24,10 +24,10 @@ export declare const AnalysisStepSchema: z.ZodObject<{
|
|
|
24
24
|
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
25
|
depends_on: z.ZodArray<z.ZodString>;
|
|
26
26
|
status: z.ZodDefault<z.ZodEnum<{
|
|
27
|
+
failed: "failed";
|
|
27
28
|
pending: "pending";
|
|
28
29
|
running: "running";
|
|
29
30
|
completed: "completed";
|
|
30
|
-
failed: "failed";
|
|
31
31
|
skipped: "skipped";
|
|
32
32
|
}>>;
|
|
33
33
|
resources: z.ZodOptional<z.ZodObject<{
|
|
@@ -61,10 +61,10 @@ export declare const AnalysisPlanSchema: z.ZodObject<{
|
|
|
61
61
|
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
62
|
depends_on: z.ZodArray<z.ZodString>;
|
|
63
63
|
status: z.ZodDefault<z.ZodEnum<{
|
|
64
|
+
failed: "failed";
|
|
64
65
|
pending: "pending";
|
|
65
66
|
running: "running";
|
|
66
67
|
completed: "completed";
|
|
67
|
-
failed: "failed";
|
|
68
68
|
skipped: "skipped";
|
|
69
69
|
}>>;
|
|
70
70
|
resources: z.ZodOptional<z.ZodObject<{
|
|
@@ -133,10 +133,10 @@ export declare const ContentBlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
133
133
|
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
134
134
|
depends_on: z.ZodArray<z.ZodString>;
|
|
135
135
|
status: z.ZodDefault<z.ZodEnum<{
|
|
136
|
+
failed: "failed";
|
|
136
137
|
pending: "pending";
|
|
137
138
|
running: "running";
|
|
138
139
|
completed: "completed";
|
|
139
|
-
failed: "failed";
|
|
140
140
|
skipped: "skipped";
|
|
141
141
|
}>>;
|
|
142
142
|
resources: z.ZodOptional<z.ZodObject<{
|
|
@@ -253,10 +253,10 @@ export declare const SuspensionPayloadSchema: z.ZodObject<{
|
|
|
253
253
|
packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
254
254
|
depends_on: z.ZodArray<z.ZodString>;
|
|
255
255
|
status: z.ZodDefault<z.ZodEnum<{
|
|
256
|
+
failed: "failed";
|
|
256
257
|
pending: "pending";
|
|
257
258
|
running: "running";
|
|
258
259
|
completed: "completed";
|
|
259
|
-
failed: "failed";
|
|
260
260
|
skipped: "skipped";
|
|
261
261
|
}>>;
|
|
262
262
|
resources: z.ZodOptional<z.ZodObject<{
|
package/dist/state/analyses.d.ts
CHANGED
|
@@ -20,12 +20,12 @@ export declare function loadAnalysisStatus(pool: Querier, resourceId: string): R
|
|
|
20
20
|
context: string | null;
|
|
21
21
|
} | null, DbError>;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Mark an analysis as suspended.
|
|
24
24
|
* Idempotent — no-op if already suspended.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* `suspended_insufficient_funds` is the name of the one suspended state, for
|
|
27
|
+
* each reason of the host, thus no reason is persisted. A chat turn never calls
|
|
28
|
+
* it.
|
|
29
29
|
*/
|
|
30
30
|
export declare function suspendAnalysis(pool: Querier, analysisId: string): ResultAsync<void, DbError>;
|
|
31
31
|
/**
|
package/dist/state/analyses.js
CHANGED
|
@@ -41,12 +41,12 @@ export function loadAnalysisStatus(pool, resourceId) {
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Mark an analysis as suspended.
|
|
45
45
|
* Idempotent — no-op if already suspended.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* `suspended_insufficient_funds` is the name of the one suspended state, for
|
|
48
|
+
* each reason of the host, thus no reason is persisted. A chat turn never calls
|
|
49
|
+
* it.
|
|
50
50
|
*/
|
|
51
51
|
export function suspendAnalysis(pool, analysisId) {
|
|
52
52
|
return tryMutation("analyses.suspendAnalysis", async () => {
|
package/dist/state/schema.d.ts
CHANGED
|
@@ -10,10 +10,10 @@ export declare const AnalysisStateRowSchema: z.ZodObject<{
|
|
|
10
10
|
status: z.ZodString;
|
|
11
11
|
context: z.ZodNullable<z.ZodString>;
|
|
12
12
|
dataProfileStatus: z.ZodEnum<{
|
|
13
|
+
failed: "failed";
|
|
13
14
|
pending: "pending";
|
|
14
15
|
running: "running";
|
|
15
16
|
completed: "completed";
|
|
16
|
-
failed: "failed";
|
|
17
17
|
}>;
|
|
18
18
|
dataProfileError: z.ZodNullable<z.ZodString>;
|
|
19
19
|
dataProfileStartedAt: z.ZodNullable<z.ZodString>;
|
|
@@ -46,19 +46,19 @@ export declare const ArtifactRowSchema: z.ZodObject<{
|
|
|
46
46
|
}, z.core.$strip>;
|
|
47
47
|
export type ArtifactRow = z.infer<typeof ArtifactRowSchema>;
|
|
48
48
|
export declare const RunStatus: z.ZodEnum<{
|
|
49
|
+
failed: "failed";
|
|
49
50
|
running: "running";
|
|
50
51
|
completed: "completed";
|
|
51
|
-
failed: "failed";
|
|
52
52
|
partial: "partial";
|
|
53
53
|
canceled: "canceled";
|
|
54
54
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
55
55
|
}>;
|
|
56
56
|
export type RunStatus = z.infer<typeof RunStatus>;
|
|
57
57
|
export declare const StepExecutionStatus: z.ZodEnum<{
|
|
58
|
+
failed: "failed";
|
|
58
59
|
pending: "pending";
|
|
59
60
|
running: "running";
|
|
60
61
|
completed: "completed";
|
|
61
|
-
failed: "failed";
|
|
62
62
|
canceled: "canceled";
|
|
63
63
|
skipped: "skipped";
|
|
64
64
|
blocked: "blocked";
|
|
@@ -77,9 +77,9 @@ export declare const CortexRunRowSchema: z.ZodObject<{
|
|
|
77
77
|
threadId: z.ZodNullable<z.ZodString>;
|
|
78
78
|
workflowName: z.ZodString;
|
|
79
79
|
status: z.ZodEnum<{
|
|
80
|
+
failed: "failed";
|
|
80
81
|
running: "running";
|
|
81
82
|
completed: "completed";
|
|
82
|
-
failed: "failed";
|
|
83
83
|
partial: "partial";
|
|
84
84
|
canceled: "canceled";
|
|
85
85
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
@@ -124,10 +124,10 @@ export declare const StepExecutionRowSchema: z.ZodObject<{
|
|
|
124
124
|
wave: z.ZodNumber;
|
|
125
125
|
agentId: z.ZodString;
|
|
126
126
|
status: z.ZodEnum<{
|
|
127
|
+
failed: "failed";
|
|
127
128
|
pending: "pending";
|
|
128
129
|
running: "running";
|
|
129
130
|
completed: "completed";
|
|
130
|
-
failed: "failed";
|
|
131
131
|
canceled: "canceled";
|
|
132
132
|
skipped: "skipped";
|
|
133
133
|
blocked: "blocked";
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* triggers dedup via the ledger CAS (only one `tryStart/tryRerun/tryRetry`
|
|
18
18
|
* UPDATE wins); each winning attempt starts a workflow under a per-attempt id.
|
|
19
19
|
*/
|
|
20
|
+
import { type ResultAsync } from "neverthrow";
|
|
20
21
|
import type { Pool } from "pg";
|
|
21
22
|
import { type AuthContext, type RunSession } from "../auth/types.js";
|
|
22
23
|
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
@@ -25,6 +26,7 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
|
|
|
25
26
|
import type { StagedInput } from "../execution/staged-input.js";
|
|
26
27
|
import type { SandboxAgentDeps } from "../agents/sandbox/shared.js";
|
|
27
28
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
29
|
+
import { type GateRefusal } from "../lib/hooks.js";
|
|
28
30
|
import type { Logger } from "../lib/logger.js";
|
|
29
31
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
30
32
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
@@ -60,13 +62,6 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
|
60
62
|
readonly skillsDir: string;
|
|
61
63
|
/** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
|
|
62
64
|
readonly usageRecorder?: UsageRecorder;
|
|
63
|
-
/**
|
|
64
|
-
* Host-supplied labels for the profiler's sandbox pod, resolved under the
|
|
65
|
-
* profiling session. The map is opaque to the harness: it stamps each entry
|
|
66
|
-
* and interprets none of it. Absent in a wiring that attributes nothing —
|
|
67
|
-
* the pod then carries the harness's own labels only.
|
|
68
|
-
*/
|
|
69
|
-
readonly resolvePodLabels?: (session: RunSession) => Promise<Record<string, string>>;
|
|
70
65
|
/**
|
|
71
66
|
* The farm-extension seam of the embedder. Bound, it rides into the
|
|
72
67
|
* profiler agent deps, and the substrate attaches `link_packages`. Thus
|
|
@@ -187,7 +182,7 @@ export interface DataProfileTriggerParams {
|
|
|
187
182
|
* `RunAuthorizer` seam from the opaque auth; this passes that auth straight
|
|
188
183
|
* through and never inspects it.
|
|
189
184
|
*/
|
|
190
|
-
export declare function authorizeDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams):
|
|
185
|
+
export declare function authorizeDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams): ResultAsync<RunAuthorization, GateRefusal>;
|
|
191
186
|
/**
|
|
192
187
|
* Per-attempt DBOS workflow id. The `nonce` is fresh per attempt (a bare
|
|
193
188
|
* `randomUUID()` minted at the trigger), so each (re-)profile of the same
|
|
@@ -227,5 +222,7 @@ export declare function triggerDataProfile(deps: DataProfileTriggerDeps, params:
|
|
|
227
222
|
* await the workflow's completion, but a start that rejects compensates the
|
|
228
223
|
* ledger (see {@link compensateStartFailure}) before re-throwing, so a caller's
|
|
229
224
|
* own `.catch` still observes the error and the row never wedges at `running`.
|
|
225
|
+
* A refused authorization fails the row with the reason of the host, and the
|
|
226
|
+
* promise resolves: the row is where a caller reads the outcome.
|
|
230
227
|
*/
|
|
231
228
|
export declare function runDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams): Promise<void>;
|
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
* triggers dedup via the ledger CAS (only one `tryStart/tryRerun/tryRetry`
|
|
18
18
|
* UPDATE wins); each winning attempt starts a workflow under a per-attempt id.
|
|
19
19
|
*/
|
|
20
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
20
|
+
import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
|
|
21
21
|
import { randomUUID } from "node:crypto";
|
|
22
|
-
import { ok } from "neverthrow";
|
|
23
|
-
import { forSubAgent } from "../auth/types.js";
|
|
22
|
+
import { err, ok } from "neverthrow";
|
|
23
|
+
import { forStep, forSubAgent } from "../auth/types.js";
|
|
24
24
|
import { createDataProfilerAgent } from "../agents/sandbox/data-profiler.js";
|
|
25
25
|
import { runToTerminal } from "../loop/run-to-terminal.js";
|
|
26
26
|
import { durableStep } from "../loop/run-step.js";
|
|
27
27
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
28
|
+
import { deliverNotice, passGate } from "../lib/hooks.js";
|
|
28
29
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
29
30
|
import { defineTool } from "../tools/define-tool.js";
|
|
30
31
|
import { createDetailResolver } from "../tools/detail-resolver.js";
|
|
31
32
|
import { renderWorkspace } from "../prompts/briefing.js";
|
|
33
|
+
import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
|
|
34
|
+
import { cancelSelf, suspensionOfFailure, suspensionOfRefusal, suspensionOfSpawnRefusal } from "../workflows/suspension.js";
|
|
32
35
|
import { toSandboxPath } from "../workspace/paths.js";
|
|
33
36
|
import { estimateDataProfileResources } from "../sandbox/estimate-data-profile-resources.js";
|
|
34
37
|
import { generateExecutionId } from "../sandbox/execution-id.js";
|
|
@@ -44,7 +47,7 @@ import { scanInputTree } from "../input-scan/scan.js";
|
|
|
44
47
|
import { PROFILE_INDEX_TYPES, buildProfileIndexEntries } from "./data-profile-index.js";
|
|
45
48
|
import { absorbRecipe, renderAbsorbDelta } from "./data-profile-absorb.js";
|
|
46
49
|
import { formatResolutionErrors, resolveProfileSubmission } from "./data-profile-resolve.js";
|
|
47
|
-
import { completeDataProfile, completeEmptyDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
50
|
+
import { completeDataProfile, completeEmptyDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, suspendAnalysis, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
48
51
|
import { elapsedSinceIso, recordRunCompleted } from "../lib/metrics.js";
|
|
49
52
|
import { createProfileActivityEmitter } from "./data-profile-activity.js";
|
|
50
53
|
import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.js";
|
|
@@ -139,6 +142,8 @@ async function readSetHeaders(args) {
|
|
|
139
142
|
});
|
|
140
143
|
}
|
|
141
144
|
catch (err) {
|
|
145
|
+
if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
146
|
+
throw err;
|
|
142
147
|
logger.warn("input-scan header readout failed (non-fatal)", logger.errorFields(err));
|
|
143
148
|
return new Map();
|
|
144
149
|
}
|
|
@@ -285,6 +290,21 @@ export async function runDataProfileBody(input, deps) {
|
|
|
285
290
|
// Cortex-owned and must be revoked here.
|
|
286
291
|
const { analysisId, runSession, ownsMandate = true, stagedInputs } = input; // oss-core-managed-ok
|
|
287
292
|
const authorization = { runSession, ownsMandate }; // oss-core-managed-ok
|
|
293
|
+
const revoke = (reason) => deliverNotice(logger, "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, reason));
|
|
294
|
+
let selfCancelled = false;
|
|
295
|
+
// A suspension fails the row with the reason of the host, because the profile
|
|
296
|
+
// ledger has no suspended state and the retry path takes a `failed` row again.
|
|
297
|
+
const suspendProfile = async (suspension) => {
|
|
298
|
+
logger.warn("profile suspended", { reason: suspension.reason });
|
|
299
|
+
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, suspension.reason)), "failed");
|
|
300
|
+
await revoke("data-profile-suspended");
|
|
301
|
+
unwrapOrThrow(await suspendAnalysis(deps.pool, analysisId));
|
|
302
|
+
await activity.failed(suspension.reason);
|
|
303
|
+
if (DBOS.workflowID === undefined)
|
|
304
|
+
return;
|
|
305
|
+
selfCancelled = true;
|
|
306
|
+
await cancelSelf("self-cancel-suspended");
|
|
307
|
+
};
|
|
288
308
|
// The profile's activity channel. `DBOS.writeStream` is body-only, so the write is bound here
|
|
289
309
|
// while every phase and phrase lives in the emitter — the phrases are the observable contract of
|
|
290
310
|
// this capability, and a body that composed its own strings is a body they can drift from.
|
|
@@ -318,7 +338,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
318
338
|
// empty-manifest path invisible to the same monitoring that watches every other.
|
|
319
339
|
logProfileMonitoring(logger, { dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
320
340
|
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)), "completed");
|
|
321
|
-
await
|
|
341
|
+
await revoke("data-profile-completed");
|
|
322
342
|
// This is a terminal completion like any other, so it reports one — a consumer watching
|
|
323
343
|
// an empty-manifest profile sees it settle rather than seeing the stream simply stop.
|
|
324
344
|
await activity.complete();
|
|
@@ -371,7 +391,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
371
391
|
await indexProfile({ deps, analysisId, session: runSession, record, scan, logger });
|
|
372
392
|
logProfileMonitoring(logger, { resolution, dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
373
393
|
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)), "completed");
|
|
374
|
-
await
|
|
394
|
+
await revoke("data-profile-completed");
|
|
375
395
|
await activity.complete();
|
|
376
396
|
return;
|
|
377
397
|
}
|
|
@@ -397,7 +417,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
397
417
|
absorb: absorb.kind,
|
|
398
418
|
});
|
|
399
419
|
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)), "completed");
|
|
400
|
-
await
|
|
420
|
+
await revoke("data-profile-completed");
|
|
401
421
|
await activity.complete();
|
|
402
422
|
return;
|
|
403
423
|
}
|
|
@@ -408,27 +428,10 @@ export async function runDataProfileBody(input, deps) {
|
|
|
408
428
|
// from the agent loop would leave the longest wait of all unreported — the same defect the
|
|
409
429
|
// run panel's activity readout was built to remove.
|
|
410
430
|
await activity.sandboxInit();
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
catch (err) {
|
|
417
|
-
logger.warn("pod-label resolution failed", logger.errorFields(err));
|
|
418
|
-
}
|
|
419
|
-
// A wired resolver that yields nothing is the one loud case: the host
|
|
420
|
-
// asked for attribution and the pod spawns without it.
|
|
421
|
-
if (!podLabels || Object.keys(podLabels).length === 0)
|
|
422
|
-
logger.warn("sandbox spawned with no pod labels");
|
|
423
|
-
}
|
|
424
|
-
const sandbox = await deps.sandboxClient.createSandbox({
|
|
425
|
-
runId: DATA_PROFILE_RUN_LITERAL,
|
|
426
|
-
stepId: DATA_PROFILE_STEP_LITERAL,
|
|
427
|
-
analysisId,
|
|
428
|
-
childWorkflowId: workflowId,
|
|
429
|
-
resources: estimateDataProfileResources(stagedInputs),
|
|
430
|
-
podLabels,
|
|
431
|
-
}, mintSandboxIdentity(DATA_PROFILE_RUN_LITERAL));
|
|
431
|
+
const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(runSession, DATA_PROFILE_STEP_LITERAL), { childWorkflowId: workflowId, resources: estimateDataProfileResources(stagedInputs) }, mintSandboxIdentity(DATA_PROFILE_RUN_LITERAL)));
|
|
432
|
+
if (spawned.isErr())
|
|
433
|
+
return await suspendProfile(suspensionOfSpawnRefusal(spawned.error));
|
|
434
|
+
const sandbox = spawned.value;
|
|
432
435
|
try {
|
|
433
436
|
// Checkpointed clock, not `Date.now()`: `awaitExec` gates on this absolute
|
|
434
437
|
// deadline and its recovery-pull is a durable step, so a wall-clock deadline
|
|
@@ -545,6 +548,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
545
548
|
runStep: durableStep,
|
|
546
549
|
resolved: () => accepted !== null,
|
|
547
550
|
usageRecorder: deps.usageRecorder,
|
|
551
|
+
isFatalLoopError: (err) => err instanceof DBOSErrors.DBOSWorkflowCancelledError,
|
|
548
552
|
}, {
|
|
549
553
|
tools: [submitProfileTool],
|
|
550
554
|
nudge: "You stopped without calling submit_profile, so no profile was " +
|
|
@@ -574,7 +578,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
574
578
|
// 6. Complete — store the FULL profiler finding plus the input signature for
|
|
575
579
|
// staleness detection. The scratch tree is gone; this row is all that survives.
|
|
576
580
|
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord)), "completed");
|
|
577
|
-
await
|
|
581
|
+
await revoke("data-profile-completed");
|
|
578
582
|
// LAST statement of the success path, and that placement is the guarantee that exactly
|
|
579
583
|
// one terminal activity is ever emitted. Everything that could still throw — the ledger
|
|
580
584
|
// write above, the revoke — is already behind us, so any failure on the way here reaches
|
|
@@ -593,10 +597,15 @@ export async function runDataProfileBody(input, deps) {
|
|
|
593
597
|
}
|
|
594
598
|
}
|
|
595
599
|
catch (err) {
|
|
600
|
+
if (selfCancelled || err instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
601
|
+
throw err;
|
|
602
|
+
const suspension = suspensionOfFailure(err);
|
|
603
|
+
if (suspension !== undefined)
|
|
604
|
+
return await suspendProfile(suspension);
|
|
596
605
|
logger.error("profile failed", logger.errorFields(err));
|
|
597
606
|
const reason = profileFailureReason(err);
|
|
598
607
|
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)), "failed");
|
|
599
|
-
await
|
|
608
|
+
await revoke("data-profile-failed");
|
|
600
609
|
// The same bounded, user-safe line the ledger receives — never the raw error, whose paths and
|
|
601
610
|
// stack frames stay in the log record above. This is the only other terminal emission, so
|
|
602
611
|
// reaching it means the success path did not emit one.
|
|
@@ -633,14 +642,14 @@ function settleTerminalWrite(logger, analysisId, write, status) {
|
|
|
633
642
|
* `RunAuthorizer` seam from the opaque auth; this passes that auth straight
|
|
634
643
|
* through and never inspects it.
|
|
635
644
|
*/
|
|
636
|
-
export
|
|
645
|
+
export function authorizeDataProfile(deps, params) {
|
|
637
646
|
const { auth, analysisId } = params;
|
|
638
|
-
return deps.runAuthorizer.authorize({
|
|
647
|
+
return passGate("RunAuthorizer.authorize", deps.runAuthorizer.authorize({
|
|
639
648
|
auth,
|
|
640
649
|
scope: { kind: "analysis", analysisId },
|
|
641
650
|
provenance: { agentId: DATA_PROFILE_AGENT_ID, callPath: [DATA_PROFILE_AGENT_ID] },
|
|
642
651
|
frame: { runId: DATA_PROFILE_RUN_LITERAL, stepId: DATA_PROFILE_STEP_LITERAL },
|
|
643
|
-
});
|
|
652
|
+
}));
|
|
644
653
|
}
|
|
645
654
|
/**
|
|
646
655
|
* Per-attempt DBOS workflow id. The `nonce` is fresh per attempt (a bare
|
|
@@ -666,7 +675,10 @@ export function dataProfileWorkflowId(analysisId, nonce) {
|
|
|
666
675
|
* not awaited.
|
|
667
676
|
*/
|
|
668
677
|
async function startDataProfileWorkflow(deps, params) {
|
|
669
|
-
const
|
|
678
|
+
const authorized = await authorizeDataProfile(deps, params);
|
|
679
|
+
if (authorized.isErr())
|
|
680
|
+
return err(authorized.error);
|
|
681
|
+
const { runSession, ownsMandate } = authorized.value; // oss-core-managed-ok
|
|
670
682
|
const attemptNonce = randomUUID();
|
|
671
683
|
await DBOS.startWorkflow(deps.workflow, {
|
|
672
684
|
workflowID: dataProfileWorkflowId(params.analysisId, attemptNonce),
|
|
@@ -676,6 +688,20 @@ async function startDataProfileWorkflow(deps, params) {
|
|
|
676
688
|
ownsMandate, // oss-core-managed-ok
|
|
677
689
|
stagedInputs: params.stagedInputs,
|
|
678
690
|
});
|
|
691
|
+
return ok(undefined);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Dispatch a claimed profile, fire-and-forget. The claim already flipped the
|
|
695
|
+
* row to `running`, thus a refused authorization and a rejected start each
|
|
696
|
+
* settle the row as `failed`, and it never wedges at `running`.
|
|
697
|
+
*/
|
|
698
|
+
async function dispatchClaimedProfile(deps, params, phase) {
|
|
699
|
+
const started = await startDataProfileWorkflow(deps, params).catch(async (cause) => {
|
|
700
|
+
await compensateStartFailure(deps, params.analysisId, phase, cause);
|
|
701
|
+
return undefined;
|
|
702
|
+
});
|
|
703
|
+
if (started?.isErr())
|
|
704
|
+
await refuseClaimedProfile(deps, params.analysisId, phase, started.error);
|
|
679
705
|
}
|
|
680
706
|
/**
|
|
681
707
|
* Attempt to claim and run data profiling for an analysis, then start the workflow
|
|
@@ -738,12 +764,12 @@ export async function triggerDataProfile(deps, params) {
|
|
|
738
764
|
}
|
|
739
765
|
const started = unwrapOrThrow(await tryStartDataProfile(deps.pool, analysisId));
|
|
740
766
|
if (started) {
|
|
741
|
-
|
|
767
|
+
void dispatchClaimedProfile(deps, params, "Run");
|
|
742
768
|
return "started";
|
|
743
769
|
}
|
|
744
770
|
const restarted = unwrapOrThrow(await tryRerunDataProfile(deps.pool, analysisId));
|
|
745
771
|
if (restarted) {
|
|
746
|
-
|
|
772
|
+
void dispatchClaimedProfile(deps, params, "Re-run");
|
|
747
773
|
return "restarted";
|
|
748
774
|
}
|
|
749
775
|
const status = unwrapOrThrow(await loadDataProfileStatus(deps.pool, analysisId));
|
|
@@ -765,10 +791,24 @@ export async function triggerDataProfile(deps, params) {
|
|
|
765
791
|
* Best-effort — a compensation write that itself fails is only logged, since
|
|
766
792
|
* there is no further channel to report it on.
|
|
767
793
|
*/
|
|
768
|
-
async function compensateStartFailure(deps, analysisId, phase,
|
|
794
|
+
async function compensateStartFailure(deps, analysisId, phase, cause) {
|
|
795
|
+
const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId });
|
|
796
|
+
logger.error("start failed", { phase, ...logger.errorFields(cause) });
|
|
797
|
+
await failClaimedProfile(deps, analysisId, phase, profileFailureReason(cause));
|
|
798
|
+
}
|
|
799
|
+
async function refuseClaimedProfile(deps, analysisId, phase, refusal) {
|
|
769
800
|
const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId });
|
|
770
|
-
logger.error("
|
|
771
|
-
|
|
801
|
+
logger.error("the profile was not authorized", { phase, reason: refusal.reason, suspend: refusal.kind === "suspended" });
|
|
802
|
+
await failClaimedProfile(deps, analysisId, phase, refusal.reason);
|
|
803
|
+
if (suspensionOfRefusal(refusal) === undefined)
|
|
804
|
+
return;
|
|
805
|
+
const marked = await suspendAnalysis(deps.pool, analysisId);
|
|
806
|
+
if (marked.isErr())
|
|
807
|
+
logger.error("the analysis was not marked as suspended", { phase, err: marked.error });
|
|
808
|
+
}
|
|
809
|
+
async function failClaimedProfile(deps, analysisId, phase, reason) {
|
|
810
|
+
const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId });
|
|
811
|
+
const failed = await failDataProfile(deps.pool, analysisId, reason);
|
|
772
812
|
if (failed.isErr()) {
|
|
773
813
|
logger.error("failed to mark failed after a start error", { phase, err: failed.error });
|
|
774
814
|
}
|
|
@@ -790,15 +830,20 @@ async function compensateStartFailure(deps, analysisId, phase, err) {
|
|
|
790
830
|
* await the workflow's completion, but a start that rejects compensates the
|
|
791
831
|
* ledger (see {@link compensateStartFailure}) before re-throwing, so a caller's
|
|
792
832
|
* own `.catch` still observes the error and the row never wedges at `running`.
|
|
833
|
+
* A refused authorization fails the row with the reason of the host, and the
|
|
834
|
+
* promise resolves: the row is where a caller reads the outcome.
|
|
793
835
|
*/
|
|
794
836
|
export async function runDataProfile(deps, params) {
|
|
837
|
+
let started;
|
|
795
838
|
try {
|
|
796
|
-
await startDataProfileWorkflow(deps, params);
|
|
839
|
+
started = await startDataProfileWorkflow(deps, params);
|
|
797
840
|
}
|
|
798
|
-
catch (
|
|
799
|
-
await compensateStartFailure(deps, params.analysisId, "Retry",
|
|
800
|
-
throw
|
|
841
|
+
catch (cause) {
|
|
842
|
+
await compensateStartFailure(deps, params.analysisId, "Retry", cause);
|
|
843
|
+
throw cause;
|
|
801
844
|
}
|
|
845
|
+
if (started.isErr())
|
|
846
|
+
await refuseClaimedProfile(deps, params.analysisId, "Retry", started.error);
|
|
802
847
|
}
|
|
803
848
|
/**
|
|
804
849
|
* Concise, UI-safe failure reason. `data_profile_error` is surfaced verbatim
|