@inflexa-ai/harness 0.28.0 → 0.28.1
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.
|
@@ -49,6 +49,15 @@ logger = createNoopLogger()) {
|
|
|
49
49
|
await upsertArtifacts(db, localEntries);
|
|
50
50
|
const localPaths = new Set(localEntries.map((e) => e.path));
|
|
51
51
|
const result = await registry.register(input, session);
|
|
52
|
+
// The registry excluded these from `failed` by its own severity judgement, so
|
|
53
|
+
// this is their only record. Logged here rather than left to the registry:
|
|
54
|
+
// the seam is implementable by an embedder, and a rejection does not get to
|
|
55
|
+
// go unrecorded because one implementation chose not to log it.
|
|
56
|
+
if (result.notCounted && result.notCounted.length > 0) {
|
|
57
|
+
log.warn("registry rejections excluded from the failure count", {
|
|
58
|
+
rejected: result.notCounted.map((f) => ({ path: f.path, error: f.error })),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
52
61
|
let externalRegistered = 0;
|
|
53
62
|
for (const reg of result.registered) {
|
|
54
63
|
// Skip files the registry also returned that we didn't upsert locally
|
|
@@ -64,6 +64,20 @@ export interface ExternalRegistrationResult {
|
|
|
64
64
|
* back a whole batch (`failed` then carries one summary entry).
|
|
65
65
|
*/
|
|
66
66
|
failedCount: number;
|
|
67
|
+
/**
|
|
68
|
+
* Rejections the registry judged non-terminal and deliberately kept out of
|
|
69
|
+
* `failed`/`failedCount`: rows it never attempted, so nothing registered and
|
|
70
|
+
* no bytes are at risk. Reported rather than dropped — excluding a rejection
|
|
71
|
+
* decides only that it is not worth failing a step over, never that it goes
|
|
72
|
+
* unrecorded. Keeping them out of `failed` is what lets the fail-fast message
|
|
73
|
+
* list only the paths that actually cost the step something; surfacing them
|
|
74
|
+
* here is what keeps the registry's verdict checkable against what the
|
|
75
|
+
* external system actually said.
|
|
76
|
+
*/
|
|
77
|
+
notCounted?: ReadonlyArray<{
|
|
78
|
+
path: string;
|
|
79
|
+
error: string;
|
|
80
|
+
}>;
|
|
67
81
|
}
|
|
68
82
|
export interface ArtifactRegistry {
|
|
69
83
|
/**
|
|
@@ -56,10 +56,12 @@ export declare function generateStepSummaryAndWrite(deps: PostStepPipelineDeps,
|
|
|
56
56
|
/**
|
|
57
57
|
* Reconcile the manifest against disk (drop phantoms, rehash) and register the
|
|
58
58
|
* survivors with the local ledger + the injected `ArtifactRegistry`. Fail-fast
|
|
59
|
-
* (see the artifact-manifest spec):
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* the
|
|
59
|
+
* (see the artifact-manifest spec): `externalFailed` counts only the terminal
|
|
60
|
+
* rejections — a rejected output whose bytes exist nowhere but the step tree,
|
|
61
|
+
* plus anything rejected as a consequence of one — so a non-zero count means
|
|
62
|
+
* real outputs went unregistered, and it throws with the per-file detail (the
|
|
63
|
+
* OSS filesystem registry returns `externalFailed: 0`, so it never trips).
|
|
64
|
+
* Returns the reconciled manifest.
|
|
63
65
|
*/
|
|
64
66
|
export declare function reconcileAndRegisterStepArtifacts(deps: PostStepPipelineDeps, postCtx: PostStepContext, manifest: readonly ArtifactManifestEntry[]): Promise<readonly ArtifactManifestEntry[]>;
|
|
65
67
|
/**
|
|
@@ -95,10 +95,12 @@ export async function generateStepSummaryAndWrite(deps, postCtx, manifest) {
|
|
|
95
95
|
/**
|
|
96
96
|
* Reconcile the manifest against disk (drop phantoms, rehash) and register the
|
|
97
97
|
* survivors with the local ledger + the injected `ArtifactRegistry`. Fail-fast
|
|
98
|
-
* (see the artifact-manifest spec):
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* the
|
|
98
|
+
* (see the artifact-manifest spec): `externalFailed` counts only the terminal
|
|
99
|
+
* rejections — a rejected output whose bytes exist nowhere but the step tree,
|
|
100
|
+
* plus anything rejected as a consequence of one — so a non-zero count means
|
|
101
|
+
* real outputs went unregistered, and it throws with the per-file detail (the
|
|
102
|
+
* OSS filesystem registry returns `externalFailed: 0`, so it never trips).
|
|
103
|
+
* Returns the reconciled manifest.
|
|
102
104
|
*/
|
|
103
105
|
export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest) {
|
|
104
106
|
const { input, session, lineageCollector } = postCtx;
|
|
@@ -122,11 +124,18 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
|
|
|
122
124
|
collector: lineageCollector,
|
|
123
125
|
}, session, deps.logger);
|
|
124
126
|
if (reg.externalFailed > 0) {
|
|
125
|
-
|
|
127
|
+
// The registry commits per leaf and per activity, so accepted and rejected
|
|
128
|
+
// rows arriving together is ordinary and `externalRegistered` is what
|
|
129
|
+
// genuinely landed. Zero of it means this batch registered nothing at all:
|
|
130
|
+
// no row carries an `artifact_id`, so the byte-sync that follows selects
|
|
131
|
+
// none of them and every output stays local-only. That reads as a payload-
|
|
132
|
+
// or credential-shaped rejection rather than a per-file one, so it rides on
|
|
133
|
+
// the message and as its own field.
|
|
134
|
+
const noneRegistered = reg.externalRegistered === 0;
|
|
126
135
|
const detail = reg.failureDetails.map((f) => `${f.path}: ${f.error}`).join("\n ");
|
|
127
136
|
const msg = `[post-step.reconcile] external registration failed for ${input.stepId}: ` +
|
|
128
|
-
`${reg.externalFailed}
|
|
129
|
-
(
|
|
137
|
+
`${reg.externalFailed} terminal rejection(s), ${reg.externalRegistered}/${reg.localCount} local artifact(s) registered` +
|
|
138
|
+
(noneRegistered ? " (nothing in this batch registered)" : "") +
|
|
130
139
|
(detail ? `\n ${detail}` : "");
|
|
131
140
|
// Logged as fields as well as thrown: the throw reaches `failStep` as one
|
|
132
141
|
// opaque string, so the per-path rejections are only queryable from here.
|
|
@@ -138,7 +147,7 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
|
|
|
138
147
|
externalFailed: reg.externalFailed,
|
|
139
148
|
externalRegistered: reg.externalRegistered,
|
|
140
149
|
localCount: reg.localCount,
|
|
141
|
-
|
|
150
|
+
noneRegistered,
|
|
142
151
|
failures: reg.failureDetails,
|
|
143
152
|
});
|
|
144
153
|
throw new Error(msg);
|
|
@@ -500,16 +500,46 @@ export async function runSandboxStepBody(input, deps) {
|
|
|
500
500
|
// outputs, so it fails the step loudly instead of finishing green. Transient
|
|
501
501
|
// managed-root errors are already retried in the client, so anything thrown here is
|
|
502
502
|
// persistent — tear down, mark failed, and re-raise so the parent's fail-fast
|
|
503
|
-
// cascade fires (mirrors the agent-loop failure path).
|
|
503
|
+
// cascade fires (mirrors the agent-loop failure path). The two operations are
|
|
504
|
+
// sequenced independently: byte-sync is attempted on both arms, so a rejected
|
|
505
|
+
// registration still uploads the bytes of whatever DID register.
|
|
504
506
|
await emitActivity("persisting", "Registering artifacts");
|
|
507
|
+
// A bare call, deliberately not `DBOS.runStep`-wrapped: it is reachable from
|
|
508
|
+
// two arms below, and a checkpointed step reached from a conditional arm
|
|
509
|
+
// would shift the body's function-ID sequence between the two paths (see the
|
|
510
|
+
// harness-durable-runtime spec).
|
|
511
|
+
const syncStepArtifacts = () => deps.artifactRegistry.sync({
|
|
512
|
+
resourceId: input.analysisId,
|
|
513
|
+
runId: input.runId,
|
|
514
|
+
stepId: input.stepId,
|
|
515
|
+
}, session);
|
|
505
516
|
let reconciledManifest;
|
|
506
517
|
try {
|
|
507
518
|
reconciledManifest = await reconcileAndRegisterStepArtifacts(deps, postCtx, manifest);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
519
|
+
}
|
|
520
|
+
catch (registrationErr) {
|
|
521
|
+
// Registration and byte-sync are independent, and a registry rejection is
|
|
522
|
+
// partial: the rows that DID register carry `artifact_id` with `file_id`
|
|
523
|
+
// still NULL, which is exactly what `queryUnsyncedStepArtifacts` selects.
|
|
524
|
+
// Syncing anyway uploads their bytes (rows that never registered are not
|
|
525
|
+
// selected, so this stays idempotent under re-execution); skipping it is
|
|
526
|
+
// what strands them registered-but-byteless.
|
|
527
|
+
try {
|
|
528
|
+
await syncStepArtifacts();
|
|
529
|
+
}
|
|
530
|
+
catch (syncErr) {
|
|
531
|
+
// A cancellation is control-flow, not a sync failure — re-raise it
|
|
532
|
+
// verbatim, as every other stage in this body does. Any real failure
|
|
533
|
+
// is swallowed so it cannot displace the registration rejection as the
|
|
534
|
+
// step's cause; this record is the only account of it.
|
|
535
|
+
if (syncErr instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
536
|
+
throw syncErr;
|
|
537
|
+
logger.named("post-step.sync").warn("failed after a registration rejection (non-fatal)", logger.errorFields(syncErr));
|
|
538
|
+
}
|
|
539
|
+
throw await failStep("lineage_attestation", registrationErr);
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
await syncStepArtifacts();
|
|
513
543
|
}
|
|
514
544
|
catch (err) {
|
|
515
545
|
throw await failStep("lineage_attestation", err);
|
package/package.json
CHANGED