diff --git a/src/main/agent/continue-host-adapter.ts b/src/main/agent/continue-host-adapter.ts
index 8e7b61c5..5ebff759 100644
--- a/src/main/agent/continue-host-adapter.ts
+++ b/src/main/agent/continue-host-adapter.ts
@@ -120,14 +120,13 @@ const continueHostStreamEventSchema = z.discriminatedUnion('type', [
const continueHostQuestionSchema = z
.object({
- requestId: z.string().min(1).max(128),
+ requestId: z.string().min(1),
timestamp: z.number().finite().optional(),
- question: z.string().trim().min(1).max(2_000),
+ question: z.string().trim().min(1),
options: z
- .array(z.string().trim().min(1).max(200))
- .max(20)
+ .array(z.string().trim().min(1))
.optional(),
- defaultAnswer: z.string().trim().max(2_000).optional()
+ defaultAnswer: z.string().trim().optional()
})
.strict()
@@ -925,7 +924,7 @@ export class ContinueHostAdapter {
patched = replaceExactly(
patched,
serverMarker,
- 'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&ue.length<=128&&typeof ce==="string"&&ce.length>0&&ce.length<=2e3?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"'
+ 'let j=(0,atn.default)();if(!process.env.GOODBUDDY_CONTINUE_HOST_TOKEN)throw new Error("Missing GoodBuddy host token");j.use((we,Te,ue)=>{we.headers.authorization===`Bearer ${process.env.GOODBUDDY_CONTINUE_HOST_TOKEN}`?ue():Te.status(401).json({error:"Unauthorized"})}),j.use(atn.default.json({limit:"20mb"})),j.post("/goodbuddy/question-answer",(we,Te)=>{let{requestId:ue,answer:ce,isCustomAnswer:de}=we.body??{};typeof ue==="string"&&ue.length>0&&typeof ce==="string"&&ce.length>0?Lbe.answerQuestion(ue,ce,de===!0)?Te.json({success:!0}):Te.status(404).json({error:"Question not pending"}):Te.status(400).json({error:"Invalid question answer"})}),j.get("/state"'
)
patched = replaceExactly(
patched,
diff --git a/src/main/agent/opencode-runtime.test.ts b/src/main/agent/opencode-runtime.test.ts
index c5422aa8..7856de11 100644
--- a/src/main/agent/opencode-runtime.test.ts
+++ b/src/main/agent/opencode-runtime.test.ts
@@ -2075,7 +2075,26 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
);
it.each(["parent", "child"] as const)("parses %s OpenCode questions and sends selected answers back", async (owner) => {
+ const longQuestion = `请选择实现方式\n${"发布说明内容。".repeat(6_000)}`;
const setup = runClient([
+ {
+ id: "question-tool-running",
+ type: "message.part.updated",
+ properties: {
+ sessionID: "session-1",
+ part: {
+ id: "part-question-1",
+ callID: "call-question-1",
+ type: "tool",
+ tool: "question",
+ state: {
+ status: "running",
+ input: { questions: [] },
+ time: { start: 1 },
+ },
+ },
+ },
+ },
{
id: "question-event",
type: "question.asked",
@@ -2085,7 +2104,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
questions: [
{
header: "实现方式",
- question: "请选择实现方式",
+ question: longQuestion,
options: [
{
label: "直接修改",
@@ -2106,6 +2125,25 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
},
},
},
+ {
+ id: "question-tool-completed",
+ type: "message.part.updated",
+ properties: {
+ sessionID: "session-1",
+ part: {
+ id: "part-question-1",
+ callID: "call-question-1",
+ type: "tool",
+ tool: "question",
+ state: {
+ status: "completed",
+ input: { questions: [] },
+ output: "Answered",
+ time: { start: 1, end: 2 },
+ },
+ },
+ },
+ },
{
id: "idle",
type: "session.idle",
@@ -2136,7 +2174,7 @@ describe("OpenCodeRuntime embedded permission mediation", () => {
questions: [
{
header: "实现方式",
- question: "请选择实现方式",
+ question: longQuestion,
multiple: false,
custom: true,
},
diff --git a/src/main/agent/opencode-runtime.ts b/src/main/agent/opencode-runtime.ts
index eb0a2c6b..46580f26 100644
--- a/src/main/agent/opencode-runtime.ts
+++ b/src/main/agent/opencode-runtime.ts
@@ -81,9 +81,6 @@ const MAX_PERMISSION_PATTERN_LENGTH = 1_024;
const MAX_PERMISSION_PATTERNS_BYTES = 8 * 1_024;
const MAX_PERMISSION_METADATA_BYTES = 8 * 1_024;
const MAX_EXECUTION_OUTPUT_BYTES = 1024 * 1024;
-const MAX_QUESTION_REQUEST_BYTES = 32 * 1_024;
-const MAX_QUESTIONS_PER_REQUEST = 4;
-const MAX_QUESTION_OPTIONS = 20;
const MAX_NATIVE_AGENTS = runtimeNativeInventoryLimits.agents;
const MAX_NATIVE_TOOLS = runtimeNativeInventoryLimits.tools;
const MAX_NATIVE_COMMANDS = runtimeNativeInventoryLimits.commands;
@@ -384,29 +381,22 @@ function parseQuestionRequest(
if (
typeof id !== "string" ||
id.length === 0 ||
- id.length > MAX_PERMISSION_NAME_LENGTH ||
!Array.isArray(questions) ||
questions.length === 0 ||
- questions.length > MAX_QUESTIONS_PER_REQUEST ||
!questions.every(
(question) =>
isRecord(question) &&
typeof question.question === "string" &&
question.question.trim().length > 0 &&
- question.question.length <= 2_000 &&
typeof question.header === "string" &&
question.header.trim().length > 0 &&
- question.header.length <= 120 &&
Array.isArray(question.options) &&
- question.options.length <= MAX_QUESTION_OPTIONS &&
question.options.every(
(option) =>
isRecord(option) &&
typeof option.label === "string" &&
option.label.trim().length > 0 &&
- option.label.length <= 200 &&
- typeof option.description === "string" &&
- option.description.length <= 1_000,
+ typeof option.description === "string",
) &&
(question.multiple === undefined ||
typeof question.multiple === "boolean") &&
@@ -416,22 +406,11 @@ function parseQuestionRequest(
(!isRecord(tool) ||
typeof tool.messageID !== "string" ||
tool.messageID.length === 0 ||
- tool.messageID.length > 256 ||
typeof tool.callID !== "string" ||
- tool.callID.length === 0 ||
- tool.callID.length > 256))
+ tool.callID.length === 0))
) {
throw new Error("OpenCode 提问请求格式无效");
}
- let serialized: string;
- try {
- serialized = JSON.stringify(properties);
- } catch {
- throw new Error("OpenCode 提问请求无法序列化");
- }
- if (!byteLengthWithin(serialized, MAX_QUESTION_REQUEST_BYTES)) {
- throw new Error("OpenCode 提问请求超过安全限制");
- }
return properties as QuestionRequest;
}
@@ -2569,6 +2548,7 @@ export class OpenCodeRuntime implements AgentRuntime {
throw new Error("OpenCode 工具调用 ID 格式无效");
}
const toolName = part.tool.slice(0, 200);
+ const nativeControlTool = toolName.trim().toLowerCase() === "question";
const state =
part.state.status === "error" ? "failed" : part.state.status;
if (state === "failed") {
@@ -2609,7 +2589,7 @@ export class OpenCodeRuntime implements AgentRuntime {
const retained = subagentProgress.retain(subagent);
toolStates.get(callId)!.subagent = retained;
yield retained;
- } else {
+ } else if (!nativeControlTool) {
yield {
requestId: request.requestId,
type: "tool",
diff --git a/src/renderer/src/AgentQuestionCard.tsx b/src/renderer/src/AgentQuestionCard.tsx
index 4ee4ceba..c9ae8283 100644
--- a/src/renderer/src/AgentQuestionCard.tsx
+++ b/src/renderer/src/AgentQuestionCard.tsx
@@ -122,7 +122,6 @@ export function AgentQuestionCard({
{t('question.otherAnswer')}
{
const answer = event.target.value
setCustom((current) =>
diff --git a/src/shared/assistant-contracts.test.ts b/src/shared/assistant-contracts.test.ts
index 701031e5..0a2e55b3 100644
--- a/src/shared/assistant-contracts.test.ts
+++ b/src/shared/assistant-contracts.test.ts
@@ -15,7 +15,10 @@ import {
projectExecutionSpaceSchema,
projectUpdateSchema
} from './assistant-contracts'
-import { subagentEventSchema } from './contracts'
+import {
+ agentQuestionResponseSchema,
+ subagentEventSchema
+} from './contracts'
const untouchedProject: AssistantProject = {
id: '00000000-0000-4000-8000-000000000101',
@@ -34,6 +37,25 @@ const untouchedProject: AssistantProject = {
updatedAt: '2026-08-01T00:00:00.000Z'
}
+describe('agent question contracts', () => {
+ it('does not impose GoodBuddy-specific question answer limits', () => {
+ const answers = Array.from({ length: 5 }, (_, questionIndex) =>
+ Array.from(
+ { length: 21 },
+ (_, optionIndex) =>
+ `${questionIndex}:${optionIndex}:${'回答内容'.repeat(600)}`
+ )
+ )
+
+ expect(
+ agentQuestionResponseSchema.parse({
+ questionId: `question-${'id'.repeat(100)}`,
+ answers
+ }).answers
+ ).toEqual(answers)
+ })
+})
+
describe('activity history contracts', () => {
it('accepts more than 500 records and details longer than 4,000 characters', () => {
const records = Array.from({ length: 501 }, (_, index) => ({
diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts
index 4def7cdb..8cb5e461 100644
--- a/src/shared/contracts.ts
+++ b/src/shared/contracts.ts
@@ -305,13 +305,12 @@ export const workspaceOpenPathRequestSchema = z
.strict()
export const agentQuestionAnswerSchema = z
- .array(z.string().trim().min(1).max(2_000))
- .max(20)
+ .array(z.string().trim().min(1))
export const agentQuestionResponseSchema = z
.object({
- questionId: z.string().trim().min(1).max(128),
- answers: z.array(agentQuestionAnswerSchema).max(4)
+ questionId: z.string().trim().min(1),
+ answers: z.array(agentQuestionAnswerSchema)
})
.strict()