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
@@ -10,6 +10,7 @@ require_relative "../guardrails/context"
10
10
  require_relative "peers"
11
11
  require_relative "delegate_wait"
12
12
  require_relative "../child_reports"
13
+ require_relative "../worker_link"
13
14
 
14
15
  module Samagotchi
15
16
  # Loaded on first use: this autoload keeps SessionManager and its ~80
@@ -32,11 +33,6 @@ module Samagotchi
32
33
  # A path in a task: absolute, ~/ or ../, not glued to a word (a URL's
33
34
  # /api is not one) and stopping at quotes, backticks or punctuation.
34
35
  PATH_TOKEN = %r{(?<![\w./~-])(?:~/|\.\./|/(?!/))[^\s`'"<>()\[\]{},;&|]*}
35
- # A child spawned this many seconds ago counts as running even before
36
- # its worker holds the owner lock: two delegate calls in one model
37
- # message come milliseconds apart, and the first child's worker is
38
- # still starting when the second call counts.
39
- STARTING_GRACE_SECONDS = 15
40
36
  # The memory every child starts with, scoped so a project memory of
41
37
  # the same name cannot shadow it.
42
38
  CHILD_MEMORIES = [Session::DELEGATE_MEMORY].freeze
@@ -79,7 +75,11 @@ module Samagotchi
79
75
 
80
76
  note = Array(warnings).map { |warning| "Warning: #{warning}\n" }.join
81
77
  if wait
82
- result = DelegateWait.call(child_id, peers: peers, timeout: timeout)
78
+ started = monotonic
79
+ boot = await_worker(child_id, state_dir: sd, timeout: timeout, cancelled: -> { peers.cancelled? })
80
+ return note + DelegateWait.boot_result(child_id, boot, timeout: timeout, peers: peers) unless boot == :up
81
+
82
+ result = DelegateWait.call(child_id, peers: peers, timeout: timeout, waited: monotonic - started)
83
83
  return RefText.wrap(note + result, result.respond_to?(:ref_path) ? result.ref_path : nil)
84
84
  end
85
85
 
@@ -90,6 +90,32 @@ module Samagotchi
90
90
  "Error: #{e.message}"
91
91
  end
92
92
 
93
+ # Before a wait on a child this call started or woke: its worker
94
+ # takes the owner lock only once it has booted, so the wait's one
95
+ # dead-worker rule (ReplyWait::GONE_AFTER) runs from there. A child a
96
+ # worker (or a one-turn run) owns already goes straight on; else its
97
+ # worker gets WorkerLink::START_LIMIT, the boot limit, to come up,
98
+ # within the call's +timeout+ (its time counts against it) and
99
+ # until the parent's turn is canceled.
100
+ # @param cancelled [#call] the parent's cancel (Peers#cancelled?)
101
+ # @return [Symbol] :up (a worker owns it), :gone (none came up within
102
+ # the boot limit), :timeout (+timeout+ ran out first) or :canceled
103
+ def self.await_worker(child_id, state_dir:, timeout:, cancelled: -> { false })
104
+ dir = Session.session_dir(child_id, state_dir: state_dir)
105
+ return :up if OwnerLock.owner(dir)
106
+
107
+ limit = [WorkerLink::START_LIMIT, timeout.to_f].min
108
+ link = WorkerLink.await(child_id, session_dir: dir, timeout: limit, unlink: false,
109
+ give_up: ->(found) { found.worker_owned? || cancelled.call })
110
+ return :up if link&.worker_owned?
111
+ return :canceled if cancelled.call
112
+
113
+ limit < WorkerLink::START_LIMIT ? :timeout : :gone
114
+ end
115
+
116
+ def self.monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
117
+ private_class_method :monotonic
118
+
93
119
  # How the reply of a child left running comes back: a report only
94
120
  # reaches a parent that can get one (DelegateWait.reports_mode).
95
121
  def self.running_hint(peers)
@@ -135,7 +161,7 @@ module Samagotchi
135
161
  running = running_children(parent.id, state_dir: state_dir)
136
162
  max = max_children
137
163
  if running.size >= max
138
- ids = running.map { |s| s[:short_id] }.join(", ")
164
+ ids = running.map(&:short_id).join(", ")
139
165
  return "Error: #{running.size} delegate#{"s" if running.size != 1} of this session #{running.size == 1 ? "is" : "are"} running " \
140
166
  "(the most is #{max}, #{MAX_CHILDREN_KEY}): #{ids}. #{full_hint(peers)}"
141
167
  end
@@ -255,9 +281,10 @@ module Samagotchi
255
281
  private_class_method :unknown_toolkit
256
282
 
257
283
  # The new child's folder: the parent's, or +cwd+ (relative to the
258
- # parent's) when it is a folder of the parent's repository, in any of
259
- # its worktrees (one project_root, from the common git dir). Outside
260
- # git only a subfolder of the parent's folder. Nothing is created
284
+ # parent's) when it is a folder of the parent's project: in any of its
285
+ # repository's worktrees (one project_root, from the common git dir), or
286
+ # in its marked folder. Outside both only a subfolder of the parent's
287
+ # folder. Nothing is created
261
288
  # here: the model makes a worktree with execute, where guardrails see it.
262
289
  # @return [String] an absolute folder, or an Error: line
263
290
  def self.child_cwd(cwd, parent)
@@ -268,8 +295,8 @@ module Samagotchi
268
295
  return "Error: cwd #{folder} is not a folder (create the worktree first)" unless File.directory?(folder)
269
296
 
270
297
  folder = File.realpath(folder)
271
- unless same_repository?(folder, base)
272
- return "Error: cwd must be a folder of this session's repository (a worktree or subfolder of it), not #{folder}"
298
+ unless same_project?(folder, base)
299
+ return "Error: cwd must be a folder of this session's project (a worktree or subfolder of it), not #{folder}"
273
300
  end
274
301
  if MemoryPaths.in_repo?(base) && !work_tree?(folder)
275
302
  return "Error: cwd #{folder} is not in a work tree of this session's repository (a git dir, or a bare " \
@@ -301,8 +328,11 @@ module Samagotchi
301
328
  def self.within?(path, dir) = path == dir || path.start_with?("#{dir}/")
302
329
  private_class_method :within?
303
330
 
304
- def self.same_repository?(folder, base)
305
- if MemoryPaths.in_repo?(base)
331
+ # One project (a git repository or a marked folder): the same project
332
+ # root, so a nested repository or marked project is another one. A base
333
+ # in no project: a subfolder of it.
334
+ def self.same_project?(folder, base)
335
+ if MemoryPaths.in_project?(base)
306
336
  MemoryPaths.project_root(folder) == MemoryPaths.project_root(base)
307
337
  else
308
338
  within?(folder, File.realpath(base))
@@ -310,7 +340,7 @@ module Samagotchi
310
340
  rescue SystemCallError
311
341
  false
312
342
  end
313
- private_class_method :same_repository?
343
+ private_class_method :same_project?
314
344
 
315
345
  # @return [String] the child's id, or an Error: line
316
346
  def self.follow_up(task, session:, parent:, state_dir:)
@@ -349,28 +379,17 @@ module Samagotchi
349
379
  end
350
380
  private_class_method :follow_up
351
381
 
352
- # The child's turn a delivered follow-up joins, for the wait
353
- # (DelegateCursor#turn): the running turn's (its checkpoint mark), the
354
- # follow-up merging into it; else the delivery's +enqueued_id+, the
355
- # follow-up running as a turn of its own. Only
356
- # from a worker that names turn_ids; nil otherwise, or when the
357
- # running turn has no id (a delegate's first turn): the wait takes the
358
- # next reply.
359
- # A follow-up steered into a turn that ended before it merged runs as
360
- # its own turn, yet the wait is on the ended one (LOW, as before).
361
- # One the child's idle look missed being merged into a turn starting
362
- # then never runs as its own: ReplyWait falls back to that turn's
363
- # reply (merged_elsewhere?).
382
+ # The child's turn a delivered follow-up's wait is bound to
383
+ # (DelegateCursor#turn): its own, the delivery's +enqueued_id+,
384
+ # whether it runs as a turn of its own (the child idle, or its
385
+ # running turn ended before taking it) or a running turn's drain
386
+ # merges it (that turn's end is its answer: the drain's merge record,
387
+ # or ReplyWait's inference for a 0.156 child, ReplyWait::Poll#
388
+ # follow_merge). Only from a worker that names turn_ids; nil
389
+ # otherwise: the wait takes the next reply.
364
390
  # @return [String, nil]
365
391
  def self.follow_up_turn(id, enqueued_id, state_dir:)
366
- return nil unless ReplyWait.turn_ids?(Session.session_dir(id, state_dir: state_dir))
367
-
368
- child = Session.load(id, state_dir: state_dir)
369
- return child.interrupted_turn&.enqueued_id if child.status == Session::STATUS_RUNNING
370
-
371
- enqueued_id
372
- rescue ArgumentError
373
- nil
392
+ ReplyWait.turn_ids?(Session.session_dir(id, state_dir: state_dir)) ? enqueued_id : nil
374
393
  end
375
394
  private_class_method :follow_up_turn
376
395
 
@@ -388,23 +407,13 @@ module Samagotchi
388
407
 
389
408
  # The children of +parent_id+ that count against session.max_children
390
409
  # (a plugin's ctx.sessions.fork counts them too).
391
- # @return [Array<Hash>] SessionManager.children_of rows
410
+ # A child spawned moments ago counts as running even before its worker
411
+ # holds the owner lock (SessionRow#running_or_starting?): two delegate
412
+ # calls in one model message come milliseconds apart, and the first
413
+ # child's worker is still starting when the second call counts.
414
+ # @return [Array<SessionRow>] SessionManager.children_of rows
392
415
  def self.running_children(parent_id, state_dir:)
393
- SessionManager.children_of(parent_id, state_dir: state_dir).select { |s| running?(s) }
394
- end
395
-
396
- # busy (a worker runs its turn), or running with a worker on its way
397
- # (ChildrenStatus asks it too).
398
- # @param summary [Hash] a children_of row, or the same keys: busy,
399
- # saved_status (the file's: the displayed status shows a worker on
400
- # its way as idle), live, updated_at
401
- def self.running?(summary)
402
- return true if summary[:busy]
403
- return false unless summary[:saved_status] == Session::STATUS_RUNNING && !summary[:live]
404
-
405
- Time.now - Time.iso8601(summary[:updated_at].to_s) < STARTING_GRACE_SECONDS
406
- rescue ArgumentError
407
- false
416
+ SessionManager.children_of(parent_id, state_dir: state_dir).select(&:running_or_starting?)
408
417
  end
409
418
 
410
419
  def self.max_children
@@ -262,16 +262,16 @@ module Samagotchi
262
262
  # oldest question first
263
263
  def waiting
264
264
  rows = SessionManager.children_of(@parent_id, state_dir: @state_dir).select do |row|
265
- row[:id] != @except && KINDS.include?(row[:waiting].to_s) && !@done[row[:waiting_id].to_s]
265
+ row.id != @except && KINDS.include?(row.waiting.to_s) && !@done[row.waiting_id.to_s]
266
266
  end
267
267
  rows.filter_map do |row|
268
- child = Session.load(row[:id], state_dir: @state_dir)
268
+ child = Session.load(row.id, state_dir: @state_dir)
269
269
  pending = child.pending_question
270
- next nil unless pending && pending.id.to_s == row[:waiting_id].to_s
270
+ next nil unless pending && pending.id.to_s == row.waiting_id.to_s
271
271
  next nil unless DelegateRelay.relayable?(@relay, pending, strategy: Guardrails::ParentPolicy.for_session(child),
272
- child_id: row[:id])
272
+ child_id: row.id)
273
273
 
274
- [row[:id], pending]
274
+ [row.id, pending]
275
275
  rescue ArgumentError
276
276
  nil
277
277
  end.sort_by { |_, pending| pending.created_at.to_s }
@@ -45,7 +45,7 @@ module Samagotchi
45
45
  end
46
46
 
47
47
  def self.newest_running_child(parent_id, state_dir:)
48
- SessionManager.children_of(parent_id, state_dir: state_dir).find { |s| s[:busy] }&.fetch(:id)
48
+ SessionManager.children_of(parent_id, state_dir: state_dir).find(&:busy)&.id
49
49
  end
50
50
  private_class_method :newest_running_child
51
51
  end
@@ -8,6 +8,7 @@ require_relative "peers"
8
8
  require_relative "delegate_relay"
9
9
  require_relative "delegate_cursor"
10
10
  require_relative "ref_text"
11
+ require_relative "../worker_link"
11
12
 
12
13
  module Samagotchi
13
14
  # Loaded on first use: child_reports requires this file (a circular
@@ -27,10 +28,6 @@ module Samagotchi
27
28
  Advance = Data.define(:cursor, :already_reported)
28
29
  TIMEOUT_DEFAULT = 600
29
30
  POLL_INTERVAL = 0.5
30
- # Seconds with no worker before the child counts as gone: as long as
31
- # Delegate gives a starting child (its worker takes the owner lock
32
- # only once it has booted).
33
- OWNER_GRACE = 15
34
31
  # Cut like an execute result: full up to TRUNCATE_AT bytes, else a
35
32
  # head-and-tail preview of PREVIEW bytes.
36
33
  TRUNCATE_AT_BYTES = OutputGuardrails::DEFAULT_TRUNCATE_AT_BYTES
@@ -50,7 +47,9 @@ module Samagotchi
50
47
  # @param timeout [Integer] seconds
51
48
  # @param poll_interval [Float]
52
49
  # @param owner_grace [Numeric] seconds with no live worker before
53
- # the child's worker counts as gone
50
+ # the child's worker counts as gone (ReplyWait::GONE_AFTER; a child
51
+ # whose worker is on its way was awaited first:
52
+ # Delegate.await_worker)
54
53
  # @return [String] the tool result
55
54
  #
56
55
  # A child's approval, when this session can host a relay (Peers#relay),
@@ -59,7 +58,10 @@ module Samagotchi
59
58
  # against +timeout+, and the result gets one outcome line per relayed
60
59
  # approval. Without a relay, and for the model's and hooks' questions,
61
60
  # the question comes back to the model as before.
62
- def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL, owner_grace: OWNER_GRACE)
61
+ # @param waited [Numeric] seconds of +timeout+ already spent before
62
+ # this wait (Delegate's boot await); the result still names +timeout+
63
+ def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL, owner_grace: ReplyWait::GONE_AFTER,
64
+ waited: 0)
63
65
  sd = peers.state_dir || Session.default_state_dir
64
66
  key = [peers.session_id, child_id]
65
67
  cancelled = -> { peers.cancelled? }
@@ -73,15 +75,15 @@ module Samagotchi
73
75
  cursor = report_claim.cursor.with(report_claim: report_claim)
74
76
  end
75
77
 
76
- baseline = cursor.baseline
77
- left = timeout.to_i
78
+ target = ReplyWait::Target.from_cursor(cursor, child: Session.summary_from_file(Session.session_file(child_id, state_dir: sd)),
79
+ owner_grace: owner_grace)
80
+ left = timeout.to_i - waited
78
81
  outcomes = relayed_outcomes.delete(key) || []
79
82
  loop do
80
83
  started = monotonic
81
- wait = ReplyWait.call(child_id, state_dir: sd, cursor: cursor.reply_file, timeout: [left, 0].max,
82
- poll_interval: poll_interval, cancelled: cancelled, baseline: baseline,
83
- owner_grace: owner_grace, interject: others && -> { others.poll },
84
- turn: cursor.turn, message_waiting: message_waiting)
84
+ wait = ReplyWait.call(child_id, state_dir: sd, **target.to_h, timeout: [left, 0].max,
85
+ poll_interval: poll_interval, cancelled: cancelled,
86
+ interject: others && -> { others.poll }, message_waiting: message_waiting)
85
87
  left -= monotonic - started
86
88
  unless wait.status == :waiting_for_answer &&
87
89
  DelegateRelay.relayable?(relay, wait.question, strategy: Guardrails::ParentPolicy.for_session(wait.session),
@@ -108,7 +110,7 @@ module Samagotchi
108
110
  # asked), the relayed question no longer reported: a turn that
109
111
  # ends with no reply during the relay's POST or after it still
110
112
  # ends the wait.
111
- baseline = ReplyWait.baseline_of(wait.session, question_id: wait.question.id)
113
+ target = target.with(baseline: ReplyWait.baseline_of(wait.session, question_id: wait.question.id))
112
114
  end
113
115
  rescue ArgumentError => e
114
116
  "Error: #{e.message}"
@@ -179,6 +181,10 @@ module Samagotchi
179
181
 
180
182
  def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
181
183
 
184
+ # A worker_gone wait's outcome when the child's worker never came up
185
+ # (#boot_result).
186
+ DID_NOT_START = "did_not_start"
187
+
182
188
  REPORT_NEXT_REPLY = "chi brings the child's next reply here by itself; don't wait for it with delegate_result."
183
189
 
184
190
  # Outcomes that tell the parent something about the child's state.
@@ -254,8 +260,8 @@ module Samagotchi
254
260
  when :no_reply
255
261
  result(child_id, status, "#{no_reply_text(wait, child_id, strategy: strategy)}; its session shows what happened")
256
262
  when :worker_gone
257
- result(child_id, status,
258
- "the child's worker is gone (it stopped or crashed); delegate with session: #{child_id} starts it again with a message")
263
+ gone = wait.outcome == DID_NOT_START ? WorkerLink.did_not_start : "the child's worker is gone (it stopped or crashed)"
264
+ result(child_id, status, "#{gone}; delegate with session: #{child_id} starts it again with a message")
259
265
  else
260
266
  timeout_result(child_id, timeout, reports: reports)
261
267
  end
@@ -280,11 +286,29 @@ module Samagotchi
280
286
  DelegateCursors.update(parent_id, child_id, state_dir: state_dir) { |cursor| cursor.with(turn: turn) }
281
287
  end
282
288
 
283
- # A new child's baseline: as spawned, before its first turn ran.
289
+ # A new child's baseline: as spawned, before its first turn ran, and
290
+ # its first turn (Session#first_turn_id, the prompt's turn) as the
291
+ # one waited for: queued (ReplyWait::Target.from_cursor), so the
292
+ # window between its worker going live and that turn's mark doesn't
293
+ # read as a merge into another turn.
284
294
  # @param child [Session] the session spawn_session returned
285
295
  def mark_started(parent_id, child, state_dir:)
286
296
  DelegateCursors.update(parent_id, child.id, state_dir: state_dir) do |cursor|
287
- cursor.with(turn: nil).with_baseline(ReplyWait.baseline_of(child, question_id: nil))
297
+ cursor.with(turn: child.first_turn_id).with_baseline(ReplyWait.baseline_of(child, question_id: nil))
298
+ end
299
+ end
300
+
301
+ # How Delegate's boot await ended when no worker came up
302
+ # (Delegate.await_worker): :gone is worker_gone ("its worker did not
303
+ # start"), :timeout and :canceled read as a wait's own.
304
+ # @param boot [Symbol] :gone, :timeout or :canceled
305
+ def boot_result(child_id, boot, timeout:, peers:)
306
+ reports = reports_mode(peers)
307
+ case boot
308
+ when :canceled then canceled_result(child_id, reports: reports)
309
+ when :timeout then timeout_result(child_id, timeout, reports: reports)
310
+ else finish(ReplyWait::Result.new(status: :worker_gone, outcome: DID_NOT_START), child_id, timeout: timeout,
311
+ reports: reports)
288
312
  end
289
313
  end
290
314
 
@@ -31,9 +31,9 @@ module Samagotchi
31
31
  folder = cwd.to_s.strip.empty? ? nil : File.expand_path(cwd.to_s.strip)
32
32
  own = own_session(peers)
33
33
  project = folder ? nil : own_project(peers, own)
34
- summaries = SessionManager.session_summaries(cwd: folder, limit: LIMIT, include_tests: false,
35
- exclude: peers.session_id, state_dir: peers.state_dir,
36
- project_root: project)
34
+ summaries = SessionManager.session_rows(cwd: folder, limit: LIMIT, include_tests: false,
35
+ exclude: peers.session_id, state_dir: peers.state_dir,
36
+ project_root: project)
37
37
  if summaries.empty?
38
38
  return "No other chi sessions in this project (#{File.basename(project)}); cwd \"/\" lists every project's." if project
39
39
 
@@ -41,12 +41,12 @@ module Samagotchi
41
41
  end
42
42
 
43
43
  lines = summaries.map do |s|
44
- state = s[:live] ? "live" : "not live"
45
- relation = if s[:parent_id] == peers.session_id then " child"
46
- elsif own&.parent_id && s[:id] == own.parent_id then " parent"
44
+ state = s.live ? "live" : "not live"
45
+ relation = if s.parent_id == peers.session_id then " child"
46
+ elsif own&.parent_id && s.id == own.parent_id then " parent"
47
47
  else ""
48
48
  end
49
- "#{s[:short_id]} #{state} #{s[:busy] ? "running" : "idle"}#{relation} #{Paths.tilde(s[:cwd])} #{s[:preview].inspect}"
49
+ "#{s.short_id} #{state} #{s.busy ? "running" : "idle"}#{relation} #{Paths.tilde(s.cwd)} #{s.preview.inspect}"
50
50
  end
51
51
  scope = project ? " in this project (#{File.basename(project)}; cwd \"/\" for every project)" : ""
52
52
  <<~TEXT.chomp
@@ -22,8 +22,8 @@ require "json"
22
22
  module Samagotchi
23
23
  # `chi update`: brings an installed chi up to date in one go: the system
24
24
  # bundle, the shipped bundles the user installed, and the macOS helper.
25
- # Unedited things update; edited ones merge or stay behind (.md) or are replaced
26
- # and reported (hooks, plugin, rules, which didn't load once edited). Live
25
+ # Unedited things update; edited ones merge or stay behind (.md) or are replaced,
26
+ # copied to the trash first, and reported (hooks, plugin, rules, scripts). Live
27
27
  # workers and a running chi web are reported, never restarted. The bundles
28
28
  # chi didn't ship are only checked, under the chi that will run them
29
29
  # (BundleCommand::InstalledCheck): what it finds is rows that fail nothing.
@@ -297,10 +297,14 @@ module Samagotchi
297
297
  earlier = [] if %i[installed skipped failed].include?(result.status)
298
298
  row = Row.new(component: "system bundle", from: result.from, to: result.to)
299
299
  notes = file_notes(row, system::BUNDLE_NAME, result.to, result.merged, result.behind, result.trash_copy, earlier: earlier)
300
+ # What the run warned of (a Checksum mismatch, a file it skipped) goes
301
+ # on its row, as ensure! echoes it.
302
+ warnings = Array(result.warnings)
303
+ warned = [*notes, *warnings]
300
304
  case result.status
301
- when :installed then done(row, :install)
302
- when :updated then done(row, :update, notes.empty? ? nil : notes.join("; "))
303
- when :restored then done(row, :restore, notes.empty? ? nil : notes.join("; ")).tap { |r| r.to = nil }
305
+ when :installed then done(row, :install, warnings.empty? ? nil : warnings.join("; "))
306
+ when :updated then done(row, :update, warned.empty? ? nil : warned.join("; "))
307
+ when :restored then done(row, :restore, warned.empty? ? nil : warned.join("; ")).tap { |r| r.to = nil }
304
308
  when :merged, :up_to_date then same_version(row, notes + result.warnings, changed: result.merged.any?)
305
309
  when :newer_installed then skipped(row, ["newer than this chi's #{result.to}: left", *notes].join("; ")).tap { |r| r.to = nil }
306
310
  when :skipped then skipped(row, result.error)
@@ -352,7 +356,7 @@ module Samagotchi
352
356
  notes = file_notes(row, update.name, update.to || update.from, update.merged, update.behind, update.trash_copy, earlier: earlier)
353
357
  case update.status
354
358
  when :would_update, :updated
355
- notes = [adds(update.adds), *notes, replaced(update.replaced), *update.kept_own].compact
359
+ notes = [adds(update.adds), *notes, replaced(update.replaced), *update.kept_own, update.note].compact
356
360
  done(row, :update, notes.empty? ? nil : notes.join("; "))
357
361
  when :would_merge, :merged, :up_to_date then same_version(row, [*notes, update.note, update.manifest].compact, changed: Array(update.merged).any?)
358
362
  when :skipped then skipped(row, [update.note, *notes].join("; "))
@@ -526,7 +530,7 @@ module Samagotchi
526
530
  def replaced(files)
527
531
  return nil if files.empty?
528
532
 
529
- "#{@dry_run ? "would replace" : "replaced"} edited #{files.join(", ")} (it wasn't loading)"
533
+ "#{@dry_run ? "would replace" : "replaced"} edited #{files.join(", ")} (a copy goes to the trash first)"
530
534
  end
531
535
 
532
536
  def print_table(rows)
@@ -7,6 +7,8 @@ require_relative "atomic_file"
7
7
  require_relative "log"
8
8
  require_relative "session"
9
9
  require_relative "session_metrics"
10
+ require_relative "session_row"
11
+ require_relative "worker_link"
10
12
  require_relative "steer"
11
13
  require_relative "verify_anchor"
12
14
  require_relative "verify_fresh_origin"
@@ -37,10 +39,6 @@ module Samagotchi
37
39
  # The child's first message, after the claim.
38
40
  ASK = "Verify this claim against the files here. You have none of the original session's context. " \
39
41
  "End with a line `Verdict: confirmed|refuted|unclear — <one-line reason>`."
40
- # How long a child whose worker hasn't come up yet counts as running
41
- # (Tools::Delegate::STARTING_GRACE_SECONDS).
42
- STARTING_GRACE_SECONDS = 15
43
-
44
42
  # A start refused: the HTTP +status+, the error +code+ and its words.
45
43
  class Refused < StandardError
46
44
  attr_reader :status, :code
@@ -257,15 +255,13 @@ module Samagotchi
257
255
  private_class_method :locked
258
256
 
259
257
  # A child runs while its worker runs a turn, or while it is marked
260
- # running and its worker is on its way (Tools::Delegate.running?).
258
+ # running and its worker is on its way (SessionRow.running_or_starting?,
259
+ # the delegates' rule).
261
260
  def child_running?(child_id, state_dir)
262
261
  child = Session.summary_from_file(Session.session_file(child_id, state_dir: state_dir)) or return false
263
- return false unless child.status == Session::STATUS_RUNNING
264
- return true if SessionManager.session_owner(child_id, state_dir: state_dir)&.worker?
265
-
266
- Time.now - Time.iso8601(child.updated_at.to_s) < STARTING_GRACE_SECONDS
267
- rescue ArgumentError
268
- false
262
+ live = WorkerLink.resolve(Session.session_dir(child_id, state_dir: state_dir), unlink: false).worker_owned?
263
+ SessionRow.running_or_starting?(saved_status: child.status, live: live,
264
+ busy: live && child.status == Session::STATUS_RUNNING, updated_at: child.updated_at)
269
265
  end
270
266
  private_class_method :child_running?
271
267
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.156.0"
4
+ VERSION = "0.158.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -21,15 +21,15 @@ module Samagotchi
21
21
  def no? = value == false
22
22
  end
23
23
 
24
- # @param target [HostRegistry::ModelTarget]
24
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup);
25
+ # its target is the one the probes ask
25
26
  # @param profile [ModelProfile, nil] the model's prompt profile (native)
26
27
  # @param adapter [LLM::OpenAIChat, nil] the chat host's adapter
27
- # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
28
- # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
29
28
  # @return [Answer]
30
- def self.for(target, names:, profile: nil, adapter: nil, models: nil)
29
+ def self.for(lookup, profile: nil, adapter: nil)
30
+ target = lookup.target
31
31
  entry = target.entry
32
- configured = configured(target, entry, models, names)
32
+ configured = configured(lookup)
33
33
  return configured if configured&.no?
34
34
 
35
35
  if entry.chat?
@@ -45,18 +45,15 @@ module Samagotchi
45
45
  marker.is_a?(String) && !marker.empty? ? marker : nil
46
46
  end
47
47
 
48
- def self.configured(_target, entry, models, names)
49
- models ||= begin
50
- ModelSettings.all
51
- rescue StandardError
52
- {}
53
- end
54
- if (setting = ModelSettings.find(names, :vision, models: models))
48
+ def self.configured(lookup)
49
+ if (setting = lookup.model(:vision))
55
50
  return Answer.new(value: setting.value, reason: setting.value ? nil : "models: #{setting.key} sets vision: false")
56
51
  end
57
- return nil if entry.vision.nil?
58
52
 
59
- Answer.new(value: entry.vision, reason: entry.vision ? nil : "hosts.#{entry.name} sets vision: false")
53
+ vision = lookup.host(:vision)
54
+ return nil if vision.nil?
55
+
56
+ Answer.new(value: vision, reason: vision ? nil : "hosts.#{lookup.host_name} sets vision: false")
60
57
  end
61
58
 
62
59
  def self.native(target, _entry, profile, trust_config:)