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
@@ -142,7 +142,7 @@ module Samagotchi
142
142
  :cached_sum,
143
143
  :cache_write_sum,
144
144
  # What the server prefilled again of the previous request's prompt
145
- # (KernelLoop#reprefilled_tokens; an LLM context edit's cache break).
145
+ # (ReprefillMeter#tokens; an LLM context edit's cache break).
146
146
  :reprefill_sum,
147
147
  :reasoning_sum,
148
148
  :cost_sum,
@@ -1112,19 +1112,22 @@ module Samagotchi
1112
1112
  if guardrail.is_a?(Hash) && guardrail[:decided_by] == "system one"
1113
1113
  active[:guardrail] = { rule: guardrail[:rule], p: guardrail[:p], judgment_id: guardrail[:judgment_id] }.compact
1114
1114
  end
1115
- finish_tool_record(active, status.empty? ? "ok" : status, waited_ms: event[:waited_ms].to_i)
1115
+ finish_tool_record(active, status.empty? ? "ok" : status, waited_ms: event[:waited_ms].to_i,
1116
+ duration_ms: event[:duration_ms])
1116
1117
  end
1117
1118
  end
1118
1119
 
1119
- # Caller holds the mutex.
1120
- def finish_tool_record(active, status, waited_ms: 0)
1120
+ # Caller holds the mutex. +duration_ms+: the event's (ToolRunner's,
1121
+ # the figure every UI shows live); without one (a test kernel's
1122
+ # events), timed here.
1123
+ def finish_tool_record(active, status, waited_ms: 0, duration_ms: nil)
1121
1124
  record = active.slice(
1122
1125
  :id, :turn_id, :iteration, :call_index, :tool, :called_as, :via, :no_match, :guardrail, :started_at
1123
1126
  ).merge(
1124
1127
  status: status,
1125
1128
  completed_at: now.iso8601(3),
1126
1129
  # Less a guardrail approval wait (ToolRunner's waited_ms).
1127
- duration_ms: [elapsed_ms(active[:started_monotonic]) - waited_ms, 0].max
1130
+ duration_ms: duration_ms.is_a?(Integer) ? duration_ms : [elapsed_ms(active[:started_monotonic]) - waited_ms, 0].max
1128
1131
  )
1129
1132
  @tool_records << record
1130
1133
  count_tool(record)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "paths"
4
+
5
+ module Samagotchi
6
+ # Where a session's files are and which ids name one: the sessions dir,
7
+ # a session's <id>.json and <id>/ dir, and the id rule every id from
8
+ # outside meets before it becomes a path. A leaf (Paths only), so a file
9
+ # that needs a session's path needs no Session: Session extends it, so
10
+ # Session.session_dir and the rest answer the same. `extend self`, not
11
+ # module_function: Session's copies must stay public.
12
+ module SessionPaths
13
+ extend self # rubocop:disable Style/ModuleFunction -- module_function would make Session's copies private
14
+
15
+ STATE_SUBDIR = File.join("samagotchi", "sessions")
16
+ FILE_EXT = ".json"
17
+
18
+ # An id that is not a session id: it could name a path outside the
19
+ # sessions dir ("../x", "/etc", "a/b", a NUL), or is empty.
20
+ class InvalidId < ArgumentError; end
21
+
22
+ # Ids are SecureRandom.uuid; anything made of letters, digits, "-" and
23
+ # "_" (up to 128, not starting with "-") is taken, so a UUID prefix the
24
+ # CLI resolves passes too. This is the one check every id from outside
25
+ # (the bridge, the web routes, chi send/note, delegate) meets before it
26
+ # becomes a path: session_dir and the session file path refuse others.
27
+ VALID_ID = /\A[A-Za-z0-9_][A-Za-z0-9_-]{0,127}\z/
28
+
29
+ def valid_id?(session_id)
30
+ session_id.is_a?(String) && VALID_ID.match?(session_id)
31
+ end
32
+
33
+ # @raise [InvalidId] unless valid_id?(session_id)
34
+ def check_id!(session_id)
35
+ return session_id if valid_id?(session_id)
36
+
37
+ raise InvalidId, "invalid session id: #{session_id.to_s[0, 80].inspect}"
38
+ end
39
+
40
+ # Directory for a specific session (holds IPC files alongside session.json).
41
+ # @raise [InvalidId] for an id that isn't one (valid_id?)
42
+ def session_dir(session_id, state_dir: default_state_dir)
43
+ File.join(state_dir, check_id!(session_id))
44
+ end
45
+
46
+ # A session's <id>.json.
47
+ # @raise [InvalidId] for an id that isn't one (valid_id?)
48
+ def session_file(session_id, state_dir: default_state_dir)
49
+ File.join(state_dir, "#{check_id!(session_id)}#{FILE_EXT}")
50
+ end
51
+
52
+ # XDG-aware sessions directory.
53
+ def default_state_dir(env: ENV)
54
+ File.join(Paths.state_home(env: env), STATE_SUBDIR)
55
+ end
56
+ end
57
+ end
@@ -78,7 +78,7 @@ module Samagotchi
78
78
  Dir.children(state_dir).filter_map do |name|
79
79
  dir = File.join(state_dir, name)
80
80
  next unless Session.valid_id?(name) && File.directory?(dir)
81
- next if File.exist?(File.join(state_dir, "#{name}#{Session::FILE_EXT}"))
81
+ next if File.exist?(File.join(state_dir, "#{name}#{SessionPaths::FILE_EXT}"))
82
82
  next unless Time.now - File.mtime(dir) > EMPTY_GRACE_SECONDS && SessionManager.empty_session_dir?(dir)
83
83
  next if SessionManager.session_owner(name, state_dir: state_dir)
84
84
 
@@ -203,7 +203,7 @@ module Samagotchi
203
203
  passed_over = 0
204
204
 
205
205
  all.each_with_index do |session, idx|
206
- path = File.join(state_dir, "#{session.id}#{Session::FILE_EXT}")
206
+ path = File.join(state_dir, "#{session.id}#{SessionPaths::FILE_EXT}")
207
207
  # Only when json present
208
208
  unless File.exist?(path)
209
209
  skipped << session.id
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require_relative "session"
5
+ require_relative "owner_lock"
6
+ require_relative "worker_link"
7
+ require_relative "recap_store"
8
+ require_relative "session_metrics"
9
+ require_relative "archive_store"
10
+ require_relative "effective_model"
11
+ require_relative "host_registry"
12
+
13
+ module Samagotchi
14
+ # A session as the lists see it: the one row every listing builds and
15
+ # reads (SessionManager.session_rows, chi sessions list on both backends,
16
+ # the picker, list_sessions, a broadcast's recipients, a parent's
17
+ # children, chi web's summaries route and Web::SessionSummary). Only
18
+ # scalar facts that go on the wire: #to_summary_h is the summaries route's
19
+ # body (28 keys, this order) and .from_summary_h reads it back, so a
20
+ # remote row prints the same #cli_line as a local one.
21
+ #
22
+ # The liveness rule: +live+ is WorkerLink's worker_owned? (a worker holds
23
+ # the lock, its Bridge up or on its way); +busy+ and +waiting+ need it,
24
+ # +status+ is Session#displayed_status (a dead worker's "running" is idle).
25
+ SessionRow = Data.define(:id, :desc, :preview, :title, :cwd, :project, :updated_at, :status, :saved_status, :live,
26
+ :busy, :owner, :recap, :auto_title, :ctx_pct, :parent_id, :continues, :delegate, :archived,
27
+ :scratch, :test_run, :stopped_by, :waiting, :waiting_id, :relayed_to)
28
+
29
+ class SessionRow
30
+ DESC_LIMIT = 60
31
+ PREVIEW_LIMIT = 120
32
+ # Seconds a child saved "running" with no worker yet counts as one on
33
+ # its way (a worker boots in a few seconds): .running_or_starting?.
34
+ STARTING_GRACE_SECONDS = 15
35
+
36
+ # The summaries route's keys, in their order (#to_summary_h).
37
+ SUMMARY_KEYS = %i[id short_id desc preview title cwd project updated_at status saved_status live busy owner recap
38
+ auto_title ctx_pct parent_id parent_short_id continues continues_short_id delegate archived
39
+ scratch test_run stopped_by waiting waiting_id relayed_to].freeze
40
+ DERIVED_KEYS = %i[short_id parent_short_id continues_short_id].freeze
41
+ # `chi sessions list --format json`'s keys (of SUMMARY_KEYS: no status).
42
+ # owner: "worker", "tui" (a one-turn chi run: it takes no notes or
43
+ # messages) or nil; recap: the saved recap's first sentence, or nil;
44
+ # project: its git project's root, or nil; parent_id: the session that
45
+ # delegated it (the delegate tool), or nil; delegate: started by the
46
+ # delegate tool (a fork has a parent_id too); continues: the session it
47
+ # continues (its chain's previous link), or nil; archived: hidden from
48
+ # the lists (only with --archived can it be true); scratch: a `chi
49
+ # scratch` session; ctx_pct: how full the context was after the last
50
+ # turn, or nil; title: the session's own name (/title; desc shows it
51
+ # too), or nil; auto_title: the one its recap gave it (RecapTitle), or
52
+ # nil; waiting: the kind of question it waits on (question, approval,
53
+ # hook), or nil; waiting_id: its id, for chi answer --question;
54
+ # relayed_to: the parent session (short id) whose card it also waits in
55
+ # (the approval relay), or nil.
56
+ JSON_KEYS = %i[id short_id desc title auto_title cwd project updated_at live busy owner recap parent_id continues
57
+ delegate archived scratch ctx_pct waiting waiting_id relayed_to stopped_by].freeze
58
+
59
+ # @param session [Session] a saved session
60
+ # @param session_dir [String] its folder
61
+ # @param owner [OwnerLock::Owner, nil] the lock's holder as read
62
+ # (SessionManager.session_owner)
63
+ # @param link [WorkerLink outcome, nil] resolved with +owner+; nil:
64
+ # resolved here (one read of bridge.json)
65
+ # @param roots [Hash] ProjectScope.root_for's cache, shared by a listing
66
+ # @param budgets [#call] (session) -> its llm_context budget (.budgets:
67
+ # one HostRegistry per listing)
68
+ # @param saved [SessionMetrics::SavedSummary, nil, :read] the saved
69
+ # analytics as a caller already read it; :read reads it here
70
+ # @param card [RecapStore::Card, :read] the recap card, likewise
71
+ # @return [SessionRow]
72
+ def self.build(session, session_dir:, owner:, link: nil, roots: {}, budgets: SessionRow.budgets, saved: :read,
73
+ card: :read)
74
+ link ||= WorkerLink.resolve(session_dir, unlink: false, owner: owner)
75
+ live = link.worker_owned?
76
+ card = RecapStore.card(session_dir) if card == :read
77
+ if saved == :read
78
+ saved = SessionMetrics.saved_summary(session_dir, budget_tokens: -> { budgets.call(session) })
79
+ end
80
+ waiting = session.waiting_question(live: live)
81
+ new(id: session.id, desc: desc_of(session), preview: cut(summary_text(session), PREVIEW_LIMIT),
82
+ title: session.title, cwd: session.working_directory, project: session.project_root(cache: roots),
83
+ updated_at: session.updated_at, status: session.displayed_status(owner: owner), saved_status: session.status,
84
+ live: live, busy: live && session.status == Session::STATUS_RUNNING, owner: owner&.kind,
85
+ recap: card.preview, auto_title: card.title, ctx_pct: saved&.ctx_pct&.round(1),
86
+ parent_id: session.parent_id, continues: session.continues, delegate: session.delegate?,
87
+ archived: ArchiveStore.archived?(session_dir), scratch: session.scratch, test_run: session.test_run,
88
+ stopped_by: session.stopped_by, waiting: waiting&.dig(:kind), waiting_id: waiting&.dig(:id),
89
+ relayed_to: waiting&.dig(:relayed_to))
90
+ end
91
+
92
+ # A row read back from #to_summary_h's hash (symbol keys; a remote's
93
+ # rows, Remote#symbolize'd). A key the hash lacks reads nil.
94
+ def self.from_summary_h(hash)
95
+ new(**members.to_h { |key| [key, hash[key]] })
96
+ end
97
+
98
+ # The budget resolver a listing shares: the process's HostRegistry
99
+ # (HostRegistry.current), taken on the first row that needs it (a saved
100
+ # context to count) and kept for the rest of the listing.
101
+ # @return [#call] (session) -> the session's budget in tokens or nil
102
+ def self.budgets
103
+ registry = nil
104
+ ->(session) { EffectiveModel.session_budget(session, registry: registry ||= HostRegistry.current) }
105
+ end
106
+
107
+ # Busy (a worker runs its turn), or saved running with no worker yet
108
+ # for less than STARTING_GRACE_SECONDS: a worker on its way (it counts
109
+ # against session.max_children; Verify fresh waits for it). The
110
+ # displayed status shows that one as idle, so +saved_status+ is the
111
+ # file's.
112
+ def self.running_or_starting?(saved_status:, live:, busy:, updated_at:)
113
+ return true if busy
114
+ return false unless saved_status == Session::STATUS_RUNNING && !live
115
+
116
+ Time.now - Time.iso8601(updated_at.to_s) < STARTING_GRACE_SECONDS
117
+ rescue ArgumentError
118
+ false
119
+ end
120
+
121
+ # A session as one line of text for the lists: its title (/title), else
122
+ # its last prompt, else the first message it was started with (a session
123
+ # command like `/model x`, or a first turn not saved yet).
124
+ def self.summary_text(session)
125
+ return one_line(session.title) unless session.title.to_s.strip.empty?
126
+
127
+ one_line(session.last_prompt.to_s.strip.empty? ? session.first_preview.to_s : session.last_prompt.to_s)
128
+ end
129
+
130
+ # A prompt as one line for a session list: whitespace collapsed, and a
131
+ # quoted or annotated message (ContextQuote, the web's annotations)
132
+ # without its "> " markers, so the words show.
133
+ def self.one_line(text)
134
+ text.to_s.gsub(/^[ \t]*(?:>[ \t]?)+/, "").gsub(/\s+/, " ").strip
135
+ end
136
+
137
+ # "<cwd basename> · <last prompt, or the first preview>", one line.
138
+ # The flags a list line carries: a chi scratch session, a test run, an
139
+ # archived one, and the turn's stopper: "[looped]" for loop-guard (the
140
+ # bundle's thinking watch, and the word `chi sessions stats` uses for
141
+ # its cuts), "[stopped by X]" for any other hook (a cut with no retry
142
+ # left records the same stopped_by).
143
+ def self.flags(scratch:, test_run:, archived:, stopped_by: nil)
144
+ flag = if scratch
145
+ " [scratch]"
146
+ else
147
+ (test_run ? " [test]" : "")
148
+ end
149
+ flag += " [archived]" if archived
150
+ return flag if stopped_by.to_s.empty?
151
+
152
+ flag + (stopped_by.to_s == "loop-guard" ? " [looped]" : " [stopped by #{stopped_by}]")
153
+ end
154
+
155
+ def self.desc_of(session)
156
+ desc = [File.basename(session.working_directory.to_s), summary_text(session)].reject(&:empty?).join(" · ")
157
+ cut(desc, DESC_LIMIT)
158
+ end
159
+ private_class_method :desc_of
160
+
161
+ def self.cut(text, limit)
162
+ text.length > limit ? "#{text[0, limit - 1]}…" : text
163
+ end
164
+ private_class_method :cut
165
+
166
+ def short_id = id.to_s[0, 8]
167
+ def parent_short_id = parent_id&.[](0, 8)
168
+ def continues_short_id = continues&.[](0, 8)
169
+
170
+ def running_or_starting?
171
+ self.class.running_or_starting?(saved_status: saved_status, live: live, busy: busy, updated_at: updated_at)
172
+ end
173
+
174
+ # The session as `chi sessions list` prints it, wherever it runs (the
175
+ # plain list, the --live/--cwd text, a remote's): its id, state (a
176
+ # question it waits on, else the displayed status), the context's fill,
177
+ # updated_at, its text (a title, else the recap's auto title or first
178
+ # sentence, else the last prompt; 60 characters), its flags, and the
179
+ # parent it was delegated from (↳) and the link it continues (↪). Reads
180
+ # only row fields, so a remote row prints the same line.
181
+ def cli_line
182
+ state = if relayed_to then "waiting (in parent #{relayed_to})"
183
+ elsif waiting then "waiting"
184
+ else status.to_s
185
+ end
186
+ text = (title ? preview : auto_title || recap || preview).to_s[0, DESC_LIMIT]
187
+ links = parent_short_id ? " ↳ #{parent_short_id}" : ""
188
+ links += " ↪ #{continues_short_id}" if continues_short_id
189
+ ctx = SessionMetrics.context_label(ctx_pct)
190
+ flag = self.class.flags(scratch: scratch, test_run: test_run, archived: archived, stopped_by: stopped_by)
191
+ "#{id} #{state.ljust(8)} #{ctx.ljust(8)} #{updated_at} #{text}#{flag}#{links}"
192
+ end
193
+
194
+ # The summaries route's row: SUMMARY_KEYS, in order.
195
+ # @return [Hash] symbol keys
196
+ def to_summary_h
197
+ SUMMARY_KEYS.to_h { |key| [key, public_send(key)] }
198
+ end
199
+ end
200
+ end
@@ -42,24 +42,27 @@ module Samagotchi
42
42
  working_set_pins: WorkingSetPin.list(working_set_pins))
43
43
  end
44
44
 
45
- def empty?
46
- llm_context.nil? && thinking.nil? && reasoning_history.nil? && appended_prompt_files.empty? && toolkit.nil? &&
47
- toolkit_origin.nil? && settings.empty? && parent_answers.nil? && working_set_pins.empty?
48
- end
45
+ # Every member at its default: none of the session's own settings.
46
+ def empty? = self == self.class.new
49
47
 
50
- # A session's own setup, for a session that starts from it. Its
51
- # toolkit always: one from before toolkits is full (Toolkit.of), not
52
- # whatever config's default is now.
48
+ # A session's own setup, for a session that starts from it: each
49
+ # member is the session's field of the same name (Session.new_session
50
+ # applies them by name), but for SessionSetup::CARRY's. Its toolkit
51
+ # always: one from before toolkits is full (Toolkit.of), not whatever
52
+ # config's default is now.
53
53
  # @param session [Session, nil]
54
54
  # @return [SessionSetup]
55
55
  def self.of(session)
56
56
  return new if session.nil?
57
57
 
58
- new(llm_context: session.llm_context, thinking: session.thinking, reasoning_history: session.reasoning_history,
59
- appended_prompt_files: session.appended_prompt_files, toolkit: Toolkit.of(session.toolkit),
60
- toolkit_origin: "inherited",
61
- settings: session.settings, parent_answers: session.parent_answers,
62
- working_set_pins: session.working_set_pins)
58
+ new(**members.to_h { |member| [member, self::CARRY.fetch(member) { ->(s) { s.public_send(member) } }.call(session)] })
63
59
  end
64
60
  end
61
+
62
+ # How SessionSetup.of carries a member that isn't its session's field as
63
+ # it is: member => (session) -> value.
64
+ SessionSetup::CARRY = {
65
+ toolkit: ->(session) { Toolkit.of(session.toolkit) },
66
+ toolkit_origin: ->(_session) { "inherited" }
67
+ }.freeze
65
68
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require "json"
4
4
  require_relative "session"
5
+ require_relative "session_row"
5
6
  require_relative "session_manager"
6
7
  require_relative "session_retention"
7
8
  require_relative "project_scope"
8
9
  require_relative "analytics_file"
9
10
  require_relative "session_metrics"
10
- require_relative "recap_store"
11
11
  require_relative "session_delete_command"
12
12
  require_relative "session_archive_command"
13
13
  require_relative "worker_link"
@@ -142,28 +142,6 @@ module Samagotchi
142
142
  options.to_h { |key, value| [key, INTEGER_OPTIONS.include?(key) ? Integer(value, 10) : value] }
143
143
  end
144
144
 
145
- # The flags a list row carries: a chi scratch session, a test run, an
146
- # archived one, and the turn's stopper (see #stopped_flag).
147
- def session_flag(scratch:, test_run:, archived:, stopped_by: nil)
148
- flag = if scratch
149
- " [scratch]"
150
- else
151
- (test_run ? " [test]" : "")
152
- end
153
- flag += " [archived]" if archived
154
- flag + stopped_flag(stopped_by)
155
- end
156
-
157
- # The marker for a turn a hook stopped: "[looped]" for loop-guard (the
158
- # bundle's thinking watch, and the word `chi sessions stats` uses for its
159
- # cuts), "[stopped by X]" for any other hook; "" otherwise. A cut with no
160
- # retry left records the same stopped_by.
161
- def stopped_flag(by)
162
- return "" if by.to_s.empty?
163
-
164
- by.to_s == "loop-guard" ? " [looped]" : " [stopped by #{by}]"
165
- end
166
-
167
145
  def list
168
146
  format, scope, cwd = @opts.values_at(:format, :scope, :cwd)
169
147
  unless format.nil? || %w[text json tsv].include?(format)
@@ -175,7 +153,8 @@ module Samagotchi
175
153
  # chi web's; --scope=all, a folder in no repo or an explicit --cwd: every
176
154
  # project's. Machine-local (this cwd's project): remotely every
177
155
  # project's (D4), and --scope=project is --cwd's project there (the
178
- # server finds its root). The rows are the server's (list_summaries).
156
+ # server finds its root). The rows are SessionRows, the server's
157
+ # remotely: one line wherever the list runs (SessionRow#cli_line).
179
158
  # The picker (chi note from a script): filters apply before the limit,
180
159
  # and test runs stay out, unless this is one (SAMAGOTCHI_ENV=test, CI):
181
160
  # then its own sessions are what it looks for. The plain listing shows
@@ -191,10 +170,7 @@ module Samagotchi
191
170
  end
192
171
  project = scope == "all" || cwd ? nil : Samagotchi::ProjectScope.root_for(Dir.pwd)
193
172
  scope_note = ->(count) { "#{count} session(s) in #{File.basename(project)} (--scope=all: every project)" }
194
-
195
- return list_summaries(project, scope_note, include_tests: include_tests) if picker
196
-
197
- list_plain(project, scope_note)
173
+ list_summaries(project, scope_note, include_tests: include_tests)
198
174
  end
199
175
 
200
176
  def list_summaries(project, scope_note, include_tests:)
@@ -207,117 +183,23 @@ module Samagotchi
207
183
  )
208
184
  case format
209
185
  when "json"
210
- # owner: "worker", "tui" (a one-turn chi run: it takes no notes or messages)
211
- # or nil; recap: the saved recap's first sentence, or nil; project:
212
- # its git project's root, or nil
213
- # parent_id: the session that delegated it (the delegate tool), or nil
214
- # delegate: started by the delegate tool (a fork has a parent_id too)
215
- # continues: the session it continues (its chain's previous link), or nil
216
- # archived: hidden from the lists (only with --archived can it be true)
217
- # scratch: a `chi scratch` session (deleted when its terminal leaves,
218
- # a leftover one at the next sweep)
219
- # ctx_pct: how full the context was after the last turn, or nil
220
- # title: the session's own name (/title; desc shows it too), or nil;
221
- # auto_title: the one its recap gave it (RecapTitle), or nil
222
- # waiting: the kind of question it waits on (question, approval,
223
- # hook: chi answer or the web answers it), or nil; waiting_id: its
224
- # id, for chi answer --question; relayed_to: the parent session (short
225
- # id) whose card it also waits in (the approval relay), or nil
226
- keys = %i[id short_id desc title auto_title cwd project updated_at live busy owner recap parent_id continues delegate archived scratch ctx_pct
227
- waiting waiting_id relayed_to stopped_by]
228
- @stdout.puts JSON.generate(summaries.map { |summary| summary.slice(*keys) })
186
+ # SessionRow::JSON_KEYS (its comment says what each one is).
187
+ @stdout.puts JSON.generate(summaries.map { |row| row.to_summary_h.slice(*Samagotchi::SessionRow::JSON_KEYS) })
229
188
  when "tsv"
230
- summaries.each { |summary| @stdout.puts "#{summary[:id]}\t#{summary[:desc]}" }
189
+ summaries.each { |row| @stdout.puts "#{row.id}\t#{row.desc}" }
231
190
  else
232
- summaries.each do |summary|
233
- state = if summary[:relayed_to] then "waiting (in parent #{summary[:relayed_to]})"
234
- elsif summary[:waiting] then "waiting"
235
- elsif summary[:busy] then "running"
236
- else summary[:live] ? "live" : summary[:status].to_s
237
- end
238
- # The recap's auto title, else its first sentence, says more than
239
- # the last prompt (text only: tsv and json keep desc for pickers);
240
- # a title (in desc) says what the user named it.
241
- desc = summary[:desc]
242
- named = summary[:auto_title] || summary[:recap]
243
- if named && !summary[:title]
244
- desc = [File.basename(summary[:cwd].to_s), named].reject(&:empty?).join(" · ")
245
- desc = "#{desc[0, 59]}…" if desc.length > 60
246
- end
247
- # A delegated session points at its parent.
248
- child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
249
- # A chain's link points at the one it continues.
250
- child += " ↪ #{summary[:continues_short_id]}" if summary[:continues_short_id]
251
- flag = session_flag(scratch: summary[:scratch], test_run: summary[:test_run], archived: summary[:archived],
252
- stopped_by: summary[:stopped_by])
253
- ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
254
- @stdout.puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
255
- end
191
+ summaries.each { |row| @stdout.puts row.cli_line }
256
192
  if project
257
193
  @stdout.puts "#{"\n" unless summaries.empty?}#{scope_note.call(summaries.size)}"
194
+ elsif summaries.empty?
195
+ @stdout.puts "No sessions."
258
196
  else
259
- @stdout.puts summaries.empty? ? "No sessions." : "\n#{summaries.size} session(s)"
197
+ @stdout.puts "\n#{summaries.size} session(s) (sort=#{sort || "updated_at"} order=#{order || "desc"})"
260
198
  end
261
199
  end
262
200
  0
263
201
  end
264
202
 
265
- def list_plain(project, scope_note)
266
- sort, order, limit, include_archived = @opts.values_at(:sort, :order, :limit, :include_archived)
267
- sort ||= "updated_at"
268
- order ||= "desc"
269
- sessions = @access.list(sort: sort, order: order, limit: limit, project_root: project, include_archived: include_archived)
270
- # The session's title, else the recap's auto title, else its first
271
- # sentence, else the last prompt (or the first message, before any
272
- # turn), cut as before.
273
- # These rows read each session's folder here (its recap, saved
274
- # context, owner): machine-local, the server's rows remotely (P2's
275
- # list parity).
276
- state_dir = Samagotchi::Session.default_state_dir
277
- list_text = lambda do |s|
278
- card = s.title ? nil : Samagotchi::RecapStore.card(Samagotchi::Session.session_dir(s.id, state_dir: state_dir))
279
- (card&.title || card&.preview || Samagotchi::SessionManager.summary_text(s))[0, 60]
280
- end
281
- # How full the context was after the last turn: "ctx 12%", counted
282
- # against the session's llm_context budget when one is set and smaller
283
- # than the window, as the live meter counts it (one registry for the
284
- # whole table, built when the first row has a saved context). Blank when
285
- # unknown.
286
- budgets = nil
287
- ctx = lambda do |s|
288
- Samagotchi::SessionMetrics.context_label(
289
- Samagotchi::SessionMetrics.saved_context_pct(
290
- Samagotchi::Session.session_dir(s.id, state_dir: state_dir),
291
- budget_tokens: -> { Samagotchi::LLMContextStrategy.session_budget(s, registry: budgets ||= Samagotchi::HostRegistry.new) }
292
- )
293
- )
294
- end
295
- # A delegated session points at its parent: ↳ <parent's short id>.
296
- row = lambda do |s|
297
- flag = session_flag(scratch: s.scratch, test_run: s.test_run, archived: s.archived, stopped_by: s.stopped_by)
298
- child = s.parent_id ? " ↳ #{s.parent_id[0, 8]}" : ""
299
- child += " ↪ #{s.continues[0, 8]}" if s.continues
300
- # A question waits for an answer (chi answer, the web, chi --attach).
301
- live = s.pending_question && worker_owned?(s.id, state_dir)
302
- waiting = s.waiting_question(live: !!live)
303
- status = if waiting&.dig(:relayed_to) then "waiting (in parent #{waiting[:relayed_to]})"
304
- elsif waiting then "waiting"
305
- else s.status
306
- end
307
- "#{s.id} #{status.ljust(8)} #{ctx.call(s).ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
308
- end
309
- if project
310
- sessions.each { |s| @stdout.puts row.call(s) }
311
- @stdout.puts "#{"\n" unless sessions.empty?}#{scope_note.call(sessions.size)}"
312
- elsif sessions.empty?
313
- @stdout.puts "No sessions."
314
- else
315
- sessions.each { |s| @stdout.puts row.call(s) }
316
- @stdout.puts "\n#{sessions.size} session(s) (sort=#{sort} order=#{order})"
317
- end
318
- 0
319
- end
320
-
321
203
  # One session's cost, tokens and progress, for a script or an agent that
322
204
  # drives sessions (chi send --wait): a live worker's GET stats, else the
323
205
  # snapshot its saved analytics.json rebuilds. Never starts a worker, and
@@ -101,7 +101,7 @@ module Samagotchi
101
101
  error_line("#{command_name}: #{e.message}")
102
102
  CLI::Exit::FAILED
103
103
  rescue ArgumentError => e
104
- error_line("#{command_name}: #{e.is_a?(Session::AmbiguousId) ? e.message : e.message.sub(/\ASession not found: /, "no session ")}")
104
+ error_line("#{command_name}: #{e.message}")
105
105
  CLI::Exit::FAILED
106
106
  end
107
107
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require_relative "atomic_file"
7
+ require_relative "session"
8
+
9
+ module Samagotchi
10
+ # A continue's "starting" marker on its new link: written before the
11
+ # continue moves delegates there (ChildMove) and spawns the link's worker,
12
+ # removed by that worker once it holds the owner lock (Worker#start). While
13
+ # it is #fresh?, nothing else spawns a worker for the link (SpawnPart's
14
+ # resume_session and wake_for_report): a ring from a moved child must not
15
+ # start a second one. CROSS-VERSION: the file name and its {pid, at} are
16
+ # read and cleared by other chi versions (spec/worker_contract_spec.rb).
17
+ module StartingMarker
18
+ # <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
19
+ FILE = "starting"
20
+ # A starting marker older than this, or whose process is gone, is left
21
+ # over from a continue that died: it holds nothing back.
22
+ STALE = 60.0
23
+
24
+ module_function
25
+
26
+ # Hold off a worker for the continue's new link (#fresh?) until its
27
+ # own worker holds it: written before the move, removed by that worker
28
+ # once it has the owner lock (Worker#start).
29
+ def mark(session_id, state_dir:)
30
+ dir = Session.session_dir(session_id, state_dir: state_dir)
31
+ FileUtils.mkdir_p(dir)
32
+ AtomicFile.write(File.join(dir, FILE), JSON.generate({ "pid" => Process.pid, "at" => Time.now.iso8601(3) }))
33
+ end
34
+
35
+ def clear(session_id, state_dir:)
36
+ FileUtils.rm_f(File.join(Session.session_dir(session_id, state_dir: state_dir), FILE))
37
+ end
38
+
39
+ # Whether a continue is moving delegates to +session_id+ right now: its
40
+ # marker is there, fresh, and the process that wrote it runs.
41
+ def fresh?(session_id, state_dir:)
42
+ file = File.join(Session.session_dir(session_id, state_dir: state_dir), FILE)
43
+ return false unless File.exist?(file)
44
+
45
+ data = JSON.parse(File.read(file))
46
+ return false if Time.now - Time.iso8601(data["at"].to_s) > STALE
47
+
48
+ process_alive?(data["pid"].to_i)
49
+ rescue JSON::ParserError, ArgumentError, SystemCallError
50
+ false
51
+ end
52
+
53
+ # Signal 0 only asks whether the process exists.
54
+ def process_alive?(pid)
55
+ return false unless pid.positive?
56
+
57
+ Process.kill(0, pid)
58
+ true
59
+ rescue Errno::ESRCH
60
+ false
61
+ rescue Errno::EPERM
62
+ true
63
+ end
64
+ end
65
+ end
@@ -32,27 +32,20 @@ module Samagotchi
32
32
  nil
33
33
  end
34
34
 
35
- # The seconds for +target+ and where they came from
35
+ # The seconds for +lookup+'s model and where they came from
36
36
  # ("SAMAGOTCHI_STEER_CUT_AFTER", "models: qwen", "hosts.splash",
37
37
  # "steer.cut_after"; nil for the default).
38
- # @param target [HostRegistry::ModelTarget]
39
- # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
40
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
38
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
41
39
  # @return [Array(Integer, String|nil)]
42
- def resolve(target, names:, models: nil)
43
- value, origin = Config.get_with_origin("steer.cut_after")
40
+ def resolve(lookup)
41
+ value, origin = lookup.global("steer.cut_after")
44
42
  return [value.to_i, ENV_KEY] if origin == :env
45
43
 
46
- models ||= begin
47
- ConfigFile.model_settings
48
- rescue StandardError
49
- {}
50
- end
51
- setting = ConfigFile.model_setting(names, :steer_cut_after, models: models)
44
+ setting = lookup.model(:steer_cut_after)
52
45
  return [setting.value, "models: #{setting.key}"] if setting
53
46
 
54
- host = target.entry.steer_cut_after
55
- return [host, "hosts.#{target.entry.name}"] unless host.nil?
47
+ host = lookup.host(:steer_cut_after)
48
+ return [host, "hosts.#{lookup.host_name}"] unless host.nil?
56
49
 
57
50
  return [value.to_i, "steer.cut_after"] if origin == :file
58
51