fix(mine): identify lock holder + exit non-zero on contention

When a `mempalace mine` collided with another writer (live mcp_server,
another mine, anything taking mine_palace_lock), the operator saw a
generic "another `mempalace mine` is already running" message and the
CLI exited 0 — making the contention invisible to nohup or scripts
checking $?. The reporter ran a `nohup mempalace mine ... & disown`
and got a 200-byte log with only the auto-defaults warning, no clue
that an MCP server was holding the store.

palace.py: the lock file now records the holder's PID + first three
argv tokens on acquire. A failed acquire reads the file and surfaces
"palace <path> is held by PID N (mempalace mcp_server); wait for it
to finish or stop the holder before retrying" in the
MineAlreadyRunning message. Open mode changes from "w" to "a+" so the
prior holder's identity survives long enough to be read.

miner.mine() now lets MineAlreadyRunning propagate. cmd_mine catches
it, prints the holder-aware message to stderr, and exits non-zero so
shell wrappers detect the contention.

Note: this is a behavior change for in-process callers that depended
on miner.mine() silently swallowing MineAlreadyRunning. The silent
swallow was the bug.

Closes #1264
This commit is contained in:
Igor Lins e Silva
2026-05-08 01:00:00 -03:00
parent ea36a00f5f
commit ef8d83cc8a
5 changed files with 213 additions and 46 deletions
+39
View File
@@ -555,6 +555,45 @@ def test_cmd_mine_include_ignored_comma_split(mock_config_cls):
assert call_kwargs["include_ignored"] == ["a.txt", "b.txt", "c.txt"]
@patch("mempalace.cli.MempalaceConfig")
def test_cmd_mine_exits_nonzero_on_lock_holder(mock_config_cls, capsys):
"""Regression #1264: lock contention must exit non-zero with a clear message.
Before this fix the CLI silently returned 0 when another writer held
the palace lock — operators using nohup/scripts had no way to detect
the contention. The new behavior raises MineAlreadyRunning out of
miner.mine() and cmd_mine catches it, printing the holder identity
to stderr and exiting non-zero.
"""
from mempalace.palace import MineAlreadyRunning
mock_config_cls.return_value.palace_path = "/fake/palace"
args = argparse.Namespace(
dir="/src",
palace=None,
mode="projects",
wing=None,
agent="mempalace",
limit=0,
dry_run=False,
no_gitignore=False,
include_ignored=[],
extract="exchange",
)
with patch(
"mempalace.miner.mine",
side_effect=MineAlreadyRunning(
"palace /fake/palace is held by PID 12345 (mempalace mcp_server); wait for it to finish"
),
):
with pytest.raises(SystemExit) as excinfo:
cmd_mine(args)
assert excinfo.value.code == 1
captured = capsys.readouterr()
assert "PID 12345" in captured.err
assert "mcp_server" in captured.err
# ── cmd_wakeup ─────────────────────────────────────────────────────────