pi-crew 0.9.15 → 0.9.16
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 +64 -0
- package/package.json +1 -1
- package/src/runtime/crew-agent-records.ts +55 -0
- package/src/runtime/subagent-manager.ts +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.9.16] — cancel wipes the trace (2026-06-29)
|
|
4
|
+
|
|
5
|
+
User-driven refinement of the cancellation policy. Previously, cancelled / stopped subagents and crew-agent records were preserved on disk (`agent_*.json` files in `.crew/state/subagents/`, plus `agents.json` + per-task `status.json` in the run state directory) and continued to appear in the UI dashboard / widget / transcript viewer. The user explicitly asked: *"một khi đã cancel thì phải xóa luôn dấu vết về subagent đó chứ để lại làm gì nữa"* — once cancelled, the trace must be wiped. This release implements that policy.
|
|
6
|
+
|
|
7
|
+
### Changed — terminal-status deletion
|
|
8
|
+
|
|
9
|
+
- **Subagent records** (`.crew/state/subagents/<id>.json`) — added `removePersistedSubagentRecord(cwd, id)` in `src/runtime/subagent-manager.ts`. On terminal status `cancelled` / `stopped` / `terminated:true`, the persisted file is **deleted immediately** (no linger). The file is saved first so any concurrent reader sees the final status, then `unlink`'d within microseconds — the next widget/dashboard tick sees no entry.
|
|
10
|
+
|
|
11
|
+
- **Crew-agent records** (per-run `agents.json` index + per-task `status.json`) — added `removeCrewAgent(manifest, taskId)` in `src/runtime/crew-agent-records.ts`. `upsertCrewAgent()` now checks `shouldDeleteCrewAgentOnTerminalStatus(record)` and dispatches to `removeCrewAgent` instead of `saveCrewAgents` for `cancelled` / `stopped` records. Both the index entry AND the per-task status.json are wiped.
|
|
12
|
+
|
|
13
|
+
### What is preserved vs wiped
|
|
14
|
+
|
|
15
|
+
| Status | Persisted? | Rationale |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `completed` | ✅ yes | Successful work — audit value |
|
|
18
|
+
| `failed` | ✅ yes | Agent errored — debugging value |
|
|
19
|
+
| `error` | ✅ yes | Unexpected exception — debugging value |
|
|
20
|
+
| `cancelled` | ❌ **wiped** | User-initiated cancellation — no trace |
|
|
21
|
+
| `stopped` | ❌ **wiped** | External abort — no trace |
|
|
22
|
+
| `terminated: true` (any status) | ❌ **wiped** | Any termination event — no trace |
|
|
23
|
+
|
|
24
|
+
### In-memory handles
|
|
25
|
+
|
|
26
|
+
`terminateLiveAgent` in `src/runtime/live-agent-manager.ts:169` already deletes from the `liveAgents` Map immediately after abort — no change needed. The `evictStaleLiveAgentHandles` fallback (10-min linger for orphan terminal handles) remains as a safety net.
|
|
27
|
+
|
|
28
|
+
### New helper predicates
|
|
29
|
+
|
|
30
|
+
- `shouldDeleteOnTerminalStatus(record)` — for `.crew/state/subagents/` records
|
|
31
|
+
- `shouldDeleteCrewAgentOnTerminalStatus(record)` — for per-run `agents.json` records
|
|
32
|
+
|
|
33
|
+
Both exported for downstream callers and tests.
|
|
34
|
+
|
|
35
|
+
### Tests
|
|
36
|
+
|
|
37
|
+
- **NEW `test/unit/cancellation-trace-wipe.test.ts`** — 15 cases covering:
|
|
38
|
+
- `shouldDeleteOnTerminalStatus` / `shouldDeleteCrewAgentOnTerminalStatus` predicates for all 6 statuses
|
|
39
|
+
- `removePersistedSubagentRecord` happy path + ENOENT safe-fail
|
|
40
|
+
- `removeCrewAgent` removes from `agents.json` index AND per-task `status.json`
|
|
41
|
+
- `upsertCrewAgent` integration: cancelled → wiped, completed/failed → preserved
|
|
42
|
+
|
|
43
|
+
- **Existing tests still pass** — 118/118 across 9 test files (subagent-manager-cov, subagent-tools-integration, live-manager-cov, direct-agent-run, team-runner-merge, process-lifecycle, preflight-validator, topology-analyzer, cancellation-trace-wipe). 0 regressions.
|
|
44
|
+
|
|
45
|
+
### Verification
|
|
46
|
+
|
|
47
|
+
- `tsc --noEmit` → EXIT 0
|
|
48
|
+
- `node scripts/check-lazy-imports.mjs` → "All dynamic imports have `// LAZY:` marker"
|
|
49
|
+
- `npm test` (focused 9-file suite) → 118/118 pass
|
|
50
|
+
|
|
51
|
+
### Honesty discipline
|
|
52
|
+
|
|
53
|
+
- **Failed subagents are NOT wiped** — they may carry useful debugging info (error stack, partial output). Different from cancel. If the user later wants failed→wipe too, it's a 1-line predicate change.
|
|
54
|
+
- **No tombstone** — the cancellation event is in `events.jsonl` (the run's audit log) and `notifications/`. Disk-level agent files are intentionally gone after cancel.
|
|
55
|
+
- **Filesystem race window** is microseconds (save then unlink); under heavy concurrent access a reader might briefly see the final status before the file disappears. Acceptable trade-off for the "no trace" policy.
|
|
56
|
+
|
|
57
|
+
### Files touched
|
|
58
|
+
|
|
59
|
+
| File | Change |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `src/runtime/subagent-manager.ts` | +2 functions (`removePersistedSubagentRecord`, `shouldDeleteOnTerminalStatus`); modified `markStopped` + `runSubagent` finally block to wipe on cancel |
|
|
62
|
+
| `src/runtime/crew-agent-records.ts` | +2 functions (`removeCrewAgent`, `shouldDeleteCrewAgentOnTerminalStatus`); modified `upsertCrewAgent` to dispatch to remove on cancel |
|
|
63
|
+
| `test/unit/cancellation-trace-wipe.test.ts` | NEW (15 cases, 8.3 KB) |
|
|
64
|
+
| `CHANGELOG.md` | this entry |
|
|
65
|
+
| `package.json` | 0.9.15 → 0.9.16 |
|
|
66
|
+
|
|
3
67
|
## [v0.9.15] — workflow topology advisory (2026-06-29)
|
|
4
68
|
|
|
5
69
|
After a parallel-research assessment of v0.9.13 + v0.9.14 (`research-findings/pi-crew-performance-quality-assessment.md`, effectiveness evaluation at `.crew/research/pi-crew-effectiveness-evaluation.md`), a user-driven refinement showed that the original BLOCK-on-misuse design was too aggressive — **agents know their context better than the orchestrator**. This release ships an **advisory-only** topology classifier that prints measured-cost notes and proceeds either way. The agent (caller) decides.
|
package/package.json
CHANGED
|
@@ -206,9 +206,64 @@ export function saveCrewAgents(manifest: TeamRunManifest, records: CrewAgentReco
|
|
|
206
206
|
|
|
207
207
|
const TERMINAL_AGENT_STATUSES = new Set(["completed", "failed", "cancelled", "blocked"]);
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* User policy (v0.9.16): cancelled / stopped crew-agent records leave NO trace.
|
|
211
|
+
* `failed` records keep their audit trail (different from cancel — agent errored).
|
|
212
|
+
*/
|
|
213
|
+
export function shouldDeleteCrewAgentOnTerminalStatus(
|
|
214
|
+
record: Pick<CrewAgentRecord, "status">,
|
|
215
|
+
): boolean {
|
|
216
|
+
const s = record.status;
|
|
217
|
+
return s === "cancelled" || s === "stopped";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Remove a single crew-agent record from both `agents.json` (the index file)
|
|
222
|
+
* and the per-task `status.json`. Called on cancellation to wipe the trace.
|
|
223
|
+
* Safe-fail: missing files are treated as success (already removed).
|
|
224
|
+
*/
|
|
225
|
+
export function removeCrewAgent(
|
|
226
|
+
manifest: TeamRunManifest,
|
|
227
|
+
taskId: string,
|
|
228
|
+
): { removedIndex: boolean; removedStatus: boolean } {
|
|
229
|
+
let removedIndex = false;
|
|
230
|
+
let removedStatus = false;
|
|
231
|
+
// 1. Remove from agents.json index
|
|
232
|
+
try {
|
|
233
|
+
const existing = readCrewAgents(manifest);
|
|
234
|
+
const filtered = existing.filter((r) => r.taskId !== taskId);
|
|
235
|
+
if (filtered.length !== existing.length) {
|
|
236
|
+
saveCrewAgents(manifest, filtered);
|
|
237
|
+
removedIndex = true;
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// best-effort
|
|
241
|
+
}
|
|
242
|
+
// 2. Remove per-task status.json
|
|
243
|
+
try {
|
|
244
|
+
const statusPath = agentStatusPath(manifest, taskId);
|
|
245
|
+
if (fs.existsSync(statusPath)) {
|
|
246
|
+
fs.unlinkSync(statusPath);
|
|
247
|
+
removedStatus = true;
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// best-effort
|
|
251
|
+
}
|
|
252
|
+
return { removedIndex, removedStatus };
|
|
253
|
+
}
|
|
254
|
+
|
|
209
255
|
export function upsertCrewAgent(manifest: TeamRunManifest, record: CrewAgentRecord): void {
|
|
210
256
|
// Guard: skip if run state has been deleted (prune/forget/cleanup)
|
|
211
257
|
try { fs.statSync(manifest.stateRoot); } catch { return; }
|
|
258
|
+
// User policy (v0.9.16): cancelled / stopped crew agents leave NO trace.
|
|
259
|
+
// We delete BOTH the index entry (agents.json) and the per-task status.json
|
|
260
|
+
// immediately so the UI dashboard/widget never see the cancelled agent again.
|
|
261
|
+
if (shouldDeleteCrewAgentOnTerminalStatus(record)) {
|
|
262
|
+
removeCrewAgent(manifest, record.taskId);
|
|
263
|
+
// Invalidate the per-task reader cache so a subsequent read sees fresh state.
|
|
264
|
+
asyncAgentReaderCache.delete(agentsPath(manifest));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
212
267
|
// Read current state
|
|
213
268
|
const existing = readCrewAgents(manifest);
|
|
214
269
|
// Deduplicate by id: keep newer record when same id appears
|
|
@@ -114,6 +114,36 @@ export function savePersistedSubagentRecord(
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Delete the persisted subagent record from disk.
|
|
119
|
+
* Used when a subagent is cancelled or terminated — the user wants no trace.
|
|
120
|
+
* Safe-fail: if the file does not exist (already deleted), this is a no-op.
|
|
121
|
+
* Any other I/O error is logged but not propagated.
|
|
122
|
+
*/
|
|
123
|
+
export function removePersistedSubagentRecord(cwd: string, id: string): boolean {
|
|
124
|
+
try {
|
|
125
|
+
const filePath = persistedSubagentPath(cwd, id);
|
|
126
|
+
fs.unlinkSync(filePath);
|
|
127
|
+
return true;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
130
|
+
if (code === "ENOENT") return false;
|
|
131
|
+
logInternalError("subagent-manager.remove", error, `id=${id}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Predicate: should this record's persisted file be deleted on terminal status?
|
|
138
|
+
* Only abnormal terminations (cancelled / stopped / terminated-flag) are wiped.
|
|
139
|
+
* Successful runs (`completed`) and agent errors (`failed`, `error`) keep their audit trail.
|
|
140
|
+
*/
|
|
141
|
+
export function shouldDeleteOnTerminalStatus(record: SubagentRecord): boolean {
|
|
142
|
+
if (record.terminated === true) return true;
|
|
143
|
+
const s = record.status;
|
|
144
|
+
return s === "cancelled" || s === "stopped";
|
|
145
|
+
}
|
|
146
|
+
|
|
117
147
|
const ALLOWED_RECORD_FIELDS = new Set([
|
|
118
148
|
"id",
|
|
119
149
|
"agentId",
|
|
@@ -427,6 +457,12 @@ export class SubagentManager {
|
|
|
427
457
|
? Math.max(0, record.completedAt - record.startedAt)
|
|
428
458
|
: undefined;
|
|
429
459
|
savePersistedSubagentRecord(options.cwd, record);
|
|
460
|
+
// User policy (v0.9.16): cancelled / stopped subagents leave NO trace.
|
|
461
|
+
// Successful runs (`completed`) and agent errors (`failed`, `error`)
|
|
462
|
+
// keep their audit trail. See shouldDeleteOnTerminalStatus().
|
|
463
|
+
if (shouldDeleteOnTerminalStatus(record)) {
|
|
464
|
+
removePersistedSubagentRecord(options.cwd, record.id);
|
|
465
|
+
}
|
|
430
466
|
this.onComplete?.(record);
|
|
431
467
|
}
|
|
432
468
|
this.drainQueue();
|
|
@@ -451,10 +487,16 @@ export class SubagentManager {
|
|
|
451
487
|
|
|
452
488
|
private markStopped(record: SubagentRecord, reason?: string): void {
|
|
453
489
|
record.status = "stopped";
|
|
490
|
+
record.terminated = true;
|
|
454
491
|
record.completedAt = Date.now();
|
|
455
492
|
if (reason && !record.error) record.error = reason;
|
|
456
493
|
const cwd = this.cwdByRecord.get(record.id);
|
|
494
|
+
// User policy (v0.9.16): stopped subagents leave NO trace on disk.
|
|
495
|
+
// Save first (so any concurrent reader sees the final status), then
|
|
496
|
+
// unlink. The race window is microseconds and the file is gone before
|
|
497
|
+
// the next widget/dashboard tick.
|
|
457
498
|
if (cwd) savePersistedSubagentRecord(cwd, record);
|
|
499
|
+
if (cwd) removePersistedSubagentRecord(cwd, record.id);
|
|
458
500
|
}
|
|
459
501
|
|
|
460
502
|
private createRunSignal(id: string, signal?: AbortSignal): AbortSignal {
|