| β¦ | β¦ | @@ -6807,6 +6809,84 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio | |
| 6847 | + | }); | |
| 6848 | + | } | |
| 6849 | + | // A request is answered once; later updates cannot reopen or change it. | |
| 6850 | + | if (existing !== undefined && existing.secretStatus !== "pending") return; | |
π‘ Medium orchestration-v2/Orchestrator.ts:6850
A concurrent run termination can mark a secret request cancelled after answerSecretRequest has stored the secret but before its saved update arrives, leaving the UI cancelled while an active webhook secret exists. Because this branch silently returns for any non-pending card, the subsequent saved update is discarded; serialize the save/cancel transition or make the update atomic so a completed save cannot be cancelled.
Also found in 1 other location(s)
apps/server/src/mcp/OrchestratorMcpService.ts:1429
requestSecretcan race a user save with parent-run termination: afteranswerSecretRequesthas checked the card is pending and stored the secret, this branch can writecancelledbefore that service dispatches itssavedupdate. The command handler treats the subsequentsavedupdate as a no-op, so the MCP call reportscancelledeven though the webhook task now has an active secret. An agent may then tell the user setup failed or issue a duplicate request despite the task being configured.
π Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/server/src/orchestration-v2/Orchestrator.ts around line 6850: |
| 2 | |
| 3 | A concurrent run termination can mark a secret request `cancelled` after `answerSecretRequest` has stored the secret but before its `saved` update arrives, leaving the UI cancelled while an active webhook secret exists. Because this branch silently returns for any non-`pending` card, the subsequent `saved` update is discarded; serialize the save/cancel transition or make the update atomic so a completed save cannot be cancelled. |
| 4 | |
| 5 | Also found in 1 other location(s): |
| 6 | - apps/server/src/mcp/OrchestratorMcpService.ts:1429 -- `requestSecret` can race a user save with parent-run termination: after `answerSecretRequest` has checked the card is pending and stored the secret, this branch can write `cancelled` before that service dispatches its `saved` update. The command handler treats the subsequent `saved` update as a no-op, so the MCP call reports `cancelled` even though the webhook task now has an active secret. An agent may then tell the user setup failed or issue a duplicate request despite the task being configured. |
The save path checks the run first and refuses an answer once it has ended. If the run ends in the moment between that check and the record, the stored value expires unused and the hourly sweep deletes it, so it doesn't linger.
The check still leaves the interval before the save record. If cancellation wins there, the saved record is ignored while savedRef can retrieve the already-stored value directly until expiry. The transition still needs serialization/atomicity.
Fixed in f71850c. Instead of locking across the store write and the record, the save now re-reads the card after recording. If the agent's wait closed it first, the record changed nothing, so the stored value is deleted straight away and the user gets agent_stopped. savedRef can't return it after that. A test closes the card just before the save's record lands, and fails without the change.
| β¦ | β¦ | @@ -1151,6 +1165,75 @@ export const layer = Layer.effect( | |
| 1205 | + | if (item?.type !== "secret_request" || item.runId === null || item.nodeId === null) { | |
| 1206 | + | return yield* taskError("This secret request no longer exists."); | |
| 1207 | + | } | |
| 1208 | + | if (item.secretStatus !== "pending") { | |
π High scheduledTasks/ScheduledTaskService.ts:1208
Concurrent answerSecretRequest calls can both pass the pending check, so a save may persist webhook_secret while a concurrent decline records the request as declined, or two saves may overwrite each other. Because the secret write and secret_request.record dispatch are separate operations, the card's terminal status is not atomically claimed; claim the pending request before applying the answer and secret.
Also found in 1 other location(s)
apps/server/src/ws.ts:2116
scheduledTasks.answerSecretRequestis exposed directly without serializing answers. Two concurrent RPC calls for the same pending card can both pass the service's pending-status check before either dispatches the status update; for example, asavecan persist a secret while a simultaneousdeclinerecords the card as declined, or two saves can overwrite each other's secret. This violates the one-answer behavior and can leave the task configured with a secret the card says was declined.
No longer relevant as of 084cb628c174adf60120c34a9ffbe46ed8e47e3d
| β¦ | β¦ | @@ -1321,6 +1346,103 @@ const make = Effect.gen(function* () { | |
| 1431 | + | } | |
| 1432 | + | yield* Effect.sleep(Duration.millis(SECRET_REQUEST_POLL_INTERVAL_MS)); | |
| 1433 | + | } | |
| 1434 | + | }).pipe( | |
π‘ Medium mcp/OrchestratorMcpService.ts:1434
When the wait reaches timeoutMs, this returns status: "pending" without cancelling the secret_request turn item. The polling fiber is interrupted by Effect.timeoutOption, so a later run termination cannot mark the request cancelled and answerSecretRequest can still store a secret for the completed run. Add timeout cleanup that records cancelled before returning.
No longer relevant as of 7c080780f8cb160dcb317230856808ed0f57f8c2
| β¦ | β¦ | @@ -1299,6 +1299,16 @@ export function createServerEnvironmentAtoms<R, E>( | |
| 1306 | + | tag: WS_METHODS.scheduledTasksAnswerSecretRequest, | |
| 1307 | + | concurrency: { | |
| 1308 | + | mode: "singleFlight", | |
| 1309 | + | key: ({ environmentId, input }) => `${environmentId}:${input.threadId}:${input.turnItemId}`, | |
π‘ Medium state/server.ts:1309
Distinct secret-request cards can share this singleFlight key, so the second answer is deduplicated while the first is in flight; its secret is never sent, but its caller receives the first command's result and may show the wrong card as answered. Encode the three identifiers structurally (for example, with JSON.stringify) so delimiter-containing IDs remain distinct.
π Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @packages/client-runtime/src/state/server.ts around line 1309: |
| 2 | |
| 3 | Distinct secret-request cards can share this `singleFlight` key, so the second answer is deduplicated while the first is in flight; its secret is never sent, but its caller receives the first command's result and may show the wrong card as answered. Encode the three identifiers structurally (for example, with `JSON.stringify`) so delimiter-containing IDs remain distinct. |