samagotchi 0.120.0 → 0.121.1
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 +116 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +36 -11
- data/docs/bundle-authoring.md +2 -1
- data/docs/cli.md +10 -4
- data/docs/configuration.md +34 -11
- data/docs/guardrails.md +48 -19
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/participants.md +6 -2
- data/docs/internals/prompt-caching.md +7 -6
- data/docs/internals/remote-sessions.md +15 -11
- data/docs/internals/web-push.md +22 -11
- data/docs/plugins.md +28 -8
- data/docs/sessions.md +15 -0
- data/docs/testing.md +44 -2
- data/lib/samagotchi/answer_command.rb +1 -1
- data/lib/samagotchi/bridge.rb +8 -8
- data/lib/samagotchi/bridge_client/event_stream.rb +11 -27
- data/lib/samagotchi/bridge_client/stream_open.rb +79 -0
- data/lib/samagotchi/bridge_client/stream_request.rb +8 -2
- data/lib/samagotchi/bridge_client.rb +52 -65
- data/lib/samagotchi/bridge_secret.rb +9 -7
- data/lib/samagotchi/bundle_command/check.rb +27 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +21 -7
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +14 -3
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +5 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_move.rb +5 -7
- data/lib/samagotchi/children_status.rb +2 -1
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/guardrail_wiring.rb +19 -2
- data/lib/samagotchi/guardrails/parent_approvals.rb +9 -0
- data/lib/samagotchi/guardrails_command.rb +41 -6
- data/lib/samagotchi/image_store.rb +6 -3
- data/lib/samagotchi/interrupted_turn.rb +6 -3
- data/lib/samagotchi/kernel_loop.rb +5 -0
- data/lib/samagotchi/live_versions.rb +6 -7
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
- data/lib/samagotchi/plugin/api.rb +14 -8
- data/lib/samagotchi/plugin/sessions.rb +13 -4
- data/lib/samagotchi/push_command.rb +1 -1
- data/lib/samagotchi/relay_verifier.rb +2 -3
- data/lib/samagotchi/relay_watcher.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +8 -2
- data/lib/samagotchi/reply_wait.rb +4 -3
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session.rb +4 -0
- data/lib/samagotchi/session_access/local.rb +18 -8
- data/lib/samagotchi/session_access/remote.rb +5 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +31 -19
- data/lib/samagotchi/session_access.rb +21 -9
- data/lib/samagotchi/session_continue.rb +2 -3
- data/lib/samagotchi/session_manager.rb +118 -28
- data/lib/samagotchi/sessions_command.rb +10 -5
- data/lib/samagotchi/system_one/client.rb +6 -1
- data/lib/samagotchi/system_one/fake_backend.rb +0 -2
- data/lib/samagotchi/system_one/log.rb +21 -6
- data/lib/samagotchi/system_one/self_protect.rb +39 -43
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
- data/lib/samagotchi/terminal_ui/local_link.rb +28 -19
- data/lib/samagotchi/terminal_ui/question_slot.rb +19 -8
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/toolkit.rb +117 -27
- data/lib/samagotchi/toolkit_binding.rb +32 -0
- data/lib/samagotchi/toolkit_command.rb +23 -0
- data/lib/samagotchi/tools/deferred_tools.rb +10 -2
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +9 -7
- data/lib/samagotchi/tools/registry.rb +20 -6
- data/lib/samagotchi/tools/tool_index.rb +103 -20
- data/lib/samagotchi/turn_checkpoint.rb +15 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +123 -65
- data/lib/samagotchi/web/participant_access.rb +26 -7
- data/lib/samagotchi/web/participant_review.rb +11 -6
- data/lib/samagotchi/web/participant_view.rb +7 -3
- data/lib/samagotchi/web/participants_admin.rb +13 -0
- data/lib/samagotchi/web/public/app.js +1 -1
- data/lib/samagotchi/web/public/forms.css +12 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/level_chip.js +2 -2
- data/lib/samagotchi/web/public/llm_context_chip.js +4 -4
- data/lib/samagotchi/web/public/notifications.js +21 -1
- data/lib/samagotchi/web/public/participant.html +4 -2
- data/lib/samagotchi/web/public/participant.js +34 -11
- data/lib/samagotchi/web/public/participants_panel.js +49 -14
- data/lib/samagotchi/web/public/push.js +4 -3
- data/lib/samagotchi/web/public/push_devices.js +17 -3
- data/lib/samagotchi/web/public/question_card.js +15 -1
- data/lib/samagotchi/web/public/question_cards.js +11 -2
- data/lib/samagotchi/web/public/review_history.js +24 -4
- data/lib/samagotchi/web/public/review_start.js +1 -3
- data/lib/samagotchi/web/public/session_actions.js +14 -2
- data/lib/samagotchi/web/public/storage.js +16 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -0
- data/lib/samagotchi/web/push/client.rb +5 -0
- data/lib/samagotchi/web/push/store.rb +16 -3
- data/lib/samagotchi/web/push_routes.rb +3 -2
- data/lib/samagotchi/web/session_access_routes.rb +25 -17
- data/lib/samagotchi/web/session_create.rb +4 -4
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +8 -16
- data/lib/samagotchi/web/tunnel.rb +10 -1
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_link.rb +213 -0
- data/lib/samagotchi/worker_sidecar.rb +15 -37
- metadata +4 -1
|
@@ -61,6 +61,7 @@ module Samagotchi
|
|
|
61
61
|
# uses.
|
|
62
62
|
SKIP_WORDS = {
|
|
63
63
|
already_replayed: "already replayed",
|
|
64
|
+
judged_live: "already judged live",
|
|
64
65
|
answered_by_parent: "answered by a parent agent",
|
|
65
66
|
parent_cannot_allow: "answered by a parent that could not allow",
|
|
66
67
|
unanswered: "unanswered",
|
|
@@ -73,6 +74,36 @@ module Samagotchi
|
|
|
73
74
|
# and when the user was asked.
|
|
74
75
|
Ask = Data.define(:question_id, :session_id, :approval, :label, :asked_at)
|
|
75
76
|
|
|
77
|
+
# What the judgments log already holds, so no ask is judged twice: the
|
|
78
|
+
# question ids its judgments name, with their origin (a replay's own, a
|
|
79
|
+
# live one's through its label), and, for live judgments with none (an ask that was
|
|
80
|
+
# cancelled, or a line older than the id), how many each (session, call
|
|
81
|
+
# digest) has. A card with no id match takes one of those (#take_live).
|
|
82
|
+
Judged = Data.define(:question_ids, :live_calls) do
|
|
83
|
+
# @param entries [Array<Hash>] SystemOne::Log#entries
|
|
84
|
+
def self.of(entries)
|
|
85
|
+
ids = entries.filter_map { |entry| [entry["question_id"], entry["origin"]] if entry["question_id"] }.to_h
|
|
86
|
+
live = Hash.new(0)
|
|
87
|
+
entries.each do |entry|
|
|
88
|
+
next unless entry["origin"] == "live" && entry["question_id"].nil?
|
|
89
|
+
|
|
90
|
+
live[[entry["session_id"], entry["call_digest"]]] += 1
|
|
91
|
+
end
|
|
92
|
+
new(question_ids: ids, live_calls: live)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether a live judgment without a question id is this card's: true
|
|
96
|
+
# (and that judgment counted off) while one with its session and call
|
|
97
|
+
# digest is left.
|
|
98
|
+
def take_live(session_id, approval)
|
|
99
|
+
key = [session_id, SystemOne::ApprovalQuestion.call_fields(approval)["call_digest"]]
|
|
100
|
+
return false unless live_calls[key].positive?
|
|
101
|
+
|
|
102
|
+
live_calls[key] -= 1
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
76
107
|
# @param argv [Array<String>] the arguments after "guardrails"
|
|
77
108
|
# @param client [SystemOne::Client, nil] the judgment client; nil: the
|
|
78
109
|
# one config.yml describes (the seam the specs pass a fake through)
|
|
@@ -122,7 +153,7 @@ module Samagotchi
|
|
|
122
153
|
|
|
123
154
|
sessions_dir = @sessions_dir || Session.default_state_dir
|
|
124
155
|
log = @log || SystemOne::Log.new(dir: Guardrails::Approvals.dir_for(sessions_dir))
|
|
125
|
-
|
|
156
|
+
judged = Judged.of(File.exist?(log.path) ? log.entries : [])
|
|
126
157
|
@dry_run = options[:dry_run] ? true : false
|
|
127
158
|
|
|
128
159
|
asks = []
|
|
@@ -140,7 +171,7 @@ module Samagotchi
|
|
|
140
171
|
end
|
|
141
172
|
|
|
142
173
|
entries.each do |entry|
|
|
143
|
-
ask = ask_of(entry, name,
|
|
174
|
+
ask = ask_of(entry, name, judged, sessions_dir)
|
|
144
175
|
next if ask.nil?
|
|
145
176
|
|
|
146
177
|
ask.is_a?(Symbol) ? skipped[ask] += 1 : asks << ask
|
|
@@ -168,9 +199,10 @@ module Samagotchi
|
|
|
168
199
|
end
|
|
169
200
|
|
|
170
201
|
# One card entry as an ask to judge: an Ask, a skip reason (:already_replayed,
|
|
171
|
-
# :answered_by_parent, :parent_cannot_allow, :unanswered,
|
|
172
|
-
# :no_question_id), or nil for an entry that is not a
|
|
173
|
-
|
|
202
|
+
# :judged_live, :answered_by_parent, :parent_cannot_allow, :unanswered,
|
|
203
|
+
# :no_approval, :no_question_id), or nil for an entry that is not a
|
|
204
|
+
# question at all.
|
|
205
|
+
def ask_of(entry, session_id, judged, sessions_dir)
|
|
174
206
|
return nil unless entry.is_a?(Hash) && entry[:type] == "question"
|
|
175
207
|
|
|
176
208
|
pending = entry[:pending_question]
|
|
@@ -178,7 +210,10 @@ module Samagotchi
|
|
|
178
210
|
|
|
179
211
|
question_id = pending[:id].to_s
|
|
180
212
|
return :no_question_id if question_id.empty?
|
|
181
|
-
|
|
213
|
+
|
|
214
|
+
origin = judged.question_ids[question_id]
|
|
215
|
+
return origin == "replay" ? :already_replayed : :judged_live if origin
|
|
216
|
+
return :judged_live if judged.take_live(session_id, pending[:approval])
|
|
182
217
|
|
|
183
218
|
answer = entry[:answer]
|
|
184
219
|
label = if entry[:cancelled]
|
|
@@ -201,6 +201,9 @@ module Samagotchi
|
|
|
201
201
|
# a request is sent.
|
|
202
202
|
module ImageStore
|
|
203
203
|
class Error < StandardError
|
|
204
|
+
# A ref to a file this session doesn't hold as an image: one wording
|
|
205
|
+
# for a turn's refs (#check_refs) and a web upload's (#ref_for).
|
|
206
|
+
def self.unknown_image(file) = new("unknown image #{file.to_s[0, 80]}")
|
|
204
207
|
end
|
|
205
208
|
|
|
206
209
|
DIR = "images"
|
|
@@ -278,7 +281,7 @@ module Samagotchi
|
|
|
278
281
|
|
|
279
282
|
ref = symbolize(image)
|
|
280
283
|
return "images are refs to uploaded files, not paths" if ref.key?(:path)
|
|
281
|
-
return
|
|
284
|
+
return Error.unknown_image(ref[:file]).message unless valid_ref?(session_dir, ref)
|
|
282
285
|
|
|
283
286
|
{ file: ref[:file].to_s, name: File.basename(ref[:name].to_s)[0, 120] }
|
|
284
287
|
end
|
|
@@ -321,11 +324,11 @@ module Samagotchi
|
|
|
321
324
|
# A ref for an image already stored in this session (a web upload),
|
|
322
325
|
# rebuilt from the file itself: only its name comes from the caller.
|
|
323
326
|
def self.ref_for(session_dir, file:, name: nil, source: "user")
|
|
324
|
-
raise Error
|
|
327
|
+
raise Error.unknown_image(file) unless valid_ref?(session_dir, { file: file })
|
|
325
328
|
|
|
326
329
|
bytes = File.binread(File.join(session_dir.to_s, file.to_s))
|
|
327
330
|
info = ImageHeader.read(bytes)
|
|
328
|
-
raise Error
|
|
331
|
+
raise Error.unknown_image(file) unless info && MIME.key?(info.format) && info.width
|
|
329
332
|
|
|
330
333
|
name = File.basename(name.to_s).gsub(/[[:cntrl:]]/, "")[0, 120]
|
|
331
334
|
{ file: file.to_s, mime: MIME.fetch(info.format), width: info.width, height: info.height,
|
|
@@ -13,9 +13,12 @@ module Samagotchi
|
|
|
13
13
|
# runs (its ack's id, LastTurn#enqueued_id once it ends; a Continue turn
|
|
14
14
|
# carries its offer turn's), so any process that loads the session can
|
|
15
15
|
# tell which turn is running (ReplyWait.own_turn); nil when it ran none,
|
|
16
|
-
# or a worker from before it wrote one.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
# or a worker from before it wrote one. author: a participant's turn's
|
|
17
|
+
# "p:<id>" (nil for the owner's), which its settled record keeps, so a
|
|
18
|
+
# chi web restarted since still knows whose wait it is
|
|
19
|
+
# (ParticipantAccess#turn_author).
|
|
20
|
+
InterruptedTurn = Data.define(:id, :started_at, :at, :iteration, :enqueued_id, :author) do
|
|
21
|
+
def initialize(id: nil, started_at: nil, at: nil, iteration: nil, enqueued_id: nil, author: nil) = super
|
|
19
22
|
|
|
20
23
|
# A saved record (string or symbol keys), an InterruptedTurn as it is;
|
|
21
24
|
# nil for anything else.
|
|
@@ -176,6 +176,11 @@ module Samagotchi
|
|
|
176
176
|
# The Guardrails::Gate ToolRunner asks before each call; the Engine sets
|
|
177
177
|
# it (nil: ToolRunner's own, hooks only).
|
|
178
178
|
attr_accessor :guardrail_gate
|
|
179
|
+
# Whether the current turn belongs to a participant, so pre-gate
|
|
180
|
+
# refusals can defer to ReviewBoundary.
|
|
181
|
+
attr_accessor :participant_turn
|
|
182
|
+
|
|
183
|
+
def participant_turn? = @participant_turn&.call == true
|
|
179
184
|
# @return [LLM::TurnSettings] the turn's vision, sampling, thinking and
|
|
180
185
|
# model name, set by the Engine per turn (#run sets the model name
|
|
181
186
|
# too); the thinking level is its own field, since the native path
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "session"
|
|
4
4
|
require_relative "version"
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "worker_link"
|
|
6
6
|
require_relative "web/info_probe"
|
|
7
7
|
|
|
8
8
|
module Samagotchi
|
|
@@ -21,13 +21,12 @@ module Samagotchi
|
|
|
21
21
|
|
|
22
22
|
# @return [Array<Worker>] the workers whose Bridge answers, by session id
|
|
23
23
|
def workers(state_dir: Session.default_state_dir)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
next unless sidecar
|
|
24
|
+
WorkerLink.each_dir(state_dir).filter_map do |session_dir|
|
|
25
|
+
link = WorkerLink.resolve(session_dir, unlink: false)
|
|
26
|
+
next unless link.live?
|
|
28
27
|
|
|
29
|
-
Worker.new(session_id: File.basename(session_dir), version:
|
|
30
|
-
restart:
|
|
28
|
+
Worker.new(session_id: File.basename(session_dir), version: link.version,
|
|
29
|
+
restart: link.features.include?("restart"))
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -152,7 +152,7 @@ module Samagotchi
|
|
|
152
152
|
# @raise [Uninstaller::UninstallError] when +name+ isn't installed
|
|
153
153
|
def uninstall(name, shipped_dir: SourceNormalizer::SHIPPED_DIR, force: false)
|
|
154
154
|
bundle = Provenance.new(name: name).record
|
|
155
|
-
raise Uninstaller::UninstallError
|
|
155
|
+
raise Uninstaller::UninstallError.not_installed(name) unless bundle
|
|
156
156
|
|
|
157
157
|
shipped = File.join(shipped_dir, name)
|
|
158
158
|
manifest = File.file?(File.join(shipped, "manifest.yml")) ? Manifest.read(dir: shipped) : nil
|
|
@@ -12,7 +12,11 @@ require_relative "../model_overlay"
|
|
|
12
12
|
module Samagotchi
|
|
13
13
|
module MemoryBundle
|
|
14
14
|
class Uninstaller
|
|
15
|
-
class UninstallError < StandardError
|
|
15
|
+
class UninstallError < StandardError
|
|
16
|
+
# The bundle +name+ has no install record: one wording for the
|
|
17
|
+
# uninstall of a bundle and of a profile (Profile#uninstall).
|
|
18
|
+
def self.not_installed(name) = new("Bundle '#{name}' is not installed")
|
|
19
|
+
end
|
|
16
20
|
|
|
17
21
|
# removed_files: the bundle's own hooks/ and plugin/ files (deleted);
|
|
18
22
|
# trashed_files: its memory files, moved to trash_dir (Trash), nil
|
|
@@ -34,7 +38,7 @@ module Samagotchi
|
|
|
34
38
|
def run
|
|
35
39
|
provenance = Provenance.new(name: @name)
|
|
36
40
|
bundle = provenance.record
|
|
37
|
-
raise UninstallError
|
|
41
|
+
raise UninstallError.not_installed(@name) unless bundle
|
|
38
42
|
|
|
39
43
|
# The scope from the flag, else the record's
|
|
40
44
|
target_scope = @scope.to_s.strip.empty? ? bundle.effective_scope : @scope
|
|
@@ -24,9 +24,11 @@ module Samagotchi
|
|
|
24
24
|
# A call not listed gets no floor check: it is in the first tag (v0.2.0:
|
|
25
25
|
# chi.command/tool/on/service/init, ctx.settings, ctx.sessions,
|
|
26
26
|
# ctx.ask_model, ctx.notify, ctx.card, ctx.messages…). A member of
|
|
27
|
-
# one (ctx.sessions.children) is listed when it came later
|
|
28
|
-
# no "?" (API_CALL drops it). A
|
|
29
|
-
#
|
|
27
|
+
# one (ctx.sessions.children) is listed when it came later; fork, send
|
|
28
|
+
# and read are in v0.2.0. Keys have no "?" (API_CALL drops it). A
|
|
29
|
+
# keyword added later is "call(keyword:)", the call's receiver as the
|
|
30
|
+
# plugin writes it (Check#keyword_calls: `ctx.sessions.send(…,
|
|
31
|
+
# delivery:)` is seen, a `sessions = ctx.sessions` alias isn't).
|
|
30
32
|
# Events aren't listed: chi.on takes any symbol and an older chi never
|
|
31
33
|
# fires one it doesn't know (harmless; chi bundle check warns on it),
|
|
32
34
|
# and a manifest hook's events: list (BundleHook) needs the bundle's
|
|
@@ -42,7 +44,8 @@ module Samagotchi
|
|
|
42
44
|
"chi.tool_source" => "0.67.0", "chi.peer_source" => "0.67.0", "ctx.http" => "0.80.0",
|
|
43
45
|
"ctx.cost" => "0.83.0", "ctx.sessions.children" => "0.34.0", "ctx.load_tools" => "0.99.0",
|
|
44
46
|
"ctx.usage" => "0.100.0", "ctx.sessions.stop" => "0.34.0", "ctx.turn_author" => "0.106.0",
|
|
45
|
-
"ctx.sessions.review" => "0.107.0"
|
|
47
|
+
"ctx.sessions.review" => "0.107.0", "ctx.sessions.send(delivery:)" => "0.64.0",
|
|
48
|
+
"chi.toolkit(prompt:)" => "0.72.0", "ctx.ask_model(model:)" => "0.80.0", "chi.toolkit(groups:)" => "0.121.0"
|
|
46
49
|
}.freeze
|
|
47
50
|
|
|
48
51
|
# @param bundle [String]
|
|
@@ -308,16 +311,18 @@ module Samagotchi
|
|
|
308
311
|
# toolkit.default, ctx.use_toolkit): +base+'s tools (a toolkit chi or
|
|
309
312
|
# a bundle has) with +add+ declared and +remove+ left out, and
|
|
310
313
|
# +prompt+: text the sessions on it get in their system prompt (nil:
|
|
311
|
-
#
|
|
314
|
+
# its base's, "" none; at most Toolkit::PROMPT_MAX characters);
|
|
315
|
+
# +groups+: MCP source group names/globs (nil inherits its base).
|
|
316
|
+
# A
|
|
312
317
|
# name chi (full, lean, child) or another bundle has is a load error,
|
|
313
318
|
# and so is a prompt that isn't text or is too long; a base that
|
|
314
319
|
# doesn't resolve leaves the sessions on it with every tool
|
|
315
320
|
# (Toolkit#resolve).
|
|
316
|
-
def toolkit(name, base:, add: [], remove: [], prompt: nil)
|
|
321
|
+
def toolkit(name, base:, add: [], remove: [], prompt: nil, groups: nil)
|
|
317
322
|
name = name.to_s
|
|
318
323
|
raise ArgumentError, "toolkit name #{name.inspect} must be a-z, 0-9, _ and -" unless name.match?(TOOLKIT_NAME)
|
|
319
324
|
|
|
320
|
-
problem = Toolkit.prompt_error(prompt)
|
|
325
|
+
problem = Toolkit.prompt_error(prompt) || (Toolkit.groups_error(groups) unless groups == :all)
|
|
321
326
|
raise ArgumentError, "toolkit #{name}: #{problem}" if problem
|
|
322
327
|
raise ArgumentError, "this chi has no toolkits (plugins: false)" unless @registries.toolkits
|
|
323
328
|
if Toolkit::BUILTIN_KITS.key?(name)
|
|
@@ -327,7 +332,8 @@ module Samagotchi
|
|
|
327
332
|
end
|
|
328
333
|
raise ArgumentError, "toolkit #{name} is declared twice" if @toolkits.any? { |kit| kit.name == name }
|
|
329
334
|
|
|
330
|
-
@toolkits << Toolkit::Kit.new(name: name, base: base, add: add, remove: remove, prompt: prompt,
|
|
335
|
+
@toolkits << Toolkit::Kit.new(name: name, base: base, add: add, remove: remove, prompt: prompt,
|
|
336
|
+
groups: groups, source: @bundle)
|
|
331
337
|
nil
|
|
332
338
|
end
|
|
333
339
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative "../client_id"
|
|
4
4
|
require_relative "../delivery"
|
|
5
5
|
require_relative "../session"
|
|
6
|
-
require_relative "../
|
|
6
|
+
require_relative "../worker_link"
|
|
7
7
|
require_relative "../bridge/turn_accumulator"
|
|
8
8
|
require_relative "../tools/delegate"
|
|
9
9
|
require_relative "../children_status"
|
|
@@ -27,6 +27,11 @@ module Samagotchi
|
|
|
27
27
|
# A fork, send, read or stop that couldn't be done; the message says why.
|
|
28
28
|
class Error < StandardError; end
|
|
29
29
|
|
|
30
|
+
# What starting a session can be refused by, answered as Error with
|
|
31
|
+
# chi's own message: a model that is gone, a toolkit the config refuses,
|
|
32
|
+
# a bad argument. #review adds its own: a subject it won't take.
|
|
33
|
+
REFUSED = [ModelProfile::MissingModel, Toolkit::Refused, ArgumentError].freeze
|
|
34
|
+
|
|
30
35
|
# @param host [Host] session_id, cwd, model_name and state_dir
|
|
31
36
|
def initialize(host)
|
|
32
37
|
@host = host
|
|
@@ -44,7 +49,9 @@ module Samagotchi
|
|
|
44
49
|
# @param prompt [String, nil] the child's first turn
|
|
45
50
|
# @return [String] the child's id
|
|
46
51
|
# @raise [Error] no session yet, a scratch session (the child would
|
|
47
|
-
# outlive it),
|
|
52
|
+
# outlive it), too many children running, or the model or toolkit the
|
|
53
|
+
# child starts on being refused (a config kit with an invalid groups:,
|
|
54
|
+
# a model that is gone)
|
|
48
55
|
def fork(messages:, title: nil, prompt: nil)
|
|
49
56
|
parent_id = @host.session_id.call or raise Error, "this session has no id yet"
|
|
50
57
|
raise Error, "a scratch session starts no other sessions: they would outlive it" if @host.scratch&.call
|
|
@@ -60,6 +67,8 @@ module Samagotchi
|
|
|
60
67
|
setup: @host.setup&.call || SessionSetup.new
|
|
61
68
|
)
|
|
62
69
|
child.id
|
|
70
|
+
rescue *REFUSED => e
|
|
71
|
+
raise Error, e.message
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
# A review of this session (ReviewStart.fork, chi review --from): a new
|
|
@@ -81,7 +90,7 @@ module Samagotchi
|
|
|
81
90
|
|
|
82
91
|
ReviewStart.fork(from: source, subjects: Array(subjects), seed: seed, state_dir: state_dir,
|
|
83
92
|
recap_wait: recap_wait).id
|
|
84
|
-
rescue ReviewStart::Refused
|
|
93
|
+
rescue *REFUSED, ReviewStart::Refused => e
|
|
85
94
|
raise Error, e.message
|
|
86
95
|
end
|
|
87
96
|
|
|
@@ -212,7 +221,7 @@ module Samagotchi
|
|
|
212
221
|
end
|
|
213
222
|
|
|
214
223
|
def live_snapshot(sid, state_dir)
|
|
215
|
-
|
|
224
|
+
WorkerLink.client(sid, session_dir: Session.session_dir(sid, state_dir: state_dir))&.get_json("snapshot")
|
|
216
225
|
rescue StandardError
|
|
217
226
|
nil
|
|
218
227
|
end
|
|
@@ -135,7 +135,7 @@ module Samagotchi
|
|
|
135
135
|
# route does). @return [Boolean] delivered
|
|
136
136
|
def test(sub)
|
|
137
137
|
result = client_for(sub).deliver(sub, Web::Push::TEST_PAYLOAD, ttl: Web::Push::TEST_TTL, urgency: "high")
|
|
138
|
-
words =
|
|
138
|
+
words = [result.status, result.code, ("(#{result.ms} ms)" if result.ms)].reject { |part| part.to_s.empty? }.join(" ")
|
|
139
139
|
words += ": #{result.error}" if result.error && !result.ok?
|
|
140
140
|
if result.status == :gone
|
|
141
141
|
@store.remove(sub.id)
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "client_id"
|
|
4
4
|
require_relative "session"
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "worker_sidecar"
|
|
5
|
+
require_relative "worker_link"
|
|
7
6
|
require_relative "question_desk"
|
|
8
7
|
require_relative "log"
|
|
9
8
|
|
|
@@ -87,7 +86,7 @@ module Samagotchi
|
|
|
87
86
|
# @return [Hash, nil] what the parent's live Bridge says about the relay
|
|
88
87
|
def parent_relay(parent_id, relay_id)
|
|
89
88
|
dir = Session.session_dir(parent_id, state_dir: @state_dir)
|
|
90
|
-
client =
|
|
89
|
+
client = WorkerLink.client(parent_id, session_dir: dir) or return nil
|
|
91
90
|
response = client.relay_status(relay_id)
|
|
92
91
|
response.ok? ? response.json : nil
|
|
93
92
|
rescue SystemCallError, IOError
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "worker_link"
|
|
4
4
|
require_relative "log"
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
@@ -20,7 +20,7 @@ module Samagotchi
|
|
|
20
20
|
# @param live [#call] whether the parent's worker is up
|
|
21
21
|
# @return [Thread]
|
|
22
22
|
def start(engine:, question_id:, relay_id:, parent_dir:, interval: INTERVAL,
|
|
23
|
-
live: -> {
|
|
23
|
+
live: -> { WorkerLink.resolve(parent_dir, unlink: false).live? })
|
|
24
24
|
Thread.new do
|
|
25
25
|
loop do
|
|
26
26
|
sleep(interval)
|
|
@@ -48,7 +48,7 @@ module Samagotchi
|
|
|
48
48
|
ignored(id) if existing
|
|
49
49
|
launch(id, existing: existing)
|
|
50
50
|
rescue TerminalUI::AttachLauncher::Error, SessionAccess::RemoteFailed, SessionAccess::Refused,
|
|
51
|
-
SessionManager::OwnedByTUI, ModelProfile::MissingModel, ArgumentError => e
|
|
51
|
+
SessionManager::OwnedByTUI, ModelProfile::MissingModel, Toolkit::Refused, ArgumentError => e
|
|
52
52
|
e.is_a?(SessionAccess::BadRequest) ? usage(e.message) : failed(e.message)
|
|
53
53
|
rescue Interrupt
|
|
54
54
|
CLI::Exit::INTERRUPTED
|
|
@@ -5,6 +5,7 @@ require_relative "../session_inbox"
|
|
|
5
5
|
require_relative "../turn_checkpoint"
|
|
6
6
|
require_relative "../continue_offer"
|
|
7
7
|
require_relative "../steer"
|
|
8
|
+
require_relative "../worker_link"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
10
11
|
module ReplyWait
|
|
@@ -164,11 +165,16 @@ module Samagotchi
|
|
|
164
165
|
pending = session.pending_question
|
|
165
166
|
return nil unless pending && !@baseline&.question?(pending.id) &&
|
|
166
167
|
(@turn.nil? || ReplyWait.own_question?(session, @turn, @claimed)) &&
|
|
167
|
-
session.waiting_question(live:
|
|
168
|
+
session.waiting_question(live: worker_owned?)
|
|
168
169
|
|
|
169
170
|
Result.new(status: :waiting_for_answer, question: pending, session: session)
|
|
170
171
|
end
|
|
171
172
|
|
|
173
|
+
# A worker holds the session's lock (a chi REPL's question is its own).
|
|
174
|
+
def worker_owned?
|
|
175
|
+
WorkerLink.resolve(Session.session_dir(@id, state_dir: @state_dir), unlink: false).worker_owned?
|
|
176
|
+
end
|
|
177
|
+
|
|
172
178
|
# A participant's turn its worker holds while the owner's Continue
|
|
173
179
|
# offer stands (Worker#run_input_files): it waits for the owner's
|
|
174
180
|
# answer to that question, whenever it was asked (the baseline's
|
|
@@ -176,7 +182,7 @@ module Samagotchi
|
|
|
176
182
|
def held(session)
|
|
177
183
|
pending = session.pending_question
|
|
178
184
|
return nil unless @participant_input && !@claimed && pending&.kind.to_s == ContinueOffer::KIND &&
|
|
179
|
-
session.waiting_question(live:
|
|
185
|
+
session.waiting_question(live: worker_owned?)
|
|
180
186
|
|
|
181
187
|
Result.new(status: :waiting_for_answer, question: pending, session: session)
|
|
182
188
|
end
|
|
@@ -4,7 +4,7 @@ require_relative "session"
|
|
|
4
4
|
require_relative "turn_note"
|
|
5
5
|
require_relative "session_inbox"
|
|
6
6
|
require_relative "turn_checkpoint"
|
|
7
|
-
require_relative "
|
|
7
|
+
require_relative "worker_link"
|
|
8
8
|
require_relative "reply_wait/poll"
|
|
9
9
|
|
|
10
10
|
module Samagotchi
|
|
@@ -214,9 +214,10 @@ module Samagotchi
|
|
|
214
214
|
running || queued || ended
|
|
215
215
|
end
|
|
216
216
|
|
|
217
|
-
# Whether the session's worker
|
|
217
|
+
# Whether the session's live worker names TURN_IDS_FEATURE.
|
|
218
218
|
def turn_ids?(session_dir)
|
|
219
|
-
|
|
219
|
+
link = WorkerLink.resolve(session_dir, unlink: false)
|
|
220
|
+
link.live? && link.features.include?(TURN_IDS_FEATURE)
|
|
220
221
|
end
|
|
221
222
|
|
|
222
223
|
# A pending question is a turn: wait's (its enqueued_id) only when that
|
|
@@ -61,7 +61,7 @@ module Samagotchi
|
|
|
61
61
|
start(parsed.options, parsed.args)
|
|
62
62
|
rescue ReviewStart::Refused, SessionAccess::BadRequest => e
|
|
63
63
|
refused(e.message)
|
|
64
|
-
rescue ModelProfile::MissingModel => e
|
|
64
|
+
rescue ModelProfile::MissingModel, Toolkit::Refused => e
|
|
65
65
|
refused("could not start: #{e.message}")
|
|
66
66
|
rescue ArgumentError => e
|
|
67
67
|
refused(e.is_a?(Session::AmbiguousId) ? e.message : e.message.sub(/\ASession not found: /, "no session "))
|
|
@@ -165,6 +165,7 @@ module Samagotchi
|
|
|
165
165
|
chi review FILE... [--dir DIR] | --from ID a review session to share (chi share)
|
|
166
166
|
chi memory <ls|rm> [--scope project|system] the memories; rm moves one to the bundle trash
|
|
167
167
|
chi bundle <install|upgrade|uninstall|status|diff|list|build|trash|new|test|check|try>
|
|
168
|
+
chi guardrails replay [--dry-run] [--threshold T] [--model M] seed System 1's record from answered approvals
|
|
168
169
|
chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
|
|
169
170
|
chi models [--format json] [TEXT] the models --model takes, from every host
|
|
170
171
|
chi remote <add|ls|rm> other machines' chi web, for chi --remote NAME
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -427,10 +427,14 @@ module Samagotchi
|
|
|
427
427
|
# seed); [] by default
|
|
428
428
|
# @param setup [SessionSetup] the session's own settings (none by default)
|
|
429
429
|
# @param now [Time] its created_at and updated_at (a spec's clock)
|
|
430
|
+
# @raise [Toolkit::Refused] its toolkit (setup's, else the configured
|
|
431
|
+
# one SessionManager.spawn_session gives it) builds on a config kit
|
|
432
|
+
# with an invalid groups: (Toolkit.check_new!)
|
|
430
433
|
def self.new_session(mode:, model_name:, working_directory:, test_run: nil,
|
|
431
434
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, messages: [],
|
|
432
435
|
scratch: false, model_typed: nil, delegate: false, setup: SessionSetup.new, continues: nil,
|
|
433
436
|
review: nil, now: Time.now)
|
|
437
|
+
Toolkit.check_new!(setup.toolkit || Toolkit.configured(delegate: delegate))
|
|
434
438
|
now = now.iso8601(3)
|
|
435
439
|
resolved_test = if test_run.nil?
|
|
436
440
|
test_session_env?
|
|
@@ -14,6 +14,7 @@ require_relative "../context_sources"
|
|
|
14
14
|
require_relative "../send_command/busy"
|
|
15
15
|
require_relative "../chi_invocation"
|
|
16
16
|
require_relative "../worker_sidecar"
|
|
17
|
+
require_relative "../worker_link"
|
|
17
18
|
require_relative "../review_start"
|
|
18
19
|
require_relative "../launch_mode"
|
|
19
20
|
require_relative "../participants"
|
|
@@ -95,13 +96,22 @@ module Samagotchi
|
|
|
95
96
|
session = show(id)
|
|
96
97
|
raise Refused.new(LaunchMode::SCRATCH_LEFTOVER, code: "scratch") if session.scratch
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
link = WorkerLink.resolve(session_dir(id), unlink: true)
|
|
100
|
+
case link
|
|
101
|
+
when WorkerLink::Live then return Woken.new(woke: false, sidecar: link.sidecar)
|
|
102
|
+
when WorkerLink::Skew then raise Refused.new(link.message(id), code: "worker_newer")
|
|
103
|
+
when WorkerLink::Starting
|
|
104
|
+
# A worker that never publishes its Bridge (a failed start, a
|
|
105
|
+
# crash's teardown) lets go of the lock: then resume, not 504.
|
|
106
|
+
found = WorkerLink.await(id, session_dir: session_dir(id), timeout: wait,
|
|
107
|
+
give_up: ->(link) { !link.worker_owned? })
|
|
108
|
+
return Woken.new(woke: false, sidecar: found.live? ? found.sidecar : nil) if found.worker_owned?
|
|
109
|
+
end
|
|
100
110
|
|
|
101
111
|
OwnerLock.await_release(session_dir(id))
|
|
102
112
|
SessionManager.resume_session(id, **dir)
|
|
103
|
-
found =
|
|
104
|
-
Woken.new(woke: true, sidecar: found
|
|
113
|
+
found = WorkerLink.await(id, session_dir: session_dir(id), timeout: wait)
|
|
114
|
+
Woken.new(woke: true, sidecar: found.live? ? found.sidecar : nil)
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
# The session's worker now, never started (chi web's GET …/worker).
|
|
@@ -109,8 +119,8 @@ module Samagotchi
|
|
|
109
119
|
# @raise [ArgumentError] no such session
|
|
110
120
|
def worker(id)
|
|
111
121
|
show(id)
|
|
112
|
-
WorkerState.new(
|
|
113
|
-
stopped: Session.stopped_marker?(id, state_dir: state_dir!)
|
|
122
|
+
WorkerState.new(link: WorkerLink.resolve(session_dir(id), unlink: true),
|
|
123
|
+
stopped: Session.stopped_marker?(id, state_dir: state_dir!))
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
# --- create and send
|
|
@@ -148,7 +158,7 @@ module Samagotchi
|
|
|
148
158
|
# Whether a just-created session's worker came up (its Bridge
|
|
149
159
|
# answers) within +timeout+ seconds.
|
|
150
160
|
def await_worker(id, timeout:)
|
|
151
|
-
|
|
161
|
+
WorkerLink.await(id, session_dir: session_dir(id), timeout: timeout).live?
|
|
152
162
|
end
|
|
153
163
|
|
|
154
164
|
# Copies of +refs+ (ImageStore refs stored in +from+) in the
|
|
@@ -479,7 +489,7 @@ module Samagotchi
|
|
|
479
489
|
# The block's post to the session's live worker, again on a 408 (the
|
|
480
490
|
# worker didn't take it in time).
|
|
481
491
|
def on_worker(id)
|
|
482
|
-
client =
|
|
492
|
+
client = WorkerLink.client(id, session_dir: session_dir(id)) or return nil
|
|
483
493
|
reply = nil
|
|
484
494
|
2.times do
|
|
485
495
|
reply = yield client
|
|
@@ -12,6 +12,7 @@ require_relative "../bridge_client"
|
|
|
12
12
|
require_relative "../session_continue"
|
|
13
13
|
require_relative "../guardrails/parent_policy"
|
|
14
14
|
require_relative "../model_profile"
|
|
15
|
+
require_relative "../web/grants"
|
|
15
16
|
require_relative "http_transport"
|
|
16
17
|
require_relative "../chi_invocation"
|
|
17
18
|
|
|
@@ -679,6 +680,10 @@ module Samagotchi
|
|
|
679
680
|
# No model configured there, or --model's host unknown there: what
|
|
680
681
|
# spawn_session raises here (chi send: could not start a session).
|
|
681
682
|
when "invalid_model" then raise ModelProfile::MissingModel, message.to_s
|
|
683
|
+
# A toolkit whose config kit's groups: is invalid there: the same.
|
|
684
|
+
when "toolkit_refused" then raise Toolkit::Refused, message.to_s
|
|
685
|
+
# The grants store doesn't read there: chi share says what it says here.
|
|
686
|
+
when "grants_unwritable" then raise Web::Grants::Unwritable, message.to_s
|
|
682
687
|
when /\Ainvalid_/, "missing_fields", "bad_images"
|
|
683
688
|
# invalid_participant: chi share's name, kind or ttl, an archived session
|
|
684
689
|
raise BadRequest.new(message.to_s, code: error)
|