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
@@ -14,6 +14,7 @@ require_relative "turn_note"
14
14
  require_relative "steer"
15
15
  require_relative "turn_origin"
16
16
  require_relative "worker_idle_exit"
17
+ require_relative "degrade"
17
18
  require_relative "hold_stop"
18
19
  require_relative "children_status"
19
20
  require_relative "worker_wakes"
@@ -51,7 +52,7 @@ module Samagotchi
51
52
  # It runs the session's Engine and Bridge, takes queued prompts from the
52
53
  # input dir one turn at a time, and returns when nobody has used it for the
53
54
  # idle-exit timeout. A failed turn is rolled back and its prompt handed back
54
- # (:prompt_restored), as in the REPL, and the loop goes on. A stop marked on
55
+ # (:prompt_restored), and the loop goes on. A stop marked on
55
56
  # disk exits the process; an error outside a turn marks the session and
56
57
  # exits with 1.
57
58
  #
@@ -188,6 +189,10 @@ module Samagotchi
188
189
  # checks again then).
189
190
  def discard? = @discard
190
191
 
192
+ # Whether this worker runs a `chi scratch` session: deleted as it leaves
193
+ # (SessionManager.run_session_loop).
194
+ def scratch? = !!@session&.scratch
195
+
191
196
  # Whether delegate reports wait that an idle parent runs a turn for
192
197
  # (#wake_state): SessionManager.run_session_loop starts a new worker
193
198
  # for them after this one leaves. Reports the budget or a failure held
@@ -201,8 +206,9 @@ module Samagotchi
201
206
  # to exit (Bridge POST /exit), :restart when it asked for a restart
202
207
  # (the caller starts a new worker), :stopped when the session was stopped
203
208
  # (chi stop), :signal when a signal stopped it (WorkerSignals),
204
- # :crashed when the loop raised (the session is marked errored);
205
- # SessionManager.run_session_loop turns it into the exit
209
+ # :crashed when the loop raised (the session is marked errored),
210
+ # :scratch_left when a scratch session's terminal is gone (the caller
211
+ # deletes it); SessionManager.run_session_loop turns it into the exit
206
212
  def run
207
213
  # The thread a signal's expired grace raises in (the main thread in a
208
214
  # worker process).
@@ -248,6 +254,8 @@ module Samagotchi
248
254
  # The session, its Engine and what runs its turns, before anyone can
249
255
  # reach it.
250
256
  def start
257
+ # A scratch worker's grace for its terminal's first stream counts from here.
258
+ @started_at = monotonic_now
251
259
  # The wake turns' budget (delegate reports, attached context, peers), from now.
252
260
  @wakes = WorkerWakes.new(grace: WAKE_START_GRACE)
253
261
  # This worker holds the session now (a continue's starting marker held
@@ -340,7 +348,7 @@ module Samagotchi
340
348
  @bridge.queue_command(line, client_id: client_id, after_file: after_file,
341
349
  enqueued_id: enqueued_id, author: author)
342
350
  })
343
- # /model's default is the config's, as in the REPL; the Engine started
351
+ # /model's default is the config's; the Engine started
344
352
  # on the session's model. nil when there is no default.model.
345
353
  @default_model = ModelProfile.default_model_name
346
354
  @commands = SessionCommands.new(engine: @engine, turn_flow: @turn_flow,
@@ -416,6 +424,31 @@ module Samagotchi
416
424
  @context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
417
425
  project_root: @session.project_root, cwd: @session.working_directory,
418
426
  on_change: -> { @waker.wake }).start
427
+ @scratch_watch = start_scratch_watch if @session.scratch
428
+ end
429
+
430
+ # A scratch session's turn never runs on with no terminal: a turn
431
+ # waiting on a question or an approval would keep the worker up for
432
+ # nobody. Each tick while a turn runs and its terminal is gone
433
+ # (#scratch_unattended?), the question is dropped and the turn
434
+ # cancelled as a stop cancels it; the idle pass then leaves
435
+ # (:scratch_left) and the session is deleted.
436
+ def start_scratch_watch
437
+ Thread.new do
438
+ loop do
439
+ sleep(@poll_interval)
440
+ Degrade.call(:worker, "scratch_watch_failed") { cancel_unattended_turn }
441
+ end
442
+ end.tap { |thread| thread.report_on_exception = false }
443
+ end
444
+
445
+ def cancel_unattended_turn
446
+ return unless @engine.turn_running? && scratch_unattended?
447
+
448
+ Log.info(:worker, "scratch_turn_cancelled", sid: @session_id)
449
+ @engine.cancel_question(SessionManager::STOP_CANCEL_REASON) if @engine.pending_question
450
+ @engine.cancel_current_turn!(SessionManager::STOP_CANCEL_REASON.to_sym)
451
+ @waker.wake
419
452
  end
420
453
 
421
454
  # One pass of the loop: the first work found runs (queued commands, a
@@ -468,6 +501,7 @@ module Samagotchi
468
501
  return if run_wake_turn
469
502
  return if run_task_wake
470
503
  return if run_due_reminders
504
+ return left(:scratch_left) if scratch_unattended? && !@engine.turn_running? && leave_scratch
471
505
  return left(:idle_exit) if @idle_exit.due? && leave_idle
472
506
 
473
507
  leaving = @exit_requested && leave_on_request
@@ -553,6 +587,7 @@ module Samagotchi
553
587
  TEARDOWN_LEAVE_WAIT = 10
554
588
 
555
589
  def teardown
590
+ @scratch_watch&.kill
556
591
  @config_restart_unsubscribe&.call
557
592
  @context_poller&.stop
558
593
  drop_queued_commands
@@ -571,6 +606,8 @@ module Samagotchi
571
606
  engine = nil
572
607
  engine = Samagotchi::Engine.new(
573
608
  model_name: @session.model_name,
609
+ # `chi scratch`: no memory_write, no delegates, approvals in memory.
610
+ scratch: @session.scratch,
574
611
  model_typed: @session.model_typed,
575
612
  # The session's --memory and --mute lists: the same prompt on every
576
613
  # (re)spawn.
@@ -783,7 +820,7 @@ module Samagotchi
783
820
  end
784
821
 
785
822
  # A participant's cap in dollars: their own, else the config's.
786
- def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f : Web::Grants.default_cap
823
+ def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f : Grants.default_cap
787
824
 
788
825
  # A participant's cleaned name, else their id.
789
826
  def participant_label(author, name)
@@ -830,7 +867,7 @@ module Samagotchi
830
867
  nil
831
868
  end
832
869
 
833
- # A due reminder runs as a continue turn, as in the REPL: Engine#run_turn
870
+ # A due reminder runs as a continue turn: Engine#run_turn
834
871
  # injects the reminders as a tail system message, and no user message is
835
872
  # added. A prompt's turn may have injected them already (a stale latch):
836
873
  # then there is nothing to run.
@@ -1414,7 +1451,7 @@ module Samagotchi
1414
1451
  # WorkerInbound.save_or_log: a failed save between turns is logged.
1415
1452
  def save_or_log(at, &) = WorkerInbound.save_or_log(at, &)
1416
1453
 
1417
- # Back to the conversation before the failed turn, as the REPL does (so
1454
+ # Back to the conversation before the failed turn (so
1418
1455
  # failed prompts don't pile up as consecutive user messages), and each
1419
1456
  # prompt it took (its own and any merged into it) goes back to its
1420
1457
  # sender, who can send it again. The rollback and the announcements are
@@ -1559,6 +1596,60 @@ module Samagotchi
1559
1596
  end
1560
1597
  end
1561
1598
 
1599
+ # How long a scratch worker waits for its terminal's first stream
1600
+ # (AttachLauncher attaches within BRIDGE_WAIT of the spawn).
1601
+ SCRATCH_JOIN_GRACE = 30
1602
+
1603
+ # How long a scratch worker's terminal may have no stream open, after
1604
+ # it had one, before it counts as gone: twice the attached terminal's
1605
+ # whole reconnect ladder (TerminalUI::LocalLink#reconnect_delays), so
1606
+ # a reconnect gap (a dropped stream, a slow worker) never reads as a
1607
+ # terminal that left.
1608
+ SCRATCH_GONE_AFTER = 2 * BridgeClient::EventStream::REDISCOVER_DELAYS.sum
1609
+
1610
+ # Whether this is a `chi scratch` session's worker whose terminal is
1611
+ # gone: no terminal stream (a tui: client id's) open for
1612
+ # SCRATCH_GONE_AFTER, after a stream was (a killed TUI's closes as its
1613
+ # writer reads the hang-up), or none came within SCRATCH_JOIN_GRACE.
1614
+ # Any other stream (a web tab's, which chi web refuses for a scratch
1615
+ # session anyway) never keeps it. Its terminal deletes it on every way
1616
+ # out it sees; this is for the ones it doesn't (kill -9, a closed
1617
+ # terminal window).
1618
+ def scratch_unattended?
1619
+ return false unless @session&.scratch && @bridge
1620
+
1621
+ if @bridge.open_streams_of(ClientId::TUI_PREFIX).positive?
1622
+ @unattended_since = nil
1623
+ return false
1624
+ end
1625
+ return monotonic_now - @started_at >= SCRATCH_JOIN_GRACE unless @bridge.streams_served.positive?
1626
+
1627
+ # Set by the first look (the scratch watch or the idle pass) that
1628
+ # finds no stream; a stream back resets it above.
1629
+ @unattended_since ||= monotonic_now
1630
+ monotonic_now - @unattended_since >= SCRATCH_GONE_AFTER
1631
+ end
1632
+
1633
+ def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1634
+
1635
+ # #leave_idle for a scratch worker its terminal left: checked again with
1636
+ # the event log held, then the intake closes. The session is deleted
1637
+ # once the lock is free (SessionManager.run_session_loop: :scratch_left).
1638
+ # @return [Boolean] false when a client came back since
1639
+ def leave_scratch
1640
+ stop_bridge_after do
1641
+ @engine.synchronize_events do
1642
+ # A turn still running is the scratch watch's to cancel first.
1643
+ next false unless scratch_unattended? && !@engine.turn_running?
1644
+
1645
+ @bridge&.close_intake
1646
+ @engine.stop_idle(join: false)
1647
+ Log.info(:worker, "scratch_left", served: @bridge.streams_served)
1648
+ true
1649
+ end
1650
+ end
1651
+ end
1652
+
1562
1653
  # Check again with the event log held, which the Bridge holds while it
1563
1654
  # queues a POST /turn, then close the Bridge's intake so no client can
1564
1655
  # queue one after the check, and stop the idle jobs (reminder callback,
@@ -1694,6 +1785,9 @@ module Samagotchi
1694
1785
  # @return [WorkerIdleExit::Hold, nil]
1695
1786
  def exit_hold(client_id, kind:, stop:, ask:)
1696
1787
  if kind == :restart
1788
+ # A scratch session's worker ends with its terminal: never restarted.
1789
+ return WorkerIdleExit::Hold.new(reason: :scratch) if @session.scratch
1790
+
1697
1791
  hold = @idle_exit.hold_for_restart
1698
1792
  return hold && WorkerIdleExit::Hold.new(reason: hold)
1699
1793
  end
@@ -1792,7 +1886,7 @@ module Samagotchi
1792
1886
  # A restart keeps even an empty session (a new worker takes it over at
1793
1887
  # once) and leaves the recap to the conversation's next real pause.
1794
1888
  def left(result)
1795
- deleting = result == :exit_requested && @exit_deletes
1889
+ deleting = (result == :exit_requested && @exit_deletes) || result == :scratch_left
1796
1890
  # The attached context goes with this process: a change that landed after
1797
1891
  # the leaving pass looked at it has no later pass to note it, so note it
1798
1892
  # here (no wake turn: the worker is on its way out). Not into a session
@@ -1805,7 +1899,7 @@ module Samagotchi
1805
1899
  # session /exit --delete deletes next: its save could land after the
1806
1900
  # delete and write the session back.
1807
1901
  @task_wakes.absorb unless deleting
1808
- return result if result == :restart
1902
+ return result if %i[restart scratch_left].include?(result)
1809
1903
 
1810
1904
  @discard = empty_session?
1811
1905
  write_recap_on_leave unless @discard || deleting
@@ -23,9 +23,9 @@ module Samagotchi
23
23
  # past START_LIMIT without one;
24
24
  # - Leaving: the worker closed its Bridge and holds the lock for its last work;
25
25
  # - Skew: a worker whose sidecar speaks a newer PROTOCOL than this chi;
26
- # - Stale: a sidecar, but the lock is free (or a chi REPL holds it, which
26
+ # - Stale: a sidecar, but the lock is free (or a one-turn chi run holds it, which
27
27
  # writes none): its worker died;
28
- # - None: no worker and no sidecar, or a chi REPL.
28
+ # - None: no worker and no sidecar, or a one-turn chi run.
29
29
  # A kernel releases a dead worker's lock, so a free lock is the proof of
30
30
  # death a TCP probe never gave (a port can be reused by another session's
31
31
  # Bridge).
@@ -80,7 +80,7 @@ module Samagotchi
80
80
  end
81
81
 
82
82
  # No worker: no sidecar in the session folder, and no worker's lock (a
83
- # chi REPL's, +owner+, is no worker's).
83
+ # one-turn chi run's, +owner+, is no worker's).
84
84
  None = Data.define(:owner) do
85
85
  def initialize(owner: nil) = super
86
86
 
@@ -3,6 +3,7 @@
3
3
  require_relative "working_set"
4
4
  require_relative "working_set_steer"
5
5
  require_relative "tools/memory"
6
+ require_relative "session"
6
7
 
7
8
  module Samagotchi
8
9
  # /working-set: with no arguments the session's working set (WorkingSet,
@@ -50,7 +51,7 @@ module Samagotchi
50
51
  return [listing, [], :ok] if verb.nil?
51
52
  raise ArgumentError, USAGE unless VERBS.include?(verb) && !words.empty?
52
53
 
53
- session = @engine.session or raise ArgumentError, "no session yet"
54
+ session = @engine.session or raise ArgumentError, Session::NONE_YET
54
55
  cwd = session.working_directory
55
56
  strategy = @engine.working_set_strategy
56
57
  messages = @engine.clone_messages(session.messages)
@@ -79,7 +80,7 @@ module Samagotchi
79
80
  # The set under the strategy the next turn runs, and the pending ops.
80
81
  def listing
81
82
  session = @engine.session
82
- return "no session yet" unless session
83
+ return Session::NONE_YET unless session
83
84
 
84
85
  messages = Array(session.messages)
85
86
  items = WorkingSet.build(messages, cwd: session.working_directory, strategy: @engine.working_set_strategy,
data/lib/samagotchi.rb CHANGED
@@ -7,7 +7,6 @@ require_relative "samagotchi/prompt"
7
7
  require_relative "samagotchi/client"
8
8
  require_relative "samagotchi/debug_log"
9
9
  require_relative "samagotchi/delivery"
10
- require_relative "samagotchi/pending_input_queue"
11
10
  require_relative "samagotchi/kernel_loop"
12
11
  require_relative "samagotchi/session"
13
12
  require_relative "samagotchi/terminal_ui"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.153.0
4
+ version: 0.154.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -185,6 +185,7 @@ files:
185
185
  - lib/samagotchi/answer_display.rb
186
186
  - lib/samagotchi/answer_refs.rb
187
187
  - lib/samagotchi/answer_tail.rb
188
+ - lib/samagotchi/answer_text.rb
188
189
  - lib/samagotchi/archive_store.rb
189
190
  - lib/samagotchi/atomic_file.rb
190
191
  - lib/samagotchi/block_renderers.rb
@@ -335,6 +336,7 @@ files:
335
336
  - lib/samagotchi/cut.rb
336
337
  - lib/samagotchi/cut_policy.rb
337
338
  - lib/samagotchi/debug_log.rb
339
+ - lib/samagotchi/degrade.rb
338
340
  - lib/samagotchi/delivery.rb
339
341
  - lib/samagotchi/desktop.rb
340
342
  - lib/samagotchi/desktop/macos.rb
@@ -356,6 +358,26 @@ files:
356
358
  - lib/samagotchi/effective_model.rb
357
359
  - lib/samagotchi/empty_answer_retry.rb
358
360
  - lib/samagotchi/engine.rb
361
+ - lib/samagotchi/engine/cards.rb
362
+ - lib/samagotchi/engine/context_judge.rb
363
+ - lib/samagotchi/engine/events.rb
364
+ - lib/samagotchi/engine/guardrails.rb
365
+ - lib/samagotchi/engine/hooks.rb
366
+ - lib/samagotchi/engine/messages.rb
367
+ - lib/samagotchi/engine/model_settings.rb
368
+ - lib/samagotchi/engine/models.rb
369
+ - lib/samagotchi/engine/objectives.rb
370
+ - lib/samagotchi/engine/participants.rb
371
+ - lib/samagotchi/engine/plugins.rb
372
+ - lib/samagotchi/engine/questions.rb
373
+ - lib/samagotchi/engine/recap.rb
374
+ - lib/samagotchi/engine/session.rb
375
+ - lib/samagotchi/engine/side_requests.rb
376
+ - lib/samagotchi/engine/steering.rb
377
+ - lib/samagotchi/engine/toolkit.rb
378
+ - lib/samagotchi/engine/turn_end.rb
379
+ - lib/samagotchi/engine/turn_helpers.rb
380
+ - lib/samagotchi/engine/verify.rb
359
381
  - lib/samagotchi/engine_peers.rb
360
382
  - lib/samagotchi/engine_snapshots.rb
361
383
  - lib/samagotchi/event_fanout.rb
@@ -367,6 +389,7 @@ files:
367
389
  - lib/samagotchi/generation_phase.rb
368
390
  - lib/samagotchi/git_env.rb
369
391
  - lib/samagotchi/git_head.rb
392
+ - lib/samagotchi/grants.rb
370
393
  - lib/samagotchi/guardrail_wiring.rb
371
394
  - lib/samagotchi/guardrails.rb
372
395
  - lib/samagotchi/guardrails/approval.rb
@@ -526,11 +549,13 @@ files:
526
549
  - lib/samagotchi/objectives_audit.rb
527
550
  - lib/samagotchi/objectives_check.rb
528
551
  - lib/samagotchi/objectives_command.rb
552
+ - lib/samagotchi/objectives_edit.rb
529
553
  - lib/samagotchi/output_formatter.rb
530
554
  - lib/samagotchi/owner_lock.rb
531
555
  - lib/samagotchi/owner_prompt.rb
532
556
  - lib/samagotchi/parent_answers_command.rb
533
557
  - lib/samagotchi/parent_report.rb
558
+ - lib/samagotchi/participant_exposure.rb
534
559
  - lib/samagotchi/participant_messages.rb
535
560
  - lib/samagotchi/participant_mode.rb
536
561
  - lib/samagotchi/participant_name.rb
@@ -538,7 +563,6 @@ files:
538
563
  - lib/samagotchi/participants_watch.rb
539
564
  - lib/samagotchi/paths.rb
540
565
  - lib/samagotchi/peer_sources.rb
541
- - lib/samagotchi/pending_input_queue.rb
542
566
  - lib/samagotchi/pending_question.rb
543
567
  - lib/samagotchi/plugin/api.rb
544
568
  - lib/samagotchi/plugin/attached_context.rb
@@ -557,6 +581,7 @@ files:
557
581
  - lib/samagotchi/plugin/tool_spec.rb
558
582
  - lib/samagotchi/plugin_session_state.rb
559
583
  - lib/samagotchi/plugin_tasks.rb
584
+ - lib/samagotchi/private_files.rb
560
585
  - lib/samagotchi/process_group.rb
561
586
  - lib/samagotchi/project_env.rb
562
587
  - lib/samagotchi/project_scope.rb
@@ -578,6 +603,7 @@ files:
578
603
  - lib/samagotchi/recap_setup.rb
579
604
  - lib/samagotchi/recap_store.rb
580
605
  - lib/samagotchi/recap_title.rb
606
+ - lib/samagotchi/redact.rb
581
607
  - lib/samagotchi/redacted_inspect.rb
582
608
  - lib/samagotchi/ref.rb
583
609
  - lib/samagotchi/relay.rb
@@ -643,6 +669,7 @@ files:
643
669
  - lib/samagotchi/steer.rb
644
670
  - lib/samagotchi/steer_cut.rb
645
671
  - lib/samagotchi/steer_cut_after.rb
672
+ - lib/samagotchi/stream_event.rb
646
673
  - lib/samagotchi/system_one.rb
647
674
  - lib/samagotchi/system_one/answer.rb
648
675
  - lib/samagotchi/system_one/approval_label.rb
@@ -674,7 +701,6 @@ files:
674
701
  - lib/samagotchi/terminal_ui/attached_loop/queueing.rb
675
702
  - lib/samagotchi/terminal_ui/attached_loop/sending.rb
676
703
  - lib/samagotchi/terminal_ui/attached_view.rb
677
- - lib/samagotchi/terminal_ui/between_turns.rb
678
704
  - lib/samagotchi/terminal_ui/event_renderer.rb
679
705
  - lib/samagotchi/terminal_ui/exit_lines.rb
680
706
  - lib/samagotchi/terminal_ui/formatting.rb
@@ -685,6 +711,7 @@ files:
685
711
  - lib/samagotchi/terminal_ui/line_reader.rb
686
712
  - lib/samagotchi/terminal_ui/live_region.rb
687
713
  - lib/samagotchi/terminal_ui/local_link.rb
714
+ - lib/samagotchi/terminal_ui/log_tail.rb
688
715
  - lib/samagotchi/terminal_ui/model_segments_text.rb
689
716
  - lib/samagotchi/terminal_ui/one_shot_sink.rb
690
717
  - lib/samagotchi/terminal_ui/own_turns.rb
@@ -696,7 +723,6 @@ files:
696
723
  - lib/samagotchi/terminal_ui/reline_seam.rb
697
724
  - lib/samagotchi/terminal_ui/remote_connector.rb
698
725
  - lib/samagotchi/terminal_ui/remote_link.rb
699
- - lib/samagotchi/terminal_ui/repl_input.rb
700
726
  - lib/samagotchi/terminal_ui/screen.rb
701
727
  - lib/samagotchi/terminal_ui/status_row.rb
702
728
  - lib/samagotchi/terminal_ui/surface.rb
@@ -810,20 +836,17 @@ files:
810
836
  - lib/samagotchi/web/block_renderer_routes.rb
811
837
  - lib/samagotchi/web/capabilities.rb
812
838
  - lib/samagotchi/web/editor.rb
813
- - lib/samagotchi/web/grants.rb
814
839
  - lib/samagotchi/web/info_probe.rb
815
840
  - lib/samagotchi/web/lan.rb
816
841
  - lib/samagotchi/web/link_codes.rb
817
842
  - lib/samagotchi/web/markdown_renderer.rb
818
843
  - lib/samagotchi/web/message_display.rb
819
844
  - lib/samagotchi/web/participant_access.rb
820
- - lib/samagotchi/web/participant_exposure.rb
821
845
  - lib/samagotchi/web/participant_review.rb
822
846
  - lib/samagotchi/web/participant_stream_filter.rb
823
847
  - lib/samagotchi/web/participant_view.rb
824
848
  - lib/samagotchi/web/participants_admin.rb
825
849
  - lib/samagotchi/web/principal.rb
826
- - lib/samagotchi/web/private_files.rb
827
850
  - lib/samagotchi/web/project_list.rb
828
851
  - lib/samagotchi/web/proxy_gate.rb
829
852
  - lib/samagotchi/web/public/activity.js
@@ -844,6 +867,7 @@ files:
844
867
  - lib/samagotchi/web/public/block_source.js
845
868
  - lib/samagotchi/web/public/bubbles.js
846
869
  - lib/samagotchi/web/public/card.js
870
+ - lib/samagotchi/web/public/card_stamps.js
847
871
  - lib/samagotchi/web/public/cards_place.js
848
872
  - lib/samagotchi/web/public/chains.js
849
873
  - lib/samagotchi/web/public/chip_popover.js
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Samagotchi
4
- # Thread-safe FIFO of user steering messages submitted while a turn is
5
- # running. UIs (TUI, web, background workers) are push-only producers;
6
- # KernelLoop drains the queue at iteration boundaries and injects the
7
- # messages into the conversation. True mid-stream injection is impossible
8
- # with llama.cpp's /completion API, so draining only happens between full
9
- # LLM responses / tool dispatches.
10
- #
11
- # Usage:
12
- # queue = PendingInputQueue.new
13
- # queue.push("please also check the specs")
14
- # kernel.run(messages, pending_input: queue.method(:drain))
15
- class PendingInputQueue
16
- def initialize
17
- @mutex = Mutex.new
18
- @messages = []
19
- end
20
-
21
- # Append a message to the queue. Nil/empty strings are ignored.
22
- # +seq+ is the sender's own order number for it (the REPL numbers every
23
- # line it takes during a turn, so its leftovers keep send order across
24
- # this queue and its after-turn list: #drain_entries).
25
- def push(text, seq: nil)
26
- text = text.to_s
27
- return if text.empty?
28
-
29
- @mutex.synchronize { @messages << [seq, text] }
30
- nil
31
- end
32
-
33
- # Remove and return all queued messages in FIFO order. Non-blocking.
34
- # @return [Array<String>]
35
- def drain = drain_entries.map(&:last)
36
-
37
- # #drain with each message's +seq+ (nil when pushed without one).
38
- # @return [Array(Integer|nil, String)]
39
- def drain_entries
40
- @mutex.synchronize do
41
- drained = @messages
42
- @messages = []
43
- drained
44
- end
45
- end
46
-
47
- def empty?
48
- @mutex.synchronize { @messages.empty? }
49
- end
50
-
51
- def size
52
- @mutex.synchronize { @messages.size }
53
- end
54
- end
55
- end
@@ -1,138 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "formatting"
4
- require_relative "event_renderer"
5
-
6
- module Samagotchi
7
- class TerminalUI
8
- # What the REPL prints between turns: cards and plugins' notices shown
9
- # outside a turn, anytime commands' output, plugins' init lines and load
10
- # warnings, and a recap written while idle.
11
- #
12
- # They are announced on other threads (the Engine's observers, the idle
13
- # scheduler, an anytime command's own), where they are only kept; the
14
- # main thread prints them at the open prompt (#flush_recap,
15
- # #flush_cards), after the command that showed them. One that comes
16
- # while a turn runs prints at once (above the live region) where the
17
- # turn wouldn't print it itself.
18
- class BetweenTurns
19
- include Formatting
20
-
21
- # A plugin's init task and a load warning: shown between turns too.
22
- INIT_EVENTS = %i[plugin_init_started plugin_init_finished guardrail_warning].freeze
23
-
24
- # @param surface [Surface] where lines print (swapped with #surface=)
25
- # @param view [AttachedView] turns the activity row for init tasks
26
- # @param renderer [EventRenderer] draws cards
27
- # @param turn_running [#call] whether a turn runs now
28
- # @param quiet [Boolean] a --non-interactive run: no init lines
29
- # @param gaps [GapClock, nil] the session's last activity: a recap
30
- # after a quiet gap has its divider
31
- def initialize(surface:, view:, renderer:, turn_running:, quiet: false, gaps: nil)
32
- @surface = surface
33
- @view = view
34
- @renderer = renderer
35
- @turn_running = turn_running
36
- @quiet = quiet
37
- @cards = Queue.new
38
- @recap = nil
39
- @gaps = gaps
40
- end
41
-
42
- attr_writer :surface
43
-
44
- # The Engine's observer for cards, notices and init events (any
45
- # thread). A card or a plugin's notice shown outside a turn is kept
46
- # for the next flush; one shown during a turn is a turn event, the
47
- # turn's sink prints it where it happens (EventRenderer).
48
- #
49
- # An anytime command's (event[:anytime]) print as it shows them: on
50
- # the main thread (the command runs at the prompt) or beside a running
51
- # turn, else at the next flush.
52
- #
53
- # A plugin's init task (chi.init) turns the activity row while it runs
54
- # and prints a line when it is done; a load warning announced before
55
- # the first turn, one. Beside a running turn they print at once.
56
- def observe(event)
57
- if INIT_EVENTS.include?(event[:type])
58
- return if @quiet
59
- return @view.init_started(event) if event[:type] == :plugin_init_started
60
-
61
- @view.init_finished(event) if event[:type] == :plugin_init_finished
62
- return show(event) if turn_running?
63
-
64
- return @cards << event
65
- end
66
- return unless (event[:type] == :card && !event[:in_turn]) || (event[:type] == :hook_notice && event[:between_turns])
67
- return show(event) if event[:anytime] && (Thread.current == Thread.main || turn_running?)
68
-
69
- @cards << event
70
- end
71
-
72
- # Keep an item for the next flush (any thread).
73
- def keep(item)
74
- @cards << item
75
- end
76
-
77
- # An anytime command's output: beside a running turn at once, else at
78
- # the next flush.
79
- def show_or_keep(item)
80
- turn_running? ? show(item) : keep(item)
81
- end
82
-
83
- # The Engine's observer for :recap_ready (the idle scheduler's thread):
84
- # kept for #flush_recap. One collected just as a turn started describes
85
- # the chat before it, and is dropped.
86
- def take_recap(event)
87
- return unless event[:type] == :recap_ready
88
- return if event[:recap].to_s.strip.empty? || turn_running?
89
-
90
- @recap = { text: event[:recap].to_s, at: event[:created_at] }
91
- end
92
-
93
- # Print a recap written while idle (main thread, at the open prompt).
94
- def flush_recap
95
- recap = @recap
96
- return unless recap
97
-
98
- @recap = nil
99
- line = @gaps&.divider
100
- @surface.commit(gap_divider(line)) if line
101
- @surface.commit(recap_block(recap[:text], at: recap[:at]))
102
- end
103
-
104
- # Print the cards, notices and anytime commands' output kept since the
105
- # last flush (main thread). A card replaced later in the same batch
106
- # (the same id: btw's "thinking…", then its answer) prints once, as its
107
- # last.
108
- def flush_cards
109
- items = []
110
- loop { items << @cards.pop(true) }
111
- rescue ThreadError
112
- items.each_with_index do |item, index|
113
- replaced = item[:type] == :card && items.drop(index + 1).any? { |later| later[:type] == :card && later[:id] == item[:id] }
114
- show(item) unless replaced
115
- end
116
- nil
117
- end
118
-
119
- # A card, a notice, an anytime command's output (:command_output), a
120
- # plugin init task's done line or a load warning.
121
- def show(item)
122
- case item[:type]
123
- when :card then @renderer.render_card(item)
124
- when :command_output then @surface.commit(item[:text])
125
- when :guardrail_warning then @surface.commit(EventRenderer.load_warning_line(item))
126
- when :plugin_init_finished
127
- line = EventRenderer.init_line(item)
128
- @surface.commit(line) if line
129
- else @surface.commit(EventRenderer.hook_notice_line(item))
130
- end
131
- end
132
-
133
- private
134
-
135
- def turn_running? = @turn_running.call
136
- end
137
- end
138
- end