@@ -32,6 +32,7 @@ pub const BIND_PORT: u16 = 1066;
3232
3333/// Coord server URL — cartridge acts as a client during OnEnter/OnExit.
3434pub const COORD_URL = "http://127.0.0.1:7745" ;
35+ pub const COORD_REGISTER_URL = COORD_URL ++ "/tools/coord_register" ;
3536
3637pub const MAX_TOOL_NAME : usize = 48 ;
3738pub const MAX_ARG_STRING : usize = 4096 ;
@@ -264,14 +265,17 @@ pub const EnterResult = struct {
264265 methodology_files : []const []const u8 ,
265266 memory_hits : []const []const u8 ,
266267 pub fn deinit (self : * EnterResult , allocator : std.mem.Allocator ) void {
268+ for (self .methodology_files ) | p | allocator .free (p );
267269 allocator .free (self .methodology_files );
270+ for (self .memory_hits ) | p | allocator .free (p );
268271 allocator .free (self .memory_hits );
269272 }
270273};
271274
272275/// OnEnter: register as coord peer (soft-fail to Degraded), load 6a2,
273276/// run memory auto-lift. Idempotent: re-calling from Registered or
274- /// Degraded is a no-op that returns the cached peer_id.
277+ /// Degraded is a no-op that returns the cached peer_id. Re-entering
278+ /// after Deregistered starts a new registration attempt.
275279///
276280/// The adapter handles the HTTP framing; this function only does the
277281/// state transition + the coord POST + the file-system reads.
@@ -283,18 +287,40 @@ pub fn onEnter(
283287 _ = session_hint ; // surfaced in future; no state effect today
284288
285289 const cur = state ();
286- if (cur == .registered or cur == .degraded ) {
290+ if (cur == .registered ) {
287291 // Idempotent — return the existing identity and a re-read methodology pack.
288292 const methodology = try readMethodologyPack (allocator , worktree );
289293 const memory = try memoryAutolift (allocator , worktree );
290294 return .{
291295 .peer_id = peerId (),
292- .coord_state = if ( cur == .registered ) "registered" else "degraded " ,
296+ .coord_state = "registered" ,
293297 .methodology_files = methodology ,
294298 .memory_hits = memory ,
295299 };
296300 }
297- if (cur != .fresh ) return error .BadState ;
301+ if (cur == .degraded ) {
302+ // Retry registration so degraded sessions can self-heal when
303+ // local-coord-mcp becomes reachable again.
304+ const upgraded = coordRegister (allocator ) catch false ;
305+ if (upgraded ) {
306+ setState (.registered );
307+ } else if (g_peer_id_len == 0 ) {
308+ const local_id = "claude-0000@007-lang-local" ;
309+ @memcpy (g_peer_id_buf [0.. local_id .len ], local_id );
310+ g_peer_id_len = local_id .len ;
311+ g_coord_token_len = 0 ;
312+ }
313+
314+ const methodology = try readMethodologyPack (allocator , worktree );
315+ const memory = try memoryAutolift (allocator , worktree );
316+ return .{
317+ .peer_id = peerId (),
318+ .coord_state = if (state () == .registered ) "registered" else "degraded" ,
319+ .methodology_files = methodology ,
320+ .memory_hits = memory ,
321+ };
322+ }
323+ if (cur != .fresh and cur != .deregistered ) return error .BadState ;
298324
299325 // Try coord_register. Soft-fail to Degraded on any error.
300326 const registered = coordRegister (allocator ) catch | e | blk : {
@@ -310,6 +336,7 @@ pub fn onEnter(
310336 const local_id = "claude-0000@007-lang-local" ;
311337 @memcpy (g_peer_id_buf [0.. local_id .len ], local_id );
312338 g_peer_id_len = local_id .len ;
339+ g_coord_token_len = 0 ;
313340 }
314341
315342 const methodology = try readMethodologyPack (allocator , worktree );
@@ -327,6 +354,7 @@ pub const ExitResult = struct {
327354 drift_findings : []const []const u8 ,
328355 coord_state : []const u8 ,
329356 pub fn deinit (self : * ExitResult , allocator : std.mem.Allocator ) void {
357+ for (self .drift_findings ) | p | allocator .free (p );
330358 allocator .free (self .drift_findings );
331359 }
332360};
@@ -343,12 +371,12 @@ pub fn onExit(
343371
344372 const findings = try driftCheck (allocator , worktree );
345373
346- if ( cur == .registered ) {
347- // Best-effort coord deregister; failure is logged but not fatal.
348- coordDeregister ( allocator ) catch | e | {
374+ // Best-effort coord teardown; failure is non-fatal.
375+ coordDeregister ( allocator ) catch | e | {
376+ if ( cur == .registered ) {
349377 std .log .warn ("007-mcp: coord_deregister failed ({}); proceeding to Deregistered" , .{e });
350- };
351- }
378+ }
379+ };
352380
353381 setState (.deregistered );
354382 return .{
@@ -384,11 +412,19 @@ fn readMethodologyPack(
384412 // explicit sentinel like "MISSING::<path>".
385413 var path_buf : [std .fs .max_path_bytes ]u8 = undefined ;
386414 const full = std .fmt .bufPrint (& path_buf , "{s}/{s}" , .{ worktree , rel }) catch rel ;
387- std .fs .accessAbsolute (full , .{}) catch {
388- const missing = try std .fmt .allocPrint (allocator , "MISSING::{s}" , .{rel });
389- out [i ] = missing ;
390- continue ;
391- };
415+ if (std .fs .path .isAbsolute (full )) {
416+ std .fs .accessAbsolute (full , .{}) catch {
417+ const missing = try std .fmt .allocPrint (allocator , "MISSING::{s}" , .{rel });
418+ out [i ] = missing ;
419+ continue ;
420+ };
421+ } else {
422+ std .fs .cwd ().access (full , .{}) catch {
423+ const missing = try std .fmt .allocPrint (allocator , "MISSING::{s}" , .{rel });
424+ out [i ] = missing ;
425+ continue ;
426+ };
427+ }
392428 out [i ] = try allocator .dupe (u8 , rel );
393429 }
394430 return out ;
@@ -427,6 +463,13 @@ pub const TAG_MAP_PATHS = [_][]const u8{
427463 "/var/mnt/eclipse/repos/boj-server/cartridges/007-mcp/schemas/memory-tag-map.a2ml" ,
428464};
429465
466+ fn openMaybeAbsolute (path : []const u8 ) ! std.fs.File {
467+ if (std .fs .path .isAbsolute (path )) {
468+ return try std .fs .openFileAbsolute (path , .{});
469+ }
470+ return try std .fs .cwd ().openFile (path , .{});
471+ }
472+
430473/// Read the tag map from disk (first candidate that exists wins).
431474/// Caller owns the returned slice.
432475fn readTagMap (allocator : std.mem.Allocator , worktree : []const u8 ) ! []u8 {
@@ -436,7 +479,7 @@ fn readTagMap(allocator: std.mem.Allocator, worktree: []const u8) ![]u8 {
436479 rel
437480 else
438481 std .fmt .bufPrint (& path_buf , "{s}/{s}" , .{ worktree , rel }) catch continue ;
439- const file = std . fs . openFileAbsolute (full , .{} ) catch continue ;
482+ const file = openMaybeAbsolute (full ) catch continue ;
440483 defer file .close ();
441484 const stat = try file .stat ();
442485 const buf = try allocator .alloc (u8 , stat .size );
@@ -570,24 +613,57 @@ fn driftCheck(
570613
571614// ─── Coord client ──────────────────────────────────────────────────────
572615
573- /// POST coord_register to local-coord-mcp. Returns true on success.
574- /// Current implementation returns false (Degraded) when the socket isn't
575- /// reachable — the adapter layer owns the actual HTTP write and token
576- /// parse. Kept as a hookable function here so tests can stub it.
616+ /// POST coord_register to local-coord-mcp.
617+ /// Returns true only when an HTTP 200 response carries
618+ /// {"success":true,"peer_id":"...","token":"..."}.
577619fn coordRegister (allocator : std.mem.Allocator ) ! bool {
578- _ = allocator ;
579- // Probe TCP connect to 127.0.0.1:7745; any failure → Degraded.
580- const addr = std .net .Address .initIp4 (BIND_ADDR , 7745 );
581- const sock = std .net .tcpConnectToAddress (addr ) catch return false ;
582- sock .close ();
583- // Real register happens in the adapter layer where the HTTP client
584- // is wired; here we only certify reachability.
585- const synth = "claude-007a@007-lang" ;
586- @memcpy (g_peer_id_buf [0.. synth .len ], synth );
587- g_peer_id_len = synth .len ;
588- const tok = "0000000000000000000000000000000000000000000000000000000000000000" ;
589- @memcpy (g_coord_token_buf [0.. 64], tok );
590- g_coord_token_len = 64 ;
620+ g_peer_id_len = 0 ;
621+ g_coord_token_len = 0 ;
622+
623+ var client = std.http.Client { .allocator = allocator };
624+ defer client .deinit ();
625+
626+ const uri = std .Uri .parse (COORD_REGISTER_URL ) catch return false ;
627+ const payload = "{\" client_kind\" :\" claude\" ,\" context\" :\" 007-lang\" }" ;
628+
629+ var headers_buf : [2 ]std.http.Header = .{
630+ .{ .name = "Content-Type" , .value = "application/json" },
631+ .{ .name = "User-Agent" , .value = "007-mcp/0.1 coord-register" },
632+ };
633+
634+ var aw : std.Io.Writer.Allocating = .init (allocator );
635+ defer aw .deinit ();
636+
637+ const fetch_result = client .fetch (.{
638+ .method = .POST ,
639+ .location = .{ .uri = uri },
640+ .extra_headers = & headers_buf ,
641+ .payload = payload ,
642+ .response_writer = & aw .writer ,
643+ }) catch return false ;
644+
645+ if (@intFromEnum (fetch_result .status ) != 200 ) return false ;
646+
647+ const body = aw .writer .buffered ();
648+ const parsed = std .json .parseFromSlice (std .json .Value , allocator , body , .{}) catch return false ;
649+ defer parsed .deinit ();
650+
651+ const obj = if (parsed .value == .object ) parsed .value .object else return false ;
652+ const ok_val = obj .get ("success" ) orelse return false ;
653+ if (ok_val != .bool or ! ok_val .bool ) return false ;
654+ const peer_id_val = obj .get ("peer_id" ) orelse return false ;
655+ const token_val = obj .get ("token" ) orelse return false ;
656+ if (peer_id_val != .string or token_val != .string ) return false ;
657+
658+ const peer_id = peer_id_val .string ;
659+ const token = token_val .string ;
660+ if (peer_id .len == 0 or peer_id .len > g_peer_id_buf .len ) return false ;
661+ if (token .len == 0 or token .len > g_coord_token_buf .len ) return false ;
662+
663+ @memcpy (g_peer_id_buf [0.. peer_id .len ], peer_id );
664+ g_peer_id_len = peer_id .len ;
665+ @memcpy (g_coord_token_buf [0.. token .len ], token );
666+ g_coord_token_len = token .len ;
591667 return true ;
592668}
593669
@@ -624,6 +700,15 @@ test "lifecycle state round-trip" {
624700 setState (.fresh ); // reset for other tests
625701}
626702
703+ test "onEnter accepts re-entry from deregistered state" {
704+ setState (.deregistered );
705+ var enter = try onEnter (std .testing .allocator , "." , "" );
706+ defer enter .deinit (std .testing .allocator );
707+
708+ try std .testing .expect (enter .peer_id .len > 0 );
709+ try std .testing .expect (std .mem .eql (u8 , enter .coord_state , "registered" ) or std .mem .eql (u8 , enter .coord_state , "degraded" ));
710+ }
711+
627712test "matchMemories picks memories from matching tag blocks only" {
628713 const map =
629714 \\;; comment line
0 commit comments