Skip to content

fix(memory): only unlink serve.port files that still hold our own port - #163

Open
KM-IA-Agency wants to merge 5 commits into
elara-labs:mainfrom
KML-C:fix/serve-port-unlink-own-only
Open

KM-IA-Agency wants to merge 5 commits into
elara-labs:mainfrom
KML-C:fix/serve-port-unlink-own-only

Conversation

@KM-IA-Agency

@KM-IA-Agency KM-IA-Agency commented Oct 4, 2026 •

Copy link
Copy Markdown

Problem

When several cce serve processes run for the same project (one per agent session, or e.g. Claude Code and Codex on the same repo), each start overwrites serve.port and the rendezvous file ~/.cce/projects/<name>/serve.port with its own port. On graceful exit, _unlink_port_files removed both files unconditionally.

So as soon as an older server exited, the newer one kept running with no rendezvous: cce_hook.cmd / the hook script found no port and silently dropped every captured turn until the next server start. In our setup (six concurrent cce serve on one repo), live memory capture was mostly dead for four days without any visible error.

Fix

_unlink_port_files now removes a port file only when it still contains this server's bound port. The bound port is kept in a closure because the aiohttp app is frozen once the runner is set up.

Unchanged: the single-server case (files are still removed on exit, the existing tests pass), tolerance of files already removed, and the storage == rendezvous layout.

Tests

  • New test_cleanup_leaves_port_files_owned_by_another_server: two servers on one project; stopping the first must leave the second's port in both files, and stopping the second removes them.
  • tests/memory/test_hook_server_cleanup.py: 4 passed with the fix; the new test fails without it (1 failed, 3 passed). Run locally on Windows / Python 3.13 with -n0; I have not run the full suite locally.

Not covered

SIGKILL still bypasses on_cleanup, so a killed server leaves a stale file. That is the existing #66 limit, handled by the socket-liveness probe in the hook script.

Update: cross-process lock (review feedback)

The ownership check and the unlink were not atomic across processes: a peer could publish between them. Publication (_publish_port) and the compare-and-delete now share one cross-process lock on <storage_base>/serve.port.lock (fcntl on POSIX, msvcrt on Windows), bounded to 2 s. On timeout, cleanup leaves the files rather than risk deleting a peer's rendezvous.

New regression test_peer_publishing_during_cleanup_keeps_its_rendezvous: a peer publishes between the read and the unlink. It passes with the lock and fails with the lock disabled. tests/memory/test_hook_server_cleanup.py + test_hooks.py: 33 passed locally (Windows / Python 3.13, -n0).

Update: hand the rendezvous over to a live peer

Compare-and-delete still left survivors without a rendezvous when the most recently started server exited first: it legitimately owned the files, so it removed them. A short-lived server next to a long-running one (a headless agent run beside an interactive session) hits this on every exit.

Each server now registers itself in <storage_base>/serve.ports/<port> under the same lock. On exit, a server whose port is still published rewrites the files with the most recently registered peer that accepts a loopback connection (0.2 s probe), and deletes them only when no peer is alive. Registry entries of peers killed without cleanup are pruned on the way.

New regressions: test_last_started_server_exiting_hands_rendezvous_to_live_peer and test_dead_peer_in_registry_is_pruned_not_published; both fail without this commit (2 failed, 5 passed). With it: tests/memory/test_hook_server_cleanup.py 7 passed, tests/memory 369 passed, ruff check clean (Windows / Python 3.13).

Update: sweep dead registry entries at start

Servers killed without cleanup (TerminateProcess on Windows, SIGKILL) never remove their serve.ports/ entry, and the exit-time handover stops at the first live peer, so the registry kept growing (23 entries for 4 live servers after a few hours next to an agent host that restarts its MCP servers). Each start now drops every entry whose port refuses a loopback connection, under the same lock, before registering itself. New regression test_start_prunes_registry_entries_of_dead_peers (fails without the change). tests/memory: 370 passed, ruff clean.

Known limit, unchanged by this PR: a server killed without cleanup while it owns serve.port still leaves a stale file until the next server start (the hook script's liveness probe covers it, as for #66).

Update: CCE_NO_HOOK_SERVER=1 for headless agent runs

The known limit above (a server killed without cleanup while it owns serve.port) is what headless claude -p workers hit: their host kills the MCP server hard on exit. Those runs need the MCP tools, not live capture, so cce serve now skips the hook listener when CCE_NO_HOOK_SERVER is 1/true/yes/on, and never touches serve.port or the registry. Default unchanged. Unit test for the switch; end-to-end on Windows: serve.port and the registry unchanged across start and a hard kill. tests/test_cli_serve.py + tests/memory: 378 passed, ruff clean.

🤖 Generated with Claude Code

Several `cce serve` processes can run for the same project (one per agent
session, or two agents on one repo). Each start overwrites serve.port and
the rendezvous file with its own port, but on graceful exit every server
unlinked both files unconditionally. When an older server exited, the
newer one kept running with no rendezvous: hooks found no port and
silently dropped captured turns until the next server start.

The cleanup now removes a port file only when it still contains this
server's bound port, and adds a regression test with two servers on one
project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 20:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Concurrent publication between the ownership check and deletion can still cause cleanup to remove a newer server’s rendezvous.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Protects memory-hook discovery when multiple cce serve processes share a project.

Changes:

  • Records the bound port and checks file ownership before cleanup.
  • Adds a two-server regression test for sequential shutdowns.
File Description
tests/​memory/​test_hook_server_cleanup.py Tests preservation of a newer server’s port files.
src/​context_engine/​memory/​hook_server.py Adds ownership checks before deleting port files.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/context_engine/memory/hook_server.py Outdated
KM-IA-Agency and others added 2 commits October 5, 2026 02:48
The ownership check and the unlink were not atomic across processes: a
peer could publish its port after cleanup read this server's port and
before it unlinked the file, losing the peer's rendezvous.

Publication and compare-and-delete now run under the same cross-process
lock on `<storage_base>/serve.port.lock` (fcntl on POSIX, msvcrt on
Windows), bounded to 2 s. On timeout, cleanup leaves the files rather
than risk deleting a peer's rendezvous. Adds a regression test that
publishes from a peer between the read and the unlink; it fails without
the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compare-and-delete still left survivors without a rendezvous when the
most recently started server exited first: it legitimately owned the
port files, so it removed them. A short-lived server next to a
long-running one (a headless agent run beside an interactive session)
hits this on every exit.

Each server now registers itself in `<storage_base>/serve.ports/<port>`
under the port-files lock. On exit, a server whose port is still
published rewrites the files with the most recently registered peer
that accepts a loopback connection (0.2 s probe), and only deletes them
when no peer is alive. Entries of peers killed without cleanup are
pruned on the way. Adds two regression tests (handover to the survivor,
dead registry entry skipped); both fail without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KM-IA-Agency

Copy link
Copy Markdown
Author

@fazleelahhee pushed ec31e61 on top of the lock commit: on exit, a server that still owns serve.port now hands it over to the most recent live peer instead of deleting it (registry in <storage_base>/serve.ports/, loopback probe, dead entries pruned). This closes the remaining gap where a short-lived server started after a long-running one left it without a rendezvous. Details and test results are in the description; the branch is mergeable on current main (7507115). Happy to adjust the approach if you prefer a different design.

KM-IA-Agency and others added 2 commits October 11, 2026 02:33
Servers killed without cleanup (TerminateProcess on Windows, SIGKILL)
never remove their registry entry, and the exit-time sweep stops at the
first live peer, so the registry kept growing: 23 entries for 4 live
servers after a few hours with an agent host that restarts its MCP
servers. Each start now drops every entry whose port refuses a loopback
connection, under the port-files lock, before registering itself. Adds a
regression test that fails without the sweep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headless agent runs (`claude -p` workers) need the MCP tools but not live
capture, and their host may kill the server without cleanup. Such a
server used to publish its port to serve.port at start and leave it
pointing at a dead port, cutting live capture for every other session of
the project until the next server start. With CCE_NO_HOOK_SERVER set to
1/true/yes/on, `cce serve` skips the hook listener entirely and never
touches serve.port or the registry. Unit test for the switch; checked
end to end on Windows (serve.port and registry unchanged across start and
a hard kill).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KM-IA-Agency added a commit to KML-C/code-context-engine-new-for-test that referenced this pull request Oct 11, 2026
Bundles the fixes proposed upstream and not yet merged there: serve.port
ownership, handover and registry sweep, CCE_NO_HOOK_SERVER (PR elara-labs#163);
PostToolUse tool_response (PR elara-labs#166); dotted .cceignore patterns and
directory targets (PR elara-labs#167). Full suite on Windows / Python 3.13: 1153
passed, 2 failed (symlink tests, failing on upstream main too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants