Skip to content

Commit f8cafbf

Browse files
hyperpolymathclaude
andcommitted
feat(local-coord-mcp): extend track_update log schema for Task #13
Redefines TRACK_UPDATE payload to (client_kind:u8, outcome:u8, risk_tier:u8, duration_ms:u32, timestamp_ms:u64, tag_len:u8, tag[tag_len]). DD-29: key on client_kind not peer_id so track record survives peer crash+restart. logTrackUpdate() was never called — no deployed events to migrate. FORMAT_VERSION stays 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f3ca344 commit f8cafbf

1 file changed

Lines changed: 47 additions & 19 deletions

File tree

cartridges/local-coord-mcp/ffi/coord_durability.zig

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -326,16 +326,30 @@ pub fn logAudit(kind: u8, detail: []const u8) void {
326326
append(.audit, buf[0 .. 3 + detail.len]);
327327
}
328328

329-
/// TRACK_UPDATE — peer_suffix[4]u8 outcome:u8 (0=fail, 1=success)
330-
/// tag_len:u8 tag[tag_len]
331-
pub fn logTrackUpdate(peer_suffix: *const [4]u8, outcome: u8, tag: []const u8) void {
329+
/// TRACK_UPDATE — client_kind:u8 outcome:u8 risk_tier:u8 duration_ms:u32
330+
/// timestamp_ms:u64 tag_len:u8 tag[tag_len]
331+
///
332+
/// DD-29: keyed on client_kind (not peer_id/suffix) so the track record
333+
/// survives peer crash+restart — a fresh peer of the same client_kind
334+
/// inherits the track record.
335+
pub fn logTrackUpdate(
336+
client_kind: u8,
337+
outcome: u8,
338+
risk_tier: u8,
339+
duration_ms: u32,
340+
timestamp_ms: u64,
341+
tag: []const u8,
342+
) void {
332343
if (tag.len > 64) return;
333-
var buf: [6 + 64]u8 = undefined;
334-
@memcpy(buf[0..4], peer_suffix);
335-
buf[4] = outcome;
336-
buf[5] = @intCast(tag.len);
337-
if (tag.len > 0) @memcpy(buf[6 .. 6 + tag.len], tag);
338-
append(.track_update, buf[0 .. 6 + tag.len]);
344+
var buf: [16 + 64]u8 = undefined;
345+
buf[0] = client_kind;
346+
buf[1] = outcome;
347+
buf[2] = risk_tier;
348+
std.mem.writeInt(u32, buf[3..7], duration_ms, .little);
349+
std.mem.writeInt(u64, buf[7..15], timestamp_ms, .little);
350+
buf[15] = @intCast(tag.len);
351+
if (tag.len > 0) @memcpy(buf[16 .. 16 + tag.len], tag);
352+
append(.track_update, buf[0 .. 16 + tag.len]);
339353
}
340354

341355
// ═══════════════════════════════════════════════════════════════════════
@@ -442,16 +456,26 @@ pub fn decodeAudit(p: []const u8) ?Audit {
442456
return .{ .kind = p[0], .detail = p[3 .. 3 + n] };
443457
}
444458

445-
pub const TrackUpdate = struct { suffix: [4]u8, outcome: u8, tag: []const u8 };
459+
pub const TrackUpdate = struct {
460+
client_kind: u8,
461+
outcome: u8,
462+
risk_tier: u8,
463+
duration_ms: u32,
464+
timestamp_ms: u64,
465+
tag: []const u8,
466+
};
446467
pub fn decodeTrackUpdate(p: []const u8) ?TrackUpdate {
447-
if (p.len < 6) return null;
448-
const n: usize = p[5];
449-
if (p.len < 6 + n) return null;
450-
var out: TrackUpdate = undefined;
451-
@memcpy(&out.suffix, p[0..4]);
452-
out.outcome = p[4];
453-
out.tag = p[6 .. 6 + n];
454-
return out;
468+
if (p.len < 16) return null;
469+
const n: usize = p[15];
470+
if (p.len < 16 + n) return null;
471+
return .{
472+
.client_kind = p[0],
473+
.outcome = p[1],
474+
.risk_tier = p[2],
475+
.duration_ms = std.mem.readInt(u32, p[3..7], .little),
476+
.timestamp_ms = std.mem.readInt(u64, p[7..15], .little),
477+
.tag = p[16 .. 16 + n],
478+
};
455479
}
456480

457481
// ═══════════════════════════════════════════════════════════════════════
@@ -587,7 +611,7 @@ test "replay decodes every event type" {
587611
logQuarApprove(42);
588612
logQuarReject(43, "confabulated path");
589613
logAudit(1, "tier3-from-supervised");
590-
logTrackUpdate(&suffix, 1, "proof-analysis");
614+
logTrackUpdate(0, 1, 2, 1234, 1_700_000_000_000, "proof-analysis");
591615
logPeerRemove(0);
592616
close();
593617

@@ -612,6 +636,10 @@ test "replay decodes every event type" {
612636

613637
try std.testing.expectEqual(EventType.track_update, t_events[12]);
614638
const tr = decodeTrackUpdate(t_payloads[12][0..t_payload_lens[12]]) orelse return error.DecodeFailed;
639+
try std.testing.expectEqual(@as(u8, 0), tr.client_kind);
615640
try std.testing.expectEqual(@as(u8, 1), tr.outcome);
641+
try std.testing.expectEqual(@as(u8, 2), tr.risk_tier);
642+
try std.testing.expectEqual(@as(u32, 1234), tr.duration_ms);
643+
try std.testing.expectEqual(@as(u64, 1_700_000_000_000), tr.timestamp_ms);
616644
try std.testing.expectEqualSlices(u8, "proof-analysis", tr.tag);
617645
}

0 commit comments

Comments
 (0)