samagotchi 0.106.0 → 0.108.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 +157 -1
- data/bin/chi +2 -0
- data/docs/architecture.md +6 -1
- data/docs/cli.md +45 -5
- data/docs/guardrails.md +8 -2
- data/docs/internals/engine.md +17 -6
- data/docs/internals/participants.md +129 -30
- data/docs/internals/remote-sessions.md +12 -5
- data/docs/internals/working-set.md +2 -1
- data/docs/memory.md +20 -0
- data/docs/plugins.md +38 -3
- data/docs/releasing.md +13 -6
- data/docs/sessions.md +39 -4
- data/docs/sub-agent.md +3 -1
- data/docs/testing.md +7 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -1
- data/lib/samagotchi/bridge.rb +7 -2
- data/lib/samagotchi/bridge_client/stream_request.rb +10 -3
- data/lib/samagotchi/bundles/coordinator/manifest.yml +6 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +85 -5
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +18 -0
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +51 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/work-cap/plugin.rb +50 -14
- data/lib/samagotchi/child_reports.rb +2 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/context_sources.rb +1 -6
- data/lib/samagotchi/continue_offer.rb +56 -7
- data/lib/samagotchi/engine.rb +36 -25
- data/lib/samagotchi/guardrail_wiring.rb +9 -6
- data/lib/samagotchi/guardrails/review_boundary.rb +6 -57
- data/lib/samagotchi/guardrails/review_paths.rb +93 -0
- data/lib/samagotchi/idle_scheduler.rb +17 -2
- data/lib/samagotchi/last_turn.rb +7 -0
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_orphans.rb +74 -0
- data/lib/samagotchi/memory_paths.rb +4 -4
- data/lib/samagotchi/model_profile.rb +5 -3
- data/lib/samagotchi/parent_report.rb +34 -4
- data/lib/samagotchi/participant_messages.rb +11 -0
- data/lib/samagotchi/participants.rb +20 -6
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +25 -0
- data/lib/samagotchi/remote_command.rb +1 -1
- data/lib/samagotchi/remotes.rb +23 -8
- data/lib/samagotchi/reply_wait.rb +7 -2
- data/lib/samagotchi/review_apply.rb +123 -0
- data/lib/samagotchi/review_command.rb +121 -0
- data/lib/samagotchi/review_start.rb +161 -0
- data/lib/samagotchi/run_options.rb +17 -2
- data/lib/samagotchi/send_command/busy.rb +5 -2
- data/lib/samagotchi/send_command/wait_target.rb +18 -8
- data/lib/samagotchi/send_command.rb +2 -0
- data/lib/samagotchi/session.rb +33 -5
- data/lib/samagotchi/session_access/contract.rb +10 -2
- data/lib/samagotchi/session_access/dispatch.rb +8 -2
- data/lib/samagotchi/session_access/http_transport.rb +10 -1
- data/lib/samagotchi/session_access/local.rb +19 -2
- data/lib/samagotchi/session_access/remote.rb +15 -1
- data/lib/samagotchi/session_access/web_bridge_client.rb +1 -1
- data/lib/samagotchi/session_access.rb +23 -10
- data/lib/samagotchi/session_chain.rb +10 -2
- data/lib/samagotchi/session_continue.rb +4 -3
- data/lib/samagotchi/session_inbox.rb +12 -8
- data/lib/samagotchi/session_manager.rb +30 -8
- data/lib/samagotchi/session_metrics.rb +7 -6
- data/lib/samagotchi/session_participants.rb +3 -1
- data/lib/samagotchi/session_review.rb +28 -0
- data/lib/samagotchi/share_command.rb +7 -1
- data/lib/samagotchi/steer.rb +11 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +34 -27
- data/lib/samagotchi/terminal_ui/formatting.rb +2 -2
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +5 -1
- data/lib/samagotchi/testing/sessions.rb +10 -2
- data/lib/samagotchi/tools/memory.rb +16 -9
- data/lib/samagotchi/turn_checkpoint.rb +13 -0
- data/lib/samagotchi/turn_origin.rb +68 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +108 -21
- data/lib/samagotchi/web/grants.rb +92 -17
- data/lib/samagotchi/web/participant_access.rb +56 -110
- data/lib/samagotchi/web/participant_review.rb +45 -2
- data/lib/samagotchi/web/participant_stream_filter.rb +118 -17
- data/lib/samagotchi/web/participant_view.rb +19 -14
- data/lib/samagotchi/web/participants_admin.rb +129 -0
- data/lib/samagotchi/web/public/app.js +60 -5
- data/lib/samagotchi/web/public/chip_popover.js +10 -3
- data/lib/samagotchi/web/public/data.js +10 -2
- data/lib/samagotchi/web/public/delivery_line.js +79 -15
- data/lib/samagotchi/web/public/index.html +26 -0
- data/lib/samagotchi/web/public/info_bar.js +7 -3
- data/lib/samagotchi/web/public/info_bar_model.js +7 -1
- data/lib/samagotchi/web/public/live_turn.js +3 -0
- data/lib/samagotchi/web/public/participant.html +58 -5
- data/lib/samagotchi/web/public/participant.js +172 -25
- data/lib/samagotchi/web/public/participant_model.js +89 -24
- data/lib/samagotchi/web/public/participant_names.js +37 -5
- data/lib/samagotchi/web/public/participants_panel.js +86 -18
- data/lib/samagotchi/web/public/question_cards.js +1 -0
- data/lib/samagotchi/web/public/review_apply.js +34 -0
- data/lib/samagotchi/web/public/review_start.js +101 -0
- data/lib/samagotchi/web/public/send_turn.js +25 -1
- data/lib/samagotchi/web/public/session_actions.js +44 -3
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/start_page.js +38 -2
- data/lib/samagotchi/web/public/stream_handlers.js +10 -0
- data/lib/samagotchi/web/public/turn_html.js +2 -1
- data/lib/samagotchi/web/public/turn_view.js +5 -0
- data/lib/samagotchi/web/session_summary.rb +3 -0
- data/lib/samagotchi/worker.rb +44 -27
- data/lib/samagotchi/worker_inbound.rb +12 -9
- data/lib/samagotchi/working_set.rb +3 -2
- metadata +11 -1
|
@@ -78,7 +78,10 @@ module Samagotchi
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# The session command against the remote: refused (exit 2), or run
|
|
81
|
-
# with a Remote. A failure to reach it is the command's line, exit 1
|
|
81
|
+
# with a Remote. A failure to reach it is the command's line, exit 1;
|
|
82
|
+
# a refusal the command doesn't word itself is its line too: exit 2
|
|
83
|
+
# when chi web refused a value (BadRequest, a usage error here), 1
|
|
84
|
+
# for any other.
|
|
82
85
|
# @param subcommands [Hash] bin/chi's SUBCOMMANDS
|
|
83
86
|
# @return [Integer] the exit status
|
|
84
87
|
def run(subcommands, stderr: $stderr, transport: nil)
|
|
@@ -100,7 +103,10 @@ module Samagotchi
|
|
|
100
103
|
rescue Remotes::Invalid => e
|
|
101
104
|
stderr.puts("Error: #{e.message}")
|
|
102
105
|
CLI::Exit::USAGE
|
|
103
|
-
rescue
|
|
106
|
+
rescue BadRequest => e
|
|
107
|
+
stderr.puts("chi #{@argv.first}: #{e.message}")
|
|
108
|
+
CLI::Exit::USAGE
|
|
109
|
+
rescue RemoteFailed, Refused => e
|
|
104
110
|
stderr.puts("chi #{@argv.first}: #{e.message}")
|
|
105
111
|
CLI::Exit::FAILED
|
|
106
112
|
end
|
|
@@ -74,6 +74,13 @@ module Samagotchi
|
|
|
74
74
|
failed("no answer within #{@open_timeout} s")
|
|
75
75
|
rescue Net::ReadTimeout
|
|
76
76
|
failed("it took the request but sent no answer within #{read_timeout} s")
|
|
77
|
+
rescue Net::WriteTimeout
|
|
78
|
+
failed("it took no request within #{read_timeout} s")
|
|
79
|
+
rescue Timeout::Error
|
|
80
|
+
failed("no answer in time")
|
|
81
|
+
rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError
|
|
82
|
+
# Something else listens there (an SSH banner, another server).
|
|
83
|
+
failed("it didn't answer HTTP (is it chi web's port?)")
|
|
77
84
|
rescue SocketError => e
|
|
78
85
|
failed(e.message.include?("getaddrinfo") ? "can't find host #{@uri.host}" : e.message)
|
|
79
86
|
rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
|
|
@@ -87,7 +94,9 @@ module Samagotchi
|
|
|
87
94
|
private
|
|
88
95
|
|
|
89
96
|
def http(read_timeout)
|
|
90
|
-
|
|
97
|
+
# hostname: an IPv6 address bare ([::1] is the URL's); Net::HTTP
|
|
98
|
+
# brackets it in Host.
|
|
99
|
+
Net::HTTP.new(@uri.hostname, @uri.port).tap do |http|
|
|
91
100
|
http.use_ssl = @uri.scheme == "https"
|
|
92
101
|
http.open_timeout = @open_timeout
|
|
93
102
|
http.read_timeout = read_timeout
|
|
@@ -12,6 +12,7 @@ require_relative "../context_sources"
|
|
|
12
12
|
require_relative "../send_command/busy"
|
|
13
13
|
require_relative "../chi_invocation"
|
|
14
14
|
require_relative "../worker_sidecar"
|
|
15
|
+
require_relative "../review_start"
|
|
15
16
|
require_relative "../launch_mode"
|
|
16
17
|
require_relative "../participants"
|
|
17
18
|
require_relative "../config"
|
|
@@ -114,8 +115,11 @@ module Samagotchi
|
|
|
114
115
|
# @return [Session] the session created (its id, model_warning,
|
|
115
116
|
# continues, moved_children)
|
|
116
117
|
# @raise [SessionContinue::Refused, SessionManager::ArchiveRefused,
|
|
117
|
-
# SessionManager::OwnedByTUI] refused;
|
|
118
|
+
# SessionManager::OwnedByTUI, ReviewStart::Refused] refused;
|
|
119
|
+
# StandardError: could not start
|
|
118
120
|
def create(spec)
|
|
121
|
+
return create_review(spec) if spec.review
|
|
122
|
+
|
|
119
123
|
start = spec.preview ? { prompt: spec.prompt, preview: spec.preview } : { prompt: spec.prompt }
|
|
120
124
|
if spec.continues
|
|
121
125
|
return SessionManager.continue_session(spec.continues, **start, model: spec.model_name, **dir)
|
|
@@ -125,6 +129,18 @@ module Samagotchi
|
|
|
125
129
|
setup: spec.setup, **dir)
|
|
126
130
|
end
|
|
127
131
|
|
|
132
|
+
# A review session (ReviewStart): a fork of review.from, or a fresh
|
|
133
|
+
# one in the spec's folder.
|
|
134
|
+
def create_review(spec)
|
|
135
|
+
review = spec.review
|
|
136
|
+
if review.from
|
|
137
|
+
return ReviewStart.fork(from: review.from, subjects: review.subjects, model: spec.model_name, state_dir: state_dir!)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
ReviewStart.start(subjects: review.subjects, dir: spec.working_directory || Dir.pwd, model: spec.model_name,
|
|
141
|
+
setup: spec.setup || SessionSetup.new, state_dir: state_dir!)
|
|
142
|
+
end
|
|
143
|
+
|
|
128
144
|
# Whether a just-created session's worker came up (its Bridge
|
|
129
145
|
# answers) within +timeout+ seconds.
|
|
130
146
|
def await_worker(id, timeout:)
|
|
@@ -353,7 +369,8 @@ module Samagotchi
|
|
|
353
369
|
issued = Participants.invite(grants, session_id: id, name: name, kind: kind, ttl: ttl, cap: cap, state_dir: state_dir!)
|
|
354
370
|
ip, port = local_web
|
|
355
371
|
Invitation.new(participant: ParticipantView.from_h(issued.grant.public_h(grants.now)), token: issued.token,
|
|
356
|
-
link: "http://#{ip || "127.0.0.1"}:#{port}/?token=#{issued.token}#s=#{id}", lan: !ip.nil
|
|
372
|
+
link: "http://#{ip || "127.0.0.1"}:#{port}/?token=#{issued.token}#s=#{id}", lan: !ip.nil?,
|
|
373
|
+
warning: ReviewStart.share_warning(Session.load(id, **dir)))
|
|
357
374
|
rescue Web::Grants::Invalid => e
|
|
358
375
|
raise BadRequest.new(e.message, code: "invalid_participant")
|
|
359
376
|
end
|
|
@@ -259,7 +259,7 @@ module Samagotchi
|
|
|
259
259
|
|
|
260
260
|
json = reply.json
|
|
261
261
|
Invitation.new(participant: ParticipantView.from_h(json.fetch("participant")), token: json.fetch("token"),
|
|
262
|
-
link: json.fetch("link"), lan: json["lan"] == true)
|
|
262
|
+
link: json.fetch("link"), lan: json["lan"] == true, warning: json["warning"])
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
# Revokes one participant (+participant+), a session's, or with no
|
|
@@ -458,6 +458,7 @@ module Samagotchi
|
|
|
458
458
|
# @param fetched [ContextSources::Fetched]
|
|
459
459
|
# @return [ContextPush]
|
|
460
460
|
def context_push(target, name, fetched)
|
|
461
|
+
ContextSources.check_name!(name)
|
|
461
462
|
reply = request(:post, "#{session_path(target.session_id)}/context/#{name}/push",
|
|
462
463
|
body: { text: fetched.text, summary: fetched.summary, wake: fetched.wake, hint: fetched.hint })
|
|
463
464
|
case reply.status
|
|
@@ -647,6 +648,8 @@ module Samagotchi
|
|
|
647
648
|
end
|
|
648
649
|
|
|
649
650
|
def create_body(spec)
|
|
651
|
+
return review_body(spec) if spec.review
|
|
652
|
+
|
|
650
653
|
body = { model: spec.model_name, client_id: spec.client_id, history: false }
|
|
651
654
|
if spec.preview || spec.idle
|
|
652
655
|
body.merge!(idle: true, preview: spec.preview)
|
|
@@ -663,6 +666,17 @@ module Samagotchi
|
|
|
663
666
|
body.compact
|
|
664
667
|
end
|
|
665
668
|
|
|
669
|
+
# A review session's create (chi review): its subjects as typed,
|
|
670
|
+
# checked by the server against its own files; a fork names its
|
|
671
|
+
# source and takes its folder.
|
|
672
|
+
def review_body(spec)
|
|
673
|
+
review = { subjects: spec.review.subjects, from: spec.review.from }.compact
|
|
674
|
+
body = { review: review, model: spec.model_name }
|
|
675
|
+
body[:dir] = spec.working_directory unless spec.review.from
|
|
676
|
+
body.merge!(setup_body(spec.setup)) if spec.setup && !spec.review.from
|
|
677
|
+
body.compact
|
|
678
|
+
end
|
|
679
|
+
|
|
666
680
|
def setup_body(words)
|
|
667
681
|
llm = words.llm_context&.to_h { |field, word| [LLM_CONTEXT_KEYS.fetch(field), word] }
|
|
668
682
|
{ llm_context: llm, thinking: words.thinking, toolkit: words.toolkit,
|
|
@@ -203,7 +203,7 @@ module Samagotchi
|
|
|
203
203
|
return refused("tls_unsupported") if uri.scheme == "https"
|
|
204
204
|
|
|
205
205
|
token = @remote.entry.token
|
|
206
|
-
opened = BridgeClient::StreamRequest.open(host: uri.
|
|
206
|
+
opened = BridgeClient::StreamRequest.open(host: uri.hostname, port: uri.port,
|
|
207
207
|
path: "/api/sessions/#{@session_id}/stream#{query}",
|
|
208
208
|
last_event_id: last_event_id, timeout: timeout,
|
|
209
209
|
headers: token ? { "Authorization" => "Bearer #{token}" } : {})
|
|
@@ -35,27 +35,27 @@ module Samagotchi
|
|
|
35
35
|
class AuthRefused < RemoteFailed; end
|
|
36
36
|
|
|
37
37
|
# The errors a participant's token meets (docs/internals/participants.md),
|
|
38
|
-
#
|
|
39
|
-
# (http://…)"). chi web's code for each is its CODE.
|
|
38
|
+
# each the server's own words (ParticipantMessages) led by the remote's
|
|
39
|
+
# label ("remote studio (http://…)"). chi web's code for each is its CODE.
|
|
40
40
|
|
|
41
41
|
# 401 token_revoked: the owner revoked the grant, or archived or
|
|
42
42
|
# deleted its session.
|
|
43
43
|
class TokenRevoked < AuthRefused
|
|
44
44
|
CODE = "token_revoked"
|
|
45
|
-
def initialize(label) = super("#{label}:
|
|
45
|
+
def initialize(label) = super("#{label}: #{ParticipantMessages::TOKEN_REVOKED}")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# 401 token_expired: the grant's time ran out.
|
|
49
49
|
class TokenExpired < AuthRefused
|
|
50
50
|
CODE = "token_expired"
|
|
51
|
-
def initialize(label) = super("#{label}:
|
|
51
|
+
def initialize(label) = super("#{label}: #{ParticipantMessages::TOKEN_EXPIRED}")
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# 403 not_permitted: a participant's token on a route only the session's
|
|
55
55
|
# owner may use.
|
|
56
56
|
class NotPermitted < RemoteFailed
|
|
57
57
|
CODE = "not_permitted"
|
|
58
|
-
def initialize(label) = super("#{label}:
|
|
58
|
+
def initialize(label) = super("#{label}: #{ParticipantMessages::NOT_PERMITTED}")
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# 429 too_many_turns (P3b): the participant's turns already wait in the
|
|
@@ -102,8 +102,11 @@ module Samagotchi
|
|
|
102
102
|
# What chi share made: the participant, their token and the link that
|
|
103
103
|
# carries it (both shown this once: chi keeps only a digest), and
|
|
104
104
|
# whether that chi web listens on the LAN (+lan+ false: the link works
|
|
105
|
-
# on that machine only)
|
|
106
|
-
|
|
105
|
+
# on that machine only), and the warning for sharing a working session
|
|
106
|
+
# (ReviewStart.share_warning; nil for a review session).
|
|
107
|
+
Invitation = Data.define(:participant, :token, :link, :lan, :warning) do
|
|
108
|
+
def initialize(participant:, token:, link:, lan:, warning: nil) = super
|
|
109
|
+
|
|
107
110
|
def inspect = "#<Invitation #{participant.id}>"
|
|
108
111
|
alias_method :to_s, :inspect
|
|
109
112
|
end
|
|
@@ -120,11 +123,21 @@ module Samagotchi
|
|
|
120
123
|
# bundles). +client_id+ (chi send's, or chi answer's for a parent's
|
|
121
124
|
# /continue yes) and +images+ (images follow) are for a Remote's server,
|
|
122
125
|
# which checks the first line and the model as chi send checks them
|
|
123
|
-
# here (Local checks before it creates).
|
|
126
|
+
# here (Local checks before it creates). +review+ (a Review) makes it a
|
|
127
|
+
# review session (chi review): idle, no prompt, and with +review.from+
|
|
128
|
+
# a review fork of that session (its folder and setup: the rest is nil).
|
|
124
129
|
NewSession = Data.define(:prompt, :preview, :continues, :working_directory, :model_name, :setup, :client_id,
|
|
125
|
-
:images, :idle) do
|
|
130
|
+
:images, :idle, :review) do
|
|
126
131
|
def initialize(prompt:, preview: nil, continues: nil, working_directory: nil, model_name: nil, setup: nil,
|
|
127
|
-
client_id: nil, images: false, idle: false) = super
|
|
132
|
+
client_id: nil, images: false, idle: false, review: nil) = super
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A review session to create (NewSession#review): +subjects+ as typed
|
|
136
|
+
# (relative to the session's folder; the side that has the files checks
|
|
137
|
+
# them, ReviewStart.check_subjects), +from+ the session a review fork
|
|
138
|
+
# starts from (an id, a prefix or last:<id>), or nil.
|
|
139
|
+
Review = Data.define(:subjects, :from) do
|
|
140
|
+
def initialize(subjects: [], from: nil) = super(subjects: Array(subjects).map(&:to_s), from: from)
|
|
128
141
|
end
|
|
129
142
|
|
|
130
143
|
# chi send --new's setup flags as typed, for a Remote: +llm_context+
|
|
@@ -70,12 +70,20 @@ module Samagotchi
|
|
|
70
70
|
# @return [String]
|
|
71
71
|
def note_text(previous, recap:)
|
|
72
72
|
head = "This session continues #{previous.id[0, 8]} (#{clock(previous.created_at) || previous.created_at}), the previous link of its chain."
|
|
73
|
+
"#{head}#{recap_text(recap)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# +recap+ as a note's tail: " Its recap, as of <time>:\n<text>" (cut
|
|
77
|
+
# to RECAP_CHARS), or " It left no recap."
|
|
78
|
+
# @param recap [Hash, nil] RecapStore.read's
|
|
79
|
+
# @return [String]
|
|
80
|
+
def recap_text(recap)
|
|
73
81
|
text = recap && recap[:text].to_s.strip
|
|
74
|
-
return "
|
|
82
|
+
return " It left no recap." if text.nil? || text.empty?
|
|
75
83
|
|
|
76
84
|
text = "#{text[0, RECAP_CHARS - 1]}…" if text.length > RECAP_CHARS
|
|
77
85
|
as_of = clock(recap[:created_at])
|
|
78
|
-
"
|
|
86
|
+
" Its recap#{", as of #{as_of}" if as_of}:\n#{text}"
|
|
79
87
|
end
|
|
80
88
|
|
|
81
89
|
# What web `continue →` prefills in +previous+'s next link: +previous+'s
|
|
@@ -204,7 +204,7 @@ module Samagotchi
|
|
|
204
204
|
end
|
|
205
205
|
link = SessionManager.spawn_session(
|
|
206
206
|
prompt: start, preview: preview, title: previous.title, working_directory: previous.working_directory,
|
|
207
|
-
model_name: choice.model, setup: SessionSetup.of(previous),
|
|
207
|
+
model_name: choice.model, setup: SessionSetup.of(previous), review: previous.review,
|
|
208
208
|
continues: previous.id, note: SessionChain.note_text(previous, recap: recap),
|
|
209
209
|
note_source: SessionChain::NOTE_SOURCE, state_dir: state_dir, before_spawn: before_spawn
|
|
210
210
|
)
|
|
@@ -300,8 +300,9 @@ module Samagotchi
|
|
|
300
300
|
# IdleRecap#request_now) and wait, up to +wait+ seconds, for recap.json
|
|
301
301
|
# to change. A worker that is gone wrote its recap when it left; one
|
|
302
302
|
# that has nothing new to say, too short a conversation, or recaps off
|
|
303
|
-
# answers at once. Best effort: the continue goes on without.
|
|
304
|
-
|
|
303
|
+
# answers at once. Best effort: the continue goes on without. A review
|
|
304
|
+
# fork (ReviewStart) asks the same, with nothing archived after.
|
|
305
|
+
def self.recap_before_archive(id, state_dir, wait:)
|
|
305
306
|
return unless SessionManager.worker_live?(id, state_dir: state_dir)
|
|
306
307
|
|
|
307
308
|
dir = Session.session_dir(id, state_dir: state_dir)
|
|
@@ -9,6 +9,7 @@ require_relative "delivery"
|
|
|
9
9
|
require_relative "log"
|
|
10
10
|
require_relative "session"
|
|
11
11
|
require_relative "steer"
|
|
12
|
+
require_relative "turn_origin"
|
|
12
13
|
|
|
13
14
|
module Samagotchi
|
|
14
15
|
# A session directory's file inbox and outbox (see SessionManager for the
|
|
@@ -221,13 +222,16 @@ module Samagotchi
|
|
|
221
222
|
Dir.glob(File.join(input_dir, "*.json"))
|
|
222
223
|
end
|
|
223
224
|
|
|
224
|
-
# A claimed input file as read: its +prompt+, +origin+ (
|
|
225
|
-
#
|
|
226
|
-
# with the raised iteration limit
|
|
227
|
-
# +images+ ({file:, name:} refs),
|
|
228
|
-
# file without the key is one an
|
|
225
|
+
# A claimed input file as read: its +prompt+, +origin+ (a TurnOrigin
|
|
226
|
+
# from the file's client_id, enqueued_id and author; nil when it names
|
|
227
|
+
# no sender), whether its turn runs with the raised iteration limit
|
|
228
|
+
# (+no_interrupt+, --no-interrupt), its +images+ ({file:, name:} refs),
|
|
229
|
+
# and its +delivery+ ("queue" or nil; a file without the key is one an
|
|
230
|
+
# older chi wrote, so nil).
|
|
229
231
|
Input = Data.define(:prompt, :origin, :no_interrupt, :images, :delivery) do
|
|
230
|
-
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
232
|
+
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
233
|
+
super(prompt:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:)
|
|
234
|
+
end
|
|
231
235
|
end
|
|
232
236
|
# A file that doesn't parse, or isn't a JSON object: no prompt.
|
|
233
237
|
UNREADABLE_INPUT = Input.new(prompt: nil)
|
|
@@ -239,9 +243,9 @@ module Samagotchi
|
|
|
239
243
|
return UNREADABLE_INPUT unless data.is_a?(Hash)
|
|
240
244
|
|
|
241
245
|
# author: only a file that names one (an owner's names none).
|
|
242
|
-
origin =
|
|
246
|
+
origin = TurnOrigin.from(data.slice("client_id", "enqueued_id", "author"))
|
|
243
247
|
images = Array(data["images"]).select { |image| image.is_a?(Hash) }.map { |image| image.transform_keys(&:to_sym) }
|
|
244
|
-
Input.new(prompt: data["prompt"].to_s, origin: origin
|
|
248
|
+
Input.new(prompt: data["prompt"].to_s, origin: origin, no_interrupt: data["no_interrupt"] == true,
|
|
245
249
|
images: images, delivery: Delivery.queue?(data["delivery"]) ? Delivery::QUEUE : nil)
|
|
246
250
|
rescue JSON::ParserError
|
|
247
251
|
UNREADABLE_INPUT
|
|
@@ -157,6 +157,8 @@ module Samagotchi
|
|
|
157
157
|
# @param note [String, nil] a context note the session starts with,
|
|
158
158
|
# from +note_source+: queued before its worker spawns, so its first
|
|
159
159
|
# turn sees it
|
|
160
|
+
# @param review [SessionReview, nil] a review session's (ReviewStart; a
|
|
161
|
+
# continue keeps its previous link's)
|
|
160
162
|
# @param before_spawn [#call, nil] (session) runs once the session and
|
|
161
163
|
# its note are saved, before its worker spawns
|
|
162
164
|
# @return [Session] with #seed_images_dropped: refs whose file was gone,
|
|
@@ -164,7 +166,7 @@ module Samagotchi
|
|
|
164
166
|
def self.spawn_session(prompt:, mode: "assist", working_directory: nil, model_name: nil, state_dir: nil,
|
|
165
167
|
memories: [], muted_memories: [], parent_id: nil, messages: [], images_from: nil,
|
|
166
168
|
preview: nil, title: nil, delegate: false, setup: SessionSetup.new, continues: nil, note: nil,
|
|
167
|
-
note_source: "chi", before_spawn: nil)
|
|
169
|
+
note_source: "chi", before_spawn: nil, review: nil)
|
|
168
170
|
sd = state_dir || Session.default_state_dir
|
|
169
171
|
# The resolved ref is stored (a resumed session keeps its model when an
|
|
170
172
|
# alias is retargeted), with the name as typed beside it. A wrong host
|
|
@@ -186,7 +188,8 @@ module Samagotchi
|
|
|
186
188
|
messages: messages,
|
|
187
189
|
delegate: delegate,
|
|
188
190
|
setup: setup,
|
|
189
|
-
continues: continues
|
|
191
|
+
continues: continues,
|
|
192
|
+
review: review
|
|
190
193
|
)
|
|
191
194
|
session.toolkit = Toolkit.configured(delegate: delegate) if session.toolkit.nil?
|
|
192
195
|
session.model_warning = model_warning
|
|
@@ -1284,18 +1287,23 @@ module Samagotchi
|
|
|
1284
1287
|
ack: { "error" => "worker_timeout",
|
|
1285
1288
|
"detail" => "the session's worker is exiting, so the message was not sent; send it again" })
|
|
1286
1289
|
end
|
|
1287
|
-
# No worker: the participant's waiting files are all their turns.
|
|
1288
|
-
if author && pending_limit && pending_files(session_dir, author) >= pending_limit
|
|
1289
|
-
return DeliverResult.new(status: :refused, code: 429,
|
|
1290
|
-
ack: { "error" => "too_many_turns", "detail" => ParticipantMessages::TOO_MANY_TURNS })
|
|
1291
|
-
end
|
|
1292
1290
|
enqueued_id = SecureRandom.uuid
|
|
1293
1291
|
input = { prompt: prompt, client_id: client_id, enqueued_id: enqueued_id, state_dir: state_dir }
|
|
1294
1292
|
input[:images] = images unless images.empty?
|
|
1295
1293
|
input[:delivery] = delivery unless Delivery.next_step?(delivery)
|
|
1296
1294
|
input[:no_interrupt] = true if no_interrupt
|
|
1297
1295
|
input[:author] = author if author
|
|
1298
|
-
path =
|
|
1296
|
+
path = participant_queue_lock(session_dir, author && pending_limit) do
|
|
1297
|
+
# No worker: the participant's waiting files are all their turns,
|
|
1298
|
+
# counted and written under one lock (the Bridge counts under its).
|
|
1299
|
+
next :too_many if author && pending_limit && pending_files(session_dir, author) >= pending_limit
|
|
1300
|
+
|
|
1301
|
+
manager.write_turn_input(session_id, **input)
|
|
1302
|
+
end
|
|
1303
|
+
if path == :too_many
|
|
1304
|
+
return DeliverResult.new(status: :refused, code: 429,
|
|
1305
|
+
ack: { "error" => "too_many_turns", "detail" => ParticipantMessages::TOO_MANY_TURNS })
|
|
1306
|
+
end
|
|
1299
1307
|
return DeliverResult.new(status: :failed) unless path
|
|
1300
1308
|
|
|
1301
1309
|
owner = delivery_owner(manager, session_id, state_dir)
|
|
@@ -1326,6 +1334,20 @@ module Samagotchi
|
|
|
1326
1334
|
end
|
|
1327
1335
|
|
|
1328
1336
|
# The unclaimed input files +author+ wrote.
|
|
1337
|
+
PARTICIPANT_QUEUE_LOCK = "participants.lock"
|
|
1338
|
+
|
|
1339
|
+
# The block under the session's participant queue lock (a file lock:
|
|
1340
|
+
# chi web's threads, and another chi web), when +locked+; else as is.
|
|
1341
|
+
private_class_method def self.participant_queue_lock(session_dir, locked)
|
|
1342
|
+
return yield unless locked
|
|
1343
|
+
|
|
1344
|
+
FileUtils.mkdir_p(session_dir)
|
|
1345
|
+
File.open(File.join(session_dir, PARTICIPANT_QUEUE_LOCK), File::RDWR | File::CREAT, 0o600) do |lock|
|
|
1346
|
+
lock.flock(File::LOCK_EX)
|
|
1347
|
+
yield
|
|
1348
|
+
end
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1329
1351
|
private_class_method def self.pending_files(session_dir, author)
|
|
1330
1352
|
SessionInbox.find_new_input_files(session_dir).count do |file|
|
|
1331
1353
|
data = JSON.parse(File.read(file))
|
|
@@ -19,6 +19,7 @@ require_relative "session_metrics/usage"
|
|
|
19
19
|
require_relative "session_metrics/side_totals"
|
|
20
20
|
require_relative "side_usage"
|
|
21
21
|
require_relative "steer"
|
|
22
|
+
require_relative "turn_origin"
|
|
22
23
|
|
|
23
24
|
module Samagotchi
|
|
24
25
|
# SessionMetrics is a persistent, error-isolated collector of per-session
|
|
@@ -727,6 +728,7 @@ module Samagotchi
|
|
|
727
728
|
load_persisted
|
|
728
729
|
end
|
|
729
730
|
@started_at ||= now.iso8601(3)
|
|
731
|
+
origin = TurnOrigin.from(event[:origin])
|
|
730
732
|
@turn = TurnState.new(
|
|
731
733
|
session_id: @session_id,
|
|
732
734
|
iteration_count: 0,
|
|
@@ -766,8 +768,8 @@ module Samagotchi
|
|
|
766
768
|
gen_switch: false,
|
|
767
769
|
continue: event[:continue] ? true : false,
|
|
768
770
|
resumed: event[:resumed] ? true : false,
|
|
769
|
-
author:
|
|
770
|
-
automatic: automatic_origin?(
|
|
771
|
+
author: origin&.participant_author,
|
|
772
|
+
automatic: automatic_origin?(origin),
|
|
771
773
|
id: event[:turn_id] || SecureRandom.uuid,
|
|
772
774
|
started_at: now.iso8601(3),
|
|
773
775
|
started_monotonic: monotonic_time,
|
|
@@ -818,11 +820,10 @@ module Samagotchi
|
|
|
818
820
|
end
|
|
819
821
|
end
|
|
820
822
|
|
|
821
|
-
# Whether a turn's +origin+ names a client that is no
|
|
822
|
-
# without one (an in-process turn is the owner's).
|
|
823
|
+
# Whether a turn's +origin+ (a TurnOrigin) names a client that is no
|
|
824
|
+
# person's: false without one (an in-process turn is the owner's).
|
|
823
825
|
def automatic_origin?(origin)
|
|
824
|
-
client_id
|
|
825
|
-
!client_id.nil? && !ClientId.human?(client_id)
|
|
826
|
+
!origin&.client_id.nil? && !origin.human?
|
|
826
827
|
end
|
|
827
828
|
|
|
828
829
|
# The running turn's record so far, as end_turn would make it for a
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "steer"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
# Loaded only when a session is read: it brings Web::Grants (and Rack).
|
|
5
7
|
autoload :Participants, File.expand_path("participants", __dir__)
|
|
@@ -38,7 +40,7 @@ module Samagotchi
|
|
|
38
40
|
return nil unless Steer.participant_author?(author)
|
|
39
41
|
|
|
40
42
|
refresh!(session) if session && session.id != @session_id
|
|
41
|
-
id =
|
|
43
|
+
id = Steer.participant_id(author)
|
|
42
44
|
entry = @entries.find { |e| e.id == id } || (@entries = read).find { |e| e.id == id }
|
|
43
45
|
entry&.name
|
|
44
46
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A review session's own field (chi review, the web's Start review): the
|
|
5
|
+
# files under review, project-relative (the session's work tree top),
|
|
6
|
+
# and, for a review fork (chi review --from), the session it started from.
|
|
7
|
+
# A session without one isn't a review session; its file has no "review"
|
|
8
|
+
# key at all (Session::OPTIONAL_FIELDS), so other session files stay as
|
|
9
|
+
# they were. Kept by continues; never by forks or delegate children.
|
|
10
|
+
# See docs/sessions.md (Review sessions).
|
|
11
|
+
SessionReview = Data.define(:subjects, :from) do
|
|
12
|
+
def initialize(subjects: [], from: nil)
|
|
13
|
+
super(subjects: Array(subjects).map(&:to_s).reject(&:empty?).uniq.freeze, from: from&.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A SessionReview from the session file's form ({"subjects" => […],
|
|
17
|
+
# "from" => id}), a SessionReview, or nil (nil for nil or a non-hash).
|
|
18
|
+
def self.of(value)
|
|
19
|
+
case value
|
|
20
|
+
when SessionReview then value
|
|
21
|
+
when Hash then new(subjects: value["subjects"] || value[:subjects], from: value["from"] || value[:from])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The session file's form: "from" only when there is one.
|
|
26
|
+
def to_file = { "subjects" => subjects, "from" => from }.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -93,6 +93,9 @@ module Samagotchi
|
|
|
93
93
|
send(:"run_#{@sub || "invite"}", parsed.options, parsed.args)
|
|
94
94
|
rescue SessionAccess::BadRequest => e
|
|
95
95
|
usage_error(e.message)
|
|
96
|
+
rescue Web::Grants::Corrupt => e
|
|
97
|
+
error_line("#{command_name}: #{e.message}")
|
|
98
|
+
CLI::Exit::FAILED
|
|
96
99
|
rescue ArgumentError => e
|
|
97
100
|
error_line("#{command_name}: #{e.is_a?(Session::AmbiguousId) ? e.message : e.message.sub(/\ASession not found: /, "no session ")}")
|
|
98
101
|
CLI::Exit::FAILED
|
|
@@ -189,8 +192,11 @@ module Samagotchi
|
|
|
189
192
|
"#{local(row.expires_at, "%Y-%m-%d %H:%M")} · cap #{money(row.cap)}")
|
|
190
193
|
@stdout.puts(invitation.link)
|
|
191
194
|
@stdout.puts("(shown once: chi keeps only its digest; chi share revoke #{id[0, 8]} #{row.id} ends it)")
|
|
195
|
+
if invitation.warning
|
|
196
|
+
error_line("#{command_name}: warning: #{invitation.warning} (#{@access.invocation.command("review --from #{id[0, 8]}")} starts one)")
|
|
197
|
+
end
|
|
192
198
|
unless invitation.lan
|
|
193
|
-
@stdout.puts("chi web isn't on the LAN there (web.host: lan): this link opens on that machine only")
|
|
199
|
+
@stdout.puts("chi web isn't on the LAN there (web.host: lan): this link opens on that machine only, as the participant")
|
|
194
200
|
end
|
|
195
201
|
@stdout.puts(Web::QR.lines(invitation.link)) if @tty
|
|
196
202
|
end
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -128,6 +128,9 @@ module Samagotchi
|
|
|
128
128
|
PARTICIPANT_SOURCE = "participant"
|
|
129
129
|
# The owner's author, when one is written out; absent means the same.
|
|
130
130
|
OWNER_AUTHOR = "owner"
|
|
131
|
+
# A participant's author: this prefix, then its id (Web::Grants'
|
|
132
|
+
# grant id), "p:<id>" (#author_for, #participant_id).
|
|
133
|
+
PARTICIPANT_PREFIX = "p:"
|
|
131
134
|
# A participant's message's keys, which every message copy keeps.
|
|
132
135
|
AUTHOR_KEYS = %i[author author_name].freeze
|
|
133
136
|
|
|
@@ -187,7 +190,7 @@ module Samagotchi
|
|
|
187
190
|
# again (ParticipantName), or the participant's id when it has none.
|
|
188
191
|
def review_header(message)
|
|
189
192
|
name = ParticipantName.clean(message[:author_name] || message["author_name"])
|
|
190
|
-
name = ParticipantName.clean((message[:author] || message["author"])
|
|
193
|
+
name = ParticipantName.clean(participant_id(message[:author] || message["author"])) if name.empty?
|
|
191
194
|
format(REVIEW_HEADER, name.empty? ? "a participant" : name)
|
|
192
195
|
end
|
|
193
196
|
|
|
@@ -219,6 +222,13 @@ module Samagotchi
|
|
|
219
222
|
# +author+ when it is a participant's, else nil (the owner).
|
|
220
223
|
def sender_author(author) = participant_author?(author) ? author.to_s : nil
|
|
221
224
|
|
|
225
|
+
# The author a participant's turns and messages carry: "p:<id>".
|
|
226
|
+
def author_for(participant_id) = "#{PARTICIPANT_PREFIX}#{participant_id}"
|
|
227
|
+
|
|
228
|
+
# The participant id in +author+ ("p:<id>" → "<id>"); a string without
|
|
229
|
+
# the prefix as it is ("" for nil).
|
|
230
|
+
def participant_id(author) = author.to_s.delete_prefix(PARTICIPANT_PREFIX)
|
|
231
|
+
|
|
222
232
|
# The text a steer or merged input goes out as: its header, a newline,
|
|
223
233
|
# then +text+ (the message's content by default). Other messages: +text+.
|
|
224
234
|
def wire_text(message, text = (message[:content] || message["content"]).to_s)
|