@atolis-hq/wake 0.3.52 → 0.3.54
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/dist/src/bootstrap/composition-root.js +1 -0
- package/dist/src/bootstrap/surface-api-execution-applications.js +20 -0
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/control-plane/application/advance-once.js +18 -10
- package/dist/src/execution/application/activation-claim.js +7 -1
- package/dist/src/execution/application/recovery-service.js +6 -1
- package/dist/src/execution/application/run-repository.js +2 -1
- package/dist/src/execution/index.js +1 -0
- package/dist/src/orchestration/application/advance-workflow.js +5 -1
- package/dist/src/surfaces/api/routes/commands.js +35 -0
- package/dist/src/surfaces/api/routes/execution.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ControlStreamKind, ineligibleRunners, } from '../control-plane/index.js';
|
|
2
|
+
import { ExecutionFailureCode, RunStatus } from '../execution/index.js';
|
|
3
|
+
import { correlationId, EventActorKind } from '../kernel/index.js';
|
|
2
4
|
import { ApiCommandStatus, presentRun } from '../surfaces/index.js';
|
|
3
5
|
import { projectionMeta, sampledMeta } from './surface-api-metadata.js';
|
|
4
6
|
import { projectionPage } from './surface-api-projection-pages.js';
|
|
@@ -6,6 +8,15 @@ import { withWorkflowContext } from './surface-api-run-context.js';
|
|
|
6
8
|
import { readWorkTranscript } from './surface-api-transcripts.js';
|
|
7
9
|
export function createExecutionApplications(root, now) {
|
|
8
10
|
return {
|
|
11
|
+
async resolveAmbiguousRun(runId, command) {
|
|
12
|
+
const context = resolutionContext(runId, command.idempotencyKey, now);
|
|
13
|
+
const run = await root.recovery.resolve(runId, {
|
|
14
|
+
kind: RunStatus.Failed,
|
|
15
|
+
failure: { kind: ExecutionFailureCode.Unexpected, message: command.message },
|
|
16
|
+
}, context);
|
|
17
|
+
await root.orchestration.resolveExecutionFailure(run.workflowInstanceId, { activationId: run.activationId, runId: run.runId, reason: command.message }, context);
|
|
18
|
+
return commandAccepted(command, now());
|
|
19
|
+
},
|
|
9
20
|
async pauseRunner(runnerId, command) {
|
|
10
21
|
await root.runnerControls.pause(runnerId, command.idempotencyKey);
|
|
11
22
|
return commandAccepted(command, now());
|
|
@@ -85,6 +96,15 @@ export function createExecutionApplications(root, now) {
|
|
|
85
96
|
},
|
|
86
97
|
};
|
|
87
98
|
}
|
|
99
|
+
function resolutionContext(runId, idempotencyKey, now) {
|
|
100
|
+
const commandId = `run:${runId}:resolve-failed:${idempotencyKey}`;
|
|
101
|
+
return {
|
|
102
|
+
commandId,
|
|
103
|
+
correlationId: correlationId(commandId),
|
|
104
|
+
occurredAt: now(),
|
|
105
|
+
actor: { kind: EventActorKind.Operator, id: 'web' },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
88
108
|
function commandAccepted(command, acceptedAt) {
|
|
89
109
|
return {
|
|
90
110
|
commandId: `runner:${command.idempotencyKey}`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RunStatus, WorkspaceMode } from '../../execution/index.js';
|
|
1
|
+
import { ActivationClaimConflictError, RunStatus, WorkspaceMode } from '../../execution/index.js';
|
|
2
2
|
import { correlationId, EventActorKind, } from '../../kernel/index.js';
|
|
3
3
|
import { WorkflowStatus } from '../../orchestration/index.js';
|
|
4
4
|
import { WorkStatus } from '../../work/index.js';
|
|
@@ -141,15 +141,23 @@ export function createAdvanceOnce(orchestration, execution, resources, clock, de
|
|
|
141
141
|
const correlated = await resources.correlationsForWork(selected.workflow.workItemId);
|
|
142
142
|
const resourceViews = (await Promise.all(correlated.map((entry) => resources.get(entry.resourceId)))).filter((resource) => resource !== null);
|
|
143
143
|
const ineligible = await runnerIneligibility();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
144
|
+
let run;
|
|
145
|
+
try {
|
|
146
|
+
run = await execution.attempt(selected.activation, {
|
|
147
|
+
workItemId: selected.workflow.workItemId,
|
|
148
|
+
workflowInstanceId: selected.workflow.workflowInstanceId,
|
|
149
|
+
orchestrationGroupId: selected.workflow.orchestrationGroupId,
|
|
150
|
+
resources: resourceViews,
|
|
151
|
+
sessionPolicy: selected.workflow.parentWorkflowInstanceId === undefined ? 'resume-stage' : 'fresh',
|
|
152
|
+
awaitImmediateCompletion: true,
|
|
153
|
+
...(ineligible.size === 0 ? {} : { ineligibleRunners: ineligible }),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error instanceof ActivationClaimConflictError)
|
|
158
|
+
return { kind: 'no-work' };
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
153
161
|
if (run.status === RunStatus.Succeeded && run.outcome !== undefined) {
|
|
154
162
|
if (await isDispatchPaused())
|
|
155
163
|
return { kind: 'paused' };
|
|
@@ -2,13 +2,19 @@ import { EventActorKind, EventSourceKind, } from '../../kernel/index.js';
|
|
|
2
2
|
import { createActivationExecutionEventDraft } from '../contracts/event-factory.js';
|
|
3
3
|
import { decodeActivationExecutionEvent, ExecutionEventType, } from '../contracts/events.js';
|
|
4
4
|
import { activationStream } from '../contracts/streams.js';
|
|
5
|
+
export class ActivationClaimConflictError extends Error {
|
|
6
|
+
constructor(activationId) {
|
|
7
|
+
super(`Activation ${activationId} already has an active Run claim`);
|
|
8
|
+
this.name = 'ActivationClaimConflictError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
5
11
|
export async function claimActivation(input) {
|
|
6
12
|
const { journal, clock, config, activationId, runId, owner, occurredAt } = input;
|
|
7
13
|
const stream = activationStream(activationId);
|
|
8
14
|
const events = (await journal.readStream(stream)).map(decodeActivationExecutionEvent);
|
|
9
15
|
const last = events.at(-1);
|
|
10
16
|
if (last?.eventType === ExecutionEventType.ActivationClaimed && claimIsUnexpired(last, clock))
|
|
11
|
-
throw new
|
|
17
|
+
throw new ActivationClaimConflictError(activationId);
|
|
12
18
|
await journal.append(stream, events.length, [
|
|
13
19
|
createActivationExecutionEventDraft({
|
|
14
20
|
eventId: `${activationId}:claim:${runId}`,
|
|
@@ -46,8 +46,13 @@ export class RecoveryService {
|
|
|
46
46
|
const loaded = await this.repository.load(currentRunId);
|
|
47
47
|
if (loaded.view === null)
|
|
48
48
|
throw new Error(`Run ${id} does not exist`);
|
|
49
|
-
if (!loaded.view.escalated || loaded.view.status !== RunStatus.Ambiguous)
|
|
49
|
+
if (!loaded.view.escalated || loaded.view.status !== RunStatus.Ambiguous) {
|
|
50
|
+
if (loaded.events?.some((event) => event.causationId === context.commandId &&
|
|
51
|
+
(event.eventType === ExecutionEventType.RunSucceeded ||
|
|
52
|
+
event.eventType === ExecutionEventType.RunFailed)))
|
|
53
|
+
return loaded.view;
|
|
50
54
|
throw new Error(`Run ${id} is not escalated`);
|
|
55
|
+
}
|
|
51
56
|
const draft = resolution.kind === RunStatus.Succeeded
|
|
52
57
|
? createRunExecutionEventDraft({
|
|
53
58
|
...resolutionMetadata(currentRunId, loaded.view, context),
|
|
@@ -9,7 +9,8 @@ export class RunRepository {
|
|
|
9
9
|
}
|
|
10
10
|
async load(runId) {
|
|
11
11
|
const events = await this.journal.readStream(runStream(runId));
|
|
12
|
-
|
|
12
|
+
const decoded = events.map(decodeRunExecutionEvent);
|
|
13
|
+
return { sequence: events.length, events: decoded, view: foldRun(decoded) };
|
|
13
14
|
}
|
|
14
15
|
async append(runId, sequence, drafts) {
|
|
15
16
|
const events = await this.journal.append(runStream(runId), sequence, drafts);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './application/execution-projection.js';
|
|
2
2
|
export * from './application/execution-service.js';
|
|
3
|
+
export * from './application/activation-claim.js';
|
|
3
4
|
export * from './application/recovery-service.js';
|
|
4
5
|
export * from './application/run-repository.js';
|
|
5
6
|
export * from './contracts/commands.js';
|
|
@@ -80,7 +80,8 @@ export class AdvanceWorkflow {
|
|
|
80
80
|
async resolveExecutionFailure(id, input, context) {
|
|
81
81
|
const loaded = await this.repository.load(id);
|
|
82
82
|
if (loaded.view === null ||
|
|
83
|
-
loaded.view.status === WorkflowStatus.Blocked
|
|
83
|
+
(loaded.view.status === WorkflowStatus.Blocked &&
|
|
84
|
+
!isAmbiguityResolutionBlock(loaded.view.blockReason)) ||
|
|
84
85
|
loaded.view.pendingActivation?.activationId !== input.activationId ||
|
|
85
86
|
loaded.view.acceptedOutcomes.includes(input.activationId))
|
|
86
87
|
return loaded.view;
|
|
@@ -203,3 +204,6 @@ export class AdvanceWorkflow {
|
|
|
203
204
|
return matchWatches(await this.listAllLoaded(), event, this.workflows, context);
|
|
204
205
|
}
|
|
205
206
|
}
|
|
207
|
+
function isAmbiguityResolutionBlock(reason) {
|
|
208
|
+
return reason !== undefined && /^run-ambiguous-after-\d+-attempts$/.test(reason);
|
|
209
|
+
}
|
|
@@ -3,6 +3,9 @@ import { accepted, ApiPathError, decodePathSegment, invalidPath, invalidQuery, i
|
|
|
3
3
|
export async function dispatchCommand(applications, url, body) {
|
|
4
4
|
if (url.search !== '')
|
|
5
5
|
return invalidQuery('Command routes do not accept query parameters');
|
|
6
|
+
const ambiguityResolution = await dispatchAmbiguousRunResolution(applications, url.pathname, body);
|
|
7
|
+
if (ambiguityResolution !== undefined)
|
|
8
|
+
return ambiguityResolution;
|
|
6
9
|
const request = commandRequest(body);
|
|
7
10
|
if ('status' in request)
|
|
8
11
|
return request;
|
|
@@ -15,6 +18,21 @@ export async function dispatchCommand(applications, url, body) {
|
|
|
15
18
|
const runner = await dispatchRunnerCommand(applications, url.pathname, request);
|
|
16
19
|
return runner ?? problem(404, 'Not Found', `No command route for ${url.pathname}`);
|
|
17
20
|
}
|
|
21
|
+
async function dispatchAmbiguousRunResolution(applications, pathname, body) {
|
|
22
|
+
const match = /^\/api\/v1\/runs\/([^/]+)\/commands\/resolve-failed$/.exec(pathname);
|
|
23
|
+
if (match === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const runId = decodePathSegment(match[1]);
|
|
26
|
+
if (runId instanceof ApiPathError)
|
|
27
|
+
return invalidPath(runId.message);
|
|
28
|
+
const request = ambiguityFailureResolutionRequest(body);
|
|
29
|
+
if ('status' in request)
|
|
30
|
+
return request;
|
|
31
|
+
const operation = applications.execution.resolveAmbiguousRun;
|
|
32
|
+
if (operation === undefined)
|
|
33
|
+
return unavailable('resolve-ambiguous-run', await applications.execution.get?.(runId));
|
|
34
|
+
return accepted(await operation(runId, request), applications.now());
|
|
35
|
+
}
|
|
18
36
|
async function dispatchWorkCommand(applications, pathname, request) {
|
|
19
37
|
const match = /^\/api\/v1\/work-items\/([^/]+)\/commands\/(freeze|unfreeze|delete|retry)$/.exec(pathname);
|
|
20
38
|
if (match === null)
|
|
@@ -80,3 +98,20 @@ function commandRequest(body) {
|
|
|
80
98
|
? { idempotencyKey: value }
|
|
81
99
|
: invalidRequest('idempotencyKey', 'Must be at most 200 characters');
|
|
82
100
|
}
|
|
101
|
+
function ambiguityFailureResolutionRequest(body) {
|
|
102
|
+
if (!isObject(body))
|
|
103
|
+
return invalidRequest('idempotencyKey', 'A JSON object with an idempotency key is required');
|
|
104
|
+
if (Object.keys(body).some((key) => key !== 'idempotencyKey' && key !== 'message'))
|
|
105
|
+
return invalidRequest('', 'The command body contains unknown fields');
|
|
106
|
+
const idempotencyKey = body.idempotencyKey;
|
|
107
|
+
if (typeof idempotencyKey !== 'string' || idempotencyKey.trim() === '')
|
|
108
|
+
return invalidRequest('idempotencyKey', 'Must be a non-empty string');
|
|
109
|
+
if (idempotencyKey.length > 200)
|
|
110
|
+
return invalidRequest('idempotencyKey', 'Must be at most 200 characters');
|
|
111
|
+
const message = body.message;
|
|
112
|
+
if (typeof message !== 'string' || message.trim() === '')
|
|
113
|
+
return invalidRequest('message', 'Must be a non-empty string');
|
|
114
|
+
if (message.length > 2_000)
|
|
115
|
+
return invalidRequest('message', 'Must be at most 2000 characters');
|
|
116
|
+
return { idempotencyKey, message };
|
|
117
|
+
}
|
|
@@ -2,6 +2,7 @@ export const executionRoutes = [
|
|
|
2
2
|
'/api/v1/runs',
|
|
3
3
|
'/api/v1/runs/:runId',
|
|
4
4
|
'/api/v1/runs/:runId/transcript',
|
|
5
|
+
'/api/v1/runs/:runId/commands/resolve-failed',
|
|
5
6
|
'/api/v1/runners',
|
|
6
7
|
'/api/v1/runners/:runnerId/commands/pause',
|
|
7
8
|
'/api/v1/runners/:runnerId/commands/unpause',
|