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
@@ -21,8 +21,7 @@ module Samagotchi
21
21
  # Owns the inactivity clock bookkeeping shared across UIs: it reads the
22
22
  # Engine's `last_activity_at` / `activity_seq` / `turn_running?` seam (the
23
23
  # single source of truth) rather than tracking its own timeline, so the
24
- # clock behaves identically for the interactive REPL and any Engine-backed
25
- # worker.
24
+ # clock behaves identically for any Engine-backed worker.
26
25
  #
27
26
  # When the session has been idle for `@inactivity` seconds, no turn is
28
27
  # running, and there are >= `@min_user_turns` user turns, the job
@@ -285,7 +284,7 @@ module Samagotchi
285
284
  attr_reader :generation, :inactivity, :min_user_turns, :sentences, :timeout
286
285
 
287
286
  # @param callable [#call] true while a continue offer waits for an
288
- # answer (the Worker's or the REPL's TurnFlow); read at each attempt
287
+ # answer (the Worker's TurnFlow); read at each attempt
289
288
  attr_writer :awaiting_continue
290
289
 
291
290
  # @return [Hash, nil] the last recap written: {text:, title:, covered:,
@@ -345,7 +344,7 @@ module Samagotchi
345
344
 
346
345
  @mutex = Monitor.new
347
346
  # Serializes the attempt state machine (#tick on the scheduler thread,
348
- # #request_now on a Bridge or REPL thread, #write_now). Never held by
347
+ # #request_now on a Bridge thread, #write_now). Never held by
349
348
  # #state, so a snapshot reading it doesn't wait on an emit.
350
349
  @run_mutex = Monitor.new
351
350
  @generation = 0
@@ -389,7 +388,7 @@ module Samagotchi
389
388
  end
390
389
 
391
390
  # Write a recap now and wait for it, bounded by the timeout: the worker
392
- # (or the REPL) leaving. Skips the inactivity window and the once-per-
391
+ # leaving. Skips the inactivity window and the once-per-
393
392
  # window latch, not the rules on what to recap (minimum user turns,
394
393
  # something new). Takes over an attempt already in flight. Call it with
395
394
  # the idle scheduler stopped.
@@ -11,8 +11,7 @@ module Samagotchi
11
11
  # Engine#run_turn) as [SYSTEM: REMINDERS DUE <local time>] and marks it fired.
12
12
  # 2. Synthetic turns: when this job's tick finds due reminders while
13
13
  # idle, it fires @auto_turn_callback, which queues their names
14
- # (Engine#note_due_reminders) for the worker's or the REPL's
15
- # reminder turn. While names are queued the tick waits; whoever
14
+ # (Engine#note_due_reminders) for the worker's reminder turn. While names are queued the tick waits; whoever
16
15
  # clears them (a turn's injection, the reminder turn's consumer)
17
16
  # re-arms it. The queue is the only "already due" state.
18
17
  class IdleReminders
@@ -64,8 +63,8 @@ module Samagotchi
64
63
 
65
64
  # Signal the engine to create a synthetic turn via callback.
66
65
  # The callback is responsible for triggering a turn (e.g. SessionManager
67
- # writes a file, TerminalUI queues input). The engine's run_turn or
68
- # REPL injection point then picks up and delivers the reminders.
66
+ # writes a file). The engine's run_turn then picks up and delivers
67
+ # the reminders.
69
68
  return unless @auto_turn_callback
70
69
 
71
70
  @auto_turn_callback.call(due_names)
@@ -5,8 +5,8 @@ require_relative "log"
5
5
 
6
6
  module Samagotchi
7
7
  # A turn's iteration limit (how many model ↔ tool rounds before the turn
8
- # stops and offers to continue), one resolver for the Engine, the worker
9
- # and the REPL.
8
+ # stops and offers to continue), one resolver for the Engine and the
9
+ # worker.
10
10
  module IterationLimit
11
11
  KEY = "turn.max_iterations"
12
12
  # A turn's limit unless turn.max_iterations says otherwise, and the
@@ -11,6 +11,7 @@ require_relative "prompt_literal_guard"
11
11
  require_relative "token_usage"
12
12
  require_relative "client"
13
13
  require_relative "llm/errors"
14
+ require_relative "degrade"
14
15
  require_relative "log"
15
16
  require_relative "empty_answer_retry"
16
17
  require_relative "cut"
@@ -198,9 +199,9 @@ module Samagotchi
198
199
  # memory_read's overflow guard. nil when it can't be read.
199
200
  # @return [ContextWindow::Resolved, nil]
200
201
  def tool_output_window
201
- ContextWindow.resolve(client: @client, model: @turn_settings&.model_name, setting: @turn_settings&.window_setting)
202
- rescue StandardError
203
- nil
202
+ Degrade.call(:model, "context_window_unresolved") do
203
+ ContextWindow.resolve(client: @client, model: @turn_settings&.model_name, setting: @turn_settings&.window_setting)
204
+ end
204
205
  end
205
206
 
206
207
  def guard_generation
@@ -383,9 +384,9 @@ module Samagotchi
383
384
  end
384
385
 
385
386
  def model_props
386
- @client.respond_to?(:server_props) ? @client.server_props(model: @turn_settings.model_name) : nil
387
- rescue StandardError
388
- nil
387
+ Degrade.call(:model, "server_props_unreadable") do
388
+ @client.respond_to?(:server_props) ? @client.server_props(model: @turn_settings.model_name) : nil
389
+ end
389
390
  end
390
391
 
391
392
  # One iteration. Returns :next, :answer (the turn ends), or the turn's
@@ -831,10 +832,9 @@ module Samagotchi
831
832
  cancel_controller: turn.cancel_controller, answer: answer)
832
833
  end
833
834
 
835
+ # A failing stream callback (a UI gone) must not break the turn.
834
836
  def emit_stream_event(callback, event)
835
- callback&.call(event)
836
- rescue StandardError
837
- nil
837
+ Degrade.call(:turn, "stream_event_callback_failed", type: (event[:type] if event.is_a?(Hash))) { callback&.call(event) }
838
838
  end
839
839
 
840
840
  # ── Hook dispatch helper ───────────────────────────────────────────────────
@@ -849,9 +849,8 @@ module Samagotchi
849
849
  def fire_hook(name, event)
850
850
  return unless @hooks
851
851
 
852
- @hooks.fire(name, event)
853
- rescue StandardError
854
852
  # A failing hook must not break the turn.
853
+ Degrade.call(:hooks, "hook_fire_failed", hook: name) { @hooks.fire(name, event) }
855
854
  end
856
855
 
857
856
  # Coerce a value to boolean — handles true/false, nil, and string "true"/"false".
@@ -1,21 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- # How `bin/chi` runs a session: attached to a worker (as `--attach` and
5
- # `--shared` do) or in the plain in-process REPL. With `session.shared` on,
6
- # plain `chi` runs attached unless it asks for something attached mode
7
- # can't do yet.
4
+ # How `bin/chi` runs a session: attached to a worker (the attached TUI,
5
+ # AttachLauncher), or --non-interactive's one turn in this process
6
+ # (TerminalUI, the one-shot host).
8
7
  module LaunchMode
9
- # Options an attached TUI can't honor: the worker prints nothing, so
10
- # --verbose is about this process's output. (--model and --no-interrupt
11
- # reach the worker: a /model command, and no_interrupt on each posted
12
- # turn; --memory and --mute are session fields the worker reads.)
13
- REPL_ONLY = { verbose: "--verbose" }.freeze
14
- # What an attached run can't be combined with (the worker runs its turns).
15
- ATTACHED_CONFLICTS = { non_interactive: "--non-interactive", verbose: "--verbose" }.freeze
16
- # Opening a `chi scratch` session a killed REPL left behind would turn it
17
- # into a kept one.
18
- SCRATCH_LEFTOVER = "that's a leftover scratch session; it is deleted at the next sweep (chi sessions clean)"
8
+ # What an attached run can't be combined with.
9
+ ATTACHED_CONFLICTS = { non_interactive: "--non-interactive" }.freeze
19
10
  # A worker spawned or resumed for an attach whose Bridge never came (%s:
20
11
  # the session id). A worker whose chi home couldn't load chi logs
21
12
  # SessionManager::HOME_BOOT_FAILED and exits: HomeDoctor's boot row says
@@ -30,20 +21,12 @@ module Samagotchi
30
21
  module_function
31
22
 
32
23
  # @param options [Hash] bin/chi's parsed options
33
- # @param shared_config [Boolean] the session.shared config value
34
- # @return [Array(Symbol, String)] :attached or :repl, and a note for the
35
- # user when the config asked for attached mode but didn't get it
36
- def resolve(options, shared_config:)
37
- return [:attached, nil] if options[:attach] || options[:shared]
38
- # --no-shared, or nobody asked for attached mode.
39
- return [:repl, nil] if options[:shared] == false || !shared_config
40
- # A one-shot with no REPL: nothing to attach.
41
- return [:repl, nil] if options[:non_interactive]
24
+ # @return [Symbol] :attached, or :one_shot (--non-interactive: one turn
25
+ # in this process)
26
+ def resolve(options)
27
+ return :attached if options[:attach]
42
28
 
43
- flag = REPL_ONLY.find { |key, _flag| options[key] }&.last
44
- return [:repl, "(session.shared: #{flag} runs in a plain REPL)"] if flag
45
-
46
- [:attached, nil]
29
+ options[:non_interactive] ? :one_shot : :attached
47
30
  end
48
31
 
49
32
  # Why an attached run's flags don't go together, or nil when they do.
@@ -51,12 +34,10 @@ module Samagotchi
51
34
  # @return [String, nil]
52
35
  def attached_conflict(options)
53
36
  conflict = ATTACHED_CONFLICTS.find { |key, _flag| options[key] }
54
- if options[:attach] && options[:shared]
55
- "use either --attach ID or --shared"
56
- elsif options[:attach] && options[:resume]
57
- "--attach takes the session id; --resume goes with --shared"
37
+ if options[:attach] && options[:resume]
38
+ "use either --attach ID or --resume ID"
58
39
  elsif conflict
59
- "--attach/--shared can't be combined with #{conflict.last}"
40
+ "--attach can't be combined with #{conflict.last}"
60
41
  end
61
42
  end
62
43
  end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "cache_confirm"
4
+ require_relative "session"
4
5
 
5
6
  module Samagotchi
6
7
  # A command for a session's own level of a setting (/thinking,
7
8
  # /reasoning-history): with no argument, the level the next turn runs at
8
9
  # and where it came from; with a level, sets the session's own (the
9
10
  # setting's unset word unsets it). A change takes effect at the next
10
- # turn's start (the REPL and the worker run it between turns), and the
11
+ # turn's start (the worker runs it between turns), and the
11
12
  # reply says what it costs the prompt cache. A costly change asks first
12
13
  # (CacheConfirm); --yes (or -y) applies it.
13
14
  #
@@ -47,7 +48,7 @@ module Samagotchi
47
48
  known, own = spec.parse.call(word)
48
49
  raise ArgumentError, "unknown #{spec.label} level #{word}\n#{spec.usage}" unless known
49
50
 
50
- session = @engine.session or raise ArgumentError, "no session yet"
51
+ session = @engine.session or raise ArgumentError, Session::NONE_YET
51
52
  before = explained
52
53
  ask = confirm && !yes ? ask(before, own, word) : nil
53
54
  return ask if ask
@@ -5,6 +5,7 @@ require_relative "llm_context_strategy"
5
5
  require_relative "llm_context_edit"
6
6
  require_relative "llm_context_stale"
7
7
  require_relative "cache_confirm"
8
+ require_relative "session"
8
9
 
9
10
  module Samagotchi
10
11
  # /llm-context, the session's own LLM context values
@@ -14,7 +15,7 @@ module Samagotchi
14
15
  # `strategy <layers|none>`, `apply <rule>` and `budget <N|off>` (any of
15
16
  # them, in one line; `default` unsets one) it sets the session's own, and
16
17
  # `reset` unsets them all. A change takes effect at the next turn's
17
- # start (the REPL and the worker run it between turns), and the reply
18
+ # start (the worker runs it between turns), and the reply
18
19
  # says what it costs: forget on or off changes the tool list and the
19
20
  # system prompt (a full cache break); stale turned on stages the reads
20
21
  # already superseded as one batch under the apply rule; a layer turned
@@ -49,7 +50,7 @@ module Samagotchi
49
50
  args, yes = CacheConfirm.split(args, downcase: false)
50
51
  return [listing(@engine.llm_context_explained), false, nil] if args.empty?
51
52
 
52
- session = @engine.session or raise ArgumentError, "no session yet"
53
+ session = @engine.session or raise ArgumentError, Session::NONE_YET
53
54
  before = @engine.llm_context_explained
54
55
  updated = args.casecmp?(RESET) ? LLMContextOverride.new : LLMContextOverride.update(@engine.llm_context_override, words(args))
55
56
  ask = confirm && !yes ? ask(session, before, updated, args) : nil
@@ -197,8 +197,8 @@ module Samagotchi
197
197
  return texts ? [[refs.first, call, texts.first.text]] : []
198
198
  end
199
199
 
200
- # A call_tool of a built-in ran as it: its run's lead names the
201
- # built-in (BuiltinCalls.unwrap_saved).
200
+ # A call_tool of a chi tool (a built-in or a plugin's) ran as it: its
201
+ # run's lead names the tool (BuiltinCalls.unwrap_saved).
202
202
  calls = batch.map(&:last)
203
203
  calls = calls.zip(ToolResponse.runs(entry[:content], calls.size)).map do |call, run|
204
204
  Tools::BuiltinCalls.unwrap_saved(Tools::BuiltinCalls.wrap_saved(call, run&.text), run&.text)
@@ -71,7 +71,7 @@ module Samagotchi
71
71
  @monitor.synchronize { close_state }
72
72
  end
73
73
 
74
- # The session this process works for (a worker, the REPL); records
74
+ # The session this process works for (a worker, a one-shot); records
75
75
  # carry its first SID_LENGTH chars unless they pass their own sid:.
76
76
  attr_accessor :session_id
77
77
 
@@ -127,7 +127,7 @@ module Samagotchi
127
127
  state[:writer].write(text)
128
128
  mirror(text) if state[:mirror] && !echo
129
129
  text
130
- rescue StandardError
130
+ rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
131
131
  nil
132
132
  end
133
133
 
@@ -152,7 +152,7 @@ module Samagotchi
152
152
  ts: Time.now, level: level.to_s.upcase, tag: tag, pid: Process.pid,
153
153
  sid: sid && sid.to_s[0, SID_LENGTH], event: event, fields: fields, payload: payload
154
154
  ))
155
- rescue StandardError
155
+ rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
156
156
  nil
157
157
  end
158
158
 
@@ -170,13 +170,13 @@ module Samagotchi
170
170
 
171
171
  def echo_to_stderr(text)
172
172
  $stderr.puts(text)
173
- rescue StandardError
173
+ rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
174
174
  nil
175
175
  end
176
176
 
177
177
  def mirror(text)
178
178
  $stderr.write(text)
179
- rescue StandardError
179
+ rescue StandardError # degrade-ok: Log's own internals: a failure here must not recurse into Log
180
180
  nil
181
181
  end
182
182
 
@@ -201,8 +201,8 @@ module Samagotchi
201
201
  def build_state
202
202
  options = @options
203
203
  path = options.fetch(:path, :auto)
204
- path = LogPath.resolve rescue nil if path == :auto
205
- level = options[:level] || (Config.get("log.level") rescue nil)
204
+ path = LogPath.resolve rescue nil if path == :auto # degrade-ok: Log's own internals: a failure here must not recurse into Log
205
+ level = options[:level] || (Config.get("log.level") rescue nil) # degrade-ok: Log's own internals: a failure here must not recurse into Log
206
206
  level = LEVELS.key?(level.to_s.downcase.to_sym) ? level.to_s.downcase.to_sym : DEFAULT_LEVEL
207
207
  { writer: DebugLog.new(path: path), level: LEVELS.fetch(level), mirror: options[:mirror] ? true : false }
208
208
  end
@@ -16,7 +16,7 @@ module Samagotchi
16
16
  # (debug dumps) are indented by four spaces and belong to the record above.
17
17
  module LogLine
18
18
  LEVELS = %w[DEBUG INFO WARN ERROR].freeze
19
- TAGS = %w[turn http worker bridge web attached repl idle recap hooks plugins guardrails config memory model context broadcast toolkit system_one].freeze
19
+ TAGS = %w[turn http worker bridge web attached idle recap hooks plugins guardrails config memory model context broadcast toolkit system_one prompt].freeze
20
20
  INDENT = " "
21
21
 
22
22
  Record = Struct.new(:ts, :level, :tag, :pid, :sid, :event, :fields, :payload, keyword_init: true)
@@ -4,7 +4,7 @@ require_relative "config"
4
4
  require_relative "paths"
5
5
 
6
6
  module Samagotchi
7
- # Where the debug log (DebugLog) goes, for the REPL, the attached terminal
7
+ # Where the debug log (DebugLog) goes, for a one-shot, the attached terminal
8
8
  # and the background worker alike: nil when log.disable is set, else log.file
9
9
  # (~ and relative paths expanded against cwd), else
10
10
  # $XDG_STATE_HOME/samagotchi/samagotchi.log (~/.local/state/... by default),
@@ -240,8 +240,9 @@ module Samagotchi
240
240
  # +tool_id+: its output's ToolIds id, with its ✂ mark in +marks+.
241
241
  def tool_part(call, output, registry, shown = nil, images = nil, label: nil, diff: nil, refs: nil, cwd: nil,
242
242
  tool_id: nil, marks: {})
243
- # A built-in reached through call_tool reloads as that built-in's
244
- # row, with via (its saved params and label are the built-in's).
243
+ # A chi tool (a built-in or a plugin's) reached through call_tool
244
+ # reloads as that tool's row, with via (its saved params and label
245
+ # are the tool's).
245
246
  # A direct call of an MCP tool reloads as the call_tool row it ran as.
246
247
  call = Tools::BuiltinCalls.unwrap_saved(Tools::BuiltinCalls.wrap_saved(call, output), output)
247
248
  name = call[:name].to_s
@@ -123,7 +123,7 @@ module Samagotchi
123
123
 
124
124
  def outcome(result)
125
125
  case result.status
126
- when :refused then "refused: #{NoteDelivery::REPL_REFUSAL}"
126
+ when :refused then "refused: #{result.refusal}"
127
127
  when :queued then "queued: its worker adds it within a few seconds"
128
128
  else "waits for the session's next start (#{result.queued} #{result.queued == 1 ? "note" : "notes"} queued)"
129
129
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "session"
4
+ require_relative "owner_lock"
4
5
  require_relative "session_inbox"
5
6
 
6
7
  module Samagotchi
@@ -9,21 +10,28 @@ module Samagotchi
9
10
  autoload :SessionManager, File.expand_path("session_manager", __dir__)
10
11
 
11
12
  # The one rule for putting a context note into a session, shared by
12
- # `chi note`, the agent's send_note and `chi broadcast`: a session a chi
13
- # REPL owns refuses it (notes need attached mode); any other gets the
14
- # note in its notes/ folder, which a live worker adds within seconds and
15
- # a session with no worker at its next start. Callers word the outcome.
13
+ # `chi note`, the agent's send_note and `chi broadcast`: a session a
14
+ # one-turn chi run owns, or a chi scratch session, refuses it; any other
15
+ # gets the note in its notes/ folder, which a live worker adds within
16
+ # seconds and a session with no worker at its next start. Callers word
17
+ # the outcome (#refusal for a refused one).
16
18
  module NoteDelivery
17
- # Why a chi REPL's session takes no note.
18
- REPL_REFUSAL = "it is open in a chi REPL; notes need attached mode"
19
+ # Why a one-turn chi run's session takes no note.
20
+ ONE_SHOT_REFUSAL = "it is #{OwnerLock::TUI_BUSY}; send the note once it ends"
19
21
 
20
22
  # The outcome for one session.
21
23
  # @!attribute status [Symbol] :queued (a worker runs it), :waits (no
22
- # worker: it waits for the next start) or :refused (a chi REPL owns it)
24
+ # worker: it waits for the next start) or :refused (a one-turn chi run
25
+ # owns it, or it is a chi scratch session: +scratch+)
23
26
  # @!attribute queued [Integer] notes waiting in its notes/ folder
24
27
  # (:waits only; 0 otherwise)
25
- Result = Data.define(:id, :status, :queued) do
28
+ Result = Data.define(:id, :status, :queued, :scratch) do
29
+ def initialize(id:, status:, queued:, scratch: false) = super
30
+
26
31
  def delivered? = status != :refused
32
+
33
+ # Why a refused note was refused, in its condition's one wording.
34
+ def refusal = scratch ? Session::SCRATCH_REFUSED : ONE_SHOT_REFUSAL
27
35
  end
28
36
 
29
37
  module_function
@@ -35,6 +43,8 @@ module Samagotchi
35
43
  def deliver(id, text:, source:, state_dir:, from_session: nil, from_cwd: nil, objectives: nil)
36
44
  owner = SessionManager.session_owner(id, state_dir: state_dir)
37
45
  return Result.new(id: id, status: :refused, queued: 0) if owner&.tui?
46
+ # Its terminal's alone, live or left behind.
47
+ return Result.new(id: id, status: :refused, queued: 0, scratch: true) if Session.load(id, state_dir: state_dir).scratch
38
48
 
39
49
  path = SessionInbox.write_note(id, text: text, source: source, from_session: from_session, from_cwd: from_cwd,
40
50
  objectives: objectives, state_dir: state_dir)
@@ -10,6 +10,7 @@ require_relative "llm_context_stale"
10
10
  require_relative "log"
11
11
  require_relative "message_time"
12
12
  require_relative "objectives_check"
13
+ require_relative "objectives_edit"
13
14
  require_relative "session"
14
15
  require_relative "session_chain"
15
16
  require_relative "session_inbox"
@@ -103,13 +104,19 @@ module Samagotchi
103
104
  new(change: change, id: id, text: kept, note: note, was: Objectives.fingerprint(item))
104
105
  end
105
106
 
106
- # What's wrong with it against +list+, nil when it can apply.
107
+ # What's wrong with it against +list+, nil when it can apply: its #line
108
+ # through the same Edit its card's Apply runs, so what Apply would refuse
109
+ # is refused here.
107
110
  def refusal(list)
108
111
  return "change must be add, reword, done or drop" unless %w[add reword done drop].include?(change)
109
112
  return "#{change} needs a text" if %w[add reword].include?(change) && text.empty?
110
- return nil if change == "add"
113
+ return Objectives.unknown(id, list) unless change == "add" || list.find(id)
111
114
 
112
- list.find(id) ? nil : "no objective #{id} (this session's: #{list.items.map(&:id).join(", ")})"
115
+ verb, rest = line.delete_prefix("/objectives ").split(/\s+/, 2)
116
+ edited = Edit.apply(list, verb, rest.to_s, was: was)
117
+ return "#{change} needs a text of its own, not only a status or a done-when" if edited.nil?
118
+
119
+ edited.is_a?(String) ? edited : nil
113
120
  end
114
121
 
115
122
  # The /objectives line that applies it. An add's text can't split in two.
@@ -240,6 +247,20 @@ module Samagotchi
240
247
  AtomicFile.write(path, JSON.generate(known.merge(card => state)))
241
248
  end
242
249
 
250
+ # The one wording per refusal: the command, a proposal, a move and a check
251
+ # say these, each adding only its own context.
252
+ # @param ids [Array<String>, String] the ids the list doesn't have
253
+ def unknown(ids, list)
254
+ known = list.items.map(&:id)
255
+ "no objective #{Array(ids).join(", ")}#{" (this session's: #{known.join(", ")})" unless known.empty?}"
256
+ end
257
+
258
+ # @param ids [Array<String>, String] dropped ids
259
+ def dropped(ids)
260
+ ids = Array(ids)
261
+ "#{ids.join(", ")} #{ids.size == 1 ? "is" : "are"} dropped; /objectives undo #{ids.join(" ")} brings #{ids.size == 1 ? "it" : "them"} back"
262
+ end
263
+
243
264
  # The id a call names, as the list spells it.
244
265
  def move_id(call) = call[:id].to_s.strip.upcase
245
266
 
@@ -6,16 +6,14 @@ require "json"
6
6
  require "securerandom"
7
7
  require "time"
8
8
 
9
+ require_relative "answer_text"
9
10
  require_relative "config"
10
11
  require_relative "context_note"
11
12
  require_relative "llm_context_stale"
12
13
  require_relative "log"
13
- require_relative "message_parts"
14
- require_relative "model_profile"
15
- require_relative "output_formatter"
14
+ require_relative "redact"
16
15
  require_relative "session"
17
16
  require_relative "steer"
18
- require_relative "tool_call_parser"
19
17
  require_relative "verdict"
20
18
 
21
19
  module Samagotchi
@@ -51,9 +49,6 @@ module Samagotchi
51
49
  LEFT_OUT = "[earlier record left out]"
52
50
  # What the agent said to the user, in the record.
53
51
  OWN_WORDS = "[agent's own words]"
54
- # A thought cut off before its close: the whole entry, from an opening at its
55
- # start. MessageParts' forms are the closed one and the template-opened one.
56
- UNCLOSED_THINK = /\A\s*<think>.*\z/m
57
52
  SYSTEM = "You check one claim a coding agent made about its own work, for the user. You see only the claim and the " \
58
53
  "session's record: what the user and others wrote, what the tools returned, and what the agent said to the " \
59
54
  "user, marked #{OWN_WORDS}. You didn't do the work and you owe the agent nothing. The agent's own words " \
@@ -77,7 +72,7 @@ module Samagotchi
77
72
  # @param args [String] the objective's id
78
73
  # @return [Array(String, Symbol)] the reply, and :ok or :error
79
74
  def run(engine, args)
80
- session = engine.session or return ["no session yet", :error]
75
+ session = engine.session or return [Session::NONE_YET, :error]
81
76
  id = args.to_s.strip.upcase
82
77
  return ["usage: /objectives check ID", :error] unless Objectives::ID.match?(id)
83
78
  # The running turn's text is theirs: it doesn't go to a side request.
@@ -98,8 +93,8 @@ module Samagotchi
98
93
  model = configured_model
99
94
  Objectives.with_lock(dir) do
100
95
  list = Objectives.current(session.id, session.messages, state_dir: engine.session_state_dir, running: conversation)
101
- item = list.find(id) or next ["no objective #{id}", :error]
102
- next ["#{id} is dropped", :error] if item.status == Objectives::DROPPED
96
+ item = list.find(id) or next [Objectives.unknown(id, list), :error]
97
+ next [Objectives.dropped(id), :error] if item.status == Objectives::DROPPED
103
98
 
104
99
  move = list.moves[id] or next ["#{id} has no move to check yet", :error]
105
100
  name = model || engine.effective_model_ref
@@ -167,7 +162,7 @@ module Samagotchi
167
162
 
168
163
  # The chat request: the objective, the claim and the record, in a frame whose
169
164
  # tag no text in them could know, so tool output can't close it. Secrets
170
- # chi's participant redaction knows (ParticipantView::Redact, by shape and
165
+ # chi's participant redaction knows (Redact, by shape and
171
166
  # by name, no model involved) don't leave: objectives.check_model may be
172
167
  # another host.
173
168
  # @param item [Objectives::Item]
@@ -182,8 +177,7 @@ module Samagotchi
182
177
  end
183
178
 
184
179
  def redacted(text)
185
- require_relative "web/participant_view"
186
- Web::ParticipantView::Redact.text(text, Web::ParticipantView::Viewer.new(cwd: nil))
180
+ Redact.text(text, Redact::Viewer.new(cwd: nil))
187
181
  end
188
182
  private_class_method :redacted
189
183
 
@@ -217,7 +211,7 @@ module Samagotchi
217
211
 
218
212
  case (entry[:role] || entry["role"]).to_s
219
213
  when "model"
220
- words = own_words(text)
214
+ words = AnswerText.visible(text)
221
215
  "#{OWN_WORDS}\n#{cut(words, OUTPUT_CHARS)}" unless words.empty?
222
216
  when "user" then "[#{Steer.participant_message?(entry) ? "participant" : "user"}]\n#{cut(text, OUTPUT_CHARS)}"
223
217
  when "system"
@@ -229,19 +223,6 @@ module Samagotchi
229
223
  end
230
224
  private_class_method :said_part
231
225
 
232
- # What the user saw of a model entry. A chat entry's content is that already
233
- # (its reasoning is the separate :thinking, its calls :tool_calls); a native
234
- # entry's is the raw response, thoughts and call markup inline.
235
- # @return [String]
236
- def own_words(text)
237
- gemma = ToolCallParser::Gemma.new(ModelProfile.gemma4)
238
- text = gemma.strip_thought(text).gsub(MessageParts::QWEN_THINK, "").sub(UNCLOSED_THINK, "")
239
- .sub(MessageParts::QWEN_OPEN_THINK, "")
240
- text = ToolCallParser.strip_calls(gemma.strip_tool_calls(text), "<tool_call>", "</tool_call>")
241
- OutputFormatter.strip_markup(text)
242
- end
243
- private_class_method :own_words
244
-
245
226
  # A multipart entry's text parts.
246
227
  def text_of(content)
247
228
  return content.to_s.scrub if content.is_a?(String)