samagotchi 0.53.0 → 0.55.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -1
  3. data/README.md +1 -1
  4. data/bin/chi +3 -2
  5. data/docs/architecture.md +94 -54
  6. data/docs/configuration.md +30 -2
  7. data/docs/context.md +104 -9
  8. data/docs/internals/background-tasks.md +1 -1
  9. data/docs/internals/bundles.md +73 -0
  10. data/docs/internals/context-telemetry.md +27 -6
  11. data/docs/internals/engine.md +55 -24
  12. data/docs/internals/tool-guardrails.md +8 -6
  13. data/docs/plugins.md +2 -1
  14. data/docs/sessions.md +3 -3
  15. data/docs/testing.md +23 -3
  16. data/lib/samagotchi/bridge/turn_accumulator.rb +8 -6
  17. data/lib/samagotchi/bundle_command.rb +31 -36
  18. data/lib/samagotchi/bundle_needs.rb +4 -5
  19. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  20. data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
  21. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  22. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  23. data/lib/samagotchi/child_move.rb +6 -4
  24. data/lib/samagotchi/config.rb +2 -1
  25. data/lib/samagotchi/context_command.rb +1 -1
  26. data/lib/samagotchi/context_providers.rb +11 -14
  27. data/lib/samagotchi/context_status.rb +10 -18
  28. data/lib/samagotchi/engine.rb +117 -134
  29. data/lib/samagotchi/extension_load.rb +102 -0
  30. data/lib/samagotchi/file_ref.rb +59 -0
  31. data/lib/samagotchi/guardrail_wiring.rb +16 -16
  32. data/lib/samagotchi/kernel_loop.rb +5 -3
  33. data/lib/samagotchi/llm/chat_loop.rb +5 -4
  34. data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
  35. data/lib/samagotchi/memory_bundle/builder.rb +170 -178
  36. data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
  37. data/lib/samagotchi/memory_bundle/installer.rb +252 -488
  38. data/lib/samagotchi/memory_bundle/listing.rb +6 -6
  39. data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
  40. data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
  41. data/lib/samagotchi/memory_bundle/profile.rb +21 -21
  42. data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
  43. data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
  44. data/lib/samagotchi/memory_bundle/status.rb +22 -24
  45. data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
  46. data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
  47. data/lib/samagotchi/plugin/loader.rb +8 -8
  48. data/lib/samagotchi/self_report.rb +2 -2
  49. data/lib/samagotchi/send_command.rb +1 -1
  50. data/lib/samagotchi/session_continue.rb +275 -0
  51. data/lib/samagotchi/session_manager.rb +9 -253
  52. data/lib/samagotchi/session_metrics.rb +3 -1
  53. data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
  54. data/lib/samagotchi/terminal_ui.rb +12 -2
  55. data/lib/samagotchi/tool_activity.rb +84 -14
  56. data/lib/samagotchi/tool_row_fields.rb +46 -0
  57. data/lib/samagotchi/tool_runner.rb +7 -11
  58. data/lib/samagotchi/version.rb +1 -1
  59. data/lib/samagotchi/web/app.rb +26 -3
  60. data/lib/samagotchi/web/capabilities.rb +13 -9
  61. data/lib/samagotchi/web/editor.rb +38 -0
  62. data/lib/samagotchi/web/message_parts.rb +5 -10
  63. data/lib/samagotchi/web/public/activity.js +35 -18
  64. data/lib/samagotchi/web/public/app.js +240 -1136
  65. data/lib/samagotchi/web/public/commands.js +236 -0
  66. data/lib/samagotchi/web/public/composer.js +101 -0
  67. data/lib/samagotchi/web/public/ctx.js +18 -4
  68. data/lib/samagotchi/web/public/diff_view.js +30 -4
  69. data/lib/samagotchi/web/public/index.html +9 -2
  70. data/lib/samagotchi/web/public/info_bar.js +78 -0
  71. data/lib/samagotchi/web/public/lists_view.js +725 -0
  72. data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
  73. data/lib/samagotchi/web/public/question_card.js +9 -1
  74. data/lib/samagotchi/web/public/question_cards.js +7 -4
  75. data/lib/samagotchi/web/public/refs.js +134 -0
  76. data/lib/samagotchi/web/public/restore.js +81 -0
  77. data/lib/samagotchi/web/public/session_state.js +52 -0
  78. data/lib/samagotchi/web/public/stream_handlers.js +324 -0
  79. data/lib/samagotchi/web/public/thinking_chip.js +6 -7
  80. data/lib/samagotchi/web/public/timing.js +8 -2
  81. data/lib/samagotchi/web/public/turn_events.js +17 -258
  82. data/lib/samagotchi/web/public/turn_html.js +15 -7
  83. data/lib/samagotchi/web/server.rb +4 -4
  84. metadata +17 -2
  85. data/lib/samagotchi/web/public/stable_html.js +0 -18
@@ -293,8 +293,8 @@ module Samagotchi
293
293
  @stderr.puts "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
294
294
  return CLI::Exit::USAGE
295
295
  end
296
- data = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).read
297
- return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name, data)
296
+ record = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).record
297
+ return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name, record)
298
298
 
299
299
  uninstaller = Samagotchi::MemoryBundle::Uninstaller.new(name: bundle_name, scope: scope, force: force)
300
300
  begin
@@ -370,7 +370,7 @@ module Samagotchi
370
370
  @stdout.puts "Bundle '#{name}' not installed."
371
371
  return 0
372
372
  end
373
- @stdout.puts "Bundle: #{name} v#{st[:provenance][:version]} scope=#{status_scope(st)} installed=#{st[:provenance][:installed_at]}"
373
+ @stdout.puts "Bundle: #{name} v#{st[:provenance].version} scope=#{status_scope(st)} installed=#{st[:provenance].installed_at}"
374
374
  if st[:scope_error]
375
375
  @stdout.puts "Target: (unknown scope: this chi can't resolve it; upgrade chi or reinstall the bundle)"
376
376
  else
@@ -380,7 +380,7 @@ module Samagotchi
380
380
  next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
381
381
 
382
382
  mods = []
383
- mods << "conflict (kept your edits over v#{st[:provenance][:version]}: chi bundle diff #{name} #{k})" if info[:conflict]
383
+ mods << "conflict (kept your edits over v#{st[:provenance].version}: chi bundle diff #{name} #{k})" if info[:conflict]
384
384
  mods << "modified" if info[:modified]
385
385
  mods << "missing" if info[:missing]
386
386
  mods << "no-index" unless info[:index_present] || info[:overlay]
@@ -389,20 +389,19 @@ module Samagotchi
389
389
  @stdout.puts " #{k}: #{label}"
390
390
  end
391
391
  # Hooks
392
- hooks = st[:provenance][:hooks] || {}
393
- trust = st[:provenance][:trust_level] || "experimental"
394
- commit = st[:provenance][:source_commit]
392
+ hooks = st[:provenance].hooks
393
+ trust = st[:provenance].trust_level || "experimental"
394
+ commit = st[:provenance].source_commit
395
395
  @stdout.puts " trust_level: #{trust}" if hooks.any? || trust
396
396
  @stdout.puts " source_commit: #{commit}" if commit
397
397
  if hooks.any?
398
398
  @stdout.puts " Hooks (#{hooks.size}):"
399
- hooks.each do |hk, meta|
400
- hook = Samagotchi::MemoryBundle::BundleHook.parse(meta)
401
- hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk.to_s)
399
+ hooks.each do |hk, hook|
400
+ hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk)
402
401
  exists = File.exist?(hook_path) ? "ok" : "missing"
403
402
  @stdout.puts " #{hk}: event=#{hook.event} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
404
403
  end
405
- @stdout.puts " requires_chi: #{st[:provenance][:requires_chi]}" if st[:hooks_requires_failure]
404
+ @stdout.puts " requires_chi: #{st[:provenance].requires_chi}" if st[:hooks_requires_failure]
406
405
  @stdout.puts " not loaded: #{st[:hooks_requires_failure]}" if st[:hooks_requires_failure]
407
406
  end
408
407
  if (plugin = st[:plugin])
@@ -412,28 +411,28 @@ module Samagotchi
412
411
  @stdout.puts " not loaded: #{plugin[:requires_failure]}" if plugin[:requires_failure]
413
412
  end
414
413
  st[:needs].each { |need| @stdout.puts " #{Samagotchi::MemoryBundle::Status.need_line(need)}" }
415
- (st[:provenance][:includes] || []).each do |member|
416
- state = Samagotchi::MemoryBundle::Provenance.new(name: member.to_s).installed? ? "installed" : "not installed: chi bundle install #{member}"
414
+ (st[:provenance].includes || []).each do |member|
415
+ state = Samagotchi::MemoryBundle::Provenance.new(name: member).installed? ? "installed" : "not installed: chi bundle install #{member}"
417
416
  @stdout.puts " includes #{member} [#{state}]"
418
417
  end
419
418
  else
420
419
  bundles = Samagotchi::MemoryBundle::Provenance.each_installed.to_a
421
420
  @stdout.puts "No installed bundles." if bundles.empty?
422
- bundles.each do |bname, data|
423
- next @stdout.puts(" #{bname} (manifest.json unreadable)") if data[:error]
421
+ bundles.each do |bname, record|
422
+ next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
424
423
 
425
424
  st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
426
425
  mods = st[:files].values.count { |v| v[:modified] || v[:missing] }
427
- hooks_count = (st[:provenance][:hooks] || {}).size
426
+ hooks_count = st[:provenance].hooks.size
428
427
  hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
429
428
  plugin = st[:plugin]
430
429
  mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
431
430
  mods += 1 if st[:hooks_requires_failure]
432
431
  mods += 1 if st[:scope_error]
433
432
  plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
434
- includes = st[:provenance][:includes]
433
+ includes = st[:provenance].includes
435
434
  includes_info = includes ? " includes=#{includes.join(",")}" : ""
436
- @stdout.puts " #{bname} v#{st[:provenance][:version]} scope=#{status_scope(st)} files=#{st[:files].size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
435
+ @stdout.puts " #{bname} v#{st[:provenance].version} scope=#{status_scope(st)} files=#{st[:files].size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
437
436
  end
438
437
  end
439
438
  0
@@ -450,19 +449,19 @@ module Samagotchi
450
449
  @stderr.puts "Usage: chi bundle diff <bundle> [file]"; return CLI::Exit::USAGE
451
450
  end
452
451
  prov = Samagotchi::MemoryBundle::Provenance.new(name: bname)
453
- data = prov.read
454
- unless data
452
+ record = prov.record
453
+ unless record
455
454
  @stderr.puts "Bundle '#{bname}' not installed"; return 1
456
455
  end
457
- scope = data[:scope]&.to_s || "system"
456
+ scope = record.effective_scope
458
457
  begin
459
458
  target_dir = Samagotchi::MemoryPaths.scope_dir!(scope)
460
459
  rescue ArgumentError => e
461
460
  @stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
462
461
  end
463
- files = data[:files] || {}
464
- hooks = data[:hooks] || {}
465
- plugin_path = prov.plugin_path(data)
462
+ files = record.files
463
+ hooks = record.hooks
464
+ plugin_path = prov.plugin_path(record)
466
465
  plugin_file = plugin_path && File.basename(plugin_path)
467
466
  show_plugin = lambda do
468
467
  @stdout.puts "=== plugin/#{plugin_file} ==="
@@ -471,11 +470,11 @@ module Samagotchi
471
470
  @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
472
471
  @stdout.puts "--- current (on-disk) ---"
473
472
  @stdout.puts File.exist?(plugin_path) ? File.read(plugin_path) : "(missing)"
474
- @stdout.puts "--- metadata: sha256=#{data[:plugin][:sha256]}#{" requires_chi=#{data[:requires_chi]}" if data[:requires_chi]}"
473
+ @stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
475
474
  end
476
475
  if file_arg
477
476
  # Try file first, then hook
478
- if files.key?(file_arg.to_sym) || files.key?(file_arg)
477
+ if record.owns?(file_arg)
479
478
  base = prov.base_path(file_arg)
480
479
  cur = File.join(target_dir, file_arg)
481
480
  @stdout.puts "=== #{file_arg} ==="
@@ -483,7 +482,7 @@ module Samagotchi
483
482
  @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
484
483
  @stdout.puts "--- current (on-disk) ---"
485
484
  @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
486
- elsif hooks.key?(file_arg.to_sym) || hooks.key?(file_arg)
485
+ elsif hooks.key?(file_arg)
487
486
  base = prov.base_path(file_arg)
488
487
  cur = File.join(prov.hooks_dir, file_arg)
489
488
  @stdout.puts "=== hooks/#{file_arg} ==="
@@ -492,8 +491,7 @@ module Samagotchi
492
491
  @stdout.puts "--- current (on-disk) ---"
493
492
  @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
494
493
  # Show hook metadata
495
- meta = hooks[file_arg.to_sym] || hooks[file_arg]
496
- @stdout.puts hook_metadata_line(meta) if meta
494
+ @stdout.puts hook_metadata_line(hooks[file_arg])
497
495
  elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
498
496
  show_plugin.call
499
497
  else
@@ -502,8 +500,7 @@ module Samagotchi
502
500
  end
503
501
  @stdout.puts ""
504
502
  else
505
- keys = files.keys.map(&:to_s)
506
- keys.each do |k|
503
+ files.each_key do |k|
507
504
  base = prov.base_path(k)
508
505
  cur = File.join(target_dir, k)
509
506
  @stdout.puts "=== #{k} ==="
@@ -514,7 +511,7 @@ module Samagotchi
514
511
  @stdout.puts ""
515
512
  end
516
513
  # Hooks diff
517
- hooks.keys.map(&:to_s).each do |k|
514
+ hooks.each do |k, hook|
518
515
  base = prov.base_path(k)
519
516
  cur = File.join(prov.hooks_dir, k)
520
517
  @stdout.puts "=== hooks/#{k} ==="
@@ -522,8 +519,7 @@ module Samagotchi
522
519
  @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
523
520
  @stdout.puts "--- current (on-disk) ---"
524
521
  @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
525
- meta = hooks[k.to_sym] || hooks[k]
526
- @stdout.puts hook_metadata_line(meta) if meta
522
+ @stdout.puts hook_metadata_line(hook)
527
523
  @stdout.puts ""
528
524
  end
529
525
  if plugin_file
@@ -577,8 +573,7 @@ module Samagotchi
577
573
  "includes=#{here.join(",")}#{" left out=#{gone.join(",")}" unless gone.empty?}"
578
574
  end
579
575
 
580
- def hook_metadata_line(meta)
581
- hook = Samagotchi::MemoryBundle::BundleHook.parse(meta)
576
+ def hook_metadata_line(hook)
582
577
  "--- metadata: event=#{hook.event} on_error=#{hook.on_error} priority=#{hook.priority} sha256=#{hook.sha256}"
583
578
  end
584
579
 
@@ -65,19 +65,18 @@ module Samagotchi
65
65
  # entry name → marker, for the installed bundles of +scope+ with a
66
66
  # missing need. A manifest.json or needs: that doesn't parse is skipped.
67
67
  def self.entry_markers(scope, path:, bundles_dir:)
68
- MemoryBundle::Provenance.each_installed(dir: bundles_dir).each_with_object({}) do |(_name, data), acc|
69
- next if data[:error] || data[:scope].to_s != scope.to_s
70
- next unless data[:needs].is_a?(Array) && !data[:needs].empty?
68
+ MemoryBundle::Provenance.each_installed(dir: bundles_dir).each_with_object({}) do |(_name, bundle), acc|
69
+ next if bundle.error? || bundle.effective_scope != scope.to_s || !bundle.needs?
71
70
 
72
71
  needs = begin
73
- MemoryBundle::Manifest.parse_needs(data[:needs])
72
+ MemoryBundle::Manifest.parse_needs(bundle.needs)
74
73
  rescue MemoryBundle::Manifest::ValidationError
75
74
  next
76
75
  end
77
76
  marker = marker(missing(needs, path: path))
78
77
  next unless marker
79
78
 
80
- (data[:files].is_a?(Hash) ? data[:files].keys : []).each do |key|
79
+ bundle.files.each_key do |key|
81
80
  name = MutedMemories.normalize(key)
82
81
  acc[name] ||= marker if name
83
82
  end
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: skills
3
- version: 0.1.6
3
+ version: 0.1.7
4
4
  scope: system
5
5
  description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
6
6
  trust_level: reviewed
7
7
  plugin:
8
8
  file: plugin.rb
9
- sha256: sha256:16aefdff7d7702a5feb9b2bafb9d975ceaa47f2864ad40f6df3624f4ba714136
9
+ sha256: sha256:42f535eb1dd616bdf2e027031538c9419767c531e2b52e2baa8f1cc83accaba7
10
10
  requires_chi: ">= 0.11.0"
@@ -25,7 +25,8 @@
25
25
  # fixing the skill: in a turn that read a skill (memory_read of a skill_*
26
26
  # name, or a read of its file), the first failing tool call after it (an
27
27
  # execute with "exit: N", N ≠ 0, or with no exit line an Error: line near
28
- # the top; any tool's "[tool] Error: …") steers the model once to find out
28
+ # the top; any tool's "[tool] Error: …"; not a grep that found nothing,
29
+ # which core counts ok) steers the model once to find out
29
30
  # why and fix the skill, unless a skill read was changed already (by any
30
31
  # tool, see above). A call a guardrail or the user denied (its first line
31
32
  # "[tool] Error: blocked by guardrail: …", "… denied by guardrail (…): …" or
@@ -18,7 +18,7 @@ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/confi
18
18
 
19
19
  Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`); leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
20
20
 
21
- **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds/status_cadence`, `memory.index_warn_tokens`, `thinking.turn_preamble`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
21
+ **Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens`, `thinking.turn_preamble`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
22
22
 
23
23
  Example `config.yml` (new nested form, preferred):
24
24
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.53.0
3
+ version: 0.55.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
8
  self_map.md: sha256:dfc19266259a9af1e0d769b7e0f67996b740b0c9921fdf3dadad973f6e5c144a
9
- config_modification_protocol.md: sha256:a7a6352c03979db69d98955bee8f36eb468a6cf68904d418b7f3ddb788c83e7b
9
+ config_modification_protocol.md: sha256:45064ac6b19863e1646d15891d0e9cc7a8f10ab981a0d25d6e13891591538b8a
10
10
  memory_guide.md: sha256:15cd3b24eda853cc458371c6c75809a2d4b5352311d075aaa4384926b5221c27
11
11
  delegated.md: sha256:aedaa1e756a244904c5560fff0424e06fb468bbdcb0358bccefe321d67cd2614
@@ -14,6 +14,8 @@ module Samagotchi
14
14
  # Loaded on first use: session_manager requires terminal_ui, which
15
15
  # requires KernelLoop and so the tools (a require cycle otherwise).
16
16
  autoload :SessionManager, File.expand_path("session_manager", __dir__)
17
+ # SessionContinue::OpenChild, the rows a plan refuses: loaded on first use too.
18
+ autoload :SessionContinue, File.expand_path("session_continue", __dir__)
17
19
 
18
20
  # A continue moving the previous link's open delegates to the chain's new
19
21
  # link (SessionManager.continue_session), instead of archiving them with
@@ -45,7 +47,7 @@ module Samagotchi
45
47
  # What a continue does with the previous link's children: +move+ the
46
48
  # direct children whose subtree has an open node (they go to the new
47
49
  # link, their subtrees with them), +refuse+ the open ones it can't move
48
- # (SessionManager::OpenChild, why: an older chi worker or a chi REPL).
50
+ # (SessionContinue::OpenChild, why: an older chi worker or a chi REPL).
49
51
  Plan = Data.define(:move, :refuse)
50
52
  # A move under way: from the session, the children moved, and those
51
53
  # already sent their note (so a move finished again notes no one twice).
@@ -55,8 +57,8 @@ module Samagotchi
55
57
 
56
58
  module_function
57
59
 
58
- # @param open [Array<SessionManager::OpenChild>] the previous link's open
59
- # nodes, at any depth (SessionManager.open_children)
60
+ # @param open [Array<SessionContinue::OpenChild>] the previous link's open
61
+ # nodes, at any depth (SessionContinue.open_children)
60
62
  # @return [Plan]
61
63
  def plan(prev_id, open:, state_dir:)
62
64
  open_ids = open.map(&:id)
@@ -77,7 +79,7 @@ module Samagotchi
77
79
 
78
80
  why = unmovable(row[:id], state_dir: state_dir)
79
81
  if why
80
- refuse << SessionManager::OpenChild.new(id: row[:id], short_id: row[:id][0, 8], why: why)
82
+ refuse << SessionContinue::OpenChild.new(id: row[:id], short_id: row[:id][0, 8], why: why)
81
83
  else
82
84
  move << row[:id]
83
85
  end
@@ -156,7 +156,6 @@ module Samagotchi
156
156
  # (MemoryBundle::IndexSize); 0: no note. `memory:` is its own section, not the `memories:` preload list.
157
157
  Entry.new(key: "memory.index_warn_tokens", yaml_path: %w[memory index_warn_tokens], type: :integer, default: 2500, expose: %i[env config cli]),
158
158
  Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli]),
159
- Entry.new(key: "context.status_cadence", yaml_path: %w[context status_cadence], type: :integer, default: 0, expose: %i[env config cli]),
160
159
  # Attached context (ContextSources): how often a source's command runs when it names no --every (seconds; less
161
160
  # than 30 counts as 30).
162
161
  Entry.new(key: "context.every_seconds", yaml_path: %w[context every_seconds], type: :integer, default: 300, expose: %i[env config]),
@@ -251,6 +250,8 @@ module Samagotchi
251
250
  Entry.new(key: "web.view", yaml_path: %w[web view], type: :enum, default: "stage", expose: %i[env config cli], enum_values: %w[stage turn]),
252
251
  # Quick replies next to Annotate in the page's selection bubble, "|"-separated (a YAML list works too); "" leaves only Annotate.
253
252
  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]),
253
+ # 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).
254
+ Entry.new(key: "web.editor", yaml_path: %w[web editor], type: :string, default: "vscode", expose: %i[env config cli]),
254
255
 
255
256
  Entry.new(key: "no_interrupt", yaml_path: %w[no_interrupt], type: :bool, default: false, expose: %i[env config cli]),
256
257
  Entry.new(key: "no_default_input", yaml_path: %w[no_default_input], type: :bool, default: false, expose: %i[env config cli]),
@@ -22,7 +22,7 @@ module Samagotchi
22
22
  USAGE = <<~TEXT
23
23
  Usage: chi context <add|push|ls|show|refresh|rm|mute|unmute> [options] [TARGET]
24
24
  add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
25
- add URL [--why TEXT] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
25
+ add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
26
26
  a source: CMD prints its text (plain, or JSON {"text", "summary", "wake", "hint"});
27
27
  --push: text comes from chi context push. --every: how often CMD runs
28
28
  (seconds, at least 30; default context.every_seconds). --why: why it's attached;
@@ -84,10 +84,10 @@ module Samagotchi
84
84
  # @raise [Invalid] the provider makes a name that isn't a source name
85
85
  def resolve(url)
86
86
  url = url.to_s.strip
87
- MemoryBundle::Provenance.each_installed do |bundle, data|
88
- next if data[:error] || !data[:context_providers].is_a?(Array)
87
+ MemoryBundle::Provenance.each_installed do |bundle, record|
88
+ next if record.error? || !record.context_providers?
89
89
 
90
- providers(bundle, data).each do |provider|
90
+ providers(bundle, record).each do |provider|
91
91
  match = provider.regexp.match(url) or next
92
92
  return resolved(bundle, provider, match)
93
93
  end
@@ -97,13 +97,11 @@ module Samagotchi
97
97
 
98
98
  # Whether an installed bundle declares a provider (the web's "+ URL").
99
99
  def any?
100
- MemoryBundle::Provenance.each_installed.any? do |_bundle, data|
101
- !data[:error] && data[:context_providers].is_a?(Array) && !data[:context_providers].empty?
102
- end
100
+ MemoryBundle::Provenance.each_installed.any? { |_bundle, record| !record.error? && record.context_providers? }
103
101
  end
104
102
 
105
- def providers(bundle, data)
106
- parse_list(data[:context_providers])
103
+ def providers(bundle, record)
104
+ parse_list(record.context_providers)
107
105
  rescue Invalid => e
108
106
  Log.warn(:context, "providers_unreadable", bundle: bundle, msg: e.message)
109
107
  []
@@ -131,13 +129,12 @@ module Samagotchi
131
129
  return source.cmd unless source.provider
132
130
 
133
131
  provenance = MemoryBundle::Provenance.new(name: source.provider)
134
- data = provenance.read
135
- raise Invalid, "bundle #{source.provider} isn't installed" unless data
132
+ record = provenance.record
133
+ raise Invalid, "bundle #{source.provider} isn't installed" unless record
136
134
 
137
- scripts = data[:scripts].is_a?(Hash) ? data[:scripts] : {}
138
- scripts.each do |file, meta|
139
- path = File.join(provenance.scripts_dir, file.to_s)
140
- recorded = MemoryBundle::Provenance.recorded_sha(meta.is_a?(Hash) ? meta[:sha256] : nil)
135
+ record.scripts.each do |file, sha|
136
+ path = File.join(provenance.scripts_dir, file)
137
+ recorded = MemoryBundle::Provenance.recorded_sha(sha)
141
138
  next if File.file?(path) && MemoryBundle::Provenance.file_sha(path) == recorded
142
139
 
143
140
  raise Invalid, "bundle #{source.provider}'s scripts/#{file} differs from the installed one (reinstall the bundle)"
@@ -12,8 +12,9 @@ module Samagotchi
12
12
  #
13
13
  # - the status line's value (#display), every request and again from the
14
14
  # server's final counts after each generation;
15
- # - a :context_status stream event (#observe's return), on a bucket change
16
- # or every context.status_cadence requests;
15
+ # - a :context_status stream event (#observe's return), on every request,
16
+ # so the web and the attached TUI get a fresh estimate each time (the
17
+ # context.status_cadence setting no longer throttles this event);
17
18
  # - the model's own short line (#take_guidance), once per rise into a
18
19
  # bucket whose guidance asks for a change (the second threshold up).
19
20
  #
@@ -125,7 +126,6 @@ module Samagotchi
125
126
  parsed = Config.get("context.status_thresholds").to_s.split(",").map { |value| value.strip.to_i }
126
127
  .select { |value| value.between?(1, 99) }.uniq.sort
127
128
  @thresholds = parsed.empty? ? DEFAULT_THRESHOLDS : parsed
128
- @cadence = [Config.get("context.status_cadence").to_i, 0].max
129
129
  @last_bucket = self.class.last_bucket(conversation)
130
130
  @server_usage = nil
131
131
  @counted = nil
@@ -153,16 +153,19 @@ module Samagotchi
153
153
  # +image_tokens+: the images' estimate (their base64 is not in the chars).
154
154
  #
155
155
  # @return [Hash, nil] the :context_status event's fields (without
156
- # :type/:iteration) when the emit gate fires, else nil
156
+ # :type/:iteration) for every request when enabled, else nil (the
157
+ # context.status off). The estimate is returned every request, so the
158
+ # web and the attached TUI get a fresh number on each; the model's own
159
+ # line is still only left on a rise into a guidance bucket (below).
157
160
  def observe(prompt_chars, iteration_index:, window:, image_tokens: 0)
158
161
  return nil unless @enabled
159
162
 
160
163
  usage = estimate(prompt_chars, server_usage: @server_usage, window: window, image_tokens: image_tokens, counted: @counted)
161
164
  bucket = bucket_for(usage[:estimated_pct])
162
- # The status line's value, every iteration; the gate below decides
163
- # only the event and the model's guidance line.
165
+ # The status line's value, every iteration; the event below too, so the
166
+ # consumers get a fresh estimate on every request. Only the model's line
167
+ # is still gated: it is left only on a rise (below).
164
168
  @display = { est_pct: usage[:estimated_pct], bucket: bucket }
165
- emit = emit?(bucket: bucket, iteration_index: iteration_index)
166
169
  previous_bucket = @last_bucket
167
170
  @last_bucket = bucket
168
171
  line = if @forget
@@ -171,7 +174,6 @@ module Samagotchi
171
174
  guidance_message(usage: usage, bucket: bucket)
172
175
  end
173
176
  @guidance = line if line
174
- return nil unless emit
175
177
 
176
178
  { status: status_message(usage: usage, bucket: bucket), usage: usage, bucket: bucket, source: usage[:source] }
177
179
  end
@@ -293,16 +295,6 @@ module Samagotchi
293
295
  rank >= GUIDANCE_FROM_RANK && rank > bucket_rank(previous)
294
296
  end
295
297
 
296
- def emit?(bucket:, iteration_index:)
297
- bucket_changed = bucket != if @last_bucket.nil?
298
- "under#{@thresholds.first}"
299
- else
300
- @last_bucket
301
- end
302
- cadence_due = @cadence.positive? && ((iteration_index + 1) % @cadence).zero?
303
- bucket_changed || cadence_due
304
- end
305
-
306
298
  # The forget layer's line for this request, or nil (the tiers and when
307
299
  # they come: the class comment).
308
300
  def forget_line(usage, bucket, previous, iteration_index)