@tmustier/pi-agent-teams 0.5.0 → 0.5.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 +11 -0
- package/extensions/teams/cleanup.ts +7 -1
- package/extensions/teams/leader.ts +60 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Automatic startup GC** — on session start, silently removes stale team directories older than 24h (fire-and-forget, never blocks). Reuses the existing `gcStaleTeamDirs()` with age + state checks. (Thanks **@RensTillmann** — #8, #30)
|
|
8
|
+
- **Exit cleanup of empty team dirs** — on session shutdown, deletes the session's own team directory if it has no tasks in any namespace, no active teammates (RPC or manual), and no attach claim from another session. (Thanks **@RensTillmann** — #8, #30)
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
- Added `excludeTeamIds` parameter to `gcStaleTeamDirs()` to prevent startup GC from removing the current session's team (important for resumed sessions older than 24h).
|
|
13
|
+
|
|
3
14
|
## 0.5.0
|
|
4
15
|
|
|
5
16
|
### Features
|
|
@@ -79,13 +79,15 @@ export async function gcStaleTeamDirs(opts: {
|
|
|
79
79
|
maxAgeMs: number;
|
|
80
80
|
repoCwd?: string;
|
|
81
81
|
dryRun?: boolean;
|
|
82
|
+
/** Team IDs to skip (e.g. the current session's team). */
|
|
83
|
+
excludeTeamIds?: ReadonlySet<string>;
|
|
82
84
|
}): Promise<{
|
|
83
85
|
scanned: number;
|
|
84
86
|
removed: string[];
|
|
85
87
|
skipped: Array<{ teamId: string; reason: string }>;
|
|
86
88
|
warnings: string[];
|
|
87
89
|
}> {
|
|
88
|
-
const { teamsRootDir, maxAgeMs, repoCwd, dryRun } = opts;
|
|
90
|
+
const { teamsRootDir, maxAgeMs, repoCwd, dryRun, excludeTeamIds } = opts;
|
|
89
91
|
const teamsRootAbs = path.resolve(teamsRootDir);
|
|
90
92
|
const removed: string[] = [];
|
|
91
93
|
const skipped: Array<{ teamId: string; reason: string }> = [];
|
|
@@ -103,6 +105,10 @@ export async function gcStaleTeamDirs(opts: {
|
|
|
103
105
|
const now = Date.now();
|
|
104
106
|
|
|
105
107
|
for (const teamId of teamEntries) {
|
|
108
|
+
if (excludeTeamIds?.has(teamId)) {
|
|
109
|
+
skipped.push({ teamId, reason: "excluded" });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
106
112
|
const teamDir = path.join(teamsRootAbs, teamId);
|
|
107
113
|
let stat: fs.Stats;
|
|
108
114
|
try {
|
|
@@ -9,8 +9,9 @@ import { TEAM_MAILBOX_NS, taskAssignmentPayload } from "./protocol.js";
|
|
|
9
9
|
import { createTask, listTasks, unassignTasksForAgent, updateTask, type TeamTask } from "./task-store.js";
|
|
10
10
|
import { TeammateRpc } from "./teammate-rpc.js";
|
|
11
11
|
import { ensureTeamConfig, loadTeamConfig, setMemberStatus, upsertMember, type TeamConfig } from "./team-config.js";
|
|
12
|
-
import { getTeamDir } from "./paths.js";
|
|
13
|
-
import { heartbeatTeamAttachClaim, releaseTeamAttachClaim } from "./team-attach-claim.js";
|
|
12
|
+
import { getTeamDir, getTeamsRootDir } from "./paths.js";
|
|
13
|
+
import { assessAttachClaimFreshness, heartbeatTeamAttachClaim, readTeamAttachClaim, releaseTeamAttachClaim } from "./team-attach-claim.js";
|
|
14
|
+
import { cleanupTeamDir, gcStaleTeamDirs } from "./cleanup.js";
|
|
14
15
|
import { ensureWorktreeCwd, cleanupWorktrees } from "./worktree.js";
|
|
15
16
|
import { ActivityTracker, TranscriptTracker } from "./activity-tracker.js";
|
|
16
17
|
import { openInteractiveWidget } from "./teams-panel.js";
|
|
@@ -106,6 +107,29 @@ async function createSessionForTeammate(
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/** Check if a team dir has any task files across all task-list namespaces. */
|
|
111
|
+
async function teamDirHasAnyTasks(teamDir: string): Promise<boolean> {
|
|
112
|
+
const tasksDir = path.join(teamDir, "tasks");
|
|
113
|
+
let taskListDirs: string[];
|
|
114
|
+
try {
|
|
115
|
+
taskListDirs = await fs.promises.readdir(tasksDir);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
for (const listDir of taskListDirs) {
|
|
120
|
+
const listPath = path.join(tasksDir, listDir);
|
|
121
|
+
try {
|
|
122
|
+
const stat = await fs.promises.stat(listPath);
|
|
123
|
+
if (!stat.isDirectory()) continue;
|
|
124
|
+
const files = await fs.promises.readdir(listPath);
|
|
125
|
+
if (files.some((f) => f.endsWith(".json") && !f.startsWith("."))) return true;
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
109
133
|
// Message parsers are shared with the worker implementation.
|
|
110
134
|
export function runLeader(pi: ExtensionAPI): void {
|
|
111
135
|
const teammates = new Map<string, TeammateRpc>();
|
|
@@ -701,6 +725,15 @@ export function runLeader(pi: ExtensionAPI): void {
|
|
|
701
725
|
style,
|
|
702
726
|
});
|
|
703
727
|
|
|
728
|
+
// Startup GC: silently remove stale team directories from previous sessions (24h age floor).
|
|
729
|
+
void gcStaleTeamDirs({
|
|
730
|
+
teamsRootDir: getTeamsRootDir(),
|
|
731
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
732
|
+
excludeTeamIds: new Set([currentTeamId]),
|
|
733
|
+
}).catch(() => {
|
|
734
|
+
// Best-effort; never block the session.
|
|
735
|
+
});
|
|
736
|
+
|
|
704
737
|
await refreshTasks();
|
|
705
738
|
renderWidget();
|
|
706
739
|
|
|
@@ -804,6 +837,7 @@ export function runLeader(pi: ExtensionAPI): void {
|
|
|
804
837
|
if (!currentCtx) return;
|
|
805
838
|
await releaseActiveAttachClaim(currentCtx);
|
|
806
839
|
stopLoops();
|
|
840
|
+
const hadTeammates = teammates.size > 0;
|
|
807
841
|
const strings = getTeamsStrings(style);
|
|
808
842
|
await stopAllTeammates(currentCtx, `The ${strings.teamNoun} is over`);
|
|
809
843
|
|
|
@@ -817,6 +851,30 @@ export function runLeader(pi: ExtensionAPI): void {
|
|
|
817
851
|
} catch {
|
|
818
852
|
// Best-effort — don't block shutdown.
|
|
819
853
|
}
|
|
854
|
+
|
|
855
|
+
// Exit cleanup: delete own team directory if it's empty.
|
|
856
|
+
// Conservative: only if no RPC teammates were active, no online workers in
|
|
857
|
+
// config (manual/tmux), no tasks in ANY namespace, and no fresh attach claim.
|
|
858
|
+
// (Dirs with completed tasks are left for the 24h startup GC — intentionally
|
|
859
|
+
// asymmetric for safety.)
|
|
860
|
+
if (!hadTeammates) {
|
|
861
|
+
try {
|
|
862
|
+
const claim = await readTeamAttachClaim(teamDir);
|
|
863
|
+
const claimIsLive = claim !== null && !assessAttachClaimFreshness(claim).isStale;
|
|
864
|
+
if (claimIsLive) {
|
|
865
|
+
// Another session is using this team — don't delete.
|
|
866
|
+
} else {
|
|
867
|
+
// Also check config for online non-lead members (manual/tmux workers).
|
|
868
|
+
const cfg = await loadTeamConfig(teamDir);
|
|
869
|
+
const hasOnlineWorkers = cfg?.members.some((m) => m.role !== "lead" && m.status === "online") ?? false;
|
|
870
|
+
if (!hasOnlineWorkers && !(await teamDirHasAnyTasks(teamDir))) {
|
|
871
|
+
await cleanupTeamDir(getTeamsRootDir(), teamDir);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
// Best-effort; never block shutdown.
|
|
876
|
+
}
|
|
877
|
+
}
|
|
820
878
|
}
|
|
821
879
|
});
|
|
822
880
|
|