@atolis-hq/wake 0.2.31 → 0.2.32

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.
@@ -66,6 +66,14 @@ export function createGitHubClient(token) {
66
66
  }),
67
67
  });
68
68
  },
69
+ async getIssue(owner, repo, issueNumber) {
70
+ const { data } = await octokit.rest.issues.get({
71
+ owner,
72
+ repo,
73
+ issue_number: issueNumber,
74
+ });
75
+ return data;
76
+ },
69
77
  async createComment(owner, repo, issueNumber, body) {
70
78
  return octokit.rest.issues.createComment({
71
79
  owner,
@@ -185,6 +185,9 @@ export async function readControlPlaneUiUrl(wakeRoot) {
185
185
  return undefined;
186
186
  }
187
187
  }
188
+ function isGitHubNotFound(error) {
189
+ return error instanceof Error && error.status === 404;
190
+ }
188
191
  export function formatGitHubError(error) {
189
192
  if (error instanceof Error) {
190
193
  const octokit = error;
@@ -301,6 +304,71 @@ export function createGitHubIssuesWorkSource(deps) {
301
304
  return deps.stateStore.readIssueState(workItemKey);
302
305
  }
303
306
  return {
307
+ async refreshForDispatch(input) {
308
+ const repoRef = input.projection.issue.repo;
309
+ if (deps.client.getIssue === undefined) {
310
+ return null;
311
+ }
312
+ if (!deps.config.sources.github.repos.includes(repoRef)) {
313
+ return null;
314
+ }
315
+ const [owner, repo] = repoRef.split('/');
316
+ if (owner === undefined || repo === undefined) {
317
+ return null;
318
+ }
319
+ const ingestedAt = deps.now().toISOString();
320
+ let issue;
321
+ try {
322
+ issue = await deps.client.getIssue(owner, repo, input.projection.issue.number);
323
+ }
324
+ catch (error) {
325
+ if (isGitHubNotFound(error)) {
326
+ return {
327
+ events: [],
328
+ sourceRevision: `github:issue:${repoRef}#${input.projection.issue.number}@missing`,
329
+ sourceExists: false,
330
+ };
331
+ }
332
+ throw error;
333
+ }
334
+ const events = [];
335
+ if (input.projection.issue.updatedAt !== issue.updated_at) {
336
+ events.push(normalizeTicketUpsert({
337
+ repo: repoRef,
338
+ issue,
339
+ ingestedAt,
340
+ expectedEcho: isExpectedLabelEcho(issue, input.projection),
341
+ }));
342
+ }
343
+ const comments = await deps.client.listComments(owner, repo, input.projection.issue.number, deps.config.sources.github.polling.commentPageSize);
344
+ let latestCommentRevision = '';
345
+ for (const comment of comments) {
346
+ if (comment.updated_at > latestCommentRevision) {
347
+ latestCommentRevision = comment.updated_at;
348
+ }
349
+ const known = input.projection.comments.find((entry) => entry.id === String(comment.id));
350
+ if (known?.updatedAt === comment.updated_at) {
351
+ continue;
352
+ }
353
+ if (input.projection.wake.expectedEcho.commentIds.includes(String(comment.id))) {
354
+ continue;
355
+ }
356
+ events.push(normalizeTicketCommentEvent({
357
+ repo: repoRef,
358
+ issueNumber: input.projection.issue.number,
359
+ comment,
360
+ ingestedAt,
361
+ ...(deps.selfLogin === undefined ? {} : { selfLogin: deps.selfLogin }),
362
+ ...(known?.updatedAt === undefined ? {} : { existingUpdatedAt: known.updatedAt }),
363
+ }));
364
+ }
365
+ return {
366
+ events,
367
+ sourceRevision: latestCommentRevision.length > 0
368
+ ? `github:issue:${repoRef}#${issue.number}@${issue.updated_at};comments@${latestCommentRevision}`
369
+ : `github:issue:${repoRef}#${issue.number}@${issue.updated_at}`,
370
+ };
371
+ },
304
372
  async pollEvents(_input) {
305
373
  const ingestedAt = deps.now().toISOString();
306
374
  const events = [];
@@ -4,6 +4,18 @@ export function createWorkSourceFanIn(sources) {
4
4
  const batches = await Promise.all(sources.map((source) => source.pollEvents(input)));
5
5
  return batches.flat();
6
6
  },
7
+ async refreshForDispatch(input) {
8
+ for (const source of sources) {
9
+ if (source.refreshForDispatch === undefined) {
10
+ continue;
11
+ }
12
+ const result = await source.refreshForDispatch(input);
13
+ if (result !== null) {
14
+ return result;
15
+ }
16
+ }
17
+ return null;
18
+ },
7
19
  };
8
20
  }
9
21
  function intentKind(event) {
@@ -21,6 +21,15 @@ function latestHumanCommentId(candidate) {
21
21
  const human = candidate.comments.filter((c) => !c.isBotAuthored);
22
22
  return human.at(-1)?.id;
23
23
  }
24
+ function projectedSourceRevision(projection) {
25
+ const latestCommentUpdatedAt = projection.comments
26
+ .map((comment) => comment.updatedAt)
27
+ .sort()
28
+ .at(-1);
29
+ return latestCommentUpdatedAt === undefined
30
+ ? `${projection.issue.repo}#${projection.issue.number}@${projection.issue.updatedAt}`
31
+ : `${projection.issue.repo}#${projection.issue.number}@${projection.issue.updatedAt};comments@${latestCommentUpdatedAt}`;
32
+ }
24
33
  function isLateralReadOnlyAction(action, config) {
25
34
  return isCustomCommandAction(action, config);
26
35
  }
@@ -221,6 +230,18 @@ export function createTickRunner(deps) {
221
230
  processStartedAt: record.workerProcessStartedAt,
222
231
  }));
223
232
  }
233
+ async function hasSchedulerCapacity(now) {
234
+ const runRecords = await deps.stateStore.listRunRecords();
235
+ for (const record of runRecords) {
236
+ if (record.status !== 'running') {
237
+ continue;
238
+ }
239
+ if (await isRunningRecordActive(record, now)) {
240
+ return false;
241
+ }
242
+ }
243
+ return true;
244
+ }
224
245
  async function parkConfigDriftedProjections(projections) {
225
246
  let parked = false;
226
247
  for (const projection of projections) {
@@ -314,10 +335,31 @@ export function createTickRunner(deps) {
314
335
  if (await parkConfigDriftedProjections(projections)) {
315
336
  return { status: 'processed' };
316
337
  }
317
- const candidate = projections.find((issue) => policy.resolveNextEligibleAction(issue, deps.config) !== null);
338
+ let candidate = projections.find((issue) => policy.resolveNextEligibleAction(issue, deps.config) !== null);
318
339
  if (candidate === undefined) {
319
340
  return { status: 'idle' };
320
341
  }
342
+ let sourceRevision = projectedSourceRevision(candidate);
343
+ let refresh;
344
+ try {
345
+ refresh = await deps.workSource.refreshForDispatch?.({ projection: candidate });
346
+ }
347
+ catch {
348
+ return { status: 'idle' };
349
+ }
350
+ if (refresh !== undefined && refresh !== null) {
351
+ if (refresh.sourceExists === false) {
352
+ return { status: 'idle' };
353
+ }
354
+ sourceRevision = refresh.sourceRevision;
355
+ if (refresh.events.length > 0) {
356
+ await ingestInboundEvents(refresh.events);
357
+ candidate = (await deps.stateStore.readIssueState(candidate.workItemKey)) ?? candidate;
358
+ }
359
+ }
360
+ if (policy.resolveNextEligibleAction(candidate, deps.config) === null) {
361
+ return { status: 'idle' };
362
+ }
321
363
  const workflow = workflowForProjection(candidate, deps.config);
322
364
  if (workflow === null) {
323
365
  return { status: 'idle' };
@@ -444,6 +486,9 @@ export function createTickRunner(deps) {
444
486
  if (routing === null) {
445
487
  return { status: 'idle' };
446
488
  }
489
+ if (!(await hasSchedulerCapacity(deps.clock.now()))) {
490
+ return { status: 'idle' };
491
+ }
447
492
  const runId = `run-${candidate.issue.number}-${deps.clock.now().getTime()}`;
448
493
  const lease = createRunLease({ clock: deps.clock, ownerInstanceId });
449
494
  const workerIdentity = currentProcessIdentity();
@@ -461,6 +506,9 @@ export function createTickRunner(deps) {
461
506
  lease,
462
507
  workerPid: workerIdentity.pid,
463
508
  workerProcessStartedAt: workerIdentity.processStartedAt,
509
+ metadata: {
510
+ sourceRevision,
511
+ },
464
512
  };
465
513
  await deps.stateStore.writeRunRecord(runningRecord);
466
514
  async function transitionRunLifecycle(lifecycle) {
@@ -489,6 +537,7 @@ export function createTickRunner(deps) {
489
537
  action,
490
538
  priorStage: candidate.wake.stage,
491
539
  claimedStage,
540
+ sourceRevision,
492
541
  },
493
542
  });
494
543
  await deps.stateStore.appendEventEnvelope(claimedEvent);
@@ -768,6 +817,7 @@ export function createTickRunner(deps) {
768
817
  executionOutcome: 'PROCESS_FAILED',
769
818
  summary: err instanceof Error ? err.message : String(err),
770
819
  metadata: {
820
+ ...failedRecord.metadata,
771
821
  failureClass: 'infra',
772
822
  },
773
823
  });
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g18ca5a7";
127
+ export const wakeVersion = "gd481d17";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {