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
@@ -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,16 +60,6 @@ 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
65
- # moves there.
66
- def surface=(surface)
67
- @lock.synchronize do
68
- @screen = surface
69
- redraw_status if status_text
70
- end
71
- end
72
-
73
63
  # A new turn: its tally starts over too.
74
64
  def reset_turn_feedback
75
65
  @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)
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "reline"
6
6
 
7
7
  require_relative "../config"
8
+ require_relative "../degrade"
8
9
  require_relative "../memory_paths"
9
10
  require_relative "../prompt_history"
10
11
  require_relative "../tools/memory"
@@ -14,7 +15,7 @@ require_relative "reline_history_hook"
14
15
 
15
16
  module Samagotchi
16
17
  class TerminalUI
17
- # Input the REPL and the attached TUI share: the persistent prompt
18
+ # The attached TUI's input: the persistent prompt
18
19
  # history, Tab completion (/commands, @path in the cwd, #memory, which
19
20
  # inserts #name and the model reads it as typed), multiline reads at the
20
21
  # main prompt, and a one-shot prefill of the next read (the default
@@ -29,12 +30,6 @@ module Samagotchi
29
30
 
30
31
  private
31
32
 
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
33
  # Whether a new session's first read gets the default input
39
34
  # (SAMAGOTCHI_DEFAULT_INPUT); --no-default-input says no.
40
35
  def default_input_wanted? = !@no_default_input
@@ -96,20 +91,18 @@ module Samagotchi
96
91
  fragment = slash_fragment.to_s.strip
97
92
  return [] unless fragment.start_with?("/")
98
93
 
99
- begin
94
+ # Only at the line's start: "/x" mid-line is a path, not a command.
95
+ Degrade.call(:attached, "completion_line_unreadable") do
100
96
  buf = Reline.line_buffer.to_s
101
- if !buf.empty? && !buf.lstrip.start_with?("/")
102
- return []
103
- end
104
- rescue StandardError
105
- nil
97
+ return [] if !buf.empty? && !buf.lstrip.start_with?("/")
106
98
  end
107
99
 
108
100
  lowered = fragment.downcase
109
101
  return slash_commands.dup if lowered == "/"
110
102
 
111
103
  slash_commands.select { |cmd| cmd.start_with?(lowered) }
112
- rescue StandardError
104
+ rescue StandardError => e
105
+ Degrade.note(:attached, "slash_completion_failed", e)
113
106
  []
114
107
  end
115
108
 
@@ -140,7 +133,8 @@ module Samagotchi
140
133
  relative_path = "#{relative_path}/" if File.directory?(absolute_path)
141
134
  "#{AT_PATH_COMPLETION_PREFIX}#{relative_path}"
142
135
  end
143
- rescue StandardError
136
+ rescue StandardError => e
137
+ Degrade.note(:attached, "path_completion_failed", e)
144
138
  []
145
139
  end
146
140
 
@@ -196,7 +190,8 @@ module Samagotchi
196
190
  yield scope, name
197
191
  end
198
192
  end
199
- rescue StandardError
193
+ rescue StandardError => e
194
+ Degrade.note(:attached, "memory_completion_failed", e)
200
195
  []
201
196
  end
202
197
 
@@ -213,8 +208,8 @@ module Samagotchi
213
208
  @history_signature = PromptHistory.signature
214
209
  @history_seen = PromptHistory.entries
215
210
  @history_seen.last(PromptHistory::LIMIT).each { |entry| Reline::HISTORY << entry }
216
- rescue StandardError
217
- nil
211
+ rescue StandardError => e
212
+ Degrade.note(:attached, "history_load_failed", e)
218
213
  end
219
214
 
220
215
  # A scratch session keeps nothing, its typed lines neither.
@@ -229,8 +224,8 @@ module Samagotchi
229
224
  history_lock.synchronize { forget_own_line(own) } if own
230
225
  raise
231
226
  end
232
- rescue StandardError
233
- nil
227
+ rescue StandardError => e
228
+ Degrade.note(:attached, "history_append_failed", e)
234
229
  end
235
230
 
236
231
  # Before a main-prompt read, and on the first ↑ of a walk at an open
@@ -261,8 +256,8 @@ module Samagotchi
261
256
  @history_signature = signature
262
257
  @history_seen = entries
263
258
  history_lock.synchronize { add_others_lines(fresh) }
264
- rescue StandardError
265
- nil
259
+ rescue StandardError => e
260
+ Degrade.note(:attached, "history_pickup_failed", e)
266
261
  end
267
262
  end
268
263
 
@@ -77,8 +77,11 @@ module Samagotchi
77
77
  Log.session_id = @link.client.session_id
78
78
  Log.info(:attached, "joined", session: @link.client.session_id, messages: exchange.size,
79
79
  notes: messages.count { |m| ContextNote.note?(m) })
80
- last_user = exchange.rindex { |m| Steer.prompt?(m) }
81
- render_join_notes(messages)
80
+ # The last turn's own prompt (Steer.turn_prompt?, as the page's
81
+ # turns): not Qwen's recovery line, nor input merged into the turn.
82
+ last_user = exchange.rindex { |m| Steer.turn_prompt?(m) }
83
+ from = last_user && messages.index { |m| m.equal?(exchange[last_user]) }
84
+ render_join_notes(messages, from)
82
85
  return unless last_user
83
86
 
84
87
  @screen.commit(join_prompt_line(exchange[last_user]))
@@ -86,7 +89,6 @@ module Samagotchi
86
89
  unless ImageRef.placeholder_only?(exchange[last_user][:content])
87
90
  Array(exchange[last_user][:images]).each { |ref| @screen.commit(format_image_line(ref)) }
88
91
  end
89
- from = messages.index { |m| m.equal?(exchange[last_user]) }
90
92
  render_join_steps(messages, from, durations: join_tool_records(exchange[last_user]))
91
93
  record = @link.saved_turn_record(exchange[last_user][:turn_id] || exchange[last_user]["turn_id"])
92
94
  failed = join_failed_record(record)
@@ -114,19 +116,21 @@ module Samagotchi
114
116
  .find { |text| !text.empty? }
115
117
  @screen.commit(last_lines(answer)) if answer
116
118
  end
117
- render_join_notes(messages, after_exchange: true)
119
+ render_join_notes(messages, from, after_exchange: true)
118
120
  end
119
121
 
120
122
  # What the last exchange did, as the live turn drew it: each saved
121
123
  # call's tool row (action, params, status from its result, and its
122
124
  # duration from +durations+, the turn's saved tool records) and a
123
- # plugin's steer where it came. The step texts stay out, as live (the
124
- # ticker's).
125
+ # plugin's steer and the user's input merged into the turn where they
126
+ # came. The step texts stay out, as live (the ticker's).
125
127
  def render_join_steps(messages, from, durations: [])
126
128
  records = durations.dup
127
129
  messages.each_with_index.drop(from + 1).each do |m, i|
128
130
  if Steer.steer?(m)
129
131
  @screen.commit(format_steer_line(source: m[:source], text: m[:content]))
132
+ elsif Steer.input?(m) && !Steer.turn_prompt?(m)
133
+ @screen.commit("input> #{m[:content]}")
130
134
  elsif %w[model assistant].include?(m[:role].to_s)
131
135
  responses = messages.drop(i + 1).take_while { |r| r[:role].to_s == "tool_response" }
132
136
  Array(MessageParts.for_message(m, responses)&.dig(:tools)).each do |part|
@@ -209,13 +213,13 @@ module Samagotchi
209
213
  format_tool_guardrail_suffix({ decided_by: "system one", p: saved["p"] || saved[:p] })
210
214
  end
211
215
 
212
- # The context notes that came since the last prompt (all of them in a
213
- # session with none), after the exchange the header shows.
214
- def render_join_notes(messages, after_exchange: false)
215
- last_user = messages.rindex { |m| Steer.prompt?(m) }
216
- return if after_exchange != !last_user.nil?
216
+ # The context notes that came since the last prompt (+from+, its index
217
+ # in +messages+; all of them in a session with none), after the
218
+ # exchange the header shows.
219
+ def render_join_notes(messages, from, after_exchange: false)
220
+ return if after_exchange != !from.nil?
217
221
 
218
- messages[(last_user ? last_user + 1 : 0)..].select { |m| ContextNote.note?(m) }.each do |m|
222
+ messages[(from ? from + 1 : 0)..].select { |m| ContextNote.note?(m) }.each do |m|
219
223
  @screen.commit(context_note_line(ContextNote.label_of(m), ContextNote.text_of(m)))
220
224
  end
221
225
  end
@@ -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