samagotchi 0.156.0 → 0.158.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/README.md +1 -1
  4. data/bin/chi +4 -1
  5. data/docs/architecture.md +18 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +4 -1
  8. data/docs/cli.md +29 -24
  9. data/docs/configuration.md +43 -9
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +12 -7
  12. data/docs/hooks.md +53 -83
  13. data/docs/internals/bridge.md +84 -7
  14. data/docs/internals/bundles.md +69 -46
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +23 -7
  17. data/docs/internals/participants.md +3 -2
  18. data/docs/internals/remote-sessions.md +5 -2
  19. data/docs/internals/web-frontend.md +3 -2
  20. data/docs/internals/working-set.md +1 -1
  21. data/docs/memory.md +51 -10
  22. data/docs/plugins.md +48 -11
  23. data/docs/releasing.md +6 -5
  24. data/docs/sessions.md +13 -13
  25. data/docs/sub-agent.md +17 -2
  26. data/docs/testing.md +56 -5
  27. data/lib/samagotchi/answer_command.rb +2 -1
  28. data/lib/samagotchi/archive_store.rb +4 -3
  29. data/lib/samagotchi/bootstrap_command.rb +3 -2
  30. data/lib/samagotchi/bridge/card_places.rb +5 -7
  31. data/lib/samagotchi/bridge/card_store.rb +3 -0
  32. data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
  33. data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
  34. data/lib/samagotchi/bridge.rb +31 -6
  35. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  36. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  37. data/lib/samagotchi/bundle_command/check.rb +41 -10
  38. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  39. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  40. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  42. data/lib/samagotchi/bundle_command.rb +20 -45
  43. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  44. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  45. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  46. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  48. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
  50. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  51. data/lib/samagotchi/child_move.rb +6 -6
  52. data/lib/samagotchi/child_reports.rb +2 -2
  53. data/lib/samagotchi/children_status.rb +15 -21
  54. data/lib/samagotchi/cli/parent_wait.rb +8 -22
  55. data/lib/samagotchi/config.rb +3 -0
  56. data/lib/samagotchi/context_command.rb +7 -5
  57. data/lib/samagotchi/context_sources.rb +9 -6
  58. data/lib/samagotchi/context_window.rb +9 -12
  59. data/lib/samagotchi/effective_model.rb +57 -22
  60. data/lib/samagotchi/engine/plugins.rb +1 -1
  61. data/lib/samagotchi/engine/recap.rb +8 -4
  62. data/lib/samagotchi/engine.rb +30 -9
  63. data/lib/samagotchi/events.rb +3 -2
  64. data/lib/samagotchi/extension_load.rb +26 -44
  65. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  66. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  67. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  68. data/lib/samagotchi/guardrails_command.rb +1 -1
  69. data/lib/samagotchi/hook_routes.rb +2 -2
  70. data/lib/samagotchi/hooks.rb +2 -3
  71. data/lib/samagotchi/host_address.rb +52 -0
  72. data/lib/samagotchi/host_registry.rb +57 -45
  73. data/lib/samagotchi/kernel_loop.rb +7 -3
  74. data/lib/samagotchi/llm/chat_loop.rb +6 -3
  75. data/lib/samagotchi/llm_context_strategy.rb +27 -63
  76. data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
  77. data/lib/samagotchi/memory_bundle/builder.rb +12 -35
  78. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  79. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  80. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  81. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  82. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  83. data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
  84. data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
  85. data/lib/samagotchi/memory_bundle/installer.rb +212 -178
  86. data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
  87. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  88. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  89. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  90. data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
  91. data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
  92. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  93. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
  94. data/lib/samagotchi/memory_bundle/status.rb +26 -30
  95. data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
  96. data/lib/samagotchi/memory_bundle/trash.rb +28 -18
  97. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  98. data/lib/samagotchi/memory_orphans.rb +4 -4
  99. data/lib/samagotchi/memory_paths.rb +102 -13
  100. data/lib/samagotchi/memory_usage.rb +1 -1
  101. data/lib/samagotchi/model_lookup.rb +75 -0
  102. data/lib/samagotchi/model_profile.rb +1 -0
  103. data/lib/samagotchi/models_command.rb +1 -1
  104. data/lib/samagotchi/note_command.rb +1 -1
  105. data/lib/samagotchi/participant_mode.rb +2 -1
  106. data/lib/samagotchi/plugin/api.rb +42 -10
  107. data/lib/samagotchi/plugin/loader.rb +75 -15
  108. data/lib/samagotchi/plugin/sessions.rb +1 -1
  109. data/lib/samagotchi/project_command.rb +167 -0
  110. data/lib/samagotchi/project_init.rb +69 -0
  111. data/lib/samagotchi/project_scope.rb +6 -4
  112. data/lib/samagotchi/prompt_files.rb +10 -7
  113. data/lib/samagotchi/reasoning_history.rb +14 -20
  114. data/lib/samagotchi/recap_place.rb +26 -0
  115. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  116. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  117. data/lib/samagotchi/reply_wait/target.rb +113 -0
  118. data/lib/samagotchi/reply_wait.rb +22 -2
  119. data/lib/samagotchi/run_options.rb +1 -0
  120. data/lib/samagotchi/sampling_settings.rb +9 -16
  121. data/lib/samagotchi/self_report.rb +34 -35
  122. data/lib/samagotchi/send_command.rb +17 -23
  123. data/lib/samagotchi/session.rb +349 -441
  124. data/lib/samagotchi/session_access/contract.rb +1 -1
  125. data/lib/samagotchi/session_access/local.rb +5 -6
  126. data/lib/samagotchi/session_access/remote.rb +5 -3
  127. data/lib/samagotchi/session_field.rb +134 -0
  128. data/lib/samagotchi/session_inbox.rb +49 -1
  129. data/lib/samagotchi/session_manager/emptiness.rb +2 -1
  130. data/lib/samagotchi/session_manager/listing.rb +29 -100
  131. data/lib/samagotchi/session_manager.rb +2 -1
  132. data/lib/samagotchi/session_metrics.rb +7 -4
  133. data/lib/samagotchi/session_paths.rb +57 -0
  134. data/lib/samagotchi/session_retention.rb +2 -2
  135. data/lib/samagotchi/session_row.rb +200 -0
  136. data/lib/samagotchi/session_setup.rb +15 -12
  137. data/lib/samagotchi/sessions_command.rb +11 -129
  138. data/lib/samagotchi/steer.rb +10 -0
  139. data/lib/samagotchi/steer_cut_after.rb +7 -14
  140. data/lib/samagotchi/system_prompt.rb +20 -11
  141. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  142. data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
  143. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  144. data/lib/samagotchi/terminal_ui.rb +3 -1
  145. data/lib/samagotchi/testing/bundle.rb +5 -12
  146. data/lib/samagotchi/testing/install.rb +5 -4
  147. data/lib/samagotchi/testing/recorder.rb +1 -1
  148. data/lib/samagotchi/testing.rb +1 -1
  149. data/lib/samagotchi/thinking.rb +16 -21
  150. data/lib/samagotchi/tool_declarations.rb +1 -1
  151. data/lib/samagotchi/tool_response.rb +5 -3
  152. data/lib/samagotchi/tool_runner.rb +18 -1
  153. data/lib/samagotchi/toolkit_binding.rb +1 -4
  154. data/lib/samagotchi/toolkit_command.rb +1 -4
  155. data/lib/samagotchi/tools/delegate.rb +60 -51
  156. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  157. data/lib/samagotchi/tools/delegate_result.rb +1 -1
  158. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  159. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  160. data/lib/samagotchi/update_command.rb +11 -7
  161. data/lib/samagotchi/verify_fresh.rb +7 -11
  162. data/lib/samagotchi/version.rb +1 -1
  163. data/lib/samagotchi/vision_support.rb +11 -14
  164. data/lib/samagotchi/web/app.rb +66 -21
  165. data/lib/samagotchi/web/message_display.rb +57 -7
  166. data/lib/samagotchi/web/participant_access.rb +1 -1
  167. data/lib/samagotchi/web/project_list.rb +3 -2
  168. data/lib/samagotchi/web/public/activity.js +16 -7
  169. data/lib/samagotchi/web/public/app.js +29 -8
  170. data/lib/samagotchi/web/public/bubbles.js +8 -3
  171. data/lib/samagotchi/web/public/cards_place.js +15 -0
  172. data/lib/samagotchi/web/public/commands.js +5 -1
  173. data/lib/samagotchi/web/public/data.js +4 -2
  174. data/lib/samagotchi/web/public/history_shape.js +220 -0
  175. data/lib/samagotchi/web/public/index.html +6 -1
  176. data/lib/samagotchi/web/public/project_picker.js +86 -4
  177. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  178. data/lib/samagotchi/web/public/reload_check.js +71 -0
  179. data/lib/samagotchi/web/public/stage_model.js +7 -5
  180. data/lib/samagotchi/web/public/stage_view.js +7 -3
  181. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  182. data/lib/samagotchi/web/public/timing.js +43 -2
  183. data/lib/samagotchi/web/public/turn_block.js +215 -30
  184. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  185. data/lib/samagotchi/web/public/turn_events.js +10 -1
  186. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  187. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  188. data/lib/samagotchi/web/server.rb +4 -2
  189. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  190. data/lib/samagotchi/web/session_control.rb +1 -1
  191. data/lib/samagotchi/web/session_hub.rb +9 -17
  192. data/lib/samagotchi/web/session_show.rb +64 -2
  193. data/lib/samagotchi/web/session_summary.rb +36 -24
  194. data/lib/samagotchi/worker.rb +28 -5
  195. data/lib/samagotchi/worker_link.rb +3 -0
  196. metadata +22 -5
  197. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
  198. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -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:)
@@ -213,6 +213,7 @@ module Samagotchi
213
213
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
214
214
  chi models [--format json] [TEXT] the models --model takes, from every host
215
215
  chi home [set [--ruby PATH] | doctor] the Ruby every chi launch boots
216
+ chi project [init [DIR]] this folder's project; init makes a folder in no git repository one
216
217
  chi remote <add|ls|rm> other machines' chi web, for chi --remote NAME
217
218
  chi --remote NAME <send|answer|note|context|sessions|share|review> … those commands on that machine (or CHI_REMOTE=NAME)
218
219
  chi --remote NAME --attach ID | --resume ID | --dir DIR [-p TEXT] a session on that machine, in this terminal
@@ -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] ModelSettings.all (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
- ModelSettings.all
43
- rescue StandardError
44
- {}
45
- end
46
- host = target.entry.sampling || EMPTY
47
- model = ModelSettings.find(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
@@ -4,6 +4,7 @@ require_relative "version"
4
4
  require_relative "config"
5
5
  require_relative "context_window"
6
6
  require_relative "degrade"
7
+ require_relative "effective_model"
7
8
  require_relative "session"
8
9
  require_relative "log_path"
9
10
  require_relative "model_profile"
@@ -181,15 +182,12 @@ module Samagotchi
181
182
  Degrade.call(:config, "model_window_unreadable") do
182
183
  return nil unless model
183
184
 
184
- registry = HostRegistry.new(env: env)
185
- target = registry.resolve(model)
186
- names = registry.lookup_names(model, target: target)
187
- models = ModelSettings.all(env: env)
188
- window = ContextWindow.setting(target, names: names, models: models)
185
+ effective = effective_model(model, env)
186
+ window = effective.window_setting
189
187
  return nil unless window
190
- return [window.tokens, "hosts.#{target.entry.name}"] if window.source == :host_setting
188
+ return [window.tokens, "hosts.#{effective.host_name}"] if window.source == :host_setting
191
189
 
192
- [window.tokens, "models: #{ModelSettings.find(names, :window_tokens, models: models).key}"]
190
+ [window.tokens, "models: #{effective.lookup.model(:window_tokens).key}"]
193
191
  end
194
192
  end
195
193
 
@@ -232,7 +230,7 @@ module Samagotchi
232
230
  # engine keys memory_write current_model_only): which `<name>.<key>.md`
233
231
  # overlays are this model's.
234
232
  def model_key_for(model, env)
235
- ModelOverlay.key_for(HostRegistry.new(env: env).bare_name(model)) || "-"
233
+ ModelOverlay.key_for(registry(env).bare_name(model)) || "-"
236
234
  rescue StandardError
237
235
  ModelOverlay.key_for(model) || "-"
238
236
  end
@@ -243,7 +241,7 @@ module Samagotchi
243
241
  # without any. Read from the memories dirs now: a session's prompt
244
242
  # takes them at its next build (/stats names the ones it carries).
245
243
  def model_notes_for(model, env)
246
- bare = HostRegistry.new(env: env).bare_name(model)
244
+ bare = registry(env).bare_name(model)
247
245
  key = ModelOverlay.key_for(bare)
248
246
  notes = ModelNotes.for(name: bare, key: key, fallback_key: fallback_model_key(model, key, env),
249
247
  muted: session_muted(env), env: env, warn: false)
@@ -260,7 +258,7 @@ module Samagotchi
260
258
  def fallback_model_key(model, key, env)
261
259
  Degrade.call(:model, "fallback_key_unresolved") do
262
260
  typed = session_typed_model(env) || model
263
- typed_key = ModelOverlay.key_for(HostRegistry.new(env: env).parse_qualified_model(typed).last)
261
+ typed_key = ModelOverlay.key_for(registry(env).parse_qualified_model(typed).last)
264
262
  typed_key == key ? nil : typed_key
265
263
  end
266
264
  end
@@ -311,7 +309,7 @@ module Samagotchi
311
309
  # Routed without model discovery, so an unqualified name may land on
312
310
  # another host at runtime after /models.
313
311
  def loop_for(model, env)
314
- entry = HostRegistry.new(env: env).resolve(model).entry
312
+ entry = registry(env).resolve(model).entry
315
313
  return "-" unless entry
316
314
 
317
315
  return "native (raw prompt)" unless entry.chat?
@@ -327,15 +325,15 @@ module Samagotchi
327
325
  # chat host (api: openai or anthropic) has no /props, so it gets its model list
328
326
  # with the same timeouts: up/down and the ids it serves.
329
327
  def served_model_for(model, env)
330
- registry = HostRegistry.new(env: env)
331
- target = registry.resolve(model)
328
+ hosts = registry(env)
329
+ target = hosts.resolve(model)
332
330
  entry = target.entry
333
331
  bare = target.bare_model
334
332
  return "-" unless entry
335
333
  return "reported per turn (remote host)" if entry.remote?
336
334
  return chat_served_model_for(entry, bare, env) if entry.chat?
337
335
 
338
- client = registry.client_for(entry)
336
+ client = hosts.client_for(entry)
339
337
  props = client.server_props(model: bare)
340
338
  return NO_PROPS if props.nil? || props.status == :http_error
341
339
  return "unknown (the server didn't answer; is it running?)" if props.status == :network_error
@@ -374,16 +372,12 @@ module Samagotchi
374
372
  # Offline, so no /props probe: where nothing is configured, a native
375
373
  # llama.cpp host's chat template decides at runtime.
376
374
  def profile_for(model, env)
377
- registry = HostRegistry.new(env: env)
378
- target = registry.resolve(model)
379
- entry = target.entry
380
- bare = target.bare_model
381
- return "name-based (chat API: only strips thoughts)" if entry&.chat?
375
+ effective = effective_model(model, env)
376
+ return "name-based (chat API: only strips thoughts)" if effective.entry&.chat?
382
377
 
383
- names = registry.lookup_names(model)
384
- override = Config.resolve_with_origin("model.profile", env: env, cli_overrides: Config.cli_overrides)
385
- result = ModelProfile.resolve(names: names, entry: entry, client: nil, bare_model: bare, override: override,
386
- models: ModelSettings.all(env: env))
378
+ lookup = effective.lookup
379
+ result = ModelProfile.resolve(names: effective.names, entry: effective.entry, client: nil, bare_model: effective.bare,
380
+ override: lookup.global("model.profile"), models: lookup.models)
387
381
  name = result.profile.name
388
382
  case result.source
389
383
  when :config then "#{name} (config #{result.detail})"
@@ -397,11 +391,8 @@ module Samagotchi
397
391
  # The model's thinking level and where it came from (Thinking.resolve);
398
392
  # in a session's commands its own level first (SAMAGOTCHI_SESSION_THINKING).
399
393
  def thinking_for(model, env)
400
- registry = HostRegistry.new(env: env)
401
- target = registry.resolve(model)
402
394
  session = session_model(env) ? Thinking.session_level(env[SESSION_THINKING_ENV]) : nil
403
- Thinking.explain(target, names: registry.lookup_names(model, target: target),
404
- models: ModelSettings.all(env: env), session: session).label
395
+ Thinking.explain(effective_model(model, env).lookup, session: session).label
405
396
  rescue StandardError => e
406
397
  "(unknown: #{e.message})"
407
398
  end
@@ -411,20 +402,28 @@ module Samagotchi
411
402
  # first (SAMAGOTCHI_SESSION_REASONING_HISTORY); a native host keeps the
412
403
  # running turn's on its own.
413
404
  def reasoning_history_for(model, env)
414
- registry = HostRegistry.new(env: env)
415
- target = registry.resolve(model)
416
- return "native loop: the running turn's, always" unless target.entry&.chat?
405
+ effective = effective_model(model, env)
406
+ return "native loop: the running turn's, always" unless effective.entry&.chat?
417
407
 
418
408
  session = session_model(env) ? ReasoningHistory.session_level(env[SESSION_REASONING_HISTORY_ENV]) : nil
419
- level, source = ReasoningHistory.resolve(target, names: registry.lookup_names(model, target: target),
420
- models: ModelSettings.all(env: env), session: session)
421
- ReasoningHistory.label(level, source)
409
+ ReasoningHistory.label(*ReasoningHistory.resolve(effective.lookup, session: session))
422
410
  rescue StandardError => e
423
411
  "(unknown: #{e.message})"
424
412
  end
425
413
 
414
+ # The hosts chi self reads (HostRegistry.current: the process's shared
415
+ # one for ENV, a fresh one for another env).
416
+ def registry(env) = HostRegistry.current(env: env)
417
+
418
+ # +model+ as chi self reads its settings (EffectiveModel: every layer
419
+ # through its ModelLookup, with +env+). Raises as HostRegistry#resolve does.
420
+ # @return [EffectiveModel]
421
+ def effective_model(model, env)
422
+ EffectiveModel.of(registry(env), ModelChoice.new(name: model, typed: model))
423
+ end
424
+
426
425
  def host_for(model, env)
427
- target = HostRegistry.new(env: env).resolve(model)
426
+ target = registry(env).resolve(model)
428
427
  entry = target.entry
429
428
  bare = target.bare_model
430
429
  return "(no hosts configured)" unless entry
@@ -437,7 +436,7 @@ module Samagotchi
437
436
  # The variable the host's API key comes from and whether it is set, or
438
437
  # comes from the Keychain (HostKeys); the key itself is never shown.
439
438
  def api_key_for(model, env)
440
- name = HostRegistry.new(env: env).resolve(model).entry&.api_key_env
439
+ name = registry(env).resolve(model).entry&.api_key_env
441
440
  return "-" unless name
442
441
 
443
442
  state = { env: "set", keychain: "keychain" }.fetch(HostKeys.source(name, env: env), "unset")
@@ -4,6 +4,7 @@ require_relative "client_id"
4
4
  require_relative "delivery"
5
5
  require_relative "terminal_ui/formatting"
6
6
  require_relative "session"
7
+ require_relative "worker_link"
7
8
  require_relative "session_commands"
8
9
  require_relative "parent_answers_command"
9
10
  require_relative "session_inbox"
@@ -17,6 +18,7 @@ require_relative "turn_flow"
17
18
  require_relative "guardrails/parent_approvals"
18
19
  require_relative "guardrails/parent_policy"
19
20
  require_relative "image_store"
21
+ require_relative "effective_model"
20
22
  require_relative "host_registry"
21
23
  require_relative "model_profile"
22
24
  require_relative "vision_support"
@@ -24,7 +26,6 @@ require_relative "cli/command"
24
26
  require_relative "cli/flags"
25
27
  require_relative "cli/parent_wait"
26
28
  require_relative "send_command/parent_gate"
27
- require_relative "send_command/wait_target"
28
29
  require_relative "send_command/busy"
29
30
  require_relative "session_access/local"
30
31
 
@@ -193,16 +194,13 @@ module Samagotchi
193
194
  # @param typed [String, nil] the name the model was typed as (a session's model_typed)
194
195
  # @return [VisionSupport::Answer] value nil when unknown
195
196
  def self.vision_answer(model_name, registry: nil, typed: nil)
196
- registry ||= HostRegistry.new
197
+ registry ||= HostRegistry.current
197
198
  name = ModelProfile.required_model_name(model_name)
198
- target = registry.resolve(name)
199
- names = registry.lookup_names(typed.to_s.strip.empty? ? name : typed, resolved: name, target: target)
200
- entry = target.entry
201
- return VisionSupport.for(target, adapter: registry.adapter_for(entry), names: names) if entry.chat?
202
-
203
- profile = ModelProfile.resolve(names: names, entry: entry, client: target.client, bare_model: target.bare_model).profile
204
- answer = VisionSupport.for(target, profile: profile, names: names)
205
- props = target.client.server_props(model: target.bare_model)
199
+ model = EffectiveModel.of(registry, ModelChoice.new(name: name, typed: typed.to_s.strip.empty? ? name : typed))
200
+ return model.vision_support(nil) if model.chat?
201
+
202
+ answer = model.vision_support(model.profile_resolution.profile)
203
+ props = model.client.server_props(model: model.bare)
206
204
  return UNKNOWN_VISION if answer.no? && !props&.answered? && !configured_no?(answer)
207
205
 
208
206
  answer
@@ -521,9 +519,8 @@ module Samagotchi
521
519
  return report_command(session.id) if @sent_command
522
520
 
523
521
  start = @access.prepare_wait(session.id, session: session, created: true)
524
- wait_for_reply(session.id, cursor: start.cursor, baseline: start.baseline,
525
- timeout: options[:timeout],
526
- **WaitTarget.first_message(@delivered, direct_turn: session.first_turn_id).to_h)
522
+ target = ReplyWait::Target.first_message(start, @delivered, direct_turn: session.first_turn_id)
523
+ wait_for_reply(session.id, target: target, timeout: options[:timeout])
527
524
  end
528
525
 
529
526
  # What chi send --new asks to create: through the worker (images, a
@@ -601,9 +598,8 @@ module Samagotchi
601
598
  return 1 unless deliver(id, prompt)
602
599
  return report_command(id) if @sent_command
603
600
 
604
- target = WaitTarget.message(@delivered, queue: @queue, running: start.running,
605
- running_turn: (start.own_turn if start.running))
606
- wait_for_reply(id, cursor: start.cursor, baseline: start.baseline, timeout: options[:timeout], **target.to_h)
601
+ target = ReplyWait::Target.message(start, @delivered, queue: @queue)
602
+ wait_for_reply(id, target: target, timeout: options[:timeout])
607
603
  end
608
604
 
609
605
  # A participant's --wait -m (a review turn through chi --remote with
@@ -622,9 +618,8 @@ module Samagotchi
622
618
  return 1
623
619
  end
624
620
  start = @access.prepare_wait(id, turn: turn)
625
- wait_for_reply(id, cursor: start.cursor, baseline: start.baseline, timeout: options[:timeout],
626
- owner_grace: start.live ? self.class::WORKER_GONE_AFTER : nil,
627
- **WaitTarget.after(start, requested: turn).to_h)
621
+ target = ReplyWait::Target.participant(start, turn: turn)
622
+ wait_for_reply(id, target: target, timeout: options[:timeout])
628
623
  end
629
624
 
630
625
  # --wait with no message: the session's next reply, sending nothing,
@@ -648,9 +643,8 @@ module Samagotchi
648
643
  error_line("chi send: turn #{requested_turn} is not a running, queued, or last ended turn")
649
644
  return 1
650
645
  end
651
- target = WaitTarget.after(start, requested: requested_turn)
652
- wait_for_reply(id, cursor: start.cursor, baseline: start.baseline,
653
- timeout: options[:timeout], owner_grace: start.live ? self.class::WORKER_GONE_AFTER : nil, **target.to_h)
646
+ target = ReplyWait::Target.wait_only(start, requested: requested_turn)
647
+ wait_for_reply(id, target: target, timeout: options[:timeout])
654
648
  end
655
649
 
656
650
  # A new idle session's first turn, with the images. Its worker's Bridge
@@ -675,7 +669,7 @@ module Samagotchi
675
669
  # its preview; the user can attach and send it again.
676
670
  unless result
677
671
  attach = @access.invocation.attach(session.id)
678
- @info.puts("#{session.id} failed: its worker did not start; the session is kept (#{attach})")
672
+ @info.puts("#{session.id} failed: #{WorkerLink.did_not_start}; the session is kept (#{attach})")
679
673
  return false
680
674
  end
681
675
  unless result.accepted?