Skip to content

Commit 2e8da30

Browse files
IMNMVclaude
andcommitted
Arm the console-log callback before anything that can fail (addin path)
ClaudeR 0.14.2. From a user report with screenshot: on 0.14.1 the addin checkbox "Also log my own console commands" logged nothing, while the same feature started from the console worked. globalCallingHandlers() may only be called with an empty handler stack. The addin checkbox is handled inside a Shiny observer, where handlers are on the stack, so it errors with "should not be called with handlers on the stack". 0.14.1 had moved that call to the front of start_console_logging(), so the error aborted the function before addTaskCallback() ran and the user's commands were never recorded. (It also fails inside task callbacks, later() callbacks and tryCatch, so it cannot be deferred or guarded; only a bare top-level call works.) Now: the task callback is registered first and unconditionally, the sink and options(error=) follow, and globalCallingHandlers() is attempted last and only when sys.nframe() shows a bare top-level call. Without it, warnings are read from last.warning after each command (reported only when changed, since it lingers); message() output is not captured from the addin path and the startup message says so. stop_console_logging() likewise removes the handlers only at top level; otherwise they stay installed as no-ops (console_note_condition checks the active flag). start also re-arms if the callback has gone missing. Verified: start/stop from inside withCallingHandlers register/remove the callback without error and log commands, printed output and warnings; bare top-level start installs the handlers and captures message(). R CMD check: Status OK. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 0538b73 commit 2e8da30

3 files changed

Lines changed: 80 additions & 28 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.1
3+
Version: 0.14.2
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: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
# addTaskCallback the expression the user typed, and its visible value
66
# globalCallingHandlers warnings and messages (they go to stderr, so a sink
77
# cannot see them; R >= 4.0 lets us observe without
8-
# suppressing)
8+
# suppressing). CONSTRAINT: it can only be called with
9+
# an empty handler stack, i.e. from a bare top-level
10+
# console call. Inside a Shiny observer (the addin
11+
# checkbox), a task callback, a later() callback, or
12+
# tryCatch() it errors "should not be called with
13+
# handlers on the stack". So it is attempted only when
14+
# sys.nframe() says we are at top level; otherwise
15+
# warnings are read from last.warning after each
16+
# command instead, and message() output is not captured.
917
# options(error=) uncaught errors
1018
# sink(split = TRUE) everything printed as a side effect: cat(), progress
1119
# output, print() called inside a function. The task
@@ -43,6 +51,7 @@ console_trim <- function(x, max_lines = 40L) {
4351
}
4452

4553
console_note_condition <- function(kind, msg) {
54+
if (!isTRUE(.console_state$active)) return(invisible(NULL))
4655
msg <- trimws(paste(msg, collapse = " "))
4756
if (!nzchar(msg)) return(invisible(NULL))
4857
.console_state$pending <- c(.console_state$pending,
@@ -133,6 +142,17 @@ console_task_callback <- function(expr, value, ok, visible) {
133142
if (length(out)) lines <- paste0("#> ", console_trim(out))
134143
}
135144
if (!isTRUE(ok)) lines <- c(lines, "#> error: command did not complete")
145+
# Without globalCallingHandlers (see header), pick warnings up from
146+
# last.warning. It lingers across commands, so only report it when it
147+
# changed since the previous command.
148+
if (!isTRUE(.console_state$gch_installed)) {
149+
lw <- tryCatch(get0("last.warning", envir = baseenv(), inherits = FALSE),
150+
error = function(e) NULL)
151+
if (!is.null(lw) && !identical(lw, .console_state$last_lw)) {
152+
.console_state$last_lw <- lw
153+
lines <- c(lines, paste0("#> warning: ", names(lw)))
154+
}
155+
}
136156
if (length(.console_state$pending)) {
137157
lines <- c(.console_state$pending, lines)
138158
.console_state$pending <- NULL
@@ -153,45 +173,67 @@ console_task_callback <- function(expr, value, ok, visible) {
153173
#' @return Invisibly TRUE if capture started.
154174
#' @export
155175
start_console_logging <- function() {
156-
if (isTRUE(.console_state$active)) return(invisible(TRUE))
176+
# Called from the console (top level) OR from the addin's Shiny observer.
177+
at_top <- sys.nframe() == 1L
178+
registered <- "clauder_console_log" %in% getTaskCallbackNames()
179+
if (isTRUE(.console_state$active) && registered) return(invisible(TRUE))
157180
.console_state$pending <- NULL
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.
162-
if (getRversion() >= "4.0.0") {
181+
.console_state$last_lw <- tryCatch(get0("last.warning", envir = baseenv(), inherits = FALSE),
182+
error = function(e) NULL)
183+
184+
# 1. The essential half, first and unconditionally: the task callback that
185+
# writes each command. Nothing below may prevent this from being armed.
186+
if (!registered) {
187+
.console_state$handle <- addTaskCallback(console_task_callback,
188+
name = "clauder_console_log")
189+
}
190+
191+
# 2. Printed output (optional). If the sink cannot be opened we still log
192+
# commands and their values.
193+
ok_sink <- isTRUE(tryCatch({ console_sink_open(); TRUE }, error = function(e) FALSE))
194+
195+
# 3. Uncaught errors.
196+
if (is.null(.console_state$old_error_set)) {
197+
.console_state$old_error <- getOption("error")
198+
.console_state$old_error_set <- TRUE
199+
}
200+
options(error = function() {
201+
tryCatch({
202+
msg <- geterrmessage()
203+
console_write(sprintf("#> error: %s", trimws(msg)), tag = "user")
204+
}, error = function(e) NULL)
205+
})
206+
207+
.console_state$active <- TRUE
208+
209+
# 4. Warnings and messages via globalCallingHandlers, LAST, and only from a
210+
# bare top-level call (see header). Not wrapped in tryCatch: that would
211+
# itself put a handler on the stack and guarantee the error.
212+
if (!isTRUE(.console_state$gch_installed) && at_top && getRversion() >= "4.0.0") {
163213
.console_state$old_handlers <- globalCallingHandlers()
164214
# Observe only. Do NOT invoke the muffle restarts: the user must still see
165215
# their own warnings and messages in the console.
166216
globalCallingHandlers(
167217
warning = function(w) console_note_condition("warning", conditionMessage(w)),
168218
message = function(m) console_note_condition("message", conditionMessage(m))
169219
)
220+
.console_state$gch_installed <- TRUE
170221
}
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")
178-
.console_state$old_error <- getOption("error")
179-
options(error = function() {
180-
tryCatch({
181-
msg <- geterrmessage()
182-
console_write(sprintf("#> error: %s", trimws(msg)), tag = "user")
183-
}, error = function(e) NULL)
184-
})
222+
185223
reg.finalizer(.console_state,
186224
function(e) tryCatch(console_sink_close(), error = function(x) NULL),
187225
onexit = TRUE)
188-
.console_state$active <- TRUE
189226
message("ClaudeR: console logging on. Your console commands now appear in the session log.")
190227
if (!ok_sink) {
191228
message("ClaudeR: could not capture printed output here; ",
192229
"commands and their values will still be logged.")
193230
}
194-
# Our own startup message must not show up as the user's first log entry.
231+
if (!isTRUE(.console_state$gch_installed)) {
232+
message("ClaudeR: warnings are logged from last.warning; message() output is not ",
233+
"captured when logging is started from the addin. Run ",
234+
"start_console_logging() in the console for full capture.")
235+
}
236+
# Our own startup messages must not show up as the user's first log entry.
195237
.console_state$pending <- NULL
196238
invisible(TRUE)
197239
}
@@ -201,17 +243,25 @@ start_console_logging <- function() {
201243
#' @return Invisibly TRUE.
202244
#' @export
203245
stop_console_logging <- function() {
204-
if (!isTRUE(.console_state$active)) return(invisible(TRUE))
246+
at_top <- sys.nframe() == 1L
247+
if (!isTRUE(.console_state$active) &&
248+
!("clauder_console_log" %in% getTaskCallbackNames())) return(invisible(TRUE))
205249
tryCatch(removeTaskCallback("clauder_console_log"), error = function(e) NULL)
206250
console_sink_close()
207-
if (getRversion() >= "4.0.0") {
251+
if (isTRUE(.console_state$gch_installed) && at_top) {
208252
# Drop ours, then put back whatever was registered before, so handlers
209-
# belonging to other packages survive.
253+
# belonging to other packages survive. Only possible at bare top level;
254+
# from the addin the handlers stay installed but become no-ops because
255+
# console_note_condition() checks .console_state$active.
210256
globalCallingHandlers(NULL)
211257
old <- .console_state$old_handlers
212258
if (length(old)) do.call(globalCallingHandlers, old)
259+
.console_state$gch_installed <- FALSE
260+
}
261+
if (isTRUE(.console_state$old_error_set)) {
262+
options(error = .console_state$old_error)
263+
.console_state$old_error_set <- NULL
213264
}
214-
options(error = .console_state$old_error)
215265
.console_state$active <- FALSE
216266
.console_state$pending <- NULL
217267
message("ClaudeR: console logging off.")

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 from the addin fixed (R 0.14.2).** Ticking "Also log my own console commands" in the addin armed nothing on 0.14.1, while calling `start_console_logging()` in the console worked. Cause: `globalCallingHandlers()` (used to observe warnings and messages) can only be called with an empty handler stack, and inside the Shiny observer that handles the checkbox it errors, which aborted the function before the console callback was registered. The callback is now registered first and unconditionally; the handlers are installed only when logging is started from the console, and otherwise warnings are read from `last.warning` after each command. Started from the addin, `message()` output is the one thing not captured, and the addin says so.
54+
5355
- **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.
5456

5557
- **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.

0 commit comments

Comments
 (0)