samagotchi 0.152.0 → 0.154.0

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -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/bundle-authoring.md +1 -1
  8. data/docs/bundles.md +93 -12
  9. data/docs/cli.md +83 -99
  10. data/docs/configuration.md +46 -40
  11. data/docs/context.md +3 -3
  12. data/docs/desktop.md +1 -1
  13. data/docs/guardrails.md +15 -15
  14. data/docs/hooks.md +13 -12
  15. data/docs/internals/anthropic.md +3 -3
  16. data/docs/internals/attached-context.md +5 -2
  17. data/docs/internals/background-tasks.md +3 -4
  18. data/docs/internals/block-renderers.md +4 -0
  19. data/docs/internals/bridge.md +3 -3
  20. data/docs/internals/context-telemetry.md +1 -1
  21. data/docs/internals/engine.md +42 -12
  22. data/docs/internals/objectives.md +6 -5
  23. data/docs/internals/participants.md +8 -8
  24. data/docs/internals/prompt-caching.md +8 -4
  25. data/docs/internals/remote-sessions.md +7 -7
  26. data/docs/internals/web-frontend.md +20 -17
  27. data/docs/internals/web-push.md +1 -1
  28. data/docs/memory.md +2 -2
  29. data/docs/plugins.md +52 -43
  30. data/docs/releasing.md +1 -1
  31. data/docs/sessions.md +25 -25
  32. data/docs/sub-agent.md +2 -2
  33. data/docs/system-one.md +1 -1
  34. data/docs/testing.md +18 -8
  35. data/lib/samagotchi/answer_text.rb +34 -0
  36. data/lib/samagotchi/bridge.rb +43 -32
  37. data/lib/samagotchi/bridge_client.rb +10 -9
  38. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  39. data/lib/samagotchi/broadcast/triage.rb +1 -1
  40. data/lib/samagotchi/broadcast_command.rb +5 -5
  41. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  42. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  43. data/lib/samagotchi/bundle_command.rb +2 -2
  44. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  45. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  46. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  48. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  49. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  50. data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
  51. data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
  52. data/lib/samagotchi/child_move.rb +3 -3
  53. data/lib/samagotchi/commands/registry.rb +6 -6
  54. data/lib/samagotchi/composer_route.rb +4 -5
  55. data/lib/samagotchi/config.rb +14 -12
  56. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  57. data/lib/samagotchi/context_candidates.rb +4 -3
  58. data/lib/samagotchi/continue_offer.rb +1 -1
  59. data/lib/samagotchi/debug_log.rb +1 -1
  60. data/lib/samagotchi/degrade.rb +75 -0
  61. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  62. data/lib/samagotchi/engine/cards.rb +87 -0
  63. data/lib/samagotchi/engine/context_judge.rb +138 -0
  64. data/lib/samagotchi/engine/events.rb +131 -0
  65. data/lib/samagotchi/engine/guardrails.rb +87 -0
  66. data/lib/samagotchi/engine/hooks.rb +119 -0
  67. data/lib/samagotchi/engine/messages.rb +124 -0
  68. data/lib/samagotchi/engine/model_settings.rb +127 -0
  69. data/lib/samagotchi/engine/models.rb +261 -0
  70. data/lib/samagotchi/engine/objectives.rb +116 -0
  71. data/lib/samagotchi/engine/participants.rb +86 -0
  72. data/lib/samagotchi/engine/plugins.rb +208 -0
  73. data/lib/samagotchi/engine/questions.rb +98 -0
  74. data/lib/samagotchi/engine/recap.rb +130 -0
  75. data/lib/samagotchi/engine/session.rb +131 -0
  76. data/lib/samagotchi/engine/side_requests.rb +135 -0
  77. data/lib/samagotchi/engine/steering.rb +134 -0
  78. data/lib/samagotchi/engine/toolkit.rb +269 -0
  79. data/lib/samagotchi/engine/turn_end.rb +180 -0
  80. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  81. data/lib/samagotchi/engine/verify.rb +153 -0
  82. data/lib/samagotchi/engine.rb +63 -2642
  83. data/lib/samagotchi/grants.rb +575 -0
  84. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  85. data/lib/samagotchi/guardrails/context.rb +2 -3
  86. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  87. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  88. data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
  89. data/lib/samagotchi/hook_routes.rb +3 -3
  90. data/lib/samagotchi/idle_recap.rb +4 -5
  91. data/lib/samagotchi/idle_reminders.rb +3 -4
  92. data/lib/samagotchi/iteration_limit.rb +2 -2
  93. data/lib/samagotchi/kernel_loop.rb +10 -11
  94. data/lib/samagotchi/launch_mode.rb +13 -32
  95. data/lib/samagotchi/level_setting_command.rb +3 -2
  96. data/lib/samagotchi/llm_context_command.rb +3 -2
  97. data/lib/samagotchi/llm_context_stale.rb +2 -2
  98. data/lib/samagotchi/log.rb +7 -7
  99. data/lib/samagotchi/log_line.rb +1 -1
  100. data/lib/samagotchi/log_path.rb +1 -1
  101. data/lib/samagotchi/message_parts.rb +3 -2
  102. data/lib/samagotchi/note_command.rb +1 -1
  103. data/lib/samagotchi/note_delivery.rb +18 -8
  104. data/lib/samagotchi/objectives.rb +24 -3
  105. data/lib/samagotchi/objectives_check.rb +8 -27
  106. data/lib/samagotchi/objectives_command.rb +4 -69
  107. data/lib/samagotchi/objectives_edit.rb +87 -0
  108. data/lib/samagotchi/output_formatter.rb +7 -1
  109. data/lib/samagotchi/owner_lock.rb +7 -1
  110. data/lib/samagotchi/parent_answers_command.rb +5 -4
  111. data/lib/samagotchi/participant_exposure.rb +42 -0
  112. data/lib/samagotchi/participant_mode.rb +1 -1
  113. data/lib/samagotchi/participant_name.rb +1 -1
  114. data/lib/samagotchi/participants.rb +17 -17
  115. data/lib/samagotchi/participants_watch.rb +1 -1
  116. data/lib/samagotchi/pending_input_queue.rb +2 -2
  117. data/lib/samagotchi/plugin/api.rb +3 -2
  118. data/lib/samagotchi/plugin/context.rb +21 -7
  119. data/lib/samagotchi/plugin/peer.rb +2 -1
  120. data/lib/samagotchi/plugin/sessions.rb +6 -4
  121. data/lib/samagotchi/plugin_tasks.rb +1 -2
  122. data/lib/samagotchi/private_files.rb +29 -0
  123. data/lib/samagotchi/project_env.rb +2 -2
  124. data/lib/samagotchi/question_desk.rb +8 -7
  125. data/lib/samagotchi/recap_command.rb +2 -1
  126. data/lib/samagotchi/recap_setup.rb +1 -1
  127. data/lib/samagotchi/recap_store.rb +1 -1
  128. data/lib/samagotchi/redact.rb +86 -0
  129. data/lib/samagotchi/reminder_queue.rb +2 -2
  130. data/lib/samagotchi/remote_attach_command.rb +1 -1
  131. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  132. data/lib/samagotchi/review_start.rb +1 -1
  133. data/lib/samagotchi/run_options.rb +6 -6
  134. data/lib/samagotchi/self_report.rb +42 -39
  135. data/lib/samagotchi/send_command/busy.rb +1 -1
  136. data/lib/samagotchi/send_command.rb +4 -1
  137. data/lib/samagotchi/session.rb +11 -2
  138. data/lib/samagotchi/session_access/contract.rb +3 -3
  139. data/lib/samagotchi/session_access/local.rb +9 -9
  140. data/lib/samagotchi/session_access/remote.rb +17 -7
  141. data/lib/samagotchi/session_access.rb +4 -4
  142. data/lib/samagotchi/session_archive_command.rb +2 -2
  143. data/lib/samagotchi/session_chain.rb +8 -6
  144. data/lib/samagotchi/session_commands.rb +45 -48
  145. data/lib/samagotchi/session_continue.rb +1 -1
  146. data/lib/samagotchi/session_delete_command.rb +4 -4
  147. data/lib/samagotchi/session_manager.rb +59 -36
  148. data/lib/samagotchi/session_metrics.rb +2 -2
  149. data/lib/samagotchi/session_model_settings.rb +3 -2
  150. data/lib/samagotchi/session_participants.rb +1 -1
  151. data/lib/samagotchi/session_retention.rb +3 -3
  152. data/lib/samagotchi/sessions_command.rb +4 -4
  153. data/lib/samagotchi/share_command.rb +10 -10
  154. data/lib/samagotchi/steer.rb +1 -1
  155. data/lib/samagotchi/system_prompt.rb +102 -109
  156. data/lib/samagotchi/task_watch.rb +1 -1
  157. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  158. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  159. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  160. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  161. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  162. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  163. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  164. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  165. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  166. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  167. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  168. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  169. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  170. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  171. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  172. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  173. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  174. data/lib/samagotchi/terminal_ui.rb +142 -1387
  175. data/lib/samagotchi/title_command.rb +2 -2
  176. data/lib/samagotchi/tool_runner.rb +14 -16
  177. data/lib/samagotchi/toolkit.rb +8 -5
  178. data/lib/samagotchi/toolkit_binding.rb +30 -5
  179. data/lib/samagotchi/toolkit_command.rb +2 -1
  180. data/lib/samagotchi/toolkit_report_command.rb +9 -5
  181. data/lib/samagotchi/toolkit_suggest.rb +12 -6
  182. data/lib/samagotchi/tools/builtin_calls.rb +14 -10
  183. data/lib/samagotchi/tools/deferred_tools.rb +2 -2
  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/registry.rb +44 -20
  189. data/lib/samagotchi/tools/send_note.rb +2 -1
  190. data/lib/samagotchi/tools/tool_index.rb +27 -21
  191. data/lib/samagotchi/turn_endings.rb +2 -2
  192. data/lib/samagotchi/turn_flow.rb +4 -5
  193. data/lib/samagotchi/update_hint.rb +2 -2
  194. data/lib/samagotchi/version.rb +1 -1
  195. data/lib/samagotchi/web/app.rb +39 -50
  196. data/lib/samagotchi/web/participant_access.rb +2 -2
  197. data/lib/samagotchi/web/participant_review.rb +5 -5
  198. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  199. data/lib/samagotchi/web/participant_view.rb +2 -82
  200. data/lib/samagotchi/web/participants_admin.rb +1 -1
  201. data/lib/samagotchi/web/public/app.js +61 -357
  202. data/lib/samagotchi/web/public/block_host.js +35 -8
  203. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  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 +110 -0
  207. data/lib/samagotchi/web/public/format.js +3 -3
  208. data/lib/samagotchi/web/public/owner_claim.js +81 -0
  209. data/lib/samagotchi/web/public/owner_verify.js +112 -0
  210. data/lib/samagotchi/web/public/session_fields.js +3 -3
  211. data/lib/samagotchi/web/public/strip_grip.js +3 -3
  212. data/lib/samagotchi/web/public/strip_hidden.js +65 -0
  213. data/lib/samagotchi/web/public/version_notices.js +132 -0
  214. data/lib/samagotchi/web/push/store.rb +1 -1
  215. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  216. data/lib/samagotchi/web/session_control.rb +12 -8
  217. data/lib/samagotchi/web/session_create.rb +1 -1
  218. data/lib/samagotchi/web/session_hub.rb +2 -2
  219. data/lib/samagotchi/web/session_show.rb +2 -2
  220. data/lib/samagotchi/web/session_summary.rb +1 -1
  221. data/lib/samagotchi/worker.rb +106 -9
  222. data/lib/samagotchi/worker_link.rb +3 -3
  223. data/lib/samagotchi/working_set_command.rb +3 -2
  224. metadata +36 -6
  225. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  226. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  227. data/lib/samagotchi/web/grants.rb +0 -575
  228. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  229. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -8,7 +8,7 @@ module Samagotchi
8
8
  # /objectives: with no arguments the session's list (Objectives), each
9
9
  # objective's latest move under it (the model's objectives tool; a move
10
10
  # changes nothing); `set`, `add`, `edit`, `done`, `drop` and `undo` edit it. An edit is a
11
- # whole new list in a note file (NoteDelivery, with its refusal of a chi REPL's session), so it is
11
+ # whole new list in a note file (NoteDelivery, with its refusal of a one-turn chi run's session), so it is
12
12
  # safe from any thread, a turn running or not: it goes into the conversation
13
13
  # at the worker's next pass between turns, and a second edit stacks on an
14
14
  # unabsorbed first. Nothing here saves the session.
@@ -18,10 +18,6 @@ module Samagotchi
18
18
  "(a text may end \"(done when: …)\"; lines work as | does)".freeze
19
19
  NONE = "no objectives; #{NAME} set A | B sets them".freeze
20
20
  BETWEEN_TURNS = "(an edit goes into the conversation between turns)"
21
- SEPARATOR = /\n|(?:\A|\s)\|(?:\s|\z)/
22
- # What the frame shows a status as: typed at a text's start it would pass
23
- # for one (Objectives::Item#line).
24
- STATUS_MARK = /\A(?:\[(?:#{Objectives::STATUSES.join("|")})\]\s*)+/i
25
21
  # A proposal card's action (Tools::ObjectivesTool): the edit, then the card
26
22
  # folds to a line, once: it is recorded, so a repeat applies nothing. A
27
23
  # reword's --was says what it was proposed against. The prefix is the
@@ -30,7 +26,6 @@ module Samagotchi
30
26
  FOLDS = { Objectives::APPLIED => "Proposal applied", Objectives::DISMISSED => "Proposal dismissed" }.freeze
31
27
  DISMISS = "dismiss"
32
28
  CHECK = /\Acheck(?:\s|\z)/i
33
- MARKS = { "done" => Objectives::DONE, "drop" => Objectives::DROPPED, "undo" => Objectives::ACTIVE }.freeze
34
29
 
35
30
  # @param engine [Engine] #session, #session_state_dir
36
31
  def initialize(engine:)
@@ -84,7 +79,7 @@ module Samagotchi
84
79
  end
85
80
 
86
81
  def run_line(args, was: nil)
87
- session = @engine.session or return ["no session yet", :error]
82
+ session = @engine.session or return [Session::NONE_YET, :error]
88
83
  # The running turn's moves aren't in session.messages yet. A snapshot from
89
84
  # this thread, read only: an entry is there whole or not yet, and a call
90
85
  # with no result yet pairs with nothing.
@@ -93,7 +88,7 @@ module Samagotchi
93
88
  verb, rest = args.to_s.strip.split(/\s+/, 2)
94
89
  return [listing(list, session), :ok] if verb.nil?
95
90
 
96
- edited = edit(list, verb.downcase, rest.to_s, was: was)
91
+ edited = Objectives::Edit.apply(list, verb.downcase, rest.to_s, was: was)
97
92
  return [USAGE, :error] if edited.nil?
98
93
  return [edited, :error] if edited.is_a?(String)
99
94
  return ["#{listing(list, session)}\n(no change)", :ok] if edited == list
@@ -101,70 +96,10 @@ module Samagotchi
101
96
  deliver(edited, session)
102
97
  end
103
98
 
104
- # @return [Objectives::List, String, nil] the new list, what's wrong with
105
- # the line, or nil for a line that isn't one
106
- def edit(list, verb, rest, was: nil)
107
- case verb
108
- when "set", "add"
109
- entries = entries_of(rest)
110
- return nil if entries.nil? || entries.empty?
111
-
112
- verb == "set" ? list.set(entries) : entries.reduce(list) { |so_far, e| so_far.add(e[:text], done_when: e[:done_when]) }
113
- when "edit" then reword(list, rest, was: was)
114
- when *MARKS.keys then mark(list, MARKS.fetch(verb), rest)
115
- end
116
- end
117
-
118
- def mark(list, status, rest)
119
- ids = rest.upcase.split(/[\s,]+/).reject(&:empty?)
120
- return nil if ids.empty?
121
-
122
- missing = ids.reject { |id| list.find(id) }
123
- return "no objective #{missing.join(", ")}" unless missing.empty?
124
-
125
- if status == Objectives::DONE
126
- dropped = ids.select { |id| list.find(id).status == Objectives::DROPPED }
127
- return "#{dropped.join(", ")} is dropped; #{NAME} undo #{dropped.join(" ")} brings it back" unless dropped.empty?
128
- end
129
-
130
- ids.reduce(list) { |so_far, id| so_far.mark(id, status) }
131
- end
132
-
133
- # The objective's new text: the whole rest of the line, a | too.
134
- def reword(list, rest, was: nil)
135
- id, text = rest.strip.split(/\s+/, 2)
136
- entry = text && entry_of(text.gsub(/[[:space:]]+/, " ").strip)
137
- return nil unless entry
138
-
139
- id = id.upcase
140
- item = list.find(id) or return "no objective #{id}"
141
- return "#{id} has changed since this was proposed: #{item.text}" if was && Objectives.fingerprint(item) != was
142
-
143
- list.reword(id, entry[:text], done_when: entry[:done_when])
144
- end
145
-
146
- # One {text:, done_when:} per line or |-part, any whitespace collapsed
147
- # (Unicode line separators too: the frame is one line per item).
148
- # @return [Array<Hash>, nil] nil when a part has no text of its own
149
- def entries_of(rest)
150
- parts = rest.split(SEPARATOR).map { |part| part.gsub(/[[:space:]]+/, " ").strip }.reject(&:empty?)
151
- entries = parts.map { |part| entry_of(part) }
152
- entries.all? ? entries : nil
153
- end
154
-
155
- def entry_of(part)
156
- found = Objectives::DONE_WHEN.match(part)
157
- text = (found ? found[1] : part).sub(STATUS_MARK, "").strip
158
- return nil if text.empty?
159
-
160
- done_when = found && found[2]
161
- { text: text, done_when: done_when.to_s.empty? ? nil : done_when }
162
- end
163
-
164
99
  def deliver(list, session)
165
100
  result = NoteDelivery.deliver(session.id, text: list.text, source: Objectives::SOURCE,
166
101
  state_dir: @engine.session_state_dir, objectives: list.to_payload)
167
- return ["refused: #{NoteDelivery::REPL_REFUSAL}", :error] unless result.delivered?
102
+ return ["refused: #{result.refusal}", :error] unless result.delivered?
168
103
 
169
104
  @engine.announce_objectives(session)
170
105
  ["#{lines_of(list)}\n#{BETWEEN_TURNS}", :ok]
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ module Objectives
5
+ # One /objectives edit line against a list, pure: what ObjectivesCommand
6
+ # applies and what a proposal (Proposal#refusal) is checked with, so the
7
+ # objectives tool refuses exactly what its card's Apply would. Reads the
8
+ # Objectives constants at a call, so objectives.rb can load it first.
9
+ module Edit
10
+ SEPARATOR = /\n|(?:\A|\s)\|(?:\s|\z)/
11
+
12
+ module_function
13
+
14
+ # What the frame shows a status as: typed at a text's start it would pass
15
+ # for one (Item#line).
16
+ def status_mark = /\A(?:\[(?:#{Objectives::STATUSES.join("|")})\]\s*)+/i
17
+
18
+ # The verbs that set a status, and the status each sets.
19
+ def marks = { "done" => Objectives::DONE, "drop" => Objectives::DROPPED, "undo" => Objectives::ACTIVE }
20
+
21
+ # @param verb [String] set, add, edit, done, drop or undo, lower case
22
+ # @param rest [String] what follows it
23
+ # @param was [String, nil] what a reword was proposed against (Objectives.fingerprint)
24
+ # @return [List, String, nil] the new list, what's wrong with the line,
25
+ # or nil for a line that isn't one
26
+ def apply(list, verb, rest, was: nil)
27
+ case verb
28
+ when "set", "add"
29
+ entries = entries_of(rest)
30
+ return nil if entries.nil? || entries.empty?
31
+
32
+ verb == "set" ? list.set(entries) : entries.reduce(list) { |so_far, e| so_far.add(e[:text], done_when: e[:done_when]) }
33
+ when "edit" then reword(list, rest, was: was)
34
+ when *marks.keys then mark(list, marks.fetch(verb), rest)
35
+ end
36
+ end
37
+
38
+ def mark(list, status, rest)
39
+ ids = rest.upcase.split(/[\s,]+/).reject(&:empty?)
40
+ return nil if ids.empty?
41
+
42
+ missing = ids.reject { |id| list.find(id) }
43
+ return Objectives.unknown(missing, list) unless missing.empty?
44
+
45
+ if status == Objectives::DONE
46
+ dropped = ids.select { |id| list.find(id).status == Objectives::DROPPED }
47
+ return Objectives.dropped(dropped) unless dropped.empty?
48
+ end
49
+
50
+ ids.reduce(list) { |so_far, id| so_far.mark(id, status) }
51
+ end
52
+
53
+ # The objective's new text: the whole rest of the line, a | too.
54
+ def reword(list, rest, was: nil)
55
+ id, text = rest.strip.split(/\s+/, 2)
56
+ entry = text && entry_of(text.gsub(/[[:space:]]+/, " ").strip)
57
+ return nil unless entry
58
+
59
+ id = id.upcase
60
+ item = list.find(id) or return Objectives.unknown(id, list)
61
+ return "#{id} has changed since this was proposed: #{item.text}" if was && Objectives.fingerprint(item) != was
62
+
63
+ list.reword(id, entry[:text], done_when: entry[:done_when])
64
+ end
65
+
66
+ # One {text:, done_when:} per line or |-part, any whitespace collapsed
67
+ # (Unicode line separators too: the frame is one line per item).
68
+ # @return [Array<Hash>, nil] nil when a part has no text of its own
69
+ def entries_of(rest)
70
+ parts = rest.split(SEPARATOR).map { |part| part.gsub(/[[:space:]]+/, " ").strip }.reject(&:empty?)
71
+ entries = parts.map { |part| entry_of(part) }
72
+ entries.all? ? entries : nil
73
+ end
74
+
75
+ # A part's text and done-when, nil when it has no text of its own (only a
76
+ # done-when, or only status marks). A leading status mark is stripped.
77
+ def entry_of(part)
78
+ found = Objectives::DONE_WHEN.match(part)
79
+ text = (found ? found[1] : part).sub(status_mark, "").strip
80
+ return nil if text.empty?
81
+
82
+ done_when = found && found[2]
83
+ { text: text, done_when: done_when.to_s.empty? ? nil : done_when }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -62,6 +62,11 @@ module Samagotchi
62
62
 
63
63
  # Qwen3.6 thinking blocks: <think>...</think> (including empty)
64
64
  QWEN_THINK_RE = %r{<think>.*?</think>}m
65
+ # A Qwen thought cut off before it closed (no </think>), when the text
66
+ # opens with it (after any closed ones): the rest is thought, as with the
67
+ # Gemma channel. Only #strip_markup drops it; #remove_tokens (a streamed
68
+ # thought being shown) keeps it. A <think> mentioned mid-answer stays.
69
+ UNCLOSED_THINK_RE = /\A\s*<think>.*\z/m
65
70
  # Qwen tool_call XML blocks that survive when a tool call is rendered as text
66
71
  # e.g. <tool_call>\n<function=list_reminders>\n</function>\n</tool_call>
67
72
  QWEN_TOOL_CALL_RE = %r{<tool_call>.*?</tool_call>}m
@@ -93,7 +98,8 @@ module Samagotchi
93
98
  # a native Gemma step's closed tool-call blocks go with their bodies, as
94
99
  # Qwen's do (a step that only calls a tool has no text).
95
100
  def strip_markup(text)
96
- remove_tokens(text.to_s.gsub(GEMMA_TOOL_CALL_RE, "")).gsub(/\n[ \t]*\n(?:[ \t]*\n)+/, "\n\n").strip
101
+ text = text.to_s.gsub(GEMMA_TOOL_CALL_RE, "").gsub(QWEN_THINK_RE, "").sub(UNCLOSED_THINK_RE, "")
102
+ remove_tokens(text).gsub(/\n[ \t]*\n(?:[ \t]*\n)+/, "\n\n").strip
97
103
  end
98
104
 
99
105
  def remove_tokens(text)
@@ -16,6 +16,12 @@ module Samagotchi
16
16
  # (even in the owner's own process) never releases the owner's lock.
17
17
  class OwnerLock
18
18
  FILE = "owner.lock"
19
+ # Who holds a "tui" lock now: a -p --non-interactive one-shot
20
+ # (TerminalUI), or an older chi's interactive REPL while an update is
21
+ # under way (N-1). In every refusal's words (SessionManager::OwnedByTUI
22
+ # and the callers that add what they refused); no parentheses: the
23
+ # web's continueRefusal reads "<short> (why)" out of a refusal.
24
+ TUI_BUSY = "in use by a one-turn chi --non-interactive run or an older chi's terminal"
19
25
  DEFAULT_WAIT = 2.0
20
26
  RETRY_INTERVAL = 0.05
21
27
  OWNER_READ_ATTEMPTS = 10
@@ -101,7 +107,7 @@ module Samagotchi
101
107
  # Wait for a leaving worker (Owner#leaving?) to release the lock: up to
102
108
  # its leave_wait still to run plus LEAVE_MARGIN, looking every
103
109
  # RELEASE_POLL seconds. An owner that isn't leaving (a live worker, a
104
- # stuck one, a chi REPL) or no owner returns at once. A Ctrl-C (Interrupt)
110
+ # stuck one, a one-turn chi run) or no owner returns at once. A Ctrl-C (Interrupt)
105
111
  # stops the wait as it would any sleep.
106
112
  # @param on_wait [#call, nil] called once with the Owner before waiting
107
113
  # @return [Symbol] :not_leaving, :released, or :timeout (still held)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "guardrails/parent_policy"
4
+ require_relative "session"
4
5
 
5
6
  module Samagotchi
6
7
  # /parent-answers, the session's own guardrails.parent_answers
@@ -9,13 +10,13 @@ module Samagotchi
9
10
  # soft) or `default` (none of its own) sets it, saved at once, for the next
10
11
  # answer a parent gives. It may only tighten config.yml's
11
12
  # (Guardrails::ParentPolicy.effective). Tightening goes from anyone;
12
- # loosening (toward soft, or default) only from a user at a UI: the REPL,
13
- # an attached TUI, the web (+user_ui+). A parent agent's line (chi send,
13
+ # loosening (toward soft, or default) only from a user at a UI: an
14
+ # attached TUI, the web (+user_ui+). A parent agent's line (chi send,
14
15
  # a piped or marked attach, a delegate's follow-up) is refused.
15
16
  class ParentAnswersCommand
16
17
  NAME = "/parent-answers"
17
18
  USAGE = "usage: #{NAME} [hard|standard|soft|default]".freeze
18
- REFUSED = "loosening parent_answers is the user's: #{NAME} in chi --attach, the REPL or the web " \
19
+ REFUSED = "loosening parent_answers is the user's: #{NAME} in a chi terminal or the web " \
19
20
  "(not chi send or a parent agent)".freeze
20
21
 
21
22
  # What +args+ (the text after the name) asks for.
@@ -51,7 +52,7 @@ module Samagotchi
51
52
  # @return [Array(String, Boolean, Symbol)] the reply, whether the
52
53
  # session's value changed, and :ok or :error
53
54
  def run(args, user_ui:)
54
- session = @engine.session or return ["no session yet", false, :error]
55
+ session = @engine.session or return [Session::NONE_YET, false, :error]
55
56
  kind, value = self.class.parse(args)
56
57
  return [USAGE, false, :error] if kind == :usage
57
58
  return [listing(session), false, :ok] if kind == :show
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ # What a participant may learn of their review's usage, by their grant's
5
+ # exposure level (Grants::Grant#exposure; the review-composer plan's D3):
6
+ # the policy that maps a level to the usage fields the review route
7
+ # gives them (Web::ParticipantReview#review_usage). One level today:
8
+ #
9
+ # - +own+ (every grant's): their own spend and cap, their own queued
10
+ # turns and queue limit, their link's expiry, the session's model.
11
+ # Never another participant's or the owner's usage, nor the session's.
12
+ #
13
+ # A later level (a tier that also shows the session's spend, say) adds
14
+ # its row to LEVELS and its fields' sources in Web::ParticipantReview: one
15
+ # place each. A level this chi doesn't know (written by a newer chi, or
16
+ # a hand-edited store) reads as DEFAULT, the narrowest: an unknown level
17
+ # never shows more. See docs/internals/participants.md (Exposure).
18
+ module ParticipantExposure
19
+ OWN = "own"
20
+ DEFAULT = OWN
21
+ # level => the usage fields it shows, in the order the page reads them.
22
+ LEVELS = {
23
+ OWN => %i[spent cap queued queue_limit expires_at model].freeze
24
+ }.freeze
25
+
26
+ # +value+ as a level this chi knows, else DEFAULT.
27
+ def self.level(value) = LEVELS.key?(value) ? value : DEFAULT
28
+
29
+ # @return [Array<Symbol>] the fields +value+'s level shows
30
+ def self.fields(value) = LEVELS.fetch(level(value))
31
+
32
+ # The usage +value+'s level shows, each field read from +sources+
33
+ # (field => a callable): only the fields shown are computed, and a
34
+ # field a level names with no source raises (KeyError), never shows
35
+ # nothing silently.
36
+ # @param sources [Hash{Symbol => #call}]
37
+ # @return [Hash{Symbol => Object}]
38
+ def self.usage(value, sources)
39
+ fields(value).to_h { |field| [field, sources.fetch(field).call] }
40
+ end
41
+ end
42
+ end
@@ -38,7 +38,7 @@ module Samagotchi
38
38
  end
39
39
 
40
40
  # The mode a turn of the participant +entry+ (a Participants::Entry or a
41
- # Web::Grants::Grant; nil: unknown) runs under in +session+.
41
+ # Grants::Grant; nil: unknown) runs under in +session+.
42
42
  # @return [Symbol]
43
43
  def for_turn(session, entry, features: nil)
44
44
  return REVIEW unless of(session, features: features) == PAIR
@@ -5,7 +5,7 @@ module Samagotchi
5
5
  # line of at most MAX characters with no brackets (nor Unicode line
6
6
  # breaks, nor full-width square brackets), so a name can't close or forge
7
7
  # the review-contribution header (Steer.review_header). The grants store
8
- # cleans a name at issue (Web::Grants.clean_name); the header cleans it
8
+ # cleans a name at issue (Grants.clean_name); the header cleans it
9
9
  # again, whatever a session file or a sidecar holds.
10
10
  module ParticipantName
11
11
  MAX = 64
@@ -7,12 +7,12 @@ require_relative "atomic_file"
7
7
  require_relative "participant_messages"
8
8
  require_relative "archive_store"
9
9
  require_relative "session"
10
- require_relative "web/grants"
10
+ require_relative "grants"
11
11
 
12
12
  module Samagotchi
13
13
  # A session's participants: the people (or agents) its owner invited to
14
14
  # review it (`chi share`, the owner's participants routes), each through a
15
- # Web::Grants grant. Two files in the session dir, neither in the session
15
+ # Grants grant. Two files in the session dir, neither in the session
16
16
  # JSON (chi web never saves that, and a worker would overwrite a field it
17
17
  # added):
18
18
  # - participants.json, the sidecar: who is or was invited, never a token
@@ -62,7 +62,7 @@ module Samagotchi
62
62
  []
63
63
  end
64
64
 
65
- # The sidecar rewritten from +grants+ (that session's Web::Grants::Grant
65
+ # The sidecar rewritten from +grants+ (that session's Grants::Grant
66
66
  # list). Nothing is written for a session without grants, nor for one
67
67
  # whose dir is gone.
68
68
  def write(session_id, grants, state_dir: Session.default_state_dir)
@@ -79,7 +79,7 @@ module Samagotchi
79
79
  AtomicFile.write(File.join(dir, FILE), "#{JSON.pretty_generate({ "participants" => entries })}\n", perm: 0o600)
80
80
  end
81
81
 
82
- # Web::Grants' on_write for a change to +session_ids+ (nil: the sessions
82
+ # Grants' on_write for a change to +session_ids+ (nil: the sessions
83
83
  # of the grants it changed, a revoke's result): their sidecars rewritten
84
84
  # from the grants just written, under the store's lock (two writers, chi
85
85
  # web and chi share, can't finish out of order and lose a revoke here).
@@ -100,21 +100,21 @@ module Samagotchi
100
100
  end
101
101
 
102
102
  # Invites a participant to +session_id+: the grant (+grants+, a
103
- # Web::Grants), the sidecar and the audit line. A grant bound to
103
+ # Grants), the sidecar and the audit line. A grant bound to
104
104
  # +identity+ needs +public_url+ (the chi web's Web::PublicUrl) with an
105
105
  # identity header: only a request through that proxy can carry it.
106
- # @return [Web::Grants::Issued]
107
- # @raise [Web::Grants::Invalid] an archived session, or bad values
106
+ # @return [Grants::Issued]
107
+ # @raise [Grants::Invalid] an archived session, or bad values
108
108
  # (Unbindable: an identity without such a public_url)
109
109
  # @raise [ArgumentError] no such session
110
110
  def invite(grants, session_id:, name:, kind: "human", ttl: nil, cap: nil, identity: nil, public_url: nil,
111
111
  mode: nil, state_dir: Session.default_state_dir)
112
112
  raise ArgumentError, "Session not found: #{session_id}" unless File.exist?(Session.session_file(session_id, state_dir: state_dir))
113
113
 
114
- identity = Web::Grants.clean_identity(identity) unless identity.nil?
115
- raise Web::Grants::Unbindable if !identity.nil? && public_url&.identity_header.nil?
114
+ identity = Grants.clean_identity(identity) unless identity.nil?
115
+ raise Grants::Unbindable if !identity.nil? && public_url&.identity_header.nil?
116
116
  if ArchiveStore.archived?(Session.session_dir(session_id, state_dir: state_dir))
117
- raise Web::Grants::Invalid, "session #{session_id[0, 8]} is archived: unarchive it to invite anyone"
117
+ raise Grants::Invalid, "session #{session_id[0, 8]} is archived: unarchive it to invite anyone"
118
118
  end
119
119
 
120
120
  issued = grants.issue(session_id: session_id, name: name, kind: kind, ttl: ttl, cap: cap, identity: identity,
@@ -128,8 +128,8 @@ module Samagotchi
128
128
 
129
129
  # Sets participant +id+'s cap on +session_id+ (chi share cap): the
130
130
  # grant, the sidecar (the worker's check) and the audit line.
131
- # @return [Web::Grants::Grant, nil] nil when +id+ isn't an active grant there
132
- # @raise [Web::Grants::Invalid] a cap out of range
131
+ # @return [Grants::Grant, nil] nil when +id+ isn't an active grant there
132
+ # @raise [Grants::Invalid] a cap out of range
133
133
  def set_cap(grants, session_id:, id:, cap:, state_dir: Session.default_state_dir)
134
134
  grant = grants.set_cap(id, cap, session_id: session_id, on_write: sidecar_writer(state_dir, [session_id]))
135
135
  return nil unless grant
@@ -142,9 +142,9 @@ module Samagotchi
142
142
  # +session_id+.
143
143
  def no_participant(id, session_id) = "no active participant #{id} in #{session_id.to_s[0, 8]} (chi share ls lists them)"
144
144
 
145
- # Revokes grants (Web::Grants#revoke's arguments) and brings each
145
+ # Revokes grants (Grants#revoke's arguments) and brings each
146
146
  # affected session's sidecar and audit trail along.
147
- # @return [Array<Web::Grants::Grant>] the grants it revoked
147
+ # @return [Array<Grants::Grant>] the grants it revoked
148
148
  def revoke(grants, session_id: nil, id: nil, by: "owner", state_dir: Session.default_state_dir)
149
149
  revoked = grants.revoke(session_id: session_id, id: id, by: by, on_write: sidecar_writer(state_dir))
150
150
  revoked.group_by(&:session_id).each do |sid, ended|
@@ -156,15 +156,15 @@ module Samagotchi
156
156
  # Ends the grants of sessions that were archived or deleted
157
157
  # (SessionManager): +by+ "archive" or "delete". Nothing is written for a
158
158
  # session nobody was invited to.
159
- # A grants store that doesn't read (Web::Grants::Corrupt) doesn't stop
159
+ # A grants store that doesn't read (Grants::Corrupt) doesn't stop
160
160
  # the archive or delete: its links already find no grant, and the
161
161
  # store itself says so (Log.warn) until it's fixed. Nor does one a newer
162
162
  # chi wrote (Newer): chi web's gate refuses a grant whose session is
163
163
  # archived or gone even when it can't write the revoke.
164
164
  def end_sessions(session_ids, by:, state_dir: Session.default_state_dir)
165
- grants = Web::Grants.new(Web::Grants.path(state_dir: state_dir))
165
+ grants = Grants.new(Grants.path(state_dir: state_dir))
166
166
  session_ids.each { |sid| revoke(grants, session_id: sid, by: by, state_dir: state_dir) }
167
- rescue Web::Grants::Unwritable
167
+ rescue Grants::Unwritable
168
168
  nil
169
169
  end
170
170
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- # Loaded only when a sidecar is read: it brings Web::Grants (and Rack).
4
+ # Loaded only when a sidecar is read: it brings Grants.
5
5
  autoload :Participants, File.expand_path("participants", __dir__)
6
6
 
7
7
  # Who is in a session's review now, and who came or went since the last
@@ -19,8 +19,8 @@ module Samagotchi
19
19
  end
20
20
 
21
21
  # Append a message to the queue. Nil/empty strings are ignored.
22
- # +seq+ is the sender's own order number for it (the REPL numbers every
23
- # line it takes during a turn, so its leftovers keep send order across
22
+ # +seq+ is the sender's own order number for it (a sender that numbers
23
+ # every line it takes during a turn, so its leftovers keep send order across
24
24
  # this queue and its after-turn list: #drain_entries).
25
25
  def push(text, seq: nil)
26
26
  text = text.to_s
@@ -56,7 +56,8 @@ module Samagotchi
56
56
  "ctx.context.propose" => "0.140.0", "ctx.context.candidates" => "0.140.0", "ctx.context.withdraw" => "0.140.0",
57
57
  "ctx.continues" => "0.141.0", "ctx.context.left_behind" => "0.141.0", "ctx.context.attach(movable:)" => "0.141.0",
58
58
  "ctx.session_dir" => "0.143.0", "ctx.session_dir(create:)" => "0.144.0",
59
- "ctx.objectives" => "0.149.0", "ctx.turn_running?" => "0.149.0"
59
+ "ctx.objectives" => "0.149.0", "ctx.turn_running?" => "0.149.0",
60
+ "ctx.last_answer" => "0.154.0"
60
61
  }.freeze
61
62
 
62
63
  # @param bundle [String]
@@ -234,7 +235,7 @@ module Samagotchi
234
235
  # says how to stop it. It starts on first svc.value, or now with
235
236
  # eager: true (a raise then fails the plugin's load, unless the
236
237
  # plugin rescues it). The Engine stops its services when it shuts
237
- # down (the REPL or the session's worker exits), newest first.
238
+ # down (the session's worker or a one-shot exits), newest first.
238
239
  # @param name [String, Symbol] unique in the plugin
239
240
  # @return [Service]
240
241
  def service(name, eager: false, &block)
@@ -18,6 +18,9 @@ module Samagotchi
18
18
  autoload :PluginSessionState, File.expand_path("../plugin_session_state", __dir__)
19
19
  # Context#objectives reads it; the kit shouldn't carry its files.
20
20
  autoload :Objectives, File.expand_path("../objectives", __dir__)
21
+ # Context#last_answer reads them.
22
+ autoload :AnswerTail, File.expand_path("../answer_tail", __dir__)
23
+ autoload :AnswerText, File.expand_path("../answer_text", __dir__)
21
24
 
22
25
  module Plugin
23
26
  # Loaded on first ctx.sessions / ctx.context (or a bundle naming
@@ -50,7 +53,7 @@ module Samagotchi
50
53
  # model's memory overlay key (ctx.model, ctx.model_key).
51
54
  # +steer+ takes (text, label), +stop_turn+ and +stop_generation+
52
55
  # (reason, label), each true when it acted on a running turn.
53
- # +frontend+ returns the Engine's interface (:repl, :worker,
56
+ # +frontend+ returns the Engine's interface (:worker or
54
57
  # :non_interactive), which ctx.frontend names for plugins.
55
58
  # +toolkit+ returns the session's toolkit name, +declared_tools+ the
56
59
  # tool names its next turn declares, +use_toolkit+ takes a name and
@@ -260,12 +263,12 @@ module Samagotchi
260
263
  def scratch? = !!@host.scratch&.call
261
264
 
262
265
  # ctx.frontend's values, by the Engine's interface.
263
- FRONTENDS = { repl: :repl, non_interactive: :one_shot, worker: :worker }.freeze
266
+ FRONTENDS = { non_interactive: :one_shot, worker: :worker }.freeze
264
267
 
265
268
  # What runs the session, read now (the host sets it after the plugins
266
- # load): :repl (the REPL, -p without --non-interactive), :one_shot
267
- # (-p --non-interactive: one turn, then exit) or :worker (a session
268
- # worker: the web, an attached TUI). A host without one is :worker.
269
+ # load): :one_shot (-p --non-interactive: one turn, then exit) or
270
+ # :worker (a session worker: the web, an attached TUI). A host without
271
+ # one is :worker.
269
272
  # @return [Symbol]
270
273
  def frontend = FRONTENDS.fetch(@host.frontend&.call, :worker)
271
274
 
@@ -316,19 +319,30 @@ module Samagotchi
316
319
  # The conversation so far, a frozen copy, without the system prompt.
317
320
  # While a turn runs, a session worker's (attached TUI, web) adds the
318
321
  # turn so far: its prompt, the model's text and the lines merged into
319
- # it; the REPL's is the conversation before that turn
322
+ # it; a one-shot's is the conversation before that turn
320
323
  # (#messages_partial?).
321
324
  # @return [Array<Hash>]
322
325
  def messages
323
326
  Array(@host.messages.call).map { |message| message.dup.freeze }.freeze
324
327
  end
325
328
 
329
+ # The newest answer's text as the user saw it (AnswerText.visible): the
330
+ # newest bubble of #messages, its thoughts (a cut-off one too) and call
331
+ # markup out. Mid-turn a worker's can be the running turn's interim text.
332
+ # @return [String, nil] nil when there is none, or it was a thought alone
333
+ def last_answer
334
+ found = AnswerTail.find(messages)
335
+ content = found && (found[:content] || found["content"])
336
+ text = content.is_a?(String) ? AnswerText.visible(content) : ""
337
+ text.empty? ? nil : text
338
+ end
339
+
326
340
  # Whether a turn runs now. Its :after_turn hooks run inside it, so a thread
327
341
  # one of them starts can use this to wait for the end: a card shown before
328
342
  # then belongs to that turn's block.
329
343
  def turn_running? = !!@host.turn_running&.call
330
344
 
331
- # Whether #messages leaves out a running turn (the REPL mid-turn), so
345
+ # Whether #messages leaves out a running turn (a one-shot mid-turn), so
332
346
  # a plugin can say what its answer is about.
333
347
  def messages_partial? = !!@host.messages_partial&.call
334
348
 
@@ -6,6 +6,7 @@ require "open3"
6
6
  require "tempfile"
7
7
  require "time"
8
8
  require_relative "../paths"
9
+ require_relative "../session"
9
10
 
10
11
  module Samagotchi
11
12
  module Plugin
@@ -149,7 +150,7 @@ module Samagotchi
149
150
  def pair_again(ctx)
150
151
  peer = self.class
151
152
  previous = ctx.continues or return "not a continue"
152
- id = ctx.session_id or return "no session yet"
153
+ id = ctx.session_id or return Session::NONE_YET
153
154
  left = ctx.context.left_behind.find { |row| row.name == peer::INBOX }
154
155
  hint = left&.hint.to_s.strip
155
156
  return "no #{peer::INBOX} left behind" if hint.empty?
@@ -110,7 +110,7 @@ module Samagotchi
110
110
  # @param delivery [Symbol, String, nil] nil or :next_step (default),
111
111
  # :queue
112
112
  # @return [String] the id of the session it went to
113
- # @raise [Error] no such session, a REPL owns it, it wasn't taken
113
+ # @raise [Error] no such session, a one-shot owns it, it wasn't taken
114
114
  # (a worker older than queueing refuses a queue), or delivery is
115
115
  # :cut or unknown
116
116
  def send(id, text, delivery: nil)
@@ -128,8 +128,10 @@ module Samagotchi
128
128
  when :timeout then raise Error, "session #{sid[0, 8]}'s worker did not answer in time; the message was not sent"
129
129
  else raise Error, "the message to session #{sid[0, 8]} could not be written"
130
130
  end
131
+ rescue SessionManager::ScratchSession => e
132
+ raise Error, "session #{sid[0, 8]}: #{e.message}"
131
133
  rescue SessionManager::OwnedByTUI
132
- raise Error, "session #{sid[0, 8]} is open in a chi REPL, which takes no messages from others"
134
+ raise Error, "session #{sid[0, 8]} is #{OwnerLock::TUI_BUSY}, which takes no messages from others"
133
135
  end
134
136
 
135
137
  # The wire value for send's delivery: (nil for the default).
@@ -182,7 +184,7 @@ module Samagotchi
182
184
  # its worker to let go.
183
185
  # @return [String] the child's id
184
186
  # @raise [Error] not a child of this session, no such session, or a
185
- # REPL owns it
187
+ # one-shot owns it
186
188
  def stop(id)
187
189
  state_dir = self.state_dir
188
190
  sid = resolve(id, state_dir)
@@ -194,7 +196,7 @@ module Samagotchi
194
196
  SessionManager.stop_session(sid, state_dir: state_dir, wait: STOP_WAIT)
195
197
  sid
196
198
  rescue SessionManager::OwnedByTUI
197
- raise Error, "session #{sid[0, 8]} is open in a chi REPL; stop it there"
199
+ raise Error, "session #{sid[0, 8]} is #{OwnerLock::TUI_BUSY}; let it finish"
198
200
  end
199
201
 
200
202
  private
@@ -70,8 +70,7 @@ module Samagotchi
70
70
  end
71
71
 
72
72
  # Start the init tasks not started yet, each on its own thread. The
73
- # worker calls it once its Bridge is up, the REPL once it renders
74
- # events, and every turn (a -p run has only that); later calls start
73
+ # worker calls it once its Bridge is up, and every turn (a -p run has only that); later calls start
75
74
  # nothing new.
76
75
  def start!
77
76
  @mutex.synchronize do