Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Server startup no longer marks actively-running workflows as failed.** The `failOrphanedRuns()` call has been removed from `packages/server/src/index.ts` to match the CLI precedent (`packages/cli/src/cli.ts:256-258`). Per the new CLAUDE.md principle "No Autonomous Lifecycle Mutation Across Process Boundaries", a stuck `running` row is now transitioned explicitly by the user: via the per-row Cancel/Abandon buttons on the dashboard workflow card, or `archon workflow abandon <run-id>` from the CLI. (`archon workflow cleanup` is a separate command that deletes OLD terminal runs for disk hygiene — it does not handle stuck `running` rows.) Closes #1216.
- **`MCP server connection failed: <plugin>` noise no longer surfaces in workflow runs.** The dag-executor now loads the workflow node's `mcp:` config file once and filters the SDK's failure message to only the servers the workflow actually configured. User-level Claude plugin MCPs (e.g. `telegram` inherited from `~/.claude/`) that fail to connect in the headless subprocess are debug-logged as `dag.mcp_plugin_connection_suppressed` instead of being forwarded to the conversation. Other provider warnings (⚠️) surface unchanged. Credits @MrFadiAi for reporting the issue in #1134 (that PR was 9 days stale and conflicting; this is a fresh re-do on current `dev`).
- **Web UI approval gates now auto-resume.** Previously, clicking Approve or Reject on a paused workflow from the Web UI only recorded the decision — the workflow never continued, and the user had to send a follow-up chat message (or use the CLI) to resume. Three fixes: (1) orchestrator-agent now threads `parentConversationId` through `executeWorkflow` for every web dispatch, (2) the `POST /approve` and `POST /reject` API handlers dispatch `/workflow run <name> <userMessage>` back through the orchestrator when `parent_conversation_id` is set and points at a web-platform parent (mirrors `workflowApproveCommand`/`workflowRejectCommand` on the CLI; non-web parents skip the auto-resume to prevent cross-adapter misrouting), and (3) the during-streaming status check in the DAG executor tolerates the `paused` state so a concurrent AI node in the same topological layer finishes its own stream rather than being aborted when a sibling approval node pauses the run. The Web UI reject button uses the proper `ConfirmRunActionDialog` with an optional reason textarea (was `window.confirm` in the chat card, and lacked a reason input on the dashboard) — the trimmed reason propagates to `$REJECTION_REASON` in the workflow's `on_reject` prompt. Credits @jonasvanderhaegen for surfacing and diagnosing the bug in #1147 (that PR was 87 commits stale on a dev that had since refactored the reject UX; this is a fresh re-do on current `dev`). Closes #1131.

### Changed

Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/orchestrator/orchestrator-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ async function dispatchOrchestratorWorkflow(
workflow,
userMessage,
conversation.id,
codebase.id
codebase.id,
undefined, // issueContext
undefined, // isolationContext
conversation.id // parentConversationId — enables approve/reject auto-resume
);
} else if (workflow.interactive) {
// Interactive workflows run in foreground so output stays in the user's conversation
Expand All @@ -305,7 +308,10 @@ async function dispatchOrchestratorWorkflow(
workflow,
userMessage,
conversation.id,
codebase.id
codebase.id,
undefined, // issueContext
undefined, // isolationContext
conversation.id // parentConversationId — enables approve/reject auto-resume
);
} else {
await dispatchBackgroundWorkflow(
Expand All @@ -331,7 +337,10 @@ async function dispatchOrchestratorWorkflow(
workflow,
userMessage,
conversation.id,
codebase.id
codebase.id,
undefined, // issueContext
undefined, // isolationContext
conversation.id // parentConversationId — enables approve/reject auto-resume
);
}
}
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/orchestrator/orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,10 @@ describe('orchestrator-agent handleMessage', () => {
expect.anything(), // workflow
synthesized, // synthesizedPrompt, not original message
expect.anything(), // conversation.id
expect.anything() // codebase.id
expect.anything(), // codebase.id
undefined, // issueContext
undefined, // isolationContext
expect.anything() // parentConversationId — web approval auto-resume
);
});

Expand All @@ -1106,7 +1109,10 @@ describe('orchestrator-agent handleMessage', () => {
expect.anything(),
'fix the login bug', // original message used as fallback
expect.anything(),
expect.anything()
expect.anything(),
undefined, // issueContext
undefined, // isolationContext
expect.anything() // parentConversationId — web approval auto-resume
);
});

Expand Down
20 changes: 16 additions & 4 deletions packages/docs-web/src/content/docs/guides/approval-nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ to the user on whatever platform they're using (CLI, Slack, GitHub, etc.). On th
block the worktree path guard (no other workflow can start on the same path).
4. **Approve**: The user approves, which writes a `node_completed` event for
the approval node and transitions the run to resumable. Natural-language
messages (recommended) and the CLI auto-resume immediately. The explicit
`/workflow approve` command records the approval; send a follow-up message
to resume.
messages, the CLI, and the Web UI approve button all auto-resume the
workflow from the paused gate. (The explicit `/workflow approve <run-id>`
slash command also auto-resumes when issued in the originating conversation.)
5. **Reject**: The user rejects.
- **Without `on_reject`**: The workflow is cancelled immediately.
- **With `on_reject`**: The executor runs the `on_reject.prompt` via AI (with
Expand Down Expand Up @@ -140,7 +140,19 @@ bun run cli workflow reject <run-id> --reason "Plan needs more test coverage"
### Web UI

Paused workflows show an amber pulsing badge on the dashboard. Click **Approve**
or **Reject** directly on the workflow card.
or **Reject** directly on the workflow card. Both actions auto-resume the
workflow from the paused gate — no follow-up message required.

**Reject with reason**: the Reject dialog includes an optional free-text
reason field. The trimmed value (empty after trim → omitted) is passed to
the workflow as `$REJECTION_REASON`, available in the `on_reject.prompt`.
Rejects on web and chat cards use the same confirmation dialog.

**Cross-platform caveat**: auto-resume via the Web UI only applies when the
run was originally dispatched from the Web UI (parent conversation is a web
conversation). If you approve a Slack / Telegram / GitHub-dispatched run
from the dashboard, the decision is recorded, but the resume flow has to
happen in the originating platform (re-run the workflow there).

### REST API

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1021,12 +1021,12 @@ nodes:
When the workflow reaches `review-gate`, it pauses and notifies you. Approve or reject via:

- **Natural language** (recommended): Just type your response in the conversation — the system detects the paused workflow and auto-resumes
- **CLI**: `bun run cli workflow approve <run-id>` or `bun run cli workflow reject <run-id>`
- **Explicit command**: `/workflow approve <run-id>` or `/workflow reject <run-id>` (records approval; send a follow-up message to resume)
- **Web UI**: Click the Approve/Reject buttons on the dashboard card
- **CLI**: `bun run cli workflow approve <run-id>` or `bun run cli workflow reject <run-id>` — auto-resumes
- **Explicit command**: `/workflow approve <run-id>` or `/workflow reject <run-id>` — auto-resumes when issued in the originating conversation
- **Web UI**: Click the Approve/Reject buttons on the dashboard card — auto-resumes for Web-UI-dispatched runs; the Reject dialog includes an optional reason field that flows to `$REJECTION_REASON`
- **API**: `POST /api/workflows/runs/<run-id>/approve` or `/reject`

After approval via natural language or CLI, the workflow auto-resumes from the next node. The user's approval comment is available as `$review-gate.output` in downstream nodes only when `capture_response: true` is set on the approval node.
All four paths auto-resume the workflow from the next node. The user's approval comment is available as `$review-gate.output` in downstream nodes only when `capture_response: true` is set on the approval node. Cross-platform caveat: Web-UI approvals on Slack / Telegram / GitHub-dispatched runs record the decision but do not auto-resume — re-run from the originating platform to continue.

Without `on_reject`: rejecting cancels the workflow.
With `on_reject`: rejecting triggers an AI rework prompt and re-pauses for re-review.
Expand Down
104 changes: 101 additions & 3 deletions packages/server/src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {
RESUMABLE_WORKFLOW_STATUSES,
TERMINAL_WORKFLOW_STATUSES,
} from '@archon/workflows/schemas/workflow-run';
import type { ApprovalContext } from '@archon/workflows/schemas/workflow-run';
import type { ApprovalContext, WorkflowRun } from '@archon/workflows/schemas/workflow-run';
import { findMarkdownFilesRecursive } from '@archon/core/utils/commands';

/** Lazy-initialized logger (deferred so test mocks can intercept createLogger) */
Expand Down Expand Up @@ -1035,6 +1035,84 @@ export function registerApiRoutes(
return { accepted: true, status: result.status };
}

/**
* Re-enter the orchestrator after a paused approval gate is resolved, so a
* web-dispatched workflow continues (approve) or runs its on_reject prompt
* (reject) without the user having to re-run the workflow command. The CLI's
* `workflowApproveCommand` / `workflowRejectCommand` already auto-resume via
* `workflowRunCommand({ resume: true })`; this is the web-side equivalent.
*
* Returns `true` when a resume dispatch was initiated, `false` otherwise (no
* parent conversation on the run, parent conversation deleted, parent was on
* a non-web platform, or dispatch threw). Failures are non-fatal: the gate
* decision is recorded regardless; when this returns `false` the response
* text instructs the user to re-run the workflow command.
*
* **Cross-adapter guard**: only web-sourced parents qualify.
* `dispatchToOrchestrator` is wired to the web adapter + its lock manager,
* so a Slack / Telegram / GitHub / Discord run being approved from the
* dashboard must not route through it — the Slack thread would never see
* the resumed output. Non-web parents skip auto-resume and the originating
* platform's own re-run flow applies.
*/
async function tryAutoResumeAfterGate(
run: WorkflowRun,
action: 'approve' | 'reject'
): Promise<boolean> {
if (!run.parent_conversation_id) return false;
// Literal event names per action — greppable for ops tooling. Keeping the
// branch explicit rather than templating avoids the earlier 3-segment
// `api.workflow_*.dispatched` shape that broke `{domain}.{action}_{state}`.
const events =
action === 'approve'
? {
dispatched: 'api.workflow_approve_auto_resume_dispatched' as const,
skippedNoPlatformConv:
'api.workflow_approve_auto_resume_skipped_no_platform_conv' as const,
skippedNonWebParent: 'api.workflow_approve_auto_resume_skipped_non_web_parent' as const,
failed: 'api.workflow_approve_auto_resume_failed' as const,
}
: {
dispatched: 'api.workflow_reject_auto_resume_dispatched' as const,
skippedNoPlatformConv:
'api.workflow_reject_auto_resume_skipped_no_platform_conv' as const,
skippedNonWebParent: 'api.workflow_reject_auto_resume_skipped_non_web_parent' as const,
failed: 'api.workflow_reject_auto_resume_failed' as const,
};
try {
const parentConv = await conversationDb.getConversationById(run.parent_conversation_id);
const platformConvId = parentConv?.platform_conversation_id;
if (!platformConvId) {
getLog().debug(
{ runId: run.id, parentConversationId: run.parent_conversation_id },
events.skippedNoPlatformConv
);
return false;
}
if (parentConv.platform_type !== 'web') {
getLog().debug(
{
runId: run.id,
parentConversationId: run.parent_conversation_id,
platformType: parentConv.platform_type,
},
events.skippedNonWebParent
);
return false;
}
const resumeMessage = `/workflow run ${run.workflow_name} ${run.user_message ?? ''}`.trim();
await dispatchToOrchestrator(platformConvId, resumeMessage);
getLog().info(
{ runId: run.id, workflowName: run.workflow_name, platformConvId },
events.dispatched
);
return true;
} catch (err) {
getLog().warn({ err: err as Error, runId: run.id }, events.failed);
return false;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// GET /api/conversations - List conversations
registerOpenApiRoute(getConversationsRoute, async c => {
try {
Expand Down Expand Up @@ -1894,9 +1972,20 @@ export function registerApiRoutes(
status: 'failed',
metadata: metadataUpdate,
});

// Auto-resume: dispatch to the orchestrator so the workflow continues
// without requiring the user to re-run the workflow command. Mirrors
// what `workflowApproveCommand` does in the CLI. Requires
// `parent_conversation_id` on the run (set by orchestrator-agent for any
// web-dispatched workflow — foreground, interactive, and background via
// the pre-created run) and a web-platform parent (guarded in the helper).
const autoResumed = await tryAutoResumeAfterGate(run, 'approve');

return c.json({
success: true,
message: `Workflow approved: ${run.workflow_name}. Send a message to continue the workflow.`,
message: autoResumed
? `Workflow approved: ${run.workflow_name}. Resuming workflow.`
: `Workflow approved: ${run.workflow_name}. Send a message to continue.`,
});
} catch (error) {
getLog().error({ err: error, runId }, 'api.workflow_run_approve_failed');
Expand Down Expand Up @@ -1940,9 +2029,18 @@ export function registerApiRoutes(
status: 'failed',
metadata: { rejection_reason: reason, rejection_count: currentCount + 1 },
});

// Auto-resume: dispatch to the orchestrator so the on_reject prompt runs
// without requiring the user to re-run the workflow command. Mirrors
// what `workflowRejectCommand` does in the CLI. Same cross-adapter
// guard as approve — only web parents auto-resume.
const autoResumed = await tryAutoResumeAfterGate(run, 'reject');

return c.json({
success: true,
message: `Workflow rejected: ${run.workflow_name}. On-reject prompt will run on resume.`,
message: autoResumed
? `Workflow rejected: ${run.workflow_name}. Running on-reject prompt.`
: `Workflow rejected: ${run.workflow_name}. On-reject prompt will run on resume.`,
});
}

Expand Down
Loading
Loading