Skip to content

Commit 0538b73

Browse files
IMNMVclaude
andcommitted
Log agent output, and stop output capture from breaking logging
ClaudeR 0.14.1. Two problems from a user report. Agent output was never logged. log_code_to_file() is called before the code runs, so the entry could only ever hold the code. The executed output is already captured for the response, so it is now appended to the log entry as well. The output capture added in 0.14.0 could stop console logging altogether. start_console_logging() opened its sink before registering the task callback, so a failure opening the sink aborted the function and no commands were logged at all, which is worse than the gap it was fixing. Opening the sink is now optional: on failure it degrades to logging commands and their values, and says so. Three further robustness fixes around the shared sink stack: - execute_r unwound with a fallback that popped a sink when it had not opened one, which could remove the console sink. It no longer pops what it does not own. - The console sink is restored if something else unwinds it, instead of silently recording nothing from that point on. - execute_r opens its sink with split = TRUE on top of the console sink, so agent output cascaded into the console buffer and would have been attributed to the user. That buffer is now discarded after each run. Logged output lines carry a #> marker, and replay, past-history and notebook export skip them so output is never re-run as code. The same filters now also skip the console entry header, which they did not. R CMD check: Status OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 84c2b2a commit 0538b73

5 files changed

Lines changed: 84 additions & 18 deletions

File tree

‎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.14.0
3+
Version: 0.14.1
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/console_log.R‎

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ console_note_condition <- function(kind, msg) {
4646
msg <- trimws(paste(msg, collapse = " "))
4747
if (!nzchar(msg)) return(invisible(NULL))
4848
.console_state$pending <- c(.console_state$pending,
49-
sprintf("# %s: %s", kind, msg))
49+
sprintf("#> %s: %s", kind, msg))
5050
invisible(NULL)
5151
}
5252

@@ -59,6 +59,7 @@ console_sink_open <- function() {
5959
.console_state$sink_con <- con
6060
.console_state$consumed <- 0L
6161
sink(con, split = TRUE)
62+
.console_state$sink_owned <- TRUE
6263
invisible(TRUE)
6364
}
6465

@@ -68,7 +69,10 @@ console_sink_close <- function() {
6869
con <- .console_state$sink_con
6970
if (is.null(con)) return(invisible(FALSE))
7071
tryCatch({
71-
if (sink.number() > 0) sink()
72+
# Only pop if ours is the sink on top. Popping blindly would remove one
73+
# that an agent execution opened and leave that execution writing nowhere.
74+
if (isTRUE(.console_state$sink_owned) && sink.number() > 0) sink()
75+
.console_state$sink_owned <- FALSE
7276
flush(con); close(con)
7377
}, error = function(e) NULL)
7478
tryCatch(unlink(.console_state$sink_file), error = function(e) NULL)
@@ -95,8 +99,22 @@ console_drain <- function() {
9599

96100
`%||%` <- function(a, b) if (is.null(a)) b else a
97101

102+
# Our sink can be removed by something else unwinding the sink stack. At top
103+
# level, with every execution finished, ours should be the only one left. If it
104+
# is gone, put it back rather than silently recording nothing from here on.
105+
console_sink_heal <- function() {
106+
if (!isTRUE(.console_state$active)) return(invisible(FALSE))
107+
if (isTRUE(.console_state$sink_owned) && sink.number() >= 1) return(invisible(FALSE))
108+
if (sink.number() != 0) return(invisible(FALSE))
109+
con <- .console_state$sink_con
110+
if (!is.null(con)) tryCatch(close(con), error = function(e) NULL)
111+
tryCatch(console_sink_open(), error = function(e) NULL)
112+
invisible(TRUE)
113+
}
114+
98115
console_task_callback <- function(expr, value, ok, visible) {
99-
# Never let logging break the user's session.
116+
# A task callback that signals an error is removed by R, which would end
117+
# console logging silently, so nothing here is allowed to escape.
100118
tryCatch({
101119
if (is.null(console_log_path())) return(TRUE)
102120
code <- paste(deparse(expr), collapse = "\n")
@@ -107,21 +125,22 @@ console_task_callback <- function(expr, value, ok, visible) {
107125
lines <- character(0)
108126
printed <- console_drain()
109127
if (length(printed)) {
110-
lines <- paste("#", console_trim(printed))
128+
lines <- paste0("#> ", console_trim(printed))
111129
} else if (isTRUE(visible) && isTRUE(ok)) {
112130
# Nothing was printed as a side effect, so fall back to the value itself.
113131
out <- tryCatch(utils::capture.output(print(value)),
114132
error = function(e) character(0))
115-
if (length(out)) lines <- paste("#", console_trim(out))
133+
if (length(out)) lines <- paste0("#> ", console_trim(out))
116134
}
117-
if (!isTRUE(ok)) lines <- c(lines, "# error: command did not complete")
135+
if (!isTRUE(ok)) lines <- c(lines, "#> error: command did not complete")
118136
if (length(.console_state$pending)) {
119137
lines <- c(.console_state$pending, lines)
120138
.console_state$pending <- NULL
121139
}
122140
body <- if (length(lines)) paste0(code, "\n", paste(lines, collapse = "\n")) else code
123141
console_write(body, tag = "user")
124142
}, error = function(e) NULL)
143+
tryCatch(console_sink_heal(), error = function(e) NULL)
125144
TRUE
126145
}
127146

@@ -136,12 +155,11 @@ console_task_callback <- function(expr, value, ok, visible) {
136155
start_console_logging <- function() {
137156
if (isTRUE(.console_state$active)) return(invisible(TRUE))
138157
.console_state$pending <- NULL
139-
console_sink_open()
140-
.console_state$handle <- addTaskCallback(console_task_callback,
141-
name = "clauder_console_log")
158+
# Order matters. globalCallingHandlers() cannot be wrapped in tryCatch (it
159+
# refuses to run with handlers on the stack), so it goes first: if it fails,
160+
# it fails before a sink or a callback has been installed, leaving nothing
161+
# half-configured behind.
142162
if (getRversion() >= "4.0.0") {
143-
# globalCallingHandlers() refuses to run with handlers on the stack, so it
144-
# must not be wrapped in tryCatch here.
145163
.console_state$old_handlers <- globalCallingHandlers()
146164
# Observe only. Do NOT invoke the muffle restarts: the user must still see
147165
# their own warnings and messages in the console.
@@ -150,18 +168,29 @@ start_console_logging <- function() {
150168
message = function(m) console_note_condition("message", conditionMessage(m))
151169
)
152170
}
171+
# Capturing printed output is the optional half. If the sink cannot be opened
172+
# the callback must still be registered, or a failure here would silently
173+
# stop commands being logged at all, which is worse than losing the output.
174+
ok_sink <- isTRUE(tryCatch({ console_sink_open(); TRUE },
175+
error = function(e) FALSE))
176+
.console_state$handle <- addTaskCallback(console_task_callback,
177+
name = "clauder_console_log")
153178
.console_state$old_error <- getOption("error")
154179
options(error = function() {
155180
tryCatch({
156181
msg <- geterrmessage()
157-
console_write(sprintf("# error: %s", trimws(msg)), tag = "user")
182+
console_write(sprintf("#> error: %s", trimws(msg)), tag = "user")
158183
}, error = function(e) NULL)
159184
})
160185
reg.finalizer(.console_state,
161186
function(e) tryCatch(console_sink_close(), error = function(x) NULL),
162187
onexit = TRUE)
163188
.console_state$active <- TRUE
164189
message("ClaudeR: console logging on. Your console commands now appear in the session log.")
190+
if (!ok_sink) {
191+
message("ClaudeR: could not capture printed output here; ",
192+
"commands and their values will still be logged.")
193+
}
165194
# Our own startup message must not show up as the user's first log entry.
166195
.console_state$pending <- NULL
167196
invisible(TRUE)

‎R/notebook.R‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export_log_as_notebook <- function(log_path = NULL, output_path = NULL,
9292
is_error <- grepl("(ERROR)", agent_line, fixed = TRUE)
9393
err_msgs <- sub("^# Error: ", "", block[grepl("^# Error: ", block)])
9494

95-
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Error: ", block)]
95+
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Run by |^# Error: |^#> ", block)]
9696
while (length(code_lines) > 0 && trimws(code_lines[length(code_lines)]) == "") {
9797
code_lines <- code_lines[-length(code_lines)]
9898
}

‎R/ui.R‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,21 @@ execute_code_in_session <- function(code, settings = NULL, agent_id = NULL) {
14411441
output <- c(output, collected_conditions)
14421442
}
14431443

1444+
# This execution's sink was opened on top of the console-logging sink (if
1445+
# one is active), and split = TRUE cascades, so everything printed here has
1446+
# also landed in the console buffer. Drop it, or the next thing the user
1447+
# types would carry the agent's output as if the user had produced it.
1448+
tryCatch(console_drain(), error = function(e) NULL)
1449+
1450+
# Record what the code printed, not just the code. The log is the one file
1451+
# an agent reads back to see what happened, and until now it showed the
1452+
# call but never its result.
1453+
if (settings$log_to_file && !is.null(settings$log_file_path) &&
1454+
settings$log_file_path != "" && length(output) > 0) {
1455+
tryCatch(log_output_to_file(output, settings$log_file_path),
1456+
error = function(e) NULL)
1457+
}
1458+
14441459
# --- AFTER eval: only capture if a NEW plot was actually created ---
14451460
captured_plot <- FALSE
14461461
plot_data <- NULL
@@ -1583,7 +1598,10 @@ execute_code_in_session <- function(code, settings = NULL, agent_id = NULL) {
15831598
# Unwind any sinks opened during this execution
15841599
if (exists("sink_depth")) {
15851600
while (sink.number() > sink_depth) sink()
1586-
} else if (sink.number() > 0) sink()
1601+
}
1602+
# No sink_depth means we failed before opening one. Do NOT pop blindly:
1603+
# console logging keeps a sink of its own underneath, and popping it
1604+
# silently stops the user's console from being recorded.
15871605

15881606
# Recover whatever was printed before the error: partial output plus
15891607
# captured warnings/messages are often exactly the context the agent
@@ -1636,7 +1654,10 @@ execute_code_in_session <- function(code, settings = NULL, agent_id = NULL) {
16361654
# Unwind any sinks this execution opened
16371655
if (exists("sink_depth")) {
16381656
while (sink.number() > sink_depth) sink()
1639-
} else if (sink.number() > 0) sink()
1657+
}
1658+
# No sink_depth means we failed before opening one. Do NOT pop blindly:
1659+
# console logging keeps a sink of its own underneath, and popping it
1660+
# silently stops the user's console from being recorded.
16401661

16411662
# Clean up temporary files
16421663
if (exists("output_file") && file.exists(output_file)) {
@@ -1681,7 +1702,7 @@ past_history_entries <- function(max_files = 5L) {
16811702
agent_line <- if (length(block) >= 2) block[2] else ""
16821703
agent <- sub("^# Code executed by ([^ ]+).*$", "\\1", agent_line)
16831704
agent <- sub(":$", "", agent)
1684-
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Error: ", block)]
1705+
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Run by |^# Error: |^#> ", block)]
16851706
code_lines <- code_lines[nzchar(trimws(code_lines))]
16861707
entries[[length(entries) + 1L]] <- list(
16871708
timestamp = suppressWarnings(as.POSIXct(ts)),
@@ -1799,6 +1820,20 @@ validate_code_security <- function(code) {
17991820
return(list(blocked = FALSE))
18001821
}
18011822

1823+
# Append what a command printed, under the entry just written for it.
1824+
# The "#> " prefix marks these lines as output rather than code, so replay,
1825+
# history and notebook export skip them.
1826+
log_output_to_file <- function(output, log_path, max_lines = 40L) {
1827+
if (length(output) == 0) return(invisible(NULL))
1828+
if (length(output) > max_lines) {
1829+
output <- c(output[seq_len(max_lines)],
1830+
sprintf("... %d more lines not logged", length(output) - max_lines))
1831+
}
1832+
entry <- paste0(paste0("#> ", output, collapse = "\n"), "\n\n")
1833+
tryCatch(cat(entry, file = log_path, append = TRUE), error = function(e) NULL)
1834+
invisible(NULL)
1835+
}
1836+
18021837
#' Log code to file
18031838
#'
18041839
#' @param code The R code to log
@@ -1971,7 +2006,7 @@ export_log_as_script <- function(log_path = NULL, output_path = NULL, include_er
19712006

19722007
# Extract code lines (skip the header comments)
19732008
# Header lines: "# --- [timestamp] ---", "# Code executed by ...", "# Error: ..."
1974-
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Error: |^#\\s*$", block)]
2009+
code_lines <- block[!grepl("^# --- \\[|^# Code executed by |^# Run by |^# Error: |^#> |^#\\s*$", block)]
19752010

19762011
# Remove trailing blank lines
19772012
while (length(code_lines) > 0 && code_lines[length(code_lines)] == "") {

‎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+
- **Logging fixes (R 0.14.1).** Two problems from a user report. Agent output was never written to the log, only the code, because the entry was written before the code ran; the log now records what each call printed. And the output capture added in 0.14.0 could stop console logging entirely: it opened its own output sink before registering the console callback, so if that sink could not be opened, nothing was logged at all. Opening it is now optional and failing back to logging commands and their values, the sink is restored if another execution unwinds it, and an agent run no longer pops a sink it did not open. Logged output lines are marked so that replay, history and notebook export skip them.
54+
5355
- **Console logging now captures output, not just results (R 0.14.0).** From follow-up on the logging request. The log recorded the value a command returned, so anything printed as a side effect was missing: `cat()`, progress output, and `print()` called inside a function. Running `f()` showed the call but not what `f()` printed, which is exactly the context an agent needs when you hand it an error you hit yourself. Standard output is now teed to the log and grouped under the command that produced it, alongside the warnings, messages, and errors already captured. Your console still shows everything as normal.
5456

5557
- **Windows session-liveness fix (R 0.13.2).** Checking whether a recorded R session was still alive used `tools::pskill(pid, signal = 0)`. That is the standard idiom on Unix, but on Windows `pskill` always calls `TerminateProcess` regardless of signal, so the check killed the session it was asking about, then reported it as alive and left the stale discovery file in place. Starting a server could therefore terminate another RStudio session and leave agents routed to a dead port. Liveness is now probed without signalling. Separately, registering a session whose name is already held by a different live session now warns instead of silently overwriting its discovery file, which had left agents holding the wrong port and token.

0 commit comments

Comments
 (0)