samagotchi 0.155.0 → 0.157.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 (234) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/bin/chi +2 -1
  4. data/docs/architecture.md +22 -13
  5. data/docs/bundles.md +3 -1
  6. data/docs/cli.md +9 -8
  7. data/docs/configuration.md +36 -4
  8. data/docs/guardrails.md +9 -5
  9. data/docs/hooks.md +11 -5
  10. data/docs/internals/anthropic.md +1 -1
  11. data/docs/internals/attached-context.md +3 -2
  12. data/docs/internals/background-tasks.md +1 -1
  13. data/docs/internals/bridge.md +83 -6
  14. data/docs/internals/bundles.md +28 -18
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +641 -937
  17. data/docs/internals/llm-context-forget.md +8 -3
  18. data/docs/internals/objectives.md +2 -2
  19. data/docs/internals/participants.md +3 -2
  20. data/docs/internals/prompt-caching.md +4 -1
  21. data/docs/internals/remote-sessions.md +8 -3
  22. data/docs/internals/web-frontend.md +3 -2
  23. data/docs/internals/working-set.md +2 -2
  24. data/docs/memory.md +13 -2
  25. data/docs/plugins.md +35 -2
  26. data/docs/sessions.md +11 -11
  27. data/docs/sub-agent.md +17 -2
  28. data/docs/system-one.md +3 -2
  29. data/docs/testing.md +53 -2
  30. data/lib/samagotchi/answer_command.rb +2 -1
  31. data/lib/samagotchi/archive_store.rb +4 -3
  32. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  33. data/lib/samagotchi/bootstrap_command.rb +3 -2
  34. data/lib/samagotchi/bridge/card_places.rb +10 -4
  35. data/lib/samagotchi/bridge/card_store.rb +3 -0
  36. data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
  37. data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
  38. data/lib/samagotchi/bridge.rb +31 -6
  39. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  40. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  41. data/lib/samagotchi/broadcast_command.rb +3 -3
  42. data/lib/samagotchi/bundle_command/check.rb +18 -0
  43. data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
  44. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  45. data/lib/samagotchi/bundle_command.rb +5 -2
  46. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  47. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  48. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  49. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  50. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  51. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  52. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  53. data/lib/samagotchi/child_move.rb +13 -57
  54. data/lib/samagotchi/child_reports.rb +2 -2
  55. data/lib/samagotchi/children_status.rb +15 -21
  56. data/lib/samagotchi/cli/parent_wait.rb +9 -23
  57. data/lib/samagotchi/config.rb +20 -112
  58. data/lib/samagotchi/context_command.rb +1 -2
  59. data/lib/samagotchi/context_sources.rb +5 -4
  60. data/lib/samagotchi/context_window.rb +9 -12
  61. data/lib/samagotchi/desktop/macos.rb +1 -1
  62. data/lib/samagotchi/effective_model.rb +57 -22
  63. data/lib/samagotchi/engine/context_judge.rb +19 -11
  64. data/lib/samagotchi/engine/messages.rb +1 -1
  65. data/lib/samagotchi/engine/models.rb +5 -2
  66. data/lib/samagotchi/engine/objectives.rb +29 -11
  67. data/lib/samagotchi/engine/plugins.rb +1 -1
  68. data/lib/samagotchi/engine/recap.rb +8 -4
  69. data/lib/samagotchi/engine/side_requests.rb +21 -11
  70. data/lib/samagotchi/engine/toolkit.rb +10 -9
  71. data/lib/samagotchi/engine/turn_end.rb +1 -1
  72. data/lib/samagotchi/engine/turn_helpers.rb +7 -7
  73. data/lib/samagotchi/engine/verify.rb +1 -1
  74. data/lib/samagotchi/engine.rb +43 -12
  75. data/lib/samagotchi/events.rb +3 -2
  76. data/lib/samagotchi/extension_load.rb +6 -3
  77. data/lib/samagotchi/guardrails/targets.rb +24 -9
  78. data/lib/samagotchi/guardrails_command.rb +1 -1
  79. data/lib/samagotchi/host_address.rb +52 -0
  80. data/lib/samagotchi/host_config.rb +72 -1
  81. data/lib/samagotchi/host_model.rb +2 -6
  82. data/lib/samagotchi/host_registry.rb +58 -46
  83. data/lib/samagotchi/kernel_loop.rb +53 -124
  84. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  85. data/lib/samagotchi/llm/chat_loop.rb +40 -21
  86. data/lib/samagotchi/llm/native_backend.rb +1 -1
  87. data/lib/samagotchi/llm_context_apply.rb +54 -1
  88. data/lib/samagotchi/llm_context_notice.rb +1 -1
  89. data/lib/samagotchi/llm_context_strategy.rb +29 -67
  90. data/lib/samagotchi/llm_context_view.rb +9 -0
  91. data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
  92. data/lib/samagotchi/memory_bundle/builder.rb +2 -1
  93. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  94. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  95. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  96. data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
  97. data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
  98. data/lib/samagotchi/memory_bundle/installer.rb +185 -169
  99. data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
  100. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  101. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  102. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  103. data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
  104. data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
  105. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
  106. data/lib/samagotchi/memory_bundle/status.rb +14 -20
  107. data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
  108. data/lib/samagotchi/memory_bundle/trash.rb +23 -8
  109. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  110. data/lib/samagotchi/memory_usage.rb +1 -1
  111. data/lib/samagotchi/model_lookup.rb +75 -0
  112. data/lib/samagotchi/model_price.rb +4 -10
  113. data/lib/samagotchi/model_profile.rb +7 -6
  114. data/lib/samagotchi/model_settings.rb +37 -2
  115. data/lib/samagotchi/models_command.rb +1 -1
  116. data/lib/samagotchi/note_command.rb +2 -3
  117. data/lib/samagotchi/objectives_audit.rb +4 -3
  118. data/lib/samagotchi/participant_mode.rb +2 -1
  119. data/lib/samagotchi/participants.rb +2 -2
  120. data/lib/samagotchi/plugin/api.rb +41 -6
  121. data/lib/samagotchi/plugin/context.rb +1 -1
  122. data/lib/samagotchi/plugin/loader.rb +33 -10
  123. data/lib/samagotchi/plugin/sessions.rb +3 -6
  124. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  125. data/lib/samagotchi/project_env.rb +1 -1
  126. data/lib/samagotchi/reasoning_history.rb +14 -20
  127. data/lib/samagotchi/recap_place.rb +27 -0
  128. data/lib/samagotchi/reminder_carry.rb +1 -1
  129. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  130. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  131. data/lib/samagotchi/reply_wait/target.rb +113 -0
  132. data/lib/samagotchi/reply_wait.rb +22 -2
  133. data/lib/samagotchi/reprefill_meter.rb +45 -0
  134. data/lib/samagotchi/review_command.rb +1 -1
  135. data/lib/samagotchi/sampling_settings.rb +9 -16
  136. data/lib/samagotchi/self_report.rb +36 -37
  137. data/lib/samagotchi/send_command.rb +17 -23
  138. data/lib/samagotchi/session.rb +370 -445
  139. data/lib/samagotchi/session_access/local.rb +12 -11
  140. data/lib/samagotchi/session_access/remote.rb +22 -6
  141. data/lib/samagotchi/session_archive_command.rb +2 -2
  142. data/lib/samagotchi/session_chain.rb +7 -6
  143. data/lib/samagotchi/session_continue.rb +6 -5
  144. data/lib/samagotchi/session_delete_command.rb +1 -1
  145. data/lib/samagotchi/session_field.rb +133 -0
  146. data/lib/samagotchi/session_inbox.rb +49 -1
  147. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  148. data/lib/samagotchi/session_manager/emptiness.rb +139 -0
  149. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  150. data/lib/samagotchi/session_manager/listing.rb +118 -0
  151. data/lib/samagotchi/session_manager/removal.rb +190 -0
  152. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  153. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  154. data/lib/samagotchi/session_manager.rb +28 -1541
  155. data/lib/samagotchi/session_metrics.rb +8 -5
  156. data/lib/samagotchi/session_paths.rb +57 -0
  157. data/lib/samagotchi/session_retention.rb +2 -2
  158. data/lib/samagotchi/session_row.rb +200 -0
  159. data/lib/samagotchi/session_setup.rb +15 -12
  160. data/lib/samagotchi/sessions_command.rb +11 -129
  161. data/lib/samagotchi/share_command.rb +1 -1
  162. data/lib/samagotchi/starting_marker.rb +65 -0
  163. data/lib/samagotchi/steer_cut_after.rb +7 -14
  164. data/lib/samagotchi/system_one/recorder.rb +49 -0
  165. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  166. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  167. data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
  168. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  169. data/lib/samagotchi/terminal_ui.rb +5 -7
  170. data/lib/samagotchi/testing/install.rb +2 -1
  171. data/lib/samagotchi/testing/sessions.rb +1 -1
  172. data/lib/samagotchi/thinking.rb +16 -21
  173. data/lib/samagotchi/token_usage.rb +1 -1
  174. data/lib/samagotchi/tool_activity.rb +10 -4
  175. data/lib/samagotchi/tool_declarations.rb +2 -2
  176. data/lib/samagotchi/tool_output_cut.rb +17 -0
  177. data/lib/samagotchi/tool_response.rb +5 -3
  178. data/lib/samagotchi/tool_runner.rb +25 -5
  179. data/lib/samagotchi/toolkit_command.rb +1 -1
  180. data/lib/samagotchi/tools/delegate.rb +55 -45
  181. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  182. data/lib/samagotchi/tools/delegate_result.rb +5 -5
  183. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  184. data/lib/samagotchi/tools/index_hint.rb +14 -0
  185. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  186. data/lib/samagotchi/tools/registry.rb +28 -11
  187. data/lib/samagotchi/tools/send_note.rb +4 -6
  188. data/lib/samagotchi/tools/tool_index.rb +52 -51
  189. data/lib/samagotchi/tools/tool_surface.rb +22 -8
  190. data/lib/samagotchi/turn_context.rb +2 -1
  191. data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
  192. data/lib/samagotchi/turn_stages/objectives.rb +56 -52
  193. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
  194. data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
  195. data/lib/samagotchi/turn_stages/warmup.rb +14 -15
  196. data/lib/samagotchi/update_command.rb +11 -7
  197. data/lib/samagotchi/verify_fresh.rb +7 -11
  198. data/lib/samagotchi/version.rb +1 -1
  199. data/lib/samagotchi/vision_support.rb +11 -14
  200. data/lib/samagotchi/web/app.rb +45 -28
  201. data/lib/samagotchi/web/message_display.rb +57 -7
  202. data/lib/samagotchi/web/participant_access.rb +3 -3
  203. data/lib/samagotchi/web/public/activity.js +16 -7
  204. data/lib/samagotchi/web/public/app.js +29 -8
  205. data/lib/samagotchi/web/public/bubbles.js +8 -3
  206. data/lib/samagotchi/web/public/cards_place.js +15 -0
  207. data/lib/samagotchi/web/public/commands.js +5 -1
  208. data/lib/samagotchi/web/public/data.js +4 -2
  209. data/lib/samagotchi/web/public/history_shape.js +220 -0
  210. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  211. data/lib/samagotchi/web/public/reload_check.js +71 -0
  212. data/lib/samagotchi/web/public/stage_model.js +7 -5
  213. data/lib/samagotchi/web/public/stage_view.js +7 -3
  214. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  215. data/lib/samagotchi/web/public/timing.js +43 -2
  216. data/lib/samagotchi/web/public/turn_block.js +215 -30
  217. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  218. data/lib/samagotchi/web/public/turn_events.js +10 -1
  219. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  220. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  221. data/lib/samagotchi/web/server.rb +4 -2
  222. data/lib/samagotchi/web/session_access_routes.rb +6 -6
  223. data/lib/samagotchi/web/session_control.rb +4 -4
  224. data/lib/samagotchi/web/session_create.rb +3 -3
  225. data/lib/samagotchi/web/session_hub.rb +9 -17
  226. data/lib/samagotchi/web/session_show.rb +67 -5
  227. data/lib/samagotchi/web/session_summary.rb +39 -28
  228. data/lib/samagotchi/worker.rb +83 -32
  229. data/lib/samagotchi/worker_boot.rb +184 -0
  230. data/lib/samagotchi/worker_link.rb +3 -0
  231. data/lib/samagotchi/worker_signals.rb +1 -1
  232. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  233. metadata +32 -4
  234. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -63,38 +63,31 @@ module Samagotchi
63
63
  nil
64
64
  end
65
65
 
66
- # The level for +target+ and where it came from ("session",
66
+ # The level for +lookup+'s model and where it came from ("session",
67
67
  # "SAMAGOTCHI_REASONING_HISTORY", "models: qwen", "hosts.work"; nil for
68
68
  # the default).
69
- # @param target [HostRegistry::ModelTarget]
70
- # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
71
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
69
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
72
70
  # @param session [Symbol, nil] the session's own level (Session#reasoning_history)
73
71
  # @return [Array(Symbol, String|nil)]
74
- def resolve(target, names:, models: nil, session: nil)
72
+ def resolve(lookup, session: nil)
75
73
  return [session, SESSION_SOURCE] if session
76
74
 
77
- env = env_level
75
+ env = env_level(lookup)
78
76
  return [env, ENV_NAME] if env
79
77
 
80
- models ||= begin
81
- ConfigFile.model_settings
82
- rescue StandardError
83
- {}
84
- end
85
- setting = ConfigFile.model_setting(names, :reasoning_history, models: models)
78
+ setting = lookup.model(:reasoning_history)
86
79
  return [setting.value, "models: #{setting.key}"] if setting
87
80
 
88
- host = target.entry&.reasoning_history
89
- return [host, "hosts.#{target.entry.name}"] if host
81
+ host = lookup.host(:reasoning_history)
82
+ return [host, "hosts.#{lookup.host_name}"] if host
90
83
 
91
- [API_DEFAULTS.fetch(target.entry&.api&.to_sym, DEFAULT), nil]
84
+ [API_DEFAULTS.fetch(lookup.entry&.api&.to_sym, DEFAULT), nil]
92
85
  end
93
86
 
94
87
  # #resolve, as a SettingExplained with the session's own level beside it.
95
88
  # @return [SettingExplained]
96
- def explain(target, names:, models: nil, session: nil)
97
- level, source = resolve(target, names: names, models: models, session: session)
89
+ def explain(lookup, session: nil)
90
+ level, source = resolve(lookup, session: session)
98
91
  SettingExplained.new(level: level, source: source, own: session)
99
92
  end
100
93
 
@@ -150,9 +143,10 @@ module Samagotchi
150
143
  # "drop (models: qwen)", "turn (default)".
151
144
  def label(level, source) = "#{level} (#{source || "default"})"
152
145
 
153
- # SAMAGOTCHI_REASONING_HISTORY as a level, nil when unset or unknown.
154
- def env_level
155
- raw, origin = Config.get_with_origin(KEY)
146
+ # SAMAGOTCHI_REASONING_HISTORY (read with +lookup+'s env) as a level,
147
+ # nil when unset or unknown.
148
+ def env_level(lookup)
149
+ raw, origin = lookup.global(KEY)
156
150
  origin == :env ? level(raw, ENV_NAME) : nil
157
151
  rescue StandardError
158
152
  nil
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "steer"
4
+ require_relative "bridge/card_places"
5
+
6
+ module Samagotchi
7
+ # Where a saved recap sits among the turns a page draws: after the last
8
+ # turn opening it covers (Steer.turn_opening?, the rule its turns_since
9
+ # count goes by). Its stamp is that opening's id, as a card's is
10
+ # (Bridge::CardPlaces.stamp: a prompt's turn_id, a wake note's, a
11
+ # /continue no reason line's opening_id), so the page finds the bubble by
12
+ # id, not by counting back (a rollback or a check-in could make the count
13
+ # land a turn off). Read at read time from recap.json's `covered`: the file
14
+ # is unchanged, an old one gets a stamp too. Pure.
15
+ module RecapPlace
16
+ module_function
17
+
18
+ # @param messages [Array<Hash>] the session's messages (symbol or string keys)
19
+ # @param covered [Integer] how many of them the recap covers
20
+ # @return [String, nil] the id of the last opening in the covered
21
+ # messages; nil for none, or one with no id (a line an older chi wrote)
22
+ def opening_id(messages, covered)
23
+ opening = Array(messages).first([covered.to_i, 0].max).reverse_each.find { |message| Steer.turn_opening?(message) }
24
+ opening && Bridge::CardPlaces.stamp(opening)
25
+ end
26
+ end
27
+ end
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  # restart writes one: a stop, an idle exit, a crash and a fallback restart
16
16
  # (a TERM) drop the reminders as before, and a stop or an archive removes a
17
17
  # file left behind (SessionManager.stop_session, .archive_session); a
18
- # restart a stop landed on starts no successor (.run_session_loop). A file is honoured only by the same
18
+ # restart a stop landed on starts no successor (WorkerBoot.run_session_loop). A file is honoured only by the same
19
19
  # session, within WINDOW of its writing (a successor starts in seconds), so
20
20
  # one no successor read can't bring reminders back a day later.
21
21
  module ReminderCarry
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../session"
4
+ require_relative "../worker_link"
5
+
6
+ module Samagotchi
7
+ module ReplyWait
8
+ # Who runs the session, as one look of a wait reads it: one owner lock
9
+ # read and one WorkerLink resolve on it. Two readers on purpose:
10
+ # #owner? (any kind, a one-turn chi run's too: its turn is alive) for
11
+ # whether the worker is gone, #worker_owned? (a worker's lock) for
12
+ # whether a question waits for an answer (a one-turn run's is answered
13
+ # there; a dead worker's waits for no one).
14
+ class Liveness
15
+ def initialize(id, state_dir:)
16
+ @id = id
17
+ @state_dir = state_dir
18
+ end
19
+
20
+ # The lock's holder (SessionManager.session_owner).
21
+ # @return [OwnerLock::Owner, nil]
22
+ def owner
23
+ return @owner if defined?(@owner)
24
+
25
+ @owner = SessionManager.session_owner(@id, state_dir: @state_dir)
26
+ end
27
+
28
+ def owner? = !owner.nil?
29
+
30
+ def worker_owned?
31
+ return @worker_owned if defined?(@worker_owned)
32
+
33
+ @worker_owned = WorkerLink.resolve(Session.session_dir(@id, state_dir: @state_dir), unlink: false,
34
+ owner: owner).worker_owned?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -5,7 +5,7 @@ require_relative "../session_inbox"
5
5
  require_relative "../turn_checkpoint"
6
6
  require_relative "../continue_offer"
7
7
  require_relative "../steer"
8
- require_relative "../worker_link"
8
+ require_relative "liveness"
9
9
 
10
10
  module Samagotchi
11
11
  module ReplyWait
@@ -72,13 +72,17 @@ module Samagotchi
72
72
  # @return [Result, nil]
73
73
  def look
74
74
  look_at_input
75
+ # Before the load: the turn it names began before what the load sees.
76
+ merge = merge_record
75
77
  session = Session.load(@id, state_dir: @state_dir)
78
+ # Who runs it, read once for this look's checks.
79
+ @live = Liveness.new(@id, state_dir: @state_dir)
76
80
  if (result = removed(session))
77
81
  return result
78
82
  end
79
83
 
80
84
  note_own_running(session)
81
- follow_merge(session)
85
+ follow_merge(session, merge)
82
86
  reply(session) || settled_turn(session) || status_end(session) || question(session) || held(session) || canceled ||
83
87
  ended_before_reply(session) || ended_before_turn(session) || turn_end(session) || owner_gone(session) ||
84
88
  message_waiting || timed_out
@@ -106,8 +110,11 @@ module Samagotchi
106
110
  Result.new(status: :no_reply, outcome: "removed", session: session)
107
111
  end
108
112
 
113
+ # Its turn ran, or a drain merged it (its merge record): a dequeue's
114
+ # tombstone that lost the claim to the drain is a stray too.
109
115
  def turn_ran?(session)
110
- session.last_turn&.enqueued_id == @turn || !SessionInbox.reply_for(@session_dir, @turn).nil?
116
+ session.last_turn&.enqueued_id == @turn || !SessionInbox.reply_for(@session_dir, @turn).nil? ||
117
+ !SessionInbox.merged_into(@session_dir, @turn).nil?
111
118
  end
112
119
 
113
120
  def note_own_running(session)
@@ -119,14 +126,37 @@ module Samagotchi
119
126
 
120
127
  # Taken but never run as a turn of its own: a running turn's drain
121
128
  # merged it as a steer, so that turn's reply (or question) is its
122
- # answer. Waited for as the next reply past the cursor, as without
123
- # turn:, never as a turn that will not come.
124
- def follow_merge(session)
125
- return unless @turn && @claimed && !@seen_own && !@own_turn_input &&
126
- ReplyWait.merged_elsewhere?(session, @turn, @baseline, @session_dir)
127
-
128
- @turn = nil
129
- @seen_running = running?(session)
129
+ # answer. With the drain's merge record (SessionInbox.mark_merged)
130
+ # the wait is that turn's from here on, as if it had been bound to it
131
+ # (#follow_into). Without one (a 0.156 worker, a turn with no id, a
132
+ # merge from before the update) the merge is inferred
133
+ # (ReplyWait.merged_elsewhere?) and waited for as the next reply past
134
+ # the cursor, as without turn:, never as a turn that will not come.
135
+ def follow_merge(session, merge)
136
+ return unless may_follow?
137
+
138
+ if merge
139
+ follow_into(session, merge)
140
+ elsif ReplyWait.merged_elsewhere?(session, @turn, @baseline, @session_dir)
141
+ @turn = nil
142
+ @seen_running = running?(session)
143
+ end
144
+ end
145
+
146
+ def may_follow? = @turn && @claimed && !@seen_own && !@own_turn_input
147
+
148
+ def merge_record = may_follow? ? SessionInbox.merged_into(@session_dir, @turn) : nil
149
+
150
+ # Bound to the turn that took it, once (its own input ran as its own
151
+ # turn). Not running in this look, which loaded after the record:
152
+ # that turn is over (its end, its reply, its settle say how), and a
153
+ # turn that ended after the merge with last_turn not its is past it
154
+ # (#own_turn_end).
155
+ def follow_into(session, merge)
156
+ @turn = merge.into
157
+ @own_turn_input = true
158
+ note_own_running(session)
159
+ @merged_at = merge.at unless @seen_own
130
160
  end
131
161
 
132
162
  def reply(session)
@@ -172,9 +202,7 @@ module Samagotchi
172
202
  end
173
203
 
174
204
  # A worker holds the session's lock (a one-turn chi run's question is its own).
175
- def worker_owned?
176
- WorkerLink.resolve(Session.session_dir(@id, state_dir: @state_dir), unlink: false).worker_owned?
177
- end
205
+ def worker_owned? = @live.worker_owned?
178
206
 
179
207
  # A participant's turn its worker holds while the owner's Continue
180
208
  # offer stands (Worker#run_input_files): it waits for the owner's
@@ -273,6 +301,7 @@ module Samagotchi
273
301
  # died), or it ended with no reply and a next turn ran and ended
274
302
  # between two looks. Waiting on would sit through those turns.
275
303
  return Result.new(status: :no_reply, session: session) if @seen_own && ReplyWait.past_own?(session, @turn, @own_base)
304
+ return Result.new(status: :no_reply, session: session) if @merged_at && ReplyWait.ended_after?(session, @merged_at)
276
305
 
277
306
  nil
278
307
  end
@@ -307,7 +336,7 @@ module Samagotchi
307
336
  def owner_gone(session)
308
337
  return nil unless @owner_grace
309
338
 
310
- if SessionManager.session_owner(@id, state_dir: @state_dir)
339
+ if @live.owner?
311
340
  @gone_since = nil
312
341
  elsif ReplyWait.monotonic - (@gone_since ||= ReplyWait.monotonic) > @owner_grace
313
342
  return Result.new(status: :worker_gone, session: session)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../delivery"
4
+ require_relative "../session"
5
+
6
+ module Samagotchi
7
+ module ReplyWait
8
+ # Which turn a reply wait waits for, and how: the one binding every
9
+ # caller builds (chi send --wait, chi answer, a participant's wait, the
10
+ # delegate tools and a delegate report), each with its constructor.
11
+ # #to_h is ReplyWait.call's kwargs (+cursor+, +baseline+,
12
+ # +owner_grace+, +turn+, +queued+, +ended_before+: ReplyWait.call's
13
+ # parameters say what each means); +report_turn_id+ (the turn_id of
14
+ # --format json's line, ParentWait's) is read on its own.
15
+ Target = Data.define(:turn, :report_turn_id, :queued, :ended_before, :cursor, :baseline, :owner_grace) do
16
+ def initialize(turn: nil, report_turn_id: nil, queued: false, ended_before: false, cursor: nil, baseline: nil,
17
+ owner_grace: nil)
18
+ super
19
+ end
20
+
21
+ # A --new session's first message: its own turn, the ack's id (a
22
+ # file-delivered one's also names the outcome of a session command
23
+ # that runs no turn), so a worker that dies in it and the next one's
24
+ # settle end the wait (TurnCheckpoint.settled_turn). A message merged
25
+ # into a turn starting then falls back to that turn's reply
26
+ # (ReplyWait's turn: without queued:); no id (an older worker), the
27
+ # next reply. A prompt the session was created with has no delivery:
28
+ # its turn is the id the create gave it (+direct_turn+,
29
+ # Session#first_turn_id), and nothing can merge into it, so the wait
30
+ # is for that turn alone (ReplyWait's queued:).
31
+ # @param start [SessionAccess::WaitStart] prepare_wait's (its cursor
32
+ # and baseline)
33
+ # @param delivered [SessionManager::DeliverResult, nil] nil when the
34
+ # message started the session itself (no delivery)
35
+ # @param direct_turn [String, nil] that first turn's id, for a direct
36
+ # start
37
+ def self.first_message(start, delivered, direct_turn: nil)
38
+ reads = { cursor: start&.cursor, baseline: start&.baseline, owner_grace: ReplyWait::GONE_AFTER }
39
+ return new(**reads) unless delivered || !direct_turn.to_s.strip.empty?
40
+
41
+ return new(turn: direct_turn, report_turn_id: direct_turn, queued: true, **reads) if delivered.nil?
42
+
43
+ new(turn: delivered.enqueued_id, report_turn_id: delivered.enqueued_id, **reads)
44
+ end
45
+
46
+ # A message to an existing session: its own turn, the ack's id. With
47
+ # --queue it runs as that turn, also when the turn running at the
48
+ # check ended before it went in (an idle worker's ack says next_step,
49
+ # and the file still runs as that turn). A plain one may run as its
50
+ # own turn or merge into a running one (a session left running by a
51
+ # dead worker, the busy check's owner gone; or a turn starting then):
52
+ # that turn's end is its answer (ReplyWait's follow_merge). A plain
53
+ # message the worker queued (its ack says queue: a participant's
54
+ # review turn was running, which takes no steer) is a --queue's. An
55
+ # ack with no id (an older worker): the next reply, reported as the
56
+ # turn that was running when it went in (start's own_turn).
57
+ # @param start [SessionAccess::WaitStart] read before it went in (its
58
+ # running says the session was running then)
59
+ def self.message(start, delivered, queue:)
60
+ queue ||= delivered.delivery == Delivery::QUEUE
61
+ turn = delivered.enqueued_id
62
+ new(turn: turn, report_turn_id: turn || (start.own_turn if start.running && !queue), queued: queue && !turn.nil?,
63
+ cursor: start.cursor, baseline: start.baseline, owner_grace: ReplyWait::GONE_AFTER)
64
+ end
65
+
66
+ # --wait with no message: --turn's (+requested+), or the turn running
67
+ # now or whose continue offer stands (start's own_turn,
68
+ # ReplyWait.own_turn). Settled (idle, a turn ended, no question
69
+ # standing: a continue offer waits for its answer), its last turn's
70
+ # end is the answer (ended_before); with --turn, when that turn is the
71
+ # last one. A session no worker runs waits for whatever wakes one (no
72
+ # owner_grace).
73
+ # @param start [SessionAccess::WaitStart]
74
+ def self.wait_only(start, requested:)
75
+ turn = requested || start.own_turn
76
+ ended_before = requested ? start.last_turn_id == requested : start.settled
77
+ new(turn: turn, report_turn_id: turn || (start.last_turn_id if start.settled), ended_before: ended_before,
78
+ cursor: start.cursor, baseline: start.baseline, owner_grace: start.live ? ReplyWait::GONE_AFTER : nil)
79
+ end
80
+
81
+ # chi answer: the turn the question is about (start's own_turn: the
82
+ # one running, or the one whose continue offer stands), else the next
83
+ # reply.
84
+ def self.answer(start)
85
+ new(turn: start.own_turn, report_turn_id: start.own_turn, cursor: start.cursor, baseline: start.baseline,
86
+ owner_grace: ReplyWait::GONE_AFTER)
87
+ end
88
+
89
+ # A participant's --wait -m: their review turn just queued (+turn+),
90
+ # as --wait --turn T (their wait routes take only a turn of theirs).
91
+ def self.participant(start, turn:) = wait_only(start, requested: turn)
92
+
93
+ # A delegate's wait (DelegateWait, a delegate report) from the
94
+ # parent's DelegateCursor: the newest reply it was given, the child as
95
+ # it was then, and the turn a follow-up joined. The child's first turn
96
+ # (Session#first_turn_id: the prompt it was spawned with) runs as its
97
+ # own and nothing merges into it, so a wait bound to it is queued.
98
+ # @param cursor [Tools::DelegateCursor]
99
+ # @param child [Session, nil] the child as read now (its first_turn_id)
100
+ # @param owner_grace [Numeric, nil] nil: no gone check (a report's
101
+ # look)
102
+ def self.from_cursor(cursor, child:, owner_grace: nil)
103
+ first = child&.first_turn_id
104
+ new(turn: cursor.turn, cursor: cursor.reply_file, baseline: cursor.baseline, owner_grace: owner_grace,
105
+ queued: !cursor.turn.to_s.empty? && cursor.turn == first)
106
+ end
107
+
108
+ # ReplyWait.call's kwargs (report_turn_id is ParentWait's, not one).
109
+ def to_h = { cursor: cursor, baseline: baseline, owner_grace: owner_grace, turn: turn, queued: queued,
110
+ ended_before: ended_before }
111
+ end
112
+ end
113
+ end
@@ -6,6 +6,7 @@ require_relative "session_inbox"
6
6
  require_relative "turn_checkpoint"
7
7
  require_relative "worker_link"
8
8
  require_relative "reply_wait/poll"
9
+ require_relative "reply_wait/target"
9
10
 
10
11
  module Samagotchi
11
12
  # Loaded on first use: this autoload keeps SessionManager and its ~80
@@ -21,6 +22,12 @@ module Samagotchi
21
22
  # The delegate tools and `chi send --wait` wrap it in their own words.
22
23
  module ReplyWait
23
24
  POLL_INTERVAL = 0.5
25
+ # The one dead-worker rule: no owner of any kind this long while
26
+ # waiting (a Target's owner_grace) and the worker is gone (it died
27
+ # before it could mark the session). A caller that just spawned or woke
28
+ # a worker awaits it first (Tools::Delegate.await_worker,
29
+ # SendCommand#deliver_new), so a booting worker isn't read as gone.
30
+ GONE_AFTER = 5
24
31
  # A worker's sidecar feature (Bridge::FEATURES): its running turn's
25
32
  # checkpoint mark (InterruptedTurn#enqueued_id), a continue turn and a
26
33
  # Stop's last_turn carry the turn's enqueued_id (#own_turn).
@@ -209,7 +216,8 @@ module Samagotchi
209
216
  end
210
217
 
211
218
  # Whether +turn+ is a turn the caller can wait for: running now, queued
212
- # as its own turn, or the last one to have ended.
219
+ # as its own turn, the last one to have ended, or an input a turn's
220
+ # drain merged (its merge record: the wait follows it to that turn).
213
221
  def known_turn?(session, session_dir:, turn:)
214
222
  id = turn.to_s
215
223
  return false if id.empty?
@@ -217,7 +225,7 @@ module Samagotchi
217
225
  running = session.status == Session::STATUS_RUNNING && session.interrupted_turn&.enqueued_id == id
218
226
  queued = SessionInbox.waiting_input(session_dir, id) == :turn_end
219
227
  ended = session.last_turn&.enqueued_id == id || !TurnCheckpoint.settled_turn(session_dir, id).nil?
220
- running || queued || ended
228
+ running || queued || ended || !SessionInbox.merged_into(session_dir, id).nil?
221
229
  end
222
230
 
223
231
  # Whether the session's live worker names TURN_IDS_FEATURE.
@@ -287,6 +295,18 @@ module Samagotchi
287
295
  !ended.nil? && ended != own_base
288
296
  end
289
297
 
298
+ # Whether the session's last turn ended after +at+ (a Time): a turn
299
+ # that isn't own_turn_end's (its last_turn is checked first) ended
300
+ # after the turn a merge record named began, so that turn is over.
301
+ # Clock skew can't fire it: +at+ and every ended_at come from the
302
+ # same machine's clock (its workers'; a remote wait's Poll runs there).
303
+ def ended_after?(session, at)
304
+ ended = session.last_turn&.ended_at
305
+ !ended.to_s.empty? && Time.iso8601(ended.to_s) > at
306
+ rescue ArgumentError
307
+ false
308
+ end
309
+
290
310
  # @return [Result, nil] :done with the reply of the turn that ran the
291
311
  # input +turn+ (its enqueued_id)
292
312
  def reply_for_turn(id, state_dir:, turn:)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "token_usage"
4
+
5
+ module Samagotchi
6
+ # A request's re-prefilled tokens, for both loops: of the prompt the last
7
+ # request on the same model through the same client (the host) sent, what
8
+ # the server didn't reuse from its cache this time (that prompt less the
9
+ # cached count), so an LLM context edit's cache break shows, and so does
10
+ # anything else that rewrote the prompt's middle or lost the cache.
11
+ #
12
+ # One per Engine, given to its KernelLoop and its ChatLoop, so the
13
+ # baseline is the last request's whichever loop sent it: a switch to
14
+ # another host and back starts over on the way back. A bare loop (a spec,
15
+ # a bench) builds its own. Read and written on the turn's thread only
16
+ # (each loop's completed generation; the turn-end warm-up's #reset!).
17
+ class ReprefillMeter
18
+ # The last counted request: its model, its client, its prompt tokens.
19
+ Baseline = Data.define(:model, :client, :prompt)
20
+
21
+ # The tokens the server prefilled again for this request (+prompt+ and
22
+ # +cached+, its counts), and this request becomes the baseline. nil
23
+ # under TokenUsage::REPREFILL_MIN_TOKENS (a cache block's rounding), for
24
+ # a server that reports no cached count (or 0: a cold cache isn't told
25
+ # from no cache), the first request, a model switch (another bare model,
26
+ # or the same one on another host's +client+: SessionMetrics counts that
27
+ # prefill as the switch's), and the first request after #reset!.
28
+ # @return [Integer, nil]
29
+ def tokens(model, client:, prompt:, cached:)
30
+ previous = @baseline
31
+ @baseline = Baseline.new(model: model, client: client, prompt: prompt.to_i) if prompt.to_i.positive?
32
+ return nil unless previous && previous.model == model && previous.client.equal?(client) && cached.to_i.positive?
33
+
34
+ tokens = previous.prompt - cached.to_i
35
+ tokens >= TokenUsage::REPREFILL_MIN_TOKENS ? tokens : nil
36
+ end
37
+
38
+ # A turn-end warm-up prefilled the next prompt off the clock, so the
39
+ # next request's cache counts are against it, not the last request:
40
+ # start over (KernelLoop#warmed_up!).
41
+ def reset!
42
+ @baseline = nil
43
+ end
44
+ end
45
+ end
@@ -64,7 +64,7 @@ module Samagotchi
64
64
  rescue ModelProfile::MissingModel, Toolkit::Refused => e
65
65
  refused("could not start: #{e.message}")
66
66
  rescue ArgumentError => e
67
- refused(e.is_a?(Session::AmbiguousId) ? e.message : e.message.sub(/\ASession not found: /, "no session "))
67
+ refused(e.message)
68
68
  end
69
69
 
70
70
  private
@@ -11,18 +11,16 @@ module Samagotchi
11
11
  module SamplingSettings
12
12
  EMPTY = {}.freeze
13
13
 
14
- # @param target [HostRegistry::ModelTarget]
15
- # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
16
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
14
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
17
15
  # @return [Hash] frozen, symbol keys; may be empty
18
- def self.for(target, names:, models: nil)
19
- resolve(target, names, models).first
16
+ def self.for(lookup)
17
+ resolve(lookup).first
20
18
  end
21
19
 
22
20
  # "temperature=0.6 presence_penalty=1.5 (hosts.work, models: qwen)" for
23
21
  # /model, or nil when nothing is configured.
24
- def self.summary(target, names:, models: nil)
25
- params, sources = resolve(target, names, models)
22
+ def self.summary(lookup)
23
+ params, sources = resolve(lookup)
26
24
  return nil if params.empty?
27
25
 
28
26
  pairs = params.map { |key, value| "#{key}=#{value.nil? ? "(not sent)" : value.inspect}" }
@@ -37,16 +35,11 @@ module Samagotchi
37
35
  fields.map { |key, value| "#{key}=#{value.is_a?(String) ? value : JSON.generate(value)}" }.join(" ")
38
36
  end
39
37
 
40
- def self.resolve(target, names, models)
41
- models ||= begin
42
- ConfigFile.model_settings
43
- rescue StandardError
44
- {}
45
- end
46
- host = target.entry.sampling || EMPTY
47
- model = ConfigFile.model_setting(names, :sampling, models: models)
38
+ def self.resolve(lookup)
39
+ host = lookup.host(:sampling) || EMPTY
40
+ model = lookup.model(:sampling)
48
41
  sources = []
49
- sources << "hosts.#{target.entry.name}" unless host.empty?
42
+ sources << "hosts.#{lookup.host_name}" unless host.empty?
50
43
  sources << "models: #{model.key}" if model
51
44
  [host.merge(model&.value || EMPTY).freeze, sources]
52
45
  end