Skip to content

Commit f42cf0a

Browse files
fix(server,web): address devin provider review findings
- decline permission/elicitation/extension requests during Devin text generation so short-lived turns cannot stall on input that never arrives - pass linkedContext and surface needsRefinement for Devin thread titles - keep a saved Devin model visible in the picker while unavailable - fail a Devin sendTurn that captured a session replaced mid-preparation - give concurrent MCP bridge writes unique temp files Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 47c4e2c commit f42cf0a

5 files changed

Lines changed: 41 additions & 3 deletions

File tree

‎apps/server/src/mcp/McpStdioBridge.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ main();
144144

145145
export const MCP_STDIO_BRIDGE_FILE_NAME = "mcp-stdio-bridge.mjs";
146146

147+
// Concurrent writers in one process share the pid, so a counter keeps each
148+
// temporary file unique.
149+
let bridgeWriteCounter = 0;
150+
147151
/**
148152
* Ensures the bridge script exists under `stateDir` and returns its absolute
149153
* path. Reuses an up-to-date copy so warm restarts do not rewrite the file.
@@ -160,7 +164,7 @@ export const ensureMcpStdioBridge = (
160164
return bridgePath;
161165
}
162166
yield* fileSystem.makeDirectory(stateDir, { recursive: true });
163-
const tempPath = `${bridgePath}.${process.pid}.tmp`;
167+
const tempPath = `${bridgePath}.${process.pid}.${bridgeWriteCounter++}.tmp`;
164168
yield* fileSystem.writeFileString(tempPath, MCP_STDIO_BRIDGE_SOURCE);
165169
yield* fileSystem.rename(tempPath, bridgePath);
166170
return bridgePath;

‎apps/server/src/provider/Layers/DevinAdapter.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,18 @@ export const makeDevinAdapter = Effect.fn("makeDevinAdapter")(function* (
817817
return yield* Effect.gen(function* () {
818818
const launch = yield* context.promptLock.withPermit(
819819
Effect.gen(function* () {
820-
yield* requireSession(input.threadId);
820+
// Attachments were read before the lock was acquired; if the session
821+
// was stopped and restarted meanwhile, `context` names a closed
822+
// runtime and this send must not proceed on it.
823+
const current = yield* requireSession(input.threadId);
824+
if (current !== context) {
825+
return yield* new ProviderAdapterRequestError({
826+
provider: PROVIDER,
827+
method: "session/prompt",
828+
detail:
829+
"The Devin session was restarted while the turn was being prepared. Send again.",
830+
});
831+
}
821832
const requestedModel = input.modelSelection?.model ?? context.session.model;
822833
const requested = resolveDevinModelId(requestedModel);
823834
const modelOption = findDevinModelConfigOption(yield* context.runtime.getConfigOptions);

‎apps/server/src/textGeneration/DevinTextGeneration.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ export const makeDevinTextGeneration = Effect.fn("makeDevinTextGeneration")(func
7575
Effect.provideService(FileSystem.FileSystem, fileSystem),
7676
);
7777

78+
// Text generation has no interactive user and no workspace tools: any
79+
// permission, elicitation, or extension request is declined so the turn
80+
// cannot stall waiting for input that will never arrive.
81+
yield* runtime.handleRequestPermission(() =>
82+
Effect.succeed({ outcome: { outcome: "cancelled" as const } }),
83+
);
84+
yield* runtime.handleElicitation(() =>
85+
Effect.succeed({ action: { action: "decline" as const } }),
86+
);
87+
yield* runtime.handleUnknownExtRequest((method) =>
88+
Effect.fail(
89+
new EffectAcpErrors.AcpRequestError({
90+
code: -32601,
91+
errorMessage: `Extension request '${method}' is disabled for text generation.`,
92+
}),
93+
),
94+
);
7895
yield* runtime.handleSessionUpdate((notification) => {
7996
const update = notification.update;
8097
if (update.sessionUpdate !== "agent_message_chunk") {
@@ -256,6 +273,7 @@ export const makeDevinTextGeneration = Effect.fn("makeDevinTextGeneration")(func
256273
const { prompt, outputSchema } = buildThreadTitlePrompt({
257274
message: input.message,
258275
previousTitle: input.previousTitle,
276+
linkedContext: input.linkedContext,
259277
attachments: input.attachments,
260278
});
261279

@@ -269,6 +287,7 @@ export const makeDevinTextGeneration = Effect.fn("makeDevinTextGeneration")(func
269287

270288
return {
271289
title: sanitizeThreadTitle(generated.title),
290+
...(generated.needsRefinement ? { needsRefinement: true } : {}),
272291
} satisfies TextGeneration.ThreadTitleGenerationResult;
273292
});
274293

‎apps/web/src/components/chat/ModelPickerContent.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ describe("shouldIncludeModelPickerOption", () => {
5757
["opencode", "warning"],
5858
["antigravity", "error"],
5959
["antigravity", "warning"],
60+
["devin", "error"],
61+
["devin", "warning"],
6062
] as const)(
6163
"keeps only the active synthetic %s row when the provider status is %s",
6264
(driver, status) => {

‎apps/web/src/components/chat/ModelPickerContent.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ export function shouldIncludeModelPickerOption(input: {
9191
if (isProviderInstancePickerReady(input.entry)) return true;
9292
return (
9393
input.entry.enabled &&
94-
(input.entry.driverKind === "opencode" || input.entry.driverKind === "antigravity") &&
94+
(input.entry.driverKind === "opencode" ||
95+
input.entry.driverKind === "antigravity" ||
96+
input.entry.driverKind === "devin") &&
9597
input.entry.instanceId === input.activeInstanceId &&
9698
input.option.slug === input.activeModel &&
9799
input.option.isUnavailable === true

0 commit comments

Comments
 (0)