Quellcode durchsuchen

feat(subtask): make worker timeout configurable via subtask.timeoutMs

The subtask worker timeout was hardcoded to 5 minutes (300000ms), which
caused 'Prompt timed out after 300000ms' failures for any long-running
subtask with no way to override it via config.

Add a new `subtask.timeoutMs` config option (default 300000, max 24h,
0 disables the timeout) and thread it through createSubtaskTool.

- src/config/schema.ts: add SubtaskConfigSchema (zod) and register it
- src/tools/subtask/tools.ts: accept options.timeoutMs in factory
- src/index.ts: pass config.subtask?.timeoutMs to createSubtaskTool
- regenerated oh-my-opencode-slim.schema.json
- docs/configuration.md and docs/subtask.md: documented the option
- src/tools/subtask/tools.test.ts: cover custom timeout propagation
patcher vor 2 Monaten
Ursprung
Commit
486e353561

+ 1 - 0
docs/configuration.md

@@ -136,6 +136,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `council.timeout` | number | `180000` | Per-councillor timeout (ms) |
 | `council.councillor_execution_mode` | string | `"parallel"` | Run councillors in `parallel` or `serial`; use `serial` for single-model setups |
 | `council.councillor_retries` | number | `3` | Max retries per councillor on empty provider response (0–5) |
+| `subtask.timeoutMs` | integer | `300000` | Subtask worker timeout in ms. `0` disables the timeout. Max `86400000` (24h) |
 | `todoContinuation.maxContinuations` | integer | `5` | Max consecutive auto-continuations before stopping (1–50) |
 | `todoContinuation.cooldownMs` | integer | `3000` | Delay in ms before auto-continuing — gives user time to abort (0–30000) |
 | `todoContinuation.autoEnable` | boolean | `false` | Automatically enable auto-continue when session has enough todos |

+ 21 - 0
docs/subtask.md

@@ -89,6 +89,27 @@ Safety rules:
 - large files are capped before injection,
 - unreadable or missing files are skipped.
 
+## Timeout
+
+Each subtask worker has a timeout. If the worker has not returned a summary
+before the timeout elapses, Slim aborts the child session and the `subtask` tool
+call fails with `Prompt timed out after <ms>ms`.
+
+The default timeout is **5 minutes** (`300000` ms). Override it via
+`subtask.timeoutMs` in your plugin config:
+
+```jsonc
+{
+  // Give workers up to 30 minutes before timing out
+  "subtask": {
+    "timeoutMs": 1800000
+  }
+}
+```
+
+Set `timeoutMs` to `0` to disable the timeout entirely. The maximum accepted
+value is `86400000` (24 hours).
+
 ## Summary format
 
 The worker is instructed to finish with:

+ 12 - 0
oh-my-opencode-slim.schema.json

@@ -599,6 +599,18 @@
         }
       }
     },
+    "subtask": {
+      "type": "object",
+      "properties": {
+        "timeoutMs": {
+          "default": 300000,
+          "description": "Subtask worker timeout in ms. 0 disables the timeout. Defaults to 300000 (5 minutes).",
+          "type": "integer",
+          "minimum": 0,
+          "maximum": 86400000
+        }
+      }
+    },
     "fallback": {
       "type": "object",
       "properties": {

+ 15 - 0
src/config/schema.ts

@@ -252,6 +252,20 @@ export type TodoContinuationConfig = z.infer<
   typeof TodoContinuationConfigSchema
 >;
 
+export const SubtaskConfigSchema = z.object({
+  timeoutMs: z
+    .number()
+    .int()
+    .min(0)
+    .max(24 * 60 * 60 * 1000)
+    .default(5 * 60 * 1000)
+    .describe(
+      'Subtask worker timeout in ms. 0 disables the timeout. Defaults to 300000 (5 minutes).',
+    ),
+});
+
+export type SubtaskConfig = z.infer<typeof SubtaskConfigSchema>;
+
 export const FailoverConfigSchema = z.object({
   enabled: z.boolean().default(true),
   timeoutMs: z.number().min(0).default(15000),
@@ -335,6 +349,7 @@ export const PluginConfigSchema = z
     sessionManager: SessionManagerConfigSchema.optional(),
     divoom: DivoomConfigSchema.optional(),
     todoContinuation: TodoContinuationConfigSchema.optional(),
+    subtask: SubtaskConfigSchema.optional(),
     fallback: FailoverConfigSchema.optional(),
     council: CouncilConfigSchema.optional(),
   })

+ 3 - 1
src/index.ts

@@ -401,7 +401,9 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ...todoContinuationHook.tool,
       ast_grep_search,
       ast_grep_replace,
-      subtask: createSubtaskTool(ctx, subtaskState, depthTracker),
+      subtask: createSubtaskTool(ctx, subtaskState, depthTracker, {
+        timeoutMs: config.subtask?.timeoutMs,
+      }),
       read_session: createReadSessionTool(ctx.client, subtaskState),
     },
 

+ 40 - 0
src/tools/subtask/tools.test.ts

@@ -243,6 +243,46 @@ describe('subtask tool', () => {
       fs.rmSync(directory, { recursive: true, force: true });
     }
   });
+
+  test('honors custom timeoutMs option', async () => {
+    const directory = makeTempDir();
+    try {
+      const sessionCreate = mock(async () => ({ data: { id: 'ses_new' } }));
+      const sessionPrompt = mock(() => new Promise(() => {}));
+      const sessionMessages = mock(async () => ({ data: [] }));
+      const sessionAbort = mock(async () => ({}));
+      const state = createSubtaskState();
+      const tool = createSubtaskTool(
+        {
+          directory,
+          client: {
+            session: {
+              abort: sessionAbort,
+              create: sessionCreate,
+              messages: sessionMessages,
+              prompt: sessionPrompt,
+            },
+          },
+        } as any,
+        state,
+        undefined,
+        { timeoutMs: 5 },
+      );
+
+      await expect(
+        tool.execute({ prompt: 'Will time out' }, {
+          sessionID: 'ses_old',
+        } as any),
+      ).rejects.toThrow('Prompt timed out after 5ms');
+
+      expect(sessionAbort).toHaveBeenCalledWith({
+        path: { id: 'ses_new' },
+        query: { directory },
+      });
+    } finally {
+      fs.rmSync(directory, { recursive: true, force: true });
+    }
+  });
 });
 
 describe('read_session tool', () => {

+ 9 - 2
src/tools/subtask/tools.ts

@@ -18,9 +18,14 @@ import {
 import type { SubtaskState } from './state';
 
 export type OpencodeClient = PluginInput['client'];
-const SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
+export const DEFAULT_SUBTASK_TIMEOUT_MS = 5 * 60 * 1000;
 const SUBTASK_SUMMARY_TAG_REGEX = /<\/?subtask_summary>/g;
 
+export interface CreateSubtaskToolOptions {
+  /** Worker timeout in ms. 0 disables the timeout. */
+  timeoutMs?: number;
+}
+
 function normalizeSubtaskSummary(text: string): string {
   return text.replace(SUBTASK_SUMMARY_TAG_REGEX, '').trim();
 }
@@ -49,8 +54,10 @@ export function createSubtaskTool(
   ctx: PluginInput,
   state: SubtaskState,
   depthTracker?: SubagentDepthTracker,
+  options: CreateSubtaskToolOptions = {},
 ): ToolDefinition {
   const client = ctx.client;
+  const timeoutMs = options.timeoutMs ?? DEFAULT_SUBTASK_TIMEOUT_MS;
 
   return tool({
     description:
@@ -150,7 +157,7 @@ Do not spawn another subtask.`;
               ],
             },
           },
-          SUBTASK_TIMEOUT_MS,
+          timeoutMs,
           abortSignal,
         );