pi-crew 0.9.14 → 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.
@@ -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 {