Skip to content

Commit b2f0dd7

Browse files
committed
Reconcile the staging rebase: finish rename-followed ports, async selector resolution
Rebasing onto origin/staging carried staging's lib/copilot work into our renamed lib/mothership files via rename detection; this finishes what the replay left incomplete: - Complete the #7151 port: cancel_workflow_run handler + registration on the mothership executor (types, tests, and catalog entry had already rename-followed in). - pickRunBlockOutputs awaits the now-async resolveOutputIds (#7346 made selector resolution child-workflow-aware) and re-imports isValidUuid. - Slack execution stream + tool-call-lifecycle import getToolDisplayTitle from the mothership tool-display module (#7296 semantics kept). - Resolve leftover conflict markers from the first replay's failed batch checkout (browser-tool hardening #7311, YAML bounds #7319 kept). - Drop the obsolete lib/mothership/tools/server/blocks scan root: the block-metadata tool died in the dead-code sweep; blocks reads flow through the already-guarded v2 blocks routes. - Regenerate OpenAPI, CLI API, and CLI docs from the merged contracts. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent 7083e16 commit b2f0dd7

13 files changed

Lines changed: 84 additions & 92 deletions

File tree

apps/docs/content/docs/cli/reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5497,7 +5497,7 @@ sim workflows run <workflowId> [options]
54975497
| `--input <json\|@file>` | No | Trigger input as JSON (JSON, or @path / @- to read a file or stdin). |
54985498
| `--async` | No | Queue the run and return immediately. |
54995499
| `--execution-timeout-seconds <value>` | No | Maximum duration of an asynchronous run, in seconds, capped by the plan's execution timeout. Requires `async: true`; otherwise returns `400`. |
5500-
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content) — in blockOutputs on a sync run, or from the streamed result with --follow; missing fields are omitted. Not available with --async (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
5500+
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content), or childWorkflowId.blockName.field for a child workflow (applies to every invocation) — in blockOutputs on a sync run, or from the streamed result with --follow; missing fields are omitted. Not available with --async (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
55015501
| `--include-file-base64` | No | Inline eligible output files as base64 content. Rejected when `async` is true. |
55025502
| `--no-include-file-base64` | No | Send --include-file-base64 as false. |
55035503
| `--base64-max-bytes <value>` | No | Maximum total bytes of file content to inline as base64, lowering but never raising the server limit of 16 MiB. Rejected when `async` is true. |

apps/docs/content/docs/cli/workflows.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ sim workflows run <workflowId> [options]
533533
| `--input <json\|@file>` | No | Trigger input as JSON (JSON, or @path / @- to read a file or stdin). |
534534
| `--async` | No | Queue the run and return immediately. |
535535
| `--execution-timeout-seconds <value>` | No | Maximum duration of an asynchronous run, in seconds, capped by the plan's execution timeout. Requires `async: true`; otherwise returns `400`. |
536-
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content) — in blockOutputs on a sync run, or from the streamed result with --follow; missing fields are omitted. Not available with --async (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
536+
| `--select-output <value...>` | No | Return blockName.field values (e.g. agent_1.content), or childWorkflowId.blockName.field for a child workflow (applies to every invocation) — in blockOutputs on a sync run, or from the streamed result with --follow; missing fields are omitted. Not available with --async (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
537537
| `--include-file-base64` | No | Inline eligible output files as base64 content. Rejected when `async` is true. |
538538
| `--no-include-file-base64` | No | Send --include-file-base64 as false. |
539539
| `--base64-max-bytes <value>` | No | Maximum total bytes of file content to inline as base64, lowering but never raising the server limit of 16 MiB. Rejected when `async` is true. |

apps/docs/openapi-v2-workflows.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10989,6 +10989,10 @@
1098910989
"StoredChatDeploymentOutputConfig": {
1099010990
"type": "object",
1099110991
"properties": {
10992+
"workflowId": {
10993+
"description": "Child workflow containing the selected block. Omitted for the deployed workflow.",
10994+
"type": "string"
10995+
},
1099210996
"blockId": {
1099310997
"type": "string",
1099410998
"description": "Block whose output the chat streams."
@@ -11298,6 +11302,11 @@
1129811302
"ChatDeploymentOutputConfig": {
1129911303
"type": "object",
1130011304
"properties": {
11305+
"workflowId": {
11306+
"description": "Child workflow containing the selected block. Omit for the deployed workflow.",
11307+
"type": "string",
11308+
"minLength": 1
11309+
},
1130111310
"blockId": {
1130211311
"type": "string",
1130311312
"minLength": 1,
@@ -11719,7 +11728,7 @@
1171911728
"type": "boolean"
1172011729
},
1172111730
"selectedOutputs": {
11722-
"description": "Block output references to include in the response, as `blockId`, `blockId.path`, or `BlockName.path` (resolved against the workflow state being run). On a sync request the named outputs come back in `blockOutputs`, keyed by these selector strings; on a stream they shape the streamed envelope. Selectors that resolve to no block or no value are omitted. Rejected when `async` is true — a queued run has produced nothing to select; narrow the finished run via the run resource instead.",
11731+
"description": "Block output references to include in the response. Use `<blockName>.<outputPath>` for the executed workflow or `<childWorkflowId>.<blockName>.<outputPath>` for a child workflow; block names are normalized workflow reference names, and selecting a child workflow applies to every invocation of it. On a sync request the named outputs come back in `blockOutputs`, keyed by these selector strings; on a stream they shape the streamed envelope. Selectors that resolve to no block or no value are omitted. Rejected when `async` is true — a queued run has produced nothing to select; narrow the finished run via the run resource instead.",
1172311732
"maxItems": 100,
1172411733
"type": "array",
1172511734
"items": {
@@ -12412,7 +12421,7 @@
1241212421
"description": "Whether a paused execution was cancelled."
1241312422
},
1241412423
"reason": {
12415-
"description": "Machine-readable cancellation outcome, present on every cancellation including full successes. `recorded` is the success value. `already_cancelled`, `already_completed`, and `already_failed` mean the run had already reached that terminal state, so nothing was cancelled and `durablyRecorded` is false. `redis_unavailable` and `redis_write_failed` mean the distributed cancellation signal was not written, so an already-running execution may not observe the cancellation. `paused_event_publish_failed` and `paused_database_cancel_failed` name the failing step for a paused run.",
12424+
"description": "Machine-readable cancellation outcome, present on every cancellation including full successes. `recorded` and `queue_cancelled` are successful cancellation values. `already_cancelled`, `already_completed`, and `already_failed` mean the run had already reached that terminal state, so nothing was cancelled and `durablyRecorded` is false. The remaining values identify a degraded or incomplete cancellation step.",
1241612425
"type": "string",
1241712426
"enum": [
1241812427
"recorded",
@@ -12422,7 +12431,10 @@
1242212431
"redis_unavailable",
1242312432
"redis_write_failed",
1242412433
"paused_event_publish_failed",
12425-
"paused_database_cancel_failed"
12434+
"paused_database_cancel_failed",
12435+
"queue_cancelled",
12436+
"active_resume_signal_failed",
12437+
"cancellation_not_finalized"
1242612438
]
1242712439
}
1242812440
},

apps/sim/lib/mothership/tool-executor/register-handlers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import {
3+
CancelWorkflowRun,
34
RunBlock,
45
RunFromBlock,
56
RunWorkflow,
@@ -11,6 +12,7 @@ import { executeFunctionExecute } from '../tools/handlers/function-execute'
1112
import { executeRunCode } from '../tools/handlers/run-code'
1213
import { executeSimCli } from '../tools/handlers/sim-cli'
1314
import {
15+
executeCancelWorkflowRun,
1416
executeRunBlock,
1517
executeRunFromBlock,
1618
executeRunWorkflow,
@@ -47,6 +49,7 @@ function h(fn: (params: any, context: any) => Promise<any>): ToolHandler {
4749
*/
4850
function buildHandlerMap(): Record<string, ToolHandler> {
4951
return {
52+
[CancelWorkflowRun.id]: h(executeCancelWorkflowRun),
5053
[RunWorkflow.id]: h(executeRunWorkflow),
5154
[RunWorkflowUntilBlock.id]: h(executeRunWorkflowUntilBlock),
5255
[RunFromBlock.id]: h(executeRunFromBlock),

apps/sim/lib/mothership/tool-executor/router.test.ts

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,14 @@
22
* @vitest-environment node
33
*/
44

5-
import { describe, expect, it, vi } from 'vitest'
6-
7-
/**
8-
* The handler map is a wiring table from tool id to implementation. Only its
9-
* shape is asserted here, so every implementation module it imports is stubbed
10-
* except `workflow/mutations`, which holds the cancellation handler under test
11-
* and loads for real so a renamed or removed export fails at link time.
12-
* Loading the rest reaches the block registry, the executor, and most of
13-
* `lib/`; every stubbed export resolves to a mock function, which is all the
14-
* table needs to bind.
15-
*/
16-
const { stubHandlerModule } = vi.hoisted(() => ({
17-
stubHandlerModule: () =>
18-
new Proxy(
19-
{},
20-
{
21-
get: (_target, name) => (typeof name === 'string' && name !== 'then' ? vi.fn() : undefined),
22-
has: (_target, name) => typeof name === 'string' && name !== 'then',
23-
}
24-
),
25-
}))
26-
27-
vi.mock('@/lib/copilot/tools/handlers/deployment/custom-block', stubHandlerModule)
28-
vi.mock('@/lib/copilot/tools/handlers/deployment/deploy', stubHandlerModule)
29-
vi.mock('@/lib/copilot/tools/handlers/deployment/manage', stubHandlerModule)
30-
vi.mock('@/lib/copilot/tools/handlers/function-execute', stubHandlerModule)
31-
vi.mock('@/lib/copilot/tools/handlers/integration-tools', stubHandlerModule)
32-
vi.mock('@/lib/copilot/tools/handlers/management/connect-slack-bot', stubHandlerModule)
33-
vi.mock('@/lib/copilot/tools/handlers/management/manage-credential', stubHandlerModule)
34-
vi.mock('@/lib/copilot/tools/handlers/management/manage-custom-tool', stubHandlerModule)
35-
vi.mock('@/lib/copilot/tools/handlers/management/manage-mcp-tool', stubHandlerModule)
36-
vi.mock('@/lib/copilot/tools/handlers/management/manage-sandbox', stubHandlerModule)
37-
vi.mock('@/lib/copilot/tools/handlers/management/manage-skill', stubHandlerModule)
38-
vi.mock('@/lib/copilot/tools/handlers/materialize-file', stubHandlerModule)
39-
vi.mock('@/lib/copilot/tools/handlers/oauth', stubHandlerModule)
40-
vi.mock('@/lib/copilot/tools/handlers/resources', stubHandlerModule)
41-
vi.mock('@/lib/copilot/tools/handlers/restore-resource', stubHandlerModule)
42-
vi.mock('@/lib/copilot/tools/handlers/run-code', stubHandlerModule)
43-
vi.mock('@/lib/copilot/tools/handlers/vfs', stubHandlerModule)
44-
vi.mock('@/lib/copilot/tools/handlers/vfs-mutate', stubHandlerModule)
45-
vi.mock('@/lib/copilot/tools/handlers/workflow/queries', stubHandlerModule)
46-
47-
/** Server-router tools are appended to the map from their own registry, which this test does not cover. */
48-
vi.mock('@/lib/copilot/tools/server/router', () => ({ getRegisteredServerToolNames: () => [] }))
49-
50-
import { hasHandler } from '@/lib/copilot/tool-executor/executor'
51-
import { buildHandlerMap } from '@/lib/copilot/tool-executor/handler-map'
52-
import { ensureHandlersRegistered } from '@/lib/copilot/tool-executor/register-handlers'
5+
import { describe, expect, it } from 'vitest'
6+
import { hasHandler } from '@/lib/mothership/tool-executor/executor'
7+
import { ensureHandlersRegistered } from '@/lib/mothership/tool-executor/register-handlers'
538
import {
549
getToolEntry,
5510
isSimExecuted,
5611
toolRequiresApproval,
57-
} from '@/lib/copilot/tool-executor/router'
58-
import { executeCancelWorkflowRun } from '@/lib/copilot/tools/handlers/workflow/mutations'
12+
} from '@/lib/mothership/tool-executor/router'
5913

6014
describe('workflow-run cancellation tool routing', () => {
6115
it('routes cancellation through Sim with write permission and explicit approval', () => {

apps/sim/lib/mothership/tools/handlers/workflow/mutations.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
* @vitest-environment node
33
*/
44
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
import { WorkflowRunAlreadyTerminalError } from '@/lib/execution/workflow-run-already-terminal-error'
56
import type { ExecutionContext } from '@/lib/mothership/request/types'
67
import type { CancelWorkflowRunParams } from '@/lib/mothership/tools/handlers/param-types'
7-
import { WorkflowRunAlreadyTerminalError } from '@/lib/execution/workflow-run-already-terminal-error'
88

99
const { mocks } = vi.hoisted(() => ({
1010
mocks: {

apps/sim/lib/mothership/tools/handlers/workflow/mutations.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type ToolEffectPhase,
1515
} from '@/lib/mothership/tool-executor/types'
1616
import type {
17+
CancelWorkflowRunParams,
1718
CreateWorkflowParams,
1819
GenerateApiKeyParams,
1920
MoveWorkflowParams,
@@ -28,7 +29,6 @@ import type {
2829
} from '@/lib/mothership/tools/handlers/param-types'
2930
import { requireCopilotWorkspace } from '@/lib/mothership/tools/server/workspace-scope'
3031
import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/mothership/vfs/path-utils'
31-
import { PlatformEvents } from '@/lib/core/telemetry'
3232
import { cancelWorkflowRun } from '@/lib/workflows/application/cancel-run'
3333
import { createWorkflow } from '@/lib/workflows/application/create-workflow'
3434
import { moveWorkflowsBulk } from '@/lib/workflows/application/move-workflows-bulk'
@@ -283,6 +283,45 @@ export async function executeRunWorkflow(
283283
}
284284
}
285285

286+
export async function executeCancelWorkflowRun(
287+
params: CancelWorkflowRunParams,
288+
context: ExecutionContext
289+
): Promise<ToolCallResult> {
290+
try {
291+
const executionId = resolveInputFromExecutionId(params.executionId)
292+
if (!executionId) {
293+
return { success: false, error: 'executionId is required' }
294+
}
295+
296+
assertWorkflowMutationNotAborted(
297+
context,
298+
'Request aborted before workflow run cancellation could be applied.'
299+
)
300+
const result = await executeCopilotWorkflowUseCase(context, cancelWorkflowRun, {
301+
runId: executionId,
302+
...(context.abortSignal ? { abortSignal: context.abortSignal } : {}),
303+
})
304+
305+
return {
306+
success: result.success,
307+
output: {
308+
workflowId: result.workflowId,
309+
executionId: result.executionId,
310+
durablyRecorded: result.durablyRecorded,
311+
locallyAborted: result.locallyAborted,
312+
pausedCancelled: result.pausedCancelled,
313+
reason: result.reason,
314+
},
315+
error: result.success ? undefined : 'Workflow run cancellation could not be completed',
316+
}
317+
} catch (error) {
318+
return {
319+
success: false,
320+
error: messageForCopilotWorkflowError(error, 'Failed to cancel workflow run'),
321+
}
322+
}
323+
}
324+
286325
export async function executeSetGlobalWorkflowVariables(
287326
params: SetGlobalWorkflowVariablesParams,
288327
context: ExecutionContext

apps/sim/lib/mothership/tools/server/files/doc-compiled-store.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ vi.mock('@/lib/uploads/core/storage-service', () => ({
1515
uploadFile: mockUploadFile,
1616
}))
1717

18+
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
1819
import {
1920
loadPublishedCompiledDoc,
2021
storeCompiledDoc,
2122
} from '@/lib/mothership/tools/server/files/doc-compiled-store'
22-
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
2323
import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'
2424

2525
describe('compiled document publication', () => {

apps/sim/lib/webhooks/slack-execution-stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { getErrorMessage } from '@sim/utils/errors'
22
import { getValueAtPath, isRecordLike } from '@sim/utils/object'
33
import { truncate } from '@sim/utils/string'
4-
import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display'
54
import type { LoggingSession } from '@/lib/logs/execution/logging-session'
5+
import { getToolDisplayTitle } from '@/lib/mothership/tools/tool-display'
66
import { getSlackBotCredential } from '@/lib/oauth/credential-service'
77
import {
88
appendSlackAgentStream,

apps/sim/lib/workflows/executor/execute-service.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { WorkflowExecutionPrincipal } from '@sim/auth/principal'
22
import type { workflow as workflowTable } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { getErrorMessage, toError } from '@sim/utils/errors'
5-
import { generateId } from '@sim/utils/id'
5+
import { generateId, isValidUuid } from '@sim/utils/id'
66
import type { BlockState } from '@sim/workflow-types/workflow'
77
import { releaseExecutionSlot } from '@/lib/billing/calculations/usage-reservation'
88
import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
@@ -747,7 +747,7 @@ export async function executeWorkflowService(
747747
status: 'failed',
748748
aborted: 'timeout',
749749
output: compactTimeoutOutput,
750-
blockOutputs: await compactServiceOutput(pickRunBlockOutputs(selectedOutputs, workflowBlocks, result.logs), compactionContext),
750+
blockOutputs: await compactServiceOutput(await pickRunBlockOutputs(selectedOutputs, workflowBlocks, result.logs), compactionContext),
751751
error: { message: timeoutErrorMessage, code: 'TIMEOUT' },
752752
resolvedSecretTraceProvenance: result.executionState?.resolvedSecretTraceProvenance,
753753
hasResponseBlock: false,
@@ -793,7 +793,7 @@ export async function executeWorkflowService(
793793
status,
794794
aborted: null,
795795
output: compactOutput,
796-
blockOutputs: await compactServiceOutput(pickRunBlockOutputs(selectedOutputs, workflowBlocks, result.logs), compactionContext),
796+
blockOutputs: await compactServiceOutput(await pickRunBlockOutputs(selectedOutputs, workflowBlocks, result.logs), compactionContext),
797797
error:
798798
status === 'failed' || (status === 'cancelled' && result.error)
799799
? classifyExecutionError(result.error ? new Error(result.error) : undefined, result)
@@ -848,7 +848,7 @@ export async function executeWorkflowService(
848848
executionResult.output,
849849
compactionContext
850850
)
851-
compactErrorBlockOutputs = await compactServiceOutput(pickRunBlockOutputs(selectedOutputs, workflowBlocks, executionResult.logs), compactionContext)
851+
compactErrorBlockOutputs = await compactServiceOutput(await pickRunBlockOutputs(selectedOutputs, workflowBlocks, executionResult.logs), compactionContext)
852852
} catch (compactError) {
853853
if (
854854
compactError instanceof PayloadSizeLimitError &&
@@ -952,19 +952,19 @@ function resolveOutputPath(value: unknown, path: string[]): unknown {
952952
* follow. The last log per block wins, so a block inside a loop reports its
953953
* final iteration's output.
954954
*/
955-
export function pickRunBlockOutputs(
955+
export async function pickRunBlockOutputs(
956956
selectedOutputs: string[] | undefined,
957957
blocks: Record<string, unknown>,
958958
logs: BlockLog[] | undefined
959-
): Record<string, unknown> | null {
959+
): Promise<Record<string, unknown> | null> {
960960
if (!selectedOutputs || selectedOutputs.length === 0) return null
961961

962962
const outputByBlockId = new Map<string, unknown>()
963963
for (const log of logs ?? []) {
964964
if (log.output !== undefined) outputByBlockId.set(log.blockId, log.output)
965965
}
966966

967-
const resolved = resolveOutputIds(selectedOutputs, blocks) ?? []
967+
const resolved = (await resolveOutputIds(selectedOutputs, blocks)) ?? []
968968
const picked: Record<string, unknown> = {}
969969
for (let i = 0; i < selectedOutputs.length; i++) {
970970
const selector = selectedOutputs[i]

0 commit comments

Comments
 (0)