samagotchi 0.153.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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -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 +80 -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 +3 -3
  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 +1 -1
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-push.md +1 -1
  25. data/docs/memory.md +2 -2
  26. data/docs/plugins.md +23 -28
  27. data/docs/sessions.md +24 -24
  28. data/docs/sub-agent.md +2 -2
  29. data/docs/system-one.md +1 -1
  30. data/docs/testing.md +16 -6
  31. data/lib/samagotchi/answer_text.rb +34 -0
  32. data/lib/samagotchi/bridge.rb +43 -32
  33. data/lib/samagotchi/bridge_client.rb +10 -9
  34. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  35. data/lib/samagotchi/broadcast/triage.rb +1 -1
  36. data/lib/samagotchi/broadcast_command.rb +5 -5
  37. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  38. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  39. data/lib/samagotchi/bundle_command.rb +2 -2
  40. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  41. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  42. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  44. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  45. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  46. data/lib/samagotchi/child_move.rb +3 -3
  47. data/lib/samagotchi/commands/registry.rb +6 -6
  48. data/lib/samagotchi/composer_route.rb +4 -5
  49. data/lib/samagotchi/config.rb +14 -12
  50. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  51. data/lib/samagotchi/context_candidates.rb +4 -3
  52. data/lib/samagotchi/continue_offer.rb +1 -1
  53. data/lib/samagotchi/debug_log.rb +1 -1
  54. data/lib/samagotchi/degrade.rb +75 -0
  55. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  56. data/lib/samagotchi/engine/cards.rb +87 -0
  57. data/lib/samagotchi/engine/context_judge.rb +138 -0
  58. data/lib/samagotchi/engine/events.rb +131 -0
  59. data/lib/samagotchi/engine/guardrails.rb +87 -0
  60. data/lib/samagotchi/engine/hooks.rb +119 -0
  61. data/lib/samagotchi/engine/messages.rb +124 -0
  62. data/lib/samagotchi/engine/model_settings.rb +127 -0
  63. data/lib/samagotchi/engine/models.rb +261 -0
  64. data/lib/samagotchi/engine/objectives.rb +116 -0
  65. data/lib/samagotchi/engine/participants.rb +86 -0
  66. data/lib/samagotchi/engine/plugins.rb +208 -0
  67. data/lib/samagotchi/engine/questions.rb +98 -0
  68. data/lib/samagotchi/engine/recap.rb +130 -0
  69. data/lib/samagotchi/engine/session.rb +131 -0
  70. data/lib/samagotchi/engine/side_requests.rb +135 -0
  71. data/lib/samagotchi/engine/steering.rb +134 -0
  72. data/lib/samagotchi/engine/toolkit.rb +269 -0
  73. data/lib/samagotchi/engine/turn_end.rb +180 -0
  74. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  75. data/lib/samagotchi/engine/verify.rb +153 -0
  76. data/lib/samagotchi/engine.rb +62 -2646
  77. data/lib/samagotchi/grants.rb +575 -0
  78. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  79. data/lib/samagotchi/guardrails/context.rb +2 -3
  80. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  81. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  82. data/lib/samagotchi/hook_routes.rb +3 -3
  83. data/lib/samagotchi/idle_recap.rb +4 -5
  84. data/lib/samagotchi/idle_reminders.rb +3 -4
  85. data/lib/samagotchi/iteration_limit.rb +2 -2
  86. data/lib/samagotchi/kernel_loop.rb +10 -11
  87. data/lib/samagotchi/launch_mode.rb +13 -32
  88. data/lib/samagotchi/level_setting_command.rb +3 -2
  89. data/lib/samagotchi/llm_context_command.rb +3 -2
  90. data/lib/samagotchi/log.rb +7 -7
  91. data/lib/samagotchi/log_line.rb +1 -1
  92. data/lib/samagotchi/log_path.rb +1 -1
  93. data/lib/samagotchi/note_command.rb +1 -1
  94. data/lib/samagotchi/note_delivery.rb +18 -8
  95. data/lib/samagotchi/objectives.rb +24 -3
  96. data/lib/samagotchi/objectives_check.rb +8 -27
  97. data/lib/samagotchi/objectives_command.rb +4 -69
  98. data/lib/samagotchi/objectives_edit.rb +87 -0
  99. data/lib/samagotchi/output_formatter.rb +7 -1
  100. data/lib/samagotchi/owner_lock.rb +7 -1
  101. data/lib/samagotchi/parent_answers_command.rb +5 -4
  102. data/lib/samagotchi/participant_exposure.rb +42 -0
  103. data/lib/samagotchi/participant_mode.rb +1 -1
  104. data/lib/samagotchi/participant_name.rb +1 -1
  105. data/lib/samagotchi/participants.rb +17 -17
  106. data/lib/samagotchi/participants_watch.rb +1 -1
  107. data/lib/samagotchi/pending_input_queue.rb +2 -2
  108. data/lib/samagotchi/plugin/api.rb +3 -2
  109. data/lib/samagotchi/plugin/context.rb +21 -7
  110. data/lib/samagotchi/plugin/peer.rb +2 -1
  111. data/lib/samagotchi/plugin/sessions.rb +6 -4
  112. data/lib/samagotchi/plugin_tasks.rb +1 -2
  113. data/lib/samagotchi/private_files.rb +29 -0
  114. data/lib/samagotchi/project_env.rb +2 -2
  115. data/lib/samagotchi/question_desk.rb +8 -7
  116. data/lib/samagotchi/recap_command.rb +2 -1
  117. data/lib/samagotchi/recap_setup.rb +1 -1
  118. data/lib/samagotchi/recap_store.rb +1 -1
  119. data/lib/samagotchi/redact.rb +86 -0
  120. data/lib/samagotchi/reminder_queue.rb +2 -2
  121. data/lib/samagotchi/remote_attach_command.rb +1 -1
  122. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  123. data/lib/samagotchi/review_start.rb +1 -1
  124. data/lib/samagotchi/run_options.rb +6 -6
  125. data/lib/samagotchi/self_report.rb +42 -39
  126. data/lib/samagotchi/send_command/busy.rb +1 -1
  127. data/lib/samagotchi/send_command.rb +4 -1
  128. data/lib/samagotchi/session.rb +11 -2
  129. data/lib/samagotchi/session_access/contract.rb +3 -3
  130. data/lib/samagotchi/session_access/local.rb +9 -9
  131. data/lib/samagotchi/session_access/remote.rb +17 -7
  132. data/lib/samagotchi/session_access.rb +4 -4
  133. data/lib/samagotchi/session_archive_command.rb +2 -2
  134. data/lib/samagotchi/session_chain.rb +8 -6
  135. data/lib/samagotchi/session_commands.rb +45 -48
  136. data/lib/samagotchi/session_continue.rb +1 -1
  137. data/lib/samagotchi/session_delete_command.rb +4 -4
  138. data/lib/samagotchi/session_manager.rb +59 -36
  139. data/lib/samagotchi/session_metrics.rb +2 -2
  140. data/lib/samagotchi/session_model_settings.rb +3 -2
  141. data/lib/samagotchi/session_participants.rb +1 -1
  142. data/lib/samagotchi/session_retention.rb +3 -3
  143. data/lib/samagotchi/sessions_command.rb +4 -4
  144. data/lib/samagotchi/share_command.rb +10 -10
  145. data/lib/samagotchi/steer.rb +1 -1
  146. data/lib/samagotchi/system_prompt.rb +102 -109
  147. data/lib/samagotchi/task_watch.rb +1 -1
  148. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  149. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  150. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  151. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  152. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  153. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  154. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  155. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  156. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  157. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  158. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  159. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  160. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  161. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  163. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  164. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  165. data/lib/samagotchi/terminal_ui.rb +142 -1387
  166. data/lib/samagotchi/title_command.rb +2 -2
  167. data/lib/samagotchi/tool_runner.rb +14 -16
  168. data/lib/samagotchi/toolkit_command.rb +2 -1
  169. data/lib/samagotchi/tools/delegate.rb +3 -1
  170. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  171. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  172. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  173. data/lib/samagotchi/tools/send_note.rb +2 -1
  174. data/lib/samagotchi/turn_endings.rb +2 -2
  175. data/lib/samagotchi/turn_flow.rb +4 -5
  176. data/lib/samagotchi/update_hint.rb +2 -2
  177. data/lib/samagotchi/version.rb +1 -1
  178. data/lib/samagotchi/web/app.rb +39 -50
  179. data/lib/samagotchi/web/participant_access.rb +2 -2
  180. data/lib/samagotchi/web/participant_review.rb +5 -5
  181. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  182. data/lib/samagotchi/web/participant_view.rb +2 -82
  183. data/lib/samagotchi/web/participants_admin.rb +1 -1
  184. data/lib/samagotchi/web/public/block_host.js +35 -8
  185. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  186. data/lib/samagotchi/web/public/chains.js +2 -2
  187. data/lib/samagotchi/web/public/claim_link.js +4 -4
  188. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  189. data/lib/samagotchi/web/public/format.js +3 -3
  190. data/lib/samagotchi/web/push/store.rb +1 -1
  191. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  192. data/lib/samagotchi/web/session_control.rb +12 -8
  193. data/lib/samagotchi/web/session_create.rb +1 -1
  194. data/lib/samagotchi/web/session_hub.rb +2 -2
  195. data/lib/samagotchi/web/session_show.rb +2 -2
  196. data/lib/samagotchi/web/session_summary.rb +1 -1
  197. data/lib/samagotchi/worker.rb +100 -9
  198. data/lib/samagotchi/worker_link.rb +3 -3
  199. data/lib/samagotchi/working_set_command.rb +3 -2
  200. metadata +29 -6
  201. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  202. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  203. data/lib/samagotchi/web/grants.rb +0 -575
  204. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  205. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -12,10 +12,11 @@ module Samagotchi
12
12
  # Input composition and Ctrl-C handling for the attached loop.
13
13
  # Reads @client, @clock, @command_registry, @continue_offer, @exit_ask,
14
14
  # @gaps, @link, @prefilled, @question, @queue, @queued_ids, @reader,
15
- # @running, @screen; writes @prefilled, @last_idle_interrupt_at.
15
+ # @running, @scratch, @screen; writes @prefilled, @last_idle_interrupt_at.
16
16
  # Calls the loop helpers #answer_continue, #answer_exit_ask, #answer_question,
17
17
  # #archive_after_turn, #command_registry, #detach, #echo_answer, #exit_and_archive,
18
- # #exit_and_delete, #exit_worker, #gap_divider, #participant_commands, #persist_recent_history,
18
+ # #end_scratch, #exit_and_delete, #exit_worker, #refuse_scratch_archive, #gap_divider,
19
+ # #participant_commands, #persist_recent_history,
19
20
  # #queue_input_prefill, #read_prompt_line, #send_command, #send_prompt,
20
21
  # #show_recap, #show_stats, #submit_delivery, #unqueue_newest, #via, and
21
22
  # #worker_down.
@@ -46,6 +47,7 @@ module Samagotchi
46
47
  return answer_exit_ask(line)
47
48
  end
48
49
 
50
+ return end_scratch if line.nil? && @scratch
49
51
  return detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if line.nil?
50
52
 
51
53
  text = line.strip
@@ -104,11 +106,12 @@ module Samagotchi
104
106
  route = ComposerRoute.call(text, running: @running, registry: command_registry)
105
107
  return submit_delivery(route) if route.choice
106
108
 
107
- # The terminal's own commands (SessionCommands registers them, the
108
- # REPL reads the same words). Before the continue offer: the
109
+ # The terminal's own commands (SessionCommands registers them). Before the continue offer: the
109
110
  # leaving ones are no answer to it.
110
111
  local = route.local
111
112
  return submit(nil) if local == :detach
113
+ return end_scratch if local == :exit && @scratch
114
+ return refuse_scratch_archive if local == :archive && @scratch
112
115
  return SessionCommands.delete_on_exit?(text) ? exit_and_delete : exit_worker if local == :exit
113
116
  return archive_after_turn if local == :archive && (@running || !@queued_ids.empty?)
114
117
  return exit_and_archive if local == :archive
@@ -130,7 +133,7 @@ module Samagotchi
130
133
  elsif %i[share participants].include?(local)
131
134
  @screen.commit(participant_commands.run(text))
132
135
  elsif route.kind == :command
133
- # The history keeps !cmds, as the REPL's does (prompts: #send_prompt).
136
+ # The history keeps !cmds (prompts: #send_prompt).
134
137
  persist_recent_history(text) if PromptHistory.shell_line?(text)
135
138
  send_command(text)
136
139
  elsif route.kind == :typo
@@ -142,7 +145,7 @@ module Samagotchi
142
145
  nil
143
146
  end
144
147
 
145
- # One read on the reader thread: the REPL's multiline read with Tab
148
+ # One read on the reader thread: a multiline read with Tab
146
149
  # completion at the main prompt, a plain line for a question's choice
147
150
  # or the continue offer's answer.
148
151
  def read_input_line(prompt, prefill)
@@ -169,7 +172,7 @@ module Samagotchi
169
172
 
170
173
  # The session's commands as its worker's snapshot names them (its
171
174
  # plugins' too); the built-ins until one arrives, or from a worker too
172
- # old to name them. An unknown /foo is a prompt, as in the REPL.
175
+ # old to name them. An unknown /foo is a prompt.
173
176
  def command_registry = @command_registry || SessionCommands.builtin_registry
174
177
 
175
178
  # Ctrl-C cancels the running turn (whoever started it). At an idle
@@ -197,13 +200,13 @@ module Samagotchi
197
200
  return submit(nil) if @last_idle_interrupt_at && now - @last_idle_interrupt_at <= DETACH_WINDOW
198
201
 
199
202
  @last_idle_interrupt_at = now
200
- @screen.commit(DETACH_HINT)
203
+ @screen.commit(@scratch ? SCRATCH_DETACH_HINT : DETACH_HINT)
201
204
  nil
202
205
  end
203
206
 
204
207
  # On the reader thread, from Reline's Ctrl-C: what the line held, and
205
208
  # when. During a turn the press goes to the loop (it cancels the turn)
206
- # and the read goes on with the typed text still in it, as in the REPL;
209
+ # and the read goes on with the typed text still in it;
207
210
  # otherwise it is noted for the Interrupt the LineReader queues next.
208
211
  # @return [Boolean] true: handled, the read goes on; false: Reline ends
209
212
  # the read with Interrupt
@@ -19,6 +19,22 @@ module Samagotchi
19
19
  :detach
20
20
  end
21
21
 
22
+ # A scratch session's way out (Ctrl-D, /detach, /exit, a second
23
+ # Ctrl-C): the loop ends with no line; its AttachLauncher stops the
24
+ # worker and deletes the session, and says so.
25
+ # @return [Symbol] :detach
26
+ def end_scratch
27
+ @view.finish_thinking_spinner
28
+ :detach
29
+ end
30
+
31
+ # /archive in a scratch session: nothing to archive (ArchiveRefused's words).
32
+ # @return [nil]
33
+ def refuse_scratch_archive
34
+ @screen.commit(SessionManager::ArchiveRefused.new(@client.session_id, :scratch).message)
35
+ nil
36
+ end
37
+
22
38
  # Detach, and ask the worker to exit now. The worker decides: it stays
23
39
  # up while anything still needs it (a turn, queued input, a continue
24
40
  # offer, another UI, reminders), and the line says which. When the only
@@ -27,8 +27,7 @@ module Samagotchi
27
27
  nil
28
28
  end
29
29
 
30
- # A bare answer at the continue prompt, as the REPL reads it (an empty
31
- # one is yes).
30
+ # A bare answer at the continue prompt (an empty one is yes).
32
31
  def continue_line(text)
33
32
  text.empty? ? SessionCommands::CONTINUE_COMMAND : "#{SessionCommands::CONTINUE_COMMAND} #{text}"
34
33
  end
@@ -149,7 +148,7 @@ module Samagotchi
149
148
  nil
150
149
  end
151
150
 
152
- # An empty answer dismisses the question, as in the REPL: the tool
151
+ # An empty answer dismisses the question: the tool
153
152
  # returns unanswered (an approval: denied) and the turn goes on. The
154
153
  # :question_cancelled every UI gets finds it already closed here.
155
154
  def dismiss_question
@@ -52,7 +52,7 @@ module Samagotchi
52
52
  # #handle_event turns the worker's event stream into terminal output:
53
53
  # the snapshot a join starts with (the last exchange, queued prompts,
54
54
  # the turn so far), prompts other clients sent, and each turn through the
55
- # same EventRenderer the local REPL uses, drawn by an AttachedView.
55
+ # EventRenderer, drawn by an AttachedView.
56
56
  class AttachedLoop
57
57
  include Formatting
58
58
  include InputSupport
@@ -63,9 +63,14 @@ module Samagotchi
63
63
  include Sending
64
64
 
65
65
  PROMPT = "> "
66
+ # The prompt while a question or a continue offer waits for its answer
67
+ # (its choices are in the notes slot).
68
+ QUESTION_PROMPT = "? "
66
69
  # A second Ctrl-C at an empty idle prompt within this many seconds detaches.
67
70
  DETACH_WINDOW = 2.0
68
71
  DETACH_HINT = "Ctrl-D to detach, /exit stops the worker"
72
+ # A `chi scratch` session's: every way out ends it.
73
+ SCRATCH_DETACH_HINT = "Ctrl-D or /exit ends the scratch session (nothing is kept)"
69
74
  attr_reader :client_id
70
75
 
71
76
  # @return [String, nil] the model the worker's turns run on, as the
@@ -92,7 +97,7 @@ module Samagotchi
92
97
  # through, the launch stops
93
98
  # @param no_interrupt [Boolean] post every turn with no_interrupt
94
99
  # @param default_input [Boolean] type SAMAGOTCHI_DEFAULT_INPUT into
95
- # the first read (a new session with no -p, as the REPL)
100
+ # the first read (a new session with no -p)
96
101
  # @param clock [#call] monotonic seconds (the Ctrl-C detach window)
97
102
  # @param now [#call] the wall clock, a Time (the gap dividers)
98
103
  # @param delete_session [#call] session id -> deletes it (/exit --delete)
@@ -107,8 +112,11 @@ module Samagotchi
107
112
  # session folder (@path images, saved records) is under it
108
113
  # @param link [AttachLink, nil] where the session is, instead of
109
114
  # +client+ and the keywords that make a LocalLink
115
+ # @param scratch [Boolean] a `chi scratch` session: Ctrl-D, /detach,
116
+ # /exit and a second Ctrl-C all end it (its AttachLauncher deletes
117
+ # it), /archive is refused, and typed lines stay out of the history
110
118
  def initialize(screen:, client_id:, client: nil, first_prompt: nil, first_command: nil, no_interrupt: false,
111
- default_input: false, wait_at_eof: false, as_parent: false,
119
+ default_input: false, wait_at_eof: false, as_parent: false, scratch: false,
112
120
  clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, now: -> { MessageTime.now },
113
121
  delete_session: ->(id) { SessionManager.delete_session(id, stop: true, wait: LocalLink::DELETE_WAIT) },
114
122
  archive_session: ->(id) { SessionManager.archive_session(id, wait: LocalLink::DELETE_WAIT) },
@@ -119,6 +127,7 @@ module Samagotchi
119
127
  archive_session: archive_session, installed_version: installed_version,
120
128
  children_counts: children_counts)
121
129
  @client = @link.client
130
+ @scratch = scratch
122
131
  @screen = screen
123
132
  @client_id = client_id
124
133
  # A parent agent drives this attach (AttachLauncher:
@@ -285,7 +294,7 @@ module Samagotchi
285
294
  @gaps.touch
286
295
  refresh_children
287
296
  own_turn_ended(event)
288
- # The renderer says how it ended (the REPL's words too).
297
+ # The renderer says how it ended (a one-shot's words too).
289
298
  when :turn_canceled, :turn_failed
290
299
  @status.take_event(event)
291
300
  @renderer.call(event)
@@ -413,7 +422,7 @@ module Samagotchi
413
422
 
414
423
  # Here a stopped session has always read as unreachable.
415
424
  reason = "unreachable" if reason == "stopped"
416
- "Lost the session's worker (#{reason}). Resume it with: #{via.command("--shared --resume #{id}")}"
425
+ "Lost the session's worker (#{reason}). Resume it with: #{via.command("--resume #{id}")}"
417
426
  end
418
427
 
419
428
  # Each reason in its own words (the remote's name, its chi web's
@@ -478,7 +487,7 @@ module Samagotchi
478
487
  end
479
488
 
480
489
  # The saved recap, and a new one asked for at once (it arrives as
481
- # :recap_ready), with the REPL's words.
490
+ # :recap_ready), in Formatting's /recap words.
482
491
  def show_recap
483
492
  reply = @client.request_recap
484
493
  answer = reply.status == 200 ? reply.json : nil
@@ -504,11 +513,11 @@ module Samagotchi
504
513
  end
505
514
 
506
515
  # The worker rolled a failed turn back and handed its prompt back: ours
507
- # goes back into the input, as the REPL restores it for a retry.
516
+ # goes back into the input for a retry.
508
517
  def restore_prompt(event)
509
518
  origin = origin_of(event[:origin])
510
519
  return unless own?(origin.client_id) && @own_turns.sent?(origin.enqueued_id)
511
- # Lines from a pipe: nobody to retry it (the REPL's rule).
520
+ # Lines from a pipe: nobody to retry it.
512
521
  return if @wait_at_eof
513
522
 
514
523
  if @reader&.prefill(event[:prompt].to_s)
@@ -10,7 +10,7 @@ require_relative "thinking_line"
10
10
 
11
11
  module Samagotchi
12
12
  class TerminalUI
13
- # EventRenderer's view in both TUIs (attached mode and the REPL), drawn
13
+ # EventRenderer's view in the attached TUI, drawn
14
14
  # on a Screen: the thinking feedback is the activity slot, one row right
15
15
  # above the prompt (spinner frame, then the running tool or the newest
16
16
  # sentence of the model's thinking or text, ThinkingLine), and every
@@ -60,8 +60,7 @@ module Samagotchi
60
60
  @screen.commit(text)
61
61
  end
62
62
 
63
- # Draw on +surface+ from now on (the REPL swaps in its live region
64
- # after the view is built, and back when it ends): what the slot shows
63
+ # Draw on +surface+ from now on (no caller since the REPL went): what the slot shows
65
64
  # moves there.
66
65
  def surface=(surface)
67
66
  @lock.synchronize do
@@ -8,9 +8,8 @@ module Samagotchi
8
8
  # Renders one UI's view of Engine turn events: the thinking spinner while
9
9
  # the model generates, a `tool>` line per completed tool call, and the
10
10
  # answer (with any tool activity not already shown) from :turn_completed's
11
- # turn_summary, and how a turn ended. It is the `on_event:` sink the REPL
12
- # hands Engine#run_turn, and renders what attached mode gets over the
13
- # Bridge.
11
+ # turn_summary, and how a turn ended. It renders what attached mode gets
12
+ # over the Bridge.
14
13
  #
15
14
  # Drawing is delegated to a +view+ (an AttachedView in both TUIs). The
16
15
  # renderer keeps
@@ -158,8 +157,7 @@ module Samagotchi
158
157
  end
159
158
 
160
159
  # Start a turn's bookkeeping and reset the view's per-turn feedback.
161
- # Also called directly by REPL paths that drive the kernel without
162
- # Engine#run_turn (so no :turn_started).
160
+ # Also called from #call on a :turn_started.
163
161
  def begin_turn
164
162
  @streamed_tool_activity = Hash.new(0)
165
163
  @tool_started_at = {}
@@ -17,14 +17,14 @@ require_relative "model_segments_text"
17
17
 
18
18
  module Samagotchi
19
19
  class TerminalUI
20
- # Line formatting shared by the REPL and the attached view: colour, the
20
+ # Line formatting shared by the attached view and a one-shot: colour, the
21
21
  # `tool>` line, elapsed durations. Pure apart from reading whether
22
22
  # $stdout is a colour terminal.
23
23
  module Formatting
24
24
  include ModelSegmentsText
25
25
 
26
- # The /stats text for a metrics snapshot, for the REPL and attached
27
- # views (an instance includes the module) and for `chi sessions stats`
26
+ # The /stats text for a metrics snapshot, for the attached view (an
27
+ # instance includes the module) and for `chi sessions stats`
28
28
  # (no UI: the same text from the CLI).
29
29
  # @return [String] "(no metrics yet)" without a snapshot
30
30
  def self.format_session_metrics(snapshot)
@@ -420,7 +420,7 @@ module Samagotchi
420
420
  text.strip.empty? ? [] : lines
421
421
  end
422
422
 
423
- # ── Turn ends: one set of words for the REPL, attached mode and the
423
+ # ── Turn ends: one set of words for a one-shot, attached mode and the
424
424
  # web (timing.js cancelLineText, format.js failedTurnText) ──────────
425
425
 
426
426
  # A cancel's reason as the UIs name it (anything else as it is);
@@ -552,7 +552,7 @@ module Samagotchi
552
552
  # Off by the session's own recap.enabled (/recap off, --set recap.enabled=false).
553
553
  RECAP_SESSION_OFF_TEXT = "recap is off for this session (/recap on turns it on, /recap default goes back to config.yml)"
554
554
 
555
- # What /recap says (the REPL's words, attached mode's too): the saved
555
+ # What /recap says (attached mode's words): the saved
556
556
  # recap, then what came of asking for a new one.
557
557
  # @param saved [Hash, nil] {text:, turns_since:}
558
558
  # @param request [Symbol, String, nil] IdleRecap#request_now's answer
@@ -573,7 +573,7 @@ module Samagotchi
573
573
  lines.compact.join("\n")
574
574
  end
575
575
 
576
- # ── The status line (the REPL's, and attached mode's idle one) ────────
576
+ # ── The status line (attached mode's idle one) ───────────────────────
577
577
 
578
578
  # status.line: off hides it.
579
579
  STATUS_LINE_OFF = "off"
@@ -662,7 +662,7 @@ module Samagotchi
662
662
  end
663
663
 
664
664
  # Render the analytics snapshot as a compact, user-facing report. Raw event
665
- # logs (debug-only) are intentionally excluded; this surface is for the REPL
665
+ # logs (debug-only) are intentionally excluded; this surface is for /stats
666
666
  # and `chi sessions stats` (the module function above).
667
667
  def format_session_metrics(snapshot)
668
668
  return "(no metrics yet)" unless snapshot.is_a?(Hash)
@@ -14,7 +14,7 @@ require_relative "reline_history_hook"
14
14
 
15
15
  module Samagotchi
16
16
  class TerminalUI
17
- # Input the REPL and the attached TUI share: the persistent prompt
17
+ # The attached TUI's input: the persistent prompt
18
18
  # history, Tab completion (/commands, @path in the cwd, #memory, which
19
19
  # inserts #name and the model reads it as typed), multiline reads at the
20
20
  # main prompt, and a one-shot prefill of the next read (the default
@@ -29,12 +29,6 @@ module Samagotchi
29
29
 
30
30
  private
31
31
 
32
- # The /commands Tab offers: the session's registry, the REPL's own
33
- # among them.
34
- def slash_commands = command_registry.completions(:repl)
35
-
36
- def command_registry = @commands&.registry || SessionCommands.builtin_registry
37
-
38
32
  # Whether a new session's first read gets the default input
39
33
  # (SAMAGOTCHI_DEFAULT_INPUT); --no-default-input says no.
40
34
  def default_input_wanted? = !@no_default_input
@@ -6,8 +6,8 @@ module Samagotchi
6
6
  class TerminalUI
7
7
  # Reads input lines on its own thread, so output keeps rendering while
8
8
  # the user types, and hands each line (nil = Ctrl-D) or Ctrl-C to a sink
9
- # (anything with #<<, such as a Thread::Queue); the attached loop and the
10
- # REPL both read this way. #reprompt makes it drop the open read and start
9
+ # (anything with #<<, such as a Thread::Queue); the attached loop reads
10
+ # this way. #reprompt makes it drop the open read and start
11
11
  # again with the current prompt (a question opened or closed); #prefill
12
12
  # starts it again with text already typed in (a failed prompt).
13
13
  class LineReader
@@ -7,8 +7,8 @@ require_relative "reline_seam"
7
7
  module Samagotchi
8
8
  class TerminalUI
9
9
  # Opens and closes a live region: a Screen with Reline drawing into it
10
- # through RelineSeam. The REPL and attached mode both use one when the
11
- # terminal can show it, and fall back to their own plain output otherwise.
10
+ # through RelineSeam. Attached mode uses one when the terminal can show
11
+ # it, and falls back to a PlainSurface otherwise.
12
12
  module LiveRegion
13
13
  module_function
14
14
 
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../log"
4
+ require_relative "../log_line"
5
+ require_relative "../degrade"
6
+
7
+ module Samagotchi
8
+ class TerminalUI
9
+ # `chi -v` attached: the debug log's new records of one session (its
10
+ # sid), followed from where the file ends as it starts, each handed to
11
+ # +on_record+ (AttachLauncher commits it above the live region). The
12
+ # worker writes to the file this chi spawned it with
13
+ # (SAMAGOTCHI_LOG_FILE); a rotation (DebugLog renames the file to .1) is
14
+ # seen as another inode or a shorter file, and the new one is read from
15
+ # its start.
16
+ class LogTail
17
+ POLL_INTERVAL = 0.2
18
+
19
+ # @param path [String] the debug log (Log.path)
20
+ # @param session_id [String] whose records are shown (its first
21
+ # Log::SID_LENGTH chars are the records' sid)
22
+ # @param on_record [#call] LogLine::Record ->, on the tail's thread
23
+ def initialize(path:, session_id:, on_record:, poll_interval: POLL_INTERVAL)
24
+ @path = path
25
+ @sid = session_id.to_s[0, Log::SID_LENGTH]
26
+ @on_record = on_record
27
+ @poll_interval = poll_interval
28
+ @io = nil
29
+ @ino = nil
30
+ @buffer = +""
31
+ @record = nil
32
+ @stopping = false
33
+ @wake = Thread::Queue.new
34
+ end
35
+
36
+ # Start following from the file's current end.
37
+ # @return [self]
38
+ def start
39
+ open_at(:end)
40
+ @thread = Thread.new { follow }
41
+ @thread.report_on_exception = false
42
+ self
43
+ end
44
+
45
+ # How long #stop waits for the record being handed on.
46
+ STOP_WAIT = 1
47
+
48
+ # The records written since the last look are shown first. The tail
49
+ # ends after the look it is in (a record half committed is finished);
50
+ # one stuck past STOP_WAIT is killed.
51
+ def stop
52
+ if @thread
53
+ @stopping = true
54
+ @wake << :stop
55
+ @thread.kill unless @thread.join(STOP_WAIT)
56
+ Degrade.call(:attached, "log_tail_failed") { poll }
57
+ end
58
+ @io&.close
59
+ @io = nil
60
+ end
61
+
62
+ # One look: the lines written since the last (tests call it directly).
63
+ def poll
64
+ reopen_if_rotated
65
+ return unless @io
66
+
67
+ drain
68
+ take_lines
69
+ end
70
+
71
+ private
72
+
73
+ def follow
74
+ until @stopping
75
+ Degrade.call(:attached, "log_tail_failed") { poll }
76
+ @wake.pop(timeout: @poll_interval)
77
+ end
78
+ end
79
+
80
+ # What the open file holds past the last read, into the buffer.
81
+ def drain
82
+ chunk = @io.read_nonblock(1 << 16, exception: false)
83
+ while chunk.is_a?(String)
84
+ @buffer << chunk
85
+ chunk = @io.read_nonblock(1 << 16, exception: false)
86
+ end
87
+ end
88
+
89
+ def open_at(where)
90
+ @io = File.open(@path, "rb")
91
+ @ino = @io.stat.ino
92
+ @io.seek(0, IO::SEEK_END) if where == :end
93
+ rescue SystemCallError
94
+ @io = nil # not there yet: the next look opens it
95
+ end
96
+
97
+ def reopen_if_rotated
98
+ return open_at(:start) unless @io
99
+
100
+ stat = File.stat(@path)
101
+ return if stat.ino == @ino && stat.size >= @io.pos
102
+
103
+ # Renamed: what the worker wrote to it since the last look is still
104
+ # there, read before the new file (a truncated one has nothing left).
105
+ drain if stat.ino != @ino
106
+ @io.close
107
+ open_at(:start)
108
+ rescue SystemCallError
109
+ nil # renamed and not written again yet
110
+ end
111
+
112
+ # Whole lines only; a payload line joins the record above it. DebugLog
113
+ # writes a record and its payload in one write, so a look ends on a
114
+ # whole record: each one is handed on by the end of the look.
115
+ def take_lines
116
+ while (cut = @buffer.index("\n"))
117
+ line = LogLine.clean(@buffer.slice!(0, cut + 1)).chomp
118
+ if LogLine.payload_line?(line)
119
+ @record.payload = [@record.payload, line.delete_prefix(LogLine::INDENT)].compact.join("\n") if @record
120
+ next
121
+ end
122
+ flush
123
+ @record = LogLine.parse(line)
124
+ end
125
+ flush
126
+ end
127
+
128
+ def flush
129
+ record = @record
130
+ @record = nil
131
+ @on_record.call(record) if record && record.sid == @sid
132
+ end
133
+ end
134
+ end
135
+ end
@@ -10,7 +10,7 @@ module Samagotchi
10
10
  # stderr. stdout is the answer's alone, printed after the turn: a
11
11
  # script or a parent agent reads it.
12
12
  #
13
- # It prints the retry lines the REPL shows: the loop asking again after
13
+ # It prints the retry lines the attached terminal shows: the loop asking again after
14
14
  # an empty answer (:empty_answer_retry), and a generation retried after
15
15
  # a failed request (:generation_retrying). And the hooks' notices
16
16
  # (:hook_notice): one during the turn as it comes, one the after_turn
@@ -18,7 +18,7 @@ module Samagotchi
18
18
  # #flush, which the caller runs after it has printed the answer. And
19
19
  # the load warnings the first turn announces (:guardrail_warning: the
20
20
  # guardrails, the plugins, an orphaned project memory folder), as the
21
- # REPL's "<label>> …" lines.
21
+ # attached terminal's "<label>> …" lines.
22
22
  class OneShotSink
23
23
  include Formatting
24
24
 
@@ -12,7 +12,7 @@ module Samagotchi
12
12
  # print as lines. The activity slot is dropped, because redrawing it in
13
13
  # place needs cursor movement (the turn's lines still print as they end).
14
14
  # The prompt is Reline's own, so output may cross it; the editor slot
15
- # prints the prompt of a plain read (the REPL's ? off a terminal).
15
+ # prints the prompt of a plain read (a question's ? off a terminal).
16
16
  class PlainSurface
17
17
  include Surface
18
18
 
@@ -11,8 +11,7 @@ module Samagotchi
11
11
  # slot content (#slot), the line it leaves (#summary) and the parsing of
12
12
  # a typed answer ("2", "1,3", "1 3; text", option labels). No I/O:
13
13
  # callers draw the slot and print the messages, and
14
- # record the answer (the REPL on its Engine, an attached UI over the
15
- # Bridge). An approval (kind "approval") takes only a number, an exact
14
+ # record the answer (an attached UI over the Bridge). An approval (kind "approval") takes only a number, an exact
16
15
  # label, y (the first option, Allow once) or n (the last, Deny), plus
17
16
  # an optional "; reason"; a substring would pick the wrong one ("y" is
18
17
  # in "Deny"). The step-limit question (kind "continue") reads the
@@ -86,8 +85,8 @@ module Samagotchi
86
85
  end
87
86
 
88
87
  # The line a question dismissed in a --non-interactive run leaves (no
89
- # one could answer it there): the REPL's own, and an attached UI's on
90
- # its cancel (QuestionDesk::UNANSWERABLE_REASON).
88
+ # one could answer it there): the one-shot's own, and an attached UI's
89
+ # on its cancel (QuestionDesk::UNANSWERABLE_REASON).
91
90
  UNANSWERABLE_TEXT = "(dismissed: no one to answer in a non-interactive run)"
92
91
 
93
92
  # What a question closed with no answer here says: a relayed approval
@@ -220,7 +219,7 @@ module Samagotchi
220
219
  end
221
220
 
222
221
  # The continue words, or an option's number. Empty is Continue (Enter
223
- # alone, as at the REPL's continue prompt).
222
+ # alone).
224
223
  def parse_continue(raw)
225
224
  text = raw.to_s.strip
226
225
  if text.match?(/\A\d+\z/)
@@ -39,13 +39,13 @@ module Samagotchi
39
39
  attr_reader :screen
40
40
 
41
41
  # Asked first on Ctrl-C during a read, with or without a screen: a
42
- # truthy answer means it handled the key (the REPL cancelled a running
43
- # turn) and the read goes on with the typed text as it is.
42
+ # truthy answer means it handled the key (the attached loop cancelled a
43
+ # running turn) and the read goes on with the typed text as it is.
44
44
  # @return [#call, nil]
45
45
  attr_accessor :interrupt_handler
46
46
 
47
47
  # Called with no arguments for each key a read takes (typing is
48
- # activity for the REPL's idle clock).
48
+ # activity for an idle clock; nobody sets it now).
49
49
  # @return [#call, nil]
50
50
  attr_accessor :key_handler
51
51
 
@@ -6,8 +6,8 @@ module Samagotchi
6
6
  class TerminalUI
7
7
  # The status row under the prompt, the same for both TUIs:
8
8
  # `status> Title | model=… | ↳ parent | ⑂ 2 running · 1 waiting | 2 participants | ctx=12.3% (under20) | mem: … | muted: …`.
9
- # A UI feeds it what it learns (attached mode from the Bridge's events,
10
- # the REPL from its Engine's) through #update, and the row is redrawn in
9
+ # The attached TUI feeds it what it learns from the Bridge's events
10
+ # through #update, and the row is redrawn in
11
11
  # the surface's status slot only when its text changes.
12
12
  class StatusRow
13
13
  include Formatting
@@ -54,7 +54,7 @@ module Samagotchi
54
54
  @shown = nil
55
55
  end
56
56
 
57
- # The surface it draws on (the REPL swaps in a live region): the row
57
+ # The surface it draws on (no caller since the REPL went): the row
58
58
  # is drawn there at the next change.
59
59
  def surface=(surface)
60
60
  @surface = surface
@@ -72,8 +72,8 @@ module Samagotchi
72
72
  refresh
73
73
  end
74
74
 
75
- # Take a session's state: the REPL's Engine#session_state_snapshot, or
76
- # the one a worker sends an attached TUI when it joins. A key the state
75
+ # Take a session's state: the one a worker sends an attached TUI when
76
+ # it joins (Engine#session_state_snapshot). A key the state
77
77
  # lacks (an older worker's) leaves its field as it was; the served pair
78
78
  # is cleared without one, and the model is kept without a name.
79
79
  # @param default_model [String, nil] the config's default, named beside
@@ -94,8 +94,7 @@ module Samagotchi
94
94
  STATE_LISTS = { used_memories: :used_memory_names, preloaded: :preloaded_memory_names,
95
95
  muted: :muted_memory_names }.freeze
96
96
 
97
- # Take what a turn event says about the row (the Engine's events in
98
- # the REPL, the Bridge's in attached mode): ctx, the memories used,
97
+ # Take what a turn event says about the row (the Bridge's events): ctx, the memories used,
99
98
  # the model the server served, whether a turn runs (the steer
100
99
  # hint). Other events change nothing.
101
100
  def take_event(event)
@@ -178,7 +177,7 @@ module Samagotchi
178
177
  end
179
178
 
180
179
  # `⑂ 2 running · 1 waiting`, the counts there are; "" with neither (no
181
- # children, or all ended). Not "not reported": a REPL parent is never
180
+ # children, or all ended). Not "not reported": a one-shot parent is never
182
181
  # given its children's replies (no wakes there), and a worker moves
183
182
  # the cursors only after the turn_completed a terminal refreshes on.
184
183
  def children_text(counts)