samagotchi 0.156.0 → 0.158.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/README.md +1 -1
  4. data/bin/chi +4 -1
  5. data/docs/architecture.md +18 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +4 -1
  8. data/docs/cli.md +29 -24
  9. data/docs/configuration.md +43 -9
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +12 -7
  12. data/docs/hooks.md +53 -83
  13. data/docs/internals/bridge.md +84 -7
  14. data/docs/internals/bundles.md +69 -46
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +23 -7
  17. data/docs/internals/participants.md +3 -2
  18. data/docs/internals/remote-sessions.md +5 -2
  19. data/docs/internals/web-frontend.md +3 -2
  20. data/docs/internals/working-set.md +1 -1
  21. data/docs/memory.md +51 -10
  22. data/docs/plugins.md +48 -11
  23. data/docs/releasing.md +6 -5
  24. data/docs/sessions.md +13 -13
  25. data/docs/sub-agent.md +17 -2
  26. data/docs/testing.md +56 -5
  27. data/lib/samagotchi/answer_command.rb +2 -1
  28. data/lib/samagotchi/archive_store.rb +4 -3
  29. data/lib/samagotchi/bootstrap_command.rb +3 -2
  30. data/lib/samagotchi/bridge/card_places.rb +5 -7
  31. data/lib/samagotchi/bridge/card_store.rb +3 -0
  32. data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
  33. data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
  34. data/lib/samagotchi/bridge.rb +31 -6
  35. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  36. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  37. data/lib/samagotchi/bundle_command/check.rb +41 -10
  38. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  39. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  40. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  42. data/lib/samagotchi/bundle_command.rb +20 -45
  43. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  44. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  45. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  46. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  48. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
  50. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  51. data/lib/samagotchi/child_move.rb +6 -6
  52. data/lib/samagotchi/child_reports.rb +2 -2
  53. data/lib/samagotchi/children_status.rb +15 -21
  54. data/lib/samagotchi/cli/parent_wait.rb +8 -22
  55. data/lib/samagotchi/config.rb +3 -0
  56. data/lib/samagotchi/context_command.rb +7 -5
  57. data/lib/samagotchi/context_sources.rb +9 -6
  58. data/lib/samagotchi/context_window.rb +9 -12
  59. data/lib/samagotchi/effective_model.rb +57 -22
  60. data/lib/samagotchi/engine/plugins.rb +1 -1
  61. data/lib/samagotchi/engine/recap.rb +8 -4
  62. data/lib/samagotchi/engine.rb +30 -9
  63. data/lib/samagotchi/events.rb +3 -2
  64. data/lib/samagotchi/extension_load.rb +26 -44
  65. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  66. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  67. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  68. data/lib/samagotchi/guardrails_command.rb +1 -1
  69. data/lib/samagotchi/hook_routes.rb +2 -2
  70. data/lib/samagotchi/hooks.rb +2 -3
  71. data/lib/samagotchi/host_address.rb +52 -0
  72. data/lib/samagotchi/host_registry.rb +57 -45
  73. data/lib/samagotchi/kernel_loop.rb +7 -3
  74. data/lib/samagotchi/llm/chat_loop.rb +6 -3
  75. data/lib/samagotchi/llm_context_strategy.rb +27 -63
  76. data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
  77. data/lib/samagotchi/memory_bundle/builder.rb +12 -35
  78. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  79. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  80. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  81. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  82. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  83. data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
  84. data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
  85. data/lib/samagotchi/memory_bundle/installer.rb +212 -178
  86. data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
  87. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  88. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  89. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  90. data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
  91. data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
  92. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  93. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
  94. data/lib/samagotchi/memory_bundle/status.rb +26 -30
  95. data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
  96. data/lib/samagotchi/memory_bundle/trash.rb +28 -18
  97. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  98. data/lib/samagotchi/memory_orphans.rb +4 -4
  99. data/lib/samagotchi/memory_paths.rb +102 -13
  100. data/lib/samagotchi/memory_usage.rb +1 -1
  101. data/lib/samagotchi/model_lookup.rb +75 -0
  102. data/lib/samagotchi/model_profile.rb +1 -0
  103. data/lib/samagotchi/models_command.rb +1 -1
  104. data/lib/samagotchi/note_command.rb +1 -1
  105. data/lib/samagotchi/participant_mode.rb +2 -1
  106. data/lib/samagotchi/plugin/api.rb +42 -10
  107. data/lib/samagotchi/plugin/loader.rb +75 -15
  108. data/lib/samagotchi/plugin/sessions.rb +1 -1
  109. data/lib/samagotchi/project_command.rb +167 -0
  110. data/lib/samagotchi/project_init.rb +69 -0
  111. data/lib/samagotchi/project_scope.rb +6 -4
  112. data/lib/samagotchi/prompt_files.rb +10 -7
  113. data/lib/samagotchi/reasoning_history.rb +14 -20
  114. data/lib/samagotchi/recap_place.rb +26 -0
  115. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  116. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  117. data/lib/samagotchi/reply_wait/target.rb +113 -0
  118. data/lib/samagotchi/reply_wait.rb +22 -2
  119. data/lib/samagotchi/run_options.rb +1 -0
  120. data/lib/samagotchi/sampling_settings.rb +9 -16
  121. data/lib/samagotchi/self_report.rb +34 -35
  122. data/lib/samagotchi/send_command.rb +17 -23
  123. data/lib/samagotchi/session.rb +349 -441
  124. data/lib/samagotchi/session_access/contract.rb +1 -1
  125. data/lib/samagotchi/session_access/local.rb +5 -6
  126. data/lib/samagotchi/session_access/remote.rb +5 -3
  127. data/lib/samagotchi/session_field.rb +134 -0
  128. data/lib/samagotchi/session_inbox.rb +49 -1
  129. data/lib/samagotchi/session_manager/emptiness.rb +2 -1
  130. data/lib/samagotchi/session_manager/listing.rb +29 -100
  131. data/lib/samagotchi/session_manager.rb +2 -1
  132. data/lib/samagotchi/session_metrics.rb +7 -4
  133. data/lib/samagotchi/session_paths.rb +57 -0
  134. data/lib/samagotchi/session_retention.rb +2 -2
  135. data/lib/samagotchi/session_row.rb +200 -0
  136. data/lib/samagotchi/session_setup.rb +15 -12
  137. data/lib/samagotchi/sessions_command.rb +11 -129
  138. data/lib/samagotchi/steer.rb +10 -0
  139. data/lib/samagotchi/steer_cut_after.rb +7 -14
  140. data/lib/samagotchi/system_prompt.rb +20 -11
  141. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  142. data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
  143. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  144. data/lib/samagotchi/terminal_ui.rb +3 -1
  145. data/lib/samagotchi/testing/bundle.rb +5 -12
  146. data/lib/samagotchi/testing/install.rb +5 -4
  147. data/lib/samagotchi/testing/recorder.rb +1 -1
  148. data/lib/samagotchi/testing.rb +1 -1
  149. data/lib/samagotchi/thinking.rb +16 -21
  150. data/lib/samagotchi/tool_declarations.rb +1 -1
  151. data/lib/samagotchi/tool_response.rb +5 -3
  152. data/lib/samagotchi/tool_runner.rb +18 -1
  153. data/lib/samagotchi/toolkit_binding.rb +1 -4
  154. data/lib/samagotchi/toolkit_command.rb +1 -4
  155. data/lib/samagotchi/tools/delegate.rb +60 -51
  156. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  157. data/lib/samagotchi/tools/delegate_result.rb +1 -1
  158. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  159. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  160. data/lib/samagotchi/update_command.rb +11 -7
  161. data/lib/samagotchi/verify_fresh.rb +7 -11
  162. data/lib/samagotchi/version.rb +1 -1
  163. data/lib/samagotchi/vision_support.rb +11 -14
  164. data/lib/samagotchi/web/app.rb +66 -21
  165. data/lib/samagotchi/web/message_display.rb +57 -7
  166. data/lib/samagotchi/web/participant_access.rb +1 -1
  167. data/lib/samagotchi/web/project_list.rb +3 -2
  168. data/lib/samagotchi/web/public/activity.js +16 -7
  169. data/lib/samagotchi/web/public/app.js +29 -8
  170. data/lib/samagotchi/web/public/bubbles.js +8 -3
  171. data/lib/samagotchi/web/public/cards_place.js +15 -0
  172. data/lib/samagotchi/web/public/commands.js +5 -1
  173. data/lib/samagotchi/web/public/data.js +4 -2
  174. data/lib/samagotchi/web/public/history_shape.js +220 -0
  175. data/lib/samagotchi/web/public/index.html +6 -1
  176. data/lib/samagotchi/web/public/project_picker.js +86 -4
  177. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  178. data/lib/samagotchi/web/public/reload_check.js +71 -0
  179. data/lib/samagotchi/web/public/stage_model.js +7 -5
  180. data/lib/samagotchi/web/public/stage_view.js +7 -3
  181. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  182. data/lib/samagotchi/web/public/timing.js +43 -2
  183. data/lib/samagotchi/web/public/turn_block.js +215 -30
  184. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  185. data/lib/samagotchi/web/public/turn_events.js +10 -1
  186. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  187. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  188. data/lib/samagotchi/web/server.rb +4 -2
  189. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  190. data/lib/samagotchi/web/session_control.rb +1 -1
  191. data/lib/samagotchi/web/session_hub.rb +9 -17
  192. data/lib/samagotchi/web/session_show.rb +64 -2
  193. data/lib/samagotchi/web/session_summary.rb +36 -24
  194. data/lib/samagotchi/worker.rb +28 -5
  195. data/lib/samagotchi/worker_link.rb +3 -0
  196. metadata +22 -5
  197. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
  198. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -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
 
@@ -657,19 +658,21 @@ module Samagotchi
657
658
  end
658
659
 
659
660
  # Where a project's source runs: the project root when it is a checkout
660
- # (its .git is there), else +fallback+ (the session's folder): a bare
661
+ # (its .git is there) or a marked folder (MemoryPaths::MARKER), else
662
+ # +fallback+ (the session's folder): a bare
661
663
  # repo, proj/.bare or --separate-git-dir makes the root the git dir
662
664
  # itself (MemoryPaths.project_root), and a command run there sees no
663
665
  # files.
664
666
  def project_cwd(project_root, fallback)
665
667
  root = project_root.to_s
666
- !root.empty? && File.exist?(File.join(root, ".git")) ? root : fallback
668
+ checkout = !root.empty? && [".git", MemoryPaths::MARKER].any? { |name| File.exist?(File.join(root, name)) }
669
+ checkout ? root : fallback
667
670
  end
668
671
 
669
672
  # A deleted session's sources, snapshots, subscriptions and markers.
670
673
  # @return [Array<String>] the paths removed
671
674
  def remove_session(session_id, state_dir: nil)
672
- return [] unless Session.valid_id?(session_id.to_s)
675
+ return [] unless SessionPaths.valid_id?(session_id.to_s)
673
676
 
674
677
  dir = session_location(session_id, state_dir: state_dir).dir
675
678
  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] ModelSettings.all (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 ||= ModelSettings.all
57
- tokens = ModelSettings.find(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
@@ -4,6 +4,7 @@ require_relative "host_registry"
4
4
  require_relative "log"
5
5
  require_relative "context_window"
6
6
  require_relative "llm_context_strategy"
7
+ require_relative "model_lookup"
7
8
  require_relative "model_profile"
8
9
  require_relative "reasoning_history"
9
10
  require_relative "sampling_settings"
@@ -22,7 +23,8 @@ module Samagotchi
22
23
 
23
24
  # The effective model resolved once (HostRegistry#resolve: its host and
24
25
  # client, the id sent) with the names its models: entry may be under
25
- # (HostRegistry#lookup_names), and its settings read from them. An
26
+ # (HostRegistry#lookup_names), and its settings read from them through
27
+ # one ModelLookup (every layer's input, with the registry's env). An
26
28
  # immutable snapshot built per read, never kept across reads: config.yml,
27
29
  # a model listing (which may move a bare id to another host) and another
28
30
  # process's saved lists all change what a resolve gives without any
@@ -32,14 +34,17 @@ module Samagotchi
32
34
  # Engine has always given (Unresolved); building one raises as
33
35
  # HostRegistry#resolve does (SessionModelSettings#model_setting gives the
34
36
  # fallback then).
35
- EffectiveModel = Data.define(:registry, :name, :typed, :target, :names) do
37
+ EffectiveModel = Data.define(:registry, :name, :typed, :target, :names, :lookup) do
36
38
  # @param registry [HostRegistry]
37
39
  # @param choice [ModelChoice]
40
+ # @param models [Hash{String => ModelSettings}, nil] one models: map for
41
+ # many models (a picker); ModelSettings.all at each read by default
38
42
  # @raise what HostRegistry#resolve raises
39
- def self.of(registry, choice)
43
+ def self.of(registry, choice, models: nil)
40
44
  target = registry.resolve(choice.name)
41
- new(registry: registry, name: choice.name, typed: choice.typed, target: target,
42
- names: registry.lookup_names(choice.typed, resolved: choice.name, target: target))
45
+ names = registry.lookup_names(choice.typed, resolved: choice.name, target: target)
46
+ new(registry: registry, name: choice.name, typed: choice.typed, target: target, names: names,
47
+ lookup: ModelLookup.for(target, names: names, models: models, env: registry.env))
43
48
  end
44
49
 
45
50
  # A saved session's model (no worker): its model name, the default when
@@ -50,6 +55,34 @@ module Samagotchi
50
55
  of(registry, ModelChoice.new(name: name, typed: session.model_typed || name))
51
56
  end
52
57
 
58
+ # A saved session's LLM context strategy, apply rule and budget, each
59
+ # with where it came from (LLMContextStrategy::Explained#summary), as
60
+ # its worker would resolve them; nil when they can't be (an unreadable
61
+ # config, a model that resolves nowhere). From config and the registry
62
+ # only: no network, no model call. What a session page, a session
63
+ # list's ctx % and a spawn-time check all read.
64
+ # @param session [Session, nil]
65
+ # @param registry [HostRegistry, nil] the hosts to resolve against;
66
+ # HostRegistry.current by default
67
+ # @return [Hash, nil] Explained#summary, symbol keys
68
+ def self.session_llm_context(session, registry: nil)
69
+ return nil if session.nil?
70
+
71
+ own = session.llm_context
72
+ of_session(session, registry: registry || HostRegistry.current).llm_context_explained(own&.resolve_args || {})&.summary(own)
73
+ rescue StandardError # degrade-ok: a saved session's model that resolves nowhere has no budget to show; the readers log their own
74
+ nil
75
+ end
76
+
77
+ # The budget a saved session's turns run under
78
+ # (.session_llm_context's :budget_tokens), nil when it is off or can't
79
+ # be resolved. What a session list's ctx % counts against, so a saved
80
+ # session's fill reads as the live meter.
81
+ # @return [Integer, nil]
82
+ def self.session_budget(session, registry: nil)
83
+ session_llm_context(session, registry: registry)&.fetch(:budget_tokens, nil)
84
+ end
85
+
53
86
  def entry = target.entry
54
87
  def client = target.client
55
88
  def bare = target.bare_model
@@ -63,21 +96,21 @@ module Samagotchi
63
96
 
64
97
  # The configured request parameters (hosts: and models: sampling).
65
98
  def sampling
66
- SamplingSettings.for(target, names: names)
99
+ SamplingSettings.for(lookup)
67
100
  rescue StandardError => e
68
101
  unresolved(:sampling_unresolved, e, SamplingSettings::EMPTY)
69
102
  end
70
103
 
71
104
  # "temperature=0.6 (hosts.work)", nil when none.
72
105
  def sampling_summary
73
- SamplingSettings.summary(target, names: names)
106
+ SamplingSettings.summary(lookup)
74
107
  rescue StandardError => e
75
108
  unresolved(:sampling_unresolved, e, nil)
76
109
  end
77
110
 
78
111
  # The thinking level (Thinking.resolve), the session's +own+ first.
79
112
  def thinking(own:)
80
- Thinking.resolve(target, names: names, session: own).first
113
+ Thinking.resolve(lookup, session: own).first
81
114
  rescue StandardError => e
82
115
  unresolved(:thinking_unresolved, e, Thinking::DEFAULT)
83
116
  end
@@ -85,23 +118,25 @@ module Samagotchi
85
118
  # The thinking level with where it came from (SettingExplained), nil
86
119
  # when it can't be resolved.
87
120
  def thinking_explained(own:)
88
- Thinking.explain(target, names: names, session: own)
121
+ Thinking.explain(lookup, session: own)
89
122
  rescue StandardError => e
90
123
  unresolved(:thinking_unresolved, e, nil)
91
124
  end
92
125
 
93
126
  # What a thinking level change costs the prompt cache
94
127
  # (Thinking.cache_cost); the block gives the prompt profile, asked only
95
- # on a native host. Raises.
128
+ # on a native host; a local chat host's is the one its name says. Raises.
96
129
  # @return [Symbol]
97
130
  def thinking_cache_cost(&)
98
- Thinking.cache_cost(target, profile: entry&.chat? ? nil : yield&.name)
131
+ return Thinking.cache_cost(target, inferred: ModelProfile.inferred_profile_name(bare)) if entry&.chat?
132
+
133
+ Thinking.cache_cost(target, profile: yield&.name)
99
134
  end
100
135
 
101
136
  # The reasoning_history level (ReasoningHistory.resolve), the session's
102
137
  # +own+ first.
103
138
  def reasoning_history(own:)
104
- ReasoningHistory.resolve(target, names: names, session: own).first
139
+ ReasoningHistory.resolve(lookup, session: own).first
105
140
  rescue StandardError => e
106
141
  unresolved(:reasoning_history_unresolved, e, ReasoningHistory::DEFAULT)
107
142
  end
@@ -112,7 +147,7 @@ module Samagotchi
112
147
  def reasoning_history_explained(own:, native: false)
113
148
  return nil unless native || entry&.chat?
114
149
 
115
- ReasoningHistory.explain(target, names: names, session: own)
150
+ ReasoningHistory.explain(lookup, session: own)
116
151
  rescue StandardError => e
117
152
  unresolved(:reasoning_history_unresolved, e, nil)
118
153
  end
@@ -122,7 +157,7 @@ module Samagotchi
122
157
  def reasoning_history_summary(own:)
123
158
  return nil unless entry&.chat?
124
159
 
125
- level, source = ReasoningHistory.resolve(target, names: names, session: own)
160
+ level, source = ReasoningHistory.resolve(lookup, session: own)
126
161
  source ? ReasoningHistory.label(level, source) : nil
127
162
  rescue StandardError => e
128
163
  unresolved(:reasoning_history_unresolved, e, nil)
@@ -139,7 +174,7 @@ module Samagotchi
139
174
  # [seconds, source] a cut waits for thinking (SteerCutAfter.resolve);
140
175
  # the global and its name when it can't be resolved.
141
176
  def steer_cut_after_explained
142
- SteerCutAfter.resolve(target, names: names)
177
+ SteerCutAfter.resolve(lookup)
143
178
  rescue StandardError => e
144
179
  unresolved(:steer_cut_after_unresolved, e, [SteerCut.cut_after, "steer.cut_after"])
145
180
  end
@@ -148,7 +183,7 @@ module Samagotchi
148
183
  # (LLMContextStrategy.explain) under the session's +args+
149
184
  # (LLMContextOverride#resolve_args), nil when it can't be resolved.
150
185
  def llm_context_explained(args)
151
- LLMContextStrategy.explain(target, names: names, **args)
186
+ LLMContextStrategy.explain(lookup, **args)
152
187
  rescue StandardError => e
153
188
  unresolved(:llm_context_unresolved, e, nil)
154
189
  end
@@ -156,14 +191,14 @@ module Samagotchi
156
191
  # The LLM context layers its turns run under; [] when they can't be
157
192
  # resolved.
158
193
  def llm_context_layers(args)
159
- LLMContextStrategy.resolve(target, names: names, **args).active_layers
194
+ LLMContextStrategy.resolve(lookup, **args).active_layers
160
195
  rescue StandardError => e
161
196
  unresolved(:llm_context_unresolved, e, [])
162
197
  end
163
198
 
164
199
  # models:/hosts: window_tokens (ContextWindow.setting), nil when unset.
165
200
  def window_setting
166
- ContextWindow.setting(target, names: names)
201
+ ContextWindow.setting(lookup)
167
202
  rescue StandardError => e
168
203
  unresolved(:window_unresolved, e, nil)
169
204
  end
@@ -171,7 +206,7 @@ module Samagotchi
171
206
  # The window config.yml gives this model or its host
172
207
  # (ContextWindow.setting), never asking a server: a session loaded on
173
208
  # it (Engine#session=, before a worker's Bridge serves). nil when none.
174
- def configured_context_window = ContextWindow.setting(target, names: names)
209
+ def configured_context_window = ContextWindow.setting(lookup)
175
210
 
176
211
  # The window as its loop would see it (ChatLoop#context_window: a remote
177
212
  # chat host has no /props, only its model list); nil when it can't be
@@ -183,7 +218,7 @@ module Samagotchi
183
218
  adapter = registry.adapter_for(entry)
184
219
  client = nil if adapter.remote?
185
220
  end
186
- ContextWindow.resolve(client: client, model: bare, adapter: adapter, setting: ContextWindow.setting(target, names: names))
221
+ ContextWindow.resolve(client: client, model: bare, adapter: adapter, setting: ContextWindow.setting(lookup))
187
222
  rescue StandardError => e
188
223
  unresolved(:window_unresolved, e, nil)
189
224
  end
@@ -232,7 +267,7 @@ module Samagotchi
232
267
 
233
268
  # Whether it sees images (VisionSupport.for) under +profile+.
234
269
  def vision_support(profile)
235
- VisionSupport.for(target, profile: profile, adapter: vision_adapter, names: names)
270
+ VisionSupport.for(lookup, profile: profile, adapter: vision_adapter)
236
271
  end
237
272
 
238
273
  # A chat host's adapter (what it says of images), nil on a native host.
@@ -246,7 +281,7 @@ module Samagotchi
246
281
  # Not cached here. Raises.
247
282
  # @return [ModelProfile::Resolution]
248
283
  def profile_resolution
249
- ModelProfile.resolve(names: names, entry: entry, client: client, bare_model: bare)
284
+ ModelProfile.resolve(names: names, entry: entry, client: client, bare_model: bare, models: lookup.models)
250
285
  end
251
286
 
252
287
  # See Unresolved.fallback.
@@ -60,7 +60,7 @@ module Samagotchi
60
60
  prompt_sections: @prompt_sections,
61
61
  prompt_changed: -> { prompt_changed! },
62
62
  judgments: @judgments,
63
- hook_failed: ->(hook, failed) { @hook_routes.plugin_hook_failed(hook, failed) }
63
+ hook_failed: ->(hook, failed, effect: :log) { @hook_routes.plugin_hook_failed(hook, failed, effect: effect) }
64
64
  )
65
65
  # What plugins show while they load (an MCP server that didn't
66
66
  # start) waits for the first turn, beside the load warnings: no UI
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../recap_place"
4
+
3
5
  module Samagotchi
4
6
  class Engine
5
7
  # The idle recap (IdleRecap) and the shared idle scheduler: the recap job
@@ -96,9 +98,10 @@ module Samagotchi
96
98
  @recap ? @recap.request_now : :off
97
99
  end
98
100
 
99
- # The recap saved with the current session (recap.json), and how many
100
- # user turns came after it (0: it is current).
101
- # @return [Hash, nil] {text:, covered:, turns_since:, created_at:}
101
+ # The recap saved with the current session (recap.json), how many
102
+ # user turns came after it (0: it is current), and the stamp of the
103
+ # last turn it covers (RecapPlace: where a page puts it; nil, the count).
104
+ # @return [Hash, nil] {text:, covered:, turns_since:, opening_id:, created_at:}
102
105
  def saved_recap
103
106
  state = @recap&.state
104
107
  return nil unless state
@@ -106,7 +109,8 @@ module Samagotchi
106
109
  messages = @session&.messages || []
107
110
  covered = state[:covered].to_i
108
111
  turns_since = Array(messages).drop(covered).count { |m| Steer.turn_opening?(m) }
109
- { text: state[:text], covered: covered, turns_since: turns_since, created_at: state[:created_at] }
112
+ { text: state[:text], covered: covered, turns_since: turns_since, opening_id: RecapPlace.opening_id(messages, covered),
113
+ created_at: state[:created_at] }
110
114
  end
111
115
 
112
116
  # Start the shared idle scheduler (reminders + optional recap). The recap
@@ -230,7 +230,9 @@ module Samagotchi
230
230
  @chat_backend_mutex = Mutex.new
231
231
  @side_judgments = SideJudgments.new
232
232
  @judgments = build_judgments
233
- @host_registry = host_registry || HostRegistry.new
233
+ # Taken once: a running worker keeps the hosts it started with. A
234
+ # client (specs) goes on a registry of its own, never the shared one.
235
+ @host_registry = host_registry || (client ? HostRegistry.new : HostRegistry.current)
234
236
  # The turn stages over the objectives (its audit, view and proposals)
235
237
  # and the Verify/Revise anchors (TurnStages).
236
238
  @objectives_stage = build_objectives_stage
@@ -355,12 +357,7 @@ module Samagotchi
355
357
  # toolkit, the tool index, peer sources), then the plugins themselves
356
358
  # unless +load+ is false (see #initialize for when).
357
359
  def build_tools_and_plugins(load:, task_finish_notes:)
358
- # The routes are built already (#initialize), so a bundle's hook file
359
- # that raises at fire time is told the same way a chi.on handler is:
360
- # once per hook in this worker. The guard covers a hook that raises
361
- # before the routes are there (nothing in the Engine's path does).
362
- @extension_load = ExtensionLoad.new(hook_failures: @guardrail_failures,
363
- hook_failed: ->(hook, failed, effect: nil) { @hook_routes&.plugin_hook_failed(hook, failed, effect: effect) })
360
+ @extension_load = ExtensionLoad.new(hook_failures: @guardrail_failures)
364
361
  @hooks = @extension_load.hooks
365
362
  # The tools this session offers (the prompts' declarations and the
366
363
  # kernel's dispatch): the built-ins, per Engine. execute.description
@@ -718,7 +715,10 @@ module Samagotchi
718
715
  # (its limit goes in last_turn). The end event names its turn
719
716
  # (`turn_id:`, as :turn_started does) and, for a Verify turn,
720
717
  # `verify: true` (its opening's stamp: a page watching it live offers
721
- # no Revise on its answer, verdict or not, as a reload does).
718
+ # no Revise on its answer, verdict or not, as a reload does). When the
719
+ # turn keeps messages, the event also names its own range in them,
720
+ # `kept[kept_from...kept_to]` (display-side: Bridge::TurnDrawings
721
+ # reads it; no message gains a key).
722
722
  def end_turn(turn, outcome, save: false, exhausted: false)
723
723
  seconds = turn.elapsed
724
724
  turn.outcome = outcome
@@ -727,6 +727,7 @@ module Samagotchi
727
727
  event[:turn_id] = turn.id
728
728
  event[:verify] = true if turn.verify
729
729
  revises = @verify_anchors.stamp_opening(kept, turn, outcome)
730
+ kept_range!(event, turn, kept)
730
731
  replace_session_messages(turn.session, kept) if kept
731
732
  verdict = @verify_anchors.record_verdict(turn, outcome)
732
733
  event[:verdict] = verdict if verdict
@@ -744,6 +745,20 @@ module Samagotchi
744
745
  end
745
746
  private :end_turn
746
747
 
748
+ # The end event's kept_from/kept_to (#end_turn): only when the turn
749
+ # keeps messages and its start is known (#turn_messages ran). An ending
750
+ # that replaces an earlier turn's note at the history's tail
751
+ # (TurnNote.replace_trailing on a Continue stopped before its first step)
752
+ # leaves only system messages and its own note after it, so the range
753
+ # still starts at or before the turn's note and draws nothing else.
754
+ def kept_range!(event, turn, kept)
755
+ return unless kept && turn.history_size
756
+
757
+ event[:kept_from] = turn.history_size
758
+ event[:kept_to] = kept.size
759
+ end
760
+ private :kept_range!
761
+
747
762
  # What a notice may stand in for (hook_notify's fallback_for:).
748
763
  NOTICE_FALLBACKS = HookRoutes::NOTICE_FALLBACKS
749
764
 
@@ -806,8 +821,11 @@ module Samagotchi
806
821
  # +wall+: the wall time it started (MessageTime.now, once): the
807
822
  # :turn_started event's at and its user message's (and reminders') stamp.
808
823
  # +verify+, +revises+: a Verify turn's claim, a Revise turn's draft.
824
+ # +history_size+: turn.messages' size under the system head, before
825
+ # the reminders and the prompt (#turn_messages): where the turn's own
826
+ # messages start in what it keeps (the end event's kept_from).
809
827
  Turn = Struct.new(:context, :prompt, :continue, :on_event, :started_at, :messages, :ended, :prepared_settings,
810
- :limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome) do
828
+ :limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome, :history_size) do
811
829
  # The turn's id (:turn_started's turn_id, its metrics record's id and
812
830
  # its saved prompt's), session, origin and controller: its context's.
813
831
  def id = context.id
@@ -1124,6 +1142,9 @@ module Samagotchi
1124
1142
  # Built once per Engine (and again after a model switch) so the prompt
1125
1143
  # prefix, and the model server's KV cache for it, stay stable.
1126
1144
  turn.messages = ContextNote.with_system_head(turn.messages, { role: "system", content: system_prompt })
1145
+ # Where the turn's own messages start (the end event's kept_from):
1146
+ # after the head this may have added, before the reminders and prompt.
1147
+ turn.history_size = turn.messages.size
1127
1148
  # Explicit --memory preloads are now known after system prompt build.
1128
1149
  @used_memories.add(activated_memory_names).absorb(session)
1129
1150
 
@@ -13,8 +13,9 @@ module Samagotchi
13
13
  # (#kept_turn?).
14
14
  TURN_KEPT = %i[turn_completed turn_canceled].freeze
15
15
  # The kinds of a running turn's parts (Bridge::TurnAccumulator), which a
16
- # joining UI replays as live events.
17
- PART_KINDS = %w[generation thinking text tool input steer reminder notice].freeze
16
+ # joining UI replays as live events (a card part only in a finished
17
+ # turn's drawing: a join takes its cards from CardStore).
18
+ PART_KINDS = %w[generation thinking text tool input steer reminder notice card].freeze
18
19
  # Types no Engine emits that a UI handles: the Bridge's stream frames
19
20
  # and the web's one synthetic replay event (a prompt merged into the
20
21
  # turn, turn_events.js snapshotEvents).
@@ -7,20 +7,13 @@ require_relative "plugin/loader"
7
7
 
8
8
  module Samagotchi
9
9
  # What an Engine loads from outside chi as it starts: config.yml's hooks,
10
- # installed bundles' hooks and plugins, and config.yml `bundles:` (their
11
- # settings). It holds what plugins showed while they loaded (notices,
10
+ # installed bundles' plugins, and config.yml `bundles:` (their settings). It holds what plugins showed while they loaded (notices,
12
11
  # cards): no UI is there yet, so the Engine announces them on the first
13
12
  # turn (Engine#announce_load_events!).
14
13
  class ExtensionLoad
15
14
  # @param hook_failures [Guardrails::LoadFailures] where hooks that fail to load go
16
- # @param hook_failed [#call, nil] (label, "Class: message", effect:) when
17
- # an installed bundle's hook file raises at fire time: the Engine's
18
- # HookRoutes, so the user is told once per hook. nil where there is no
19
- # UI to tell (the test kit's sandbox): a hook that raises then says
20
- # nothing, and raises nothing.
21
- def initialize(hook_failures:, hook_failed: nil)
15
+ def initialize(hook_failures:)
22
16
  @hook_failures = hook_failures
23
- @hook_failed = hook_failed
24
17
  @loading = false
25
18
  @held_events = []
26
19
  end
@@ -40,27 +33,30 @@ module Samagotchi
40
33
  end
41
34
 
42
35
  # The hooks: config.yml's (Hooks::Loader; an empty Registry without a
43
- # hooks config), then the installed bundles' added to them.
36
+ # hooks config). An installed record that still has class-file hooks (a
37
+ # 0.157 install) adds a not-required failure each, which the Engine
38
+ # announces at the first turn: they don't load (#tell_retired_hooks).
44
39
  # @return [Hooks::Registry]
45
40
  def hooks
46
- registry = hooks_from_config
47
- add_bundle_hooks(registry)
48
- registry
41
+ tell_retired_hooks
42
+ hooks_from_config
49
43
  end
50
44
 
51
45
  # Load the installed bundles' plugins into +registries+; one that fails
52
- # goes to +failures+, and the rest still load. The block runs after them,
53
- # still loading: what it shows is held with theirs.
46
+ # goes to +failures+, a required one (a guardrail plugin) to the hook
47
+ # failures, where it denies every tool call, and the rest still load.
48
+ # The block runs after them, still loading: what it shows is held with
49
+ # theirs.
54
50
  def load_plugins(registries, failures:)
55
51
  @held_events = []
56
52
  @loading = true
57
- Plugin::Loader.load_installed(registries, failures: failures, settings: bundle_settings).tap { yield if block_given? }
53
+ Plugin::Loader.load_installed(registries, failures: failures, guardrail_failures: @hook_failures, settings: bundle_settings)
54
+ .tap { yield if block_given? }
58
55
  ensure
59
56
  @loading = false
60
57
  end
61
58
 
62
- # config.yml `bundles:` (ConfigFile.bundle_settings), read once: the
63
- # bundle hooks and the plugins both want it.
59
+ # config.yml `bundles:` (ConfigFile.bundle_settings), read once.
64
60
  def bundle_settings
65
61
  @bundle_settings ||= Samagotchi::ConfigFile.bundle_settings
66
62
  end
@@ -75,36 +71,22 @@ module Samagotchi
75
71
  Hooks::Registry.new
76
72
  end
77
73
 
78
- def add_bundle_hooks(registry)
74
+ # One failure per record whose hooks: map isn't empty; an empty
75
+ # hooks/ dir is not one, nor a manifest.json that doesn't read (the
76
+ # plugin loader and the rules report that: a record holding only hooks/
77
+ # with an unreadable manifest.json is silent at start, as its hooks
78
+ # wouldn't load anyway; `chi bundle status` lists it unreadable).
79
+ def tell_retired_hooks
79
80
  require_relative "memory_bundle/provenance"
80
- settings = bundle_settings
81
81
  MemoryBundle::Provenance.each_installed(holding: :hooks) do |bundle_name, bundle|
82
- if bundle.error?
83
- Log.warn(:hooks, "bundle_manifest_invalid", echo: "[samagotchi:hooks] bundle '#{bundle_name}': #{bundle.error}; its hooks are not loaded",
84
- bundle: bundle_name)
85
- @hook_failures.add("hooks (bundle #{bundle_name})", bundle.error, required: false)
86
- next
87
- end
88
- bundle_dir = File.join(MemoryBundle::Provenance.bundles_dir, bundle_name)
89
- hooks_dir = File.join(bundle_dir, "hooks")
90
- manifest = bundle.manifest
91
- if manifest.experimental?
92
- Log.info(:hooks, "experimental_bundle", echo: "[hooks] Bundle '#{bundle_name}' is experimental — its hooks may change or misbehave.", bundle: bundle_name)
93
- end
94
- begin
95
- Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata: manifest.hooks, registry: registry,
96
- failures: @hook_failures, settings: settings[bundle_name.to_s] || {},
97
- manifest: manifest, hook_failed: @hook_failed)
98
- rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
99
- raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
82
+ next if bundle.error?
100
83
 
101
- Log.error(:hooks, "bundle_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' failed to load hooks: #{e.class}: #{e.message}", bundle: bundle_name, error: e.class.name)
102
- end
84
+ Log.warn(:hooks, "bundle_hooks_retired", bundle: bundle_name, hooks: bundle.hooks.size)
85
+ @hook_failures.add("hooks (bundle #{bundle_name})", MemoryBundle::Manifest::HooksRetired::WORDS, required: false)
103
86
  end
104
- rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
105
- raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
106
-
107
- Log.error(:hooks, "bundles_load_failed", echo: "[samagotchi:hooks] failed to load bundle hooks: #{e.class}: #{e.message}", error: e.class.name)
87
+ rescue StandardError => e
88
+ Log.error(:hooks, "bundles_load_failed", echo: "[samagotchi:hooks] failed to read the installed bundles: #{e.class}: #{e.message}",
89
+ error: e.class.name)
108
90
  end
109
91
  end
110
92
  end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Samagotchi
4
4
  module Guardrails
5
- # What failed to load at Engine start: hooks, bundle hook checksums,
6
- # rule files. A required one makes the gate deny every tool call (a
5
+ # What failed to load at Engine start: config hooks, a required
6
+ # plugin, rule files, an older chi's class-file bundle hooks (which no
7
+ # longer load). A required one makes the gate deny every tool call (a
7
8
  # guardrail that silently vanished is the failure this guards against);
8
9
  # the rest are only announced.
9
10
  class LoadFailures
@@ -153,6 +153,17 @@ module Samagotchi
153
153
  (low == low_dir || low.start_with?(File.join(low_dir, ""))) && folds_case?(dir)
154
154
  end
155
155
 
156
+ # Whether +root+ is the home folder or a folder above it (symlinks
157
+ # resolved on both; false for a blank one). ReviewPaths (a participant
158
+ # turn's project) and MemoryPaths (where a chi project marker is
159
+ # ignored) ask this.
160
+ # @param root [String, nil] @param home [String, nil]
161
+ def home_root?(root, home)
162
+ return false if home.to_s.empty? || root.to_s.empty?
163
+
164
+ under?(real(home.to_s), real(root.to_s))
165
+ end
166
+
156
167
  # Whether +path+ is in +dir+, not +dir+ itself (.under?'s terms).
157
168
  def strictly_under?(path, dir, fold: true)
158
169
  return true if path.start_with?(File.join(dir, ""))
@@ -59,11 +59,7 @@ module Samagotchi
59
59
  # review turn reads nothing there (#refusal: HOME_ROOT); a pair turn
60
60
  # asks the owner before every write (PairBoundary).
61
61
  # @param root [String] @param home [String, nil]
62
- def self.home_root?(root, home)
63
- return false if home.to_s.empty? || root.to_s.empty?
64
-
65
- PathResolve.under?(PathResolve.real(home.to_s), PathResolve.real(root.to_s))
66
- end
62
+ def self.home_root?(root, home) = PathResolve.home_root?(root, home)
67
63
 
68
64
  # @param root [String, nil] the session's project root (the work tree
69
65
  # top of its folder); nil or blank refuses every path
@@ -190,7 +190,7 @@ module Samagotchi
190
190
  threshold = threshold(options[:threshold])
191
191
  return usage_error(threshold) if threshold.is_a?(String)
192
192
 
193
- client = @client || SystemOne::Client.from_config(host_registry: HostRegistry.new, mode: Config.get("guardrails.system_one.mode"), model: options[:model])
193
+ client = @client || SystemOne::Client.from_config(host_registry: HostRegistry.current, mode: Config.get("guardrails.system_one.mode"), model: options[:model])
194
194
  if client.nil?
195
195
  error_line("chi guardrails replay: no System 1 client — set guardrails.system_one.mode to audit or on " \
196
196
  "and system_one.host in config.yml")
@@ -21,7 +21,7 @@ module Samagotchi
21
21
  # How much of a failed hook's error #plugin_hook_failed shows.
22
22
  HOOK_FAILURE_CHARS = 300
23
23
  # What the notice says the hook now does, by the policy Hooks.wrap gave it
24
- # (a chi.on handler is always :log; a bundle's hook file any of the three).
24
+ # (a chi.on handler :log, or :deny for an on_error: :fail_closed one).
25
25
  HOOK_FAILURE_EFFECTS = {
26
26
  log: "it's skipped, and only the log has its next failures",
27
27
  skip: "it's skipped, and its next failures are silent",
@@ -98,7 +98,7 @@ module Samagotchi
98
98
  nil
99
99
  end
100
100
 
101
- # A bundle hook file or a chi.on handler raised (Hooks.wrap; the log has
101
+ # A chi.on handler raised (Hooks.wrap; the log has
102
102
  # every firing of a :log one). Its first failure on this Engine, so in
103
103
  # this worker, tells the user, as a plugin that failed to load does; then
104
104
  # only the log. One line per label — file, bundle and event — so a