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
@@ -98,7 +98,7 @@ module Samagotchi
98
98
  class BadRequest < Refused; end
99
99
 
100
100
  # One participant's grant as chi share ls shows it
101
- # (Web::Grants::Grant#public_h): never its token or digest. +state+
101
+ # (Grants::Grant#public_h): never its token or digest. +state+
102
102
  # "active", "revoked" or "expired".
103
103
  # +cap+ dollars and +queue+ turns (nil from a chi web before them).
104
104
  # +identity+ the email the grant is bound to, nil for any.
@@ -163,7 +163,7 @@ module Samagotchi
163
163
  # A session as a session command reads it (Local: the Session itself;
164
164
  # Remote: chi web's brief read). The fields the commands use, and
165
165
  # Session's #display_title.
166
- # @!attribute owner [OwnerLock::Owner, nil] its worker or chi REPL
166
+ # @!attribute owner [OwnerLock::Owner, nil] its worker or one-turn chi run
167
167
  # @!attribute pending_question [PendingQuestion, nil] the file's
168
168
  SessionView = Data.define(:id, :status, :title, :display_title, :project_root, :working_directory, :model_name,
169
169
  :model_typed, :parent_answers, :owner, :pending_question)
@@ -189,7 +189,7 @@ module Samagotchi
189
189
  # features, epoch; nil: none answers)
190
190
  def sidecar = link.live? ? link.sidecar : nil
191
191
 
192
- # @return [OwnerLock::Owner, nil] a worker, a chi REPL, nil
192
+ # @return [OwnerLock::Owner, nil] a worker, a one-turn chi run, nil
193
193
  def owner = link.owner
194
194
 
195
195
  def live? = link.live?
@@ -238,7 +238,7 @@ module Samagotchi
238
238
  # @!attribute known_turn [Boolean, nil] with a turn asked for
239
239
  # (--turn), ReplyWait.known_turn?; nil without one
240
240
  # @!attribute running [Boolean] the session's status was running
241
- # @!attribute live [Boolean] running, or a worker or a REPL owns it
241
+ # @!attribute live [Boolean] running, or a worker or a one-shot owns it
242
242
  # @!attribute settled [Boolean] idle, a turn ended and no question
243
243
  # stands (a continue offer waits for its answer)
244
244
  # @!attribute last_turn_id [String, nil] the last ended turn's
@@ -18,7 +18,7 @@ module Samagotchi
18
18
  Usage: chi sessions archive (ID|PREFIX)...
19
19
  Hides each session (and its delegates) from every list and keeps it
20
20
  for good: the retention sweep never deletes it. A live worker is
21
- stopped first; a session running a turn, or open in a chi REPL, is
21
+ stopped first; a session running a turn, or in a one-turn chi run, is
22
22
  refused. chi sessions list --archived shows them; a message you send
23
23
  to one brings it back.
24
24
  TEXT
@@ -76,7 +76,7 @@ module Samagotchi
76
76
  true
77
77
  rescue SessionManager::OwnedByTUI => e
78
78
  where = e.session_id == id ? "it is" : "a delegate of it is"
79
- @stdout.puts("#{short} refused: #{where} open in a chi REPL; close it there first")
79
+ @stdout.puts("#{short} refused: #{where} #{OwnerLock::TUI_BUSY}; let it finish first")
80
80
  false
81
81
  rescue SessionManager::ArchiveRefused => e
82
82
  @stdout.puts("#{short} refused: #{e.message}")
@@ -2,9 +2,9 @@
2
2
 
3
3
  require "time"
4
4
  require_relative "answer_tail"
5
+ require_relative "answer_text"
5
6
  require_relative "config"
6
7
  require_relative "log"
7
- require_relative "output_formatter"
8
8
  require_relative "session"
9
9
  require_relative "session_inbox"
10
10
  require_relative "steer"
@@ -109,10 +109,11 @@ module Samagotchi
109
109
  nil
110
110
  end
111
111
 
112
- # The session's last word: the text of AnswerTail's newest bubble, with
113
- # the wire-format markup stripped, unless that bubble is a step of a
114
- # turn (it asked for tools) or old text a later turn never replaced
115
- # (one that failed or was cut before it answered). A cut reply keeps
112
+ # The session's last word: the text of AnswerTail's newest bubble as the
113
+ # user saw it (AnswerText.visible: no thought, a cut-off one neither),
114
+ # unless that bubble is a step of a turn (it asked for tools), old text a
115
+ # later turn never replaced (one that failed or was cut before it
116
+ # answered) or a thought alone. A cut reply keeps
116
117
  # its "[interrupted]": that is what happened.
117
118
  # @param messages [Array<Hash>]
118
119
  # @return [String, nil]
@@ -123,7 +124,8 @@ module Samagotchi
123
124
  at = messages.rindex { |m| m.equal?(found) }
124
125
  return nil if messages[(at + 1)..].any? { |m| m[:role].to_s == "tool_response" || later_turn?(m) }
125
126
 
126
- OutputFormatter.strip_markup(found[:content].scrub)
127
+ text = AnswerText.visible(found[:content])
128
+ text.empty? ? nil : text
127
129
  end
128
130
 
129
131
  # A turn's opening, not a `!shell` line: a command the user ran after
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "config"
4
+ require_relative "degrade"
4
5
  require_relative "model_profile"
5
6
  require_relative "served_model"
6
7
  require_relative "prompt_note"
@@ -31,7 +32,7 @@ module Samagotchi
31
32
  # load budget (spec/samagotchi/testing/sandbox_spec.rb).
32
33
  autoload :ObjectivesCommand, File.expand_path("objectives_command", __dir__)
33
34
 
34
- # The session commands a REPL and a session worker both run: /model,
35
+ # The session commands a session worker runs (a -p one-shot too): /model,
35
36
  # /models, /llm-context, /guardrails, /context, !rollback, !cmd and the answer to a continue offer. They act on
36
37
  # the Engine and its TurnFlow; the host prints the result's output and
37
38
  # runs a continue turn when asked to (#run never runs a turn).
@@ -159,24 +160,24 @@ module Samagotchi
159
160
  registry.register("/quit", "leave, like /exit", id: :exit, local: true, match: exit_match("/quit"))
160
161
  registry.register("/archive", "leave and archive the session: hidden from the lists, kept for good", local: true,
161
162
  match: ->(text) { text.casecmp?("/archive") })
162
- # The REPL owns its session: it answers /detach with a note, and doesn't offer it.
163
+ # The terminal's: the web has no worker to leave running.
163
164
  registry.register("/detach", "leave and keep the worker running", local: true, uis: [:attached],
164
165
  match: ->(text) { text.casecmp?("/detach") })
165
166
  # /cut TEXT and /queue TEXT while a turn runs: a delivery the UIs parse
166
167
  # themselves (ComposerRoute.steer_choice) before any command check; the
167
168
  # entries exist for Tab completion and /help. The web composer sends
168
- # /cut too (its queue is Alt+Enter). Both terminals queue: the REPL in
169
- # its own list, the attached TUI's worker in its input files.
169
+ # /cut too (its queue is Alt+Enter). The attached TUI queues in its
170
+ # worker's input files.
170
171
  registry.register("/cut", "cut the running generation and go in now (/cut TEXT)",
171
- local: true, uis: %i[repl attached web], match: ->(text) { text.casecmp?("/cut") || text.match?(%r{\A/cut\s+\S}) })
172
+ local: true, uis: %i[attached web], match: ->(text) { text.casecmp?("/cut") || text.match?(%r{\A/cut\s+\S}) })
172
173
  registry.register("/queue", "run as the next turn after this one (/queue TEXT)",
173
- local: true, uis: %i[repl attached], match: ->(text) { text.casecmp?("/queue") || text.match?(%r{\A/queue\s+\S}) })
174
+ local: true, uis: [:attached], match: ->(text) { text.casecmp?("/queue") || text.match?(%r{\A/queue\s+\S}) })
174
175
  # Takes back the newest message this attached TUI queued, before it
175
176
  # runs (the worker's POST turn/dequeue); the web has × on the bubble.
176
177
  registry.register("/unqueue", "remove your newest queued message before it runs", local: true, uis: [:attached],
177
178
  match: ->(text) { text.casecmp?("/unqueue") })
178
179
  # chi share for the attached session (TerminalUI::ParticipantCommands):
179
- # a participant's turns run on its worker, so the REPL points there.
180
+ # a participant's turns run on its worker.
180
181
  registry.register("/share", "invite someone to review this session (/share NAME [--agent] [--expires 3d] [--cap 2])",
181
182
  local: true, uis: [:attached], match: ->(text) { text.match?(%r{\A/share(?:\s.*)?\z}i) })
182
183
  registry.register("/participants", "list this session's participants (revoke [P], cap P DOLLARS)",
@@ -286,15 +287,15 @@ module Samagotchi
286
287
  "commands:\n#{lines.join("\n")}"
287
288
  end
288
289
 
289
- # Where a UI's own entry runs, for /help: both terminals are
290
- # "terminal" (also for no uis: every terminal), then "web".
290
+ # Where a UI's own entry runs, for /help: the attached TUI is
291
+ # "terminal" (also for no uis: the terminal), then "web". An older
292
+ # worker's :repl is no UI any more.
291
293
  # @param uis [Array<Symbol>, nil]
292
- # @return [String] "terminal", "repl", "terminal and web" …
294
+ # @return [String] "terminal", "web", "terminal and web"
293
295
  def ui_words(uis)
294
296
  return "terminal" if uis.nil?
295
297
 
296
- terminals = uis & %i[repl attached]
297
- words = terminals.size == 2 ? ["terminal"] : terminals.map(&:to_s)
298
+ words = uis.include?(:attached) ? ["terminal"] : []
298
299
  words << "web" if uis.include?(:web)
299
300
  words.join(" and ")
300
301
  end
@@ -309,8 +310,8 @@ module Samagotchi
309
310
  # @param as_parent [Boolean] a parent agent sent it (client cli:answer):
310
311
  # a /continue yes is held to guardrails.parent_answers, as chi
311
312
  # answer --option Continue is (Guardrails::ParentPolicy)
312
- # @param user_ui [Boolean] a user at a UI sent it (the REPL, an attached
313
- # TUI, the web; not a piped or marked one): only such a line loosens
313
+ # @param user_ui [Boolean] a user at a UI sent it (an attached TUI, the
314
+ # web; not a piped or marked one): only such a line loosens
314
315
  # /parent-answers
315
316
  # @return [Result, nil] nil when +line+ is not one of these commands
316
317
  def run(line, confirm: true, as_parent: false, user_ui: false)
@@ -427,7 +428,7 @@ module Samagotchi
427
428
  end
428
429
 
429
430
  # The output goes into the conversation for the next turn. Not saved
430
- # here (as in the REPL, the next turn saves it). No parent-session
431
+ # here (the next turn saves it). No parent-session
431
432
  # marker: the user runs this, not the model. A worker's `chi --model X`
432
433
  # default is unset, though: it was that run's, not the default of a chi
433
434
  # run here (Tools::Builtins.cli_model_unset). It runs in the session's
@@ -726,7 +727,7 @@ module Samagotchi
726
727
 
727
728
  # /recap on|off|default (RecapCommand); :recap in changed when this
728
729
  # session's own recap.enabled moved. It doesn't save: its host does on
729
- # a changed result (Worker#run_command, the REPL).
730
+ # a changed result (Worker#run_command).
730
731
  def recap_setting(text)
731
732
  output, changed, status = RecapCommand.new(engine: @engine).run(text.delete_prefix(RECAP_COMMAND))
732
733
  reply(output, status: status, changed: changed ? [:recap] : [])
@@ -884,65 +885,61 @@ module Samagotchi
884
885
  return unless session
885
886
 
886
887
  @engine.store_model!(session)
887
- begin
888
- @save.call(session)
889
- rescue StandardError
890
- nil
891
- end
888
+ Degrade.call(:model, "model_switch_save_failed") { @save.call(session) }
892
889
  end
893
890
 
894
891
  # "; served: <name>" when the server serves another model than asked
895
892
  # and its host's served: doesn't expect it.
896
893
  def served_note
897
- served, asked = @engine.served_model
898
- ServedModel.differs?(asked, served) && !@engine.served_expected?(served) ? "; served: #{served}" : ""
899
- rescue StandardError
900
- ""
894
+ Degrade.call(:model, "served_note_failed", fallback: "") do
895
+ served, asked = @engine.served_model
896
+ ServedModel.differs?(asked, served) && !@engine.served_expected?(served) ? "; served: #{served}" : ""
897
+ end
901
898
  end
902
899
 
903
900
  # "; sampling: temperature=0.6 (hosts.work)" when the model has any.
904
901
  def sampling_note
905
- summary = @engine.sampling_summary
906
- summary ? "; sampling: #{summary}" : ""
907
- rescue StandardError
908
- ""
902
+ Degrade.call(:model, "sampling_note_failed", fallback: "") do
903
+ summary = @engine.sampling_summary
904
+ summary ? "; sampling: #{summary}" : ""
905
+ end
909
906
  end
910
907
 
911
908
  # "; steer cut: after 60 s (models: qwen)", or "; steer cut: never
912
909
  # (hosts.splash)" for 0 or less, when the model has one configured;
913
910
  # "" for the default.
914
911
  def steer_cut_after_note
915
- summary = @engine.steer_cut_summary
916
- summary ? "; steer cut: #{summary}" : ""
917
- rescue StandardError
918
- ""
912
+ Degrade.call(:model, "steer_cut_note_failed", fallback: "") do
913
+ summary = @engine.steer_cut_summary
914
+ summary ? "; steer cut: #{summary}" : ""
915
+ end
919
916
  end
920
917
 
921
918
  # "; notes: model_notes_deepseek (system, 612 chars)": the model notes
922
919
  # the session's prompt carries (after a switch, the ones the new
923
920
  # model's prompt loads); "" without any.
924
921
  def notes_note
925
- text = PromptNote.text(@engine.prompt_notes)
926
- text.empty? ? "" : "; notes: #{text}"
927
- rescue StandardError
928
- ""
922
+ Degrade.call(:model, "notes_note_failed", fallback: "") do
923
+ text = PromptNote.text(@engine.prompt_notes)
924
+ text.empty? ? "" : "; notes: #{text}"
925
+ end
929
926
  end
930
927
 
931
928
  # "; thinking: off (models: qwen)" when the model has a level set.
932
929
  def thinking_note
933
- summary = @engine.thinking_summary
934
- summary ? "; thinking: #{summary}" : ""
935
- rescue StandardError
936
- ""
930
+ Degrade.call(:model, "thinking_note_failed", fallback: "") do
931
+ summary = @engine.thinking_summary
932
+ summary ? "; thinking: #{summary}" : ""
933
+ end
937
934
  end
938
935
 
939
936
  # "; reasoning history: drop (models: qwen)" when the chat model has a
940
937
  # level set.
941
938
  def reasoning_history_note
942
- summary = @engine.reasoning_history_summary
943
- summary ? "; reasoning history: #{summary}" : ""
944
- rescue StandardError
945
- ""
939
+ Degrade.call(:model, "reasoning_history_note_failed", fallback: "") do
940
+ summary = @engine.reasoning_history_summary
941
+ summary ? "; reasoning history: #{summary}" : ""
942
+ end
946
943
  end
947
944
 
948
945
  # " (default: x, profile=qwen36, name)": +extra+ and the prompt profile,
@@ -954,9 +951,9 @@ module Samagotchi
954
951
  end
955
952
 
956
953
  def chat_model?
957
- @engine.chat_model?
958
- rescue StandardError
959
- false
954
+ Degrade.call(:model, "chat_model_unresolved", fallback: false) do
955
+ @engine.chat_model?
956
+ end
960
957
  end
961
958
 
962
959
  # "profile=qwen36, server (chat_template)": the profile and where it came from.
@@ -24,7 +24,7 @@ module Samagotchi
24
24
  # A continue that can't happen (#run). reason: :continued
25
25
  # (another session continues it already: +ids+ names it), :open_children
26
26
  # (open children it can't move to the new link: on an older chi's
27
- # worker or open in a chi REPL; +ids+ names them) or :folder_gone (its
27
+ # worker or open in a one-turn chi run; +ids+ names them) or :folder_gone (its
28
28
  # folder isn't there).
29
29
  class Refused < StandardError
30
30
  attr_reader :session_id, :reason, :ids
@@ -9,7 +9,7 @@ require_relative "session_access/local"
9
9
  module Samagotchi
10
10
  # `chi sessions delete`: remove sessions for good (SessionManager.delete_session),
11
11
  # one line per id. A live worker's session is refused unless --force stops
12
- # the worker first; a chi REPL's never is.
12
+ # the worker first; a one-turn chi run's never is.
13
13
  class SessionDeleteCommand
14
14
  include CLI::Command
15
15
 
@@ -18,8 +18,8 @@ module Samagotchi
18
18
  Deletes each session: its history, notes, images and queued input.
19
19
  This can't be undone.
20
20
  -f, --force stop a session's running worker first (without it,
21
- a live session is refused); a session open in a
22
- chi REPL is always refused
21
+ a live session is refused); a session in a one-turn
22
+ chi run (chi --non-interactive) is always refused
23
23
  TEXT
24
24
  PREVIEW_LIMIT = 60
25
25
  FLAGS = CLI::Flags.new(help: CLI::Command::HELP_WORDS) { |f| f.switch "-f", "--force" }
@@ -67,7 +67,7 @@ module Samagotchi
67
67
  @stdout.puts(["#{short} deleted#{" (stopped its worker)" if result[:stopped]}", preview].compact.join(" "))
68
68
  true
69
69
  rescue SessionManager::OwnedByTUI
70
- @stdout.puts("#{short} refused: it is open in a chi REPL; close it there first")
70
+ @stdout.puts("#{short} refused: it is #{OwnerLock::TUI_BUSY}; let it finish first")
71
71
  false
72
72
  rescue SessionManager::DeleteRefused => e
73
73
  line = if e.reason == :still_stopping then "its worker is still shutting down; try again in a moment"
@@ -33,6 +33,7 @@ require_relative "worker_idle_exit"
33
33
  require_relative "worker_signals"
34
34
  require_relative "chi_invocation"
35
35
  require_relative "log"
36
+ require_relative "degrade"
36
37
  require_relative "log_path"
37
38
  require_relative "paths"
38
39
  require_relative "project_env"
@@ -81,23 +82,32 @@ module Samagotchi
81
82
  autoload :SessionRetention, File.expand_path("session_retention", __dir__)
82
83
  # session_continue requires this file.
83
84
  autoload :SessionContinue, File.expand_path("session_continue", __dir__)
84
- # Needed only on an archive or a delete (it loads Web::Grants).
85
+ # Needed only on an archive or a delete (it loads Grants).
85
86
  autoload :Participants, File.expand_path("participants", __dir__)
86
87
 
87
88
  class SessionManager
88
- # Raised when the interactive TUI owns the session: it runs its own Engine
89
- # and reads no input files, so a worker must not be spawned or signalled.
89
+ # Raised when a terminal's own chi owns the session (OwnerLock kind
90
+ # "tui": a -p --non-interactive one-shot, TerminalUI): it runs its own
91
+ # Engine and reads no input files, so a worker must not be spawned or
92
+ # signalled.
90
93
  class OwnedByTUI < StandardError
91
- # @return [String] the session the REPL owns (a delegate's, when an
92
- # archive met one)
94
+ # @return [String] the session it owns (a delegate's, when an archive
95
+ # met one)
93
96
  attr_reader :session_id
94
97
 
95
- def initialize(session_id)
98
+ def initialize(session_id, words = nil)
96
99
  @session_id = session_id
97
- super("session #{session_id} is open in a chi REPL; close it there first")
100
+ super(words || "session #{session_id} is #{OwnerLock::TUI_BUSY}; let it finish first")
98
101
  end
99
102
  end
100
103
 
104
+ # A `chi scratch` session: its terminal's alone (Session::SCRATCH_REFUSED).
105
+ # An OwnedByTUI, so every caller that refuses a terminal's session
106
+ # (chi send, chi web's 409 owned_by_tui) refuses it the same way.
107
+ class ScratchSession < OwnedByTUI
108
+ def initialize(session_id) = super(session_id, Session::SCRATCH_REFUSED)
109
+ end
110
+
101
111
  # A delete that would pull the session from under its live worker.
102
112
  # reason: :worker_running (not asked to stop it) or :still_stopping
103
113
  # (stopped, but the worker outlived the wait).
@@ -173,12 +183,15 @@ module Samagotchi
173
183
  # requester, rules and claim (VerifyFresh.start)
174
184
  # @param before_spawn [#call, nil] (session) runs once the session and
175
185
  # its note are saved, before its worker spawns
186
+ # @param scratch [Boolean] a `chi scratch` session (its terminal's
187
+ # AttachLauncher): its worker runs a scratch Engine and deletes it
188
+ # once its last client is gone
176
189
  # @return [Session] with #seed_images_dropped: refs whose file was gone,
177
190
  # and #model_warning: an id its host's saved list doesn't have
178
191
  def self.spawn_session(prompt:, mode: "assist", working_directory: nil, model_name: nil, state_dir: nil,
179
192
  memories: [], muted_memories: [], parent_id: nil, messages: [], images_from: nil,
180
193
  preview: nil, title: nil, delegate: false, setup: SessionSetup.new, continues: nil, note: nil,
181
- note_source: "chi", before_spawn: nil, review: nil, verify_fresh: nil)
194
+ note_source: "chi", before_spawn: nil, review: nil, verify_fresh: nil, scratch: false)
182
195
  sd = state_dir || Session.default_state_dir
183
196
  # The resolved ref is stored (a resumed session keeps its model when an
184
197
  # alias is retargeted), with the name as typed beside it. A wrong host
@@ -201,7 +214,8 @@ module Samagotchi
201
214
  delegate: delegate,
202
215
  setup: setup,
203
216
  continues: continues,
204
- review: review
217
+ review: review,
218
+ scratch: scratch
205
219
  )
206
220
  session.toolkit = Toolkit.configured(delegate: delegate) if session.toolkit.nil?
207
221
  session.toolkit_origin = setup.toolkit_origin || "default"
@@ -269,7 +283,7 @@ module Samagotchi
269
283
  child_env[ModelProfile::MODEL_FROM_CLI_ENV] = "1" if child_env.key?(ModelProfile::MODEL_ENV)
270
284
  # And the spawner's debug log, absolute: a relative log.file would
271
285
  # otherwise land in the worker's (the session's) directory.
272
- log_path = begin LogPath.resolve rescue nil end
286
+ log_path = begin LogPath.resolve rescue nil end # degrade-ok: no log to say it in; the worker's is off (below)
273
287
  if log_path
274
288
  child_env["SAMAGOTCHI_LOG_FILE"] = log_path
275
289
  else
@@ -316,7 +330,8 @@ module Samagotchi
316
330
  # +project_root+ unless asked for every project.
317
331
  # @param live [Boolean] only sessions a worker owns now (the owner lock,
318
332
  # not the saved status, which a dead worker leaves at "running"); a
319
- # REPL-owned session is left out: it can't take notes
333
+ # session a one-shot (or an older chi's REPL) owns is left out: it
334
+ # can't take notes
320
335
  # @param cwd [String, nil] only sessions in this folder or below it
321
336
  # @param project_root [String, nil] only this project's sessions
322
337
  # (Session#project_root)
@@ -405,7 +420,7 @@ module Samagotchi
405
420
  # stopped first; the marker is written even while it shuts down.
406
421
  # @return [ArchiveResult]
407
422
  # @raise [ArgumentError] unknown id (Session::AmbiguousId for a prefix of several)
408
- # @raise [OwnedByTUI] a chi REPL owns it or one of its children
423
+ # @raise [OwnedByTUI] a one-turn chi run owns it or one of its children
409
424
  # @raise [ArchiveRefused] a turn runs in it or in a child, a prompt is
410
425
  # queued in one (a later resume would run it), a live one offers to
411
426
  # continue past the step limit, or it is a scratch session
@@ -447,7 +462,7 @@ module Samagotchi
447
462
  # turn runs), :queued (input/ holds a prompt, which outlives the stop and
448
463
  # runs on a resume, live worker or not), :offer (a live worker's continue
449
464
  # offer, which the stop would drop), or nil.
450
- # @raise [OwnedByTUI] a chi REPL owns it
465
+ # @raise [OwnedByTUI] a one-turn chi run owns it
451
466
  def self.archive_refusal(sid, owner, state_dir)
452
467
  raise OwnedByTUI, sid if owner&.tui?
453
468
 
@@ -555,6 +570,8 @@ module Samagotchi
555
570
  def self.resume_session(session_id, state_dir: nil)
556
571
  sd = state_dir || Session.default_state_dir
557
572
  session = Session.load(session_id, state_dir: sd)
573
+ # Its terminal's worker came from spawn_session; nothing wakes another.
574
+ raise ScratchSession, session.id if session.scratch
558
575
  return session if refuse_tui!(session.id, state_dir: sd)
559
576
 
560
577
  # status is turn state, not liveness: a new worker runs no turn yet,
@@ -576,7 +593,7 @@ module Samagotchi
576
593
 
577
594
  # A delegate child rang a parent that has no worker (ChildRing): start
578
595
  # one, which runs a turn for the report. Not for a parent that is gone,
579
- # stopped, archived, a scratch session, open in a chi REPL, or a
596
+ # stopped, archived, a scratch session, a one-turn run's, or a
580
597
  # continue's new link still taking its delegates (its own spawn comes):
581
598
  # the ring waits on disk for whoever runs it next (#resume_session
582
599
  # itself would clear the stop). A stop landing between the check and
@@ -686,7 +703,8 @@ module Samagotchi
686
703
  reminders: "it has reminders, which its worker's older chi can't carry over a restart",
687
704
  command_running: "a /btw (or another command) is still running",
688
705
  relays_open: "a delegate's approval relayed here is open or just answered",
689
- background_tasks: "background tasks it started are still running"
706
+ background_tasks: "background tasks it started are still running",
707
+ scratch: "a chi scratch session's worker ends with its terminal"
690
708
  }.merge(WorkerIdleExit::LEAVING_WORDS).freeze
691
709
 
692
710
  # What the CLI and the web say.
@@ -726,7 +744,7 @@ module Samagotchi
726
744
  # be lost and starts its successor. If the worker cannot be asked, stop
727
745
  # it without a stop marker and resume only when disk shows no work to lose.
728
746
  # @return [Restarted]
729
- # @raise [OwnedByTUI] a chi REPL owns the session
747
+ # @raise [OwnedByTUI] a one-turn chi run owns the session
730
748
  # @raise [RestartRefused]
731
749
  # @raise [ArgumentError] no such session
732
750
  def self.restart_session(session_id, state_dir: nil, wait: RESTART_WAIT, client_id: ClientId::CLI_RESTART)
@@ -917,34 +935,21 @@ module Samagotchi
917
935
  EMPTY_SKELETON_DIRS = (EMPTY_DIRS + [SessionInbox::OUTPUT_DIR]).freeze
918
936
 
919
937
  # session.keep_empty off: sessions left with nothing in them are deleted
920
- # (the worker as it exits, the TUI at /exit, the retention sweep).
938
+ # (the worker as it exits, the retention sweep).
921
939
  def self.discard_empty?
922
- Samagotchi::Config.get("session.keep_empty") != true
923
- rescue StandardError
924
- false
940
+ Degrade.call(:config, "keep_empty_unreadable", fallback: false) { Samagotchi::Config.get("session.keep_empty") != true }
925
941
  end
926
942
 
927
943
  # Whether a session being left (or found left) is deleted: session.keep_empty
928
944
  # is off and nothing happened in it (#empty_session?). The worker as it
929
- # leaves, the REPL at /exit and the retention sweep all ask this.
945
+ # leaves and the retention sweep both ask this.
930
946
  # @param default_model [String, nil] what a new session starts on
931
- # @param model_name [String, nil] the model its owner runs now (the REPL
932
- # keeps /model in its Engine); the saved one is checked either way
933
947
  # @param used_memory_names [Array<String>] memory its owner's Engine used
934
948
  # before any save
935
- # @param unsaved [Session, nil] the REPL's working copy, judged instead
936
- # when the session was never saved
937
- def self.discardable?(session_id, default_model:, state_dir: nil, model_name: default_model, used_memory_names: [],
938
- unsaved: nil)
939
- return false unless discard_empty? && same_model?(model_name, default_model) && Array(used_memory_names).empty?
949
+ def self.discardable?(session_id, default_model:, state_dir: nil, used_memory_names: [])
950
+ return false unless discard_empty? && Array(used_memory_names).empty?
940
951
 
941
- sd = state_dir || Session.default_state_dir
942
- if unsaved && !File.exist?(Session.session_file(session_id, state_dir: sd))
943
- return no_conversation?(unsaved.messages) && unsaved.last_prompt.to_s.strip.empty? &&
944
- empty_session_dir?(Session.session_dir(session_id, state_dir: sd))
945
- end
946
-
947
- empty_session?(session_id, state_dir: sd, default_model: default_model)
952
+ empty_session?(session_id, state_dir: state_dir || Session.default_state_dir, default_model: default_model)
948
953
  end
949
954
 
950
955
  # A session nothing happened in: no conversation, no turn tried (a failed
@@ -971,12 +976,12 @@ module Samagotchi
971
976
  return true if one.to_s == other.to_s
972
977
  return false if one.nil? || other.nil?
973
978
 
974
- ConfigFile.model_ref(one).ref == ConfigFile.model_ref(other).ref
975
- rescue StandardError
976
- false
979
+ Degrade.call(:config, "model_ref_unresolved", fallback: false) do
980
+ ConfigFile.model_ref(one).ref == ConfigFile.model_ref(other).ref
981
+ end
977
982
  end
978
983
 
979
- # Only the system prompt (the REPL seeds one, a saved session keeps
984
+ # Only the system prompt (an Engine seeds one, a saved session keeps
980
985
  # it); a context note is a system message too, but with its kind.
981
986
  # A turn note (the tail system message after a failed, cancelled or empty
982
987
  # turn) is not a conversation either: a session whose only turn failed
@@ -1049,7 +1054,7 @@ module Samagotchi
1049
1054
  # @param stop [Boolean] stop a live worker first (waits +wait+ seconds)
1050
1055
  # @return [Hash] {id:, removed: [paths], stopped: whether a worker was stopped}
1051
1056
  # @raise [ArgumentError] unknown id (Session::AmbiguousId for a prefix of several)
1052
- # @raise [OwnedByTUI] a chi REPL owns it (never stopped from here)
1057
+ # @raise [OwnedByTUI] a one-turn chi run owns it (never stopped from here)
1053
1058
  # @raise [DeleteRefused] a worker owns it and +stop+ is false, or it
1054
1059
  # outlived the wait
1055
1060
  # @param purge_runs [Boolean] also delete System 1's saved runs of it and
@@ -1130,7 +1135,7 @@ module Samagotchi
1130
1135
  # ticks (nil: Worker::FALLBACK_TICK_SECONDS); queued turns wake it at once
1131
1136
  # A stopped session's worker exits 0 (a signalled one too) and a
1132
1137
  # crashed one 1, there.
1133
- # @return [Symbol] :idle_exit, :exit_requested or :restart
1138
+ # @return [Symbol] :idle_exit, :exit_requested, :restart or :scratch_left
1134
1139
  def self.run_session_loop(session_id, state_dir: nil, owner_wait: OwnerLock::DEFAULT_WAIT,
1135
1140
  idle_exit_minutes: nil, poll_interval: nil)
1136
1141
  # A worker inherits chi's locale (LC_ALL=C too): read files as UTF-8.
@@ -1175,6 +1180,13 @@ module Samagotchi
1175
1180
  signals.restore!
1176
1181
  end
1177
1182
  Log.info(:worker, "stop", reason: result)
1183
+ # A scratch session goes with its worker however it leaves (its
1184
+ # terminal deletes it too, whichever comes first); nothing resumes it.
1185
+ if worker.scratch?
1186
+ delete_left_scratch(session_id, state_dir: sd)
1187
+ exit(result == :crashed ? 1 : 0) if %i[stopped signal crashed].include?(result)
1188
+ return result
1189
+ end
1178
1190
  # A stopped session is left as it is: no resume for input that came
1179
1191
  # in, no discard. One whose worker a signal stopped isn't discarded
1180
1192
  # either; input that came in (queued behind the turn it stopped)
@@ -1231,6 +1243,16 @@ module Samagotchi
1231
1243
  Process.exit!(0)
1232
1244
  end
1233
1245
 
1246
+ # Delete a scratch session its worker left (its terminal gone:
1247
+ # Worker#leave_scratch, or any other way out), now the lock is free.
1248
+ # Its terminal may have deleted it first.
1249
+ private_class_method def self.delete_left_scratch(session_id, state_dir:)
1250
+ delete_session(session_id, state_dir: state_dir)
1251
+ Log.info(:worker, "scratch_deleted", sid: session_id)
1252
+ rescue DeleteRefused, ArgumentError, SystemCallError => e
1253
+ Log.info(:worker, "scratch_kept", sid: session_id, error: e.class.name, msg: e.message)
1254
+ end
1255
+
1234
1256
  # Delete a session its worker left empty. Checked again now the lock is
1235
1257
  # free: a note or input may have come in since the worker looked, and a
1236
1258
  # worker woken meanwhile owns it (delete_session refuses). A `chi send`
@@ -1258,9 +1280,7 @@ module Samagotchi
1258
1280
  end
1259
1281
 
1260
1282
  def self.config_idle_exit_minutes
1261
- Samagotchi::Config.get("session.idle_exit_minutes")
1262
- rescue StandardError
1263
- nil
1283
+ Degrade.call(:config, "idle_exit_minutes_unreadable") { Samagotchi::Config.get("session.idle_exit_minutes") }
1264
1284
  end
1265
1285
 
1266
1286
  private_class_method def self.setup_session_directory(session_dir, session, state_dir:)
@@ -1477,7 +1497,7 @@ module Samagotchi
1477
1497
  # a worker that doesn't queue (no file then: it would steer), :timeout
1478
1498
  # when it took the request but never answered (no file then), or
1479
1499
  # :failed when the input file couldn't be written
1480
- # @raise [OwnedByTUI] a chi REPL owns the session
1500
+ # @raise [OwnedByTUI] a one-turn chi run owns the session
1481
1501
  # @raise [ArgumentError] no such session
1482
1502
  def self.deliver_turn(session_id, prompt:, client_id: nil, images: [], delivery: nil, no_interrupt: false,
1483
1503
  author: nil, pending_limit: nil, anchors: nil, state_dir: nil, manager: self, bridge: nil)
@@ -1532,7 +1552,7 @@ module Samagotchi
1532
1552
  # A participant's file only for a worker started after it: a worker
1533
1553
  # still holding the session with its Bridge gone (exiting) may be an
1534
1554
  # older one, which would run it without the review boundary.
1535
- # (A chi REPL holding it is refused as for anyone, below.)
1555
+ # (A one-turn chi run holding it is refused as for anyone, below.)
1536
1556
  if Steer.participant_author?(author) && (holder = delivery_owner(manager, session_id, state_dir)) && !holder.tui?
1537
1557
  return DeliverResult.new(status: :timeout, via: :file,
1538
1558
  ack: { "error" => "worker_timeout",
@@ -1617,9 +1637,7 @@ module Samagotchi
1617
1637
  end
1618
1638
 
1619
1639
  private_class_method def self.delivery_owner(manager, session_id, state_dir)
1620
- manager.session_owner(session_id, state_dir: state_dir)
1621
- rescue StandardError
1622
- nil
1640
+ Degrade.call(:worker, "owner_unreadable", sid: session_id) { manager.session_owner(session_id, state_dir: state_dir) }
1623
1641
  end
1624
1642
 
1625
1643
  def self.stopped_on_disk?(session_id, state_dir:)
@@ -25,8 +25,8 @@ module Samagotchi
25
25
  # SessionMetrics is a persistent, error-isolated collector of per-session
26
26
  # analytics. It implements the same #call(event) sink contract as
27
27
  # SessionObserver subscribers, so Engine#run_turn feeds it (it forwards every
28
- # event to its SessionObserver): the REPL, the -p/--non-interactive/--resume
29
- # paths and SessionManager background workers.
28
+ # event to its SessionObserver): a --non-interactive one-shot and the
29
+ # session workers.
30
30
  #
31
31
  # Each finished turn leaves a record (status, timings, model, prompt and
32
32
  # completion tokens, tool calls, iterations, retries, cuts); session totals are