@oh-my-pi/pi-coding-agent 8.4.3 → 8.5.0
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/CHANGELOG.md +26 -0
- package/package.json +6 -6
- package/src/cursor.ts +1 -1
- package/src/modes/components/model-selector.ts +43 -14
- package/src/modes/components/tool-execution.ts +1 -3
- package/src/modes/controllers/event-controller.ts +0 -6
- package/src/modes/interactive-mode.ts +1 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/custom-system-prompt.md +14 -0
- package/src/prompts/system/plan-mode-active.md +4 -0
- package/src/prompts/system/system-prompt.md +12 -0
- package/src/prompts/tools/find.md +3 -2
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/task.md +1 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +4 -0
- package/src/session/agent-storage.ts +54 -1
- package/src/session/session-manager.ts +29 -2
- package/src/system-prompt.ts +26 -1
- package/src/task/executor.ts +99 -13
- package/src/task/index.ts +58 -11
- package/src/task/template.ts +3 -1
- package/src/task/types.ts +5 -0
- package/src/task/worker-protocol.ts +1 -0
- package/src/task/worker.ts +9 -0
- package/src/tools/bash.ts +1 -3
- package/src/tools/find.ts +74 -150
- package/src/tools/grep.ts +215 -109
- package/src/tools/index.ts +0 -3
- package/src/tools/output-meta.ts +2 -2
- package/src/tools/python.ts +1 -3
- package/src/tools/read.ts +30 -20
- package/src/prompts/tools/enter-plan-mode.md +0 -92
- package/src/tools/enter-plan-mode.ts +0 -76
package/src/task/executor.ts
CHANGED
|
@@ -75,6 +75,7 @@ export interface ExecutorOptions {
|
|
|
75
75
|
eventBus?: EventBus;
|
|
76
76
|
contextFiles?: ContextFileEntry[];
|
|
77
77
|
skills?: Skill[];
|
|
78
|
+
preloadedSkills?: Skill[];
|
|
78
79
|
promptTemplates?: PromptTemplate[];
|
|
79
80
|
mcpManager?: MCPManager;
|
|
80
81
|
authStorage?: AuthStorage;
|
|
@@ -361,6 +362,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
361
362
|
|
|
362
363
|
const outputChunks: string[] = [];
|
|
363
364
|
const finalOutputChunks: string[] = [];
|
|
365
|
+
const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
|
|
366
|
+
let recentOutputTail = "";
|
|
364
367
|
let stderr = "";
|
|
365
368
|
let resolved = false;
|
|
366
369
|
type AbortReason = "signal" | "terminate";
|
|
@@ -513,7 +516,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
513
516
|
signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
|
|
514
517
|
}
|
|
515
518
|
|
|
516
|
-
const
|
|
519
|
+
const PROGRESS_COALESCE_MS = 150;
|
|
520
|
+
let lastProgressEmitMs = 0;
|
|
521
|
+
let progressTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
522
|
+
|
|
523
|
+
const emitProgressNow = () => {
|
|
517
524
|
progress.durationMs = Date.now() - startTime;
|
|
518
525
|
onProgress?.({ ...progress });
|
|
519
526
|
if (options.eventBus) {
|
|
@@ -525,6 +532,33 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
525
532
|
progress: { ...progress },
|
|
526
533
|
});
|
|
527
534
|
}
|
|
535
|
+
lastProgressEmitMs = Date.now();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const scheduleProgress = (flush = false) => {
|
|
539
|
+
if (flush) {
|
|
540
|
+
if (progressTimeoutId) {
|
|
541
|
+
clearTimeout(progressTimeoutId);
|
|
542
|
+
progressTimeoutId = null;
|
|
543
|
+
}
|
|
544
|
+
emitProgressNow();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const now = Date.now();
|
|
548
|
+
const elapsed = now - lastProgressEmitMs;
|
|
549
|
+
if (lastProgressEmitMs === 0 || elapsed >= PROGRESS_COALESCE_MS) {
|
|
550
|
+
if (progressTimeoutId) {
|
|
551
|
+
clearTimeout(progressTimeoutId);
|
|
552
|
+
progressTimeoutId = null;
|
|
553
|
+
}
|
|
554
|
+
emitProgressNow();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (progressTimeoutId) return;
|
|
558
|
+
progressTimeoutId = setTimeout(() => {
|
|
559
|
+
progressTimeoutId = null;
|
|
560
|
+
emitProgressNow();
|
|
561
|
+
}, PROGRESS_COALESCE_MS - elapsed);
|
|
528
562
|
};
|
|
529
563
|
|
|
530
564
|
const getMessageContent = (message: unknown): unknown => {
|
|
@@ -541,6 +575,40 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
541
575
|
return undefined;
|
|
542
576
|
};
|
|
543
577
|
|
|
578
|
+
const updateRecentOutputLines = () => {
|
|
579
|
+
const lines = recentOutputTail.split("\n").filter(line => line.trim());
|
|
580
|
+
progress.recentOutput = lines.slice(-8).reverse();
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const appendRecentOutputTail = (text: string) => {
|
|
584
|
+
if (!text) return;
|
|
585
|
+
recentOutputTail += text;
|
|
586
|
+
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
587
|
+
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
588
|
+
}
|
|
589
|
+
updateRecentOutputLines();
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const replaceRecentOutputFromContent = (content: unknown[]) => {
|
|
593
|
+
recentOutputTail = "";
|
|
594
|
+
for (const block of content) {
|
|
595
|
+
if (!block || typeof block !== "object") continue;
|
|
596
|
+
const record = block as { type?: unknown; text?: unknown };
|
|
597
|
+
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
598
|
+
if (!record.text) continue;
|
|
599
|
+
recentOutputTail += record.text;
|
|
600
|
+
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
601
|
+
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
updateRecentOutputLines();
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const resetRecentOutput = () => {
|
|
608
|
+
recentOutputTail = "";
|
|
609
|
+
progress.recentOutput = [];
|
|
610
|
+
};
|
|
611
|
+
|
|
544
612
|
const processEvent = (event: AgentEvent) => {
|
|
545
613
|
if (resolved) return;
|
|
546
614
|
|
|
@@ -555,8 +623,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
555
623
|
}
|
|
556
624
|
|
|
557
625
|
const now = Date.now();
|
|
626
|
+
let flushProgress = false;
|
|
558
627
|
|
|
559
628
|
switch (event.type) {
|
|
629
|
+
case "message_start":
|
|
630
|
+
if (event.message?.role === "assistant") {
|
|
631
|
+
resetRecentOutput();
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
|
|
560
635
|
case "tool_execution_start":
|
|
561
636
|
progress.toolCount++;
|
|
562
637
|
progress.currentTool = event.toolName;
|
|
@@ -616,23 +691,28 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
616
691
|
schedulePendingTermination();
|
|
617
692
|
}
|
|
618
693
|
}
|
|
694
|
+
flushProgress = true;
|
|
619
695
|
break;
|
|
620
696
|
}
|
|
621
697
|
|
|
622
698
|
case "message_update": {
|
|
623
|
-
|
|
699
|
+
if (event.message?.role !== "assistant") break;
|
|
700
|
+
const assistantEvent = (
|
|
701
|
+
event as AgentEvent & {
|
|
702
|
+
assistantMessageEvent?: { type?: string; delta?: string };
|
|
703
|
+
}
|
|
704
|
+
).assistantMessageEvent;
|
|
705
|
+
if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
|
|
706
|
+
appendRecentOutputTail(assistantEvent.delta);
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
if (assistantEvent && assistantEvent.type !== "text_delta") {
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
624
712
|
const updateContent =
|
|
625
713
|
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
626
714
|
if (updateContent && Array.isArray(updateContent)) {
|
|
627
|
-
|
|
628
|
-
for (const block of updateContent) {
|
|
629
|
-
if (block.type === "text" && block.text) {
|
|
630
|
-
const lines = block.text.split("\n").filter((l: string) => l.trim());
|
|
631
|
-
allText.push(...lines);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
// Show last 8 lines from current state (not accumulated)
|
|
635
|
-
progress.recentOutput = allText.slice(-8).reverse();
|
|
715
|
+
replaceRecentOutputFromContent(updateContent);
|
|
636
716
|
}
|
|
637
717
|
break;
|
|
638
718
|
}
|
|
@@ -698,10 +778,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
698
778
|
}
|
|
699
779
|
}
|
|
700
780
|
}
|
|
781
|
+
flushProgress = true;
|
|
701
782
|
break;
|
|
702
783
|
}
|
|
703
784
|
|
|
704
|
-
|
|
785
|
+
scheduleProgress(flushProgress);
|
|
705
786
|
};
|
|
706
787
|
|
|
707
788
|
const startMessage: SubagentWorkerRequest = {
|
|
@@ -724,6 +805,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
724
805
|
pythonPreludeDocs: pythonPreludeDocsPayload,
|
|
725
806
|
contextFiles: options.contextFiles,
|
|
726
807
|
skills: options.skills,
|
|
808
|
+
preloadedSkills: options.preloadedSkills,
|
|
727
809
|
promptTemplates: options.promptTemplates,
|
|
728
810
|
mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
|
|
729
811
|
pythonToolProxy: pythonProxyEnabled,
|
|
@@ -972,6 +1054,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
972
1054
|
clearTimeout(terminationTimeoutId);
|
|
973
1055
|
terminationTimeoutId = null;
|
|
974
1056
|
}
|
|
1057
|
+
if (progressTimeoutId) {
|
|
1058
|
+
clearTimeout(progressTimeoutId);
|
|
1059
|
+
progressTimeoutId = null;
|
|
1060
|
+
}
|
|
975
1061
|
cancelPendingTermination();
|
|
976
1062
|
if (!terminated) {
|
|
977
1063
|
terminated = true;
|
|
@@ -1066,7 +1152,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1066
1152
|
// Update final progress
|
|
1067
1153
|
const wasAborted = abortedViaComplete || (!hasComplete && (done.aborted || signal?.aborted || false));
|
|
1068
1154
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1069
|
-
|
|
1155
|
+
scheduleProgress(true);
|
|
1070
1156
|
|
|
1071
1157
|
return {
|
|
1072
1158
|
index,
|
package/src/task/index.ts
CHANGED
|
@@ -391,12 +391,57 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
391
391
|
// Build full prompts with context prepended
|
|
392
392
|
const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(context, t));
|
|
393
393
|
const contextFiles = this.session.contextFiles;
|
|
394
|
-
const
|
|
394
|
+
const availableSkills = this.session.skills;
|
|
395
|
+
const availableSkillList = availableSkills ?? [];
|
|
395
396
|
const promptTemplates = this.session.promptTemplates;
|
|
397
|
+
const skillLookup = new Map(availableSkillList.map(skill => [skill.name, skill]));
|
|
398
|
+
const missingSkillsByTask: Array<{ id: string; missing: string[] }> = [];
|
|
399
|
+
const tasksWithSkills = tasksWithContext.map(task => {
|
|
400
|
+
if (task.skills === undefined) {
|
|
401
|
+
return { ...task, resolvedSkills: availableSkills, preloadedSkills: undefined };
|
|
402
|
+
}
|
|
403
|
+
const requested = task.skills;
|
|
404
|
+
const resolved = [] as typeof availableSkillList;
|
|
405
|
+
const missing: string[] = [];
|
|
406
|
+
const seen = new Set<string>();
|
|
407
|
+
for (const name of requested) {
|
|
408
|
+
const trimmed = name.trim();
|
|
409
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
410
|
+
seen.add(trimmed);
|
|
411
|
+
const skill = skillLookup.get(trimmed);
|
|
412
|
+
if (skill) {
|
|
413
|
+
resolved.push(skill);
|
|
414
|
+
} else {
|
|
415
|
+
missing.push(trimmed);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (missing.length > 0) {
|
|
419
|
+
missingSkillsByTask.push({ id: task.id, missing });
|
|
420
|
+
}
|
|
421
|
+
return { ...task, resolvedSkills: resolved, preloadedSkills: resolved };
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if (missingSkillsByTask.length > 0) {
|
|
425
|
+
const available = availableSkillList.map(skill => skill.name).join(", ") || "none";
|
|
426
|
+
const details = missingSkillsByTask.map(entry => `${entry.id}: ${entry.missing.join(", ")}`).join("; ");
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: "text",
|
|
431
|
+
text: `Unknown skills requested: ${details}. Available skills: ${available}`,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
details: {
|
|
435
|
+
projectAgentsDir,
|
|
436
|
+
results: [],
|
|
437
|
+
totalDurationMs: Date.now() - startTime,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
396
441
|
|
|
397
442
|
// Initialize progress for all tasks
|
|
398
|
-
for (let i = 0; i <
|
|
399
|
-
const t =
|
|
443
|
+
for (let i = 0; i < tasksWithSkills.length; i++) {
|
|
444
|
+
const t = tasksWithSkills[i];
|
|
400
445
|
progressMap.set(i, {
|
|
401
446
|
index: i,
|
|
402
447
|
id: t.id,
|
|
@@ -416,7 +461,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
416
461
|
}
|
|
417
462
|
emitProgress();
|
|
418
463
|
|
|
419
|
-
const runTask = async (task: (typeof
|
|
464
|
+
const runTask = async (task: (typeof tasksWithSkills)[number], index: number) => {
|
|
420
465
|
if (!isIsolated) {
|
|
421
466
|
return runSubprocess({
|
|
422
467
|
cwd: this.session.cwd,
|
|
@@ -438,7 +483,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
438
483
|
onProgress: progress => {
|
|
439
484
|
progressMap.set(index, {
|
|
440
485
|
...structuredClone(progress),
|
|
441
|
-
args:
|
|
486
|
+
args: tasksWithSkills[index]?.args,
|
|
442
487
|
});
|
|
443
488
|
emitProgress();
|
|
444
489
|
},
|
|
@@ -447,7 +492,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
447
492
|
settingsManager: this.session.settingsManager,
|
|
448
493
|
mcpManager: this.session.mcpManager,
|
|
449
494
|
contextFiles,
|
|
450
|
-
skills,
|
|
495
|
+
skills: task.resolvedSkills,
|
|
496
|
+
preloadedSkills: task.preloadedSkills,
|
|
451
497
|
promptTemplates,
|
|
452
498
|
});
|
|
453
499
|
}
|
|
@@ -481,7 +527,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
481
527
|
onProgress: progress => {
|
|
482
528
|
progressMap.set(index, {
|
|
483
529
|
...structuredClone(progress),
|
|
484
|
-
args:
|
|
530
|
+
args: tasksWithSkills[index]?.args,
|
|
485
531
|
});
|
|
486
532
|
emitProgress();
|
|
487
533
|
},
|
|
@@ -490,7 +536,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
490
536
|
settingsManager: this.session.settingsManager,
|
|
491
537
|
mcpManager: this.session.mcpManager,
|
|
492
538
|
contextFiles,
|
|
493
|
-
skills,
|
|
539
|
+
skills: task.resolvedSkills,
|
|
540
|
+
preloadedSkills: task.preloadedSkills,
|
|
494
541
|
promptTemplates,
|
|
495
542
|
});
|
|
496
543
|
const patch = await captureDeltaPatch(worktreeDir, baseline);
|
|
@@ -527,7 +574,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
527
574
|
|
|
528
575
|
// Execute in parallel with concurrency limit
|
|
529
576
|
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
|
530
|
-
|
|
577
|
+
tasksWithSkills,
|
|
531
578
|
MAX_CONCURRENCY,
|
|
532
579
|
runTask,
|
|
533
580
|
signal,
|
|
@@ -538,10 +585,10 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
538
585
|
if (result !== undefined) {
|
|
539
586
|
return {
|
|
540
587
|
...result,
|
|
541
|
-
args:
|
|
588
|
+
args: tasksWithSkills[index]?.args,
|
|
542
589
|
};
|
|
543
590
|
}
|
|
544
|
-
const task =
|
|
591
|
+
const task = tasksWithSkills[index];
|
|
545
592
|
return {
|
|
546
593
|
index,
|
|
547
594
|
id: task.id,
|
package/src/task/template.ts
CHANGED
|
@@ -5,10 +5,11 @@ type RenderResult = {
|
|
|
5
5
|
args: Record<string, string>;
|
|
6
6
|
id: string;
|
|
7
7
|
description: string;
|
|
8
|
+
skills?: string[];
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export function renderTemplate(template: string, task: TaskItem): RenderResult {
|
|
11
|
-
const { id, description, args } = task;
|
|
12
|
+
const { id, description, args, skills } = task;
|
|
12
13
|
|
|
13
14
|
let usedPlaceholder = false;
|
|
14
15
|
const unknownArguments: string[] = [];
|
|
@@ -43,5 +44,6 @@ export function renderTemplate(template: string, task: TaskItem): RenderResult {
|
|
|
43
44
|
args: { id, description, ...args },
|
|
44
45
|
id,
|
|
45
46
|
description,
|
|
47
|
+
skills,
|
|
46
48
|
};
|
|
47
49
|
}
|
package/src/task/types.ts
CHANGED
|
@@ -49,6 +49,11 @@ export const taskItemSchema = Type.Object({
|
|
|
49
49
|
description: "Arguments to fill {{placeholders}} in context",
|
|
50
50
|
}),
|
|
51
51
|
),
|
|
52
|
+
skills: Type.Optional(
|
|
53
|
+
Type.Array(Type.String(), {
|
|
54
|
+
description: "Skill names to preload into the subagent system prompt",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
52
57
|
});
|
|
53
58
|
|
|
54
59
|
export type TaskItem = Static<typeof taskItemSchema>;
|
|
@@ -107,6 +107,7 @@ export interface SubagentWorkerStartPayload {
|
|
|
107
107
|
pythonPreludeDocs?: PreludeHelper[];
|
|
108
108
|
contextFiles?: ContextFileEntry[];
|
|
109
109
|
skills?: Skill[];
|
|
110
|
+
preloadedSkills?: Skill[];
|
|
110
111
|
promptTemplates?: PromptTemplate[];
|
|
111
112
|
mcpTools?: MCPToolMetadata[];
|
|
112
113
|
pythonToolProxy?: boolean;
|
package/src/task/worker.ts
CHANGED
|
@@ -608,6 +608,7 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
608
608
|
requireCompleteTool: true,
|
|
609
609
|
contextFiles: payload.contextFiles,
|
|
610
610
|
skills: payload.skills,
|
|
611
|
+
preloadedSkills: payload.preloadedSkills,
|
|
611
612
|
promptTemplates: payload.promptTemplates,
|
|
612
613
|
// Append system prompt (equivalent to CLI's --append-system-prompt)
|
|
613
614
|
systemPrompt: defaultPrompt =>
|
|
@@ -627,6 +628,14 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
|
|
627
628
|
runState.session = session;
|
|
628
629
|
checkAbort();
|
|
629
630
|
|
|
631
|
+
// Write session init metadata for debugging/replay
|
|
632
|
+
session.sessionManager.appendSessionInit({
|
|
633
|
+
systemPrompt: session.agent.state.systemPrompt,
|
|
634
|
+
task: payload.task,
|
|
635
|
+
tools: session.getAllToolNames(),
|
|
636
|
+
outputSchema: payload.outputSchema,
|
|
637
|
+
});
|
|
638
|
+
|
|
630
639
|
signal.addEventListener(
|
|
631
640
|
"abort",
|
|
632
641
|
() => {
|
package/src/tools/bash.ts
CHANGED
|
@@ -85,10 +85,8 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
85
85
|
throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable)
|
|
89
|
-
let timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout;
|
|
90
88
|
// Clamp to reasonable range: 1s - 3600s (1 hour)
|
|
91
|
-
timeoutSec = Math.max(1, Math.min(3600,
|
|
89
|
+
const timeoutSec = Math.max(1, Math.min(3600, rawTimeout));
|
|
92
90
|
const timeoutMs = timeoutSec * 1000;
|
|
93
91
|
|
|
94
92
|
// Track output for streaming updates (tail only)
|