samagotchi 0.153.0 → 0.154.1

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 (226) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -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 +82 -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 +21 -8
  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 +4 -2
  23. data/docs/internals/remote-sessions.md +7 -7
  24. data/docs/internals/web-frontend.md +2 -2
  25. data/docs/internals/web-push.md +1 -1
  26. data/docs/memory.md +2 -2
  27. data/docs/plugins.md +23 -28
  28. data/docs/sessions.md +24 -24
  29. data/docs/sub-agent.md +2 -2
  30. data/docs/system-one.md +1 -1
  31. data/docs/testing.md +31 -6
  32. data/lib/samagotchi/answer_text.rb +33 -0
  33. data/lib/samagotchi/bridge/card_places.rb +56 -7
  34. data/lib/samagotchi/bridge/card_store.rb +24 -9
  35. data/lib/samagotchi/bridge.rb +52 -32
  36. data/lib/samagotchi/bridge_client.rb +19 -14
  37. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  38. data/lib/samagotchi/broadcast/triage.rb +1 -1
  39. data/lib/samagotchi/broadcast_command.rb +5 -5
  40. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  42. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  43. data/lib/samagotchi/bundle_command.rb +2 -2
  44. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  45. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  46. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  47. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  48. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  50. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  51. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  52. data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
  53. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  54. data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
  55. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  56. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  57. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  58. data/lib/samagotchi/child_move.rb +3 -3
  59. data/lib/samagotchi/commands/registry.rb +6 -6
  60. data/lib/samagotchi/composer_route.rb +4 -5
  61. data/lib/samagotchi/config.rb +14 -12
  62. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  63. data/lib/samagotchi/context_candidates.rb +4 -3
  64. data/lib/samagotchi/continue_offer.rb +1 -1
  65. data/lib/samagotchi/debug_log.rb +1 -1
  66. data/lib/samagotchi/degrade.rb +75 -0
  67. data/lib/samagotchi/desktop/macos/ChiRunner.swift +3 -3
  68. data/lib/samagotchi/engine/cards.rb +87 -0
  69. data/lib/samagotchi/engine/context_judge.rb +138 -0
  70. data/lib/samagotchi/engine/events.rb +131 -0
  71. data/lib/samagotchi/engine/guardrails.rb +87 -0
  72. data/lib/samagotchi/engine/hooks.rb +119 -0
  73. data/lib/samagotchi/engine/messages.rb +124 -0
  74. data/lib/samagotchi/engine/model_settings.rb +127 -0
  75. data/lib/samagotchi/engine/models.rb +261 -0
  76. data/lib/samagotchi/engine/objectives.rb +116 -0
  77. data/lib/samagotchi/engine/participants.rb +86 -0
  78. data/lib/samagotchi/engine/plugins.rb +208 -0
  79. data/lib/samagotchi/engine/questions.rb +98 -0
  80. data/lib/samagotchi/engine/recap.rb +130 -0
  81. data/lib/samagotchi/engine/session.rb +131 -0
  82. data/lib/samagotchi/engine/side_requests.rb +135 -0
  83. data/lib/samagotchi/engine/steering.rb +134 -0
  84. data/lib/samagotchi/engine/toolkit.rb +269 -0
  85. data/lib/samagotchi/engine/turn_end.rb +180 -0
  86. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  87. data/lib/samagotchi/engine/verify.rb +153 -0
  88. data/lib/samagotchi/engine.rb +62 -2646
  89. data/lib/samagotchi/engine_snapshots.rb +9 -8
  90. data/lib/samagotchi/grants.rb +575 -0
  91. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  92. data/lib/samagotchi/guardrails/context.rb +2 -3
  93. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  94. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  95. data/lib/samagotchi/hook_routes.rb +3 -3
  96. data/lib/samagotchi/idle_recap.rb +7 -6
  97. data/lib/samagotchi/idle_reminders.rb +3 -4
  98. data/lib/samagotchi/iteration_limit.rb +2 -2
  99. data/lib/samagotchi/kernel_loop.rb +13 -13
  100. data/lib/samagotchi/launch_mode.rb +13 -32
  101. data/lib/samagotchi/level_setting_command.rb +3 -2
  102. data/lib/samagotchi/llm/chat_loop.rb +3 -1
  103. data/lib/samagotchi/llm_context_command.rb +3 -2
  104. data/lib/samagotchi/log.rb +7 -7
  105. data/lib/samagotchi/log_line.rb +1 -1
  106. data/lib/samagotchi/log_path.rb +1 -1
  107. data/lib/samagotchi/note_command.rb +1 -1
  108. data/lib/samagotchi/note_delivery.rb +18 -8
  109. data/lib/samagotchi/objectives.rb +24 -3
  110. data/lib/samagotchi/objectives_check.rb +8 -27
  111. data/lib/samagotchi/objectives_command.rb +4 -69
  112. data/lib/samagotchi/objectives_edit.rb +87 -0
  113. data/lib/samagotchi/output_formatter.rb +7 -1
  114. data/lib/samagotchi/owner_lock.rb +7 -1
  115. data/lib/samagotchi/parent_answers_command.rb +5 -4
  116. data/lib/samagotchi/participant_exposure.rb +42 -0
  117. data/lib/samagotchi/participant_mode.rb +1 -1
  118. data/lib/samagotchi/participant_name.rb +1 -1
  119. data/lib/samagotchi/participants.rb +17 -17
  120. data/lib/samagotchi/participants_watch.rb +1 -1
  121. data/lib/samagotchi/plugin/api.rb +3 -2
  122. data/lib/samagotchi/plugin/context.rb +21 -7
  123. data/lib/samagotchi/plugin/peer.rb +2 -1
  124. data/lib/samagotchi/plugin/sessions.rb +6 -4
  125. data/lib/samagotchi/plugin_tasks.rb +1 -2
  126. data/lib/samagotchi/private_files.rb +29 -0
  127. data/lib/samagotchi/project_env.rb +2 -2
  128. data/lib/samagotchi/question_desk.rb +8 -7
  129. data/lib/samagotchi/recap_command.rb +2 -1
  130. data/lib/samagotchi/recap_setup.rb +1 -1
  131. data/lib/samagotchi/recap_store.rb +1 -1
  132. data/lib/samagotchi/redact.rb +86 -0
  133. data/lib/samagotchi/reminder_queue.rb +2 -2
  134. data/lib/samagotchi/remote_attach_command.rb +1 -1
  135. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  136. data/lib/samagotchi/review_start.rb +1 -1
  137. data/lib/samagotchi/run_options.rb +6 -6
  138. data/lib/samagotchi/self_report.rb +42 -39
  139. data/lib/samagotchi/send_command/busy.rb +1 -1
  140. data/lib/samagotchi/send_command.rb +4 -1
  141. data/lib/samagotchi/session.rb +11 -2
  142. data/lib/samagotchi/session_access/contract.rb +3 -3
  143. data/lib/samagotchi/session_access/local.rb +9 -9
  144. data/lib/samagotchi/session_access/remote.rb +17 -7
  145. data/lib/samagotchi/session_access.rb +4 -4
  146. data/lib/samagotchi/session_archive_command.rb +2 -2
  147. data/lib/samagotchi/session_chain.rb +8 -6
  148. data/lib/samagotchi/session_commands.rb +45 -48
  149. data/lib/samagotchi/session_continue.rb +1 -1
  150. data/lib/samagotchi/session_delete_command.rb +4 -4
  151. data/lib/samagotchi/session_manager.rb +67 -49
  152. data/lib/samagotchi/session_metrics.rb +2 -2
  153. data/lib/samagotchi/session_model_settings.rb +3 -2
  154. data/lib/samagotchi/session_participants.rb +1 -1
  155. data/lib/samagotchi/session_retention.rb +27 -7
  156. data/lib/samagotchi/sessions_command.rb +4 -4
  157. data/lib/samagotchi/share_command.rb +10 -10
  158. data/lib/samagotchi/steer.rb +13 -2
  159. data/lib/samagotchi/stream_event.rb +22 -0
  160. data/lib/samagotchi/system_prompt.rb +154 -145
  161. data/lib/samagotchi/task_watch.rb +1 -1
  162. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  163. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  164. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  165. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  166. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  167. data/lib/samagotchi/terminal_ui/attached_view.rb +1 -11
  168. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  169. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  170. data/lib/samagotchi/terminal_ui/input_support.rb +17 -22
  171. data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
  172. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  173. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  174. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  175. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  176. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  177. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  178. data/lib/samagotchi/terminal_ui/reline_seam.rb +2 -12
  179. data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
  180. data/lib/samagotchi/terminal_ui.rb +142 -1387
  181. data/lib/samagotchi/title_command.rb +2 -2
  182. data/lib/samagotchi/tool_runner.rb +15 -16
  183. data/lib/samagotchi/toolkit_command.rb +2 -1
  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/send_note.rb +2 -1
  189. data/lib/samagotchi/turn_endings.rb +2 -2
  190. data/lib/samagotchi/turn_flow.rb +11 -7
  191. data/lib/samagotchi/update_hint.rb +2 -2
  192. data/lib/samagotchi/version.rb +1 -1
  193. data/lib/samagotchi/web/app.rb +42 -51
  194. data/lib/samagotchi/web/message_display.rb +7 -0
  195. data/lib/samagotchi/web/participant_access.rb +2 -2
  196. data/lib/samagotchi/web/participant_review.rb +5 -5
  197. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  198. data/lib/samagotchi/web/participant_view.rb +2 -82
  199. data/lib/samagotchi/web/participants_admin.rb +1 -1
  200. data/lib/samagotchi/web/public/block_host.js +35 -8
  201. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  202. data/lib/samagotchi/web/public/card_stamps.js +27 -0
  203. data/lib/samagotchi/web/public/cards_place.js +5 -3
  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 +1 -1
  207. data/lib/samagotchi/web/public/format.js +4 -3
  208. data/lib/samagotchi/web/public/turn_html.js +8 -4
  209. data/lib/samagotchi/web/push/store.rb +1 -1
  210. data/lib/samagotchi/web/session_access_routes.rb +13 -4
  211. data/lib/samagotchi/web/session_control.rb +12 -8
  212. data/lib/samagotchi/web/session_create.rb +1 -1
  213. data/lib/samagotchi/web/session_hub.rb +2 -2
  214. data/lib/samagotchi/web/session_show.rb +6 -2
  215. data/lib/samagotchi/web/session_summary.rb +1 -1
  216. data/lib/samagotchi/worker.rb +103 -9
  217. data/lib/samagotchi/worker_link.rb +3 -3
  218. data/lib/samagotchi/working_set_command.rb +3 -2
  219. data/lib/samagotchi.rb +0 -1
  220. metadata +31 -7
  221. data/lib/samagotchi/pending_input_queue.rb +0 -55
  222. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  223. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  224. data/lib/samagotchi/web/grants.rb +0 -575
  225. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  226. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -30,7 +30,7 @@ module Samagotchi
30
30
 
31
31
  def run(prompt:, options:, timeout:)
32
32
  command = [RbConfig.ruby, "-I", Sandbox::LIB_DIR, Sandbox::CHI,
33
- "--no-shared", "--non-interactive", "-p", prompt, *child_options(options)]
33
+ "--non-interactive", "-p", prompt, *child_options(options)]
34
34
  trap_state, old_traps = trap_forwarding
35
35
  pid = nil
36
36
  streams = []
@@ -124,7 +124,7 @@ module Samagotchi
124
124
  end
125
125
 
126
126
  # The listing's placement fields: nothing to place in a one-turn run.
127
- LISTING_FIELDS = %w[turns_since current earlier after_failed label].freeze
127
+ LISTING_FIELDS = %w[turns_since opening_id current earlier after_failed label].freeze
128
128
 
129
129
  # The saved cards as the web reads them (CardStore.saved), with
130
130
  # string keys and values, as the JSON trace prints them.
@@ -367,8 +367,8 @@ module Samagotchi
367
367
  prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
368
368
  before = installer.conflicts.to_h { |key, info| [key, Samagotchi::MemoryBundle::Merger.digest(info[:current])] }
369
369
  @stdout.puts "Launching an interactive session to merge them… (/exit when done)"
370
- require "samagotchi/terminal_ui"
371
- Samagotchi::TerminalUI.new(prompt: prompt).run
370
+ require "samagotchi/terminal_ui/attach_launcher"
371
+ Samagotchi::TerminalUI::AttachLauncher.run(prompt: prompt)
372
372
  settle_conflicts(installer.conflicts, before, bundle_name)
373
373
  ensure
374
374
  # An owned source (git/zip/tar) was kept for the incoming files above;
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: btw
3
- version: 0.1.4
3
+ version: 0.1.5
4
4
  scope: system
5
5
  description: "/btw <question>: a side answer about the conversation, as a card in every UI, even mid-turn; Keep as session forks it into a child session"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:6497129b06d1c3061c1d7d9d7a9c3dfdbcf790e8f2049addad0c26d24771015f
9
+ sha256: sha256:d4c0d6309308270a1430d64f9713e46c15d71ace1b068fefc22916aa8cc2d043
10
10
  requires_chi: ">= 0.143.0"
11
11
  # config.yml bundles: btw: (another key warns; an older chi ignores this)
12
12
  settings: [max_tokens, timeout]
@@ -15,7 +15,7 @@ class Plugin
15
15
  SYSTEM = "Answer the user's question about the conversation below, briefly. The user asks it on the side: " \
16
16
  "don't continue the conversation's task, and don't say what you would do next."
17
17
  # How many answers /btw keep can still fork: the last ones, for the
18
- # worker's (or the REPL's) life.
18
+ # worker's life.
19
19
  KEPT = 10
20
20
  TITLE_CHARS = 72
21
21
  EARLIER = "(About the conversation before the running turn.)"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: coordinator
3
- version: 0.8.0
3
+ version: 0.8.1
4
4
  scope: system
5
5
  description: "Coordinate parallel work: the coordinator skill (split the work, one worktree and child session per task, check each report, merges under a guardrail approval (the git-merge rule) or a question, a handoff memory to resume from), /coordinate <goal>, /coordinate resume, /coordinate end, /coordinate review (experimental: a sprint review session to share) and /children; the coordinator toolkit (lean + delegate, delegate_result, list_sessions, objectives) and the coding toolkit for build children (child + be concise)"
6
6
  trust_level: reviewed
@@ -10,7 +10,7 @@ files:
10
10
  description: "Coordinate parallel work: split it, delegate each task wait:false in its own worktree (cwd:), check each report, merge under the git-merge approval or ask the user; resume from a handoff_* memory"
11
11
  plugin:
12
12
  file: plugin.rb
13
- sha256: sha256:84535a86b754f3a12cdb2f2591027ea71bbf9ffb74734b1050899b9a4fde07ac
13
+ sha256: sha256:540dd6369342ea4dcbc6ba5ee17fb20d980b880df614a475668c70115e7ec6b8
14
14
  # the coordinator toolkit needs chi.toolkit and ctx.use_toolkit (0.66.0); the
15
15
  # coding toolkit (chi.toolkit prompt:) and the skill's delegate toolkit: need
16
16
  # the next chi.
@@ -16,8 +16,9 @@
16
16
  # (ctx.sessions.stop, by session id) and shows the card again.
17
17
  #
18
18
  # /coordinate <goal> asks the model, in this session, to follow the skill
19
- # for the goal. A REPL session (--no-shared) takes no messages: the request
20
- # is shown for the user to send.
19
+ # for the goal. A session a one-turn run holds (-p --non-interactive, or an
20
+ # older chi's terminal) takes no messages: the request is shown for the user
21
+ # to send.
21
22
  #
22
23
  # /coordinate resume picks up a coordinator handoff: the open handoff_*
23
24
  # memories of this project (an index line whose description doesn't start
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: decision-card
3
- version: 0.2.0
3
+ version: 0.2.1
4
4
  scope: system
5
5
  description: "In a session with objectives, a long answer ending in a question gets a card: a short summary and the question, answered in the composer"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:e628d84ab9521f7e8e5f3bc69da97fe00e574082412ea24e9f3f1c6234ad31a9
10
- # ctx.objectives and ctx.turn_running? are new after 0.148.0
11
- requires_chi: ">= 0.149.0"
9
+ sha256: sha256:b85fd2162e41d1ea059ff6ca89ce6bb543e4f283d18337e9d4ac6936477d3770
10
+ # ctx.last_answer is new after 0.153.0 (ctx.objectives, ctx.turn_running? after 0.148.0)
11
+ requires_chi: ">= 0.154.0"
12
12
  # config.yml bundles: decision-card: (another key warns; an older chi ignores this)
13
13
  settings: [min_chars, tail_chars, model, timeout, max_tokens]
@@ -135,7 +135,7 @@ class Plugin
135
135
  return SHORT_BUSY if ctx.turn_running?
136
136
 
137
137
  messages = ctx.messages
138
- answer = last_answer(messages) or return "no answer to shorten yet"
138
+ answer = ctx.last_answer or return "no answer to shorten yet"
139
139
 
140
140
  id = ctx.card(title: SHORT_TITLE, body: "…")
141
141
  @mutex.synchronize { @shorts[id] = ctx }
@@ -158,14 +158,6 @@ class Plugin
158
158
  ctx.card(id: id, title: SHORT_TITLE, body: body)
159
159
  end
160
160
 
161
- # @return [String, nil] the newest non-empty answer's text
162
- def last_answer(messages)
163
- found = Array(messages).reverse_each.find do |message|
164
- %w[model assistant].include?(field(message, :role).to_s) && !field(message, :content).to_s.strip.empty?
165
- end
166
- found && field(found, :content).to_s
167
- end
168
-
169
161
  def turn_ended(event, ctx)
170
162
  return unless event[:status].to_s == "completed"
171
163
  # A participant's question is for the participant, on the owner's model; a
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: dev
3
- version: 0.7.6
3
+ version: 0.7.7
4
4
  scope: system
5
5
  description: "Developer tools"
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.150.0"
7
+ requires_chi: ">= 0.154.0"
8
8
  includes:
9
9
  - known-names
10
10
  - mcp
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: github-pr
3
- version: 0.6.3
3
+ version: 0.6.4
4
4
  scope: system
5
5
  description: Proposes the branch's open GitHub pull request to a session, attached once you mention it (#N, the branch or its URL), and attaches any PR URL with chi context add or the web's "+ URL"; the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can attach the PR at once instead, or do nothing
6
6
  trust_level: reviewed
@@ -11,7 +11,7 @@ needs:
11
11
  hint: brew install gh, then gh auth login
12
12
  plugin:
13
13
  file: plugin.rb
14
- sha256: sha256:82f8c76d38f314c3503eef3a5de0f8c0b20d0bb5bf4a3daebca43b60d00f8c7a
14
+ sha256: sha256:87678c39565a3d586b462d0449a27ee3da4a96f53ef5b352dea959256f6c1eda
15
15
  scripts:
16
16
  pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
17
17
  context_providers:
@@ -29,11 +29,11 @@ require "samagotchi/git_env"
29
29
  # diff" (or "File at head") action the web's ref menu offers. The
30
30
  # after_turn hook links from a per-PR cache and never runs gh (the web holds the answer until the hooks are
31
31
  # done); before_turn, before_generation and the init task fill the cache
32
- # off the turn's thread (`gh api`), in a session worker only: the REPL and
33
- # -p --non-interactive show no web page while they run (ctx.frontend; a chi
32
+ # off the turn's thread (`gh api`), in a session worker only: -p
33
+ # --non-interactive shows no web page while it runs (ctx.frontend; a chi
34
34
  # without it counts as a worker), and a scratch session never fetches.
35
35
  # `bundles: github-pr: line_links:` false turns it off, "always" fetches in
36
- # the REPL and -p too (still not for a scratch session).
36
+ # -p --non-interactive too (still not for a scratch session).
37
37
  #
38
38
  # `bundles: github-pr: auto_attach:` says what the init task does with the
39
39
  # branch's open PR: `propose` (the default, above), `attach` (attached at
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: skills
3
- version: 0.1.9
3
+ version: 0.1.10
4
4
  scope: system
5
5
  description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:781d943749d59ade0e24d04ba774449d31267eb8ca435835c36c67a3e523e632
9
+ sha256: sha256:8d72d29286d0807c18d2c37225455ca06d45f4f974f9335b76cc77916043838c
10
10
  requires_chi: ">= 0.143.0"
11
11
  # config.yml bundles: skills: (another key warns; an older chi ignores this)
12
12
  settings: [history_keep, nudge]
@@ -242,8 +242,9 @@ class Plugin
242
242
  # what was just done (the model has seen the commands; a side answer
243
243
  # wouldn't). The request runs as a turn; sent while a turn runs it joins
244
244
  # that turn at its next step, and the request says to finish the task
245
- # first. A REPL session (--no-shared) takes no messages: the request is
246
- # shown for the user to send.
245
+ # first. A session a one-turn run holds (-p --non-interactive, or an older
246
+ # chi's terminal) takes no messages: the request is shown for the user to
247
+ # send.
247
248
  def save(rest, ctx)
248
249
  words = rest.split
249
250
  scope = words.delete("--system") ? "system" : "project"
@@ -18,7 +18,7 @@ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/confi
18
18
 
19
19
  Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`), since an env or CLI name is derived from them; a config-only section (`system_one`: `expose: [:config]`, no derived name) may carry `_`. Leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
20
20
 
21
- **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens/entry_warn_tokens/read_max_chars`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
21
+ **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens/entry_warn_tokens/read_max_chars`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
22
22
 
23
23
  Example `config.yml` (new nested form, preferred):
24
24
 
@@ -40,10 +40,9 @@ recap:
40
40
  session:
41
41
  retention_days: 14
42
42
  max_count: 500
43
- keep_status: "" # the default: no status protects a session from pruning; a live worker or REPL always does
43
+ keep_status: "" # the default: no status protects a session from pruning; a live worker or a one-turn run always does
44
44
  sweep_interval_hours: 24
45
45
  idle_exit_minutes: 30 # a background worker nobody uses exits; 0 = never
46
- shared: true # the default: plain `chi` runs its session in a background worker and attaches (as `chi --shared`); false keeps the in-process REPL (env SAMAGOTCHI_SESSION_SHARED; no CLI flag, `--no-shared` opts out per run)
47
46
  keep_empty: false # the default: a session nothing happened in (no prompt, default model, no note/image) is deleted when left; true keeps it (env SAMAGOTCHI_SESSION_KEEP_EMPTY)
48
47
  max_children: 4 # running child sessions one session may have delegated at a time (the delegate tool; env SAMAGOTCHI_SESSION_MAX_CHILDREN)
49
48
  log:
@@ -77,7 +76,7 @@ guardrails:
77
76
  disable: [guardrails:git-rebase]
78
77
  ```
79
78
 
80
- - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`) or the REPL. Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
79
+ - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`). Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
81
80
 
82
81
  ```yaml
83
82
  bundles:
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.153.0
3
+ version: 0.154.1
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
- self_map.md: sha256:e695f78f858c29ccfef2ed6f9d6dd86d0ec067ed704a5a1956c01e53617948a1
9
- config_modification_protocol.md: sha256:8896d1d364ce835040f69b504518d43dbdf959b93fdd9b10f87a8de61cae84a8
8
+ self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
9
+ config_modification_protocol.md: sha256:f3f6545c93d59b624a2bf1766e26059520ffc7b236470dd9f1b51ab77312376d
10
10
  memory_guide.md: sha256:8b7727b939b9c04486ca59859561023dcc319a73e0f5b47b3fcf5741edaa69e2
11
11
  delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
@@ -67,7 +67,7 @@
67
67
  `docs/bundle-authoring.md` in my source dir first.
68
68
 
69
69
  ## Where things live (relative to the source dir)
70
- - `docs/`: `cli.md` (flags, subcommands, REPL commands), `configuration.md` (config.yml keys,
70
+ - `docs/`: `cli.md` (flags, subcommands, the terminal's commands), `configuration.md` (config.yml keys,
71
71
  hosts, the server api), `hooks.md` (hook events table), `plugins.md` (plugin API), `bundles.md` (each shipped bundle: btw, mcp, cost-cap…),
72
72
  `guardrails.md`, `memory.md`, `sessions.md`. `README.md` is only the quickstart.
73
73
  - `lib/samagotchi/tools/<tool>.rb`: each tool's real limits and defaults (e.g. `execute.rb`,
@@ -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) {
@@ -199,7 +199,7 @@ final class ChiRunner {
199
199
  private static let recentFlags = ["--limit", "20"]
200
200
 
201
201
  /// The live rows, then up to +recentCount+ of the recent list that are
202
- /// neither live nor a REPL's; a failed recent list adds none.
202
+ /// neither live nor held by an owner (a one-turn run); a failed recent list adds none.
203
203
  private static func rows(live: [LiveSession], recent: Result<[LiveSession], ChiError>, recentCount: Int) -> [LiveSession] {
204
204
  let liveIds = Set(live.map(\.id))
205
205
  let stopped = ((try? recent.get()) ?? [])