@atolis-hq/wake 0.2.45 → 0.2.47

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.
@@ -3,6 +3,7 @@ import { runAgentCliCommand } from '../runner/cli-command.js';
3
3
  import { buildStagePrompt } from '../runner/stage-prompt.js';
4
4
  import { emitRuntimeEvent, runnerRuntimeEvent } from '../runner/runtime-events.js';
5
5
  import { writeRunnerTranscript } from '../runner/transcripts.js';
6
+ import { createAgentExecution } from '../../core/live-execution.js';
6
7
  export { buildStagePrompt } from '../runner/stage-prompt.js';
7
8
  function slugify(value, maxLength = 40) {
8
9
  return value
@@ -152,182 +153,109 @@ function readSandboxLogBreadcrumb() {
152
153
  };
153
154
  }
154
155
  export function createClaudeRunner(options) {
155
- return {
156
- async run(input) {
157
- const sessionName = buildWakeSessionName({
158
- sessionName: options.settings.sessionName,
159
- issueNumber: input.projection.issue.number,
160
- title: input.projection.issue.title,
161
- runId: input.runId,
162
- });
163
- // Resume an in-progress session when the projection carries a session ID
164
- // that was created by this same CLI. This happens when the previous run
165
- // ended with BLOCKED and the same action is being retried after a human
166
- // reply. Any forward-stage transition or FAILED run clears the stored
167
- // session ID so that the next action always starts fresh.
168
- const priorSessionId = input.projection.wake.sessionId;
169
- const priorSessionCli = input.projection.wake.sessionCli;
170
- const isResume = priorSessionId !== undefined && priorSessionCli === CLAUDE_CLI_NAME;
171
- const stagePrompt = await buildStagePrompt({
172
- action: input.action,
173
- projection: input.projection,
174
- mode: isResume ? 'resume' : 'start',
175
- config: input.config,
176
- ...(input.promptContextOverrides === undefined
177
- ? {}
178
- : { contextOverrides: input.promptContextOverrides }),
179
- ...(input.mergeConflictDetected === true ? { mergeConflictDetected: true } : {}),
180
- ...(input.upstreamChanges === undefined ? {} : { upstreamChanges: input.upstreamChanges }),
181
- });
182
- const model = resolveModel({
183
- action: input.action,
184
- settings: options.settings,
185
- });
186
- const args = buildClaudePrintArgs({
187
- model,
188
- prompt: stagePrompt.prompt,
189
- systemPrompt: stagePrompt.harnessPrompt,
190
- sessionName,
191
- allowedTools: stagePrompt.allowedTools,
192
- extraArgs: stagePrompt.extraArgs,
193
- maxTurns: stagePrompt.maxTurns,
194
- ...(stagePrompt.permissionMode === undefined
195
- ? {}
196
- : { permissionMode: stagePrompt.permissionMode }),
197
- ...(options.settings.remoteControl.enabled ? { remoteControlName: sessionName } : {}),
198
- ...(options.settings.effort === undefined ? {} : { effort: options.settings.effort }),
199
- ...(isResume ? { resumeSessionId: priorSessionId } : {}),
200
- });
201
- const promptTranscriptPath = await writeRunnerTranscript({
202
- config: input.config,
203
- projection: input.projection,
204
- runId: input.runId,
205
- action: input.action,
206
- cli: CLAUDE_CLI_NAME,
207
- kind: 'prompt',
208
- text: stagePrompt.prompt,
209
- });
210
- console.log(formatClaudeRunLogLine({
211
- phase: 'start',
212
- runId: input.runId,
213
- action: input.action,
214
- issueNumber: input.projection.issue.number,
215
- repo: input.projection.issue.repo,
216
- recentEventIds: input.recentEvents.map((event) => event.eventId),
217
- model,
218
- ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
219
- }));
220
- const result = await runClaudeCommand({
221
- command: options.command,
222
- args,
223
- cwd: input.workspacePath ?? options.cwd,
224
- timeoutMs: options.settings.timeoutMs,
225
- onProcessStart: async (identity) => {
226
- await input.onProcessStart?.(identity);
227
- await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
228
- type: 'agent.process.started',
229
- runId: input.runId,
230
- projection: input.projection,
231
- routing: input.routing,
232
- cli: CLAUDE_CLI_NAME,
233
- model,
234
- payload: { pid: identity.pid, processStartedAt: identity.processStartedAt },
235
- }));
236
- },
237
- });
238
- const responseTranscriptPath = await writeRunnerTranscript({
239
- config: input.config,
240
- projection: input.projection,
241
- runId: input.runId,
242
- action: input.action,
243
- cli: CLAUDE_CLI_NAME,
244
- kind: 'response',
245
- text: result.stdout,
246
- });
247
- if (result.exitCode !== 0 || result.timedOut || result.stdout.trim().length === 0) {
248
- const sandboxLog = readSandboxLogBreadcrumb();
249
- const failureClass = classifyClaudeCliFailure({
250
- stdout: result.stdout,
251
- stderr: result.stderr,
252
- timedOut: result.timedOut,
253
- });
254
- console.error(formatClaudeRunLogLine({
255
- phase: 'failure',
256
- runId: input.runId,
257
- action: input.action,
258
- issueNumber: input.projection.issue.number,
259
- repo: input.projection.issue.repo,
260
- recentEventIds: input.recentEvents.map((event) => event.eventId),
261
- model,
262
- ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
263
- exitCode: result.exitCode,
264
- }));
156
+ async function executeRun(input) {
157
+ const sessionName = buildWakeSessionName({
158
+ sessionName: options.settings.sessionName,
159
+ issueNumber: input.projection.issue.number,
160
+ title: input.projection.issue.title,
161
+ runId: input.runId,
162
+ });
163
+ // Resume an in-progress session when the projection carries a session ID
164
+ // that was created by this same CLI. This happens when the previous run
165
+ // ended with BLOCKED and the same action is being retried after a human
166
+ // reply. Any forward-stage transition or FAILED run clears the stored
167
+ // session ID so that the next action always starts fresh.
168
+ const priorSessionId = input.projection.wake.sessionId;
169
+ const priorSessionCli = input.projection.wake.sessionCli;
170
+ const isResume = priorSessionId !== undefined && priorSessionCli === CLAUDE_CLI_NAME;
171
+ const stagePrompt = await buildStagePrompt({
172
+ action: input.action,
173
+ projection: input.projection,
174
+ mode: isResume ? 'resume' : 'start',
175
+ config: input.config,
176
+ ...(input.promptContextOverrides === undefined
177
+ ? {}
178
+ : { contextOverrides: input.promptContextOverrides }),
179
+ ...(input.mergeConflictDetected === true ? { mergeConflictDetected: true } : {}),
180
+ ...(input.upstreamChanges === undefined ? {} : { upstreamChanges: input.upstreamChanges }),
181
+ });
182
+ const model = resolveModel({
183
+ action: input.action,
184
+ settings: options.settings,
185
+ });
186
+ const args = buildClaudePrintArgs({
187
+ model,
188
+ prompt: stagePrompt.prompt,
189
+ systemPrompt: stagePrompt.harnessPrompt,
190
+ sessionName,
191
+ allowedTools: stagePrompt.allowedTools,
192
+ extraArgs: stagePrompt.extraArgs,
193
+ maxTurns: stagePrompt.maxTurns,
194
+ ...(stagePrompt.permissionMode === undefined
195
+ ? {}
196
+ : { permissionMode: stagePrompt.permissionMode }),
197
+ ...(options.settings.remoteControl.enabled ? { remoteControlName: sessionName } : {}),
198
+ ...(options.settings.effort === undefined ? {} : { effort: options.settings.effort }),
199
+ ...(isResume ? { resumeSessionId: priorSessionId } : {}),
200
+ });
201
+ const promptTranscriptPath = await writeRunnerTranscript({
202
+ config: input.config,
203
+ projection: input.projection,
204
+ runId: input.runId,
205
+ action: input.action,
206
+ cli: CLAUDE_CLI_NAME,
207
+ kind: 'prompt',
208
+ text: stagePrompt.prompt,
209
+ });
210
+ console.log(formatClaudeRunLogLine({
211
+ phase: 'start',
212
+ runId: input.runId,
213
+ action: input.action,
214
+ issueNumber: input.projection.issue.number,
215
+ repo: input.projection.issue.repo,
216
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
217
+ model,
218
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
219
+ }));
220
+ const result = await runClaudeCommand({
221
+ command: options.command,
222
+ args,
223
+ cwd: input.workspacePath ?? options.cwd,
224
+ timeoutMs: options.settings.timeoutMs,
225
+ ...(input.cancellationSignal === undefined
226
+ ? {}
227
+ : { cancellationSignal: input.cancellationSignal }),
228
+ onProcessStart: async (identity) => {
229
+ await input.onProcessStart?.(identity);
265
230
  await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
266
- type: 'agent.process.exited',
231
+ type: 'agent.process.started',
267
232
  runId: input.runId,
268
233
  projection: input.projection,
269
234
  routing: input.routing,
270
235
  cli: CLAUDE_CLI_NAME,
271
236
  model,
272
- payload: { exitCode: result.exitCode, timedOut: result.timedOut },
237
+ payload: { pid: identity.pid, processStartedAt: identity.processStartedAt },
273
238
  }));
274
- let parsedReason;
275
- let stdoutFailureDetail;
276
- const trimmedStdout = result.stdout.trim();
277
- try {
278
- if (trimmedStdout.length > 0) {
279
- parsedReason = parseClaudePrintOutput(result.stdout).result;
280
- }
281
- }
282
- catch {
283
- // stdout wasn't valid JSON, so it is likely a CLI-level error
284
- // message rather than a Claude print result.
285
- stdoutFailureDetail = trimmedStdout;
286
- }
287
- return {
288
- result: [
289
- result.timedOut
290
- ? `Claude runner timed out after ${options.settings.timeoutMs}ms and was killed`
291
- : trimmedStdout.length === 0
292
- ? 'Claude runner produced no output'
293
- : parsedReason !== undefined
294
- ? `Claude runner failed: ${parsedReason}`
295
- : 'Claude runner failed',
296
- result.stderr,
297
- stdoutFailureDetail,
298
- sandboxLog?.text,
299
- 'FAILED',
300
- ]
301
- .filter((part) => part !== undefined && part.length > 0)
302
- .join('\n'),
303
- model,
304
- cli: CLAUDE_CLI_NAME,
305
- failureClass,
306
- metadata: {
307
- stdout: result.stdout,
308
- stderr: result.stderr,
309
- exitCode: result.exitCode,
310
- failureClass,
311
- ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
312
- ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
313
- ...(sandboxLog?.metadata ?? {}),
314
- },
315
- };
316
- }
317
- const parsed = parseClaudePrintOutput(result.stdout);
239
+ },
240
+ });
241
+ const responseTranscriptPath = await writeRunnerTranscript({
242
+ config: input.config,
243
+ projection: input.projection,
244
+ runId: input.runId,
245
+ action: input.action,
246
+ cli: CLAUDE_CLI_NAME,
247
+ kind: 'response',
248
+ text: result.stdout,
249
+ });
250
+ if (result.exitCode !== 0 || result.timedOut || result.stdout.trim().length === 0) {
318
251
  const sandboxLog = readSandboxLogBreadcrumb();
319
- await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
320
- type: 'agent.progress',
321
- runId: input.runId,
322
- projection: input.projection,
323
- routing: input.routing,
324
- cli: CLAUDE_CLI_NAME,
325
- model,
326
- ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
327
- payload: { message: 'Claude print result received', raw: parsed },
328
- }));
329
- console.log(formatClaudeRunLogLine({
330
- phase: 'success',
252
+ const failureClass = classifyClaudeCliFailure({
253
+ stdout: result.stdout,
254
+ stderr: result.stderr,
255
+ timedOut: result.timedOut,
256
+ });
257
+ console.error(formatClaudeRunLogLine({
258
+ phase: 'failure',
331
259
  runId: input.runId,
332
260
  action: input.action,
333
261
  issueNumber: input.projection.issue.number,
@@ -335,21 +263,8 @@ export function createClaudeRunner(options) {
335
263
  recentEventIds: input.recentEvents.map((event) => event.eventId),
336
264
  model,
337
265
  ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
338
- ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
266
+ exitCode: result.exitCode,
339
267
  }));
340
- const tokenUsage = extractTokenUsage(parsed);
341
- if (tokenUsage !== undefined) {
342
- await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
343
- type: 'agent.usage.updated',
344
- runId: input.runId,
345
- projection: input.projection,
346
- routing: input.routing,
347
- cli: CLAUDE_CLI_NAME,
348
- model,
349
- ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
350
- payload: { tokenUsage },
351
- }));
352
- }
353
268
  await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
354
269
  type: 'agent.process.exited',
355
270
  runId: input.runId,
@@ -357,29 +272,124 @@ export function createClaudeRunner(options) {
357
272
  routing: input.routing,
358
273
  cli: CLAUDE_CLI_NAME,
359
274
  model,
360
- ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
361
275
  payload: { exitCode: result.exitCode, timedOut: result.timedOut },
362
276
  }));
277
+ let parsedReason;
278
+ let stdoutFailureDetail;
279
+ const trimmedStdout = result.stdout.trim();
280
+ try {
281
+ if (trimmedStdout.length > 0) {
282
+ parsedReason = parseClaudePrintOutput(result.stdout).result;
283
+ }
284
+ }
285
+ catch {
286
+ // stdout wasn't valid JSON, so it is likely a CLI-level error
287
+ // message rather than a Claude print result.
288
+ stdoutFailureDetail = trimmedStdout;
289
+ }
363
290
  return {
364
- result: parsed.result,
291
+ result: [
292
+ result.timedOut
293
+ ? `Claude runner timed out after ${options.settings.timeoutMs}ms and was killed`
294
+ : trimmedStdout.length === 0
295
+ ? 'Claude runner produced no output'
296
+ : parsedReason !== undefined
297
+ ? `Claude runner failed: ${parsedReason}`
298
+ : 'Claude runner failed',
299
+ result.stderr,
300
+ stdoutFailureDetail,
301
+ sandboxLog?.text,
302
+ 'FAILED',
303
+ ]
304
+ .filter((part) => part !== undefined && part.length > 0)
305
+ .join('\n'),
365
306
  model,
366
307
  cli: CLAUDE_CLI_NAME,
367
- ...(parseRunnerResult(parsed.result).status === 'FAILED'
368
- ? { failureClass: 'task' }
369
- : {}),
370
- ...(parsed.session_id === undefined ? {} : { session_id: parsed.session_id }),
371
- ...(tokenUsage === undefined ? {} : { tokenUsage }),
308
+ failureClass,
372
309
  metadata: {
373
310
  stdout: result.stdout,
374
311
  stderr: result.stderr,
375
- raw: parsed,
376
- skipApproval: stagePrompt.skipApproval,
377
- allowAutoApproval: stagePrompt.allowAutoApproval,
312
+ exitCode: result.exitCode,
313
+ failureClass,
378
314
  ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
379
315
  ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
380
316
  ...(sandboxLog?.metadata ?? {}),
381
317
  },
382
318
  };
319
+ }
320
+ const parsed = parseClaudePrintOutput(result.stdout);
321
+ const sandboxLog = readSandboxLogBreadcrumb();
322
+ await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
323
+ type: 'agent.progress',
324
+ runId: input.runId,
325
+ projection: input.projection,
326
+ routing: input.routing,
327
+ cli: CLAUDE_CLI_NAME,
328
+ model,
329
+ ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
330
+ payload: { message: 'Claude print result received', raw: parsed },
331
+ }));
332
+ console.log(formatClaudeRunLogLine({
333
+ phase: 'success',
334
+ runId: input.runId,
335
+ action: input.action,
336
+ issueNumber: input.projection.issue.number,
337
+ repo: input.projection.issue.repo,
338
+ recentEventIds: input.recentEvents.map((event) => event.eventId),
339
+ model,
340
+ ...(input.workspacePath === undefined ? {} : { workspacePath: input.workspacePath }),
341
+ ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
342
+ }));
343
+ const tokenUsage = extractTokenUsage(parsed);
344
+ if (tokenUsage !== undefined) {
345
+ await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
346
+ type: 'agent.usage.updated',
347
+ runId: input.runId,
348
+ projection: input.projection,
349
+ routing: input.routing,
350
+ cli: CLAUDE_CLI_NAME,
351
+ model,
352
+ ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
353
+ payload: { tokenUsage },
354
+ }));
355
+ }
356
+ await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
357
+ type: 'agent.process.exited',
358
+ runId: input.runId,
359
+ projection: input.projection,
360
+ routing: input.routing,
361
+ cli: CLAUDE_CLI_NAME,
362
+ model,
363
+ ...(parsed.session_id === undefined ? {} : { sessionId: parsed.session_id }),
364
+ payload: { exitCode: result.exitCode, timedOut: result.timedOut },
365
+ }));
366
+ return {
367
+ result: parsed.result,
368
+ model,
369
+ cli: CLAUDE_CLI_NAME,
370
+ ...(parseRunnerResult(parsed.result).status === 'FAILED'
371
+ ? { failureClass: 'task' }
372
+ : {}),
373
+ ...(parsed.session_id === undefined ? {} : { session_id: parsed.session_id }),
374
+ ...(tokenUsage === undefined ? {} : { tokenUsage }),
375
+ metadata: {
376
+ stdout: result.stdout,
377
+ stderr: result.stderr,
378
+ raw: parsed,
379
+ skipApproval: stagePrompt.skipApproval,
380
+ allowAutoApproval: stagePrompt.allowAutoApproval,
381
+ ...(promptTranscriptPath === undefined ? {} : { promptTranscriptPath }),
382
+ ...(responseTranscriptPath === undefined ? {} : { responseTranscriptPath }),
383
+ ...(sandboxLog?.metadata ?? {}),
384
+ },
385
+ };
386
+ }
387
+ return {
388
+ async start(input) {
389
+ return createAgentExecution(input, executeRun);
390
+ },
391
+ async run(input) {
392
+ return (await this.start(input)).result;
383
393
  },
384
394
  async smoke() {
385
395
  const args = buildClaudePrintArgs({
@@ -2,6 +2,7 @@ import { buildStagePrompt } from '../runner/stage-prompt.js';
2
2
  import { runAgentCliCommand } from '../runner/cli-command.js';
3
3
  import { emitRuntimeEvent, runnerRuntimeEvent } from '../runner/runtime-events.js';
4
4
  import { writeRunnerTranscript } from '../runner/transcripts.js';
5
+ import { createAgentExecution } from '../../core/live-execution.js';
5
6
  import { parseRunnerResult } from '../../domain/schema.js';
6
7
  const CODEX_CLI_NAME = 'Codex';
7
8
  export function buildCodexExecArgs(input) {
@@ -201,7 +202,10 @@ export function buildCodexToolCapabilityNote(input) {
201
202
  return input.mode === 'resume' ? `Reminder: this is still a planning-only stage. ${note}` : note;
202
203
  }
203
204
  export function createCodexRunner(options) {
204
- return {
205
+ const runner = {
206
+ async start(input) {
207
+ return createAgentExecution(input, (liveInput) => runner.run(liveInput));
208
+ },
205
209
  async run(input) {
206
210
  const runMode = 'start';
207
211
  const toolCapabilityNote = buildCodexToolCapabilityNote({
@@ -269,6 +273,9 @@ export function createCodexRunner(options) {
269
273
  }),
270
274
  cwd,
271
275
  timeoutMs: options.settings.timeoutMs,
276
+ ...(input.cancellationSignal === undefined
277
+ ? {}
278
+ : { cancellationSignal: input.cancellationSignal }),
272
279
  onProcessStart: async (identity) => {
273
280
  await input.onProcessStart?.(identity);
274
281
  await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
@@ -474,4 +481,5 @@ export function createCodexRunner(options) {
474
481
  };
475
482
  },
476
483
  };
484
+ return runner;
477
485
  }
@@ -3,6 +3,7 @@ import { buildStagePrompt } from '../runner/stage-prompt.js';
3
3
  import { runAgentCliCommand } from '../runner/cli-command.js';
4
4
  import { emitRuntimeEvent, runnerRuntimeEvent } from '../runner/runtime-events.js';
5
5
  import { writeRunnerTranscript } from '../runner/transcripts.js';
6
+ import { createAgentExecution } from '../../core/live-execution.js';
6
7
  const CURSOR_CLI_NAME = 'Cursor';
7
8
  export function buildCursorAgentArgs(input) {
8
9
  const fullPrompt = buildCursorPromptText(input);
@@ -154,7 +155,10 @@ function readSandboxLogBreadcrumb() {
154
155
  };
155
156
  }
156
157
  export function createCursorRunner(options) {
157
- return {
158
+ const runner = {
159
+ async start(input) {
160
+ return createAgentExecution(input, (liveInput) => runner.run(liveInput));
161
+ },
158
162
  async run(input) {
159
163
  const priorSessionId = input.projection.wake.sessionId;
160
164
  const priorSessionCli = input.projection.wake.sessionCli;
@@ -220,6 +224,9 @@ export function createCursorRunner(options) {
220
224
  }),
221
225
  cwd,
222
226
  timeoutMs: options.settings.timeoutMs,
227
+ ...(input.cancellationSignal === undefined
228
+ ? {}
229
+ : { cancellationSignal: input.cancellationSignal }),
223
230
  onProcessStart: async (identity) => {
224
231
  await input.onProcessStart?.(identity);
225
232
  await emitRuntimeEvent(input.onRuntimeEvent, runnerRuntimeEvent({
@@ -412,4 +419,5 @@ export function createCursorRunner(options) {
412
419
  };
413
420
  },
414
421
  };
422
+ return runner;
415
423
  }
@@ -1,8 +1,22 @@
1
1
  import { emitRuntimeEvent, runnerRuntimeEvent } from '../runner/runtime-events.js';
2
+ import { createAgentExecution } from '../../core/live-execution.js';
2
3
  export function createFakeRunner(result, options) {
3
- return {
4
+ const runner = {
5
+ async start(input) {
6
+ return createAgentExecution(input, (liveInput) => runner.run(liveInput));
7
+ },
4
8
  async run(_) {
5
9
  const cli = options?.cli ?? 'Fake';
10
+ const isCanceled = () => _.cancellationSignal?.aborted === true;
11
+ if (isCanceled()) {
12
+ return {
13
+ result: 'Fake runner canceled\nFAILED',
14
+ model: 'fake',
15
+ cli,
16
+ failureClass: 'infra',
17
+ metadata: { source: 'fake-runner', canceled: true },
18
+ };
19
+ }
6
20
  await emitRuntimeEvent(_.onRuntimeEvent, runnerRuntimeEvent({
7
21
  type: 'agent.process.started',
8
22
  runId: _.runId,
@@ -32,6 +46,15 @@ export function createFakeRunner(result, options) {
32
46
  sessionId: 'fake-session-1',
33
47
  payload: { exitCode: 0, synthetic: true },
34
48
  }));
49
+ if (isCanceled()) {
50
+ return {
51
+ result: 'Fake runner canceled\nFAILED',
52
+ model: 'fake',
53
+ cli,
54
+ failureClass: 'infra',
55
+ metadata: { source: 'fake-runner', canceled: true },
56
+ };
57
+ }
35
58
  return (result ?? {
36
59
  result: [
37
60
  'Fake runner completed',
@@ -50,4 +73,5 @@ export function createFakeRunner(result, options) {
50
73
  });
51
74
  },
52
75
  };
76
+ return runner;
53
77
  }
@@ -11,6 +11,7 @@ export function runAgentCliCommand(input) {
11
11
  let stdout = '';
12
12
  let stderr = '';
13
13
  let timedOut = false;
14
+ let canceled = false;
14
15
  let killTimer;
15
16
  let startNotification = Promise.resolve();
16
17
  let startNotificationError;
@@ -22,13 +23,24 @@ export function runAgentCliCommand(input) {
22
23
  });
23
24
  }
24
25
  }
26
+ const terminate = () => {
27
+ child.kill('SIGTERM');
28
+ killTimer = setTimeout(() => child.kill('SIGKILL'), TIMEOUT_KILL_GRACE_MS);
29
+ };
25
30
  const timeoutTimer = input.timeoutMs === undefined
26
31
  ? undefined
27
32
  : setTimeout(() => {
28
33
  timedOut = true;
29
- child.kill('SIGTERM');
30
- killTimer = setTimeout(() => child.kill('SIGKILL'), TIMEOUT_KILL_GRACE_MS);
34
+ terminate();
31
35
  }, input.timeoutMs);
36
+ const abortListener = () => {
37
+ canceled = true;
38
+ terminate();
39
+ };
40
+ input.cancellationSignal?.addEventListener('abort', abortListener, { once: true });
41
+ if (input.cancellationSignal?.aborted === true) {
42
+ abortListener();
43
+ }
32
44
  child.stdout.on('data', (chunk) => {
33
45
  stdout += chunk.toString();
34
46
  });
@@ -38,11 +50,13 @@ export function runAgentCliCommand(input) {
38
50
  child.on('error', (error) => {
39
51
  clearTimeout(timeoutTimer);
40
52
  clearTimeout(killTimer);
53
+ input.cancellationSignal?.removeEventListener('abort', abortListener);
41
54
  reject(error);
42
55
  });
43
56
  child.on('close', async (exitCode) => {
44
57
  clearTimeout(timeoutTimer);
45
58
  clearTimeout(killTimer);
59
+ input.cancellationSignal?.removeEventListener('abort', abortListener);
46
60
  await startNotification;
47
61
  if (startNotificationError !== undefined) {
48
62
  reject(startNotificationError);
@@ -52,7 +66,7 @@ export function runAgentCliCommand(input) {
52
66
  stdout,
53
67
  stderr,
54
68
  exitCode: exitCode ?? 1,
55
- timedOut,
69
+ timedOut: timedOut || canceled,
56
70
  });
57
71
  });
58
72
  });
@@ -0,0 +1,81 @@
1
+ import { currentProcessIdentity } from '../lib/process-identity.js';
2
+ function createEventQueue() {
3
+ const queue = [];
4
+ const waiters = [];
5
+ let closed = false;
6
+ function emit(value) {
7
+ const waiter = waiters.shift();
8
+ if (waiter !== undefined) {
9
+ waiter(value);
10
+ return;
11
+ }
12
+ queue.push(value);
13
+ }
14
+ async function nextQueued() {
15
+ const value = queue.shift();
16
+ if (value !== undefined)
17
+ return value;
18
+ if (closed)
19
+ return { kind: 'done' };
20
+ return new Promise((resolve) => waiters.push(resolve));
21
+ }
22
+ return {
23
+ push(event) {
24
+ if (!closed)
25
+ emit({ kind: 'event', event });
26
+ },
27
+ close() {
28
+ if (closed)
29
+ return;
30
+ closed = true;
31
+ emit({ kind: 'done' });
32
+ },
33
+ fail(error) {
34
+ if (closed)
35
+ return;
36
+ closed = true;
37
+ emit({ kind: 'error', error });
38
+ },
39
+ iterable: {
40
+ async *[Symbol.asyncIterator]() {
41
+ while (true) {
42
+ const value = await nextQueued();
43
+ if (value.kind === 'done')
44
+ return;
45
+ if (value.kind === 'error')
46
+ throw value.error;
47
+ yield value.event;
48
+ }
49
+ },
50
+ },
51
+ };
52
+ }
53
+ export function createAgentExecution(input, execute) {
54
+ const controller = new AbortController();
55
+ const events = createEventQueue();
56
+ const result = Promise.resolve()
57
+ .then(() => execute({
58
+ ...input,
59
+ cancellationSignal: controller.signal,
60
+ onRuntimeEvent: async (event) => {
61
+ await input.onRuntimeEvent?.(event);
62
+ events.push(event);
63
+ },
64
+ }))
65
+ .then((value) => {
66
+ events.close();
67
+ return value;
68
+ }, (error) => {
69
+ events.fail(error);
70
+ throw error;
71
+ });
72
+ return {
73
+ runId: input.runId,
74
+ processIdentity: currentProcessIdentity(),
75
+ events: events.iterable,
76
+ async cancel(reason) {
77
+ controller.abort(reason);
78
+ },
79
+ result,
80
+ };
81
+ }
@@ -18,12 +18,14 @@ import { createOutbox } from './outbox.js';
18
18
  import { createEventResolver } from './event-resolver.js';
19
19
  import { createStaleRunReconciler } from './stale-run-reconciler.js';
20
20
  import { createWorkspaceCleanup } from './workspace-cleanup.js';
21
+ import { createAgentExecution } from './live-execution.js';
21
22
  import { createAutonomousDecisionAuditEvent, workflowRevision as computeWorkflowRevision, } from './audit-events.js';
22
23
  import { createRunLease, isRunLeaseExpired, renewRunLease, runLeaseRenewalIntervalMs, } from './run-lease.js';
23
24
  import { currentProcessIdentity, processIdentityMatches } from '../lib/process-identity.js';
24
25
  import { readJsonFile, writeJsonFile } from '../lib/json-file.js';
25
26
  import { isMissingPathError } from '../lib/state-health.js';
26
27
  const prReviewApprovalMarker = '<!-- wake:pr-review-approved -->';
28
+ const activeRunSourceRefreshIntervalMs = 1_000;
27
29
  function latestHumanCommentId(candidate) {
28
30
  const human = candidate.comments.filter((c) => !c.isBotAuthored);
29
31
  return human.at(-1)?.id;
@@ -400,7 +402,17 @@ export function createTickRunner(deps) {
400
402
  return watch;
401
403
  }
402
404
  async function markPendingActionableIssues(projections) {
405
+ const activeRunWorkItemKeys = new Set();
406
+ const now = deps.clock.now();
407
+ for (const record of await deps.stateStore.listRunRecords()) {
408
+ if (record.status === 'running' && (await isRunningRecordActive(record, now))) {
409
+ activeRunWorkItemKeys.add(record.workItemKey);
410
+ }
411
+ }
403
412
  for (const projection of projections) {
413
+ if (activeRunWorkItemKeys.has(projection.workItemKey)) {
414
+ continue;
415
+ }
404
416
  const statusLabel = statusLabelForStage(projection.wake.stage);
405
417
  const stageLabel = stageLabelForStage(projection.wake.stage);
406
418
  const workflowLabel = workflowLabelForWorkflowName(workflowNameForProjection(projection, deps.config));
@@ -423,6 +435,21 @@ export function createTickRunner(deps) {
423
435
  function runnerTimeoutMs() {
424
436
  return maxConfiguredRunnerTimeoutMs(deps.config);
425
437
  }
438
+ function cancellationReasonForIneligibleProjection(projection) {
439
+ if (projection === null || projection.issue.state !== 'open') {
440
+ return 'CANCELED_BY_SOURCE_CLOSED';
441
+ }
442
+ const requiredAssignees = deps.config.sources.github.policy.requiredAssignees;
443
+ if (requiredAssignees.length > 0 &&
444
+ !requiredAssignees.some((login) => projection.issue.assignees.includes(login))) {
445
+ return 'CANCELED_BY_UNASSIGNMENT';
446
+ }
447
+ const workflow = workflowForProjection(projection, deps.config);
448
+ if (workflow === null || !isKnownWorkflowStage(projection.wake.stage, workflow)) {
449
+ return 'CANCELED_BY_WORKFLOW_CHANGE';
450
+ }
451
+ return 'CANCELED_BY_SUPERSEDING_EVENT';
452
+ }
426
453
  async function isRunningRecordActive(record, now) {
427
454
  if (record.lease !== undefined && !isRunLeaseExpired(record, now)) {
428
455
  return true;
@@ -1161,6 +1188,12 @@ export function createTickRunner(deps) {
1161
1188
  });
1162
1189
  }, runLeaseRenewalIntervalMs);
1163
1190
  }
1191
+ function delayActiveRunRefresh() {
1192
+ return new Promise((resolve) => {
1193
+ const timer = setTimeout(resolve, activeRunSourceRefreshIntervalMs);
1194
+ timer.unref?.();
1195
+ });
1196
+ }
1164
1197
  try {
1165
1198
  await transitionRunLifecycle('PREPARING');
1166
1199
  const prepareResult = workspaceMode === 'branch'
@@ -1192,17 +1225,20 @@ export function createTickRunner(deps) {
1192
1225
  const recentEvents = await deps.stateStore.listEventEnvelopesForWorkItem(candidate.workItemKey, 6);
1193
1226
  await transitionRunLifecycle('RUNNING');
1194
1227
  startLeaseRenewal();
1228
+ const activeCandidate = candidate;
1195
1229
  const runnerProjection = watcherRun
1196
1230
  ? {
1197
- ...candidate,
1231
+ ...activeCandidate,
1198
1232
  wake: {
1199
- ...candidate.wake,
1233
+ ...activeCandidate.wake,
1200
1234
  sessionId: undefined,
1201
1235
  sessionCli: undefined,
1202
1236
  },
1203
1237
  }
1204
- : candidate;
1205
- const runnerResult = await deps.runner.run({
1238
+ : activeCandidate;
1239
+ let executionFinished = false;
1240
+ let cancellationReason = null;
1241
+ const runnerInput = {
1206
1242
  action,
1207
1243
  projection: runnerProjection,
1208
1244
  recentEvents,
@@ -1227,7 +1263,79 @@ export function createTickRunner(deps) {
1227
1263
  });
1228
1264
  },
1229
1265
  onRuntimeEvent: appendRuntimeEvent,
1230
- });
1266
+ };
1267
+ const execution = deps.runner.start === undefined
1268
+ ? createAgentExecution(runnerInput, (liveInput) => deps.runner.run(liveInput))
1269
+ : await deps.runner.start(runnerInput);
1270
+ async function persistCancellationRequest(reason) {
1271
+ const requestedAt = deps.clock.now().toISOString();
1272
+ await deps.stateStore.updateRunRecordIf(runId, {
1273
+ expect: (record) => record.status === 'running' &&
1274
+ record.lease?.leaseId === lease.leaseId &&
1275
+ record.lease.ownerInstanceId === ownerInstanceId,
1276
+ update: (record) => ({
1277
+ ...record,
1278
+ metadata: {
1279
+ ...record.metadata,
1280
+ cancellation: {
1281
+ reason,
1282
+ requestedAt,
1283
+ source: 'active-source-reconciliation',
1284
+ },
1285
+ },
1286
+ }),
1287
+ });
1288
+ }
1289
+ async function superviseActiveSource() {
1290
+ if (deps.workSource.refreshForDispatch === undefined) {
1291
+ return null;
1292
+ }
1293
+ while (!executionFinished) {
1294
+ await delayActiveRunRefresh();
1295
+ if (executionFinished) {
1296
+ return null;
1297
+ }
1298
+ let activeRefresh;
1299
+ try {
1300
+ activeRefresh = await deps.workSource.refreshForDispatch({
1301
+ projection: activeCandidate,
1302
+ });
1303
+ }
1304
+ catch {
1305
+ continue;
1306
+ }
1307
+ if (activeRefresh === null) {
1308
+ continue;
1309
+ }
1310
+ if (activeRefresh.events.length > 0) {
1311
+ await ingestInboundEvents(activeRefresh.events);
1312
+ }
1313
+ const refreshedProjection = (await deps.stateStore.readIssueState(activeCandidate.workItemKey)) ??
1314
+ activeCandidate;
1315
+ const ineligible = activeRefresh.sourceExists === false ||
1316
+ !policy.isEligible(refreshedProjection, deps.config);
1317
+ if (!ineligible) {
1318
+ continue;
1319
+ }
1320
+ const reason = cancellationReasonForIneligibleProjection(activeRefresh.sourceExists === false ? null : refreshedProjection);
1321
+ cancellationReason = reason;
1322
+ await persistCancellationRequest(reason);
1323
+ await execution.cancel(reason);
1324
+ return reason;
1325
+ }
1326
+ return null;
1327
+ }
1328
+ const runnerResult = await Promise.race([
1329
+ execution.result.finally(() => {
1330
+ executionFinished = true;
1331
+ }),
1332
+ superviseActiveSource().then(async (reason) => {
1333
+ if (reason === null) {
1334
+ return execution.result;
1335
+ }
1336
+ return execution.result;
1337
+ }),
1338
+ ]);
1231
1339
  clearInterval(leaseRenewalTimer);
1232
1340
  leaseRenewalTimer = undefined;
1233
1341
  const parsedRunnerResult = parseRunnerResult(runnerResult.result);
@@ -1333,16 +1441,19 @@ export function createTickRunner(deps) {
1333
1441
  const resultMetadata = {
1334
1442
  ...runnerResult.metadata,
1335
1443
  envelope: parsedRunnerResult.envelope,
1444
+ ...(cancellationReason === null ? {} : { cancellationReason }),
1336
1445
  ...(runnerResult.failureClass === undefined
1337
1446
  ? {}
1338
1447
  : { failureClass: runnerResult.failureClass }),
1339
1448
  ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
1340
1449
  };
1341
- const executionOutcome = runnerResult.failureClass === 'quota'
1342
- ? 'QUOTA_EXHAUSTED'
1343
- : runnerResult.failureClass === 'infra'
1344
- ? 'PROCESS_FAILED'
1345
- : 'COMPLETED';
1450
+ const executionOutcome = cancellationReason !== null
1451
+ ? cancellationReason
1452
+ : runnerResult.failureClass === 'quota'
1453
+ ? 'QUOTA_EXHAUSTED'
1454
+ : runnerResult.failureClass === 'infra'
1455
+ ? 'PROCESS_FAILED'
1456
+ : 'COMPLETED';
1346
1457
  const workflowOutcome = sentinel === 'DONE'
1347
1458
  ? 'DONE'
1348
1459
  : sentinel === 'BLOCKED'
@@ -1400,7 +1511,9 @@ export function createTickRunner(deps) {
1400
1511
  sessionId: runnerResult.session_id,
1401
1512
  sessionCli: runnerResult.cli,
1402
1513
  workspacePath,
1403
- reason: `runner:${sentinel.toLowerCase()}`,
1514
+ reason: cancellationReason === null
1515
+ ? `runner:${sentinel.toLowerCase()}`
1516
+ : `runner:${String(cancellationReason).toLowerCase()}`,
1404
1517
  ...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
1405
1518
  ...(runnerResult.failureClass === undefined
1406
1519
  ? {}
@@ -24,6 +24,10 @@ export const executionOutcomeValues = [
24
24
  'TIMED_OUT',
25
25
  'STALLED',
26
26
  'CANCELED_BY_RECONCILIATION',
27
+ 'CANCELED_BY_SOURCE_CLOSED',
28
+ 'CANCELED_BY_UNASSIGNMENT',
29
+ 'CANCELED_BY_SUPERSEDING_EVENT',
30
+ 'CANCELED_BY_WORKFLOW_CHANGE',
27
31
  'CANCELED_BY_OPERATOR',
28
32
  'QUOTA_EXHAUSTED',
29
33
  'MALFORMED_OUTPUT',
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "gee0e5b1";
127
+ export const wakeVersion = "g009f6be";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {