samagotchi 0.127.0 → 0.129.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 +106 -1
- data/docs/architecture.md +15 -5
- data/docs/cli.md +2 -1
- data/docs/guardrails.md +155 -25
- data/docs/internals/participants.md +23 -5
- data/docs/internals/web-push.md +4 -1
- data/docs/sessions.md +1 -1
- data/docs/testing.md +3 -2
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bridge_client.rb +8 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +56 -0
- data/lib/samagotchi/guardrail_wiring.rb +6 -5
- data/lib/samagotchi/guardrails/child_boundary.rb +23 -13
- data/lib/samagotchi/guardrails/outside.rb +54 -5
- data/lib/samagotchi/guardrails/pair_boundary.rb +78 -26
- data/lib/samagotchi/guardrails/pair_command.rb +261 -66
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +98 -5
- data/lib/samagotchi/guardrails/read_only_shell.rb +239 -9
- data/lib/samagotchi/guardrails/review_paths.rb +22 -8
- data/lib/samagotchi/guardrails/rm_targets.rb +4 -3
- data/lib/samagotchi/guardrails/scratch_writes.rb +3 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +67 -35
- data/lib/samagotchi/guardrails/shell_lex.rb +1 -1
- data/lib/samagotchi/guardrails/shell_paths.rb +73 -25
- data/lib/samagotchi/guardrails/targets.rb +128 -24
- data/lib/samagotchi/session_manager.rb +13 -14
- data/lib/samagotchi/system_one/self_protect.rb +108 -16
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +9 -5
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +6 -12
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +23 -35
- data/lib/samagotchi/terminal_ui/attached_loop.rb +10 -18
- data/lib/samagotchi/terminal_ui/exit_lines.rb +2 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui/own_turns.rb +81 -0
- data/lib/samagotchi/verify_anchor.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +9 -7
- data/lib/samagotchi/web/participant_review.rb +4 -2
- data/lib/samagotchi/web/public/annotate_bar.js +14 -2
- data/lib/samagotchi/web/public/app.js +25 -5
- data/lib/samagotchi/web/public/composer.css +1 -0
- data/lib/samagotchi/web/public/notify.js +2 -1
- data/lib/samagotchi/web/public/participant.html +3 -2
- data/lib/samagotchi/web/public/participant.js +18 -7
- data/lib/samagotchi/web/public/participant_model.js +3 -2
- data/lib/samagotchi/web/public/participant_names.js +2 -2
- data/lib/samagotchi/web/public/question_card.js +4 -1
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/stream_handlers.js +4 -2
- data/lib/samagotchi/web/public/verdict_card.js +18 -3
- data/lib/samagotchi/web/public/verdict_fresh.js +23 -5
- data/lib/samagotchi/web/public/verify_card.css +1 -0
- data/lib/samagotchi/web/public/verify_preset.js +6 -0
- data/lib/samagotchi/web/verify_fresh_routes.rb +12 -7
- data/lib/samagotchi/worker.rb +82 -32
- data/lib/samagotchi/worker_idle_exit.rb +17 -0
- data/lib/samagotchi/worker_link.rb +13 -5
- metadata +2 -1
|
@@ -71,7 +71,8 @@ module Samagotchi
|
|
|
71
71
|
# TUI's streams and requests there (#client_id)
|
|
72
72
|
# @return [Symbol] :detached, :closed when the worker went away,
|
|
73
73
|
# :failed when the --model switch didn't go through, or (input from a
|
|
74
|
-
# pipe) :turn_failed / :empty_answer / :unanswered
|
|
74
|
+
# pipe) :turn_failed / :empty_answer / :removed / :unanswered
|
|
75
|
+
# (AttachedLoop#run)
|
|
75
76
|
def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
|
|
76
77
|
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
77
78
|
toolkit_flags: [], settings: nil, parent_answers: nil, state_dir: nil, connector: nil)
|
|
@@ -146,11 +147,14 @@ module Samagotchi
|
|
|
146
147
|
err.flush
|
|
147
148
|
end
|
|
148
149
|
|
|
149
|
-
#
|
|
150
|
-
# line on stderr,
|
|
151
|
-
#
|
|
150
|
+
# An own turn with no answer, or a prompt removed before it ran
|
|
151
|
+
# (input from a pipe): one line on stderr, so a script doesn't take
|
|
152
|
+
# the silence for an answer (exit 1).
|
|
152
153
|
def report_ended(ended, err: $stderr)
|
|
153
|
-
|
|
154
|
+
case ended
|
|
155
|
+
when :empty_answer then err.puts(TerminalUI::EMPTY_ANSWER_ERROR)
|
|
156
|
+
when :removed then err.puts("chi: the prompt was removed before it ran")
|
|
157
|
+
end
|
|
154
158
|
end
|
|
155
159
|
|
|
156
160
|
# chi's exit status after #run: 0 detached, 3 a question left waiting
|
|
@@ -7,8 +7,8 @@ module Samagotchi
|
|
|
7
7
|
class AttachedLoop
|
|
8
8
|
# Queue and unqueue handling for the attached loop.
|
|
9
9
|
# Reads @client, @client_id, @first_command_id, @link, @my_queued,
|
|
10
|
-
# @
|
|
11
|
-
# @my_queued, @
|
|
10
|
+
# @own_turns, @reader, @screen, @shown_enqueued, @wait_at_eof; writes
|
|
11
|
+
# @my_queued, @prefilled, @queued_ids,
|
|
12
12
|
# @shown_enqueued.
|
|
13
13
|
# Calls the loop helpers #author_of, #first_command_flag, #mark_activity,
|
|
14
14
|
# #own?, #prompt_line, #send_prompt, #via, and #worker_down.
|
|
@@ -131,19 +131,13 @@ module Samagotchi
|
|
|
131
131
|
|
|
132
132
|
# A queued prompt was taken back before it ran (the web's ×, /unqueue
|
|
133
133
|
# in a terminal). One of this run's (wait_at_eof) will have no turn:
|
|
134
|
-
# it ends the wait
|
|
135
|
-
# line says so.
|
|
134
|
+
# it ends the wait as :removed. Another client's shown line says so.
|
|
136
135
|
def turn_dequeued(event)
|
|
137
136
|
id = event[:enqueued_id]
|
|
138
137
|
@my_queued.delete(id)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@screen.commit("(removed before it ran)")
|
|
143
|
-
else
|
|
144
|
-
@own_failed = true
|
|
145
|
-
@screen.commit("(your queued message was removed before it ran)")
|
|
146
|
-
end
|
|
138
|
+
by_us = own?(event[:client_id])
|
|
139
|
+
if @own_turns.dequeued(id, by_us: by_us)
|
|
140
|
+
@screen.commit(by_us ? "(removed before it ran)" : "(your queued message was removed before it ran)")
|
|
147
141
|
elsif @shown_enqueued.delete?(id)
|
|
148
142
|
@screen.commit("(a queued message was removed)")
|
|
149
143
|
end
|
|
@@ -10,19 +10,16 @@ require_relative "../image_input"
|
|
|
10
10
|
module Samagotchi
|
|
11
11
|
class TerminalUI
|
|
12
12
|
class AttachedLoop
|
|
13
|
-
# Bridge sending
|
|
14
|
-
# Reads @as_parent, @client, @client_id, @
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# #
|
|
23
|
-
# #persist_recent_history, #prompt_line, #put_back_busy_command, #prefill_confirm,
|
|
24
|
-
# #queue_needs_newer_worker, #send_command, #send_prompt, #stale_worker,
|
|
25
|
-
# #steer_cut_note, #too_late?, #via, and #worker_down.
|
|
13
|
+
# Bridge sending and first-command launch.
|
|
14
|
+
# Reads @as_parent, @client, @client_id, @first_command_id, @first_command_line,
|
|
15
|
+
# @first_commands, @first_prompt, @link, @my_queued, @no_interrupt, @own_turns,
|
|
16
|
+
# @queued_ids, @reader, @running, @screen, @status;
|
|
17
|
+
# writes @first_command_id, @first_command_line, @first_commands, @first_prompt,
|
|
18
|
+
# @my_queued, @queued_ids.
|
|
19
|
+
# Calls the loop helpers #command_registry, #default_model_name, #detach,
|
|
20
|
+
# #mark_activity, #older_worker_command, #origin_of, #own?, #paint,
|
|
21
|
+
# #persist_recent_history, #prompt_line, #queue_needs_newer_worker,
|
|
22
|
+
# #stale_worker, #steer_cut_note, #too_late?, #via, and #worker_down.
|
|
26
23
|
module Sending
|
|
27
24
|
private
|
|
28
25
|
|
|
@@ -33,7 +30,7 @@ module Samagotchi
|
|
|
33
30
|
if reply.status == 202
|
|
34
31
|
# Input from a pipe ends once its output is in (#command_ran).
|
|
35
32
|
command_id = reply.json&.fetch("command_id", nil)
|
|
36
|
-
@
|
|
33
|
+
@own_turns.command_sent(command_id) if command_id
|
|
37
34
|
return
|
|
38
35
|
end
|
|
39
36
|
return @screen.commit(stale_worker("run commands")) if reply.status == 404
|
|
@@ -49,7 +46,7 @@ module Samagotchi
|
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
def command_ran(event)
|
|
52
|
-
@
|
|
49
|
+
@own_turns.command_ran(event[:command_id])
|
|
53
50
|
@queued_ids.delete(event[:command_id])
|
|
54
51
|
unless own?(event[:client_id]) || event[:anytime] || event[:card] || event[:queued]
|
|
55
52
|
@screen.commit(prompt_line(event[:client_id], event[:line]))
|
|
@@ -147,8 +144,7 @@ module Samagotchi
|
|
|
147
144
|
reply = @client.post_turn(**options)
|
|
148
145
|
if reply.status == 202
|
|
149
146
|
enqueued_id = reply.json&.fetch("enqueued_id", nil)
|
|
150
|
-
@
|
|
151
|
-
@open_ids << enqueued_id if enqueued_id && @wait_at_eof
|
|
147
|
+
@own_turns.sent(enqueued_id) if enqueued_id
|
|
152
148
|
@my_queued << enqueued_id if enqueued_id && reply.json&.fetch("delivery", nil) == Delivery::QUEUE
|
|
153
149
|
note = sent_note(reply.json, delivery: delivery, images: images, running: running, choice: choice)
|
|
154
150
|
@screen.commit(note) if note
|
|
@@ -172,21 +168,15 @@ module Samagotchi
|
|
|
172
168
|
# Whether the input's end must wait: the -p prompt isn't sent yet, a
|
|
173
169
|
# prompt this run sent hasn't had its turn, or its turn's after_turn
|
|
174
170
|
# hooks still run (their notices, source-links' sources:, come next).
|
|
175
|
-
def own_turns_pending? = !@first_prompt.to_s.strip.empty? ||
|
|
171
|
+
def own_turns_pending? = !@first_prompt.to_s.strip.empty? || @own_turns.pending?
|
|
176
172
|
|
|
177
173
|
# A turn ended: this run's prompt (and ours merged into it) had its turn.
|
|
178
174
|
def own_turn_ended(event)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@own_empty = true if ours && event[:type] == :turn_completed && event.dig(:turn_summary, :empty_answer)
|
|
185
|
-
# The worker sends answer_display once its after_turn hooks ran.
|
|
186
|
-
@display_pending = true if ours && event[:type] == :turn_completed && event[:display_pending]
|
|
187
|
-
@open_ids.delete(id)
|
|
188
|
-
@open_ids.subtract(@merged_ids)
|
|
189
|
-
@merged_ids.clear
|
|
175
|
+
completed = event[:type] == :turn_completed
|
|
176
|
+
@own_turns.ended(origin_of(event[:origin]).enqueued_id,
|
|
177
|
+
failed: event[:type] == :turn_failed,
|
|
178
|
+
empty: completed && event.dig(:turn_summary, :empty_answer),
|
|
179
|
+
display_pending: completed && event[:display_pending])
|
|
190
180
|
end
|
|
191
181
|
|
|
192
182
|
# Ours merged into the running turn: they end with it.
|
|
@@ -194,22 +184,20 @@ module Samagotchi
|
|
|
194
184
|
Array(event[:origins]).each do |value|
|
|
195
185
|
origin = origin_of(value)
|
|
196
186
|
id = origin.enqueued_id
|
|
197
|
-
@
|
|
187
|
+
@own_turns.merged(id) if own?(origin.client_id)
|
|
198
188
|
end
|
|
199
189
|
nil
|
|
200
190
|
end
|
|
201
191
|
|
|
202
192
|
def own_prompt_failed
|
|
203
|
-
@
|
|
193
|
+
@own_turns.prompt_failed
|
|
204
194
|
nil
|
|
205
195
|
end
|
|
206
196
|
|
|
207
197
|
# The input ended and nothing of ours is left to wait for.
|
|
208
198
|
def end_of_input
|
|
209
|
-
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @input_ended
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@own_empty ? :empty_answer : :detached
|
|
199
|
+
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @own_turns.input_ended?
|
|
200
|
+
@own_turns.outcome
|
|
213
201
|
end
|
|
214
202
|
|
|
215
203
|
# The prompt's `@path` images, stored where the worker reads them
|
|
@@ -14,6 +14,7 @@ require_relative "image_input"
|
|
|
14
14
|
require_relative "line_reader"
|
|
15
15
|
require_relative "question_prompt"
|
|
16
16
|
require_relative "join_replay"
|
|
17
|
+
require_relative "own_turns"
|
|
17
18
|
require_relative "exit_lines"
|
|
18
19
|
require_relative "attached_loop/exit_commands"
|
|
19
20
|
require_relative "attached_loop/questions"
|
|
@@ -68,8 +69,7 @@ module Samagotchi
|
|
|
68
69
|
attr_reader :client_id
|
|
69
70
|
|
|
70
71
|
# @return [String, nil] the model the worker's turns run on, as the
|
|
71
|
-
# last
|
|
72
|
-
# @return [String, nil] the worker's model
|
|
72
|
+
# last snapshot or model change said (the status row's)
|
|
73
73
|
def model_name = @status[:model]
|
|
74
74
|
|
|
75
75
|
# @return [QuestionPrompt, nil] the question waiting for an answer
|
|
@@ -152,21 +152,12 @@ module Samagotchi
|
|
|
152
152
|
@join_replay = JoinReplay.new(screen: @screen, view: @view, link: @link, gaps: @gaps, renderer: @renderer)
|
|
153
153
|
@last_idle_interrupt_at = nil
|
|
154
154
|
@next_input_prefill = nil
|
|
155
|
-
# Prompts this run sent, by enqueued_id: only those come back into
|
|
156
|
-
# the input when their turn fails (a replayed event must not).
|
|
157
|
-
@sent_ids = Set.new
|
|
158
155
|
# This run's prompts queued for after the running turn (their ack
|
|
159
156
|
# said queue), oldest first, until their turn starts or they are
|
|
160
157
|
# removed: /unqueue takes back the newest.
|
|
161
158
|
@my_queued = []
|
|
162
159
|
@wait_at_eof = wait_at_eof
|
|
163
|
-
|
|
164
|
-
# running turn, whether the input has ended, and whether one of this
|
|
165
|
-
# run's prompts failed (wait_at_eof only).
|
|
166
|
-
@open_ids = Set.new
|
|
167
|
-
@merged_ids = Set.new
|
|
168
|
-
@input_ended = false
|
|
169
|
-
@own_failed = false
|
|
160
|
+
@own_turns = OwnTurns.new(active: wait_at_eof)
|
|
170
161
|
# A continue offer is pending: the prompt asks for the answer.
|
|
171
162
|
@continue_offer = nil
|
|
172
163
|
# The /exit ask is open: the next line is its answer (#answer_exit_ask).
|
|
@@ -188,7 +179,8 @@ module Samagotchi
|
|
|
188
179
|
# :failed when the first command (--model) didn't go through, or
|
|
189
180
|
# (wait_at_eof) :turn_failed when one of this run's prompts failed,
|
|
190
181
|
# :empty_answer when one ended with no answer, and :unanswered when
|
|
191
|
-
# a question came after the input ended
|
|
182
|
+
# a question came after the input ended; :removed when this run took
|
|
183
|
+
# back its only queued prompt before it ran
|
|
192
184
|
# @param input [#call, nil] prompt -> line (nil = Ctrl-D, raising
|
|
193
185
|
# Interrupt = Ctrl-C); defaults to Reline
|
|
194
186
|
def run(input: nil)
|
|
@@ -225,7 +217,7 @@ module Samagotchi
|
|
|
225
217
|
|
|
226
218
|
ended = take_early_lines
|
|
227
219
|
return ended if ended
|
|
228
|
-
return end_of_input if @input_ended && !own_turns_pending?
|
|
220
|
+
return end_of_input if @own_turns.input_ended? && !own_turns_pending?
|
|
229
221
|
when :line
|
|
230
222
|
# A pipe is read at once: its lines wait for the joining
|
|
231
223
|
# snapshot, which may bring the question they answer.
|
|
@@ -256,7 +248,7 @@ module Samagotchi
|
|
|
256
248
|
# has nothing left to answer it.
|
|
257
249
|
return unanswered_question(@question_pending) if @question
|
|
258
250
|
|
|
259
|
-
@
|
|
251
|
+
@own_turns.end_input
|
|
260
252
|
return nil
|
|
261
253
|
end
|
|
262
254
|
end_of_input if submit(line) == :detach
|
|
@@ -303,7 +295,7 @@ module Samagotchi
|
|
|
303
295
|
own_turn_ended(event)
|
|
304
296
|
when :prompt_restored then restore_prompt(event)
|
|
305
297
|
# The after_turn hooks are done (their notices came before it).
|
|
306
|
-
when :answer_display then @
|
|
298
|
+
when :answer_display then @own_turns.displayed
|
|
307
299
|
when :context_status, :used_memories_updated then @status.take_event(event)
|
|
308
300
|
when :participants_changed then participants_changed(event)
|
|
309
301
|
# Another client's anytime command: its line now, before the cards
|
|
@@ -333,7 +325,7 @@ module Samagotchi
|
|
|
333
325
|
refresh_children if Array(event[:origins]).any? { |origin| report_origin?(origin_of(origin)) }
|
|
334
326
|
when :question_requested
|
|
335
327
|
# Nothing is left to answer it with.
|
|
336
|
-
return unanswered_question(event[:pending_question]) if @input_ended
|
|
328
|
+
return unanswered_question(event[:pending_question]) if @own_turns.input_ended?
|
|
337
329
|
|
|
338
330
|
ask(event[:pending_question])
|
|
339
331
|
when :question_answered then question_answered(event)
|
|
@@ -515,7 +507,7 @@ module Samagotchi
|
|
|
515
507
|
# goes back into the input, as the REPL restores it for a retry.
|
|
516
508
|
def restore_prompt(event)
|
|
517
509
|
origin = origin_of(event[:origin])
|
|
518
|
-
return unless own?(origin.client_id) && @
|
|
510
|
+
return unless own?(origin.client_id) && @own_turns.sent?(origin.enqueued_id)
|
|
519
511
|
# Lines from a pipe: nobody to retry it (the REPL's rule).
|
|
520
512
|
return if @wait_at_eof
|
|
521
513
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../tools/task_record"
|
|
4
|
+
require_relative "../worker_idle_exit"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
class TerminalUI
|
|
@@ -23,7 +24,7 @@ module Samagotchi
|
|
|
23
24
|
"reminders" => "reminders are set", "starting" => "the worker is still starting",
|
|
24
25
|
"background_tasks" => "background tasks it started are still running",
|
|
25
26
|
"delegates" => "delegates of this session are still running"
|
|
26
|
-
}.freeze
|
|
27
|
+
}.merge(WorkerIdleExit::LEAVING_WORDS.transform_keys(&:to_s)).freeze
|
|
27
28
|
# How to end a background task that holds the worker up (an older
|
|
28
29
|
# worker's 409 has no hint of its own).
|
|
29
30
|
TASK_STOP_HINT = "stop one with the web's stop-task button or by asking for task_stop"
|
|
@@ -57,7 +57,7 @@ module Samagotchi
|
|
|
57
57
|
id = @session_id
|
|
58
58
|
return [:gone, "stopped"] if Session.stopped_marker?(id, state_dir: @state_dir)
|
|
59
59
|
|
|
60
|
-
link = WorkerLink.resolve(session_dir, unlink: true
|
|
60
|
+
link = WorkerLink.resolve(session_dir, unlink: true)
|
|
61
61
|
if link.is_a?(WorkerLink::Skew)
|
|
62
62
|
@failure = link.message(id)
|
|
63
63
|
return [:gone, "worker_newer"]
|
|
@@ -79,7 +79,7 @@ module Samagotchi
|
|
|
79
79
|
candidate = if found&.port == port
|
|
80
80
|
found
|
|
81
81
|
else
|
|
82
|
-
link = WorkerLink.resolve(session_dir, unlink: true
|
|
82
|
+
link = WorkerLink.resolve(session_dir, unlink: true)
|
|
83
83
|
link.live? && link.port == port ? link.client(@session_id, host: @host) : nil
|
|
84
84
|
end
|
|
85
85
|
@client = candidate if candidate
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class TerminalUI
|
|
5
|
+
# This run's own prompts and commands. Sent prompts are always recorded so
|
|
6
|
+
# only this run's failed prompts can return to the input. With pipe input
|
|
7
|
+
# (+active+), this also tracks turns still open, prompts merged into a
|
|
8
|
+
# running turn, whether input ended, and whether a turn failed, was empty, was removed,
|
|
9
|
+
# or still has an answer display pending. #outcome is what #run returns
|
|
10
|
+
# once those turns finish.
|
|
11
|
+
class OwnTurns
|
|
12
|
+
def initialize(active:)
|
|
13
|
+
@active = active
|
|
14
|
+
@sent = Set.new
|
|
15
|
+
@open = Set.new
|
|
16
|
+
@merged = Set.new
|
|
17
|
+
@input_ended = false
|
|
18
|
+
@failed = false
|
|
19
|
+
@empty = false
|
|
20
|
+
@removed = false
|
|
21
|
+
@display_pending = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sent(enqueued_id)
|
|
25
|
+
@sent << enqueued_id
|
|
26
|
+
@open << enqueued_id if @active
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sent?(enqueued_id) = @sent.include?(enqueued_id)
|
|
30
|
+
|
|
31
|
+
def command_sent(command_id)
|
|
32
|
+
@open << command_id if @active
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def command_ran(command_id) = @open.delete(command_id)
|
|
36
|
+
|
|
37
|
+
def merged(enqueued_id)
|
|
38
|
+
@merged << enqueued_id if @open.include?(enqueued_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ended(enqueued_id, failed:, empty:, display_pending:)
|
|
42
|
+
return unless @active
|
|
43
|
+
|
|
44
|
+
ours = @open.include?(enqueued_id) || !@merged.empty?
|
|
45
|
+
@failed = true if ours && failed
|
|
46
|
+
@empty = true if ours && empty
|
|
47
|
+
@display_pending = true if ours && display_pending
|
|
48
|
+
@open.delete(enqueued_id)
|
|
49
|
+
@open.subtract(@merged)
|
|
50
|
+
@merged.clear
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def displayed = (@display_pending = false)
|
|
54
|
+
|
|
55
|
+
def prompt_failed
|
|
56
|
+
@failed = true if @active
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dequeued(enqueued_id, by_us:)
|
|
60
|
+
return false unless @open.delete?(enqueued_id)
|
|
61
|
+
|
|
62
|
+
by_us ? (@removed = true) : (@failed = true)
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def end_input = (@input_ended = true)
|
|
67
|
+
|
|
68
|
+
def input_ended? = @input_ended
|
|
69
|
+
|
|
70
|
+
def pending? = !@open.empty? || @display_pending
|
|
71
|
+
|
|
72
|
+
def outcome
|
|
73
|
+
return :turn_failed if @failed
|
|
74
|
+
return :empty_answer if @empty
|
|
75
|
+
return :removed if @removed
|
|
76
|
+
|
|
77
|
+
:detached
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -25,4 +25,12 @@ module Samagotchi
|
|
|
25
25
|
|
|
26
26
|
def to_file = { "turn_id" => turn_id, "quote" => quote }
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
VerifyAnchor::FEATURE = "verify"
|
|
30
|
+
VerifyAnchor::UNAVAILABLE = {
|
|
31
|
+
"error" => "verify_unavailable",
|
|
32
|
+
"detail" => "this session's worker predates Verify turns; restart this session to use Verify"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def VerifyAnchor.worker_unavailable?(features) = !features.nil? && !features.include?(VerifyAnchor::FEATURE)
|
|
28
36
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -118,6 +118,9 @@ module Samagotchi
|
|
|
118
118
|
# Seconds GET /api/models waits for the hosts' lists before answering
|
|
119
119
|
# with what it has (the listing goes on and fills the registry's cache).
|
|
120
120
|
MODELS_WAIT_TIMEOUT = 4.0
|
|
121
|
+
# The one refusal for an update operation (an install, a workers
|
|
122
|
+
# restart, a chi web restart) asked while another one runs: 409 update_running.
|
|
123
|
+
UPDATE_RUNNING = "a chi update operation is already running; try again when it ends"
|
|
121
124
|
EVENTS_POLL = 1.0
|
|
122
125
|
|
|
123
126
|
# A callable the event loops ask whether the server still runs;
|
|
@@ -702,7 +705,7 @@ module Samagotchi
|
|
|
702
705
|
false
|
|
703
706
|
end
|
|
704
707
|
end
|
|
705
|
-
return error_response(409, "update_running",
|
|
708
|
+
return error_response(409, "update_running", UPDATE_RUNNING) if already_running
|
|
706
709
|
|
|
707
710
|
@hub.publish_update(state: "running", rows: [], operation: "install", version: version)
|
|
708
711
|
Thread.new { perform_update(version) }
|
|
@@ -774,7 +777,7 @@ module Samagotchi
|
|
|
774
777
|
false
|
|
775
778
|
end
|
|
776
779
|
end
|
|
777
|
-
return error_response(409, "update_running",
|
|
780
|
+
return error_response(409, "update_running", UPDATE_RUNNING) if already_running
|
|
778
781
|
|
|
779
782
|
@hub.publish_update(state: "running", rows: [], operation: "workers")
|
|
780
783
|
Thread.new { perform_worker_restarts }
|
|
@@ -834,7 +837,7 @@ module Samagotchi
|
|
|
834
837
|
@update_running = true # held for good once restarting: the exec ends this process
|
|
835
838
|
false
|
|
836
839
|
end
|
|
837
|
-
return error_response(409, "update_running",
|
|
840
|
+
return error_response(409, "update_running", UPDATE_RUNNING) if busy
|
|
838
841
|
|
|
839
842
|
case @restart_web.call
|
|
840
843
|
when true then return json_response(202, { state: "restarting" })
|
|
@@ -1721,8 +1724,7 @@ module Samagotchi
|
|
|
1721
1724
|
outcome.is_a?(WorkerLink::Leaving) || outcome.is_a?(WorkerLink::Stale) ||
|
|
1722
1725
|
outcome.is_a?(WorkerLink::Skew) ||
|
|
1723
1726
|
(outcome.is_a?(WorkerLink::Starting) && outcome.stuck)
|
|
1724
|
-
end
|
|
1725
|
-
host: DEFAULT_HOST)
|
|
1727
|
+
end)
|
|
1726
1728
|
end
|
|
1727
1729
|
|
|
1728
1730
|
def stream_link_error(id, link)
|
|
@@ -1741,7 +1743,7 @@ module Samagotchi
|
|
|
1741
1743
|
def stream_open_error(id, outcome)
|
|
1742
1744
|
return error_response(503, "not_live", WorkerLink.unanswered(id)) if outcome.is_a?(BridgeClient::StreamOpen::Unreachable)
|
|
1743
1745
|
return error_response(503, "not_live", WorkerLink.not_live(id)) if outcome.reason == :unknown_session
|
|
1744
|
-
return error_response(502,
|
|
1746
|
+
return error_response(502, BridgeSecret::ERROR, BridgeSecret.refused(id)) if outcome.reason == :bridge_secret
|
|
1745
1747
|
|
|
1746
1748
|
error_response(502, "bridge_error", outcome.detail || "the session's worker answered #{outcome.status}")
|
|
1747
1749
|
end
|
|
@@ -1752,7 +1754,7 @@ module Samagotchi
|
|
|
1752
1754
|
# @return [WorkerLink::Live, WorkerLink::None, WorkerLink::Stale]
|
|
1753
1755
|
def worker_link(session_id)
|
|
1754
1756
|
dir = @session_class.session_dir(session_id, state_dir: default_state_dir)
|
|
1755
|
-
WorkerLink.resolve(dir, unlink: true
|
|
1757
|
+
WorkerLink.resolve(dir, unlink: true)
|
|
1756
1758
|
rescue StandardError
|
|
1757
1759
|
WorkerLink::None.new
|
|
1758
1760
|
end
|
|
@@ -51,7 +51,8 @@ module Samagotchi
|
|
|
51
51
|
# current_turn, waiting_for_owner, queued: {mine, others}, last_event_id}
|
|
52
52
|
def handle_review(req, id)
|
|
53
53
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
54
|
-
|
|
54
|
+
features = ParticipantMode.live_features(session, default_state_dir)
|
|
55
|
+
mode = ParticipantMode.of(session, features: features).to_s
|
|
55
56
|
viewer = participant_viewer(req, session)
|
|
56
57
|
live = bridge_get_json(id, "snapshot")
|
|
57
58
|
turn_snapshot = live && live["snapshot"]
|
|
@@ -67,7 +68,8 @@ module Samagotchi
|
|
|
67
68
|
status = Session::STATUS_IDLE if status == Session::STATUS_RUNNING && current.nil?
|
|
68
69
|
json_response(200, {
|
|
69
70
|
viewer: review_viewer(req, id, session: session, state: state, queued: queued, viewer: viewer, mode: mode),
|
|
70
|
-
session: { id: session.id, title: participant_title(session), status: status
|
|
71
|
+
session: { id: session.id, title: participant_title(session), status: status,
|
|
72
|
+
verify_unavailable: VerifyAnchor.worker_unavailable?(features) },
|
|
71
73
|
subjects: review_subjects(session, viewer),
|
|
72
74
|
participants: participant_names(id).map { |entry| entry.slice(:id, :name, :kind) },
|
|
73
75
|
messages: with_reply_html(ParticipantView.messages(shown, viewer, automatic: automatic)),
|
|
@@ -32,6 +32,7 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
32
32
|
// until its step closes)
|
|
33
33
|
// @param onQuote (text) runs with the quote (and a preset's note) to append
|
|
34
34
|
// to the composer; the selection is cleared and the bar hidden first
|
|
35
|
+
// @param disabledPreset (preset) → a reason to disable it, else null
|
|
35
36
|
// @param actions () => the open session's annotation actions
|
|
36
37
|
// (annotate_actions.js annotationActions), read on every selection
|
|
37
38
|
// @param running () => whether a turn runs (a send pill may wait for it)
|
|
@@ -56,7 +57,8 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
56
57
|
// @return {schedule(), hide(), el}; schedule() re-reads the selection on the
|
|
57
58
|
// next frame (call it on scroll)
|
|
58
59
|
export function createAnnotateBar({
|
|
59
|
-
presets, canAnnotate, roots, isLive, onQuote, onPreset = () => {},
|
|
60
|
+
presets, canAnnotate, roots, isLive, onQuote, onPreset = () => {}, disabledPreset = () => null,
|
|
61
|
+
actions = () => [], running = () => false, onAction = () => {},
|
|
60
62
|
dock = () => null, source: sourceOf = annotationSource, quote = quoteBlock, link = () => null, onLink = () => {},
|
|
61
63
|
doc = globalThis.document, win = globalThis.window,
|
|
62
64
|
}) {
|
|
@@ -80,6 +82,14 @@ export function createAnnotateBar({
|
|
|
80
82
|
btn.title = text;
|
|
81
83
|
bar.appendChild(btn);
|
|
82
84
|
});
|
|
85
|
+
const presetButtons = [...bar.children].slice(1, presets.length + 1);
|
|
86
|
+
function markPresets() {
|
|
87
|
+
presetButtons.forEach((btn, i) => {
|
|
88
|
+
const reason = disabledPreset(presets[i]);
|
|
89
|
+
btn.disabled = !!reason;
|
|
90
|
+
btn.title = reason || presetParts(presets[i]).text;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
83
93
|
// The plugin pills: a separator, the inline ones, then ⋯ and the list
|
|
84
94
|
// it opens. Redrawn only when the session's actions change.
|
|
85
95
|
const group = doc.createElement("span");
|
|
@@ -175,6 +185,7 @@ export function createAnnotateBar({
|
|
|
175
185
|
linkBtn.title = pendingLink?.title || "";
|
|
176
186
|
const list = actions() || [];
|
|
177
187
|
if (!sameActions(list, drawn)) drawActions(list);
|
|
188
|
+
markPresets();
|
|
178
189
|
markActions();
|
|
179
190
|
bar.hidden = false;
|
|
180
191
|
if (touch?.matches) return placeDocked();
|
|
@@ -248,6 +259,7 @@ export function createAnnotateBar({
|
|
|
248
259
|
return;
|
|
249
260
|
}
|
|
250
261
|
const preset = btn.dataset.index === undefined ? null : presets[Number(btn.dataset.index)];
|
|
262
|
+
if (btn.disabled || (preset && disabledPreset(preset))) return;
|
|
251
263
|
const text = preset ? presetNote(pendingQuote, presetParts(preset).text) : pendingQuote;
|
|
252
264
|
const fields = { ...pendingFields, source: pendingSource, range: pendingRange };
|
|
253
265
|
win.getSelection()?.removeAllRanges();
|
|
@@ -256,7 +268,7 @@ export function createAnnotateBar({
|
|
|
256
268
|
onQuote(text);
|
|
257
269
|
});
|
|
258
270
|
|
|
259
|
-
return { schedule, hide, el: bar };
|
|
271
|
+
return { schedule, hide, refresh: markPresets, el: bar };
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
// A preset's button label and note: a text is both.
|
|
@@ -59,12 +59,12 @@ import { createTaskStopper } from "./task_stop.js";
|
|
|
59
59
|
import { createSystemOneWrongAction } from "./system_one_wrong.js";
|
|
60
60
|
import { appendQuote } from "./annotations.js";
|
|
61
61
|
import { applyComposerText, applyRowTemplate, fetchApply } from "./review_apply.js";
|
|
62
|
-
import { nameVerdictCards } from "./verdict_card.js";
|
|
63
|
-
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
62
|
+
import { CHECK_NOT_ON_PAGE, nameVerdictCards, ownerCheckHash } from "./verdict_card.js";
|
|
63
|
+
import { freshURL, placeVerdictWithFresh, redrawFresh, settleRunning } from "./verdict_fresh.js";
|
|
64
64
|
import { authorBadge, nameUnnamedBadges } from "./participant_names.js";
|
|
65
65
|
import { parsePresets } from "./annotate_presets.js";
|
|
66
66
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
67
|
-
import { VERIFY_PRESET, withVerify } from "./verify_preset.js";
|
|
67
|
+
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, verifyWorkerUnavailable, withVerify } from "./verify_preset.js";
|
|
68
68
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
69
69
|
import { copyText, installCopy } from "./copy.js";
|
|
70
70
|
import { createQuestionCards } from "./question_cards.js";
|
|
@@ -235,6 +235,7 @@ const composeHintEl = $("#composeHint");
|
|
|
235
235
|
|
|
236
236
|
let selected = null;
|
|
237
237
|
let pendingVerifyAnchor = null;
|
|
238
|
+
let annotateBar = null;
|
|
238
239
|
// The session the composer's text was last written in ("" the start page,
|
|
239
240
|
// undefined unknown): the composer is one for every session, and continue →
|
|
240
241
|
// keeps only a draft of its own (fillOpener).
|
|
@@ -589,7 +590,13 @@ function copyUpdateCommand(command) {
|
|
|
589
590
|
function followSelectedSession(sess) {
|
|
590
591
|
current.owner = sess.owner || null;
|
|
591
592
|
current.workerVersion = sess.worker_version || null;
|
|
593
|
+
const verifyWas = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
592
594
|
current.workerFeatures = Array.isArray(sess.worker_features) ? sess.worker_features : [];
|
|
595
|
+
current.bridgeUp = sess.bridge_up === true;
|
|
596
|
+
// The Verify gate flipped (a restart brought the feature): the cards' Verify fresh part follows.
|
|
597
|
+
if (verifyWas !== verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp)) {
|
|
598
|
+
redrawFresh(historyEl, verifyFreshState());
|
|
599
|
+
}
|
|
593
600
|
const streaming = !!(streamControl && streamControl.live);
|
|
594
601
|
if (!streaming) current.status = sess.status;
|
|
595
602
|
updateInfoBar();
|
|
@@ -1344,18 +1351,27 @@ function placeVerdict(record) {
|
|
|
1344
1351
|
placeVerdictWithFresh(historyEl, record, {
|
|
1345
1352
|
who: badge ? badge.name : "You",
|
|
1346
1353
|
author: record.by || null,
|
|
1354
|
+
onMissingCheck: (missing) => {
|
|
1355
|
+
const hash = ownerCheckHash(allSessions, selected, missing.check_turn_id);
|
|
1356
|
+
if (hash) location.hash = hash;
|
|
1357
|
+
else showToast(CHECK_NOT_ON_PAGE);
|
|
1358
|
+
},
|
|
1347
1359
|
}, verifyFreshState());
|
|
1348
1360
|
}
|
|
1349
1361
|
|
|
1350
1362
|
// The owner's Verify fresh on an unclear verdict's card (verdict_fresh.js):
|
|
1351
1363
|
// always theirs to start; its running check links to the child's session.
|
|
1364
|
+
const verifyFreshErrors = {};
|
|
1352
1365
|
function verifyFreshState() {
|
|
1353
1366
|
const id = selected;
|
|
1367
|
+
const unavailable = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
1354
1368
|
if (!Array.isArray(current.verifyFresh)) current.verifyFresh = [];
|
|
1355
1369
|
return {
|
|
1356
1370
|
verdicts: current.verdicts || [],
|
|
1357
1371
|
running: current.verifyFresh,
|
|
1358
|
-
|
|
1372
|
+
errors: verifyFreshErrors,
|
|
1373
|
+
canStart: !!id && !unavailable,
|
|
1374
|
+
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
1359
1375
|
start: async (of) => {
|
|
1360
1376
|
try {
|
|
1361
1377
|
const body = await api(freshURL(id, of), { method: "POST", body: "{}" });
|
|
@@ -1421,6 +1437,7 @@ async function restartWorker() {
|
|
|
1421
1437
|
// The session bar (info_bar_model.js sessionBar): its buttons, and the
|
|
1422
1438
|
// info line rendered by info_bar.js.
|
|
1423
1439
|
function updateInfoBar() {
|
|
1440
|
+
annotateBar?.refresh();
|
|
1424
1441
|
const { buttons, bar } = sessionBar({
|
|
1425
1442
|
id: selected, state: current, sessions: allSessions, versions, cardTitle: (s) => lists.cardTitle(s),
|
|
1426
1443
|
continuing: sessionActions.continuing, restarting: restartingIds, streaming: !!streamControl, running: liveTurn.running,
|
|
@@ -1574,6 +1591,7 @@ function workerGone(id, { opened = true } = {}) {
|
|
|
1574
1591
|
if (!stopped) setLiveStatus("idle");
|
|
1575
1592
|
}
|
|
1576
1593
|
if (current.owner === "worker") current.owner = null;
|
|
1594
|
+
current.bridgeUp = false;
|
|
1577
1595
|
// Its card says "live" (the list's owner, or setLiveStatus's) until the
|
|
1578
1596
|
// hub's event says otherwise: the worker is gone now, as far as this
|
|
1579
1597
|
// tab can tell.
|
|
@@ -1956,8 +1974,10 @@ function fillComposer(text) {
|
|
|
1956
1974
|
promptEl.scrollTop = promptEl.scrollHeight;
|
|
1957
1975
|
}
|
|
1958
1976
|
|
|
1959
|
-
|
|
1977
|
+
annotateBar = createAnnotateBar({
|
|
1960
1978
|
presets: withVerify(parsePresets(document.body.dataset.annotatePresets)),
|
|
1979
|
+
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
1980
|
+
&& verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp) ? VERIFY_UNAVAILABLE : null,
|
|
1961
1981
|
canAnnotate: () => !!selected && current.owner !== "tui",
|
|
1962
1982
|
roots: turnRoots,
|
|
1963
1983
|
isLive: (root) => liveTurn.running && turnDom.containsLive(root),
|