@atolis-hq/wake 0.4.39 → 0.4.40

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.
@@ -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,
@@ -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.40";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -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.40",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {