pi-crew 0.1.39 → 0.1.41
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 +34 -0
- package/README.md +50 -4
- package/docs/usage.md +11 -0
- package/package.json +1 -1
- package/schema.json +4 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config.ts +87 -2
- package/src/extension/async-notifier.ts +26 -4
- package/src/extension/notification-sink.ts +1 -1
- package/src/extension/register.ts +23 -7
- package/src/extension/registration/subagent-tools.ts +11 -6
- package/src/extension/result-watcher.ts +38 -8
- package/src/extension/team-tool/api.ts +61 -2
- package/src/extension/team-tool/doctor.ts +31 -0
- package/src/extension/team-tool/status.ts +23 -3
- package/src/observability/metric-sink.ts +1 -1
- package/src/runtime/agent-control.ts +4 -5
- package/src/runtime/attention-events.ts +23 -0
- package/src/runtime/child-pi.ts +2 -1
- package/src/runtime/completion-guard.ts +99 -0
- package/src/runtime/crew-agent-records.ts +5 -4
- package/src/runtime/crew-agent-runtime.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -16
- package/src/runtime/group-join.ts +22 -4
- package/src/runtime/live-session-runtime.ts +12 -6
- package/src/runtime/sidechain-output.ts +2 -1
- package/src/runtime/subagent-manager.ts +6 -1
- package/src/runtime/task-runner/live-executor.ts +3 -0
- package/src/runtime/task-runner.ts +25 -2
- package/src/runtime/team-runner.ts +131 -6
- package/src/schema/config-schema.ts +3 -0
- package/src/schema/team-tool-schema.ts +12 -3
- package/src/state/artifact-store.ts +4 -2
- package/src/state/event-log.ts +2 -1
- package/src/state/jsonl-writer.ts +3 -1
- package/src/state/mailbox.ts +15 -4
- package/src/state/types.ts +25 -0
- package/src/teams/discover-teams.ts +1 -1
- package/src/ui/dashboard-panes/progress-pane.ts +3 -0
- package/src/ui/run-snapshot-cache.ts +29 -1
- package/src/ui/snapshot-types.ts +8 -0
- package/src/utils/fs-watch.ts +3 -3
- package/src/utils/redaction.ts +41 -0
- package/src/workflows/discover-workflows.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.41
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added strict-provider-friendly team tool schema shapes and config schema coverage for result delivery controls.
|
|
10
|
+
- Added resilient result watcher fallback polling for resource-limit watch failures and partial JSON retry handling.
|
|
11
|
+
- Added `runtime.completionMutationGuard` (`off`/`warn`/`fail`) with structured `task.attention` events when implementation-style workers complete without observed mutations.
|
|
12
|
+
- Added group-join mailbox delivery metadata, request-id dedupe, ack observability, timeout events, and dashboard/status visibility.
|
|
13
|
+
- Expanded `team doctor` and `team status` with schema, async/result delivery, worktree/readiness, attention, transcript, and group-join diagnostics.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Recovered adaptive implementation planner output when compaction truncates the end marker but complete phase objects are still present.
|
|
18
|
+
|
|
19
|
+
## 0.1.40
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added owner-session generation guards for background subagents, async run notifications, result watchers, and live-session callbacks so stale sessions do not receive completions.
|
|
24
|
+
- Added `runtime.requirePlanApproval` with approve/cancel API support to gate mutating adaptive implementation tasks behind an explicit planner artifact approval.
|
|
25
|
+
- Added shared secret redaction for event logs, mailbox persistence, artifacts, JSONL streams, agent records, notifications, metrics, and diagnostics.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Project-local agents, teams, and workflows can no longer shadow builtin or user resources with the same name.
|
|
30
|
+
- Project-level sensitive config such as worker execution, runtime mode, autonomy, agent overrides, worktree setup hooks, and OTLP headers is ignored with warnings unless configured in trusted user scope.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Fixed lost async completion notifications after auto-compaction/session restart by continuing to track active runs across notifier restarts.
|
|
35
|
+
- Fixed stale background subagent wakeups after session switch/shutdown while preserving terminal results for explicit joins.
|
|
36
|
+
- Fixed resume bypasses in plan approval by re-gating persisted mutating adaptive tasks when approval state is missing or pending.
|
|
37
|
+
- Restricted plan approval and cancellation to non-read-only roles and rejected cancel/approve after the approval state is no longer pending.
|
|
38
|
+
|
|
5
39
|
## 0.1.39
|
|
6
40
|
|
|
7
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -25,13 +25,14 @@ Current highlights:
|
|
|
25
25
|
- metadata-aware `recommend` action for routing, decomposition, fanout hints, async/worktree suggestions
|
|
26
26
|
- configurable autonomy profiles: `manual`, `suggested`, `assisted`, `aggressive`
|
|
27
27
|
- builtin agents, teams, and workflows
|
|
28
|
-
- user/project/builtin resource discovery
|
|
28
|
+
- user/project/builtin resource discovery where user resources override builtin resources, and project resources cannot shadow trusted user/builtin names
|
|
29
29
|
- resource format support for routing metadata: `triggers`, `useWhen`, `avoidWhen`, `cost`, `category`
|
|
30
30
|
- durable run state: manifest, tasks, events, artifacts, imports/exports
|
|
31
31
|
- foreground workflow scheduler
|
|
32
32
|
- detached async/background runner
|
|
33
33
|
- stale async PID detection
|
|
34
34
|
- active run summary and async completion notifications in Pi sessions
|
|
35
|
+
- owner-session delivery guards so stale sessions do not receive background subagent/result/live-session completions
|
|
35
36
|
- real child Pi worker execution by default, with explicit scaffold/dry-run opt-out
|
|
36
37
|
- child Pi JSON output parsing for final text, usage, and event counts
|
|
37
38
|
- retryable model fallback attempts per task
|
|
@@ -58,6 +59,10 @@ Current highlights:
|
|
|
58
59
|
- observability metrics: per-session Counter/Gauge/Histogram registry, JSONL sink, `/team-metrics`, dashboard metrics pane, Prometheus/OTLP exporters (OTLP opt-in)
|
|
59
60
|
- reliability hardening: heartbeat gradient watcher, opt-in retry executor with attempt trace, crash-recovery detection, deadletter queue
|
|
60
61
|
- background `Agent`/`crew_agent` completion wake-up so parent sessions can automatically join completed subagent results
|
|
62
|
+
- optional `runtime.requirePlanApproval` gate for planner-first approval before mutating adaptive implementation workers run
|
|
63
|
+
- optional `runtime.completionMutationGuard` to warn or fail implementation-style workers that complete without observed mutation tool calls
|
|
64
|
+
- grouped result delivery is correlated through mailbox metadata, deduped by request id, and acknowledged via existing `ack-message`
|
|
65
|
+
- shared redaction for common secrets before durable event/log/mailbox/artifact/metric/diagnostic persistence
|
|
61
66
|
- package polish: `schema.json`, TypeScript semantic check, strip-types import smoke, cross-platform CI workflow, dry-run package verification
|
|
62
67
|
|
|
63
68
|
## Install
|
|
@@ -146,9 +151,13 @@ The project root is auto-detected by walking up from the current directory and s
|
|
|
146
151
|
Config merge priority:
|
|
147
152
|
|
|
148
153
|
```text
|
|
149
|
-
user < project
|
|
154
|
+
user < project for ordinary presentation/UX settings
|
|
150
155
|
```
|
|
151
156
|
|
|
157
|
+
Trust-boundary exception: project config is intentionally not trusted for sensitive execution controls. Project-level values such as `executeWorkers`, `asyncByDefault`, runtime mode/live-session inheritance, autonomy mode, `agents.disableBuiltins`, `agents.overrides`, `worktree.setupHook`, and `otlp.headers` are ignored with warnings. Set those in user config when you want to trust them explicitly.
|
|
158
|
+
|
|
159
|
+
Resource discovery trust boundary: project-local agents, teams, and workflows may add new names, but cannot shadow builtin or user resources with the same name.
|
|
160
|
+
|
|
152
161
|
Supported config:
|
|
153
162
|
|
|
154
163
|
```json
|
|
@@ -167,6 +176,13 @@ Supported config:
|
|
|
167
176
|
"review": ["review", "audit", "inspect"]
|
|
168
177
|
}
|
|
169
178
|
},
|
|
179
|
+
"runtime": {
|
|
180
|
+
"mode": "auto",
|
|
181
|
+
"groupJoin": "smart",
|
|
182
|
+
"groupJoinAckTimeoutMs": 300000,
|
|
183
|
+
"requirePlanApproval": false,
|
|
184
|
+
"completionMutationGuard": "warn"
|
|
185
|
+
},
|
|
170
186
|
"limits": {
|
|
171
187
|
"maxConcurrentWorkers": 3,
|
|
172
188
|
"maxTaskDepth": 2,
|
|
@@ -222,9 +238,13 @@ Supported config:
|
|
|
222
238
|
Safety notes:
|
|
223
239
|
|
|
224
240
|
- Foreground child-process runs continue in the Pi extension process and return control to chat immediately, so large workflows do not block the interactive session. They are interrupted on session shutdown. Use `async: true` only for intentionally detached runs that may survive the current session.
|
|
241
|
+
- Async completion notifications survive extension reload/auto-compaction: active runs are not marked consumed just because the notifier restarts, while stale owner-session callbacks are suppressed after session switches.
|
|
225
242
|
- Background `Agent`/`crew_agent` runs notify the parent session when they reach a terminal state; the parent can then call `get_subagent_result`/`crew_agent_result` and continue the original task.
|
|
226
243
|
- `tools.terminateOnForeground` is an opt-in power-user setting. When true, foreground `Agent`/`crew_agent` calls return with `terminate: true` after the child result is available, saving one follow-up LLM turn. Default is false so the assistant can still summarize raw worker output.
|
|
227
244
|
- Runtime state paths are treated as untrusted data: run ids, import bundles, artifact/transcript paths, mailbox files, and agent control/log files are validated with containment checks before reads or writes.
|
|
245
|
+
- `runtime.completionMutationGuard` defaults to `warn`; set `off` to disable or `fail` to fail implementation-style tasks that report success without observed mutation tool calls.
|
|
246
|
+
- Group-join result messages use normal mailbox delivery and normal `ack-message`; missing acknowledgements never block run completion, and duplicate delivery attempts reuse the same request id/message instead of appending spam.
|
|
247
|
+
- Common secret patterns (`token=`, `apiKey=`, `Authorization: Bearer ...`, private keys, etc.) are redacted before durable logs/events/mailbox/artifacts/metrics/diagnostics are written.
|
|
228
248
|
- `observability.enabled` defaults to true for in-memory metrics and heartbeat watching. Metric JSONL snapshots are gated by `telemetry.enabled`; set `telemetry.enabled=false` to opt out of local telemetry files.
|
|
229
249
|
- `reliability.autoRetry` and `reliability.autoRecover` default to false. Enabling retry may execute an idempotent task more than once; each attempt is recorded in `task.attempts`, and exhausted retries append a deadletter entry.
|
|
230
250
|
- `otlp.enabled` defaults to false. Configure `otlp.endpoint` only when you want to push metrics to an OTLP HTTP collector.
|
|
@@ -320,7 +340,7 @@ Supported actions:
|
|
|
320
340
|
| `config` | Show/update config |
|
|
321
341
|
| `init` | Create project `.pi` layout and update `.gitignore` |
|
|
322
342
|
| `autonomy` | Show/update autonomous delegation settings |
|
|
323
|
-
| `api` | Safe interop for run/task/event/heartbeat/claim/mailbox state |
|
|
343
|
+
| `api` | Safe interop for run/task/event/heartbeat/claim/mailbox state, including plan approval/cancel operations |
|
|
324
344
|
| `help` | Show help text |
|
|
325
345
|
|
|
326
346
|
## Example tool calls
|
|
@@ -358,6 +378,30 @@ Run with worktrees:
|
|
|
358
378
|
}
|
|
359
379
|
```
|
|
360
380
|
|
|
381
|
+
Require explicit approval after the adaptive planner writes a plan artifact and before mutating workers run:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"action": "run",
|
|
386
|
+
"team": "implementation",
|
|
387
|
+
"workflow": "implementation",
|
|
388
|
+
"goal": "Refactor auth and update tests",
|
|
389
|
+
"config": {
|
|
390
|
+
"runtime": { "requirePlanApproval": true }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Approve or cancel the pending plan:
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
{
|
|
399
|
+
"action": "api",
|
|
400
|
+
"runId": "team_...",
|
|
401
|
+
"config": { "operation": "approve-plan" }
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
361
405
|
Inspect a run:
|
|
362
406
|
|
|
363
407
|
```json
|
|
@@ -435,9 +479,11 @@ Manual slash commands are ops/debug controls. Autonomous tool use via policy/rec
|
|
|
435
479
|
/team-api team_... send-message taskId=task_... direction=inbox to=worker body="task scoped message"
|
|
436
480
|
/team-api team_... read-mailbox direction=outbox
|
|
437
481
|
/team-api team_... read-mailbox taskId=task_... direction=inbox
|
|
438
|
-
/team-api team_... ack-message messageId=msg_...
|
|
482
|
+
/team-api team_... ack-message messageId=msg_... # also acknowledges group-join result messages
|
|
439
483
|
/team-api team_... read-delivery
|
|
440
484
|
/team-api team_... validate-mailbox repair=true
|
|
485
|
+
/team-api team_... approve-plan
|
|
486
|
+
/team-api team_... cancel-plan
|
|
441
487
|
```
|
|
442
488
|
|
|
443
489
|
Use `/team-metrics` for a current metrics snapshot. The optional argument is a glob-style metric filter:
|
package/docs/usage.md
CHANGED
|
@@ -29,6 +29,13 @@ Supported fields:
|
|
|
29
29
|
"preferAsyncForLongTasks": false,
|
|
30
30
|
"allowWorktreeSuggestion": true
|
|
31
31
|
},
|
|
32
|
+
"runtime": {
|
|
33
|
+
"mode": "auto",
|
|
34
|
+
"groupJoin": "smart",
|
|
35
|
+
"groupJoinAckTimeoutMs": 300000,
|
|
36
|
+
"completionMutationGuard": "warn",
|
|
37
|
+
"requirePlanApproval": false
|
|
38
|
+
},
|
|
32
39
|
"ui": {
|
|
33
40
|
"widgetPlacement": "aboveEditor",
|
|
34
41
|
"widgetMaxLines": 8,
|
|
@@ -113,6 +120,10 @@ Background `Agent`/`crew_agent` subagents wake the parent Pi session when they c
|
|
|
113
120
|
|
|
114
121
|
State paths are validated before read/write operations. Run ids, imported bundles, artifact and transcript references, mailbox files, and agent control/log files must stay inside their expected `.crew` roots and symlink escapes are rejected. Read-only mailbox APIs return default state without creating mailbox files when no messages exist.
|
|
115
122
|
|
|
123
|
+
Group-join result delivery uses the normal outbox mailbox and normal `/team-api ... ack-message`. `runtime.groupJoinAckTimeoutMs` only emits observability (`agent.group_join.ack_timeout`) and does not block run completion.
|
|
124
|
+
|
|
125
|
+
`runtime.completionMutationGuard` defaults to `warn`. Use `off` to disable or `fail` to fail implementation-style workers that complete without observed mutation tool calls.
|
|
126
|
+
|
|
116
127
|
## Worktree mode
|
|
117
128
|
|
|
118
129
|
```json
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -68,7 +68,10 @@
|
|
|
68
68
|
"graceTurns": { "type": "integer", "minimum": 1 },
|
|
69
69
|
"inheritContext": { "type": "boolean" },
|
|
70
70
|
"promptMode": { "type": "string", "enum": ["replace", "append"] },
|
|
71
|
-
"groupJoin": { "type": "string", "enum": ["off", "group", "smart"] }
|
|
71
|
+
"groupJoin": { "type": "string", "enum": ["off", "group", "smart"] },
|
|
72
|
+
"groupJoinAckTimeoutMs": { "type": "integer", "minimum": 1 },
|
|
73
|
+
"requirePlanApproval": { "type": "boolean" },
|
|
74
|
+
"completionMutationGuard": { "type": "string", "enum": ["off", "warn", "fail"] }
|
|
72
75
|
}
|
|
73
76
|
},
|
|
74
77
|
"control": {
|
|
@@ -95,7 +95,7 @@ export function discoverAgents(cwd: string): AgentDiscoveryResult {
|
|
|
95
95
|
|
|
96
96
|
export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
|
|
97
97
|
const byName = new Map<string, AgentConfig>();
|
|
98
|
-
for (const agent of [...discovery.
|
|
98
|
+
for (const agent of [...discovery.project, ...discovery.builtin, ...discovery.user]) {
|
|
99
99
|
byName.set(agent.name.toLowerCase(), agent);
|
|
100
100
|
}
|
|
101
101
|
return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
|
package/src/config/config.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface CrewLimitsConfig {
|
|
|
30
30
|
|
|
31
31
|
export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
|
|
32
32
|
|
|
33
|
+
export type CompletionMutationGuardMode = "off" | "warn" | "fail";
|
|
34
|
+
|
|
33
35
|
export interface CrewRuntimeConfig {
|
|
34
36
|
mode?: CrewRuntimeMode;
|
|
35
37
|
preferLiveSession?: boolean;
|
|
@@ -39,6 +41,9 @@ export interface CrewRuntimeConfig {
|
|
|
39
41
|
inheritContext?: boolean;
|
|
40
42
|
promptMode?: "replace" | "append";
|
|
41
43
|
groupJoin?: "off" | "group" | "smart";
|
|
44
|
+
groupJoinAckTimeoutMs?: number;
|
|
45
|
+
requirePlanApproval?: boolean;
|
|
46
|
+
completionMutationGuard?: CompletionMutationGuardMode;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export interface CrewControlConfig {
|
|
@@ -206,6 +211,82 @@ function validateConfigWithWarnings(raw: unknown): string[] {
|
|
|
206
211
|
return [];
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
function projectOverrideWarning(projectPath: string, dottedPath: string): string {
|
|
215
|
+
return `${projectPath}: project-level sensitive config '${dottedPath}' is ignored; set it in user config to trust it explicitly`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeProjectConfig(projectPath: string, userConfig: PiTeamsConfig, config: PiTeamsConfig): ConfigValidationResult {
|
|
219
|
+
const sanitized: PiTeamsConfig = { ...config };
|
|
220
|
+
const warnings: string[] = [];
|
|
221
|
+
const dropTopLevel = (key: keyof PiTeamsConfig): void => {
|
|
222
|
+
if (config[key] === undefined) return;
|
|
223
|
+
delete sanitized[key];
|
|
224
|
+
warnings.push(projectOverrideWarning(projectPath, String(key)));
|
|
225
|
+
};
|
|
226
|
+
dropTopLevel("executeWorkers");
|
|
227
|
+
dropTopLevel("asyncByDefault");
|
|
228
|
+
dropTopLevel("requireCleanWorktreeLeader");
|
|
229
|
+
if (config.runtime) {
|
|
230
|
+
const runtime = { ...config.runtime };
|
|
231
|
+
for (const key of ["mode", "preferLiveSession", "allowChildProcessFallback", "inheritContext"] as const) {
|
|
232
|
+
if (runtime[key] !== undefined) {
|
|
233
|
+
delete runtime[key];
|
|
234
|
+
warnings.push(projectOverrideWarning(projectPath, `runtime.${key}`));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (runtime.requirePlanApproval === false) {
|
|
238
|
+
delete runtime.requirePlanApproval;
|
|
239
|
+
warnings.push(projectOverrideWarning(projectPath, "runtime.requirePlanApproval"));
|
|
240
|
+
}
|
|
241
|
+
sanitized.runtime = Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
|
|
242
|
+
}
|
|
243
|
+
if (config.autonomous) {
|
|
244
|
+
const autonomous = { ...config.autonomous };
|
|
245
|
+
for (const key of ["profile", "enabled", "injectPolicy", "preferAsyncForLongTasks", "allowWorktreeSuggestion"] as const) {
|
|
246
|
+
if (autonomous[key] !== undefined) {
|
|
247
|
+
delete autonomous[key];
|
|
248
|
+
warnings.push(projectOverrideWarning(projectPath, `autonomous.${key}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
sanitized.autonomous = Object.values(autonomous).some((entry) => entry !== undefined) ? autonomous : undefined;
|
|
252
|
+
}
|
|
253
|
+
if (config.worktree?.setupHook !== undefined) {
|
|
254
|
+
sanitized.worktree = { ...config.worktree, setupHook: undefined };
|
|
255
|
+
if (!Object.values(sanitized.worktree).some((entry) => entry !== undefined)) sanitized.worktree = undefined;
|
|
256
|
+
warnings.push(projectOverrideWarning(projectPath, "worktree.setupHook"));
|
|
257
|
+
}
|
|
258
|
+
if (config.otlp?.headers !== undefined) {
|
|
259
|
+
sanitized.otlp = { ...config.otlp, headers: undefined };
|
|
260
|
+
if (!Object.values(sanitized.otlp).some((entry) => entry !== undefined)) sanitized.otlp = undefined;
|
|
261
|
+
warnings.push(projectOverrideWarning(projectPath, "otlp.headers"));
|
|
262
|
+
}
|
|
263
|
+
if (config.agents?.disableBuiltins !== undefined || config.agents?.overrides !== undefined) {
|
|
264
|
+
const agents = { ...config.agents };
|
|
265
|
+
if (agents.disableBuiltins !== undefined) {
|
|
266
|
+
delete agents.disableBuiltins;
|
|
267
|
+
warnings.push(projectOverrideWarning(projectPath, "agents.disableBuiltins"));
|
|
268
|
+
}
|
|
269
|
+
if (agents.overrides !== undefined) {
|
|
270
|
+
delete agents.overrides;
|
|
271
|
+
warnings.push(projectOverrideWarning(projectPath, "agents.overrides"));
|
|
272
|
+
}
|
|
273
|
+
sanitized.agents = Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
|
|
274
|
+
}
|
|
275
|
+
if (config.tools?.enableSteer !== undefined || config.tools?.terminateOnForeground !== undefined) {
|
|
276
|
+
const tools = { ...config.tools };
|
|
277
|
+
if (tools.enableSteer !== undefined) {
|
|
278
|
+
delete tools.enableSteer;
|
|
279
|
+
warnings.push(projectOverrideWarning(projectPath, "tools.enableSteer"));
|
|
280
|
+
}
|
|
281
|
+
if (tools.terminateOnForeground !== undefined) {
|
|
282
|
+
delete tools.terminateOnForeground;
|
|
283
|
+
warnings.push(projectOverrideWarning(projectPath, "tools.terminateOnForeground"));
|
|
284
|
+
}
|
|
285
|
+
sanitized.tools = Object.values(tools).some((entry) => entry !== undefined) ? tools : undefined;
|
|
286
|
+
}
|
|
287
|
+
return { config: sanitized, warnings };
|
|
288
|
+
}
|
|
289
|
+
|
|
209
290
|
function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfig {
|
|
210
291
|
const merged: PiTeamsConfig = { ...base, ...withoutUndefined(override as Record<string, unknown>) };
|
|
211
292
|
if (base.autonomous || override.autonomous) {
|
|
@@ -416,6 +497,9 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
416
497
|
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext),
|
|
417
498
|
promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
|
|
418
499
|
groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
|
|
500
|
+
groupJoinAckTimeoutMs: parsePositiveInteger(obj.groupJoinAckTimeoutMs, 86_400_000),
|
|
501
|
+
requirePlanApproval: parseWithSchema(Type.Boolean(), obj.requirePlanApproval),
|
|
502
|
+
completionMutationGuard: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("fail")]), obj.completionMutationGuard),
|
|
419
503
|
};
|
|
420
504
|
return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
|
|
421
505
|
}
|
|
@@ -639,8 +723,9 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
639
723
|
if (cwd) {
|
|
640
724
|
const projectPath = projectConfigPath(cwd);
|
|
641
725
|
const projectConfig = parseConfigWithWarnings(readConfigRecord(projectPath));
|
|
642
|
-
|
|
643
|
-
|
|
726
|
+
const projectSafeConfig = sanitizeProjectConfig(projectPath, config, projectConfig.config);
|
|
727
|
+
warnings.push(...projectConfig.warnings.map((warning) => `${projectPath}: ${warning}`), ...projectSafeConfig.warnings);
|
|
728
|
+
config = mergeConfig(config, projectSafeConfig.config);
|
|
644
729
|
}
|
|
645
730
|
return { path: filePath, paths, config, warnings: warnings.length > 0 ? warnings : undefined };
|
|
646
731
|
} catch (error) {
|
|
@@ -8,6 +8,13 @@ import { listRuns } from "./run-index.ts";
|
|
|
8
8
|
export interface AsyncNotifierState {
|
|
9
9
|
seenFinishedRunIds: Set<string>;
|
|
10
10
|
interval?: ReturnType<typeof setInterval>;
|
|
11
|
+
generation?: number;
|
|
12
|
+
lastStoppedAtMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AsyncNotifierOptions {
|
|
16
|
+
generation?: number;
|
|
17
|
+
isCurrent?: (generation: number) => boolean;
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
function isFinished(status: string): boolean {
|
|
@@ -18,6 +25,12 @@ function isAsyncTerminalEvent(event: TeamEvent): boolean {
|
|
|
18
25
|
return event.type === "async.completed" || event.type === "async.failed" || event.type === "async.died";
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
function timeMs(value: string | undefined): number | undefined {
|
|
29
|
+
if (!value) return undefined;
|
|
30
|
+
const parsed = new Date(value).getTime();
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
function latestEventAgeMs(events: TeamEvent[], now = Date.now()): number {
|
|
22
35
|
const latest = events.at(-1);
|
|
23
36
|
if (!latest) return Number.POSITIVE_INFINITY;
|
|
@@ -38,14 +51,21 @@ export function markDeadAsyncRunIfNeeded(run: TeamRunManifest, now = Date.now(),
|
|
|
38
51
|
return failed;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifierState, intervalMs = 5000): void {
|
|
54
|
+
export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifierState, intervalMs = 5000, options: AsyncNotifierOptions = {}): void {
|
|
42
55
|
if (state.interval) clearInterval(state.interval);
|
|
56
|
+
const generation = options.generation ?? ((state.generation ?? 0) + 1);
|
|
57
|
+
state.generation = generation;
|
|
58
|
+
const startedAtMs = Date.now();
|
|
59
|
+
const staleBeforeMs = state.lastStoppedAtMs ?? startedAtMs;
|
|
43
60
|
for (const run of listRuns(ctx.cwd)) {
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
61
|
+
// Suppress only terminal runs that were already finished before this owner
|
|
62
|
+
// session (or before the previous session switch). Active runs must remain
|
|
63
|
+
// un-seen so completions during auto-compaction/session restart are delivered.
|
|
64
|
+
const updatedAtMs = timeMs(run.updatedAt) ?? 0;
|
|
65
|
+
if (isFinished(run.status) && updatedAtMs < staleBeforeMs) state.seenFinishedRunIds.add(run.runId);
|
|
47
66
|
}
|
|
48
67
|
state.interval = setInterval(() => {
|
|
68
|
+
if (options.isCurrent && !options.isCurrent(generation)) return;
|
|
49
69
|
try {
|
|
50
70
|
for (const run of listRuns(ctx.cwd).slice(0, 20)) {
|
|
51
71
|
const current = markDeadAsyncRunIfNeeded(run) ?? run;
|
|
@@ -64,4 +84,6 @@ export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifie
|
|
|
64
84
|
export function stopAsyncRunNotifier(state: AsyncNotifierState): void {
|
|
65
85
|
if (state.interval) clearInterval(state.interval);
|
|
66
86
|
state.interval = undefined;
|
|
87
|
+
state.generation = (state.generation ?? 0) + 1;
|
|
88
|
+
state.lastStoppedAtMs = Date.now();
|
|
67
89
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { NotificationDescriptor } from "./notification-router.ts";
|
|
4
|
-
import { redactSecrets } from "../
|
|
4
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
5
5
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
6
|
|
|
7
7
|
export interface NotificationSink {
|
|
@@ -53,6 +53,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
53
53
|
}
|
|
54
54
|
const notifierState: AsyncNotifierState = { seenFinishedRunIds: new Set() };
|
|
55
55
|
let currentCtx: ExtensionContext | undefined;
|
|
56
|
+
let sessionGeneration = 0;
|
|
56
57
|
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
57
58
|
let cleanedUp = false;
|
|
58
59
|
let manifestCache = createManifestCache(process.cwd());
|
|
@@ -153,6 +154,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
153
154
|
sendFollowUp(pi, [notification.title, notification.body].filter((line): line is string => Boolean(line)).join("\n"));
|
|
154
155
|
}
|
|
155
156
|
};
|
|
157
|
+
const captureSessionGeneration = (): number => sessionGeneration;
|
|
158
|
+
const isOwnerSessionCurrent = (ownerGeneration: number | undefined): boolean => !cleanedUp && (ownerGeneration === undefined || ownerGeneration === sessionGeneration);
|
|
159
|
+
const isContextCurrent = (ctx: ExtensionContext, ownerGeneration: number): boolean => !cleanedUp && currentCtx === ctx && sessionGeneration === ownerGeneration;
|
|
156
160
|
const subagentManager = new SubagentManager(
|
|
157
161
|
4,
|
|
158
162
|
(record) => {
|
|
@@ -170,6 +174,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
170
174
|
});
|
|
171
175
|
}
|
|
172
176
|
if (!record.background || record.resultConsumed) return;
|
|
177
|
+
if (!isOwnerSessionCurrent(record.ownerSessionGeneration)) return;
|
|
173
178
|
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "blocked" || record.status === "error") {
|
|
174
179
|
const metadata = JSON.stringify({ id: record.id, status: record.status, type: record.type, runId: record.runId, description: record.description }, null, 2);
|
|
175
180
|
const joinInstruction = [
|
|
@@ -186,6 +191,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
186
191
|
},
|
|
187
192
|
1000,
|
|
188
193
|
(event, payload) => {
|
|
194
|
+
const ownerGeneration = typeof payload.ownerSessionGeneration === "number" ? payload.ownerSessionGeneration : undefined;
|
|
195
|
+
if (ownerGeneration !== undefined && !isOwnerSessionCurrent(ownerGeneration)) return;
|
|
189
196
|
if (event === "subagent.stuck-blocked") {
|
|
190
197
|
const id = typeof payload.id === "string" ? payload.id : "unknown";
|
|
191
198
|
const runId = typeof payload.runId === "string" ? payload.runId : "unknown";
|
|
@@ -233,6 +240,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
233
240
|
});
|
|
234
241
|
};
|
|
235
242
|
const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
|
|
243
|
+
const ownerGeneration = captureSessionGeneration();
|
|
236
244
|
const controller = new AbortController();
|
|
237
245
|
foregroundControllers.add(controller);
|
|
238
246
|
if (ctx.hasUI) {
|
|
@@ -251,15 +259,16 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
251
259
|
logInternalError("register.foreground-run-failure", statusError, `runId=${runId}`);
|
|
252
260
|
}
|
|
253
261
|
}
|
|
254
|
-
ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
|
|
262
|
+
if (isContextCurrent(ctx, ownerGeneration)) ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
|
|
255
263
|
})
|
|
256
264
|
.finally(() => {
|
|
257
265
|
foregroundControllers.delete(controller);
|
|
258
|
-
|
|
266
|
+
const ownerCurrent = isContextCurrent(ctx, ownerGeneration);
|
|
267
|
+
if (ownerCurrent && ctx.hasUI) {
|
|
259
268
|
setWorkingIndicator(ctx);
|
|
260
269
|
ctx.ui.setWorkingMessage();
|
|
261
270
|
}
|
|
262
|
-
if (runId) {
|
|
271
|
+
if (ownerCurrent && runId) {
|
|
263
272
|
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
264
273
|
const status = loaded?.manifest.status ?? "finished";
|
|
265
274
|
const level = status === "failed" || status === "blocked" ? "error" : status === "cancelled" ? "warning" : "info";
|
|
@@ -287,7 +296,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
287
296
|
});
|
|
288
297
|
}
|
|
289
298
|
}
|
|
290
|
-
if (currentCtx) {
|
|
299
|
+
if (ownerCurrent && currentCtx) {
|
|
291
300
|
const config = loadConfig(currentCtx.cwd).config.ui;
|
|
292
301
|
updateCrewWidget(currentCtx, widgetState, config, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd));
|
|
293
302
|
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, getManifestCache(currentCtx.cwd), getRunSnapshotCache(currentCtx.cwd), currentCtx, widgetState.notificationCount ?? 0);
|
|
@@ -328,6 +337,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
328
337
|
notificationSink = undefined;
|
|
329
338
|
rpcHandle?.unsubscribe();
|
|
330
339
|
rpcHandle = undefined;
|
|
340
|
+
sessionGeneration += 1;
|
|
331
341
|
currentCtx = undefined;
|
|
332
342
|
if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
|
|
333
343
|
};
|
|
@@ -337,6 +347,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
337
347
|
runArtifactCleanup(ctx.cwd);
|
|
338
348
|
time("register.session-start");
|
|
339
349
|
cleanedUp = false;
|
|
350
|
+
sessionGeneration++;
|
|
351
|
+
const ownerGeneration = sessionGeneration;
|
|
340
352
|
currentCtx = ctx;
|
|
341
353
|
if (widgetState.interval) clearInterval(widgetState.interval);
|
|
342
354
|
widgetState.interval = undefined;
|
|
@@ -346,7 +358,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
346
358
|
configureNotifications(ctx);
|
|
347
359
|
configureObservability(ctx);
|
|
348
360
|
registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
|
|
349
|
-
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs);
|
|
361
|
+
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, { generation: ownerGeneration, isCurrent: (generation) => generation === sessionGeneration && currentCtx === ctx && !cleanedUp });
|
|
350
362
|
const cache = getManifestCache(ctx.cwd);
|
|
351
363
|
updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd));
|
|
352
364
|
updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd), ctx, widgetState.notificationCount ?? 0);
|
|
@@ -395,7 +407,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
395
407
|
onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
|
|
396
408
|
});
|
|
397
409
|
});
|
|
398
|
-
pi.on("session_before_switch", () =>
|
|
410
|
+
pi.on("session_before_switch", () => {
|
|
411
|
+
sessionGeneration++;
|
|
412
|
+
stopAsyncRunNotifier(notifierState);
|
|
413
|
+
stopSessionBoundSubagents();
|
|
414
|
+
});
|
|
399
415
|
pi.on("session_shutdown", () => cleanupRuntime());
|
|
400
416
|
|
|
401
417
|
registerCompactionGuard(pi, { foregroundControllers });
|
|
@@ -417,7 +433,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
417
433
|
});
|
|
418
434
|
|
|
419
435
|
registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState });
|
|
420
|
-
registerSubagentTools(pi, subagentManager);
|
|
436
|
+
registerSubagentTools(pi, subagentManager, { ownerSessionGeneration: captureSessionGeneration });
|
|
421
437
|
time("register.tools");
|
|
422
438
|
|
|
423
439
|
registerTeamCommands(pi, { startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, dismissNotifications: () => {
|
|
@@ -8,7 +8,11 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
8
8
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
9
9
|
import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export interface SubagentToolRegistrationOptions {
|
|
12
|
+
ownerSessionGeneration?: () => number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerSubagentTools(pi: ExtensionAPI, subagentManager: SubagentManager, options: SubagentToolRegistrationOptions = {}): void {
|
|
12
16
|
const agentTool: ToolDefinition = {
|
|
13
17
|
name: "Agent",
|
|
14
18
|
label: "Agent",
|
|
@@ -31,11 +35,12 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
31
35
|
const currentRole = currentCrewRole();
|
|
32
36
|
const permission = checkSubagentSpawnPermission(currentRole);
|
|
33
37
|
if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
38
|
+
const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
|
|
39
|
+
spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
|
|
40
|
+
if (!spawnOptions.prompt.trim()) return subagentToolResult("Agent requires prompt.", {}, true);
|
|
41
|
+
const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
|
|
42
|
+
const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
|
|
43
|
+
if (spawnOptions.background || record.status === "queued") {
|
|
39
44
|
// Phase 1.1a: Terminate turn for background queued — no LLM follow-up needed.
|
|
40
45
|
// Phase 1.6: Record was terminated for telemetry.
|
|
41
46
|
record.terminated = true;
|