@atolis-hq/wake 0.4.40 → 0.4.42
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 +4 -1
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/control-plane/application/activation-scheduler.d.ts +3 -0
- package/dist/src/control-plane/application/activation-scheduler.js +18 -4
- package/dist/src/control-plane/application/advance-once-dispatch.js +6 -4
- package/dist/src/control-plane/application/advance-once.d.ts +4 -1
- package/dist/src/control-plane/application/advance-once.js +4 -1
- package/dist/src/control-plane/contracts/commands.d.ts +9 -0
- package/dist/src/control-plane/infrastructure/tick-host.d.ts +2 -2
- package/dist/src/control-plane/infrastructure/tick-host.js +14 -1
- package/dist/src/integrations/github/application/inbound-watch-gate-signals.js +4 -6
- package/dist/src/integrations/github/infrastructure/client-reads.d.ts +1 -0
- package/dist/src/integrations/github/infrastructure/client-reads.js +16 -0
- package/dist/src/integrations/github/infrastructure/client.js +5 -1
- package/dist/src/orchestration/application/request-child.js +3 -4
- package/package.json +1 -1
|
@@ -127,7 +127,10 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
|
|
|
127
127
|
: createTranscriptRetention(transcriptStore, projections, config, clock)),
|
|
128
128
|
runnerIneligibility: createDurableRunnerIneligibility(journal, () => clock.now().toISOString()),
|
|
129
129
|
});
|
|
130
|
-
const advanceOnce = activationScheduler.runOnce.bind(activationScheduler)
|
|
130
|
+
const advanceOnce = Object.assign(activationScheduler.runOnce.bind(activationScheduler), {
|
|
131
|
+
maintain: activationScheduler.maintain.bind(activationScheduler),
|
|
132
|
+
dispatch: activationScheduler.dispatch.bind(activationScheduler),
|
|
133
|
+
});
|
|
131
134
|
const activationSchedulerSubscriber = createActivationSchedulerSubscriber(activationScheduler);
|
|
132
135
|
const runtime = await composeIntegrationRuntime({
|
|
133
136
|
config,
|
|
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
|
|
|
124
124
|
return `g${headHash.slice(0, 7)}`;
|
|
125
125
|
return '0.1.0-dev';
|
|
126
126
|
}
|
|
127
|
-
export const wakeVersion = "v0.4.
|
|
127
|
+
export const wakeVersion = "v0.4.42";
|
|
128
128
|
/** The registry-installable version used by packaged sandbox images. */
|
|
129
129
|
export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type Clock } from '../../kernel/index.js';
|
|
2
2
|
import type { ResourceService } from '../../resources/index.js';
|
|
3
|
+
import type { MaintenanceResult } from '../contracts/commands.js';
|
|
3
4
|
import type { AdvanceOptions, AdvanceResult } from '../contracts/views.js';
|
|
4
5
|
import type { ActivationSchedulerDependencies, ExecutionPort, OrchestrationPort } from './activation-scheduler-ports.js';
|
|
5
6
|
export interface ActivationScheduler {
|
|
6
7
|
runOnce(options: AdvanceOptions, signal?: AbortSignal): Promise<AdvanceResult>;
|
|
8
|
+
maintain?(signal?: AbortSignal): Promise<MaintenanceResult>;
|
|
9
|
+
dispatch?(options: AdvanceOptions, signal?: AbortSignal): Promise<AdvanceResult>;
|
|
7
10
|
}
|
|
8
11
|
export declare function createActivationScheduler(orchestration: OrchestrationPort, execution: ExecutionPort, resources: ResourceService, clock: Clock, dependencies: ActivationSchedulerDependencies): ActivationScheduler;
|
|
@@ -21,9 +21,7 @@ export function createActivationScheduler(orchestration, execution, resources, c
|
|
|
21
21
|
occurredAt: clock.now().toISOString(),
|
|
22
22
|
actor: { kind: EventActorKind.System, id: ControlStreamKind.Global },
|
|
23
23
|
});
|
|
24
|
-
const
|
|
25
|
-
if (options.maxProgress < 1)
|
|
26
|
-
return { kind: 'exhausted', progressCount: 0 };
|
|
24
|
+
const maintain = async () => {
|
|
27
25
|
if (await isDispatchPaused())
|
|
28
26
|
return { kind: 'paused' };
|
|
29
27
|
if (workspaceRecovery !== undefined) {
|
|
@@ -76,6 +74,13 @@ export function createActivationScheduler(orchestration, execution, resources, c
|
|
|
76
74
|
if (await isDispatchPaused())
|
|
77
75
|
return { kind: 'paused' };
|
|
78
76
|
await orchestration.reconcileChildCompletions(context('child-completion-reconciliation'));
|
|
77
|
+
if (await isDispatchPaused())
|
|
78
|
+
return { kind: 'paused' };
|
|
79
|
+
return { kind: 'ready' };
|
|
80
|
+
};
|
|
81
|
+
const dispatch = async (options) => {
|
|
82
|
+
if (options.maxProgress < 1)
|
|
83
|
+
return { kind: 'exhausted', progressCount: 0 };
|
|
79
84
|
if (await isDispatchPaused())
|
|
80
85
|
return { kind: 'paused' };
|
|
81
86
|
const rawPending = await orchestration.listPendingActivations(options.workItemId);
|
|
@@ -146,7 +151,16 @@ export function createActivationScheduler(orchestration, execution, resources, c
|
|
|
146
151
|
});
|
|
147
152
|
};
|
|
148
153
|
const serialise = dependencies.schedulerSerialiser ?? createInProcessSerialiser();
|
|
149
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
runOnce: (options, signal) => serialise(async () => {
|
|
156
|
+
if (options.maxProgress < 1)
|
|
157
|
+
return { kind: 'exhausted', progressCount: 0 };
|
|
158
|
+
const maintenance = await maintain();
|
|
159
|
+
return maintenance.kind === 'paused' ? { kind: 'paused' } : dispatch(options);
|
|
160
|
+
}, signal),
|
|
161
|
+
maintain: (signal) => serialise(maintain, signal),
|
|
162
|
+
dispatch: (options, signal) => serialise(() => dispatch(options), signal),
|
|
163
|
+
};
|
|
150
164
|
}
|
|
151
165
|
function createInProcessSerialiser() {
|
|
152
166
|
let prior = Promise.resolve();
|
|
@@ -40,6 +40,10 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
40
40
|
let stopReason;
|
|
41
41
|
while (dispatched.length < ctx.maxDispatches) {
|
|
42
42
|
const allRuns = await ctx.execution.list();
|
|
43
|
+
const activeActivationIds = new Set(allRuns.filter((run) => isActiveRunStatus(run.status)).map((run) => run.activationId));
|
|
44
|
+
const branchActiveWorkflowIds = new Set(allRuns
|
|
45
|
+
.filter((run) => isActiveRunStatus(run.status) && run.workspace?.mode === WorkspaceMode.Branch)
|
|
46
|
+
.map((run) => run.workflowInstanceId));
|
|
43
47
|
if (allRuns.filter((run) => isActiveRunStatus(run.status)).length >= ctx.maxConcurrentRuns) {
|
|
44
48
|
stopReason = { kind: 'no-work' };
|
|
45
49
|
break;
|
|
@@ -49,10 +53,8 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
49
53
|
activationId: item.activation.activationId,
|
|
50
54
|
requestedPosition,
|
|
51
55
|
hasActiveRun: dispatchedIds.has(item.activation.activationId) ||
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
run.workflowInstanceId === item.workflow.workflowInstanceId &&
|
|
55
|
-
run.workspace?.mode === WorkspaceMode.Branch),
|
|
56
|
+
activeActivationIds.has(item.activation.activationId) ||
|
|
57
|
+
branchActiveWorkflowIds.has(item.workflow.workflowInstanceId),
|
|
56
58
|
cancelled: false,
|
|
57
59
|
}))))[0];
|
|
58
60
|
const selected = selectedCandidate === undefined
|
|
@@ -2,4 +2,7 @@ import type { Clock } from '../../kernel/index.js';
|
|
|
2
2
|
import type { ResourceService } from '../../resources/index.js';
|
|
3
3
|
import type { ActivationSchedulerDependencies, ExecutionPort, OrchestrationPort } from './activation-scheduler-ports.js';
|
|
4
4
|
/** Compatibility facade for callers that still use the former Advancement name. */
|
|
5
|
-
export declare function createAdvanceOnce(orchestration: OrchestrationPort, execution: ExecutionPort, resources: ResourceService, clock: Clock, dependencies: ActivationSchedulerDependencies): (
|
|
5
|
+
export declare function createAdvanceOnce(orchestration: OrchestrationPort, execution: ExecutionPort, resources: ResourceService, clock: Clock, dependencies: ActivationSchedulerDependencies): ((options: import("../index.js").AdvanceOptions, signal?: AbortSignal) => Promise<import("../index.js").AdvanceResult>) & {
|
|
6
|
+
maintain: (signal?: AbortSignal) => Promise<import("../contracts/commands.js").MaintenanceResult>;
|
|
7
|
+
dispatch: (options: import("../index.js").AdvanceOptions, signal?: AbortSignal) => Promise<import("../index.js").AdvanceResult>;
|
|
8
|
+
};
|
|
@@ -2,5 +2,8 @@ import { createActivationScheduler } from './activation-scheduler.js';
|
|
|
2
2
|
/** Compatibility facade for callers that still use the former Advancement name. */
|
|
3
3
|
export function createAdvanceOnce(orchestration, execution, resources, clock, dependencies) {
|
|
4
4
|
const scheduler = createActivationScheduler(orchestration, execution, resources, clock, dependencies);
|
|
5
|
-
return (
|
|
5
|
+
return Object.assign(scheduler.runOnce.bind(scheduler), {
|
|
6
|
+
maintain: scheduler.maintain.bind(scheduler),
|
|
7
|
+
dispatch: scheduler.dispatch.bind(scheduler),
|
|
8
|
+
});
|
|
6
9
|
}
|
|
@@ -18,3 +18,12 @@ export interface HostResult {
|
|
|
18
18
|
readonly stoppedBecause: 'idle' | typeof HostStopReason.Waiting | typeof HostStopReason.Blocked | 'budget' | 'paused' | typeof HostStopReason.Shutdown;
|
|
19
19
|
}
|
|
20
20
|
export type AdvanceOnce = (options: AdvanceOptions, signal?: AbortSignal) => Promise<AdvanceResult>;
|
|
21
|
+
/** Optional scheduler phases used by hosts that can make several dispatch attempts in one pass. */
|
|
22
|
+
export interface AdvancePhases {
|
|
23
|
+
maintain(signal?: AbortSignal): Promise<MaintenanceResult>;
|
|
24
|
+
dispatch(options: AdvanceOptions, signal?: AbortSignal): Promise<AdvanceResult>;
|
|
25
|
+
}
|
|
26
|
+
export type MaintenanceResult = {
|
|
27
|
+
readonly kind: 'ready' | 'paused';
|
|
28
|
+
};
|
|
29
|
+
export type PhasedAdvanceOnce = AdvanceOnce & Partial<AdvancePhases>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type HostBudget, type HostResult, type PhasedAdvanceOnce } from '../contracts/commands.js';
|
|
2
2
|
export declare class TickHost {
|
|
3
3
|
private readonly advance;
|
|
4
|
-
constructor(advance:
|
|
4
|
+
constructor(advance: PhasedAdvanceOnce);
|
|
5
5
|
run(budget: HostBudget, signal?: AbortSignal): Promise<HostResult>;
|
|
6
6
|
}
|
|
@@ -9,10 +9,14 @@ export class TickHost {
|
|
|
9
9
|
const started = Date.now();
|
|
10
10
|
let advances = 0;
|
|
11
11
|
let runs = 0;
|
|
12
|
+
const preparation = await prepareDispatch(this.advance, budget, started, signal);
|
|
13
|
+
if (preparation !== undefined)
|
|
14
|
+
return preparation;
|
|
15
|
+
const dispatch = this.advance.dispatch;
|
|
12
16
|
while (advances < budget.maxAdvances && runs < budget.maxRuns) {
|
|
13
17
|
if (Date.now() - started >= budget.maxDurationMs)
|
|
14
18
|
return { advances, runs, stoppedBecause: HostStopReason.Budget };
|
|
15
|
-
const result = await this.advance({ maxProgress: 1 }, signal);
|
|
19
|
+
const result = await (dispatch ?? this.advance)({ maxProgress: 1 }, signal);
|
|
16
20
|
if (result.kind === 'progressed') {
|
|
17
21
|
advances += 1;
|
|
18
22
|
runs += result.dispatched.length;
|
|
@@ -33,3 +37,12 @@ export class TickHost {
|
|
|
33
37
|
return { advances, runs, stoppedBecause: HostStopReason.Budget };
|
|
34
38
|
}
|
|
35
39
|
}
|
|
40
|
+
async function prepareDispatch(advance, budget, started, signal) {
|
|
41
|
+
if (advance.dispatch === undefined || advance.maintain === undefined || budget.maxAdvances < 1)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (Date.now() - started >= budget.maxDurationMs)
|
|
44
|
+
return { advances: 0, runs: 0, stoppedBecause: HostStopReason.Budget };
|
|
45
|
+
return (await advance.maintain(signal)).kind === 'paused'
|
|
46
|
+
? { advances: 0, runs: 0, stoppedBecause: HostStopReason.Paused }
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
@@ -35,9 +35,7 @@ function extractMarker(body) {
|
|
|
35
35
|
function translateOutcome(outcome) {
|
|
36
36
|
if (outcome === 'DONE')
|
|
37
37
|
return ActivityOutcomeKind.Done;
|
|
38
|
-
|
|
39
|
-
return ActivityOutcomeKind.Rejected;
|
|
40
|
-
return null;
|
|
38
|
+
return ActivityOutcomeKind.Rejected;
|
|
41
39
|
}
|
|
42
40
|
export async function applyWatchGateVerdictSignal(input) {
|
|
43
41
|
const { event, runs, orchestration } = input;
|
|
@@ -47,8 +45,6 @@ export async function applyWatchGateVerdictSignal(input) {
|
|
|
47
45
|
if (marker === null)
|
|
48
46
|
return;
|
|
49
47
|
const outcome = translateOutcome(marker.outcome);
|
|
50
|
-
if (outcome === null)
|
|
51
|
-
return;
|
|
52
48
|
const verdict = await verifyWatchGateVerdict({ marker, outcome, runs, orchestration });
|
|
53
49
|
if (verdict === null)
|
|
54
50
|
return;
|
|
@@ -66,7 +62,9 @@ async function verifyWatchGateVerdict(input) {
|
|
|
66
62
|
const run = (await runs.load(runId(marker.runId))).view;
|
|
67
63
|
if (run === null)
|
|
68
64
|
return null;
|
|
69
|
-
if (run.status !== RunStatus.Succeeded ||
|
|
65
|
+
if (run.status !== RunStatus.Succeeded ||
|
|
66
|
+
run.outcome === undefined ||
|
|
67
|
+
translateOutcome(run.outcome.kind.toUpperCase()) !== outcome)
|
|
70
68
|
return null;
|
|
71
69
|
const workflows = await orchestration.listAll();
|
|
72
70
|
const child = workflows.find((workflow) => workflow.workflowInstanceId === run.workflowInstanceId);
|
|
@@ -3,6 +3,7 @@ import { PullRequestState } from '../../../activities/index.js';
|
|
|
3
3
|
import type { GitHubIssueQueryFilters } from '../contracts/issue-query.js';
|
|
4
4
|
import type { GitHubIssueCommentPayload, GitHubIssuePayload } from '../contracts/payloads.js';
|
|
5
5
|
import type { createEtagCache } from './etag-cache.js';
|
|
6
|
+
export declare function findIssueCommentByMarker(octokit: Octokit, owner: string, repo: string, issueNumber: number, marker: string): Promise<string | null>;
|
|
6
7
|
export declare function listIssues(octokit: Octokit, cache: ReturnType<typeof createEtagCache>, owner: string, repo: string, maxResults: number, options?: {
|
|
7
8
|
readonly since?: string;
|
|
8
9
|
readonly filters?: GitHubIssueQueryFilters;
|
|
@@ -2,6 +2,22 @@
|
|
|
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 async function findIssueCommentByMarker(octokit, owner, repo, issueNumber, marker) {
|
|
6
|
+
for await (const page of octokit.paginate.iterator(octokit.rest.issues.listComments, {
|
|
7
|
+
owner,
|
|
8
|
+
repo,
|
|
9
|
+
issue_number: issueNumber,
|
|
10
|
+
per_page: 100,
|
|
11
|
+
})) {
|
|
12
|
+
const comment = page.data.find((value) => value.body?.includes(marker));
|
|
13
|
+
if (comment !== undefined) {
|
|
14
|
+
if (!Number.isSafeInteger(comment.id) || comment.id <= 0)
|
|
15
|
+
throw new Error('GitHub comment marker lookup returned an invalid comment id');
|
|
16
|
+
return String(comment.id);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
5
21
|
export function listIssues(octokit, cache, owner, repo, maxResults, options = {}) {
|
|
6
22
|
const { since, filters = {} } = options;
|
|
7
23
|
return fetchPaginatedWithEtag({
|
|
@@ -2,8 +2,9 @@ import { Octokit } from '@octokit/rest';
|
|
|
2
2
|
import { MergeMethod, ProviderPermission, PullRequestState } from '../../../activities/index.js';
|
|
3
3
|
import { GitHubOutboundAction } from '../contracts/vocabulary.js';
|
|
4
4
|
import { createBoundedGitHubFetch } from './bounded-fetch.js';
|
|
5
|
-
import { branch, getCombinedStatusForRef, getIssueLabels, getPullRequest, listCheckRunsForRef, listIssueComments, listIssues, listPullRequestFiles, listPullRequests, listReviewComments, listReviews, } from './client-reads.js';
|
|
5
|
+
import { branch, findIssueCommentByMarker, getCombinedStatusForRef, getIssueLabels, getPullRequest, listCheckRunsForRef, listIssueComments, listIssues, listPullRequestFiles, listPullRequests, listReviewComments, listReviews, } from './client-reads.js';
|
|
6
6
|
import { createEtagCache } from './etag-cache.js';
|
|
7
|
+
/* eslint-disable max-lines */
|
|
7
8
|
// Octokit's request-log plugin reports every non-2xx response through this
|
|
8
9
|
// callback, including the expected 304 responses produced by conditional ETag
|
|
9
10
|
// reads. Keep those cache hits quiet, but retain a small, safe operator signal
|
|
@@ -169,6 +170,9 @@ async function deliver(octokit, command) {
|
|
|
169
170
|
const issueNumber = command.issue_number ?? command.pull_number;
|
|
170
171
|
if (issueNumber === undefined)
|
|
171
172
|
throw new Error('GitHub comment requires an issue or pull request');
|
|
173
|
+
const existingCommentId = await findIssueCommentByMarker(octokit, command.owner, command.repo, issueNumber, marker);
|
|
174
|
+
if (existingCommentId !== null)
|
|
175
|
+
return existingCommentId;
|
|
172
176
|
const response = await octokit.rest.issues.createComment({
|
|
173
177
|
owner: command.owner,
|
|
174
178
|
repo: command.repo,
|
|
@@ -177,8 +177,7 @@ function watchIdsFor(wait) {
|
|
|
177
177
|
function watchGateVerdictOutcome(parent, child) {
|
|
178
178
|
if (parent.waitingFor?.signalKind !== WatchGateVerdictSignal)
|
|
179
179
|
return undefined;
|
|
180
|
-
return child.lastOutcome?.kind === ActivityOutcomeKind.Done
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: undefined;
|
|
180
|
+
return child.lastOutcome?.kind === ActivityOutcomeKind.Done
|
|
181
|
+
? ActivityOutcomeKind.Done
|
|
182
|
+
: ActivityOutcomeKind.Rejected;
|
|
184
183
|
}
|