@atolis-hq/wake 0.3.54 → 0.3.56
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/surface-api-work-applications.js +6 -5
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/github/application/inbound-review-signals.js +7 -6
- package/dist/src/orchestration/domain/interpreter.js +1 -1
- package/dist/src/orchestration/domain/operator-retry-policy.js +20 -3
- package/dist/src/persistence/filesystem/file-lock.js +62 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pullRequestProjection } from '../activities/index.js';
|
|
2
2
|
import { executionProjection, runsByWorkflowInstanceProjection, } from '../execution/index.js';
|
|
3
3
|
import { correlationId, EventActorKind } from '../kernel/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { OperatorRetryIneligibleError, orchestrationProjection, selectOperatorRetryTarget, workflowsByWorkItemProjection, } from '../orchestration/index.js';
|
|
5
5
|
import { workCorrelationsProjection, } from '../resources/index.js';
|
|
6
6
|
import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, } from '../surfaces/index.js';
|
|
7
7
|
import { workItemId, WorkStatus } from '../work/index.js';
|
|
@@ -70,13 +70,14 @@ export function createSurfaceWorkApplications(root, now) {
|
|
|
70
70
|
return retryIneligible('Work item is deleted');
|
|
71
71
|
if (work.state !== WorkStatus.Open)
|
|
72
72
|
return retryIneligible('Work item is not open');
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
73
|
+
const workflows = (await root.orchestration.listAll()).filter((workflow) => workflow.workItemId === id);
|
|
74
|
+
if (!workflows.some((workflow) => workflow.parentWorkflowInstanceId === undefined))
|
|
75
75
|
return retryIneligible('Work item has no primary workflow');
|
|
76
|
-
|
|
76
|
+
const target = selectOperatorRetryTarget(workflows);
|
|
77
|
+
if (target === undefined)
|
|
77
78
|
return retryIneligible('Workflow is not retry eligible');
|
|
78
79
|
try {
|
|
79
|
-
await root.orchestration.retryBlockedFailedStage(
|
|
80
|
+
await root.orchestration.retryBlockedFailedStage(target.workflowInstanceId, commandContext(command.idempotencyKey, now));
|
|
80
81
|
}
|
|
81
82
|
catch (error) {
|
|
82
83
|
if (error instanceof OperatorRetryIneligibleError)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { ActivityOutcomeKind, ReviewActorKind, ReviewDecisionKind, ReviewerAuthorizationSource, createPullRequestService, isReviewAuthorized, } from '../../../activities/index.js';
|
|
3
|
-
import { ApprovalAuthorityKind } from '../../../orchestration/index.js';
|
|
3
|
+
import { ApprovalAuthorityKind, selectOperatorRetryTarget, } from '../../../orchestration/index.js';
|
|
4
4
|
import { BuiltInResourceKind, ResourceCorrelationRole, ResourceStreamKind, resourceId, } from '../../../resources/index.js';
|
|
5
5
|
import { WorkStatus } from '../../../work/index.js';
|
|
6
6
|
import { UnknownGitHubIdentity } from '../contracts/vocabulary.js';
|
|
@@ -128,13 +128,14 @@ async function applyIssueRetrySignal(input) {
|
|
|
128
128
|
const workItemIds = (await resources.correlations(resourceIdValue))
|
|
129
129
|
.filter((correlation) => correlation.role === ResourceCorrelationRole.Primary)
|
|
130
130
|
.map((correlation) => correlation.workItemId);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
const workflows = await orchestration.listAll();
|
|
132
|
+
for (const workItemId of workItemIds) {
|
|
133
|
+
if (!(await isEligibleWorkItem(work, workItemId)))
|
|
134
134
|
continue;
|
|
135
|
-
|
|
135
|
+
const target = selectOperatorRetryTarget(workflows.filter((workflow) => workflow.workItemId === workItemId));
|
|
136
|
+
if (target === undefined)
|
|
136
137
|
continue;
|
|
137
|
-
await ignoreIneligibleOperatorRetry(() => orchestration.retryBlockedFailedStage(
|
|
138
|
+
await ignoreIneligibleOperatorRetry(() => orchestration.retryBlockedFailedStage(target.workflowInstanceId, commandContext(event)));
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
async function applyIssueApprovalSignal(input) {
|
|
@@ -10,7 +10,7 @@ import { finishRoute } from './transition.js';
|
|
|
10
10
|
export { startInstance } from './activation-policy.js';
|
|
11
11
|
export { acceptSignal, waitForSignal } from './signal-policy.js';
|
|
12
12
|
export { requestSupplementalActivity } from './supplemental-policy.js';
|
|
13
|
-
export { isChangesResumeEligible, isOperatorRetryEligible, requestChangesResume, requestOperatorRetry, } from './operator-retry-policy.js';
|
|
13
|
+
export { isChangesResumeEligible, isOperatorRetryEligible, requestChangesResume, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
|
|
14
14
|
export function acceptActivityOutcome(definition, state, input) {
|
|
15
15
|
if (!isPendingOutcome(state, input))
|
|
16
16
|
return { kind: 'ignored', reason: 'outcome is not for the pending activation' };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ActivityOutcomeKind, BuiltInActivityName } from '../../activities/index.js';
|
|
2
|
-
import { OrchestrationEventType } from '../contracts/events.js';
|
|
3
|
-
import { stageName } from '../contracts/identifiers.js';
|
|
4
|
-
import { ActivityActivationStatus, WorkflowStatus } from '../contracts/vocabulary.js';
|
|
2
|
+
import { OrchestrationEventType, WatchGateVerdictSignal } from '../contracts/events.js';
|
|
3
|
+
import { stageName, watchId } from '../contracts/identifiers.js';
|
|
4
|
+
import { ActivityActivationStatus, ApprovalAuthorityKind, WorkflowStatus, } from '../contracts/vocabulary.js';
|
|
5
5
|
import { activation, nextOrdinal, stateDraft } from './decision-events.js';
|
|
6
6
|
export function isOperatorRetryEligible(view) {
|
|
7
7
|
const pending = view.pendingActivation;
|
|
@@ -17,6 +17,23 @@ export function isOperatorRetryEligible(view) {
|
|
|
17
17
|
view.lastOutcome?.kind === ActivityOutcomeKind.Failed) ||
|
|
18
18
|
view.executionFailure?.activationId === pending.activationId);
|
|
19
19
|
}
|
|
20
|
+
export function selectOperatorRetryTarget(workflows) {
|
|
21
|
+
const primary = workflows.find((workflow) => workflow.parentWorkflowInstanceId === undefined);
|
|
22
|
+
if (primary === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (isOperatorRetryEligible(primary))
|
|
25
|
+
return primary;
|
|
26
|
+
if (primary.status !== WorkflowStatus.Waiting ||
|
|
27
|
+
primary.waitingFor?.signalKind !== WatchGateVerdictSignal)
|
|
28
|
+
return undefined;
|
|
29
|
+
const watched = new Set((primary.waitingFor.from ?? []).flatMap((authority) => authority.kind === ApprovalAuthorityKind.Watch ? [authority.watch] : []));
|
|
30
|
+
return workflows.find((workflow) => workflow.parentWorkflowInstanceId === primary.workflowInstanceId &&
|
|
31
|
+
workflow.workItemId === primary.workItemId &&
|
|
32
|
+
workflow.orchestrationGroupId === primary.orchestrationGroupId &&
|
|
33
|
+
workflow.watchId !== undefined &&
|
|
34
|
+
watched.has(watchId(workflow.watchId)) &&
|
|
35
|
+
isOperatorRetryEligible(workflow));
|
|
36
|
+
}
|
|
20
37
|
export function isChangesResumeEligible(view) {
|
|
21
38
|
const pending = view.pendingActivation;
|
|
22
39
|
return (view.status === WorkflowStatus.Blocked &&
|
|
@@ -5,10 +5,12 @@ const maximumOwnerRecords = 1024;
|
|
|
5
5
|
const compatibilityAcquiredAt = '9999-12-31T23:59:59.999Z';
|
|
6
6
|
export async function acquireFileLock(path, options) {
|
|
7
7
|
await mkdir(dirname(path), { recursive: true });
|
|
8
|
+
const identity = await processIdentity(process.pid, options);
|
|
8
9
|
const metadata = {
|
|
9
10
|
pid: process.pid,
|
|
10
11
|
acquiredAt: (options?.now ?? new Date()).toISOString(),
|
|
11
12
|
lockId: randomUUID(),
|
|
13
|
+
...(identity === null ? {} : { processIdentity: identity }),
|
|
12
14
|
};
|
|
13
15
|
return options?.staleRequiresDeadProcess
|
|
14
16
|
? acquireStrictFileLock(path, metadata, options)
|
|
@@ -226,10 +228,17 @@ async function ownerBlocksAcquisition(path, options) {
|
|
|
226
228
|
return true;
|
|
227
229
|
return ownerMetadataBlocks(owner, options);
|
|
228
230
|
}
|
|
229
|
-
function ownerMetadataBlocks(owner, options) {
|
|
231
|
+
async function ownerMetadataBlocks(owner, options) {
|
|
230
232
|
if (options?.staleAfterMs === undefined || !isStale(owner, options))
|
|
231
233
|
return true;
|
|
232
|
-
|
|
234
|
+
if (!options.staleRequiresDeadProcess || !ownerMayBeAlive(options, owner.pid))
|
|
235
|
+
return false;
|
|
236
|
+
const identity = await processIdentity(owner.pid, options);
|
|
237
|
+
if (owner.processIdentity !== undefined)
|
|
238
|
+
return identity === null || identity === owner.processIdentity;
|
|
239
|
+
const startedAt = await processStartedAt(owner.pid, options);
|
|
240
|
+
return (startedAt === null ||
|
|
241
|
+
startedAt.getTime() <= Date.parse(owner.acquiredAt) + processStartToleranceMilliseconds);
|
|
233
242
|
}
|
|
234
243
|
function ownerRecordName(metadata) {
|
|
235
244
|
return `${metadata.pid}-${Date.parse(metadata.acquiredAt)}-${metadata.lockId}.json`;
|
|
@@ -257,6 +266,57 @@ function ownerMayBeAlive(options, pid) {
|
|
|
257
266
|
return true;
|
|
258
267
|
}
|
|
259
268
|
}
|
|
269
|
+
const processStartToleranceMilliseconds = 1_000;
|
|
270
|
+
async function processIdentity(pid, options) {
|
|
271
|
+
if (options?.processIdentity !== undefined)
|
|
272
|
+
return options.processIdentity(pid);
|
|
273
|
+
if (process.platform !== 'linux')
|
|
274
|
+
return null;
|
|
275
|
+
try {
|
|
276
|
+
return `linux-start-ticks:${await linuxProcessStartTicks(pid)}`;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function processStartedAt(pid, options) {
|
|
283
|
+
if (options.processStartedAt !== undefined)
|
|
284
|
+
return options.processStartedAt(pid);
|
|
285
|
+
if (process.platform !== 'linux')
|
|
286
|
+
return null;
|
|
287
|
+
try {
|
|
288
|
+
const [targetTicks, selfTicks, procStat] = await Promise.all([
|
|
289
|
+
linuxProcessStartTicks(pid),
|
|
290
|
+
linuxProcessStartTicks(process.pid),
|
|
291
|
+
readFile('/proc/stat', 'utf8'),
|
|
292
|
+
]);
|
|
293
|
+
const bootSeconds = /^btime (\d+)$/m.exec(procStat)?.[1];
|
|
294
|
+
if (bootSeconds === undefined)
|
|
295
|
+
return null;
|
|
296
|
+
const currentProcessStartedAt = Date.now() - process.uptime() * 1_000;
|
|
297
|
+
const elapsedSinceBootMilliseconds = currentProcessStartedAt - Number(bootSeconds) * 1_000;
|
|
298
|
+
if (elapsedSinceBootMilliseconds <= 0)
|
|
299
|
+
return null;
|
|
300
|
+
const ticksPerMillisecond = selfTicks / elapsedSinceBootMilliseconds;
|
|
301
|
+
if (!Number.isFinite(ticksPerMillisecond) || ticksPerMillisecond <= 0)
|
|
302
|
+
return null;
|
|
303
|
+
return new Date(Number(bootSeconds) * 1_000 + targetTicks / ticksPerMillisecond);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function linuxProcessStartTicks(pid) {
|
|
310
|
+
const raw = await readFile(`/proc/${pid}/stat`, 'utf8');
|
|
311
|
+
const fields = raw
|
|
312
|
+
.slice(raw.lastIndexOf(')') + 2)
|
|
313
|
+
.trim()
|
|
314
|
+
.split(/\s+/);
|
|
315
|
+
const startTicks = Number(fields[19]);
|
|
316
|
+
if (!Number.isSafeInteger(startTicks) || startTicks < 0)
|
|
317
|
+
throw new Error('Invalid process start ticks');
|
|
318
|
+
return startTicks;
|
|
319
|
+
}
|
|
260
320
|
export async function withFileLock(path, operation) {
|
|
261
321
|
const lock = await acquireFileLock(path, {
|
|
262
322
|
staleAfterMs: 60_000,
|