@atolis-hq/wake 0.4.39 → 0.4.41

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.
Files changed (30) hide show
  1. package/dist/src/activities/agent/agent-result.d.ts +2 -0
  2. package/dist/src/activities/agent/agent-result.js +8 -0
  3. package/dist/src/bootstrap/composition-root.js +4 -1
  4. package/dist/src/bootstrap/version.js +1 -1
  5. package/dist/src/control-plane/application/activation-scheduler.d.ts +3 -0
  6. package/dist/src/control-plane/application/activation-scheduler.js +18 -4
  7. package/dist/src/control-plane/application/advance-once-dispatch.js +6 -4
  8. package/dist/src/control-plane/application/advance-once.d.ts +4 -1
  9. package/dist/src/control-plane/application/advance-once.js +4 -1
  10. package/dist/src/control-plane/contracts/commands.d.ts +9 -0
  11. package/dist/src/control-plane/infrastructure/tick-host.d.ts +2 -2
  12. package/dist/src/control-plane/infrastructure/tick-host.js +14 -1
  13. package/dist/src/integrations/github/infrastructure/comment-source.js +7 -4
  14. package/dist/src/integrations/github/infrastructure/poll-watermark.d.ts +24 -0
  15. package/dist/src/integrations/github/infrastructure/poll-watermark.js +37 -1
  16. package/dist/src/integrations/github/infrastructure/pr-evidence.d.ts +18 -0
  17. package/dist/src/integrations/github/infrastructure/pr-evidence.js +32 -0
  18. package/dist/src/integrations/github/infrastructure/pr-source.d.ts +6 -2
  19. package/dist/src/integrations/github/infrastructure/pr-source.js +27 -38
  20. package/dist/src/integrations/github/infrastructure/source.js +33 -25
  21. package/dist/src/orchestration/application/advance-workflow.d.ts +1 -0
  22. package/dist/src/orchestration/application/advance-workflow.js +7 -1
  23. package/dist/src/orchestration/application/conversation-command-reactor.js +5 -2
  24. package/dist/src/orchestration/application/orchestration-service.d.ts +1 -0
  25. package/dist/src/orchestration/application/orchestration-service.js +3 -0
  26. package/dist/src/orchestration/domain/interpreter.d.ts +1 -1
  27. package/dist/src/orchestration/domain/interpreter.js +1 -1
  28. package/dist/src/orchestration/domain/operator-retry-policy.d.ts +2 -0
  29. package/dist/src/orchestration/domain/operator-retry-policy.js +15 -9
  30. package/package.json +1 -1
@@ -39,5 +39,7 @@ export declare const agentActivityOutcomeSchema: z.ZodUnion<readonly [z.ZodObjec
39
39
  }, z.core.$strict>]>;
40
40
  }, z.core.$strict>]>;
41
41
  export type AgentActivityOutcome = z.output<typeof agentActivityOutcomeSchema>;
42
+ /** Identifies the agent sentinel that explicitly asks a human for input. */
43
+ export declare function isNeedsClarificationAgentOutcome(value: unknown): boolean;
42
44
  export declare const agentActivityOutcomeKinds: readonly ["done", "rejected", "blocked", "failed"];
43
45
  export declare function translateAgentResult(value: unknown): AgentActivityOutcome;
@@ -52,6 +52,14 @@ export const agentActivityOutcomeSchema = z.union([
52
52
  })
53
53
  .strict(),
54
54
  ]);
55
+ /** Identifies the agent sentinel that explicitly asks a human for input. */
56
+ export function isNeedsClarificationAgentOutcome(value) {
57
+ const parsed = agentActivityOutcomeSchema.safeParse(value);
58
+ return (parsed.success &&
59
+ parsed.data.kind === ActivityOutcomeKind.Blocked &&
60
+ 'status' in parsed.data.data &&
61
+ parsed.data.data.status === 'NEEDS_CLARIFICATION');
62
+ }
55
63
  export const agentActivityOutcomeKinds = [
56
64
  ActivityOutcomeKind.Done,
57
65
  ActivityOutcomeKind.Rejected,
@@ -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.39";
127
+ export const wakeVersion = "v0.4.41";
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 run = async (options) => {
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 { runOnce: (options, signal) => serialise(() => run(options), signal) };
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
- (await ctx.execution.list(item.activation.activationId)).some((run) => isActiveRunStatus(run.status)) ||
53
- allRuns.some((run) => isActiveRunStatus(run.status) &&
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): (options: Parameters<(options: import("../index.js").AdvanceOptions, signal?: AbortSignal) => Promise<import("../index.js").AdvanceResult>>[0]) => Promise<import("../index.js").AdvanceResult>;
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 (options) => scheduler.runOnce(options);
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 AdvanceOnce, type HostBudget, type HostResult } from '../contracts/commands.js';
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: AdvanceOnce);
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
+ }
@@ -5,18 +5,19 @@ import { mergeBatches, reportPartialPollFailure } from './poll-watermark.js';
5
5
  import { githubReviewObservation } from './review-source.js';
6
6
  export async function reviewCommentEventsFor(context, pullRequests) {
7
7
  if (context.client.listReviewComments === undefined)
8
- return { drafts: [], succeeded: true };
8
+ return { drafts: [], succeeded: true, complete: true };
9
9
  const items = await Promise.all(pullRequests.map(async (pullRequest) => {
10
10
  try {
11
11
  const comments = await context.client.listReviewComments(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize, context.config.polling.maxPerRepo);
12
12
  return {
13
13
  succeeded: true,
14
+ complete: comments.length < context.config.polling.maxPerRepo,
14
15
  drafts: (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, pullRequest, comment)))).flat(),
15
16
  };
16
17
  }
17
18
  catch {
18
19
  reportPartialPollFailure(context.repository, `pull-request review comments #${pullRequest.number}`);
19
- return { drafts: [], succeeded: false };
20
+ return { drafts: [], succeeded: false, complete: false };
20
21
  }
21
22
  }));
22
23
  return mergeBatches(items);
@@ -27,6 +28,7 @@ export async function reviewEventsFor(context, pullRequests) {
27
28
  const reviews = await context.client.listReviews(context.owner, context.repo, pullRequest.number, context.config.polling.commentPageSize, context.config.polling.maxPerRepo);
28
29
  return {
29
30
  succeeded: true,
31
+ complete: reviews.length < context.config.polling.maxPerRepo,
30
32
  drafts: reviews.flatMap((review) => githubReviewObservation({
31
33
  repository: context.repository,
32
34
  pullRequest,
@@ -37,7 +39,7 @@ export async function reviewEventsFor(context, pullRequests) {
37
39
  }
38
40
  catch {
39
41
  reportPartialPollFailure(context.repository, `pull-request reviews #${pullRequest.number}`);
40
- return { drafts: [], succeeded: false };
42
+ return { drafts: [], succeeded: false, complete: false };
41
43
  }
42
44
  }));
43
45
  return mergeBatches(items);
@@ -48,12 +50,13 @@ export async function issueCommentEventsFor(context, issues, since) {
48
50
  const comments = await context.client.listIssueComments(context.owner, context.repo, issue.number, context.config.polling.commentPageSize, since, context.config.polling.maxPerRepo);
49
51
  return {
50
52
  succeeded: true,
53
+ complete: comments.length < context.config.polling.maxPerRepo,
51
54
  drafts: (await Promise.all(comments.map((comment) => issueCommentEventsForComment(context, issue, comment)))).flat(),
52
55
  };
53
56
  }
54
57
  catch {
55
58
  reportPartialPollFailure(context.repository, `issue comments #${issue.number}`);
56
- return { drafts: [], succeeded: false };
59
+ return { drafts: [], succeeded: false, complete: false };
57
60
  }
58
61
  }));
59
62
  return mergeBatches(items);
@@ -1,12 +1,36 @@
1
1
  import type { CheckpointStore } from '@atolis-hq/eventing';
2
2
  import type { AdapterId } from '../../contracts/identifiers.js';
3
3
  import type { GitHubAdapterEventData } from '../contracts/events.js';
4
+ export declare const minimumPollOverlapMs: number;
4
5
  export interface PollBatch {
5
6
  readonly drafts: readonly GitHubAdapterEventData[];
6
7
  readonly succeeded: boolean;
8
+ readonly complete: boolean;
7
9
  }
8
10
  export declare function mergeBatches(batches: readonly PollBatch[]): PollBatch;
9
11
  export declare function watermarkCheckpoint(adapter: AdapterId | undefined, repository: string): string;
10
12
  export declare function loadWatermark(checkpoints: CheckpointStore | undefined, adapter: AdapterId | undefined, owner: string, repo: string): Promise<number>;
11
13
  export declare function overlapSince(watermark: number, overlapMs: number): string | undefined;
14
+ export declare function providerWatermark(previous: number, updatedAts: readonly string[]): number | undefined;
15
+ export declare function hasProviderTimestamp(payload: {
16
+ readonly updated_at: string;
17
+ }): boolean;
18
+ export declare function timestampsValid(...groups: readonly (readonly {
19
+ readonly updated_at: string;
20
+ }[])[]): boolean;
21
+ export declare function batchesSucceeded(batches: readonly PollBatch[]): boolean;
22
+ export declare function batchesComplete(batches: readonly PollBatch[]): boolean;
23
+ export declare function completedProviderWatermark(input: {
24
+ readonly previous: number;
25
+ readonly maximumResults: number;
26
+ readonly issues: readonly {
27
+ readonly updated_at: string;
28
+ }[];
29
+ readonly pullRequests: readonly {
30
+ readonly updated_at: string;
31
+ }[];
32
+ readonly topLevelSucceeded: boolean;
33
+ readonly topLevelTimestampsValid: boolean;
34
+ readonly nestedBatches: readonly PollBatch[];
35
+ }): number | undefined;
12
36
  export declare function reportPartialPollFailure(repository: string, query: string): void;
@@ -1,7 +1,9 @@
1
+ export const minimumPollOverlapMs = 5 * 60_000;
1
2
  export function mergeBatches(batches) {
2
3
  return {
3
4
  drafts: batches.flatMap((batch) => batch.drafts),
4
5
  succeeded: batches.every((batch) => batch.succeeded),
6
+ complete: batches.every((batch) => batch.complete),
5
7
  };
6
8
  }
7
9
  export function watermarkCheckpoint(adapter, repository) {
@@ -15,7 +17,41 @@ export async function loadWatermark(checkpoints, adapter, owner, repo) {
15
17
  export function overlapSince(watermark, overlapMs) {
16
18
  if (!Number.isFinite(watermark) || watermark <= 0)
17
19
  return undefined;
18
- return new Date(Math.max(0, watermark - overlapMs)).toISOString();
20
+ return new Date(Math.max(0, watermark - Math.max(overlapMs, minimumPollOverlapMs))).toISOString();
21
+ }
22
+ export function providerWatermark(previous, updatedAts) {
23
+ const latest = updatedAts.reduce((current, updatedAt) => {
24
+ const timestamp = Date.parse(updatedAt);
25
+ if (!Number.isFinite(timestamp) || timestamp < previous)
26
+ return current;
27
+ return current === undefined || timestamp > current ? timestamp : current;
28
+ }, undefined);
29
+ return latest === undefined || latest === previous ? undefined : latest;
30
+ }
31
+ export function hasProviderTimestamp(payload) {
32
+ return Number.isFinite(Date.parse(payload.updated_at));
33
+ }
34
+ export function timestampsValid(...groups) {
35
+ return groups.flat().every(hasProviderTimestamp);
36
+ }
37
+ export function batchesSucceeded(batches) {
38
+ return batches.every((batch) => batch.succeeded);
39
+ }
40
+ export function batchesComplete(batches) {
41
+ return batches.every((batch) => batch.complete);
42
+ }
43
+ export function completedProviderWatermark(input) {
44
+ if (!input.topLevelSucceeded ||
45
+ !input.topLevelTimestampsValid ||
46
+ !batchesSucceeded(input.nestedBatches) ||
47
+ !batchesComplete(input.nestedBatches) ||
48
+ input.issues.length >= input.maximumResults ||
49
+ input.pullRequests.length >= input.maximumResults)
50
+ return undefined;
51
+ return providerWatermark(input.previous, [
52
+ ...input.issues.map((issue) => issue.updated_at),
53
+ ...input.pullRequests.map((pullRequest) => pullRequest.updated_at),
54
+ ]);
19
55
  }
20
56
  export function reportPartialPollFailure(repository, query) {
21
57
  process.stderr.write(`GitHub poll partial failure for ${repository}: ${query}; preserving watermark for replay\n`);
@@ -0,0 +1,18 @@
1
+ import type { GitHubCheckRunPayload, GitHubCommitStatusPayload } from '../contracts/payloads.js';
2
+ export interface CheckEvidence {
3
+ readonly available: boolean;
4
+ readonly complete: boolean;
5
+ readonly checkRuns: readonly GitHubCheckRunPayload[];
6
+ readonly statuses: readonly GitHubCommitStatusPayload[];
7
+ }
8
+ export interface ChangedFiles {
9
+ readonly files: readonly string[] | undefined;
10
+ readonly complete: boolean;
11
+ }
12
+ interface PullRequestEvidenceClient {
13
+ listCheckRunsForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCheckRunPayload[]>;
14
+ getCombinedStatusForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCommitStatusPayload[]>;
15
+ listPullRequestFiles(owner: string, repo: string, pullNumber: number, maxResults?: number): Promise<readonly string[]>;
16
+ }
17
+ export declare function readPullRequestEvidence(client: PullRequestEvidenceClient, owner: string, repo: string, headRevision: string, pullNumber: number, maxResults: number): Promise<readonly [CheckEvidence, ChangedFiles]>;
18
+ export {};
@@ -0,0 +1,32 @@
1
+ export async function readPullRequestEvidence(client, owner, repo, headRevision, pullNumber, maxResults) {
2
+ return Promise.all([
3
+ readCheckEvidence(client, owner, repo, headRevision, maxResults),
4
+ readChangedFiles(client, owner, repo, pullNumber, maxResults),
5
+ ]);
6
+ }
7
+ async function readCheckEvidence(client, owner, repo, headRevision, maxResults) {
8
+ try {
9
+ const [checkRuns, statuses] = await Promise.all([
10
+ client.listCheckRunsForRef(owner, repo, headRevision, maxResults),
11
+ client.getCombinedStatusForRef(owner, repo, headRevision, maxResults),
12
+ ]);
13
+ return {
14
+ available: true,
15
+ complete: checkRuns.length < maxResults && statuses.length < maxResults,
16
+ checkRuns,
17
+ statuses,
18
+ };
19
+ }
20
+ catch {
21
+ return { available: false, complete: false, checkRuns: [], statuses: [] };
22
+ }
23
+ }
24
+ async function readChangedFiles(client, owner, repo, pullNumber, maxResults) {
25
+ try {
26
+ const files = await client.listPullRequestFiles(owner, repo, pullNumber, maxResults);
27
+ return { files, complete: files.length < maxResults };
28
+ }
29
+ catch {
30
+ return { files: undefined, complete: false };
31
+ }
32
+ }
@@ -1,16 +1,20 @@
1
1
  import type { AdapterId } from '../../contracts/identifiers.js';
2
2
  import type { ExternalEventSource } from '../application/poll-service.js';
3
3
  import { type GitHubCheckRunPayload, type GitHubCommitStatusPayload, type GitHubPullRequestPayload } from '../contracts/payloads.js';
4
+ import type { PollBatch } from './poll-watermark.js';
4
5
  export interface GitHubPullRequestSourceClient {
5
6
  listPullRequests(owner: string, repo: string, maxResults: number): Promise<readonly GitHubPullRequestPayload[]>;
6
7
  listCheckRunsForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCheckRunPayload[]>;
7
8
  getCombinedStatusForRef(owner: string, repo: string, ref: string, maxResults?: number): Promise<readonly GitHubCommitStatusPayload[]>;
8
9
  listPullRequestFiles(owner: string, repo: string, pullNumber: number, maxResults?: number): Promise<readonly string[]>;
9
10
  }
10
- export declare function createGitHubPullRequestSource(input: {
11
+ interface GitHubPullRequestSourceInput {
11
12
  readonly client: GitHubPullRequestSourceClient;
12
13
  readonly repository: string;
13
14
  readonly maxResults: number;
14
15
  readonly maxConcurrency?: number;
15
16
  readonly adapter?: AdapterId;
16
- }): ExternalEventSource;
17
+ }
18
+ export declare function createGitHubPullRequestSource(input: GitHubPullRequestSourceInput): ExternalEventSource;
19
+ export declare function pullRequestEventsFor(input: GitHubPullRequestSourceInput, signal: AbortSignal): Promise<PollBatch>;
20
+ export {};
@@ -8,30 +8,39 @@ import { formatGitHubResourceKey } from '../contracts/external-key.js';
8
8
  import { gitHubAssigneeLogins, gitHubLabelNames, } from '../contracts/payloads.js';
9
9
  import { GitHubAdapter, GitHubCheckRunStatus, UnknownGitHubIdentity, UnknownGitHubRevision, } from '../contracts/vocabulary.js';
10
10
  import { withoutWakeMarkers } from './content-fingerprint.js';
11
+ import { readPullRequestEvidence } from './pr-evidence.js';
11
12
  export function createGitHubPullRequestSource(input) {
12
- const { owner, repo } = parseRepository(input.repository);
13
13
  return {
14
14
  async poll(signal) {
15
- signal.throwIfAborted();
16
- const pullRequests = await input.client.listPullRequests(owner, repo, input.maxResults);
17
- return mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
18
- signal.throwIfAborted();
19
- const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
20
- const [evidence, changedFiles] = await Promise.all([
21
- readCheckEvidence(input.client, owner, repo, headRevision, input.maxResults),
22
- readChangedFiles(input.client, owner, repo, pullRequest.number, input.maxResults),
23
- ]);
24
- return pullRequestObservation({
25
- repository: input.repository,
26
- pullRequest,
27
- evidence,
28
- changedFiles,
29
- ...(input.adapter === undefined ? {} : { adapter: input.adapter }),
30
- });
31
- });
15
+ return (await pullRequestEventsFor(input, signal)).drafts;
32
16
  },
33
17
  };
34
18
  }
19
+ export async function pullRequestEventsFor(input, signal) {
20
+ const { owner, repo } = parseRepository(input.repository);
21
+ signal.throwIfAborted();
22
+ const pullRequests = await input.client.listPullRequests(owner, repo, input.maxResults);
23
+ const results = await mapConcurrent(pullRequests, input.maxConcurrency ?? 4, async (pullRequest) => {
24
+ signal.throwIfAborted();
25
+ const headRevision = fallback(pullRequest.head?.sha, pullRequest.updated_at);
26
+ const [evidence, changedFiles] = await readPullRequestEvidence(input.client, owner, repo, headRevision, pullRequest.number, input.maxResults);
27
+ return {
28
+ complete: evidence.complete && changedFiles.complete,
29
+ draft: pullRequestObservation({
30
+ repository: input.repository,
31
+ pullRequest,
32
+ evidence,
33
+ changedFiles: changedFiles.files,
34
+ ...(input.adapter === undefined ? {} : { adapter: input.adapter }),
35
+ }),
36
+ };
37
+ });
38
+ return {
39
+ drafts: results.map((result) => result.draft),
40
+ succeeded: true,
41
+ complete: results.every((result) => result.complete),
42
+ };
43
+ }
35
44
  function pullRequestObservation(input) {
36
45
  const pullRequest = input.pullRequest;
37
46
  const key = formatGitHubResourceKey({
@@ -93,26 +102,6 @@ function normalizeCheckEvidence(checkRuns, statuses) {
93
102
  return PullRequestCheckState.Passing;
94
103
  return PullRequestCheckState.Unknown;
95
104
  }
96
- async function readCheckEvidence(client, owner, repo, headRevision, maxResults) {
97
- try {
98
- const [checkRuns, statuses] = await Promise.all([
99
- client.listCheckRunsForRef(owner, repo, headRevision, maxResults),
100
- client.getCombinedStatusForRef(owner, repo, headRevision, maxResults),
101
- ]);
102
- return { available: true, checkRuns, statuses };
103
- }
104
- catch {
105
- return { available: false, checkRuns: [], statuses: [] };
106
- }
107
- }
108
- async function readChangedFiles(client, owner, repo, pullNumber, maxResults) {
109
- try {
110
- return await client.listPullRequestFiles(owner, repo, pullNumber, maxResults);
111
- }
112
- catch {
113
- return undefined;
114
- }
115
- }
116
105
  async function mapConcurrent(values, concurrency, transform) {
117
106
  const output = new Array(values.length);
118
107
  let nextIndex = 0;
@@ -3,8 +3,8 @@ import { GitHubEventType } from '../contracts/events.js';
3
3
  import { createGitHubAdapterHealthRegistry, } from './adapter-health-registry.js';
4
4
  import { issueCommentEventsFor, reviewCommentEventsFor, reviewEventsFor, } from './comment-source.js';
5
5
  import { issueObservation } from './issue-source.js';
6
- import { loadWatermark, overlapSince, reportPartialPollFailure, watermarkCheckpoint, } from './poll-watermark.js';
7
- import { createGitHubPullRequestSource } from './pr-source.js';
6
+ import { batchesComplete, batchesSucceeded, completedProviderWatermark, hasProviderTimestamp, loadWatermark, overlapSince, reportPartialPollFailure, timestampsValid, watermarkCheckpoint, } from './poll-watermark.js';
7
+ import { pullRequestEventsFor } from './pr-source.js';
8
8
  import { createGitHubRequestCoordinator, } from './request-coordinator.js';
9
9
  export function createGitHubSource(config, client, adapter, requests = createGitHubRequestCoordinator({
10
10
  maxConcurrent: config.polling.maxConcurrent,
@@ -36,12 +36,11 @@ export function createGitHubSource(config, client, adapter, requests = createGit
36
36
  owner,
37
37
  repo,
38
38
  watermark: await loadWatermark(state?.checkpoints, adapter, owner, repo),
39
- now,
40
39
  health,
41
40
  })));
42
41
  for (const result of perRepository)
43
- if (result.succeeded)
44
- pendingWatermarks.set(result.repository, result.completedAt);
42
+ if (result.watermark !== undefined)
43
+ pendingWatermarks.set(result.repository, result.watermark);
45
44
  return perRepository
46
45
  .flatMap((result) => result.drafts)
47
46
  .filter((draft) => {
@@ -87,15 +86,7 @@ function limitGitHubSourceClient(client, requests) {
87
86
  }
88
87
  async function pollRepository(input) {
89
88
  const { client, config, adapter, signal, owner, repo, health } = input;
90
- const queriedAt = input.now();
91
- const context = {
92
- client,
93
- config,
94
- adapter,
95
- owner,
96
- repo,
97
- repository: `${owner}/${repo}`,
98
- };
89
+ const context = createRepositoryPollContext(client, config, adapter, owner, repo);
99
90
  const since = overlapSince(input.watermark, config.polling.lookbackMs);
100
91
  const [issuesResult, pullRequestsResult] = await fetchRepositoryItems({
101
92
  client,
@@ -105,29 +96,43 @@ async function pollRepository(input) {
105
96
  since,
106
97
  });
107
98
  reportRepositoryReadResults(context, health, issuesResult, pullRequestsResult);
108
- const issues = isFulfilled(issuesResult) ? issuesResult.value : [];
109
- const pullRequestPayloads = isFulfilled(pullRequestsResult)
110
- ? pullRequestsResult.value.filter((pullRequest) => since === undefined || pullRequest.updated_at >= since)
99
+ const returnedIssues = isFulfilled(issuesResult) ? issuesResult.value : [];
100
+ const issues = returnedIssues.filter(hasProviderTimestamp);
101
+ const returnedPullRequestPayloads = isFulfilled(pullRequestsResult)
102
+ ? pullRequestsResult.value
111
103
  : [];
104
+ const topLevelTimestampsValid = timestampsValid(returnedIssues, returnedPullRequestPayloads);
105
+ const pullRequestPayloads = returnedPullRequestPayloads
106
+ .filter(hasProviderTimestamp)
107
+ .filter((pullRequest) => since === undefined || pullRequest.updated_at >= since);
112
108
  const [pullRequests, reviews, reviewComments, issueComments] = await Promise.all([
113
- createGitHubPullRequestSource({
109
+ pullRequestEventsFor({
114
110
  client: { ...client, listPullRequests: async () => pullRequestPayloads },
115
111
  repository: context.repository,
116
112
  maxResults: config.polling.maxPerRepo,
117
113
  ...(adapter === undefined ? {} : { adapter }),
118
- }).poll(signal),
114
+ }, signal),
119
115
  reviewEventsFor(context, pullRequestPayloads),
120
116
  reviewCommentEventsFor(context, pullRequestPayloads),
121
117
  issueCommentEventsFor(context, [...issues, ...pullRequestPayloads], since),
122
118
  ]);
119
+ const nestedBatches = [pullRequests, reviews, reviewComments, issueComments];
123
120
  return {
124
121
  repository: context.repository,
125
- completedAt: queriedAt,
126
- succeeded: isFulfilled(issuesResult) &&
122
+ watermark: completedProviderWatermark({
123
+ previous: input.watermark,
124
+ maximumResults: config.polling.maxPerRepo,
125
+ issues: returnedIssues,
126
+ pullRequests: returnedPullRequestPayloads,
127
+ topLevelSucceeded: isFulfilled(issuesResult) && isFulfilled(pullRequestsResult),
128
+ topLevelTimestampsValid,
129
+ nestedBatches,
130
+ }),
131
+ succeeded: topLevelTimestampsValid &&
132
+ isFulfilled(issuesResult) &&
127
133
  isFulfilled(pullRequestsResult) &&
128
- reviews.succeeded &&
129
- reviewComments.succeeded &&
130
- issueComments.succeeded,
134
+ batchesComplete(nestedBatches) &&
135
+ batchesSucceeded(nestedBatches),
131
136
  drafts: [
132
137
  ...issues
133
138
  .filter((issue) => issue.pull_request === undefined)
@@ -136,13 +141,16 @@ async function pollRepository(input) {
136
141
  issue,
137
142
  ...(adapter === undefined ? {} : { adapter }),
138
143
  })),
139
- ...pullRequests,
144
+ ...pullRequests.drafts,
140
145
  ...reviews.drafts,
141
146
  ...reviewComments.drafts,
142
147
  ...issueComments.drafts,
143
148
  ],
144
149
  };
145
150
  }
151
+ function createRepositoryPollContext(client, config, adapter, owner, repo) {
152
+ return { client, config, adapter, owner, repo, repository: `${owner}/${repo}` };
153
+ }
146
154
  function fetchRepositoryItems(input) {
147
155
  const { client, config, owner, repo, since } = input;
148
156
  return Promise.allSettled([
@@ -31,6 +31,7 @@ export declare class AdvanceWorkflow {
31
31
  restartBlockedFailedStage(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView>;
32
32
  private retryBlockedFailedStageWith;
33
33
  resumeBlockedStageForChanges(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView | null>;
34
+ resumeNeedsClarificationStage(id: WorkflowInstanceId, context: CommandContext): Promise<import("../index.js").WorkflowInstanceView | null>;
34
35
  get(id: WorkflowInstanceId): Promise<import("../index.js").WorkflowInstanceView | null>;
35
36
  listPendingActivations(workItemId?: string): Promise<{
36
37
  workflow: import("../index.js").WorkflowInstanceView;
@@ -3,7 +3,7 @@ import { OrchestrationEventType } from '../contracts/events.js';
3
3
  import { commandName } from '../contracts/identifiers.js';
4
4
  import { WorkflowStatus } from '../contracts/vocabulary.js';
5
5
  import { workflowEventData } from '../domain/decision-events.js';
6
- import { requestChangesResume as decideChangesResume, requestFreshOperatorRetry as decideFreshOperatorRetry, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, requestRunnerQuotaRetry, } from '../domain/interpreter.js';
6
+ import { requestChangesResume as decideChangesResume, requestFreshOperatorRetry as decideFreshOperatorRetry, requestOperatorRetry as decideOperatorRetry, requestSupplementalActivity as decideSupplementalActivity, isNeedsClarificationResumeEligible, requestRunnerQuotaRetry, } from '../domain/interpreter.js';
7
7
  import { isAuthorisedActor } from '../domain/supplemental-policy.js';
8
8
  import { appendWithIntentRecovery } from './durable-append.js';
9
9
  import { acceptResourceTransition, matchResourceTransitions, } from './resource-transition-matching.js';
@@ -190,6 +190,12 @@ export class AdvanceWorkflow {
190
190
  return recovered.view;
191
191
  return (await this.repository.loadRequired(id)).view;
192
192
  }
193
+ async resumeNeedsClarificationStage(id, context) {
194
+ const loaded = await this.repository.load(id);
195
+ if (loaded.view === null || !isNeedsClarificationResumeEligible(loaded.view))
196
+ return loaded.view;
197
+ return this.resumeBlockedStageForChanges(id, context);
198
+ }
193
199
  async get(id) {
194
200
  return (await this.repository.load(id)).view;
195
201
  }
@@ -41,12 +41,15 @@ export function createConversationCommandReactor(orchestration) {
41
41
  occurredAt: event.occurredAt,
42
42
  actor: { kind: EventActorKind.Integration, id: entry.origin.adapter },
43
43
  });
44
- if (conversationCommand(entry.body) !== null || !input.authorized)
44
+ if (conversationCommand(entry.body) !== null)
45
45
  return;
46
46
  for (const workflow of await orchestration.listAll()) {
47
47
  if (workflow.workItemId !== workItemId)
48
48
  continue;
49
- await orchestration.resumeBlockedStageForChanges(workflow.workflowInstanceId, {
49
+ const resume = input.authorized
50
+ ? orchestration.resumeBlockedStageForChanges.bind(orchestration)
51
+ : orchestration.resumeNeedsClarificationStage.bind(orchestration);
52
+ await resume(workflow.workflowInstanceId, {
50
53
  commandId: `${event.eventId}:conversation-reply`,
51
54
  correlationId: correlationId(event.correlationId),
52
55
  occurredAt: event.occurredAt,
@@ -61,6 +61,7 @@ export declare class OrchestrationService {
61
61
  retryBlockedFailedStage(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView>;
62
62
  restartBlockedFailedStage(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView>;
63
63
  resumeBlockedStageForChanges(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView | null>;
64
+ resumeNeedsClarificationStage(workflowInstanceId: WorkflowInstanceId, context: CommandContext): Promise<WorkflowInstanceView | null>;
64
65
  extendBlockedGroupBudget(workflowInstanceId: WorkflowInstanceId, authority: {
65
66
  readonly kind: string;
66
67
  }, context: CommandContext): Promise<import("../contracts/views.js").GroupBudgetExhaustedView | WorkflowInstanceView>;
@@ -137,6 +137,9 @@ export class OrchestrationService {
137
137
  resumeBlockedStageForChanges(workflowInstanceId, context) {
138
138
  return this.transitionWatchChildren(context, () => this.advanceWorkflow.resumeBlockedStageForChanges(workflowInstanceId, context));
139
139
  }
140
+ resumeNeedsClarificationStage(workflowInstanceId, context) {
141
+ return this.transitionWatchChildren(context, () => this.advanceWorkflow.resumeNeedsClarificationStage(workflowInstanceId, context));
142
+ }
140
143
  async extendBlockedGroupBudget(workflowInstanceId, authority, context) {
141
144
  if (authority.kind !== ApprovalAuthorityKind.Human)
142
145
  throw new GroupBudgetExtensionIneligibleError('A group budget extension requires human authority');
@@ -7,7 +7,7 @@ export { acceptSignal, waitForSignal } from './signal-policy.js';
7
7
  export type { AcceptSignal } from './signal-policy.js';
8
8
  export { requestSupplementalActivity } from './supplemental-policy.js';
9
9
  export type { SupplementalActivityRequest } from './supplemental-policy.js';
10
- export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
10
+ export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isNeedsClarificationResumeEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
11
11
  export type { OperatorRetryRequest } from './operator-retry-policy.js';
12
12
  export { isRunnerQuotaRetryEligible, requestRunnerQuotaRetry, } from './runner-quota-retry-policy.js';
13
13
  export type { RunnerQuotaRetryRequest } from './runner-quota-retry-policy.js';
@@ -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, isGroupBudgetExtensionEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
13
+ export { isChangesResumeEligible, isGroupBudgetExtensionEligible, isNeedsClarificationResumeEligible, isOperatorRetryEligible, requestChangesResume, requestFreshOperatorRetry, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
14
14
  export { isRunnerQuotaRetryEligible, requestRunnerQuotaRetry, } from './runner-quota-retry-policy.js';
15
15
  export function acceptActivityOutcome(definition, state, input) {
16
16
  if (!isPendingOutcome(state, input))
@@ -7,6 +7,8 @@ export interface OperatorRetryRequest extends DecisionContext {
7
7
  export declare function isOperatorRetryEligible(view: WorkflowInstanceView): boolean;
8
8
  export declare function selectOperatorRetryTarget(workflows: readonly WorkflowInstanceView[]): WorkflowInstanceView | undefined;
9
9
  export declare function isChangesResumeEligible(view: WorkflowInstanceView): boolean;
10
+ /** A clarification reply may be supplied by any conversation participant. */
11
+ export declare function isNeedsClarificationResumeEligible(view: WorkflowInstanceView): boolean;
10
12
  export declare function isGroupBudgetExtensionEligible(view: WorkflowInstanceView): boolean;
11
13
  export declare function requestOperatorRetry(definition: CompiledWorkflow, state: WorkflowInstanceView, input: OperatorRetryRequest): OrchestrationDecision;
12
14
  /** Re-requests the failed stage while requiring a new runner session. */
@@ -1,4 +1,4 @@
1
- import { ActivityOutcomeKind, BuiltInActivityName } from '../../activities/index.js';
1
+ import { ActivityOutcomeKind, BuiltInActivityName, isNeedsClarificationAgentOutcome, } from '../../activities/index.js';
2
2
  import { OrchestrationEventType, WatchGateVerdictSignal } from '../contracts/events.js';
3
3
  import { signalName, stageName, watchId } from '../contracts/identifiers.js';
4
4
  import { ActivityActivationStatus, ApprovalAuthorityKind, WorkflowStatus, } from '../contracts/vocabulary.js';
@@ -49,20 +49,26 @@ export function selectOperatorRetryTarget(workflows) {
49
49
  isOperatorRetryEligible(workflow));
50
50
  }
51
51
  export function isChangesResumeEligible(view) {
52
- const pending = view.pendingActivation;
53
- const completedAgentStage = pending !== undefined &&
54
- pending.activity === BuiltInActivityName.Agent &&
55
- pending.status === ActivityActivationStatus.Completed &&
56
- pending.supplemental !== true &&
57
- pending.followOnIndex === undefined &&
58
- view.acceptedOutcomes.includes(pending.activationId);
59
- if (!completedAgentStage)
52
+ if (!isCompletedAgentStage(view))
60
53
  return false;
61
54
  return ((view.status === WorkflowStatus.Blocked &&
62
55
  view.blockReason === 'unconfigured outcome blocked' &&
63
56
  view.lastOutcome?.kind === ActivityOutcomeKind.Blocked) ||
64
57
  (view.status === WorkflowStatus.Waiting && view.waitingFor !== undefined));
65
58
  }
59
+ /** A clarification reply may be supplied by any conversation participant. */
60
+ export function isNeedsClarificationResumeEligible(view) {
61
+ return isChangesResumeEligible(view) && isNeedsClarificationAgentOutcome(view.lastOutcome);
62
+ }
63
+ function isCompletedAgentStage(view) {
64
+ const pending = view.pendingActivation;
65
+ return (pending !== undefined &&
66
+ pending.activity === BuiltInActivityName.Agent &&
67
+ pending.status === ActivityActivationStatus.Completed &&
68
+ pending.supplemental !== true &&
69
+ pending.followOnIndex === undefined &&
70
+ view.acceptedOutcomes.includes(pending.activationId));
71
+ }
66
72
  export function isGroupBudgetExtensionEligible(view) {
67
73
  return (view.status === WorkflowStatus.Blocked &&
68
74
  view.blockReason?.startsWith('watch group budget exhausted for ') === true &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {