samagotchi 0.141.0 → 0.143.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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -7
  3. data/README.md +2 -1
  4. data/bin/chi +2 -0
  5. data/docs/architecture.md +25 -289
  6. data/docs/bundle-authoring.md +7 -6
  7. data/docs/bundles.md +1232 -0
  8. data/docs/cli.md +51 -13
  9. data/docs/configuration.md +68 -30
  10. data/docs/context.md +12 -12
  11. data/docs/guardrails.md +27 -20
  12. data/docs/hooks.md +28 -9
  13. data/docs/internals/attached-context.md +225 -0
  14. data/docs/internals/background-tasks.md +1 -1
  15. data/docs/internals/bridge.md +380 -0
  16. data/docs/internals/bundles.md +6 -3
  17. data/docs/internals/context-telemetry.md +1 -1
  18. data/docs/internals/engine.md +31 -19
  19. data/docs/internals/participants.md +13 -12
  20. data/docs/internals/prompt-caching.md +12 -12
  21. data/docs/internals/remote-sessions.md +1 -1
  22. data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
  23. data/docs/internals/web-push.md +1 -1
  24. data/docs/memory.md +33 -3
  25. data/docs/plugins.md +127 -1231
  26. data/docs/sessions.md +51 -27
  27. data/docs/sub-agent.md +6 -6
  28. data/docs/system-one.md +122 -0
  29. data/docs/testing.md +3 -1
  30. data/lib/samagotchi/bridge.rb +2 -0
  31. data/lib/samagotchi/bundle_command/api_calls.rb +132 -0
  32. data/lib/samagotchi/bundle_command/check.rb +50 -34
  33. data/lib/samagotchi/bundle_command/installed_check.rb +86 -0
  34. data/lib/samagotchi/bundle_command/scaffold.rb +1 -0
  35. data/lib/samagotchi/bundle_settings_check.rb +58 -0
  36. data/lib/samagotchi/bundles/agents-md/manifest.yml +4 -2
  37. data/lib/samagotchi/bundles/agents-md/plugin.rb +1 -1
  38. data/lib/samagotchi/bundles/btw/manifest.yml +4 -2
  39. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  40. data/lib/samagotchi/bundles/check-in/manifest.yml +7 -3
  41. data/lib/samagotchi/bundles/check-in/plugin.rb +101 -11
  42. data/lib/samagotchi/bundles/claude-peer/manifest.yml +3 -3
  43. data/lib/samagotchi/bundles/claude-peer/plugin.rb +1 -1
  44. data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
  45. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
  46. data/lib/samagotchi/bundles/codex-peer/plugin.rb +1 -1
  47. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  48. data/lib/samagotchi/bundles/coordinator/plugin.rb +1 -1
  49. data/lib/samagotchi/bundles/core/manifest.yml +2 -2
  50. data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -3
  51. data/lib/samagotchi/bundles/cost-cap/plugin.rb +5 -7
  52. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -1
  53. data/lib/samagotchi/bundles/known-names/manifest.yml +3 -1
  54. data/lib/samagotchi/bundles/loop-guard/manifest.yml +4 -2
  55. data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
  56. data/lib/samagotchi/bundles/mcp/manifest.yml +4 -2
  57. data/lib/samagotchi/bundles/mcp/plugin.rb +2 -2
  58. data/lib/samagotchi/bundles/ruby/manifest.yml +2 -2
  59. data/lib/samagotchi/bundles/ruby/plugin.rb +1 -1
  60. data/lib/samagotchi/bundles/skills/manifest.yml +4 -2
  61. data/lib/samagotchi/bundles/skills/plugin.rb +1 -1
  62. data/lib/samagotchi/bundles/slow-commands/manifest.yml +4 -2
  63. data/lib/samagotchi/bundles/slow-commands/plugin.rb +1 -1
  64. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -1
  65. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -3
  66. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  67. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  68. data/lib/samagotchi/bundles/system/self_map.md +3 -3
  69. data/lib/samagotchi/bundles/work-cap/manifest.yml +6 -3
  70. data/lib/samagotchi/bundles/work-cap/plugin.rb +11 -13
  71. data/lib/samagotchi/config.rb +74 -14
  72. data/lib/samagotchi/context_candidate_choice.rb +20 -3
  73. data/lib/samagotchi/context_candidates.rb +4 -1
  74. data/lib/samagotchi/context_candidates_log.rb +36 -1
  75. data/lib/samagotchi/context_judge.rb +6 -2
  76. data/lib/samagotchi/context_status.rb +1 -2
  77. data/lib/samagotchi/engine.rb +96 -70
  78. data/lib/samagotchi/extension_load.rb +11 -4
  79. data/lib/samagotchi/guardrail_wiring.rb +36 -31
  80. data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
  81. data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
  82. data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
  83. data/lib/samagotchi/guardrails_command.rb +3 -3
  84. data/lib/samagotchi/hook_routes.rb +37 -2
  85. data/lib/samagotchi/hooks/bundle_loader.rb +20 -4
  86. data/lib/samagotchi/hooks/registry.rb +23 -5
  87. data/lib/samagotchi/host_model.rb +3 -3
  88. data/lib/samagotchi/host_registry.rb +14 -7
  89. data/lib/samagotchi/judgments_command.rb +155 -0
  90. data/lib/samagotchi/kernel_loop.rb +20 -3
  91. data/lib/samagotchi/llm/chat_loop.rb +79 -10
  92. data/lib/samagotchi/llm/errors.rb +7 -5
  93. data/lib/samagotchi/llm/stray_tool_call.rb +68 -0
  94. data/lib/samagotchi/log_line.rb +1 -1
  95. data/lib/samagotchi/memory_bundle/builder.rb +4 -2
  96. data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
  97. data/lib/samagotchi/memory_bundle/installed_bundle.rb +9 -2
  98. data/lib/samagotchi/memory_bundle/installed_copy.rb +92 -0
  99. data/lib/samagotchi/memory_bundle/installer.rb +2 -0
  100. data/lib/samagotchi/memory_bundle/manifest.rb +23 -13
  101. data/lib/samagotchi/memory_bundle/provenance.rb +4 -1
  102. data/lib/samagotchi/memory_bundle/shipped_update.rb +13 -1
  103. data/lib/samagotchi/model_catalog.rb +3 -2
  104. data/lib/samagotchi/models_command.rb +1 -1
  105. data/lib/samagotchi/plugin/api.rb +41 -7
  106. data/lib/samagotchi/plugin/context.rb +59 -1
  107. data/lib/samagotchi/plugin/judgments.rb +72 -0
  108. data/lib/samagotchi/plugin/loader.rb +6 -3
  109. data/lib/samagotchi/plugin/peer.rb +17 -22
  110. data/lib/samagotchi/plugin_session_state.rb +32 -5
  111. data/lib/samagotchi/prompt_warmup.rb +1 -9
  112. data/lib/samagotchi/run_options.rb +1 -0
  113. data/lib/samagotchi/self_report.rb +36 -0
  114. data/lib/samagotchi/session_access/contract.rb +1 -1
  115. data/lib/samagotchi/session_access/local.rb +1 -1
  116. data/lib/samagotchi/session_access/remote.rb +3 -3
  117. data/lib/samagotchi/session_archive_command.rb +3 -3
  118. data/lib/samagotchi/session_commands.rb +7 -4
  119. data/lib/samagotchi/session_config.rb +9 -3
  120. data/lib/samagotchi/session_continue.rb +1 -1
  121. data/lib/samagotchi/session_manager.rb +25 -5
  122. data/lib/samagotchi/session_retention.rb +10 -0
  123. data/lib/samagotchi/side_judgments.rb +12 -1
  124. data/lib/samagotchi/system_one/answer.rb +63 -11
  125. data/lib/samagotchi/system_one/approval_label.rb +56 -0
  126. data/lib/samagotchi/system_one/approval_question.rb +2 -0
  127. data/lib/samagotchi/system_one/client.rb +43 -17
  128. data/lib/samagotchi/system_one/fake_backend.rb +1 -1
  129. data/lib/samagotchi/system_one/http_backend.rb +12 -4
  130. data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
  131. data/lib/samagotchi/system_one/judgments.rb +243 -0
  132. data/lib/samagotchi/system_one/log.rb +25 -2
  133. data/lib/samagotchi/system_one/recording.rb +117 -0
  134. data/lib/samagotchi/system_one/runs.rb +179 -0
  135. data/lib/samagotchi/system_one/runs_store.rb +230 -0
  136. data/lib/samagotchi/system_one.rb +9 -0
  137. data/lib/samagotchi/terminal_ui/attach_link.rb +2 -2
  138. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +1 -1
  139. data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
  140. data/lib/samagotchi/terminal_ui/local_link.rb +1 -1
  141. data/lib/samagotchi/terminal_ui/remote_link.rb +1 -2
  142. data/lib/samagotchi/testing/bundle.rb +9 -2
  143. data/lib/samagotchi/testing/host.rb +2 -1
  144. data/lib/samagotchi/testing/judgments.rb +176 -0
  145. data/lib/samagotchi/testing/recorder.rb +5 -0
  146. data/lib/samagotchi/testing/recording_api.rb +1 -1
  147. data/lib/samagotchi/tool_output_cut.rb +26 -6
  148. data/lib/samagotchi/tool_response.rb +1 -1
  149. data/lib/samagotchi/tool_runner.rb +63 -4
  150. data/lib/samagotchi/toolkit_suggest.rb +11 -2
  151. data/lib/samagotchi/tools/memory.rb +28 -0
  152. data/lib/samagotchi/turn_note.rb +9 -0
  153. data/lib/samagotchi/update_command.rb +49 -16
  154. data/lib/samagotchi/version.rb +1 -1
  155. data/lib/samagotchi/web/app.rb +1 -0
  156. data/lib/samagotchi/web/auto_update.rb +79 -49
  157. data/lib/samagotchi/web/auto_update_record.rb +59 -0
  158. data/lib/samagotchi/web/public/app.js +8 -4
  159. data/lib/samagotchi/web/public/hash_intent.js +24 -0
  160. data/lib/samagotchi/web/public/index.html +3 -1
  161. data/lib/samagotchi/web/public/revise_stack.js +14 -5
  162. data/lib/samagotchi/web/session_control.rb +3 -3
  163. data/lib/samagotchi/web/update_check.rb +7 -6
  164. data/lib/samagotchi/web/update_ops.rb +80 -25
  165. metadata +23 -2
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../system_one/runs"
4
+
5
+ module Samagotchi
6
+ module Plugin
7
+ # ctx.judgments: what came of a bundle's own runs (docs/plugins.md,
8
+ # Judgments), saved as later lines on them. A bundle's lines say it is
9
+ # theirs (+by+ is its name), and it reaches no one else's runs.
10
+ class Judgments
11
+ MAX_TEXT = 200
12
+ STRENGTHS = %w[strong weak].freeze
13
+
14
+ # @param layer [#call] → the SystemOne::Judgments to save through
15
+ # @param bundle [String]
16
+ def initialize(layer, bundle:)
17
+ @layer = layer
18
+ @bundle = bundle
19
+ end
20
+
21
+ # The bundle's own word for its run: a raw value now, mapped at analysis.
22
+ # @param run_id [String, nil] ctx.judge's Result#run_id, or what
23
+ # ctx.judge_later returned; nil (no run was saved) saves nothing
24
+ # @param value [String] up to 200 characters
25
+ # @param by [String, nil] what said it, after the bundle's name ("card":
26
+ # "check-in:card")
27
+ # @param strength [String, nil] "strong" or "weak"
28
+ # @param question [String, nil] the question it answers
29
+ # @return [Boolean] whether it was saved
30
+ # @raise [ArgumentError] a run of another bundle's, a bad value
31
+ def label(run_id, value, by: nil, strength: nil, question: nil)
32
+ value = text("label", value)
33
+ raise ArgumentError, "strength: is #{STRENGTHS.join(" or ")}, not #{strength.inspect}" unless strength.nil? || STRENGTHS.include?(strength.to_s)
34
+ return false unless own?(run_id)
35
+
36
+ @layer.call.label(run_id, value, by: by.to_s.strip.empty? ? @bundle : "#{@bundle}:#{text("by", by)}",
37
+ strength: strength&.to_s, question: question && text("question", question.to_s))
38
+ end
39
+
40
+ # What the bundle did with the answer: never known when the run is saved.
41
+ # @param what [String] up to 200 characters
42
+ # @return [Boolean] whether it was saved
43
+ # @raise [ArgumentError] a run of another bundle's, a bad value
44
+ def acted(run_id, what)
45
+ what = text("acted", what)
46
+ return false unless own?(run_id)
47
+
48
+ @layer.call.acted(run_id, what)
49
+ end
50
+
51
+ private
52
+
53
+ # False for nil; a run of anything but a judgment this bundle declared
54
+ # raises (a name it can't declare has no run of its).
55
+ def own?(run_id)
56
+ return false if run_id.nil?
57
+
58
+ judgment = SystemOne::Runs.judgment_of(run_id)
59
+ return true if judgment&.start_with?("#{@bundle}.") && @layer.call.declared?(judgment)
60
+
61
+ raise ArgumentError, "#{run_id.inspect} is not a run of bundle #{@bundle}'s"
62
+ end
63
+
64
+ def text(what, value)
65
+ word = value.to_s.strip
66
+ return word if value.is_a?(String) && word.length.between?(1, MAX_TEXT)
67
+
68
+ raise ArgumentError, "#{what} takes a String of 1 to #{MAX_TEXT} characters, not #{value.inspect[0, 40]}"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -22,10 +22,13 @@ module Samagotchi
22
22
  # +tool_index+ the Engine's Tools::ToolIndex (chi.tool_source);
23
23
  # +peer_sources+ the Engine's PeerSources (chi.peer_source);
24
24
  # +prompt_sections+ the Engine's PromptSections (chi.prompt_section),
25
- # +prompt_changed+ rebuilds its system prompts (chi.prompt_changed!).
25
+ # +prompt_changed+ rebuilds its system prompts (chi.prompt_changed!),
26
+ # +judgments+ the Engine's SystemOne::Judgments (chi.judgment),
27
+ # +hook_failed+ is (label, "Class: message") when a chi.on handler
28
+ # raises (HookRoutes#plugin_hook_failed).
26
29
  Registries = Struct.new(:commands, :tools, :hooks, :context_for, :tools_changed, :services, :stage_tools, :init,
27
- :toolkits, :tool_index, :peer_sources, :prompt_sections, :prompt_changed,
28
- keyword_init: true)
30
+ :toolkits, :tool_index, :peer_sources, :prompt_sections, :prompt_changed, :judgments,
31
+ :hook_failed, keyword_init: true)
29
32
 
30
33
  # Loads installed bundles' plugins (manifest plugin: {file:, sha256:})
31
34
  # into an Engine's registries (docs/plugins.md).
@@ -10,7 +10,7 @@ require_relative "../paths"
10
10
  module Samagotchi
11
11
  module Plugin
12
12
  # The base of the peer bundles' plugins (claude-peer, codex-peer;
13
- # docs/plugins.md): a live link between a chi session and another agent's
13
+ # docs/bundles.md): a live link between a chi session and another agent's
14
14
  # session the user works with. The peer pairs (its chi-peer skill,
15
15
  # scripts/pair): it attaches the `<peer>-inbox` context, whose hint says
16
16
  # where to send (a subclass's #check_hint!), and `<peer>-outbox`, where
@@ -131,11 +131,11 @@ module Samagotchi
131
131
  body, where = if text.length <= INLINE_CHARS
132
132
  [text, "inline"]
133
133
  else
134
- path = write_payload(ctx, id, title, text)
134
+ path = write_payload(ctx, title, text)
135
135
  ["payload: #{path} (#{text.lines.size} lines, #{text.length} chars)", path]
136
136
  end
137
137
  post(hint, "chi #{id[0, 8]} · #{kind} · #{title}\n#{body}")
138
- record(ctx, id, kind: kind, title: title, chars: text.length, payload: where == "inline" ? nil : where)
138
+ record(ctx, kind: kind, title: title, chars: text.length, payload: where == "inline" ? nil : where)
139
139
  "sent to #{self.class::NAME}: #{kind} · #{title}#{" (the text in #{where})" unless where == "inline"}"
140
140
  end
141
141
 
@@ -153,7 +153,7 @@ module Samagotchi
153
153
  left = ctx.context.left_behind.find { |row| row.name == peer::INBOX }
154
154
  hint = left&.hint.to_s.strip
155
155
  return "no #{peer::INBOX} left behind" if hint.empty?
156
- return "asked already" unless claim_moved(ctx, id)
156
+ return "asked already" unless claim_moved(ctx)
157
157
 
158
158
  check_hint!(hint)
159
159
  post(hint, moved_message(previous, id))
@@ -175,9 +175,9 @@ module Samagotchi
175
175
 
176
176
  private
177
177
 
178
- # Takes the moved marker for session +id+; false when it is there.
179
- def claim_moved(ctx, id)
180
- File.open(File.join(session_dir(ctx, id), MOVED_MARKER), File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
178
+ # Takes this session's moved marker; false when it is there.
179
+ def claim_moved(ctx)
180
+ File.open(File.join(ctx.session_dir, MOVED_MARKER), File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
181
181
  file.write("#{Time.now.utc.iso8601}\n")
182
182
  end
183
183
  true
@@ -302,7 +302,7 @@ module Samagotchi
302
302
  ["**not paired.** In #{peer::APP}, install the chi-peer skill (Install skill below), then tell " \
303
303
  "#{peer::NAME} \"pair with chi #{id.to_s[0, 8]}\"."]
304
304
  end
305
- lines << "last send: #{last_send_line(ctx, id)}" if id && last_send(ctx, id)
305
+ lines << "last send: #{last_send_line(ctx)}" if id && last_send(ctx)
306
306
  lines << "last answer (#{peer::OUTBOX}): #{outbox_line(outbox)}" if outbox
307
307
  lines << "skill: **differs from this bundle's**: #{peer::COMMAND} install-skill (Replace), then pair again" if skill_differs?(ctx)
308
308
  lines << transport_line
@@ -312,15 +312,15 @@ module Samagotchi
312
312
  nil
313
313
  end
314
314
 
315
- def last_send(ctx, id)
316
- path = File.join(ctx.data_dir, "sessions", id, LAST_SEND)
315
+ def last_send(ctx)
316
+ path = File.join(ctx.session_dir, LAST_SEND)
317
317
  File.file?(path) ? JSON.parse(File.read(path)) : nil
318
318
  rescue JSON::ParserError, SystemCallError
319
319
  nil
320
320
  end
321
321
 
322
- def last_send_line(ctx, id)
323
- sent = last_send(ctx, id)
322
+ def last_send_line(ctx)
323
+ sent = last_send(ctx)
324
324
  where = sent["payload"] ? "in `#{sent["payload"]}`" : "inline"
325
325
  "#{ago(sent["at"])} · #{sent["kind"]} · #{sent["title"]} (#{sent["chars"]} chars, #{where})"
326
326
  end
@@ -424,10 +424,9 @@ module Samagotchi
424
424
  end
425
425
  end
426
426
 
427
- # sessions/<id>/<HHMMSS>-<slug>.md under the bundle's data dir, readable
428
- # by its owner only.
429
- def write_payload(ctx, id, title, text)
430
- dir = session_dir(ctx, id)
427
+ # <HHMMSS>-<slug>.md in ctx.session_dir, readable by its owner only.
428
+ def write_payload(ctx, title, text)
429
+ dir = ctx.session_dir
431
430
  base = "#{Time.now.strftime("%H%M%S")}-#{slug(title)}"
432
431
  path = File.join(dir, "#{base}.md")
433
432
  n = 1
@@ -438,13 +437,9 @@ module Samagotchi
438
437
  raise SendError, "couldn't write the payload file: #{e.message}"
439
438
  end
440
439
 
441
- def session_dir(ctx, id)
442
- File.join(ctx.data_dir, "sessions", id).tap { |dir| FileUtils.mkdir_p(dir) }
443
- end
444
-
445
- def record(ctx, id, **send)
440
+ def record(ctx, **send)
446
441
  entry = { at: Time.now.utc.iso8601, **send }
447
- @lock.synchronize { File.write(File.join(session_dir(ctx, id), LAST_SEND), JSON.generate(entry)) }
442
+ @lock.synchronize { File.write(File.join(ctx.session_dir, LAST_SEND), JSON.generate(entry)) }
448
443
  rescue SystemCallError => e
449
444
  ctx.log.info(:last_send_not_saved, error: e.message)
450
445
  end
@@ -5,14 +5,41 @@ require_relative "paths"
5
5
  require_relative "session"
6
6
 
7
7
  module Samagotchi
8
- # A plugin's state for one session, by convention under its data dir
9
- # (ctx.data_dir): sessions/<id>.json or sessions/<id>/. It goes when the
10
- # session does (SessionManager.delete_session: the CLI, the web, a
11
- # discarded empty session; SessionRetention's prune), so no bundle keeps
12
- # state for sessions that are gone (check-in's /checkin settings).
8
+ # A plugin's state for one session, under its data dir (ctx.data_dir):
9
+ # sessions/<id>/ (ctx.session_dir), or sessions/<id>.json, the file a bundle
10
+ # written before ctx.session_dir may still keep. It goes when the session
11
+ # does (SessionManager.delete_session: the CLI, the web, a discarded empty
12
+ # session; SessionRetention's prune), so no bundle keeps state for sessions
13
+ # that are gone (check-in's /checkin settings).
13
14
  module PluginSessionState
15
+ # What sessions/<id>.json becomes in the folder (.dir), the name the
16
+ # shipped bundles that used it now write.
17
+ STATE_FILE = "state.json"
18
+
14
19
  module_function
15
20
 
21
+ # A session's folder under a bundle's +data_dir+, created. A
22
+ # sessions/<id>.json there moves in as STATE_FILE, so a running session
23
+ # keeps it when its bundle moves to ctx.session_dir.
24
+ # @return [String, nil] nil for an id that isn't a plain one (it could
25
+ # name a path outside the dir)
26
+ def dir(data_dir, session_id)
27
+ id = session_id.to_s
28
+ return nil unless Session.valid_id?(id)
29
+
30
+ path = File.join(data_dir, "sessions", id)
31
+ FileUtils.mkdir_p(path)
32
+ adopt("#{path}.json", File.join(path, STATE_FILE))
33
+ path
34
+ end
35
+
36
+ def adopt(old, state)
37
+ File.rename(old, state) if File.file?(old) && !File.exist?(state)
38
+ rescue SystemCallError
39
+ nil # another worker's move, or a file that stays where it is
40
+ end
41
+ private_class_method :adopt
42
+
16
43
  # @return [Array<String>] the paths removed
17
44
  def remove(session_id, env: ENV)
18
45
  id = session_id.to_s
@@ -38,10 +38,6 @@ module Samagotchi
38
38
  DECIDE_TIMEOUT = 3
39
39
  # Seconds #stop waits for a killed warm-up's thread to end.
40
40
  STOP_JOIN_SECONDS = 1
41
- # Seconds a warm-up waits for the side asks sent to the same server
42
- # before it (#start's +after+: the candidate judge's, which evict the
43
- # cached prefix the warm-up is there to restore).
44
- AFTER_SECONDS = 5
45
41
 
46
42
  # Whether cache.warmup allows a warm-up (auto, the default; off, false,
47
43
  # no or 0 turn it off; on/true/1 (YAML's unquoted on) are auto; anything
@@ -71,11 +67,8 @@ module Samagotchi
71
67
  # Start a warm-up of +prompt+ on +client+ in its own thread.
72
68
  # @param slot [Integer, nil] the last request's slot (nil: unpinned, and
73
69
  # nothing to pin the next request to)
74
- # @param after [Thread, nil] asks to the same server that go first: the
75
- # warm-up waits for them (AFTER_SECONDS at most), so it re-warms what
76
- # they evicted
77
70
  # @return [Thread, nil] nil once #stop ran
78
- def start(client:, prompt:, model:, slot:, images: [], after: nil)
71
+ def start(client:, prompt:, model:, slot:, images: [])
79
72
  return nil if @mutex.synchronize { @stopped }
80
73
 
81
74
  # Whether the warm-up went to its slot (true) or was skipped: the
@@ -83,7 +76,6 @@ module Samagotchi
83
76
  sent = Queue.new
84
77
  thread = Thread.new do
85
78
  Thread.current.report_on_exception = false
86
- after&.join(AFTER_SECONDS)
87
79
  busy = slot.is_a?(Integer) && client.slot_status(slot, model: model) == :busy
88
80
  sent << !busy
89
81
  next skip(slot) if busy
@@ -207,6 +207,7 @@ module Samagotchi
207
207
  chi memory <ls|rm> [--scope project|system] the memories; rm moves one to the bundle trash
208
208
  chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
209
209
  chi guardrails replay [--dry-run] [--threshold T] [--model M] seed System 1's record from answered approvals
210
+ chi judgments ls | report NAME System 1's saved asks on this machine
210
211
  chi toolkit report [--days N] grade toolkit suggestions against the tools sessions used
211
212
  chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
212
213
  chi models [--format json] [TEXT] the models --model takes, from every host
@@ -21,6 +21,7 @@ require_relative "memory_bundle/provenance"
21
21
  require_relative "memory_bundle/manifest"
22
22
  require_relative "memory_bundle/system_bundle"
23
23
  require_relative "memory_bundle/index_size"
24
+ require_relative "memory_bundle/entry_size"
24
25
  require_relative "desktop"
25
26
  require_relative "live_versions"
26
27
  require_relative "web/lan"
@@ -60,6 +61,7 @@ module Samagotchi
60
61
  ["memories", Tools::MemoryRead.memories_dir("system", env: env)],
61
62
  ["project memories", Tools::MemoryRead.memories_dir("project", env: env)],
62
63
  ["memory index", memory_index_summary(env)],
64
+ ["big memories", big_memories_summary(env)],
63
65
  ["sessions", Session.default_state_dir(env: env)],
64
66
  ["log", log_path(env)],
65
67
  ["model", model ? model_label(model, default, env) : "(not configured)"],
@@ -122,6 +124,40 @@ module Samagotchi
122
124
  "(unreadable: #{e.message})"
123
125
  end
124
126
 
127
+ # How many big memories "big memories" names before "+N more".
128
+ BIG_MEMORIES_SHOWN = 5
129
+
130
+ # The memories over memory.entry_warn_tokens in both scopes, largest
131
+ # first, "name ~5.6k tokens (system, from samagotchi-system)": each
132
+ # memory_read of one pays its size, and a memory already over gets no
133
+ # crossing note. A bundle's memory says whose it is (upstream's to
134
+ # tighten). "none" when there are none, "off" with the limit 0. A scope
135
+ # that can't be read is named instead.
136
+ def big_memories_summary(env)
137
+ limit = MemoryBundle::EntrySize.warn_limit
138
+ return "off (memory.entry_warn_tokens 0)" unless limit.positive?
139
+
140
+ big = []
141
+ unreadable = []
142
+ %w[system project].each do |scope|
143
+ big.concat(MemoryBundle::EntrySize.over_limit(scope, limit: limit, env: env))
144
+ rescue StandardError => e
145
+ unreadable << "#{scope} memories unreadable: #{e.message}"
146
+ end
147
+ shown = big.sort_by { |size| [-size.tokens, size.scope, size.name] }.first(BIG_MEMORIES_SHOWN).map { |size| big_memory(size, env) }
148
+ shown << "+#{big.size - BIG_MEMORIES_SHOWN} more" if big.size > BIG_MEMORIES_SHOWN
149
+ parts = shown + unreadable.map { |text| "(#{text})" }
150
+ parts.empty? ? "none" : parts.join(", ")
151
+ end
152
+
153
+ def big_memory(size, env)
154
+ owner = MemoryBundle::Provenance.claimants("#{size.name}.md", scope: size.scope, dir: MemoryPaths.bundles_dir(env: env)).first
155
+ where = [size.scope, ("from #{owner}" if owner)].compact.join(", ")
156
+ "#{size.name} ~#{MemoryBundle::IndexSize.count_text(size.tokens)} tokens (#{where})"
157
+ rescue StandardError
158
+ "#{size.name} ~#{MemoryBundle::IndexSize.count_text(size.tokens)} tokens (#{size.scope})"
159
+ end
160
+
125
161
  def with_presence(path)
126
162
  File.exist?(path) ? path : "#{path} (missing)"
127
163
  end
@@ -26,7 +26,7 @@ module Samagotchi
26
26
  # bin/chi subcommands that act on this machine: never sessions. chi
27
27
  # remote (this machine's list of remotes) runs here under a remote too
28
28
  # (Dispatch); the others are refused there.
29
- MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit remote home].freeze
29
+ MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit judgments remote home].freeze
30
30
 
31
31
  # "command", "command sub" or either with a flag (its long name) →
32
32
  # SUPPORTED or LOCAL_ONLY. A LOCAL_ONLY command or subcommand lists
@@ -271,7 +271,7 @@ module Samagotchi
271
271
  # @return [SessionManager::RestartResult]
272
272
  def restart(id) = SessionManager.restart_session(id, **dir)
273
273
 
274
- # @return [Hash] SessionManager.archive_session's
274
+ # @return [SessionManager::ArchiveResult]
275
275
  def archive(id) = SessionManager.archive_session(id, **dir)
276
276
 
277
277
  # @return [Hash] SessionManager.unarchive_session's
@@ -432,13 +432,13 @@ module Samagotchi
432
432
  version: reply.json["version"])
433
433
  end
434
434
 
435
- # @return [Hash] {id:, archived:, stopped:, discarded:}
435
+ # @return [SessionManager::ArchiveResult] the reply's lists coerced to arrays
436
436
  def archive(id)
437
437
  reply = request(:post, "#{session_path(id)}/archive")
438
438
  raise_refusal(reply) unless reply.status == 200
439
439
 
440
- { id: reply.json["session_id"] || id, archived: Array(reply.json["archived"]), stopped: Array(reply.json["stopped"]),
441
- discarded: Array(reply.json["discarded"]) }
440
+ SessionManager::ArchiveResult.new(id: reply.json["session_id"] || id, archived: Array(reply.json["archived"]),
441
+ stopped: Array(reply.json["stopped"]), discarded: Array(reply.json["discarded"]))
442
442
  end
443
443
 
444
444
  # @return [Hash] {id:, unarchived:}
@@ -64,10 +64,10 @@ module Samagotchi
64
64
  id = @access.resolve_id(given)
65
65
  short = id[0, 8]
66
66
  result = @access.archive(id)
67
- if result[:archived].include?(id)
67
+ if result.archived.include?(id)
68
68
  notes = []
69
- notes << "stopped its worker" if result[:stopped].include?(id)
70
- others = result[:archived].size - 1
69
+ notes << "stopped its worker" if result.stopped.include?(id)
70
+ others = result.archived.size - 1
71
71
  notes << "and #{others} delegate#{"s" if others != 1}" if others.positive?
72
72
  @stdout.puts("#{short} archived#{" (#{notes.join(", ")})" unless notes.empty?}")
73
73
  else
@@ -23,6 +23,7 @@ require_relative "working_set_command"
23
23
  require_relative "recap_command"
24
24
  require_relative "guardrails/parent_policy"
25
25
  require_relative "system_one/log"
26
+ require_relative "system_one/approval_label"
26
27
  require_relative "system_one/record"
27
28
 
28
29
  module Samagotchi
@@ -480,6 +481,8 @@ module Samagotchi
480
481
  return reply("could not label System 1 auto-allow #{index} (see the guardrails log)", status: :error)
481
482
  end
482
483
 
484
+ SystemOne::ApprovalLabel.save(SystemOne::Runs.for_sessions_dir(@engine.session_state_dir), entry["id"], SystemOne::ApprovalLabel::WRONG)
485
+
483
486
  call = shorten_call(entry["call"].to_s.gsub(/\s+/, " "))
484
487
  probability = entry["p"].nil? ? "unknown" : format("%.2f", entry["p"].to_f)
485
488
  reply("labelled auto-allow #{index}: #{entry["rule"]}: #{call} (p=#{probability}); " \
@@ -965,13 +968,13 @@ module Samagotchi
965
968
  # Sort hosts for deterministic output
966
969
  results.keys.sort.each do |hname|
967
970
  data = results[hname]
968
- host_label = "#{hname} (#{data[:host]}:#{data[:port]})"
971
+ host_label = "#{hname} (#{data.host}:#{data.port})"
969
972
  # Declared ids (hosts.<name>.models) first, as "(config)", never cut
970
973
  # by MODELS_PER_HOST; a host that failed keeps only those, as
971
974
  # "(config, host down)" under its unreachable line.
972
975
  rows = host_rows.fetch(hname, [])
973
- if data[:error]
974
- lines << "#{host_label} — unreachable: #{data[:error]}"
976
+ if data.error
977
+ lines << "#{host_label} — unreachable: #{data.error}"
975
978
  next if rows.empty?
976
979
  elsif rows.empty?
977
980
  lines << "#{host_label} — no models discovered"
@@ -992,7 +995,7 @@ module Samagotchi
992
995
  end
993
996
  next if declared.empty? && shown.empty?
994
997
 
995
- lines << "#{host_label}:" unless data[:error]
998
+ lines << "#{host_label}:" unless data.error
996
999
  hidden = needle.empty? ? [shown.size - MODELS_PER_HOST, 0].max : 0
997
1000
  (declared + shown.first(shown.size - hidden)).each do |row, identifier|
998
1001
  raw = row.info&.raw || {}
@@ -196,18 +196,24 @@ module Samagotchi
196
196
  if keys.nil?
197
197
  raise Error, "#{where}: #{unknown_core("#{bundle}.#{key}", bundles: bundles)}" if note.nil? && core_section?(bundle)
198
198
 
199
- raise Error, "#{where}: #{note || "no installed bundle #{bundle} (#{takers(bundles)})"}"
199
+ raise Error, "#{where}: #{note || "#{no_installed_bundle(bundle)} (#{takers(bundles)})"}"
200
200
  end
201
201
  raise Error, "#{where}: #{bundle} takes no session settings#{" #{note}" if note}" if keys.empty?
202
- raise Error, "#{where}: #{bundle} takes #{keys.join(", ")}" unless keys.include?(key)
202
+ raise Error, "#{where}: #{takes_keys(bundle, keys)}" unless keys.include?(key)
203
203
 
204
204
  takes, what = TYPES[types.dig(bundle, key)]
205
- raise Error, "#{where}: #{bundle} takes #{what} for #{key}, not #{value.inspect}" if takes && !takes.call(value)
205
+ raise Error, "#{where}: #{wrong_type(bundle, key, what, value)}" if takes && !takes.call(value)
206
206
  end
207
207
  end
208
208
  settings
209
209
  end
210
210
 
211
+ # One message per condition: --set's refusals (.check!) and config.yml's
212
+ # warnings (BundleSettingsCheck).
213
+ def no_installed_bundle(bundle) = "no installed bundle #{bundle}"
214
+ def takes_keys(bundle, keys) = keys.empty? ? "#{bundle} takes no settings" : "#{bundle} takes #{keys.join(", ")}"
215
+ def wrong_type(bundle, key, what, value) = "#{bundle} takes #{what} for #{key}, not #{value.inspect}"
216
+
211
217
  # --set's values parsed and checked (.parse, .check!) against the
212
218
  # installed bundles (.availability).
213
219
  # @return [Settings]
@@ -134,7 +134,7 @@ module Samagotchi
134
134
  ArchiveStore.archived?(Session.session_dir(sid, state_dir: state_dir))
135
135
  end
136
136
  archive = SessionManager.archive_session(id, state_dir: state_dir, wait: archive_wait, except: plan.move)
137
- archived = archive[:archived] - already
137
+ archived = archive.archived - already
138
138
  begin
139
139
  spawn_continuation(previous, prompt: prompt, preview: preview, choice: checked.choice, state_dir: state_dir,
140
140
  move: plan.move)
@@ -6,6 +6,7 @@ require "time"
6
6
  require "securerandom"
7
7
  require "rbconfig"
8
8
  require_relative "chi_home"
9
+ require_relative "system_one/runs"
9
10
  require_relative "child_env"
10
11
  require_relative "client_id"
11
12
  require_relative "analytics_file"
@@ -45,6 +46,7 @@ require_relative "continue_offer"
45
46
  require_relative "image_store"
46
47
  require_relative "plugin_session_state"
47
48
  require_relative "context_sources"
49
+ require_relative "context_candidates_log"
48
50
  require_relative "session_chain"
49
51
  require_relative "child_move"
50
52
  require_relative "tools/task_runtime"
@@ -393,11 +395,15 @@ module Samagotchi
393
395
  .select { |s| s[:parent_id] == parent_id.to_s }
394
396
  end
395
397
 
398
+ # What one archive did: id: the session it was asked for; archived: the
399
+ # ids (it and its delegates) now hidden for good; stopped: the live
400
+ # workers stopped first; discarded: empty sessions its stopping worker deleted.
401
+ ArchiveResult = Data.define(:id, :archived, :stopped, :discarded)
402
+
396
403
  # Archive a session and its delegated children (ArchiveStore): hidden
397
404
  # from every list, kept by the retention sweep. A live idle worker is
398
405
  # stopped first; the marker is written even while it shuts down.
399
- # @return [Hash] {id:, archived: [ids], stopped: [ids], discarded: [ids]};
400
- # discarded: empty sessions their stopping worker deleted
406
+ # @return [ArchiveResult]
401
407
  # @raise [ArgumentError] unknown id (Session::AmbiguousId for a prefix of several)
402
408
  # @raise [OwnedByTUI] a chi REPL owns it or one of its children
403
409
  # @raise [ArchiveRefused] a turn runs in it or in a child, a prompt is
@@ -417,7 +423,7 @@ module Samagotchi
417
423
  archived.each { |sid| ReminderCarry.remove(Session.session_dir(sid, state_dir: sd)) }
418
424
  # D8: an archive ends the review (unarchiving doesn't restore it).
419
425
  Participants.end_sessions(archived, by: "archive", state_dir: sd)
420
- { id: id, archived: archived, stopped: stopped, discarded: discarded }
426
+ ArchiveResult.new(id: id, archived: archived, stopped: stopped, discarded: discarded)
421
427
  end
422
428
 
423
429
  # The session and its descendants, with their owners, when they can be
@@ -1043,7 +1049,10 @@ module Samagotchi
1043
1049
  # @raise [OwnedByTUI] a chi REPL owns it (never stopped from here)
1044
1050
  # @raise [DeleteRefused] a worker owns it and +stop+ is false, or it
1045
1051
  # outlived the wait
1046
- def self.delete_session(id_or_prefix, state_dir: nil, stop: false, wait: STOP_WAIT)
1052
+ # @param purge_runs [Boolean] also delete System 1's saved runs of it and
1053
+ # its candidate-log lines; a session a worker left empty has none worth
1054
+ # the read
1055
+ def self.delete_session(id_or_prefix, state_dir: nil, stop: false, wait: STOP_WAIT, purge_runs: true)
1047
1056
  sd = state_dir || Session.default_state_dir
1048
1057
  given = id_or_prefix.to_s
1049
1058
  # Only a plain id: anything else could name a path outside the state dir.
@@ -1072,9 +1081,20 @@ module Samagotchi
1072
1081
  PluginSessionState.remove(id)
1073
1082
  removed.concat(ContextSources.remove_session(id, state_dir: sd))
1074
1083
  Participants.end_sessions([id], by: "delete", state_dir: sd)
1084
+ if purge_runs
1085
+ purge_session_runs(id, sd)
1086
+ ContextCandidatesLog.purge_session(id, state_dir: sd)
1087
+ end
1075
1088
  { id: id, removed: removed, stopped: !owner.nil? }
1076
1089
  end
1077
1090
 
1091
+ # Never a reason a delete fails: one warn, and the sweep ages them out.
1092
+ private_class_method def self.purge_session_runs(id, state_dir)
1093
+ SystemOne::Runs.purge_session(id, sessions_dir: state_dir)
1094
+ rescue StandardError => e
1095
+ Log.warn(:worker, "session_runs_purge_failed", sid: id, error: e.class.name, msg: e.message.to_s[0, 200])
1096
+ end
1097
+
1078
1098
  # @return [Boolean] whether the session had no owner within +timeout+ seconds
1079
1099
  def self.wait_for_owner_release(session_id, timeout:, state_dir:)
1080
1100
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout.to_f
@@ -1216,7 +1236,7 @@ module Samagotchi
1216
1236
  private_class_method def self.discard_left_session(session_id, state_dir:, default_model:)
1217
1237
  return unless discardable?(session_id, state_dir: state_dir, default_model: default_model)
1218
1238
 
1219
- delete_session(session_id, state_dir: state_dir)
1239
+ delete_session(session_id, state_dir: state_dir, purge_runs: false)
1220
1240
  Log.info(:worker, "discarded_empty", sid: session_id)
1221
1241
  rescue DeleteRefused, OwnedByTUI, ArgumentError, SystemCallError => e
1222
1242
  Log.info(:worker, "kept_session", sid: session_id, error: e.class.name, msg: e.message)
@@ -10,6 +10,7 @@ require_relative "model_profile"
10
10
  require_relative "session_manager"
11
11
  require_relative "plugin_session_state"
12
12
  require_relative "context_sources"
13
+ require_relative "system_one/runs"
13
14
 
14
15
  module Samagotchi
15
16
  # Which sessions go: `chi sessions prune` / `clean` (.prune) and the lazy
@@ -109,6 +110,7 @@ module Samagotchi
109
110
  return if age < interval
110
111
  end
111
112
  result = prune(state_dir: sd)
113
+ sweep_runs(sd)
112
114
  FileUtils.touch(marker)
113
115
  if result[:deleted].any?
114
116
  Log.info(:worker, "retention_pruned", echo: "[retention] pruned #{result[:deleted].size} sessions (kept #{result[:kept].size})", deleted: result[:deleted].size, kept: result[:kept].size)
@@ -119,6 +121,14 @@ module Samagotchi
119
121
  nil
120
122
  end
121
123
 
124
+ # System 1's saved runs last as long as sessions, by day file. Never a
125
+ # reason the session sweep fails.
126
+ private_class_method def self.sweep_runs(state_dir)
127
+ SystemOne::Runs.sweep_at(File.dirname(state_dir))
128
+ rescue StandardError => e
129
+ Log.warn(:worker, "retention_runs_failed", echo: "[retention] saved runs: #{e.class}: #{e.message}", error: e.class.name)
130
+ end
131
+
122
132
  # The caller's value (a sessions prune flag) or session.retention_days.
123
133
  private_class_method def self.resolve_days(val)
124
134
  return val.to_i if !val.nil? && val.to_s.strip != ""
@@ -16,6 +16,7 @@ module Samagotchi
16
16
  class SideJudgments
17
17
  def initialize
18
18
  @threads = {}
19
+ @asides = []
19
20
  @mutex = Mutex.new
20
21
  end
21
22
 
@@ -42,9 +43,19 @@ module Samagotchi
42
43
  end
43
44
  end
44
45
 
46
+ # Run the block on its own thread, in no kind: telling a caller there
47
+ # was no ask. #join waits for it too. A raise goes to +warn+.
48
+ # @return [Thread]
49
+ def aside(warn:, &block)
50
+ @mutex.synchronize do
51
+ @asides.select!(&:alive?)
52
+ Thread.new { run(warn, nil, &block) }.tap { |thread| @asides << thread }
53
+ end
54
+ end
55
+
45
56
  # Wait for every running ask, up to +seconds+ in all.
46
57
  def join(seconds)
47
- threads = @mutex.synchronize { @threads.values }
58
+ threads = @mutex.synchronize { @threads.values + @asides }
48
59
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
49
60
  threads.each do |thread|
50
61
  next if thread == Thread.current