Skip to content

Commit f83d1f8

Browse files
blankseclaude
andauthored
fix(server): surface CLI resume command in web approve/reject responses (#1523)
When a workflow run is approved/rejected via the Web UI but `tryAutoResumeAfterGate` cannot auto-resume — because there is no `parent_conversation_id`, the parent conversation is gone, or the parent sits on a non-web platform (Slack/Telegram/GitHub/CLI) — the success message said only "Send a message to continue" / "On-reject prompt will run on resume". A web-UI user whose run originated from a terminal has no obvious next step from that text and the run sits in `failed` status. Both approve and reject (on_reject branch) now include the exact `archon workflow resume <runId>` command in the non-auto-resumed response, so the web-UI surface always carries an actionable next step. The auto-resume happy path and the no-on_reject cancellation path are unchanged. The Resume endpoint's CLI hints (covered by #1329) are not touched. Closes #1522. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c571a93 commit f83d1f8

2 files changed

Lines changed: 41 additions & 6 deletions

File tree

packages/server/src/routes/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2027,7 +2027,7 @@ export function registerApiRoutes(
20272027
success: true,
20282028
message: autoResumed
20292029
? `Workflow approved: ${run.workflow_name}. Resuming workflow.`
2030-
: `Workflow approved: ${run.workflow_name}. Send a message to continue.`,
2030+
: `Workflow approved: ${run.workflow_name}. Run \`archon workflow resume ${runId}\` from the CLI to continue, or send a new message in the originating conversation.`,
20312031
});
20322032
} catch (error) {
20332033
getLog().error({ err: error, runId }, 'api.workflow_run_approve_failed');
@@ -2082,7 +2082,7 @@ export function registerApiRoutes(
20822082
success: true,
20832083
message: autoResumed
20842084
? `Workflow rejected: ${run.workflow_name}. Running on-reject prompt.`
2085-
: `Workflow rejected: ${run.workflow_name}. On-reject prompt will run on resume.`,
2085+
: `Workflow rejected: ${run.workflow_name}. On-reject prompt will run when the run resumes — run \`archon workflow resume ${runId}\` from the CLI to trigger it.`,
20862086
});
20872087
}
20882088

packages/server/src/routes/api.workflow-runs.test.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,7 +1506,7 @@ describe('approve/reject auto-resume', () => {
15061506

15071507
expect(response.status).toBe(200);
15081508
const body = (await response.json()) as { message: string };
1509-
expect(body.message).toContain('Send a message to continue');
1509+
expect(body.message).toContain('archon workflow resume run-paused-1');
15101510
expect(mockHandleMessage).not.toHaveBeenCalled();
15111511
expect(mockGetConversationById).not.toHaveBeenCalled();
15121512
});
@@ -1527,7 +1527,7 @@ describe('approve/reject auto-resume', () => {
15271527

15281528
expect(response.status).toBe(200);
15291529
const body = (await response.json()) as { message: string };
1530-
expect(body.message).toContain('Send a message to continue');
1530+
expect(body.message).toContain('archon workflow resume run-paused-1');
15311531
expect(mockHandleMessage).not.toHaveBeenCalled();
15321532
});
15331533

@@ -1555,8 +1555,8 @@ describe('approve/reject auto-resume', () => {
15551555

15561556
expect(response.status).toBe(200);
15571557
const body = (await response.json()) as { message: string };
1558-
// Same fallback text as no-parent case — user re-runs from the originating platform.
1559-
expect(body.message).toContain('Send a message to continue');
1558+
// Surfaces the exact CLI command so the web-UI user has a concrete next step.
1559+
expect(body.message).toContain('archon workflow resume run-paused-1');
15601560
expect(mockHandleMessage).not.toHaveBeenCalled();
15611561
});
15621562

@@ -1603,6 +1603,41 @@ describe('approve/reject auto-resume', () => {
16031603
expect(dispatchedMessage).toBe('/workflow run deploy Review PR');
16041604
});
16051605

1606+
test('reject: surfaces CLI resume hint when on_reject configured but parent is non-web', async () => {
1607+
mockGetWorkflowRun.mockResolvedValueOnce({
1608+
...MOCK_PAUSED_RUN,
1609+
id: 'run-reject-non-web',
1610+
parent_conversation_id: 'slack-parent-conv-uuid',
1611+
metadata: {
1612+
approval: {
1613+
type: 'approval',
1614+
nodeId: 'review-gate',
1615+
message: 'Approve?',
1616+
onRejectPrompt: 'Fix: $REJECTION_REASON',
1617+
onRejectMaxAttempts: 3,
1618+
},
1619+
rejection_count: 0,
1620+
},
1621+
});
1622+
mockGetConversationById.mockResolvedValueOnce({
1623+
id: 'slack-parent-conv-uuid',
1624+
platform_conversation_id: '1234567890.123456',
1625+
platform_type: 'slack',
1626+
});
1627+
1628+
const { app } = makeApp();
1629+
const response = await app.request('/api/workflows/runs/run-reject-non-web/reject', {
1630+
method: 'POST',
1631+
body: JSON.stringify({ reason: 'tests missing' }),
1632+
headers: { 'Content-Type': 'application/json' },
1633+
});
1634+
1635+
expect(response.status).toBe(200);
1636+
const body = (await response.json()) as { message: string };
1637+
expect(body.message).toContain('archon workflow resume run-reject-non-web');
1638+
expect(mockHandleMessage).not.toHaveBeenCalled();
1639+
});
1640+
16061641
test('reject: does NOT dispatch when the run is being cancelled (no on_reject configured)', async () => {
16071642
mockGetWorkflowRun.mockResolvedValueOnce({
16081643
...MOCK_PAUSED_RUN,

0 commit comments

Comments
 (0)