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
@@ -39,7 +39,7 @@ module Samagotchi
39
39
  List installed bundles and the ones shipped with chi that aren't installed.
40
40
 
41
41
  build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
42
- Build local memories and installed hooks (and plugin) into a shareable bundle (dir or zip).
42
+ Build local memories and an installed plugin, rules and scripts into a shareable bundle (dir or zip).
43
43
  --scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
44
44
  FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones);
45
45
  a named memory brings its model overlays (<name>.<model-key>.md).
@@ -75,7 +75,7 @@ module Samagotchi
75
75
  It also installs a bundle whose requires_chi this chi doesn't meet: without --force that install is
76
76
  refused, and the bundle is not written anywhere.
77
77
  install --dev PATH
78
- Install the bundle in the local directory PATH as a link to it: its plugin, hooks, rules and scripts
78
+ Install the bundle in the local directory PATH as a link to it: its plugin, rules and scripts
79
79
  are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
80
80
  their sha256 isn't checked, for this bundle only. Its memories are copied; re-install with --force to
81
81
  pick up memory edits. Upgrade refuses it; uninstall removes the link, never PATH's files.
@@ -90,7 +90,7 @@ module Samagotchi
90
90
  Usage: chi bundle build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
91
91
 
92
92
  build [--scope system|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]
93
- Build local memories and installed hooks into a shareable bundle (dir or zip).
93
+ Build local memories and an installed plugin, rules and scripts into a shareable bundle (dir or zip).
94
94
  --scope selects source dir (default: system). --out inferred from extension; default <name>.zip.
95
95
  FILES... optional allowlist of *.md basenames to include (default: all but installed bundles' ones);
96
96
  a named memory brings its model overlays (<name>.<model-key>.md).
@@ -200,7 +200,7 @@ module Samagotchi
200
200
 
201
201
  installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, dev: dev)
202
202
  run_installer(installer, failed: "Install") do |manifest|
203
- print_hooks_and_plugin(manifest)
203
+ print_plugin(manifest)
204
204
  print_provenance(bundle_name, manifest)
205
205
  end
206
206
  end
@@ -236,7 +236,7 @@ module Samagotchi
236
236
 
237
237
  unless Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).installed?
238
238
  @stderr.puts "Bundle '#{bundle_name}' not installed — falling back to install"
239
- # (no Hooks/Plugin lines here)
239
+ # (no Plugin line here)
240
240
  installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, dry_run: dry_run)
241
241
  return run_installer(installer, failed: "Install") do |manifest|
242
242
  next dry_run_done if dry_run
@@ -246,7 +246,7 @@ module Samagotchi
246
246
  end
247
247
  installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, upgrade: true, dry_run: dry_run)
248
248
  run_installer(installer, failed: "Upgrade") do |manifest|
249
- print_hooks_and_plugin(manifest, plugin: !dry_run)
249
+ print_plugin(manifest, plugin: !dry_run)
250
250
  next dry_run_done if dry_run
251
251
 
252
252
  # Its summary said so, to someone reading.
@@ -304,7 +304,7 @@ module Samagotchi
304
304
  # Runs +installer+, prints its summary, then yields the manifest.
305
305
  # Ensures the extracted source dir is cleaned up even if an exception
306
306
  # occurs between installer.run and the block (e.g. summary,
307
- # print_hooks_and_plugin). The block (upgrade_conflicts) still sees
307
+ # print_plugin). The block (upgrade_conflicts) still sees
308
308
  # the files because cleanup happens after the block returns or raises.
309
309
  # @return [Integer] the block's exit status, or 1 after "<failed> failed: …"
310
310
  def run_installer(installer, failed:)
@@ -318,8 +318,7 @@ module Samagotchi
318
318
  installer.cleanup_source!
319
319
  end
320
320
 
321
- def print_hooks_and_plugin(manifest, plugin: true)
322
- @stdout.puts "Hooks: #{manifest.hooks.size} hook(s) (#{manifest.hooks.keys.join(", ")})" if manifest&.hooks&.any?
321
+ def print_plugin(manifest, plugin: true)
323
322
  return unless plugin && manifest&.plugin
324
323
 
325
324
  # Installed with --force past its requires_chi: the Engine won't load it.
@@ -425,8 +424,6 @@ module Samagotchi
425
424
  puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
426
425
  @stdout.puts uninstaller.trash_copy_line if uninstaller.trash_copy_line
427
426
  @stdout.puts "Removed: #{uninstaller.removed_files.join(", ")}" unless uninstaller.removed_files.empty?
428
- hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
429
- @stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
430
427
  uninstaller.warnings.each { |w| @stdout.puts w }
431
428
  0
432
429
  rescue Samagotchi::MemoryBundle::Uninstaller::UninstallError => e
@@ -519,22 +516,14 @@ module Samagotchi
519
516
  @stdout.puts " #{k}: #{label}"
520
517
  end
521
518
  @stdout.puts " #{manifest_line(st.provenance)}"
522
- # Hooks
523
519
  manifest = st.provenance.manifest
524
- hooks = manifest.hooks
525
520
  trust = manifest.trust_level || "experimental"
526
521
  commit = st.provenance.source_commit
527
- @stdout.puts " trust_level: #{trust}" if hooks.any? || trust
522
+ @stdout.puts " trust_level: #{trust}"
528
523
  @stdout.puts " source_commit: #{commit}" if commit
529
- if hooks.any?
530
- @stdout.puts " Hooks (#{hooks.size}):"
531
- hooks.each do |hk, hook|
532
- hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk)
533
- exists = File.exist?(hook_path) ? "ok" : "missing"
534
- @stdout.puts " #{hk}: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
535
- end
536
- @stdout.puts " requires_chi: #{manifest.requires_chi}" if st.hooks_requires_failure
537
- @stdout.puts " not loaded: #{st.hooks_requires_failure}" if st.hooks_requires_failure
524
+ unless st.retired_hooks.empty?
525
+ @stdout.puts " Hooks (#{st.retired_hooks.size}): not loaded: #{Samagotchi::MemoryBundle::Manifest::HooksRetired::WORDS}"
526
+ st.retired_hooks.each { |file, state| @stdout.puts " #{file} [#{state}]" }
538
527
  end
539
528
  if (plugin = st.plugin)
540
529
  note = plugin[:state] == "modified" ? " (edited after install: not loaded; reinstall the bundle)" : ""
@@ -555,11 +544,10 @@ module Samagotchi
555
544
 
556
545
  st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
557
546
  mods = st.files.values.count { |v| v[:behind] || v[:missing] || v[:unreadable] }
558
- hooks_count = st.provenance.manifest.hooks.size
559
- hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
547
+ hook_info = st.retired_hooks.empty? ? "" : " hooks=#{st.retired_hooks.size}"
560
548
  plugin = st.plugin
561
549
  mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
562
- mods += 1 if st.hooks_requires_failure
550
+ mods += 1 unless st.retired_hooks.empty?
563
551
  mods += 1 if st.scope_error
564
552
  plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
565
553
  includes = st.provenance.includes
@@ -606,15 +594,12 @@ module Samagotchi
606
594
  rescue ArgumentError => e
607
595
  @stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
608
596
  end
609
- hooks = record.manifest.hooks
610
597
  plugin_path = prov.plugin_path(record)
611
598
  plugin_file = plugin_path && File.basename(plugin_path)
612
599
  if file_arg
613
- # Try file first, then hook
600
+ # A memory first, then the plugin (an older chi's hook is no file of the bundle's)
614
601
  if record.owns?(file_arg)
615
602
  diff_memory(prov, target_dir, file_arg)
616
- elsif hooks.key?(file_arg)
617
- diff_hook(prov, file_arg, hooks[file_arg])
618
603
  elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
619
604
  diff_plugin(prov, record, plugin_path)
620
605
  else
@@ -627,10 +612,6 @@ module Samagotchi
627
612
  diff_memory(prov, target_dir, key)
628
613
  @stdout.puts ""
629
614
  end
630
- hooks.each do |key, hook|
631
- diff_hook(prov, key, hook)
632
- @stdout.puts ""
633
- end
634
615
  if plugin_file
635
616
  diff_plugin(prov, record, plugin_path)
636
617
  @stdout.puts ""
@@ -639,9 +620,12 @@ module Samagotchi
639
620
  0
640
621
  end
641
622
 
623
+ # The base and the file; a marked one also the bundle's text it stays
624
+ # behind (its source-less FileState says which).
642
625
  def diff_memory(prov, target_dir, key)
643
- print_base_and_current(key, prov.base_path(key), File.join(target_dir, key))
644
- print_incoming(prov, key)
626
+ state = Samagotchi::MemoryBundle::FileState.of(key, record: prov.record, provenance: prov, target_dir: target_dir, source_dir: nil)
627
+ print_base_and_current(key, state.paths[:base], state.paths[:current])
628
+ print_incoming(prov, key) if state.marked?
645
629
  end
646
630
 
647
631
  # The bundle's text a file stays behind (Provenance#incoming_path), so the
@@ -663,11 +647,6 @@ module Samagotchi
663
647
  end
664
648
  end
665
649
 
666
- def diff_hook(prov, key, hook)
667
- print_base_and_current("hooks/#{key}", prov.base_path(key), File.join(prov.hooks_dir, key))
668
- @stdout.puts hook_metadata_line(hook)
669
- end
670
-
671
650
  def diff_plugin(prov, record, plugin_path)
672
651
  file = File.basename(plugin_path)
673
652
  print_base_and_current("plugin/#{file}", prov.plugin_base_path(file), plugin_path)
@@ -730,10 +709,6 @@ module Samagotchi
730
709
  "includes=#{here.join(",")}#{" left out=#{gone.join(",")}" unless gone.empty?}"
731
710
  end
732
711
 
733
- def hook_metadata_line(hook)
734
- "--- metadata: #{hook.events_field} on_error=#{hook.on_error} priority=#{hook.priority} sha256=#{hook.sha256}"
735
- end
736
-
737
712
  def build(rest)
738
713
  parsed = parse_flags(BUILD_FLAGS, rest, "build", help: BUILD_HELP)
739
714
  return parsed if parsed.is_a?(Integer)
@@ -1,17 +1,14 @@
1
1
  ---
2
2
  name: known-names
3
- version: 0.1.9
3
+ version: 0.2.0
4
4
  requires_chi: ">= 0.150.0"
5
5
  scope: system
6
6
  description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
7
7
  trust_level: reviewed
8
8
  files:
9
9
  known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
10
- hooks:
11
- known_names.rb:
12
- sha256: sha256:d7eed0dd0f418d8099a147ed124c6e6062b8c752035dd6b5aa5e926e29941c8b
13
- event: before_tool_call
14
- on_error: log
15
- priority: 50
10
+ plugin:
11
+ file: plugin.rb
12
+ sha256: sha256:d140612e6c34a9d2d205bf818a2a0797ad1e133829481f354aaa9e56698017e3
16
13
  # config.yml bundles: known-names: (another key warns; an older chi ignores this)
17
14
  settings: [names, ignore, mode, derive, min_length, max_distance]
@@ -243,3 +243,20 @@ class KnownNames
243
243
  Array(value).map { |name| name.to_s.strip }.reject(&:empty?)
244
244
  end
245
245
  end
246
+
247
+ # What chi loads: one KnownNames, built once from config.yml
248
+ # `bundles: known-names:` when the plugin loads, on before_tool_call at
249
+ # priority 50. A raise in the handler is logged and the call goes on
250
+ # (chi.on's log policy); KnownNames#call turns its own errors into a
251
+ # notice first.
252
+ class Plugin
253
+ PRIORITY = 50
254
+
255
+ def initialize(settings = {})
256
+ @names = KnownNames.new(settings)
257
+ end
258
+
259
+ def register(chi)
260
+ chi.on(:before_tool_call, priority: PRIORITY) { |event, _ctx| @names.call(event) }
261
+ end
262
+ end
@@ -1,17 +1,14 @@
1
1
  ---
2
2
  name: source-links
3
- version: 0.6.3
3
+ version: 0.7.0
4
4
  scope: system
5
5
  description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, refs in a waiting question's reply, and a one-line note after the turn (note false turns it off); sources are configured patterns
6
6
  trust_level: reviewed
7
7
  requires_chi: ">= 0.150.0"
8
8
  files:
9
9
  source_links.md: sha256:ace01a0f7c308a0ae54ece714c31c0e6d7a49c372d98111030e6d268320bed3a
10
- hooks:
11
- source_links.rb:
12
- sha256: sha256:21d7a9f7c381c653b126b625feed0e0b6422eaa84c8dfe716b6c5b41644a33b3
13
- events: [after_turn, question_asked]
14
- on_error: log
15
- priority: 90
10
+ plugin:
11
+ file: plugin.rb
12
+ sha256: sha256:b4a777d8b6135f80e248dd21b5370c91dfcf2aea17057734e77241eb9cc4afe4
16
13
  # config.yml bundles: source-links: (another key warns; an older chi ignores this)
17
14
  settings: [sources, max, note]
@@ -59,10 +59,10 @@ require "samagotchi/ref"
59
59
  # action, "Open in <name>", the ref's URL. A URL that isn't http(s) stays a
60
60
  # markdown link only. The web's ref menu shows the action.
61
61
  #
62
- # The hook is on :question_asked too (the manifest's events:, chi newer than
63
- # 0.97.0): while a question of the model's (or an approval of its call)
64
- # waits, the refs in the reply that asks it, and in the question's text and
65
- # options, are refs on that reply, the same as an answer's. The web pairs
62
+ # The hook is on :question_asked too (Plugin below hooks both events): while
63
+ # a question of the model's (or an approval of its call) waits, the refs in
64
+ # the reply that asks it, and in the question's text and options, are refs
65
+ # on that reply, the same as an answer's. The web pairs
66
66
  # them in the stage's asking slot, on the question's card and on that step
67
67
  # in the history (a ref only in the question's text pairs there alone). No
68
68
  # links in the text and no note line for it: the turn's end has those.
@@ -657,3 +657,22 @@ class SourceLinks
657
657
  value.to_s.gsub(/[^A-Za-z0-9\-._~]/) { |c| c.bytes.map { |b| format("%%%02X", b) }.join }
658
658
  end
659
659
  end
660
+
661
+ # What chi loads: one SourceLinks, built once from config.yml
662
+ # `bundles: source-links:` when the plugin loads, on after_turn and
663
+ # question_asked at priority 90 (its state, the remotes it asked git for,
664
+ # is shared). A raise in the handler is logged and the turn goes on
665
+ # (chi.on's log policy).
666
+ class Plugin
667
+ PRIORITY = 90
668
+
669
+ def initialize(settings = {})
670
+ @links = SourceLinks.new(settings)
671
+ end
672
+
673
+ def register(chi)
674
+ %i[after_turn question_asked].each do |event|
675
+ chi.on(event, priority: PRIORITY) { |payload, _ctx| @links.call(payload) }
676
+ end
677
+ end
678
+ end
@@ -76,7 +76,7 @@ guardrails:
76
76
  disable: [guardrails:git-rebase]
77
77
  ```
78
78
 
79
- - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's hook/plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`). Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
79
+ - `bundles:` map of installed bundle name → its settings (one Hash, string keys, handed to the bundle's plugin `initialize(settings)`). Read when a session's Engine starts: after a change restart the session's worker (`chi sessions stop <id>`). Unknown keys are ignored by the bundle, not validated. `chi bundle list` names the bundles; each bundle's keys are in `docs/bundles.md` / `docs/guardrails.md`.
80
80
 
81
81
  ```yaml
82
82
  bundles:
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.156.0
3
+ version: 0.158.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
- self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
9
- config_modification_protocol.md: sha256:25ed2d4901f00eeea226db5eb2b75a05d6cdee4015ac8daa4bed624f745c3922
10
- memory_guide.md: sha256:8b7727b939b9c04486ca59859561023dcc319a73e0f5b47b3fcf5741edaa69e2
8
+ self_map.md: sha256:750822b3488b302c6e32638e6224f270d8e06b50ea85cd6e0bb5a6ff12eed1d8
9
+ config_modification_protocol.md: sha256:44cf7bdac8e526805bff36b76b54fefecd41db30ca525c2ed661d78fe9155dd8
10
+ memory_guide.md: sha256:91e8fd248a155afad6b825097d9ee0a0215ba0b79092c8ab25501c0fc19dedfc
11
11
  delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
@@ -15,7 +15,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
15
15
  | `system` | `$XDG_CONFIG_HOME/samagotchi/memories/`, default `~/.config/samagotchi/memories/` (`MemoryPaths.system_dir`) | User-wide preferences, identity, cross-project knowledge |
16
16
  | `project` | `<system dir>/projects/<basename>_<hash>/` (`MemoryPaths.project_dir`: `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`) | Repo-specific conventions, workflow, stack decisions |
17
17
 
18
- - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows that folder (not the root: in a linked worktree, work stays in the worktree). Outside a repository the root is the working directory. A separate clone is a different project.
18
+ - The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows that folder (not the root: in a linked worktree, work stays in the worktree). A separate clone is a different project. Outside a repository, a folder marked by `chi project init` (its `.chi/project.yml`) is the project for it and every subfolder; outside both, the root is the working directory.
19
19
  - `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`; a memory a bundle installed adds `· from <bundle>` before the description. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
20
20
 
21
21
  ## Tools you have
@@ -101,11 +101,11 @@ Writing or updating a toolkit memory is free like writing a skill; say so in the
101
101
 
102
102
  ## Memory Bundles — shareable packs
103
103
 
104
- Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and any of: memories (`*.md`), `hooks/*.rb` (bundle hooks, `docs/hooks.md`), `guardrails/*.yml` (rules, `docs/guardrails.md`), a `plugin.rb` (commands, tools, hooks, services; `docs/plugins.md`). They are shareable and installable.
104
+ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and any of: memories (`*.md`), `guardrails/*.yml` (rules, `docs/guardrails.md`), a `plugin.rb` (commands, tools, hooks, services; `docs/plugins.md`; class-file `hooks/*.rb` are retired: chi refuses a source with them, `docs/hooks.md`). They are shareable and installable.
105
105
 
106
- - Manifest: `files:` (memory → `sha256:`), `hooks:` (file → sha256/event/on_error/priority), `plugin: {file: plugin.rb, sha256: sha256:…}`, `requires_chi: ">= 0.1.30"` (gem-style), `needs:` (outside commands looked up on `PATH`, advisory; `docs/memory.md` "Bundles that need outside commands").
107
- - Integrity: each file's sha256 is recorded at install; a hook/plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
108
- - Installing = trusting its Ruby code (hooks, plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
106
+ - Manifest: `files:` (memory → `sha256:`), `plugin: {file: plugin.rb, sha256: sha256:…}`, `requires_chi: ">= 0.1.30"` (gem-style), `needs:` (outside commands looked up on `PATH`, advisory; `docs/memory.md` "Bundles that need outside commands").
107
+ - Integrity: each file's sha256 is recorded at install; a plugin/rule file changed afterwards is not loaded (rules: every call denied) until reinstalled. It is an integrity check, not proof of authorship.
108
+ - Installing = trusting its Ruby code (the plugin), like a gem. Install only copies; the code runs at the next session start (`Engine.new`), so a running worker needs a restart to pick it up.
109
109
  - A bundle without `*.md` (btw, mcp, loop-guard) adds no line to the prompt's memory index.
110
110
  - Profiles are shipped bundles with only `includes:` (bundle names): `core` (loop-guard, check-in, guardrails, slow-commands; `chi bootstrap` installs it) and `dev` (known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer). `chi bundle install core` installs the ones not installed and records them; a bundle the user uninstalled stays out on a later install, upgrade or `chi update`, which install only bundles new to the profile. `chi bundle uninstall core` removes its recorded bundles, then core (`docs/memory.md` "Bundle profiles").
111
111
 
@@ -114,12 +114,12 @@ 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. |
121
121
  | `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
122
- | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
122
+ | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and an installed bundle's plugin, rules and scripts. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
123
123
 
124
124
  **Scope resolution for install/build:**
125
125
  - CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<basename>_<hash>` of the project root (`MemoryPaths.scope_dir`).
@@ -51,7 +51,7 @@
51
51
  normal session: it shows in `chi sessions list` with `↳ <parent>`, and the user can steer it
52
52
  with `chi --attach <id>`. While I wait for a child, its tool approvals go to my user as my own
53
53
  approvals (I see only how each was answered); see `docs/sessions.md` "Delegating".
54
- - Bundles extend me: memories, `hooks/*.rb`, `guardrails/*.yml`, and a `plugin.rb` that adds slash
54
+ - Bundles extend me: memories, `guardrails/*.yml`, and a `plugin.rb` that adds slash
55
55
  commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
56
56
  cards, side answers (`ask_model`), System 1 judgments (`chi.judgment`, `ctx.judge`), HTTP calls (`ctx.http`),
57
57
  child sessions and services (e.g. MCP server processes). Shipped: `btw` (`/btw` side question),
@@ -60,23 +60,23 @@ module Samagotchi
60
60
  move = []
61
61
  refuse = []
62
62
  SessionManager.children_of(prev_id, state_dir: state_dir).each do |row|
63
- subtree = [row[:id], *SessionManager.descendant_ids(row[:id], state_dir)]
63
+ subtree = [row.id, *SessionManager.descendant_ids(row.id, state_dir)]
64
64
  next if (subtree & open_ids).empty?
65
65
 
66
- if row[:archived]
66
+ if row.archived
67
67
  # An open delegate below an archived one: the archive's cascade
68
68
  # would take it, the move doesn't (only unarchived ones move).
69
69
  refuse.concat(open.select { |c| subtree.include?(c.id) }.map do |c|
70
- 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")
71
71
  end)
72
72
  next
73
73
  end
74
74
 
75
- why = unmovable(row[:id], state_dir: state_dir)
75
+ why = unmovable(row.id, state_dir: state_dir)
76
76
  if why
77
- 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)
78
78
  else
79
- move << row[:id]
79
+ move << row.id
80
80
  end
81
81
  end
82
82
  Plan.new(move: move, refuse: refuse)
@@ -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, saved_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
@@ -59,26 +56,15 @@ module Samagotchi
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
@@ -373,6 +373,9 @@ module Samagotchi
373
373
  # The stage view's working set (files, memories and URLs the turn brought into context): a rail in the stage, a
374
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.
375
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),
376
379
  # Quick replies next to Annotate in the page's selection bubble, "|"-separated (a YAML list works too); "" leaves only Annotate.
377
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),
378
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).
@@ -409,17 +409,19 @@ module Samagotchi
409
409
  end
410
410
 
411
411
  # A project's sources: local only (SessionAccess::Contract), this
412
- # machine's checkout.
412
+ # machine's checkout or marked folder.
413
413
  def project_target
414
- unless MemoryPaths.in_repo?(@cwd)
415
- error_line("#{command_name}: --project needs a git repository, and #{@cwd} is in none")
414
+ found = MemoryPaths.lookup(@cwd)
415
+ unless found.project?
416
+ error_line("#{command_name}: --project needs a project (a git repository, or a folder marked with " \
417
+ "`chi project init`), and #{@cwd} is in none")
416
418
  return nil
417
419
  end
418
420
 
419
- root = MemoryPaths.project_root(@cwd)
421
+ root = found.root
420
422
  # Its sources run in the root, or in this checkout when the root is a
421
423
  # git dir (ContextSources.project_cwd).
422
- Target.new(session_id: nil, project_root: root, cwd: MemoryPaths.work_tree_root(@cwd) || @cwd,
424
+ Target.new(session_id: nil, project_root: root, cwd: found.top || @cwd,
423
425
  label: "project #{File.basename(root)}")
424
426
  end
425
427