pi-long-task 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-long-task",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "Pi extension for breaking down and running long coding tasks safely.",
6
6
  "keywords": [
@@ -11,6 +11,7 @@ import type {
11
11
  import { commitAfterSession, gitDirtyPaths, shouldCommitOutcome, type CommitAfterSessionResult } from "./git.ts";
12
12
  import { formatCoordinatorResultMessage } from "./render.ts";
13
13
  import { extractResultSummary } from "./result_writer.ts";
14
+ import { parseWorkerRuntimeConfig } from "./worker_config.ts";
14
15
  import { buildTaskProgressModel, type TaskProgressModel, type TaskProgressStatus } from "./task_progress.ts";
15
16
  import {
16
17
  buildTodoCreationPrompt,
@@ -97,6 +98,8 @@ export interface RunCoordinatorOptions extends PiLongTaskInput {
97
98
  todoPlanner?: TodoPlanner;
98
99
  workerSessionFactory?: WorkerSessionFactory;
99
100
  todoSessionFactory?: WorkerSessionFactory;
101
+ workerModel?: unknown;
102
+ workerModelName?: string;
100
103
  maxAttemptsPerTask?: number;
101
104
  taskTimeoutMs?: number;
102
105
  maxBashTimeoutMs?: number;
@@ -111,6 +114,7 @@ export interface TodoPlannerOptions {
111
114
  cwd: string;
112
115
  runDir: string;
113
116
  thinkingLevel: string;
117
+ model?: unknown;
114
118
  abortSignal?: AbortSignal;
115
119
  sessionFactory?: WorkerSessionFactory;
116
120
  }
@@ -167,6 +171,8 @@ interface RuntimeOptions {
167
171
  maxAttemptsPerTask: number;
168
172
  taskTimeoutSeconds: number;
169
173
  maxBashTimeoutSeconds: number;
174
+ workerModel?: unknown;
175
+ workerModelName?: string;
170
176
  taskThinking: string;
171
177
  todoThinking: string;
172
178
  workerRunner: WorkerRunner;
@@ -240,6 +246,8 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
240
246
  globalInstructions: todoGlobalInstructions(todoMarkdown),
241
247
  maxBashTimeoutSeconds: runtime.maxBashTimeoutSeconds,
242
248
  taskTimeoutSeconds: runtime.taskTimeoutSeconds,
249
+ model: runtime.workerModel,
250
+ modelName: runtime.workerModelName,
243
251
  thinkingLevel: runtime.taskThinking,
244
252
  abortSignal: runtime.abortSignal,
245
253
  sessionFactory: runtime.workerSessionFactory,
@@ -422,6 +430,7 @@ async function generateOrNormalizeTodoMarkdown(inputText: string, runtime: Runti
422
430
  cwd: runtime.cwd,
423
431
  runDir: runtime.runDir,
424
432
  thinkingLevel: runtime.todoThinking,
433
+ model: runtime.workerModel,
425
434
  abortSignal: runtime.abortSignal,
426
435
  sessionFactory: runtime.todoSessionFactory,
427
436
  });
@@ -435,6 +444,7 @@ export async function runTodoPlanner(options: TodoPlannerOptions): Promise<strin
435
444
  const result = await sessionFactory({
436
445
  cwd: options.cwd,
437
446
  tools: [],
447
+ model: options.model,
438
448
  thinkingLevel: options.thinkingLevel,
439
449
  });
440
450
  session = result.session;
@@ -460,6 +470,12 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
460
470
  const cwd = path.resolve(options.cwd ?? process.cwd());
461
471
  const runId = sanitizeRunId(options.runId ?? defaultRunId(options.now?.() ?? new Date()));
462
472
  const runDir = path.join(cwd, "tmp", "pi-long-task", runId);
473
+ const parsedWorkerConfig = parseWorkerRuntimeConfig(options.inputText);
474
+ const configuredAttempts = options.maxAttemptsPerTask ?? parsedWorkerConfig.maxAttemptsPerTask;
475
+ const configuredTaskTimeoutMs = options.taskTimeoutMs ?? parsedWorkerConfig.taskTimeoutMs;
476
+ const configuredMaxBashTimeoutMs = options.maxBashTimeoutMs ?? parsedWorkerConfig.maxBashTimeoutMs;
477
+ const workerModelName = options.workerModelName ?? parsedWorkerConfig.modelName;
478
+ const workerModel = workerModelName ? undefined : options.workerModel;
463
479
 
464
480
  return {
465
481
  cwd,
@@ -467,10 +483,12 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
467
483
  runDir,
468
484
  todoPath: path.join(runDir, "TODO.md"),
469
485
  taskResultPath: path.join(runDir, "TASK_RESULT.md"),
470
- maxAttemptsPerTask: positiveInteger(options.maxAttemptsPerTask, DEFAULT_COORDINATOR_OPTIONS.maxAttemptsPerTask),
471
- taskTimeoutSeconds: positiveMilliseconds(options.taskTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.taskTimeoutMs) / 1000,
486
+ maxAttemptsPerTask: positiveInteger(configuredAttempts, DEFAULT_COORDINATOR_OPTIONS.maxAttemptsPerTask),
487
+ taskTimeoutSeconds: positiveMilliseconds(configuredTaskTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.taskTimeoutMs) / 1000,
472
488
  maxBashTimeoutSeconds:
473
- positiveMilliseconds(options.maxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
489
+ positiveMilliseconds(configuredMaxBashTimeoutMs, DEFAULT_COORDINATOR_OPTIONS.maxBashTimeoutMs) / 1000,
490
+ workerModel,
491
+ workerModelName,
474
492
  taskThinking: options.taskThinking ?? DEFAULT_COORDINATOR_OPTIONS.taskThinking,
475
493
  todoThinking: options.todoThinking ?? DEFAULT_COORDINATOR_OPTIONS.todoThinking,
476
494
  workerRunner: options.workerRunner ?? runWorkerTask,
package/src/index.ts CHANGED
@@ -76,6 +76,9 @@ function toolDetails(result: CoordinatorResult) {
76
76
  }
77
77
 
78
78
  const LONG_TASK_WIDGET_KEY = "pi-long-task-sidebar";
79
+ const SIDEBAR_WIDGET_RESERVED_INPUT_ROWS = 8;
80
+ const SIDEBAR_WIDGET_MIN_ROWS = 4;
81
+ const SIDEBAR_WIDGET_MAX_ROWS = 24;
79
82
 
80
83
  type UiContext = ExtensionContext;
81
84
 
@@ -86,10 +89,12 @@ export interface LongTaskSidebarController {
86
89
 
87
90
  class PiLongTaskSidebarComponent implements Component {
88
91
  private readonly theme: Theme;
92
+ private readonly maxRows: (() => number | undefined) | undefined;
89
93
  private update: CoordinatorProgressUpdate | undefined;
90
94
 
91
- constructor(theme: Theme) {
95
+ constructor(theme: Theme, maxRows?: () => number | undefined) {
92
96
  this.theme = theme;
97
+ this.maxRows = maxRows;
93
98
  }
94
99
 
95
100
  setUpdate(update: CoordinatorProgressUpdate): void {
@@ -98,7 +103,8 @@ class PiLongTaskSidebarComponent implements Component {
98
103
  }
99
104
 
100
105
  render(width: number): string[] {
101
- return renderSidebarOverlayLines(this.update, this.theme, width);
106
+ const lines = renderSidebarOverlayLines(this.update, this.theme, width);
107
+ return limitSidebarPanelLines(lines, this.theme, width, this.maxRows?.());
102
108
  }
103
109
 
104
110
  invalidate(): void {
@@ -112,48 +118,69 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
112
118
  }
113
119
 
114
120
  let latestUpdate: CoordinatorProgressUpdate | undefined;
121
+ let widgetComponent: PiLongTaskSidebarComponent | undefined;
122
+ let widgetTui: TUI | undefined;
115
123
  let overlayComponent: PiLongTaskSidebarComponent | undefined;
116
124
  let overlayTui: TUI | undefined;
117
125
  let overlayDone: ((result: undefined) => void) | undefined;
118
126
  let overlayHandle: OverlayHandle | undefined;
119
127
  let closed = false;
120
128
 
121
- ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, ["Pi Long Task: preparing sidebar..."], { placement: "aboveEditor" });
122
-
123
- if (supportsTuiOverlay(ctx)) {
124
- const overlayPromise = ctx.ui.custom<undefined>(
125
- (tui, theme, _keybindings, done) => {
126
- overlayTui = tui;
127
- overlayDone = done;
128
- overlayComponent = new PiLongTaskSidebarComponent(theme);
129
+ if (supportsTuiWidget(ctx)) {
130
+ ctx.ui.setWidget(
131
+ LONG_TASK_WIDGET_KEY,
132
+ (tui, theme) => {
133
+ widgetTui = tui;
134
+ widgetComponent = new PiLongTaskSidebarComponent(theme, () => sidebarWidgetLineLimit(tui));
129
135
  if (latestUpdate) {
130
- overlayComponent.setUpdate(latestUpdate);
136
+ widgetComponent.setUpdate(latestUpdate);
131
137
  }
132
- if (closed) {
133
- done(undefined);
134
- }
135
- return overlayComponent;
138
+ return widgetComponent;
136
139
  },
137
- {
138
- overlay: true,
139
- overlayOptions: {
140
- anchor: "right-center",
141
- width: "34%",
142
- minWidth: 36,
143
- maxHeight: "100%",
144
- margin: 0,
145
- nonCapturing: true,
146
- visible: (termWidth, termHeight) => termWidth >= 96 && termHeight >= 16,
140
+ { placement: "aboveEditor" },
141
+ );
142
+ } else {
143
+ ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, ["Pi Long Task: preparing sidebar..."], { placement: "aboveEditor" });
144
+ }
145
+
146
+ if (supportsTuiOverlay(ctx)) {
147
+ try {
148
+ const overlayPromise = ctx.ui.custom<undefined>(
149
+ (tui, theme, _keybindings, done) => {
150
+ overlayTui = tui;
151
+ overlayDone = done;
152
+ overlayComponent = new PiLongTaskSidebarComponent(theme);
153
+ if (latestUpdate) {
154
+ overlayComponent.setUpdate(latestUpdate);
155
+ }
156
+ if (closed) {
157
+ done(undefined);
158
+ }
159
+ return overlayComponent;
147
160
  },
148
- onHandle: (handle) => {
149
- overlayHandle = handle;
150
- handle.unfocus();
161
+ {
162
+ overlay: true,
163
+ overlayOptions: {
164
+ anchor: "right-center",
165
+ width: "34%",
166
+ minWidth: 32,
167
+ maxHeight: "85%",
168
+ margin: 1,
169
+ nonCapturing: true,
170
+ visible: (termWidth, termHeight) => termWidth >= 96 && termHeight >= 16,
171
+ },
172
+ onHandle: (handle) => {
173
+ overlayHandle = handle;
174
+ handle.unfocus();
175
+ },
151
176
  },
152
- },
153
- );
154
- void overlayPromise.catch(() => {
177
+ );
178
+ void overlayPromise.catch(() => {
179
+ // The widget fallback remains active if overlay registration is unavailable.
180
+ });
181
+ } catch {
155
182
  // The widget fallback remains active if overlay registration is unavailable.
156
- });
183
+ }
157
184
  }
158
185
 
159
186
  return {
@@ -162,9 +189,15 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
162
189
  return;
163
190
  }
164
191
  latestUpdate = update;
192
+ widgetComponent?.setUpdate(update);
165
193
  overlayComponent?.setUpdate(update);
166
- overlayTui?.requestRender();
167
- ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, renderSidebarWidgetLines(update), { placement: "aboveEditor" });
194
+ widgetTui?.requestRender();
195
+ if (overlayTui && overlayTui !== widgetTui) {
196
+ overlayTui.requestRender();
197
+ }
198
+ if (!widgetComponent) {
199
+ ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, renderSidebarWidgetLines(update), { placement: "aboveEditor" });
200
+ }
168
201
  },
169
202
  close(): void {
170
203
  if (closed) {
@@ -172,11 +205,13 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
172
205
  }
173
206
  closed = true;
174
207
  ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, undefined);
175
- if (overlayDone) {
176
- overlayDone(undefined);
208
+ if (overlayHandle) {
209
+ overlayHandle.hide();
177
210
  } else {
178
- overlayHandle?.hide();
211
+ overlayDone?.(undefined);
179
212
  }
213
+ widgetComponent = undefined;
214
+ widgetTui = undefined;
180
215
  overlayComponent = undefined;
181
216
  overlayTui = undefined;
182
217
  overlayDone = undefined;
@@ -185,11 +220,23 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
185
220
  };
186
221
  }
187
222
 
223
+ function supportsTuiWidget(ctx: UiContext): boolean {
224
+ const mode = (ctx as UiContext & { mode?: string }).mode;
225
+ return mode === "tui" || mode === undefined;
226
+ }
227
+
188
228
  function supportsTuiOverlay(ctx: UiContext): boolean {
189
229
  const mode = (ctx as UiContext & { mode?: string }).mode;
190
230
  return mode === "tui" || mode === undefined;
191
231
  }
192
232
 
233
+ function sidebarWidgetLineLimit(tui: TUI): number {
234
+ const terminalRows = tui.terminal.rows;
235
+ const rows = Number.isFinite(terminalRows) ? Math.max(0, Math.floor(terminalRows)) : 24;
236
+ const availableRows = rows - SIDEBAR_WIDGET_RESERVED_INPUT_ROWS;
237
+ return Math.max(SIDEBAR_WIDGET_MIN_ROWS, Math.min(SIDEBAR_WIDGET_MAX_ROWS, availableRows));
238
+ }
239
+
193
240
  function renderSidebarWidgetLines(update: CoordinatorProgressUpdate): string[] {
194
241
  const progress = update.taskProgress;
195
242
  const summary = progress?.summary;
@@ -229,6 +276,30 @@ function renderSidebarOverlayLines(
229
276
  return rows.map((row) => sidebarPanelRow(row, contentWidth, theme));
230
277
  }
231
278
 
279
+ function limitSidebarPanelLines(lines: string[], theme: Theme, width: number, maxRows: number | undefined): string[] {
280
+ if (maxRows === undefined || !Number.isFinite(maxRows)) {
281
+ return lines;
282
+ }
283
+
284
+ const limit = Math.max(0, Math.floor(maxRows));
285
+ if (lines.length <= limit) {
286
+ return lines;
287
+ }
288
+ if (limit === 0) {
289
+ return [];
290
+ }
291
+
292
+ const contentWidth = Math.max(8, Math.max(28, width) - 4);
293
+ if (limit === 1) {
294
+ return [sidebarPanelRow(theme.fg("dim", "…"), contentWidth, theme)];
295
+ }
296
+
297
+ const visibleLines = lines.slice(0, limit - 1);
298
+ const omitted = lines.length - visibleLines.length;
299
+ visibleLines.push(sidebarPanelRow(theme.fg("dim", `… ${omitted} more`), contentWidth, theme));
300
+ return visibleLines;
301
+ }
302
+
232
303
  function renderSidebarRows(update: CoordinatorProgressUpdate | undefined, theme: Theme, width: number): string[] {
233
304
  if (!update) {
234
305
  return ["", sidebarHeading("Pi Long Task", theme), "", theme.fg("muted", "Preparing long-task sidebar...")];
@@ -670,6 +741,7 @@ export default function registerPiLongTaskExtension(pi: ExtensionAPI) {
670
741
  const result = await runCoordinator({
671
742
  ...params,
672
743
  cwd: ctx?.cwd,
744
+ workerModel: ctx?.model,
673
745
  abortSignal: signal,
674
746
  onProgress: publishProgress,
675
747
  });
@@ -0,0 +1,291 @@
1
+ export interface ParsedWorkerRuntimeConfig {
2
+ modelName?: string;
3
+ maxAttemptsPerTask?: number;
4
+ taskTimeoutMs?: number;
5
+ maxBashTimeoutMs?: number;
6
+ }
7
+
8
+ const MODEL_TOKEN_RE = /[A-Za-z0-9][A-Za-z0-9._~:+/@-]*/;
9
+ const STOP_WORDS = new Set([
10
+ "and",
11
+ "as",
12
+ "for",
13
+ "from",
14
+ "in",
15
+ "is",
16
+ "of",
17
+ "on",
18
+ "per",
19
+ "please",
20
+ "task",
21
+ "tasks",
22
+ "the",
23
+ "to",
24
+ "use",
25
+ "with",
26
+ "worker",
27
+ "workers",
28
+ ]);
29
+
30
+ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfig {
31
+ const state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string } = {};
32
+
33
+ parseLineDirectives(text, state);
34
+ parseNaturalLanguageDirectives(text, state);
35
+
36
+ const modelName = combineProviderAndModel(state.provider, state.model);
37
+ return {
38
+ ...(modelName ? { modelName } : {}),
39
+ ...(state.maxAttemptsPerTask !== undefined ? { maxAttemptsPerTask: state.maxAttemptsPerTask } : {}),
40
+ ...(state.taskTimeoutMs !== undefined ? { taskTimeoutMs: state.taskTimeoutMs } : {}),
41
+ ...(state.maxBashTimeoutMs !== undefined ? { maxBashTimeoutMs: state.maxBashTimeoutMs } : {}),
42
+ };
43
+ }
44
+
45
+ function parseLineDirectives(
46
+ text: string,
47
+ state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
48
+ ): void {
49
+ for (const rawLine of text.split(/\r?\n/)) {
50
+ const line = rawLine.replace(/^\s{0,3}>+\s?/, "").trim();
51
+ const match = line.match(
52
+ /^(?:[-*+]\s*)?(?:(pi\s+long\s+task|long\s+task|worker|workers?|task)\s+)?([a-z][a-z\s-]{0,40})\s*(?::|=)\s*(.+)$/i,
53
+ );
54
+ if (!match) {
55
+ continue;
56
+ }
57
+
58
+ const prefix = normalizeWords(match[1] ?? "");
59
+ const key = normalizeWords(match[2] ?? "");
60
+ const fullKey = normalizeWords(`${prefix} ${key}`);
61
+ const value = match[3] ?? "";
62
+
63
+ applyDirective(fullKey, value, state);
64
+ }
65
+ }
66
+
67
+ function parseNaturalLanguageDirectives(
68
+ text: string,
69
+ state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
70
+ ): void {
71
+ captureTokens(
72
+ text,
73
+ /\bworker\s+(?:model|provider\/model)\s*(?:is|=|:|to|as)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
74
+ (token) => {
75
+ state.model = token;
76
+ },
77
+ );
78
+ captureTokens(
79
+ text,
80
+ /\b(?:use|using|with|set|run(?:ning)?)\s+(?:the\s+)?(?:worker\s+)?model\s*(?:to|as|is|=|:)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
81
+ (token) => {
82
+ state.model = token;
83
+ },
84
+ );
85
+ captureTokens(text, /\bmodel\s*(?:=|:)\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi, (token) => {
86
+ state.model = token;
87
+ });
88
+
89
+ captureTokens(
90
+ text,
91
+ /\bworker\s+provider\s*(?:is|=|:|to|as)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
92
+ (token) => {
93
+ state.provider = token;
94
+ },
95
+ );
96
+ captureTokens(text, /\bprovider\s*(?:=|:)\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi, (token) => {
97
+ state.provider = token;
98
+ });
99
+ captureTokens(
100
+ text,
101
+ /\b(?:use|using|with|set)\s+(?:the\s+)?(?:worker\s+)?provider\s*(?:to|as|is|=|:)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
102
+ (token) => {
103
+ state.provider = token;
104
+ },
105
+ );
106
+
107
+ captureNumbers(
108
+ text,
109
+ /\b(?:max(?:imum)?\s+)?(?:worker\s+|task\s+)?attempts?\s*(?:per\s+task)?\s*(?:is|=|:|to|at)?\s*(\d+)/gi,
110
+ (value) => {
111
+ state.maxAttemptsPerTask = value;
112
+ },
113
+ );
114
+ captureNumbers(text, /\b(\d+)\s+(?:worker\s+|task\s+)?attempts?\b/gi, (value) => {
115
+ state.maxAttemptsPerTask = value;
116
+ });
117
+ captureNumbers(text, /\btry\s+(?:each\s+task\s+)?(?:up\s+to\s+)?(\d+)\s+times\b/gi, (value) => {
118
+ state.maxAttemptsPerTask = value;
119
+ });
120
+
121
+ captureDurations(
122
+ text,
123
+ /\b(?<!bash\s)(?<!max\s)(?:worker\s+|task\s+)?timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
124
+ (value) => {
125
+ state.taskTimeoutMs = value;
126
+ },
127
+ );
128
+ captureDurations(
129
+ text,
130
+ /\b(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))\s+(?:worker\s+|task\s+)?timeout\b/gi,
131
+ (value) => {
132
+ state.taskTimeoutMs = value;
133
+ },
134
+ );
135
+ captureDurations(
136
+ text,
137
+ /\b(?:max\s+)?bash\s+timeout\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?\s*(?:milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?)/gi,
138
+ (value) => {
139
+ state.maxBashTimeoutMs = value;
140
+ },
141
+ );
142
+ }
143
+
144
+ function applyDirective(
145
+ key: string,
146
+ value: string,
147
+ state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
148
+ ): void {
149
+ if (/\bprovider\b/.test(key)) {
150
+ const token = modelToken(value);
151
+ if (token) {
152
+ state.provider = token;
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (/\bmodel\b/.test(key)) {
158
+ const token = modelToken(value);
159
+ if (token) {
160
+ state.model = token;
161
+ }
162
+ return;
163
+ }
164
+
165
+ if (/\b(?:attempt|attempts|retry|retries)\b/.test(key)) {
166
+ const attempts = positiveIntegerFromText(value);
167
+ if (attempts !== undefined) {
168
+ state.maxAttemptsPerTask = attempts;
169
+ }
170
+ return;
171
+ }
172
+
173
+ if (/\bbash\b/.test(key) && /\btimeout\b/.test(key)) {
174
+ const timeout = durationMsFromText(value, { allowBareSeconds: true });
175
+ if (timeout !== undefined) {
176
+ state.maxBashTimeoutMs = timeout;
177
+ }
178
+ return;
179
+ }
180
+
181
+ if (/\btimeout\b/.test(key)) {
182
+ const timeout = durationMsFromText(value, { allowBareSeconds: true });
183
+ if (timeout !== undefined) {
184
+ state.taskTimeoutMs = timeout;
185
+ }
186
+ }
187
+ }
188
+
189
+ function captureTokens(text: string, pattern: RegExp, apply: (token: string) => void): void {
190
+ for (const match of text.matchAll(pattern)) {
191
+ const token = modelToken(match[1] ?? "");
192
+ if (token) {
193
+ apply(token);
194
+ }
195
+ }
196
+ }
197
+
198
+ function captureNumbers(text: string, pattern: RegExp, apply: (value: number) => void): void {
199
+ for (const match of text.matchAll(pattern)) {
200
+ const value = positiveIntegerFromText(match[1] ?? "");
201
+ if (value !== undefined) {
202
+ apply(value);
203
+ }
204
+ }
205
+ }
206
+
207
+ function captureDurations(text: string, pattern: RegExp, apply: (value: number) => void): void {
208
+ for (const match of text.matchAll(pattern)) {
209
+ const value = durationMsFromText(match[1] ?? "", { allowBareSeconds: true });
210
+ if (value !== undefined) {
211
+ apply(value);
212
+ }
213
+ }
214
+ }
215
+
216
+ function combineProviderAndModel(provider: string | undefined, model: string | undefined): string | undefined {
217
+ if (!model) {
218
+ return undefined;
219
+ }
220
+ if (model.includes("/") || !provider) {
221
+ return model;
222
+ }
223
+ return `${provider}/${model}`;
224
+ }
225
+
226
+ function modelToken(value: string): string | undefined {
227
+ const trimmed = trimDirectiveValue(value);
228
+ const match = MODEL_TOKEN_RE.exec(trimmed);
229
+ if (!match) {
230
+ return undefined;
231
+ }
232
+ const token = match[0].replace(/[.,;:]+$/g, "");
233
+ return token && !STOP_WORDS.has(token.toLowerCase()) ? token : undefined;
234
+ }
235
+
236
+ function trimDirectiveValue(value: string): string {
237
+ return value
238
+ .trim()
239
+ .replace(/^['"`]+/, "")
240
+ .replace(/['"`]+$/, "")
241
+ .trim();
242
+ }
243
+
244
+ function positiveIntegerFromText(value: string): number | undefined {
245
+ const match = /\d+/.exec(value);
246
+ if (!match) {
247
+ return undefined;
248
+ }
249
+ const parsed = Number.parseInt(match[0], 10);
250
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
251
+ }
252
+
253
+ function durationMsFromText(value: string, options: { allowBareSeconds: boolean }): number | undefined {
254
+ const match = /(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?\b/i.exec(
255
+ value,
256
+ );
257
+ if (!match) {
258
+ return undefined;
259
+ }
260
+
261
+ const amount = Number.parseFloat(match[1] ?? "");
262
+ if (!Number.isFinite(amount) || amount <= 0) {
263
+ return undefined;
264
+ }
265
+
266
+ const unit = (match[2] ?? "").toLowerCase();
267
+ if (!unit && !options.allowBareSeconds) {
268
+ return undefined;
269
+ }
270
+
271
+ const multiplier = durationMultiplier(unit || "seconds");
272
+ const milliseconds = Math.round(amount * multiplier);
273
+ return Number.isSafeInteger(milliseconds) && milliseconds > 0 ? milliseconds : undefined;
274
+ }
275
+
276
+ function durationMultiplier(unit: string): number {
277
+ if (unit === "ms" || unit.startsWith("millisecond") || unit.startsWith("msec")) {
278
+ return 1;
279
+ }
280
+ if (unit === "h" || unit.startsWith("hour") || unit.startsWith("hr")) {
281
+ return 60 * 60 * 1000;
282
+ }
283
+ if (unit === "m" || unit.startsWith("minute") || unit.startsWith("min")) {
284
+ return 60 * 1000;
285
+ }
286
+ return 1000;
287
+ }
288
+
289
+ function normalizeWords(value: string): string {
290
+ return value.toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
291
+ }
@@ -186,7 +186,6 @@ export const extractAssistantTextFromEvent = assistantTextFromEvent;
186
186
  export const extractLastAssistantTextFromEvents = lastAssistantTextFromEvents;
187
187
 
188
188
  export const DEFAULT_WORKER_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"] as const;
189
- export const DEFAULT_WORKER_MODEL = "openai-codex/gpt-5.5";
190
189
  export const DEFAULT_WORKER_THINKING_LEVEL = "high";
191
190
  export const DEFAULT_TASK_TIMEOUT_SECONDS = 60 * 60;
192
191
  export const DEFAULT_GRACEFUL_SHUTDOWN_SECONDS = 60;
@@ -309,7 +308,8 @@ export async function createIsolatedWorkerSession(
309
308
  const resourceLoader = disableExtensionsForWorker(discoveredResourceLoader, () => pi.createExtensionRuntime());
310
309
  await resourceLoader.reload();
311
310
 
312
- const model = options.model ?? (await resolveWorkerModel(modelRegistry, options.modelName ?? DEFAULT_WORKER_MODEL));
311
+ const model =
312
+ options.model ?? (options.modelName ? await resolveWorkerModel(modelRegistry, options.modelName) : undefined);
313
313
  const createOptions: Record<string, unknown> = {
314
314
  cwd,
315
315
  agentDir,