@atolis-hq/wake 0.3.28 → 0.3.30

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.
@@ -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 = "g69f927b";
111
+ export const wakeVersion = "ga9fe8dd";
@@ -4,6 +4,7 @@ import { BuiltInResourceKind, ResourceCorrelationRole, ResourceStreamKind, resou
4
4
  import { WorkStatus } from '../../../work/index.js';
5
5
  import { UnknownGitHubIdentity } from '../contracts/vocabulary.js';
6
6
  import { commandContext } from './inbound-context.js';
7
+ import { ignoreIneligibleOperatorRetry } from './operator-retry-command.js';
7
8
  import { translateGitHubReviewCommand } from './review-command-translator.js';
8
9
  export async function applyReviewSignal(input) {
9
10
  const { event, journal, resources, work, lookup, adapter, orchestration } = input;
@@ -116,7 +117,7 @@ async function applyIssueRetrySignal(input) {
116
117
  const item = await work.get(workflow.workItemId);
117
118
  if (item === null || item.deleted || item.frozen || item.state !== WorkStatus.Open)
118
119
  continue;
119
- await orchestration.retryBlockedFailedStage(workflow.workflowInstanceId, commandContext(event));
120
+ await ignoreIneligibleOperatorRetry(() => orchestration.retryBlockedFailedStage(workflow.workflowInstanceId, commandContext(event)));
120
121
  }
121
122
  }
122
123
  async function applyIssueApprovalSignal(input) {
@@ -1,12 +1,12 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { ActivityEventType, createPullRequestService, } from '../../../activities/index.js';
3
- import { UlidIdGenerator, } from '../../../kernel/index.js';
3
+ import { createEventDraft, EventActorKind, EventSourceKind, UlidIdGenerator, WrongExpectedSequenceError, } from '../../../kernel/index.js';
4
4
  import { BuiltInResourceCapability, BuiltInResourceKind, ResourceCorrelationRole, ResourceEventType, resourceId, resourceStream, } from '../../../resources/index.js';
5
5
  import { workItemId } from '../../../work/index.js';
6
6
  import { admitObservedWork } from '../../application/work-admission.js';
7
7
  import { concludeObservedWork } from '../../application/work-conclusion.js';
8
8
  import { evaluateIntakeRules } from '../../contracts/intake-rules.js';
9
- import { deliveryStream } from '../../contracts/streams.js';
9
+ import { deliveryStream, integrationStream } from '../../contracts/streams.js';
10
10
  import { DeliveryEventType, selectDeliveryEvent } from '../../delivery/contracts/events.js';
11
11
  import { GitHubEventType, selectGitHubAdapterEvent } from '../contracts/events.js';
12
12
  import { GitHubAdapter } from '../contracts/vocabulary.js';
@@ -117,6 +117,10 @@ export class InboundTranslator {
117
117
  const identity = await this.resolveIdentity({ adapter: this.adapter, key: payload.externalKey }, intake.admitted);
118
118
  if (identity === null)
119
119
  return;
120
+ if (identity.deleted) {
121
+ await this.recordDeletedWorkObservation(event, identity.workItemId);
122
+ return;
123
+ }
120
124
  if (!identity.created) {
121
125
  await this.applyReobservation({
122
126
  payload,
@@ -244,25 +248,85 @@ export class InboundTranslator {
244
248
  const key = `${externalKey.adapter}:${externalKey.key}`;
245
249
  const inBatch = this.minted.get(key);
246
250
  if (inBatch !== undefined)
247
- return { ...inBatch, created: false };
251
+ return this.inBatchIdentity(inBatch);
248
252
  if (this.lookup === undefined)
249
253
  throw new Error('InboundTranslator lookup is required');
250
254
  const resourceIdValue = await this.lookup.resourceIdForExternalKey(externalKey);
251
- if (resourceIdValue !== null) {
252
- if (this.resources === undefined)
253
- throw new Error('InboundTranslator resources are required');
254
- const correlation = (await this.resources.correlations(resourceIdValue)).find((value) => value.role === ResourceCorrelationRole.Primary);
255
- // A deleted WorkItem retracts its primary correlation while retaining the external
256
- // resource as a durable tombstone. Do not remint it or let it block later intake.
257
- if (correlation === undefined)
258
- return null;
259
- const identity = { resourceId: resourceIdValue, workItemId: correlation.workItemId };
260
- this.minted.set(key, identity);
261
- return { ...identity, created: false };
262
- }
255
+ if (resourceIdValue !== null)
256
+ return this.correlatedIdentity(key, resourceIdValue);
263
257
  // An ineligible object Wake has never seen produces no WorkItem, Run, or effect.
264
258
  if (!admitted)
265
259
  return null;
266
- return { ...this.mintIdentity(externalKey), created: true };
260
+ return { ...this.mintIdentity(externalKey), created: true, deleted: false };
261
+ }
262
+ async inBatchIdentity(identity) {
263
+ const existing = await this.work?.get(identity.workItemId);
264
+ return { ...identity, created: false, deleted: existing?.deleted === true };
265
+ }
266
+ async correlatedIdentity(key, resourceIdValue) {
267
+ if (this.resources === undefined)
268
+ throw new Error('InboundTranslator resources are required');
269
+ const correlation = (await this.resources.correlations(resourceIdValue)).find((value) => value.role === ResourceCorrelationRole.Primary);
270
+ if (correlation === undefined)
271
+ return this.historicalIdentity(resourceIdValue);
272
+ const identity = { resourceId: resourceIdValue, workItemId: correlation.workItemId };
273
+ this.minted.set(key, identity);
274
+ return { ...identity, created: false, deleted: false };
275
+ }
276
+ async historicalIdentity(resourceIdValue) {
277
+ const historical = await this.resources.primaryCorrelation(resourceIdValue);
278
+ if (historical === null)
279
+ throw new Error(`Resource ${resourceIdValue} has no primary correlation`);
280
+ const historicalWork = await this.work?.get(historical.workItemId);
281
+ if (historicalWork?.deleted)
282
+ return {
283
+ resourceId: resourceIdValue,
284
+ workItemId: historical.workItemId,
285
+ created: false,
286
+ deleted: true,
287
+ };
288
+ throw new Error(`Resource ${resourceIdValue} has no active primary correlation`);
289
+ }
290
+ async recordDeletedWorkObservation(event, workItemId) {
291
+ const stream = integrationStream(this.adapter);
292
+ const eventId = `github:deleted-work-skip:${event.eventId}:${workItemId}`;
293
+ const payload = {
294
+ externalKey: event.payload.externalKey,
295
+ workItemId,
296
+ sourceEventId: event.eventId,
297
+ revision: event.payload.revision,
298
+ reason: 'work-item-deleted',
299
+ };
300
+ for (;;) {
301
+ const existing = await this.journal.readStream(stream);
302
+ const recorded = existing.find((candidate) => candidate.eventId === eventId);
303
+ if (recorded !== undefined) {
304
+ const diagnostic = selectGitHubAdapterEvent(recorded);
305
+ if (diagnostic?.eventType !== GitHubEventType.DeletedWorkObservationSkipped ||
306
+ JSON.stringify(diagnostic.payload) !== JSON.stringify(payload))
307
+ throw new Error(`Event id ${eventId} has already been used with different content`);
308
+ return;
309
+ }
310
+ try {
311
+ await this.journal.append(stream, existing.length, [
312
+ createEventDraft({
313
+ eventId,
314
+ eventType: GitHubEventType.DeletedWorkObservationSkipped,
315
+ occurredAt: event.occurredAt,
316
+ correlationId: event.correlationId,
317
+ causationId: event.eventId,
318
+ actor: { kind: EventActorKind.Integration, id: this.adapter },
319
+ source: { kind: EventSourceKind.Internal, id: 'github-inbound-translator' },
320
+ stream,
321
+ payload,
322
+ }),
323
+ ]);
324
+ return;
325
+ }
326
+ catch (error) {
327
+ if (!(error instanceof WrongExpectedSequenceError))
328
+ throw error;
329
+ }
330
+ }
267
331
  }
268
332
  }
@@ -0,0 +1,11 @@
1
+ import { OperatorRetryIneligibleError } from '../../../orchestration/index.js';
2
+ export async function ignoreIneligibleOperatorRetry(retry) {
3
+ try {
4
+ await retry();
5
+ }
6
+ catch (error) {
7
+ if (error instanceof OperatorRetryIneligibleError)
8
+ return;
9
+ throw error;
10
+ }
11
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { ProviderPermission, PullRequestCheckState, PullRequestState, ReviewActorKind, ReviewerAuthorizationSource, } from '../../../activities/index.js';
3
- import { eventEnvelopeSchema, } from '../../../kernel/index.js';
3
+ import { brandedStringSchema, eventEnvelopeSchema, } from '../../../kernel/index.js';
4
+ import { workItemId } from '../../../work/index.js';
4
5
  import { adapterId } from '../../contracts/identifiers.js';
5
6
  import { ExternalWorkOutcome } from '../../contracts/outcome-vocabulary.js';
6
7
  import { IntegrationStreamKind } from '../../contracts/streams.js';
@@ -8,6 +9,7 @@ export const GitHubEventType = {
8
9
  WorkObserved: 'integration.github.work-observed',
9
10
  CommentObserved: 'integration.github.comment-observed',
10
11
  DeliveryObserved: 'integration.github.delivery-observed',
12
+ DeletedWorkObservationSkipped: 'integration.github.deleted-work-observation-skipped',
11
13
  };
12
14
  const streamSchema = z
13
15
  .object({
@@ -114,6 +116,19 @@ const eventSchema = z.discriminatedUnion('eventType', [
114
116
  stream: streamSchema,
115
117
  payload: z.object({ deliveryId: z.string(), raw: rawSchema }).strict(),
116
118
  }),
119
+ eventEnvelopeSchema.extend({
120
+ eventType: z.literal(GitHubEventType.DeletedWorkObservationSkipped),
121
+ stream: streamSchema,
122
+ payload: z
123
+ .object({
124
+ externalKey: z.string(),
125
+ workItemId: brandedStringSchema(workItemId),
126
+ sourceEventId: z.string(),
127
+ revision: z.string(),
128
+ reason: z.literal('work-item-deleted'),
129
+ })
130
+ .strict(),
131
+ }),
117
132
  ]);
118
133
  export function decodeGitHubAdapterEvent(event) {
119
134
  const result = eventSchema.safeParse(event);
@@ -1,3 +1,6 @@
1
+ import { ResourceEventType, selectResourceEvent } from '../contracts/events.js';
2
+ import { resourceStream } from '../contracts/streams.js';
3
+ import { ResourceCorrelationRole } from '../contracts/vocabulary.js';
1
4
  import { externalKeyProjectionKey, resourcesByExternalKeyProjection, workCorrelationsProjection, } from './lookup-projections.js';
2
5
  export function createResourceLookup(dependencies) {
3
6
  const seeded = async (definition, key) => {
@@ -12,5 +15,32 @@ export function createResourceLookup(dependencies) {
12
15
  return {
13
16
  resourceIdForExternalKey: (externalKey) => seeded(resourcesByExternalKeyProjection, externalKeyProjectionKey(externalKey)),
14
17
  correlationsForWork: (workItemId) => seeded(workCorrelationsProjection, workItemId),
18
+ async primaryCorrelation(resourceId) {
19
+ const active = new Map();
20
+ const retracted = [];
21
+ for (const event of await dependencies.journal.readStream(resourceStream(resourceId))) {
22
+ const owned = selectResourceEvent(event);
23
+ if (owned?.eventType === ResourceEventType.WorkCorrelationEstablished) {
24
+ active.set(owned.payload.workItemId, {
25
+ resourceId,
26
+ workItemId: owned.payload.workItemId,
27
+ role: owned.payload.role,
28
+ provenance: owned.payload.provenance,
29
+ establishedByEventId: owned.eventId,
30
+ });
31
+ }
32
+ if (owned?.eventType === ResourceEventType.WorkCorrelationRetracted) {
33
+ const correlation = active.get(owned.payload.workItemId);
34
+ if (correlation !== undefined) {
35
+ active.delete(owned.payload.workItemId);
36
+ if (correlation.role === ResourceCorrelationRole.Primary)
37
+ retracted.push(correlation);
38
+ }
39
+ }
40
+ }
41
+ return ([...active.values()].find((value) => value.role === ResourceCorrelationRole.Primary) ??
42
+ retracted.at(-1) ??
43
+ null);
44
+ },
15
45
  };
16
46
  }
@@ -17,6 +17,7 @@ export function createResourceService(journal, lookup) {
17
17
  async correlations(resourceId) {
18
18
  return (await repository.load(resourceId)).resource?.correlations ?? [];
19
19
  },
20
+ primaryCorrelation: (resourceId) => lookup.primaryCorrelation(resourceId),
20
21
  correlationsForWork: (workItemId) => lookup.correlationsForWork(workItemId),
21
22
  correlate: (resourceId, workItemId, role, context, provenance) => correlateResource(repository, resourceId, workItemId, role, context, provenance),
22
23
  async retract(resourceId, workItemId, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {