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
@@ -25,8 +25,8 @@ module Samagotchi
25
25
  # SessionMetrics is a persistent, error-isolated collector of per-session
26
26
  # analytics. It implements the same #call(event) sink contract as
27
27
  # SessionObserver subscribers, so Engine#run_turn feeds it (it forwards every
28
- # event to its SessionObserver): the REPL, the -p/--non-interactive/--resume
29
- # paths and SessionManager background workers.
28
+ # event to its SessionObserver): a --non-interactive one-shot and the
29
+ # session workers.
30
30
  #
31
31
  # Each finished turn leaves a record (status, timings, model, prompt and
32
32
  # completion tokens, tool calls, iterations, retries, cuts); session totals are
@@ -7,6 +7,7 @@ require_relative "session_setting"
7
7
  require_relative "steer_cut"
8
8
  require_relative "steer_cut_after"
9
9
  require_relative "thinking"
10
+ require_relative "session"
10
11
 
11
12
  module Samagotchi
12
13
  # The session's settings on its effective model, as the Engine reads them:
@@ -163,7 +164,7 @@ module Samagotchi
163
164
  # @raise [ArgumentError] with no session yet
164
165
  def reasoning_history_override=(level)
165
166
  session = @session.call
166
- raise ArgumentError, "no session yet" unless session
167
+ raise ArgumentError, Session::NONE_YET unless session
167
168
 
168
169
  SessionSetting::REASONING_HISTORY.set(session, level)
169
170
  end
@@ -178,7 +179,7 @@ module Samagotchi
178
179
  # @raise [ArgumentError] with no session yet
179
180
  def thinking_override=(level)
180
181
  session = @session.call
181
- raise ArgumentError, "no session yet" unless session
182
+ raise ArgumentError, Session::NONE_YET unless session
182
183
 
183
184
  SessionSetting::THINKING.set(session, level)
184
185
  end
@@ -4,7 +4,7 @@ require_relative "steer"
4
4
  require_relative "participant_mode"
5
5
 
6
6
  module Samagotchi
7
- # Loaded only when a session is read: it brings Web::Grants (and Rack).
7
+ # Loaded only when a session is read: it brings Grants.
8
8
  autoload :Participants, File.expand_path("participants", __dir__)
9
9
 
10
10
  # What a session's Engine knows of its participants: participants.json
@@ -30,7 +30,7 @@ module Samagotchi
30
30
  DEFAULT_KEEP_STATUS = [].freeze
31
31
 
32
32
  # How long a session may sit empty before the sweep takes it: its
33
- # worker (or a REPL) deletes it as it leaves, so the sweep only catches
33
+ # worker deletes it as it leaves, so the sweep only catches
34
34
  # those killed first (a reboot, kill -9).
35
35
  EMPTY_GRACE_SECONDS = 3600
36
36
 
@@ -69,7 +69,7 @@ module Samagotchi
69
69
  end
70
70
 
71
71
  # Directories with no session file and nothing but the skeleton, nobody
72
- # owning them: a REPL killed before its first save, or a worker woken
72
+ # owning them: a one-shot killed before its first save, or a worker woken
73
73
  # just as its session was discarded.
74
74
  # @return [Array<String>] their ids
75
75
  private_class_method def self.prune_orphan_dirs(state_dir, dry_run:)
@@ -161,7 +161,7 @@ module Samagotchi
161
161
  # +any_age+ makes every session eligible, whatever its age and the
162
162
  # count (`chi sessions clean` with no --days: test runs are throwaway).
163
163
  #
164
- # A scratch session nobody owns (its REPL was killed) goes whatever its
164
+ # A scratch session nobody owns (its terminal and worker were killed) goes whatever its
165
165
  # age, its status and the count; +test_only+ takes it too.
166
166
  #
167
167
  # +max_count+ counts the sessions that stay: one deleted whatever its
@@ -21,7 +21,7 @@ module Samagotchi
21
21
  # `chi sessions`: list, stop, restart, archive/unarchive, delete, prune,
22
22
  # clean and stats sessions from the shell (bin/chi dispatches here before
23
23
  # OptionParser; the subcommands have their own flags). Not SessionCommands,
24
- # the REPL's slash commands for sessions (session_commands.rb).
24
+ # the slash commands a session's worker runs (session_commands.rb).
25
25
  class SessionsCommand
26
26
  include CLI::Command
27
27
 
@@ -207,15 +207,15 @@ module Samagotchi
207
207
  )
208
208
  case format
209
209
  when "json"
210
- # owner: "worker", "tui" (a chi REPL: it takes no notes or messages)
210
+ # owner: "worker", "tui" (a one-turn chi run: it takes no notes or messages)
211
211
  # or nil; recap: the saved recap's first sentence, or nil; project:
212
212
  # its git project's root, or nil
213
213
  # parent_id: the session that delegated it (the delegate tool), or nil
214
214
  # delegate: started by the delegate tool (a fork has a parent_id too)
215
215
  # continues: the session it continues (its chain's previous link), or nil
216
216
  # archived: hidden from the lists (only with --archived can it be true)
217
- # scratch: a `chi scratch` session (deleted when its REPL ends, a
218
- # leftover one at the next sweep)
217
+ # scratch: a `chi scratch` session (deleted when its terminal leaves,
218
+ # a leftover one at the next sweep)
219
219
  # ctx_pct: how full the context was after the last turn, or nil
220
220
  # title: the session's own name (/title; desc shows it too), or nil;
221
221
  # auto_title: the one its recap gave it (RecapTitle), or nil
@@ -8,12 +8,12 @@ require_relative "cli/flags"
8
8
  require_relative "session"
9
9
  require_relative "session_access"
10
10
  require_relative "session_access/local"
11
- require_relative "web/grants"
11
+ require_relative "grants"
12
12
  require_relative "web/qr"
13
13
 
14
14
  module Samagotchi
15
15
  # `chi share`: invite a participant to review a session (the
16
- # shared-sessions plan: a Web::Grants grant, one session, one person or
16
+ # shared-sessions plan: a Grants grant, one session, one person or
17
17
  # agent, 7 days unless --expires says otherwise), list the participants,
18
18
  # revoke them. Local or through chi --remote NAME (SessionAccess). The
19
19
  # token is printed once, in the link; chi keeps only its digest. See
@@ -95,9 +95,9 @@ module Samagotchi
95
95
  return parsed if parsed.is_a?(Integer)
96
96
 
97
97
  send(:"run_#{@sub || "invite"}", parsed.options, parsed.args)
98
- rescue SessionAccess::BadRequest, Web::Grants::BadIdentity => e
98
+ rescue SessionAccess::BadRequest, Grants::BadIdentity => e
99
99
  usage_error(e.message)
100
- rescue Web::Grants::Unwritable => e
100
+ rescue Grants::Unwritable => e
101
101
  error_line("#{command_name}: #{e.message}")
102
102
  CLI::Exit::FAILED
103
103
  rescue ArgumentError => e
@@ -114,13 +114,13 @@ module Samagotchi
114
114
  return usage_error("give one session ID") unless args.size == 1
115
115
  return usage_error("--name NAME: who the link is for") if options[:name].to_s.strip.empty?
116
116
 
117
- ttl = Web::Grants.parse_ttl(options[:expires])
117
+ ttl = Grants.parse_ttl(options[:expires])
118
118
  return usage_error("--expires takes Nh or Nd, 1h to 30d (e.g. 3d)") if ttl == false
119
119
 
120
120
  cap = options[:cap] && dollars(options[:cap])
121
121
  return usage_error(CAP_USAGE) if cap == false
122
122
 
123
- identity = options[:identity] && Web::Grants.clean_identity(options[:identity])
123
+ identity = options[:identity] && Grants.clean_identity(options[:identity])
124
124
 
125
125
  id = @access.resolve(args.first)
126
126
  invitation = @access.invite(id, name: options[:name], kind: options[:agent] ? "agent" : "human", ttl: ttl, cap: cap,
@@ -149,13 +149,13 @@ module Samagotchi
149
149
  0
150
150
  end
151
151
 
152
- CAP_USAGE = "a cap is dollars, $0 to $#{Web::Grants::MAX_CAP.to_i} (e.g. --cap 5)".freeze
152
+ CAP_USAGE = "a cap is dollars, $0 to $#{Grants::MAX_CAP.to_i} (e.g. --cap 5)".freeze
153
153
 
154
154
  def run_cap(_options, args)
155
155
  return usage_error("give a session ID, a participant and dollars (chi share cap ID p_… 5)") unless args.size == 3
156
156
 
157
157
  participant = args[1]
158
- return usage_error("not a participant: #{participant} (chi share ls lists them)") unless Web::Grants::ID_RE.match?(participant)
158
+ return usage_error("not a participant: #{participant} (chi share ls lists them)") unless Grants::ID_RE.match?(participant)
159
159
 
160
160
  cap = dollars(args[2])
161
161
  return usage_error(CAP_USAGE) if cap == false
@@ -172,7 +172,7 @@ module Samagotchi
172
172
  return false unless text.match?(/\A\d+(?:\.\d+)?\z/)
173
173
 
174
174
  value = text.to_f
175
- Web::Grants.cap?(value) ? value : false
175
+ Grants.cap?(value) ? value : false
176
176
  end
177
177
 
178
178
  def money(value) = value.nil? ? "-" : format("$%.2f", value)
@@ -184,7 +184,7 @@ module Samagotchi
184
184
  return usage_error("give a session ID (and a participant), or --all")
185
185
  end
186
186
  participant = args[1]
187
- return usage_error("not a participant: #{participant} (chi share ls lists them)") if participant && !Web::Grants::ID_RE.match?(participant)
187
+ return usage_error("not a participant: #{participant} (chi share ls lists them)") if participant && !Grants::ID_RE.match?(participant)
188
188
 
189
189
  id = args.first && @access.resolve(args.first)
190
190
  revoked = @access.revoke_participants(id, participant: participant)
@@ -136,7 +136,7 @@ module Samagotchi
136
136
  PARTICIPANT_SOURCE = "participant"
137
137
  # The owner's author, when one is written out; absent means the same.
138
138
  OWNER_AUTHOR = "owner"
139
- # A participant's author: this prefix, then its id (Web::Grants'
139
+ # A participant's author: this prefix, then its id (Grants'
140
140
  # grant id), "p:<id>" (#author_for, #participant_id).
141
141
  PARTICIPANT_PREFIX = "p:"
142
142
  # A participant's message's keys, which every message copy keeps.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "config"
4
4
  require_relative "log"
5
+ require_relative "degrade"
5
6
  require_relative "log_path"
6
7
  require_relative "memory_paths"
7
8
  require_relative "paths"
@@ -74,11 +75,7 @@ module Samagotchi
74
75
  @requested_memories = effective_preload_list(preload_memory_list(memories))
75
76
  # Read once, as the memories: list is: a config edit is picked up by
76
77
  # a new process. The files' text is read at each build.
77
- @config_appended = begin
78
- ConfigFile.appended_prompt_files
79
- rescue StandardError
80
- []
81
- end
78
+ @config_appended = Degrade.call(:prompt, "appended_list_unreadable", fallback: []) { ConfigFile.appended_prompt_files }
82
79
  @cli_appended = Array(appended_files).map { |path| File.expand_path(path.to_s) }
83
80
  end
84
81
 
@@ -386,11 +383,11 @@ module Samagotchi
386
383
  # (Plugin::PromptSections). nil with none: the prompt is what it was
387
384
  # before prompt sections.
388
385
  def plugin_prompt_section
389
- built = Array(@sections_lookup.call)
390
- @section_plugins = built
391
- Plugin::PromptSections.text(built)
392
- rescue StandardError
393
- nil
386
+ Degrade.call(:prompt, "plugin_section_failed") do
387
+ built = Array(@sections_lookup.call)
388
+ @section_plugins = built
389
+ Plugin::PromptSections.text(built)
390
+ end
394
391
  end
395
392
 
396
393
  def with_tail_declarations(prompt, chat:, layers:)
@@ -405,35 +402,35 @@ module Samagotchi
405
402
  # Identity is not tracked as an "activated" memory for the sticky status line
406
403
  # to avoid always showing `mem: identity`.
407
404
  def system_identity_section
408
- DEFAULT_SYSTEM_MEMORIES.each do |name|
409
- next if memory_muted?(name)
405
+ Degrade.call(:prompt, "identity_unreadable") do
406
+ DEFAULT_SYSTEM_MEMORIES.each do |name|
407
+ next if memory_muted?(name)
410
408
 
411
- body = Tools::MemoryRead.call(name, scope: "system", **overlay_keys)
412
- next if body.start_with?("Error:")
413
- next if body.strip.empty?
409
+ body = Tools::MemoryRead.call(name, scope: "system", **overlay_keys)
410
+ next if body.start_with?("Error:")
411
+ next if body.strip.empty?
414
412
 
415
- return "System identity (auto-loaded, scope=system):\n#{body}"
413
+ return "System identity (auto-loaded, scope=system):\n#{body}"
414
+ end
415
+ nil
416
416
  end
417
- nil
418
- rescue StandardError
419
- nil
420
417
  end
421
418
 
422
419
  # The model notes for the session's model (ModelNotes), right after
423
420
  # identity: a heading per scope, each note under its name, the models:
424
421
  # line left out. nil without any, so the prompt stays as it was.
425
422
  def model_notes_section
426
- @section_notes = []
427
- notes = Array(@model_notes_lookup.call)
428
- return nil if notes.empty?
429
-
430
- ref = @model_lookup.call&.ref
431
- notes.group_by(&:scope).map do |scope, list|
432
- heading = ref.to_s.empty? ? "Model notes (scope=#{scope}):" : "Model notes (for #{ref}, scope=#{scope}):"
433
- [heading, *list.map { |note| "## #{note.name}\n#{note.body}" }].join("\n\n")
434
- end.join("\n\n").tap { @section_notes = notes }
435
- rescue StandardError
436
- nil
423
+ Degrade.call(:prompt, "model_notes_failed") do
424
+ @section_notes = []
425
+ notes = Array(@model_notes_lookup.call)
426
+ return nil if notes.empty?
427
+
428
+ ref = @model_lookup.call&.ref
429
+ notes.group_by(&:scope).map do |scope, list|
430
+ heading = ref.to_s.empty? ? "Model notes (scope=#{scope}):" : "Model notes (for #{ref}, scope=#{scope}):"
431
+ [heading, *list.map { |note| "## #{note.name}\n#{note.body}" }].join("\n\n")
432
+ end.join("\n\n").tap { @section_notes = notes }
433
+ end
437
434
  end
438
435
 
439
436
  # ── Memory helpers ─────────────────────────────────────────────────────────
@@ -441,23 +438,23 @@ module Samagotchi
441
438
  # The session model's overlay keys, so the prompt's own memory bodies
442
439
  # (identity, preloads) get `<name>.<key>.md` as memory_read gives it.
443
440
  def overlay_keys
444
- model = @model_lookup.call
445
- return {} unless model&.key
441
+ Degrade.call(:prompt, "overlay_keys_failed", fallback: {}) do
442
+ model = @model_lookup.call
443
+ return {} unless model&.key
446
444
 
447
- { model_key: model.key, fallback_model_key: model.fallback_key }
448
- rescue StandardError
449
- {}
445
+ { model_key: model.key, fallback_model_key: model.fallback_key }
446
+ end
450
447
  end
451
448
 
452
449
  # { system: { tokens:, lines: }, project: { … } } of the index texts
453
450
  # the prompt holds; nil when they can't be measured.
454
451
  def measure_indexes(indexes)
455
- chars_per_token = MemoryBundle::IndexSize.chars_per_token
456
- %i[system project].to_h do |scope|
457
- [scope, MemoryBundle::IndexSize.of_text(scope, indexes[scope], chars_per_token: chars_per_token).figures]
452
+ Degrade.call(:prompt, "index_size_failed") do
453
+ chars_per_token = MemoryBundle::IndexSize.chars_per_token
454
+ %i[system project].to_h do |scope|
455
+ [scope, MemoryBundle::IndexSize.of_text(scope, indexes[scope], chars_per_token: chars_per_token).figures]
456
+ end
458
457
  end
459
- rescue StandardError
460
- nil
461
458
  end
462
459
 
463
460
  # The scope's index text without the muted memories' lines, nor the
@@ -473,11 +470,7 @@ module Samagotchi
473
470
  # comma-split and appended without duplicates (same ref shape as --memory:
474
471
  # bare name or scope/name).
475
472
  def preload_memory_list(cli_memories)
476
- baseline = begin
477
- ConfigFile.preloaded_memories
478
- rescue StandardError
479
- []
480
- end
473
+ baseline = Degrade.call(:prompt, "preload_list_unreadable", fallback: []) { ConfigFile.preloaded_memories }
481
474
 
482
475
  merged = Array(baseline).dup
483
476
  # For the warning when one can't be loaded: it names where it came from.
@@ -555,15 +548,15 @@ module Samagotchi
555
548
  # The AGENTS.md chain from the top of the git work tree down to the cwd
556
549
  # (PromptFiles.agents_sections), one section per file with text.
557
550
  def project_specific_description
558
- return nil if skip_agent_description?
551
+ Degrade.call(:prompt, "agents_md_unreadable") do
552
+ return nil if skip_agent_description?
559
553
 
560
- sections = PromptFiles.agents_sections(Dir.pwd)
561
- return nil if sections.empty?
554
+ sections = PromptFiles.agents_sections(Dir.pwd)
555
+ return nil if sections.empty?
562
556
 
563
- @section_files&.concat(sections.map(&:path))
564
- sections.map(&:text).join("\n\n")
565
- rescue StandardError
566
- nil
557
+ @section_files&.concat(sections.map(&:path))
558
+ sections.map(&:text).join("\n\n")
559
+ end
567
560
  end
568
561
 
569
562
  # The appended files' text, right after AGENTS.md (the stable, cached
@@ -572,26 +565,26 @@ module Samagotchi
572
565
  # then --append-system-prompt's. A file the AGENTS.md chain holds, a
573
566
  # repeat and an empty or vanished file are left out.
574
567
  def appended_section
575
- cwd = Dir.pwd
576
- top = MemoryPaths.work_tree_root(cwd)
577
- seen = Set.new((skip_agent_description? ? [] : PromptFiles.agents_paths(cwd)).map { |path| canonical_path(path) })
578
- sections = []
579
- appended_candidates(cwd).each do |path, source|
580
- next unless seen.add?(canonical_path(path))
581
-
582
- unless File.file?(path)
583
- Log.debug(:config, "appended_prompt_file_missing", path: path, source: source)
584
- next
585
- end
586
- text = PromptFiles.section(path, "Appended from #{PromptFiles.display_path(path, top: top)}:")
587
- next unless text
568
+ Degrade.call(:prompt, "appended_files_failed") do
569
+ cwd = Dir.pwd
570
+ top = MemoryPaths.work_tree_root(cwd)
571
+ seen = Set.new((skip_agent_description? ? [] : PromptFiles.agents_paths(cwd)).map { |path| canonical_path(path) })
572
+ sections = []
573
+ appended_candidates(cwd).each do |path, source|
574
+ next unless seen.add?(canonical_path(path))
575
+
576
+ unless File.file?(path)
577
+ Log.debug(:config, "appended_prompt_file_missing", path: path, source: source)
578
+ next
579
+ end
580
+ text = PromptFiles.section(path, "Appended from #{PromptFiles.display_path(path, top: top)}:")
581
+ next unless text
588
582
 
589
- @section_files&.push(path)
590
- sections << text
583
+ @section_files&.push(path)
584
+ sections << text
585
+ end
586
+ sections.empty? ? nil : sections.join("\n\n")
591
587
  end
592
- sections.empty? ? nil : sections.join("\n\n")
593
- rescue StandardError
594
- nil
595
588
  end
596
589
 
597
590
  # [absolute path, "config" | "cli"], in prompt order.
@@ -617,27 +610,27 @@ module Samagotchi
617
610
  # The home directory is spelled out once so the model copies the right
618
611
  # sequence, with the advice to write it as ~ or $HOME instead.
619
612
  def project_location
620
- cwd = Dir.pwd
621
- top = MemoryPaths.work_tree_root(cwd)
622
- lines = ["Current working directory:", cwd]
623
- if top && top != cwd
624
- lines << "Top of this checkout (where the project's commands run from; stay inside it):"
625
- lines << top
626
- elsif top
627
- lines << "This checkout (the project's files; its commands run here):"
628
- lines << top
629
- end
630
- if top && MemoryPaths.project_root(cwd) != top
631
- lines << "This checkout is a linked git worktree: read, edit, run and commit only in it, never in the " \
632
- "repository's other checkouts."
613
+ Degrade.call(:prompt, "location_failed") do
614
+ cwd = Dir.pwd
615
+ top = MemoryPaths.work_tree_root(cwd)
616
+ lines = ["Current working directory:", cwd]
617
+ if top && top != cwd
618
+ lines << "Top of this checkout (where the project's commands run from; stay inside it):"
619
+ lines << top
620
+ elsif top
621
+ lines << "This checkout (the project's files; its commands run here):"
622
+ lines << top
623
+ end
624
+ if top && MemoryPaths.project_root(cwd) != top
625
+ lines << "This checkout is a linked git worktree: read, edit, run and commit only in it, never in the " \
626
+ "repository's other checkouts."
627
+ end
628
+ home = Dir.home
629
+ lines << "Home directory: #{home} (write it as ~ or $HOME in commands and paths)" unless home.to_s.empty?
630
+ lines << "Project memories folder (your notes about the project, not its files; never work or run commands there):"
631
+ lines << Paths.tilde(Tools::MemoryRead.memories_dir("project"))
632
+ lines.join("\n")
633
633
  end
634
- home = Dir.home
635
- lines << "Home directory: #{home} (write it as ~ or $HOME in commands and paths)" unless home.to_s.empty?
636
- lines << "Project memories folder (your notes about the project, not its files; never work or run commands there):"
637
- lines << Paths.tilde(Tools::MemoryRead.memories_dir("project"))
638
- lines.join("\n")
639
- rescue StandardError
640
- nil
641
634
  end
642
635
 
643
636
  # Only while the session has participants (people its owner invited to
@@ -662,12 +655,12 @@ module Samagotchi
662
655
  # once a participant invited to act is in a working session, the review
663
656
  # line for review participants; nothing with none (nil, false).
664
657
  def participants_section
665
- mode = @participants_lookup.call
666
- return nil unless mode
658
+ Degrade.call(:prompt, "participants_line_failed") do
659
+ mode = @participants_lookup.call
660
+ return nil unless mode
667
661
 
668
- mode == ParticipantMode::PAIR ? PAIR_PARTICIPANTS_LINE : PARTICIPANTS_LINE
669
- rescue StandardError
670
- nil
662
+ mode == ParticipantMode::PAIR ? PAIR_PARTICIPANTS_LINE : PARTICIPANTS_LINE
663
+ end
671
664
  end
672
665
 
673
666
  # The model this session runs on. A model guesses its name from
@@ -675,19 +668,19 @@ module Samagotchi
675
668
  # says to answer from here. It depends only on the effective model, and
676
669
  # the prompt is rebuilt only on a switch (#reset!): no per-turn churn.
677
670
  def current_model
678
- model = @model_lookup.call
679
- return nil unless model && !model.ref.to_s.empty?
680
-
681
- details = [model.host, model.key && "model key #{model.key}", model.served && "the server says it serves #{model.served}"]
682
- details = details.compact.reject(&:empty?)
683
- where = details.empty? ? "" : " (#{details.join("; ")})"
684
- "Model: this session runs on #{model.ref}#{where}.\n" \
685
- "Asked which model you are, answer with this line, not from training: a fine-tuned model often knows only " \
686
- "its base model's name, but this session runs what is named here; it changes only with /model. " \
687
- "Guidance for this model only goes in a model note: a memory named model_notes_<name> whose first line is " \
688
- "`models: <glob on this model's id or key>`."
689
- rescue StandardError
690
- nil
671
+ Degrade.call(:prompt, "model_line_failed") do
672
+ model = @model_lookup.call
673
+ return nil unless model && !model.ref.to_s.empty?
674
+
675
+ details = [model.host, model.key && "model key #{model.key}", model.served && "the server says it serves #{model.served}"]
676
+ details = details.compact.reject(&:empty?)
677
+ where = details.empty? ? "" : " (#{details.join("; ")})"
678
+ "Model: this session runs on #{model.ref}#{where}.\n" \
679
+ "Asked which model you are, answer with this line, not from training: a fine-tuned model often knows only " \
680
+ "its base model's name, but this session runs what is named here; it changes only with /model. " \
681
+ "Guidance for this model only goes in a model note: a memory named model_notes_<name> whose first line is " \
682
+ "`models: <glob on this model's id or key>`."
683
+ end
691
684
  end
692
685
 
693
686
  # Fixed for the session's lifetime, so it doesn't churn the prompt cache.
@@ -701,7 +694,7 @@ module Samagotchi
701
694
  line = "Current session id: #{id} (resume later with `chi --resume #{id}`)"
702
695
  # The log path too: asked what went wrong, a model that has to look
703
696
  # it up guesses ~/.local/state first (the self-awareness probes).
704
- log = begin; LogPath.resolve; rescue StandardError; nil; end
697
+ log = Degrade.call(:prompt, "log_path_failed") { LogPath.resolve }
705
698
  line = "#{line}\nMy debug log: #{log} (one record per line; this session's carry sid=#{id[0, Log::SID_LENGTH]})" if log
706
699
  parent = session.parent_id.to_s
707
700
  return line if parent.empty?
@@ -9,7 +9,7 @@ require_relative "tools/task_runtime"
9
9
  module Samagotchi
10
10
  # The background tasks a worker's session started that the model hasn't
11
11
  # seen finish (docs/internals/background-tasks.md, Finish wake). Held by
12
- # the Worker (the --no-shared REPL has none) and fed by the Engine's raw
12
+ # the Worker (a --non-interactive one-shot has none) and fed by the Engine's raw
13
13
  # :tool_call_completed events (#observe), the input Bridge#own_task_ids
14
14
  # reads too; the idle loop takes the finished ones (#take_finished) for a
15
15
  # wake turn or a plain note. In memory only: a crash loses them, and they