mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(acp): support configurable prompt timeout (#608)
This commit is contained in:
parent
5b3285c78f
commit
656a0b0436
@ -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;
|
||||
|
||||
|
||||
@ -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<String> args = endpointService.parseArgs(endpoint);
|
||||
Map<String, String> 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
|
||||
|
||||
@ -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<String> parseStringList(String json) {
|
||||
if (json == null || json.isBlank()) return Collections.emptyList();
|
||||
try {
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 |
|
||||
|
||||
@ -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 | 默认关闭,按需打开 |
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -2034,6 +2034,8 @@ export default {
|
||||
args: '参数 (JSON 数组)',
|
||||
env: '环境变量',
|
||||
toolParseMode: 'Tool 解析模式',
|
||||
promptTimeoutSeconds: '调用超时(秒)',
|
||||
promptTimeoutHint: '用于 session/prompt。默认 300 秒,复杂 OpenCode 任务可调高,最大 3600 秒。',
|
||||
enabled: '启用',
|
||||
},
|
||||
env: {
|
||||
|
||||
@ -126,6 +126,18 @@
|
||||
<option value="update_detail">update_detail</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ t('acp.fields.promptTimeoutSeconds') }}</label>
|
||||
<input
|
||||
v-model.number="form.promptTimeoutSeconds"
|
||||
class="form-input"
|
||||
type="number"
|
||||
min="1"
|
||||
max="3600"
|
||||
step="30"
|
||||
/>
|
||||
<p class="form-help">{{ t('acp.fields.promptTimeoutHint') }}</p>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">{{ t('acp.fields.args') }}</label>
|
||||
<input v-model="form.argsJson" class="form-input mono" placeholder='["-y","@zed-industries/codex-acp"]' />
|
||||
@ -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<any>(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); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user