mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +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. */
|
/** Stdio buffer ceiling in bytes; defaults to 50 MiB. */
|
||||||
private Long stdioBufferLimitBytes;
|
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. */
|
/** UNKNOWN / OK / ERROR — last test result. */
|
||||||
private String lastStatus;
|
private String lastStatus;
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import vip.mate.acp.model.AcpEndpointEntity;
|
|||||||
import vip.mate.exception.MateClawException;
|
import vip.mate.exception.MateClawException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -47,11 +46,6 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AcpDelegationService {
|
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 INITIALIZE_TIMEOUT_MS = 15_000L;
|
||||||
private static final long SESSION_NEW_TIMEOUT_MS = 10_000L;
|
private static final long SESSION_NEW_TIMEOUT_MS = 10_000L;
|
||||||
|
|
||||||
@ -89,6 +83,7 @@ public class AcpDelegationService {
|
|||||||
List<String> args = endpointService.parseArgs(endpoint);
|
List<String> args = endpointService.parseArgs(endpoint);
|
||||||
Map<String, String> env = endpointService.parseEnv(endpoint);
|
Map<String, String> env = endpointService.parseEnv(endpoint);
|
||||||
boolean trusted = !Boolean.FALSE.equals(endpoint.getTrusted());
|
boolean trusted = !Boolean.FALSE.equals(endpoint.getTrusted());
|
||||||
|
long promptTimeoutMillis = resolvePromptTimeoutMillis(endpoint);
|
||||||
// Always resolve cwd to a real directory: Zed's ACP Zod schema
|
// Always resolve cwd to a real directory: Zed's ACP Zod schema
|
||||||
// marks cwd as a required string and rejects {@code undefined}
|
// marks cwd as a required string and rejects {@code undefined}
|
||||||
// with -32602. See {@link AcpRuntimeSupport#resolveCwd}.
|
// with -32602. See {@link AcpRuntimeSupport#resolveCwd}.
|
||||||
@ -124,7 +119,7 @@ public class AcpDelegationService {
|
|||||||
ObjectNode promptParams = objectMapper.createObjectNode();
|
ObjectNode promptParams = objectMapper.createObjectNode();
|
||||||
promptParams.put("sessionId", sessionId);
|
promptParams.put("sessionId", sessionId);
|
||||||
promptParams.set("prompt", buildPromptArray(userPrompt));
|
promptParams.set("prompt", buildPromptArray(userPrompt));
|
||||||
autoClose.sendRequest("session/prompt", promptParams, PROMPT_TIMEOUT.toMillis());
|
autoClose.sendRequest("session/prompt", promptParams, promptTimeoutMillis);
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
|
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
|
||||||
log.warn("ACP delegation failed for endpoint '{}': {}", endpointName, e.getMessage());
|
log.warn("ACP delegation failed for endpoint '{}': {}", endpointName, e.getMessage());
|
||||||
@ -144,6 +139,12 @@ public class AcpDelegationService {
|
|||||||
return accumulator.toString().trim();
|
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,
|
private void wireHandlers(AcpStdioClient client, StringBuilder buf,
|
||||||
boolean trusted, String endpointName) {
|
boolean trusted, String endpointName) {
|
||||||
// Notifications carry session/update messages; agent_message_chunk
|
// Notifications carry session/update messages; agent_message_chunk
|
||||||
|
|||||||
@ -36,6 +36,9 @@ import java.util.Map;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AcpEndpointService {
|
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 AcpEndpointMapper mapper;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
@ -91,6 +94,7 @@ public class AcpEndpointService {
|
|||||||
if (input.getStdioBufferLimitBytes() == null || input.getStdioBufferLimitBytes() <= 0) {
|
if (input.getStdioBufferLimitBytes() == null || input.getStdioBufferLimitBytes() <= 0) {
|
||||||
input.setStdioBufferLimitBytes(50L * 1024L * 1024L);
|
input.setStdioBufferLimitBytes(50L * 1024L * 1024L);
|
||||||
}
|
}
|
||||||
|
input.setPromptTimeoutSeconds(normalizePromptTimeoutSeconds(input.getPromptTimeoutSeconds()));
|
||||||
if (input.getWorkspaceId() == null) input.setWorkspaceId(1L);
|
if (input.getWorkspaceId() == null) input.setWorkspaceId(1L);
|
||||||
mapper.insert(input);
|
mapper.insert(input);
|
||||||
log.info("Created ACP endpoint: {}", input.getName());
|
log.info("Created ACP endpoint: {}", input.getName());
|
||||||
@ -118,6 +122,9 @@ public class AcpEndpointService {
|
|||||||
if (patch.getStdioBufferLimitBytes() != null && patch.getStdioBufferLimitBytes() > 0) {
|
if (patch.getStdioBufferLimitBytes() != null && patch.getStdioBufferLimitBytes() > 0) {
|
||||||
existing.setStdioBufferLimitBytes(patch.getStdioBufferLimitBytes());
|
existing.setStdioBufferLimitBytes(patch.getStdioBufferLimitBytes());
|
||||||
}
|
}
|
||||||
|
if (patch.getPromptTimeoutSeconds() != null) {
|
||||||
|
existing.setPromptTimeoutSeconds(normalizePromptTimeoutSeconds(patch.getPromptTimeoutSeconds()));
|
||||||
|
}
|
||||||
mapper.updateById(existing);
|
mapper.updateById(existing);
|
||||||
publish(existing, AcpEndpointChangedEvent.Type.UPDATED);
|
publish(existing, AcpEndpointChangedEvent.Type.UPDATED);
|
||||||
return existing;
|
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) {
|
private List<String> parseStringList(String json) {
|
||||||
if (json == null || json.isBlank()) return Collections.emptyList();
|
if (json == null || json.isBlank()) return Collections.emptyList();
|
||||||
try {
|
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
|
- `initialize` handshake: 15s
|
||||||
- `session/new`: 10s
|
- `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`)
|
- 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) |
|
| `args_json` | TEXT | NULL | CLI args (JSON array) |
|
||||||
| `env_json` | TEXT | NULL | Env overrides (JSON object) |
|
| `env_json` | TEXT | NULL | Env overrides (JSON object) |
|
||||||
| `tool_parse_mode` | VARCHAR(32) | `call_title` | `call_title` / `call_detail` / `update_detail` |
|
| `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 |
|
| `builtin` | BOOLEAN | FALSE | Built-in rows are write-protected |
|
||||||
| `trusted` | BOOLEAN | TRUE | Auto-allow permission requests |
|
| `trusted` | BOOLEAN | TRUE | Auto-allow permission requests |
|
||||||
| `enabled` | BOOLEAN | FALSE | Off until you opt in |
|
| `enabled` | BOOLEAN | FALSE | Off until you opt in |
|
||||||
|
|||||||
@ -198,7 +198,7 @@ ACP 服务端可以在做敏感动作前(写文件、跑 shell 命令等)发
|
|||||||
|
|
||||||
- `initialize` 握手:15 秒
|
- `initialize` 握手:15 秒
|
||||||
- `session/new`:10 秒
|
- `session/new`:10 秒
|
||||||
- 整个 `session/prompt` 往返:5 分钟
|
- 整个 `session/prompt` 往返:默认 300 秒,可按端点配置,最高 3600 秒
|
||||||
- stdio 缓冲上限:单次 50 MiB(行级 `stdio_buffer_limit_bytes` 可改)
|
- stdio 缓冲上限:单次 50 MiB(行级 `stdio_buffer_limit_bytes` 可改)
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -215,6 +215,7 @@ ACP 服务端可以在做敏感动作前(写文件、跑 shell 命令等)发
|
|||||||
| `args_json` | TEXT | NULL | CLI 参数(JSON 数组) |
|
| `args_json` | TEXT | NULL | CLI 参数(JSON 数组) |
|
||||||
| `env_json` | TEXT | NULL | 环境变量覆盖(JSON 对象) |
|
| `env_json` | TEXT | NULL | 环境变量覆盖(JSON 对象) |
|
||||||
| `tool_parse_mode` | VARCHAR(32) | `call_title` | `call_title` / `call_detail` / `update_detail` |
|
| `tool_parse_mode` | VARCHAR(32) | `call_title` | `call_title` / `call_detail` / `update_detail` |
|
||||||
|
| `prompt_timeout_seconds` | INT | 300 | `session/prompt` 调用超时,最高 3600 秒 |
|
||||||
| `builtin` | BOOLEAN | FALSE | 内置行写保护 |
|
| `builtin` | BOOLEAN | FALSE | 内置行写保护 |
|
||||||
| `trusted` | BOOLEAN | TRUE | 自动放行权限请求 |
|
| `trusted` | BOOLEAN | TRUE | 自动放行权限请求 |
|
||||||
| `enabled` | BOOLEAN | FALSE | 默认关闭,按需打开 |
|
| `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)',
|
args: 'Args (JSON array)',
|
||||||
env: 'Environment variables',
|
env: 'Environment variables',
|
||||||
toolParseMode: 'Tool parse mode',
|
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',
|
enabled: 'Enabled',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@ -2034,6 +2034,8 @@ export default {
|
|||||||
args: '参数 (JSON 数组)',
|
args: '参数 (JSON 数组)',
|
||||||
env: '环境变量',
|
env: '环境变量',
|
||||||
toolParseMode: 'Tool 解析模式',
|
toolParseMode: 'Tool 解析模式',
|
||||||
|
promptTimeoutSeconds: '调用超时(秒)',
|
||||||
|
promptTimeoutHint: '用于 session/prompt。默认 300 秒,复杂 OpenCode 任务可调高,最大 3600 秒。',
|
||||||
enabled: '启用',
|
enabled: '启用',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@ -126,6 +126,18 @@
|
|||||||
<option value="update_detail">update_detail</option>
|
<option value="update_detail">update_detail</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="form-group full-width">
|
||||||
<label class="form-label">{{ t('acp.fields.args') }}</label>
|
<label class="form-label">{{ t('acp.fields.args') }}</label>
|
||||||
<input v-model="form.argsJson" class="form-input mono" placeholder='["-y","@zed-industries/codex-acp"]' />
|
<input v-model="form.argsJson" class="form-input mono" placeholder='["-y","@zed-industries/codex-acp"]' />
|
||||||
@ -247,6 +259,7 @@ interface AcpEndpoint {
|
|||||||
lastError?: string
|
lastError?: string
|
||||||
lastTestedAt?: string
|
lastTestedAt?: string
|
||||||
stdioBufferLimitBytes?: number
|
stdioBufferLimitBytes?: number
|
||||||
|
promptTimeoutSeconds?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -264,6 +277,7 @@ const defaultForm = (): any => ({
|
|||||||
argsJson: '[]',
|
argsJson: '[]',
|
||||||
envJson: '{}',
|
envJson: '{}',
|
||||||
toolParseMode: 'call_title',
|
toolParseMode: 'call_title',
|
||||||
|
promptTimeoutSeconds: 300,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
})
|
})
|
||||||
const form = reactive<any>(defaultForm())
|
const form = reactive<any>(defaultForm())
|
||||||
@ -459,6 +473,7 @@ function openEditModal(ep: AcpEndpoint) {
|
|||||||
argsJson: ep.argsJson || '[]',
|
argsJson: ep.argsJson || '[]',
|
||||||
envJson: ep.envJson || '{}',
|
envJson: ep.envJson || '{}',
|
||||||
toolParseMode: ep.toolParseMode || 'call_title',
|
toolParseMode: ep.toolParseMode || 'call_title',
|
||||||
|
promptTimeoutSeconds: ep.promptTimeoutSeconds || 300,
|
||||||
enabled: !!ep.enabled,
|
enabled: !!ep.enabled,
|
||||||
})
|
})
|
||||||
// Always open in form mode so users see the structured editor first.
|
// 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 { 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: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-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; }
|
.form-textarea { resize: vertical; }
|
||||||
.toggle-inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
.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); }
|
.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