pi-better-harness 0.42.1 → 0.43.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 +6 -0
- package/node_modules/pi-better-subagents/README.md +1 -1
- package/node_modules/pi-better-subagents/config.ts +5 -1
- package/node_modules/pi-better-subagents/docs/usage.md +10 -6
- package/node_modules/pi-better-subagents/index.ts +54 -24
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/settings-page.ts +16 -7
- package/node_modules/pi-better-subagents/timing.ts +14 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -18,7 +18,7 @@ Use `pi-better-subagents` when you want Pi to launch independent agent work with
|
|
|
18
18
|
- Explicit tool allowlists for child sessions, and `skills` for prompt exposure: omit it to keep user-catalog discovery, pass `[]` for no skills, or pass absolute `SKILL.md` paths for exactly those. See [skill exposure](docs/usage.md#skill-exposure).
|
|
19
19
|
- Durable logs, result retrieval, and [failure observations](docs/failure-observations.md) independent of lifecycle status. A live child handles its own tool errors; the parent is woken only for actionable incidents, and children can classify handled failures with `failure_disposition`.
|
|
20
20
|
- Live background-work navigator for active runs.
|
|
21
|
-
- Harness-owned run timing: every run gets independent factual alerts for a tool call left open (5 min) and for no progress (10 min),
|
|
21
|
+
- Harness-owned run timing: every run gets independent factual alerts for a tool call left open (5 min) and for no progress (10 min), plus a 60-minute hard timeout. Change the timeout in `/subagents settings`; `0` disables it. Soft deadlines remain opt-in. Configure per spawn with `deadline_minutes`, `grace_minutes`, `max_minutes`, `tool_wait_minutes`, `stuck_minutes`, globally in `config.json` / `PI_SUBAGENT_*_MINUTES`, or in `/subagents settings`. See [usage notes](docs/usage.md#run-timing-deadline-ceiling-tool-wait-stuck).
|
|
22
22
|
|
|
23
23
|
Every single or batch launch automatically joins one shared supervision watcher in the
|
|
24
24
|
foreground Pi process. The last active run's exit or stop tears it down; a later
|
|
@@ -195,6 +195,7 @@ export function writeSubagentSettings(
|
|
|
195
195
|
settings: {
|
|
196
196
|
delegationMode: DelegationMode;
|
|
197
197
|
maxConcurrent: number;
|
|
198
|
+
maxMinutes?: number;
|
|
198
199
|
/** Omit to preserve the saved value; `null` explicitly records discovery. */
|
|
199
200
|
defaultSkills?: string[] | null;
|
|
200
201
|
/** Omit to preserve the saved value. `0` disables that alert. */
|
|
@@ -207,12 +208,15 @@ export function writeSubagentSettings(
|
|
|
207
208
|
if (!isDelegationMode(settings.delegationMode)) throw new Error("Invalid delegation mode.");
|
|
208
209
|
if (!isConcurrencyCap(settings.maxConcurrent)) throw new Error("Concurrent subagents must be a positive whole number.");
|
|
209
210
|
const next: {
|
|
210
|
-
delegationMode: DelegationMode; maxConcurrent: number; defaultSkills?: string[] | null;
|
|
211
|
+
delegationMode: DelegationMode; maxConcurrent: number; maxMinutes?: number; defaultSkills?: string[] | null;
|
|
211
212
|
toolWaitMinutes?: number; stuckMinutes?: number;
|
|
212
213
|
} = {
|
|
213
214
|
delegationMode: settings.delegationMode,
|
|
214
215
|
maxConcurrent: settings.maxConcurrent,
|
|
215
216
|
};
|
|
217
|
+
if (settings.maxMinutes !== undefined) {
|
|
218
|
+
next.maxMinutes = validatedTimingMinutes(settings.maxMinutes, "maxMinutes");
|
|
219
|
+
}
|
|
216
220
|
if (Object.hasOwn(settings, "defaultSkills")) {
|
|
217
221
|
if (settings.defaultSkills === null) {
|
|
218
222
|
next.defaultSkills = null;
|
|
@@ -283,15 +283,18 @@ by the existing harness runtime.
|
|
|
283
283
|
Timeout control belongs to the harness, not to the prompt or the skill that
|
|
284
284
|
launched the run. A time limit written into a prompt is enforced by nothing, and
|
|
285
285
|
a parent that only notices late tends to kill a child that was still making
|
|
286
|
-
progress. Every run gets a tool-wait alert
|
|
287
|
-
|
|
288
|
-
agent from a stalled one.
|
|
286
|
+
progress. Every run gets a tool-wait alert, stall detection, and a 60-minute hard
|
|
287
|
+
timeout by default. Soft deadlines remain opt-in because total runtime does not
|
|
288
|
+
distinguish a slow agent from a stalled one.
|
|
289
|
+
Use Timeout (minutes) in `/subagents settings` to change the session default;
|
|
290
|
+
Ctrl+S saves it for future sessions. `0` disables the timeout. Changes apply to
|
|
291
|
+
new attempts, not runs already in progress.
|
|
289
292
|
|
|
290
293
|
| Control | Default | What happens |
|
|
291
294
|
|---------|---------|--------------|
|
|
292
295
|
| Soft deadline (`deadline_minutes`) | Off | When configured, the child gets one steering message (delivered through Pi's steer queue after its current tool call finishes): stop starting new work, commit or save what is done, and report what is complete, what is not, and where the work is. The parent gets one wake saying so. |
|
|
293
296
|
| Grace (`grace_minutes`) | 5 | Grace starts when the steering message actually reaches the child, which is after its current tool call. While the message is still waiting behind a running tool call, the deadline stop is held, so a test run that began just before the deadline is not killed mid-run. If the child has not finished by the end of grace, the harness stops it; the ordinary completion callback reports `killed; stopped: deadline`. A child that completes inside grace is not stopped; its surfaces say `deadline: finished in grace`. |
|
|
294
|
-
| Hard ceiling (`max_minutes`) |
|
|
297
|
+
| Hard ceiling (`max_minutes`) | 60 | When configured, the run is stopped at once, without grace or a steer, and reported as `stopped: ceiling`. The ceiling bounds the run, including a tool call that never ends and an orphaned run (child gone, its process group still alive). |
|
|
295
298
|
| Tool-wait window (`tool_wait_minutes`) | 5 | One tool call that stays open this long sends one factual parent wake for that call, including when it is a normal long test. Parallel calls alert independently. The alert never stops, cancels, or restarts the run, and a later call can alert on its own. |
|
|
296
299
|
| Stuck window (`stuck_minutes`) | 10 | No progress (see below) for this long wakes the parent once (`stuck`). It never stops the run. After new progress, a later stuck spell wakes again. Time inside a running tool call is excluded, so tool waiting and no-progress are reported separately. |
|
|
297
300
|
|
|
@@ -351,8 +354,9 @@ three times wakes the parent once as an `Action required` incident
|
|
|
351
354
|
deadline), `deadline: passed` (ended some other way after the deadline, for
|
|
352
355
|
example it crashed or a user stopped it), `stopped: ceiling`, or `stuck`. The spawn response lists the limits in force.
|
|
353
356
|
|
|
354
|
-
**Global defaults.** Precedence is spawn parameter, then
|
|
355
|
-
`config.json`, then the
|
|
357
|
+
**Global defaults.** Precedence is spawn parameter, then the session timeout
|
|
358
|
+
choice (for `max_minutes`), then environment, then `config.json`, then the
|
|
359
|
+
built-in default:
|
|
356
360
|
|
|
357
361
|
| Setting | Environment | `config.json` |
|
|
358
362
|
|---------|-------------|---------------|
|
|
@@ -41,6 +41,7 @@ import { createSubagentSettingsPage } from "./settings-page.ts";
|
|
|
41
41
|
import { deliverSubagentPrompt, effectiveSubagentSettings, formatSubagentSettings, subagentSettingsTool, SUBAGENT_SETTINGS_CONTEXT } from "./effective-settings.ts";
|
|
42
42
|
import { STEER_FILE_ENV, readSteerReceipt } from "./child-steer.ts";
|
|
43
43
|
import {
|
|
44
|
+
continuationTimingParams,
|
|
44
45
|
decideTiming,
|
|
45
46
|
describeTiming,
|
|
46
47
|
DEFAULT_TIMING_MINUTES,
|
|
@@ -2082,7 +2083,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2082
2083
|
let catalogModelRegistry: ExtensionContext["modelRegistry"] | undefined;
|
|
2083
2084
|
let delegationOverride: DelegationMode | undefined;
|
|
2084
2085
|
let capOverride: number | undefined;
|
|
2085
|
-
let timingOverride: { toolWaitMinutes?: number; stuckMinutes?: number } = {};
|
|
2086
|
+
let timingOverride: { maxMinutes?: number; toolWaitMinutes?: number; stuckMinutes?: number } = {};
|
|
2086
2087
|
interface SkillSelection { mode: SkillExposureMode; paths: string[] }
|
|
2087
2088
|
let skillsOverride: SkillSelection | undefined;
|
|
2088
2089
|
const savedSkills = (): SkillSelection & { source: "config" | "default"; error?: string } => {
|
|
@@ -2128,11 +2129,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2128
2129
|
};
|
|
2129
2130
|
const activeConcurrencyCap = (): number => capOverride ?? normalizeConcurrencyCap(loadConfig().maxConcurrent);
|
|
2130
2131
|
const timingSettingsFor = (cfg = loadConfig()): TimingSettings => ({ ...cfg, ...timingOverride });
|
|
2132
|
+
/** A per-run max_minutes wins; otherwise a session timeout choice beats the environment. */
|
|
2133
|
+
const launchTimingParams = (params: TimingParams = {}): TimingParams => ({
|
|
2134
|
+
...params,
|
|
2135
|
+
max_minutes: params.max_minutes ?? timingOverride.maxMinutes,
|
|
2136
|
+
});
|
|
2131
2137
|
interface TimingMinutesState {
|
|
2138
|
+
maxMinutes: number;
|
|
2132
2139
|
toolWaitMinutes: number;
|
|
2133
2140
|
stuckMinutes: number;
|
|
2141
|
+
timeoutSource: "environment" | "session" | "config" | "default";
|
|
2134
2142
|
toolWaitSource: "environment" | "session" | "config" | "default";
|
|
2135
2143
|
stuckSource: "environment" | "session" | "config" | "default";
|
|
2144
|
+
defaultMaxMinutes: number;
|
|
2136
2145
|
defaultToolWaitMinutes: number;
|
|
2137
2146
|
defaultStuckMinutes: number;
|
|
2138
2147
|
timingError?: string;
|
|
@@ -2147,24 +2156,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
2147
2156
|
envFor(key) !== undefined ? "environment"
|
|
2148
2157
|
: timingOverride[key] !== undefined ? "session"
|
|
2149
2158
|
: cfg[key] !== undefined && cfg[key] !== null ? "config" : "default";
|
|
2159
|
+
const timeoutEnv = process.env[TIMING_ENV.max];
|
|
2160
|
+
const timeoutSource = (): "environment" | "session" | "config" | "default" =>
|
|
2161
|
+
timingOverride.maxMinutes !== undefined ? "session"
|
|
2162
|
+
: timeoutEnv !== undefined && timeoutEnv.trim() !== "" ? "environment"
|
|
2163
|
+
: cfg.maxMinutes !== undefined && cfg.maxMinutes !== null ? "config" : "default";
|
|
2150
2164
|
try {
|
|
2151
|
-
const
|
|
2165
|
+
const sessionTimeout = timingOverride.maxMinutes !== undefined ? { max_minutes: timingOverride.maxMinutes } : {};
|
|
2166
|
+
const effective = resolveRunTiming({ params: sessionTimeout, settings: timingSettingsFor(cfg), env: process.env, startedAt: 0 });
|
|
2152
2167
|
const defaults = resolveRunTiming({ settings: cfg, env: process.env, startedAt: 0 });
|
|
2153
2168
|
const minutes = (ms: number | undefined): number => ms === undefined ? 0 : ms / 60_000;
|
|
2154
2169
|
return {
|
|
2170
|
+
maxMinutes: minutes(effective.ceilingAt),
|
|
2155
2171
|
toolWaitMinutes: minutes(effective.toolWaitMs),
|
|
2156
2172
|
stuckMinutes: minutes(effective.stuckMs),
|
|
2173
|
+
timeoutSource: timeoutSource(),
|
|
2157
2174
|
toolWaitSource: sourceFor("toolWaitMinutes"),
|
|
2158
2175
|
stuckSource: sourceFor("stuckMinutes"),
|
|
2176
|
+
defaultMaxMinutes: minutes(defaults.ceilingAt),
|
|
2159
2177
|
defaultToolWaitMinutes: minutes(defaults.toolWaitMs),
|
|
2160
2178
|
defaultStuckMinutes: minutes(defaults.stuckMs),
|
|
2161
2179
|
};
|
|
2162
2180
|
} catch (error) {
|
|
2163
2181
|
return {
|
|
2182
|
+
maxMinutes: DEFAULT_TIMING_MINUTES.max,
|
|
2164
2183
|
toolWaitMinutes: DEFAULT_TIMING_MINUTES.toolWait,
|
|
2165
2184
|
stuckMinutes: DEFAULT_TIMING_MINUTES.stuck,
|
|
2185
|
+
timeoutSource: timeoutSource(),
|
|
2166
2186
|
toolWaitSource: sourceFor("toolWaitMinutes"),
|
|
2167
2187
|
stuckSource: sourceFor("stuckMinutes"),
|
|
2188
|
+
defaultMaxMinutes: DEFAULT_TIMING_MINUTES.max,
|
|
2168
2189
|
defaultToolWaitMinutes: DEFAULT_TIMING_MINUTES.toolWait,
|
|
2169
2190
|
defaultStuckMinutes: DEFAULT_TIMING_MINUTES.stuck,
|
|
2170
2191
|
timingError: error instanceof Error ? error.message : String(error),
|
|
@@ -2176,7 +2197,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2176
2197
|
capOverride = cap;
|
|
2177
2198
|
pi.appendEntry("pi-better-subagents-cap", { version: 1, maxConcurrent: cap });
|
|
2178
2199
|
};
|
|
2179
|
-
const changeTiming = (key: "toolWaitMinutes" | "stuckMinutes", minutes: number): void => {
|
|
2200
|
+
const changeTiming = (key: "maxMinutes" | "toolWaitMinutes" | "stuckMinutes", minutes: number): void => {
|
|
2180
2201
|
const value = validatedTimingMinutes(minutes, key);
|
|
2181
2202
|
timingOverride = { ...timingOverride, [key]: value };
|
|
2182
2203
|
pi.appendEntry("pi-better-subagents-timing", { version: 1, ...timingOverride });
|
|
@@ -2192,7 +2213,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2192
2213
|
skillsOverride = undefined;
|
|
2193
2214
|
pi.appendEntry("pi-better-subagents-cap", { version: 1, maxConcurrent: null });
|
|
2194
2215
|
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: null });
|
|
2195
|
-
pi.appendEntry("pi-better-subagents-timing", { version: 1, toolWaitMinutes: null, stuckMinutes: null });
|
|
2216
|
+
pi.appendEntry("pi-better-subagents-timing", { version: 1, maxMinutes: null, toolWaitMinutes: null, stuckMinutes: null });
|
|
2196
2217
|
pi.appendEntry("pi-better-subagents-skills", { version: 1, mode: null });
|
|
2197
2218
|
};
|
|
2198
2219
|
const activeDelegationMode = (): DelegationMode => delegationOverride ?? normalizeDelegationMode(loadConfig().delegationMode);
|
|
@@ -2254,7 +2275,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2254
2275
|
const hasSavedSkillChoice = savedSkillValue !== undefined && savedSkillValue !== null;
|
|
2255
2276
|
// Like skills, only save a timing key this session overrides or the
|
|
2256
2277
|
// config already carries; built-in defaults need no stored copy.
|
|
2257
|
-
const savedTiming: { toolWaitMinutes?: number; stuckMinutes?: number } = {};
|
|
2278
|
+
const savedTiming: { maxMinutes?: number; toolWaitMinutes?: number; stuckMinutes?: number } = {};
|
|
2279
|
+
if (timingOverride.maxMinutes !== undefined) {
|
|
2280
|
+
savedTiming.maxMinutes = timingOverride.maxMinutes;
|
|
2281
|
+
} else if (isTimingMinutes(cfg.maxMinutes)) {
|
|
2282
|
+
savedTiming.maxMinutes = cfg.maxMinutes;
|
|
2283
|
+
}
|
|
2258
2284
|
if (timingOverride.toolWaitMinutes !== undefined) {
|
|
2259
2285
|
savedTiming.toolWaitMinutes = timingOverride.toolWaitMinutes;
|
|
2260
2286
|
} else if (isTimingMinutes(cfg.toolWaitMinutes)) {
|
|
@@ -2307,6 +2333,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2307
2333
|
const branch = ctx.sessionManager?.getBranch?.();
|
|
2308
2334
|
if (!Array.isArray(branch)) return;
|
|
2309
2335
|
for (const entry of branch) {
|
|
2336
|
+
if (entry.type === "custom" && entry.customType === "pi-better-subagents-timeout") {
|
|
2337
|
+
const data = entry.data as { version?: unknown; maxMinutes?: unknown } | null;
|
|
2338
|
+
if (data?.version === 1) {
|
|
2339
|
+
const next = { ...timingOverride };
|
|
2340
|
+
if (data.maxMinutes === null) delete next.maxMinutes;
|
|
2341
|
+
else if (isTimingMinutes(data.maxMinutes)) next.maxMinutes = data.maxMinutes;
|
|
2342
|
+
timingOverride = next;
|
|
2343
|
+
}
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2310
2346
|
if (entry.type === "custom" && entry.customType === "pi-better-subagents-cap") {
|
|
2311
2347
|
const data = entry.data as { version?: unknown; maxConcurrent?: unknown } | null;
|
|
2312
2348
|
if (data?.version === 1) {
|
|
@@ -2329,10 +2365,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2329
2365
|
}
|
|
2330
2366
|
if (entry.type === "custom" && entry.customType === "pi-better-subagents-timing") {
|
|
2331
2367
|
const data = entry.data as {
|
|
2332
|
-
version?: unknown; toolWaitMinutes?: unknown; stuckMinutes?: unknown;
|
|
2368
|
+
version?: unknown; maxMinutes?: unknown; toolWaitMinutes?: unknown; stuckMinutes?: unknown;
|
|
2333
2369
|
} | null;
|
|
2334
2370
|
if (data?.version !== 1) continue;
|
|
2335
|
-
const next: { toolWaitMinutes?: number; stuckMinutes?: number } = {};
|
|
2371
|
+
const next: { maxMinutes?: number; toolWaitMinutes?: number; stuckMinutes?: number } = {};
|
|
2372
|
+
if (isTimingMinutes(data.maxMinutes)) next.maxMinutes = data.maxMinutes;
|
|
2336
2373
|
if (isTimingMinutes(data.toolWaitMinutes)) next.toolWaitMinutes = data.toolWaitMinutes;
|
|
2337
2374
|
if (isTimingMinutes(data.stuckMinutes)) next.stuckMinutes = data.stuckMinutes;
|
|
2338
2375
|
timingOverride = next;
|
|
@@ -2538,7 +2575,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2538
2575
|
}
|
|
2539
2576
|
const cfg = loadConfig();
|
|
2540
2577
|
// Harness-owned timing (soft deadline, ceiling, tool-wait and stuck windows); validated before any side effect.
|
|
2541
|
-
resolveRunTiming({ params: p, settings: timingSettingsFor(cfg), env: process.env, startedAt: 0 });
|
|
2578
|
+
resolveRunTiming({ params: launchTimingParams(p), settings: timingSettingsFor(cfg), env: process.env, startedAt: 0 });
|
|
2542
2579
|
let model: string | undefined;
|
|
2543
2580
|
let thinking: ThinkingLevel | undefined;
|
|
2544
2581
|
if (resume) {
|
|
@@ -2935,7 +2972,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2935
2972
|
authorityReference: "",
|
|
2936
2973
|
// The assignment deadline the attempt was admitted with; a
|
|
2937
2974
|
// continuation must not silently extend it.
|
|
2938
|
-
deadlineAt: resolveRunTiming({ params: p, settings: timingSettingsFor(cfg), env: process.env, startedAt: Date.now() }).deadlineAt ?? null,
|
|
2975
|
+
deadlineAt: resolveRunTiming({ params: launchTimingParams(p), settings: timingSettingsFor(cfg), env: process.env, startedAt: Date.now() }).deadlineAt ?? null,
|
|
2939
2976
|
taskPolicy: taskRuntime?.policy ?? null,
|
|
2940
2977
|
});
|
|
2941
2978
|
const authorityReference = `worker-policy:${createHash("sha256")
|
|
@@ -3074,7 +3111,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3074
3111
|
tools: bridgePlan.tools,
|
|
3075
3112
|
},
|
|
3076
3113
|
} : {}),
|
|
3077
|
-
timing: resolveRunTiming({ params: p, settings: timingSettingsFor(cfg), env: process.env, startedAt }),
|
|
3114
|
+
timing: resolveRunTiming({ params: launchTimingParams(p), settings: timingSettingsFor(cfg), env: process.env, startedAt }),
|
|
3078
3115
|
};
|
|
3079
3116
|
// This instance holds the exit observer for a run it actually
|
|
3080
3117
|
// started; only it can finalize on the exit event (#625).
|
|
@@ -3316,17 +3353,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3316
3353
|
function continuationTiming(prior: RunMeta): TimingParams {
|
|
3317
3354
|
const timing = prior.timing;
|
|
3318
3355
|
if (!timing) return {};
|
|
3319
|
-
|
|
3320
|
-
if (at === undefined) return undefined;
|
|
3321
|
-
return Math.max(1, at - Date.now()) / 60_000;
|
|
3322
|
-
};
|
|
3323
|
-
return {
|
|
3324
|
-
...(timing.deadlineAt !== undefined ? { deadline_minutes: minutes(timing.deadlineAt) } : {}),
|
|
3325
|
-
...(timing.ceilingAt !== undefined ? { max_minutes: minutes(timing.ceilingAt) } : {}),
|
|
3326
|
-
...(timing.graceMs !== undefined ? { grace_minutes: timing.graceMs / 60_000 } : {}),
|
|
3327
|
-
...(timing.toolWaitMs !== undefined ? { tool_wait_minutes: timing.toolWaitMs / 60_000 } : {}),
|
|
3328
|
-
...(timing.stuckMs !== undefined ? { stuck_minutes: timing.stuckMs / 60_000 } : {}),
|
|
3329
|
-
};
|
|
3356
|
+
return continuationTimingParams(timing, Date.now());
|
|
3330
3357
|
}
|
|
3331
3358
|
|
|
3332
3359
|
/**
|
|
@@ -3415,7 +3442,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3415
3442
|
"The tools param is both the tool allowlist AND what determines which extensions load in the child (e.g. tools='read,bash,web_fetch' loads only the web-tools package). Ask for the tools the task needs and nothing more; clean:true gives a built-ins-only child. When the task already names its SKILL.md by path, pass skills with those absolute paths (or [] for none) so the child does not advertise the whole skills catalog. Without an agent or role, pick a model with the model param (e.g. 'xai/grok-4.5@high'); providerless model patterns are resolved by Pi, while provider/model is deterministic and loads mapped provider extensions.",
|
|
3416
3443
|
...CATALOG_GUIDELINES,
|
|
3417
3444
|
"Subagent sandboxing defaults to off. Human settings in /sandbox control file, credential-file, command, and network permissions; sandbox:false cannot override an enabled human profile. Set sandbox:true to request kernel confinement, including without published settings. Set callback:false to finish quietly — then read the result on demand via subagent_result.",
|
|
3418
|
-
"Every launch automatically joins one shared harness-owned watcher, including batch launches; it stops when the last active run ends. Do not start a separate heartbeat or background watcher for subagents. A tool call still open for tool_wait_minutes (default 5) or no progress for stuck_minutes (default 10) sends one factual alert; these alerts never stop, cancel, or restart the run.
|
|
3445
|
+
"Every launch automatically joins one shared harness-owned watcher, including batch launches; it stops when the last active run ends. Do not start a separate heartbeat or background watcher for subagents. A tool call still open for tool_wait_minutes (default 5) or no progress for stuck_minutes (default 10) sends one factual alert; these alerts never stop, cancel, or restart the run. Hard timeouts default to 60 minutes; max_minutes overrides that default (0 disables). Soft deadlines remain opt-in for work with a real wall-clock bound: deadline_minutes steers the child to wrap up and later stops it after grace_minutes, while max_minutes stops it without grace. Do not set elapsed-time limits merely because a task may take a while; use them only when the work needs a real wall-clock bound.",
|
|
3419
3446
|
"Use git_clone_workspace:true when the subagent will mutate Git in a sandbox. The parent prepares a disposable, self-contained clone with a real .git/ directory inside the sandbox root, so linked-worktree metadata outside the sandbox cannot stall the child.",
|
|
3420
3447
|
"Saved conversations persist automatically. To continue a completed or stopped worker in its own history, call subagent_spawn with session_id (the native session id reported for the saved conversation) and the new feedback as prompt. Omit session_id for a brand-new conversation.",
|
|
3421
3448
|
"On a continuation, pass prompt, session_id, and only optional model, thinking, callback, or timing overrides. Launch-only options (name, agent, role, alias, tools, exclude_tools, skills, clean, sandbox, sandbox_dir, cwd, git_clone_workspace, approve, allow_nested) keep the saved recipe; repeat them only with the same value, otherwise the launch is refused.",
|
|
@@ -3914,6 +3941,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3914
3941
|
const timing = timingState();
|
|
3915
3942
|
return {
|
|
3916
3943
|
mode: settings.delegationMode, maxConcurrent: settings.maxConcurrent,
|
|
3944
|
+
maxMinutes: timing.maxMinutes,
|
|
3917
3945
|
modeSource: settings.sources.delegationMode,
|
|
3918
3946
|
capSource: settings.sources.maxConcurrent,
|
|
3919
3947
|
defaultMode: normalizeDelegationMode(loadConfig().delegationMode),
|
|
@@ -3940,7 +3968,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3940
3968
|
: skills.mode === "none" ? "no skills" : `${skills.paths.length} selected skills`;
|
|
3941
3969
|
const timing = timingState();
|
|
3942
3970
|
ctx.ui.notify(`Delegation mode: ${activeDelegationMode()}. Concurrent subagents: ${activeConcurrencyCap()}. ` +
|
|
3943
|
-
`
|
|
3971
|
+
`Timeout: ${timing.maxMinutes} minutes (0 disables). Tool-wait alert: ${timing.toolWaitMinutes} min. ` +
|
|
3972
|
+
`No-progress alert: ${timing.stuckMinutes} min. Skills: ${skillChoice}. ` +
|
|
3944
3973
|
`Use /subagents mode <mode>, cap <number>, tool-wait <minutes>, stuck <minutes>, or settings; /subagents save persists them.`, "info");
|
|
3945
3974
|
}
|
|
3946
3975
|
};
|
|
@@ -3976,7 +4005,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
3976
4005
|
const timing = timingState();
|
|
3977
4006
|
if (ctx.hasUI && !(await ctx.ui.confirm("Save subagent defaults?",
|
|
3978
4007
|
`Future sessions will start in ${activeDelegationMode()} with a cap of ${activeConcurrencyCap()}, ` +
|
|
3979
|
-
`
|
|
4008
|
+
`timeout ${timing.maxMinutes} minutes (0 disables), tool-wait alert ${timing.toolWaitMinutes} min, ` +
|
|
4009
|
+
`no-progress alert ${timing.stuckMinutes} min, and ${skillChoice}.`))) {
|
|
3980
4010
|
ctx.ui.notify("Subagent defaults unchanged.", "info");
|
|
3981
4011
|
return;
|
|
3982
4012
|
}
|
|
@@ -6,11 +6,12 @@ import type { SkillExposureMode } from "./skills.ts";
|
|
|
6
6
|
import { createSkillPicker, type DiscoveredSkill } from "./skill-picker.ts";
|
|
7
7
|
import { validatedTimingMinutes } from "./timing.ts";
|
|
8
8
|
|
|
9
|
-
export type TimingSettingKey = "toolWaitMinutes" | "stuckMinutes";
|
|
9
|
+
export type TimingSettingKey = "maxMinutes" | "toolWaitMinutes" | "stuckMinutes";
|
|
10
10
|
|
|
11
11
|
export interface SubagentSettingsSnapshot {
|
|
12
12
|
mode: DelegationMode;
|
|
13
13
|
maxConcurrent: number;
|
|
14
|
+
maxMinutes: number;
|
|
14
15
|
modeSource: "session" | "config";
|
|
15
16
|
capSource: "session" | "config";
|
|
16
17
|
defaultMode: DelegationMode;
|
|
@@ -45,13 +46,14 @@ export interface SubagentSettingsHandlers {
|
|
|
45
46
|
openAgents?(): void;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
type EditTarget = { kind: "cap" } | { kind: "toolWait" } | { kind: "stuck" };
|
|
49
|
+
type EditTarget = { kind: "cap" } | { kind: "toolWait" } | { kind: "stuck" } | { kind: "timeout" };
|
|
49
50
|
|
|
50
51
|
type Row =
|
|
51
52
|
| { kind: "mode" }
|
|
52
53
|
| { kind: "cap" }
|
|
53
54
|
| { kind: "toolWait" }
|
|
54
55
|
| { kind: "stuck" }
|
|
56
|
+
| { kind: "timeout" }
|
|
55
57
|
| { kind: "skills" }
|
|
56
58
|
| { kind: "choose" }
|
|
57
59
|
| { kind: "agents" }
|
|
@@ -110,7 +112,7 @@ export function createSubagentSettingsPage(
|
|
|
110
112
|
|
|
111
113
|
function rowsOf(settings: SubagentSettingsSnapshot): Row[] {
|
|
112
114
|
const rows: Row[] = [
|
|
113
|
-
{ kind: "mode" }, { kind: "cap" }, { kind: "toolWait" }, { kind: "stuck" },
|
|
115
|
+
{ kind: "mode" }, { kind: "cap" }, { kind: "toolWait" }, { kind: "stuck" }, { kind: "timeout" },
|
|
114
116
|
{ kind: "skills" }, { kind: "choose" },
|
|
115
117
|
];
|
|
116
118
|
rows.push({ kind: "agents" }, { kind: "reset" });
|
|
@@ -178,6 +180,8 @@ export function createSubagentSettingsPage(
|
|
|
178
180
|
startEditing({ kind: "toolWait" }, String(settings.toolWaitMinutes));
|
|
179
181
|
} else if (selected.kind === "stuck") {
|
|
180
182
|
startEditing({ kind: "stuck" }, String(settings.stuckMinutes));
|
|
183
|
+
} else if (selected.kind === "timeout") {
|
|
184
|
+
startEditing({ kind: "timeout" }, String(settings.maxMinutes));
|
|
181
185
|
} else if (selected.kind === "skills") {
|
|
182
186
|
if (settings.skillsMode === "explicit") rememberedSkillPaths = [...settings.skillsPaths];
|
|
183
187
|
const next = cycleSkillMode(settings.skillsMode);
|
|
@@ -219,11 +223,13 @@ export function createSubagentSettingsPage(
|
|
|
219
223
|
}
|
|
220
224
|
return;
|
|
221
225
|
}
|
|
222
|
-
const key = target.kind === "toolWait" ? "toolWaitMinutes"
|
|
226
|
+
const key = target.kind === "toolWait" ? "toolWaitMinutes"
|
|
227
|
+
: target.kind === "stuck" ? "stuckMinutes" : "maxMinutes";
|
|
223
228
|
try {
|
|
224
229
|
const minutes = validatedTimingMinutes(value.trim(), key);
|
|
225
230
|
handlers.changeTiming(key, minutes);
|
|
226
|
-
finishEditing(
|
|
231
|
+
finishEditing(target.kind === "toolWait" ? "Tool-wait alert updated."
|
|
232
|
+
: target.kind === "stuck" ? "No-progress alert updated." : "Timeout updated.");
|
|
227
233
|
} catch (cause) {
|
|
228
234
|
report(cause instanceof Error ? cause.message : String(cause), true);
|
|
229
235
|
}
|
|
@@ -302,12 +308,15 @@ export function createSubagentSettingsPage(
|
|
|
302
308
|
rows.forEach((entry, index) => {
|
|
303
309
|
const selected = index === row;
|
|
304
310
|
if (selected) selectedStart = lines.length;
|
|
305
|
-
if (entry.kind === "mode" || entry.kind === "cap" || entry.kind === "toolWait"
|
|
311
|
+
if (entry.kind === "mode" || entry.kind === "cap" || entry.kind === "toolWait"
|
|
312
|
+
|| entry.kind === "stuck" || entry.kind === "timeout") {
|
|
306
313
|
const label = entry.kind === "mode" ? "Delegation mode"
|
|
307
314
|
: entry.kind === "cap" ? "Concurrent subagents"
|
|
308
|
-
: entry.kind === "toolWait" ? "Tool-wait alert"
|
|
315
|
+
: entry.kind === "toolWait" ? "Tool-wait alert"
|
|
316
|
+
: entry.kind === "stuck" ? "No-progress alert" : "Timeout (minutes)";
|
|
309
317
|
const value = entry.kind === "mode" ? settings.mode
|
|
310
318
|
: entry.kind === "cap" ? String(settings.maxConcurrent)
|
|
319
|
+
: entry.kind === "timeout" ? String(settings.maxMinutes)
|
|
311
320
|
: `${entry.kind === "toolWait" ? settings.toolWaitMinutes : settings.stuckMinutes} min`;
|
|
312
321
|
const active = editing?.kind === entry.kind
|
|
313
322
|
? input.render(compact ? Math.max(1, w - 2) : valueWidth)[0]!
|
|
@@ -58,7 +58,7 @@ export interface OpenCallRecord {
|
|
|
58
58
|
export const DEFAULT_TIMING_MINUTES = Object.freeze({
|
|
59
59
|
deadline: 0,
|
|
60
60
|
grace: 5,
|
|
61
|
-
max:
|
|
61
|
+
max: 60,
|
|
62
62
|
toolWait: 5,
|
|
63
63
|
stuck: 10,
|
|
64
64
|
});
|
|
@@ -111,6 +111,18 @@ export interface TimingParams {
|
|
|
111
111
|
stuck_minutes?: number | null;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Carry a parked run's remaining limits, including an explicitly disabled ceiling. */
|
|
115
|
+
export function continuationTimingParams(timing: RunTiming, now: number): TimingParams {
|
|
116
|
+
const remaining = (at: number): number => Math.max(1, at - now) / 60_000;
|
|
117
|
+
return {
|
|
118
|
+
...(timing.deadlineAt !== undefined ? { deadline_minutes: remaining(timing.deadlineAt) } : {}),
|
|
119
|
+
max_minutes: timing.ceilingAt === undefined ? 0 : remaining(timing.ceilingAt),
|
|
120
|
+
grace_minutes: timing.graceMs / 60_000,
|
|
121
|
+
...(timing.toolWaitMs !== undefined ? { tool_wait_minutes: timing.toolWaitMs / 60_000 } : {}),
|
|
122
|
+
...(timing.stuckMs !== undefined ? { stuck_minutes: timing.stuckMs / 60_000 } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
export interface TimingSettings {
|
|
115
127
|
deadlineMinutes?: number | null;
|
|
116
128
|
graceMinutes?: number | null;
|
|
@@ -148,11 +160,6 @@ export function validatedTimingMinutes(value: unknown, label = "minutes"): numbe
|
|
|
148
160
|
return parsed;
|
|
149
161
|
}
|
|
150
162
|
|
|
151
|
-
/**
|
|
152
|
-
* One optional minutes value. `undefined`, `null`, and (for env) an empty
|
|
153
|
-
* string mean "not set". Anything else must be a finite number from 0 to a
|
|
154
|
-
* week; `0` disables.
|
|
155
|
-
*/
|
|
156
163
|
function minutes(value: unknown, label: string): number | undefined {
|
|
157
164
|
if (value === undefined || value === null) return undefined;
|
|
158
165
|
if (typeof value === "string") {
|
|
@@ -212,7 +219,7 @@ export function resolveRunTiming(input: {
|
|
|
212
219
|
const TIMING_DESCRIPTIONS = {
|
|
213
220
|
deadline_minutes: "Optional soft deadline in minutes (default: none; 0 = none; null or omitted = inherit: in a batch job the shared value, otherwise the default). At the deadline the child is told to stop starting new work, commit what is done, and report, and you get one wake. Grace starts when the message reaches the child (after its current tool call); if it has not finished by then, the harness stops it with reason deadline.",
|
|
214
221
|
grace_minutes: "Minutes after the wrap-up message reaches the child before the run is stopped (default 5; null or omitted = inherit).",
|
|
215
|
-
max_minutes: "Optional hard ceiling in minutes (default:
|
|
222
|
+
max_minutes: "Optional hard ceiling in minutes (default: 60; 0 = none; null or omitted = inherit). The run is stopped at once, without grace, with reason ceiling.",
|
|
216
223
|
tool_wait_minutes: "Tool-call wait alert in minutes (default 5; 0 = off; null or omitted = inherit). While a tool call stays open this long you get one factual wake for that call, including a normal long test. Parallel calls alert independently. The alert does not stop or cancel the run.",
|
|
217
224
|
stuck_minutes: "No-progress window in minutes (default 10; 0 = off; null or omitted = inherit). Progress is any successful tool call that is not an exact repeat of an earlier one (same tool, same arguments); edits, writes, commits, and a success after a failure always count. Time inside a running tool call does not count. Wakes you once per stuck spell; never stops the run.",
|
|
218
225
|
} as const;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, model routing, and Outcome & Todo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"pi-ollama-cloud": "0.13.1",
|
|
65
65
|
"pi-better-sandbox": "0.14.1",
|
|
66
66
|
"pi-better-ssh": "0.1.5",
|
|
67
|
-
"pi-better-subagents": "0.
|
|
67
|
+
"pi-better-subagents": "0.30.0",
|
|
68
68
|
"proper-lockfile": "^4.1.2",
|
|
69
69
|
"shell-quote": "1.12.0",
|
|
70
70
|
"smol-toml": "1.9.0",
|