Skip to content

Commit 9c9161c

Browse files
icecrasher321claude
andcommitted
fix(copilot): name the run from the executor, not the logging session
Review found the logging session wrong in both directions, and it is: the result of `safeStart` is never checked, so blocks execute even when it fails — reporting that nothing started for a run that did, which is the direction that duplicates work — and it flips before trigger resolution and serialization, reporting a run for failures that never reached a block. A resume whose conditional update matches no row returns true and named a run that does not exist. Entering the executor is the only honest answer to "could a side effect have occurred", because side effects come from blocks rather than from log rows. Moving the marker there settles all three at once. Also from review: - Stop returning the thrown environment or database error to the model when the egress catalog is unavailable. Nothing there can project it — the catalog it would need is the very thing that is missing — so the reason stays in the log and the response carries fixed text plus the workspace id the caller itself supplied. - Guard the attach against a frozen failure, which would otherwise throw and replace the original error partway through cleanup, making a diagnostic aid the thing that loses the diagnosis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 2e2b803 commit 9c9161c

4 files changed

Lines changed: 37 additions & 15 deletions

File tree

apps/sim/app/api/copilot/tools/execute/route.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ describe('POST /api/copilot/tools/execute (in-band)', () => {
109109

110110
expect(mockHandler).not.toHaveBeenCalled()
111111
expect(body.success).toBe(false)
112-
expect(body.error).toContain('Workspace ws-gone does not exist')
113112
expect(body.output).toEqual({ resultWithheld: true, effect: 'not_attempted' })
113+
// The thrown reason is an unprojectable environment failure — the catalog that would
114+
// vouch for it is the very thing missing — so it stays in the log.
115+
expect(body.error).not.toContain('does not exist')
116+
expect(body.error).toContain(BASE_BODY.workspaceId)
117+
expect(body.error).toContain('could not be resolved')
114118
})
115119

116120
it('reuses one turn registry across calls that share a messageId', async () => {

apps/sim/app/api/copilot/tools/execute/route.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,25 @@ export const POST = withRouteHandler((request: NextRequest) =>
134134
* The cause is almost always the workspace itself — a deleted or inaccessible id
135135
* reaching this lane — which is actionable, so it is reported rather than swallowed.
136136
*/
137-
const reason = getErrorMessage(err)
138137
logger.error('In-band egress registry unavailable; refusing the call', {
139138
toolName,
140139
toolCallId,
141140
userId,
142141
workspaceId,
143-
error: reason,
142+
error: getErrorMessage(err),
144143
})
145144
rootSpan.setAttributes({ [TraceAttr.ToolOutcome]: MothershipStreamV1ToolOutcome.error })
145+
/**
146+
* The thrown reason stays in the log. It is an environment or database failure that
147+
* nothing here can project — the catalog it needed is the very thing that is missing —
148+
* so this is the one message on this route that must be fixed text. The workspace id
149+
* is echoed because the caller supplied it, and it is what makes this actionable.
150+
*/
146151
return NextResponse.json({
147152
success: false,
148-
error: `${toolName} was not run: ${reason}`,
153+
error: workspaceId
154+
? `${toolName} was not run: its workspace (${workspaceId}) could not be resolved. Check that the workspace exists and is accessible before retrying.`
155+
: `${toolName} was not run: its execution environment could not be resolved.`,
149156
output: { resultWithheld: true, effect: TOOL_EFFECT_PHASE.notAttempted },
150157
})
151158
}

apps/sim/executor/utils/errors.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ const ATTEMPTED_EXECUTION_ID = 'attemptedExecutionId'
5353
export function attachAttemptedExecutionId(error: unknown, executionId: string): void {
5454
if (!isAttachableThrown(error) || !executionId) return
5555
if (Object.hasOwn(error, ATTEMPTED_EXECUTION_ID)) return
56-
Object.assign(error, { [ATTEMPTED_EXECUTION_ID]: executionId })
56+
/**
57+
* A frozen or sealed failure would otherwise throw here and replace the original error
58+
* partway through cleanup, turning a diagnostic aid into the thing that loses the
59+
* diagnosis. Losing the id is the lesser failure, and the log still records the run.
60+
*/
61+
try {
62+
Object.assign(error, { [ATTEMPTED_EXECUTION_ID]: executionId })
63+
} catch {}
5764
}
5865

5966
/** Reads the dispatched-run id a thrown value carries, if dispatch was reached at all. */

apps/sim/lib/workflows/executor/execution-core.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ async function executeWorkflowCoreImpl(
424424
let processedInput = input || {}
425425
let deploymentVersionId: string | undefined
426426
let loggingStarted = false
427+
let executorStarted = false
427428
let resolvedSecretTraceRegistry: ResolvedSecretTraceRegistry | undefined
428429
const pendingLifecycleCallbacks = new Set<Promise<void>>()
429430

@@ -1051,6 +1052,14 @@ async function executeWorkflowCoreImpl(
10511052
contextExtensions,
10521053
})
10531054

1055+
/**
1056+
* The last statement before a block can run, and therefore the only honest answer to
1057+
* "could a side effect have occurred". The logging session is the wrong proxy in both
1058+
* directions: `safeStart`'s result is never checked, so blocks execute even when it
1059+
* fails — reporting nothing started for a run that did — and it flips before trigger
1060+
* resolution and serialization, reporting a run for failures that never reached a block.
1061+
*/
1062+
executorStarted = true
10541063
const result = runFromBlock
10551064
? ((await executorInstance.executeFromBlock(
10561065
workflowId,
@@ -1098,17 +1107,12 @@ async function executeWorkflowCoreImpl(
10981107
return result
10991108
} catch (error: unknown) {
11001109
/**
1101-
* Whether the run had started when it failed, read before the recovery `safeStart` below
1102-
* writes a row for the failure itself. This — not entry into this function — is what says
1103-
* blocks may have executed: everything above it (custom-block loading, state loading,
1104-
* environment and secret setup) fails having run nothing, and a caller told otherwise
1105-
* would refuse a retry that was safe.
1106-
*
1107-
* The thrown value is rethrown exactly as received, including a non-Error one, because
1108-
* the finalization guard below identifies it. A primitive therefore carries no id, which
1109-
* costs nothing today: every throw site past `safeStart` raises an Error.
1110+
* Named only once a block could have run. The thrown value is rethrown exactly as
1111+
* received, including a non-Error one, because the finalization guard below identifies
1112+
* it; a primitive therefore carries no id, which costs nothing today because every throw
1113+
* site past this point raises an Error.
11101114
*/
1111-
if (loggingStarted) attachAttemptedExecutionId(error, executionId)
1115+
if (executorStarted) attachAttemptedExecutionId(error, executionId)
11121116
const errorCause = describeErrorCause(error)
11131117
logger.error(
11141118
`[${requestId}] Execution failed:`,

0 commit comments

Comments
 (0)