Skip to content

Commit ae3a75b

Browse files
blankseclaude
andcommitted
fix(server): surface CLI resume command in web approve/reject responses
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 3d290d8 commit ae3a75b

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
@@ -2024,7 +2024,7 @@ export function registerApiRoutes(
20242024
success: true,
20252025
message: autoResumed
20262026
? `Workflow approved: ${run.workflow_name}. Resuming workflow.`
2027-
: `Workflow approved: ${run.workflow_name}. Send a message to continue.`,
2027+
: `Workflow approved: ${run.workflow_name}. Run \`archon workflow resume ${runId}\` from the CLI to continue, or send a new message in the originating conversation.`,
20282028
});
20292029
} catch (error) {
20302030
getLog().error({ err: error, runId }, 'api.workflow_run_approve_failed');
@@ -2079,7 +2079,7 @@ export function registerApiRoutes(
20792079
success: true,
20802080
message: autoResumed
20812081
? `Workflow rejected: ${run.workflow_name}. Running on-reject prompt.`
2082-
: `Workflow rejected: ${run.workflow_name}. On-reject prompt will run on resume.`,
2082+
: `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.`,
20832083
});
20842084
}
20852085

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)