@atolis-hq/wake 0.3.84 → 0.3.86
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/board-projection.js +42 -25
- package/dist/src/bootstrap/self-update-failure-log.js +8 -0
- package/dist/src/bootstrap/surface-api-applications.js +13 -3
- package/dist/src/bootstrap/surface-cli-applications.js +18 -2
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/integrations/github/application/intake-policy.js +23 -0
- package/dist/src/integrations/github/contracts/issue-query.js +1 -0
- package/dist/src/integrations/github/index.js +1 -0
- package/dist/src/integrations/github/infrastructure/client-reads.js +5 -2
- package/dist/src/integrations/github/infrastructure/client.js +4 -1
- package/dist/src/integrations/github/infrastructure/source.js +29 -17
- package/dist/src/persistence/filesystem/file-event-journal.js +20 -1
- package/dist/src/surfaces/api/contracts/transport-values.js +8 -0
- package/dist/src/surfaces/api/presenters/orchestration.js +4 -1
- package/dist/src/surfaces/cli/commands/sandbox-entrypoint.js +14 -0
- package/dist/src/surfaces/web-assets/assets/index-BO92xZ4y.js +1156 -0
- package/dist/src/surfaces/web-assets/index.html +1 -1
- package/package.json +1 -1
- package/dist/src/surfaces/web-assets/assets/index-DPstcTxm.js +0 -1090
|
@@ -93,33 +93,38 @@ function projectWork(view, event, occurredAt) {
|
|
|
93
93
|
return view;
|
|
94
94
|
}
|
|
95
95
|
function projectWorkflow(view, event, occurredAt) {
|
|
96
|
-
if (event.eventType === OrchestrationEventType.InstanceStarted)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
};
|
|
96
|
+
if (event.eventType === OrchestrationEventType.InstanceStarted)
|
|
97
|
+
return projectWorkflowStarted(view, event, occurredAt);
|
|
98
|
+
return projectWorkflowUpdate(view, event, occurredAt);
|
|
99
|
+
}
|
|
100
|
+
function projectWorkflowStarted(view, event, occurredAt) {
|
|
101
|
+
const workId = event.payload.workItemId;
|
|
102
|
+
const card = view.cards[workId];
|
|
103
|
+
if (card === undefined)
|
|
104
|
+
return view;
|
|
105
|
+
const workflows = { ...view.workflows, [event.stream.id]: workId };
|
|
106
|
+
if ('parentWorkflowInstanceId' in event.payload)
|
|
108
107
|
return {
|
|
109
108
|
...view,
|
|
110
|
-
cards: {
|
|
111
|
-
...view.cards,
|
|
112
|
-
[workId]: {
|
|
113
|
-
...card,
|
|
114
|
-
workflowName: event.payload.workflowName,
|
|
115
|
-
stage: event.payload.entry,
|
|
116
|
-
dwellSince: occurredAt,
|
|
117
|
-
condition: BoardCondition.Ready,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
109
|
workflows,
|
|
110
|
+
children: { ...view.children, [event.stream.id]: event.payload.entry },
|
|
121
111
|
};
|
|
122
|
-
|
|
112
|
+
return {
|
|
113
|
+
...view,
|
|
114
|
+
cards: {
|
|
115
|
+
...view.cards,
|
|
116
|
+
[workId]: {
|
|
117
|
+
...card,
|
|
118
|
+
workflowName: event.payload.workflowName,
|
|
119
|
+
stage: event.payload.entry,
|
|
120
|
+
dwellSince: occurredAt,
|
|
121
|
+
condition: BoardCondition.Ready,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
workflows,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function projectWorkflowUpdate(view, event, occurredAt) {
|
|
123
128
|
if (view.children?.[event.stream.id] !== undefined)
|
|
124
129
|
return view;
|
|
125
130
|
const located = lookupWorkflowCard(view, event.stream.id);
|
|
@@ -142,7 +147,7 @@ function projectWorkflow(view, event, occurredAt) {
|
|
|
142
147
|
cards: {
|
|
143
148
|
...view.cards,
|
|
144
149
|
[workId]: {
|
|
145
|
-
...card,
|
|
150
|
+
...withoutBlockReason(card),
|
|
146
151
|
stage: event.payload.stage,
|
|
147
152
|
dwellSince: occurredAt,
|
|
148
153
|
condition: BoardCondition.Ready,
|
|
@@ -158,7 +163,15 @@ function projectWorkflow(view, event, occurredAt) {
|
|
|
158
163
|
const status = orchestrationStatusTransitions[event.eventType];
|
|
159
164
|
if (status === undefined)
|
|
160
165
|
return view;
|
|
161
|
-
const withCondition = {
|
|
166
|
+
const withCondition = {
|
|
167
|
+
...(event.eventType === OrchestrationEventType.InstanceBlocked
|
|
168
|
+
? card
|
|
169
|
+
: withoutBlockReason(card)),
|
|
170
|
+
condition: boardConditionForStatus(status),
|
|
171
|
+
...(event.eventType === OrchestrationEventType.InstanceBlocked
|
|
172
|
+
? { blockReason: event.payload.reason }
|
|
173
|
+
: {}),
|
|
174
|
+
};
|
|
162
175
|
if (event.eventType === OrchestrationEventType.SignalWaitStarted)
|
|
163
176
|
return {
|
|
164
177
|
...view,
|
|
@@ -196,6 +209,10 @@ function lookupWorkflowCard(view, streamId) {
|
|
|
196
209
|
function awaitingApprovalField(signalKind) {
|
|
197
210
|
return isApprovalAwaitingSignalKind(signalKind) ? { awaitingApproval: true } : {};
|
|
198
211
|
}
|
|
212
|
+
function withoutBlockReason(card) {
|
|
213
|
+
const { blockReason: _blockReason, ...withoutReason } = card;
|
|
214
|
+
return withoutReason;
|
|
215
|
+
}
|
|
199
216
|
const runTerminalEventTypes = new Set([
|
|
200
217
|
ExecutionEventType.RunSucceeded,
|
|
201
218
|
ExecutionEventType.RunFailed,
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
// recordUpdateFailure (self-update-application.ts) records every failed
|
|
4
|
+
// attempt through this same log regardless of stage — including one that
|
|
5
|
+
// never reached rollout.deploy() at all, e.g. a quiesce timeout waiting on
|
|
6
|
+
// active Runs. "rolled back" would misreport those as a completed deploy
|
|
7
|
+
// that had to be undone, when nothing was ever deployed.
|
|
8
|
+
export function describeSelfUpdateFailure(failure) {
|
|
9
|
+
return `update to ${failure.tag} failed at ${failure.occurredAt}: ${failure.message}`;
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Persists the most recent self-update rollback so it can surface on the
|
|
5
13
|
* operator health screen instead of relying on a third-party notification —
|
|
@@ -3,7 +3,7 @@ import { ApiCommandStatus, fromWorkItemKey, presentBoardCard, presentResource, p
|
|
|
3
3
|
import { analyticsProjection } from './analytics-projection.js';
|
|
4
4
|
import { boardConditionCounts, boardProjection, } from './board-projection.js';
|
|
5
5
|
import { primaryExternalRef } from './external-ref.js';
|
|
6
|
-
import { createSelfUpdateFailureLog } from './self-update-failure-log.js';
|
|
6
|
+
import { createSelfUpdateFailureLog, describeSelfUpdateFailure, } from './self-update-failure-log.js';
|
|
7
7
|
import { createExecutionApplications } from './surface-api-execution-applications.js';
|
|
8
8
|
import { projectionMeta, sampledMeta } from './surface-api-metadata.js';
|
|
9
9
|
import { projectionPage } from './surface-api-projection-pages.js';
|
|
@@ -200,7 +200,7 @@ function createSystemApplications(root, now) {
|
|
|
200
200
|
: {
|
|
201
201
|
name: 'self-update',
|
|
202
202
|
status: 'degraded',
|
|
203
|
-
detail:
|
|
203
|
+
detail: describeSelfUpdateFailure(selfUpdateFailure),
|
|
204
204
|
},
|
|
205
205
|
];
|
|
206
206
|
const adapters = root.providers.flatMap((instance) => (instance.health?.() ?? []).map((check) => ({
|
|
@@ -309,15 +309,25 @@ async function performTick(root, now, command, sequence) {
|
|
|
309
309
|
result: await readControlPlaneStatus(root, now),
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
|
-
async function readControlPlaneStatus(root, now) {
|
|
312
|
+
export async function readControlPlaneStatus(root, now) {
|
|
313
313
|
const stored = await root.projections.read(ControlStreamKind.Global, 'global');
|
|
314
314
|
const meta = await projectionMeta(root.journal, stored === null ? [] : [stored], now());
|
|
315
|
+
const lease = await root.maintenance.read();
|
|
315
316
|
return {
|
|
316
317
|
data: {
|
|
317
318
|
paused: stored?.value.pausedUntil !== null && stored?.value.pausedUntil !== undefined,
|
|
318
319
|
...(stored?.value.pausedUntil == null ? {} : { pausedUntil: stored.value.pausedUntil }),
|
|
319
320
|
...(stored?.value.reason === undefined ? {} : { reason: stored.value.reason }),
|
|
320
321
|
updatedAt: meta.asOf,
|
|
322
|
+
...(lease === null
|
|
323
|
+
? {}
|
|
324
|
+
: {
|
|
325
|
+
maintenanceLease: {
|
|
326
|
+
phase: lease.phase,
|
|
327
|
+
startedAt: lease.startedAt,
|
|
328
|
+
...(lease.failure === undefined ? {} : { failure: lease.failure }),
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
321
331
|
},
|
|
322
332
|
meta,
|
|
323
333
|
};
|
|
@@ -9,7 +9,7 @@ import { IntakeHost, ResidentHost, TickHost } from '../control-plane/index.js';
|
|
|
9
9
|
import { ExecutionCancellationReason, ExecutionFailureCode, RunStatus, loadPromptTemplate, } from '../execution/index.js';
|
|
10
10
|
import { EventActorKind, correlationId } from '../kernel/index.js';
|
|
11
11
|
import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
|
|
12
|
-
import { DockerProcessError, createApiDispatcher, createApiHttpServer, createLoggedDockerCli, createPackagedAssetSource, createProcessLogSink, createSandboxDockerPort, drainProcessOutput, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, } from '../surfaces/index.js';
|
|
12
|
+
import { DockerProcessError, createApiDispatcher, createApiHttpServer, createLoggedDockerCli, createPackagedAssetSource, createProcessLogSink, createSandboxDockerPort, drainProcessOutput, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, waitForever, } from '../surfaces/index.js';
|
|
13
13
|
import { WorkStreamKind, workItemId } from '../work/index.js';
|
|
14
14
|
import { loadConfig } from './config/load-config.js';
|
|
15
15
|
import { runtimeProjectionDefinitions } from './projection-runtime.js';
|
|
@@ -456,6 +456,7 @@ async function doctorDiagnostics(root) {
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
await checkProviders(root, failures, notices);
|
|
459
|
+
await checkMaintenanceLease(root, failures);
|
|
459
460
|
notices.push(...(await dockerSandboxHealthNotices(root)));
|
|
460
461
|
for (const [name, path] of Object.entries({
|
|
461
462
|
events: root.paths.eventsRoot,
|
|
@@ -490,6 +491,21 @@ function referencedPromptTemplateNames(workflows) {
|
|
|
490
491
|
}
|
|
491
492
|
return [...names];
|
|
492
493
|
}
|
|
494
|
+
// A retained maintenance lease pauses every resident loop (intake and
|
|
495
|
+
// dispatch) regardless of phase -- including Failed, since it's the
|
|
496
|
+
// operator's decision whether a failed attempt is safe to retry or clear.
|
|
497
|
+
// Without this check the pause is invisible: the process ticks normally and
|
|
498
|
+
// logs nothing, so a stuck lease from an update that couldn't quiesce active
|
|
499
|
+
// Runs looks identical to a healthy idle system.
|
|
500
|
+
async function checkMaintenanceLease(root, failures) {
|
|
501
|
+
const lease = await root.maintenance.read();
|
|
502
|
+
if (lease === null)
|
|
503
|
+
return;
|
|
504
|
+
const detail = lease.failure === undefined ? '' : ` (${lease.failure})`;
|
|
505
|
+
failures.push(`update maintenance lease is held in phase "${lease.phase}" since ${lease.startedAt}${detail} -- ` +
|
|
506
|
+
'every resident loop stays paused until it is cleared or resumes; run `wake self-update` ' +
|
|
507
|
+
'to retry, or clear the lease manually if the attempt is abandoned');
|
|
508
|
+
}
|
|
493
509
|
async function checkProviders(root, failures, notices) {
|
|
494
510
|
for (const provider of root.providers) {
|
|
495
511
|
if (provider.adapter.trim().length === 0) {
|
|
@@ -661,7 +677,7 @@ function createSandboxEntrypointDependencies(root) {
|
|
|
661
677
|
waitForExit: async (pid) => children.get(pid) ?? 1,
|
|
662
678
|
writeFile: (path, content) => writeFileContent(path, content, 'utf8'),
|
|
663
679
|
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
664
|
-
waitForever
|
|
680
|
+
waitForever,
|
|
665
681
|
log: (message) => process.stdout.write(`${message}\n`),
|
|
666
682
|
};
|
|
667
683
|
}
|
|
@@ -14,6 +14,29 @@ export function gitHubIntakeRules(configured) {
|
|
|
14
14
|
tags: rule.tags,
|
|
15
15
|
}));
|
|
16
16
|
}
|
|
17
|
+
// Rules are OR-composed. A native facet filter is safe only when every rule
|
|
18
|
+
// requires the same single value; otherwise it could hide an item another rule admits.
|
|
19
|
+
export function gitHubIssueQueryFilters(configured) {
|
|
20
|
+
const assignee = sharedRequiredValue(configured, (rule) => rule.where.requiredAssignees);
|
|
21
|
+
const labels = sharedRequiredValue(configured, (rule) => rule.where.labels);
|
|
22
|
+
return {
|
|
23
|
+
...(assignee === undefined ? {} : { assignee }),
|
|
24
|
+
...(labels === undefined ? {} : { labels }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function sharedRequiredValue(configured, values) {
|
|
28
|
+
if (configured.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const candidate = [...new Set(values(configured[0]))];
|
|
31
|
+
if (candidate.length !== 1)
|
|
32
|
+
return undefined;
|
|
33
|
+
return configured.every((rule) => {
|
|
34
|
+
const required = new Set(values(rule));
|
|
35
|
+
return required.size === 1 && required.has(candidate[0]);
|
|
36
|
+
})
|
|
37
|
+
? candidate[0]
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
17
40
|
export function gitHubIntakeFacts(payload) {
|
|
18
41
|
return {
|
|
19
42
|
[GitHubIntakeFacet.Kind]: [payload.kind],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -19,6 +19,7 @@ export * from './application/review-command-translator.js';
|
|
|
19
19
|
export * from './application/wake-labels.js';
|
|
20
20
|
export * from './contracts/config.js';
|
|
21
21
|
export * from './contracts/events.js';
|
|
22
|
+
export * from './contracts/issue-query.js';
|
|
22
23
|
export * from './contracts/payloads.js';
|
|
23
24
|
export * from './contracts/vocabulary.js';
|
|
24
25
|
export * from './infrastructure/client.js';
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import { PullRequestState } from '../../../activities/index.js';
|
|
3
3
|
import { GitHubListState } from '../contracts/vocabulary.js';
|
|
4
4
|
import { fetchPaginatedWithEtag, fetchWithEtag } from './etag-cache.js';
|
|
5
|
-
export function listIssues(octokit, cache, owner, repo, maxResults,
|
|
5
|
+
export function listIssues(octokit, cache, owner, repo, maxResults, options = {}) {
|
|
6
|
+
const { since, filters = {} } = options;
|
|
6
7
|
return fetchPaginatedWithEtag({
|
|
7
8
|
cache,
|
|
8
|
-
key: `issues:${owner
|
|
9
|
+
key: `issues:${JSON.stringify([owner, repo, since ?? null, filters.assignee ?? null, filters.labels ?? null])}`,
|
|
9
10
|
maxResults,
|
|
10
11
|
pages: (headers) => octokit.paginate.iterator(octokit.rest.issues.listForRepo, {
|
|
11
12
|
owner,
|
|
@@ -15,6 +16,8 @@ export function listIssues(octokit, cache, owner, repo, maxResults, since) {
|
|
|
15
16
|
direction: 'desc',
|
|
16
17
|
per_page: Math.min(maxResults, 100),
|
|
17
18
|
...(since === undefined ? {} : { since }),
|
|
19
|
+
...(filters.assignee === undefined ? {} : { assignee: filters.assignee }),
|
|
20
|
+
...(filters.labels === undefined ? {} : { labels: filters.labels }),
|
|
18
21
|
...(headers === undefined ? {} : { headers }),
|
|
19
22
|
}),
|
|
20
23
|
}).then((items) => items.map(normalizeIssue));
|
|
@@ -66,7 +66,10 @@ export function createGitHubClient(token) {
|
|
|
66
66
|
}
|
|
67
67
|
function createGitHubReadClient(octokit, cache) {
|
|
68
68
|
return {
|
|
69
|
-
listIssues: (owner, repo, maxResults, since) => listIssues(octokit, cache, owner, repo, maxResults,
|
|
69
|
+
listIssues: (owner, repo, maxResults, since, filters) => listIssues(octokit, cache, owner, repo, maxResults, {
|
|
70
|
+
...(since === undefined ? {} : { since }),
|
|
71
|
+
...(filters === undefined ? {} : { filters }),
|
|
72
|
+
}),
|
|
70
73
|
listPullRequests: (owner, repo, maxResults) => listPullRequests(octokit, cache, owner, repo, maxResults),
|
|
71
74
|
listIssueComments: (owner, repo, issueNumber, pageSize, since, maxResults) => listIssueComments(octokit, cache, owner, repo, issueNumber, {
|
|
72
75
|
pageSize,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { gitHubIssueQueryFilters } from '../application/intake-policy.js';
|
|
1
2
|
import { GitHubEventType } from '../contracts/events.js';
|
|
2
3
|
import { createGitHubAdapterHealthRegistry, } from './adapter-health-registry.js';
|
|
3
4
|
import { issueCommentEventsFor, reviewCommentEventsFor, reviewEventsFor, } from './comment-source.js';
|
|
@@ -65,7 +66,7 @@ export function createGitHubSource(config, client, adapter, requests = createGit
|
|
|
65
66
|
function limitGitHubSourceClient(client, requests) {
|
|
66
67
|
return {
|
|
67
68
|
...client,
|
|
68
|
-
listIssues: (owner, repo, maxResults, since) => requests.run(() => client.listIssues(owner, repo, maxResults, since)),
|
|
69
|
+
listIssues: (owner, repo, maxResults, since, filters) => requests.run(() => client.listIssues(owner, repo, maxResults, since, filters)),
|
|
69
70
|
listPullRequests: (owner, repo, maxResults) => requests.run(() => client.listPullRequests(owner, repo, maxResults)),
|
|
70
71
|
listCheckRunsForRef: (owner, repo, ref, maxResults) => requests.run(() => client.listCheckRunsForRef(owner, repo, ref, maxResults)),
|
|
71
72
|
getCombinedStatusForRef: (owner, repo, ref, maxResults) => requests.run(() => client.getCombinedStatusForRef(owner, repo, ref, maxResults)),
|
|
@@ -96,22 +97,14 @@ async function pollRepository(input) {
|
|
|
96
97
|
repository: `${owner}/${repo}`,
|
|
97
98
|
};
|
|
98
99
|
const since = overlapSince(input.watermark, config.polling.lookbackMs);
|
|
99
|
-
const [issuesResult, pullRequestsResult] = await
|
|
100
|
-
client
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
health.recordFailure(context.repository, 'poll', issuesResult.reason);
|
|
108
|
-
}
|
|
109
|
-
if (isFulfilled(pullRequestsResult))
|
|
110
|
-
health.recordSuccess(context.repository, 'poll');
|
|
111
|
-
else {
|
|
112
|
-
reportPartialPollFailure(context.repository, 'pull requests');
|
|
113
|
-
health.recordFailure(context.repository, 'poll', pullRequestsResult.reason);
|
|
114
|
-
}
|
|
100
|
+
const [issuesResult, pullRequestsResult] = await fetchRepositoryItems({
|
|
101
|
+
client,
|
|
102
|
+
config,
|
|
103
|
+
owner,
|
|
104
|
+
repo,
|
|
105
|
+
since,
|
|
106
|
+
});
|
|
107
|
+
reportRepositoryReadResults(context, health, issuesResult, pullRequestsResult);
|
|
115
108
|
const issues = isFulfilled(issuesResult) ? issuesResult.value : [];
|
|
116
109
|
const pullRequestPayloads = isFulfilled(pullRequestsResult)
|
|
117
110
|
? pullRequestsResult.value.filter((pullRequest) => since === undefined || pullRequest.updated_at >= since)
|
|
@@ -150,6 +143,25 @@ async function pollRepository(input) {
|
|
|
150
143
|
],
|
|
151
144
|
};
|
|
152
145
|
}
|
|
146
|
+
function fetchRepositoryItems(input) {
|
|
147
|
+
const { client, config, owner, repo, since } = input;
|
|
148
|
+
return Promise.allSettled([
|
|
149
|
+
client.listIssues(owner, repo, config.polling.maxPerRepo, since, gitHubIssueQueryFilters(config.intake)),
|
|
150
|
+
client.listPullRequests(owner, repo, config.polling.maxPerRepo),
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
function reportRepositoryReadResults(context, health, issuesResult, pullRequestsResult) {
|
|
154
|
+
reportRepositoryReadResult(context.repository, health, 'issues', issuesResult);
|
|
155
|
+
reportRepositoryReadResult(context.repository, health, 'pull requests', pullRequestsResult);
|
|
156
|
+
}
|
|
157
|
+
function reportRepositoryReadResult(repository, health, read, result) {
|
|
158
|
+
if (isFulfilled(result)) {
|
|
159
|
+
health.recordSuccess(repository, 'poll');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
reportPartialPollFailure(repository, read);
|
|
163
|
+
health.recordFailure(repository, 'poll', result.reason);
|
|
164
|
+
}
|
|
153
165
|
function isFulfilled(result) {
|
|
154
166
|
return 'value' in result;
|
|
155
167
|
}
|
|
@@ -15,6 +15,15 @@ export class FileEventJournal {
|
|
|
15
15
|
return this.changeSignalSource;
|
|
16
16
|
}
|
|
17
17
|
cached;
|
|
18
|
+
// Every read (readStream/readAll/readLatest/latestGlobalPosition) reaches
|
|
19
|
+
// scan(), and the resident host's tick loop calls those many times a
|
|
20
|
+
// second while work is progressing. Without coalescing, calls that land
|
|
21
|
+
// concurrently each independently decide the cache is stale and each
|
|
22
|
+
// re-read and re-parse the entire on-disk journal — under I/O pressure
|
|
23
|
+
// (e.g. a concurrently running build/test process competing for disk),
|
|
24
|
+
// enough of these can overlap at once to exhaust the heap. Sharing one
|
|
25
|
+
// in-flight decode among concurrent callers makes that impossible.
|
|
26
|
+
inFlightScan;
|
|
18
27
|
async append(stream, expectedSequence, drafts) {
|
|
19
28
|
return withFileLock(join(this.root, 'locks', 'event-journal.lock'), async () => {
|
|
20
29
|
const current = await this.scan();
|
|
@@ -97,7 +106,17 @@ export class FileEventJournal {
|
|
|
97
106
|
: [...priorEntries, updatedEntry];
|
|
98
107
|
this.cached = { entries, events: [...priorEvents, ...newEnvelopes] };
|
|
99
108
|
}
|
|
100
|
-
|
|
109
|
+
scan() {
|
|
110
|
+
if (this.inFlightScan !== undefined)
|
|
111
|
+
return this.inFlightScan;
|
|
112
|
+
const run = this.scanUncoalesced().finally(() => {
|
|
113
|
+
if (this.inFlightScan === run)
|
|
114
|
+
this.inFlightScan = undefined;
|
|
115
|
+
});
|
|
116
|
+
this.inFlightScan = run;
|
|
117
|
+
return run;
|
|
118
|
+
}
|
|
119
|
+
async scanUncoalesced() {
|
|
101
120
|
const directory = join(this.root, 'events');
|
|
102
121
|
let files;
|
|
103
122
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ActivityOutcomeKind } from '../../../activities/index.js';
|
|
1
2
|
const commandStatusShape = { accepted: true, completed: true };
|
|
2
3
|
const commandStatuses = Object.keys(commandStatusShape);
|
|
3
4
|
export const AcceptedCommandStatusValue = {
|
|
@@ -8,6 +9,13 @@ const runResponseShape = { active: true };
|
|
|
8
9
|
export const RunResponseField = { Active: Object.keys(runResponseShape)[0] };
|
|
9
10
|
const resourceItemFieldShape = { adapter: true };
|
|
10
11
|
export const ResourceItemField = { Adapter: Object.keys(resourceItemFieldShape)[0] };
|
|
12
|
+
const runResolutionStatusShape = { failed: true, succeeded: true };
|
|
13
|
+
const runResolutionStatuses = Object.keys(runResolutionStatusShape);
|
|
14
|
+
export const RunResolutionStatusValue = {
|
|
15
|
+
Failed: runResolutionStatuses[0],
|
|
16
|
+
Succeeded: runResolutionStatuses[1],
|
|
17
|
+
};
|
|
18
|
+
export const ActivityOutcomeKindValue = ActivityOutcomeKind;
|
|
11
19
|
const boardConditionShape = {
|
|
12
20
|
ready: true,
|
|
13
21
|
active: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isOperatorRetryEligible, } from '../../../orchestration/index.js';
|
|
1
|
+
import { isOperatorRetryEligible, WorkflowStatus, } from '../../../orchestration/index.js';
|
|
2
2
|
import { toWorkItemKey } from '../contracts/work.js';
|
|
3
3
|
export function presentWorkflowInstance(value) {
|
|
4
4
|
return {
|
|
@@ -11,6 +11,9 @@ export function presentWorkflowInstance(value) {
|
|
|
11
11
|
: { parentWorkflowInstanceId: value.parentWorkflowInstanceId }),
|
|
12
12
|
status: value.status,
|
|
13
13
|
currentStage: value.currentStage,
|
|
14
|
+
...(value.status === WorkflowStatus.Blocked && value.blockReason !== undefined
|
|
15
|
+
? { blockReason: value.blockReason }
|
|
16
|
+
: {}),
|
|
14
17
|
...(isOperatorRetryEligible(value) ? { retryEligible: true } : {}),
|
|
15
18
|
...(value.waitingFor === undefined
|
|
16
19
|
? {}
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Never resolves, and never lets the process exit either. The supervised
|
|
3
|
+
* child is spawned detached and unref'd (see spawnDetached), so an
|
|
4
|
+
* unresolved Promise alone is not enough here — a Promise executor registers
|
|
5
|
+
* no libuv handle, and once other pending I/O quiets down Node treats the
|
|
6
|
+
* still-pending top-level await as unsettled and exits anyway. A ref'd
|
|
7
|
+
* timer is a real handle, so it keeps the process alive for as long as this
|
|
8
|
+
* Promise is meant to.
|
|
9
|
+
*/
|
|
10
|
+
export function waitForever() {
|
|
11
|
+
return new Promise(() => {
|
|
12
|
+
setInterval(() => { }, 1 << 30);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Restarts `wake start` on every exit — a first-boot crash (e.g. missing
|
|
3
17
|
* sandbox auth) must not stop the retry loop, since the operator's only path
|