pi-long-task 0.3.7 → 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.7",
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
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { AssistantMessage } from "@earendil-works/pi-ai";
3
- import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
3
+ import { truncateToWidth, type Component, type OverlayHandle, type TUI } from "@earendil-works/pi-tui";
4
4
 
5
5
  import { runCoordinator, type CoordinatorProgressUpdate, type CoordinatorResult } from "./coordinator.ts";
6
6
  import { longTaskInputTransform } from "./input_router.ts";
@@ -118,20 +118,24 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
118
118
  }
119
119
 
120
120
  let latestUpdate: CoordinatorProgressUpdate | undefined;
121
- let sidebarComponent: PiLongTaskSidebarComponent | undefined;
122
- let sidebarTui: TUI | undefined;
121
+ let widgetComponent: PiLongTaskSidebarComponent | undefined;
122
+ let widgetTui: TUI | undefined;
123
+ let overlayComponent: PiLongTaskSidebarComponent | undefined;
124
+ let overlayTui: TUI | undefined;
125
+ let overlayDone: ((result: undefined) => void) | undefined;
126
+ let overlayHandle: OverlayHandle | undefined;
123
127
  let closed = false;
124
128
 
125
129
  if (supportsTuiWidget(ctx)) {
126
130
  ctx.ui.setWidget(
127
131
  LONG_TASK_WIDGET_KEY,
128
132
  (tui, theme) => {
129
- sidebarTui = tui;
130
- sidebarComponent = new PiLongTaskSidebarComponent(theme, () => sidebarWidgetLineLimit(tui));
133
+ widgetTui = tui;
134
+ widgetComponent = new PiLongTaskSidebarComponent(theme, () => sidebarWidgetLineLimit(tui));
131
135
  if (latestUpdate) {
132
- sidebarComponent.setUpdate(latestUpdate);
136
+ widgetComponent.setUpdate(latestUpdate);
133
137
  }
134
- return sidebarComponent;
138
+ return widgetComponent;
135
139
  },
136
140
  { placement: "aboveEditor" },
137
141
  );
@@ -139,15 +143,59 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
139
143
  ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, ["Pi Long Task: preparing sidebar..."], { placement: "aboveEditor" });
140
144
  }
141
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;
160
+ },
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
+ },
176
+ },
177
+ );
178
+ void overlayPromise.catch(() => {
179
+ // The widget fallback remains active if overlay registration is unavailable.
180
+ });
181
+ } catch {
182
+ // The widget fallback remains active if overlay registration is unavailable.
183
+ }
184
+ }
185
+
142
186
  return {
143
187
  update(update: CoordinatorProgressUpdate): void {
144
188
  if (closed) {
145
189
  return;
146
190
  }
147
191
  latestUpdate = update;
148
- sidebarComponent?.setUpdate(update);
149
- sidebarTui?.requestRender();
150
- if (!sidebarComponent) {
192
+ widgetComponent?.setUpdate(update);
193
+ overlayComponent?.setUpdate(update);
194
+ widgetTui?.requestRender();
195
+ if (overlayTui && overlayTui !== widgetTui) {
196
+ overlayTui.requestRender();
197
+ }
198
+ if (!widgetComponent) {
151
199
  ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, renderSidebarWidgetLines(update), { placement: "aboveEditor" });
152
200
  }
153
201
  },
@@ -157,8 +205,17 @@ export function createLongTaskSidebarController(ctx: UiContext | undefined): Lon
157
205
  }
158
206
  closed = true;
159
207
  ctx.ui.setWidget(LONG_TASK_WIDGET_KEY, undefined);
160
- sidebarComponent = undefined;
161
- sidebarTui = undefined;
208
+ if (overlayHandle) {
209
+ overlayHandle.hide();
210
+ } else {
211
+ overlayDone?.(undefined);
212
+ }
213
+ widgetComponent = undefined;
214
+ widgetTui = undefined;
215
+ overlayComponent = undefined;
216
+ overlayTui = undefined;
217
+ overlayDone = undefined;
218
+ overlayHandle = undefined;
162
219
  },
163
220
  };
164
221
  }
@@ -168,6 +225,11 @@ function supportsTuiWidget(ctx: UiContext): boolean {
168
225
  return mode === "tui" || mode === undefined;
169
226
  }
170
227
 
228
+ function supportsTuiOverlay(ctx: UiContext): boolean {
229
+ const mode = (ctx as UiContext & { mode?: string }).mode;
230
+ return mode === "tui" || mode === undefined;
231
+ }
232
+
171
233
  function sidebarWidgetLineLimit(tui: TUI): number {
172
234
  const terminalRows = tui.terminal.rows;
173
235
  const rows = Number.isFinite(terminalRows) ? Math.max(0, Math.floor(terminalRows)) : 24;
@@ -679,6 +741,7 @@ export default function registerPiLongTaskExtension(pi: ExtensionAPI) {
679
741
  const result = await runCoordinator({
680
742
  ...params,
681
743
  cwd: ctx?.cwd,
744
+ workerModel: ctx?.model,
682
745
  abortSignal: signal,
683
746
  onProgress: publishProgress,
684
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,