()
| 120 | * initExtractMemories), or per-test in beforeEach for a fresh closure. |
| 121 | */ |
| 122 | export function initAutoDream(): void { |
| 123 | let lastSessionScanAt = 0 |
| 124 | |
| 125 | runner = async function runAutoDream(context, appendSystemMessage) { |
| 126 | const cfg = getConfig() |
| 127 | const force = isForced() |
| 128 | if (!force && !isGateOpen()) return |
| 129 | |
| 130 | // --- Time gate --- |
| 131 | let lastAt: number |
| 132 | try { |
| 133 | lastAt = await readLastConsolidatedAt() |
| 134 | } catch (e: unknown) { |
| 135 | logForDebugging( |
| 136 | `[autoDream] readLastConsolidatedAt failed: ${(e as Error).message}`, |
| 137 | ) |
| 138 | return |
| 139 | } |
| 140 | const hoursSince = (Date.now() - lastAt) / 3_600_000 |
| 141 | if (!force && hoursSince < cfg.minHours) return |
| 142 | |
| 143 | // --- Scan throttle --- |
| 144 | const sinceScanMs = Date.now() - lastSessionScanAt |
| 145 | if (!force && sinceScanMs < SESSION_SCAN_INTERVAL_MS) { |
| 146 | logForDebugging( |
| 147 | `[autoDream] scan throttle — time-gate passed but last scan was ${Math.round(sinceScanMs / 1000)}s ago`, |
| 148 | ) |
| 149 | return |
| 150 | } |
| 151 | lastSessionScanAt = Date.now() |
| 152 | |
| 153 | // --- Session gate --- |
| 154 | let sessionIds: string[] |
| 155 | try { |
| 156 | sessionIds = await listSessionsTouchedSince(lastAt) |
| 157 | } catch (e: unknown) { |
| 158 | logForDebugging( |
| 159 | `[autoDream] listSessionsTouchedSince failed: ${(e as Error).message}`, |
| 160 | ) |
| 161 | return |
| 162 | } |
| 163 | // Exclude the current session (its mtime is always recent). |
| 164 | const currentSession = getSessionId() |
| 165 | sessionIds = sessionIds.filter(id => id !== currentSession) |
| 166 | if (!force && sessionIds.length < cfg.minSessions) { |
| 167 | logForDebugging( |
| 168 | `[autoDream] skip — ${sessionIds.length} sessions since last consolidation, need ${cfg.minSessions}`, |
| 169 | ) |
| 170 | return |
| 171 | } |
| 172 | |
| 173 | // --- Lock --- |
| 174 | // Under force, skip acquire entirely — use the existing mtime so |
| 175 | // kill's rollback is a no-op (rewinds to where it already is). |
| 176 | // The lock file stays untouched; next non-force turn sees it as-is. |
| 177 | let priorMtime: number | null |
| 178 | if (force) { |
| 179 | priorMtime = lastAt |
no test coverage detected