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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -7
- data/README.md +2 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +25 -289
- data/docs/bundle-authoring.md +7 -6
- data/docs/bundles.md +1232 -0
- data/docs/cli.md +51 -13
- data/docs/configuration.md +68 -30
- data/docs/context.md +12 -12
- data/docs/guardrails.md +27 -20
- data/docs/hooks.md +28 -9
- data/docs/internals/attached-context.md +225 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +380 -0
- data/docs/internals/bundles.md +6 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +31 -19
- data/docs/internals/participants.md +13 -12
- data/docs/internals/prompt-caching.md +12 -12
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/{tool-guardrails.md → tool-output-limits.md} +21 -2
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +33 -3
- data/docs/plugins.md +127 -1231
- data/docs/sessions.md +51 -27
- data/docs/sub-agent.md +6 -6
- data/docs/system-one.md +122 -0
- data/docs/testing.md +3 -1
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundle_command/api_calls.rb +132 -0
- data/lib/samagotchi/bundle_command/check.rb +50 -34
- data/lib/samagotchi/bundle_command/installed_check.rb +86 -0
- data/lib/samagotchi/bundle_command/scaffold.rb +1 -0
- data/lib/samagotchi/bundle_settings_check.rb +58 -0
- data/lib/samagotchi/bundles/agents-md/manifest.yml +4 -2
- data/lib/samagotchi/bundles/agents-md/plugin.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +4 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/check-in/manifest.yml +7 -3
- data/lib/samagotchi/bundles/check-in/plugin.rb +101 -11
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +3 -3
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +1 -1
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +1 -1
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +6 -3
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +5 -7
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +4 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +4 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -2
- data/lib/samagotchi/bundles/ruby/manifest.yml +2 -2
- data/lib/samagotchi/bundles/ruby/plugin.rb +1 -1
- data/lib/samagotchi/bundles/skills/manifest.yml +4 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +1 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +4 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +1 -1
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +3 -3
- data/lib/samagotchi/bundles/work-cap/manifest.yml +6 -3
- data/lib/samagotchi/bundles/work-cap/plugin.rb +11 -13
- data/lib/samagotchi/config.rb +74 -14
- data/lib/samagotchi/context_candidate_choice.rb +20 -3
- data/lib/samagotchi/context_candidates.rb +4 -1
- data/lib/samagotchi/context_candidates_log.rb +36 -1
- data/lib/samagotchi/context_judge.rb +6 -2
- data/lib/samagotchi/context_status.rb +1 -2
- data/lib/samagotchi/engine.rb +96 -70
- data/lib/samagotchi/extension_load.rb +11 -4
- data/lib/samagotchi/guardrail_wiring.rb +36 -31
- data/lib/samagotchi/guardrails/chi_dirs.rb +7 -4
- data/lib/samagotchi/guardrails/path_resolve.rb +1 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +7 -4
- data/lib/samagotchi/guardrails_command.rb +3 -3
- data/lib/samagotchi/hook_routes.rb +37 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +20 -4
- data/lib/samagotchi/hooks/registry.rb +23 -5
- data/lib/samagotchi/host_model.rb +3 -3
- data/lib/samagotchi/host_registry.rb +14 -7
- data/lib/samagotchi/judgments_command.rb +155 -0
- data/lib/samagotchi/kernel_loop.rb +20 -3
- data/lib/samagotchi/llm/chat_loop.rb +79 -10
- data/lib/samagotchi/llm/errors.rb +7 -5
- data/lib/samagotchi/llm/stray_tool_call.rb +68 -0
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +4 -2
- data/lib/samagotchi/memory_bundle/entry_size.rb +100 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +9 -2
- data/lib/samagotchi/memory_bundle/installed_copy.rb +92 -0
- data/lib/samagotchi/memory_bundle/installer.rb +2 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -13
- data/lib/samagotchi/memory_bundle/provenance.rb +4 -1
- data/lib/samagotchi/memory_bundle/shipped_update.rb +13 -1
- data/lib/samagotchi/model_catalog.rb +3 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +41 -7
- data/lib/samagotchi/plugin/context.rb +59 -1
- data/lib/samagotchi/plugin/judgments.rb +72 -0
- data/lib/samagotchi/plugin/loader.rb +6 -3
- data/lib/samagotchi/plugin/peer.rb +17 -22
- data/lib/samagotchi/plugin_session_state.rb +32 -5
- data/lib/samagotchi/prompt_warmup.rb +1 -9
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/self_report.rb +36 -0
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +1 -1
- data/lib/samagotchi/session_access/remote.rb +3 -3
- data/lib/samagotchi/session_archive_command.rb +3 -3
- data/lib/samagotchi/session_commands.rb +7 -4
- data/lib/samagotchi/session_config.rb +9 -3
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_manager.rb +25 -5
- data/lib/samagotchi/session_retention.rb +10 -0
- data/lib/samagotchi/side_judgments.rb +12 -1
- data/lib/samagotchi/system_one/answer.rb +63 -11
- data/lib/samagotchi/system_one/approval_label.rb +56 -0
- data/lib/samagotchi/system_one/approval_question.rb +2 -0
- data/lib/samagotchi/system_one/client.rb +43 -17
- data/lib/samagotchi/system_one/fake_backend.rb +1 -1
- data/lib/samagotchi/system_one/http_backend.rb +12 -4
- data/lib/samagotchi/system_one/judgment_settings.rb +61 -0
- data/lib/samagotchi/system_one/judgments.rb +243 -0
- data/lib/samagotchi/system_one/log.rb +25 -2
- data/lib/samagotchi/system_one/recording.rb +117 -0
- data/lib/samagotchi/system_one/runs.rb +179 -0
- data/lib/samagotchi/system_one/runs_store.rb +230 -0
- data/lib/samagotchi/system_one.rb +9 -0
- data/lib/samagotchi/terminal_ui/attach_link.rb +2 -2
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +1 -1
- data/lib/samagotchi/terminal_ui/remote_link.rb +1 -2
- data/lib/samagotchi/testing/bundle.rb +9 -2
- data/lib/samagotchi/testing/host.rb +2 -1
- data/lib/samagotchi/testing/judgments.rb +176 -0
- data/lib/samagotchi/testing/recorder.rb +5 -0
- data/lib/samagotchi/testing/recording_api.rb +1 -1
- data/lib/samagotchi/tool_output_cut.rb +26 -6
- data/lib/samagotchi/tool_response.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +63 -4
- data/lib/samagotchi/toolkit_suggest.rb +11 -2
- data/lib/samagotchi/tools/memory.rb +28 -0
- data/lib/samagotchi/turn_note.rb +9 -0
- data/lib/samagotchi/update_command.rb +49 -16
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -0
- data/lib/samagotchi/web/auto_update.rb +79 -49
- data/lib/samagotchi/web/auto_update_record.rb +59 -0
- data/lib/samagotchi/web/public/app.js +8 -4
- data/lib/samagotchi/web/public/hash_intent.js +24 -0
- data/lib/samagotchi/web/public/index.html +3 -1
- data/lib/samagotchi/web/public/revise_stack.js +14 -5
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/update_check.rb +7 -6
- data/lib/samagotchi/web/update_ops.rb +80 -25
- 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/
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
179
|
-
def claim_moved(ctx
|
|
180
|
-
File.open(File.join(session_dir
|
|
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
|
|
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
|
|
316
|
-
path = File.join(ctx.
|
|
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
|
|
323
|
-
sent = last_send(ctx
|
|
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
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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
|
|
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,
|
|
9
|
-
# (ctx.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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: []
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
441
|
-
|
|
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
|
|
67
|
+
if result.archived.include?(id)
|
|
68
68
|
notes = []
|
|
69
|
-
notes << "stopped its worker" if result
|
|
70
|
-
others = result
|
|
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
|
|
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
|
|
974
|
-
lines << "#{host_label} — unreachable: #{data
|
|
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
|
|
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 || "
|
|
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
|
|
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
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|