@@ -44,6 +44,12 @@ enum {
4444 MAIN_PATH_CAP = 4096 ,
4545 MAIN_CONNECT_TIMEOUT_MS = 1000 ,
4646 MAIN_STARTUP_TIMEOUT_MS = 10000 ,
47+ /* Backstop for waiting out a held startup transition — see
48+ * main_local_transition_acquire. Not a budget for healthy contention: a busy
49+ * lock always resolves, either because the live holder finishes or because
50+ * the OS finishes reclaiming a dead holder's lock. This only bounds a peer
51+ * that never finishes, so a command cannot hang indefinitely. */
52+ MAIN_STARTUP_CONTENTION_CEILING_MS = 120000 ,
4753 MAIN_MCP_STARTUP_TIMEOUT_MS = 30000 ,
4854 MAIN_REQUEST_TIMEOUT_MS = 24 * 60 * 60 * 1000 ,
4955 MAIN_HOOK_CONNECT_TIMEOUT_MS = 250 ,
@@ -1288,14 +1294,47 @@ static bool main_local_cli_feedback_enabled(int argc, char **argv) {
12881294 return cbm_cli_progress_enabled (requested , cli_isatty (2 ) != 0 );
12891295}
12901296
1297+ /* Acquire the exclusive startup transition, waiting out whatever currently holds it.
1298+ *
1299+ * try_acquire answers one of three things, and they must not be conflated:
1300+ * 1 acquired
1301+ * -1 coordination is unsafe or unverifiable — fail now, waiting cannot help
1302+ * 0 BUSY: the lock is held
1303+ *
1304+ * A busy lock always resolves, by one of two routes:
1305+ * - a live peer holds it, and releases when its command finishes;
1306+ * - the holder is already dead and the operating system has not finished
1307+ * reclaiming the lock yet.
1308+ *
1309+ * The second route is Windows-specific and is why this wait needs room. On POSIX
1310+ * the kernel drops flock the instant the owner dies. Windows byte-range locks do
1311+ * not work that way: Microsoft documents that after a process terminates holding
1312+ * one, "the time it takes for the operating system to unlock these locks depends
1313+ * upon available system resources", and that until then "access to these files
1314+ * may be denied". A loaded CI runner is precisely where those resources are
1315+ * scarce, and `tests/windows/test_daemon_stability.py` manufactures the situation
1316+ * deliberately — it hard-kills daemons with `taskkill /F`, including a crash
1317+ * recovery section, so the next client meets a lock whose owner no longer exists.
1318+ *
1319+ * Waiting was previously capped at MAIN_STARTUP_TIMEOUT_MS (10s), which let a
1320+ * clock decide a user-visible outcome: a command was refused with "coordination
1321+ * remained busy" while the only thing wrong was that the OS had not yet swept up
1322+ * after a killed process. Since both routes above terminate, the correct response
1323+ * to busy is to keep waiting; the ceiling below is a backstop against a peer that
1324+ * never finishes, not a budget for healthy contention. Clean exits already
1325+ * release through main_local_transition_close, so this path is only reached after
1326+ * an abrupt termination or under genuine concurrency. */
12911327static int main_local_transition_acquire (const cbm_daemon_ipc_endpoint_t * endpoint , FILE * feedback ,
12921328 cbm_daemon_ipc_local_transition_t * * transition_out ) {
1293- uint64_t deadline = main_deadline_after (MAIN_STARTUP_TIMEOUT_MS );
1329+ uint64_t ceiling = main_deadline_after (MAIN_STARTUP_CONTENTION_CEILING_MS );
12941330 bool waiting_reported = false;
12951331 for (;;) {
12961332 int status = cbm_daemon_ipc_local_transition_try_acquire (endpoint , transition_out );
1297- if (status != 0 || cbm_now_ms () >= deadline ) {
1298- return status ;
1333+ if (status != 0 ) {
1334+ return status ; /* acquired, or a genuine coordination failure */
1335+ }
1336+ if (cbm_now_ms () >= ceiling ) {
1337+ return 0 ; /* still busy after the backstop */
12991338 }
13001339 if (feedback && !waiting_reported ) {
13011340 (void )fputs ("Waiting for CBM startup coordination...\n" , feedback );
@@ -2542,11 +2581,24 @@ int main(int argc, char **argv) {
25422581 int transition_status =
25432582 main_local_transition_acquire (local_endpoint , feedback , & local_transition );
25442583 if (transition_status != 1 || !local_transition ) {
2545- (void )fprintf (stderr ,
2546- "codebase-memory-mcp: CLI startup coordination %s; retry after the "
2547- "active CBM transition exits\n" ,
2548- transition_status == 0 ? "remained busy"
2549- : "could not be verified safely" );
2584+ if (transition_status == 0 ) {
2585+ /* The backstop fired. Name both explanations: after this much
2586+ * waiting the likely causes are a peer that is genuinely stuck,
2587+ * or (on Windows) a lock left behind by a force-killed process
2588+ * that the OS has not reclaimed. "Busy" alone sent reporters
2589+ * hunting for a CBM session that had already exited. */
2590+ (void )fprintf (stderr ,
2591+ "codebase-memory-mcp: CLI startup coordination stayed busy for "
2592+ "%d seconds. Either another CBM command is still running, or a "
2593+ "previous one was force-killed and the operating system has not "
2594+ "released its lock yet. Check for running CBM processes; if there "
2595+ "are none, retry shortly.\n" ,
2596+ MAIN_STARTUP_CONTENTION_CEILING_MS / 1000 );
2597+ } else {
2598+ (void )fprintf (stderr , "codebase-memory-mcp: CLI startup coordination could not "
2599+ "be verified safely; retry after active CBM sessions "
2600+ "exit\n" );
2601+ }
25502602 goto local_cli_cleanup ;
25512603 }
25522604 int seal_status = cbm_daemon_ipc_local_transition_seal_legacy (local_transition );
0 commit comments