samagotchi 0.156.0 → 0.158.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require_relative "api"
|
|
4
5
|
require_relative "context"
|
|
5
6
|
require_relative "../hooks"
|
|
@@ -10,6 +11,11 @@ require_relative "../memory_bundle/manifest"
|
|
|
10
11
|
require_relative "../memory_bundle/provenance"
|
|
11
12
|
|
|
12
13
|
module Samagotchi
|
|
14
|
+
# Where installed bundles' plugin code is evaluated: one module per
|
|
15
|
+
# bundle (Plugin::Loader.namespace_for), so two bundles defining the same
|
|
16
|
+
# class name can't collide on a top-level constant.
|
|
17
|
+
module Bundles; end
|
|
18
|
+
|
|
13
19
|
module Plugin
|
|
14
20
|
# What a plugin registers into: an Engine's own registries.
|
|
15
21
|
# +context_for+ is (bundle_name, settings, label) → the Plugin::Context
|
|
@@ -25,7 +31,8 @@ module Samagotchi
|
|
|
25
31
|
# +prompt_changed+ rebuilds its system prompts (chi.prompt_changed!),
|
|
26
32
|
# +judgments+ the Engine's SystemOne::Judgments (chi.judgment),
|
|
27
33
|
# +hook_failed+ is (label, "Class: message") when a chi.on handler
|
|
28
|
-
# raises
|
|
34
|
+
# raises, with effect: :deny for a fail-closed one
|
|
35
|
+
# (HookRoutes#plugin_hook_failed).
|
|
29
36
|
Registries = Struct.new(:commands, :tools, :hooks, :context_for, :tools_changed, :services, :stage_tools, :init,
|
|
30
37
|
:toolkits, :tool_index, :peer_sources, :prompt_sections, :prompt_changed, :judgments,
|
|
31
38
|
:hook_failed, keyword_init: true)
|
|
@@ -35,14 +42,19 @@ module Samagotchi
|
|
|
35
42
|
#
|
|
36
43
|
# For each bundle, by name: the file must match the sha256 recorded at
|
|
37
44
|
# install and chi must meet the bundle's requires_chi. The file is
|
|
38
|
-
# module_eval'd into the bundle's namespace (
|
|
45
|
+
# module_eval'd into the bundle's namespace (#namespace_for), the
|
|
39
46
|
# class named like the file (plugin.rb → Plugin) is built with the
|
|
40
47
|
# bundle's settings, and its #register gets a Plugin::Api. What it
|
|
41
48
|
# registered takes effect only when #register returns: a plugin that
|
|
42
49
|
# fails adds nothing.
|
|
43
50
|
#
|
|
44
51
|
# A failure is logged and added to +failures+ (not required: tool calls
|
|
45
|
-
# still run), which the Engine announces once.
|
|
52
|
+
# still run), which the Engine announces once. A guardrail plugin's
|
|
53
|
+
# (the manifest's plugin: required: true, read from the kept manifest:
|
|
54
|
+
# InstalledManifest#plugin_required) goes to +guardrail_failures+ as
|
|
55
|
+
# required instead: every tool call is denied, as for a fail-closed
|
|
56
|
+
# config hook. Its chi.on(..., on_error: :fail_closed) hooks deny the
|
|
57
|
+
# call they raise on (Api#on).
|
|
46
58
|
module Loader
|
|
47
59
|
TAG = :plugins
|
|
48
60
|
|
|
@@ -50,12 +62,16 @@ module Samagotchi
|
|
|
50
62
|
|
|
51
63
|
# @param registries [Registries]
|
|
52
64
|
# @param failures [Guardrails::LoadFailures, nil]
|
|
65
|
+
# @param guardrail_failures [Guardrails::LoadFailures, nil] where a
|
|
66
|
+
# required plugin's failure goes (the Engine's guardrail failures);
|
|
67
|
+
# nil: +failures+, still required
|
|
53
68
|
# @param settings [Hash{String => Hash}] config.yml `bundles:`
|
|
54
69
|
# @return [Array<String>] the bundles whose plugin loaded
|
|
55
|
-
def load_installed(registries, failures: nil, settings: {})
|
|
70
|
+
def load_installed(registries, failures: nil, guardrail_failures: nil, settings: {})
|
|
56
71
|
loaded = []
|
|
57
72
|
MemoryBundle::Provenance.each_installed(holding: :plugin) do |bundle_name, bundle|
|
|
58
|
-
ok = load_bundle(bundle_name, bundle, registries, failures: failures,
|
|
73
|
+
ok = load_bundle(bundle_name, bundle, registries, failures: failures, guardrail_failures: guardrail_failures,
|
|
74
|
+
settings: settings[bundle_name.to_s] || {})
|
|
59
75
|
loaded << bundle_name if ok
|
|
60
76
|
end
|
|
61
77
|
loaded
|
|
@@ -66,22 +82,35 @@ module Samagotchi
|
|
|
66
82
|
end
|
|
67
83
|
|
|
68
84
|
# @return [Boolean] whether the plugin loaded
|
|
69
|
-
def load_bundle(bundle_name, bundle, registries, failures: nil, settings: {})
|
|
85
|
+
def load_bundle(bundle_name, bundle, registries, failures: nil, guardrail_failures: nil, settings: {})
|
|
70
86
|
file = MemoryBundle::Provenance.new(name: bundle_name).plugin_path(bundle) unless bundle.error?
|
|
71
87
|
basename = file ? File.basename(file) : "plugin"
|
|
88
|
+
# Known before it registers anything: a guardrail that fails to load
|
|
89
|
+
# (its sha, requires_chi, a raise) denies every tool call.
|
|
90
|
+
required = !bundle.error? && bundle.manifest.plugin_required
|
|
91
|
+
into = required ? guardrail_failures || failures : failures
|
|
72
92
|
reason = bundle.error || unloadable_reason(file, bundle)
|
|
73
|
-
return failed(bundle_name, basename, reason,
|
|
93
|
+
return failed(bundle_name, basename, reason, into, required: required) if reason
|
|
74
94
|
|
|
75
95
|
api = register_file(bundle_name, file, registries, settings: settings)
|
|
96
|
+
warn_unrequired(bundle_name, basename) if api.fail_closed? && !required
|
|
76
97
|
Log.info(TAG, "plugin_loaded", bundle: bundle_name, file: basename, **api.counts)
|
|
77
98
|
true
|
|
78
99
|
rescue Exception => e # rubocop:disable Lint/RescueException -- a plugin's SyntaxError or exit must not stop chi
|
|
79
100
|
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
80
101
|
|
|
81
|
-
failed(bundle_name, basename, "#{e.class}: #{e.message}", failures,
|
|
102
|
+
failed(bundle_name, basename, "#{e.class}: #{e.message}", into || failures, required: required == true,
|
|
82
103
|
location: file && Guardrails::LoadFailures.location(e, File.dirname(file)))
|
|
83
104
|
end
|
|
84
105
|
|
|
106
|
+
# A fail-closed hook from a plugin its manifest doesn't call required:
|
|
107
|
+
# it denies the call it raises on, but a load failure would not deny.
|
|
108
|
+
def warn_unrequired(bundle_name, basename)
|
|
109
|
+
text = "#{label(bundle_name, basename)} has a fail-closed hook but its manifest's plugin: has no required: true, " \
|
|
110
|
+
"so a failure to load it would not deny tool calls"
|
|
111
|
+
Log.warn(TAG, "plugin_fail_closed_unrequired", bundle: bundle_name, file: basename, msg: text)
|
|
112
|
+
end
|
|
113
|
+
|
|
85
114
|
# Build +file+'s plugin with +settings+ and commit what its #register
|
|
86
115
|
# staged into +registries+ (no sha or requires_chi check: #load_bundle
|
|
87
116
|
# makes them; the test kit loads a bundle's source this way).
|
|
@@ -117,14 +146,45 @@ module Samagotchi
|
|
|
117
146
|
return failure if failure
|
|
118
147
|
return "the file is missing" unless file && File.file?(file)
|
|
119
148
|
|
|
120
|
-
|
|
149
|
+
sha_mismatch(file, bundle.plugin.sha256)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Why +file+ doesn't match the sha256 recorded at install (+recorded+,
|
|
153
|
+
# "sha256:<hex>" or bare hex), or nil. None recorded is a mismatch;
|
|
154
|
+
# Provenance.unchecked? (a dev install) is none.
|
|
155
|
+
def sha_mismatch(file, recorded)
|
|
156
|
+
return nil if MemoryBundle::Provenance.unchecked?(recorded)
|
|
157
|
+
|
|
158
|
+
expected = MemoryBundle::Provenance.recorded_sha(recorded)
|
|
159
|
+
return "no sha256 recorded for it (reinstall the bundle)" if expected.empty?
|
|
160
|
+
|
|
161
|
+
actual = MemoryBundle::Provenance.file_sha(file)
|
|
162
|
+
return nil if actual == expected
|
|
163
|
+
|
|
164
|
+
"its sha256 #{actual[0, 12]}… differs from the installed #{expected[0, 12]}… (edited after install? reinstall the bundle)"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The (lazily created) namespace module for a bundle, under
|
|
168
|
+
# Samagotchi::Bundles. The name is sanitized and made injective by a
|
|
169
|
+
# short digest, so `my-bundle` and `my_bundle` don't collide.
|
|
170
|
+
def namespace_for(bundle_name)
|
|
171
|
+
raw = bundle_name.to_s
|
|
172
|
+
base = raw.gsub(/[^A-Za-z0-9_]/, "_")
|
|
173
|
+
base = "B_#{base}" unless base.match?(/\A[A-Z]/)
|
|
174
|
+
ns_name = :"#{base}_#{Digest::MD5.hexdigest(raw)[0, 4]}"
|
|
175
|
+
if Samagotchi::Bundles.const_defined?(ns_name, false)
|
|
176
|
+
Samagotchi::Bundles.const_get(ns_name)
|
|
177
|
+
else
|
|
178
|
+
Samagotchi::Bundles.const_set(ns_name, Module.new)
|
|
179
|
+
end
|
|
121
180
|
end
|
|
122
181
|
|
|
123
182
|
# module_eval the file into a fresh module in the bundle's namespace
|
|
124
183
|
# (Samagotchi::Bundles::<bundle>::PluginLoad<n>: each load gets new
|
|
125
184
|
# classes, not the last load's reopened) and build its class
|
|
126
|
-
# (plugin.rb → Plugin; my_plugin.rb → MyPlugin) as hooks are
|
|
127
|
-
# an initialize that takes an argument
|
|
185
|
+
# (plugin.rb → Plugin; my_plugin.rb → MyPlugin) as config hooks are
|
|
186
|
+
# built (Hooks.build_plugin): an initialize that takes an argument
|
|
187
|
+
# gets the settings.
|
|
128
188
|
def instantiate(bundle_name, file, settings, namespace: nil)
|
|
129
189
|
namespace ||= fresh_namespace(bundle_name)
|
|
130
190
|
namespace.module_eval(File.read(file), file, 1)
|
|
@@ -140,14 +200,14 @@ module Samagotchi
|
|
|
140
200
|
def fresh_namespace(bundle_name)
|
|
141
201
|
LOADS.synchronize do
|
|
142
202
|
@loads = (@loads || 0) + 1
|
|
143
|
-
|
|
203
|
+
namespace_for(bundle_name).const_set(:"PluginLoad#{@loads}", Module.new)
|
|
144
204
|
end
|
|
145
205
|
end
|
|
146
206
|
|
|
147
|
-
def failed(bundle_name, basename, reason, failures, location: nil)
|
|
207
|
+
def failed(bundle_name, basename, reason, failures, required: false, location: nil)
|
|
148
208
|
Log.warn(TAG, "plugin_not_loaded", echo: "[samagotchi:plugins] bundle '#{bundle_name}' plugin '#{basename}' not loaded: #{reason}",
|
|
149
|
-
bundle: bundle_name, file: basename)
|
|
150
|
-
failures&.add("plugin #{basename} (bundle #{bundle_name})", reason, required:
|
|
209
|
+
bundle: bundle_name, file: basename, required: required)
|
|
210
|
+
failures&.add("plugin #{basename} (bundle #{bundle_name})", reason, required: required, location: location)
|
|
151
211
|
false
|
|
152
212
|
end
|
|
153
213
|
end
|
|
@@ -223,7 +223,7 @@ module Samagotchi
|
|
|
223
223
|
return if running.size < max
|
|
224
224
|
|
|
225
225
|
raise Error, "#{running.size} child sessions of this session are running (the most is #{max}, " \
|
|
226
|
-
"#{Tools::Delegate::MAX_CHILDREN_KEY}): #{running.map
|
|
226
|
+
"#{Tools::Delegate::MAX_CHILDREN_KEY}): #{running.map(&:short_id).join(", ")}"
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
def live_snapshot(sid, state_dir)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cli/command"
|
|
4
|
+
require_relative "cli/exit"
|
|
5
|
+
require_relative "cli/flags"
|
|
6
|
+
require_relative "guardrails/path_resolve"
|
|
7
|
+
require_relative "memory_paths"
|
|
8
|
+
require_relative "paths"
|
|
9
|
+
require_relative "project_init"
|
|
10
|
+
|
|
11
|
+
module Samagotchi
|
|
12
|
+
# `chi project`: which project this folder is in (MemoryPaths.lookup: a
|
|
13
|
+
# git repository, a folder marked with `.chi/project.yml`, or none) and
|
|
14
|
+
# its project memories folder; `chi project init [DIR]` marks a folder in
|
|
15
|
+
# no git repository as a project. Git wins: a repository is already the
|
|
16
|
+
# project, so init writes nothing there. Machine-local
|
|
17
|
+
# (SessionAccess::Contract::MACHINE).
|
|
18
|
+
class ProjectCommand
|
|
19
|
+
include CLI::Command
|
|
20
|
+
|
|
21
|
+
USAGE = <<~TEXT
|
|
22
|
+
Usage: chi project [init [DIR]]
|
|
23
|
+
(none) this folder's project: how it was found (git, marker or none), its root, its
|
|
24
|
+
top and its project memories folder
|
|
25
|
+
init [DIR] make DIR (default: this folder; created when missing) a chi project: writes
|
|
26
|
+
DIR/#{MemoryPaths::MARKER}, so DIR and every subfolder share one project memories
|
|
27
|
+
folder and group their sessions. Not in a git repository (it already is the
|
|
28
|
+
project), nor $HOME or a folder above it
|
|
29
|
+
TEXT
|
|
30
|
+
|
|
31
|
+
SUBCOMMANDS = %w[init].freeze
|
|
32
|
+
FLAGS = {
|
|
33
|
+
nil => CLI::Flags.new(help: HELP_WORDS, args: false),
|
|
34
|
+
"init" => CLI::Flags.new(help: HELP_WORDS)
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# @param cwd [String] the folder asked about (Dir.pwd)
|
|
38
|
+
# @param home [String, nil] $HOME (markers at or above it are ignored)
|
|
39
|
+
# @param sessions [#call, nil] () → this machine's SessionRows (for
|
|
40
|
+
# init's restart note; SessionManager.session_rows by default)
|
|
41
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, cwd: Dir.pwd, env: ENV, home: MemoryPaths.default_home,
|
|
42
|
+
sessions: nil)
|
|
43
|
+
@argv = argv.dup
|
|
44
|
+
@stdout = stdout
|
|
45
|
+
@stderr = stderr
|
|
46
|
+
@cwd = cwd
|
|
47
|
+
@env = env
|
|
48
|
+
@home = home
|
|
49
|
+
@sessions = sessions || lambda {
|
|
50
|
+
require_relative "session_manager"
|
|
51
|
+
SessionManager.session_rows
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Integer] 0, 1 (a refusal), 2 usage
|
|
56
|
+
def run
|
|
57
|
+
@sub = SUBCOMMANDS.include?(@argv.first) ? @argv.first : nil
|
|
58
|
+
parsed = parse_flags(FLAGS.fetch(@sub), @sub ? @argv[1..] : @argv)
|
|
59
|
+
return parsed if parsed.is_a?(Integer)
|
|
60
|
+
|
|
61
|
+
return run_show unless @sub
|
|
62
|
+
return usage_error("init takes one folder") if parsed.args.size > 1
|
|
63
|
+
|
|
64
|
+
run_init(parsed.args.first || @cwd)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def command_name = @sub ? "chi project #{@sub}" : "chi project"
|
|
70
|
+
def usage_on_error = nil
|
|
71
|
+
|
|
72
|
+
def lookup(dir) = MemoryPaths.lookup(dir, home: @home)
|
|
73
|
+
|
|
74
|
+
def run_show
|
|
75
|
+
found = lookup(@cwd)
|
|
76
|
+
row("kind", found.kind.to_s)
|
|
77
|
+
if found.project?
|
|
78
|
+
row("root", found.root)
|
|
79
|
+
row("top", found.top)
|
|
80
|
+
end
|
|
81
|
+
row("memories", memories_line(found.root))
|
|
82
|
+
notes(found).each { |note| row("note", note) }
|
|
83
|
+
CLI::Exit::OK
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def row(label, value) = @stdout.puts("#{label.ljust(9)}#{value}")
|
|
87
|
+
|
|
88
|
+
def memories_line(root)
|
|
89
|
+
dir = File.join(MemoryPaths.projects_dir(env: @env), MemoryPaths.key_for_root(root))
|
|
90
|
+
count = memory_count(dir)
|
|
91
|
+
"#{Paths.tilde(dir)}#{" (#{count} #{count == 1 ? "memory" : "memories"})" if count.positive?}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def memory_count(dir)
|
|
95
|
+
return 0 unless File.directory?(dir)
|
|
96
|
+
|
|
97
|
+
Dir.children(dir).count { |name| name.end_with?(".md") && name != "index.md" }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def notes(found)
|
|
101
|
+
case found.kind
|
|
102
|
+
when :git
|
|
103
|
+
list = []
|
|
104
|
+
list << "#{Paths.tilde(found.top)} is $HOME, a git repository: every folder under it is in that repository" if home_repo?(found)
|
|
105
|
+
marker = MemoryPaths.nearest_marker(@cwd, home: @home)
|
|
106
|
+
list << "#{Paths.tilde(File.join(marker, MemoryPaths::MARKER))} is ignored: the git repository is the project" if marker
|
|
107
|
+
list
|
|
108
|
+
when :none then ["no git repository or chi project marker here: chi project init makes this folder a project"]
|
|
109
|
+
else []
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def home_repo?(found)
|
|
114
|
+
!@home.to_s.empty? && Guardrails::PathResolve.real(found.top) == Guardrails::PathResolve.real(@home)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def run_init(dir)
|
|
118
|
+
dir = File.expand_path(dir, @cwd)
|
|
119
|
+
outcome = ProjectInit.call(dir, home: @home)
|
|
120
|
+
case outcome.kind
|
|
121
|
+
when :git
|
|
122
|
+
found = MemoryPaths::ProjectLookup.new(kind: :git, root: outcome.root, top: outcome.top)
|
|
123
|
+
note = home_repo?(found) ? " ($HOME is a git repository: every folder under it is in it)" : ""
|
|
124
|
+
@stdout.puts("#{outcome.message}#{note}")
|
|
125
|
+
CLI::Exit::OK
|
|
126
|
+
when :refused
|
|
127
|
+
error_line("#{command_name}: #{outcome.message}")
|
|
128
|
+
CLI::Exit::FAILED
|
|
129
|
+
when :already_marked
|
|
130
|
+
@stdout.puts(outcome.message)
|
|
131
|
+
CLI::Exit::OK
|
|
132
|
+
when :created
|
|
133
|
+
report_init(outcome.root, nested_in: outcome.nested_in)
|
|
134
|
+
CLI::Exit::OK
|
|
135
|
+
end
|
|
136
|
+
rescue SystemCallError => e
|
|
137
|
+
error_line("#{command_name}: couldn't write #{Paths.tilde(File.join(dir, MemoryPaths::MARKER))}: #{e.message}")
|
|
138
|
+
CLI::Exit::FAILED
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def report_init(root, nested_in:)
|
|
142
|
+
nested = nested_in ? ", its own project nested in #{Paths.tilde(nested_in)}" : ""
|
|
143
|
+
@stdout.puts("#{Paths.tilde(root)} is now a chi project#{nested} (#{Paths.tilde(File.join(root, MemoryPaths::MARKER))})")
|
|
144
|
+
row("memories", memories_line(root))
|
|
145
|
+
sessions_note(root)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Sessions in the new project (not one under DIR that a nested
|
|
149
|
+
# repository or marked folder keeps): a running one keeps its old project in its
|
|
150
|
+
# prompt (the memories folder line, the AGENTS.md chain) until it
|
|
151
|
+
# restarts; a running chi web groups the older ones after its restart
|
|
152
|
+
# (SessionHub looked their project up once).
|
|
153
|
+
def sessions_note(root)
|
|
154
|
+
rows = @sessions.call.select { |r| !r.cwd.to_s.empty? && lookup(r.cwd.to_s).root == root }
|
|
155
|
+
return if rows.empty?
|
|
156
|
+
|
|
157
|
+
live = rows.select(&:live)
|
|
158
|
+
unless live.empty?
|
|
159
|
+
@stdout.puts("running sessions in it keep the old project in their prompt until restarted " \
|
|
160
|
+
"(chi sessions restart ID): #{live.map { |r| r.id[0, 8] }.join(", ")}")
|
|
161
|
+
end
|
|
162
|
+
@stdout.puts("#{rows.size} saved session(s) in it join the project in a running chi web after its restart")
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
error_line("#{command_name}: couldn't list this machine's sessions (#{e.message})")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "guardrails/path_resolve"
|
|
5
|
+
require_relative "memory_paths"
|
|
6
|
+
require_relative "paths"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
# The shared decisions for `chi project init` and chi web's project picker.
|
|
10
|
+
module ProjectInit
|
|
11
|
+
Outcome = Data.define(:kind, :reason, :root, :top, :nested_in, :message)
|
|
12
|
+
|
|
13
|
+
MARKER_TEXT = "# chi project marker: this folder is a chi project (docs/memory.md)\n"
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def call(dir, home:)
|
|
18
|
+
dir = File.expand_path(dir.to_s)
|
|
19
|
+
found = MemoryPaths.lookup(dir, home: home)
|
|
20
|
+
if found.git?
|
|
21
|
+
message = "#{Paths.tilde(dir)} is in the git repository #{Paths.tilde(found.top)}: the repository already is " \
|
|
22
|
+
"the project, nothing written"
|
|
23
|
+
return Outcome.new(kind: :git, reason: nil, root: found.root, top: found.top, nested_in: nil, message: message)
|
|
24
|
+
end
|
|
25
|
+
if MemoryPaths.above_home?(dir, home: home)
|
|
26
|
+
message = "#{Paths.tilde(dir)} is $HOME, a folder above it or /: it can't be a chi project " \
|
|
27
|
+
"(every folder under it would be one); mark a folder inside it"
|
|
28
|
+
return Outcome.new(kind: :refused, reason: :above_home, root: found.root, top: found.top, nested_in: nil,
|
|
29
|
+
message: message)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The marked project this folder sits in, if any: the folder itself (already marked) or its parent project.
|
|
33
|
+
enclosing = found.marker? ? found.root : nil
|
|
34
|
+
if enclosing == Guardrails::PathResolve.real(dir)
|
|
35
|
+
return Outcome.new(kind: :already_marked, reason: nil, root: enclosing, top: found.top, nested_in: nil,
|
|
36
|
+
message: "#{Paths.tilde(enclosing)} is already a chi project")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
write_marker(dir)
|
|
40
|
+
root = File.realpath(dir)
|
|
41
|
+
# No message: each surface words its own success (the CLI's report, the web's navigation).
|
|
42
|
+
Outcome.new(kind: :created, reason: nil, root: root, top: root, nested_in: enclosing, message: nil)
|
|
43
|
+
rescue SystemCallError => e
|
|
44
|
+
marker = File.join(dir, MemoryPaths::MARKER)
|
|
45
|
+
write_failed("couldn't write #{Paths.tilde(marker)}: #{e.message}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# chi web opens a folder in a git repository without marking it, so the folder has to exist.
|
|
49
|
+
# @return [Outcome, nil] a refused (:write_failed) outcome when it can't be made, else nil
|
|
50
|
+
def ensure_folder(dir)
|
|
51
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
52
|
+
nil
|
|
53
|
+
rescue SystemCallError => e
|
|
54
|
+
write_failed("couldn't create #{Paths.tilde(dir)}: #{e.message}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def write_failed(message)
|
|
58
|
+
Outcome.new(kind: :refused, reason: :write_failed, root: nil, top: nil, nested_in: nil, message: message)
|
|
59
|
+
end
|
|
60
|
+
private_class_method :write_failed
|
|
61
|
+
|
|
62
|
+
def write_marker(dir)
|
|
63
|
+
marker = File.join(dir, MemoryPaths::MARKER)
|
|
64
|
+
FileUtils.mkdir_p(File.dirname(marker))
|
|
65
|
+
File.write(marker, MARKER_TEXT)
|
|
66
|
+
end
|
|
67
|
+
private_class_method :write_marker
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -4,9 +4,10 @@ require_relative "memory_paths"
|
|
|
4
4
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
# Which project a folder belongs to, for scoping session lists (web and
|
|
7
|
-
# terminal alike): the project root memories use (MemoryPaths.
|
|
8
|
-
#
|
|
9
|
-
# folder
|
|
7
|
+
# terminal alike): the project root memories use (MemoryPaths.lookup), so
|
|
8
|
+
# every worktree and subfolder of one repository, or every subfolder of a
|
|
9
|
+
# folder marked by `chi project init`, is one project. A folder in neither
|
|
10
|
+
# has no project (nil): its scope is "all".
|
|
10
11
|
module ProjectScope
|
|
11
12
|
module_function
|
|
12
13
|
|
|
@@ -17,7 +18,8 @@ module Samagotchi
|
|
|
17
18
|
return nil if dir.empty?
|
|
18
19
|
return cache[dir] if cache&.key?(dir)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
found = MemoryPaths.lookup(File.expand_path(dir))
|
|
22
|
+
root = found.project? ? found.root : nil
|
|
21
23
|
cache[dir] = root if cache
|
|
22
24
|
root
|
|
23
25
|
end
|
|
@@ -19,9 +19,10 @@ module Samagotchi
|
|
|
19
19
|
module_function
|
|
20
20
|
|
|
21
21
|
# The AGENTS.md files a session in +cwd+ loads, as absolute paths,
|
|
22
|
-
# outermost first: inside a
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# outermost first: inside a project every one from its top
|
|
23
|
+
# (MemoryPaths.project_top: a git work tree's, a linked worktree's own
|
|
24
|
+
# checkout, or a folder marked by `chi project init`) down to the cwd;
|
|
25
|
+
# outside a project only the cwd's ([] with none).
|
|
25
26
|
# @param cwd [String] where the prompt is built (Dir.pwd, not the
|
|
26
27
|
# session's working directory: /tmp vs /private/tmp would change the
|
|
27
28
|
# prompt bytes)
|
|
@@ -33,10 +34,10 @@ module Samagotchi
|
|
|
33
34
|
# The sections of the AGENTS.md chain (#agents_paths) with text: the
|
|
34
35
|
# first keeps the plain header (a one-file session's prompt is as it
|
|
35
36
|
# always was), each further one names its directory relative to the
|
|
36
|
-
#
|
|
37
|
+
# project's top ("Project specific description for pkg/:").
|
|
37
38
|
# @return [Array<Section>]
|
|
38
39
|
def agents_sections(cwd)
|
|
39
|
-
top = MemoryPaths.
|
|
40
|
+
top = MemoryPaths.project_top(cwd)
|
|
40
41
|
sections = []
|
|
41
42
|
agents_paths(cwd).each do |path|
|
|
42
43
|
dir = File.dirname(path)
|
|
@@ -47,10 +48,12 @@ module Samagotchi
|
|
|
47
48
|
sections
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
# The directories whose AGENTS.md a session in +cwd+ reads, outermost
|
|
51
|
+
# The directories whose AGENTS.md a session in +cwd+ reads, outermost
|
|
52
|
+
# first: from its project's top (the work tree's, or the marked root)
|
|
53
|
+
# down to +cwd+.
|
|
51
54
|
def chain_dirs(cwd)
|
|
52
55
|
cwd = File.expand_path(cwd)
|
|
53
|
-
top = MemoryPaths.
|
|
56
|
+
top = MemoryPaths.project_top(cwd)
|
|
54
57
|
return [cwd] unless top
|
|
55
58
|
|
|
56
59
|
rel = relative_dir(cwd, top)
|
|
@@ -63,38 +63,31 @@ module Samagotchi
|
|
|
63
63
|
nil
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
# The level for +
|
|
66
|
+
# The level for +lookup+'s model and where it came from ("session",
|
|
67
67
|
# "SAMAGOTCHI_REASONING_HISTORY", "models: qwen", "hosts.work"; nil for
|
|
68
68
|
# the default).
|
|
69
|
-
# @param
|
|
70
|
-
# @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
|
|
71
|
-
# @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
|
|
69
|
+
# @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
|
|
72
70
|
# @param session [Symbol, nil] the session's own level (Session#reasoning_history)
|
|
73
71
|
# @return [Array(Symbol, String|nil)]
|
|
74
|
-
def resolve(
|
|
72
|
+
def resolve(lookup, session: nil)
|
|
75
73
|
return [session, SESSION_SOURCE] if session
|
|
76
74
|
|
|
77
|
-
env = env_level
|
|
75
|
+
env = env_level(lookup)
|
|
78
76
|
return [env, ENV_NAME] if env
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
ModelSettings.all
|
|
82
|
-
rescue StandardError
|
|
83
|
-
{}
|
|
84
|
-
end
|
|
85
|
-
setting = ModelSettings.find(names, :reasoning_history, models: models)
|
|
78
|
+
setting = lookup.model(:reasoning_history)
|
|
86
79
|
return [setting.value, "models: #{setting.key}"] if setting
|
|
87
80
|
|
|
88
|
-
host =
|
|
89
|
-
return [host, "hosts.#{
|
|
81
|
+
host = lookup.host(:reasoning_history)
|
|
82
|
+
return [host, "hosts.#{lookup.host_name}"] if host
|
|
90
83
|
|
|
91
|
-
[API_DEFAULTS.fetch(
|
|
84
|
+
[API_DEFAULTS.fetch(lookup.entry&.api&.to_sym, DEFAULT), nil]
|
|
92
85
|
end
|
|
93
86
|
|
|
94
87
|
# #resolve, as a SettingExplained with the session's own level beside it.
|
|
95
88
|
# @return [SettingExplained]
|
|
96
|
-
def explain(
|
|
97
|
-
level, source = resolve(
|
|
89
|
+
def explain(lookup, session: nil)
|
|
90
|
+
level, source = resolve(lookup, session: session)
|
|
98
91
|
SettingExplained.new(level: level, source: source, own: session)
|
|
99
92
|
end
|
|
100
93
|
|
|
@@ -150,9 +143,10 @@ module Samagotchi
|
|
|
150
143
|
# "drop (models: qwen)", "turn (default)".
|
|
151
144
|
def label(level, source) = "#{level} (#{source || "default"})"
|
|
152
145
|
|
|
153
|
-
# SAMAGOTCHI_REASONING_HISTORY as a level,
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
# SAMAGOTCHI_REASONING_HISTORY (read with +lookup+'s env) as a level,
|
|
147
|
+
# nil when unset or unknown.
|
|
148
|
+
def env_level(lookup)
|
|
149
|
+
raw, origin = lookup.global(KEY)
|
|
156
150
|
origin == :env ? level(raw, ENV_NAME) : nil
|
|
157
151
|
rescue StandardError
|
|
158
152
|
nil
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "steer"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# Where a saved recap sits among the turns a page draws: after the last
|
|
7
|
+
# turn opening it covers (Steer.turn_opening?, the rule its turns_since
|
|
8
|
+
# count goes by). Its stamp is that opening's id, as a card's is
|
|
9
|
+
# (Steer.opening_stamp: a prompt's turn_id, a wake note's, a /continue no
|
|
10
|
+
# reason line's opening_id), so the page finds the bubble by
|
|
11
|
+
# id, not by counting back (a rollback or a check-in could make the count
|
|
12
|
+
# land a turn off). Read at read time from recap.json's `covered`: the file
|
|
13
|
+
# is unchanged, an old one gets a stamp too. Pure.
|
|
14
|
+
module RecapPlace
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param messages [Array<Hash>] the session's messages (symbol or string keys)
|
|
18
|
+
# @param covered [Integer] how many of them the recap covers
|
|
19
|
+
# @return [String, nil] the id of the last opening in the covered
|
|
20
|
+
# messages; nil for none, or one with no id (a line an older chi wrote)
|
|
21
|
+
def opening_id(messages, covered)
|
|
22
|
+
opening = Array(messages).first([covered.to_i, 0].max).reverse_each.find { |message| Steer.turn_opening?(message) }
|
|
23
|
+
opening && Steer.opening_stamp(opening)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../session"
|
|
4
|
+
require_relative "../worker_link"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
module ReplyWait
|
|
8
|
+
# Who runs the session, as one look of a wait reads it: one owner lock
|
|
9
|
+
# read and one WorkerLink resolve on it. Two readers on purpose:
|
|
10
|
+
# #owner? (any kind, a one-turn chi run's too: its turn is alive) for
|
|
11
|
+
# whether the worker is gone, #worker_owned? (a worker's lock) for
|
|
12
|
+
# whether a question waits for an answer (a one-turn run's is answered
|
|
13
|
+
# there; a dead worker's waits for no one).
|
|
14
|
+
class Liveness
|
|
15
|
+
def initialize(id, state_dir:)
|
|
16
|
+
@id = id
|
|
17
|
+
@state_dir = state_dir
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The lock's holder (SessionManager.session_owner).
|
|
21
|
+
# @return [OwnerLock::Owner, nil]
|
|
22
|
+
def owner
|
|
23
|
+
return @owner if defined?(@owner)
|
|
24
|
+
|
|
25
|
+
@owner = SessionManager.session_owner(@id, state_dir: @state_dir)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def owner? = !owner.nil?
|
|
29
|
+
|
|
30
|
+
def worker_owned?
|
|
31
|
+
return @worker_owned if defined?(@worker_owned)
|
|
32
|
+
|
|
33
|
+
@worker_owned = WorkerLink.resolve(Session.session_dir(@id, state_dir: @state_dir), unlink: false,
|
|
34
|
+
owner: owner).worker_owned?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|