* Daemon bridge: if a daemon is running, notify the user. * * Interactive sessions (user at the keyboard) get a notification only -- * no tokens spent without explicit intent. The user runs /do continue. * * Non-interactive sessions (claude -p, RemoteTrigger, cron) get a command * the agent ac
()
| 289 | } |
| 290 | |
| 291 | function activationRuntime() { |
| 292 | const declared = String(process.env.CITADEL_RUNTIME || '').toLowerCase(); |
| 293 | if (declared === 'codex') return 'codex'; |
| 294 | if (declared === 'claude' || declared === 'claude-code') return 'claude-code'; |
| 295 | return 'unknown'; |
| 296 | } |
| 297 | |
| 298 | function recordActivationMilestones() { |
| 299 | try { |
| 300 | const config = path.join(PROJECT_ROOT, '.claude', 'harness.json'); |
| 301 | if (!fs.existsSync(config)) return; |
| 302 | JSON.parse(fs.readFileSync(config, 'utf8')); |
| 303 | const input = { status: 'succeeded', runtime: activationRuntime() }; |
| 304 | activation.recordOnce({ ...input, stage: 'setup_completed' }, { root: PROJECT_ROOT }); |
| 305 | activation.recordOnce({ ...input, stage: 'return_session' }, { |
| 306 | root: PROJECT_ROOT, |
| 307 | minimum_day_since_install: 1, |
| 308 | }); |
| 309 | } catch { /* activation evidence is best-effort and never blocks startup */ } |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * Watchdog: check for stale command results from a previous session. |
| 314 | * |
| 315 | * If last-command-result.json exists and is older than 10 minutes, |
| 316 | * the previous session may have completed a command that wasn't seen. |
| 317 | * Surface it so the user knows what happened. |
| 318 | */ |
| 319 | function checkStaleCommandResult() { |
| 320 | try { |
| 321 | const resultPath = path.join(PROJECT_ROOT, '.planning', 'telemetry', 'last-command-result.json'); |
| 322 | if (!fs.existsSync(resultPath)) return; |
| 323 | |
| 324 | const result = JSON.parse(fs.readFileSync(resultPath, 'utf8')); |
| 325 | if (!result.timestamp) return; |
| 326 | |
| 327 | const ageMs = Date.now() - new Date(result.timestamp).getTime(); |
| 328 | const STALE_THRESHOLD = 10 * 60 * 1000; // 10 minutes |
| 329 | |
| 330 | if (ageMs > STALE_THRESHOLD) { |
| 331 | const ageMins = Math.round(ageMs / 60000); |
| 332 | const exitCode = result.exitCode !== null && result.exitCode !== undefined ? result.exitCode : 'unknown'; |
| 333 | const duration = result.durationSec || '?'; |
| 334 | const cmd = (result.command || 'unknown').slice(0, 100); |
| 335 | |
| 336 | const parts = [`[watchdog] A previous command completed ${ageMins}m ago but may not have been seen.`]; |
| 337 | parts.push(` Command: ${cmd}`); |
| 338 | parts.push(` Exit code: ${exitCode}, duration: ${duration}s`); |
| 339 | |
| 340 | if (result.timedOut) { |
| 341 | parts.push(` NOTE: This command was killed by timeout after ${result.timeoutLimit}s.`); |
| 342 | } |
| 343 | |
| 344 | parts.push(` Check .planning/telemetry/last-command-result.json for details.`); |
| 345 | |
| 346 | process.stdout.write(parts.join('\n') + '\n'); |
| 347 | |
| 348 | // Clean up so we don't warn again |