fix(sessions): reprendre/forker une session managée morte par UUID (résout le 404)

Le bouton « Reprendre » envoie l'UUID Arboretum, mais resume/fork ne
résolvaient l'id que via la découverte (indexée par claudeSessionId, qui
exclut justement les sessions managées) → 404 systématique après fermeture.
Ajoute PtyManager.resumeTargetById (cwd d'origine + claudeSessionId + groupe
lus en DB par UUID) et un résolveur unifié dans resume ET fork : session
managée morte par UUID, sinon session claude externe découverte par
claudeSessionId. Garde-fous anti-resume d'une session vivante conservés.
This commit is contained in:
2026-06-22 14:41:06 +02:00
parent afe39ac072
commit 4e3b1f5604
4 changed files with 97 additions and 11 deletions

View File

@@ -26,24 +26,29 @@ export function registerSessionRoutes(app: FastifyInstance, manager: PtyManager,
}
});
// Reprise d'une session morte : nouveau PTY managé `--resume <id>` DANS SON CWD D'ORIGINE (spike S1).
// Le cwd n'est jamais fourni par le client : il est lu sur disque via la découverte.
// Reprise d'une session morte : nouveau PTY managé `--resume <claudeSessionId>` DANS SON CWD D'ORIGINE
// (spike S1). Le cwd n'est JAMAIS fourni par le client. Deux origines d'id possibles :
// - session managée morte (id = UUID Arboretum) → cwd + claudeSessionId + groupe lus en DB ;
// - session claude EXTERNE découverte (id = claudeSessionId) → cwd lu sur disque.
app.post('/api/v1/sessions/:id/resume', async (req, reply) => {
const { id } = req.params as { id: string };
const discovered = discovery.getDiscovered(id);
if (!discovered) {
const managed = manager.resumeTargetById(id);
const discovered = managed ? null : discovery.getDiscovered(id);
if (!managed && !discovered) {
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No resumable session with this id' } });
}
const cwd = managed ? managed.cwd : discovered!.cwd;
const claudeSessionId = managed ? managed.claudeSessionId : id;
// Garde-fou anti-corruption : jamais de resume direct d'une session vivante (vérif FRAÎCHE).
if (discovery.isClaudeSessionLive(id) || manager.findLiveByClaudeSessionId(id)) {
if (discovery.isClaudeSessionLive(claudeSessionId) || manager.findLiveByClaudeSessionId(claudeSessionId)) {
return reply.status(409).send({ error: { code: 'SESSION_LIVE', message: 'Session is live — fork it instead' } });
}
try {
// Session de groupe (P6) : re-relie les mêmes répertoires (--add-dir) et son groupe au resume.
const ctx = manager.groupSessionContext(id);
const ctx = managed ? { addedDirs: managed.addedDirs, groupId: managed.groupId } : manager.groupSessionContext(claudeSessionId);
const session = manager.spawn({
cwd: discovered.cwd,
resume: { claudeSessionId: id },
cwd,
resume: { claudeSessionId },
...(ctx?.addedDirs.length ? { addDirs: ctx.addedDirs } : {}),
...(ctx?.groupId ? { groupId: ctx.groupId } : {}),
});
@@ -56,14 +61,18 @@ export function registerSessionRoutes(app: FastifyInstance, manager: PtyManager,
});
// Fork : duplique une session (vivante ou morte) sans la corrompre (`--resume <id> --fork-session`).
// Même résolution d'id que /resume (managée morte par UUID, sinon externe découverte par claudeSessionId).
app.post('/api/v1/sessions/:id/fork', async (req, reply) => {
const { id } = req.params as { id: string };
const discovered = discovery.getDiscovered(id);
if (!discovered) {
const managed = manager.resumeTargetById(id);
const discovered = managed ? null : discovery.getDiscovered(id);
if (!managed && !discovered) {
return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No session with this id to fork' } });
}
const cwd = managed ? managed.cwd : discovered!.cwd;
const claudeSessionId = managed ? managed.claudeSessionId : id;
try {
const session = manager.spawn({ cwd: discovered.cwd, resume: { claudeSessionId: id, fork: true } });
const session = manager.spawn({ cwd, resume: { claudeSessionId, fork: true } });
const res: SessionResponse = { session };
return reply.status(201).send(res);
} catch (err) {