@atolis-hq/wake 0.3.55 → 0.3.57

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.
@@ -1,7 +1,7 @@
1
1
  import { pullRequestProjection } from '../activities/index.js';
2
2
  import { executionProjection, runsByWorkflowInstanceProjection, } from '../execution/index.js';
3
3
  import { correlationId, EventActorKind } from '../kernel/index.js';
4
- import { isOperatorRetryEligible, OperatorRetryIneligibleError, orchestrationProjection, workflowsByWorkItemProjection, } from '../orchestration/index.js';
4
+ import { OperatorRetryIneligibleError, orchestrationProjection, selectOperatorRetryTarget, workflowsByWorkItemProjection, } from '../orchestration/index.js';
5
5
  import { workCorrelationsProjection, } from '../resources/index.js';
6
6
  import { ApiCommandStatus, fromWorkItemKey, presentResource, presentRun, presentWorkflowInstance, presentWorkItem, } from '../surfaces/index.js';
7
7
  import { workItemId, WorkStatus } from '../work/index.js';
@@ -70,13 +70,14 @@ export function createSurfaceWorkApplications(root, now) {
70
70
  return retryIneligible('Work item is deleted');
71
71
  if (work.state !== WorkStatus.Open)
72
72
  return retryIneligible('Work item is not open');
73
- const primary = (await root.orchestration.listAll()).find((value) => value.workItemId === id && value.parentWorkflowInstanceId === undefined);
74
- if (primary === undefined)
73
+ const workflows = (await root.orchestration.listAll()).filter((workflow) => workflow.workItemId === id);
74
+ if (!workflows.some((workflow) => workflow.parentWorkflowInstanceId === undefined))
75
75
  return retryIneligible('Work item has no primary workflow');
76
- if (!isOperatorRetryEligible(primary))
76
+ const target = selectOperatorRetryTarget(workflows);
77
+ if (target === undefined)
77
78
  return retryIneligible('Workflow is not retry eligible');
78
79
  try {
79
- await root.orchestration.retryBlockedFailedStage(primary.workflowInstanceId, commandContext(command.idempotencyKey, now));
80
+ await root.orchestration.retryBlockedFailedStage(target.workflowInstanceId, commandContext(command.idempotencyKey, now));
80
81
  }
81
82
  catch (error) {
82
83
  if (error instanceof OperatorRetryIneligibleError)
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "g4d17da0";
111
+ export const wakeVersion = "g17796f1";
@@ -9,7 +9,7 @@ export function createGitHubAgentContextReader(journal, resources, options = {})
9
9
  const commentHistory = createCommentHistoryReader(journal, resources, options);
10
10
  return {
11
11
  async forWorkItem(workItemId, options) {
12
- const comments = await commentHistory.forWorkItem(workItemId, options);
12
+ const comments = boundedAgentContextComments(await commentHistory.forWorkItem(workItemId, options));
13
13
  return {
14
14
  ...(await currentWorkItemContent(journal, resources, workItemId)),
15
15
  comments,
@@ -18,6 +18,64 @@ export function createGitHubAgentContextReader(journal, resources, options = {})
18
18
  },
19
19
  };
20
20
  }
21
+ const maximumAgentContextComments = 12;
22
+ const maximumAgentContextCommentCharacters = 8_000;
23
+ const maximumAgentContextCharacters = 48_000;
24
+ const truncationNotice = '\n[Wake truncated this historical comment for context bounds.]';
25
+ function boundedAgentContextComments(comments) {
26
+ const indexed = comments.map((comment, index) => ({ comment, index }));
27
+ const latestWakeReviewerFeedback = [...indexed]
28
+ .reverse()
29
+ .find(({ comment }) => isWakeReviewerFeedback(comment));
30
+ const latestWakeAgentArtifact = [...indexed]
31
+ .reverse()
32
+ .find(({ comment }) => isWakeAgentArtifact(comment));
33
+ const protectedWakeArtifacts = [latestWakeReviewerFeedback, latestWakeAgentArtifact].flatMap((candidate, index, values) => candidate === undefined || values.slice(0, index).some((value) => value === candidate)
34
+ ? []
35
+ : [candidate]);
36
+ const retained = [];
37
+ let characters = 0;
38
+ for (const artifact of protectedWakeArtifacts) {
39
+ const remaining = maximumAgentContextCharacters - characters;
40
+ if (remaining <= 0)
41
+ break;
42
+ const body = truncateComment(artifact.comment.body, Math.min(maximumAgentContextCommentCharacters, remaining));
43
+ retained.push({ ...artifact, comment: { ...artifact.comment, body } });
44
+ characters += body.length;
45
+ }
46
+ for (const candidate of [...indexed].reverse()) {
47
+ const { comment } = candidate;
48
+ if ((isWakeDelivery(comment.body) && !protectedWakeArtifacts.includes(candidate)) ||
49
+ protectedWakeArtifacts.includes(candidate) ||
50
+ retained.length === maximumAgentContextComments)
51
+ continue;
52
+ const remaining = maximumAgentContextCharacters - characters;
53
+ if (remaining <= 0)
54
+ break;
55
+ const body = truncateComment(comment.body, Math.min(maximumAgentContextCommentCharacters, remaining));
56
+ retained.push({ ...candidate, comment: { ...comment, body } });
57
+ characters += body.length;
58
+ }
59
+ return retained.sort((left, right) => left.index - right.index).map(({ comment }) => comment);
60
+ }
61
+ function isWakeDelivery(body) {
62
+ return /<!--\s*wake:delivery:[^\s>]+\s*-->/.test(body);
63
+ }
64
+ function isWakeReviewerFeedback(comment) {
65
+ return (comment.body.includes('<!-- wake:agent -->') &&
66
+ (comment.body.includes('**Outcome:** 🔴 Changes Requested') ||
67
+ /"watchGateVerdict"[\s\S]*"outcome"\s*:\s*"REJECTED"/.test(comment.body)));
68
+ }
69
+ function isWakeAgentArtifact(comment) {
70
+ return comment.body.includes('<!-- wake:agent -->') && isWakeDelivery(comment.body);
71
+ }
72
+ function truncateComment(body, maximumCharacters) {
73
+ if (body.length <= maximumCharacters)
74
+ return body;
75
+ if (maximumCharacters <= truncationNotice.length)
76
+ return body.slice(0, maximumCharacters);
77
+ return `${body.slice(0, maximumCharacters - truncationNotice.length)}${truncationNotice}`;
78
+ }
21
79
  async function currentWorkItemContent(journal, resources, workItemId) {
22
80
  const primary = (await resources.correlationsForWork(workItemId)).find((correlation) => correlation.role === ResourceCorrelationRole.Primary);
23
81
  if (primary === undefined)
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { ActivityOutcomeKind, ReviewActorKind, ReviewDecisionKind, ReviewerAuthorizationSource, createPullRequestService, isReviewAuthorized, } from '../../../activities/index.js';
3
- import { ApprovalAuthorityKind } from '../../../orchestration/index.js';
3
+ import { ApprovalAuthorityKind, selectOperatorRetryTarget, } from '../../../orchestration/index.js';
4
4
  import { BuiltInResourceKind, ResourceCorrelationRole, ResourceStreamKind, resourceId, } from '../../../resources/index.js';
5
5
  import { WorkStatus } from '../../../work/index.js';
6
6
  import { UnknownGitHubIdentity } from '../contracts/vocabulary.js';
@@ -128,13 +128,14 @@ async function applyIssueRetrySignal(input) {
128
128
  const workItemIds = (await resources.correlations(resourceIdValue))
129
129
  .filter((correlation) => correlation.role === ResourceCorrelationRole.Primary)
130
130
  .map((correlation) => correlation.workItemId);
131
- for (const workflow of await orchestration.listAll()) {
132
- if (!workItemIds.includes(workflow.workItemId) ||
133
- workflow.parentWorkflowInstanceId !== undefined)
131
+ const workflows = await orchestration.listAll();
132
+ for (const workItemId of workItemIds) {
133
+ if (!(await isEligibleWorkItem(work, workItemId)))
134
134
  continue;
135
- if (!(await isEligibleWorkItem(work, workflow.workItemId)))
135
+ const target = selectOperatorRetryTarget(workflows.filter((workflow) => workflow.workItemId === workItemId));
136
+ if (target === undefined)
136
137
  continue;
137
- await ignoreIneligibleOperatorRetry(() => orchestration.retryBlockedFailedStage(workflow.workflowInstanceId, commandContext(event)));
138
+ await ignoreIneligibleOperatorRetry(() => orchestration.retryBlockedFailedStage(target.workflowInstanceId, commandContext(event)));
138
139
  }
139
140
  }
140
141
  async function applyIssueApprovalSignal(input) {
@@ -10,7 +10,7 @@ import { finishRoute } from './transition.js';
10
10
  export { startInstance } from './activation-policy.js';
11
11
  export { acceptSignal, waitForSignal } from './signal-policy.js';
12
12
  export { requestSupplementalActivity } from './supplemental-policy.js';
13
- export { isChangesResumeEligible, isOperatorRetryEligible, requestChangesResume, requestOperatorRetry, } from './operator-retry-policy.js';
13
+ export { isChangesResumeEligible, isOperatorRetryEligible, requestChangesResume, requestOperatorRetry, selectOperatorRetryTarget, } from './operator-retry-policy.js';
14
14
  export function acceptActivityOutcome(definition, state, input) {
15
15
  if (!isPendingOutcome(state, input))
16
16
  return { kind: 'ignored', reason: 'outcome is not for the pending activation' };
@@ -1,7 +1,7 @@
1
1
  import { ActivityOutcomeKind, BuiltInActivityName } from '../../activities/index.js';
2
- import { OrchestrationEventType } from '../contracts/events.js';
3
- import { stageName } from '../contracts/identifiers.js';
4
- import { ActivityActivationStatus, WorkflowStatus } from '../contracts/vocabulary.js';
2
+ import { OrchestrationEventType, WatchGateVerdictSignal } from '../contracts/events.js';
3
+ import { stageName, watchId } from '../contracts/identifiers.js';
4
+ import { ActivityActivationStatus, ApprovalAuthorityKind, WorkflowStatus, } from '../contracts/vocabulary.js';
5
5
  import { activation, nextOrdinal, stateDraft } from './decision-events.js';
6
6
  export function isOperatorRetryEligible(view) {
7
7
  const pending = view.pendingActivation;
@@ -17,6 +17,23 @@ export function isOperatorRetryEligible(view) {
17
17
  view.lastOutcome?.kind === ActivityOutcomeKind.Failed) ||
18
18
  view.executionFailure?.activationId === pending.activationId);
19
19
  }
20
+ export function selectOperatorRetryTarget(workflows) {
21
+ const primary = workflows.find((workflow) => workflow.parentWorkflowInstanceId === undefined);
22
+ if (primary === undefined)
23
+ return undefined;
24
+ if (isOperatorRetryEligible(primary))
25
+ return primary;
26
+ if (primary.status !== WorkflowStatus.Waiting ||
27
+ primary.waitingFor?.signalKind !== WatchGateVerdictSignal)
28
+ return undefined;
29
+ const watched = new Set((primary.waitingFor.from ?? []).flatMap((authority) => authority.kind === ApprovalAuthorityKind.Watch ? [authority.watch] : []));
30
+ return workflows.find((workflow) => workflow.parentWorkflowInstanceId === primary.workflowInstanceId &&
31
+ workflow.workItemId === primary.workItemId &&
32
+ workflow.orchestrationGroupId === primary.orchestrationGroupId &&
33
+ workflow.watchId !== undefined &&
34
+ watched.has(watchId(workflow.watchId)) &&
35
+ isOperatorRetryEligible(workflow));
36
+ }
20
37
  export function isChangesResumeEligible(view) {
21
38
  const pending = view.pendingActivation;
22
39
  return (view.status === WorkflowStatus.Blocked &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.55",
3
+ "version": "0.3.57",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {