@bacnh85/pi-subagent 0.15.2 → 0.15.3

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.3 (2026-08-20)
4
+ ### Improvements
5
+ - Timeouts have been extracted as Environment Variables enabling overriding.
6
+ - `PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS` default : 3 Mins
7
+ - `PI_SUBAGENT_HARD_TIMEOUT_MINS` default: 20 Mins
8
+
3
9
  ## 0.15.2 (2026-08-18)
4
10
 
5
11
  ### Improvements
package/README.md CHANGED
@@ -140,8 +140,8 @@ each writes into its own checkout.
140
140
 
141
141
  Every child execution receives a timeout:
142
142
 
143
- - **Default inactivity window:** 3 minutes (`DEFAULT_TIMEOUT_MS`); real SDK lifecycle activity resets it.
144
- - **Absolute cap:** 20 minutes for every child, even when active.
143
+ - **Default inactivity window:** 3 minutes (`DEFAULT_TIMEOUT_MS`); real SDK lifecycle activity resets it. Overridable with Environment Variable `PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS`
144
+ - **Absolute cap:** Default 20 minutes for every child, even when active. Overridable with Environment variable `PI_SUBAGENT_HARD_TIMEOUT_MINS`
145
145
  - **Maximum requested inactivity window:** 60 minutes (`MAX_TIMEOUT_MS`); values must be positive integers.
146
146
  - Timeout diagnostics distinguish `Idle timeout` from `Hard timeout` and parent cancellation.
147
147
  - 30-second progress heartbeats only keep the parent transport alive; they never reset inactivity.
@@ -39,6 +39,7 @@ import {
39
39
  startHeartbeat,
40
40
  } from "./runner.ts";
41
41
  import {
42
+ flushWarnings,
42
43
  isRateLimitError,
43
44
  normalizeTimeout,
44
45
  resolveSafeCwd,
@@ -107,14 +108,14 @@ const TaskItem = Type.Object({
107
108
  agent: Type.String({ description: "Name of the agent to invoke" }),
108
109
  task: Type.String({ description: "Task to delegate to the agent" }),
109
110
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent" })),
110
- timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in milliseconds for this task; real child activity resets it (default 3 minutes, absolute cap 20 minutes)" })),
111
+ timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in ms; aborts on no activity within timeout. Default: 3 min (PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS). The agent always has a lifetime cap: default 20 min or (PI_SUBAGENT_HARD_TIMEOUT_MINS)." })),
111
112
  });
112
113
 
113
114
  const ChainItem = Type.Object({
114
115
  agent: Type.String({ description: "Name of the agent to invoke" }),
115
116
  task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
116
117
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent" })),
117
- timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in milliseconds for this step; real child activity resets it (default 3 minutes, absolute cap 20 minutes)" })),
118
+ timeout: Type.Optional(Type.Number({ description: "Inactivity timeout in ms; aborts on no activity within timeout. Default: 3 min (PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS). The agent always has a lifetime cap: default 20 min or (PI_SUBAGENT_HARD_TIMEOUT_MINS)." })),
118
119
  });
119
120
 
120
121
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
@@ -153,7 +154,7 @@ const SubagentParams = Type.Object({
153
154
  // Project-agent confirmation is enforced via trusted configuration.
154
155
  // See Security model section in README.
155
156
  cwd: Type.Optional(Type.String({ description: "Working directory (single mode, must be inside workspace)" })),
156
- timeout: Type.Optional(Type.Number({ description: "Global inactivity timeout in milliseconds (default 3 minutes; real activity resets it; fixed 20-minute absolute cap)" })),
157
+ timeout: Type.Optional(Type.Number({ description: "Inactivity timeout for the whole run, in ms; resets on activity, aborts on silence. Default 3 min (PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS). Lifetime cap: 20 min or (PI_SUBAGENT_HARD_TIMEOUT_MINS)." })),
157
158
  instructions: Type.Optional(Type.String({ description: "Bounded repository/task instructions passed to each child (max 16 KB)" })),
158
159
  abortOnFailure: Type.Optional(Type.Boolean({ description: "In parallel mode, cancel remaining tasks when one fails. Default: false.", default: false })),
159
160
  });
@@ -477,6 +478,11 @@ export default function (pi: ExtensionAPI) {
477
478
  "Use /subagent to list all available agents or /subagent <name> for agent details.",
478
479
  ],
479
480
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
481
+ // Surface env-var timeout warnings collected at module load. The
482
+ // interactive TUI swallows module-load stderr, so notify on launch.
483
+ for (const msg of flushWarnings()) {
484
+ ctx.ui?.notify?.(msg, "warning");
485
+ }
480
486
  const agentScope: AgentScope = params.agentScope ?? "user";
481
487
  const discovery = discoverAgents(ctx.cwd, agentScope, bundledAgentsDir);
482
488
  const agents = discovery.agents;
@@ -1711,4 +1717,4 @@ export default function (pi: ExtensionAPI) {
1711
1717
  }
1712
1718
  }, { overlay: true, overlayOptions: { maxHeight: "70%" } }); // Overlay: editor stays visible below
1713
1719
  }
1714
- }
1720
+ }
@@ -30,6 +30,8 @@ import {
30
30
  classifyStopReason,
31
31
  createCombinedAbortSignal,
32
32
  type SubagentStatus,
33
+ DEFAULT_TIMEOUT_MS,
34
+ HARD_TIMEOUT_MS,
33
35
  } from "./security.ts";
34
36
 
35
37
  // ---------------------------------------------------------------------------
@@ -46,8 +48,10 @@ export interface UsageStats {
46
48
  turns: number;
47
49
  }
48
50
 
49
- export const DEFAULT_INACTIVITY_TIMEOUT_MS = 3 * 60 * 1000;
50
- export const HARD_TIMEOUT_MS = 20 * 60 * 1000;
51
+ /** Re-export from security.ts single source of truth for both timeouts. */
52
+ export const DEFAULT_INACTIVITY_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
53
+ export { HARD_TIMEOUT_MS };
54
+
51
55
 
52
56
  // ---------------------------------------------------------------------------
53
57
  // Extension resource loader
@@ -65,10 +65,57 @@ export const MUTATION_TOOLS: readonly string[] = ["edit", "write"];
65
65
  export const EXECUTION_TOOLS: readonly string[] = ["bash"];
66
66
 
67
67
  /**
68
- * Default inactivity timeout. Real SDK lifecycle activity resets this window;
69
- * the runner separately enforces a fixed 20-minute absolute cap.
68
+ * Generic warning sink for configuration problems. Stderr so it is visible
69
+ * with or without a TUI attached. Warnings are also buffered so the extension
70
+ * host can surface them as TUI notifications — the interactive TUI swallows
71
+ * module-load stderr, so module-load warnings alone are invisible in-TUI.
72
+ */
73
+ const warnings: string[] = [];
74
+ function warn(message: string): void {
75
+ process.stderr.write(`[pi-subagent] ${message}\n`);
76
+ warnings.push(message);
77
+ }
78
+
79
+ /**
80
+ * Flush env-var config warnings collected at module load. Called once from
81
+ * the /subagent tool handler so the warnings surface as TUI notifications
82
+ * (module-load stderr is not visible inside the interactive TUI).
70
83
  */
71
- export const DEFAULT_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
84
+ export function flushWarnings(): string[] {
85
+ return warnings.splice(0, warnings.length);
86
+ }
87
+
88
+ /**
89
+ * Read a numeric value from an env var, with validation:
90
+ * - undefined/empty -> defaultValue (no warning)
91
+ * - not a finite number (NaN) -> defaultValue + warning
92
+ * - below `min` -> `min` + warning
93
+ * - above `max` -> `max` + warning
94
+ * - otherwise -> the parsed value (no warning)
95
+ */
96
+ export function getNumericEnvVar(
97
+ envVar: string,
98
+ defaultValue: number,
99
+ min: number,
100
+ max: number,
101
+ ): number {
102
+ const raw = process.env[envVar];
103
+ if (raw === undefined || raw === "") return defaultValue;
104
+ const value = Number(raw);
105
+ if (!Number.isFinite(value)) {
106
+ warn(`ENV VAR: ${envVar}="${raw}" is not a number; using default ${defaultValue}.`);
107
+ return defaultValue;
108
+ }
109
+ if (value < min) {
110
+ warn(`ENV VAR: ${envVar}=${value} is below the minimum of ${min}; using ${min}.`);
111
+ return min;
112
+ }
113
+ if (value > max) {
114
+ warn(`ENV VAR: ${envVar}=${value} is above the maximum of ${max}; using ${max}.`);
115
+ return max;
116
+ }
117
+ return value;
118
+ }
72
119
 
73
120
  /**
74
121
  * Absolute maximum timeout. Any requested value above this cap is rejected
@@ -76,6 +123,35 @@ export const DEFAULT_TIMEOUT_MS = 3 * 60 * 1_000; // 3 minutes
76
123
  */
77
124
  export const MAX_TIMEOUT_MS = 60 * 60 * 1_000; // 60 minutes
78
125
 
126
+ /**
127
+ * Timeout caps, in minutes, shared by getNumericEnvVar for both env vars.
128
+ * No timeout may be shorter than 1 minute; the inactivity window may not
129
+ * exceed the 60-minute absolute maximum.
130
+ */
131
+ const MIN_TIMEOUT_MINS = 1;
132
+ const MAX_TIMEOUT_MINS = MAX_TIMEOUT_MS / 60_000;
133
+
134
+ /**
135
+ * Default inactivity timeout. Real SDK lifecycle activity resets this window;
136
+ * the runner enforces a fixed absolute cap (HARD_TIMEOUT_MS).
137
+ *
138
+ * Reads PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS (default 3 min, range 1–60) and
139
+ * PI_SUBAGENT_HARD_TIMEOUT_MINS (default 20 min, range 1–60) from env via
140
+ * getNumericEnvVar, which warns on invalid/out-of-range values. The hard cap
141
+ * is additionally clamped up to the inactivity window: a lifetime cap smaller
142
+ * than the idle window is nonsensical.
143
+ */
144
+ const INACTIVITY_TIMEOUT_MINS = getNumericEnvVar("PI_SUBAGENT_INACTIVITY_TIMEOUT_MINS", 3, MIN_TIMEOUT_MINS, MAX_TIMEOUT_MINS);
145
+ let hardTimeoutMins = getNumericEnvVar("PI_SUBAGENT_HARD_TIMEOUT_MINS", 20, MIN_TIMEOUT_MINS, MAX_TIMEOUT_MINS);
146
+ if (hardTimeoutMins < INACTIVITY_TIMEOUT_MINS) {
147
+ warn(
148
+ `PI_SUBAGENT_HARD_TIMEOUT_MINS (${hardTimeoutMins}) is below the inactivity window (${INACTIVITY_TIMEOUT_MINS}); raising hard cap to ${INACTIVITY_TIMEOUT_MINS}.`,
149
+ );
150
+ hardTimeoutMins = INACTIVITY_TIMEOUT_MINS;
151
+ }
152
+ export const DEFAULT_TIMEOUT_MS = INACTIVITY_TIMEOUT_MINS * 60 * 1_000;
153
+ export const HARD_TIMEOUT_MS = hardTimeoutMins * 60 * 1_000;
154
+
79
155
  // ---------------------------------------------------------------------------
80
156
  // Canonical result status
81
157
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-subagent",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,9 +73,15 @@
73
73
  "@earendil-works/pi-tui": "^0.84.0",
74
74
  "@types/mocha": "^10.0.10",
75
75
  "@types/node": "^20.19.43",
76
- "mocha": "^10.8.2",
76
+ "mocha": "^11.8.0",
77
77
  "tsx": "^4.22.4",
78
78
  "typescript": "^5.9.3",
79
79
  "typebox": "^1.3.1"
80
+ },
81
+ "overrides": {
82
+ "serialize-javascript@>=5.0.0 <7.0.5": "^7.0.5",
83
+ "js-yaml@>=4.0.0 <4.3.1": "^4.3.1",
84
+ "brace-expansion@>=2.0.0 <2.1.4": "^2.1.4",
85
+ "diff": "^8.0.3"
80
86
  }
81
87
  }