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
@@ -114,7 +114,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
114
114
  | Command | Purpose |
115
115
  |---------|---------|
116
116
  | `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). It also installs a bundle whose `requires_chi` this chi doesn't meet: without `--force` that install is refused (`NAME can't be installed: it requires chi …`). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
117
- | `upgrade <source\|name> [--force] [--dry-run] [--agent]` | A bare `name` is an installed bundle: chi upgrades it from the source its record keeps (its git URL with the `#ref` it recorded, its folder, or the archive it came from) — a dev install is refused, and a record whose source is gone says so and asks for the path or URL. Otherwise 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#outcome` (`Merger.classify`, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
117
+ | `upgrade <source\|name> [--force] [--dry-run] [--agent]` | A bare `name` is an installed bundle: chi upgrades it from the source its record keeps (its git URL with the `#ref` it recorded, its folder, or the archive it came from) — a dev install is refused, and a record whose source is gone says so and asks for the path or URL. Otherwise 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#decision` (the file's `FileState` and its `FileActions` row, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
118
118
  | `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
119
119
  | `status [<bundle>]` | Provenance + per-file `ok|modified|missing|unreadable|no-index` vs stored checksum and base snapshot, and what stays behind or merged at a chi start; `issues=` counts files behind, missing or unreadable, not edits; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
120
120
  | `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
@@ -18,27 +18,22 @@ module Samagotchi
18
18
  autoload :SessionContinue, File.expand_path("session_continue", __dir__)
19
19
 
20
20
  # A continue moving the previous link's open delegates to the chain's new
21
- # link (SessionManager.continue_session), instead of archiving them with
21
+ # link (SessionContinue.run), instead of archiving them with
22
22
  # it. A moved child gets a parent override (Session::PARENT_FILE), never a
23
23
  # rewritten session file, so its live worker saving a stale copy changes
24
24
  # nothing; its worker reads the override for its rings and at each turn's
25
25
  # start (Bridge::FEATURES "reparent").
26
26
  #
27
27
  # #apply runs between the new link's save and its worker's spawn, while
28
- # the link is #mark_starting (a ring from a moved child must not spawn a
29
- # second worker from the child's process until the link's own holds it).
30
- # Each step is idempotent,
31
- # and an intent file (INTENT_FILE) in the new link's folder names the move
32
- # until it is done: a worker that starts and finds one finishes the move
33
- # (#adopt), and a failed continue moves the same children back.
28
+ # the link is marked starting (StartingMarker): a ring from a moved child
29
+ # must not spawn a second worker from the child's process until the link's
30
+ # own holds it. Each step is idempotent, and an intent file (INTENT_FILE)
31
+ # in the new link's folder names the move until it is done: a worker that
32
+ # starts and finds one finishes the move (#adopt), and a failed continue
33
+ # moves the same children back.
34
34
  module ChildMove
35
35
  # <new link dir>/move.json: {from, ids, noted}, while a move is under way.
36
36
  INTENT_FILE = "move.json"
37
- # <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
38
- STARTING_FILE = "starting"
39
- # A starting marker older than this, or whose process is gone, is left
40
- # over from a continue that died: it holds nothing back.
41
- STARTING_STALE = 60.0
42
37
  # Who the note to a moved child is from (ContextNote's label).
43
38
  NOTE_SOURCE = "session chain"
44
39
  # What a live child's worker must name in its sidecar to be moved.
@@ -65,23 +60,23 @@ module Samagotchi
65
60
  move = []
66
61
  refuse = []
67
62
  SessionManager.children_of(prev_id, state_dir: state_dir).each do |row|
68
- subtree = [row[:id], *SessionManager.descendant_ids(row[:id], state_dir)]
63
+ subtree = [row.id, *SessionManager.descendant_ids(row.id, state_dir)]
69
64
  next if (subtree & open_ids).empty?
70
65
 
71
- if row[:archived]
66
+ if row.archived
72
67
  # An open delegate below an archived one: the archive's cascade
73
68
  # would take it, the move doesn't (only unarchived ones move).
74
69
  refuse.concat(open.select { |c| subtree.include?(c.id) }.map do |c|
75
- c.with(why: "under archived #{row[:id][0, 8]}; unarchive that one or archive this one")
70
+ c.with(why: "under archived #{row.short_id}; unarchive that one or archive this one")
76
71
  end)
77
72
  next
78
73
  end
79
74
 
80
- why = unmovable(row[:id], state_dir: state_dir)
75
+ why = unmovable(row.id, state_dir: state_dir)
81
76
  if why
82
- refuse << SessionContinue::OpenChild.new(id: row[:id], short_id: row[:id][0, 8], why: why)
77
+ refuse << SessionContinue::OpenChild.new(id: row.id, short_id: row.short_id, why: why)
83
78
  else
84
- move << row[:id]
79
+ move << row.id
85
80
  end
86
81
  end
87
82
  Plan.new(move: move, refuse: refuse)
@@ -206,44 +201,5 @@ module Samagotchi
206
201
  end
207
202
  SessionInbox.write_note(child_id, text: text, source: NOTE_SOURCE, state_dir: state_dir)
208
203
  end
209
-
210
- # Hold off a worker for the continue's new link (#starting?) until its
211
- # own worker holds it: written before the move, removed by that worker
212
- # once it has the owner lock (Worker#start).
213
- def mark_starting(session_id, state_dir:)
214
- dir = Session.session_dir(session_id, state_dir: state_dir)
215
- FileUtils.mkdir_p(dir)
216
- AtomicFile.write(File.join(dir, STARTING_FILE), JSON.generate({ "pid" => Process.pid, "at" => Time.now.iso8601(3) }))
217
- end
218
-
219
- def clear_starting(session_id, state_dir:)
220
- FileUtils.rm_f(File.join(Session.session_dir(session_id, state_dir: state_dir), STARTING_FILE))
221
- end
222
-
223
- # Whether a continue is moving delegates to +session_id+ right now: its
224
- # marker is there, fresh, and the process that wrote it runs.
225
- def starting?(session_id, state_dir:)
226
- file = File.join(Session.session_dir(session_id, state_dir: state_dir), STARTING_FILE)
227
- return false unless File.exist?(file)
228
-
229
- data = JSON.parse(File.read(file))
230
- return false if Time.now - Time.iso8601(data["at"].to_s) > STARTING_STALE
231
-
232
- process_alive?(data["pid"].to_i)
233
- rescue JSON::ParserError, ArgumentError, SystemCallError
234
- false
235
- end
236
-
237
- # Signal 0 only asks whether the process exists.
238
- def process_alive?(pid)
239
- return false unless pid.positive?
240
-
241
- Process.kill(0, pid)
242
- true
243
- rescue Errno::ESRCH
244
- false
245
- rescue Errno::EPERM
246
- true
247
- end
248
204
  end
249
205
  end
@@ -335,8 +335,8 @@ module Samagotchi
335
335
  before = kept ? kept.after : current
336
336
  # A follow-up's turn (DelegateCursor#turn) is reported by its own
337
337
  # reply, as delegate_result takes it, not a newer queued turn's.
338
- wait = ReplyWait.call(child_id, state_dir: @state_dir, cursor: before.reply_file, baseline: before.baseline,
339
- timeout: 0, turn: before.turn)
338
+ wait = ReplyWait.call(child_id, state_dir: @state_dir, **ReplyWait::Target.from_cursor(before, child: child).to_h,
339
+ timeout: 0)
340
340
  return nil unless reportable?(wait)
341
341
 
342
342
  claim_id = before.report_claim&.id || SecureRandom.hex(16)
@@ -5,7 +5,8 @@ require_relative "session"
5
5
  require_relative "archive_store"
6
6
  require_relative "git_head"
7
7
  require_relative "reply_wait"
8
- require_relative "worker_link"
8
+ require_relative "owner_lock"
9
+ require_relative "session_row"
9
10
  require_relative "tools/delegate"
10
11
  require_relative "tools/delegate_cursor"
11
12
 
@@ -50,44 +51,47 @@ module Samagotchi
50
51
  def of(parent_id, state_dir:, include_archived: false)
51
52
  cursors = Tools::DelegateCursors.read(parent_id, state_dir: state_dir)
52
53
  SessionManager.children_of(parent_id, state_dir: state_dir).filter_map do |row|
53
- next if row[:archived] && !include_archived
54
+ next if row.archived && !include_archived
54
55
 
55
- session = load(row[:id], state_dir: state_dir)
56
+ session = load(row.id, state_dir: state_dir)
56
57
  session && child(session, row, cursors, state_dir: state_dir)
57
58
  end
58
59
  end
59
60
 
60
61
  # The cheap path for the glance views: only the children in the
61
- # parent's delegates.json (one Session.load and an owner check each, no
62
+ # parent's delegates.json (one Session.load and a SessionRow each, no
62
63
  # scan). Its keys can also name a fork the model passed to delegate
63
64
  # session: or a deleted child: only live delegates of this parent count,
64
65
  # archived ones don't.
65
66
  # @return [Counts]
66
67
  def counts(parent_id, state_dir:)
67
68
  cursors = Tools::DelegateCursors.read(parent_id, state_dir: state_dir)
69
+ budgets = SessionRow.budgets
68
70
  children = cursors.keys.filter_map do |id|
69
71
  session = load(id, state_dir: state_dir)
70
72
  next unless session&.delegate? && session.parent_id == parent_id
71
73
  next if ArchiveStore.archived?(Session.session_dir(id, state_dir: state_dir))
72
74
 
73
- child(session, summary_of(session, state_dir: state_dir), cursors, state_dir: state_dir)
75
+ dir = Session.session_dir(id, state_dir: state_dir)
76
+ child(session, SessionRow.build(session, session_dir: dir, owner: OwnerLock.owner(dir), budgets: budgets), cursors,
77
+ state_dir: state_dir)
74
78
  end
75
79
  Counts.new(running: children.count { |c| c.state == "running" },
76
80
  waiting: children.count { |c| c.state == "waiting" },
77
81
  unreported: children.count { |c| c.last_reply && !c.reported })
78
82
  end
79
83
 
80
- # @param row [Hash] a children_of row, or summary_of's keys
84
+ # @param row [SessionRow] the child's row
81
85
  # @param cursors [Hash] the parent's delegates.json
82
86
  def child(session, row, cursors, state_dir:)
83
87
  reply = ReplyWait.newest_reply(session.id, state_dir: state_dir)
84
88
  cursor = Tools::DelegateCursors.get_from(cursors, session.id)
85
- Child.new(id: session.id, short_id: session.id[0, 8], title: (session.title || row[:preview]).to_s,
86
- state: state_of(session, row, reply), waiting: row[:waiting], live: !!row[:live],
89
+ Child.new(id: session.id, short_id: session.id[0, 8], title: (session.title || row.preview).to_s,
90
+ state: state_of(session, row, reply), waiting: row.waiting, live: !!row.live,
87
91
  delegate: session.delegate?, cwd: session.working_directory, branch: GitHead.branch(session.working_directory),
88
92
  last_reply: reply && first_line(session.id, reply, state_dir: state_dir),
89
93
  last_reply_at: reply && reply_time(reply), reported: !reply.nil? && reported?(session, cursor, reply),
90
- updated_at: session.updated_at, archived: !!row[:archived])
94
+ updated_at: session.updated_at, archived: !!row.archived)
91
95
  end
92
96
  private_class_method :child
93
97
 
@@ -97,8 +101,8 @@ module Samagotchi
97
101
  private_class_method :reported?
98
102
 
99
103
  def state_of(session, row, reply)
100
- return "waiting" if row[:waiting]
101
- return "running" if Tools::Delegate.running?(row)
104
+ return "waiting" if row.waiting
105
+ return "running" if row.running_or_starting?
102
106
  return "stopped" if session.status == Session::STATUS_STOPPED
103
107
 
104
108
  outcome = session.last_turn&.outcome
@@ -109,16 +113,6 @@ module Samagotchi
109
113
  end
110
114
  private_class_method :state_of
111
115
 
112
- # The keys of a children_of row that state_of and child read, for a
113
- # session counts loaded itself.
114
- def summary_of(session, state_dir:)
115
- live = WorkerLink.resolve(Session.session_dir(session.id, state_dir: state_dir), unlink: false).worker_owned?
116
- { live: live, busy: live && session.status == Session::STATUS_RUNNING, status: session.status,
117
- updated_at: session.updated_at, waiting: session.waiting_question(live: live)&.dig(:kind),
118
- preview: session.last_prompt.to_s }
119
- end
120
- private_class_method :summary_of
121
-
122
116
  def first_line(id, file, state_dir:)
123
117
  text = File.read(File.join(ReplyWait.reply_dir(id, state_dir: state_dir), file), 4096)
124
118
  text.each_line.map(&:strip).find { |line| !line.empty? }.to_s
@@ -17,9 +17,6 @@ module Samagotchi
17
17
  # override the constants (specs stub them on the class).
18
18
  module ParentWait
19
19
  POLL_INTERVAL = ReplyWait::POLL_INTERVAL
20
- # No live worker this long while waiting: it died before it could mark
21
- # the session (a worker takes well under a second to start).
22
- WORKER_GONE_AFTER = 5
23
20
  FORMATS = %w[text json].freeze
24
21
 
25
22
  # @return [Integer] the exit status
@@ -55,30 +52,19 @@ module Samagotchi
55
52
  def resolve(given)
56
53
  @session_id = @access.resolve(given)
57
54
  rescue ArgumentError => e
58
- error_line("#{command_name}: #{e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"}")
55
+ error_line("#{command_name}: #{e.message}")
59
56
  nil
60
57
  end
61
58
 
62
- # Wait for the session's next reply past +cursor+ (ReplyWait) and
63
- # report it. Ctrl-C leaves the turn running (130).
64
- # @param owner_grace [Numeric, nil] seconds with no live worker before
65
- # it counts as gone; nil waits for whatever wakes one
66
- # @param turn [String, nil] the enqueued_id of the turn waited for
67
- # (ReplyWait's turn:): a queued message's, or the turn an answer or
68
- # a --wait with no message is about (ReplyWait.own_turn)
69
- # @param queued [Boolean] +turn+ is a queued message's (ReplyWait's
70
- # queued:)
71
- # @param ended_before [Boolean] a last_turn that ended before the
72
- # baseline is the answer (ReplyWait's ended_before:) — the
73
- # --wait-with-no-message's settled session
74
- # @param report_turn_id [String, nil] the turn id to include in the
75
- # report, even when ReplyWait uses the next-reply path
59
+ # Wait for what +target+ binds (ReplyWait) and report it. Ctrl-C
60
+ # leaves the turn running (130).
61
+ # @param target [ReplyWait::Target] which turn, from the cursor and
62
+ # baseline prepare_wait read; its report_turn_id is the turn id the
63
+ # report names, even when ReplyWait takes the next reply
76
64
  # @return [Integer] the exit status
77
- def wait_for_reply(id, cursor:, baseline:, timeout:, owner_grace: self.class::WORKER_GONE_AFTER, turn: nil,
78
- queued: false, ended_before: false, report_turn_id: turn)
79
- result = @access.wait_reply(id, cursor: cursor, timeout: timeout, baseline: baseline, owner_grace: owner_grace,
80
- poll_interval: self.class::POLL_INTERVAL, turn: turn, queued: queued,
81
- ended_before: ended_before)
65
+ def wait_for_reply(id, target:, timeout:)
66
+ report_turn_id = target.report_turn_id
67
+ result = @access.wait_reply(id, **target.to_h, timeout: timeout, poll_interval: self.class::POLL_INTERVAL)
82
68
  result.turn_id = report_turn_id if report_turn_id && !report_turn_id.to_s.empty?
83
69
  result.text = result.text&.dup&.force_encoding(Encoding::UTF_8)&.scrub
84
70
  @reported = true
@@ -8,11 +8,14 @@ require_relative "config_text_edit"
8
8
  require_relative "log"
9
9
  require_relative "model_ref"
10
10
  require_relative "paths"
11
- require_relative "llm_context_strategy"
12
- require_relative "host_model"
13
11
  require_relative "config/restart_watch"
14
12
 
15
13
  module Samagotchi
14
+ # Which LLM context strategy a turn runs under; it reads Config.
15
+ autoload :LLMContextStrategy, File.expand_path("llm_context_strategy", __dir__)
16
+ # A hosts.<name>.models entry and its price; they read ConfigFile.
17
+ autoload :HostModel, File.expand_path("host_model", __dir__)
18
+ autoload :ModelPrice, File.expand_path("model_price", __dir__)
16
19
  # Parses the thinking: levels of host and model entries (it needs Config).
17
20
  autoload :Thinking, File.expand_path("thinking", __dir__)
18
21
  # One hosts: entry; it needs ConfigFile's readers.
@@ -89,6 +92,12 @@ module Samagotchi
89
92
  next_history_access next_matching_turn_display_poll_write
90
93
  ].freeze
91
94
 
95
+ # llm_context.policy's default: the sentence forget_outputs' description
96
+ # carries (ToolDeclarations::FORGET_OUTPUTS_POLICY), the plan's D7 line,
97
+ # adapted from CLM's steering example.
98
+ DEFAULT_LLM_CONTEXT_POLICY = "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note " \
99
+ "what it established; keep anything you'll still edit against."
100
+
92
101
  ENTRIES = [
93
102
  # universal – env+config+cli
94
103
  Entry.new(key: "default.model", yaml_path: %w[default model], type: :string, default: nil, expose: %i[env config cli], lifecycle: :new_session),
@@ -261,7 +270,7 @@ module Samagotchi
261
270
  # and hosts.<name>.llm_context_budget_tokens come first. Kept as written: LLMContextStrategy.parse_budget reads it.
262
271
  Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :string, default: nil, expose: %i[env config], lifecycle: :next_matching_turn_display_poll_write),
263
272
  # The policy sentence forget_outputs' description carries (the forget layer); blank: none.
264
- Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: LLMContextStrategy::DEFAULT_POLICY, expose: %i[env config], lifecycle: :worker_start),
273
+ Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: DEFAULT_LLM_CONTEXT_POLICY, expose: %i[env config], lifecycle: :worker_start),
265
274
 
266
275
  # How much models think: off|low|medium|high|default, parsed by Thinking
267
276
  # (a :string, since YAML reads an unquoted off as false). The CLI flag
@@ -364,6 +373,9 @@ module Samagotchi
364
373
  # The stage view's working set (files, memories and URLs the turn brought into context): a rail in the stage, a
365
374
  # footprint under each finished turn and a "set N" chip in place of the mem chip; ?working_set=1|0 overrides it per page load.
366
375
  Entry.new(key: "web.working_set", yaml_path: %w[web working_set], type: :bool, default: true, expose: %i[env config cli], lifecycle: :web_start),
376
+ # Opt-in: the page checks each finished turn against a reload of it (one full read per turn end) and chi web logs
377
+ # reload_checked, with reload_mismatch and the kinds that differed (counts only, no content).
378
+ Entry.new(key: "web.reload_check", yaml_path: %w[web reload_check], type: :bool, default: false, expose: %i[env config cli], lifecycle: :web_start),
367
379
  # Quick replies next to Annotate in the page's selection bubble, "|"-separated (a YAML list works too); "" leaves only Annotate.
368
380
  Entry.new(key: "web.annotate_presets", yaml_path: %w[web annotate_presets], type: :string, default: "Agreed|Could you please elaborate?", expose: %i[env config cli], lifecycle: :web_start),
369
381
  # What a file ref (a tool row's file, a diff line) opens on this machine: a preset (vscode, vscode-insiders, cursor, zed, none) or a URL template with {path} and an optional {line} (Web::Editor).
@@ -399,7 +411,7 @@ module Samagotchi
399
411
  # appended_prompt_files, Hooks::Loader, ConfigFile.bundle_settings.
400
412
  FREE_FORM_MAPS = %w[model_aliases hooks bundles memories append_system_prompt].freeze
401
413
  # Maps of named entries and the keys an entry may hold
402
- # (ConfigFile.hosts_config, ConfigFile.model_settings).
414
+ # (HostConfig.all, ModelSettings.all).
403
415
  MAP_ENTRY_KEYS = {
404
416
  "hosts" => %w[host port url api api_key_env auth dialect profile first_token_timeout vision sampling thinking
405
417
  reasoning_history enabled remote window_tokens steer_cut_after llm_context_strategy llm_context_apply llm_context_budget_tokens models].freeze,
@@ -408,6 +420,8 @@ module Samagotchi
408
420
  }.freeze
409
421
  # The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
410
422
  HOST_MODEL_KEYS = %w[price served].freeze
423
+ # The keys a hosts.<name>.models.<id>.price may hold (ModelPrice.parse).
424
+ MODEL_PRICE_KEYS = %w[input cache_read cache_write output].freeze
411
425
  # Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
412
426
  # And toolkit's kits (Toolkit.kits), and bundles' judgments' settings (SystemOne::JudgmentSettings).
413
427
  SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze, "toolkit" => %w[kits].freeze, "system_one" => %w[judgments].freeze }.freeze
@@ -887,9 +901,9 @@ module Samagotchi
887
901
  def price_key_problems(prefix, price)
888
902
  return [] unless price.is_a?(Hash)
889
903
 
890
- (price.keys.map(&:to_s) - ModelPrice::KEYS).map do |k|
904
+ (price.keys.map(&:to_s) - MODEL_PRICE_KEYS).map do |k|
891
905
  # A renamed key is compared by its new name, so that name is the hint.
892
- unknown_key_message("#{prefix}#{k}", RENAMED_PRICE_KEYS.fetch(k, k), ModelPrice::KEYS, prefix: prefix)
906
+ unknown_key_message("#{prefix}#{k}", RENAMED_PRICE_KEYS.fetch(k, k), MODEL_PRICE_KEYS, prefix: prefix)
893
907
  end
894
908
  end
895
909
 
@@ -1172,71 +1186,6 @@ module Samagotchi
1172
1186
  HOST_NAME_RE = /\A[a-z0-9][a-z0-9._-]*\z/i
1173
1187
  ENV_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
1174
1188
 
1175
- # config.yml's hosts: by lowercased name; an entry that is disabled or
1176
- # invalid (warned) is left out. No entry left: "default", from
1177
- # server.host/port.
1178
- # @return [Hash{String => HostConfig}]
1179
- def hosts_config(env: ENV, path: global_path(env: env))
1180
- data = read_yaml(env: env, path: path)
1181
- raw_hosts = data[HOSTS_KEY] if data.is_a?(Hash)
1182
-
1183
- normalized = {}
1184
- if raw_hosts.is_a?(Hash)
1185
- raw_hosts.each do |raw_name, raw_cfg|
1186
- host = HostConfig.parse(raw_name, raw_cfg)
1187
- normalized[host.name] = host if host
1188
- end
1189
- warn_duplicate_host_models(normalized)
1190
- end
1191
- normalized["default"] = default_host_config(data, env) if normalized.empty?
1192
- normalized
1193
- rescue StandardError => e
1194
- Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
1195
- {}
1196
- end
1197
-
1198
- # The host chi uses when hosts: names none: server.host/port.
1199
- def default_host_config(data, env)
1200
- opts = { file_data: data, env: env, cli_overrides: Samagotchi::Config.cli_overrides }
1201
- host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
1202
- host = "localhost" if host.empty?
1203
- port = Samagotchi::Config.resolve("server.port", **opts).to_i
1204
- port = 8080 if port <= 0 || port > 65_535
1205
- HostConfig.new(name: "default", host: host, port: port)
1206
- end
1207
- private_class_method :default_host_config
1208
-
1209
- # Two hosts declaring one id under hosts.<name>.models: a bare id goes
1210
- # where HostRegistry#host_for_model sends it (the default host when it
1211
- # declares the id, else the first in hosts: order); say so once.
1212
- def warn_duplicate_host_models(hosts)
1213
- declared = Hash.new { |h, k| h[k] = [] }
1214
- hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
1215
- declared.each_value do |owners|
1216
- next if owners.size < 2
1217
-
1218
- names = owners.map(&:first)
1219
- id = owners.first.last
1220
- goes = names.include?("default") ? "default" : "#{names.first} unless the default host lists it"
1221
- warn_once "Warning: #{names.map { |n| "hosts.#{n}.models" }.join(" and ")} #{names.size == 2 ? "both" : "all"} declare #{id}; " \
1222
- "a bare #{id} goes to #{goes}"
1223
- end
1224
- end
1225
- private_class_method :warn_duplicate_host_models
1226
-
1227
- # The names (lowercased) of the hosts entries with enabled: false, which
1228
- # hosts_config leaves out, from config.yml's hosts:.
1229
- # @return [Array<String>]
1230
- def disabled_host_names(env: ENV, path: global_path(env: env))
1231
- data = read_yaml(env: env, path: path)
1232
- raw = data[HOSTS_KEY] if data.is_a?(Hash)
1233
- return [] unless raw.is_a?(Hash)
1234
-
1235
- raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && HostConfig.disabled?(cfg) }
1236
- rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1237
- []
1238
- end
1239
-
1240
1189
  # Resolve the `recap:` section through the Config registry so scalar
1241
1190
  # recap settings share the single precedence path (CLI > ENV > file >
1242
1191
  # default). Returns false when explicitly disabled (file `recap: false`
@@ -1420,47 +1369,6 @@ module Samagotchi
1420
1369
  {}
1421
1370
  end
1422
1371
 
1423
- # The top-level `models:` map: per-model settings keyed by model id or
1424
- # alias (downcased), e.g. `models: {my-alias: {profile: qwen36}}`, each a
1425
- # ModelSettings; an entry that is not a map is skipped.
1426
- # @return [Hash{String => ModelSettings}]
1427
- def model_settings(env: ENV, path: global_path(env: env))
1428
- data = read_yaml(env: env, path: path)
1429
- raw = data[MODELS_KEY] if data.is_a?(Hash)
1430
- return {} unless raw.is_a?(Hash)
1431
-
1432
- raw.each_with_object({}) do |(k, v), result|
1433
- key = k.to_s.strip.downcase
1434
- settings = ModelSettings.parse(key, v)
1435
- result[key] = settings if settings
1436
- end
1437
- rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
1438
- {}
1439
- end
1440
-
1441
- # The first +names+ (model as typed, alias-resolved, bare, …) whose
1442
- # models: entry sets +field+ (a ModelSettings member; another name
1443
- # raises ArgumentError): a ModelSetting (key, value), or nil.
1444
- # VisionSupport and SamplingSettings look a model up the same way.
1445
- # @param models [Hash{String => ModelSettings}, nil] model_settings by default
1446
- # @return [ModelSetting, nil]
1447
- def model_setting(names, field, models: nil)
1448
- raise ArgumentError, "unknown models: field #{field.inspect}" unless ModelSettings.members.include?(field)
1449
-
1450
- models ||= model_settings
1451
- Array(names).map { |name| name.to_s.strip.downcase }.reject(&:empty?).uniq.each do |key|
1452
- value = models[key]&.public_send(field)
1453
- return ModelSetting.new(key: key, value: value) unless value.nil?
1454
- end
1455
- nil
1456
- end
1457
-
1458
- # +raw+ parsed against config.yml's hosts (or +hosts+) and aliases.
1459
- # @return [ModelRef]
1460
- def model_ref(raw, env: ENV, path: global_path(env: env), hosts: nil)
1461
- ModelRef.parse(raw, hosts: hosts || hosts_config(env: env, path: path), aliases: model_aliases(env: env, path: path))
1462
- end
1463
-
1464
1372
  # Words /model takes as arguments (clear/default/none/off reset the
1465
1373
  # runtime model), so no alias may be named after one.
1466
1374
  RESERVED_MODEL_ALIASES = %w[clear default none off].freeze
@@ -428,8 +428,7 @@ module Samagotchi
428
428
  session = @access.show(id)
429
429
  Target.new(session_id: id, project_root: session.project_root, cwd: session.working_directory, label: id[0, 8])
430
430
  rescue ArgumentError => e
431
- message = e.is_a?(Session::AmbiguousId) ? e.message : "no session #{given}"
432
- error_line("#{command_name}: #{message}")
431
+ error_line("#{command_name}: #{e.message}")
433
432
  nil
434
433
  end
435
434
 
@@ -7,6 +7,7 @@ require "time"
7
7
  require_relative "atomic_file"
8
8
  require_relative "memory_paths"
9
9
  require_relative "paths"
10
+ require_relative "session_paths"
10
11
  require_relative "session"
11
12
 
12
13
  module Samagotchi
@@ -617,13 +618,13 @@ module Samagotchi
617
618
  nil
618
619
  end
619
620
 
620
- # <state dir>/context; +state_dir+ is the sessions dir (Session.default_state_dir).
621
+ # <state dir>/context; +state_dir+ is the sessions dir (SessionPaths.default_state_dir).
621
622
  def root(state_dir: nil)
622
- File.join(File.dirname(state_dir || Session.default_state_dir), DIR)
623
+ File.join(File.dirname(state_dir || SessionPaths.default_state_dir), DIR)
623
624
  end
624
625
 
625
626
  def session_location(session_id, state_dir: nil)
626
- Location.new(scope: "session", dir: File.join(root(state_dir: state_dir), "sessions", Session.check_id!(session_id)),
627
+ Location.new(scope: "session", dir: File.join(root(state_dir: state_dir), "sessions", SessionPaths.check_id!(session_id)),
627
628
  key: session_id)
628
629
  end
629
630
 
@@ -669,7 +670,7 @@ module Samagotchi
669
670
  # A deleted session's sources, snapshots, subscriptions and markers.
670
671
  # @return [Array<String>] the paths removed
671
672
  def remove_session(session_id, state_dir: nil)
672
- return [] unless Session.valid_id?(session_id.to_s)
673
+ return [] unless SessionPaths.valid_id?(session_id.to_s)
673
674
 
674
675
  dir = session_location(session_id, state_dir: state_dir).dir
675
676
  return [] unless Dir.exist?(dir)
@@ -11,14 +11,14 @@ module Samagotchi
11
11
  # provider's context_length)
12
12
  # 3. models.<key>.window_tokens, else hosts.<name>.window_tokens
13
13
  # (#setting: the most specific wins)
14
- # 4. context.window_tokens (CLI, SAMAGOTCHI_CONTEXT_WINDOW_TOKENS or the
15
- # config file)
14
+ # 4. context.window_tokens (--context-window-tokens,
15
+ # SAMAGOTCHI_CONTEXT_WINDOW_TOKENS or the config file)
16
16
  # 5. DEFAULT_TOKENS
17
17
  # Settings (3 and 4) only fill in when the server and its list report
18
18
  # nothing.
19
19
  #
20
- # Sources: :server, :model_list, :model_setting, :host_setting, :config
21
- # (CLI or file), :env, :default.
20
+ # Sources: :server, :model_list, :model_setting, :host_setting, :cli,
21
+ # :env, :config (the file), :default.
22
22
  module ContextWindow
23
23
  DEFAULT_TOKENS = 256_000
24
24
 
@@ -48,16 +48,13 @@ module Samagotchi
48
48
  # The window config.yml gives this model or its host:
49
49
  # models.<key>.window_tokens (by the model's lookup names, as sampling
50
50
  # and vision look a model up), else hosts.<name>.window_tokens.
51
- # @param target [HostRegistry::ModelTarget]
52
- # @param names [Array<String>] HostRegistry#lookup_names
53
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
51
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
54
52
  # @return [Resolved, nil]
55
- def setting(target, names:, models: nil)
56
- models ||= ConfigFile.model_settings
57
- tokens = ConfigFile.model_setting(names, :window_tokens, models: models)&.value
53
+ def setting(lookup)
54
+ tokens = lookup.model(:window_tokens)&.value
58
55
  return Resolved.new(tokens: tokens, source: :model_setting) if positive_integer?(tokens)
59
56
 
60
- tokens = target&.entry&.window_tokens
57
+ tokens = lookup.host(:window_tokens)
61
58
  positive_integer?(tokens) ? Resolved.new(tokens: tokens, source: :host_setting) : nil
62
59
  rescue StandardError
63
60
  nil
@@ -79,7 +76,7 @@ module Samagotchi
79
76
  end
80
77
  return Resolved.new(tokens: DEFAULT_TOKENS, source: :default) unless positive_integer?(value)
81
78
 
82
- Resolved.new(tokens: value, source: origin == :env ? :env : :config)
79
+ Resolved.new(tokens: value, source: { cli: :cli, env: :env }.fetch(origin, :config))
83
80
  end
84
81
 
85
82
  # For callers with no client at hand (tool output guardrails): the last
@@ -335,7 +335,7 @@ module Samagotchi
335
335
  # are left out. @return [Array<Hash>] name:, hosts:, state:
336
336
  def host_keys
337
337
  config_env = { "XDG_CONFIG_HOME" => launch_config_home }
338
- hosts = ConfigFile.hosts_config(env: config_env).values.select(&:api_key_env).group_by(&:api_key_env)
338
+ hosts = HostConfig.all(env: config_env).values.select(&:api_key_env).group_by(&:api_key_env)
339
339
  hosts.map do |name, entries|
340
340
  state = if HostKeys.from_keychain(name, env: @env) then :keychain
341
341
  elsif !@env[name].to_s.strip.empty? then :shell_only