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
@@ -22,7 +22,7 @@ module Samagotchi
22
22
  # chi's own settings, keys, proxies or dirs (LLM calls, HostKeys, Config,
23
23
  # Paths, hooks, plugins). A pair turn's tools don't get it either. A
24
24
  # mise.toml edit reaches a session at its next worker (`chi sessions
25
- # restart`). The REPL modes run no worker and keep the shell's env.
25
+ # restart`). A --non-interactive one-shot runs no worker and keeps the shell's env.
26
26
  # docs/sessions.md (Project env).
27
27
  #
28
28
  # And `chi` inside those tools stays the chi home's (ChiShim): the project's
@@ -75,7 +75,7 @@ module Samagotchi
75
75
  Overlay = Data.define(:env, :applied, :reason, :ruby) do
76
76
  def marker = applied ? "mise:#{ruby || "no ruby"}" : "none:#{reason}"
77
77
  end
78
- # No project env: specs, the REPL modes, a worker before #install!.
78
+ # No project env: specs, a one-shot, a worker before #install!.
79
79
  NONE = Overlay.new(env: {}.freeze, applied: false, reason: "none", ruby: nil)
80
80
 
81
81
  class Failed < StandardError; end
@@ -17,7 +17,8 @@ module Samagotchi
17
17
  # An Engine's question flow: the one open question (ask_user_question, a
18
18
  # hook's ask_user, a guardrail approval), its answer and its cancel, shared
19
19
  # by every UI. The turn thread blocks in #open_question; the UIs answer or
20
- # cancel from theirs (a REPL answers inline through the sync handler).
20
+ # cancel from theirs (a --non-interactive run dismisses it inline through
21
+ # the sync handler).
21
22
  #
22
23
  # A standing question (#post: a worker's step-limit question, kind
23
24
  # "continue") blocks nobody: it waits between turns, its answer goes to
@@ -52,7 +53,7 @@ module Samagotchi
52
53
  end
53
54
 
54
55
  # The line for the parent (ParentPolicy.message).
55
- # @param typed [Boolean] typed at a ? prompt (the REPL)
56
+ # @param typed [Boolean] typed at a terminal's ? prompt
56
57
  def detail(typed: false)
57
58
  Guardrails::ParentPolicy.message(reason, pending: @pending, strategy: @strategy, typed: typed)
58
59
  end
@@ -212,8 +213,8 @@ module Samagotchi
212
213
  id = pending.id
213
214
 
214
215
  # If a synchronous UI handler is registered (TUI), invoke it inline on the
215
- # SAME thread that called request_question (TerminalUI's REPL thread is the
216
- # turn thread — no second thread exists to answer). This avoids deadlock.
216
+ # SAME thread that called request_question (a --non-interactive run's
217
+ # TerminalUI thread is the turn thread — no second thread exists to answer). This avoids deadlock.
217
218
  # This path is TUI-specific but the surrounding emit/clear is generic, so
218
219
  # any future UI that registers a sync handler gets the same guarantee.
219
220
  if @sync_handler
@@ -369,7 +370,7 @@ module Samagotchi
369
370
 
370
371
  attr_writer :sync_handler
371
372
 
372
- # @return [Boolean] a REPL answers inline (no wait loop)
373
+ # @return [Boolean] a sync handler answers inline (a --non-interactive run: no wait loop)
373
374
  def sync_handler? = !@sync_handler.nil?
374
375
 
375
376
  # Cancel the pending question (e.g. /cancel, a dismiss). Announces which
@@ -402,8 +403,8 @@ module Samagotchi
402
403
  true
403
404
  end
404
405
 
405
- # A question through the question flow (REPL sync handler, attached TUI,
406
- # web), single-select, kind "hook". A --non-interactive run has no one
406
+ # A question through the question flow (the one-shot's sync handler,
407
+ # attached TUI, web), single-select, kind "hook". A --non-interactive run has no one
407
408
  # to ask: nil at once. Anything but an answer (a sync handler's text, no
408
409
  # answer, cancelled) is nil too.
409
410
  # @param asked [HookQuestion] its effects (label => effect for a parent
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "config"
4
+ require_relative "session"
4
5
 
5
6
  module Samagotchi
6
7
  # /recap on|off|default, the setting form of /recap (the bare /recap,
@@ -27,7 +28,7 @@ module Samagotchi
27
28
  def run(args)
28
29
  word = args.to_s.strip.downcase
29
30
  return [USAGE, false, :error] unless CHOICES.key?(word)
30
- return ["no session yet", false, :error] unless @engine.session
31
+ return [Session::NONE_YET, false, :error] unless @engine.session
31
32
 
32
33
  value = CHOICES.fetch(word)
33
34
  changed = @engine.recap_override != value
@@ -42,7 +42,7 @@ module Samagotchi
42
42
  if session.key?("recap.enabled")
43
43
  return nil unless session["recap.enabled"]
44
44
  else
45
- # The REPL and a worker pass nothing: the config is read here alone
45
+ # A worker passes nothing: the config is read here alone
46
46
  # (a scalar `recap: false` is only seen this way).
47
47
  return nil if recap.nil? && ConfigFile.recap_config == false
48
48
  return nil if Samagotchi::Config.get("recap.enabled") == false
@@ -12,7 +12,7 @@ module Samagotchi
12
12
  # (title: the auto title the recap reply led with, RecapTitle; absent in
13
13
  # a recap saved before it, or when no reply gave one).
14
14
  # A separate file, so it never races the session-file save at the end of a
15
- # turn. Follows the Engine's current session (the REPL can switch).
15
+ # turn. Follows the Engine's current session.
16
16
  class RecapStore
17
17
  FILE = "recap.json"
18
18
  # A session card's or the picker's recap line.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ # Secret shapes and host paths out of what a participant gets (the
5
+ # session's folder as relative paths, home as ~). Defence in depth: the
6
+ # allowlist (Web::ParticipantView) is the control; this catches a key the
7
+ # model quoted in a reply, or one in a prompt. Also how the Engine reads a
8
+ # Verify quote and ObjectivesCheck a text: as a participant's page shows it.
9
+ # Pure, stdlib only: core files use it without loading chi web.
10
+ module Redact
11
+ # Who is looking: +author+ the participant's ("p:<id>"; nil for the
12
+ # owner through the participant view: nothing is theirs), +cwd+ the
13
+ # session's folder (tool paths are shown relative to it, only inside
14
+ # it), +home+ the folder shown as ~, +secrets+ literal strings never
15
+ # shown (the owner's token).
16
+ Viewer = Data.define(:author, :cwd, :home, :secrets) do
17
+ def initialize(author: nil, cwd: nil, home: Dir.home, secrets: [])
18
+ super(author: author, cwd: cwd, home: home, secrets: Array(secrets).map(&:to_s).reject(&:empty?))
19
+ end
20
+
21
+ def own?(author) = !self.author.nil? && author.to_s == self.author
22
+ end
23
+
24
+ MARK = "[redacted]"
25
+ PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|\z)/m
26
+ PATTERNS = [
27
+ PRIVATE_KEY,
28
+ /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/,
29
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/,
30
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/,
31
+ /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
32
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}/,
33
+ /\bchp_[A-Za-z0-9_-]{20,}/,
34
+ # A one-time link code (LinkCodes::CODE_RE): a bearer credential
35
+ # until its grant expires (an owner's, 10 minutes).
36
+ /\bchc_[A-Za-z0-9_-]{43}/,
37
+ /\bAIza[0-9A-Za-z_-]{30,}/,
38
+ /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/,
39
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
40
+ %r{https://hooks\.slack\.com/services/[A-Za-z0-9/_-]+}
41
+ ].freeze
42
+ # The value after an Authorization (or a Bridge's secret,
43
+ # BridgeSecret::HEADER) header or a token=/key= pair.
44
+ VALUES = [
45
+ /(\b(?:Authorization|X-Chi-Bridge):\s*(?:Bearer\s+|Basic\s+|Token\s+)?)[^\s"'`]+/i,
46
+ /((?<![A-Za-z0-9])[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*[=:]\s*["']?)[^\s"'&`,;}]+/i,
47
+ # A URL's password: scheme://user:PASSWORD@host.
48
+ %r{(\b[a-z][a-z0-9+.-]*://[^/\s:@]+:)[^@\s/]+(?=@)}i
49
+ ].freeze
50
+ # A text that ends where one of VALUES' values (or a private key's
51
+ # BEGIN line) would go on: a stream cut there would send the value
52
+ # on its own, unredacted (ParticipantStreamFilter never cuts there).
53
+ 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
54
+
55
+ module_function
56
+
57
+ # +value+ with every string in it redacted (hash values, array items).
58
+ def deep(value, viewer)
59
+ case value
60
+ when String then text(value, viewer)
61
+ when Hash then value.transform_values { |item| deep(item, viewer) }
62
+ when Array then value.map { |item| deep(item, viewer) }
63
+ else value
64
+ end
65
+ end
66
+
67
+ def text(value, viewer)
68
+ out = value.dup
69
+ viewer.secrets.each { |secret| out = out.gsub(secret, MARK) }
70
+ PATTERNS.each { |pattern| out = out.gsub(pattern, MARK) }
71
+ VALUES.each { |pattern| out = out.gsub(pattern) { "#{Regexp.last_match(1)}#{MARK}" } }
72
+ out = within(out, viewer.cwd.to_s)
73
+ home = viewer.home.to_s
74
+ out = out.gsub(%r{#{Regexp.escape(home)}(?=/|\b|\z)}, "~") if home.length > 1
75
+ out
76
+ end
77
+
78
+ # +text+ with +dir+ (the session's folder) shown relative: "dir/x"
79
+ # as "x", "dir" alone as ".".
80
+ def within(text, dir)
81
+ return text if dir.length < 2
82
+
83
+ text.gsub(%r{#{Regexp.escape(dir)}(/|(?=\b|\z))}) { Regexp.last_match(1) == "/" ? "" : "." }
84
+ end
85
+ end
86
+ end
@@ -8,8 +8,8 @@ module Samagotchi
8
8
  #
9
9
  # The ReminderStore is the truth (a reminder is due once its interval
10
10
  # passed). This adds the names the idle tick found due and handed to the
11
- # UI's callback (#note_pending): the worker and the REPL run a reminder
12
- # turn while they are pending. A turn injects whatever is due then
11
+ # UI's callback (#note_pending): the worker runs a reminder turn while
12
+ # they are pending. A turn injects whatever is due then
13
13
  # (#inject!), which marks it fired and clears the pending names.
14
14
  class ReminderQueue
15
15
  # The reminders message's first line at +time+ (a Time in the zone to
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  # attached TUI on a session of another machine, through its chi web
16
16
  # (bin/chi branches here after RunOptions.parse, before its own checks,
17
17
  # which are this machine's). The run flags come parsed; the ones that
18
- # run this machine's REPL or read its files were refused already
18
+ # run this machine's Engine or read its files were refused already
19
19
  # (SessionAccess::Contract::RUN). The setup flags go as typed: that
20
20
  # machine checks them against its own config and bundles.
21
21
  class RemoteAttachCommand
@@ -160,7 +160,7 @@ module Samagotchi
160
160
  end
161
161
 
162
162
  # Only a question a live worker holds waits for an answer (the lists'
163
- # rule): one a dead worker left waits for no one, and a chi REPL's is
163
+ # rule): one a dead worker left waits for no one, and a one-turn chi run's is
164
164
  # answered there.
165
165
  def question(session)
166
166
  pending = session.pending_question
@@ -171,7 +171,7 @@ module Samagotchi
171
171
  Result.new(status: :waiting_for_answer, question: pending, session: session)
172
172
  end
173
173
 
174
- # A worker holds the session's lock (a chi REPL's question is its own).
174
+ # A worker holds the session's lock (a one-turn chi run's question is its own).
175
175
  def worker_owned?
176
176
  WorkerLink.resolve(Session.session_dir(@id, state_dir: @state_dir), unlink: false).worker_owned?
177
177
  end
@@ -143,7 +143,7 @@ module Samagotchi
143
143
  end
144
144
 
145
145
  # What inviting a participant to +session+ now means: the grant's mode
146
- # (Web::Grants::PAIR_MODE only when the owner is shown PAIR_WARNING: a
146
+ # (Grants::PAIR_MODE only when the owner is shown PAIR_WARNING: a
147
147
  # working session whose live worker runs pair turns; nil otherwise, a
148
148
  # review participant), the mode their turns get (:pair or :review) and
149
149
  # the warning shown for it (nil for a review session). One read of the
@@ -14,7 +14,7 @@ module Samagotchi
14
14
  module RunOptions
15
15
  # The flags an underscored spelling is refused for (--recap_base_url),
16
16
  # with the config registry's.
17
- KNOWN = %w[--prompt --resume --attach --shared --no-shared --model --thinking --toolkit --tool --no-tool --set --parent-answers --memory --mute --append-system-prompt --dir --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token --tunnel --start-dir].freeze
17
+ KNOWN = %w[--prompt --resume --attach --model --thinking --toolkit --tool --no-tool --set --parent-answers --memory --mute --append-system-prompt --dir --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token --tunnel --start-dir].freeze
18
18
 
19
19
  # An OptionParser that also records each switch it took, as the
20
20
  # normalized [flag] or [flag, value] (a --[no-]x as --x or --no-x; an
@@ -221,12 +221,11 @@ module Samagotchi
221
221
  (each subcommand takes --help)
222
222
 
223
223
  BANNER
224
- opts.on("-p", "--prompt PROMPT", "Run PROMPT as the first turn, then stay in the session (attached; the REPL with --no-shared)") { |p| options[:prompt] = p }
224
+ opts.on("-p", "--prompt PROMPT", "Run PROMPT as the first turn, then stay in the session (attached; one turn and out with --non-interactive)") { |p| options[:prompt] = p }
225
225
  opts.on("--resume SESSION_ID", "Resume a previous session by its ID (or a unique prefix of it)") { |s| options[:resume] = s }
226
226
  opts.on("--attach SESSION_ID", "Attach to a session's worker, waking one if it has exited; ID or a unique prefix") { |s| options[:attach] = s }
227
227
  opts.on("--dir DIR", "With --remote: a new session's folder on that machine, its absolute path (required there; " \
228
228
  "here cd instead)") { |d| options[:dir] = d }
229
- opts.on("--[no-]shared", "Run the session in a background worker and attach to it, so `chi web` can share it (the default, config session.shared; with --resume: that session); --no-shared runs a plain in-process REPL") { |v| options[:shared] = v }
230
229
  opts.on("--model NAME", "Model for this run, not saved (overrides --resume's; config default.model) [env SAMAGOTCHI_DEFAULT_MODEL]") { |m| options[:model] = m; cli_overrides["default.model"] = m }
231
230
  opts.on("--profile NAME", Samagotchi::ModelProfile::NAMES,
232
231
  "Prompt profile for every model in this run (#{Samagotchi::ModelProfile::NAMES.join("|")}) [alias for --model-profile]") { |p| cli_overrides["model.profile"] = p }
@@ -260,9 +259,10 @@ module Samagotchi
260
259
  opts.on("--llm-context-apply RULE", "This session's own apply rule for it: payoff|next_request|turn_end") { |v| (options[:llm_context] ||= {})[:apply] = v }
261
260
  opts.on("--llm-context-budget TOKENS", "This session's own context budget: tokens (64000, 64k) or off") { |v| (options[:llm_context] ||= {})[:budget_tokens] = v }
262
261
  opts.on("--no-interrupt", "Raise the tool call limit to 1000 iterations (useful for long tasks)") { options[:no_interrupt] = true; cli_overrides["no_interrupt"] = true }
263
- opts.on("--non-interactive", "Run a single prompt and exit (no REPL, implies --no-interrupt)") { options[:non_interactive] = true }
264
- opts.on("-v", "--verbose", "Print raw LLM responses and tool calls/results (runs a plain REPL, not attached)") { options[:verbose] = true }
265
- opts.on("--no-default-input", "Skip default REPL input prefill") { options[:no_default_input] = true; cli_overrides["no_default_input"] = true }
262
+ opts.on("--non-interactive", "Run the -p prompt in this process, one turn, and exit (implies --no-interrupt)") { options[:non_interactive] = true }
263
+ opts.on("-v", "--verbose", "Show the session's debug records (raw LLM responses, tool calls/results) as they come; " \
264
+ "a worker this chi starts logs at debug (with --non-interactive: on stderr)") { options[:verbose] = true }
265
+ opts.on("--no-default-input", "Skip the default input typed into a new session's first read (SAMAGOTCHI_DEFAULT_INPUT)") { options[:no_default_input] = true; cli_overrides["no_default_input"] = true }
266
266
  opts.on("--port PORT", "Port for `chi web` (default: 4567, env SAMAGOTCHI_WEB_PORT)") { |p| options[:web_port] = p.to_i; cli_overrides["web.port"] = p.to_s }
267
267
  opts.on("--open", "Open browser for `chi web`") { options[:web_open] = true }
268
268
  opts.on("--new-token", "`chi web`: replace the LAN access token (web.host: lan); links and QR codes made with the old one stop working") { options[:new_token] = true }
@@ -3,6 +3,7 @@
3
3
  require_relative "version"
4
4
  require_relative "config"
5
5
  require_relative "context_window"
6
+ require_relative "degrade"
6
7
  require_relative "session"
7
8
  require_relative "log_path"
8
9
  require_relative "model_profile"
@@ -177,19 +178,19 @@ module Samagotchi
177
178
 
178
179
  # [tokens, "models: KEY" | "hosts.NAME"], or nil when neither is set.
179
180
  def model_window(env, model)
180
- return nil unless model
181
-
182
- registry = HostRegistry.new(env: env)
183
- target = registry.resolve(model)
184
- names = registry.lookup_names(model, target: target)
185
- models = ConfigFile.model_settings(env: env)
186
- window = ContextWindow.setting(target, names: names, models: models)
187
- return nil unless window
188
- return [window.tokens, "hosts.#{target.entry.name}"] if window.source == :host_setting
189
-
190
- [window.tokens, "models: #{ConfigFile.model_setting(names, :window_tokens, models: models).key}"]
191
- rescue StandardError
192
- nil
181
+ Degrade.call(:config, "model_window_unreadable") do
182
+ return nil unless model
183
+
184
+ registry = HostRegistry.new(env: env)
185
+ target = registry.resolve(model)
186
+ names = registry.lookup_names(model, target: target)
187
+ models = ConfigFile.model_settings(env: env)
188
+ window = ContextWindow.setting(target, names: names, models: models)
189
+ return nil unless window
190
+ return [window.tokens, "hosts.#{target.entry.name}"] if window.source == :host_setting
191
+
192
+ [window.tokens, "models: #{ConfigFile.model_setting(names, :window_tokens, models: models).key}"]
193
+ end
193
194
  end
194
195
 
195
196
  SESSION_MODEL_ENV = "SAMAGOTCHI_SESSION_MODEL"
@@ -220,11 +221,11 @@ module Samagotchi
220
221
  end
221
222
 
222
223
  def same_model?(one, other, env)
223
- return true if one == other
224
+ Degrade.call(:config, "model_ref_unresolved", fallback: false) do
225
+ return true if one == other
224
226
 
225
- ConfigFile.model_ref(one, env: env).ref == ConfigFile.model_ref(other, env: env).ref
226
- rescue StandardError
227
- false
227
+ ConfigFile.model_ref(one, env: env).ref == ConfigFile.model_ref(other, env: env).ref
228
+ end
228
229
  end
229
230
 
230
231
  # The memory overlay key (ModelOverlay.key_for the bare id, as the
@@ -257,41 +258,41 @@ module Samagotchi
257
258
  # session chi self runs in has one (an alias), else of the name chi self
258
259
  # reports. nil when that is the key itself. Read-only, never raising.
259
260
  def fallback_model_key(model, key, env)
260
- typed = session_typed_model(env) || model
261
- typed_key = ModelOverlay.key_for(HostRegistry.new(env: env).parse_qualified_model(typed).last)
262
- typed_key == key ? nil : typed_key
263
- rescue StandardError
264
- nil
261
+ Degrade.call(:model, "fallback_key_unresolved") do
262
+ typed = session_typed_model(env) || model
263
+ typed_key = ModelOverlay.key_for(HostRegistry.new(env: env).parse_qualified_model(typed).last)
264
+ typed_key == key ? nil : typed_key
265
+ end
265
266
  end
266
267
 
267
268
  # The model name the session chi self runs in was typed as (an alias),
268
269
  # its model_typed, else nil.
269
270
  def session_typed_model(env)
270
- id = env[PARENT_SESSION_ENV].to_s
271
- return nil if id.empty? || !Session.valid_id?(id)
271
+ Degrade.call(:config, "session_typed_model_unreadable") do
272
+ id = env[PARENT_SESSION_ENV].to_s
273
+ return nil if id.empty? || !Session.valid_id?(id)
272
274
 
273
- state_dir = Session.default_state_dir(env: env)
274
- return nil unless Session.exist?(id, state_dir: state_dir)
275
+ state_dir = Session.default_state_dir(env: env)
276
+ return nil unless Session.exist?(id, state_dir: state_dir)
275
277
 
276
- session = Session.load(id, state_dir: state_dir)
277
- typed = session.model_typed.to_s.strip
278
- typed.empty? ? nil : typed
279
- rescue StandardError
280
- nil
278
+ session = Session.load(id, state_dir: state_dir)
279
+ typed = session.model_typed.to_s.strip
280
+ typed.empty? ? nil : typed
281
+ end
281
282
  end
282
283
 
283
284
  # The --mute list of the session chi self runs in (its execute exports
284
285
  # SAMAGOTCHI_PARENT_SESSION); [] outside one.
285
286
  def session_muted(env)
286
- id = env[PARENT_SESSION_ENV].to_s
287
- return [] if id.empty? || !Session.valid_id?(id)
287
+ Degrade.call(:config, "session_mutes_unreadable", fallback: []) do
288
+ id = env[PARENT_SESSION_ENV].to_s
289
+ return [] if id.empty? || !Session.valid_id?(id)
288
290
 
289
- state_dir = Session.default_state_dir(env: env)
290
- return [] unless Session.exist?(id, state_dir: state_dir)
291
+ state_dir = Session.default_state_dir(env: env)
292
+ return [] unless Session.exist?(id, state_dir: state_dir)
291
293
 
292
- MutedMemories.normalize_list(Session.load(id, state_dir: state_dir).muted_memory_names)
293
- rescue StandardError
294
- []
294
+ MutedMemories.normalize_list(Session.load(id, state_dir: state_dir).muted_memory_names)
295
+ end
295
296
  end
296
297
 
297
298
  def model_name
@@ -489,7 +490,9 @@ module Samagotchi
489
490
 
490
491
  # "samagotchi-system 0.1.5 (shipped 0.1.5), other 1.0.0"
491
492
  def bundles_summary
492
- shipped = MemoryBundle::Manifest.read(dir: MemoryBundle::SystemBundle::GEM_BUNDLE_DIR).version rescue nil
493
+ shipped = Degrade.call(:memory, "shipped_bundle_unreadable") do
494
+ MemoryBundle::Manifest.read(dir: MemoryBundle::SystemBundle::GEM_BUNDLE_DIR).version
495
+ end
493
496
  installed = MemoryBundle::Provenance.each_installed.map do |name, bundle|
494
497
  version = bundle.version || "?"
495
498
  label = "#{name} #{version}"
@@ -23,7 +23,7 @@ module Samagotchi
23
23
  # parent_answers
24
24
  Busy = Data.define(:error, :session_id, :question, :strategy) do
25
25
  # @param session [Session] the session as just read
26
- # @param owner [OwnerLock::Owner, nil] the worker or REPL that owns it
26
+ # @param owner [OwnerLock::Owner, nil] the worker or one-shot that owns it
27
27
  # @param queue [Boolean] chi send --queue
28
28
  # @return [Busy, nil] nil when the message may go in
29
29
  def self.for(session, owner:, queue:)
@@ -799,8 +799,11 @@ module Samagotchi
799
799
  end
800
800
  @info.puts("#{short} sent#{with_images}#{note}")
801
801
  true
802
+ rescue SessionManager::ScratchSession => e
803
+ @info.puts("#{short} refused: #{e.message}")
804
+ false
802
805
  rescue SessionManager::OwnedByTUI
803
- @info.puts("#{short} refused: it is open in a chi REPL; messages need attached mode")
806
+ @info.puts("#{short} refused: it is #{OwnerLock::TUI_BUSY}; send once it ends")
804
807
  false
805
808
  rescue StandardError => e
806
809
  @info.puts("#{short} failed: #{e.message}")
@@ -38,6 +38,11 @@ module Samagotchi
38
38
  STATUS_ERROR = "error"
39
39
  STATUS_STOPPED = "stopped"
40
40
  TOOLKIT_ORIGINS = %w[chosen default inherited suggested].freeze
41
+ # Why anything but the terminal that started a `chi scratch` session is
42
+ # refused it (chi --attach/--resume, chi send, chi web's wake, turn and
43
+ # command routes): one wording for a live one and a leftover one.
44
+ SCRATCH_REFUSED = "that's a chi scratch session: only the terminal that started it uses it " \
45
+ "(a leftover one is deleted at the next sweep: chi sessions clean)"
41
46
 
42
47
  # A stopped session's marker, <session dir>/stopped: a stop (chi
43
48
  # sessions stop, from another process) creates it instead of rewriting
@@ -54,6 +59,9 @@ module Samagotchi
54
59
  # The memory a delegate child starts with (Tools::Delegate::CHILD_MEMORIES).
55
60
  DELEGATE_MEMORY = "system/delegated"
56
61
 
62
+ # What a command answers when there is no session to act on yet.
63
+ NONE_YET = "no session yet"
64
+
57
65
  SORT_KEYS = %w[created_at updated_at].freeze
58
66
  SORT_ORDERS = %w[asc desc].freeze
59
67
 
@@ -116,8 +124,9 @@ module Samagotchi
116
124
  # nil. Not a parent: archiving one link leaves the next alone. The chain
117
125
  # itself is derived (SessionChain), never stored.
118
126
  attr_accessor :continues
119
- # A `chi scratch` session: deleted when its REPL ends, and by the next
120
- # sweep (or `chi sessions clean`) when the process died first.
127
+ # A `chi scratch` session: deleted when its terminal leaves (its worker
128
+ # deletes it too once its last client is gone), and by the next sweep
129
+ # (or `chi sessions clean`) when both died first.
121
130
  attr_accessor :scratch
122
131
  # The question this session waits on (PendingQuestion), saved as
123
132
  # session.json's "pending_question"; nil with none. A Hash (a saved
@@ -149,14 +149,14 @@ module Samagotchi
149
149
 
150
150
  # bin/chi's run flags under a remote (chi --remote NAME --attach ID,
151
151
  # RemoteAttachCommand): what a remote attach takes, and what runs
152
- # this machine's REPL or reads this machine's files (P4-D2..D4). A
152
+ # this machine's Engine or reads this machine's files (P4-D2..D4). A
153
153
  # config registry flag (--recap-base-url …) is this machine's
154
154
  # config: LOCAL_ONLY without being listed.
155
155
  RUN = {
156
- SUPPORTED => %w[--attach --resume -p --prompt --shared --model --thinking --llm-context --llm-context-apply
156
+ SUPPORTED => %w[--attach --resume -p --prompt --model --thinking --llm-context --llm-context-apply
157
157
  --llm-context-budget --toolkit --tool --no-tool --set --parent-answers --no-interrupt
158
158
  --no-default-input --dir --version -h --help],
159
- LOCAL_ONLY => %w[--no-shared --non-interactive -v --verbose --append-system-prompt --memory --mute --profile
159
+ LOCAL_ONLY => %w[--non-interactive -v --verbose --append-system-prompt --memory --mute --profile
160
160
  --port --open --new-token --tunnel --start-dir --scope --web-markdown --no-web-markdown]
161
161
  }.freeze
162
162
  # A short run flag's name in a refusal.
@@ -23,7 +23,7 @@ require_relative "../launch_mode"
23
23
  require_relative "../participants"
24
24
  require_relative "../config"
25
25
  require_relative "../live_versions"
26
- require_relative "../web/grants"
26
+ require_relative "../grants"
27
27
  require_relative "../web/info_probe"
28
28
  require_relative "../web/public_url"
29
29
  require_relative "../web/link_codes"
@@ -76,7 +76,7 @@ module Samagotchi
76
76
  # @raise [ArgumentError] no such session
77
77
  def show(id) = Session.load(id, **dir)
78
78
 
79
- # @return [OwnerLock::Owner, nil] the worker or chi REPL that owns it
79
+ # @return [OwnerLock::Owner, nil] the worker or one-turn chi run that owns it
80
80
  def owner(id) = SessionManager.session_owner(id, **dir)
81
81
 
82
82
  # SessionManager.list_sessions: Session objects, for chi sessions list.
@@ -94,11 +94,11 @@ module Samagotchi
94
94
  # a margin), so the resume finds the session free.
95
95
  # @return [Woken] its sidecar nil when the Bridge didn't come in time
96
96
  # @raise [ArgumentError] no such session
97
- # @raise [Refused] code scratch: a leftover chi scratch session
98
- # @raise [SessionManager::OwnedByTUI] a chi REPL has it
97
+ # @raise [Refused] code scratch: a chi scratch session (its terminal's alone)
98
+ # @raise [SessionManager::OwnedByTUI] a one-turn chi run has it
99
99
  def wake(id, wait:)
100
100
  session = show(id)
101
- raise Refused.new(LaunchMode::SCRATCH_LEFTOVER, code: "scratch") if session.scratch
101
+ raise Refused.new(Session::SCRATCH_REFUSED, code: "scratch") if session.scratch
102
102
 
103
103
  link = WorkerLink.resolve(session_dir(id), unlink: true)
104
104
  case link
@@ -177,7 +177,7 @@ module Samagotchi
177
177
  end
178
178
 
179
179
  # @return [SessionManager::DeliverResult]
180
- # @raise [SessionManager::OwnedByTUI] a chi REPL owns it
180
+ # @raise [SessionManager::OwnedByTUI] a one-turn chi run owns it
181
181
  def deliver_turn(id, prompt:, client_id:, images:, delivery: nil)
182
182
  SessionManager.deliver_turn(id, prompt: prompt, client_id: client_id, images: images, delivery: delivery, **dir)
183
183
  end
@@ -422,7 +422,7 @@ module Samagotchi
422
422
  Invitation.new(participant: ParticipantView.from_h(issued.grant.public_h(grants.now)), token: issued.token,
423
423
  link: link, lan: !public_url.nil? || !web.ip.nil?,
424
424
  warning: terms.warning)
425
- rescue Web::Grants::Invalid => e
425
+ rescue Grants::Invalid => e
426
426
  raise BadRequest.new(e.message, code: "invalid_participant")
427
427
  end
428
428
 
@@ -446,13 +446,13 @@ module Samagotchi
446
446
  raise BadRequest.new(Participants.no_participant(participant, id), code: "invalid_participant") unless grant
447
447
 
448
448
  ParticipantView.from_h(grant.public_h(grants.now))
449
- rescue Web::Grants::Invalid => e
449
+ rescue Grants::Invalid => e
450
450
  raise BadRequest.new(e.message, code: "invalid_participant")
451
451
  end
452
452
 
453
453
  private
454
454
 
455
- def grants = @grants ||= Web::Grants.new(Web::Grants.path(state_dir: state_dir!))
455
+ def grants = @grants ||= Grants.new(Grants.path(state_dir: state_dir!))
456
456
 
457
457
  LOCAL_PROBE_TIMEOUT = 1.0
458
458
 
@@ -12,7 +12,7 @@ require_relative "../bridge_client"
12
12
  require_relative "../session_continue"
13
13
  require_relative "../guardrails/parent_policy"
14
14
  require_relative "../model_profile"
15
- require_relative "../web/grants"
15
+ require_relative "../grants"
16
16
  require_relative "http_transport"
17
17
  require_relative "../chi_invocation"
18
18
 
@@ -59,7 +59,7 @@ module Samagotchi
59
59
  def remote? = true
60
60
  def name = @entry.name
61
61
 
62
- # Web::Grants::PREFIX, without loading chi web.
62
+ # Grants::PREFIX, without loading chi web.
63
63
  PARTICIPANT_PREFIX = "chp_"
64
64
 
65
65
  # Whether the token is a participant's (chi share's link), not the
@@ -229,7 +229,12 @@ module Samagotchi
229
229
  SessionManager::DeliverResult.new(status: :accepted, via: via, ack: ack.except("via"))
230
230
  when 504 then SessionManager::DeliverResult.new(status: :timeout, ack: { "detail" => ack["detail"] })
231
231
  when 409
232
- raise SessionManager::OwnedByTUI, ack["session_id"] || id if ack["error"] == "owned_by_tui"
232
+ if ack["error"] == "owned_by_tui"
233
+ # A chi scratch session (scratch: true; an older chi web sends none).
234
+ raise SessionManager::ScratchSession, ack["session_id"] || id if ack["scratch"] == true
235
+
236
+ raise SessionManager::OwnedByTUI, ack["session_id"] || id
237
+ end
233
238
 
234
239
  SessionManager::DeliverResult.new(status: :refused, code: 409, ack: ack)
235
240
  when 404 then raise_refusal(reply)
@@ -316,7 +321,9 @@ module Samagotchi
316
321
 
317
322
  NoteDelivery::Result.new(id: id, status: json["status"].to_sym, queued: json["queued"].to_i)
318
323
  when 409
319
- return NoteDelivery::Result.new(id: id, status: :refused, queued: 0) if json["error"] == "owned_by_tui"
324
+ if json["error"] == "owned_by_tui"
325
+ return NoteDelivery::Result.new(id: id, status: :refused, queued: 0, scratch: json["scratch"] == true)
326
+ end
320
327
 
321
328
  raise_refusal(reply)
322
329
  when 400
@@ -584,7 +591,7 @@ module Samagotchi
584
591
  # when none runs, waited for up to +wait+ seconds there (a leaving
585
592
  # worker first: +read_timeout+ covers that wait too).
586
593
  # @return [WakeReply]
587
- # @raise [SessionManager::OwnedByTUI] a chi REPL has it there
594
+ # @raise [SessionManager::OwnedByTUI] a one-turn chi run has it there
588
595
  # @raise [Refused] a leftover scratch session (code scratch), or its
589
596
  # Bridge didn't come in time (code worker_timeout): chi web's words,
590
597
  # LaunchMode's constants
@@ -709,7 +716,10 @@ module Samagotchi
709
716
  case error
710
717
  when "not_found" then raise ArgumentError, message.to_s
711
718
  when "ambiguous_id" then raise Session::AmbiguousId.new(message, ids: Array(json["ids"]))
712
- when "owned_by_tui" then raise SessionManager::OwnedByTUI, sid
719
+ when "owned_by_tui"
720
+ raise SessionManager::ScratchSession, sid if json["scratch"] == true
721
+
722
+ raise SessionManager::OwnedByTUI, sid
713
723
  when "worker_running", "still_stopping" then raise SessionManager::DeleteRefused.new(sid, reason)
714
724
  when "busy", "scratch"
715
725
  raise SessionManager::ArchiveRefused.new(sid, reason, busy_id: json["busy_id"]) if reason
@@ -728,7 +738,7 @@ module Samagotchi
728
738
  # A toolkit whose config kit's groups: is invalid there: the same.
729
739
  when "toolkit_refused" then raise Toolkit::Refused, message.to_s
730
740
  # The grants store doesn't read there: chi share says what it says here.
731
- when "grants_unwritable" then raise Web::Grants::Unwritable, message.to_s
741
+ when "grants_unwritable" then raise Grants::Unwritable, message.to_s
732
742
  when /\Ainvalid_/, "missing_fields", "bad_images"
733
743
  # invalid_participant: chi share's name, kind or ttl, an archived session
734
744
  raise BadRequest.new(message.to_s, code: error)