diff --git a/mateclaw-server/src/main/java/vip/mate/acp/model/AcpEndpointEntity.java b/mateclaw-server/src/main/java/vip/mate/acp/model/AcpEndpointEntity.java index 88befe46..5effe8c2 100644 --- a/mateclaw-server/src/main/java/vip/mate/acp/model/AcpEndpointEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/acp/model/AcpEndpointEntity.java @@ -60,6 +60,9 @@ public class AcpEndpointEntity { /** Stdio buffer ceiling in bytes; defaults to 50 MiB. */ private Long stdioBufferLimitBytes; + /** Max wait for session/prompt, in seconds. Defaults to 300, capped at 3600. */ + private Integer promptTimeoutSeconds; + /** UNKNOWN / OK / ERROR — last test result. */ private String lastStatus; diff --git a/mateclaw-server/src/main/java/vip/mate/acp/service/AcpDelegationService.java b/mateclaw-server/src/main/java/vip/mate/acp/service/AcpDelegationService.java index 59b4eab2..dcc910e0 100644 --- a/mateclaw-server/src/main/java/vip/mate/acp/service/AcpDelegationService.java +++ b/mateclaw-server/src/main/java/vip/mate/acp/service/AcpDelegationService.java @@ -11,7 +11,6 @@ import vip.mate.acp.model.AcpEndpointEntity; import vip.mate.exception.MateClawException; import java.io.IOException; -import java.time.Duration; import java.util.List; import java.util.Map; @@ -47,11 +46,6 @@ import java.util.Map; @RequiredArgsConstructor public class AcpDelegationService { - /** Hard ceiling on a single ACP delegation. Long enough for a - * multi-turn coding session, short enough that a hung agent can't - * permanently block an LLM tool call. */ - private static final Duration PROMPT_TIMEOUT = Duration.ofMinutes(5); - private static final long INITIALIZE_TIMEOUT_MS = 15_000L; private static final long SESSION_NEW_TIMEOUT_MS = 10_000L; @@ -89,6 +83,7 @@ public class AcpDelegationService { List args = endpointService.parseArgs(endpoint); Map env = endpointService.parseEnv(endpoint); boolean trusted = !Boolean.FALSE.equals(endpoint.getTrusted()); + long promptTimeoutMillis = resolvePromptTimeoutMillis(endpoint); // Always resolve cwd to a real directory: Zed's ACP Zod schema // marks cwd as a required string and rejects {@code undefined} // with -32602. See {@link AcpRuntimeSupport#resolveCwd}. @@ -124,7 +119,7 @@ public class AcpDelegationService { ObjectNode promptParams = objectMapper.createObjectNode(); promptParams.put("sessionId", sessionId); promptParams.set("prompt", buildPromptArray(userPrompt)); - autoClose.sendRequest("session/prompt", promptParams, PROMPT_TIMEOUT.toMillis()); + autoClose.sendRequest("session/prompt", promptParams, promptTimeoutMillis); } catch (IOException | InterruptedException e) { if (e instanceof InterruptedException) Thread.currentThread().interrupt(); log.warn("ACP delegation failed for endpoint '{}': {}", endpointName, e.getMessage()); @@ -144,6 +139,12 @@ public class AcpDelegationService { return accumulator.toString().trim(); } + static long resolvePromptTimeoutMillis(AcpEndpointEntity endpoint) { + int seconds = AcpEndpointService.normalizePromptTimeoutSeconds( + endpoint != null ? endpoint.getPromptTimeoutSeconds() : null); + return seconds * 1000L; + } + private void wireHandlers(AcpStdioClient client, StringBuilder buf, boolean trusted, String endpointName) { // Notifications carry session/update messages; agent_message_chunk diff --git a/mateclaw-server/src/main/java/vip/mate/acp/service/AcpEndpointService.java b/mateclaw-server/src/main/java/vip/mate/acp/service/AcpEndpointService.java index fd2e7f60..5f660497 100644 --- a/mateclaw-server/src/main/java/vip/mate/acp/service/AcpEndpointService.java +++ b/mateclaw-server/src/main/java/vip/mate/acp/service/AcpEndpointService.java @@ -36,6 +36,9 @@ import java.util.Map; @RequiredArgsConstructor public class AcpEndpointService { + public static final int DEFAULT_PROMPT_TIMEOUT_SECONDS = 300; + public static final int MAX_PROMPT_TIMEOUT_SECONDS = 3600; + private final AcpEndpointMapper mapper; private final ObjectMapper objectMapper; private final ApplicationEventPublisher eventPublisher; @@ -91,6 +94,7 @@ public class AcpEndpointService { if (input.getStdioBufferLimitBytes() == null || input.getStdioBufferLimitBytes() <= 0) { input.setStdioBufferLimitBytes(50L * 1024L * 1024L); } + input.setPromptTimeoutSeconds(normalizePromptTimeoutSeconds(input.getPromptTimeoutSeconds())); if (input.getWorkspaceId() == null) input.setWorkspaceId(1L); mapper.insert(input); log.info("Created ACP endpoint: {}", input.getName()); @@ -118,6 +122,9 @@ public class AcpEndpointService { if (patch.getStdioBufferLimitBytes() != null && patch.getStdioBufferLimitBytes() > 0) { existing.setStdioBufferLimitBytes(patch.getStdioBufferLimitBytes()); } + if (patch.getPromptTimeoutSeconds() != null) { + existing.setPromptTimeoutSeconds(normalizePromptTimeoutSeconds(patch.getPromptTimeoutSeconds())); + } mapper.updateById(existing); publish(existing, AcpEndpointChangedEvent.Type.UPDATED); return existing; @@ -180,6 +187,13 @@ public class AcpEndpointService { } } + public static int normalizePromptTimeoutSeconds(Integer seconds) { + if (seconds == null || seconds <= 0) { + return DEFAULT_PROMPT_TIMEOUT_SECONDS; + } + return Math.min(seconds, MAX_PROMPT_TIMEOUT_SECONDS); + } + private List parseStringList(String json) { if (json == null || json.isBlank()) return Collections.emptyList(); try { diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V187__acp_prompt_timeout.sql b/mateclaw-server/src/main/resources/db/migration/h2/V187__acp_prompt_timeout.sql new file mode 100644 index 00000000..8e1364b4 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V187__acp_prompt_timeout.sql @@ -0,0 +1,7 @@ +-- Issue #608: make ACP session/prompt timeout configurable per endpoint. +ALTER TABLE mate_acp_endpoint + ADD COLUMN IF NOT EXISTS prompt_timeout_seconds INT NOT NULL DEFAULT 300; + +UPDATE mate_acp_endpoint +SET prompt_timeout_seconds = 300 +WHERE prompt_timeout_seconds IS NULL OR prompt_timeout_seconds <= 0; diff --git a/mateclaw-server/src/main/resources/db/migration/kingbase/V187__acp_prompt_timeout.sql b/mateclaw-server/src/main/resources/db/migration/kingbase/V187__acp_prompt_timeout.sql new file mode 100644 index 00000000..8e1364b4 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/kingbase/V187__acp_prompt_timeout.sql @@ -0,0 +1,7 @@ +-- Issue #608: make ACP session/prompt timeout configurable per endpoint. +ALTER TABLE mate_acp_endpoint + ADD COLUMN IF NOT EXISTS prompt_timeout_seconds INT NOT NULL DEFAULT 300; + +UPDATE mate_acp_endpoint +SET prompt_timeout_seconds = 300 +WHERE prompt_timeout_seconds IS NULL OR prompt_timeout_seconds <= 0; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V187__acp_prompt_timeout.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V187__acp_prompt_timeout.sql new file mode 100644 index 00000000..9f6ddf80 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V187__acp_prompt_timeout.sql @@ -0,0 +1,7 @@ +-- Issue #608: make ACP session/prompt timeout configurable per endpoint. +ALTER TABLE mate_acp_endpoint + ADD COLUMN prompt_timeout_seconds INT NOT NULL DEFAULT 300; + +UPDATE mate_acp_endpoint +SET prompt_timeout_seconds = 300 +WHERE prompt_timeout_seconds IS NULL OR prompt_timeout_seconds <= 0; diff --git a/mateclaw-server/src/main/resources/docs/en/acp.md b/mateclaw-server/src/main/resources/docs/en/acp.md index a0f925cf..fb5f77de 100644 --- a/mateclaw-server/src/main/resources/docs/en/acp.md +++ b/mateclaw-server/src/main/resources/docs/en/acp.md @@ -198,7 +198,7 @@ Hints surface in the test panel and in the streamed error message your agent rec - `initialize` handshake: 15s - `session/new`: 10s -- Whole `session/prompt` round-trip: 5 min +- Whole `session/prompt` round-trip: 300s by default, configurable per endpoint up to 3600s - Stdio buffer cap: 50 MiB per call (configurable on the row via `stdio_buffer_limit_bytes`) --- @@ -215,6 +215,7 @@ Hints surface in the test panel and in the streamed error message your agent rec | `args_json` | TEXT | NULL | CLI args (JSON array) | | `env_json` | TEXT | NULL | Env overrides (JSON object) | | `tool_parse_mode` | VARCHAR(32) | `call_title` | `call_title` / `call_detail` / `update_detail` | +| `prompt_timeout_seconds` | INT | 300 | `session/prompt` call timeout, capped at 3600s | | `builtin` | BOOLEAN | FALSE | Built-in rows are write-protected | | `trusted` | BOOLEAN | TRUE | Auto-allow permission requests | | `enabled` | BOOLEAN | FALSE | Off until you opt in | diff --git a/mateclaw-server/src/main/resources/docs/zh/acp.md b/mateclaw-server/src/main/resources/docs/zh/acp.md index a09246b7..5b173a35 100644 --- a/mateclaw-server/src/main/resources/docs/zh/acp.md +++ b/mateclaw-server/src/main/resources/docs/zh/acp.md @@ -198,7 +198,7 @@ ACP 服务端可以在做敏感动作前(写文件、跑 shell 命令等)发 - `initialize` 握手:15 秒 - `session/new`:10 秒 -- 整个 `session/prompt` 往返:5 分钟 +- 整个 `session/prompt` 往返:默认 300 秒,可按端点配置,最高 3600 秒 - stdio 缓冲上限:单次 50 MiB(行级 `stdio_buffer_limit_bytes` 可改) --- @@ -215,6 +215,7 @@ ACP 服务端可以在做敏感动作前(写文件、跑 shell 命令等)发 | `args_json` | TEXT | NULL | CLI 参数(JSON 数组) | | `env_json` | TEXT | NULL | 环境变量覆盖(JSON 对象) | | `tool_parse_mode` | VARCHAR(32) | `call_title` | `call_title` / `call_detail` / `update_detail` | +| `prompt_timeout_seconds` | INT | 300 | `session/prompt` 调用超时,最高 3600 秒 | | `builtin` | BOOLEAN | FALSE | 内置行写保护 | | `trusted` | BOOLEAN | TRUE | 自动放行权限请求 | | `enabled` | BOOLEAN | FALSE | 默认关闭,按需打开 | diff --git a/mateclaw-server/src/test/java/vip/mate/acp/service/AcpDelegationServiceTimeoutTest.java b/mateclaw-server/src/test/java/vip/mate/acp/service/AcpDelegationServiceTimeoutTest.java new file mode 100644 index 00000000..55c9c9a0 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/acp/service/AcpDelegationServiceTimeoutTest.java @@ -0,0 +1,36 @@ +package vip.mate.acp.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vip.mate.acp.model.AcpEndpointEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AcpDelegationServiceTimeoutTest { + + @Test + @DisplayName("ACP prompt timeout defaults to 5 minutes when endpoint is unset") + void defaultPromptTimeout() { + AcpEndpointEntity endpoint = new AcpEndpointEntity(); + + assertEquals(300_000L, AcpDelegationService.resolvePromptTimeoutMillis(endpoint)); + } + + @Test + @DisplayName("ACP prompt timeout uses the endpoint setting") + void endpointPromptTimeout() { + AcpEndpointEntity endpoint = new AcpEndpointEntity(); + endpoint.setPromptTimeoutSeconds(900); + + assertEquals(900_000L, AcpDelegationService.resolvePromptTimeoutMillis(endpoint)); + } + + @Test + @DisplayName("ACP prompt timeout is clamped to a one-hour hard ceiling") + void clampPromptTimeout() { + AcpEndpointEntity endpoint = new AcpEndpointEntity(); + endpoint.setPromptTimeoutSeconds(7200); + + assertEquals(3_600_000L, AcpDelegationService.resolvePromptTimeoutMillis(endpoint)); + } +} diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 91932603..34ce5962 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -2177,6 +2177,8 @@ export default { args: 'Args (JSON array)', env: 'Environment variables', toolParseMode: 'Tool parse mode', + promptTimeoutSeconds: 'Call timeout (seconds)', + promptTimeoutHint: 'Applies to session/prompt. Default 300 seconds, maximum 3600 seconds for long OpenCode tasks.', enabled: 'Enabled', }, env: { diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 7aadf6c3..fe31c3ea 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -2034,6 +2034,8 @@ export default { args: '参数 (JSON 数组)', env: '环境变量', toolParseMode: 'Tool 解析模式', + promptTimeoutSeconds: '调用超时(秒)', + promptTimeoutHint: '用于 session/prompt。默认 300 秒,复杂 OpenCode 任务可调高,最大 3600 秒。', enabled: '启用', }, env: { diff --git a/mateclaw-ui/src/views/AcpEndpoints.vue b/mateclaw-ui/src/views/AcpEndpoints.vue index 8b7f5e55..686ac4ae 100644 --- a/mateclaw-ui/src/views/AcpEndpoints.vue +++ b/mateclaw-ui/src/views/AcpEndpoints.vue @@ -126,6 +126,18 @@ +
+ + +

{{ t('acp.fields.promptTimeoutHint') }}

+
@@ -247,6 +259,7 @@ interface AcpEndpoint { lastError?: string lastTestedAt?: string stdioBufferLimitBytes?: number + promptTimeoutSeconds?: number } const { t } = useI18n() @@ -264,6 +277,7 @@ const defaultForm = (): any => ({ argsJson: '[]', envJson: '{}', toolParseMode: 'call_title', + promptTimeoutSeconds: 300, enabled: false, }) const form = reactive(defaultForm()) @@ -459,6 +473,7 @@ function openEditModal(ep: AcpEndpoint) { argsJson: ep.argsJson || '[]', envJson: ep.envJson || '{}', toolParseMode: ep.toolParseMode || 'call_title', + promptTimeoutSeconds: ep.promptTimeoutSeconds || 300, enabled: !!ep.enabled, }) // Always open in form mode so users see the structured editor first. @@ -616,6 +631,7 @@ td .status-error { background: none; color: var(--mc-text-tertiary); font-size: .form-input { padding: 8px 10px; border: 1px solid var(--mc-border); border-radius: 8px; font-size: 13px; color: var(--mc-text-primary); outline: none; background: var(--mc-bg-sunken); } .form-input:focus { border-color: var(--mc-primary); box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.1); } .form-input.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.form-help { margin: 0; font-size: 11px; line-height: 1.4; color: var(--mc-text-tertiary); } .form-textarea { resize: vertical; } .toggle-inline { display: flex; align-items: center; gap: 8px; font-size: 13px; } .modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 14px 22px; border-top: 1px solid var(--mc-border-light); }