Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ Current format:
Notes:

- If a UI-enabled binary finds its verified external asset pack and no UI config file exists yet, the UI auto-enables on first run. Missing or invalid assets leave the MCP/daemon service available and keep the UI disabled.
- A bare `daemon start` honors the saved `ui_enabled` value, including `false`. Enable the UI with `codebase-memory-mcp config set ui_enabled true`.
- On a cold start, `daemon start --open` or `daemon start --port=N` explicitly enables the UI and persists that choice. An already-active daemon keeps its current settings; stop it first to change ports with `--port=N`.
- `CBM_CACHE_DIR` changes both the UI config location and the runtime settings database location.
- CBM resolves `CBM_CACHE_DIR` to one canonical per-account cache root. A process configured with a different root fails while any CBM session or command is active; close them before switching roots.

Expand Down
22 changes: 16 additions & 6 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -2333,7 +2333,7 @@ static void main_daemon_ctl_print_ui_configuration(void) {
"`daemon start --open` verifies it)\n",
ui_config.ui_port);
} else {
printf(" ui: disabled (enable with `daemon start` in a UI build)\n");
printf(" ui: disabled (enable with `config set ui_enabled true`)\n");
}
}

Expand Down Expand Up @@ -2556,8 +2556,12 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
cbm_ui_config_t ui_config;
cbm_ui_config_load(&ui_config);
if (!ui_config.ui_enabled) {
(void)fprintf(stderr, "error: UI is disabled for the active daemon; browser was not "
"opened\n");
main_daemon_ctl_print_ui_configuration();
if (open_browser) {
(void)fprintf(stderr,
"error: UI is disabled for the active daemon; browser was not "
"opened\n");
}
return open_browser ? EXIT_FAILURE : EXIT_SUCCESS;
}
if (requested_port > 0 && requested_port != ui_config.ui_port) {
Expand Down Expand Up @@ -2612,9 +2616,12 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
* startup window; configure the UI before departing. */
int ui_port = 0;
bool ui_configured = false;
if ((CBM_EMBEDDED_FILE_COUNT > 0)) {
cbm_ui_config_t ui_config;
cbm_ui_config_load(&ui_config);
cbm_ui_config_t ui_config;
cbm_ui_config_load(&ui_config);
/* A bare start inherits the saved choice, just like automatic startup.
* Only an explicit UI request may re-enable a deliberately disabled UI. */
bool ui_requested = ui_config.ui_enabled || requested_port > 0 || open_browser;
if (CBM_EMBEDDED_FILE_COUNT > 0 && ui_requested) {
ui_port = requested_port > 0 ? requested_port : ui_config.ui_port;
uint8_t update_mask = 0x03U; /* enabled + port */
bool context_set =
Expand Down Expand Up @@ -2660,6 +2667,9 @@ static int main_run_daemon_ctl(int argc, char **argv, const cbm_daemon_ipc_endpo
}
printf("It survives idle periods and session ends; `codebase-memory-mcp daemon stop` "
"retires it.\n");
if (CBM_EMBEDDED_FILE_COUNT > 0 && !ui_requested) {
main_daemon_ctl_print_ui_configuration();
}
/* Skipped when the handshake was refused: that path announces the port as
* warming, which would be a promise nothing is keeping. */
int ui_result =
Expand Down
77 changes: 77 additions & 0 deletions tests/test_daemon_open_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

This guard uses a real UI build and isolated daemon generations:

* a saved disabled UI survives cold and repeated bare starts, while explicit
UI requests and saved enabled settings still start the listener;
* a temporarily occupied port proves the command waits and then succeeds once
the verified UI listener becomes available;
* a foreign service returning the exact formerly accepted HTML markers proves
Expand All @@ -22,6 +24,7 @@
Exit code: 0 == green, 1 == behavior regression, 2 == fixture/setup error.
"""

import json
import os
import re
import shutil
Expand Down Expand Up @@ -327,6 +330,76 @@ def assert_active_daemon_open(binary, work):
stop_daemon(binary, env, daemon_pid)


def assert_persisted_ui_setting(binary, work, enabled=False, flags=()):
name = "enabled" if enabled else "disabled"
name += "-" + ("-".join(flag.lstrip("-") for flag in flags) or "bare")
cache = os.path.join(work, "cache-" + name)
marker = os.path.join(work, "browser-" + name + ".txt")
probe, port = occupied_loopback_port()
probe.close()
os.makedirs(cache, exist_ok=True)
env = fixture_environment(work, cache, marker, 10000)
config_path = os.path.join(cache, "config.json")
daemon_pid = 0
try:
for key, value in (("ui_enabled", str(enabled).lower()), ("ui_port", str(port))):
configured = subprocess.run([binary, "config", "set", key, value],
capture_output=True, timeout=10, env=env, cwd=work)
if configured.returncode != 0:
print("SETUP FAIL: could not persist UI setting:\n%s" % output_text(configured))
return False
with open(config_path, "rb") as handle:
original_config = handle.read()
arguments = ["--port=%d" % port if flag == "--port" else flag for flag in flags]
first = subprocess.run([binary, "daemon", "start", *arguments], capture_output=True,
timeout=30, env=env, cwd=work)
first_text = output_text(first)
daemon_pid = pid_from(first_text)
if first.returncode != 0 or "daemon: started" not in first_text:
print("RED: daemon start failed with saved UI setting (%s):\n%s" %
(name, first_text[:700]))
return False
with open(config_path, "rb") as handle:
current_config = handle.read()
expected_enabled = enabled or bool(flags)
if (json.loads(current_config).get("ui_enabled") != expected_enabled or
(not expected_enabled and current_config != original_config)):
print("RED: daemon start rewrote the saved UI setting (%s): %r" %
(name, current_config))
return False
if expected_enabled:
opened = subprocess.run([binary, "daemon", "start", "--open"], capture_output=True,
timeout=20, env=env, cwd=work)
if opened.returncode != 0 or not os.path.exists(marker):
print("RED: requested UI did not become ready (%s):\n%s" %
(name, output_text(opened)[:700]))
return False
else:
for arguments in (("start",), ("status",), ("start", "--open")):
result = subprocess.run([binary, "daemon", *arguments], capture_output=True,
timeout=10, env=env, cwd=work)
text = output_text(result)
expected_result = 1 if "--open" in arguments else 0
if (result.returncode != expected_result or "disabled" not in text or
"warming" in text or os.path.exists(marker)):
print("RED: disabled UI was not preserved by %r:\n%s" %
(arguments, text[:700]))
return False
with open(config_path, "rb") as handle:
if handle.read() != original_config:
print("RED: repeated daemon start rewrote disabled UI config")
return False
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as connection:
connection.settimeout(0.5)
if connection.connect_ex(("127.0.0.1", port)) == 0:
print("RED: disabled UI opened a listener")
return False
print("PASS: daemon start respects saved settings and explicit UI requests (%s)" % name)
return True
finally:
stop_daemon(binary, env, daemon_pid)


def main():
if len(sys.argv) != 2:
print("usage: python3 test_daemon_open_readiness.py <ui-binary>")
Expand All @@ -346,6 +419,10 @@ def main():
# Darwin temporary root so the product endpoint itself remains valid.
short_temp_root = "/private/tmp" if sys.platform == "darwin" else tempfile.gettempdir()
with tempfile.TemporaryDirectory(prefix="cbm_uiopen_", dir=short_temp_root) as work:
for enabled, flags in ((False, ()), (True, ()),
(False, ("--port",)), (False, ("--open",))):
if not assert_persisted_ui_setting(binary, work, enabled, flags):
return 1
if not assert_delayed_success(binary, work):
return 1
if not assert_bounded_foreign_port_failure(binary, work):
Expand Down
Loading