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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -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 {