Skip to content

Commit 69bb6b3

Browse files
IMNMVclaude
andcommitted
Agent identity and cross-restart history (coordination v2.1)
ClaudeR 0.11.0 / clauder-mcp 0.13.0 (40 tools). From field reports of a multi-day three-agent session. - set_agent_name tool: an agent sets its working name once and execution history, message attribution, presence, and its read cursor all carry it. Fixes the identity collapse both testers hit: agents or personas sharing one MCP connection resolve to a single random per-connection id that get_session_history cannot separate. Identity is now step 1 of the coordination check-in; CLAUDER_AGENT_ID remains the permanent-name alternative. - get_session_history include_past: parses prior clauder_*.R session logs from disk (newest five, excluding the live log), so the audit of who ran what survives R restarts. Entries carry a {logfile} tag. - Agent intro briefs multi-agent sessions to name themselves first. CI: past-log parsing functional test. R CMD check: Status OK. pytest 30/30, handshake 40 tools. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8170b17 commit 69bb6b3

10 files changed

Lines changed: 236 additions & 18 deletions

File tree

.github/scripts/checks.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,5 +412,30 @@ r <- tryCatch({
412412
}, error = function(e) conditionMessage(e))
413413
if (isTRUE(r)) pass("response letter export + undrafted gate") else fail("response letter:", r)
414414

415+
# --- 13. cross-restart history from past session logs ---
416+
r <- tryCatch({
417+
logdir <- tempfile("logs"); dir.create(logdir)
418+
writeLines(c(
419+
"# --- [2026-08-10 12:01:00] ---",
420+
"# Code executed by Claude-Stasis:",
421+
"x <- rnorm(10)",
422+
"",
423+
"# --- [2026-08-10 12:02:00] ---",
424+
"# Code executed by Claude-Gatherers (ERROR):",
425+
"lm(y ~ broken)",
426+
"# Error: object not found",
427+
""
428+
), file.path(logdir, "clauder_t_8787_20260810_120000.R"))
429+
live <- file.path(logdir, "clauder_t_8787_20260811_090000.R")
430+
writeLines("# live", live)
431+
assign("load_claude_settings",
432+
function() list(log_to_file = TRUE, log_file_path = live), envir = env)
433+
o1 <- env$query_agent_history("all", "t", 20, include_past = TRUE)
434+
o2 <- env$query_agent_history("Claude-Stasis", "t", 20, include_past = TRUE)
435+
grepl("Claude-Gatherers \\(ERR\\)", o1) && grepl("\\{clauder_t_8787_20260810", o1) &&
436+
grepl("Claude-Stasis", o2) && !grepl("Gatherers", o2)
437+
}, error = function(e) conditionMessage(e))
438+
if (isTRUE(r)) pass("cross-restart history: past logs parsed, agent filter works") else fail("past history:", r)
439+
415440
if (!ok) quit(status = 1)
416441
cat("\nAll checks passed.\n")

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: ClaudeR
22
Title: R Integration for Claude AI
3-
Version: 0.10.0
3+
Version: 0.11.0
44
Authors@R: person("Nykko", "Vitali", email = "nykvt@icloud.com", role = c("aut", "cre"))
55
Description: Connects RStudio with Claude AI to enable interactive coding sessions.
66
License: MIT + file LICENSE

R/ui.R

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,18 +1564,71 @@ execute_code_in_session <- function(code, settings = NULL, agent_id = NULL) {
15641564
})
15651565
}
15661566

1567+
# Parse execution entries out of past session log files on disk. The
1568+
# in-memory history dies with the R session; the timestamped logs do not,
1569+
# so they are the cross-restart audit trail of who ran what.
1570+
past_history_entries <- function(max_files = 5L) {
1571+
settings <- load_claude_settings()
1572+
if (is.null(settings$log_file_path) || !nzchar(settings$log_file_path)) {
1573+
return(list())
1574+
}
1575+
log_dir <- dirname(settings$log_file_path)
1576+
if (!dir.exists(log_dir)) return(list())
1577+
files <- list.files(log_dir, pattern = "^clauder_.*\\.R$", full.names = TRUE)
1578+
files <- setdiff(files, normalizePath(settings$log_file_path, mustWork = FALSE))
1579+
if (length(files) == 0) return(list())
1580+
files <- files[order(file.info(files)$mtime, decreasing = TRUE)]
1581+
files <- utils::head(files, max_files)
1582+
1583+
entries <- list()
1584+
for (f in files) {
1585+
lines <- tryCatch(readLines(f, warn = FALSE), error = function(e) character(0))
1586+
starts <- grep("^# --- \\[", lines)
1587+
if (length(starts) == 0) next
1588+
ends <- c(starts[-1] - 1L, length(lines))
1589+
for (k in seq_along(starts)) {
1590+
block <- lines[starts[k]:ends[k]]
1591+
ts <- sub("^# --- \\[(.*)\\] ---$", "\\1", block[1])
1592+
agent_line <- if (length(block) >= 2) block[2] else ""
1593+
agent <- sub("^# Code executed by ([^ ]+).*$", "\\1", agent_line)
1594+
agent <- sub(":$", "", agent)
1595+
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Error: ", block)]
1596+
code_lines <- code_lines[nzchar(trimws(code_lines))]
1597+
entries[[length(entries) + 1L]] <- list(
1598+
timestamp = suppressWarnings(as.POSIXct(ts)),
1599+
agent_id = agent,
1600+
code = paste(code_lines, collapse = "\n"),
1601+
success = !grepl("(ERROR)", agent_line, fixed = TRUE),
1602+
has_plot = FALSE,
1603+
source_log = basename(f)
1604+
)
1605+
}
1606+
}
1607+
entries
1608+
}
1609+
15671610
#' Query agent execution history
15681611
#'
15691612
#' @param agent_filter "all", or a specific agent ID to filter by
15701613
#' @param requesting_agent The agent making the request (for context)
15711614
#' @param last_n Number of entries to return
1615+
#' @param include_past Also parse prior session log files on disk, so the
1616+
#' audit trail survives R restarts
15721617
#' @return Character string with formatted history
1573-
1574-
query_agent_history <- function(agent_filter = "all", requesting_agent = NULL, last_n = 20) {
1618+
query_agent_history <- function(agent_filter = "all", requesting_agent = NULL,
1619+
last_n = 20, include_past = FALSE) {
15751620
entries <- .claude_history_env$entries
15761621

1622+
if (isTRUE(include_past)) {
1623+
entries <- c(past_history_entries(), entries)
1624+
}
1625+
15771626
if (length(entries) == 0) {
1578-
return("No execution history recorded yet.")
1627+
return(if (isTRUE(include_past)) {
1628+
"No execution history in memory and no past session logs found."
1629+
} else {
1630+
"No execution history recorded yet. Pass include_past = TRUE to search prior session logs on disk."
1631+
})
15791632
}
15801633

15811634
# Filter by agent if requested
@@ -1584,7 +1637,11 @@ query_agent_history <- function(agent_filter = "all", requesting_agent = NULL, l
15841637
}
15851638

15861639
if (length(entries) == 0) {
1587-
return(sprintf("No history found for agent '%s'.", agent_filter))
1640+
return(sprintf(
1641+
"No history found for agent '%s'.%s", agent_filter,
1642+
if (isTRUE(include_past)) "" else
1643+
" Pass include_past = TRUE to also search prior session logs."
1644+
))
15881645
}
15891646

15901647
# Take last N
@@ -1596,9 +1653,12 @@ query_agent_history <- function(agent_filter = "all", requesting_agent = NULL, l
15961653
lines <- vapply(entries, function(e) {
15971654
status <- if (e$success) "OK" else "ERR"
15981655
plot_flag <- if (e$has_plot) " [plot]" else ""
1656+
src <- if (!is.null(e$source_log)) paste0(" {", e$source_log, "}") else ""
15991657
code_preview <- substr(gsub("\n", " ", e$code), 1, 80)
1600-
sprintf("[%s] %s (%s%s): %s",
1601-
format(e$timestamp, "%H:%M:%S"), e$agent_id, status, plot_flag, code_preview)
1658+
ts_txt <- tryCatch(format(e$timestamp, "%Y-%m-%d %H:%M:%S"),
1659+
error = function(err) as.character(e$timestamp))
1660+
sprintf("[%s] %s (%s%s)%s: %s",
1661+
ts_txt, e$agent_id, status, plot_flag, src, code_preview)
16021662
}, character(1))
16031663

16041664
paste(lines, collapse = "\n")

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ claudeAddin()
5050
<details>
5151
<summary><b>Recent Updates</b> (click to expand)</summary>
5252

53+
- **Agent identity and cross-restart history (R 0.11.0 / clauder-mcp 0.13.0).** Built from field reports of a multi-day, three-agent session. New `set_agent_name` tool: an agent sets its working name (for example "Claude-Stasis") once, and execution history, message attribution, presence, and its read cursor all carry that name. This fixes the case where several agents or personas share one MCP connection and collapse into a single random id. `get_session_history` gains `include_past`: it parses prior session log files on disk, so the audit of who ran what now survives R restarts. The coordination protocol now makes identity the first step of check-in.
54+
5355
- **Researcher workflow release (R 0.10.0 / clauder-mcp 0.12.0).** Three workflows that paid tools charge for, built on machinery ClaudeR already had. (1) Systematic review screening: two independent AI screeners from different model families judge every abstract against your criteria, and the new `screening_report` tool computes agreement, Cohen's kappa, PRISMA flow counts, and the conflict set, so the human reads only the disagreements. (2) Grant Panel Mode: `grant_panel_prompt(rubric = "nih")` convenes a mock study section, one reviewer per criterion, anchored weaknesses, and a ranked list of revisions that would move the score. (3) Response to Reviewers: `reviewer_response_prompt()` parses a decision letter into a point-by-point registry, reruns analyses so answers carry real computed numbers, gates until every point is answered, and exports the response letter with `export_response_letter()` plus Word comments in the manuscript.
5456

5557
- **Coordination v2 + consensus gate (R 0.9.0 / clauder-mcp 0.11.0).** The multi-agent board is now a typed, append-only event log on disk, designed from field reports of real multi-hour multi-agent sessions: typed signals instead of prose-grepping, per-agent read cursors (the shared-dataframe race is structurally impossible), reply threading, lease-based task claims, a fact store for shared state, auto-stamped presence, and a `wait_for_message` tool that blocks until a partner's event arrives, without touching the busy R session. New consensus gate: after `propose_plan()`, every execution response carries an agreement banner until all agents run `confirm_agreement()` with the required sentence verbatim. Only then is the plan marked approved. Plus `referee_prompt(stance = "reviewer2")`: a hostile-but-fair Reviewer 2 that opens with an unprimed three-sentence read (central claim, and would a strong venue accept it?) and ranks findings fatal / must-fix / minor, each tagged to the study it concerns.

clauder-mcp/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "clauder-mcp"
3-
version = "0.12.0"
3+
version = "0.13.0"
44
description = "MCP server connecting AI assistants to RStudio for interactive R coding and data analysis"
55
readme = "README.md"
66
requires-python = ">=3.10"

clauder-mcp/server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
"url": "https://github.com/IMNMV/ClaudeR",
88
"source": "github"
99
},
10-
"version": "0.12.0",
10+
"version": "0.13.0",
1111
"packages": [
1212
{
1313
"registryType": "pypi",
1414
"identifier": "clauder-mcp",
15-
"version": "0.12.0",
15+
"version": "0.13.0",
1616
"transport": {
1717
"type": "stdio"
1818
}

clauder-mcp/src/clauder_mcp/server.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ async def get_agent_introduction() -> str:
300300
lines.append(" ClaudeR::r_best_practices_prompt() - Statistical analysis protocol")
301301
lines.append(" ClaudeR::multi_agent_prompt() - Multi-agent coordination protocol")
302302
lines.append("")
303+
lines.append("Multi-agent identity: call set_agent_name with your working name (e.g.")
304+
lines.append("'Claude-Stasis') BEFORE other work, so history and messages carry a name")
305+
lines.append("your partners recognize instead of the random id above.")
306+
lines.append("")
303307
lines.append("Safety: call checkpoint_session before risky changes (overwrites, removals,")
304308
lines.append("destructive transformations); restore_session rolls the environment back.")
305309
lines.append("")
@@ -1228,6 +1232,10 @@ async def list_tools() -> List[types.Tool]:
12281232
"last_n": {
12291233
"type": "number",
12301234
"description": "Number of recent entries to return (default 20)"
1235+
},
1236+
"include_past": {
1237+
"type": "boolean",
1238+
"description": "Also parse prior session log files on disk, so the audit trail survives R restarts. Entries from past logs are tagged {logfile}. Default false."
12311239
}
12321240
}
12331241
},
@@ -1477,6 +1485,36 @@ async def list_tools() -> List[types.Tool]:
14771485
"openWorldHint": False,
14781486
}
14791487
),
1488+
types.Tool(
1489+
name="set_agent_name",
1490+
description=(
1491+
"Set this agent's working identity for the rest of the session. Call this "
1492+
"FIRST in any multi-agent work, before executing code or sending messages, "
1493+
"so execution history, message attribution, presence, and your read cursor "
1494+
"all carry your working name (e.g. 'Claude-Stasis') instead of a random "
1495+
"per-connection id. Critical when several agents or personas share one MCP "
1496+
"connection (subagents), where the default id cannot tell them apart. Pick "
1497+
"a short name unique to you and reuse it across sessions. For a permanent "
1498+
"name, set the CLAUDER_AGENT_ID environment variable in the MCP server "
1499+
"registration instead."
1500+
),
1501+
inputSchema={
1502+
"type": "object",
1503+
"properties": {
1504+
"name": {
1505+
"type": "string",
1506+
"description": "The identity to use: 1-40 chars, letters, digits, dash, underscore; must start with a letter or digit."
1507+
}
1508+
},
1509+
"required": ["name"]
1510+
},
1511+
annotations={
1512+
"readOnlyHint": False,
1513+
"destructiveHint": False,
1514+
"idempotentHint": True,
1515+
"openWorldHint": False,
1516+
}
1517+
),
14801518
types.Tool(
14811519
name="send_message",
14821520
description=(
@@ -1770,7 +1808,7 @@ async def list_tools() -> List[types.Tool]:
17701808
@server.call_tool()
17711809
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextContent | types.ImageContent]:
17721810
"""Handle R tool calls."""
1773-
global _target_session, _agent_introduced
1811+
global _target_session, _agent_introduced, _agent_id
17741812

17751813
# These tools check Python-side state only — skip addin check
17761814
_skip_addin_check = {"list_sessions", "connect_session", "load_annotation_data", "annotate", "run_annotation_job", "get_annotation_job_status", "cancel_annotation_job",
@@ -2338,7 +2376,8 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextCont
23382376
else:
23392377
filter_value = escape_r_string(agent_filter)
23402378

2341-
r_code = f'ClaudeR:::query_agent_history("{filter_value}", "{escape_r_string(_agent_id or "unknown")}", {last_n})'
2379+
include_past = "TRUE" if arguments.get("include_past") else "FALSE"
2380+
r_code = f'ClaudeR:::query_agent_history("{filter_value}", "{escape_r_string(_agent_id or "unknown")}", {last_n}, include_past = {include_past})'
23422381
result = await execute_r_code_via_addin(r_code)
23432382

23442383
if not result.get("success", False):
@@ -2937,6 +2976,25 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextCont
29372976
))
29382977
return result_contents
29392978

2979+
elif name == "set_agent_name":
2980+
new_name = (arguments.get("name") or "").strip()
2981+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,39}", new_name):
2982+
return [types.TextContent(
2983+
type="text",
2984+
text="Error: invalid name. Use 1-40 characters: letters, digits, dash, underscore; start with a letter or digit."
2985+
)]
2986+
old_name = _agent_id
2987+
_agent_id = new_name
2988+
return [types.TextContent(
2989+
type="text",
2990+
text=(
2991+
f"Agent identity set: {old_name} -> {new_name}. Execution history, "
2992+
f"coordination messages, presence, and your read cursor now use this name. "
2993+
f"If you had already sent messages as {old_name}, mention the rename to "
2994+
f"your partners so they can map the two."
2995+
)
2996+
)]
2997+
29402998
elif name == "send_message":
29412999
body = arguments.get("body")
29423000
if body is None or (isinstance(body, str) and not body.strip()):

inst/prompts/multi_agent.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@ Two ways to use it, same log underneath:
1919

2020
## Phase 0: Check in
2121

22-
1. `coordination_roster` -- who is here, who is stale.
23-
2. `check_messages` -- read everything unread addressed to you or to all.
24-
3. `consensus_status()` (via execute_r) -- is there a plan already, and is
22+
1. `set_agent_name` -- set your working identity FIRST (e.g. "Claude-Stasis").
23+
Do this before any code execution or message. It names your execution
24+
history, message attribution, presence, and read cursor. Without it,
25+
agents that share one MCP connection (subagents, personas) collapse into
26+
one random id and cannot be told apart afterward. Reuse the same name
27+
every session; for a permanent name, set the CLAUDER_AGENT_ID environment
28+
variable in your MCP registration.
29+
2. `coordination_roster` -- who is here, who is stale.
30+
3. `check_messages` -- read everything unread addressed to you or to all.
31+
4. `consensus_status()` (via execute_r) -- is there a plan already, and is
2532
it approved?
2633

2734
If a plan exists and is approved, skip negotiation: claim an open task and

0 commit comments

Comments
 (0)