@tintinweb/pi-subagents 0.7.0 → 0.7.1
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 +15 -0
- package/README.md +1 -1
- package/dist/agent-manager.js +39 -19
- package/dist/index.js +38 -23
- package/package.json +1 -1
- package/src/agent-manager.ts +37 -20
- package/src/index.ts +33 -23
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.1] - 2026-05-07
|
|
11
|
+
|
|
12
|
+
> **Heads-up — behavior change:**
|
|
13
|
+
> - `isolation: "worktree"` now fails loud (returns an error) instead of silently falling back to the main tree. Affects users running pi in a non-git directory or a fresh repo with no commits.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **`isolation: "worktree"` now fails loud instead of silently falling back.** Previously when `createWorktree` returned undefined (not a git repo, no commits yet, or `git worktree add` failed), the agent ran in the main `cwd` with a `[WARNING: ...]` block prepended to its prompt — visible only to the LLM, never surfaced to the caller. Now the failure throws a structured error that propagates back to the `Agent` tool response; no agent record is created. Failed scheduled fires are recorded as `lastStatus: "error"` with the reason in the `subagents:scheduled` error event. Queued background spawns whose worktree creation fails when they dequeue are marked terminal-error and don't block the rest of the queue.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Headless `pi --print` runs no longer hang or crash after background
|
|
21
|
+
subagents complete.** Cleanup timers no longer keep the process alive, and
|
|
22
|
+
stale completion notifications are treated as best-effort shutdown side
|
|
23
|
+
effects.
|
|
24
|
+
|
|
10
25
|
## [0.7.0] - 2026-05-04
|
|
11
26
|
|
|
12
27
|
> **Heads-up — behavior changes:**
|
package/README.md
CHANGED
|
@@ -453,7 +453,7 @@ The agent gets a full, isolated copy of the repository. On completion:
|
|
|
453
453
|
- **No changes:** worktree is cleaned up automatically
|
|
454
454
|
- **Changes made:** changes are committed to a new branch (`pi-agent-<id>`) and returned in the result
|
|
455
455
|
|
|
456
|
-
If the worktree cannot be created (not a git repo, no commits), the
|
|
456
|
+
If the worktree cannot be created (not a git repo, no commits, or `git worktree add` fails), the `Agent` tool returns a clear error instead of running unisolated — `isolation: "worktree"` is a strict guarantee, not a hint. Initialize git and commit at least once, or omit `isolation`.
|
|
457
457
|
|
|
458
458
|
## Skill Preloading
|
|
459
459
|
|
package/dist/agent-manager.js
CHANGED
|
@@ -29,6 +29,7 @@ export class AgentManager {
|
|
|
29
29
|
this.maxConcurrent = maxConcurrent;
|
|
30
30
|
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
31
31
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
32
|
+
this.cleanupInterval.unref();
|
|
32
33
|
}
|
|
33
34
|
/** Update the max concurrent background agents limit. */
|
|
34
35
|
setMaxConcurrent(n) {
|
|
@@ -64,11 +65,32 @@ export class AgentManager {
|
|
|
64
65
|
this.queue.push({ id, args });
|
|
65
66
|
return id;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
// startAgent can throw (e.g. strict worktree-isolation failure) — clean
|
|
69
|
+
// up the record so callers don't see an orphan in `listAgents()`.
|
|
70
|
+
try {
|
|
71
|
+
this.startAgent(id, record, args);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
this.agents.delete(id);
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
68
77
|
return id;
|
|
69
78
|
}
|
|
70
79
|
/** Actually start an agent (called immediately or from queue drain). */
|
|
71
80
|
startAgent(id, record, { pi, ctx, type, prompt, options }) {
|
|
81
|
+
// Worktree isolation: try to create a temporary git worktree. Strict —
|
|
82
|
+
// fail loud if not possible (no silent fallback to main tree). Done
|
|
83
|
+
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
84
|
+
let worktreeCwd;
|
|
85
|
+
if (options.isolation === "worktree") {
|
|
86
|
+
const wt = createWorktree(ctx.cwd, id);
|
|
87
|
+
if (!wt) {
|
|
88
|
+
throw new Error('Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
89
|
+
'Initialize git and commit at least once, or omit `isolation`.');
|
|
90
|
+
}
|
|
91
|
+
record.worktree = wt;
|
|
92
|
+
worktreeCwd = wt.path;
|
|
93
|
+
}
|
|
72
94
|
record.status = "running";
|
|
73
95
|
record.startedAt = Date.now();
|
|
74
96
|
if (options.isBackground)
|
|
@@ -82,22 +104,7 @@ export class AgentManager {
|
|
|
82
104
|
detachParentSignal = () => options.signal.removeEventListener("abort", onParentAbort);
|
|
83
105
|
}
|
|
84
106
|
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
85
|
-
|
|
86
|
-
let worktreeCwd;
|
|
87
|
-
let worktreeWarning = "";
|
|
88
|
-
if (options.isolation === "worktree") {
|
|
89
|
-
const wt = createWorktree(ctx.cwd, id);
|
|
90
|
-
if (wt) {
|
|
91
|
-
record.worktree = wt;
|
|
92
|
-
worktreeCwd = wt.path;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
worktreeWarning = "\n\n[WARNING: Worktree isolation was requested but failed (not a git repo, or no commits yet). Running in the main working directory instead.]";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Prepend worktree warning to prompt if isolation failed
|
|
99
|
-
const effectivePrompt = worktreeWarning ? worktreeWarning + "\n\n" + prompt : prompt;
|
|
100
|
-
const promise = runAgent(ctx, type, effectivePrompt, {
|
|
107
|
+
const promise = runAgent(ctx, type, prompt, {
|
|
101
108
|
pi,
|
|
102
109
|
model: options.model,
|
|
103
110
|
maxTurns: options.maxTurns,
|
|
@@ -162,7 +169,10 @@ export class AgentManager {
|
|
|
162
169
|
}
|
|
163
170
|
if (options.isBackground) {
|
|
164
171
|
this.runningBackground--;
|
|
165
|
-
|
|
172
|
+
try {
|
|
173
|
+
this.onComplete?.(record);
|
|
174
|
+
}
|
|
175
|
+
catch { /* ignore completion side-effect errors */ }
|
|
166
176
|
this.drainQueue();
|
|
167
177
|
}
|
|
168
178
|
return responseText;
|
|
@@ -207,7 +217,17 @@ export class AgentManager {
|
|
|
207
217
|
const record = this.agents.get(next.id);
|
|
208
218
|
if (!record || record.status !== "queued")
|
|
209
219
|
continue;
|
|
210
|
-
|
|
220
|
+
try {
|
|
221
|
+
this.startAgent(next.id, record, next.args);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
// Late failure (e.g. strict worktree-isolation) — surface on the record
|
|
225
|
+
// so the user/agent can see it via /agents, then keep draining.
|
|
226
|
+
record.status = "error";
|
|
227
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
228
|
+
record.completedAt = Date.now();
|
|
229
|
+
this.onComplete?.(record);
|
|
230
|
+
}
|
|
211
231
|
}
|
|
212
232
|
}
|
|
213
233
|
/**
|
package/dist/index.js
CHANGED
|
@@ -235,7 +235,10 @@ export default function (pi) {
|
|
|
235
235
|
cancelNudge(key);
|
|
236
236
|
pendingNudges.set(key, setTimeout(() => {
|
|
237
237
|
pendingNudges.delete(key);
|
|
238
|
-
|
|
238
|
+
try {
|
|
239
|
+
send();
|
|
240
|
+
}
|
|
241
|
+
catch { /* ignore stale completion side-effect errors */ }
|
|
239
242
|
}, delay));
|
|
240
243
|
}
|
|
241
244
|
function cancelNudge(key) {
|
|
@@ -830,17 +833,22 @@ Guidelines:
|
|
|
830
833
|
rec.outputCleanup = streamToOutputFile(session, rec.outputFile, id, ctx.cwd);
|
|
831
834
|
}
|
|
832
835
|
};
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
836
|
+
try {
|
|
837
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
838
|
+
description: params.description,
|
|
839
|
+
model,
|
|
840
|
+
maxTurns: effectiveMaxTurns,
|
|
841
|
+
isolated,
|
|
842
|
+
inheritContext,
|
|
843
|
+
thinkingLevel: thinking,
|
|
844
|
+
isBackground: true,
|
|
845
|
+
isolation,
|
|
846
|
+
...bgCallbacks,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
851
|
+
}
|
|
844
852
|
// Set output file + join mode synchronously after spawn, before the
|
|
845
853
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
846
854
|
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
@@ -925,17 +933,24 @@ Guidelines:
|
|
|
925
933
|
streamUpdate();
|
|
926
934
|
}, 80);
|
|
927
935
|
streamUpdate();
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
936
|
+
let record;
|
|
937
|
+
try {
|
|
938
|
+
record = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
939
|
+
description: params.description,
|
|
940
|
+
model,
|
|
941
|
+
maxTurns: effectiveMaxTurns,
|
|
942
|
+
isolated,
|
|
943
|
+
inheritContext,
|
|
944
|
+
thinkingLevel: thinking,
|
|
945
|
+
isolation,
|
|
946
|
+
signal,
|
|
947
|
+
...fgCallbacks,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
catch (err) {
|
|
951
|
+
clearInterval(spinnerInterval);
|
|
952
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
953
|
+
}
|
|
939
954
|
clearInterval(spinnerInterval);
|
|
940
955
|
// Clean up foreground agent from widget
|
|
941
956
|
if (fgId) {
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -87,6 +87,7 @@ export class AgentManager {
|
|
|
87
87
|
this.maxConcurrent = maxConcurrent;
|
|
88
88
|
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
89
89
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
90
|
+
this.cleanupInterval.unref();
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/** Update the max concurrent background agents limit. */
|
|
@@ -134,12 +135,35 @@ export class AgentManager {
|
|
|
134
135
|
return id;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
// startAgent can throw (e.g. strict worktree-isolation failure) — clean
|
|
139
|
+
// up the record so callers don't see an orphan in `listAgents()`.
|
|
140
|
+
try {
|
|
141
|
+
this.startAgent(id, record, args);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.agents.delete(id);
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
138
146
|
return id;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
/** Actually start an agent (called immediately or from queue drain). */
|
|
142
150
|
private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
|
|
151
|
+
// Worktree isolation: try to create a temporary git worktree. Strict —
|
|
152
|
+
// fail loud if not possible (no silent fallback to main tree). Done
|
|
153
|
+
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
154
|
+
let worktreeCwd: string | undefined;
|
|
155
|
+
if (options.isolation === "worktree") {
|
|
156
|
+
const wt = createWorktree(ctx.cwd, id);
|
|
157
|
+
if (!wt) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
160
|
+
'Initialize git and commit at least once, or omit `isolation`.',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
record.worktree = wt;
|
|
164
|
+
worktreeCwd = wt.path;
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
record.status = "running";
|
|
144
168
|
record.startedAt = Date.now();
|
|
145
169
|
if (options.isBackground) this.runningBackground++;
|
|
@@ -154,23 +178,7 @@ export class AgentManager {
|
|
|
154
178
|
}
|
|
155
179
|
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
156
180
|
|
|
157
|
-
|
|
158
|
-
let worktreeCwd: string | undefined;
|
|
159
|
-
let worktreeWarning = "";
|
|
160
|
-
if (options.isolation === "worktree") {
|
|
161
|
-
const wt = createWorktree(ctx.cwd, id);
|
|
162
|
-
if (wt) {
|
|
163
|
-
record.worktree = wt;
|
|
164
|
-
worktreeCwd = wt.path;
|
|
165
|
-
} else {
|
|
166
|
-
worktreeWarning = "\n\n[WARNING: Worktree isolation was requested but failed (not a git repo, or no commits yet). Running in the main working directory instead.]";
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Prepend worktree warning to prompt if isolation failed
|
|
171
|
-
const effectivePrompt = worktreeWarning ? worktreeWarning + "\n\n" + prompt : prompt;
|
|
172
|
-
|
|
173
|
-
const promise = runAgent(ctx, type, effectivePrompt, {
|
|
181
|
+
const promise = runAgent(ctx, type, prompt, {
|
|
174
182
|
pi,
|
|
175
183
|
model: options.model,
|
|
176
184
|
maxTurns: options.maxTurns,
|
|
@@ -235,7 +243,7 @@ export class AgentManager {
|
|
|
235
243
|
|
|
236
244
|
if (options.isBackground) {
|
|
237
245
|
this.runningBackground--;
|
|
238
|
-
this.onComplete?.(record);
|
|
246
|
+
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
|
|
239
247
|
this.drainQueue();
|
|
240
248
|
}
|
|
241
249
|
return responseText;
|
|
@@ -281,7 +289,16 @@ export class AgentManager {
|
|
|
281
289
|
const next = this.queue.shift()!;
|
|
282
290
|
const record = this.agents.get(next.id);
|
|
283
291
|
if (!record || record.status !== "queued") continue;
|
|
284
|
-
|
|
292
|
+
try {
|
|
293
|
+
this.startAgent(next.id, record, next.args);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// Late failure (e.g. strict worktree-isolation) — surface on the record
|
|
296
|
+
// so the user/agent can see it via /agents, then keep draining.
|
|
297
|
+
record.status = "error";
|
|
298
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
299
|
+
record.completedAt = Date.now();
|
|
300
|
+
this.onComplete?.(record);
|
|
301
|
+
}
|
|
285
302
|
}
|
|
286
303
|
}
|
|
287
304
|
|
package/src/index.ts
CHANGED
|
@@ -275,7 +275,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
275
275
|
cancelNudge(key);
|
|
276
276
|
pendingNudges.set(key, setTimeout(() => {
|
|
277
277
|
pendingNudges.delete(key);
|
|
278
|
-
send();
|
|
278
|
+
try { send(); } catch { /* ignore stale completion side-effect errors */ }
|
|
279
279
|
}, delay));
|
|
280
280
|
}
|
|
281
281
|
|
|
@@ -946,17 +946,21 @@ Guidelines:
|
|
|
946
946
|
}
|
|
947
947
|
};
|
|
948
948
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
949
|
+
try {
|
|
950
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
951
|
+
description: params.description,
|
|
952
|
+
model,
|
|
953
|
+
maxTurns: effectiveMaxTurns,
|
|
954
|
+
isolated,
|
|
955
|
+
inheritContext,
|
|
956
|
+
thinkingLevel: thinking,
|
|
957
|
+
isBackground: true,
|
|
958
|
+
isolation,
|
|
959
|
+
...bgCallbacks,
|
|
960
|
+
});
|
|
961
|
+
} catch (err) {
|
|
962
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
963
|
+
}
|
|
960
964
|
|
|
961
965
|
// Set output file + join mode synchronously after spawn, before the
|
|
962
966
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
@@ -1054,17 +1058,23 @@ Guidelines:
|
|
|
1054
1058
|
|
|
1055
1059
|
streamUpdate();
|
|
1056
1060
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1061
|
+
let record: AgentRecord;
|
|
1062
|
+
try {
|
|
1063
|
+
record = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
1064
|
+
description: params.description,
|
|
1065
|
+
model,
|
|
1066
|
+
maxTurns: effectiveMaxTurns,
|
|
1067
|
+
isolated,
|
|
1068
|
+
inheritContext,
|
|
1069
|
+
thinkingLevel: thinking,
|
|
1070
|
+
isolation,
|
|
1071
|
+
signal,
|
|
1072
|
+
...fgCallbacks,
|
|
1073
|
+
});
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
clearInterval(spinnerInterval);
|
|
1076
|
+
return textResult(err instanceof Error ? err.message : String(err));
|
|
1077
|
+
}
|
|
1068
1078
|
|
|
1069
1079
|
clearInterval(spinnerInterval);
|
|
1070
1080
|
|