samagotchi 0.153.0 → 0.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/README.md +2 -3
  4. data/bin/chi +23 -24
  5. data/docs/architecture.md +30 -33
  6. data/docs/broadcast.md +5 -5
  7. data/docs/bundles.md +9 -12
  8. data/docs/cli.md +80 -96
  9. data/docs/configuration.md +27 -26
  10. data/docs/context.md +3 -3
  11. data/docs/desktop.md +1 -1
  12. data/docs/guardrails.md +8 -10
  13. data/docs/hooks.md +9 -9
  14. data/docs/internals/attached-context.md +2 -2
  15. data/docs/internals/background-tasks.md +3 -4
  16. data/docs/internals/block-renderers.md +4 -0
  17. data/docs/internals/bridge.md +3 -3
  18. data/docs/internals/context-telemetry.md +1 -1
  19. data/docs/internals/engine.md +33 -7
  20. data/docs/internals/objectives.md +6 -5
  21. data/docs/internals/participants.md +7 -7
  22. data/docs/internals/prompt-caching.md +1 -1
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-push.md +1 -1
  25. data/docs/memory.md +2 -2
  26. data/docs/plugins.md +23 -28
  27. data/docs/sessions.md +24 -24
  28. data/docs/sub-agent.md +2 -2
  29. data/docs/system-one.md +1 -1
  30. data/docs/testing.md +16 -6
  31. data/lib/samagotchi/answer_text.rb +34 -0
  32. data/lib/samagotchi/bridge.rb +43 -32
  33. data/lib/samagotchi/bridge_client.rb +10 -9
  34. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  35. data/lib/samagotchi/broadcast/triage.rb +1 -1
  36. data/lib/samagotchi/broadcast_command.rb +5 -5
  37. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  38. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  39. data/lib/samagotchi/bundle_command.rb +2 -2
  40. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  41. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  42. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  44. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  45. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  46. data/lib/samagotchi/child_move.rb +3 -3
  47. data/lib/samagotchi/commands/registry.rb +6 -6
  48. data/lib/samagotchi/composer_route.rb +4 -5
  49. data/lib/samagotchi/config.rb +14 -12
  50. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  51. data/lib/samagotchi/context_candidates.rb +4 -3
  52. data/lib/samagotchi/continue_offer.rb +1 -1
  53. data/lib/samagotchi/debug_log.rb +1 -1
  54. data/lib/samagotchi/degrade.rb +75 -0
  55. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  56. data/lib/samagotchi/engine/cards.rb +87 -0
  57. data/lib/samagotchi/engine/context_judge.rb +138 -0
  58. data/lib/samagotchi/engine/events.rb +131 -0
  59. data/lib/samagotchi/engine/guardrails.rb +87 -0
  60. data/lib/samagotchi/engine/hooks.rb +119 -0
  61. data/lib/samagotchi/engine/messages.rb +124 -0
  62. data/lib/samagotchi/engine/model_settings.rb +127 -0
  63. data/lib/samagotchi/engine/models.rb +261 -0
  64. data/lib/samagotchi/engine/objectives.rb +116 -0
  65. data/lib/samagotchi/engine/participants.rb +86 -0
  66. data/lib/samagotchi/engine/plugins.rb +208 -0
  67. data/lib/samagotchi/engine/questions.rb +98 -0
  68. data/lib/samagotchi/engine/recap.rb +130 -0
  69. data/lib/samagotchi/engine/session.rb +131 -0
  70. data/lib/samagotchi/engine/side_requests.rb +135 -0
  71. data/lib/samagotchi/engine/steering.rb +134 -0
  72. data/lib/samagotchi/engine/toolkit.rb +269 -0
  73. data/lib/samagotchi/engine/turn_end.rb +180 -0
  74. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  75. data/lib/samagotchi/engine/verify.rb +153 -0
  76. data/lib/samagotchi/engine.rb +62 -2646
  77. data/lib/samagotchi/grants.rb +575 -0
  78. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  79. data/lib/samagotchi/guardrails/context.rb +2 -3
  80. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  81. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  82. data/lib/samagotchi/hook_routes.rb +3 -3
  83. data/lib/samagotchi/idle_recap.rb +4 -5
  84. data/lib/samagotchi/idle_reminders.rb +3 -4
  85. data/lib/samagotchi/iteration_limit.rb +2 -2
  86. data/lib/samagotchi/kernel_loop.rb +10 -11
  87. data/lib/samagotchi/launch_mode.rb +13 -32
  88. data/lib/samagotchi/level_setting_command.rb +3 -2
  89. data/lib/samagotchi/llm_context_command.rb +3 -2
  90. data/lib/samagotchi/log.rb +7 -7
  91. data/lib/samagotchi/log_line.rb +1 -1
  92. data/lib/samagotchi/log_path.rb +1 -1
  93. data/lib/samagotchi/note_command.rb +1 -1
  94. data/lib/samagotchi/note_delivery.rb +18 -8
  95. data/lib/samagotchi/objectives.rb +24 -3
  96. data/lib/samagotchi/objectives_check.rb +8 -27
  97. data/lib/samagotchi/objectives_command.rb +4 -69
  98. data/lib/samagotchi/objectives_edit.rb +87 -0
  99. data/lib/samagotchi/output_formatter.rb +7 -1
  100. data/lib/samagotchi/owner_lock.rb +7 -1
  101. data/lib/samagotchi/parent_answers_command.rb +5 -4
  102. data/lib/samagotchi/participant_exposure.rb +42 -0
  103. data/lib/samagotchi/participant_mode.rb +1 -1
  104. data/lib/samagotchi/participant_name.rb +1 -1
  105. data/lib/samagotchi/participants.rb +17 -17
  106. data/lib/samagotchi/participants_watch.rb +1 -1
  107. data/lib/samagotchi/pending_input_queue.rb +2 -2
  108. data/lib/samagotchi/plugin/api.rb +3 -2
  109. data/lib/samagotchi/plugin/context.rb +21 -7
  110. data/lib/samagotchi/plugin/peer.rb +2 -1
  111. data/lib/samagotchi/plugin/sessions.rb +6 -4
  112. data/lib/samagotchi/plugin_tasks.rb +1 -2
  113. data/lib/samagotchi/private_files.rb +29 -0
  114. data/lib/samagotchi/project_env.rb +2 -2
  115. data/lib/samagotchi/question_desk.rb +8 -7
  116. data/lib/samagotchi/recap_command.rb +2 -1
  117. data/lib/samagotchi/recap_setup.rb +1 -1
  118. data/lib/samagotchi/recap_store.rb +1 -1
  119. data/lib/samagotchi/redact.rb +86 -0
  120. data/lib/samagotchi/reminder_queue.rb +2 -2
  121. data/lib/samagotchi/remote_attach_command.rb +1 -1
  122. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  123. data/lib/samagotchi/review_start.rb +1 -1
  124. data/lib/samagotchi/run_options.rb +6 -6
  125. data/lib/samagotchi/self_report.rb +42 -39
  126. data/lib/samagotchi/send_command/busy.rb +1 -1
  127. data/lib/samagotchi/send_command.rb +4 -1
  128. data/lib/samagotchi/session.rb +11 -2
  129. data/lib/samagotchi/session_access/contract.rb +3 -3
  130. data/lib/samagotchi/session_access/local.rb +9 -9
  131. data/lib/samagotchi/session_access/remote.rb +17 -7
  132. data/lib/samagotchi/session_access.rb +4 -4
  133. data/lib/samagotchi/session_archive_command.rb +2 -2
  134. data/lib/samagotchi/session_chain.rb +8 -6
  135. data/lib/samagotchi/session_commands.rb +45 -48
  136. data/lib/samagotchi/session_continue.rb +1 -1
  137. data/lib/samagotchi/session_delete_command.rb +4 -4
  138. data/lib/samagotchi/session_manager.rb +59 -36
  139. data/lib/samagotchi/session_metrics.rb +2 -2
  140. data/lib/samagotchi/session_model_settings.rb +3 -2
  141. data/lib/samagotchi/session_participants.rb +1 -1
  142. data/lib/samagotchi/session_retention.rb +3 -3
  143. data/lib/samagotchi/sessions_command.rb +4 -4
  144. data/lib/samagotchi/share_command.rb +10 -10
  145. data/lib/samagotchi/steer.rb +1 -1
  146. data/lib/samagotchi/system_prompt.rb +102 -109
  147. data/lib/samagotchi/task_watch.rb +1 -1
  148. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  149. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  150. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  151. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  152. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  153. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  154. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  155. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  156. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  157. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  158. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  159. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  160. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  161. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  163. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  164. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  165. data/lib/samagotchi/terminal_ui.rb +142 -1387
  166. data/lib/samagotchi/title_command.rb +2 -2
  167. data/lib/samagotchi/tool_runner.rb +14 -16
  168. data/lib/samagotchi/toolkit_command.rb +2 -1
  169. data/lib/samagotchi/tools/delegate.rb +3 -1
  170. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  171. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  172. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  173. data/lib/samagotchi/tools/send_note.rb +2 -1
  174. data/lib/samagotchi/turn_endings.rb +2 -2
  175. data/lib/samagotchi/turn_flow.rb +4 -5
  176. data/lib/samagotchi/update_hint.rb +2 -2
  177. data/lib/samagotchi/version.rb +1 -1
  178. data/lib/samagotchi/web/app.rb +39 -50
  179. data/lib/samagotchi/web/participant_access.rb +2 -2
  180. data/lib/samagotchi/web/participant_review.rb +5 -5
  181. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  182. data/lib/samagotchi/web/participant_view.rb +2 -82
  183. data/lib/samagotchi/web/participants_admin.rb +1 -1
  184. data/lib/samagotchi/web/public/block_host.js +35 -8
  185. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  186. data/lib/samagotchi/web/public/chains.js +2 -2
  187. data/lib/samagotchi/web/public/claim_link.js +4 -4
  188. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  189. data/lib/samagotchi/web/public/format.js +3 -3
  190. data/lib/samagotchi/web/push/store.rb +1 -1
  191. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  192. data/lib/samagotchi/web/session_control.rb +12 -8
  193. data/lib/samagotchi/web/session_create.rb +1 -1
  194. data/lib/samagotchi/web/session_hub.rb +2 -2
  195. data/lib/samagotchi/web/session_show.rb +2 -2
  196. data/lib/samagotchi/web/session_summary.rb +1 -1
  197. data/lib/samagotchi/worker.rb +100 -9
  198. data/lib/samagotchi/worker_link.rb +3 -3
  199. data/lib/samagotchi/working_set_command.rb +3 -2
  200. metadata +29 -6
  201. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  202. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  203. data/lib/samagotchi/web/grants.rb +0 -575
  204. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  205. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -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
@@ -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