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
@@ -13,14 +13,19 @@ require_relative "../launch_mode"
13
13
  require_relative "live_region"
14
14
  require_relative "plain_surface"
15
15
  require_relative "attached_loop"
16
+ require_relative "log_tail"
17
+ require_relative "formatting"
16
18
 
17
19
  module Samagotchi
18
20
  class TerminalUI
19
- # `chi --attach ID` and `chi --shared [--resume ID]`: find or start the
21
+ # `chi`, `chi --attach ID` and `chi --resume ID`: find or start the
20
22
  # session's worker, then run the TUI as a client of its Bridge
21
23
  # (AttachedLoop). It takes no OwnerLock and builds no Engine: the worker
22
24
  # owns the session, and any number of UIs can attach to it.
23
25
  module AttachLauncher
26
+ # #paint, for the -v records.
27
+ extend Formatting
28
+
24
29
  # Why attaching failed, worded for the terminal.
25
30
  class Error < StandardError; end
26
31
 
@@ -28,6 +33,13 @@ module Samagotchi
28
33
  # A connector that answered no session (its own failures raise Error
29
34
  # with their words).
30
35
  NOTHING_CONNECTED = "could not attach: no session was found or started"
36
+ # `chi scratch`'s first line, and its last once the session is gone.
37
+ SCRATCH_BANNER = "Scratch session: nothing is kept, it is deleted when you leave."
38
+ SCRATCH_DELETED = "Scratch session deleted."
39
+ # chi -v on a worker that was running before this launch (%s: the
40
+ # debug log): its level is its own.
41
+ VERBOSE_RUNNING = "(--verbose: this worker was already running and logs at the level it started with; " \
42
+ "its records show here only if it writes to %s: one chi web started may log elsewhere)"
31
43
 
32
44
  module_function
33
45
 
@@ -62,6 +74,13 @@ module Samagotchi
62
74
  # own guardrails.parent_answers; an existing session keeps its own
63
75
  # (never sent as a /parent-answers command: that would be a
64
76
  # loosening route from a command line)
77
+ # @param scratch [Boolean] `chi scratch`: a new scratch session (its
78
+ # worker's Engine saves nothing), stopped and deleted however the
79
+ # attach ends; its worker deletes it too once no client is left
80
+ # @param verbose [Boolean] -v: the session's debug records in the
81
+ # debug log (Log.path), shown above the live region as they come
82
+ # (LogTail); a worker this launch spawns logs at debug (bin/chi's
83
+ # log.level override reaches it through Config.cli_env)
65
84
  # @param state_dir [String, nil] where the sessions are (the default
66
85
  # state dir by default): the worker's, for the attached loop too
67
86
  # @param connector [#connect, nil] finds or starts the session
@@ -73,19 +92,23 @@ module Samagotchi
73
92
  # :failed when the --model switch didn't go through, or (input from a
74
93
  # pipe) :turn_failed / :empty_answer / :removed / :unanswered
75
94
  # (AttachedLoop#run)
76
- def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
95
+ def run(attach: nil, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
77
96
  memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
78
- toolkit_flags: [], settings: nil, parent_answers: nil, state_dir: nil, connector: nil)
97
+ toolkit_flags: [], settings: nil, parent_answers: nil, scratch: false, verbose: false, state_dir: nil,
98
+ connector: nil)
79
99
  state_dir ||= Session.default_state_dir
100
+ running_before = verbose && !connector && worker_running?(attach || resume, state_dir)
80
101
  link = connector && (connector.connect || raise(Error, NOTHING_CONNECTED))
81
102
  client = if connector
82
103
  link.client
83
104
  else
84
- connect(attach: attach, shared: shared, resume: resume, model: model, state_dir: state_dir,
105
+ connect(attach: attach, resume: resume, model: model, state_dir: state_dir,
85
106
  memories: memories, muted_memories: muted_memories, llm_context: llm_context,
86
107
  thinking: thinking, appended_prompt_files: appended_prompt_files, toolkit: toolkit,
87
- toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers)
108
+ toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers,
109
+ scratch: scratch)
88
110
  end
111
+ $stdout.puts(SCRATCH_BANNER) if scratch
89
112
  commands = []
90
113
  # --yes on /model and /thinking: typing the flag is the choice (no
91
114
  # confirm step); its applied reply ends the launch's command.
@@ -93,11 +116,13 @@ module Samagotchi
93
116
  commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
94
117
  commands << "#{ThinkingCommand::NAME} #{thinking} --yes" if (attach || resume) && thinking
95
118
  first_command = commands.size > 1 ? commands : commands.first
119
+ $stderr.puts(format(VERBOSE_RUNNING, Log.path)) if running_before
96
120
  surface = open_surface
121
+ tail = (start_log_tail(surface, client.session_id) if verbose && Log.path)
97
122
  options = { screen: surface, client_id: client_id(connector), first_prompt: prompt,
98
123
  first_command: first_command, no_interrupt: no_interrupt,
99
124
  default_input: default_input && !prompt && !attach && !resume,
100
- wait_at_eof: !$stdin.tty?,
125
+ wait_at_eof: !$stdin.tty?, scratch: scratch,
101
126
  as_parent: Guardrails::ParentApprovals.parent_process? }
102
127
  attached = if connector
103
128
  AttachedLoop.new(link: link, **options)
@@ -107,6 +132,7 @@ module Samagotchi
107
132
  ended = begin
108
133
  attached.run
109
134
  ensure
135
+ tail&.stop
110
136
  close_surface(surface)
111
137
  end
112
138
  if ended == :unanswered && attached.unanswered
@@ -119,6 +145,34 @@ module Samagotchi
119
145
  ended
120
146
  ensure
121
147
  link&.close
148
+ # Every way out (Ctrl-D, /exit, the worker gone, an error, Ctrl-C or
149
+ # a signal): only kill -9 leaves it to its worker (and the sweep).
150
+ end_scratch(client.session_id, state_dir: state_dir, quiet: !$!.nil?) if scratch && client
151
+ end
152
+
153
+ # Whether +session_id+'s worker runs now (nil: a new session).
154
+ def worker_running?(session_id, state_dir)
155
+ return false unless session_id && Session.valid_id?(session_id)
156
+
157
+ WorkerLink.resolve(Session.session_dir(session_id, state_dir: state_dir), unlink: false).live?
158
+ end
159
+
160
+ # The session's debug records, dim, above the live region.
161
+ def start_log_tail(surface, session_id)
162
+ LogTail.new(path: Log.path, session_id: session_id, on_record: lambda { |record|
163
+ surface.commit(paint(LogLine.format(record).chomp, 2))
164
+ }).start
165
+ end
166
+
167
+ # Stop the scratch session's worker and delete the session; its worker
168
+ # may have deleted it first (it found no client left).
169
+ def end_scratch(session_id, state_dir:, quiet: false, out: $stdout, err: $stderr)
170
+ SessionManager.delete_session(session_id, state_dir: state_dir, stop: true, wait: LocalLink::DELETE_WAIT)
171
+ out.puts(SCRATCH_DELETED) unless quiet
172
+ rescue ArgumentError
173
+ out.puts(SCRATCH_DELETED) unless quiet
174
+ rescue SessionManager::DeleteRefused, SystemCallError => e
175
+ err.puts("Scratch session #{session_id} was not deleted (#{e.message}): chi sessions delete #{session_id}")
122
176
  end
123
177
 
124
178
  # This TUI's client id: tui:PID, or the connector's own (a remote one
@@ -185,10 +239,11 @@ module Samagotchi
185
239
 
186
240
  # @return [BridgeClient] a client for the live Bridge of the session
187
241
  # @raise [Error]
188
- def connect(attach: nil, shared: false, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
242
+ def connect(attach: nil, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
189
243
  memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
190
- toolkit_flags: [], settings: nil, parent_answers: nil, err: $stderr)
244
+ toolkit_flags: [], settings: nil, parent_answers: nil, scratch: false, err: $stderr)
191
245
  sd = state_dir || Session.default_state_dir
246
+ connected = false
192
247
  if attach || resume
193
248
  warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files,
194
249
  toolkit_flags: toolkit_flags)
@@ -211,14 +266,21 @@ module Samagotchi
211
266
  thinking: Thinking.session_level(thinking),
212
267
  appended_prompt_files: appended_prompt_files,
213
268
  toolkit: toolkit, settings: settings,
214
- parent_answers: parent_answers))
269
+ parent_answers: parent_answers),
270
+ scratch: scratch)
215
271
  end
216
272
  link = WorkerLink.await(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
217
- link.live? ? link.client(session.id) : raise(Error, format(LaunchMode::BRIDGE_LATE, session.id))
273
+ raise(Error, format(LaunchMode::BRIDGE_LATE, session.id)) unless link.live?
274
+
275
+ connected = true
276
+ link.client(session.id)
218
277
  rescue SessionManager::OwnedByTUI => e
219
278
  raise Error, e.message
220
279
  rescue ArgumentError => e
221
280
  raise Error, e.message
281
+ ensure
282
+ # A scratch session whose Bridge never came (or Ctrl-C meanwhile).
283
+ end_scratch(session.id, state_dir: sd, quiet: true, err: err) if scratch && session && !connected
222
284
  end
223
285
 
224
286
  # The session's previous worker may still be leaving: its Bridge
@@ -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