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
@@ -47,7 +47,7 @@ module Samagotchi
47
47
  # What a continue does with the previous link's children: +move+ the
48
48
  # direct children whose subtree has an open node (they go to the new
49
49
  # link, their subtrees with them), +refuse+ the open ones it can't move
50
- # (SessionContinue::OpenChild, why: an older chi worker or a chi REPL).
50
+ # (SessionContinue::OpenChild, why: an older chi worker or a one-turn chi run).
51
51
  Plan = Data.define(:move, :refuse)
52
52
  # A move under way: from the session, the children moved, and those
53
53
  # already sent their note (so a move finished again notes no one twice).
@@ -87,14 +87,14 @@ module Samagotchi
87
87
  Plan.new(move: move, refuse: refuse)
88
88
  end
89
89
 
90
- # Why a child's live owner keeps it from moving: a chi REPL holds its
90
+ # Why a child's live owner keeps it from moving: a one-turn chi run holds its
91
91
  # own Session and never reads the override; a worker on an older chi
92
92
  # neither. A worker with no sidecar yet is starting, on the newest chi
93
93
  # installed (SessionManager.worker_command): it moves.
94
94
  # @return [String, nil]
95
95
  def unmovable(child_id, state_dir:)
96
96
  link = WorkerLink.resolve(Session.session_dir(child_id, state_dir: state_dir), unlink: false)
97
- return "open in a chi REPL; close it there" if link.owner&.tui?
97
+ return "#{OwnerLock::TUI_BUSY}; let it finish" if link.owner&.tui?
98
98
  return nil if !link.live? || link.features.include?(FEATURE)
99
99
 
100
100
  "older chi worker#{" #{link.version}" if link.version}; restart it"
@@ -29,9 +29,9 @@ module Samagotchi
29
29
  # @!attribute mid_turn [Symbol, #call] what a line of it does while a
30
30
  # turn runs (MID_TURN): a symbol, or a lambda line (stripped) → one
31
31
  # @!attribute local [Boolean] the UI runs it; completion and help only
32
- # @!attribute uis [Array<Symbol>, nil] a local entry's UIs (:repl,
33
- # :attached, :web) for completion and /help; nil means every UI
34
- # (for a local entry, every terminal)
32
+ # @!attribute uis [Array<Symbol>, nil] a local entry's UIs (:attached,
33
+ # :web) for completion and /help; nil means every UI (for a local
34
+ # entry, the terminal)
35
35
  # @!attribute match [#call] line (stripped) → whether it is this command
36
36
  # @!attribute handler [Proc, nil] runs it; SessionCommands instance_execs
37
37
  # a built-in's with the line, and calls a bundle's with the text
@@ -165,8 +165,8 @@ module Samagotchi
165
165
  end
166
166
 
167
167
  # @return [Entry, nil] the first entry the UI runs itself (local) that
168
- # matches +line+, whatever its uis (a UI answers the others' too:
169
- # the REPL's /detach note)
168
+ # matches +line+, whatever its uis (a UI answers the others' too,
169
+ # with a note)
170
170
  def lookup_local(line)
171
171
  text = line.to_s.strip
172
172
  @entries.find { |entry| entry.local && entry.match?(text) }
@@ -190,7 +190,7 @@ module Samagotchi
190
190
  end
191
191
  end
192
192
 
193
- # @param ui [Symbol] :repl or :attached
193
+ # @param ui [Symbol] :attached
194
194
  # @return [Array<String>] the /names Tab offers in +ui+, sorted
195
195
  def completions(ui)
196
196
  @entries.select { |entry| entry.slash? && entry.in_ui?(ui) }.map(&:name).uniq.sort
@@ -3,11 +3,10 @@
3
3
  require_relative "delivery"
4
4
 
5
5
  module Samagotchi
6
- # Where a line typed at a terminal's prompt goes, for both terminals (the
7
- # REPL's TerminalUI#steer_line / #run_input_line, the attached
8
- # AttachedLoop#submit). It only classifies; each UI keeps what it does
9
- # with the answer (the REPL's mid-turn command policy and image lines, the
10
- # attached TUI's continue offer and question). The web's composer.js
6
+ # Where a line typed at a terminal's prompt goes (AttachedLoop#submit; a
7
+ # -p prompt's /cut or /queue: TerminalUI#run). It only classifies; each
8
+ # UI keeps what it does with the answer (the attached TUI's continue
9
+ # offer and question). The web's composer.js
11
10
  # routeComposerLine decides the same; spec/fixtures/composer_routes.json
12
11
  # holds the cases both answer (docs/testing.md).
13
12
  #
@@ -119,7 +119,7 @@ module Samagotchi
119
119
  check: ->(value) { "N or N-M (1-#{IdleRecap::RecapPrompt::MAX_SENTENCES})" unless IdleRecap::RecapPrompt.sentences_range(value) }),
120
120
 
121
121
  # chi broadcast (Broadcast::Recipients, Broadcast::Tags): a session that ended no turn in this many hours (and
122
- # no worker or REPL runs) gets no broadcast; a ticket id in the note and in a session's branch or prompts
122
+ # no worker runs) gets no broadcast; a ticket id in the note and in a session's branch or prompts
123
123
  # delivers it there without triage.
124
124
  Entry.new(key: "broadcast.active_hours", yaml_path: %w[broadcast active_hours], type: :float, default: 8.0, expose: %i[env config], lifecycle: :next_broadcast),
125
125
  Entry.new(key: "broadcast.ticket_pattern", yaml_path: %w[broadcast ticket_pattern], type: :string, default: '\b[A-Z][A-Z0-9]+-\d+\b', expose: %i[env config], lifecycle: :next_broadcast),
@@ -140,8 +140,6 @@ module Samagotchi
140
140
  Entry.new(key: "session.idle_exit_minutes", yaml_path: %w[session idle_exit_minutes], type: :float, default: 30.0, expose: %i[env config cli], lifecycle: :worker_start),
141
141
  # Tools follow the project: a worker applies its folder's `mise env` at start (ProjectEnv); off keeps the spawner's env.
142
142
  Entry.new(key: "session.project_env", yaml_path: %w[session project_env], type: :enum, default: "mise", expose: %i[env config], enum_values: %w[mise off], lifecycle: :worker_start),
143
- # Plain `chi` runs like `chi --shared` (bin/chi, LaunchMode); false, or --no-shared per run, keeps the plain REPL. No CLI flag: that would duplicate --shared.
144
- Entry.new(key: "session.shared", yaml_path: %w[session shared], type: :bool, default: true, expose: %i[env config], lifecycle: :next_invocation),
145
143
  # false: a continue's carry note holds the previous link's recap only (SessionChain.note_text), not its last answer too.
146
144
  Entry.new(key: "session.carry_last_answer", yaml_path: %w[session carry_last_answer], type: :bool, default: true, expose: %i[env config], lifecycle: :next_lifecycle_decision),
147
145
  # false: a session nothing happened in is deleted when it is left (SessionManager.discardable?).
@@ -698,7 +696,7 @@ module Samagotchi
698
696
  # Live resolve so ENV changes (as in specs) are reflected without explicit reload
699
697
  # Use current ENV and file (via ConfigFile's cached reader), plus any CLI
700
698
  # overrides captured via reload!
701
- path = Samagotchi::ConfigFile.global_path rescue nil
699
+ path = Samagotchi::ConfigFile.global_path rescue nil # degrade-ok: Config↔Log: Log resolves its file and level through Config
702
700
  file_data = Samagotchi::ConfigFile.read_yaml(path: path) if path
703
701
  resolve_with_origin(entry.key, file_data: file_data, env: ENV, cli_overrides: cli_overrides, session: session)
704
702
  end
@@ -817,7 +815,7 @@ module Samagotchi
817
815
  end
818
816
 
819
817
  # Keys replaced by another, and what the warning adds: it names the new
820
- # one (no alias).
818
+ # one (no alias). A key removed with nothing in its place has nil.
821
819
  PARENT_ANSWERS_HINT = "hard, standard or soft; see docs/guardrails.md"
822
820
  SYSTEM_ONE_MOVED_HINT = "it has moved: until all of host, model and timeout_ms have, nothing is judged or " \
823
821
  "auto-allowed, and every ask goes to you; see docs/configuration.md"
@@ -826,14 +824,18 @@ module Samagotchi
826
824
  "turn.parent_continue" => ["guardrails.parent_answers", PARENT_ANSWERS_HINT],
827
825
  "guardrails.system_one.host" => ["system_one.host", SYSTEM_ONE_MOVED_HINT],
828
826
  "guardrails.system_one.model" => ["system_one.model", SYSTEM_ONE_MOVED_HINT],
829
- "guardrails.system_one.timeout_ms" => ["system_one.timeout_ms", SYSTEM_ONE_MOVED_HINT]
827
+ "guardrails.system_one.timeout_ms" => ["system_one.timeout_ms", SYSTEM_ONE_MOVED_HINT],
828
+ "session.shared" => [nil, "chi always runs attached"]
830
829
  }.freeze
831
830
 
832
831
  # nil when +dotted+ is a key the file may hold.
833
832
  def key_problem(dotted)
834
833
  return nil if known_config_keys.include?(dotted)
835
834
  return "config: #{ConfigFile.removed_transport_problem("'#{dotted}'")}" if dotted == "server.transport"
835
+
836
836
  if (replaced = REPLACED_KEYS[dotted])
837
+ return "config: unknown key '#{dotted}': removed: #{replaced.last}" unless replaced.first
838
+
837
839
  return "config: unknown key '#{dotted}': replaced by '#{replaced.first}' (#{replaced.last})"
838
840
  end
839
841
 
@@ -1231,7 +1233,7 @@ module Samagotchi
1231
1233
  return [] unless raw.is_a?(Hash)
1232
1234
 
1233
1235
  raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && HostConfig.disabled?(cfg) }
1234
- rescue StandardError
1236
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1235
1237
  []
1236
1238
  end
1237
1239
 
@@ -1277,7 +1279,7 @@ module Samagotchi
1277
1279
 
1278
1280
  items = raw.is_a?(Array) ? raw : [raw]
1279
1281
  items.flat_map { |v| v.to_s.split(",") }.map(&:strip).reject(&:empty?)
1280
- rescue StandardError
1282
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1281
1283
  []
1282
1284
  end
1283
1285
 
@@ -1296,7 +1298,7 @@ module Samagotchi
1296
1298
  else []
1297
1299
  end
1298
1300
  items.select { |v| v.is_a?(String) }.map(&:strip).reject(&:empty?).filter_map { |v| expand_appended_entry(v) }
1299
- rescue StandardError
1301
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1300
1302
  []
1301
1303
  end
1302
1304
 
@@ -1355,7 +1357,7 @@ module Samagotchi
1355
1357
  end
1356
1358
 
1357
1359
  raw.each_with_object({}) { |(name, value), acc| acc[name.to_s] = value.is_a?(Hash) ? value : {} }
1358
- rescue StandardError
1360
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1359
1361
  {}
1360
1362
  end
1361
1363
 
@@ -1414,7 +1416,7 @@ module Samagotchi
1414
1416
 
1415
1417
  result[key.downcase] = val
1416
1418
  end
1417
- rescue StandardError
1419
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1418
1420
  {}
1419
1421
  end
1420
1422
 
@@ -1432,7 +1434,7 @@ module Samagotchi
1432
1434
  settings = ModelSettings.parse(key, v)
1433
1435
  result[key] = settings if settings
1434
1436
  end
1435
- rescue StandardError
1437
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1436
1438
  {}
1437
1439
  end
1438
1440
 
@@ -117,7 +117,7 @@ module Samagotchi
117
117
  # @param show_card [#call] (**card) → replaces the card
118
118
  # @return [Array(Symbol, String)] :ok or :error, and the answer
119
119
  def choose(action, name, session:, state_dir:, show_card:)
120
- return [:error, "no session yet"] unless session&.id
120
+ return [:error, Session::NONE_YET] unless session&.id
121
121
  return [:error, "a review session takes no context candidates"] if session.review?
122
122
  return [:error, USAGE] if name.to_s.empty?
123
123
 
@@ -176,7 +176,7 @@ module Samagotchi
176
176
  .filter_map { |candidate, state| candidate if state == "proposed" }
177
177
  end
178
178
 
179
- # Turn start (Worker#run_prompt before its absorb; the REPL's turn):
179
+ # Turn start (Worker#run_prompt before its absorb; a one-shot's turn):
180
180
  # the proposed candidates +prompt+ (or an earlier owner prompt: a late
181
181
  # proposal) mentions are attached, then each gets one first fetch
182
182
  # (together within +seconds+), so the note reaches this turn. Only an
@@ -192,7 +192,7 @@ module Samagotchi
192
192
  return [] if own.candidates.empty? || !OwnerPrompt.turn?(session: session, author: author)
193
193
 
194
194
  # The earlier owner prompts too: a proposal that came while one ran
195
- # (the REPL's plugins never see a running turn's prompt).
195
+ # (a one-shot's plugins never see a running turn's prompt).
196
196
  texts = owner_texts(session.messages) + (prompt.is_a?(String) ? [prompt] : [])
197
197
  found = hits(texts, proposed(session.id, project_root: session.project_root, state_dir: state_dir))
198
198
  names = found.filter_map { |hit| attach(hit, own: own, state_dir: state_dir) }
@@ -203,7 +203,8 @@ module Samagotchi
203
203
  []
204
204
  end
205
205
 
206
- # The REPL's turn start (it has no ContextAbsorber running): the notes
206
+ # A --non-interactive one-shot's turn start (it has no ContextAbsorber
207
+ # running, TerminalUI#attach_mentioned_context): the notes
207
208
  # of the sources attached from candidates, as the worker's absorb gives
208
209
  # them, added to +engine+'s session and marked delivered.
209
210
  # @return [Integer] how many notes were added
@@ -134,7 +134,7 @@ module Samagotchi
134
134
  # The continue offer was answered yes: resume the conversation without a
135
135
  # user message, with the iteration limit the offer's turn had, from the
136
136
  # run that turn started (TurnFlow#resume_from: its reasoning stays). A failure
137
- # keeps the offer, as the REPL does ("continue prompt preserved"), and
137
+ # keeps the offer ("continue prompt preserved"), and
138
138
  # asks again, one before the turn began too (logged, no :turn_failed).
139
139
  # The answer's text is carried into the turn as it begins; a turn that
140
140
  # never began leaves it to no other.
@@ -101,7 +101,7 @@ module Samagotchi
101
101
 
102
102
  def close_io
103
103
  @io&.close
104
- rescue StandardError
104
+ rescue StandardError # degrade-ok: the log writer itself: a failure here must not recurse into Log
105
105
  nil
106
106
  ensure
107
107
  @io = nil
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "log"
4
+
5
+ module Samagotchi
6
+ # A logged degradation: a piece of work whose failure has a fallback (a
7
+ # prompt section left out, an empty list, nil), with a record in the debug
8
+ # log instead of silence.
9
+ #
10
+ # Degrade.call(:prompt, "model_notes_unreadable", fallback: nil, model: name) { read_notes }
11
+ # Degrade.call(:bridge, "state_unreadable", fallback: {}, rescue: [SystemCallError, JSON::ParserError]) { … }
12
+ #
13
+ # Runs the block and returns its value. When it raises one of +rescue:+
14
+ # (StandardError by default; never Exception), returns +fallback+ and logs
15
+ # Log.warn(area, event, error:, msg:, **fields) the first time this
16
+ # process sees [area, event], Log.debug after: a hot path that fails on
17
+ # every call writes one WARN, and the happy path takes no lock. Nothing
18
+ # goes to stderr (a worker has none; a terminal line would be noise).
19
+ #
20
+ # A `return` in the block returns from the enclosing method as usual; a
21
+ # `next` or `break` would change what the site does, so a site that needs
22
+ # one logs by hand (Degrade.note) instead.
23
+ #
24
+ # Written Degrade.call (Style/LambdaCall turns Degrade.() into it).
25
+ #
26
+ # +area+ is a Log tag (LogLine::TAGS), +event+ a slug saying what was
27
+ # lost; with Log's strict mode on (the specs) both are checked on every
28
+ # call, the happy path's included.
29
+ module Degrade
30
+ MSG_CHARS = 500
31
+
32
+ @seen = Set.new
33
+ @mutex = Mutex.new
34
+
35
+ class << self
36
+ # @param area [Symbol] a Log tag
37
+ # @param event [String, Symbol] what was lost (agents_md_unreadable)
38
+ # @param fallback [Object] the value when the block raises
39
+ # @param fields [Hash] extra record fields; +rescue:+ (a class or an
40
+ # array of them, default StandardError) is the one taken out
41
+ # @return the block's value, or +fallback+
42
+ def call(area, event, fallback: nil, **fields)
43
+ classes = Array(fields.delete(:rescue) || StandardError)
44
+ check!(area, event) if Log.strict
45
+ begin
46
+ yield
47
+ rescue *classes => e
48
+ note(area, event, e, **fields)
49
+ fallback
50
+ end
51
+ end
52
+
53
+ # Log a degradation the caller has already handled (its rescue keeps
54
+ # a next/break or a fallback of its own): the same once-per-process
55
+ # WARN, DEBUG after.
56
+ # @return nil
57
+ def note(area, event, error, **fields)
58
+ first = @mutex.synchronize { @seen.add?([area.to_sym, event.to_s]) }
59
+ record = { error: error.class.name, msg: error.message.to_s[0, MSG_CHARS], **fields }
60
+ first ? Log.warn(area, event.to_s, **record) : Log.debug(area, event.to_s, **record)
61
+ nil
62
+ end
63
+
64
+ # Forget what was logged (specs, with Log.reset!).
65
+ def reset! = @mutex.synchronize { @seen.clear }
66
+
67
+ private
68
+
69
+ def check!(area, event)
70
+ raise ArgumentError, "unknown log tag #{area}" unless LogLine::TAGS.include?(area.to_s)
71
+ raise ArgumentError, "bad log event #{event}" unless event.to_s.match?(LogLine::SLUG)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -182,8 +182,8 @@ final class ChiRunner {
182
182
  /// The live sessions, oldest started first, then up to +recentCount+
183
183
  /// stopped ones, newest first. The live order is by creation so ⌘1…⌘9
184
184
  /// stay on their sessions as turns update them; a new one comes last.
185
- /// A session a chi REPL holds is in neither: it takes no notes or
186
- /// messages. A failed second call just leaves "recent" empty. Calls
185
+ /// A session a one-turn chi --non-interactive run holds is in neither: it
186
+ /// takes no notes or messages. A failed second call just leaves "recent" empty. Calls
187
187
  /// back on +queue+ (a headless seam waits on another one).
188
188
  func sessions(recentCount: Int = 3, queue: DispatchQueue = .main,
189
189
  completion: @escaping (Result<[LiveSession], ChiError>) -> Void) {
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ class Engine
5
+ # Cards and anytime commands: #show_card (routed by EventRoutes), the
6
+ # anytime and session-command thread marks, the anytime threads #shutdown
7
+ # waits for. Reads @event_routes, @anytime_threads under @lifecycle_mutex.
8
+ module CardsPart
9
+ # ── Cards ───────────────────────────────────────────────────────────────
10
+
11
+ # Show a card in every UI (docs/plugins.md, Cards): {type: :card, id:,
12
+ # source:, title:, body:, level:, actions: [{label:, command:}], in_turn:}.
13
+ # Where it goes (held while the plugins load, an anytime command's, an
14
+ # init task's, a turn event, else announced) is EventRoutes#route's,
15
+ # marked in_turn: (CARD_MARKS). A card with the id of an earlier one
16
+ # replaces it (btw's "thinking…" → the answer).
17
+ # @param source [String] who shows it (a bundle's name)
18
+ # @param actions [Array<Hash>] {label:, command:}; a command is a line
19
+ # the session runs, as typed (D3)
20
+ # @return [String] the card's id
21
+ # @raise [ArgumentError] a card without a title, or a bad action
22
+ def show_card(source:, title:, body: "", actions: [], level: :info, id: nil)
23
+ card = Plugin::Card.build(source: source, title: title, body: body, actions: actions, level: level, id: id)
24
+ command = Thread.current[running_command_key]
25
+ card[:command] = command if command
26
+ @event_routes.route(card, **EventRoutes::CARD_MARKS)
27
+ card[:id]
28
+ end
29
+
30
+ # Run an anytime command's block (D8): the cards and notices it shows on
31
+ # this thread are announced at once, marked anytime: true, and are
32
+ # never a running turn's events. They belong to the command, not the
33
+ # turn, and a UI shows them after the command's own line (a web
34
+ # command bubble drawn at its command_queued), even mid-turn.
35
+ # @return the block's value
36
+ def running_anytime
37
+ key = :"samagotchi_anytime_#{object_id}"
38
+ outer = Thread.current[key]
39
+ Thread.current[key] = true
40
+ yield
41
+ ensure
42
+ Thread.current[key] = outer
43
+ end
44
+
45
+ # Run a session command's dispatch: a card shown on this thread says
46
+ # it's that command's (:card's command:), for a reload that no longer has
47
+ # the command's line above it (Bridge::CardStore). A thread of its own
48
+ # isn't the command, nor a card shown after it.
49
+ # @param name [String] the command, "/short"
50
+ # @return the block's value
51
+ def running_command(name)
52
+ outer = Thread.current[running_command_key]
53
+ Thread.current[running_command_key] = name
54
+ yield
55
+ ensure
56
+ Thread.current[running_command_key] = outer
57
+ end
58
+
59
+ # Whether an anytime command (/btw) still runs on its thread.
60
+ def anytime_running?
61
+ @lifecycle_mutex.synchronize { @anytime_threads.any?(&:alive?) }
62
+ end
63
+
64
+ # Start an anytime command on its own thread (D8), one #shutdown waits
65
+ # for, so its command_ran is announced before the process leaves.
66
+ # @return [Thread]
67
+ def spawn_anytime(&)
68
+ thread = Thread.new(&)
69
+ @lifecycle_mutex.synchronize do
70
+ @anytime_threads.select!(&:alive?)
71
+ @anytime_threads << thread
72
+ end
73
+ thread
74
+ end
75
+
76
+ def anytime_thread? = Thread.current[:"samagotchi_anytime_#{object_id}"] == true
77
+ private :anytime_thread?
78
+
79
+ def announce_anytime(event)
80
+ event = event.merge(anytime: true)
81
+ announce(event)
82
+ event
83
+ end
84
+ private :announce_anytime
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ class Engine
5
+ # The attached-context candidate judge (ContextJudge): its pass at an
6
+ # owner turn's start and for a late candidate, the asks it starts, and
7
+ # the cards it shows after the turn. Reads @context_cards_after_turn
8
+ # under @context_cards_mutex, @side_judgments, @host_registry, @session.
9
+ module ContextJudgePart
10
+ private
11
+
12
+ # The candidate judge (ContextJudge) at an owner turn's start, after
13
+ # the toolkit's ask settled: the session's candidates' status changes
14
+ # are logged (ContextCandidatesLog#look) and, unless
15
+ # context.candidates.mode is off, the proposed candidates due an ask
16
+ # are asked about the owner's prompts so far, this one included. Never
17
+ # a participant's turn, a review session or a Continue (no new prompt).
18
+ def start_context_judge(turn)
19
+ session = turn.session
20
+ return unless turn.prompt.is_a?(String) && OwnerPrompt.turn?(session: session, author: turn.origin.author)
21
+
22
+ context_judge_pass(session, ContextCandidates.owner_texts(session.messages) + [turn.prompt], phase: "turn")
23
+ end
24
+ private :start_context_judge
25
+
26
+ # A late proposal no owner prompt mentions (Plugin::Host
27
+ # #candidate_proposed: github-pr's init task runs beside the first
28
+ # turn): judged on the owner's prompts so far, the running turn's too.
29
+ def judge_late_candidates
30
+ session = @session
31
+ return if session.nil? || session.review?
32
+
33
+ texts = ContextCandidates.owner_texts(plugin_messages)
34
+ context_judge_pass(session, texts, phase: "late") unless texts.empty?
35
+ end
36
+ private :judge_late_candidates
37
+
38
+ # One look at +session+'s candidates; the asks go to SideJudgments'
39
+ # :context kind, one thread asking each due candidate in turn.
40
+ def context_judge_pass(session, texts, phase:)
41
+ return if session.id.nil? || session.scratch
42
+
43
+ rows = ContextCandidates.with_status(session.id, project_root: session.project_root, state_dir: session_state_dir)
44
+ return if rows.empty?
45
+
46
+ log = ContextCandidatesLog.log_for(session_state_dir)
47
+ records = ContextCandidatesLog.records_for(log, session.id)
48
+ ContextCandidatesLog.look(log, session.id, rows, records) if phase == "turn"
49
+ settings = ContextJudge::Settings.from_config
50
+ return if settings.mode == "off"
51
+
52
+ jobs = ContextJudge.jobs(rows, records, session_id: session.id, texts: texts, settings: settings, phase: phase,
53
+ parent_attached: context_parent_attached(session))
54
+ return if jobs.empty?
55
+
56
+ client = system_one_recording(SystemOne::Client.from_config(host_registry: @host_registry, mode: "on", tag: :context),
57
+ ContextJudge::JUDGMENT, session, mode: settings.mode)
58
+ return unless client
59
+
60
+ start_context_asks(client, jobs, log, settings, context_choice(session, log, settings))
61
+ rescue StandardError => e
62
+ ContextJudge.warn(e)
63
+ end
64
+ private :context_judge_pass
65
+
66
+ # @return [Thread, nil] the asks' thread, nil when none started
67
+ def start_context_asks(client, jobs, log, settings, choice)
68
+ if @side_judgments.in_flight?(:context)
69
+ Log.info(:context, "context_judge_in_flight_skipped", session_id: jobs.first.session_id)
70
+ return nil
71
+ end
72
+
73
+ @side_judgments.start(:context, warn: ContextJudge.method(:warn)) do
74
+ jobs.each { |job| choice.call(job, ContextJudge.ask(client: client, job: job, log: log, settings: settings)) }
75
+ end
76
+ end
77
+ private :start_context_asks
78
+
79
+ # What an answer does on the asks' thread (ContextCandidateChoice
80
+ # #after_judged: on mode attaches or cards; audit nothing). Taken on
81
+ # the caller's thread. The attach only writes the session's folder: its
82
+ # note comes with the next between-turn delivery, never into a running
83
+ # turn. A card shows only while this Engine is still on that session,
84
+ # and not in a running turn (it isn't the turn's: no "waiting for you"
85
+ # on it), but once it has ended.
86
+ # @return [#call] (job, fields)
87
+ def context_choice(session, log, settings)
88
+ session_id = session.id
89
+ project_root = session.project_root
90
+ state_dir = session_state_dir
91
+ show = lambda do |**card|
92
+ current = @session
93
+ next nil unless current.nil? || current.id == session_id
94
+
95
+ @context_cards_mutex.synchronize do
96
+ next @context_cards_after_turn << card if turn_running?
97
+
98
+ show_card(**card)
99
+ end
100
+ end
101
+ lambda do |job, fields|
102
+ ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
103
+ state_dir: state_dir, log: log, show_card: show)
104
+ end
105
+ end
106
+ private :context_choice
107
+
108
+ # The cards #context_choice held while a turn ran (#run_turn's ensure,
109
+ # after the turn flag is released).
110
+ def show_context_cards_after_turn
111
+ cards = @context_cards_mutex.synchronize { @context_cards_after_turn.slice!(0..) }
112
+ cards.each { |card| show_card(**card) }
113
+ rescue StandardError => e
114
+ ContextJudge.warn(e)
115
+ end
116
+ private :show_context_cards_after_turn
117
+
118
+ # A delegate child's parent's sources, by title (else name): the
119
+ # judge's started_from. nil for any other session (a chi send --new
120
+ # session has no parent; a fork isn't a delegate).
121
+ def context_parent_attached(session)
122
+ return nil unless session.delegate? && session.parent_id
123
+
124
+ parent_root = if Session.exist?(session.parent_id, state_dir: session_state_dir)
125
+ Session.load(session.parent_id, state_dir: session_state_dir).project_root
126
+ else
127
+ session.project_root
128
+ end
129
+ ContextSources.attached(session.parent_id, project_root: parent_root, state_dir: session_state_dir)
130
+ .map { |attached| attached.source.title || attached.name }
131
+ rescue StandardError => e
132
+ Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
133
+ []
134
+ end
135
+ private :context_parent_attached
136
+ end
137
+ end
138
+ end