mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
feat(agent-cases): tag agent-spawned case dirs and sweep their leftovers
A long orchestration creates one case directory per worker and deleting the sessions never removed them, so ~/codeman-cases accumulated scratch folders that were indistinguishable from real projects. They are now labelled and have a cleanup path. - src/agent-case-marker.ts: a case dir quick-start CREATES for an agent-driven spawn gets a .codeman-agent-case.json marker (when, by whom, parent session, mode). Only the create branch writes it, so a linked case, a cloned repo or any pre-existing path is never labelled; reading is total, so a malformed marker means "not agent-created" rather than a half-trusted entry. - The signal is the new X-Codeman-Agent-Origin header the skill preamble sets on its shared curl (preamble bumped to 1.22.0), or an agentOrigin body field, falling back to a resolved parentSessionId so a worker spawned by a stale skill copy is still labelled. - GET /api/cases publishes it as agentCreated; GET /api/cases/agent-created is a read-only cleanup listing adding inUse and modifiedAt; Add Case -> Manage badges each case and offers a review-then-delete sweep that names every directory in its confirm and skips any case a live session is working in. Removal stays on the existing DELETE /api/cases/:name. - Agent preamble caches are collected too: ~/.cache/codeman-agent-<id>.sh was written per claude session and never removed (236 leftovers measured on a working machine). Now deleted with the session and swept at boot, guarded by a live-session keep set plus a 7-day age floor. Verified end to end on an isolated instance: marker written for header, body and lineage-only spawns, absent with no agent signal and for a pre-existing directory; inUse flipping on session end; badge, sticky bar, confirm and sweep driven in a browser; preamble seeded on create, removed on delete, boot sweep taking only the aged orphans. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+74
-3
@@ -1066,9 +1066,80 @@ export async function installAgentSkillInto(skillDir: string): Promise<AgentSkil
|
||||
*/
|
||||
export async function seedAgentSessionPreamble(sessionId: string): Promise<void> {
|
||||
const content = await readFile(join(agentSkillSourceDir(), 'preamble.sh'), 'utf-8');
|
||||
const cacheDir = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
|
||||
await mkdir(cacheDir, { recursive: true });
|
||||
await writeFile(join(cacheDir, `codeman-agent-${sessionId}.sh`), content, { mode: 0o600 });
|
||||
await mkdir(agentPreambleCacheDir(), { recursive: true });
|
||||
await writeFile(agentPreamblePath(sessionId), content, { mode: 0o600 });
|
||||
}
|
||||
|
||||
/** Where the preamble caches live. One formula, shared by seed / remove / prune. */
|
||||
function agentPreambleCacheDir(): string {
|
||||
return process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
|
||||
}
|
||||
|
||||
/** `codeman-agent-<sessionId>.sh` in that directory. */
|
||||
function agentPreamblePath(sessionId: string): string {
|
||||
return join(agentPreambleCacheDir(), `codeman-agent-${sessionId}.sh`);
|
||||
}
|
||||
|
||||
/** Matches exactly what seedAgentSessionPreamble writes, and nothing else in ~/.cache. */
|
||||
const AGENT_PREAMBLE_FILE_PATTERN = /^codeman-agent-(.+)\.sh$/;
|
||||
|
||||
/** How long a preamble cache with no live session behind it is kept before the sweep takes it. */
|
||||
export const AGENT_PREAMBLE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Drop one session's preamble cache. Called when a session is deleted, which is the
|
||||
* precise counterpart to seeding it at create: one file per claude session was being
|
||||
* written and nothing ever removed them (236 leftovers measured on a working machine,
|
||||
* the oldest three weeks old). Best-effort — a file that will not delete is litter,
|
||||
* never a reason to fail a teardown.
|
||||
*/
|
||||
export async function removeAgentSessionPreamble(sessionId: string): Promise<void> {
|
||||
await unlink(agentPreamblePath(sessionId)).catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sweep preamble caches left by sessions that are gone: the delete path above covers
|
||||
* an orderly teardown, and this covers everything else (a crash, a killed server, a
|
||||
* session deleted by an older build, another instance's leftovers).
|
||||
*
|
||||
* ⚠️ Two guards, and both matter: a file whose session is in `keepSessionIds` is never
|
||||
* touched however old it is, and everything else needs `maxAgeMs` of age on top. A live
|
||||
* session's cache is load-bearing — remove it and the skill's two-line loader fails its
|
||||
* version check mid-run — and the age floor is what keeps a session belonging to
|
||||
* ANOTHER instance (whose ids this process cannot see) out of the blast radius. Losing
|
||||
* one is degradation rather than breakage: the §0 fallback block rewrites it.
|
||||
*
|
||||
* Returns how many it removed. Best-effort throughout; a missing cache dir is 0.
|
||||
*/
|
||||
export async function pruneAgentSessionPreambles(
|
||||
keepSessionIds: Iterable<string>,
|
||||
maxAgeMs: number = AGENT_PREAMBLE_MAX_AGE_MS
|
||||
): Promise<number> {
|
||||
const cacheDir = agentPreambleCacheDir();
|
||||
const keep = new Set(keepSessionIds);
|
||||
const cutoff = Date.now() - maxAgeMs;
|
||||
let removed = 0;
|
||||
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = await readdir(cacheDir);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const sessionId = AGENT_PREAMBLE_FILE_PATTERN.exec(entry)?.[1];
|
||||
if (!sessionId || keep.has(sessionId)) continue;
|
||||
const path = join(cacheDir, entry);
|
||||
try {
|
||||
if ((await lstat(path)).mtimeMs > cutoff) continue;
|
||||
await unlink(path);
|
||||
removed++;
|
||||
} catch {
|
||||
/* best-effort — a vanished or unreadable file is not our problem */
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user