#!/usr/bin/env node /** * router-task-id (✅O17) — persisted first-plan-anchor. task-id присваивается при ПЕРВОМ * плане задачи, ПЕРСИСТИТСЯ, НЕ меняется через re-issue (новый plan-hash не сбрасывает). * Чистое ядро; персист (loadTaskId/saveTaskId) — отдельно, инъектируемый fs, * assertSafeSessionId guard (N3-shared, path-injection). */ import { assertSafeSessionId } from './action-journal.mjs'; /** Стабильный task-id: существующий → возвращается как есть (персист); иначе якорь из * firstPlanHash (первый план задачи). Нет ни того ни другого → null. */ export function deriveTaskId({ existingTaskId = null, firstPlanHash = '' } = {}) { if (typeof existingTaskId === 'string' && existingTaskId.trim()) return existingTaskId; const h = String(firstPlanHash || '').trim(); return h ? `task:${h}` : null; } function taskIdPath(runtimeDir, sessionId) { assertSafeSessionId(sessionId); const sep = runtimeDir.endsWith('/') ? '' : '/'; return `${runtimeDir}${sep}router-task-id-${sessionId}.txt`; } /** Загрузить персистнутый task-id (или null). fsImpl инъектируем. */ export function loadTaskId({ sessionId, runtimeDir, fsImpl }) { try { return String(fsImpl.readFileSync(taskIdPath(runtimeDir, sessionId), 'utf8')).trim() || null; } catch (e) { if (e && e.code === 'ENOENT') return null; throw e; } } /** Персистнуть task-id (идемпотентно — A0 пишет один раз при первом плане). * F-C1 (sharp-edges): атомарно temp→rename (зеркало plan-lock writeAtomic) — частичная * запись не оставляет битый якорь на финальном пути. */ export function saveTaskId({ taskId, sessionId, runtimeDir, fsImpl }) { const path = taskIdPath(runtimeDir, sessionId); const tmp = `${path}.tmp`; fsImpl.writeFileSync(tmp, String(taskId || '')); fsImpl.renameSync(tmp, path); }