Skip to content

Commit 3bd4710

Browse files
hyperpolymathclaude
andcommitted
feat(local-coord-mcp): track-record + effective_affinity (Task #13)
Ring-buffered TrackEntry[512] keyed on client_kind (DD-29). Window rule (DD-28): include entries within last 7 days OR among last 20 attempts per (kind, tag), whichever set is larger. FFI: coord_report_outcome(token, tag, outcome, duration_ms, risk_tier), coord_get_affinities(token, out) returning 64-byte packed records (client_kind, attempts, successes, affinity_pct, tag). Replay dispatcher now reconstructs the ring from track_update events with original timestamps preserved. Adapter renders affinities as JSON. Cartridge.json + tools.js + main.js dispatch updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f8cafbf commit 3bd4710

5 files changed

Lines changed: 654 additions & 3 deletions

File tree

cartridges/local-coord-mcp/adapter/local_coord_adapter.zig

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,93 @@ fn dispatch(tool: []const u8, body: []const u8, resp: []u8, allocator: std.mem.A
385385
return .{ .status = 500, .body = errJson(resp, "approve failed") };
386386
}
387387

388+
if (std.mem.eql(u8, tool, "coord_report_outcome")) {
389+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return .{ .status = 400, .body = errJson(resp, "invalid json") };
390+
defer parsed.deinit();
391+
const token_val = parsed.value.object.get("token") orelse return .{ .status = 400, .body = errJson(resp, "missing token") };
392+
const tag_val = parsed.value.object.get("tag") orelse return .{ .status = 400, .body = errJson(resp, "missing tag") };
393+
const outcome_val = parsed.value.object.get("outcome") orelse return .{ .status = 400, .body = errJson(resp, "missing outcome") };
394+
const tier_val = parsed.value.object.get("risk_tier") orelse return .{ .status = 400, .body = errJson(resp, "missing risk_tier") };
395+
// duration_ms is optional; default 0.
396+
const duration_val: i64 = blk: {
397+
const v = parsed.value.object.get("duration_ms") orelse break :blk 0;
398+
break :blk v.integer;
399+
};
400+
401+
var token: [16]u8 = undefined;
402+
if (!parseToken(token_val.string, &token)) return .{ .status = 400, .body = errJson(resp, "invalid token hex") };
403+
404+
const tag_str = tag_val.string;
405+
if (tag_str.len > 64) return .{ .status = 400, .body = errJson(resp, "tag exceeds 64 bytes") };
406+
407+
// outcome may arrive as string ("success"/"fail") or integer (0/1).
408+
var outcome: i32 = -1;
409+
switch (outcome_val) {
410+
.string => |s| {
411+
if (std.mem.eql(u8, s, "success")) outcome = 1;
412+
if (std.mem.eql(u8, s, "fail")) outcome = 0;
413+
},
414+
.integer => |i| outcome = @intCast(i),
415+
else => {},
416+
}
417+
if (outcome != 0 and outcome != 1) return .{ .status = 400, .body = errJson(resp, "outcome must be 'success'/'fail' or 0/1") };
418+
419+
const tier: i32 = @intCast(tier_val.integer);
420+
const duration: i32 = @intCast(duration_val);
421+
const rc = ffi.coord_report_outcome(&token, 16, tag_str.ptr, @intCast(tag_str.len), outcome, duration, tier);
422+
if (rc == 0) return .{ .status = 200, .body = okJson(resp, "recorded") };
423+
if (rc == -1) return .{ .status = 401, .body = errJson(resp, "unauthenticated") };
424+
if (rc == -2) return .{ .status = 400, .body = errJson(resp, "invalid args") };
425+
return .{ .status = 500, .body = errJson(resp, "report failed") };
426+
}
427+
428+
if (std.mem.eql(u8, tool, "coord_get_affinities")) {
429+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return .{ .status = 400, .body = errJson(resp, "invalid json") };
430+
defer parsed.deinit();
431+
const token_val = parsed.value.object.get("token") orelse return .{ .status = 400, .body = errJson(resp, "missing token") };
432+
var token: [16]u8 = undefined;
433+
if (!parseToken(token_val.string, &token)) return .{ .status = 400, .body = errJson(resp, "invalid token hex") };
434+
435+
// Up to 64 aggregates * 64 bytes = 4096 bytes.
436+
var raw: [4096]u8 = undefined;
437+
const n = ffi.coord_get_affinities(&token, 16, &raw, @intCast(raw.len));
438+
if (n == -1) return .{ .status = 401, .body = errJson(resp, "unauthenticated") };
439+
if (n < -1000) return .{ .status = 500, .body = errJson(resp, "affinity buffer overflow — too many distinct (kind, tag) pairs") };
440+
if (n < 0) return .{ .status = 500, .body = errJson(resp, "affinity query failed") };
441+
442+
var stream = std.io.fixedBufferStream(resp);
443+
const w = stream.writer();
444+
w.writeAll("{\"success\":true,\"affinities\":[") catch return .{ .status = 500, .body = errJson(resp, "buffer overflow") };
445+
446+
const REC_SIZE: usize = 64;
447+
const cnt: usize = @intCast(n);
448+
var i: usize = 0;
449+
while (i < cnt) : (i += 1) {
450+
const rec = raw[i * REC_SIZE ..][0..REC_SIZE];
451+
const kind: u8 = rec[0];
452+
const attempts: u16 = @bitCast([2]u8{ rec[1], rec[2] });
453+
const successes: u16 = @bitCast([2]u8{ rec[3], rec[4] });
454+
const pct: u8 = rec[5];
455+
const tag_len: u8 = rec[6];
456+
const tag = rec[7 .. 7 + @min(@as(usize, tag_len), 57)];
457+
458+
if (i > 0) w.writeAll(",") catch return .{ .status = 500, .body = errJson(resp, "buffer overflow") };
459+
460+
// affinity as decimal; 255 sentinel means no data.
461+
if (pct == 255) {
462+
std.fmt.format(w,
463+
"{{\"client_kind\":\"{s}\",\"tag\":\"{s}\",\"attempts\":{d},\"successes\":{d},\"effective_affinity\":null}}",
464+
.{ kindName(@intCast(kind)), tag, attempts, successes }) catch return .{ .status = 500, .body = errJson(resp, "buffer overflow") };
465+
} else {
466+
std.fmt.format(w,
467+
"{{\"client_kind\":\"{s}\",\"tag\":\"{s}\",\"attempts\":{d},\"successes\":{d},\"effective_affinity\":{d}.{d:0>2}}}",
468+
.{ kindName(@intCast(kind)), tag, attempts, successes, pct / 100, pct % 100 }) catch return .{ .status = 500, .body = errJson(resp, "buffer overflow") };
469+
}
470+
}
471+
w.writeAll("]}") catch return .{ .status = 500, .body = errJson(resp, "buffer overflow") };
472+
return .{ .status = 200, .body = resp[0..stream.pos] };
473+
}
474+
388475
if (std.mem.eql(u8, tool, "coord_reject")) {
389476
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return .{ .status = 400, .body = errJson(resp, "invalid json") };
390477
defer parsed.deinit();

cartridges/local-coord-mcp/cartridge.json

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,66 @@
298298
"type": "object"
299299
},
300300
"name": "coord_reject"
301+
},
302+
{
303+
"description": "Report the outcome of a claim or attempted op against an affinity tag. Aggregated into the track record keyed on client_kind (survives peer restart). Feeds effective_affinity + reassignment suggestions.",
304+
"inputSchema": {
305+
"properties": {
306+
"duration_ms": {
307+
"description": "Wall-time duration of the op in ms (optional, default 0)",
308+
"minimum": 0,
309+
"type": "integer"
310+
},
311+
"outcome": {
312+
"description": "'success' or 'fail' (or 1/0)",
313+
"enum": [
314+
"success",
315+
"fail"
316+
],
317+
"type": "string"
318+
},
319+
"risk_tier": {
320+
"description": "Risk tier of the op (0-4)",
321+
"maximum": 4,
322+
"minimum": 0,
323+
"type": "integer"
324+
},
325+
"tag": {
326+
"description": "Affinity tag the op belongs to (e.g. 'proof-analysis', 'routine-edit')",
327+
"maxLength": 64,
328+
"type": "string"
329+
},
330+
"token": {
331+
"description": "Session token from coord_register",
332+
"type": "string"
333+
}
334+
},
335+
"required": [
336+
"token",
337+
"tag",
338+
"outcome",
339+
"risk_tier"
340+
],
341+
"type": "object"
342+
},
343+
"name": "coord_report_outcome"
344+
},
345+
{
346+
"description": "Return per-(client_kind, tag) effective_affinity computed over the last 20 attempts OR last 7 days (whichever is larger). Used by Opus to drive attester selection (DD-27) and reassignment suggestions (DD-28).",
347+
"inputSchema": {
348+
"properties": {
349+
"token": {
350+
"description": "Session token from coord_register",
351+
"type": "string"
352+
}
353+
},
354+
"required": [
355+
"token"
356+
],
357+
"type": "object"
358+
},
359+
"name": "coord_get_affinities"
301360
}
302361
],
303-
"version": "0.3.0"
362+
"version": "0.4.0"
304363
}

0 commit comments

Comments
 (0)