@atolis-hq/wake 0.2.51 → 0.2.52

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.
@@ -110,7 +110,7 @@ export function createPolicyEngine() {
110
110
  const handledCommentId = typeof context.lastHandledCommentId === 'string' ? context.lastHandledCommentId : undefined;
111
111
  const lastCompletedAction = typeof context.lastCompletedAction === 'string' ? context.lastCompletedAction : undefined;
112
112
  const lastRunSentinel = typeof context.lastRunSentinel === 'string' ? context.lastRunSentinel : undefined;
113
- const lastFailureClass = typeof context.lastFailureClass === 'string' ? context.lastFailureClass : undefined;
113
+ const lastRetrySafety = typeof context.lastRetrySafety === 'string' ? context.lastRetrySafety : undefined;
114
114
  if (issue.wake.lastRunId === undefined) {
115
115
  return true;
116
116
  }
@@ -122,15 +122,15 @@ export function createPolicyEngine() {
122
122
  if (isAwaitingApproval(issue)) {
123
123
  return false;
124
124
  }
125
- if (lastRunSentinel === failedRunnerSentinel && lastFailureClass !== 'quota') {
125
+ if (lastRunSentinel === failedRunnerSentinel) {
126
+ if (lastRetrySafety === 'SAFE_TO_RETRY' || lastRetrySafety === 'SAFE_TO_RESUME') {
127
+ return belowFailureRetryLimit(issue, config);
128
+ }
126
129
  return false;
127
130
  }
128
131
  if (lastRunSentinel === 'BLOCKED') {
129
132
  return false;
130
133
  }
131
- if (lastFailureClass === 'quota') {
132
- return belowFailureRetryLimit(issue, config);
133
- }
134
134
  const workflowAction = chooseWorkflowAction(issue, workflow);
135
135
  return workflowAction !== null && lastCompletedAction !== workflowAction.action;
136
136
  },
@@ -253,6 +253,17 @@ async function applyEvent(current, event, ctx, config) {
253
253
  ...(payload.workflowOutcome !== undefined
254
254
  ? { lastWorkflowOutcome: payload.workflowOutcome }
255
255
  : {}),
256
+ ...(payload.failurePhase !== undefined ? { lastFailurePhase: payload.failurePhase } : {}),
257
+ ...(payload.processStarted !== undefined
258
+ ? { lastProcessStarted: payload.processStarted }
259
+ : {}),
260
+ ...(payload.workspaceChanged !== undefined
261
+ ? { lastWorkspaceChanged: payload.workspaceChanged }
262
+ : {}),
263
+ ...(payload.externalSideEffects !== undefined
264
+ ? { lastExternalSideEffects: payload.externalSideEffects }
265
+ : {}),
266
+ ...(payload.retrySafety !== undefined ? { lastRetrySafety: payload.retrySafety } : {}),
256
267
  };
257
268
  if (payload.sentinel === 'BLOCKED' || payload.sentinel === 'FAILED') {
258
269
  nextContext.blockedFromStage = current.wake.stage;
@@ -26,6 +26,37 @@ export function createStaleRunReconciler(deps) {
26
26
  return 'SUPERSEDED';
27
27
  }
28
28
  }
29
+ function failurePhaseForLifecycle(lifecycle) {
30
+ switch (lifecycle) {
31
+ case 'CREATED':
32
+ case 'CLAIMED':
33
+ case 'PREPARING':
34
+ return 'workspace-prep';
35
+ case 'PROCESS_STARTING':
36
+ return 'process-starting';
37
+ case 'RUNNING':
38
+ case 'FINALISING':
39
+ return 'running';
40
+ case 'TERMINAL':
41
+ return 'unknown';
42
+ }
43
+ }
44
+ function classifyReconciledFailure(record) {
45
+ const processStarted = record.agentPid !== undefined ||
46
+ record.agentProcessStartedAt !== undefined ||
47
+ record.lifecycle === 'RUNNING' ||
48
+ record.lifecycle === 'FINALISING';
49
+ const metadata = record.metadata;
50
+ const workspaceChanged = typeof metadata?.workspacePath === 'string';
51
+ const retrySafety = processStarted || workspaceChanged ? 'REQUIRES_RECONCILIATION' : 'SAFE_TO_RETRY';
52
+ return {
53
+ failurePhase: failurePhaseForLifecycle(record.lifecycle),
54
+ processStarted,
55
+ workspaceChanged,
56
+ externalSideEffects: processStarted ? 'unknown' : 'none',
57
+ retrySafety,
58
+ };
59
+ }
29
60
  async function staleReason(record, now) {
30
61
  if (record.status !== 'running') {
31
62
  return null;
@@ -94,6 +125,11 @@ export function createStaleRunReconciler(deps) {
94
125
  action: projection.context.lastRunAction,
95
126
  sentinel: 'FAILED',
96
127
  executionOutcome: 'CANCELED_BY_RECONCILIATION',
128
+ failurePhase: 'unknown',
129
+ processStarted: false,
130
+ workspaceChanged: false,
131
+ externalSideEffects: 'unknown',
132
+ retrySafety: 'MANUAL_REVIEW_REQUIRED',
97
133
  runId,
98
134
  reason: 'runner:missing-run-record',
99
135
  },
@@ -146,6 +182,7 @@ export function createStaleRunReconciler(deps) {
146
182
  continue;
147
183
  }
148
184
  const staleExecutionOutcome = reason === 'timeout' ? 'TIMED_OUT' : recoveryOutcomeForLifecycle(record.lifecycle);
185
+ const failureContext = classifyReconciledFailure(record);
149
186
  await deps.stateStore.writeRunRecord({
150
187
  ...record,
151
188
  lifecycle: 'TERMINAL',
@@ -153,6 +190,7 @@ export function createStaleRunReconciler(deps) {
153
190
  finishedAt,
154
191
  sentinel: 'FAILED',
155
192
  executionOutcome: staleExecutionOutcome,
193
+ ...failureContext,
156
194
  summary: `Run exceeded timeout while marked running and was reconciled by a later tick.`,
157
195
  metadata: {
158
196
  ...record.metadata,
@@ -160,6 +198,7 @@ export function createStaleRunReconciler(deps) {
160
198
  recoveryLifecycle: record.lifecycle,
161
199
  staleReason: reason,
162
200
  timeoutMs: deps.runnerTimeoutMs(),
201
+ ...failureContext,
163
202
  },
164
203
  });
165
204
  const runCompletedEvent = createEventEnvelope({
@@ -181,6 +220,7 @@ export function createStaleRunReconciler(deps) {
181
220
  action: record.action,
182
221
  sentinel: 'FAILED',
183
222
  executionOutcome: staleExecutionOutcome,
223
+ ...failureContext,
184
224
  runId: record.runId,
185
225
  reason: reason === 'timeout'
186
226
  ? 'runner:stale-timeout'
@@ -52,6 +52,54 @@ function shouldPublishRunResult(input) {
52
52
  }
53
53
  return input.failureClass !== input.previousFailureClass;
54
54
  }
55
+ function hasConfirmedExternalSideEffect(projection) {
56
+ return projection.correlatedResources.some((resource) => /^[a-z0-9-]+:pr:/.test(resource.resourceUri));
57
+ }
58
+ function failurePhaseForRecord(record) {
59
+ if (record.agentPid !== undefined || record.agentProcessStartedAt !== undefined) {
60
+ return 'running';
61
+ }
62
+ if (record.lifecycle === 'PROCESS_STARTING' || record.lifecycle === 'RUNNING') {
63
+ return 'process-starting';
64
+ }
65
+ if (record.lifecycle === 'PREPARING') {
66
+ return 'workspace-prep';
67
+ }
68
+ return 'unknown';
69
+ }
70
+ function classifyFailedRun(input) {
71
+ const processStarted = input.record.agentPid !== undefined || input.record.agentProcessStartedAt !== undefined;
72
+ const workspaceChanged = input.workspacePath !== undefined;
73
+ const externalSideEffects = hasConfirmedExternalSideEffect(input.projection)
74
+ ? 'confirmed'
75
+ : processStarted
76
+ ? 'unknown'
77
+ : 'none';
78
+ const failurePhase = input.failurePhase ??
79
+ (input.envelope === 'degraded' && input.sentinel === 'FAILED'
80
+ ? 'result-parsing'
81
+ : failurePhaseForRecord(input.record));
82
+ let retrySafety;
83
+ if (input.failureClass === 'task') {
84
+ retrySafety = 'NOT_RETRYABLE';
85
+ }
86
+ else if (externalSideEffects === 'confirmed' || workspaceChanged) {
87
+ retrySafety = 'REQUIRES_RECONCILIATION';
88
+ }
89
+ else if (processStarted) {
90
+ retrySafety = 'SAFE_TO_RESUME';
91
+ }
92
+ else {
93
+ retrySafety = 'SAFE_TO_RETRY';
94
+ }
95
+ return {
96
+ failurePhase,
97
+ processStarted,
98
+ workspaceChanged,
99
+ externalSideEffects,
100
+ retrySafety,
101
+ };
102
+ }
55
103
  export function createTickRunner(deps) {
56
104
  const policy = createPolicyEngine();
57
105
  const lifecycle = createLifecycleService();
@@ -1464,6 +1512,16 @@ export function createTickRunner(deps) {
1464
1512
  : undefined;
1465
1513
  await transitionRunLifecycle('FINALISING');
1466
1514
  const finalisingRecord = (await deps.stateStore.readRunRecord(runId));
1515
+ const failureContext = sentinel === 'FAILED'
1516
+ ? classifyFailedRun({
1517
+ projection: candidate,
1518
+ record: finalisingRecord,
1519
+ failureClass: runnerResult.failureClass ?? 'task',
1520
+ ...(workspacePath === undefined ? {} : { workspacePath }),
1521
+ envelope: parsedRunnerResult.envelope,
1522
+ sentinel,
1523
+ })
1524
+ : undefined;
1467
1525
  await deps.stateStore.writeRunRecord({
1468
1526
  ...finalisingRecord,
1469
1527
  lifecycle: 'TERMINAL',
@@ -1479,6 +1537,7 @@ export function createTickRunner(deps) {
1479
1537
  sentinel,
1480
1538
  executionOutcome,
1481
1539
  ...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
1540
+ ...(failureContext === undefined ? {} : failureContext),
1482
1541
  summary: parsedRunnerResult.body,
1483
1542
  ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
1484
1543
  ...(runnerResult.tokenUsage === undefined ? {} : { tokenUsage: runnerResult.tokenUsage }),
@@ -1519,6 +1578,7 @@ export function createTickRunner(deps) {
1519
1578
  ...(runnerResult.failureClass === undefined
1520
1579
  ? {}
1521
1580
  : { failureClass: runnerResult.failureClass }),
1581
+ ...(failureContext === undefined ? {} : failureContext),
1522
1582
  // Only mark the triggering comment handled when the run reached the
1523
1583
  // agent and produced a real outcome. Quota/infra failures are transient
1524
1584
  // blips, not an answer to the human's comment — leaving handledCommentId
@@ -1612,6 +1672,18 @@ export function createTickRunner(deps) {
1612
1672
  const finishedAt = deps.clock.now().toISOString();
1613
1673
  const sentinel = 'FAILED';
1614
1674
  const failedRecord = (await deps.stateStore.readRunRecord(runId)) ?? runningRecord;
1675
+ const failedRecordMetadata = failedRecord.metadata;
1676
+ const failedRecordWorkspacePath = typeof failedRecordMetadata?.workspacePath === 'string'
1677
+ ? failedRecordMetadata.workspacePath
1678
+ : undefined;
1679
+ const failureContext = classifyFailedRun({
1680
+ projection: candidate,
1681
+ record: failedRecord,
1682
+ failureClass: 'infra',
1683
+ ...(failedRecordWorkspacePath === undefined
1684
+ ? {}
1685
+ : { workspacePath: failedRecordWorkspacePath }),
1686
+ });
1615
1687
  await deps.stateStore.writeRunRecord({
1616
1688
  ...failedRecord,
1617
1689
  lifecycle: 'TERMINAL',
@@ -1619,10 +1691,12 @@ export function createTickRunner(deps) {
1619
1691
  finishedAt,
1620
1692
  sentinel,
1621
1693
  executionOutcome: 'PROCESS_FAILED',
1694
+ ...failureContext,
1622
1695
  summary: err instanceof Error ? err.message : String(err),
1623
1696
  metadata: {
1624
1697
  ...failedRecord.metadata,
1625
1698
  failureClass: 'infra',
1699
+ ...failureContext,
1626
1700
  },
1627
1701
  });
1628
1702
  const runCompletedEvent = createEventEnvelope({
@@ -1646,6 +1720,7 @@ export function createTickRunner(deps) {
1646
1720
  runId,
1647
1721
  reason: 'runner:infrastructure-error',
1648
1722
  failureClass: 'infra',
1723
+ ...failureContext,
1649
1724
  executionOutcome: 'PROCESS_FAILED',
1650
1725
  // Deliberately omit handledCommentId: an infra blip (CLI crash, timeout,
1651
1726
  // network error) never reached the agent, so it isn't an answer to the
@@ -326,6 +326,22 @@ const runLeaseSchema = z.object({
326
326
  lastRenewedAt: isoTimestampSchema,
327
327
  expiresAt: isoTimestampSchema,
328
328
  });
329
+ export const failurePhaseSchema = z.enum([
330
+ 'workspace-prep',
331
+ 'process-starting',
332
+ 'running',
333
+ 'result-parsing',
334
+ 'publishing',
335
+ 'unknown',
336
+ ]);
337
+ export const externalSideEffectsSchema = z.enum(['none', 'confirmed', 'unknown']);
338
+ export const retrySafetySchema = z.enum([
339
+ 'SAFE_TO_RETRY',
340
+ 'SAFE_TO_RESUME',
341
+ 'REQUIRES_RECONCILIATION',
342
+ 'MANUAL_REVIEW_REQUIRED',
343
+ 'NOT_RETRYABLE',
344
+ ]);
329
345
  function legacyRunLifecycle(input) {
330
346
  if (input.lifecycle !== undefined) {
331
347
  return input;
@@ -369,6 +385,11 @@ export const runRecordSchema = z.preprocess((input) => {
369
385
  sentinel: runnerSentinelSchema.optional(),
370
386
  executionOutcome: executionOutcomeSchema.optional(),
371
387
  workflowOutcome: workflowOutcomeSchema.optional(),
388
+ failurePhase: failurePhaseSchema.optional(),
389
+ processStarted: z.boolean().optional(),
390
+ workspaceChanged: z.boolean().optional(),
391
+ externalSideEffects: externalSideEffectsSchema.optional(),
392
+ retrySafety: retrySafetySchema.optional(),
372
393
  summary: z.string().optional(),
373
394
  routing: runnerRoutingSchema.optional(),
374
395
  lease: runLeaseSchema.optional(),
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g1a18fca";
127
+ export const wakeVersion = "g572b993";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.51",
3
+ "version": "0.2.52",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {