Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9bab3c3
feat(serve): expose workflow tasks and controls
qqqys Aug 20, 2026
75f2975
Merge upstream/main into codex/issue-9033-workflow-daemon-api
qqqys Aug 20, 2026
80f94d6
Merge upstream/main into codex/issue-9033-workflow-daemon-api
qqqys Aug 23, 2026
bea0746
fix(serve): align workflow capability gating
qqqys Aug 23, 2026
6febdb2
fix(cli): reject live workflow history deletion
qqqys Aug 23, 2026
fdadec7
fix(serve): start controlled workflows in background
qqqys Aug 23, 2026
135bf45
fix(serve): address workflow control review blockers
qqqys Aug 23, 2026
4ab652a
fix(serve): make workflow history deletion race-safe
qqqys Aug 23, 2026
e139338
chore(serve): resolve merge conflicts with main
qqqys Aug 24, 2026
3497d0d
fix(serve): enforce workspace trust on the daemon Workflow surfaces (…
Aug 25, 2026
32d8934
Merge branch 'main' into codex/issue-9033-workflow-daemon-api
qwen-code-dev-bot Aug 25, 2026
c25a5f9
fix(serve): close cross-session workflow deletion races and untrusted…
qwen-code-dev-bot Aug 25, 2026
2ac1f68
Merge branch 'main' into codex/issue-9033-workflow-daemon-api
qqqys Aug 26, 2026
15847f9
Merge branch 'main' into codex/issue-9033-workflow-daemon-api
qqqys Aug 26, 2026
e80f785
Merge remote-tracking branch 'upstream/main' into tmp-sync-9546
qqqys Aug 26, 2026
eeca4d7
Merge upstream/main into codex/issue-9033-workflow-daemon-api
qqqys Aug 26, 2026
f8506d3
fix(serve): enforce workflow trust gates
qqqys Aug 26, 2026
b799ea6
fix(cli): close the four cross-session workflow-history consistency h…
qqqys Aug 27, 2026
3d6b3bc
Merge remote-tracking branch 'upstream/main' into HEAD
qqqys Aug 27, 2026
f42b285
fix(core): drop the duplicated telemetry-swap mock property
qqqys Aug 27, 2026
0b5d23c
fix(workflows): guard pending run lifecycle
qqqys Aug 27, 2026
6c79490
fix(workflows): let a starting run be cancelled, and stop two slow leaks
qqqys Aug 27, 2026
f119607
fix(core): close the round-5 review findings on workflow task controls
qqqys Aug 28, 2026
c58b3fd
Merge remote-tracking branch 'upstream/main' into codex/issue-9033-wo…
qqqys Aug 28, 2026
2c2c232
Merge branch 'main' into codex/issue-9033-workflow-daemon-api
qqqys Aug 28, 2026
4b441a9
Merge branch 'main' into codex/issue-9033-workflow-daemon-api
qqqys Aug 28, 2026
8bdd956
fix(serve): keep workflow retry and the workflowsEnabled flag consist…
qqqys Aug 28, 2026
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
21 changes: 20 additions & 1 deletion packages/acp-bridge/src/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3161,7 +3161,9 @@ describe('createAcpSessionBridge', () => {
availableSkills: [],
});
await expect(
bridge.getSessionTasksStatus(session.sessionId),
bridge.getSessionTasksStatus(session.sessionId, {
includeWorkflows: true,
}),
).resolves.toMatchObject({
sessionId: session.sessionId,
tasks: [],
Expand All @@ -3185,6 +3187,10 @@ describe('createAcpSessionBridge', () => {
'qwen/status/session/tasks',
'qwen/status/session/lsp',
]);
expect(handles[0]?.agent.extMethodCalls[2]?.params).toMatchObject({
sessionId: session.sessionId,
includeWorkflows: true,
});

await bridge.shutdown();
});
Expand Down Expand Up @@ -23073,6 +23079,19 @@ describe('createAcpSessionBridge', () => {
{ clientId: 'client-not-issued' },
),
).rejects.toBeInstanceOf(InvalidClientIdError);
await expect(
bridge.cancelSessionTask(session.sessionId, 'task-1', 'workflow', {
clientId: 'client-not-issued',
}),
).rejects.toBeInstanceOf(InvalidClientIdError);
await expect(
bridge.controlSessionWorkflowTask(
session.sessionId,
'task-1',
'rerun',
{ clientId: 'client-not-issued' },
),
).rejects.toBeInstanceOf(InvalidClientIdError);
await bridge.shutdown();
});

Expand Down
23 changes: 21 additions & 2 deletions packages/acp-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
type ServeSessionContextStatus,
type ServeSessionLspStatus,
type ServeSessionTasksStatus,
type ServeSessionWorkflowTaskStatus,
type ServeWorkspaceMcpResourcesStatus,
type ServeWorkspaceMcpStatus,
type ServeWorkspaceMcpToolsStatus,
Expand Down Expand Up @@ -10919,10 +10920,11 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
);
},

async getSessionTasksStatus(sessionId) {
async getSessionTasksStatus(sessionId, opts) {
return requestSessionStatus<ServeSessionTasksStatus>(
sessionId,
SERVE_STATUS_EXT_METHODS.sessionTasks,
{ includeWorkflows: opts?.includeWorkflows === true },
);
},

Expand All @@ -10937,14 +10939,31 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
return requestSessionTranscriptPage(req);
},

async cancelSessionTask(sessionId, taskId, taskKind) {
async cancelSessionTask(sessionId, taskId, taskKind, context) {
const entry = byId.get(sessionId);
if (!entry) throw new SessionNotFoundError(sessionId);
resolveTrustedClientId(entry, context?.clientId);
return requestSessionStatus<{ cancelled: boolean }>(
sessionId,
SERVE_CONTROL_EXT_METHODS.sessionTaskCancel,
{ taskId, taskKind },
);
},

async controlSessionWorkflowTask(sessionId, taskId, action, context) {
const entry = byId.get(sessionId);
if (!entry) throw new SessionNotFoundError(sessionId);
resolveTrustedClientId(entry, context?.clientId);
return requestSessionStatus<{
changed: boolean;
status?: ServeSessionWorkflowTaskStatus['status'];
taskId?: string;
}>(sessionId, SERVE_CONTROL_EXT_METHODS.sessionWorkflowTaskAction, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] controlSessionWorkflowTask — the only code that joins the serve/ACP-http routes to the agent-side qwen/control/session/task/workflow-action ext handler by constructing the { taskId, action } payload — has no happy-path test: the sole bridge.test.ts reference is the InvalidClientIdError rejection, which throws before requestSessionStatus is reached, so the payload construction is never exercised. Probe: renaming the payload keys to { task_id, act } passes all 775 bridge tests; in production the agent handler then throws invalidParams on every request and all SDK/WebUI controls fail while CI is green. Part of a pattern in this PR: the cross-layer wire junctions (bridge, SDK client, ACP dispatch, route table) have no request-construction tests.

Fix: a bridge.test.ts happy path mirroring the includeWorkflows assertion style used in this diff — the fake agent records qwen/control/session/task/workflow-action calls; invoke bridge.controlSessionWorkflowTask(sessionId, 'task-1', 'pause') and assert the recorded params are { sessionId, taskId: 'task-1', action: 'pause' } and the echoed response is returned.

中文说明

[Suggestion] controlSessionWorkflowTask——唯一通过构造 { taskId, action } 载荷把 serve/ACP-http 路由与 agent 侧 qwen/control/session/task/workflow-action ext handler 连接起来的代码——没有正向路径测试:bridge.test.ts 中唯一的引用是 InvalidClientIdError 拒绝用例,它在 requestSessionStatus 之前就抛错,因此载荷构造从未被执行。探针:把载荷键改名为 { task_id, act } 后全部 775 个 bridge 测试通过;而在生产中,agent handler 会对每个请求抛出 invalidParams,所有 SDK/WebUI 控制都会失败,CI 却是绿的。这是本 PR 的一个模式:跨层接线点(bridge、SDK client、ACP dispatch、路由表)没有请求构造测试。

修复:在 bridge.test.ts 中仿照本 diff 的 includeWorkflows 断言风格增加正向路径——让假 agent 记录 qwen/control/session/task/workflow-action 调用;调用 bridge.controlSessionWorkflowTask(sessionId, 'task-1', 'pause'),断言记录的参数为 { sessionId, taskId: 'task-1', action: 'pause' } 且响应被原样返回。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-13: Still stands at 135bf45 — bridge.test.ts was not touched this round; controlSessionWorkflowTask — the only code joining the serve/ACP-http routes to the agent-side workflow-action handler by constructing the {taskId, action} payload — still has no happy-path test, so payload-construction mutants ship green.

中文说明

R1-13 在 135bf45 仍然存在——本轮未改动 bridge.test.ts;controlSessionWorkflowTask(唯一通过构造 {taskId, action} 载荷把 serve/ACP-http 路由接到 agent 侧 workflow-action 处理器的代码)依旧没有 happy-path 测试,载荷构造突变可在套件全绿时合入。

— qwen3.8-max via Qwen Code /review (v0.22.0)

taskId,
action,
});
Comment on lines +10961 to +10964

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-13: Still stands at e1393380 — bridge.test.ts was not touched this round; controlSessionWorkflowTask — the only code joining the serve/ACP-http routes to the agent-side workflow-action handler by constructing the {taskId, action} payload — still has only the InvalidClientId rejection test. Swapping the forwarded fields ({ taskId: action, action: taskId } — both strings) passes type-check and makes the daemon pause/run the wrong target for real clients with every existing test green. Add a success-path case asserting the agent receives qwen/control/session/task/workflow-action with params { sessionId, taskId, action } (the pattern the session-tasks includeWorkflows test already uses).

中文说明

仍然成立:bridge.test.ts 本轮未改动;controlSessionWorkflowTask——唯一构造 {taskId, action} 载荷、把 serve/ACP-http 路由接到 agent 侧 workflow-action 处理器的代码——仍只有 InvalidClientId 拒绝测试。交换转发字段(同为 string,类型检查通过)会让 daemon 对真实客户端操作错误目标而所有现有测试全绿。建议补一个成功路径用例,断言 agent 收到 qwen/control/session/task/workflow-action 且参数为 { sessionId, taskId, action }(session-tasks includeWorkflows 测试已有的模式)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-13: Still stands at 3497d0d — bridge.test.ts was not touched this round; controlSessionWorkflowTask — the only code joining the serve/ACP-http routes to the agent-side workflow-action handler by constructing the {taskId, action} payload — still has only the InvalidClientId rejection test. Swapping the forwarded fields ({ taskId: action, action: taskId } — both strings) passes type-check and makes the daemon pause/run the wrong target for real clients with every existing test green. Add a success-path case asserting the agent receives qwen/control/session/task/workflow-action with params { sessionId, taskId, action } (the pattern the session-tasks includeWorkflows test already uses).

中文说明

仍然成立:bridge.test.ts 本轮未改动;controlSessionWorkflowTask——唯一构造 {taskId, action} 载荷、把 serve/ACP-http 路由接到 agent 侧 workflow-action 处理器的代码——仍只有 InvalidClientId 拒绝测试。交换转发字段(同为 string,类型检查通过)会让 daemon 对真实客户端操作错误目标而所有现有测试全绿。建议补一个成功路径用例,断言 agent 收到 qwen/control/session/task/workflow-action 且参数为 { sessionId, taskId, action }(session-tasks includeWorkflows 测试已有的模式)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

},
Comment on lines +10963 to +10965

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-13: Still stands at 32d89344 — re-verified this round: since round 4 the branch only merged main (no new feature commits); the anchored code is unchanged. bridge.test.ts was not touched this round; controlSessionWorkflowTask — the only code joining the serve/ACP-http routes to the agent-side workflow-action handler by constructing the {taskId, action} payload — still has only the InvalidClientId rejection test. Swapping the forwarded fields ({ taskId: action, action: taskId } — both strings) passes type-check and makes the daemon pause/run the wrong target for real clients with every existing test green. Add a success-path case asserting the agent receives qwen/control/session/task/workflow-action with params { sessionId, taskId, action } (the pattern the session-tasks includeWorkflows test already uses).

Round-5 probe: re-traced at HEAD: bridge.test.ts still exercises only the InvalidClientIdError path of controlSessionWorkflowTask.

中文说明

第 5 轮复查:自第 4 轮以来分支仅合并了 main(无新功能提交),锚点代码未变。

仍然成立:bridge.test.ts 本轮未改动;controlSessionWorkflowTask——唯一构造 {taskId, action} 载荷、把 serve/ACP-http 路由接到 agent 侧 workflow-action 处理器的代码——仍只有 InvalidClientId 拒绝测试。交换转发字段(同为 string,类型检查通过)会让 daemon 对真实客户端操作错误目标而所有现有测试全绿。建议补一个成功路径用例,断言 agent 收到 qwen/control/session/task/workflow-action 且参数为 { sessionId, taskId, action }(session-tasks includeWorkflows 测试已有的模式)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred this round: Critical-only mode is active and the batch was capped at the six Critical findings (the cross-session deletion/resurrection cluster and the untrusted-workspace includeWorkflows leak), each landed with regression tests and mutation-probed witnesses. This Suggestion remains queued for a follow-up round. The controlSessionWorkflowTask success-path payload assertion was not added this round.

本轮顺延:当前处于 Critical-only 模式,批次上限为 6 个 Critical 发现(跨会话删除/复活问题簇与不可信工作区的 includeWorkflows 泄露),均已随回归测试和变异探针见证落库。该建议保持排队,留待后续轮次处理。


async controlSessionGoal(sessionId, request, context) {
const entry = byId.get(sessionId);
if (!entry) throw new SessionNotFoundError(sessionId);
Expand Down
38 changes: 33 additions & 5 deletions packages/acp-bridge/src/bridgeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type {
ServeSessionLspStatus,
ServeSessionSupportedCommandsStatus,
ServeSessionTasksStatus,
ServeSessionWorkflowTaskStatus,
ServeWorkspaceExtensionsStatus,
ServeWorkspaceHooksStatus,
ServeWorkspaceMcpToolsStatus,
Expand Down Expand Up @@ -312,11 +313,15 @@ export const ACTIVE_WORK_MAX_SESSION_HOLDS = 1024;
export const WORKTREE_MCP_DEFER_META_KEY = 'qwen.session.deferMcpDiscovery';

/**
* Work categories a child reports holds for. Monitors, workflows, and cron
* remain outside `activeWork`'s declared scope. The category travels on every
* Work categories a child reports holds for. Monitors and cron remain outside
* `activeWork`'s declared scope. The category travels on every
* hold so peers can negotiate coverage explicitly when the scope widens.
*/
export type ActiveWorkHoldCategory = 'agent' | 'notification' | 'shell';
export type ActiveWorkHoldCategory =
| 'agent'
| 'notification'
| 'shell'
| 'workflow';

/** Categories understood by active-work v1 before category negotiation was
* added to the daemon's initialize request. */
Expand All @@ -327,6 +332,7 @@ export const ACTIVE_WORK_HOLD_CATEGORIES: readonly ActiveWorkHoldCategory[] = [
'agent',
'notification',
'shell',
'workflow',
];

export interface ActiveWorkHeartbeatCapabilityV1 {
Expand Down Expand Up @@ -1777,7 +1783,10 @@ export interface AcpSessionBridge extends WorkspaceEventBridge {
): Promise<ServeSessionSupportedCommandsStatus>;

/** Read the live background task snapshot for a live session. */
getSessionTasksStatus(sessionId: string): Promise<ServeSessionTasksStatus>;
getSessionTasksStatus(
sessionId: string,
opts?: { includeWorkflows?: boolean },
): Promise<ServeSessionTasksStatus>;

/** Read sanitized LSP server status for a live session. */
getSessionLspStatus(sessionId: string): Promise<ServeSessionLspStatus>;
Expand All @@ -1795,9 +1804,28 @@ export interface AcpSessionBridge extends WorkspaceEventBridge {
cancelSessionTask(
sessionId: string,
taskId: string,
taskKind: 'agent' | 'shell' | 'monitor',
taskKind: 'agent' | 'shell' | 'monitor' | 'workflow',
context?: BridgeClientRequestContext,
): Promise<{ cancelled: boolean }>;

/** Control a run, delete history, or start a saved workflow definition. */
controlSessionWorkflowTask(
sessionId: string,
taskId: string,
action:
| 'pause'
| 'resume'
| 'retry'
| 'rerun'
| 'delete-history'
| 'run-saved',
context?: BridgeClientRequestContext,
): Promise<{
changed: boolean;
status?: ServeSessionWorkflowTaskStatus['status'];
taskId?: string;
}>;

/** Clear an active goal in a live session without cancelling the running prompt. */
clearSessionGoal(
sessionId: string,
Expand Down
124 changes: 123 additions & 1 deletion packages/acp-bridge/src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export const SERVE_CONTROL_EXT_METHODS = {
workspaceMemoryDream: 'qwen/control/workspace/memory/dream',
// Runtime MCP server mutation ext-methods
sessionTaskCancel: 'qwen/control/session/task/cancel',
sessionWorkflowTaskAction: 'qwen/control/session/task/workflow-action',
sessionGoalControl: 'qwen/control/session/goal/control',
sessionGoalClear: 'qwen/control/session/goal/clear',
/**
Expand Down Expand Up @@ -615,6 +616,13 @@ export interface ServeSessionSupportedCommandsStatus {
sessionId: string;
availableCommands: AvailableCommand[];
availableSkills: string[];
/** Whether Workflow is available for this session. */
workflowsEnabled?: boolean;
/** Reusable workflow definitions visible to this session. */
savedWorkflows?: Array<{
name: string;
source: 'project' | 'user';
}>;
}

export interface ServeLspServerStatus {
Expand Down Expand Up @@ -726,10 +734,124 @@ export interface ServeSessionMonitorTaskStatus {
toolUseId?: string;
}

export interface ServeWorkflowPhaseVisit {
id: string;
index: number;
title: string;
startedAt: number;
endedAt?: number;
}

export type ServeWorkflowDispatchStatus =
| 'queued'
| 'running'
| 'completed'
| 'failed'
| 'cancelled'
| 'cached';

export interface ServeWorkflowDispatchStatusEntry {
id: string;
phaseVisitId: string | null;
label: string;
prompt: string;
subagentId?: string;
status: ServeWorkflowDispatchStatus;
dependsOn: string[];
queuedAt: number;
startedAt?: number;
endedAt?: number;
error?: string;
}

export interface ServeWorkflowApprovalStatusEntry {
approvalId: string;
subagentId: string;
name: string;
description: string;
at: number;
}

interface ServeWorkflowEventBase {
id: string;
at: number;
}

export type ServeWorkflowEvent =
| (ServeWorkflowEventBase & {
type: 'phase-started';
phaseVisitId: string;
title: string;
})
| (ServeWorkflowEventBase & {
type: 'phase-completed';
phaseVisitId: string;
})
| (ServeWorkflowEventBase & {
type:
| 'dispatch-queued'
| 'dispatch-started'
| 'dispatch-completed'
| 'dispatch-cancelled'
| 'dispatch-cached';
dispatchId: string;
})
| (ServeWorkflowEventBase & {
type: 'dispatch-failed';
dispatchId: string;
error: string;
})
| (ServeWorkflowEventBase & { type: 'log'; message: string })
| (ServeWorkflowEventBase & {
type: 'approval-requested' | 'approval-settled';
name: string;
dispatchId?: string;
})
| (ServeWorkflowEventBase & {
type: 'workflow-completed' | 'workflow-cancelled';
})
| (ServeWorkflowEventBase & {
type: 'workflow-failed';
error: string;
});

export interface ServeSessionWorkflowTaskStatus {
kind: 'workflow';
id: string;
/** Tool call in the parent session that launched this workflow. */
toolUseId?: string;
/** Restored from the project snapshot store; controls are read-only. */
isHistorical?: boolean;
sourceRunId?: string;
startMode?: 'retry' | 'rerun';
label: string;
description: string;
status: ServeSessionTaskLifecycleStatus | 'pausing';
startTime: number;
endTime?: number;
runtimeMs: number;
outputFile?: string;
isBackgrounded: boolean;
currentPhase: string | null;
phaseVisits: ServeWorkflowPhaseVisit[];
dispatches: ServeWorkflowDispatchStatusEntry[];
agentsDispatched: number;
agentsCompleted: number;
tokensSpent: number;
tokenBudgetTotal: number | null;
recentLogs: string[];
/** Ordered runtime facts; absent for snapshots created before event tracing. */
events?: ServeWorkflowEvent[];
pendingApprovalCount: number;
pendingApprovals?: ServeWorkflowApprovalStatusEntry[];
error?: string;
}

export type ServeSessionTaskStatus =
| ServeSessionAgentTaskStatus
| ServeSessionShellTaskStatus
| ServeSessionMonitorTaskStatus;
| ServeSessionMonitorTaskStatus
| ServeSessionWorkflowTaskStatus;

export interface ServeSessionTasksStatus {
v: typeof STATUS_SCHEMA_VERSION;
Expand Down
Loading
Loading