samagotchi 0.158.0 → 0.159.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/bin/chi +3 -1
  4. data/docs/architecture.md +7 -6
  5. data/docs/cli.md +56 -1
  6. data/docs/configuration.md +9 -0
  7. data/docs/context.md +7 -1
  8. data/docs/guardrails.md +3 -1
  9. data/docs/hooks.md +3 -0
  10. data/docs/internals/bridge.md +1 -0
  11. data/docs/internals/bundles.md +9 -7
  12. data/docs/memory.md +5 -1
  13. data/docs/scheduling.md +228 -0
  14. data/docs/sessions.md +25 -1
  15. data/lib/samagotchi/bridge.rb +26 -2
  16. data/lib/samagotchi/bundle_command.rb +38 -1
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/client_id.rb +13 -3
  19. data/lib/samagotchi/config.rb +12 -0
  20. data/lib/samagotchi/context_move.rb +74 -0
  21. data/lib/samagotchi/guardrails/pair_boundary.rb +1 -1
  22. data/lib/samagotchi/memory_bundle/asset_installer.rb +84 -103
  23. data/lib/samagotchi/memory_bundle/asset_state.rb +81 -0
  24. data/lib/samagotchi/memory_bundle/file_actions.rb +51 -0
  25. data/lib/samagotchi/memory_bundle/shipped_update.rb +17 -12
  26. data/lib/samagotchi/memory_bundle/status.rb +20 -1
  27. data/lib/samagotchi/memory_bundle/uninstaller.rb +10 -4
  28. data/lib/samagotchi/run_options.rb +1 -0
  29. data/lib/samagotchi/schedule_command.rb +252 -0
  30. data/lib/samagotchi/schedules.rb +948 -0
  31. data/lib/samagotchi/session_access/contract.rb +1 -1
  32. data/lib/samagotchi/session_access/local.rb +20 -0
  33. data/lib/samagotchi/session_manager/spawn.rb +25 -11
  34. data/lib/samagotchi/session_manager/worker_command.rb +13 -13
  35. data/lib/samagotchi/terminal_ui/formatting.rb +2 -0
  36. data/lib/samagotchi/tool_declarations.rb +43 -0
  37. data/lib/samagotchi/tools/builtin_calls.rb +2 -0
  38. data/lib/samagotchi/tools/builtins.rb +17 -0
  39. data/lib/samagotchi/tools/cancel_schedule.rb +43 -0
  40. data/lib/samagotchi/tools/list_schedules.rb +43 -0
  41. data/lib/samagotchi/tools/schedule_task.rb +39 -0
  42. data/lib/samagotchi/version.rb +1 -1
  43. data/lib/samagotchi/web/public/chains.js +32 -8
  44. data/lib/samagotchi/web/public/index.html +4 -1
  45. data/lib/samagotchi/web/public/info_bar.js +4 -3
  46. data/lib/samagotchi/web/public/turn_events.js +4 -0
  47. data/lib/samagotchi/web/session_hub.rb +20 -0
  48. data/lib/samagotchi/worker.rb +24 -6
  49. data/lib/samagotchi/worker_boot.rb +3 -4
  50. data/lib/samagotchi/worker_inbound.rb +23 -0
  51. data/lib/samagotchi/worker_link.rb +5 -0
  52. data/lib/samagotchi/worker_wakes.rb +9 -1
  53. metadata +8 -1
@@ -374,7 +374,9 @@ module Samagotchi
374
374
  # chi web's Stop asks about these before TERMed-ing
375
375
  # the worker (its own holds routes).
376
376
  on_holds: -> { @idle_exit&.stoppable_holds },
377
- on_holds_stop: -> { @hold_stop&.stop_all })
377
+ on_holds_stop: -> { @hold_stop&.stop_all },
378
+ # `chi schedule tick` asks this before firing.
379
+ on_activity: method(:activity_snapshot))
378
380
  # What failed to load and what plugins showed as they loaded, now that
379
381
  # a UI can be there (a late one gets them in the snapshot).
380
382
  @engine.announce_load_events!
@@ -410,12 +412,12 @@ module Samagotchi
410
412
  # there is no live SSE or in-process cancel/answer).
411
413
  # @param on_input [#call, nil] called after the Bridge queues a turn
412
414
  def start_bridge(engine:, state_dir:, session_id:, on_input: nil, on_command: nil, on_exit_request: nil,
413
- exit_discards: nil, exit_stopped: nil, on_holds: nil, on_holds_stop: nil)
415
+ exit_discards: nil, exit_stopped: nil, on_holds: nil, on_holds_stop: nil, on_activity: nil)
414
416
  require_relative "bridge"
415
417
  Samagotchi::Bridge.new(
416
418
  engine: engine, state_dir: state_dir, session_id: session_id, input_format: SessionInbox::INPUT_FORMAT,
417
419
  on_input: on_input, on_command: on_command, on_exit_request: on_exit_request, exit_discards: exit_discards,
418
- exit_stopped: exit_stopped, on_holds: on_holds, on_holds_stop: on_holds_stop
420
+ exit_stopped: exit_stopped, on_holds: on_holds, on_holds_stop: on_holds_stop, on_activity: on_activity
419
421
  ).start
420
422
  rescue StandardError => e
421
423
  Log.error(:bridge, "start_failed", echo: "Bridge: failed to start for session #{session_id}: #{e.class}: #{e.message}", sid: session_id, error: e.class.name)
@@ -426,6 +428,22 @@ module Samagotchi
426
428
  Degrade.call(:config, "idle_exit_minutes_unreadable") { Samagotchi::Config.get("session.idle_exit_minutes") }
427
429
  end
428
430
 
431
+ # What this worker is busy with right now, for GET /activity: the same
432
+ # signals WorkerIdleExit#hold reads. A schedule never holds a worker up,
433
+ # so +reminders+ and +tasks_holding+ are reported for the log but are not a
434
+ # reason for a tick to skip a firing.
435
+ # @return [Hash]
436
+ def activity_snapshot
437
+ {
438
+ turn_running: @engine.turn_running?,
439
+ input_pending: input_pending?,
440
+ awaiting_continue: @turn_flow.awaiting_continue?,
441
+ open_streams: @bridge&.open_streams.to_i,
442
+ reminders: @engine.reminder_store&.any? ? 1 : 0,
443
+ tasks_holding: Degrade.call(:worker, "activity_tasks_unreadable") { @task_wakes&.watch&.holding&.size.to_i }
444
+ }
445
+ end
446
+
429
447
  # The delegate children of this session that are running a turn or have
430
448
  # a live worker. ChildrenStatus.of scans the session files; this runs on
431
449
  # an exit request, not every pass.
@@ -1033,9 +1051,9 @@ module Samagotchi
1033
1051
  kept: "Its note stays for the user's next message.", mark: task_wake)
1034
1052
  end
1035
1053
 
1036
- # The idle loop's sleep: the fallback tick, or less while reports wait
1037
- # out the start grace.
1038
- def idle_wait = @wakes.idle_wait(@poll_interval) { wake_state == :due }
1054
+ # The idle loop's sleep: the fallback tick, or less while reports or
1055
+ # attached-context wakes wait out the start grace.
1056
+ def idle_wait = @wakes.idle_wait(@poll_interval) { wake_state == :due || @inbound.context_wake_waiting? }
1039
1057
 
1040
1058
  # The wake budget is spent: the parent's UIs hear once that reports
1041
1059
  # wait for the next message.
@@ -3,9 +3,8 @@
3
3
  require_relative "session_manager"
4
4
 
5
5
  module Samagotchi
6
- # WorkerBoot: the worker process, from run_session_loop (what the boot
7
- # string SessionManager#worker_command writes runs, through
8
- # SessionManager.run_session_loop) to what it does as it leaves (CROSS-VERSION
6
+ # WorkerBoot: the worker process, from the entry the boot string names
7
+ # (WorkerBoot.run_session_loop) to what it does as it leaves (CROSS-VERSION
9
8
  # CONTRACT: spec/worker_contract_spec.rb). It takes the session's OwnerLock,
10
9
  # leaves the spawner's process tree, installs the signals and the project
11
10
  # env, and runs the Worker (worker.rb loads only then, after the lock). Calls
@@ -14,7 +13,7 @@ module Samagotchi
14
13
  module WorkerBoot
15
14
  class << self
16
15
  # Run the session loop inside the forked process: the worker's main,
17
- # which the boot string reaches through SessionManager.run_session_loop.
16
+ # which the boot string names directly.
18
17
  #
19
18
  # Every worker starts its per-session Bridge (the single live client
20
19
  # transport) on a port bound to 127.0.0.1 before the loop and stops it on
@@ -106,6 +106,7 @@ module Samagotchi
106
106
  @stopped = stopped
107
107
  @queue_command = queue_command
108
108
  @initial_prompt_taken = false
109
+ @context_wake_waiting = false
109
110
  end
110
111
 
111
112
  # Add the queued context notes to the conversation (between turns only,
@@ -148,12 +149,18 @@ module Samagotchi
148
149
  # @return [ContextWake, nil] the wake turn to run now
149
150
  # @raise [SystemCallError, IOError] the caller logs it (context_failed)
150
151
  def absorb_context(before_turn: false, wake: false)
152
+ @context_wake_waiting = false
151
153
  return nil unless before_turn || conversation_started?
152
154
  return nil if @stopped.call
153
155
 
154
156
  batch = @context_absorber.pending
155
157
  return nil unless batch
156
158
 
159
+ if wake && context_wake_waiting_for?(batch)
160
+ @context_wake_waiting = true
161
+ return nil
162
+ end
163
+
157
164
  waking = wake ? context_wake_for(batch) : nil
158
165
  turn_id = SecureRandom.uuid if waking
159
166
  batch.deliveries.each do |delivery|
@@ -170,6 +177,10 @@ module Samagotchi
170
177
  waking && ContextWake.new(delivery: waking, turn_id: turn_id)
171
178
  end
172
179
 
180
+ # Whether #absorb_context left an eligible wake batch uncommitted for
181
+ # the start grace, so the idle loop can revisit it when grace expires.
182
+ def context_wake_waiting? = @context_wake_waiting
183
+
173
184
  # A failed wake turn kept nothing but its note: the note goes back to
174
185
  # a plain note (no turn start, the background wording), so a reload
175
186
  # draws no empty turn for it. +wake+: a ContextWake or a TaskWake.
@@ -300,6 +311,18 @@ module Samagotchi
300
311
  waking.first
301
312
  end
302
313
 
314
+ def context_wake_waiting_for?(batch)
315
+ return false unless @wakes.in_grace?
316
+
317
+ candidates = batch.deliveries.select(&:wake_note)
318
+ return false if candidates.empty? || !context_wakes_on?
319
+ return false unless @wakes.context_ready?(awaiting_continue: @awaiting_continue.call, names: candidates.map(&:name))
320
+
321
+ candidates.any? do |delivery|
322
+ delivery.peer ? @wakes.peer_in_a_row < @wakes.peer_turns : !woke_lately?(delivery.subscription, Time.now)
323
+ end
324
+ end
325
+
303
326
  # A peer's answer waits as a note: the UIs hear it once per
304
327
  # interactive input (WorkerWakes#notice_peer_budget!). +key+: the
305
328
  # budget that holds it.
@@ -34,6 +34,11 @@ module Samagotchi
34
34
  # newer worker's (Skew).
35
35
  PROTOCOL = 1
36
36
 
37
+ # A worker whose sidecar publishes this may be asked what it is busy with
38
+ # (Bridge's GET /activity, Worker#activity_snapshot): a sidecar without it
39
+ # is an older chi's, and asking it would only cost a 404.
40
+ ACTIVITY_FEATURE = "activity"
41
+
37
42
  # Seconds a worker may hold the lock without publishing its Bridge before
38
43
  # Starting is stuck (SessionManager::WORKER_START_GRACE).
39
44
  START_LIMIT = 30.0
@@ -101,7 +101,15 @@ module Samagotchi
101
101
  # not paused, past the start grace and under the budget. +names+: the
102
102
  # sources asking, for the log line when the budget holds them.
103
103
  def context_open?(awaiting_continue:, names:)
104
- return false if @paused || awaiting_continue || in_grace?
104
+ return false if in_grace?
105
+
106
+ context_ready?(awaiting_continue:, names:)
107
+ end
108
+
109
+ # The context wake gates apart from start grace; used to decide whether
110
+ # a wake must remain pending until that grace ends.
111
+ def context_ready?(awaiting_continue:, names:)
112
+ return false if @paused || awaiting_continue
105
113
 
106
114
  if @in_a_row >= max
107
115
  Log.info(:worker, "context_wake_held", reason: "max_wakes", names: names.join(","))
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.158.0
4
+ version: 0.159.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -176,6 +176,7 @@ files:
176
176
  - docs/memory.md
177
177
  - docs/plugins.md
178
178
  - docs/releasing.md
179
+ - docs/scheduling.md
179
180
  - docs/sessions.md
180
181
  - docs/sub-agent.md
181
182
  - docs/system-one.md
@@ -499,6 +500,7 @@ files:
499
500
  - lib/samagotchi/log_subscriber.rb
500
501
  - lib/samagotchi/memory_bundle.rb
501
502
  - lib/samagotchi/memory_bundle/asset_installer.rb
503
+ - lib/samagotchi/memory_bundle/asset_state.rb
502
504
  - lib/samagotchi/memory_bundle/builder.rb
503
505
  - lib/samagotchi/memory_bundle/bundle_hook.rb
504
506
  - lib/samagotchi/memory_bundle/entry_size.rb
@@ -640,6 +642,8 @@ files:
640
642
  - lib/samagotchi/run_options.rb
641
643
  - lib/samagotchi/sampling_settings.rb
642
644
  - lib/samagotchi/sandbox_layout.rb
645
+ - lib/samagotchi/schedule_command.rb
646
+ - lib/samagotchi/schedules.rb
643
647
  - lib/samagotchi/self_report.rb
644
648
  - lib/samagotchi/send_command.rb
645
649
  - lib/samagotchi/send_command/busy.rb
@@ -802,6 +806,7 @@ files:
802
806
  - lib/samagotchi/tools/builtin_calls.rb
803
807
  - lib/samagotchi/tools/builtins.rb
804
808
  - lib/samagotchi/tools/cancel_reminder.rb
809
+ - lib/samagotchi/tools/cancel_schedule.rb
805
810
  - lib/samagotchi/tools/context_read.rb
806
811
  - lib/samagotchi/tools/deferred_tools.rb
807
812
  - lib/samagotchi/tools/delegate.rb
@@ -814,6 +819,7 @@ files:
814
819
  - lib/samagotchi/tools/forget_outputs.rb
815
820
  - lib/samagotchi/tools/index_hint.rb
816
821
  - lib/samagotchi/tools/list_reminders.rb
822
+ - lib/samagotchi/tools/list_schedules.rb
817
823
  - lib/samagotchi/tools/list_sessions.rb
818
824
  - lib/samagotchi/tools/memory.rb
819
825
  - lib/samagotchi/tools/objectives_tool.rb
@@ -823,6 +829,7 @@ files:
823
829
  - lib/samagotchi/tools/ref_text.rb
824
830
  - lib/samagotchi/tools/register_reminder.rb
825
831
  - lib/samagotchi/tools/registry.rb
832
+ - lib/samagotchi/tools/schedule_task.rb
826
833
  - lib/samagotchi/tools/send_note.rb
827
834
  - lib/samagotchi/tools/shell.rb
828
835
  - lib/samagotchi/tools/task_create.rb