samagotchi 0.155.0 → 0.156.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/bin/chi +1 -1
  4. data/docs/architecture.md +10 -6
  5. data/docs/internals/anthropic.md +1 -1
  6. data/docs/internals/attached-context.md +3 -2
  7. data/docs/internals/background-tasks.md +1 -1
  8. data/docs/internals/engine.md +625 -935
  9. data/docs/internals/llm-context-forget.md +8 -3
  10. data/docs/internals/objectives.md +2 -2
  11. data/docs/internals/prompt-caching.md +4 -1
  12. data/docs/internals/remote-sessions.md +3 -1
  13. data/docs/internals/working-set.md +1 -1
  14. data/docs/sessions.md +2 -2
  15. data/docs/system-one.md +3 -2
  16. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  17. data/lib/samagotchi/broadcast_command.rb +3 -3
  18. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  19. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  20. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  21. data/lib/samagotchi/child_move.rb +7 -51
  22. data/lib/samagotchi/children_status.rb +1 -1
  23. data/lib/samagotchi/cli/parent_wait.rb +1 -1
  24. data/lib/samagotchi/config.rb +17 -112
  25. data/lib/samagotchi/context_command.rb +1 -2
  26. data/lib/samagotchi/context_window.rb +3 -3
  27. data/lib/samagotchi/desktop/macos.rb +1 -1
  28. data/lib/samagotchi/engine/context_judge.rb +19 -11
  29. data/lib/samagotchi/engine/messages.rb +1 -1
  30. data/lib/samagotchi/engine/models.rb +5 -2
  31. data/lib/samagotchi/engine/objectives.rb +29 -11
  32. data/lib/samagotchi/engine/side_requests.rb +21 -11
  33. data/lib/samagotchi/engine/toolkit.rb +10 -9
  34. data/lib/samagotchi/engine/turn_end.rb +1 -1
  35. data/lib/samagotchi/engine/turn_helpers.rb +7 -7
  36. data/lib/samagotchi/engine/verify.rb +1 -1
  37. data/lib/samagotchi/engine.rb +14 -9
  38. data/lib/samagotchi/guardrails/targets.rb +24 -9
  39. data/lib/samagotchi/host_config.rb +72 -1
  40. data/lib/samagotchi/host_model.rb +2 -6
  41. data/lib/samagotchi/host_registry.rb +1 -1
  42. data/lib/samagotchi/kernel_loop.rb +46 -121
  43. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  44. data/lib/samagotchi/llm/chat_loop.rb +34 -18
  45. data/lib/samagotchi/llm/native_backend.rb +1 -1
  46. data/lib/samagotchi/llm_context_apply.rb +54 -1
  47. data/lib/samagotchi/llm_context_notice.rb +1 -1
  48. data/lib/samagotchi/llm_context_strategy.rb +7 -9
  49. data/lib/samagotchi/llm_context_view.rb +9 -0
  50. data/lib/samagotchi/model_price.rb +4 -10
  51. data/lib/samagotchi/model_profile.rb +6 -6
  52. data/lib/samagotchi/model_settings.rb +37 -2
  53. data/lib/samagotchi/note_command.rb +1 -2
  54. data/lib/samagotchi/objectives_audit.rb +4 -3
  55. data/lib/samagotchi/participants.rb +2 -2
  56. data/lib/samagotchi/plugin/context.rb +1 -1
  57. data/lib/samagotchi/plugin/sessions.rb +2 -5
  58. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  59. data/lib/samagotchi/project_env.rb +1 -1
  60. data/lib/samagotchi/reasoning_history.rb +3 -3
  61. data/lib/samagotchi/reminder_carry.rb +1 -1
  62. data/lib/samagotchi/reprefill_meter.rb +45 -0
  63. data/lib/samagotchi/review_command.rb +1 -1
  64. data/lib/samagotchi/sampling_settings.rb +3 -3
  65. data/lib/samagotchi/self_report.rb +7 -7
  66. data/lib/samagotchi/session.rb +50 -7
  67. data/lib/samagotchi/session_access/local.rb +7 -5
  68. data/lib/samagotchi/session_access/remote.rb +17 -3
  69. data/lib/samagotchi/session_archive_command.rb +2 -2
  70. data/lib/samagotchi/session_chain.rb +7 -6
  71. data/lib/samagotchi/session_continue.rb +6 -5
  72. data/lib/samagotchi/session_delete_command.rb +1 -1
  73. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  74. data/lib/samagotchi/session_manager/emptiness.rb +138 -0
  75. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  76. data/lib/samagotchi/session_manager/listing.rb +189 -0
  77. data/lib/samagotchi/session_manager/removal.rb +190 -0
  78. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  79. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  80. data/lib/samagotchi/session_manager.rb +26 -1540
  81. data/lib/samagotchi/session_metrics.rb +1 -1
  82. data/lib/samagotchi/sessions_command.rb +1 -1
  83. data/lib/samagotchi/share_command.rb +1 -1
  84. data/lib/samagotchi/starting_marker.rb +65 -0
  85. data/lib/samagotchi/steer_cut_after.rb +3 -3
  86. data/lib/samagotchi/system_one/recorder.rb +49 -0
  87. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  88. data/lib/samagotchi/terminal_ui.rb +2 -6
  89. data/lib/samagotchi/testing/sessions.rb +1 -1
  90. data/lib/samagotchi/thinking.rb +3 -3
  91. data/lib/samagotchi/token_usage.rb +1 -1
  92. data/lib/samagotchi/tool_activity.rb +10 -4
  93. data/lib/samagotchi/tool_declarations.rb +2 -2
  94. data/lib/samagotchi/tool_output_cut.rb +17 -0
  95. data/lib/samagotchi/tool_runner.rb +7 -4
  96. data/lib/samagotchi/toolkit_command.rb +1 -1
  97. data/lib/samagotchi/tools/delegate.rb +10 -5
  98. data/lib/samagotchi/tools/delegate_result.rb +4 -4
  99. data/lib/samagotchi/tools/index_hint.rb +14 -0
  100. data/lib/samagotchi/tools/registry.rb +28 -11
  101. data/lib/samagotchi/tools/send_note.rb +4 -6
  102. data/lib/samagotchi/tools/tool_index.rb +52 -51
  103. data/lib/samagotchi/tools/tool_surface.rb +22 -8
  104. data/lib/samagotchi/turn_context.rb +2 -1
  105. data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
  106. data/lib/samagotchi/turn_stages/objectives.rb +56 -52
  107. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
  108. data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
  109. data/lib/samagotchi/turn_stages/warmup.rb +14 -15
  110. data/lib/samagotchi/version.rb +1 -1
  111. data/lib/samagotchi/vision_support.rb +3 -3
  112. data/lib/samagotchi/web/app.rb +15 -8
  113. data/lib/samagotchi/web/participant_access.rb +2 -2
  114. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  115. data/lib/samagotchi/web/session_control.rb +3 -3
  116. data/lib/samagotchi/web/session_create.rb +3 -3
  117. data/lib/samagotchi/web/session_hub.rb +1 -1
  118. data/lib/samagotchi/web/session_show.rb +3 -3
  119. data/lib/samagotchi/web/session_summary.rb +3 -4
  120. data/lib/samagotchi/worker.rb +55 -27
  121. data/lib/samagotchi/worker_boot.rb +184 -0
  122. data/lib/samagotchi/worker_signals.rb +1 -1
  123. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  124. metadata +13 -1
@@ -176,8 +176,8 @@ module Samagotchi
176
176
  # @return [String] +model_name+
177
177
  def self.check_host!(model_name, env: ENV, hosts: nil)
178
178
  require_relative "config"
179
- hosts ||= Samagotchi::ConfigFile.hosts_config(env: env)
180
- parsed = Samagotchi::ConfigFile.model_ref(model_name, env: env, hosts: hosts)
179
+ hosts ||= Samagotchi::HostConfig.all(env: env)
180
+ parsed = Samagotchi::HostConfig.model_ref(model_name, env: env, hosts: hosts)
181
181
  if (other = parsed.host_conflict)
182
182
  raise UnknownHost, "alias '#{parsed.alias_name}' names host '#{other}', not '#{parsed.host_name}'; " \
183
183
  "use #{parsed.alias_name} or #{other}:#{parsed.alias_name}"
@@ -234,9 +234,9 @@ module Samagotchi
234
234
  def self.model_warning(model_name, env: ENV, hosts: nil, lists: nil, relist: nil, now: Time.now.to_i)
235
235
  require_relative "config"
236
236
  require_relative "model_list_store"
237
- hosts ||= Samagotchi::ConfigFile.hosts_config(env: env)
237
+ hosts ||= Samagotchi::HostConfig.all(env: env)
238
238
  lists ||= Samagotchi::ModelListStore
239
- parsed = Samagotchi::ConfigFile.model_ref(model_name, env: env, hosts: hosts)
239
+ parsed = Samagotchi::HostConfig.model_ref(model_name, env: env, hosts: hosts)
240
240
  host = parsed.host_name
241
241
  return nil unless host
242
242
  # Declared under hosts.<name>.models: served whatever the host lists.
@@ -323,7 +323,7 @@ module Samagotchi
323
323
  prefix = prefix.strip.downcase
324
324
  return nil if hosts.keys.any? { |k| k.to_s.downcase == prefix }
325
325
 
326
- Samagotchi::ConfigFile.disabled_host_names(env: env).include?(prefix) ? prefix : nil
326
+ Samagotchi::HostConfig.disabled_names(env: env).include?(prefix) ? prefix : nil
327
327
  end
328
328
 
329
329
  # "box/x" named host box until '/' stopped naming a host: a saved
@@ -437,7 +437,7 @@ module Samagotchi
437
437
  def self.resolve(names:, entry:, client:, bare_model:, override: nil, models: nil)
438
438
  require_relative "config"
439
439
  override ||= Samagotchi::Config.get_with_origin("model.profile")
440
- models ||= Samagotchi::ConfigFile.model_settings
440
+ models ||= Samagotchi::ModelSettings.all
441
441
  names = Array(names).map { |n| n.to_s.strip }.reject(&:empty?).uniq
442
442
 
443
443
  value, origin = override
@@ -6,7 +6,7 @@ require_relative "reasoning_history"
6
6
 
7
7
  module Samagotchi
8
8
  # One config.yml models: entry (per-model settings keyed by model id or
9
- # alias), parsed and checked; ConfigFile.model_settings holds them by
9
+ # alias), parsed and checked; ModelSettings.all holds them by
10
10
  # downcased key. Each field is nil when unset: profile kept as written
11
11
  # (downcased; ModelProfile.resolve validates it), vision true/false,
12
12
  # sampling a request-field map, thinking a level, reasoning_history a
@@ -21,6 +21,41 @@ module Samagotchi
21
21
  super(**members.to_h { |m| [m, nil] }, **fields)
22
22
  end
23
23
 
24
+ # The top-level `models:` map: per-model settings keyed by model id or
25
+ # alias (downcased), e.g. `models: {my-alias: {profile: qwen36}}`, each a
26
+ # ModelSettings; an entry that is not a map is skipped.
27
+ # @return [Hash{String => ModelSettings}]
28
+ def self.all(env: ENV, path: ConfigFile.global_path(env: env))
29
+ data = ConfigFile.read_yaml(env: env, path: path)
30
+ raw = data[ConfigFile::MODELS_KEY] if data.is_a?(Hash)
31
+ return {} unless raw.is_a?(Hash)
32
+
33
+ raw.each_with_object({}) do |(k, v), result|
34
+ key = k.to_s.strip.downcase
35
+ settings = parse(key, v)
36
+ result[key] = settings if settings
37
+ end
38
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
39
+ {}
40
+ end
41
+
42
+ # The first +names+ (model as typed, alias-resolved, bare, …) whose
43
+ # models: entry sets +field+ (a ModelSettings member; another name
44
+ # raises ArgumentError): a ModelSetting (key, value), or nil.
45
+ # VisionSupport and SamplingSettings look a model up the same way.
46
+ # @param models [Hash{String => ModelSettings}, nil] .all by default
47
+ # @return [ModelSetting, nil]
48
+ def self.find(names, field, models: nil)
49
+ raise ArgumentError, "unknown models: field #{field.inspect}" unless members.include?(field)
50
+
51
+ models ||= all
52
+ Array(names).map { |name| name.to_s.strip.downcase }.reject(&:empty?).uniq.each do |key|
53
+ value = models[key]&.public_send(field)
54
+ return ModelSetting.new(key: key, value: value) unless value.nil?
55
+ end
56
+ nil
57
+ end
58
+
24
59
  # A models: entry as written, or nil when it is not a map (or the key is
25
60
  # blank). A bad value warns, naming the entry, and is left unset.
26
61
  # @param key [String] the downcased model id or alias
@@ -45,7 +80,7 @@ module Samagotchi
45
80
  end
46
81
  end
47
82
 
48
- # One field ConfigFile.model_setting found: the models: key that set it
83
+ # One field ModelSettings.find found: the models: key that set it
49
84
  # and its value.
50
85
  ModelSetting = Data.define(:key, :value)
51
86
  end
@@ -109,8 +109,7 @@ module Samagotchi
109
109
  def resolve(given)
110
110
  @access.resolve(given)
111
111
  rescue ArgumentError => e
112
- message = e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"
113
- error_line("chi note: #{message}")
112
+ error_line("chi note: #{e.message}")
114
113
  nil
115
114
  end
116
115
 
@@ -40,9 +40,10 @@ module Samagotchi
40
40
  # Whether this use is on: config.yml, read at each ask.
41
41
  def self.on? = Config.get("objectives.system_one").to_s.strip.downcase == MODE
42
42
 
43
- # @param client_for [#call] (judgment, session) → a recording client, nil with no host or no saving
43
+ # @param client_for [#call] (judgment, session) → a recording client, nil with no host or no saving;
44
+ # asked only once an active objective is there
44
45
  # @param side [SideJudgments]
45
- # @param state_dir [#call] → the sessions folder
46
+ # @param state_dir [String] the sessions folder
46
47
  def initialize(client_for:, side:, state_dir:)
47
48
  @client_for = client_for
48
49
  @side = side
@@ -82,7 +83,7 @@ module Samagotchi
82
83
  def ask(judgment, session, message)
83
84
  return if session.nil? || session.id.nil? || session.scratch || session.review?
84
85
 
85
- items = Objectives.current(session.id, session.messages, state_dir: @state_dir.call).items
86
+ items = Objectives.current(session.id, session.messages, state_dir: @state_dir).items
86
87
  .select { |item| item.status == Objectives::ACTIVE }.first(MAX_OBJECTIVES)
87
88
  return if items.empty?
88
89
 
@@ -106,10 +106,10 @@ module Samagotchi
106
106
  # @return [Grants::Issued]
107
107
  # @raise [Grants::Invalid] an archived session, or bad values
108
108
  # (Unbindable: an identity without such a public_url)
109
- # @raise [ArgumentError] no such session
109
+ # @raise [Session::NotFound] no such session
110
110
  def invite(grants, session_id:, name:, kind: "human", ttl: nil, cap: nil, identity: nil, public_url: nil,
111
111
  mode: nil, state_dir: Session.default_state_dir)
112
- raise ArgumentError, "Session not found: #{session_id}" unless File.exist?(Session.session_file(session_id, state_dir: state_dir))
112
+ raise Session::NotFound, session_id unless File.exist?(Session.session_file(session_id, state_dir: state_dir))
113
113
 
114
114
  identity = Grants.clean_identity(identity) unless identity.nil?
115
115
  raise Grants::Unbindable if !identity.nil? && public_url&.identity_header.nil?
@@ -182,7 +182,7 @@ module Samagotchi
182
182
  def model_key = @host.model_key&.call
183
183
 
184
184
  # The names the session's model is looked up under now, as chi's own
185
- # per-model config is (models: maps, ConfigFile.model_setting): as
185
+ # per-model config is (models: maps, ModelSettings.find): as
186
186
  # typed (an alias), the alias's target, the resolved ref ("host:id"),
187
187
  # without the host, the bare id the host serves; deduped, first the
188
188
  # most specific. Compare them downcased, first hit, as chi does.
@@ -212,11 +212,8 @@ module Samagotchi
212
212
  end
213
213
 
214
214
  def resolve(id, state_dir)
215
- sid = Session.resolve_id(id.to_s.strip, state_dir: state_dir)
216
- raise Error, "no session #{id}" unless sid && Session.exist?(sid, state_dir: state_dir)
217
-
218
- sid
219
- rescue Session::AmbiguousId, ArgumentError => e
215
+ Session.resolve!(id.to_s.strip, state_dir: state_dir)
216
+ rescue ArgumentError => e
220
217
  raise Error, e.message
221
218
  end
222
219
 
@@ -136,19 +136,21 @@ module Samagotchi
136
136
  def [](key) = (public_send(key) if members.include?(key))
137
137
 
138
138
  # Registry#register's keywords for it: its block wrapped as a handler
139
- # (typed args, the Context), preview, targets, refs.
139
+ # (typed args, the Context), preview, targets (in the entry's form,
140
+ # (call, surface): a plugin's reads the call only), refs (call only).
140
141
  def entry_fields(context)
141
142
  block = self.block
142
143
  parameters = schema[:parameters]
143
144
  preview = per_call(self.preview, parameters)
145
+ targets = per_call(self.targets, parameters)
144
146
  {
145
147
  schema: schema, label: label,
146
148
  handler: lambda { |call, _kctx|
147
149
  result = block.call(ToolSpec.args_of(call, parameters), context)
148
150
  ToolSpec.result_text(result)
149
151
  },
150
- preview: preview && ->(call) { preview.call(call)&.to_s },
151
- targets: per_call(targets, parameters),
152
+ preview: preview && ->(call, _surface = nil) { preview.call(call)&.to_s },
153
+ targets: targets && ->(call, _surface = nil) { targets.call(call) },
152
154
  refs: per_call(refs, parameters)
153
155
  }
154
156
  end
@@ -107,7 +107,7 @@ module Samagotchi
107
107
  # @return [Overlay]
108
108
  def current = @current
109
109
 
110
- # Only from SessionManager.run_session_loop, in a spawned worker.
110
+ # Only from WorkerBoot.run_session_loop, in a spawned worker.
111
111
  def install!(overlay) = @current = overlay
112
112
 
113
113
  # #current's env for a tool's spawn, less what every tool runs without
@@ -68,7 +68,7 @@ module Samagotchi
68
68
  # the default).
69
69
  # @param target [HostRegistry::ModelTarget]
70
70
  # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
71
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
71
+ # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
72
72
  # @param session [Symbol, nil] the session's own level (Session#reasoning_history)
73
73
  # @return [Array(Symbol, String|nil)]
74
74
  def resolve(target, names:, models: nil, session: nil)
@@ -78,11 +78,11 @@ module Samagotchi
78
78
  return [env, ENV_NAME] if env
79
79
 
80
80
  models ||= begin
81
- ConfigFile.model_settings
81
+ ModelSettings.all
82
82
  rescue StandardError
83
83
  {}
84
84
  end
85
- setting = ConfigFile.model_setting(names, :reasoning_history, models: models)
85
+ setting = ModelSettings.find(names, :reasoning_history, models: models)
86
86
  return [setting.value, "models: #{setting.key}"] if setting
87
87
 
88
88
  host = target.entry&.reasoning_history
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  # restart writes one: a stop, an idle exit, a crash and a fallback restart
16
16
  # (a TERM) drop the reminders as before, and a stop or an archive removes a
17
17
  # file left behind (SessionManager.stop_session, .archive_session); a
18
- # restart a stop landed on starts no successor (.run_session_loop). A file is honoured only by the same
18
+ # restart a stop landed on starts no successor (WorkerBoot.run_session_loop). A file is honoured only by the same
19
19
  # session, within WINDOW of its writing (a successor starts in seconds), so
20
20
  # one no successor read can't bring reminders back a day later.
21
21
  module ReminderCarry
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "token_usage"
4
+
5
+ module Samagotchi
6
+ # A request's re-prefilled tokens, for both loops: of the prompt the last
7
+ # request on the same model through the same client (the host) sent, what
8
+ # the server didn't reuse from its cache this time (that prompt less the
9
+ # cached count), so an LLM context edit's cache break shows, and so does
10
+ # anything else that rewrote the prompt's middle or lost the cache.
11
+ #
12
+ # One per Engine, given to its KernelLoop and its ChatLoop, so the
13
+ # baseline is the last request's whichever loop sent it: a switch to
14
+ # another host and back starts over on the way back. A bare loop (a spec,
15
+ # a bench) builds its own. Read and written on the turn's thread only
16
+ # (each loop's completed generation; the turn-end warm-up's #reset!).
17
+ class ReprefillMeter
18
+ # The last counted request: its model, its client, its prompt tokens.
19
+ Baseline = Data.define(:model, :client, :prompt)
20
+
21
+ # The tokens the server prefilled again for this request (+prompt+ and
22
+ # +cached+, its counts), and this request becomes the baseline. nil
23
+ # under TokenUsage::REPREFILL_MIN_TOKENS (a cache block's rounding), for
24
+ # a server that reports no cached count (or 0: a cold cache isn't told
25
+ # from no cache), the first request, a model switch (another bare model,
26
+ # or the same one on another host's +client+: SessionMetrics counts that
27
+ # prefill as the switch's), and the first request after #reset!.
28
+ # @return [Integer, nil]
29
+ def tokens(model, client:, prompt:, cached:)
30
+ previous = @baseline
31
+ @baseline = Baseline.new(model: model, client: client, prompt: prompt.to_i) if prompt.to_i.positive?
32
+ return nil unless previous && previous.model == model && previous.client.equal?(client) && cached.to_i.positive?
33
+
34
+ tokens = previous.prompt - cached.to_i
35
+ tokens >= TokenUsage::REPREFILL_MIN_TOKENS ? tokens : nil
36
+ end
37
+
38
+ # A turn-end warm-up prefilled the next prompt off the clock, so the
39
+ # next request's cache counts are against it, not the last request:
40
+ # start over (KernelLoop#warmed_up!).
41
+ def reset!
42
+ @baseline = nil
43
+ end
44
+ end
45
+ end
@@ -64,7 +64,7 @@ module Samagotchi
64
64
  rescue ModelProfile::MissingModel, Toolkit::Refused => e
65
65
  refused("could not start: #{e.message}")
66
66
  rescue ArgumentError => e
67
- refused(e.is_a?(Session::AmbiguousId) ? e.message : e.message.sub(/\ASession not found: /, "no session "))
67
+ refused(e.message)
68
68
  end
69
69
 
70
70
  private
@@ -13,7 +13,7 @@ module Samagotchi
13
13
 
14
14
  # @param target [HostRegistry::ModelTarget]
15
15
  # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
16
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
16
+ # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
17
17
  # @return [Hash] frozen, symbol keys; may be empty
18
18
  def self.for(target, names:, models: nil)
19
19
  resolve(target, names, models).first
@@ -39,12 +39,12 @@ module Samagotchi
39
39
 
40
40
  def self.resolve(target, names, models)
41
41
  models ||= begin
42
- ConfigFile.model_settings
42
+ ModelSettings.all
43
43
  rescue StandardError
44
44
  {}
45
45
  end
46
46
  host = target.entry.sampling || EMPTY
47
- model = ConfigFile.model_setting(names, :sampling, models: models)
47
+ model = ModelSettings.find(names, :sampling, models: models)
48
48
  sources = []
49
49
  sources << "hosts.#{target.entry.name}" unless host.empty?
50
50
  sources << "models: #{model.key}" if model
@@ -184,12 +184,12 @@ module Samagotchi
184
184
  registry = HostRegistry.new(env: env)
185
185
  target = registry.resolve(model)
186
186
  names = registry.lookup_names(model, target: target)
187
- models = ConfigFile.model_settings(env: env)
187
+ models = ModelSettings.all(env: env)
188
188
  window = ContextWindow.setting(target, names: names, models: models)
189
189
  return nil unless window
190
190
  return [window.tokens, "hosts.#{target.entry.name}"] if window.source == :host_setting
191
191
 
192
- [window.tokens, "models: #{ConfigFile.model_setting(names, :window_tokens, models: models).key}"]
192
+ [window.tokens, "models: #{ModelSettings.find(names, :window_tokens, models: models).key}"]
193
193
  end
194
194
  end
195
195
 
@@ -224,7 +224,7 @@ module Samagotchi
224
224
  Degrade.call(:config, "model_ref_unresolved", fallback: false) do
225
225
  return true if one == other
226
226
 
227
- ConfigFile.model_ref(one, env: env).ref == ConfigFile.model_ref(other, env: env).ref
227
+ HostConfig.model_ref(one, env: env).ref == HostConfig.model_ref(other, env: env).ref
228
228
  end
229
229
  end
230
230
 
@@ -305,7 +305,7 @@ module Samagotchi
305
305
  # it resolves to (an alias applied, ModelRef), as `chi models` names it.
306
306
  def model_ref_name(env: ENV)
307
307
  name = model_name
308
- name && ConfigFile.model_ref(name, env: env).ref
308
+ name && HostConfig.model_ref(name, env: env).ref
309
309
  end
310
310
 
311
311
  # Routed without model discovery, so an unqualified name may land on
@@ -383,7 +383,7 @@ module Samagotchi
383
383
  names = registry.lookup_names(model)
384
384
  override = Config.resolve_with_origin("model.profile", env: env, cli_overrides: Config.cli_overrides)
385
385
  result = ModelProfile.resolve(names: names, entry: entry, client: nil, bare_model: bare, override: override,
386
- models: ConfigFile.model_settings(env: env))
386
+ models: ModelSettings.all(env: env))
387
387
  name = result.profile.name
388
388
  case result.source
389
389
  when :config then "#{name} (config #{result.detail})"
@@ -401,7 +401,7 @@ module Samagotchi
401
401
  target = registry.resolve(model)
402
402
  session = session_model(env) ? Thinking.session_level(env[SESSION_THINKING_ENV]) : nil
403
403
  Thinking.explain(target, names: registry.lookup_names(model, target: target),
404
- models: ConfigFile.model_settings(env: env), session: session).label
404
+ models: ModelSettings.all(env: env), session: session).label
405
405
  rescue StandardError => e
406
406
  "(unknown: #{e.message})"
407
407
  end
@@ -417,7 +417,7 @@ module Samagotchi
417
417
 
418
418
  session = session_model(env) ? ReasoningHistory.session_level(env[SESSION_REASONING_HISTORY_ENV]) : nil
419
419
  level, source = ReasoningHistory.resolve(target, names: registry.lookup_names(model, target: target),
420
- models: ConfigFile.model_settings(env: env), session: session)
420
+ models: ModelSettings.all(env: env), session: session)
421
421
  ReasoningHistory.label(level, source)
422
422
  rescue StandardError => e
423
423
  "(unknown: #{e.message})"
@@ -119,7 +119,7 @@ module Samagotchi
119
119
  # Started by the `delegate` tool (a fork has a parent_id too, but isn't
120
120
  # one): see #delegate?.
121
121
  attr_writer :delegate
122
- # The session this one continues (SessionManager.continue_session): the
122
+ # The session this one continues (SessionContinue.run): the
123
123
  # previous link of a chain, "the next run of the same routine", else
124
124
  # nil. Not a parent: archiving one link leaves the next alone. The chain
125
125
  # itself is derived (SessionChain), never stored.
@@ -353,7 +353,7 @@ module Samagotchi
353
353
  attr_accessor :model_warning
354
354
 
355
355
  # The delegates a continue moved to this new link (ChildMove;
356
- # SessionManager.continue_session sets it); not saved.
356
+ # SessionContinue.run sets it); not saved.
357
357
  attr_accessor :moved_children
358
358
 
359
359
  # The attached context a continue took to this new link
@@ -361,7 +361,7 @@ module Samagotchi
361
361
  attr_accessor :moved_context
362
362
 
363
363
  # The text web `continue →` prefills in this new link
364
- # (SessionChain.opener_for; SessionManager.continue_session sets it); not
364
+ # (SessionChain.opener_for; SessionContinue.run sets it); not
365
365
  # saved.
366
366
  attr_accessor :opener
367
367
 
@@ -452,6 +452,20 @@ module Samagotchi
452
452
  @preloaded_memory_names.include?(DELEGATE_MEMORY)
453
453
  end
454
454
 
455
+ # The turn state (idle/running/…) the lists show: the saved status,
456
+ # except a "running" with no live +owner+ (an OwnerLock::Owner, or nil):
457
+ # the worker that saved it died mid-turn, so nothing runs. The one rule
458
+ # behind chi web's rows and pages (Web::SessionSummary.displayed_status,
459
+ # which prefers a live worker's snapshot) and `chi sessions list`
460
+ # (SessionManager.session_summaries and the plain table).
461
+ # @param owner [OwnerLock::Owner, nil] the session's owner now
462
+ # @return [String]
463
+ def displayed_status(owner:)
464
+ return status unless status == STATUS_RUNNING
465
+
466
+ owner ? status : STATUS_IDLE
467
+ end
468
+
455
469
  # The question this session waits on, as the lists show it: {id:, kind:}
456
470
  # with kind "question" (the model's), "approval", "hook" or "continue"
457
471
  # (the step-limit question, asked between turns), +relayed_to+ and
@@ -559,9 +573,12 @@ module Samagotchi
559
573
  valid_id?(session_id) && File.exist?(session_path(session_id, state_dir: state_dir))
560
574
  end
561
575
 
576
+ # @raise [NotFound] no such session (an id that isn't one too)
562
577
  def self.load(session_id, state_dir: default_state_dir)
578
+ raise NotFound, session_id unless valid_id?(session_id)
579
+
563
580
  path = session_path(session_id, state_dir: state_dir)
564
- raise ArgumentError, "Session not found: #{session_id}" unless File.exist?(path)
581
+ raise NotFound, session_id unless File.exist?(path)
565
582
 
566
583
  session = from_h(JSON.parse(File.read(path)))
567
584
  session.status = STATUS_STOPPED if stopped_marker?(session.id, state_dir: state_dir)
@@ -667,8 +684,34 @@ module Samagotchi
667
684
  end
668
685
  end
669
686
 
687
+ # What every surface says when an id (or a prefix) names no session:
688
+ # this class owns the words, callers add their context around
689
+ # #message. +given+ is what was asked for, as given.
690
+ class NotFound < ArgumentError
691
+ attr_reader :given
692
+
693
+ def initialize(given)
694
+ @given = given.to_s
695
+ super("no session #{@given[0, 80]}")
696
+ end
697
+ end
698
+
699
+ # The full id of an existing session that +given+ (an id or a unique
700
+ # prefix) names: the one lookup the commands, SessionAccess::Local, chi
701
+ # web's routes and the tools share.
702
+ # @raise [NotFound] none (an id that isn't one too)
703
+ # @raise [AmbiguousId] a prefix of several
704
+ def self.resolve!(given, state_dir: default_state_dir)
705
+ raise NotFound, given unless valid_id?(given.to_s)
706
+
707
+ id = resolve_id(given, state_dir: state_dir)
708
+ raise NotFound, given unless exist?(id, state_dir: state_dir)
709
+
710
+ id
711
+ end
712
+
670
713
  # A session id or a unique prefix of one (like git's): the full id. An
671
- # unknown one comes back as is, for the caller's own "not found"; one that
714
+ # unknown one comes back as is (resolve! raises NotFound for it); one that
672
715
  # names several sessions raises AmbiguousId, which lists them.
673
716
  def self.resolve_id(id_or_prefix, state_dir: default_state_dir)
674
717
  id = id_or_prefix.to_s
@@ -832,9 +875,9 @@ module Samagotchi
832
875
 
833
876
  # Mark a session as stopped (STOPPED_FILE); its session file stays as
834
877
  # it is.
835
- # @raise [ArgumentError] when there is no such session
878
+ # @raise [NotFound] when there is no such session
836
879
  def self.mark_stopped(session_id, state_dir: default_state_dir)
837
- raise ArgumentError, "Session not found: #{session_id}" unless exist?(session_id, state_dir: state_dir)
880
+ raise NotFound, session_id unless exist?(session_id, state_dir: state_dir)
838
881
 
839
882
  dir = session_dir(session_id, state_dir: state_dir)
840
883
  FileUtils.mkdir_p(dir)
@@ -33,6 +33,8 @@ module Samagotchi
33
33
  # Loaded on first use: SessionManager and its ~80 files only when a
34
34
  # command reaches it (chi context never does).
35
35
  autoload :SessionManager, File.expand_path("../session_manager", __dir__)
36
+ # A continue (#create with continues); it requires session_manager.
37
+ autoload :SessionContinue, File.expand_path("../session_continue", __dir__)
36
38
 
37
39
  module SessionAccess
38
40
  # This machine's sessions: the calls the session commands made before
@@ -63,11 +65,11 @@ module Samagotchi
63
65
  # @raise [Session::AmbiguousId]
64
66
  def resolve_id(given) = Session.resolve_id(given, **dir)
65
67
 
66
- # The full id of +given+, of a session that exists.
67
- # @raise [ArgumentError] no such session (Session::AmbiguousId: a
68
+ # The full id of +given+, of a session that exists (and loads).
69
+ # @raise [Session::NotFound] no such session (Session::AmbiguousId: a
68
70
  # prefix of several)
69
71
  def resolve(given)
70
- id = resolve_id(given)
72
+ id = Session.resolve!(given, **dir)
71
73
  show(id)
72
74
  id
73
75
  end
@@ -140,7 +142,7 @@ module Samagotchi
140
142
 
141
143
  start = spec.preview ? { prompt: spec.prompt, preview: spec.preview } : { prompt: spec.prompt }
142
144
  if spec.continues
143
- return SessionManager.continue_session(spec.continues, **start, model: spec.model_name, **dir)
145
+ return SessionContinue.run(spec.continues, **start, model: spec.model_name, **dir)
144
146
  end
145
147
 
146
148
  SessionManager.spawn_session(**start, working_directory: spec.working_directory, model_name: spec.model_name,
@@ -269,7 +271,7 @@ module Samagotchi
269
271
  # @return [Boolean] whether the worker released the session
270
272
  def stop(id, wait:) = SessionManager.stop_session(id, wait: wait, **dir)
271
273
 
272
- # @return [SessionManager::RestartResult]
274
+ # @return [SessionManager::Restarted]
273
275
  def restart(id) = SessionManager.restart_session(id, **dir)
274
276
 
275
277
  # @return [SessionManager::ArchiveResult]
@@ -137,11 +137,11 @@ module Samagotchi
137
137
  end
138
138
  end
139
139
 
140
- # @raise [ArgumentError] no such session (Session::AmbiguousId: a
140
+ # @raise [Session::NotFound] no such session (Session::AmbiguousId: a
141
141
  # prefix of several)
142
142
  def resolve(given)
143
143
  id = resolve_id(given)
144
- raise ArgumentError, "Session not found: #{given}" if id == given.to_s && !exists?(id)
144
+ raise Session::NotFound, given if id == given.to_s && !exists?(id)
145
145
 
146
146
  id
147
147
  end
@@ -704,6 +704,20 @@ module Samagotchi
704
704
  reply.json["detail"].to_s.start_with?("no installed bundle resolves")
705
705
  end
706
706
 
707
+ # chi web's 404 not_found detail for a session it doesn't have:
708
+ # "no session X" (Session::NotFound), or "Session not found: X" from
709
+ # chi web 0.155.0 and older (compat N-1: the same line either way).
710
+ SESSION_NOT_FOUND = /\A(?:no session |Session not found: )(.+)\z/m
711
+
712
+ # Session::NotFound for a missing session's 404, else an
713
+ # ArgumentError with chi web's words (another route's 404). chi web
714
+ # never answers Session::NONE_YET ("no session yet") as a 404
715
+ # not_found; were it to, the pattern would read it as NotFound("yet").
716
+ def not_found_error(detail)
717
+ given = detail[SESSION_NOT_FOUND, 1]
718
+ given ? Session::NotFound.new(given) : ArgumentError.new(detail)
719
+ end
720
+
707
721
  # Raises what Local's callee raises for this refusal (chi web sends
708
722
  # the fields each class is built from; see docs/architecture.md, chi
709
723
  # web's routes for the session commands), else RemoteFailed.
@@ -714,7 +728,7 @@ module Samagotchi
714
728
  sid = json["session_id"]
715
729
  reason = json["reason"]&.to_sym
716
730
  case error
717
- when "not_found" then raise ArgumentError, message.to_s
731
+ when "not_found" then raise not_found_error(message.to_s)
718
732
  when "ambiguous_id" then raise Session::AmbiguousId.new(message, ids: Array(json["ids"]))
719
733
  when "owned_by_tui"
720
734
  raise SessionManager::ScratchSession, sid if json["scratch"] == true
@@ -82,7 +82,7 @@ module Samagotchi
82
82
  @stdout.puts("#{short} refused: #{e.message}")
83
83
  false
84
84
  rescue ArgumentError => e
85
- error_line("chi sessions archive: #{e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"}")
85
+ error_line("chi sessions archive: #{e.message}")
86
86
  false
87
87
  rescue SystemCallError => e
88
88
  @stdout.puts("#{short} failed: #{e.message}")
@@ -96,7 +96,7 @@ module Samagotchi
96
96
  @stdout.puts(result[:unarchived].empty? ? "#{short} not archived" : "#{short} unarchived")
97
97
  true
98
98
  rescue ArgumentError => e
99
- error_line("chi sessions unarchive: #{e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"}")
99
+ error_line("chi sessions unarchive: #{e.message}")
100
100
  false
101
101
  rescue SystemCallError => e
102
102
  @stdout.puts("#{short} failed: #{e.message}")
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  # "the next run of the same routine". The chain is derived, never stored:
16
16
  # follow +continues+ back for the earlier links, forward (the session whose
17
17
  # +continues+ names this one) for the later ones. A link is continued at
18
- # most once (SessionManager.continue_session refuses a second), so the
18
+ # most once (SessionContinue.run refuses a second), so the
19
19
  # chain's latest link is the one nobody continues.
20
20
  module SessionChain
21
21
  # `--continues last:<id>`: the latest link of the chain <id> is in.
@@ -62,15 +62,16 @@ module Samagotchi
62
62
  # A session to continue as the CLI and the web name it: an id, a unique
63
63
  # prefix, or `last:<id or prefix>` for that chain's latest link.
64
64
  # @return [String] the full id of an existing session
65
- # @raise [ArgumentError] none such (Session::AmbiguousId for a prefix of several)
65
+ # @raise [Session::NotFound] none such (Session::AmbiguousId for a prefix of several)
66
66
  def resolve(ref, state_dir:)
67
67
  given = ref.to_s.strip
68
68
  last = given.start_with?(LAST_PREFIX)
69
69
  given = given.delete_prefix(LAST_PREFIX) if last
70
- raise ArgumentError, "no session #{ref}" unless Session.valid_id?(given)
71
-
72
- id = Session.resolve_id(given, state_dir: state_dir)
73
- raise ArgumentError, "no session #{ref}" unless Session.exist?(id, state_dir: state_dir)
70
+ id = begin
71
+ Session.resolve!(given, state_dir: state_dir)
72
+ rescue Session::NotFound
73
+ raise Session::NotFound, ref # as given, last: and all
74
+ end
74
75
 
75
76
  last ? latest(id, state_dir: state_dir) : id
76
77
  end