samagotchi 0.121.0 → 0.122.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 +110 -1
- data/bin/chi +7 -3
- data/docs/architecture.md +37 -11
- data/docs/bundle-authoring.md +2 -1
- data/docs/cli.md +11 -4
- data/docs/configuration.md +36 -7
- data/docs/guardrails.md +63 -19
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +2 -0
- data/docs/internals/participants.md +16 -8
- data/docs/internals/remote-sessions.md +15 -11
- data/docs/internals/web-push.md +22 -11
- data/docs/memory.md +33 -0
- data/docs/plugins.md +8 -3
- data/docs/sessions.md +21 -0
- data/docs/testing.md +41 -1
- data/lib/samagotchi/answer_command.rb +1 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +12 -11
- data/lib/samagotchi/bridge_client/event_stream.rb +14 -26
- data/lib/samagotchi/bridge_client/stream_open.rb +85 -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 +20 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +14 -3
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +13 -0
- data/lib/samagotchi/child_move.rb +5 -7
- data/lib/samagotchi/children_status.rb +2 -1
- data/lib/samagotchi/config.rb +5 -0
- data/lib/samagotchi/engine.rb +107 -1
- data/lib/samagotchi/event_fanout.rb +0 -1
- data/lib/samagotchi/guardrail_wiring.rb +24 -6
- 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/live_versions.rb +6 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
- data/lib/samagotchi/memory_orphans.rb +11 -0
- data/lib/samagotchi/participant_mode.rb +11 -0
- data/lib/samagotchi/plugin/api.rb +14 -8
- data/lib/samagotchi/plugin/sessions.rb +13 -4
- data/lib/samagotchi/process_group.rb +13 -0
- 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/review_start.rb +6 -2
- data/lib/samagotchi/run_options.rb +2 -0
- data/lib/samagotchi/session.rb +18 -1
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +18 -8
- data/lib/samagotchi/session_access/remote.rb +37 -12
- data/lib/samagotchi/session_access/web_bridge_client.rb +38 -19
- data/lib/samagotchi/session_access.rb +21 -9
- data/lib/samagotchi/session_continue.rb +2 -3
- data/lib/samagotchi/session_manager.rb +133 -29
- data/lib/samagotchi/session_setup.rb +8 -4
- data/lib/samagotchi/sessions_command.rb +10 -5
- data/lib/samagotchi/steer.rb +7 -6
- data/lib/samagotchi/system_one/client.rb +12 -6
- data/lib/samagotchi/system_one/fake_backend.rb +0 -2
- data/lib/samagotchi/system_one/log.rb +44 -9
- 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.rb +2 -1
- data/lib/samagotchi/tool_runner.rb +2 -1
- data/lib/samagotchi/toolkit.rb +95 -10
- data/lib/samagotchi/toolkit_binding.rb +115 -7
- data/lib/samagotchi/toolkit_command.rb +15 -3
- data/lib/samagotchi/toolkit_report_command.rb +318 -0
- data/lib/samagotchi/toolkit_suggest.rb +211 -0
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +9 -7
- data/lib/samagotchi/tools/memory.rb +1 -1
- data/lib/samagotchi/turn_checkpoint.rb +15 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +127 -69
- data/lib/samagotchi/web/participant_access.rb +30 -8
- data/lib/samagotchi/web/participant_review.rb +8 -5
- data/lib/samagotchi/web/participant_view.rb +7 -3
- data/lib/samagotchi/web/participants_admin.rb +13 -0
- data/lib/samagotchi/web/principal.rb +3 -4
- data/lib/samagotchi/web/public/app.js +1 -1
- data/lib/samagotchi/web/public/bubbles.js +3 -2
- data/lib/samagotchi/web/public/notifications.js +21 -1
- data/lib/samagotchi/web/public/participant.html +3 -2
- data/lib/samagotchi/web/public/participant.js +25 -10
- data/lib/samagotchi/web/public/participants_panel.js +46 -11
- data/lib/samagotchi/web/public/push.js +4 -3
- data/lib/samagotchi/web/public/push_devices.js +17 -3
- data/lib/samagotchi/web/public/review_history.js +14 -2
- 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/turn_events.js +2 -1
- data/lib/samagotchi/web/public/turn_html.js +1 -1
- 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/server.rb +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +28 -20
- data/lib/samagotchi/web/session_control.rb +4 -2
- 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/start_dir.rb +10 -5
- data/lib/samagotchi/worker.rb +17 -3
- data/lib/samagotchi/worker_link.rb +214 -0
- data/lib/samagotchi/worker_sidecar.rb +15 -37
- metadata +6 -1
|
@@ -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 "))
|
|
@@ -7,6 +7,7 @@ require_relative "session_chain"
|
|
|
7
7
|
require_relative "recap_store"
|
|
8
8
|
require_relative "guardrails/context"
|
|
9
9
|
require_relative "guardrails/review_paths"
|
|
10
|
+
require_relative "participant_mode"
|
|
10
11
|
|
|
11
12
|
module Samagotchi
|
|
12
13
|
autoload :SessionManager, File.expand_path("session_manager", __dir__)
|
|
@@ -75,6 +76,7 @@ module Samagotchi
|
|
|
75
76
|
subjects = check_subjects(subjects, dir: dir)
|
|
76
77
|
raise Refused, NO_SUBJECTS if subjects.empty?
|
|
77
78
|
|
|
79
|
+
setup = setup.with(toolkit_origin: "chosen")
|
|
78
80
|
SessionManager.spawn_session(prompt: nil, preview: preview(subjects), working_directory: dir, model_name: model,
|
|
79
81
|
setup: setup, review: SessionReview.new(subjects: subjects),
|
|
80
82
|
note: note(subjects: subjects), note_source: NOTE_SOURCE, state_dir: state_dir)
|
|
@@ -103,7 +105,8 @@ module Samagotchi
|
|
|
103
105
|
recap = RecapStore.read(Session.session_dir(source.id, state_dir: sd))
|
|
104
106
|
session = SessionManager.spawn_session(
|
|
105
107
|
prompt: nil, preview: preview(subjects, source: source), working_directory: dir, model_name: choice.model,
|
|
106
|
-
setup: SessionSetup.of(source)
|
|
108
|
+
setup: SessionSetup.of(source).with(toolkit_origin: "chosen"),
|
|
109
|
+
review: SessionReview.new(subjects: subjects, from: source.id),
|
|
107
110
|
note: note(subjects: subjects, source: source, recap: recap, seed: seed), note_source: NOTE_SOURCE, state_dir: sd
|
|
108
111
|
)
|
|
109
112
|
warnings = [choice.warning, session.model_warning].compact
|
|
@@ -145,10 +148,11 @@ module Samagotchi
|
|
|
145
148
|
# @param session [Session]
|
|
146
149
|
# @return [String, nil]
|
|
147
150
|
def share_warning(session)
|
|
151
|
+
mode = ParticipantMode.of(session)
|
|
148
152
|
return nil if session.review?
|
|
149
153
|
|
|
150
154
|
count = session.tool_output_count
|
|
151
|
-
"#{session.id[0, 8]} is a working session, not a review session: a participant's
|
|
155
|
+
"#{session.id[0, 8]} is a working session, not a review session: a participant's #{mode} turn can quote what " \
|
|
152
156
|
"its history holds (#{count} tool output#{"s" unless count == 1} so far). A review session starts from its recap instead."
|
|
153
157
|
end
|
|
154
158
|
|
|
@@ -165,6 +165,8 @@ 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
|
|
169
|
+
chi toolkit report [--days N] grade toolkit suggestions against the tools sessions used
|
|
168
170
|
chi desktop <install|upgrade|uninstall|status> macOS "Send to chi" helper
|
|
169
171
|
chi models [--format json] [TEXT] the models --model takes, from every host
|
|
170
172
|
chi remote <add|ls|rm> other machines' chi web, for chi --remote NAME
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Samagotchi
|
|
|
32
32
|
STATUS_RUNNING = "running"
|
|
33
33
|
STATUS_ERROR = "error"
|
|
34
34
|
STATUS_STOPPED = "stopped"
|
|
35
|
+
TOOLKIT_ORIGINS = %w[chosen default inherited suggested].freeze
|
|
35
36
|
|
|
36
37
|
# A stopped session's marker, <session dir>/stopped: a stop (chi
|
|
37
38
|
# sessions stop, from another process) creates it instead of rewriting
|
|
@@ -222,6 +223,15 @@ module Samagotchi
|
|
|
222
223
|
@toolkit = value.is_a?(Toolkit) ? value : Toolkit.from_file(value)
|
|
223
224
|
end
|
|
224
225
|
|
|
226
|
+
# Why this session is on its toolkit. Files from before the field read
|
|
227
|
+
# as chosen, so an old default session is never newly suggested.
|
|
228
|
+
attr_reader :toolkit_origin
|
|
229
|
+
|
|
230
|
+
def toolkit_origin=(value)
|
|
231
|
+
origin = value.to_s
|
|
232
|
+
@toolkit_origin = TOOLKIT_ORIGINS.include?(origin) ? origin : "chosen"
|
|
233
|
+
end
|
|
234
|
+
|
|
225
235
|
# The session's own settings (chi --set), a SessionConfig::Settings:
|
|
226
236
|
# empty for none and for a session from before them. Its core part
|
|
227
237
|
# ({key => value}: recap.enabled) is the file's config_settings, its
|
|
@@ -322,7 +332,7 @@ module Samagotchi
|
|
|
322
332
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
323
333
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
324
334
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
325
|
-
toolkit: nil, settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil,
|
|
335
|
+
toolkit: nil, toolkit_origin: "chosen", settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil,
|
|
326
336
|
working_set_pins: [], review: nil)
|
|
327
337
|
@id = id
|
|
328
338
|
@metadata_version = metadata_version
|
|
@@ -355,6 +365,7 @@ module Samagotchi
|
|
|
355
365
|
self.reasoning_history = reasoning_history
|
|
356
366
|
self.appended_prompt_files = appended_prompt_files
|
|
357
367
|
self.toolkit = toolkit
|
|
368
|
+
self.toolkit_origin = toolkit_origin
|
|
358
369
|
self.settings = settings
|
|
359
370
|
self.parent_answers = parent_answers
|
|
360
371
|
self.working_set_pins = working_set_pins
|
|
@@ -427,10 +438,14 @@ module Samagotchi
|
|
|
427
438
|
# seed); [] by default
|
|
428
439
|
# @param setup [SessionSetup] the session's own settings (none by default)
|
|
429
440
|
# @param now [Time] its created_at and updated_at (a spec's clock)
|
|
441
|
+
# @raise [Toolkit::Refused] its toolkit (setup's, else the configured
|
|
442
|
+
# one SessionManager.spawn_session gives it) builds on a config kit
|
|
443
|
+
# with an invalid groups: (Toolkit.check_new!)
|
|
430
444
|
def self.new_session(mode:, model_name:, working_directory:, test_run: nil,
|
|
431
445
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, messages: [],
|
|
432
446
|
scratch: false, model_typed: nil, delegate: false, setup: SessionSetup.new, continues: nil,
|
|
433
447
|
review: nil, now: Time.now)
|
|
448
|
+
Toolkit.check_new!(setup.toolkit || Toolkit.configured(delegate: delegate))
|
|
434
449
|
now = now.iso8601(3)
|
|
435
450
|
resolved_test = if test_run.nil?
|
|
436
451
|
test_session_env?
|
|
@@ -460,6 +475,7 @@ module Samagotchi
|
|
|
460
475
|
reasoning_history: setup.reasoning_history,
|
|
461
476
|
appended_prompt_files: setup.appended_prompt_files,
|
|
462
477
|
toolkit: setup.toolkit,
|
|
478
|
+
toolkit_origin: setup.toolkit_origin || "chosen",
|
|
463
479
|
settings: setup.settings,
|
|
464
480
|
parent_answers: setup.parent_answers,
|
|
465
481
|
working_set_pins: setup.working_set_pins,
|
|
@@ -530,6 +546,7 @@ module Samagotchi
|
|
|
530
546
|
"reasoning_history" => nil,
|
|
531
547
|
"appended_prompt_files" => [],
|
|
532
548
|
"toolkit" => nil,
|
|
549
|
+
"toolkit_origin" => "chosen",
|
|
533
550
|
"bundle_settings" => {},
|
|
534
551
|
"config_settings" => {},
|
|
535
552
|
"interrupted_turn" => nil,
|
|
@@ -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 remote].freeze
|
|
29
|
+
MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit remote].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
|
|
@@ -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
|
|
|
@@ -192,7 +193,7 @@ module Samagotchi
|
|
|
192
193
|
return true if owner(id)&.worker?
|
|
193
194
|
return false if monotonic >= deadline
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
@sleeper.call(WORKER_POLL)
|
|
196
197
|
end
|
|
197
198
|
end
|
|
198
199
|
|
|
@@ -309,7 +310,10 @@ module Samagotchi
|
|
|
309
310
|
reply = request(:post, "#{session_path(id)}/notes", body: { text: text, source: source })
|
|
310
311
|
json = reply.json.is_a?(Hash) ? reply.json : {}
|
|
311
312
|
case reply.status
|
|
312
|
-
when 200
|
|
313
|
+
when 200
|
|
314
|
+
raise_refusal(reply) unless json["status"].is_a?(String)
|
|
315
|
+
|
|
316
|
+
NoteDelivery::Result.new(id: id, status: json["status"].to_sym, queued: json["queued"].to_i)
|
|
313
317
|
when 409
|
|
314
318
|
return NoteDelivery::Result.new(id: id, status: :refused, queued: 0) if json["error"] == "owned_by_tui"
|
|
315
319
|
|
|
@@ -469,14 +473,13 @@ module Samagotchi
|
|
|
469
473
|
def context_add_url(target, url, why:, hint:, added_by:)
|
|
470
474
|
reply = request(:post, "#{session_path(target.session_id)}/context",
|
|
471
475
|
body: { url: url, why: why, hint: hint, added_by: added_by }.compact)
|
|
472
|
-
if reply.status == 422
|
|
476
|
+
if reply.status == 422 && no_provider?(reply)
|
|
473
477
|
require_relative "../context_providers"
|
|
474
|
-
detail =
|
|
475
|
-
"no installed bundle on #{name} resolves #{url} (chi bundle install github-pr there for GitHub PRs)"
|
|
476
|
-
else
|
|
477
|
-
"#{reply.json["detail"]} (on #{name})"
|
|
478
|
-
end
|
|
478
|
+
detail = "no installed bundle on #{name} resolves #{url} (chi bundle install github-pr there for GitHub PRs)"
|
|
479
479
|
raise ContextProviders::Invalid, detail
|
|
480
|
+
elsif reply.status == 422
|
|
481
|
+
require_relative "../context_providers"
|
|
482
|
+
raise ContextProviders::Invalid, "#{reply.json["detail"]} (on #{name})"
|
|
480
483
|
end
|
|
481
484
|
context_added(reply)
|
|
482
485
|
reply.json.fetch("name")
|
|
@@ -491,7 +494,7 @@ module Samagotchi
|
|
|
491
494
|
case reply.status
|
|
492
495
|
when 200 then ContextPush.new(status: :pushed, before: reply.json["before"], revision: reply.json["revision"])
|
|
493
496
|
when 404
|
|
494
|
-
return ContextPush.new(status: :missing) if reply
|
|
497
|
+
return ContextPush.new(status: :missing) if context_missing?(reply)
|
|
495
498
|
|
|
496
499
|
raise_refusal(reply)
|
|
497
500
|
when 409 then ContextPush.new(status: :not_push)
|
|
@@ -512,7 +515,7 @@ module Samagotchi
|
|
|
512
515
|
def context_entry(target, name)
|
|
513
516
|
ContextSources.check_name!(name)
|
|
514
517
|
reply = request(:get, "#{session_path(target.session_id)}/context/#{name}", query: { full: 1 })
|
|
515
|
-
return nil if
|
|
518
|
+
return nil if context_missing?(reply)
|
|
516
519
|
|
|
517
520
|
raise_refusal(reply) unless reply.status == 200
|
|
518
521
|
|
|
@@ -526,7 +529,7 @@ module Samagotchi
|
|
|
526
529
|
case reply.status
|
|
527
530
|
when 200 then :removed
|
|
528
531
|
when 409 then reply.error == "project_source" ? :project : raise_refusal(reply)
|
|
529
|
-
when 404 then
|
|
532
|
+
when 404 then context_missing?(reply) ? :missing : raise_refusal(reply)
|
|
530
533
|
else raise_refusal(reply)
|
|
531
534
|
end
|
|
532
535
|
end
|
|
@@ -654,6 +657,21 @@ module Samagotchi
|
|
|
654
657
|
reply.status == 404 && reply.error == "not_found" && reply.json["detail"].to_s.start_with?("not found: /")
|
|
655
658
|
end
|
|
656
659
|
|
|
660
|
+
def context_missing?(reply)
|
|
661
|
+
return false unless reply.status == 404
|
|
662
|
+
return true if reply.error == "context_missing"
|
|
663
|
+
|
|
664
|
+
# Older chi web used not_found for a missing context source.
|
|
665
|
+
reply.error == "not_found" && reply.json["detail"].to_s.start_with?("no source")
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def no_provider?(reply)
|
|
669
|
+
return true if reply.error == "no_provider"
|
|
670
|
+
|
|
671
|
+
# Older chi web didn't give this refusal its own error code.
|
|
672
|
+
reply.json["detail"].to_s.start_with?("no installed bundle resolves")
|
|
673
|
+
end
|
|
674
|
+
|
|
657
675
|
# Raises what Local's callee raises for this refusal (chi web sends
|
|
658
676
|
# the fields each class is built from; see docs/architecture.md, chi
|
|
659
677
|
# web's routes for the session commands), else RemoteFailed.
|
|
@@ -673,12 +691,19 @@ module Samagotchi
|
|
|
673
691
|
when "continued", "open_children", "folder_gone"
|
|
674
692
|
raise SessionContinue::Refused.new(sid, reason, ids: Array(json["ids"]))
|
|
675
693
|
when "held", "not_running", "unsupported", "failed"
|
|
676
|
-
|
|
694
|
+
if reason
|
|
695
|
+
raise SessionManager::RestartRefused.new(sid, reason, version: json["version"], holds: json["holds"],
|
|
696
|
+
pid: json["pid"], via: invocation)
|
|
697
|
+
end
|
|
677
698
|
when "parent_answers_refused", "parent_continue_refused", "no_vision"
|
|
678
699
|
raise Refused.new(message.to_s, code: error)
|
|
679
700
|
# No model configured there, or --model's host unknown there: what
|
|
680
701
|
# spawn_session raises here (chi send: could not start a session).
|
|
681
702
|
when "invalid_model" then raise ModelProfile::MissingModel, message.to_s
|
|
703
|
+
# A toolkit whose config kit's groups: is invalid there: the same.
|
|
704
|
+
when "toolkit_refused" then raise Toolkit::Refused, message.to_s
|
|
705
|
+
# The grants store doesn't read there: chi share says what it says here.
|
|
706
|
+
when "grants_unwritable" then raise Web::Grants::Unwritable, message.to_s
|
|
682
707
|
when /\Ainvalid_/, "missing_fields", "bad_images"
|
|
683
708
|
# invalid_participant: chi share's name, kind or ttl, an archived session
|
|
684
709
|
raise BadRequest.new(message.to_s, code: error)
|
|
@@ -6,6 +6,7 @@ require_relative "../session_access"
|
|
|
6
6
|
require_relative "../bridge_client"
|
|
7
7
|
require_relative "../delivery"
|
|
8
8
|
require_relative "../log"
|
|
9
|
+
require_relative "../worker_link"
|
|
9
10
|
require_relative "http_transport"
|
|
10
11
|
|
|
11
12
|
module Samagotchi
|
|
@@ -39,9 +40,6 @@ module Samagotchi
|
|
|
39
40
|
# A read a join waits on (a turn's saved records, the children
|
|
40
41
|
# counts): a slow chi web can't hold the join up for long (L9).
|
|
41
42
|
JOIN_READ_TIMEOUT = 5
|
|
42
|
-
# chi web answers no live worker for the session (Mapping).
|
|
43
|
-
NO_WORKER = "no live worker for session %s on %s"
|
|
44
|
-
|
|
45
43
|
# chi web's answers as the session's Bridge would have given them.
|
|
46
44
|
module Mapping
|
|
47
45
|
module_function
|
|
@@ -60,7 +58,7 @@ module Samagotchi
|
|
|
60
58
|
def response(route, reply, session_id:, remote:)
|
|
61
59
|
json = reply.json.is_a?(Hash) ? reply.json : {}
|
|
62
60
|
case [reply.status, json["error"]]
|
|
63
|
-
in [503, "not_live"] | [404, "not_found"] then raise Unreachable,
|
|
61
|
+
in [503, "not_live"] | [404, "not_found"] then raise Unreachable, "#{WorkerLink.not_live(session_id)} on #{remote}"
|
|
64
62
|
in [504, _] then bridge(408, error: "deadline_passed", detail: json["detail"])
|
|
65
63
|
in [501, _] if route == :command && json["command"]
|
|
66
64
|
bridge(400, error: "unknown_command", detail: json["detail"], command: json["command"])
|
|
@@ -69,6 +67,15 @@ module Samagotchi
|
|
|
69
67
|
end
|
|
70
68
|
end
|
|
71
69
|
|
|
70
|
+
def stream(sock, status, headers, session_id:, remote:)
|
|
71
|
+
outcome = BridgeClient::StreamOpen.classify(sock, status, headers)
|
|
72
|
+
return outcome unless status.to_i == 503 && outcome.is_a?(BridgeClient::StreamOpen::Unreachable)
|
|
73
|
+
|
|
74
|
+
expected = [WorkerLink.unanswered(session_id), WorkerLink.not_live(session_id)]
|
|
75
|
+
detail = expected.include?(outcome.detail) ? outcome.detail : WorkerLink.not_live(session_id)
|
|
76
|
+
BridgeClient::StreamOpen::Unreachable.new(attempts: outcome.attempts, detail: "#{detail} on #{remote}")
|
|
77
|
+
end
|
|
78
|
+
|
|
72
79
|
def bridge(status, **body) = BridgeClient::Response.new(status: status, body: JSON.generate(body.compact))
|
|
73
80
|
end
|
|
74
81
|
|
|
@@ -198,24 +205,35 @@ module Samagotchi
|
|
|
198
205
|
idle_timeout: STREAM_IDLE_TIMEOUT, &on_event).start
|
|
199
206
|
end
|
|
200
207
|
|
|
201
|
-
# GET /api/sessions/:id/stream through StreamRequest,
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
|
|
208
|
+
# GET /api/sessions/:id/stream through StreamRequest, classifying chi
|
|
209
|
+
# web's stream answers as Bridge outcomes.
|
|
210
|
+
# @param header_timeout [Float] see BridgeClient::STREAM_HEADER_TIMEOUT
|
|
211
|
+
# @yield [sock] called after response headers arrive, before classifying a
|
|
212
|
+
# refusal; EventStream uses it to publish the socket for #close
|
|
213
|
+
# @return [BridgeClient::StreamOpen::Opened, BridgeClient::StreamOpen::Refused,
|
|
214
|
+
# BridgeClient::StreamOpen::Unreachable]
|
|
215
|
+
def open_stream(query: "", last_event_id: nil, header_timeout: BridgeClient::STREAM_HEADER_TIMEOUT, attempts: 1)
|
|
207
216
|
uri = URI(@remote.entry.url)
|
|
208
217
|
return refused("tls_unsupported") if uri.scheme == "https"
|
|
209
218
|
|
|
210
219
|
token = @remote.entry.token
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
220
|
+
connection = BridgeClient::StreamRequest.open(host: uri.hostname, port: uri.port,
|
|
221
|
+
path: "/api/sessions/#{@session_id}/stream#{query}",
|
|
222
|
+
last_event_id: last_event_id, timeout: header_timeout,
|
|
223
|
+
headers: token ? { "Authorization" => "Bearer #{token}" } : {})
|
|
224
|
+
return BridgeClient::StreamOpen::Unreachable.new(attempts: 1) unless connection
|
|
225
|
+
|
|
226
|
+
sock, status, headers = connection
|
|
227
|
+
unless !block_given? || yield(sock)
|
|
228
|
+
sock.close rescue nil
|
|
229
|
+
return BridgeClient::StreamOpen::Unreachable.new(attempts: 1)
|
|
230
|
+
end
|
|
231
|
+
return refused("chunked_unsupported", sock) if headers["transfer-encoding"].to_s.downcase.include?("chunked")
|
|
216
232
|
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
Mapping.stream(sock, status, headers, session_id: @session_id, remote: @remote.name)
|
|
234
|
+
rescue SystemCallError, SocketError, IOError
|
|
235
|
+
sock&.close rescue nil
|
|
236
|
+
BridgeClient::StreamOpen::Unreachable.new(attempts: 1)
|
|
219
237
|
end
|
|
220
238
|
|
|
221
239
|
private
|
|
@@ -234,9 +252,10 @@ module Samagotchi
|
|
|
234
252
|
raise Unreachable, e.message
|
|
235
253
|
end
|
|
236
254
|
|
|
237
|
-
def refused(reason)
|
|
255
|
+
def refused(reason, sock = nil)
|
|
256
|
+
sock&.close rescue nil
|
|
238
257
|
Log.warn(:attached, "stream_refused", reason: reason, remote: @remote.name)
|
|
239
|
-
|
|
258
|
+
BridgeClient::StreamOpen::Unreachable.new(attempts: 1)
|
|
240
259
|
end
|
|
241
260
|
end
|
|
242
261
|
end
|
|
@@ -179,15 +179,27 @@ module Samagotchi
|
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
# A session's worker as an attach finds it (Local#worker, chi web's GET
|
|
182
|
-
# …/worker): +
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
def
|
|
182
|
+
# …/worker): +link+ the one WorkerLink outcome every field reads, +stopped+
|
|
183
|
+
# its stop marker.
|
|
184
|
+
WorkerState = Data.define(:link, :stopped) do
|
|
185
|
+
# @return [WorkerSidecar, nil] the live worker's (port, version,
|
|
186
|
+
# features, epoch; nil: none answers)
|
|
187
|
+
def sidecar = link.live? ? link.sidecar : nil
|
|
188
|
+
|
|
189
|
+
# @return [OwnerLock::Owner, nil] a worker, a chi REPL, nil
|
|
190
|
+
def owner = link.owner
|
|
191
|
+
|
|
192
|
+
def live? = link.live?
|
|
193
|
+
|
|
194
|
+
# The owner is a worker leaving (writing its recap on leave): a wake
|
|
195
|
+
# waits for it (Local#wake).
|
|
196
|
+
def leaving? = link.is_a?(WorkerLink::Leaving)
|
|
197
|
+
|
|
198
|
+
# A worker holds the lock past WorkerLink::START_LIMIT with no Bridge.
|
|
199
|
+
def stuck? = link.is_a?(WorkerLink::Starting) && link.stuck
|
|
200
|
+
|
|
201
|
+
# A worker whose sidecar speaks a newer protocol than this chi.
|
|
202
|
+
def skew? = link.is_a?(WorkerLink::Skew)
|
|
191
203
|
end
|
|
192
204
|
|
|
193
205
|
# What a wake for an attach did (Local#wake, chi web's POST …/wake):
|
|
@@ -9,6 +9,7 @@ require_relative "child_move"
|
|
|
9
9
|
require_relative "archive_store"
|
|
10
10
|
require_relative "recap_store"
|
|
11
11
|
require_relative "bridge_client"
|
|
12
|
+
require_relative "worker_link"
|
|
12
13
|
require_relative "model_profile"
|
|
13
14
|
require_relative "log"
|
|
14
15
|
require_relative "session_manager"
|
|
@@ -303,10 +304,8 @@ module Samagotchi
|
|
|
303
304
|
# answers at once. Best effort: the continue goes on without. A review
|
|
304
305
|
# fork (ReviewStart) asks the same, with nothing archived after.
|
|
305
306
|
def self.recap_before_archive(id, state_dir, wait:)
|
|
306
|
-
return unless SessionManager.worker_live?(id, state_dir: state_dir)
|
|
307
|
-
|
|
308
307
|
dir = Session.session_dir(id, state_dir: state_dir)
|
|
309
|
-
client =
|
|
308
|
+
client = WorkerLink.client(id, session_dir: dir) or return
|
|
310
309
|
before = RecapStore.read(dir)
|
|
311
310
|
reply = client.request_recap
|
|
312
311
|
asked = reply.status == 200 && reply.json.is_a?(Hash) ? reply.json["request"] : nil
|