beatrina 0.8.6 → 0.9.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/NOTICES +1 -1
  2. package/README.md +6 -0
  3. package/beatrina_V0.9.43.html +1885 -0
  4. package/beatrina_V0.9.43.html.inputs.json +1 -0
  5. package/bin/beatrina.mjs +156 -26
  6. package/bin/browser.mjs +31 -0
  7. package/bin/cli.mjs +64 -5
  8. package/bin/doctor-rows.mjs +31 -0
  9. package/bin/failsafe.mjs +8 -3
  10. package/bin/finder.mjs +74 -0
  11. package/bin/lsquery.swift +55 -0
  12. package/bin/pages.mjs +81 -0
  13. package/bin/python-setup.mjs +207 -0
  14. package/bin/runtime-dirs.mjs +27 -0
  15. package/bin/sessions.mjs +30 -10
  16. package/bin/shortcut.mjs +271 -35
  17. package/bin/update-check.mjs +2 -2
  18. package/build-info.json +1 -1
  19. package/engines/js/worker.mjs +6 -3
  20. package/engines/python/adapter.py +14 -13
  21. package/engines/python/analyze.py +4 -2
  22. package/engines/python/bootstrap.py +32 -31
  23. package/engines/python/debugger.py +10 -6
  24. package/engines/python/engine.json +1 -1
  25. package/engines/python/handoff.py +4 -2
  26. package/engines/python/worker.py +39 -10
  27. package/engines/r/engine.json +1 -1
  28. package/engines/r/handoff.R +4 -3
  29. package/failsafe/ai-policy.R +16 -16
  30. package/failsafe/ai-store.R +6 -6
  31. package/failsafe/cite.R +11 -11
  32. package/failsafe/journal.R +11 -11
  33. package/failsafe/plugins.R +27 -27
  34. package/failsafe/serve.R +364 -239
  35. package/failsafe/session-documents.R +104 -0
  36. package/host/ai-authority.mjs +55 -0
  37. package/host/ai-policy.mjs +48 -26
  38. package/host/bundle.mjs +194 -0
  39. package/host/deployment.mjs +25 -18
  40. package/host/engine-js.mjs +16 -17
  41. package/host/engine-pool.mjs +56 -8
  42. package/host/engine-python.mjs +80 -31
  43. package/host/engine-r.mjs +38 -32
  44. package/host/engine-stdio.mjs +45 -12
  45. package/host/env-names.mjs +148 -0
  46. package/host/gateway-token.mjs +51 -0
  47. package/host/journal-store.mjs +10 -9
  48. package/host/main.mjs +216 -76
  49. package/host/payload.mjs +131 -0
  50. package/host/planes/README.md +1 -1
  51. package/host/planes/ai-store.mjs +6 -5
  52. package/host/planes/ai.mjs +49 -29
  53. package/host/planes/analyze.mjs +6 -5
  54. package/host/planes/bundle.mjs +344 -0
  55. package/host/planes/choose.mjs +404 -0
  56. package/host/planes/cite.mjs +18 -17
  57. package/host/planes/files.mjs +0 -0
  58. package/host/planes/jobs.mjs +43 -20
  59. package/host/planes/latex.mjs +41 -5
  60. package/host/planes/mcp.mjs +80 -91
  61. package/host/planes/pair.mjs +24 -24
  62. package/host/planes/pipe-term.mjs +2 -2
  63. package/host/planes/plugins.mjs +1 -1
  64. package/host/planes/recent-documents.mjs +170 -0
  65. package/host/planes/sessions.mjs +230 -82
  66. package/host/planes/settings.mjs +4 -4
  67. package/host/planes/terminal.mjs +13 -11
  68. package/host/planes/test-file.mjs +2 -2
  69. package/host/planes/update.mjs +6 -13
  70. package/host/plugin-store.mjs +35 -52
  71. package/host/recent-documents.mjs +124 -0
  72. package/host/runtime-dir.mjs +47 -0
  73. package/host/server.mjs +123 -21
  74. package/host/session-keep.mjs +70 -0
  75. package/host/settings.mjs +85 -37
  76. package/host/update-record.mjs +3 -2
  77. package/host/user-dirs.mjs +60 -18
  78. package/host/which.mjs +39 -0
  79. package/host/windows-runtime.mjs +6 -3
  80. package/host/worker-plane.mjs +80 -7
  81. package/host/ws.mjs +9 -2
  82. package/host/zip.mjs +237 -0
  83. package/kernel/analyze.R +1 -1
  84. package/{check → kernel/check}/acceptance.mjs +1 -1
  85. package/kernel/check/knit-file.mjs +19639 -0
  86. package/{check → kernel/check}/session.mjs +49 -8
  87. package/kernel/deployment.R +20 -20
  88. package/kernel/examples/NOTICE.md +1 -1
  89. package/kernel/fileio.R +6 -6
  90. package/kernel/index.html +2 -2
  91. package/kernel/job-run.R +83 -22
  92. package/kernel/jobs.R +28 -12
  93. package/kernel/kernel-version +1 -1
  94. package/kernel/kernel.R +24 -21
  95. package/kernel/knitr-run.R +50 -9
  96. package/kernel/latex.R +429 -30
  97. package/kernel/mcp/{carmar-mcp.mjs → beatrina-mcp.mjs} +249 -59
  98. package/kernel/notebook-page.R +7 -7
  99. package/kernel/project.R +10 -10
  100. package/kernel/settings.R +105 -56
  101. package/kernel/sniff.R +4 -4
  102. package/kernel/typst.R +121 -0
  103. package/kernel/worker.R +602 -123
  104. package/kernel/workspace-keep.R +188 -0
  105. package/lib/agent-authoring-contract.js +28 -19
  106. package/lib/cell-kinds.js +3 -3
  107. package/lib/engine-labels.js +4 -4
  108. package/menu/Beatrina Menu.app/Contents/Info.plist +14 -0
  109. package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
  110. package/menu/Beatrina Menu.app/Contents/_CodeSignature/CodeResources +115 -0
  111. package/package.json +4 -3
  112. package/carmar_V0.8.6.html +0 -1310
package/failsafe/serve.R CHANGED
@@ -19,17 +19,17 @@
19
19
  # before its event loop starts. The option guards this process's one re-entry.
20
20
  # Rscript writes a space in the script path as `~+~` when it builds R's
21
21
  # command line, and R decodes it only to open the file: commandArgs(FALSE)
22
- # still carries the encoded form. "CarmaR Helper.app" has a space, so its
22
+ # still carries the encoded form. "Beatrina Helper.app" has a space, so its
23
23
  # kernel resolved `here` to a path that does not exist and died at source().
24
24
  # Decoded once, here, for every reader of --file= in this file.
25
- carmar_file_arg <- function() {
25
+ beatrina_file_arg <- function() {
26
26
  gsub("~+~", " ", sub("^--file=", "",
27
27
  grep("^--file=", commandArgs(FALSE), value = TRUE)[1L]), fixed = TRUE)
28
28
  }
29
- if (!isTRUE(getOption("carmar.supervisor.parsed"))) {
30
- supervisor_entry <- carmar_file_arg()
29
+ if (!isTRUE(getOption("beatrina.supervisor.parsed"))) {
30
+ supervisor_entry <- beatrina_file_arg()
31
31
  supervisor_program <- parse(file = supervisor_entry, keep.source = FALSE)
32
- options(carmar.supervisor.parsed = TRUE)
32
+ options(beatrina.supervisor.parsed = TRUE)
33
33
  eval(supervisor_program, envir = globalenv())
34
34
  quit(save = "no", status = 0L, runLast = FALSE)
35
35
  }
@@ -39,7 +39,7 @@ suppressPackageStartupMessages({
39
39
  library(jsonlite)
40
40
  })
41
41
 
42
- here <- dirname(normalizePath(carmar_file_arg()))
42
+ here <- dirname(normalizePath(beatrina_file_arg()))
43
43
  source(file.path(here, "kernel.R"))
44
44
  source(file.path(here, "notebook-page.R"))
45
45
  source(file.path(here, "deployment.R"))
@@ -52,15 +52,20 @@ source(file.path(here, "settings.R"))
52
52
  # The file system: one body shared with the worker (docs/document-model-plan.md
53
53
  # D3). The supervisor answers the pure file ops itself so a Save never waits
54
54
  # behind a running cell; the worker keeps the same functions for the
55
- # transition flag CARMAR_FILE_OPS=worker.
55
+ # transition flag BEATRINA_FILE_OPS=worker.
56
56
  source(file.path(here, "fileio.R"))
57
57
  source(file.path(here, "journal.R"))
58
58
  source(file.path(here, "ai-store.R"))
59
+ # "Restart, keep variables": the supervisor never reads a kept workspace; it
60
+ # only checks the token (keep_token_ok) and carries it to the next worker.
61
+ source(file.path(here, "workspace-keep.R"))
62
+ # Which documents this session has open — the session's record, not the page's.
63
+ source(file.path(here, "session-documents.R"))
59
64
 
60
65
  # Apply an administrator-owned desktop policy before any subsystem reads its
61
66
  # environment. Invalid or user-writable policy fails closed at the shared
62
67
  # startup gate below; no partial values are applied.
63
- managed_config <- carmar_managed_environment()
68
+ managed_config <- beatrina_managed_environment()
64
69
 
65
70
  # Before anything reads or writes a frame. The supervisor parses every browser
66
71
  # message and re-encodes every worker reply, so its own character locale
@@ -80,16 +85,16 @@ read_kernel_protocol <- function() {
80
85
  candidates <- c(file.path(here, "kernel-protocol"),
81
86
  file.path(here, "..", "kernel.protocol"))
82
87
  hit <- candidates[file.exists(candidates)][1L]
83
- if (!length(hit) || is.na(hit)) stop("CarmaR kernel protocol stamp is missing.")
88
+ if (!length(hit) || is.na(hit)) stop("Beatrina kernel protocol stamp is missing.")
84
89
  value <- suppressWarnings(as.integer(trimws(readLines(hit, warn = FALSE, n = 1L))))
85
90
  if (length(value) != 1L || is.na(value) || value < 1L) {
86
- stop("CarmaR kernel protocol stamp is invalid.")
91
+ stop("Beatrina kernel protocol stamp is invalid.")
87
92
  }
88
93
  value
89
94
  }
90
- CARMAR_PROTOCOL_VERSION <- read_kernel_protocol()
95
+ BEATRINA_PROTOCOL_VERSION <- read_kernel_protocol()
91
96
  read_kernel_build <- function() {
92
- from_env <- trimws(Sys.getenv("CARMAR_KERNEL_BUILD", ""))
97
+ from_env <- trimws(Sys.getenv("BEATRINA_KERNEL_BUILD", ""))
93
98
  if (nzchar(from_env)) return(from_env)
94
99
  stamp <- file.path(here, "kernel-version")
95
100
  if (file.exists(stamp)) {
@@ -104,25 +109,25 @@ read_kernel_build <- function() {
104
109
  }
105
110
  "unknown"
106
111
  }
107
- CARMAR_KERNEL_BUILD <- read_kernel_build()
112
+ BEATRINA_KERNEL_BUILD <- read_kernel_build()
108
113
 
109
- #' What CarmaR is INSTALLED as, right now — re-read on every call.
114
+ #' What Beatrina is INSTALLED as, right now — re-read on every call.
110
115
  #'
111
- #' `CARMAR_KERNEL_BUILD` is what this process was started as. The bundle it
116
+ #' `BEATRINA_KERNEL_BUILD` is what this process was started as. The bundle it
112
117
  #' lives in is swapped IN PLACE by an upgrade, so the stamp beside this file
113
118
  #' can name a newer (or, after a rollback, an older) build than the one
114
119
  #' running. Reading it fresh every time is what lets a session that predates
115
120
  #' an install say so — in /health, in the ready frame and in every heartbeat
116
121
  #' — instead of serving the old page in silence until someone quits it.
117
122
  #'
118
- #' When the build was dictated by CARMAR_KERNEL_BUILD (tests, the broker),
119
- #' the installed build is dictated the same way: CARMAR_INSTALLED_BUILD, or
123
+ #' When the build was dictated by BEATRINA_KERNEL_BUILD (tests, the broker),
124
+ #' the installed build is dictated the same way: BEATRINA_INSTALLED_BUILD, or
120
125
  #' the same value — a test kernel must never read "newer installed" off a
121
126
  #' source checkout's notebook.version.cjs.
122
127
  installed_build <- function() {
123
- dictated <- trimws(Sys.getenv("CARMAR_KERNEL_BUILD", ""))
128
+ dictated <- trimws(Sys.getenv("BEATRINA_KERNEL_BUILD", ""))
124
129
  if (nzchar(dictated)) {
125
- inst <- trimws(Sys.getenv("CARMAR_INSTALLED_BUILD", ""))
130
+ inst <- trimws(Sys.getenv("BEATRINA_INSTALLED_BUILD", ""))
126
131
  return(if (nzchar(inst)) inst else dictated)
127
132
  }
128
133
  stamp <- file.path(here, "kernel-version")
@@ -141,13 +146,13 @@ installed_build <- function() {
141
146
  # The build this kernel REPLACED, when it was started by a "Restart into"
142
147
  # handoff (session_handoff_spawn below). Told to /health for the first
143
148
  # minute and a half so the page that asked can recognise its successor.
144
- HANDOFF_FROM <- trimws(Sys.getenv("CARMAR_HANDOFF_FROM", ""))
149
+ HANDOFF_FROM <- trimws(Sys.getenv("BEATRINA_HANDOFF_FROM", ""))
145
150
 
146
151
  # Where to listen. 127.0.0.1 unless an operator says otherwise, and saying
147
152
  # otherwise inverts the trust model — see spike/deployment.R, which owns that
148
153
  # decision and refuses the unsafe spellings of it outright.
149
154
  host <- local({
150
- h <- trimws(Sys.getenv("CARMAR_BIND", "127.0.0.1"))
155
+ h <- trimws(Sys.getenv("BEATRINA_BIND", "127.0.0.1"))
151
156
  if (nzchar(h)) h else "127.0.0.1"
152
157
  })
153
158
 
@@ -157,7 +162,7 @@ host <- local({
157
162
  #' here. localStorage and sessionStorage are keyed by scheme://host:PORT, so a
158
163
  #' new port on every start is a new, empty storage on every start: the saved
159
164
  #' notebooks invisible, the chat history gone, the AI key asked for again. A
160
- #' user restarting the kernel three times has three separate CarmaRs and no way
165
+ #' user restarting the kernel three times has three separate Beatrinas and no way
161
166
  #' to know why the last one forgot everything.
162
167
  #'
163
168
  #' A fixed loopback port is not a weaker position than a random one — the
@@ -172,7 +177,7 @@ port_taken <- function(p) {
172
177
  error = function(e) NULL))
173
178
  if (is.null(con)) FALSE else { close(con); TRUE }
174
179
  }
175
- # CARMAR_PORT is a PREFERENCE by default and a PROMISE when asked.
180
+ # BEATRINA_PORT is a PREFERENCE by default and a PROMISE when asked.
176
181
  #
177
182
  # Moving to a random port when the wanted one is taken is right for a person
178
183
  # double-clicking the app: they get a session instead of an error, and the
@@ -181,23 +186,23 @@ port_taken <- function(p) {
181
186
  # is already written into a registry record and about to be handed to Caddy
182
187
  # as an upstream, so a kernel that quietly moves leaves BOTH pointing at
183
188
  # nothing and the reader gets a connection refused with no explanation
184
- # anywhere in the system. CARMAR_PORT_STRICT=1 turns the silent move into a
189
+ # anywhere in the system. BEATRINA_PORT_STRICT=1 turns the silent move into a
185
190
  # refusal, which is the only honest answer when the caller cannot be told.
186
191
  port <- local({
187
- wanted <- suppressWarnings(as.integer(Sys.getenv("CARMAR_PORT", "4747")))
192
+ wanted <- suppressWarnings(as.integer(Sys.getenv("BEATRINA_PORT", "4747")))
188
193
  if (is.na(wanted) || wanted < 1024L || wanted > 65535L) wanted <- 4747L
189
194
  if (!port_taken(wanted)) return(wanted)
190
195
  # A successor started by a handoff is spawned while its predecessor still
191
196
  # holds the port; it waits for the port to free rather than moving.
192
- wait_s <- suppressWarnings(as.numeric(Sys.getenv("CARMAR_WAIT_PORT", "0")))
197
+ wait_s <- suppressWarnings(as.numeric(Sys.getenv("BEATRINA_WAIT_PORT", "0")))
193
198
  if (is.finite(wait_s) && wait_s > 0) {
194
199
  deadline <- Sys.time() + wait_s
195
200
  while (port_taken(wanted) && Sys.time() < deadline) Sys.sleep(0.25) # a wait is a loop
196
201
  if (!port_taken(wanted)) return(wanted)
197
202
  }
198
- if (identical(Sys.getenv("CARMAR_PORT_STRICT", ""), "1")) {
199
- message("CarmaR refuses to start: CARMAR_PORT=", wanted, " is already in use ",
200
- "and CARMAR_PORT_STRICT=1 forbids moving to another port. Whoever ",
203
+ if (identical(Sys.getenv("BEATRINA_PORT_STRICT", ""), "1")) {
204
+ message("Beatrina refuses to start: BEATRINA_PORT=", wanted, " is already in use ",
205
+ "and BEATRINA_PORT_STRICT=1 forbids moving to another port. Whoever ",
201
206
  "chose this port is expecting to find the kernel on it.")
202
207
  quit(status = 2L, save = "no")
203
208
  }
@@ -207,27 +212,27 @@ port <- local({
207
212
  # The posture: bind, Origin allow-list, Host allow-list, and whether an
208
213
  # Origin-less client is a same-user native process or an unauthenticated
209
214
  # stranger. Computed once; every gate below reads a decided answer.
210
- deployment <- carmar_deployment(port)
215
+ deployment <- beatrina_deployment(port)
211
216
 
212
217
  # The administrator's AI policy, read once. Its errors join the deployment's
213
218
  # in the same startup gate and for the same reason: a governance control that
214
219
  # silently failed to apply is worse than one that refuses to start, because
215
220
  # nobody finds out until the data has already gone to the wrong vendor.
216
- ai_policy <- carmar_ai_policy()
221
+ ai_policy <- beatrina_ai_policy()
217
222
 
218
223
  # The plugin policy (spike/plugins.R): which kinds exist, whether the person
219
224
  # may install, which hosts a fetch may reach, where the administrator's set
220
225
  # is. Unlike the AI policy this one is enforcement — every fetch and every
221
226
  # write is the supervisor's — and it fails closed the same way: a policy
222
227
  # that names only junk refuses to start rather than permitting everything.
223
- plugin_policy <- carmar_plugin_policy()
228
+ plugin_policy <- beatrina_plugin_policy()
224
229
 
225
230
  startup_errors <- c(managed_config$errors, deployment$errors, ai_policy$errors, plugin_policy$errors)
226
231
  if (length(startup_errors)) {
227
232
  # Refusing to start is the correct outcome. A server that binds to the
228
- # network with the loopback trust model is not a degraded CarmaR, it is a
233
+ # network with the loopback trust model is not a degraded Beatrina, it is a
229
234
  # remote-execution service, and there is no safe way to serve it "for now".
230
- for (msg in startup_errors) message("CarmaR refuses to start: ", msg)
235
+ for (msg in startup_errors) message("Beatrina refuses to start: ", msg)
231
236
  quit(status = 2L, save = "no")
232
237
  }
233
238
 
@@ -246,10 +251,10 @@ origins_ok <- deployment$origins
246
251
  hosts_ok <- deployment$hosts
247
252
 
248
253
  # Optional audit log — one JSON object per line, for deployments that must be
249
- # able to answer "what did this session do?". Off unless CARMAR_LOG is set,
254
+ # able to answer "what did this session do?". Off unless BEATRINA_LOG is set,
250
255
  # because a notebook that silently records the user's code would be worse than
251
256
  # no logging at all.
252
- log_path <- Sys.getenv("CARMAR_LOG", "")
257
+ log_path <- Sys.getenv("BEATRINA_LOG", "")
253
258
  # pandoc, found once and kept. Absent stays absent for the session — a
254
259
  # pandoc installed mid-session shows up after a restart, which is the same
255
260
  # rule R itself applies to a package installed under a running session.
@@ -278,28 +283,28 @@ audit <- function(event, ...) {
278
283
  # successful settings_set so a later read reports the value that is actually
279
284
  # in force, and `linger_s` / `HISTORY_ENABLED` below are the two the running
280
285
  # kernel can honour immediately (see each one's `effect`).
281
- settings_state <- carmar_settings_resolve()
282
- settings_value <- function(key) carmar_settings_value(settings_state, key)
286
+ settings_state <- beatrina_settings_resolve()
287
+ settings_value <- function(key) beatrina_settings_value(settings_state, key)
283
288
  audit("settings", detail = "startup", status = settings_state$file$status,
284
289
  keys = length(settings_state$file$values))
285
290
 
286
291
  # What every child inherits from the user's settings, and which R to run.
287
292
  # Defined here so the three spawn sites cannot drift: a setting that reaches
288
293
  # the worker but not the restarted worker is the worst kind of half-applied.
289
- settings_child_env <- function() carmar_settings_child_env(settings_state)
294
+ settings_child_env <- function() beatrina_settings_child_env(settings_state)
290
295
  settings_rscript <- function() {
291
296
  chosen <- settings_value("rscript_path")
292
297
  detect_rscript(if (is.character(chosen) && nzchar(chosen)) chosen
293
- else Sys.getenv("CARMAR_RSCRIPT", ""))
298
+ else Sys.getenv("BEATRINA_RSCRIPT", ""))
294
299
  }
295
300
 
296
- # The launcher's state dir (CARMAR_STATE) or the package's: where this
301
+ # The launcher's state dir (BEATRINA_STATE) or the package's: where this
297
302
  # supervisor keeps the handoff log for a successor it spawns. Nothing of the
298
303
  # R session is ever written here — a worker starts fresh, always (see the
299
304
  # handoff section below).
300
305
  state_dir <- function() {
301
- state <- Sys.getenv("CARMAR_STATE", "")
302
- if (!nzchar(state)) state <- tools::R_user_dir("carmar", "data")
306
+ state <- Sys.getenv("BEATRINA_STATE", "")
307
+ if (!nzchar(state)) state <- tools::R_user_dir("beatrina", "data")
303
308
  state
304
309
  }
305
310
  start_execution_worker <- function() {
@@ -319,6 +324,17 @@ sockets$open <- list()
319
324
  # connects. Without replaying it, every page that opens later sits on
320
325
  # "connecting…" forever waiting for a frame that was broadcast to nobody.
321
326
  sockets$hello <- NULL
327
+ # A kept workspace for the NEXT ready worker: set by `restart {resume}` or, in a
328
+ # handoff successor, by BEATRINA_RESUME_TOKEN. Consumed once, on `ready`.
329
+ sockets$resume_pending <- if (keep_token_ok(Sys.getenv("BEATRINA_RESUME_TOKEN")))
330
+ Sys.getenv("BEATRINA_RESUME_TOKEN") else NULL
331
+ # The restore's report, kept for 120 s so a page that follows a handoff — and
332
+ # connects after the restore finished — still hears what came back.
333
+ sockets$resume_report <- NULL
334
+ # The documents open in this session (spike/session-documents.R), as the pages
335
+ # last reported them. A handoff successor starts with its predecessor's list.
336
+ sockets$session_documents <- session_documents_decode(Sys.getenv("BEATRINA_SESSION_DOCUMENTS"))
337
+ Sys.unsetenv("BEATRINA_SESSION_DOCUMENTS")
322
338
  sockets$worker_recovering <- FALSE
323
339
  sockets$worker_restart_attempts <- 0L
324
340
  sockets$worker_restart_after <- 0
@@ -349,7 +365,7 @@ sockets$terms <- new.env(parent = emptyenv())
349
365
  sockets$term_seq <- 0L
350
366
  # Connection lifetime and R lifetime are separate. By default a session that
351
367
  # has been attached survives closed tabs, browser suspension and lost links.
352
- # Positive CARMAR_LINGER / linger_seconds explicitly opts into idle shutdown.
368
+ # Positive BEATRINA_LINGER / linger_seconds explicitly opts into idle shutdown.
353
369
  # A running task blocks that optional clock; reconnection resets it.
354
370
  # How long /health?hold=1 postpones an explicitly configured idle timeout.
355
371
  # Default retention does not depend on holds or their timing.
@@ -357,8 +373,8 @@ HOLD_GRACE_S <- 30
357
373
  # How long a kernel that has never been connected to waits for its browser
358
374
  # before an unopened launch is cleaned up. This is NOT a session idle timer:
359
375
  # the first connection or hold permanently cancels default boot cleanup.
360
- # Explicit CARMAR_LINGER=0 also disables unopened-launch cleanup for headless use.
361
- BOOT_GRACE_S <- as.numeric(Sys.getenv("CARMAR_BOOT_GRACE", "180"))
376
+ # Explicit BEATRINA_LINGER=0 also disables unopened-launch cleanup for headless use.
377
+ BOOT_GRACE_S <- as.numeric(Sys.getenv("BEATRINA_BOOT_GRACE", "180"))
362
378
  if (!is.finite(BOOT_GRACE_S) || BOOT_GRACE_S < 0) BOOT_GRACE_S <- 180
363
379
  # The same safe default applies locally and behind a proxy. Explicit user or
364
380
  # administrator settings remain authoritative, including an explicit zero.
@@ -382,7 +398,7 @@ linger_s <- resolve_linger()
382
398
  # Only a socket that has beaten at least once is ever reaped — a notebook built
383
399
  # before heartbeats exists on people's disks and must keep today's behaviour
384
400
  # rather than be disconnected while its reader is looking at it.
385
- socket_silence_s <- suppressWarnings(as.numeric(Sys.getenv("CARMAR_SOCKET_SILENCE", "90")))
401
+ socket_silence_s <- suppressWarnings(as.numeric(Sys.getenv("BEATRINA_SOCKET_SILENCE", "90")))
386
402
  if (!length(socket_silence_s) || !is.finite(socket_silence_s)) socket_silence_s <- 90
387
403
 
388
404
  # ── the dispatched-command deadline, PER COMMAND CLASS ─────────────────────
@@ -444,7 +460,7 @@ WORKER_DEADLINES <- c(
444
460
  # Tests need the same branch in seconds rather than minutes. Only a test sets
445
461
  # it; the numbers above ARE the contract.
446
462
  worker_deadline_scale <- suppressWarnings(
447
- as.numeric(Sys.getenv("CARMAR_WORKER_DEADLINE_SCALE", "1")))
463
+ as.numeric(Sys.getenv("BEATRINA_WORKER_DEADLINE_SCALE", "1")))
448
464
  if (!length(worker_deadline_scale) || !is.finite(worker_deadline_scale) ||
449
465
  worker_deadline_scale <= 0) worker_deadline_scale <- 1
450
466
 
@@ -456,14 +472,14 @@ if (!length(worker_deadline_scale) || !is.finite(worker_deadline_scale) ||
456
472
  # than an honest terminal state, because the attach plug is hidden while
457
473
  # recovering and the user cannot reach another session either.
458
474
  worker_respawn_max <- suppressWarnings(
459
- as.integer(Sys.getenv("CARMAR_WORKER_RESPAWN_MAX", "10")))
475
+ as.integer(Sys.getenv("BEATRINA_WORKER_RESPAWN_MAX", "10")))
460
476
  if (!length(worker_respawn_max) || is.na(worker_respawn_max) ||
461
477
  worker_respawn_max < 1L) worker_respawn_max <- 10L
462
478
 
463
479
  # An agent's tool call parked on a page that then died. The page half is the
464
480
  # precise test — a rec no longer in `sockets$open` can never answer — and this
465
481
  # is the backstop for the case where the page is alive and simply never
466
- # replies. It matches the ceiling tools/mcp/carmar-mcp.mjs allows a caller to
482
+ # replies. It matches the ceiling tools/mcp/beatrina-mcp.mjs allows a caller to
467
483
  # ask for (chunk_run's timeout_s caps at 3600), so the supervisor never gives
468
484
  # up on a call the agent is still legitimately waiting for.
469
485
  MCP_PENDING_MAX_S <- 3600
@@ -482,7 +498,7 @@ sockets$worker_queue <- list()
482
498
  # explicitly pairs one page in a loopback consent window; that LOCAL window
483
499
  # owns the socket and relays only messages from the exact approved origin.
484
500
  sockets$pairing_requests <- new.env(parent = emptyenv())
485
- # Consent belongs to this local CarmaR session, not to one popup document.
501
+ # Consent belongs to this local Beatrina session, not to one popup document.
486
502
  # Once an origin is approved, a replacement bridge window for that exact
487
503
  # origin may reconnect without asking again. The environment disappears when
488
504
  # the kernel stops, so approval is still session-scoped.
@@ -520,7 +536,7 @@ sockets$boot_at <- Sys.time()
520
536
 
521
537
  `%||%` <- function(a, b) if (is.null(a)) b else a
522
538
 
523
- #' The page to serve at `/`: the built CarmaR notebook when there is one,
539
+ #' The page to serve at `/`: the built Beatrina notebook when there is one,
524
540
  #' otherwise the bare spike page. The page is PINNED to this kernel's build
525
541
  #' (never the highest version present), `.beta.min` ignored so
526
542
  #' a debuggable build is served during development. The repo keeps builds in
@@ -531,7 +547,7 @@ notebook_page <- function() {
531
547
  # One implementation, in notebook-page.R, because tools/app/launch.sh has to
532
548
  # ask the same question at open time — the announcement below is computed
533
549
  # once at startup and goes stale the moment a new build lands.
534
- page <- carmar_notebook_page(here, CARMAR_KERNEL_BUILD)
550
+ page <- beatrina_notebook_page(here, BEATRINA_KERNEL_BUILD)
535
551
  if (nzchar(page)) return(page)
536
552
  file.path(here, "index.html")
537
553
  }
@@ -543,7 +559,7 @@ notebook_page <- function() {
543
559
  #' does that by comparing ONE string — the boot time exactly as /health
544
560
  #' reports `started`, rendered by the same jsonlite call so the two can never
545
561
  #' differ in their digits. Served pages get it as
546
- #' `<meta name="carmar-session">`; a file page gets the same value in its
562
+ #' `<meta name="beatrina-session">`; a file page gets the same value in its
547
563
  #' fragment from the launcher.
548
564
  session_stamp <- function() {
549
565
  as.character(toJSON(as.numeric(sockets$boot_at), auto_unbox = TRUE))
@@ -558,7 +574,7 @@ session_stamp <- function() {
558
574
  #' @param html the page's text.
559
575
  #' @return the page with the meta in it.
560
576
  stamp_session <- function(html) {
561
- meta <- sprintf('<meta name="carmar-session" content="%s">', session_stamp())
577
+ meta <- sprintf('<meta name="beatrina-session" content="%s">', session_stamp())
562
578
  at <- regexpr("<head\\b[^>]*>", html, perl = TRUE)
563
579
  if (at < 0) at <- regexpr("<html\\b[^>]*>", html, perl = TRUE)
564
580
  if (at < 0) return(paste0(meta, html))
@@ -566,13 +582,13 @@ stamp_session <- function(html) {
566
582
  paste0(substr(html, 1L, cut), meta, substr(html, cut + 1L, nchar(html)))
567
583
  }
568
584
 
569
- #' Where the carmar-mcp stdio server lives on THIS machine, so the setup
585
+ #' Where the beatrina-mcp stdio server lives on THIS machine, so the setup
570
586
  #' dialog can show a command that works verbatim. Repo layout first
571
587
  #' (tools/mcp beside spike/), then a distribution copy beside serve.R.
572
588
  #' Empty when neither exists — the dialog then shows a placeholder path.
573
589
  mcp_server_path <- function() {
574
- for (p in c(file.path(here, "..", "tools", "mcp", "carmar-mcp.mjs"),
575
- file.path(here, "mcp", "carmar-mcp.mjs"))) {
590
+ for (p in c(file.path(here, "..", "tools", "mcp", "beatrina-mcp.mjs"),
591
+ file.path(here, "mcp", "beatrina-mcp.mjs"))) {
576
592
  if (file.exists(p)) return(normalizePath(p))
577
593
  }
578
594
  ""
@@ -591,17 +607,17 @@ find_binary <- function(name, extra = character()) {
591
607
  ""
592
608
  }
593
609
 
594
- #' The Claude Code CLI, if this machine has one. CARMAR_CLAUDE_BIN overrides —
610
+ #' The Claude Code CLI, if this machine has one. BEATRINA_CLAUDE_BIN overrides —
595
611
  #' that is also how tests substitute a deterministic stub.
596
612
  claude_bin <- function() {
597
- override <- Sys.getenv("CARMAR_CLAUDE_BIN", "")
613
+ override <- Sys.getenv("BEATRINA_CLAUDE_BIN", "")
598
614
  if (nzchar(override) && file.exists(override)) return(override)
599
615
  find_binary("claude")
600
616
  }
601
617
 
602
- #' The Codex CLI, if this machine has one. CARMAR_CODEX_BIN overrides (tests).
618
+ #' The Codex CLI, if this machine has one. BEATRINA_CODEX_BIN overrides (tests).
603
619
  codex_bin <- function() {
604
- override <- Sys.getenv("CARMAR_CODEX_BIN", "")
620
+ override <- Sys.getenv("BEATRINA_CODEX_BIN", "")
605
621
  if (nzchar(override) && file.exists(override)) return(override)
606
622
  find_binary("codex")
607
623
  }
@@ -625,11 +641,11 @@ published_origin_valid <- function(origin) {
625
641
  grepl("^https?://[A-Za-z0-9._~-]+(:[0-9]{1,5})?$", origin)
626
642
  }
627
643
 
628
- # A native CarmaR launcher may start the kernel for one published page. The
644
+ # A native Beatrina launcher may start the kernel for one published page. The
629
645
  # URL-scheme handoff is the user's consent gesture; carrying only the exact
630
646
  # origin into this process keeps the permission session-scoped and prevents a
631
647
  # second site from borrowing it.
632
- initial_published_origin <- Sys.getenv("CARMAR_PUBLISHED_ORIGIN", "")
648
+ initial_published_origin <- Sys.getenv("BEATRINA_PUBLISHED_ORIGIN", "")
633
649
  if (published_origin_valid(initial_published_origin)) {
634
650
  sockets$published_approvals[[initial_published_origin]] <- TRUE
635
651
  audit("published-origin-authorized", origin = initial_published_origin,
@@ -654,8 +670,8 @@ if (published_origin_valid(initial_published_origin)) {
654
670
  # looking for one. Any HTML file the user opened could therefore sweep for a
655
671
  # kernel and send `exec` — the same shape as the Chrome extension deleted in
656
672
  # 0.38.0 ("no token and no page identity"), which is documented there as
657
- # unacceptable. A file page cannot hold a secret CarmaR never gave it, and
658
- # CarmaR never sees it open (CarmaR.app claims .qmd/.Rmd/.R/... but not
673
+ # unacceptable. A file page cannot hold a secret Beatrina never gave it, and
674
+ # Beatrina never sees it open (Beatrina.app claims .qmd/.Rmd/.R/... but not
659
675
  # .html, so a double-clicked notebook goes straight to the browser). What is
660
676
  # left is to ASK — once per session, on a local page that names what is
661
677
  # asking. Absent Origin (native same-user clients) is deliberately unchanged:
@@ -665,7 +681,7 @@ if (published_origin_valid(initial_published_origin)) {
665
681
  #' `sample()` draws from the Mersenne Twister, which is seeded from the clock
666
682
  #' and the pid and is trivially reconstructible — fine for shuffling a vector,
667
683
  #' not for a value that decides whether a page may run R. /dev/urandom is the
668
- #' OS CSPRNG on every platform CarmaR ships a kernel for except Windows, where
684
+ #' OS CSPRNG on every platform Beatrina ships a kernel for except Windows, where
669
685
  #' the RNG fallback is documented rather than hidden.
670
686
  secure_token <- function(n = 32L) {
671
687
  alphabet <- c(letters, LETTERS, 0:9)
@@ -685,7 +701,7 @@ secure_token <- function(n = 32L) {
685
701
 
686
702
  FILE_ORIGIN <- "null"
687
703
 
688
- # A notebook page SERVED BY ANOTHER CarmaR KERNEL on this machine. When a
704
+ # A notebook page SERVED BY ANOTHER Beatrina KERNEL on this machine. When a
689
705
  # page's own kernel dies the page stays open (it is HTML the browser already
690
706
  # holds) and can attach to a different session through the badge's plug icon;
691
707
  # it arrives here with the Origin of the kernel that served it,
@@ -705,7 +721,7 @@ local_origin <- function(origin) {
705
721
  # consent click, while a file page that merely found the port cannot.
706
722
  #
707
723
  # serve.R prints the exact file:// URL it expects to be opened, and that URL
708
- # is the one channel CarmaR controls: it already carries `#kernel=<port>`, so
724
+ # is the one channel Beatrina controls: it already carries `#kernel=<port>`, so
709
725
  # it can carry a secret too. The page reads it out of its own fragment and
710
726
  # presents it on the WebSocket URL. A drive-by html file probing 4747..65535
711
727
  # can reach /health and learn a kernel is there, but it was never handed this,
@@ -723,12 +739,12 @@ local_origin <- function(origin) {
723
739
  # so the page can be told where to go BEFORE this process exits. Only the exact
724
740
  # shape secure_token() makes is accepted; anything else is minted afresh.
725
741
  FILE_LAUNCH_CAP <- local({
726
- given <- Sys.getenv("CARMAR_FILE_LAUNCH_CAP", "")
727
- Sys.unsetenv("CARMAR_FILE_LAUNCH_CAP")
742
+ given <- Sys.getenv("BEATRINA_FILE_LAUNCH_CAP", "")
743
+ Sys.unsetenv("BEATRINA_FILE_LAUNCH_CAP")
728
744
  if (grepl("^[A-Za-z0-9]{32}$", given)) given else secure_token(32L)
729
745
  })
730
746
 
731
- open_file_env <- Sys.getenv("CARMAR_OPEN_FILE", "")
747
+ open_file_env <- Sys.getenv("BEATRINA_OPEN_FILE", "")
732
748
  sockets$pending_open <- if (nzchar(open_file_env)) open_file_env else NULL
733
749
  if (!is.null(sockets$pending_open)) audit("open-file-parked")
734
750
 
@@ -767,9 +783,9 @@ pairing_page <- function(origin, nonce = "") {
767
783
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
768
784
  '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; ',
769
785
  "style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'\">",
770
- '<title>', if (is_file) 'Allow this notebook file to use CarmaR?'
771
- else if (is_local) 'Attach this notebook to this CarmaR session?'
772
- else 'Allow this book to use CarmaR?', '</title><style>',
786
+ '<title>', if (is_file) 'Allow this notebook file to use Beatrina?'
787
+ else if (is_local) 'Attach this notebook to this Beatrina session?'
788
+ else 'Allow this book to use Beatrina?', '</title><style>',
773
789
  'body{font:16px/1.5 system-ui,sans-serif;max-width:42rem;margin:10vh auto;padding:0 1.25rem;color:#172033}',
774
790
  '.site{padding:.75rem 1rem;background:#f3f6fb;border-radius:.55rem;overflow-wrap:anywhere}',
775
791
  'button{border:0;border-radius:.5rem;background:#2357d9;color:white;padding:.7rem 1rem;font:inherit;font-weight:650;cursor:pointer}',
@@ -778,15 +794,15 @@ pairing_page <- function(origin, nonce = "") {
778
794
  paste0('<h1>Run this notebook file with your R?</h1>',
779
795
  '<p>A notebook opened from a file on this computer</p>',
780
796
  '<p class="site"><strong>a page opened from disk (file://)</strong></p>',
781
- '<p>wants to use this CarmaR session. The code will run as your user ',
797
+ '<p>wants to use this Beatrina session. The code will run as your user ',
782
798
  'and can access your files and network. A page opened from a file ',
783
799
  'cannot be identified any more exactly than this, so approve it only ',
784
800
  'if you just opened a notebook of your own.</p>')
785
801
  else if (is_local)
786
802
  paste0('<h1>Attach this notebook to this R session?</h1>',
787
- '<p>A CarmaR notebook page served at</p>',
803
+ '<p>A Beatrina notebook page served at</p>',
788
804
  '<p class="site"><strong>', html_escape(origin), '</strong></p>',
789
- '<p>wants to use this CarmaR session instead of its own &mdash; usually ',
805
+ '<p>wants to use this Beatrina session instead of its own &mdash; usually ',
790
806
  'because the session that served it has stopped. It will be treated as ',
791
807
  'your notebook: its chunks run as your user, and it may open a terminal ',
792
808
  'and use your saved AI settings. Approve it only if you just asked a ',
@@ -794,14 +810,14 @@ pairing_page <- function(origin, nonce = "") {
794
810
  else
795
811
  paste0('<h1>Run this book with your R?</h1><p>The published site</p>',
796
812
  '<p class="site"><strong>', html_escape(origin), '</strong></p>',
797
- '<p>wants to send R chunks to this CarmaR session on your computer. ',
813
+ '<p>wants to send R chunks to this Beatrina session on your computer. ',
798
814
  'The code will run as your user and can access your files and network.</p>'),
799
815
  '<form method="get" action="/pair/approve"><input type="hidden" name="challenge" value="',
800
816
  challenge, '"><button type="submit">',
801
817
  if (is_local) 'Attach for this session' else 'Allow for this session',
802
818
  '</button></form>',
803
819
  '<p><small>Nothing runs until you press a chunk\'s Run button. ',
804
- 'Approval disappears when this CarmaR session stops.</small></p></body></html>')
820
+ 'Approval disappears when this Beatrina session stops.</small></p></body></html>')
805
821
  }
806
822
 
807
823
  pairing_bridge_page <- function(rec) {
@@ -813,10 +829,10 @@ pairing_bridge_page <- function(rec) {
813
829
  '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; ',
814
830
  "script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:",
815
831
  port, "; base-uri 'none'\">",
816
- '<title>CarmaR connected</title><style>',
832
+ '<title>Beatrina connected</title><style>',
817
833
  'body{font:16px/1.5 system-ui,sans-serif;max-width:38rem;margin:10vh auto;padding:0 1.25rem;color:#172033}',
818
834
  '.site{overflow-wrap:anywhere;color:#2357d9}</style></head><body>',
819
- '<h1>CarmaR is connected</h1><p>This session now runs chunks pressed on</p>',
835
+ '<h1>Beatrina is connected</h1><p>This session now runs chunks pressed on</p>',
820
836
  '<p class="site"><strong>', html_escape(rec$origin), '</strong></p>',
821
837
  '<p>The page connects by itself and closes this window; if it stays open, you can close it. ',
822
838
  'The approval lasts until this R session stops.</p><script>(function(){',
@@ -825,15 +841,15 @@ pairing_bridge_page <- function(rec) {
825
841
  'Object.assign({type,nonce,origin:target,port:', port, '},extra||{}),target)};',
826
842
  'addEventListener("message",event=>{const data=event.data||{};',
827
843
  'if(event.source!==window.opener||event.origin!==target||data.nonce!==nonce)return;',
828
- 'if(data.type==="carmar:bridge-attach"){const generation=++wsGeneration,old=ws;',
844
+ 'if(data.type==="beatrina:bridge-attach"){const generation=++wsGeneration,old=ws;',
829
845
  'if(old){old.onmessage=old.onerror=old.onclose=null;try{old.close()}catch(e){}}',
830
846
  'ws=new WebSocket(socketUrl);const current=ws;',
831
- 'current.onmessage=message=>{if(generation===wsGeneration)tell("carmar:bridge-frame",{data:message.data})};',
832
- 'current.onerror=()=>{if(generation===wsGeneration)tell("carmar:bridge-error")};',
833
- 'current.onclose=()=>{if(generation===wsGeneration)tell("carmar:bridge-close")};return}',
834
- 'if(data.type==="carmar:bridge-send"&&ws&&ws.readyState===1)ws.send(data.data);',
835
- 'if(data.type==="carmar:bridge-close"&&ws)ws.close()});',
836
- 'tell("carmar:paired");})();</script></body></html>')
847
+ 'current.onmessage=message=>{if(generation===wsGeneration)tell("beatrina:bridge-frame",{data:message.data})};',
848
+ 'current.onerror=()=>{if(generation===wsGeneration)tell("beatrina:bridge-error")};',
849
+ 'current.onclose=()=>{if(generation===wsGeneration)tell("beatrina:bridge-close")};return}',
850
+ 'if(data.type==="beatrina:bridge-send"&&ws&&ws.readyState===1)ws.send(data.data);',
851
+ 'if(data.type==="beatrina:bridge-close"&&ws)ws.close()});',
852
+ 'tell("beatrina:paired");})();</script></body></html>')
837
853
  }
838
854
 
839
855
  pairing_approval_page <- function(challenge) {
@@ -860,14 +876,14 @@ pairing_done_page <- function(rec) {
860
876
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
861
877
  '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; ',
862
878
  "style-src 'unsafe-inline'; base-uri 'none'\">",
863
- '<title>CarmaR attached</title><style>',
879
+ '<title>Beatrina attached</title><style>',
864
880
  'body{font:16px/1.5 system-ui,sans-serif;max-width:38rem;margin:10vh auto;padding:0 1.25rem;color:#172033}',
865
881
  '.site{overflow-wrap:anywhere;color:#2357d9}</style></head><body>',
866
882
  '<h1>Attached</h1><p>The notebook</p>',
867
883
  '<p class="site"><strong>',
868
884
  if (identical(rec$origin, FILE_ORIGIN)) 'a page opened from disk (file://)' else html_escape(rec$origin),
869
885
  '</strong></p>',
870
- '<p>may use this CarmaR session until it stops. It reconnects by itself within ',
886
+ '<p>may use this Beatrina session until it stops. It reconnects by itself within ',
871
887
  'a few seconds; you can close this window.</p></body></html>')
872
888
  }
873
889
 
@@ -899,7 +915,7 @@ host_ok <- function(req) {
899
915
  #'
900
916
  #' Native same-user clients (the app launcher, R, curl, the MCP bridge) do not
901
917
  #' send Origin or Sec-Fetch-Site and remain valid. Browsers do: a foreign page
902
- #' cannot stop a kernel or ask it to open a file, while CarmaR keeps the clean,
918
+ #' cannot stop a kernel or ask it to open a file, while Beatrina keeps the clean,
903
919
  #' stable localhost URL users expect.
904
920
  control_rejection <- function(req, allow_file = FALSE, allow_approved = FALSE) {
905
921
  origin <- req$HTTP_ORIGIN %||% ""
@@ -970,14 +986,14 @@ socket_class <- function(req) {
970
986
  # returns quantiles, level counts and a histogram for ONE named column. It is
971
987
  # strictly narrower than the `view` beside it — same resolution, less surface,
972
988
  # no mutation — which is the bar a new entry has to clear, not "it is useful".
973
- #' The ports of CarmaR kernels that are actually answering right now.
989
+ #' The ports of Beatrina kernels that are actually answering right now.
974
990
  #'
975
991
  #' Read from the runtime directory rather than remembered, so sessions started
976
992
  #' by the R package launcher count too — and health-checked, because a killed
977
993
  #' kernel leaves its file behind and a dead session must not hold a slot.
978
994
  live_session_ports <- function() {
979
- dir <- Sys.getenv("CARMAR_RUNTIME_DIR",
980
- file.path(path.expand("~"), ".carmar", "run"))
995
+ dir <- Sys.getenv("BEATRINA_RUNTIME_DIR",
996
+ file.path(path.expand("~"), ".beatrina", "run"))
981
997
  files <- list.files(dir, pattern = "^kernel-[0-9]+\\.json$", full.names = TRUE)
982
998
  ports <- suppressWarnings(as.integer(sub("^kernel-([0-9]+)\\.json$", "\\1", basename(files))))
983
999
  ports <- ports[!is.na(ports)]
@@ -1021,19 +1037,19 @@ start_sibling_session <- function() {
1021
1037
  if (!(candidate %in% used)) { chosen <- candidate; break }
1022
1038
  }
1023
1039
  if (is.na(chosen)) return(NULL)
1024
- # `chosen` is a PREFERENCE, not a promise. serve.R takes CARMAR_PORT only if
1040
+ # `chosen` is a PREFERENCE, not a promise. serve.R takes BEATRINA_PORT only if
1025
1041
  # the port is still free when IT looks — between our probe and its start the
1026
1042
  # port can go, and it then falls back to a random one. Waiting for the exact
1027
1043
  # port we picked therefore reported failure for a session that had started
1028
1044
  # perfectly well, and left it orphaned. So: remember what was live before,
1029
1045
  # and accept whichever new port appears.
1030
1046
  before <- live_session_ports()
1031
- sib_log <- tryCatch(tempfile("carmar-session-", fileext = ".log"),
1047
+ sib_log <- tryCatch(tempfile("beatrina-session-", fileext = ".log"),
1032
1048
  error = function(e) nullfile())
1033
1049
  started <- tryCatch({
1034
1050
  processx::process$new(file.path(R.home("bin"), "Rscript"),
1035
1051
  c(file.path(here, "serve.R")),
1036
- env = c(Sys.getenv(), CARMAR_PORT = as.character(chosen)),
1052
+ env = c(Sys.getenv(), BEATRINA_PORT = as.character(chosen)),
1037
1053
  # cleanup = FALSE or processx kills this child when
1038
1054
  # `started` is garbage-collected, which is immediately:
1039
1055
  # it is a local that dies with this function. The
@@ -1066,7 +1082,7 @@ start_sibling_session <- function() {
1066
1082
 
1067
1083
  # ── the AI key lives HERE, not in the browser ────────────────────────────────
1068
1084
  #
1069
- # It used to sit in localStorage under `carmar-ai-key-kept`: in the browser's
1085
+ # It used to sit in localStorage under `beatrina-ai-key-kept`: in the browser's
1070
1086
  # storage for this origin, readable by anything that can run script in the page,
1071
1087
  # and one careless export away from being inside a notebook file. A provider key
1072
1088
  # is not notebook content — it is a machine credential — so it belongs where the
@@ -1078,6 +1094,8 @@ start_sibling_session <- function() {
1078
1094
  # Ops a declared MCP agent is refused outright: raw evaluation (it must go
1079
1095
  # through a visible chunk_run) and the user's credentials.
1080
1096
  AGENT_REFUSED <- c("exec", "interrupt", "force_stop", "restart", "session_upgrade", "session_restart", "debug_cmd", "ai-key",
1097
+ # A copy of every object in the session, written to disk.
1098
+ "suspend", "suspend_cancel",
1081
1099
  # The user's AI conversations. An agent reading them would be
1082
1100
  # reading every question the user has asked about their data,
1083
1101
  # including the ones they asked about a different agent.
@@ -1123,8 +1141,8 @@ AGENT_REFUSED <- c("exec", "interrupt", "force_stop", "restart", "session_upgrad
1123
1141
  # for the same reason FORWARDED and ANALYZE_FORWARDED stay two.
1124
1142
  #
1125
1143
  # Only a notebook page qualifies. A native client is refused even though it
1126
- # could read the 0600 file itself as this user: the point is not that CarmaR
1127
- # can stop it, but that CarmaR does not hand the key over — so the promise
1144
+ # could read the 0600 file itself as this user: the point is not that Beatrina
1145
+ # can stop it, but that Beatrina does not hand the key over — so the promise
1128
1146
  # holds for every agent instead of only the polite ones. A published site is
1129
1147
  # refused because its reader approved it to RUN THE CHUNKS THEY PRESS, not to
1130
1148
  # collect a credential on the way past.
@@ -1155,10 +1173,10 @@ AI_AUDIT_FIELDS <- c("provider", "model", "lane", "tool", "chunk", "decision",
1155
1173
  # The prompt and the response themselves. OFF unless an administrator asks,
1156
1174
  # because a notebook that silently recorded everything its user typed at an AI
1157
1175
  # would be a worse thing to ship than no audit at all — the same judgement
1158
- # CARMAR_LOG itself is off by default for. A compliance deployment turns it on
1176
+ # BEATRINA_LOG itself is off by default for. A compliance deployment turns it on
1159
1177
  # knowingly; a laptop never does.
1160
1178
  AI_AUDIT_TEXT_FIELDS <- c("prompt", "response")
1161
- ai_audit_text <- identical(Sys.getenv("CARMAR_LOG_AI_TEXT", ""), "1")
1179
+ ai_audit_text <- identical(Sys.getenv("BEATRINA_LOG_AI_TEXT", ""), "1")
1162
1180
 
1163
1181
  # How many ai-* records the reviewer export may read back at once.
1164
1182
  AI_AUDIT_READ_MAX <- 5000L
@@ -1174,7 +1192,7 @@ ai_audit_scalar <- function(value, limit) {
1174
1192
  }
1175
1193
 
1176
1194
  ai_key_path <- function() {
1177
- dir <- tools::R_user_dir("carmar", "config")
1195
+ dir <- tools::R_user_dir("beatrina", "config")
1178
1196
  dir.create(dir, recursive = TRUE, showWarnings = FALSE)
1179
1197
  file.path(dir, "ai-key")
1180
1198
  }
@@ -1221,7 +1239,7 @@ ai_key_write <- function(value) {
1221
1239
  # user directory and never in the notebook file, an export, or localStorage —
1222
1240
  # where any local page sharing the file:// origin could read the user's code.
1223
1241
  #
1224
- # Off entirely with CARMAR_NO_HISTORY=1, because "record everything I type"
1242
+ # Off entirely with BEATRINA_NO_HISTORY=1, because "record everything I type"
1225
1243
  # must be refusable.
1226
1244
  HISTORY_MAX <- 2000L
1227
1245
  # Consulted INSIDE history_read()/history_add() per call, never captured at the
@@ -1230,7 +1248,7 @@ HISTORY_MAX <- 2000L
1230
1248
  HISTORY_ENABLED <- isTRUE(settings_value("history_enabled"))
1231
1249
 
1232
1250
  history_path <- function() {
1233
- dir <- tools::R_user_dir("carmar", "data")
1251
+ dir <- tools::R_user_dir("beatrina", "data")
1234
1252
  dir.create(dir, recursive = TRUE, showWarnings = FALSE)
1235
1253
  file.path(dir, "console-history")
1236
1254
  }
@@ -1275,7 +1293,7 @@ history_add <- function(line) {
1275
1293
 
1276
1294
  # ── appearance, across the two origins the notebook is delivered from ────────
1277
1295
  #
1278
- # CarmaR delivers the SAME notebook from a file:// page and from this kernel's
1296
+ # Beatrina delivers the SAME notebook from a file:// page and from this kernel's
1279
1297
  # own http://127.0.0.1:PORT, and browsers partition storage by origin — so a
1280
1298
  # theme chosen at one door is invisible at the other. That is not a theory: it
1281
1299
  # cost an afternoon on 2026-09-10, when an owner's Flat theme turned out to be
@@ -1298,41 +1316,41 @@ history_add <- function(line) {
1298
1316
  # local HTML file shares the file:// origin, so without it `prefs` is a
1299
1317
  # free-form key/value bag any of them could fill.
1300
1318
  #
1301
- # The values travel as the STRINGS localStorage holds. carmar-chunk-custom is
1319
+ # The values travel as the STRINGS localStorage holds. beatrina-chunk-custom is
1302
1320
  # itself JSON; re-parsing it here would make the kernel a party to a format
1303
1321
  # only the page understands, and jsonlite would round its numbers on the way
1304
1322
  # back out — the frame-fidelity lesson, one store further in.
1305
1323
  #
1306
- # Off entirely with CARMAR_NO_PREFS=1, for the same reason CARMAR_NO_HISTORY
1324
+ # Off entirely with BEATRINA_NO_PREFS=1, for the same reason BEATRINA_NO_HISTORY
1307
1325
  # exists: "keep a record of my choices on this machine" must be refusable. It
1308
1326
  # is also what keeps the TEST BATTERY out of the real store — unlike the
1309
1327
  # history, which only fills when somebody types, this publishes on every page
1310
1328
  # connect, so any suite that boots a real page would otherwise seed the user's
1311
1329
  # appearance with whatever its scratch page happened to hold. Set by
1312
1330
  # tools/run-test-suite.mjs for every suite it runs.
1313
- PREFS_ENABLED <- !identical(Sys.getenv("CARMAR_NO_PREFS", ""), "1")
1331
+ PREFS_ENABLED <- !identical(Sys.getenv("BEATRINA_NO_PREFS", ""), "1")
1314
1332
  PREF_MAX_VALUE <- 4096L
1315
1333
  PREF_KEYS <- c(
1316
- "carmar-app-theme", # lib/app-themes.js standard/flat/plain/graphite/onyx
1317
- "carmar-editor-theme", # lib/editor-themes.js one of 37
1318
- "carmar-chunk-style", # lib/chunk-style.js the pre-7.8 key, kept in step
1319
- "carmar-chunk-style-v2", # lib/chunk-style.js the explicit choice
1320
- "carmar-chunk-custom", # lib/chunk-style.js the designer's token overrides
1321
- "carmar-writing-chunk", # lib/writing-chunk.js the Writing surface's own card
1322
- "carmar-run-style", # lib/run-style.js
1323
- "carmar-table-style-v2", # lib/table-render.js
1324
- "carmar-line-numbering", # lib/line-numbering.js
1325
- "carmar-density", # lib/density.js
1326
- "carmar-add-strip-v3", # lib/add-strip.js
1327
- "carmar-soft-wrap", # lib/soft-wrap.js
1328
- "carmar-content-width-v1", # src/notebook-entry.js Standard / Wide / Full
1329
- "carmar-brand-v1", # lib/brand.js the organisation's accent
1330
- "carmar:rich-text", # lib/rtf-editor.js
1331
- "carmar:latex-template" # lib/latex-templates.js
1334
+ "beatrina-app-theme", # lib/app-themes.js standard/flat/plain/graphite/onyx
1335
+ "beatrina-editor-theme", # lib/editor-themes.js one of 37
1336
+ "beatrina-chunk-style", # lib/chunk-style.js the pre-7.8 key, kept in step
1337
+ "beatrina-chunk-style-v2", # lib/chunk-style.js the explicit choice
1338
+ "beatrina-chunk-custom", # lib/chunk-style.js the designer's token overrides
1339
+ "beatrina-writing-chunk", # lib/writing-chunk.js the Writing surface's own card
1340
+ "beatrina-run-style", # lib/run-style.js
1341
+ "beatrina-table-style-v2", # lib/table-render.js
1342
+ "beatrina-line-numbering", # lib/line-numbering.js
1343
+ "beatrina-density", # lib/density.js
1344
+ "beatrina-add-strip-v3", # lib/add-strip.js
1345
+ "beatrina-soft-wrap", # lib/soft-wrap.js
1346
+ "beatrina-content-width-v1", # src/notebook-entry.js Standard / Wide / Full
1347
+ "beatrina-brand-v1", # lib/brand.js the organisation's accent
1348
+ "beatrina:rich-text", # lib/rtf-editor.js
1349
+ "beatrina:latex-template" # lib/latex-templates.js
1332
1350
  )
1333
1351
 
1334
1352
  prefs_path <- function() {
1335
- dir <- tools::R_user_dir("carmar", "data")
1353
+ dir <- tools::R_user_dir("beatrina", "data")
1336
1354
  dir.create(dir, recursive = TRUE, showWarnings = FALSE)
1337
1355
  file.path(dir, "appearance.json")
1338
1356
  }
@@ -1368,7 +1386,7 @@ prefs_write <- function(values) {
1368
1386
  clean <- prefs_clean(values)
1369
1387
  path <- prefs_path()
1370
1388
  # Create, then lock, then write — the order the key and the history beside
1371
- # it use. A theme is not a secret; the point is that everything CarmaR puts
1389
+ # it use. A theme is not a secret; the point is that everything Beatrina puts
1372
1390
  # in the user's data directory is the user's, and judging file by file which
1373
1391
  # ones are harmless is how one of them eventually gets it wrong.
1374
1392
  if (!file.exists(path) && !file.create(path)) return(FALSE)
@@ -1383,6 +1401,11 @@ FORWARDED <- c("env", "obj", "struct", "view", "colstats", "rm", "packages", "do
1383
1401
  "package_action", "package_help", "project_status", "project_action", "help", "wd",
1384
1402
  "parse", "complete", "files", "import", "readfile", "writefile", "writefiles_atomic",
1385
1403
  "hover", "format", "sniff",
1404
+ # "Restart, keep variables", step one (spike/workspace-keep.R).
1405
+ # Page-only in both senses: AGENT_REFUSED above, the class gate
1406
+ # in handle_frame. Step two, `resume`, is deliberately NOT here —
1407
+ # only the supervisor asks it, after a restart it performed.
1408
+ "suspend",
1386
1409
  # The file tree's New Folder, Rename and Delete. These MUTATE
1387
1410
  # the filesystem, which is a wider door than the read-only ops
1388
1411
  # around them, so the bar they clear is stated rather than
@@ -1416,7 +1439,7 @@ FORWARDED <- c("env", "obj", "struct", "view", "colstats", "rm", "packages", "do
1416
1439
  # or reaches a process that cannot do it. Adding to EITHER list is a security
1417
1440
  # decision (CLAUDE.md, "FORWARDED in serve.R is an allow-list").
1418
1441
  # The third allow-list. These are answered by THIS process (fileio.R) and never
1419
- # reach the worker's queue, unless CARMAR_FILE_OPS=worker asks for the old
1442
+ # reach the worker's queue, unless BEATRINA_FILE_OPS=worker asks for the old
1420
1443
  # routing. Kept apart from FORWARDED for the reason the first two are apart:
1421
1444
  # "may the browser ask for this?" and "which process answers?" are different
1422
1445
  # questions, and a file op's answer is "the one that is never busy".
@@ -1440,7 +1463,7 @@ SUPERVISOR_JOURNAL_OPS <- c("journal_open", "journal_append", "journal_checkpoin
1440
1463
  # a conversation holds the questions you asked about your data plus the chunk
1441
1464
  # sources those questions carried.
1442
1465
  SUPERVISOR_AI_OPS <- c("ai_list", "ai_read", "ai_write", "ai_delete", "ai_purge", "ai_keys")
1443
- FILE_OPS_IN_WORKER <- identical(Sys.getenv("CARMAR_FILE_OPS", ""), "worker")
1466
+ FILE_OPS_IN_WORKER <- identical(Sys.getenv("BEATRINA_FILE_OPS", ""), "worker")
1444
1467
  # The worker's working directory, as it last reported it (ready, wd and done
1445
1468
  # frames). setwd() in a chunk moves it; a relative path from the page is
1446
1469
  # resolved against THIS, never against the supervisor's own cwd.
@@ -1550,8 +1573,8 @@ app <- list(
1550
1573
  # unauthenticated and CORS-open on purpose.
1551
1574
  r = paste(R.version$major, R.version$minor, sep = "."),
1552
1575
  started = as.numeric(sockets$boot_at),
1553
- protocol = CARMAR_PROTOCOL_VERSION,
1554
- kernel_build = CARMAR_KERNEL_BUILD,
1576
+ protocol = BEATRINA_PROTOCOL_VERSION,
1577
+ kernel_build = BEATRINA_KERNEL_BUILD,
1555
1578
  # What is installed beside this kernel NOW, and
1556
1579
  # how many pages hold it — the two facts an
1557
1580
  # upgrade needs from a running session.
@@ -1568,7 +1591,7 @@ app <- list(
1568
1591
  auto_unbox = TRUE),
1569
1592
  extra = list("Access-Control-Allow-Origin" = "*")))
1570
1593
  }
1571
- # The custom carmar:// handler calls this endpoint as a native same-user
1594
+ # The custom beatrina:// handler calls this endpoint as a native same-user
1572
1595
  # client. Browser requests carry Origin/Sec-Fetch-Site and are refused by
1573
1596
  # control_rejection(), so visiting a site cannot silently grant itself R
1574
1597
  # execution. One launch authorizes one exact origin for this kernel only.
@@ -1595,7 +1618,7 @@ app <- list(
1595
1618
  nonce <- query_param(req$QUERY_STRING, "nonce")
1596
1619
  # FILE_ORIGIN is accepted HERE and only here. `published_origin_valid()`
1597
1620
  # stays strict (`^https?://…`) because it also guards
1598
- # /published/authorize and the CARMAR_PUBLISHED_ORIGIN seed — widening it
1621
+ # /published/authorize and the BEATRINA_PUBLISHED_ORIGIN seed — widening it
1599
1622
  # would let an environment variable or a native launch silently
1600
1623
  # pre-approve every file on disk, which is the opposite of the point.
1601
1624
  if (!identical(origin, FILE_ORIGIN) && !published_origin_valid(origin)) {
@@ -1614,7 +1637,7 @@ app <- list(
1614
1637
  page <- pairing_approval_page(query_param(req$QUERY_STRING, "challenge"))
1615
1638
  if (is.null(page)) {
1616
1639
  audit("pair-rejected", detail = "missing or expired challenge")
1617
- return(resp(400L, "text/plain", "This CarmaR approval request has expired."))
1640
+ return(resp(400L, "text/plain", "This Beatrina approval request has expired."))
1618
1641
  }
1619
1642
  return(resp(200L, "text/html", page))
1620
1643
  }
@@ -1625,12 +1648,12 @@ app <- list(
1625
1648
  blocked <- control_rejection(req, allow_file = TRUE)
1626
1649
  if (!is.null(blocked)) return(blocked)
1627
1650
  audit("shutdown")
1628
- message("CarmaR stopping: explicit Quit request.")
1651
+ message("Beatrina stopping: explicit Quit request.")
1629
1652
  sockets$quit <- TRUE
1630
1653
  return(resp(200L, "application/json",
1631
1654
  toJSON(list(ok = TRUE, stopping = TRUE), auto_unbox = TRUE)))
1632
1655
  }
1633
- # The native door of "Restart into the installed CarmaR" — the menu
1656
+ # The native door of "Restart into the installed Beatrina" — the menu
1634
1657
  # helper's row and the upgrade script use it (launch.sh --restart). Gated
1635
1658
  # exactly like /shutdown; the page's own door is the session_upgrade op.
1636
1659
  if (identical(req$PATH_INFO, "/session/upgrade")) {
@@ -1678,7 +1701,7 @@ app <- list(
1678
1701
  # document reaches a kernel that is ALREADY RUNNING, and it is the reason
1679
1702
  # opening a file no longer costs an R session.
1680
1703
  #
1681
- # The other channel — CARMAR_OPEN_FILE in the supervisor's environment —
1704
+ # The other channel — BEATRINA_OPEN_FILE in the supervisor's environment —
1682
1705
  # is read once at startup, so the only way to deliver a document through
1683
1706
  # it is to start a new process. That is precisely what the launcher used
1684
1707
  # to do: every double-click was a fresh supervisor, worker and analyzer,
@@ -1777,7 +1800,7 @@ app <- list(
1777
1800
  if (identical(method, "OPTIONS")) {
1778
1801
  return(resp(204L, "text/plain", "", extra = c(cors, list(
1779
1802
  "Access-Control-Allow-Methods" = "GET, PUT, OPTIONS",
1780
- "Access-Control-Allow-Headers" = "X-CarmaR-Cap, Content-Type, X-CarmaR-Expected-Bytes",
1803
+ "Access-Control-Allow-Headers" = "X-Beatrina-Cap, Content-Type, X-Beatrina-Expected-Bytes",
1781
1804
  "Access-Control-Max-Age" = "60"))))
1782
1805
  }
1783
1806
  if (identical(method, "GET")) {
@@ -1795,7 +1818,7 @@ app <- list(
1795
1818
  "Content-Length" = as.character(length(bytes))), cors)))
1796
1819
  }
1797
1820
  if (identical(method, "PUT")) {
1798
- grant <- file_cap_take(req$HTTP_X_CARMAR_CAP %||% "", "write")
1821
+ grant <- file_cap_take(req$HTTP_X_BEATRINA_CAP %||% "", "write")
1799
1822
  if (is.null(grant) || !within_root(grant$path)) {
1800
1823
  audit("files-refused", detail = "put")
1801
1824
  return(resp(403L, "text/plain", "no capability for that write", extra = cors))
@@ -1813,7 +1836,7 @@ app <- list(
1813
1836
  }
1814
1837
  expected <- NULL
1815
1838
  if (identical(grant$compare, "utf8-prefix-v1")) {
1816
- prefix <- req$HTTP_X_CARMAR_EXPECTED_BYTES %||% ""
1839
+ prefix <- req$HTTP_X_BEATRINA_EXPECTED_BYTES %||% ""
1817
1840
  n <- suppressWarnings(as.numeric(prefix))
1818
1841
  if (!grepl("^[0-9]+$", prefix) || !is.finite(n) || n > length(body)) {
1819
1842
  return(resp(400L, "text/plain", "missing or invalid expected-content prefix", extra = cors))
@@ -1850,7 +1873,7 @@ app <- list(
1850
1873
  # the Rook environment belongs to the request that opened this socket and
1851
1874
  # cannot change under it.
1852
1875
  rec$class <- socket_class(ws$request)
1853
- # Who the reverse proxy says this is. "" on loopback, always — CarmaR has
1876
+ # Who the reverse proxy says this is. "" on loopback, always — Beatrina has
1854
1877
  # no login of its own and must not grow one here: a header is an identity
1855
1878
  # only for as long as nobody but the proxy can reach the port, which is a
1856
1879
  # property of the deployment, not of this process.
@@ -1865,6 +1888,10 @@ app <- list(
1865
1888
  audit("socket-open", sockets = length(sockets$open), class = rec$class,
1866
1889
  user = rec$user)
1867
1890
  if (!is.null(sockets$hello)) try(ws$send(sockets$hello), silent = TRUE)
1891
+ if (!is.null(sockets$resume_report) && isTRUE(rec$class %in% PAGE_ONLY_CLASSES)
1892
+ && as.numeric(difftime(Sys.time(), sockets$resume_report$at, units = "secs")) < 120) {
1893
+ try(ws$send(sockets$resume_report$payload), silent = TRUE)
1894
+ }
1868
1895
  # …and the give-up, when there is one. A page that arrives after the
1869
1896
  # respawn ceiling has no ready frame coming and would otherwise sit on
1870
1897
  # "connecting…" forever with nothing on screen saying why.
@@ -1964,17 +1991,17 @@ target_page <- function() {
1964
1991
  # the CLI's stream-json lines back to the asking page verbatim, and that is
1965
1992
  # the whole exchange. Credentials are never read, stored, or proxied.
1966
1993
  #
1967
- # The spawned session gets the carmar MCP tools, pinned to THIS kernel via a
1994
+ # The spawned session gets the beatrina MCP tools, pinned to THIS kernel via a
1968
1995
  # 0600 config file (never argv — argv is visible to every local user in ps),
1969
1996
  # and the document tools. The embedded Claude process is a document agent, not
1970
1997
  # a chatbot that prints a draft for the user to move by hand. Inserts and
1971
1998
  # replacements cross the page's revision/contract gateway and remain
1972
1999
  # provisional with Keep/Reject; runs use the ordinary visible chunk path.
1973
2000
  AGENT_CHAT_TOOLS <- paste(
1974
- "mcp__carmar__carmar_status", "mcp__carmar__notebook_read",
1975
- "mcp__carmar__chunk_read", "mcp__carmar__chunk_insert",
1976
- "mcp__carmar__chunk_update", "mcp__carmar__chunk_run",
1977
- "mcp__carmar__file_list", "mcp__carmar__file_read",
2001
+ "mcp__beatrina__beatrina_status", "mcp__beatrina__notebook_read",
2002
+ "mcp__beatrina__chunk_read", "mcp__beatrina__chunk_insert",
2003
+ "mcp__beatrina__chunk_update", "mcp__beatrina__chunk_run",
2004
+ "mcp__beatrina__file_list", "mcp__beatrina__file_read",
1978
2005
  sep = ",")
1979
2006
  MAX_CHATS <- 3L
1980
2007
 
@@ -2029,7 +2056,7 @@ chat_start <- function(rec, cmd) {
2029
2056
  server <- mcp_server_path()
2030
2057
  node <- find_binary("node")
2031
2058
  if (identical(client, "codex")) {
2032
- cfg <- tempfile("carmar-codex-")
2059
+ cfg <- tempfile("beatrina-codex-")
2033
2060
  dir.create(cfg, mode = "0700")
2034
2061
  toml_str <- function(x) as.character(toJSON(x, auto_unbox = TRUE))
2035
2062
  args <- c("exec", "--json", "--ephemeral", "--skip-git-repo-check",
@@ -2044,9 +2071,9 @@ chat_start <- function(rec, cmd) {
2044
2071
  }
2045
2072
  if (!text_only && nzchar(server) && nzchar(node)) {
2046
2073
  args <- c(args,
2047
- "-c", paste0("mcp_servers.carmar.command=", toml_str(node)),
2048
- "-c", paste0("mcp_servers.carmar.args=[", toml_str(server), "]"),
2049
- "-c", paste0("mcp_servers.carmar.env={CARMAR_MCP_URL=", toml_str(paste0(origin_ok, "/")), "}"))
2074
+ "-c", paste0("mcp_servers.beatrina.command=", toml_str(node)),
2075
+ "-c", paste0("mcp_servers.beatrina.args=[", toml_str(server), "]"),
2076
+ "-c", paste0("mcp_servers.beatrina.env={BEATRINA_MCP_URL=", toml_str(paste0(origin_ok, "/")), "}"))
2050
2077
  }
2051
2078
  args <- c(args, "-") # the prompt arrives on stdin
2052
2079
  } else {
@@ -2062,10 +2089,10 @@ chat_start <- function(rec, cmd) {
2062
2089
  args <- c(args, "--tools", "", "--mcp-config", '{"mcpServers":{}}',
2063
2090
  "--strict-mcp-config", "--setting-sources", "")
2064
2091
  } else if (nzchar(server) && nzchar(node)) {
2065
- cfg <- tempfile("carmar-chat-", fileext = ".json")
2066
- writeLines(toJSON(list(mcpServers = list(carmar = list(
2092
+ cfg <- tempfile("beatrina-chat-", fileext = ".json")
2093
+ writeLines(toJSON(list(mcpServers = list(beatrina = list(
2067
2094
  command = node, args = I(server),
2068
- env = list(CARMAR_MCP_URL = paste0(origin_ok, "/"))
2095
+ env = list(BEATRINA_MCP_URL = paste0(origin_ok, "/"))
2069
2096
  ))), auto_unbox = TRUE), cfg)
2070
2097
  Sys.chmod(cfg, mode = "0600")
2071
2098
  args <- c(args, "--mcp-config", cfg, "--strict-mcp-config",
@@ -2150,7 +2177,7 @@ pump_chats <- function() {
2150
2177
  #' Every kernel this user is running, from the runtime records, health-checked.
2151
2178
  #' @return An unnamed list of `list(port, url, title, pid, alive, this)`.
2152
2179
  session_list <- function() {
2153
- dir <- Sys.getenv("CARMAR_RUNTIME_DIR", file.path(path.expand("~"), ".carmar", "run"))
2180
+ dir <- Sys.getenv("BEATRINA_RUNTIME_DIR", file.path(path.expand("~"), ".beatrina", "run"))
2154
2181
  files <- list.files(dir, pattern = "^kernel-[0-9]+\\.json$", full.names = TRUE)
2155
2182
  out <- lapply(files, function(f) {
2156
2183
  rec <- tryCatch(jsonlite::fromJSON(f, simplifyVector = TRUE), error = function(e) NULL)
@@ -2229,7 +2256,7 @@ r_versions <- function() {
2229
2256
  # the MCP plane refuses, wearing a prompt.
2230
2257
  #
2231
2258
  # What the shell can do is what the user can do; this grants nothing new and
2232
- # is audited like every other door. `CARMAR_NO_TERMINAL=1` removes it (a
2259
+ # is audited like every other door. `BEATRINA_NO_TERMINAL=1` removes it (a
2233
2260
  # deployment that must not offer a shell); Windows has no pty here and says so.
2234
2261
  MAX_TERMS <- 4L
2235
2262
  TERM_INPUT_MAX <- 65536L
@@ -2256,11 +2283,11 @@ term_open <- function(rec, cmd) {
2256
2283
  term_send(rec, "", event = "refused", error = why)
2257
2284
  invisible(NULL)
2258
2285
  }
2259
- if (identical(Sys.getenv("CARMAR_NO_TERMINAL"), "1")) {
2260
- return(refuse("The terminal is turned off on this kernel (CARMAR_NO_TERMINAL=1)."))
2286
+ if (identical(Sys.getenv("BEATRINA_NO_TERMINAL"), "1")) {
2287
+ return(refuse("The terminal is turned off on this kernel (BEATRINA_NO_TERMINAL=1)."))
2261
2288
  }
2262
2289
  if (.Platform$OS.type == "windows") {
2263
- return(refuse("The terminal needs a pty, which CarmaR does not provide on Windows yet."))
2290
+ return(refuse("The terminal needs a pty, which Beatrina does not provide on Windows yet."))
2264
2291
  }
2265
2292
  mine <- Filter(function(t) identical(t$rec, rec), mget(ls(sockets$terms), envir = sockets$terms))
2266
2293
  if (length(mine) >= MAX_TERMS) {
@@ -2281,7 +2308,7 @@ term_open <- function(rec, cmd) {
2281
2308
  env = c("current", TERM = "xterm-256color",
2282
2309
  COLORTERM = "truecolor",
2283
2310
  COLUMNS = as.character(cols), LINES = as.character(rows),
2284
- CARMAR_PORT = as.character(port))),
2311
+ BEATRINA_PORT = as.character(port))),
2285
2312
  error = function(e) conditionMessage(e))
2286
2313
  if (is.character(proc)) return(refuse(paste("the shell could not start:", proc)))
2287
2314
  sockets$term_seq <- sockets$term_seq + 1L
@@ -2374,7 +2401,7 @@ JOB_LOG_CHARS <- 1000000L
2374
2401
  # handed the token, and short enough that a token in a stray log is dead by
2375
2402
  # the time anyone reads it. Tests shorten it.
2376
2403
  OUTPUT_CAP_TTL <- local({
2377
- given <- suppressWarnings(as.numeric(Sys.getenv("CARMAR_OUTPUT_CAP_TTL", "")))
2404
+ given <- suppressWarnings(as.numeric(Sys.getenv("BEATRINA_OUTPUT_CAP_TTL", "")))
2378
2405
  if (is.finite(given) && given >= 1 && given <= 3600) given else 60
2379
2406
  })
2380
2407
  OUTPUT_CAP_MAX_BYTES <- 128 * 1024 * 1024
@@ -2412,7 +2439,7 @@ output_cap_sweep <- function() {
2412
2439
  # anything /health discloses; for PUT it travels in a header, never the query.
2413
2440
  FILE_CAP_TTL <- OUTPUT_CAP_TTL
2414
2441
  FILE_UPLOAD_MAX <- local({
2415
- given <- suppressWarnings(as.numeric(Sys.getenv("CARMAR_UPLOAD_MAX", "")))
2442
+ given <- suppressWarnings(as.numeric(Sys.getenv("BEATRINA_UPLOAD_MAX", "")))
2416
2443
  if (is.finite(given) && given >= 1e6 && given <= 2e9) given else 64 * 1024 * 1024
2417
2444
  })
2418
2445
  sockets$file_caps <- list()
@@ -2549,7 +2576,7 @@ job_start <- function(rec, cmd) {
2549
2576
  # settings under both, so a quarto_path set in Settings reaches the render
2550
2577
  # while a job that names its own still wins.
2551
2578
  kernel_start(file.path(here, "job-run.R"), rscript = settings_rscript(),
2552
- env_extra = c(settings_child_env(), spec$env, CARMAR_JOB_ID = id)),
2579
+ env_extra = c(settings_child_env(), spec$env, BEATRINA_JOB_ID = id)),
2553
2580
  error = function(e) conditionMessage(e))
2554
2581
  if (is.character(started)) {
2555
2582
  audit("job-start-failed", detail = started)
@@ -2563,7 +2590,7 @@ job_start <- function(rec, cmd) {
2563
2590
  job$task <- spec$task
2564
2591
  job$root <- spec$root
2565
2592
  job$target <- spec$target
2566
- identity_text <- spec$env["CARMAR_LATEX_IDENTITY"]
2593
+ identity_text <- spec$env["BEATRINA_LATEX_IDENTITY"]
2567
2594
  job$identity <- if (!is.na(identity_text) && nzchar(identity_text)) fromJSON(identity_text, simplifyVector = FALSE) else NULL
2568
2595
  job$output <- NA_character_
2569
2596
  job$title <- job_title(spec)
@@ -2737,7 +2764,7 @@ update_runner_spec <- function(script, args = character(),
2737
2764
  }
2738
2765
 
2739
2766
  update_script_path <- function() {
2740
- script <- trimws(Sys.getenv("CARMAR_UPDATE_SCRIPT", ""))
2767
+ script <- trimws(Sys.getenv("BEATRINA_UPDATE_SCRIPT", ""))
2741
2768
  if (!isTRUE(deployment$loopback) || !nzchar(script) || !file.exists(script)) return("")
2742
2769
  script <- normalizePath(script, winslash = "/", mustWork = TRUE)
2743
2770
  if (is.null(update_runner_spec(script))) return("")
@@ -2780,7 +2807,7 @@ update_job_busy <- function() {
2780
2807
  update_status_state <- function() {
2781
2808
  script <- update_script_path()
2782
2809
  if (!nzchar(script)) return(list(
2783
- supported = FALSE, state = "unavailable", current = CARMAR_KERNEL_BUILD,
2810
+ supported = FALSE, state = "unavailable", current = BEATRINA_KERNEL_BUILD,
2784
2811
  offered = "", previous = "", busy = FALSE, action = "",
2785
2812
  can_check = FALSE, can_update = FALSE, can_defer = FALSE,
2786
2813
  can_rollback = FALSE,
@@ -2789,7 +2816,7 @@ update_status_state <- function() {
2789
2816
 
2790
2817
  busy <- update_job_busy()
2791
2818
  if (busy) return(list(
2792
- supported = TRUE, state = "checking", current = CARMAR_KERNEL_BUILD,
2819
+ supported = TRUE, state = "checking", current = BEATRINA_KERNEL_BUILD,
2793
2820
  offered = "", previous = "", busy = TRUE, action = update_job$action,
2794
2821
  can_check = FALSE, can_update = FALSE, can_defer = FALSE,
2795
2822
  can_rollback = FALSE,
@@ -3072,7 +3099,7 @@ worker_deadline_check <- function() {
3072
3099
  sockets$worker_routes[[wire_id]] else NULL
3073
3100
  reason <- sprintf(
3074
3101
  paste("R did not answer `%s` within %d seconds. The session and its variables are",
3075
- "intact; only this request was abandoned. If it keeps happening, this CarmaR",
3102
+ "intact; only this request was abandoned. If it keeps happening, this Beatrina",
3076
3103
  "may be newer than the R session it is talking to — restart R."),
3077
3104
  type, as.integer(round(limit)))
3078
3105
  audit("worker-deadline", detail = type, id = wire_id,
@@ -3092,7 +3119,7 @@ worker_deadline_check <- function() {
3092
3119
  # ── restart into the installed build ───────────────────────────────────────
3093
3120
  #
3094
3121
  # An upgrade swaps the bundle IN PLACE, so the kernel-version stamp beside
3095
- # this script names the installed build while CARMAR_KERNEL_BUILD names the
3122
+ # this script names the installed build while BEATRINA_KERNEL_BUILD names the
3096
3123
  # one running. The two disagreeing is the one honest signal that a session
3097
3124
  # is stale, and this is the one door through it: start a SUCCESSOR
3098
3125
  # supervisor from the installed files on this same port, tell every page
@@ -3128,13 +3155,13 @@ handoff_refusal <- function(mode = "upgrade", force = FALSE) {
3128
3155
  refuse <- function(error, reason) list(error = error, reason = reason)
3129
3156
  installed <- installed_build()
3130
3157
  if (identical(installed, "unknown")) {
3131
- return(refuse("This kernel cannot tell which CarmaR is installed.", "unknown-install"))
3158
+ return(refuse("This kernel cannot tell which Beatrina is installed.", "unknown-install"))
3132
3159
  }
3133
- if (identical(mode, "upgrade") && identical(installed, CARMAR_KERNEL_BUILD)) {
3134
- return(refuse(sprintf("This session already runs the installed CarmaR %s.", installed), "same-build"))
3160
+ if (identical(mode, "upgrade") && identical(installed, BEATRINA_KERNEL_BUILD)) {
3161
+ return(refuse(sprintf("This session already runs the installed Beatrina %s.", installed), "same-build"))
3135
3162
  }
3136
- if (!nzchar(carmar_notebook_page(here, installed))) {
3137
- return(refuse(sprintf("The installed CarmaR %s has no notebook page beside this kernel.", installed), "no-page"))
3163
+ if (!nzchar(beatrina_notebook_page(here, installed))) {
3164
+ return(refuse(sprintf("The installed Beatrina %s has no notebook page beside this kernel.", installed), "no-page"))
3138
3165
  }
3139
3166
  # Force never spawns a second successor for one port: an attempt that has
3140
3167
  # spawned owns the port until this process exits.
@@ -3153,7 +3180,7 @@ handoff_refusal <- function(mode = "upgrade", force = FALSE) {
3153
3180
  #' kernel, with this port and a capability minted here so it can be told
3154
3181
  #' to the page NOW and honoured by the successor later.
3155
3182
  handoff_page_url <- function(installed, cap) {
3156
- page_file <- normalizePath(carmar_notebook_page(here, installed))
3183
+ page_file <- normalizePath(beatrina_notebook_page(here, installed))
3157
3184
  paste0("file://", utils::URLencode(page_file, reserved = FALSE),
3158
3185
  "#kernel=", port, "&pair=", cap)
3159
3186
  }
@@ -3166,9 +3193,11 @@ handoff_page_url <- function(installed, cap) {
3166
3193
  #' only ever mean "spawned, exiting".
3167
3194
  #' @param mode "upgrade" or "restart".
3168
3195
  #' @param force Kill a busy worker rather than refuse (restart only).
3196
+ #' @param resume A kept-workspace token (spike/workspace-keep.R) the successor's
3197
+ #' first worker restores, or NULL for a fresh R.
3169
3198
  #' @return The reply the caller sends: ok/from/to/page/started/mode/force,
3170
3199
  #' or ok = FALSE with error and reason.
3171
- session_handoff_begin <- function(mode = "upgrade", force = FALSE) {
3200
+ session_handoff_begin <- function(mode = "upgrade", force = FALSE, resume = NULL) {
3172
3201
  mode <- if (identical(mode, "restart")) "restart" else "upgrade"
3173
3202
  force <- isTRUE(force) && identical(mode, "restart")
3174
3203
  audit_name <- if (identical(mode, "restart")) "session-restart" else "session-upgrade"
@@ -3181,18 +3210,20 @@ session_handoff_begin <- function(mode = "upgrade", force = FALSE) {
3181
3210
  cap <- secure_token(32L)
3182
3211
  page <- handoff_page_url(installed, cap)
3183
3212
  sockets$handoff <- list(to = installed, cap = cap, page = page,
3184
- started = Sys.time(), mode = mode, force = force)
3185
- audit(audit_name, from = CARMAR_KERNEL_BUILD, to = installed, force = force)
3213
+ started = Sys.time(), mode = mode, force = force,
3214
+ resume = if (keep_token_ok(resume)) resume else NULL)
3215
+ audit(audit_name, from = BEATRINA_KERNEL_BUILD, to = installed, force = force)
3186
3216
  # Every page learns where its successor's notebook is — over the gated
3187
3217
  # socket only; the capability never appears in /health. `started` is this
3188
3218
  # kernel's boot time: the page recognises its successor by a LATER one,
3189
3219
  # which is the only test that also works when the build does not change.
3190
- told <- toJSON(list(type = "session-upgrade", from = CARMAR_KERNEL_BUILD,
3220
+ told <- toJSON(list(type = "session-upgrade", from = BEATRINA_KERNEL_BUILD,
3191
3221
  to = installed, page = page, started = as.numeric(sockets$boot_at),
3192
- mode = mode, force = force), auto_unbox = TRUE)
3222
+ mode = mode, force = force,
3223
+ keep = !is.null(sockets$handoff$resume)), auto_unbox = TRUE)
3193
3224
  lapply(page_recs(), function(r) try(r$ws$send(told), silent = TRUE))
3194
3225
  session_handoff_spawn()
3195
- list(ok = TRUE, from = CARMAR_KERNEL_BUILD, to = installed, page = page,
3226
+ list(ok = TRUE, from = BEATRINA_KERNEL_BUILD, to = installed, page = page,
3196
3227
  started = as.numeric(sockets$boot_at), mode = mode, force = force)
3197
3228
  }
3198
3229
 
@@ -3216,15 +3247,18 @@ session_handoff_spawn <- function() {
3216
3247
  state <- state_dir()
3217
3248
  dir.create(state, recursive = TRUE, showWarnings = FALSE)
3218
3249
  env <- Sys.getenv()
3219
- env[["CARMAR_PORT"]] <- as.character(port)
3220
- env[["CARMAR_WAIT_PORT"]] <- "30"
3250
+ env[["BEATRINA_PORT"]] <- as.character(port)
3251
+ env[["BEATRINA_WAIT_PORT"]] <- "30"
3221
3252
  # A successor that quietly moved to a free port would leave the page
3222
3253
  # polling a dead one for 90 s; strict means it refuses and its log says why.
3223
- env[["CARMAR_PORT_STRICT"]] <- "1"
3224
- env[["CARMAR_FILE_LAUNCH_CAP"]] <- h$cap
3225
- env[["CARMAR_HANDOFF_FROM"]] <- CARMAR_KERNEL_BUILD
3226
- env[["CARMAR_SESSION_TITLE"]] <- runtime_record$title %||% ""
3227
- if (isTRUE(runtime_record$listen)) env[["CARMAR_LISTEN"]] <- "1"
3254
+ env[["BEATRINA_PORT_STRICT"]] <- "1"
3255
+ env[["BEATRINA_FILE_LAUNCH_CAP"]] <- h$cap
3256
+ env[["BEATRINA_HANDOFF_FROM"]] <- BEATRINA_KERNEL_BUILD
3257
+ env[["BEATRINA_SESSION_TITLE"]] <- runtime_record$title %||% ""
3258
+ env[["BEATRINA_SESSION_DOCUMENTS"]] <- session_documents_encode(sockets$session_documents)
3259
+ # Only a token travels, never a path; the successor re-checks it.
3260
+ env[["BEATRINA_RESUME_TOKEN"]] <- h$resume %||% ""
3261
+ if (isTRUE(runtime_record$listen)) env[["BEATRINA_LISTEN"]] <- "1"
3228
3262
  # The same R this supervisor runs on, and the serve.R beside THIS file —
3229
3263
  # which, after the in-place swap, is the installed build's.
3230
3264
  rscript <- file.path(R.home("bin"), "Rscript")
@@ -3233,7 +3267,7 @@ session_handoff_spawn <- function() {
3233
3267
  rscript, file.path(here, "serve.R"), env = env,
3234
3268
  stdout = log, stderr = "2>&1", cleanup = FALSE), error = function(e) NULL)
3235
3269
  if (is.null(started) || !started$is_alive()) {
3236
- return(handoff_abandon("The installed CarmaR could not be started."))
3270
+ return(handoff_abandon("The installed Beatrina could not be started."))
3237
3271
  }
3238
3272
  audit("session-upgrade-spawned", to = h$to, pid = started$get_pid(), mode = h$mode %||% "upgrade")
3239
3273
  # Spawn first, kill second: the old worker's whole tree goes down only once
@@ -3480,6 +3514,32 @@ route_internal <- function(cmd, on_reply) {
3480
3514
  list(cmd = cmd, wire_id = wire_id)
3481
3515
  }
3482
3516
 
3517
+ #' Ask the fresh worker to restore a kept workspace, and tell every page what
3518
+ #' came back. Queued like any command, so a chunk run pressed meanwhile waits
3519
+ #' for the restore rather than racing it.
3520
+ #' @return Invisibly NULL.
3521
+ start_resume <- function() {
3522
+ token <- sockets$resume_pending
3523
+ sockets$resume_pending <- NULL
3524
+ if (!keep_token_ok(token)) return(invisible(NULL))
3525
+ audit("resume-start")
3526
+ routed <- route_internal(list(type = "resume", token = token), on_reply = function(frame) {
3527
+ if (scalar_chr(frame$cwd)) sockets$worker_wd <- frame$cwd
3528
+ report <- list(type = "workspace_restored",
3529
+ restored = frame$restored %||% 0L,
3530
+ failed = frame$failed %||% list(), skipped = frame$skipped %||% list(),
3531
+ wd_missing = frame$wd_missing %||% NULL, error = frame$error %||% NULL)
3532
+ payload <- toJSON(report, auto_unbox = TRUE, null = "null")
3533
+ sockets$resume_report <- list(at = Sys.time(), payload = payload)
3534
+ audit("resume-done", restored = report$restored, failed = length(report$failed),
3535
+ error = !is.null(report$error))
3536
+ pages <- Filter(function(r) isTRUE(r$class %in% PAGE_ONLY_CLASSES), page_recs())
3537
+ lapply(pages, function(r) try(r$ws$send(payload), silent = TRUE))
3538
+ })
3539
+ enqueue_worker_command(routed$cmd, routed$wire_id)
3540
+ invisible(NULL)
3541
+ }
3542
+
3483
3543
  #' Tell every page, and REMEMBER it for the pages that are not here yet.
3484
3544
  #'
3485
3545
  #' `sockets$hello` exists because the worker announces itself once, long
@@ -3503,8 +3563,8 @@ recover_execution_worker <- function() {
3503
3563
  sockets$worker_restart_after <- 0
3504
3564
  sockets$hello <- NULL
3505
3565
  fail_worker_routes(if (isTRUE(sockets$force_stop_asked))
3506
- "R was force-stopped. Its variables and loaded packages are gone; CarmaR is starting a fresh R session."
3507
- else "The R worker stopped unexpectedly. Its session state was lost; CarmaR is starting a fresh R session.")
3566
+ "R was force-stopped. Its variables and loaded packages are gone; Beatrina is starting a fresh R session."
3567
+ else "The R worker stopped unexpectedly. Its session state was lost; Beatrina is starting a fresh R session.")
3508
3568
  rm(list = ls(sockets$running), envir = sockets$running)
3509
3569
  rm(list = ls(sockets$worker_routes), envir = sockets$worker_routes)
3510
3570
  sockets$worker_queue <- list()
@@ -3531,7 +3591,7 @@ recover_execution_worker <- function() {
3531
3591
  sockets$worker_gave_up <- TRUE
3532
3592
  sockets$worker_recovering <- FALSE
3533
3593
  reason <- sprintf(
3534
- paste("R would not start after %d attempts. CarmaR is still running — the notebook,",
3594
+ paste("R would not start after %d attempts. Beatrina is still running — the notebook,",
3535
3595
  "your text and Save all work — but nothing can be run until R is fixed.",
3536
3596
  "A broken .Rprofile or a moved R installation is the usual cause; the kernel",
3537
3597
  "log beside this session has R's own message. Restart R to try again."),
@@ -3670,8 +3730,36 @@ handle_frame <- function(message, rec) {
3670
3730
  return(invisible(NULL))
3671
3731
  }
3672
3732
 
3733
+ # The documents this session has open (spike/session-documents.R). Page-only
3734
+ # in BOTH senses: the list is file paths, and a page that could not open a
3735
+ # terminal must not be able to rewrite what the session reopens either.
3736
+ # Replies nothing; the helper's label follows through the runtime record.
3737
+ if (identical(cmd$type, "page-documents")) {
3738
+ if (identical(rec$role, "page") && isTRUE(rec$class %in% PAGE_ONLY_CLASSES)) {
3739
+ sockets$session_documents <- session_documents_clean(cmd$documents)
3740
+ set_runtime_documents(sockets$session_documents)
3741
+ }
3742
+ return(invisible(NULL))
3743
+ }
3744
+
3745
+ # A page asks what this session had open, to reopen it. `others` counts the
3746
+ # OTHER pages attached: a page joining a session another window is showing
3747
+ # must not open the same documents a second time (that window holds their
3748
+ # history leases), so the page reopens only when it is alone.
3749
+ if (identical(cmd$type, "session-documents")) {
3750
+ if (!scalar_chr(cmd$id)) return(invisible(NULL))
3751
+ allowed <- identical(rec$role, "page") && isTRUE(rec$class %in% PAGE_ONLY_CLASSES)
3752
+ others <- Filter(function(r) !identical(r$ws, rec$ws), page_recs())
3753
+ try(rec$ws$send(toJSON(list(
3754
+ type = "session-documents", id = cmd$id,
3755
+ documents = if (allowed) sockets$session_documents else list(),
3756
+ others = length(others),
3757
+ refused = !allowed), auto_unbox = TRUE)), silent = TRUE)
3758
+ return(invisible(NULL))
3759
+ }
3760
+
3673
3761
  # ── the MCP plane ─────────────────────────────────────────────────────────
3674
- # A local agent (Claude Code / Codex via tools/mcp/carmar-mcp.mjs) connects
3762
+ # A local agent (Claude Code / Codex via tools/mcp/beatrina-mcp.mjs) connects
3675
3763
  # through the same loopback/Origin/Host gates as a page, then DECLARES itself.
3676
3764
  # From then on it may only ASK (mcp-request, routed to the active notebook
3677
3765
  # window) and use the read-side FORWARDED ops; pages may only ANSWER.
@@ -3694,7 +3782,7 @@ handle_frame <- function(message, rec) {
3694
3782
  return(invisible(NULL))
3695
3783
  }
3696
3784
  if (identical(cmd$type, "open-request")) {
3697
- # The double-clicked document parked by the launcher (CARMAR_OPEN_FILE).
3785
+ # The double-clicked document parked by the launcher (BEATRINA_OPEN_FILE).
3698
3786
  # Pages only — an agent must never consume it — and consume-once: the
3699
3787
  # first page to ask gets the path, everyone after gets "". An empty
3700
3788
  # string, not a JSON null, because jsonlite renders NULL as a truthy {}.
@@ -3719,7 +3807,7 @@ handle_frame <- function(message, rec) {
3719
3807
  audit("mcp-request", id = cmd$id, detail = cmd$tool)
3720
3808
  if (is.null(page)) {
3721
3809
  reply <- toJSON(list(type = "mcp-response", id = cmd$id, ok = FALSE,
3722
- error = "No CarmaR notebook window is connected to this kernel. Open the notebook first."),
3810
+ error = "No Beatrina notebook window is connected to this kernel. Open the notebook first."),
3723
3811
  auto_unbox = TRUE)
3724
3812
  try(rec$ws$send(reply), silent = TRUE)
3725
3813
  return(invisible(NULL))
@@ -3778,7 +3866,7 @@ handle_frame <- function(message, rec) {
3778
3866
  else reply(versions = I(r_versions()), current = current_rscript())
3779
3867
  return(invisible(NULL))
3780
3868
  }
3781
- # "Restart into the installed CarmaR": the page's door. Page-only in both
3869
+ # "Restart into the installed Beatrina": the page's door. Page-only in both
3782
3870
  # senses (AGENT_REFUSED has it too) — a session handoff is the person's
3783
3871
  # decision about their own session, never an agent's.
3784
3872
  if (cmd$type %in% c("session_upgrade", "session_restart") && identical(rec$role, "page")) {
@@ -3791,7 +3879,8 @@ handle_frame <- function(message, rec) {
3791
3879
  return(invisible(NULL))
3792
3880
  }
3793
3881
  reply(session_handoff_begin(mode = if (identical(cmd$type, "session_restart")) "restart" else "upgrade",
3794
- force = isTRUE(cmd$force)))
3882
+ force = isTRUE(cmd$force),
3883
+ resume = if (keep_token_ok(cmd$resume)) cmd$resume else NULL))
3795
3884
  return(invisible(NULL))
3796
3885
  }
3797
3886
  # ── terminals ─────────────────────────────────────────────────────────────
@@ -4112,18 +4201,18 @@ handle_frame <- function(message, rec) {
4112
4201
  "term_close" = "Agents cannot close the user's terminal.",
4113
4202
  "session_list" = "Agents cannot list the user's sessions.",
4114
4203
  "r_versions" = "Agents cannot list or choose R installations.",
4115
- "session_upgrade" = "Agents cannot restart the user's session into another CarmaR.",
4204
+ "session_upgrade" = "Agents cannot restart the user's session into another Beatrina.",
4116
4205
  "session_restart" = "Agents cannot restart the user's session.",
4117
4206
  # settings_GET is refused as firmly as the writes. Not because the
4118
4207
  # values are secret, but because handing back the confinement root,
4119
4208
  # the audit-log path and whether AI-text logging is on is a MAP OF THE
4120
4209
  # DEPLOYMENT'S CONTROLS, given to a process whose entire premise is
4121
4210
  # that it is not the user.
4122
- "settings_get" = "Agents cannot read CarmaR's settings.",
4123
- "settings_set" = "Agents cannot change CarmaR's settings.",
4124
- "settings_reset" = "Agents cannot reset CarmaR's settings.",
4211
+ "settings_get" = "Agents cannot read Beatrina's settings.",
4212
+ "settings_set" = "Agents cannot change Beatrina's settings.",
4213
+ "settings_reset" = "Agents cannot reset Beatrina's settings.",
4125
4214
  "update_status" = "Agents cannot inspect desktop update state.",
4126
- "update_action" = "Agents cannot install, defer, or roll back CarmaR.",
4215
+ "update_action" = "Agents cannot install, defer, or roll back Beatrina.",
4127
4216
  "project_action" = "Agents cannot install or restore project packages.",
4128
4217
  "job_open" = "Agents cannot open files on the user's desktop.",
4129
4218
  "Agents run code through notebook chunks (chunk_run), not raw exec.")
@@ -4163,6 +4252,25 @@ handle_frame <- function(message, rec) {
4163
4252
  return(invisible(NULL))
4164
4253
  }
4165
4254
 
4255
+ # "Restart, keep variables": the save is a copy of the whole session, so it
4256
+ # is page-only in BOTH senses, and its cancel interrupts only a save THIS
4257
+ # page asked for — Interrupt's own gate matches exec routes, never this.
4258
+ if (cmd$type %in% c("suspend", "suspend_cancel") && !isTRUE(rec$class %in% PAGE_ONLY_CLASSES)) {
4259
+ if (scalar_chr(cmd$id)) try(rec$ws$send(toJSON(list(
4260
+ type = cmd$type, id = cmd$id, error = "Only the local notebook page may keep the workspace."
4261
+ ), auto_unbox = TRUE)), silent = TRUE)
4262
+ audit("suspend-refused", reason = "class", class = rec$class %||% "unknown")
4263
+ return(invisible(NULL))
4264
+ }
4265
+ if (identical(cmd$type, "suspend_cancel")) {
4266
+ active <- sockets$worker_active
4267
+ route <- if (!is.null(active)) sockets$worker_routes[[active]] else NULL
4268
+ if (identical(sockets$worker_active_type, "suspend") && !is.null(route) && identical(route$rec, rec)) {
4269
+ audit("suspend-cancel")
4270
+ kernel_interrupt(k)
4271
+ }
4272
+ return(invisible(NULL))
4273
+ }
4166
4274
  if (identical(cmd$type, "project_action") && !isTRUE(rec$class %in% PAGE_ONLY_CLASSES)) {
4167
4275
  if (scalar_chr(cmd$id)) try(rec$ws$send(toJSON(list(
4168
4276
  type = "project_action", id = cmd$id, ok = FALSE, reason = "class",
@@ -4274,7 +4382,7 @@ handle_frame <- function(message, rec) {
4274
4382
  # a gate that silently drops the line. reply_err is this handler's own
4275
4383
  # door back to the asking page and carries cmd$id, so the console can
4276
4384
  # attribute the refusal to the line it just sent.
4277
- reply_err("CarmaR could not deliver the answer to R. The question is still open; try again.")
4385
+ reply_err("Beatrina could not deliver the answer to R. The question is still open; try again.")
4278
4386
  return(invisible(NULL))
4279
4387
  }
4280
4388
  sockets$input_waiting <- FALSE
@@ -4305,8 +4413,8 @@ handle_frame <- function(message, rec) {
4305
4413
  action <- if (scalar_chr(cmd$action)) cmd$action else ""
4306
4414
  line <- switch(action,
4307
4415
  continue = "c", over = "n", into = "s", out = "f",
4308
- where = ".carmar_debug_where()",
4309
- abort = 'invokeRestart("carmar_abort_cell")',
4416
+ where = ".beatrina_debug_where()",
4417
+ abort = 'invokeRestart("beatrina_abort_cell")',
4310
4418
  eval = if (scalar_chr(cmd$expr)) cmd$expr else NULL)
4311
4419
  if (is.null(line) || grepl("[\n\r]", line) || nchar(line) > 4000L) {
4312
4420
  reply_err("bad debug command")
@@ -4319,7 +4427,7 @@ handle_frame <- function(message, rec) {
4319
4427
  # end of the debuggable code the where line reaches the dispatch loop,
4320
4428
  # which ignores what it cannot parse — harmless by construction.
4321
4429
  if (action %in% c("over", "into", "out")) {
4322
- try(kernel_console(k, ".carmar_debug_where()"), silent = TRUE)
4430
+ try(kernel_console(k, ".beatrina_debug_where()"), silent = TRUE)
4323
4431
  }
4324
4432
  return(invisible(NULL))
4325
4433
  }
@@ -4339,11 +4447,12 @@ handle_frame <- function(message, rec) {
4339
4447
  # hello is stale the moment the old worker dies; the new worker's ready frame
4340
4448
  # replaces it via pump() and reaches every open socket.
4341
4449
  if (identical(cmd$type, "restart")) {
4342
- audit("restart")
4450
+ audit("restart", keep = keep_token_ok(cmd$resume))
4343
4451
  fail_worker_routes("R was restarted — this request was abandoned.")
4344
4452
  try(kernel_stop(k, grace = 1), silent = TRUE)
4345
4453
  k <<- start_execution_worker()
4346
4454
  sockets$hello <- NULL
4455
+ sockets$resume_pending <- if (keep_token_ok(cmd$resume)) cmd$resume else NULL
4347
4456
  sockets$worker_recovering <- FALSE
4348
4457
  sockets$worker_restart_attempts <- 0L
4349
4458
  # An explicit Restart R is the ONE door out of a give-up: the user has
@@ -4398,12 +4507,12 @@ handle_frame <- function(message, rec) {
4398
4507
  return(invisible(NULL))
4399
4508
  }
4400
4509
  # ONE OF THE TWO DOORS THE SUPERVISOR ACTUALLY OWNS. A policy that permits
4401
- # no key-based provider means CarmaR does not keep a provider key at all:
4510
+ # no key-based provider means Beatrina does not keep a provider key at all:
4402
4511
  # not handed out, not stored. The user can still paste one into the tab —
4403
4512
  # this is their own browser and no kernel can prevent that — but nothing
4404
4513
  # persists it for them and the attempt is in the audit log. The refusal
4405
- # covers `set` as much as `get`, because a key CarmaR holds under a
4406
- # local-only policy is a key CarmaR should never have accepted.
4514
+ # covers `set` as much as `get`, because a key Beatrina holds under a
4515
+ # local-only policy is a key Beatrina should never have accepted.
4407
4516
  if (isTRUE(ai_policy$set) && !isTRUE(ai_policy$allows_key)) {
4408
4517
  audit("ai-key-refused", reason = "policy", class = rec$class %||% "unknown")
4409
4518
  reply(error = ai_policy_reason(ai_policy), policy = TRUE)
@@ -4633,7 +4742,7 @@ handle_frame <- function(message, rec) {
4633
4742
  }
4634
4743
  # csl_place: the style beside the document, as a .bib sits beside a
4635
4744
  # .tex — what makes `csl: apa.csl` in the front matter render the same
4636
- # style with no CarmaR present. A dependent style's parent goes too, or
4745
+ # style with no Beatrina present. A dependent style's parent goes too, or
4637
4746
  # pandoc would go looking for it. Never overwrites: a different file of
4638
4747
  # that name beside the document is the author's, and is reported.
4639
4748
  style <- csl_resolve(here, plugin_policy, cmd$name)
@@ -4661,7 +4770,7 @@ handle_frame <- function(message, rec) {
4661
4770
  # The AI conversation, into the audit stream. Fire-and-forget: there is no
4662
4771
  # reply, because a turn must never wait on its own logging, and a page that
4663
4772
  # could tell whether the write landed would be a probe for whether auditing
4664
- # is on. `audit()` is already a no-op without CARMAR_LOG, so an ordinary
4773
+ # is on. `audit()` is already a no-op without BEATRINA_LOG, so an ordinary
4665
4774
  # laptop pays nothing here beyond the parse it already did.
4666
4775
  if (identical(cmd$type, "ai-audit")) {
4667
4776
  event <- ai_audit_scalar(cmd$event, 32L)
@@ -4690,7 +4799,7 @@ handle_frame <- function(message, rec) {
4690
4799
  # Reading the audit stream BACK, for the reviewer export.
4691
4800
  #
4692
4801
  # Page-only on the same grounds as the console history, and more so: with
4693
- # CARMAR_LOG_AI_TEXT on, these lines contain the prompts the user typed. A
4802
+ # BEATRINA_LOG_AI_TEXT on, these lines contain the prompts the user typed. A
4694
4803
  # declared agent is refused (AGENT_REFUSED); a published origin and a native
4695
4804
  # client are refused by class.
4696
4805
  #
@@ -4760,7 +4869,7 @@ handle_frame <- function(message, rec) {
4760
4869
 
4761
4870
  # Appearance carried between the two origins the notebook is delivered from
4762
4871
  # (see PREF_KEYS). Page-only on the console-history rule exactly: this is the
4763
- # user's own machine state, kept for them because CarmaR put it out of reach
4872
+ # user's own machine state, kept for them because Beatrina put it out of reach
4764
4873
  # of the origin they opened it at. A declared agent has no appearance.
4765
4874
  if (identical(cmd$type, "prefs")) {
4766
4875
  if (!scalar_chr(cmd$id)) return(invisible(NULL))
@@ -4806,7 +4915,7 @@ handle_frame <- function(message, rec) {
4806
4915
  # same reason as ai-key: the supervisor answers it itself.
4807
4916
  #
4808
4917
  # Page-only for the ai-key reason exactly: a native client could edit
4809
- # ~/.config/R/carmar/settings.json itself as this user, but CarmaR does not
4918
+ # ~/.config/R/beatrina/settings.json itself as this user, but Beatrina does not
4810
4919
  # do it for it, so the promise holds for every client rather than only the
4811
4920
  # polite ones. A published origin is refused because its reader approved that
4812
4921
  # site to run the chunks they press, not to reconfigure their kernel.
@@ -4818,7 +4927,7 @@ handle_frame <- function(message, rec) {
4818
4927
  audit("settings-refused", reason = "class", detail = cmd$type,
4819
4928
  class = rec$class %||% "unknown")
4820
4929
  reply(ok = FALSE, reason = "class",
4821
- error = "Only the notebook page may read or change CarmaR's settings.")
4930
+ error = "Only the notebook page may read or change Beatrina's settings.")
4822
4931
  return(invisible(NULL))
4823
4932
  }
4824
4933
  # An unauthenticated shared kernel has no "the user" whose home directory
@@ -4865,7 +4974,7 @@ handle_frame <- function(message, rec) {
4865
4974
  # because those are precisely the ones that can be honoured now, and
4866
4975
  # reporting a new linger while running on the old one is the lie the
4867
4976
  # whole `effect` field exists to prevent.
4868
- settings_state <<- carmar_settings_resolve()
4977
+ settings_state <<- beatrina_settings_resolve()
4869
4978
  linger_s <<- resolve_linger()
4870
4979
  HISTORY_ENABLED <<- isTRUE(settings_value("history_enabled"))
4871
4980
  audit("settings", detail = "get", status = settings_state$file$status)
@@ -4874,8 +4983,8 @@ handle_frame <- function(message, rec) {
4874
4983
  }
4875
4984
 
4876
4985
  if (identical(cmd$type, "settings_reset")) {
4877
- unlink(carmar_settings_path())
4878
- settings_state <<- carmar_settings_resolve()
4986
+ unlink(beatrina_settings_path())
4987
+ settings_state <<- beatrina_settings_resolve()
4879
4988
  linger_s <<- resolve_linger()
4880
4989
  HISTORY_ENABLED <<- isTRUE(settings_value("history_enabled"))
4881
4990
  audit("settings", detail = "reset", ok = TRUE)
@@ -4915,7 +5024,7 @@ handle_frame <- function(message, rec) {
4915
5024
  if (is.null(cmd$value)) {
4916
5025
  values[[key]] <- NULL # unset: back to env/default
4917
5026
  } else {
4918
- verdict <- carmar_settings_validate(key, cmd$value)
5027
+ verdict <- beatrina_settings_validate(key, cmd$value)
4919
5028
  if (!isTRUE(verdict$ok)) {
4920
5029
  # Refused, never coerced, and the file is NOT written — the ai-key
4921
5030
  # scar, where a payload of the wrong type fell through to "" and
@@ -4929,13 +5038,13 @@ handle_frame <- function(message, rec) {
4929
5038
  values[[key]] <- verdict$value
4930
5039
  }
4931
5040
 
4932
- if (!isTRUE(carmar_settings_write(values))) {
5041
+ if (!isTRUE(beatrina_settings_write(values))) {
4933
5042
  audit("settings", detail = "set", key = key, ok = FALSE)
4934
5043
  reply(ok = FALSE, key = key, reason = "write",
4935
5044
  error = "The settings file could not be written.")
4936
5045
  return(invisible(NULL))
4937
5046
  }
4938
- settings_state <<- carmar_settings_resolve()
5047
+ settings_state <<- beatrina_settings_resolve()
4939
5048
  applied <- FALSE
4940
5049
  if (identical(entry$effect, "live")) {
4941
5050
  if (identical(key, "linger_seconds")) {
@@ -5139,8 +5248,8 @@ pump <- function() {
5139
5248
  # Session-wide lifecycle frames have no owner and must reach every tab —
5140
5249
  # and no id to rewrite, so they forward exactly as the kernel wrote them.
5141
5250
  if (identical(e$type, "ready")) {
5142
- e$protocol <- CARMAR_PROTOCOL_VERSION
5143
- e$kernel_build <- CARMAR_KERNEL_BUILD
5251
+ e$protocol <- BEATRINA_PROTOCOL_VERSION
5252
+ e$kernel_build <- BEATRINA_KERNEL_BUILD
5144
5253
  e$installed_build <- installed_build()
5145
5254
  # session_upgrade is the SUPERVISOR's verb; it joins the worker's
5146
5255
  # vocabulary here so a page can tell, without probing, that this
@@ -5167,6 +5276,7 @@ pump <- function() {
5167
5276
  payload <- relay_frame(e)
5168
5277
  if (identical(e$type, "ready")) sockets$hello <- payload
5169
5278
  lapply(sockets$open, function(r) try(r$ws$send(payload), silent = TRUE))
5279
+ if (identical(e$type, "ready") && !is.null(sockets$resume_pending)) start_resume()
5170
5280
  invisible(NULL)
5171
5281
  })
5172
5282
  invisible(NULL)
@@ -5186,7 +5296,7 @@ if (deployment$require_origin) {
5186
5296
  # first line. An operator who has bound to the network gets the posture
5187
5297
  # stated back to them, because the two spellings differ enormously and the
5188
5298
  # difference is invisible from the outside.
5189
- message("CarmaR listening on ", host, ":", port,
5299
+ message("Beatrina listening on ", host, ":", port,
5190
5300
  " — strict: a client with no Origin is refused")
5191
5301
  message(if (deployment$trust_proxy)
5192
5302
  paste0(" identity: ", deployment$user_header, " from ",
@@ -5217,33 +5327,35 @@ file_url <- notebook_file_url(notebook_file, port, FILE_LAUNCH_CAP)
5217
5327
 
5218
5328
  #' Discovery for local agents — the Jupyter runtime-dir pattern.
5219
5329
  #'
5220
- #' `tools/mcp/carmar-mcp.mjs` (the stdio server Claude Code / Codex spawn) has
5330
+ #' `tools/mcp/beatrina-mcp.mjs` (the stdio server Claude Code / Codex spawn) has
5221
5331
  #' no stdout to read the URL from, so the kernel writes one small file a local
5222
- #' process owned by the SAME user can find: ~/.carmar/run/kernel-<port>.json,
5332
+ #' process owned by the SAME user can find: ~/.beatrina/run/kernel-<port>.json,
5223
5333
  #' mode 0600. This is discovery metadata, not a browser credential. A same-user
5224
5334
  #' local process was never outside the trust line — it could already exec
5225
5335
  #' anything. Removed on clean shutdown;
5226
5336
  #' consumers must health-check before trusting a file (kills leave litter).
5227
- #' CARMAR_RUNTIME_DIR relocates it (tests use a scratch dir);
5228
- #' CARMAR_NO_RUNTIME_FILE=1 turns it off.
5337
+ #' BEATRINA_RUNTIME_DIR relocates it (tests use a scratch dir);
5338
+ #' BEATRINA_NO_RUNTIME_FILE=1 turns it off.
5229
5339
  runtime_file <- ""
5230
5340
  runtime_record <- list(url = url, file = file_url, port = port, pid = Sys.getpid(),
5231
5341
  started = format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z"))
5232
5342
  # A successor started by a handoff keeps the session's name, so the menu
5233
5343
  # row does not flicker to "Untitled" between the two processes.
5234
5344
  local({
5235
- title <- Sys.getenv("CARMAR_SESSION_TITLE", "")
5236
- Sys.unsetenv("CARMAR_SESSION_TITLE")
5345
+ title <- Sys.getenv("BEATRINA_SESSION_TITLE", "")
5346
+ Sys.unsetenv("BEATRINA_SESSION_TITLE")
5237
5347
  title <- trimws(substr(gsub("[\"\\\\]", "'", gsub("[[:cntrl:]]", " ", title)), 1L, 120L))
5238
5348
  if (nzchar(title)) runtime_record$title <<- title
5349
+ label <- session_documents_label(sockets$session_documents)
5350
+ if (nzchar(label)) runtime_record$documents <<- label
5239
5351
  })
5240
- # CARMAR_LISTEN=1 marks a kernel started headless for published pages (the
5352
+ # BEATRINA_LISTEN=1 marks a kernel started headless for published pages (the
5241
5353
  # menu's Listen for Web Pages, the keep-ready daemon). The menu helper shows
5242
5354
  # such a kernel as its Listen checkbox, not as a session row — there is no
5243
5355
  # document behind it to name. The mark is HOW IT STARTED, so the first page
5244
5356
  # that actually attaches clears it (see the upgrade handler): from then on it
5245
5357
  # is an ordinary session.
5246
- if (identical(Sys.getenv("CARMAR_LISTEN"), "1")) runtime_record$listen <- TRUE
5358
+ if (identical(Sys.getenv("BEATRINA_LISTEN"), "1")) runtime_record$listen <- TRUE
5247
5359
  # The failsafe's session is listed by `beatrina status` and stopped by `beatrina stop` like any other:
5248
5360
  # the same app, a host name, the boot instant /health reports, and the document it opened.
5249
5361
  if (nzchar(FAILSAFE_REASON)) {
@@ -5280,6 +5392,19 @@ set_runtime_title <- function(title) {
5280
5392
  write_runtime()
5281
5393
  }
5282
5394
 
5395
+ #' The session's documents, as a label, into the runtime record.
5396
+ #'
5397
+ #' `documents` is what the menu helper shows in place of the notebook's
5398
+ #' generated name ("SezerProfiles.qmd", not "Mellow Yarrow 7Z6"). A flat
5399
+ #' string on purpose — helper-sessions.sh reads the record with a bounded sed.
5400
+ set_runtime_documents <- function(documents) {
5401
+ label <- session_documents_label(documents)
5402
+ if (identical(runtime_record$documents %||% "", label)) return(invisible(NULL))
5403
+ if (nzchar(label)) runtime_record$documents <<- label
5404
+ else runtime_record$documents <<- NULL
5405
+ write_runtime()
5406
+ }
5407
+
5283
5408
  #' A page attached: this kernel is now a session, not a bare listener.
5284
5409
  clear_runtime_listen <- function() {
5285
5410
  if (is.null(runtime_record$listen)) return(invisible(NULL))
@@ -5287,9 +5412,9 @@ clear_runtime_listen <- function() {
5287
5412
  write_runtime()
5288
5413
  }
5289
5414
 
5290
- if (!identical(Sys.getenv("CARMAR_NO_RUNTIME_FILE"), "1")) {
5291
- runtime_dir <- Sys.getenv("CARMAR_RUNTIME_DIR",
5292
- file.path(path.expand("~"), ".carmar", "run"))
5415
+ if (!identical(Sys.getenv("BEATRINA_NO_RUNTIME_FILE"), "1")) {
5416
+ runtime_dir <- Sys.getenv("BEATRINA_RUNTIME_DIR",
5417
+ file.path(path.expand("~"), ".beatrina", "run"))
5293
5418
  made <- dir.exists(runtime_dir) ||
5294
5419
  dir.create(runtime_dir, recursive = TRUE, showWarnings = FALSE)
5295
5420
  if (made) {
@@ -5298,7 +5423,7 @@ if (!identical(Sys.getenv("CARMAR_NO_RUNTIME_FILE"), "1")) {
5298
5423
  }
5299
5424
  }
5300
5425
 
5301
- audit("started", port = port, r = R.version.string, root = Sys.getenv("CARMAR_ROOT", ""))
5426
+ audit("started", port = port, r = R.version.string, root = Sys.getenv("BEATRINA_ROOT", ""))
5302
5427
  cat(toJSON(list(url = url, file = file_url), auto_unbox = TRUE), "\n")
5303
5428
  flush(stdout())
5304
5429
 
@@ -5379,7 +5504,7 @@ mcp_pending_sweep <- function() {
5379
5504
  as.numeric(difftime(now, entry$at, units = "secs")) >= MCP_PENDING_MAX_S
5380
5505
  if (!page_gone && !stale) next
5381
5506
  why <- if (page_gone)
5382
- "The CarmaR notebook window that was answering this request closed before it replied."
5507
+ "The Beatrina notebook window that was answering this request closed before it replied."
5383
5508
  else sprintf("The notebook did not answer within %d seconds.",
5384
5509
  as.integer(MCP_PENDING_MAX_S))
5385
5510
  audit("mcp-abandoned", id = id, reason = if (page_gone) "page closed" else "timeout")
@@ -5429,7 +5554,7 @@ linger_check <- function() {
5429
5554
  if (as.numeric(difftime(now, sockets$idle_since, units = "secs")) >= grace) {
5430
5555
  reason <- if (unopened_default) "unopened-launch" else "configured-idle-timeout"
5431
5556
  audit("linger-shutdown", grace = grace, reason = reason)
5432
- message(sprintf("CarmaR stopping: %s (grace %s seconds).", reason, grace))
5557
+ message(sprintf("Beatrina stopping: %s (grace %s seconds).", reason, grace))
5433
5558
  sockets$quit <- TRUE
5434
5559
  }
5435
5560
  invisible(NULL)