samagotchi 0.7.0 → 0.8.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -1
  3. data/README.md +7 -3
  4. data/docs/architecture.md +4 -4
  5. data/docs/cli.md +19 -4
  6. data/docs/guardrails.md +2 -2
  7. data/docs/hooks.md +2 -2
  8. data/docs/internals/context-telemetry.md +3 -1
  9. data/docs/memory.md +48 -1
  10. data/docs/plugins.md +7 -5
  11. data/docs/releasing.md +5 -1
  12. data/docs/sessions.md +2 -2
  13. data/lib/samagotchi/archive_store.rb +3 -3
  14. data/lib/samagotchi/atomic_file.rb +34 -0
  15. data/lib/samagotchi/bootstrap/bundles.rb +37 -0
  16. data/lib/samagotchi/bootstrap/config_writer.rb +4 -6
  17. data/lib/samagotchi/bootstrap/probe.rb +9 -5
  18. data/lib/samagotchi/bootstrap_command.rb +78 -8
  19. data/lib/samagotchi/bridge/pending_card.rb +2 -3
  20. data/lib/samagotchi/bridge.rb +56 -116
  21. data/lib/samagotchi/bridge_client.rb +7 -11
  22. data/lib/samagotchi/bundle_command.rb +81 -21
  23. data/lib/samagotchi/bundle_needs.rb +2 -11
  24. data/lib/samagotchi/bundles/core/manifest.yml +11 -0
  25. data/lib/samagotchi/bundles/dev/manifest.yml +13 -0
  26. data/lib/samagotchi/bundles/system/identity.md +1 -0
  27. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  28. data/lib/samagotchi/bundles/system/memory_guide.md +3 -2
  29. data/lib/samagotchi/client.rb +3 -23
  30. data/lib/samagotchi/config.rb +6 -11
  31. data/lib/samagotchi/context_note.rb +1 -1
  32. data/lib/samagotchi/context_status.rb +255 -0
  33. data/lib/samagotchi/desktop/macos.rb +2 -3
  34. data/lib/samagotchi/engine.rb +6 -7
  35. data/lib/samagotchi/guardrail_wiring.rb +2 -5
  36. data/lib/samagotchi/guardrails/approvals.rb +2 -7
  37. data/lib/samagotchi/hooks/bundle_loader.rb +6 -3
  38. data/lib/samagotchi/hooks/loader.rb +2 -1
  39. data/lib/samagotchi/hooks/registry.rb +21 -0
  40. data/lib/samagotchi/image_store.rb +2 -3
  41. data/lib/samagotchi/kernel_loop.rb +21 -283
  42. data/lib/samagotchi/live_versions.rb +9 -13
  43. data/lib/samagotchi/llm/chat_loop.rb +1 -1
  44. data/lib/samagotchi/llm/native_backend.rb +8 -8
  45. data/lib/samagotchi/llm/openai_chat.rb +4 -17
  46. data/lib/samagotchi/llm/utf8_scrub.rb +32 -0
  47. data/lib/samagotchi/log_path.rb +2 -3
  48. data/lib/samagotchi/memory_bundle/index_updater.rb +2 -11
  49. data/lib/samagotchi/memory_bundle/installer.rb +5 -7
  50. data/lib/samagotchi/memory_bundle/listing.rb +27 -20
  51. data/lib/samagotchi/memory_bundle/manifest.rb +32 -1
  52. data/lib/samagotchi/memory_bundle/profile.rb +145 -0
  53. data/lib/samagotchi/memory_bundle/provenance.rb +42 -53
  54. data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -27
  55. data/lib/samagotchi/memory_bundle/source.rb +2 -0
  56. data/lib/samagotchi/memory_bundle/status.rb +1 -2
  57. data/lib/samagotchi/memory_bundle.rb +1 -0
  58. data/lib/samagotchi/note_command.rb +5 -4
  59. data/lib/samagotchi/owner_lock.rb +0 -6
  60. data/lib/samagotchi/paths.rb +32 -0
  61. data/lib/samagotchi/plugin/api.rb +3 -6
  62. data/lib/samagotchi/plugin/context.rb +2 -3
  63. data/lib/samagotchi/plugin/loader.rb +1 -1
  64. data/lib/samagotchi/question_desk.rb +3 -2
  65. data/lib/samagotchi/recap_store.rb +2 -2
  66. data/lib/samagotchi/reply_wait.rb +2 -1
  67. data/lib/samagotchi/self_report.rb +2 -3
  68. data/lib/samagotchi/send_command.rb +3 -2
  69. data/lib/samagotchi/session.rb +41 -143
  70. data/lib/samagotchi/session_inbox.rb +175 -0
  71. data/lib/samagotchi/session_manager.rb +62 -363
  72. data/lib/samagotchi/session_metrics.rb +2 -3
  73. data/lib/samagotchi/session_retention.rb +259 -0
  74. data/lib/samagotchi/sessions_command.rb +3 -2
  75. data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -2
  76. data/lib/samagotchi/terminal_ui/input_support.rb +2 -9
  77. data/lib/samagotchi/terminal_ui.rb +10 -20
  78. data/lib/samagotchi/tools/memory.rb +3 -2
  79. data/lib/samagotchi/tools/send_note.rb +4 -3
  80. data/lib/samagotchi/tools/task_runtime.rb +2 -3
  81. data/lib/samagotchi/turn_note.rb +13 -2
  82. data/lib/samagotchi/update_command.rb +9 -2
  83. data/lib/samagotchi/version.rb +1 -1
  84. data/lib/samagotchi/web/app.rb +44 -76
  85. data/lib/samagotchi/web/info_probe.rb +33 -0
  86. data/lib/samagotchi/web/public/app.js +1 -1
  87. data/lib/samagotchi/web/public/index.html +1 -0
  88. data/lib/samagotchi/web/public/stage_model.js +22 -3
  89. data/lib/samagotchi/web/public/stage_view.js +25 -10
  90. data/lib/samagotchi/web/server.rb +8 -11
  91. data/lib/samagotchi/web/session_hub.rb +8 -7
  92. data/lib/samagotchi/web/session_summary.rb +4 -3
  93. data/lib/samagotchi/web/token.rb +2 -5
  94. data/lib/samagotchi/worker.rb +34 -20
  95. data/lib/samagotchi/worker_sidecar.rb +42 -0
  96. metadata +13 -1
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
 
5
5
  require_relative "session"
6
+ require_relative "session_inbox"
6
7
  require_relative "turn_note"
7
8
  require_relative "context_note"
8
9
  require_relative "worker_idle_exit"
@@ -24,9 +25,9 @@ module Samagotchi
24
25
  # disk exits the process; an error outside a turn marks the session and
25
26
  # exits with 1.
26
27
  #
27
- # The file IPC stays behind SessionManager's class methods
28
- # (find_new_input_files, claim_input_file, start_bridge, ...), which specs
29
- # stub as seams.
28
+ # The file IPC stays behind SessionInbox's functions (find_new_input_files,
29
+ # claim_input_file, ...) and the Bridge behind SessionManager.start_bridge,
30
+ # which specs stub as seams.
30
31
  #
31
32
  # The loop sleeps on a Waker, which the Bridge wakes when it queues a turn
32
33
  # or a command, and the reminder callback when it queues one. A fallback
@@ -98,6 +99,7 @@ module Samagotchi
98
99
  # to exit (Bridge POST /exit)
99
100
  def run
100
101
  @session = Session.load(@session_id, state_dir: @state_dir)
102
+ drop_dead_question
101
103
  @engine = build_engine
102
104
  # Before the Bridge serves anything: a UI joining a resumed worker's
103
105
  # stream gets the session's history and status in its snapshot, not
@@ -142,7 +144,7 @@ module Samagotchi
142
144
  @idle_exit = WorkerIdleExit.new(
143
145
  engine: @engine, bridge: @bridge,
144
146
  timeout_minutes: @idle_exit_minutes || SessionManager.config_idle_exit_minutes,
145
- input_pending: -> { !@command_queue.empty? || !SessionManager.find_new_input_files(@session_dir).empty? },
147
+ input_pending: -> { !@command_queue.empty? || !SessionInbox.find_new_input_files(@session_dir).empty? },
146
148
  awaiting_continue: -> { @turn_flow.awaiting_continue? }
147
149
  )
148
150
 
@@ -170,7 +172,7 @@ module Samagotchi
170
172
  next
171
173
  end
172
174
 
173
- input_files = SessionManager.find_new_input_files(@session_dir)
175
+ input_files = SessionInbox.find_new_input_files(@session_dir)
174
176
  if input_files.empty?
175
177
  next if run_due_reminders
176
178
  return left(:idle_exit) if @idle_exit.due? && leave_idle
@@ -208,7 +210,6 @@ module Samagotchi
208
210
  waker = @waker
209
211
  engine = nil
210
212
  engine = Samagotchi::Engine.new(
211
- mode: @session.mode.to_sym,
212
213
  model_name: @session.model_name,
213
214
  # The session's --memory and --mute lists: the same prompt on every
214
215
  # (re)spawn.
@@ -231,6 +232,19 @@ module Samagotchi
231
232
  engine
232
233
  end
233
234
 
235
+ # A question saved by a worker that died while it waited (an
236
+ # ask_user_question, an approval, a hook's question): the turn that
237
+ # asked is gone, so nobody can answer it, and left in the file it
238
+ # would keep the hub's "needs you" badge on for good. Dropped and saved
239
+ # before the Engine and the Bridge exist (no question lock, no events).
240
+ def drop_dead_question
241
+ return unless @session.pending_question
242
+
243
+ Log.info(:worker, "dead_question_dropped", id: @session.pending_question[:id])
244
+ @session.pending_question = nil
245
+ @session.save(state_dir: @state_dir)
246
+ end
247
+
234
248
  # spawn_session hands the first prompt over in last_prompt, but
235
249
  # last_prompt also records every later turn's prompt (and mark_error's
236
250
  # reason), so only a session with no conversation yet has one pending; a
@@ -255,12 +269,12 @@ module Samagotchi
255
269
  # the saved conversation already holds. Not activity: a note alone
256
270
  # neither starts a turn nor keeps an idle worker up.
257
271
  def absorb_notes
258
- files = SessionManager.find_new_note_files(@session_dir)
272
+ files = SessionInbox.find_new_note_files(@session_dir)
259
273
  return if files.empty? || stopped_on_disk?
260
274
 
261
- claimed = files.filter_map { |file| SessionManager.claim_note_file(file) }
275
+ claimed = files.filter_map { |file| SessionInbox.claim_note_file(file) }
262
276
  claimed.each do |file|
263
- note = SessionManager.read_note(file)
277
+ note = SessionInbox.read_note(file)
264
278
  @engine.add_context_note(@session, note) if note
265
279
  end
266
280
  @session.save(state_dir: @state_dir)
@@ -268,11 +282,11 @@ module Samagotchi
268
282
  end
269
283
 
270
284
  def run_input_file(input_file)
271
- claimed_file = SessionManager.claim_input_file(input_file)
285
+ claimed_file = SessionInbox.claim_input_file(input_file)
272
286
  return unless claimed_file
273
287
 
274
288
  begin
275
- message, origin, no_interrupt, images = SessionManager.read_input(claimed_file)
289
+ message, origin, no_interrupt, images = SessionInbox.read_input(claimed_file)
276
290
  run_prompt(message, origin, no_interrupt: !!no_interrupt, images: images || []) unless message.to_s.strip.empty?
277
291
  ensure
278
292
  FileUtils.rm_f(claimed_file)
@@ -303,7 +317,7 @@ module Samagotchi
303
317
  end
304
318
  after_turn(result, no_interrupt: no_interrupt)
305
319
  response = result.respond_to?(:output) ? result.output : nil
306
- SessionManager.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
320
+ SessionInbox.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
307
321
  @session.save(state_dir: @state_dir) unless stopped_on_disk?
308
322
  end
309
323
 
@@ -326,7 +340,7 @@ module Samagotchi
326
340
  origin: { client_id: SessionManager::REMINDER_CLIENT_ID },
327
341
  max_iterations: DEFAULT_MAX_ITERATIONS)
328
342
  response = result.respond_to?(:output) ? result.output : nil
329
- SessionManager.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
343
+ SessionInbox.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
330
344
  rescue StandardError
331
345
  nil # the Engine announced :turn_failed; there is no prompt to hand back
332
346
  ensure
@@ -462,7 +476,7 @@ module Samagotchi
462
476
  end
463
477
  after_turn(result, continue: true, no_interrupt: offer[:no_interrupt])
464
478
  response = result.respond_to?(:output) ? result.output : nil
465
- SessionManager.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
479
+ SessionInbox.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
466
480
  @session.save(state_dir: @state_dir) unless stopped_on_disk?
467
481
  end
468
482
 
@@ -527,14 +541,14 @@ module Samagotchi
527
541
  refuse_queued_commands(mid_turn: true)
528
542
  # A line with images runs as its own next turn (steering merges text
529
543
  # only), and so does anything queued after it, to keep the order.
530
- files = SessionManager.find_new_input_files(@session_dir).sort
531
- .take_while { |input_file| !SessionManager.input_has_images?(input_file) }
544
+ files = SessionInbox.find_new_input_files(@session_dir).sort
545
+ .take_while { |input_file| !SessionInbox.input_has_images?(input_file) }
532
546
  merged = files.filter_map do |input_file|
533
- claimed_file = SessionManager.claim_input_file(input_file)
547
+ claimed_file = SessionInbox.claim_input_file(input_file)
534
548
  next unless claimed_file
535
549
 
536
550
  begin
537
- prompt, origin = SessionManager.read_input(claimed_file)
551
+ prompt, origin = SessionInbox.read_input(claimed_file)
538
552
  prompt = prompt.to_s.strip
539
553
  prompt.empty? ? nil : [prompt, origin]
540
554
  ensure
@@ -626,8 +640,8 @@ module Samagotchi
626
640
 
627
641
  # Memory used before any turn saved it lives only in the Engine.
628
642
  def empty_session?
629
- SessionManager.discard_empty? && Array(@engine.used_memory_names).empty? &&
630
- SessionManager.empty_session?(@session_id, state_dir: @state_dir, default_model: @default_model)
643
+ SessionManager.discardable?(@session_id, state_dir: @state_dir, default_model: @default_model,
644
+ used_memory_names: @engine.used_memory_names)
631
645
  end
632
646
 
633
647
  def log_idle_exit
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require_relative "atomic_file"
6
+
7
+ module Samagotchi
8
+ # A worker's bridge.json in its session directory: how clients reach its
9
+ # Bridge (port, bind), when it started (a new worker behind the same
10
+ # "worker" owner), the chi it runs (chi update) and the input-file format
11
+ # it reads (SessionInbox::INPUT_FORMAT). The Bridge writes it on start
12
+ # and removes it on stop; one a dead worker left stays until a client's
13
+ # probe finds its port closed (BridgeClient.sidecar_port).
14
+ class WorkerSidecar < Data.define(:port, :bind, :session_id, :started_at, :version, :input_format)
15
+ FILE = "bridge.json"
16
+
17
+ def self.path(session_dir) = File.join(session_dir, FILE)
18
+
19
+ # @return [WorkerSidecar, nil] nil when there is none, or it is not a
20
+ # JSON object; port is 0 when it names none
21
+ def self.read(session_dir)
22
+ data = JSON.parse(File.read(path(session_dir)))
23
+ return nil unless data.is_a?(Hash)
24
+
25
+ port = data["port"]
26
+ new(port: port.respond_to?(:to_i) ? port.to_i : 0, bind: data["bind"], session_id: data["session_id"],
27
+ started_at: data["started_at"], version: data["version"], input_format: data["input_format"])
28
+ rescue JSON::ParserError, SystemCallError
29
+ nil
30
+ end
31
+
32
+ def initialize(port:, bind: nil, session_id: nil, started_at: nil, version: nil, input_format: nil) = super
33
+
34
+ # Written whole or not at all; input_format only when it names one.
35
+ def write(session_dir)
36
+ FileUtils.mkdir_p(session_dir)
37
+ record = to_h.transform_keys(&:to_s)
38
+ record.delete("input_format") if input_format.nil?
39
+ AtomicFile.write(self.class.path(session_dir), "#{JSON.pretty_generate(record)}\n")
40
+ end
41
+ end
42
+ end
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -139,6 +139,8 @@ files:
139
139
  - lib/samagotchi.rb
140
140
  - lib/samagotchi/answer_display.rb
141
141
  - lib/samagotchi/archive_store.rb
142
+ - lib/samagotchi/atomic_file.rb
143
+ - lib/samagotchi/bootstrap/bundles.rb
142
144
  - lib/samagotchi/bootstrap/config_writer.rb
143
145
  - lib/samagotchi/bootstrap/probe.rb
144
146
  - lib/samagotchi/bootstrap_command.rb
@@ -160,6 +162,8 @@ files:
160
162
  - lib/samagotchi/bundles/btw/plugin.rb
161
163
  - lib/samagotchi/bundles/check-in/manifest.yml
162
164
  - lib/samagotchi/bundles/check-in/plugin.rb
165
+ - lib/samagotchi/bundles/core/manifest.yml
166
+ - lib/samagotchi/bundles/dev/manifest.yml
163
167
  - lib/samagotchi/bundles/guardrails/guardrails.md
164
168
  - lib/samagotchi/bundles/guardrails/guardrails/rules.yml
165
169
  - lib/samagotchi/bundles/guardrails/manifest.yml
@@ -187,6 +191,7 @@ files:
187
191
  - lib/samagotchi/config.rb
188
192
  - lib/samagotchi/context_note.rb
189
193
  - lib/samagotchi/context_quote.rb
194
+ - lib/samagotchi/context_status.rb
190
195
  - lib/samagotchi/context_usage.rb
191
196
  - lib/samagotchi/context_window.rb
192
197
  - lib/samagotchi/debug_log.rb
@@ -242,6 +247,7 @@ files:
242
247
  - lib/samagotchi/llm/native_tool_normalizer.rb
243
248
  - lib/samagotchi/llm/openai_chat.rb
244
249
  - lib/samagotchi/llm/usage.rb
250
+ - lib/samagotchi/llm/utf8_scrub.rb
245
251
  - lib/samagotchi/log.rb
246
252
  - lib/samagotchi/log_line.rb
247
253
  - lib/samagotchi/log_path.rb
@@ -254,6 +260,7 @@ files:
254
260
  - lib/samagotchi/memory_bundle/manifest.rb
255
261
  - lib/samagotchi/memory_bundle/merger.rb
256
262
  - lib/samagotchi/memory_bundle/placeholder.rb
263
+ - lib/samagotchi/memory_bundle/profile.rb
257
264
  - lib/samagotchi/memory_bundle/provenance.rb
258
265
  - lib/samagotchi/memory_bundle/shipped_update.rb
259
266
  - lib/samagotchi/memory_bundle/source.rb
@@ -267,6 +274,7 @@ files:
267
274
  - lib/samagotchi/note_command.rb
268
275
  - lib/samagotchi/output_formatter.rb
269
276
  - lib/samagotchi/owner_lock.rb
277
+ - lib/samagotchi/paths.rb
270
278
  - lib/samagotchi/pending_input_queue.rb
271
279
  - lib/samagotchi/plugin/api.rb
272
280
  - lib/samagotchi/plugin/context.rb
@@ -292,9 +300,11 @@ files:
292
300
  - lib/samagotchi/session_archive_command.rb
293
301
  - lib/samagotchi/session_commands.rb
294
302
  - lib/samagotchi/session_delete_command.rb
303
+ - lib/samagotchi/session_inbox.rb
295
304
  - lib/samagotchi/session_manager.rb
296
305
  - lib/samagotchi/session_metrics.rb
297
306
  - lib/samagotchi/session_observer.rb
307
+ - lib/samagotchi/session_retention.rb
298
308
  - lib/samagotchi/sessions_command.rb
299
309
  - lib/samagotchi/steer.rb
300
310
  - lib/samagotchi/system_prompt.rb
@@ -362,6 +372,7 @@ files:
362
372
  - lib/samagotchi/vision_context.rb
363
373
  - lib/samagotchi/vision_support.rb
364
374
  - lib/samagotchi/web/app.rb
375
+ - lib/samagotchi/web/info_probe.rb
365
376
  - lib/samagotchi/web/lan.rb
366
377
  - lib/samagotchi/web/markdown_renderer.rb
367
378
  - lib/samagotchi/web/message_parts.rb
@@ -407,6 +418,7 @@ files:
407
418
  - lib/samagotchi/web/token.rb
408
419
  - lib/samagotchi/worker.rb
409
420
  - lib/samagotchi/worker_idle_exit.rb
421
+ - lib/samagotchi/worker_sidecar.rb
410
422
  homepage: https://github.com/dm1try/samagotchi
411
423
  licenses:
412
424
  - MIT