Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Package: pensar
Type: Package
Title: LLM Wiki Engine
Version: 0.6.3.2
Date: 2026-05-19
Version: 0.6.3.3
Date: 2026-06-03
Authors@R: c(
person("Troy", "Hernandez", role = c("aut", "cre"),
email = "troy@cornball.ai",
Expand Down
11 changes: 11 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# pensar 0.6.3.3 (dev)

## Bug fixes

* `autoresearch()` no longer crashes when a search query returns zero
results. Tagging the results with `date`, `source`, `query`, and
`angle` assigned a length-1 value onto a 0-row data.frame and errored
with "replacement has 1 row, data has 0". The validator now builds all
columns at full length before filtering empty URLs, keeping `date` and
`source` aligned when rows are dropped.

# pensar 0.6.3.2 (dev)

## Changes
Expand Down
27 changes: 11 additions & 16 deletions R/autoresearch_steps.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ autoresearch_run_searches <- function(queries, search_backend, program,
queries$query[[i]])
raw <- search_backend(queries$query[[i]], program$max_sources_per_round)
df <- .validate_autoresearch_search_results(raw)
df$query <- queries$query[[i]]
df$angle <- queries$angle[[i]]
df$query <- rep.int(queries$query[[i]], nrow(df))
df$angle <- rep.int(queries$angle[[i]], nrow(df))
results[[i]] <- df
.ar_msg(verbose, " search ", i, "/", nrow(queries), ": ",
nrow(df), " ", if (nrow(df) == 1L) "result" else "results")
Expand Down Expand Up @@ -564,21 +564,16 @@ autoresearch_write_pages <- function(pages, vault, program, overwrite = TRUE,
stop("search_backend() result missing column(s): ",
paste(missing, collapse = ", "), call. = FALSE)
}
out <- x[, required, drop = FALSE]
out$title <- as.character(out$title)
out$url <- as.character(out$url)
out$snippet <- as.character(out$snippet)
date <- if ("date" %in% names(x)) as.character(x$date) else rep("", nrow(x))
source <- if ("source" %in% names(x)) as.character(x$source) else rep("", nrow(x))
out <- data.frame(
title = as.character(x$title),
url = as.character(x$url),
snippet = as.character(x$snippet),
date = date,
source = source,
stringsAsFactors = FALSE)
out <- out[nzchar(out$url),, drop = FALSE]
if ("date" %in% names(x)) {
out$date <- as.character(x$date)
} else {
out$date <- ""
}
if ("source" %in% names(x)) {
out$source <- as.character(x$source)
} else {
out$source <- ""
}
rownames(out) <- NULL
out
}
Expand Down
26 changes: 26 additions & 0 deletions inst/tinytest/test_autoresearch.R
Original file line number Diff line number Diff line change
Expand Up @@ -1069,3 +1069,29 @@ setTimeLimit(cpu = Inf, elapsed = Inf, transient = FALSE)
expect_true(!is.null(res_timeout))
expect_equal(nrow(res_timeout$claims), 1L)
unlink(v_timeout, recursive = TRUE)

# A search query that returns zero results must not crash validation or the
# search loop (regression: scalar date/source/query assignment onto a 0-row
# data.frame errored with "replacement has 1 row, data has 0").
empty_search <- function(query, n) {
data.frame(title = character(), url = character(),
snippet = character(), stringsAsFactors = FALSE)
}
val_empty <- pensar:::.validate_autoresearch_search_results(empty_search("q", 5L))
expect_equal(nrow(val_empty), 0L)
expect_true(all(c("title", "url", "snippet", "date", "source") %in%
names(val_empty)))

# The url filter must keep date/source aligned when it drops rows.
mixed <- data.frame(title = c("a", "b"), url = c("", "https://x.test"),
snippet = c("s1", "s2"), stringsAsFactors = FALSE)
val_mixed <- pensar:::.validate_autoresearch_search_results(mixed)
expect_equal(nrow(val_mixed), 1L)
expect_equal(val_mixed$url, "https://x.test")

# The full search loop tags query/angle even when a query yields nothing.
q <- data.frame(query = "q", angle = "a", stringsAsFactors = FALSE)
loop_empty <- pensar:::autoresearch_run_searches(
q, empty_search, list(max_sources_per_round = 5L), verbose = FALSE)
expect_equal(nrow(loop_empty), 0L)
expect_true(all(c("query", "angle") %in% names(loop_empty)))