Skip to content

Commit 84c2b2a

Browse files
IMNMVclaude
andcommitted
Capture console output, not just returned values (issue #27 follow-up)
ClaudeR 0.14.0. The task callback only ever sees the value a command returns, so the log showed f() but not what f() printed. cat(), progress output and print() inside a function were all missing, which is the usual content of an error a user wants to hand to an agent. Standard output is now teed to a scratch file with sink(split = TRUE), so the console still shows everything, and drained per command by read offset. Reopening the file mid-session corrupts the sink stack, so the offset is tracked instead. Printed output is preferred over the returned value when both exist. Verified that an agent execute_r call, which pushes its own capture.output on top of this sink, still captures its own output and does not leak into the user attribution. Teardown pops the sink on stop and on session exit via a finalizer, guarded by sink.number(). R CMD check: Status OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c2a2a07 commit 84c2b2a

3 files changed

Lines changed: 63 additions & 2 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.13.2
3+
Version: 0.14.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/console_log.R

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
# cannot see them; R >= 4.0 lets us observe without
88
# suppressing)
99
# options(error=) uncaught errors
10+
# sink(split = TRUE) everything printed as a side effect: cat(), progress
11+
# output, print() called inside a function. The task
12+
# callback only ever sees the value a command returns,
13+
# so without this the log shows f() but not what f()
14+
# printed. split = TRUE keeps it visible in the console.
1015
# sink(type = "message") is deliberately NOT used: it cannot split, so it would
1116
# swallow the user's own errors in the console.
1217

@@ -45,6 +50,51 @@ console_note_condition <- function(kind, msg) {
4550
invisible(NULL)
4651
}
4752

53+
# Start teeing stdout to a scratch file. split = TRUE so the console still
54+
# shows everything; we only read a copy.
55+
console_sink_open <- function() {
56+
f <- tempfile(fileext = ".txt")
57+
con <- file(f, open = "wt")
58+
.console_state$sink_file <- f
59+
.console_state$sink_con <- con
60+
.console_state$consumed <- 0L
61+
sink(con, split = TRUE)
62+
invisible(TRUE)
63+
}
64+
65+
# Pop our sink, being careful not to disturb a capture.output() that an agent
66+
# call may have pushed on top of it.
67+
console_sink_close <- function() {
68+
con <- .console_state$sink_con
69+
if (is.null(con)) return(invisible(FALSE))
70+
tryCatch({
71+
if (sink.number() > 0) sink()
72+
flush(con); close(con)
73+
}, error = function(e) NULL)
74+
tryCatch(unlink(.console_state$sink_file), error = function(e) NULL)
75+
.console_state$sink_con <- NULL
76+
.console_state$sink_file <- NULL
77+
.console_state$consumed <- 0L
78+
invisible(TRUE)
79+
}
80+
81+
# Lines printed since the previous command. Reads by offset rather than
82+
# truncating, because reopening the file mid-session corrupts the sink stack.
83+
console_drain <- function() {
84+
con <- .console_state$sink_con
85+
f <- .console_state$sink_file
86+
if (is.null(con) || is.null(f)) return(character(0))
87+
tryCatch({
88+
flush(con)
89+
all <- readLines(f, warn = FALSE)
90+
seen <- .console_state$consumed %||% 0L
91+
.console_state$consumed <- length(all)
92+
if (length(all) > seen) all[(seen + 1L):length(all)] else character(0)
93+
}, error = function(e) character(0))
94+
}
95+
96+
`%||%` <- function(a, b) if (is.null(a)) b else a
97+
4898
console_task_callback <- function(expr, value, ok, visible) {
4999
# Never let logging break the user's session.
50100
tryCatch({
@@ -55,7 +105,11 @@ console_task_callback <- function(expr, value, ok, visible) {
55105
if (grepl("^(start_console_logging|stop_console_logging|ClaudeR:::)", code)) return(TRUE)
56106

57107
lines <- character(0)
58-
if (isTRUE(visible) && isTRUE(ok)) {
108+
printed <- console_drain()
109+
if (length(printed)) {
110+
lines <- paste("#", console_trim(printed))
111+
} else if (isTRUE(visible) && isTRUE(ok)) {
112+
# Nothing was printed as a side effect, so fall back to the value itself.
59113
out <- tryCatch(utils::capture.output(print(value)),
60114
error = function(e) character(0))
61115
if (length(out)) lines <- paste("#", console_trim(out))
@@ -82,6 +136,7 @@ console_task_callback <- function(expr, value, ok, visible) {
82136
start_console_logging <- function() {
83137
if (isTRUE(.console_state$active)) return(invisible(TRUE))
84138
.console_state$pending <- NULL
139+
console_sink_open()
85140
.console_state$handle <- addTaskCallback(console_task_callback,
86141
name = "clauder_console_log")
87142
if (getRversion() >= "4.0.0") {
@@ -102,6 +157,9 @@ start_console_logging <- function() {
102157
console_write(sprintf("# error: %s", trimws(msg)), tag = "user")
103158
}, error = function(e) NULL)
104159
})
160+
reg.finalizer(.console_state,
161+
function(e) tryCatch(console_sink_close(), error = function(x) NULL),
162+
onexit = TRUE)
105163
.console_state$active <- TRUE
106164
message("ClaudeR: console logging on. Your console commands now appear in the session log.")
107165
# Our own startup message must not show up as the user's first log entry.
@@ -116,6 +174,7 @@ start_console_logging <- function() {
116174
stop_console_logging <- function() {
117175
if (!isTRUE(.console_state$active)) return(invisible(TRUE))
118176
tryCatch(removeTaskCallback("clauder_console_log"), error = function(e) NULL)
177+
console_sink_close()
119178
if (getRversion() >= "4.0.0") {
120179
# Drop ours, then put back whatever was registered before, so handlers
121180
# belonging to other packages survive.

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+
- **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.
54+
5355
- **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.
5456

5557
- **Unified console logging (R 0.13.0).** The session log recorded what the agent ran, but not what you ran, so asking an agent to explain an error you hit in the console meant re-running the work through it. Tick "Also log my own console commands" under Logging and your console activity joins the same file, tagged by who ran what: the command, its printed result, and any warnings, messages, or errors. "Read the last 100 lines of the log" is now enough for an agent to see both sides of the session. Off by default; toggle it in the addin or call `start_console_logging()` / `stop_console_logging()`.

0 commit comments

Comments
 (0)