samagotchi 0.153.0 → 0.154.1

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 (226) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/README.md +2 -3
  4. data/bin/chi +23 -24
  5. data/docs/architecture.md +30 -33
  6. data/docs/broadcast.md +5 -5
  7. data/docs/bundles.md +9 -12
  8. data/docs/cli.md +82 -96
  9. data/docs/configuration.md +27 -26
  10. data/docs/context.md +3 -3
  11. data/docs/desktop.md +1 -1
  12. data/docs/guardrails.md +8 -10
  13. data/docs/hooks.md +9 -9
  14. data/docs/internals/attached-context.md +2 -2
  15. data/docs/internals/background-tasks.md +3 -4
  16. data/docs/internals/block-renderers.md +4 -0
  17. data/docs/internals/bridge.md +21 -8
  18. data/docs/internals/context-telemetry.md +1 -1
  19. data/docs/internals/engine.md +33 -7
  20. data/docs/internals/objectives.md +6 -5
  21. data/docs/internals/participants.md +7 -7
  22. data/docs/internals/prompt-caching.md +4 -2
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-frontend.md +2 -2
  25. data/docs/internals/web-push.md +1 -1
  26. data/docs/memory.md +2 -2
  27. data/docs/plugins.md +23 -28
  28. data/docs/sessions.md +24 -24
  29. data/docs/sub-agent.md +2 -2
  30. data/docs/system-one.md +1 -1
  31. data/docs/testing.md +31 -6
  32. data/lib/samagotchi/answer_text.rb +33 -0
  33. data/lib/samagotchi/bridge/card_places.rb +56 -7
  34. data/lib/samagotchi/bridge/card_store.rb +24 -9
  35. data/lib/samagotchi/bridge.rb +52 -32
  36. data/lib/samagotchi/bridge_client.rb +19 -14
  37. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  38. data/lib/samagotchi/broadcast/triage.rb +1 -1
  39. data/lib/samagotchi/broadcast_command.rb +5 -5
  40. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  42. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  43. data/lib/samagotchi/bundle_command.rb +2 -2
  44. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  45. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  46. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  47. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  48. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  50. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  51. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  52. data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
  53. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  54. data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
  55. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  56. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  57. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  58. data/lib/samagotchi/child_move.rb +3 -3
  59. data/lib/samagotchi/commands/registry.rb +6 -6
  60. data/lib/samagotchi/composer_route.rb +4 -5
  61. data/lib/samagotchi/config.rb +14 -12
  62. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  63. data/lib/samagotchi/context_candidates.rb +4 -3
  64. data/lib/samagotchi/continue_offer.rb +1 -1
  65. data/lib/samagotchi/debug_log.rb +1 -1
  66. data/lib/samagotchi/degrade.rb +75 -0
  67. data/lib/samagotchi/desktop/macos/ChiRunner.swift +3 -3
  68. data/lib/samagotchi/engine/cards.rb +87 -0
  69. data/lib/samagotchi/engine/context_judge.rb +138 -0
  70. data/lib/samagotchi/engine/events.rb +131 -0
  71. data/lib/samagotchi/engine/guardrails.rb +87 -0
  72. data/lib/samagotchi/engine/hooks.rb +119 -0
  73. data/lib/samagotchi/engine/messages.rb +124 -0
  74. data/lib/samagotchi/engine/model_settings.rb +127 -0
  75. data/lib/samagotchi/engine/models.rb +261 -0
  76. data/lib/samagotchi/engine/objectives.rb +116 -0
  77. data/lib/samagotchi/engine/participants.rb +86 -0
  78. data/lib/samagotchi/engine/plugins.rb +208 -0
  79. data/lib/samagotchi/engine/questions.rb +98 -0
  80. data/lib/samagotchi/engine/recap.rb +130 -0
  81. data/lib/samagotchi/engine/session.rb +131 -0
  82. data/lib/samagotchi/engine/side_requests.rb +135 -0
  83. data/lib/samagotchi/engine/steering.rb +134 -0
  84. data/lib/samagotchi/engine/toolkit.rb +269 -0
  85. data/lib/samagotchi/engine/turn_end.rb +180 -0
  86. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  87. data/lib/samagotchi/engine/verify.rb +153 -0
  88. data/lib/samagotchi/engine.rb +62 -2646
  89. data/lib/samagotchi/engine_snapshots.rb +9 -8
  90. data/lib/samagotchi/grants.rb +575 -0
  91. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  92. data/lib/samagotchi/guardrails/context.rb +2 -3
  93. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  94. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  95. data/lib/samagotchi/hook_routes.rb +3 -3
  96. data/lib/samagotchi/idle_recap.rb +7 -6
  97. data/lib/samagotchi/idle_reminders.rb +3 -4
  98. data/lib/samagotchi/iteration_limit.rb +2 -2
  99. data/lib/samagotchi/kernel_loop.rb +13 -13
  100. data/lib/samagotchi/launch_mode.rb +13 -32
  101. data/lib/samagotchi/level_setting_command.rb +3 -2
  102. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  103. data/lib/samagotchi/llm_context_command.rb +3 -2
  104. data/lib/samagotchi/log.rb +7 -7
  105. data/lib/samagotchi/log_line.rb +1 -1
  106. data/lib/samagotchi/log_path.rb +1 -1
  107. data/lib/samagotchi/note_command.rb +1 -1
  108. data/lib/samagotchi/note_delivery.rb +18 -8
  109. data/lib/samagotchi/objectives.rb +24 -3
  110. data/lib/samagotchi/objectives_check.rb +8 -27
  111. data/lib/samagotchi/objectives_command.rb +4 -69
  112. data/lib/samagotchi/objectives_edit.rb +87 -0
  113. data/lib/samagotchi/output_formatter.rb +7 -1
  114. data/lib/samagotchi/owner_lock.rb +7 -1
  115. data/lib/samagotchi/parent_answers_command.rb +5 -4
  116. data/lib/samagotchi/participant_exposure.rb +42 -0
  117. data/lib/samagotchi/participant_mode.rb +1 -1
  118. data/lib/samagotchi/participant_name.rb +1 -1
  119. data/lib/samagotchi/participants.rb +17 -17
  120. data/lib/samagotchi/participants_watch.rb +1 -1
  121. data/lib/samagotchi/plugin/api.rb +3 -2
  122. data/lib/samagotchi/plugin/context.rb +21 -7
  123. data/lib/samagotchi/plugin/peer.rb +2 -1
  124. data/lib/samagotchi/plugin/sessions.rb +6 -4
  125. data/lib/samagotchi/plugin_tasks.rb +1 -2
  126. data/lib/samagotchi/private_files.rb +29 -0
  127. data/lib/samagotchi/project_env.rb +2 -2
  128. data/lib/samagotchi/question_desk.rb +8 -7
  129. data/lib/samagotchi/recap_command.rb +2 -1
  130. data/lib/samagotchi/recap_setup.rb +1 -1
  131. data/lib/samagotchi/recap_store.rb +1 -1
  132. data/lib/samagotchi/redact.rb +86 -0
  133. data/lib/samagotchi/reminder_queue.rb +2 -2
  134. data/lib/samagotchi/remote_attach_command.rb +1 -1
  135. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  136. data/lib/samagotchi/review_start.rb +1 -1
  137. data/lib/samagotchi/run_options.rb +6 -6
  138. data/lib/samagotchi/self_report.rb +42 -39
  139. data/lib/samagotchi/send_command/busy.rb +1 -1
  140. data/lib/samagotchi/send_command.rb +4 -1
  141. data/lib/samagotchi/session.rb +11 -2
  142. data/lib/samagotchi/session_access/contract.rb +3 -3
  143. data/lib/samagotchi/session_access/local.rb +9 -9
  144. data/lib/samagotchi/session_access/remote.rb +17 -7
  145. data/lib/samagotchi/session_access.rb +4 -4
  146. data/lib/samagotchi/session_archive_command.rb +2 -2
  147. data/lib/samagotchi/session_chain.rb +8 -6
  148. data/lib/samagotchi/session_commands.rb +45 -48
  149. data/lib/samagotchi/session_continue.rb +1 -1
  150. data/lib/samagotchi/session_delete_command.rb +4 -4
  151. data/lib/samagotchi/session_manager.rb +67 -49
  152. data/lib/samagotchi/session_metrics.rb +2 -2
  153. data/lib/samagotchi/session_model_settings.rb +3 -2
  154. data/lib/samagotchi/session_participants.rb +1 -1
  155. data/lib/samagotchi/session_retention.rb +27 -7
  156. data/lib/samagotchi/sessions_command.rb +4 -4
  157. data/lib/samagotchi/share_command.rb +10 -10
  158. data/lib/samagotchi/steer.rb +13 -2
  159. data/lib/samagotchi/stream_event.rb +22 -0
  160. data/lib/samagotchi/system_prompt.rb +154 -145
  161. data/lib/samagotchi/task_watch.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  163. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  164. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  165. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  166. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  167. data/lib/samagotchi/terminal_ui/attached_view.rb +1 -11
  168. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  169. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  170. data/lib/samagotchi/terminal_ui/input_support.rb +17 -22
  171. data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
  172. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  173. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  174. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  175. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  176. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  177. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  178. data/lib/samagotchi/terminal_ui/reline_seam.rb +2 -12
  179. data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
  180. data/lib/samagotchi/terminal_ui.rb +142 -1387
  181. data/lib/samagotchi/title_command.rb +2 -2
  182. data/lib/samagotchi/tool_runner.rb +15 -16
  183. data/lib/samagotchi/toolkit_command.rb +2 -1
  184. data/lib/samagotchi/tools/delegate.rb +3 -1
  185. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  186. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  187. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  188. data/lib/samagotchi/tools/send_note.rb +2 -1
  189. data/lib/samagotchi/turn_endings.rb +2 -2
  190. data/lib/samagotchi/turn_flow.rb +11 -7
  191. data/lib/samagotchi/update_hint.rb +2 -2
  192. data/lib/samagotchi/version.rb +1 -1
  193. data/lib/samagotchi/web/app.rb +42 -51
  194. data/lib/samagotchi/web/message_display.rb +7 -0
  195. data/lib/samagotchi/web/participant_access.rb +2 -2
  196. data/lib/samagotchi/web/participant_review.rb +5 -5
  197. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  198. data/lib/samagotchi/web/participant_view.rb +2 -82
  199. data/lib/samagotchi/web/participants_admin.rb +1 -1
  200. data/lib/samagotchi/web/public/block_host.js +35 -8
  201. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  202. data/lib/samagotchi/web/public/card_stamps.js +27 -0
  203. data/lib/samagotchi/web/public/cards_place.js +5 -3
  204. data/lib/samagotchi/web/public/chains.js +2 -2
  205. data/lib/samagotchi/web/public/claim_link.js +4 -4
  206. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  207. data/lib/samagotchi/web/public/format.js +4 -3
  208. data/lib/samagotchi/web/public/turn_html.js +8 -4
  209. data/lib/samagotchi/web/push/store.rb +1 -1
  210. data/lib/samagotchi/web/session_access_routes.rb +13 -4
  211. data/lib/samagotchi/web/session_control.rb +12 -8
  212. data/lib/samagotchi/web/session_create.rb +1 -1
  213. data/lib/samagotchi/web/session_hub.rb +2 -2
  214. data/lib/samagotchi/web/session_show.rb +6 -2
  215. data/lib/samagotchi/web/session_summary.rb +1 -1
  216. data/lib/samagotchi/worker.rb +103 -9
  217. data/lib/samagotchi/worker_link.rb +3 -3
  218. data/lib/samagotchi/working_set_command.rb +3 -2
  219. data/lib/samagotchi.rb +0 -1
  220. metadata +31 -7
  221. data/lib/samagotchi/pending_input_queue.rb +0 -55
  222. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  223. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  224. data/lib/samagotchi/web/grants.rb +0 -575
  225. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  226. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -6,6 +6,7 @@ require_relative "../turn_origin"
6
6
  require_relative "../participant_mode"
7
7
  require_relative "../revise_anchor"
8
8
  require_relative "../verify_anchor"
9
+ require_relative "../redact"
9
10
 
10
11
  module Samagotchi
11
12
  module Web
@@ -23,23 +24,10 @@ module Samagotchi
23
24
  # is an allowlist that builds a new hash from named fields, so a field
24
25
  # added to an event later never reaches a participant until it is
25
26
  # named here, and an event type not named is dropped. Every string
26
- # that goes out passes Redact (secret shapes, the owner's token, the
27
+ # that goes out passes Redact (lib/samagotchi/redact.rb: secret shapes, the owner's token, the
27
28
  # home folder), defence in depth only: the allowlist is the control.
28
29
  # Pure: no I/O; keys of what comes in may be strings (JSON) or symbols.
29
30
  module ParticipantView
30
- # Who is looking: +author+ the participant's ("p:<id>"; nil for the
31
- # owner through the participant view: nothing is theirs), +cwd+ the
32
- # session's folder (tool paths are shown relative to it, only inside
33
- # it), +home+ the folder shown as ~, +secrets+ literal strings never
34
- # shown (the owner's token).
35
- Viewer = Data.define(:author, :cwd, :home, :secrets) do
36
- def initialize(author: nil, cwd: nil, home: Dir.home, secrets: [])
37
- super(author: author, cwd: cwd, home: home, secrets: Array(secrets).map(&:to_s).reject(&:empty?))
38
- end
39
-
40
- def own?(author) = !self.author.nil? && author.to_s == self.author
41
- end
42
-
43
31
  # A turn's participant_mode values the view passes on (ParticipantMode).
44
32
  PARTICIPANT_MODES = [ParticipantMode::REVIEW.to_s, ParticipantMode::PAIR.to_s].freeze
45
33
 
@@ -431,74 +419,6 @@ module Samagotchi
431
419
 
432
420
  hash.key?(key) ? hash[key] : hash[key.to_s]
433
421
  end
434
-
435
- # Secret shapes and host paths out of what a participant gets (the
436
- # session's folder as relative paths, home as ~). Defence in depth: the
437
- # allowlist above is the control; this catches a key the model quoted
438
- # in a reply, or one in a prompt.
439
- module Redact
440
- MARK = "[redacted]"
441
- PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|\z)/m
442
- PATTERNS = [
443
- PRIVATE_KEY,
444
- /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/,
445
- /\bgh[pousr]_[A-Za-z0-9]{20,}/,
446
- /\bgithub_pat_[A-Za-z0-9_]{20,}/,
447
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
448
- /\bxox[abprs]-[A-Za-z0-9-]{10,}/,
449
- /\bchp_[A-Za-z0-9_-]{20,}/,
450
- # A one-time link code (LinkCodes::CODE_RE): a bearer credential
451
- # until its grant expires (an owner's, 10 minutes).
452
- /\bchc_[A-Za-z0-9_-]{43}/,
453
- /\bAIza[0-9A-Za-z_-]{30,}/,
454
- /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/,
455
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
456
- %r{https://hooks\.slack\.com/services/[A-Za-z0-9/_-]+}
457
- ].freeze
458
- # The value after an Authorization (or a Bridge's secret,
459
- # BridgeSecret::HEADER) header or a token=/key= pair.
460
- VALUES = [
461
- /(\b(?:Authorization|X-Chi-Bridge):\s*(?:Bearer\s+|Basic\s+|Token\s+)?)[^\s"'`]+/i,
462
- /((?<![A-Za-z0-9])[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*[=:]\s*["']?)[^\s"'&`,;}]+/i,
463
- # A URL's password: scheme://user:PASSWORD@host.
464
- %r{(\b[a-z][a-z0-9+.-]*://[^/\s:@]+:)[^@\s/]+(?=@)}i
465
- ].freeze
466
- # A text that ends where one of VALUES' values (or a private key's
467
- # BEGIN line) would go on: a stream cut there would send the value
468
- # on its own, unredacted (ParticipantStreamFilter never cuts there).
469
- OPEN_VALUE = /(?:\b(?:Authorization|X-Chi-Bridge):\s*(?:(?:Bearer|Basic|Token)\s*)?|[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*(?:[=:]\s*["']?)?|-----BEGIN [A-Z0-9 ]*)\z/i
470
-
471
- module_function
472
-
473
- # +value+ with every string in it redacted (hash values, array items).
474
- def deep(value, viewer)
475
- case value
476
- when String then text(value, viewer)
477
- when Hash then value.transform_values { |item| deep(item, viewer) }
478
- when Array then value.map { |item| deep(item, viewer) }
479
- else value
480
- end
481
- end
482
-
483
- def text(value, viewer)
484
- out = value.dup
485
- viewer.secrets.each { |secret| out = out.gsub(secret, MARK) }
486
- PATTERNS.each { |pattern| out = out.gsub(pattern, MARK) }
487
- VALUES.each { |pattern| out = out.gsub(pattern) { "#{Regexp.last_match(1)}#{MARK}" } }
488
- out = within(out, viewer.cwd.to_s)
489
- home = viewer.home.to_s
490
- out = out.gsub(%r{#{Regexp.escape(home)}(?=/|\b|\z)}, "~") if home.length > 1
491
- out
492
- end
493
-
494
- # +text+ with +dir+ (the session's folder) shown relative: "dir/x"
495
- # as "x", "dir" alone as ".".
496
- def within(text, dir)
497
- return text if dir.length < 2
498
-
499
- text.gsub(%r{#{Regexp.escape(dir)}(/|(?=\b|\z))}) { Regexp.last_match(1) == "/" ? "" : "." }
500
- end
501
- end
502
422
  end
503
423
  end
504
424
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "grants"
3
+ require_relative "../grants"
4
4
  require_relative "qr"
5
5
  require_relative "../review_start"
6
6
  require_relative "../review_apply"
@@ -42,6 +42,11 @@ const NOT_AN_IMAGE = "the renderer's SVG can't be shown as an image";
42
42
  // this tall and scrolls sideways instead.
43
43
  const MIN_SHOWN_HEIGHT = 120;
44
44
 
45
+ // The zoom button's words: the picture fitted to the column, or at its natural size.
46
+ const ZOOM_IN = "Show at full size";
47
+ const ZOOM_OUT = "Fit to the column";
48
+
49
+ const ZOOM_ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><circle cx="7" cy="7" r="4.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="m10.2 10.2 3.3 3.3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
45
50
  const ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M5.5 4 2 8l3.5 4M10.5 4 14 8l-3.5 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
46
51
 
47
52
  // The picture's URL and size. The XML parser reads the root, as the image will: a
@@ -286,6 +291,9 @@ export function createBlockHost({
286
291
  return Promise.race([image, bound]).finally(() => clearTimeout(timer));
287
292
  }
288
293
 
294
+ // The picture's name says where its words are: the source <pre> is hidden, so a
295
+ // screen reader reaches it through the toggle. The zoom is a button too, for the
296
+ // keyboard (a click on the picture does the same); it shows on keyboard focus only.
289
297
  function makeFigure(picture, themeName, lang) {
290
298
  const figure = doc.createElement("figure");
291
299
  figure.className = DRAWN;
@@ -294,17 +302,33 @@ export function createBlockHost({
294
302
  scroll.className = "block-scroll";
295
303
  const img = doc.createElement("img");
296
304
  img.className = "block-img";
297
- img.alt = `${lang} diagram`;
305
+ img.alt = `${lang} diagram, its source under the Show the source button`;
298
306
  showPicture(img, picture);
299
307
  scroll.append(img);
300
- const toggle = doc.createElement("button");
301
- toggle.type = "button";
302
- toggle.className = "block-toggle";
303
- litRender(ICON, toggle);
304
- figure.append(scroll, toggle);
308
+ const zoom = button("block-zoom", ZOOM_ICON);
309
+ const toggle = button("block-toggle", ICON);
310
+ figure.append(scroll, zoom, toggle);
311
+ setZoomed(figure, false);
305
312
  return figure;
306
313
  }
307
314
 
315
+ function button(className, icon) {
316
+ const el = doc.createElement("button");
317
+ el.type = "button";
318
+ el.className = className;
319
+ litRender(icon, el);
320
+ return el;
321
+ }
322
+
323
+ function setZoomed(figure, on) {
324
+ figure.classList.toggle("natural", on);
325
+ const zoom = figure.querySelector(":scope > .block-zoom");
326
+ if (!zoom) return;
327
+ zoom.setAttribute("aria-pressed", on ? "true" : "false");
328
+ zoom.setAttribute("aria-label", on ? ZOOM_OUT : ZOOM_IN);
329
+ zoom.title = on ? ZOOM_OUT : ZOOM_IN;
330
+ }
331
+
308
332
  function showPicture(img, { url, width, height }) {
309
333
  if (width && height) {
310
334
  img.setAttribute("width", width);
@@ -476,7 +500,7 @@ export function createBlockHost({
476
500
  scan(root);
477
501
 
478
502
  root.addEventListener("mousedown", (e) => {
479
- if (e.target.closest?.(".block-toggle")) e.preventDefault();
503
+ if (e.target.closest?.(".block-toggle, .block-zoom")) e.preventDefault();
480
504
  });
481
505
  // Delegated, so a Revise clone's work too.
482
506
  root.addEventListener("click", (e) => {
@@ -490,7 +514,10 @@ export function createBlockHost({
490
514
  }
491
515
  const note = e.target.closest?.(".block-note");
492
516
  if (note) { copy(note.dataset.text); return; }
493
- if (e.target.closest?.(".block-img")) e.target.closest(`.${DRAWN}`).classList.toggle("natural");
517
+ if (e.target.closest?.(".block-zoom, .block-img")) {
518
+ const figure = e.target.closest(`.${DRAWN}`);
519
+ setZoomed(figure, !figure.classList.contains("natural"));
520
+ }
494
521
  });
495
522
  }
496
523
 
@@ -1,4 +1,4 @@
1
- /* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. */
1
+ /* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. The zoom button, the keyboard's way to the natural size, shows on keyboard focus only, left of the toggle. */
2
2
  .block-drawn{position:relative;margin:0 0 12px;white-space:normal}
3
3
  .code-wrap>.block-drawn{margin:0}
4
4
  .code-wrap>.block-drawn.source{height:0}
@@ -8,11 +8,15 @@
8
8
  .block-img{display:block;max-width:100%;max-height:50000px;object-fit:contain;height:auto;margin:0 auto;cursor:zoom-in}
9
9
  .block-drawn.natural .block-img{max-width:50000px;margin:0;cursor:zoom-out}
10
10
  .block-drawn.source .block-scroll{display:none}
11
- .block-toggle{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
11
+ .block-toggle,.block-zoom{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
12
12
  .code-wrap>.block-drawn>.block-toggle{right:34px}
13
- .block-toggle svg{display:block}
14
- .block-drawn:hover>.block-toggle,.code-wrap:hover>.block-drawn>.block-toggle,.block-toggle:focus-visible,.block-drawn.source>.block-toggle{opacity:1}
15
- .block-toggle:hover:not(:disabled){filter:none}@media(hover:hover){.block-toggle:hover:not(:disabled){color:var(--text-bright)}}
13
+ .block-zoom{right:34px}
14
+ .code-wrap>.block-drawn>.block-zoom{right:63px}
15
+ .block-drawn.source>.block-zoom{display:none}
16
+ .block-zoom:not(:focus-visible){pointer-events:none}
17
+ .block-toggle svg,.block-zoom svg{display:block}
18
+ .block-drawn:hover>.block-toggle,.code-wrap:hover>.block-drawn>.block-toggle,.block-toggle:focus-visible,.block-zoom:focus-visible,.block-drawn.source>.block-toggle{opacity:1}
19
+ .block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){filter:none}@media(hover:hover){.block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){color:var(--text-bright)}}
16
20
  .block-drawn.source:not(:last-child){margin:0}
17
21
  /* Its words are the attribute's, so a bubble's text stays the answer's (block_host.js fail). */
18
22
  .block-note{margin:4px 0 12px;font-size:12px;color:var(--text-3);white-space:normal}
@@ -0,0 +1,27 @@
1
+ // Where a snapshot's card or notice goes by its stamp: the id of the turn
2
+ // opening it comes after (+opening_id+, Bridge::CardPlaces#opening_of: a
3
+ // prompt's turn_id, a wake note's, /continue no's reason line's own id). The
4
+ // page finds that opening's bubble (data-turn-id on a prompt, data-opening-id
5
+ // on a wake note or a reason line, turn_html.js) and the entry goes before
6
+ // the next turn's opening, or at the end. No counting, so the page and the
7
+ // worker can't disagree on which bubbles open turns. An entry with no stamp
8
+ // (an older worker's, one before every opening), or whose opening the page
9
+ // doesn't draw, falls back to its count (card.js cardPlace, turns_since).
10
+ // A new module: the static JS is unversioned (docs/internals/web-frontend.md).
11
+ import { cardPlace } from "./card.js";
12
+
13
+ // The opening bubble +id+ names among the turns' openings (oldest first,
14
+ // bubbles.js turnPromptBubbles), or -1.
15
+ export function stampedOpening(userBubbles, id) {
16
+ if (!id) return -1;
17
+ const stamp = String(id);
18
+ return userBubbles.findIndex((el) => el.dataset?.turnId === stamp || el.dataset?.openingId === stamp);
19
+ }
20
+
21
+ // The element +entry+ goes before: the opening after its stamped one (null:
22
+ // the end), else what its count says (null: the end too).
23
+ export function snapshotPlace(userBubbles, entry) {
24
+ const at = stampedOpening(userBubbles, entry?.opening_id);
25
+ if (at < 0) return cardPlace(userBubbles, entry);
26
+ return userBubbles[at + 1] || null;
27
+ }
@@ -8,7 +8,8 @@
8
8
  // above the first turn after them, a finished turn's notice in its step, a
9
9
  // finished turn's question resolved where it was asked.
10
10
  import { getSession, sendCommand } from "./data.js";
11
- import { cardPlace, drawCard, isCard, isNoticeCard, refreshCardBody, turnNoticePlace } from "./card.js";
11
+ import { drawCard, isCard, isNoticeCard, refreshCardBody, turnNoticePlace } from "./card.js";
12
+ import { snapshotPlace } from "./card_stamps.js";
12
13
  import { elementOf } from "./turn_html.js";
13
14
  import { stepText } from "./turn_block.js";
14
15
  import { llmContextNotice, noticeLine } from "./turn_events.js";
@@ -83,14 +84,15 @@ export function createCardsPlace({
83
84
  bubbles.placeFollowing(el, before);
84
85
  }
85
86
 
86
- // A snapshot's cards and notices, each above the first turn after it; a
87
+ // A snapshot's cards and notices, each above the first turn after it (the
88
+ // one after its stamped opening, card_stamps.js; else by its count); a
87
89
  // turn's own notice where it showed live (placeTurnNotice). +end+: what
88
90
  // closes the history below them (a failed turn's prompt), for those with
89
91
  // no turn after them; one shown after that prompt went back stays below it.
90
92
  function placeSnapshotCards(entries, { before: end = null } = {}) {
91
93
  const users = turnPromptBubbles(historyEl);
92
94
  for (const entry of entries) {
93
- const before = cardPlace(users, entry) || (entry.after_failed ? null : end);
95
+ const before = snapshotPlace(users, entry) || (entry.after_failed ? null : end);
94
96
  if (isCard(entry)) upsertCard(entry, { before });
95
97
  else if (entry.type === "question") placeResolvedQuestion(entry, before);
96
98
  else if (entry.type === "guardrail_warning") bubbles.showGuardrailWarning(entry.message, entry.label || "guardrails", { before });
@@ -305,8 +305,8 @@ export function continueRefusal(error, sessions = []) {
305
305
  }
306
306
 
307
307
  // The open-children refusal as the toast shows it: the open delegates the
308
- // continue can't move to the next link (an older chi's worker, a chi
309
- // REPL), each a link to its session with what to do about it.
308
+ // continue can't move to the next link (an older chi's worker, a
309
+ // one-shot run), each a link to its session with what to do about it.
310
310
  // @param hrefFor (id) → its page's href
311
311
  export function openChildrenTemplate(prevId, children, hrefFor) {
312
312
  const n = children.length;
@@ -12,7 +12,7 @@
12
12
  // URL-safe and a space is one "+".
13
13
  //
14
14
  // Normalized: whitespace collapsed, and what the participant page shows
15
- // instead (ParticipantView::Redact): secret shapes "[redacted]" (so a link
15
+ // instead (Samagotchi::Redact): secret shapes "[redacted]" (so a link
16
16
  // never carries one, and matches the participant's redacted text), and on
17
17
  // the owner's page the session's folder relative ("<cwd>/x" → "x",
18
18
  // "<cwd>" → "."), the home folder "~". Not the owner's token (Redact's
@@ -41,10 +41,10 @@ export const HIT_CLASS = "claim-hit";
41
41
  const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
42
42
 
43
43
  export const MARK = "[redacted]";
44
- // ParticipantView::Redact's PATTERNS and VALUES
45
- // (lib/samagotchi/web/participant_view.rb), as JS: keep them in step.
44
+ // Samagotchi::Redact's PATTERNS and VALUES
45
+ // (lib/samagotchi/redact.rb), as JS: keep them in step.
46
46
  // spec/fixtures/redact_cases.json holds cases both sides read the same
47
- // (participant_view_spec.rb, claim_link.test.js).
47
+ // (redact_spec.rb, claim_link.test.js).
48
48
  const SECRET_PATTERNS = [
49
49
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g,
50
50
  /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
@@ -51,7 +51,7 @@ export function composerMode({ selected, owner, running, narrow }) {
51
51
  if (tui) {
52
52
  return {
53
53
  ...base,
54
- placeholder: pick("A terminal chi owns this session · run it with chi --shared to send from here", "A terminal chi owns this session"),
54
+ placeholder: pick("A terminal chi owns this session · a one-turn chi --non-interactive run or an older chi's terminal", "A terminal chi owns this session"),
55
55
  hint: "", button: "Send", disabled: true,
56
56
  };
57
57
  }
@@ -97,13 +97,14 @@ export function deleteConfirmText(session) {
97
97
  }
98
98
 
99
99
  // The badge on a session a process holds right now (the list's `owner`):
100
- // a worker (shared: every UI can send), or a plain terminal chi (not shared).
100
+ // a worker (every UI can send), or a terminal chi: a one-turn --non-interactive
101
+ // run or an older chi's terminal, in OwnerLock::TUI_BUSY's words.
101
102
  export function ownerBadge(owner, id) {
102
103
  if (owner === "worker") {
103
104
  return { kind: "worker", text: "live", title: `A worker runs this session; a terminal joins with: chi --attach ${id}` };
104
105
  }
105
106
  if (owner === "tui") {
106
- return { kind: "tui", text: "terminal", title: "A terminal chi owns this session and doesn't share it (chi --shared would)" };
107
+ return { kind: "tui", text: "terminal", title: "In use by a one-turn chi --non-interactive run or an older chi's terminal" };
107
108
  }
108
109
  return null;
109
110
  }
@@ -171,7 +172,7 @@ export function childrenSummary(parent, sessions) {
171
172
 
172
173
  // Whether the info bar offers "stop": only while a worker runs the session
173
174
  // (the list says so, or this page streams from one a send just woke). A
174
- // stopped or idle-exited session has nothing to stop, and a chi REPL's is
175
+ // stopped or idle-exited session has nothing to stop, and a one-turn run's is
175
176
  // refused by the server.
176
177
  export function canStopSession({ owner, streaming }) {
177
178
  if (owner === "tui") return false;
@@ -285,9 +285,11 @@ export function noteBubbleTemplate(note) {
285
285
  if (note?.objectives) return objectivesEntryTemplate(note, timeTitle(note.at));
286
286
  const from = note?.label ? `note from ${note.label}` : "note";
287
287
  // A wake turn's note opens that turn (TURN_PROMPT, bubbles.js); a class, not
288
- // data-turn-id, which marks a claim's anchor (claim_link.js).
288
+ // data-turn-id, which marks a claim's anchor (claim_link.js). Its turn's id
289
+ // is its data-opening-id: a card's stamp finds it (card_stamps.js).
289
290
  const cls = note?.turn_start ? "bubble note turn-start" : "bubble note";
290
- return html`<div class=${cls} title=${timeTitle(note?.at)}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
291
+ const opening = note?.turn_start && note?.turn_id ? String(note.turn_id) : nothing;
292
+ return html`<div class=${cls} title=${timeTitle(note?.at)} data-opening-id=${opening}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
291
293
  }
292
294
 
293
295
  // A plugin's steer (a nudge put into the running turn): one line that
@@ -311,7 +313,9 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
311
313
  // participant's badge (participant_names.js authorBadge: their name and
312
314
  // colour) in place of +label+. A turn's opening carries its Revise stamp
313
315
  // (+message.revises+: data-revises, revise_stack.js) and whether it was a
314
- // Verify turn (+message.verify+: data-verify, no Revise on its answer).
316
+ // Verify turn (+message.verify+: data-verify, no Revise on its answer). An
317
+ // opening no turn ran (/continue no's reason) has +message.opening_id+
318
+ // instead of a turn id: data-opening-id, a card's stamp (card_stamps.js).
315
319
  // +message.source+ (a delegate report's "delegate_report") is its
316
320
  // data-source; +lineKey+, a merged line's fold key (answer_fold.js:
317
321
  // data-line-key, the reload's "<turn id>:input:<place>").
@@ -320,7 +324,7 @@ export function userBubbleTemplate(message, { label = null, state = null, step =
320
324
  const from = author
321
325
  ? html`<span class="origin-label author-badge" title=${authorBadgeTitle(author.name, author.mode)} data-unnamed=${author.unnamed ? "1" : nothing}>${authorBadgeText(author.name, author.mode)}</span>`
322
326
  : label ? html`<span class="origin-label">${label}</span>` : nothing;
323
- return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-participant-mode=${author?.mode || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
327
+ return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-participant-mode=${author?.mode || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-opening-id=${(!message?.turn_id && message?.opening_id) || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
324
328
  }
325
329
 
326
330
  // A user line's text: plain (its quotes set apart), or the server's
@@ -7,7 +7,7 @@ require "openssl"
7
7
  require "time"
8
8
  require "uri"
9
9
  require_relative "crypto"
10
- require_relative "../private_files"
10
+ require_relative "../../private_files"
11
11
  require_relative "../../log"
12
12
  require_relative "../../atomic_file"
13
13
  require_relative "../../session"
@@ -54,6 +54,14 @@ module Samagotchi
54
54
  json_response(status, { error: code, detail: detail, message: error.message, **fields }.compact)
55
55
  end
56
56
 
57
+ # 409 scratch for a chi scratch session +id+, which only its terminal
58
+ # uses (Session::SCRATCH_REFUSED): the session page's read and stream
59
+ # (a tab opened at its id by hand: the hub never lists it), as
60
+ # POST …/wake refuses it.
61
+ def scratch_refused(id)
62
+ refusal_response(409, "scratch", SessionManager::ScratchSession.new(id), session_id: id)
63
+ end
64
+
57
65
  # The body as a Hash ({} when there is none), or nil when it isn't one.
58
66
  def body_hash(req)
59
67
  raw = request_body(req)
@@ -256,7 +264,7 @@ module Samagotchi
256
264
  # status row), parent_strategy, installed (the newest chi here)}. A
257
265
  # worker still leaving is waited for first (Local#wake: its recap's
258
266
  # timeout and a margin, on top of +wait+); 409
259
- # owned_by_tui (a chi REPL has it), 409 scratch (a leftover scratch
267
+ # owned_by_tui (a one-turn chi run has it), 409 scratch (a leftover scratch
260
268
  # session, as chi --attach here says); 504 worker_timeout (its Bridge
261
269
  # didn't come in time); 409 worker_newer (a worker whose protocol is newer
262
270
  # than this chi's, not resumed); 404 not_found.
@@ -425,7 +433,7 @@ module Samagotchi
425
433
  # session (NoteDelivery, as chi note; source: where it came from,
426
434
  # "cli" by default, never "broadcast"). 200 {status: "queued" (a
427
435
  # worker adds it) or "waits" (the next start does), session_id,
428
- # queued}; 409 owned_by_tui (status refused: a chi REPL has it); 400
436
+ # queued}; 409 owned_by_tui (status refused: a one-turn chi run has it); 400
429
437
  # note_rejected (empty, or over 16 KiB).
430
438
  def handle_notes(req, id)
431
439
  body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
@@ -438,8 +446,9 @@ module Samagotchi
438
446
  session_access.show(id)
439
447
  result = session_access.note(id, text: text, source: source.strip)
440
448
  if result.status == :refused
441
- error = SessionManager::OwnedByTUI.new(id)
442
- return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0)
449
+ error = result.scratch ? SessionManager::ScratchSession.new(id) : SessionManager::OwnedByTUI.new(id)
450
+ return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0,
451
+ scratch: result.scratch || nil)
443
452
  end
444
453
 
445
454
  json_response(200, { status: result.status.to_s, session_id: id, queued: result.queued })
@@ -16,7 +16,7 @@ module Samagotchi
16
16
  # a task, dequeue a queued turn, what it could stop and stop it
17
17
  # (handle_session_holds, handle_stop_holds — the page's Stop asks about
18
18
  # these first), stop, restart, archive, unarchive and delete
19
- # (handle_question_answer … handle_delete), with the refusals they answer (owned_by_repl, owned_by_tui, archive_refused). Included in App:
19
+ # (handle_question_answer … handle_delete), with the refusals they answer (owned_by_one_shot, owned_by_tui, archive_refused). Included in App:
20
20
  # its ROUTES name the handlers, and its helpers (json_response,
21
21
  # error_response, parse_json, request_body, relay, not_live,
22
22
  # bridge_error, reply_detail, worker_timeout, bridge_client,
@@ -443,7 +443,7 @@ module Samagotchi
443
443
 
444
444
  # POST /api/sessions/:id/archive: hide it (and its delegates) from the
445
445
  # lists, keep it for good. A live idle worker is stopped first; a turn
446
- # running in it or a delegate, a chi REPL's session and a scratch one
446
+ # running in it or a delegate, a one-turn chi run's session and a scratch one
447
447
  # are refused (409).
448
448
  def handle_archive(_req, id)
449
449
  result = @manager.archive_session(id, state_dir: @state_dir, wait: STOP_WAIT_SECONDS)
@@ -451,7 +451,7 @@ module Samagotchi
451
451
  json_response(200, { status: "archived", session_id: result.id, archived: result.archived,
452
452
  stopped: result.stopped, discarded: result.discarded })
453
453
  rescue SessionManager::OwnedByTUI => e
454
- owned_by_repl(id, e)
454
+ owned_by_one_shot(id, e)
455
455
  rescue SessionManager::ArchiveRefused => e
456
456
  archive_refused(e)
457
457
  end
@@ -466,7 +466,7 @@ module Samagotchi
466
466
  # DELETE /api/sessions/:id?force=true: the session for good. With
467
467
  # force a live worker is stopped first (the page sends it: its confirm
468
468
  # says so); without it a live worker's session is refused, 409
469
- # worker_running, as chi sessions delete without --force. A chi REPL's
469
+ # worker_running, as chi sessions delete without --force. A one-turn chi run's
470
470
  # session is always refused.
471
471
  def handle_delete(req, id)
472
472
  result = @manager.delete_session(id, state_dir: @state_dir, stop: truthy_param?(req.params["force"]),
@@ -474,7 +474,7 @@ module Samagotchi
474
474
  @hub&.touch(result[:id])
475
475
  json_response(200, { status: "deleted", session_id: result[:id], stopped: result[:stopped] })
476
476
  rescue SessionManager::OwnedByTUI => e
477
- owned_by_repl(id, e)
477
+ owned_by_one_shot(id, e)
478
478
  rescue SessionManager::DeleteRefused => e
479
479
  detail = e.reason == :still_stopping ? "#{e.message}; try again in a moment" : e.message
480
480
  refusal_response(409, e.reason.to_s, e, detail: detail, session_id: e.session_id, reason: e.reason.to_s)
@@ -482,15 +482,19 @@ module Samagotchi
482
482
 
483
483
  # 409 owned_by_tui in the words a refused archive, delete or continue
484
484
  # shows (stop, restart, a turn and a command show OwnedByTUI's own).
485
- def owned_by_repl(id, error = nil)
485
+ def owned_by_one_shot(id, error = nil)
486
486
  named = SessionManager::OwnedByTUI.new(id)
487
487
  error ||= named
488
488
  refusal_response(409, "owned_by_tui", error, detail: named.message, session_id: error.session_id)
489
489
  end
490
490
 
491
491
  # 409 owned_by_tui in OwnedByTUI's own words (stop, restart, a turn, a
492
- # command).
493
- def owned_by_tui(error) = refusal_response(409, "owned_by_tui", error, session_id: error.session_id)
492
+ # command); scratch: true for a chi scratch session
493
+ # (SessionManager::ScratchSession), which a remote client raises again.
494
+ def owned_by_tui(error)
495
+ scratch = error.is_a?(SessionManager::ScratchSession) || nil
496
+ refusal_response(409, "owned_by_tui", error, session_id: error.session_id, scratch: scratch)
497
+ end
494
498
 
495
499
  # 409 scratch or busy (every other reason: a turn, a queued prompt, an
496
500
  # open step-limit question, in it or a delegate, +busy_id+), with the
@@ -339,7 +339,7 @@ module Samagotchi
339
339
  rescue SessionManager::ArchiveRefused => e
340
340
  [nil, archive_refused(e)]
341
341
  rescue SessionManager::OwnedByTUI => e
342
- [nil, owned_by_repl(given, e)]
342
+ [nil, owned_by_one_shot(given, e)]
343
343
  rescue ModelProfile::MissingModel => e
344
344
  [nil, error_response(400, "invalid_model", e.message)]
345
345
  rescue Session::AmbiguousId => e
@@ -41,8 +41,8 @@ module Samagotchi
41
41
  # The flock leaves no file trace when its holder dies, so owners are
42
42
  # probed: every tick for the sessions the projection believes owned
43
43
  # (a handful; OwnerLock.owner is open + LOCK_SH|LOCK_NB + close), and
44
- # all sessions this often (a plain REPL that opened an old session
45
- # without saving it has no file signal until its first save).
44
+ # all sessions this often (a one-shot or an older chi's REPL that
45
+ # opened an old session without saving it has no file signal until its first save).
46
46
  FULL_PROBE_INTERVAL = 10.0
47
47
 
48
48
  # What a subscriber gets: `session` with {session: summary} for an
@@ -40,6 +40,10 @@ module Samagotchi
40
40
  return handle_brief_read(id) if req.params["brief"] == "1"
41
41
 
42
42
  session = @session_class.load(id, state_dir: default_state_dir)
43
+ # A tab opened at a chi scratch session's id: its terminal's alone.
44
+ # (The light re-reads come after a full read only.)
45
+ return scratch_refused(id) if session.scratch
46
+
43
47
  # Read-only preview: selecting a session never spawns a worker.
44
48
  # The worker is woken only on POST /turn (handle_turn) via
45
49
  # SessionManager.resume_session + write_turn_input. This avoids
@@ -120,8 +124,8 @@ module Samagotchi
120
124
  end
121
125
 
122
126
  # The file's question only while a worker owns the session: one a
123
- # dead worker saved can't be answered (its turn is gone), nor a chi
124
- # REPL's (it shares nothing with the web).
127
+ # dead worker saved can't be answered (its turn is gone), nor a
128
+ # one-shot's or an older chi's REPL's (they share nothing with the web).
125
129
  # A question between turns (the step-limit one) is the snapshot's own.
126
130
  # Its refs (the asking reply's, QuestionAsked) are checked again.
127
131
  def shown_pending_question(session, turn_snapshot, owner)
@@ -108,7 +108,7 @@ module Samagotchi
108
108
  # For the tab's notifications (notify.js): an open question, the
109
109
  # running turn's open card with actions, and how the last turn
110
110
  # ended. A question saved by a worker that died is not open, nor a
111
- # chi REPL's (only a worker shares it).
111
+ # one-turn chi run's (only a worker shares it).
112
112
  pending_question: session.waiting_question(live: !!owner&.worker?),
113
113
  pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
114
114
  last_turn: session.last_turn&.to_file,