samagotchi 0.124.0 → 0.126.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 +53 -1
- data/docs/architecture.md +4 -2
- data/docs/cli.md +20 -8
- data/docs/guardrails.md +22 -0
- data/docs/internals/participants.md +96 -3
- data/docs/internals/remote-sessions.md +2 -0
- data/docs/internals/web-push.md +10 -6
- data/docs/sessions.md +5 -2
- data/lib/samagotchi/bridge.rb +4 -3
- data/lib/samagotchi/bridge_client.rb +8 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +25 -4
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +3 -0
- data/lib/samagotchi/children_status.rb +6 -1
- data/lib/samagotchi/engine.rb +43 -7
- data/lib/samagotchi/guardrail_wiring.rb +10 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -0
- data/lib/samagotchi/process_group.rb +10 -1
- data/lib/samagotchi/reminder_carry.rb +92 -0
- data/lib/samagotchi/reminder_store.rb +49 -0
- data/lib/samagotchi/session.rb +40 -3
- data/lib/samagotchi/session_inbox.rb +14 -8
- data/lib/samagotchi/session_manager.rb +39 -8
- data/lib/samagotchi/session_participants.rb +10 -2
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +225 -0
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +138 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +225 -0
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +154 -0
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +302 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -1253
- data/lib/samagotchi/terminal_ui/exit_lines.rb +141 -0
- data/lib/samagotchi/terminal_ui/join_replay.rb +235 -0
- data/lib/samagotchi/verdict.rb +20 -0
- data/lib/samagotchi/verify_anchor.rb +28 -0
- data/lib/samagotchi/verify_fresh.rb +275 -0
- data/lib/samagotchi/verify_fresh_origin.rb +71 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +10 -3
- data/lib/samagotchi/web/participant_access.rb +15 -4
- data/lib/samagotchi/web/participant_review.rb +7 -1
- data/lib/samagotchi/web/participant_view.rb +46 -5
- data/lib/samagotchi/web/participants_admin.rb +15 -4
- data/lib/samagotchi/web/public/annotate_bar.css +8 -4
- data/lib/samagotchi/web/public/annotate_bar.js +43 -7
- data/lib/samagotchi/web/public/app.js +134 -8
- data/lib/samagotchi/web/public/claim_link.js +347 -0
- data/lib/samagotchi/web/public/data.js +2 -1
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/notify.js +13 -2
- data/lib/samagotchi/web/public/participant.html +3 -1
- data/lib/samagotchi/web/public/participant.js +113 -9
- data/lib/samagotchi/web/public/participants_panel.js +15 -0
- data/lib/samagotchi/web/public/route.js +3 -2
- data/lib/samagotchi/web/public/send_turn.js +6 -1
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +3 -0
- data/lib/samagotchi/web/public/tokens.css +2 -1
- data/lib/samagotchi/web/public/verdict_card.js +65 -0
- data/lib/samagotchi/web/public/verdict_fresh.js +120 -0
- data/lib/samagotchi/web/public/verify_card.css +17 -0
- data/lib/samagotchi/web/public/verify_preset.js +18 -0
- data/lib/samagotchi/web/push/attention.rb +25 -5
- data/lib/samagotchi/web/push/notifier.rb +4 -3
- data/lib/samagotchi/web/session_control.rb +2 -0
- data/lib/samagotchi/web/session_summary.rb +25 -1
- data/lib/samagotchi/web/verify_fresh_routes.rb +110 -0
- data/lib/samagotchi/worker.rb +125 -12
- data/lib/samagotchi/worker_idle_exit.rb +7 -6
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +19 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../delivery"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
class TerminalUI
|
|
7
|
+
class AttachedLoop
|
|
8
|
+
# Queue and unqueue handling for the attached loop.
|
|
9
|
+
# Reads @client, @client_id, @first_command_id, @link, @my_queued,
|
|
10
|
+
# @open_ids, @reader, @screen, @shown_enqueued, @wait_at_eof; writes
|
|
11
|
+
# @my_queued, @open_ids, @own_empty, @own_failed, @prefilled, @queued_ids,
|
|
12
|
+
# @shown_enqueued.
|
|
13
|
+
# Calls the loop helpers #author_of, #first_command_flag, #mark_activity,
|
|
14
|
+
# #own?, #prompt_line, #send_prompt, #via, and #worker_down.
|
|
15
|
+
module Queueing
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
QUEUED_NOTE = "(queued: runs after this turn)"
|
|
19
|
+
QUEUE_RUNS_NOW = "(the turn had ended: runs as the next turn)"
|
|
20
|
+
NOTHING_QUEUED = "(nothing queued: /queue TEXT queues a message for after the running turn)"
|
|
21
|
+
UNQUEUE_REFUSED = { "already_started" => "(not removed: it already started)",
|
|
22
|
+
"unknown_enqueued_id" => "(not removed: it is already gone)",
|
|
23
|
+
"not_queued" => "(not removed: it goes in at the next step)" }.freeze
|
|
24
|
+
|
|
25
|
+
# /cut TEXT and /queue TEXT (+route+ has a choice). During a turn the
|
|
26
|
+
# text goes with its delivery: a cut, or queued to run as a turn of
|
|
27
|
+
# its own after this one. A worker that doesn't queue (its sidecar
|
|
28
|
+
# names no "delivery" feature) would steer a queue, so it is not sent.
|
|
29
|
+
# At the prompt both send the text as typed, a normal turn: no local
|
|
30
|
+
# word or typo check (the worker's Bridge runs a session command as
|
|
31
|
+
# the command).
|
|
32
|
+
# @param route [ComposerRoute::Route]
|
|
33
|
+
# @return [nil]
|
|
34
|
+
def submit_delivery(route)
|
|
35
|
+
return @screen.commit(Formatting::STEER_CHOICE_USAGE) if route.kind == :usage
|
|
36
|
+
return @screen.commit(queue_needs_newer_worker) if Delivery.queue?(route.delivery) && !worker_queues?
|
|
37
|
+
|
|
38
|
+
# To an idle session it is an ordinary prompt: no delivery to send,
|
|
39
|
+
# and its ack says it was sent now (#sent_note).
|
|
40
|
+
send_prompt(route.text, delivery: route.delivery, choice: route.choice)
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether the attached worker runs delivery: queue (its sidecar names
|
|
45
|
+
# Delivery::QUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
46
|
+
# answers (a P0-P2 worker refuses a queue: #send_prompt).
|
|
47
|
+
def worker_queues? = @link.queues?
|
|
48
|
+
|
|
49
|
+
# /queue to a worker from before queueing, which would steer it.
|
|
50
|
+
def queue_needs_newer_worker
|
|
51
|
+
"(/queue needs a newer worker: restart it with #{via.command("sessions restart")}, then send again)"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def unqueue_needs_newer_worker = "(/unqueue needs a newer worker: restart it with #{via.command("sessions restart")})"
|
|
55
|
+
|
|
56
|
+
# /unqueue: take back the newest prompt this run queued for after the
|
|
57
|
+
# turn, before it runs (POST turn/dequeue). Its :turn_dequeued comes
|
|
58
|
+
# to every UI; here the reply says what became of it. A worker whose
|
|
59
|
+
# sidecar lacks "unqueue" is not asked.
|
|
60
|
+
def unqueue_newest
|
|
61
|
+
return @screen.commit(unqueue_needs_newer_worker) unless worker_unqueues?
|
|
62
|
+
|
|
63
|
+
id = @my_queued.last
|
|
64
|
+
return @screen.commit(NOTHING_QUEUED) unless id
|
|
65
|
+
|
|
66
|
+
reply = @client.dequeue(enqueued_id: id, client_id: @client_id)
|
|
67
|
+
error = reply.json&.fetch("error", nil)
|
|
68
|
+
return @screen.commit(unqueue_needs_newer_worker) if reply.status == 404 && error == "not_found"
|
|
69
|
+
|
|
70
|
+
# Removed, started or gone: no longer this run's to take back.
|
|
71
|
+
@my_queued.delete(id) if [200, 404, 409].include?(reply.status)
|
|
72
|
+
return unqueued(reply.json&.fetch("prompt", nil).to_s) if reply.status == 200
|
|
73
|
+
|
|
74
|
+
@screen.commit(UNQUEUE_REFUSED[error] || "could not remove the queued message (#{[reply.status, error].compact.join(" ")})")
|
|
75
|
+
rescue SystemCallError, IOError => e
|
|
76
|
+
@screen.commit("could not remove the queued message (#{worker_down(e)})")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The removal's line, and its text back in the input to change and
|
|
80
|
+
# send again (as a failed turn's prompt comes back, #restore_prompt):
|
|
81
|
+
# not over typed text, and not for lines from a pipe. Its images are
|
|
82
|
+
# not put back.
|
|
83
|
+
def unqueued(prompt)
|
|
84
|
+
@screen.commit("removed: #{first_line_clip(prompt)}")
|
|
85
|
+
return if @wait_at_eof || prompt.strip.empty?
|
|
86
|
+
|
|
87
|
+
# Set before the read restarts with it: the read's echo reads it.
|
|
88
|
+
@prefilled = prompt.strip
|
|
89
|
+
return if @reader&.prefill(prompt)
|
|
90
|
+
|
|
91
|
+
@prefilled = nil
|
|
92
|
+
@screen.commit("(the removed message is in the input history: ↑)")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def first_line_clip(text, max = 60)
|
|
96
|
+
line = text.to_s.strip.lines.first.to_s.strip
|
|
97
|
+
line.length > max ? "#{line[0, max]}…" : line
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Whether the attached worker takes queued messages back (its sidecar
|
|
101
|
+
# names SessionInbox::UNQUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
102
|
+
# answers (an older one's 404 not_found says so).
|
|
103
|
+
def worker_unqueues? = @link.unqueues?
|
|
104
|
+
|
|
105
|
+
# A command queued for after the running turn (command_queued with
|
|
106
|
+
# waits): its line now (another client's) and a note; its command_ran
|
|
107
|
+
# has no line again (queued: true).
|
|
108
|
+
def command_waits(event)
|
|
109
|
+
@queued_ids << event[:command_id]
|
|
110
|
+
if own?(event[:client_id])
|
|
111
|
+
first = event[:command_id] == @first_command_id
|
|
112
|
+
return @screen.commit(first ? first_command_queued : QUEUED_NOTE)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@screen.commit("#{prompt_line(event[:client_id], event[:line])} #{QUEUED_NOTE}")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def first_command_queued
|
|
119
|
+
"(#{first_command_flag} queued: applies after the running turn)"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def show_enqueued(event)
|
|
123
|
+
return if own?(event[:client_id])
|
|
124
|
+
|
|
125
|
+
# Another client's prompt shows here, before its turn starts.
|
|
126
|
+
mark_activity
|
|
127
|
+
@shown_enqueued << event[:enqueued_id]
|
|
128
|
+
line = prompt_line(event[:client_id], event[:prompt], **author_of(event[:author]))
|
|
129
|
+
@screen.commit(event[:delivery] == Delivery::QUEUE ? "#{line} #{QUEUED_NOTE}" : line)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# A queued prompt was taken back before it ran (the web's ×, /unqueue
|
|
133
|
+
# in a terminal). One of this run's (wait_at_eof) will have no turn:
|
|
134
|
+
# it ends the wait, as a failed one does. Another client's shown
|
|
135
|
+
# line says so.
|
|
136
|
+
def turn_dequeued(event)
|
|
137
|
+
id = event[:enqueued_id]
|
|
138
|
+
@my_queued.delete(id)
|
|
139
|
+
if @open_ids.delete?(id)
|
|
140
|
+
if own?(event[:client_id])
|
|
141
|
+
@own_empty = true
|
|
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
|
|
147
|
+
elsif @shown_enqueued.delete?(id)
|
|
148
|
+
@screen.commit("(a queued message was removed)")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../client_id"
|
|
4
|
+
require_relative "../../delivery"
|
|
5
|
+
require_relative "../../prompt_history"
|
|
6
|
+
require_relative "../../turn_origin"
|
|
7
|
+
require_relative "../exit_lines"
|
|
8
|
+
require_relative "../image_input"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
class TerminalUI
|
|
12
|
+
class AttachedLoop
|
|
13
|
+
# Bridge sending, first-command launch, and this run's turn ledger.
|
|
14
|
+
# Reads @as_parent, @client, @client_id, @display_pending, @first_command_id,
|
|
15
|
+
# @first_command_line, @first_commands, @first_prompt, @input_ended, @link,
|
|
16
|
+
# @merged_ids, @my_queued, @no_interrupt, @open_ids, @own_empty, @own_failed,
|
|
17
|
+
# @queued_ids, @reader, @running, @screen, @sent_ids, @status, @wait_at_eof;
|
|
18
|
+
# writes @display_pending, @first_command_id, @first_command_line,
|
|
19
|
+
# @first_commands, @first_prompt, @merged_ids, @my_queued, @open_ids,
|
|
20
|
+
# @own_empty, @own_failed, @queued_ids, @sent_ids.
|
|
21
|
+
# Calls the loop helpers #client_id_for, #command_registry, #default_model_name,
|
|
22
|
+
# #detach, #mark_activity, #older_worker_command, #origin_of, #own?, #paint,
|
|
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.
|
|
26
|
+
module Sending
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# The worker runs it and every UI renders its :command_ran, this one
|
|
30
|
+
# too, so nothing waits here.
|
|
31
|
+
def send_command(line)
|
|
32
|
+
reply = @client.post_command(line: line, client_id: client_id_for(line))
|
|
33
|
+
if reply.status == 202
|
|
34
|
+
# Input from a pipe ends once its output is in (#command_ran).
|
|
35
|
+
command_id = reply.json&.fetch("command_id", nil)
|
|
36
|
+
@open_ids << command_id if command_id && @wait_at_eof
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
return @screen.commit(stale_worker("run commands")) if reply.status == 404
|
|
40
|
+
|
|
41
|
+
name = older_worker_command(reply, line)
|
|
42
|
+
return @screen.commit(stale_worker("run #{name}")) if name
|
|
43
|
+
return @screen.commit("could not run the command (#{ExitLines::LATE})") if too_late?(reply)
|
|
44
|
+
|
|
45
|
+
detail = reply.json&.fetch("detail", nil) || reply.json&.fetch("error", nil)
|
|
46
|
+
@screen.commit("could not run the command (#{[reply.status, detail].compact.join(" ")})")
|
|
47
|
+
rescue SystemCallError, IOError => e
|
|
48
|
+
@screen.commit("could not run the command (#{worker_down(e)})")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def command_ran(event)
|
|
52
|
+
@open_ids.delete(event[:command_id])
|
|
53
|
+
@queued_ids.delete(event[:command_id])
|
|
54
|
+
unless own?(event[:client_id]) || event[:anytime] || event[:card] || event[:queued]
|
|
55
|
+
@screen.commit(prompt_line(event[:client_id], event[:line]))
|
|
56
|
+
end
|
|
57
|
+
output = event[:output].to_s
|
|
58
|
+
if output.empty?
|
|
59
|
+
nil
|
|
60
|
+
elsif %w[busy dropped].include?(event[:status]) || PromptHistory.shell_line?(event[:line])
|
|
61
|
+
@screen.commit(output)
|
|
62
|
+
put_back_busy_command(event) if event[:status] == "busy"
|
|
63
|
+
else
|
|
64
|
+
@screen.commit("#{paint("model>", 36)} #{output}")
|
|
65
|
+
end
|
|
66
|
+
prefill_confirm(event)
|
|
67
|
+
# /title from any client: the row's title follows.
|
|
68
|
+
@status.update(title: event[:title]) if Array(event[:changed]).include?("title")
|
|
69
|
+
return unless event[:model_name]
|
|
70
|
+
|
|
71
|
+
# After the output, so the status row changes with the line saying why.
|
|
72
|
+
served = @status[:model] == event[:model_name] ? @status[:served] : nil
|
|
73
|
+
@status.update(model: event[:model_name], default_model: default_model_name, served: served)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Our change that asks first (CacheConfirm) puts its --yes line in the
|
|
77
|
+
# input: Enter applies it. Another client's question is its own.
|
|
78
|
+
def prefill_confirm(event)
|
|
79
|
+
return unless own?(event[:client_id])
|
|
80
|
+
|
|
81
|
+
confirm = event[:confirm]
|
|
82
|
+
line = confirm.is_a?(Hash) ? confirm[:line] || confirm["line"] : nil
|
|
83
|
+
@reader&.prefill(line.to_s) if line
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Our command the worker couldn't run yet goes back into the prompt, for
|
|
87
|
+
# Enter once the turn ends (not the launch's --model: that ends the launch).
|
|
88
|
+
def put_back_busy_command(event)
|
|
89
|
+
return unless own?(event[:client_id]) && event[:command_id] != @first_command_id
|
|
90
|
+
|
|
91
|
+
@screen.commit("(the command is in the input history: ↑)") unless @reader&.prefill(event[:line].to_s)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# What a prompt's 202 says became of it, or nil when there is nothing
|
|
95
|
+
# to say: the dim line both TUIs print for a delivery. A plain line
|
|
96
|
+
# for a running turn goes in at its next step; a cut's outcome comes
|
|
97
|
+
# from the ack (the worker knows whether cutting is on for its model);
|
|
98
|
+
# a queued one waits for the turn's end (the ack says queue).
|
|
99
|
+
# An idle session's prompt simply runs, and a prompt with images runs
|
|
100
|
+
# as its own next turn (never merging mid-turn), so nothing is said;
|
|
101
|
+
# one typed as /cut or /queue says it was sent now (+choice+).
|
|
102
|
+
# @param ack [Hash, nil] the Bridge's 202 body
|
|
103
|
+
# @param delivery [String, nil] what was asked for
|
|
104
|
+
# @return [String, nil]
|
|
105
|
+
def sent_note(ack, delivery: nil, images: [], running: false, choice: nil)
|
|
106
|
+
return Formatting::STEER_CHOICE_IDLE if choice && !running
|
|
107
|
+
return nil unless running && ack.is_a?(Hash)
|
|
108
|
+
# The Bridge acks queue only while a turn ran at enqueue; a /queue
|
|
109
|
+
# it acks next_step found the turn over: it is the next turn.
|
|
110
|
+
return Queueing::QUEUED_NOTE if ack["delivery"] == Delivery::QUEUE
|
|
111
|
+
return Queueing::QUEUE_RUNS_NOW if Delivery.queue?(delivery)
|
|
112
|
+
return nil unless images.empty?
|
|
113
|
+
|
|
114
|
+
return Formatting::NEXT_STEP_NOTE unless delivery == Delivery::CUT
|
|
115
|
+
|
|
116
|
+
steer_cut_note(ack["cut"].to_s.to_sym, ack["cut_after"])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The client id +line+ goes with, as a command (#send_command) or as a
|
|
120
|
+
# message the Bridge may run as one (#send_prompt: /cut and /queue
|
|
121
|
+
# text). A parent agent's /continue yes and /parent-answers go as chi
|
|
122
|
+
# answer's: the worker holds the first to guardrails.parent_answers and
|
|
123
|
+
# never takes the second as a user's loosening. The command is the
|
|
124
|
+
# registry's own match (the worker's), not a pattern of ours.
|
|
125
|
+
def client_id_for(line)
|
|
126
|
+
return @client_id unless @as_parent
|
|
127
|
+
return ClientId::CLI_ANSWER if TurnFlow.continue_yes?(line)
|
|
128
|
+
return ClientId::CLI_ANSWER if command_registry.lookup(line)&.id == :parent_answers
|
|
129
|
+
|
|
130
|
+
@client_id
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param choice [Symbol, nil] :cut or :queue when typed with that word
|
|
134
|
+
def send_prompt(text, delivery: nil, choice: nil)
|
|
135
|
+
persist_recent_history(text)
|
|
136
|
+
images = attach_images(text)
|
|
137
|
+
return own_prompt_failed if images.nil?
|
|
138
|
+
|
|
139
|
+
running = @running
|
|
140
|
+
# Off a live region (no echo seam) the divider comes here; after
|
|
141
|
+
# #gap_above_echo it marks none.
|
|
142
|
+
mark_activity
|
|
143
|
+
options = { prompt: text, client_id: client_id_for(text) }
|
|
144
|
+
options[:no_interrupt] = true if @no_interrupt
|
|
145
|
+
options[:images] = images unless images.empty?
|
|
146
|
+
options[:delivery] = delivery if delivery
|
|
147
|
+
reply = @client.post_turn(**options)
|
|
148
|
+
if reply.status == 202
|
|
149
|
+
enqueued_id = reply.json&.fetch("enqueued_id", nil)
|
|
150
|
+
@sent_ids << enqueued_id if enqueued_id
|
|
151
|
+
@open_ids << enqueued_id if enqueued_id && @wait_at_eof
|
|
152
|
+
@my_queued << enqueued_id if enqueued_id && reply.json&.fetch("delivery", nil) == Delivery::QUEUE
|
|
153
|
+
note = sent_note(reply.json, delivery: delivery, images: images, running: running, choice: choice)
|
|
154
|
+
@screen.commit(note) if note
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
own_prompt_failed
|
|
159
|
+
|
|
160
|
+
detail = reply.json&.fetch("error", nil)
|
|
161
|
+
# Read after its deadline and dropped (BridgeClient::DEADLINE_SHARE).
|
|
162
|
+
return @screen.commit("could not send the prompt (#{ExitLines::LATE})") if detail == "deadline_passed"
|
|
163
|
+
# A worker from 0.51 to 0.63 refuses a queue (nothing was sent).
|
|
164
|
+
return @screen.commit(queue_needs_newer_worker) if detail == "delivery_unavailable"
|
|
165
|
+
|
|
166
|
+
@screen.commit("could not send the prompt (#{[reply.status, detail].compact.join(" ")})")
|
|
167
|
+
rescue SystemCallError, IOError => e
|
|
168
|
+
own_prompt_failed
|
|
169
|
+
@screen.commit("could not send the prompt (#{worker_down(e)})")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Whether the input's end must wait: the -p prompt isn't sent yet, a
|
|
173
|
+
# prompt this run sent hasn't had its turn, or its turn's after_turn
|
|
174
|
+
# hooks still run (their notices, source-links' sources:, come next).
|
|
175
|
+
def own_turns_pending? = !@first_prompt.to_s.strip.empty? || !@open_ids.empty? || @display_pending == true
|
|
176
|
+
|
|
177
|
+
# A turn ended: this run's prompt (and ours merged into it) had its turn.
|
|
178
|
+
def own_turn_ended(event)
|
|
179
|
+
return unless @wait_at_eof
|
|
180
|
+
|
|
181
|
+
id = origin_of(event[:origin]).enqueued_id
|
|
182
|
+
ours = @open_ids.include?(id) || !@merged_ids.empty?
|
|
183
|
+
@own_failed = true if ours && event[:type] == :turn_failed
|
|
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
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Ours merged into the running turn: they end with it.
|
|
193
|
+
def merged_own_prompts(event)
|
|
194
|
+
Array(event[:origins]).each do |value|
|
|
195
|
+
origin = origin_of(value)
|
|
196
|
+
id = origin.enqueued_id
|
|
197
|
+
@merged_ids << id if own?(origin.client_id) && @open_ids.include?(id)
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def own_prompt_failed
|
|
203
|
+
@own_failed = true if @wait_at_eof
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The input ended and nothing of ours is left to wait for.
|
|
208
|
+
def end_of_input
|
|
209
|
+
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @input_ended
|
|
210
|
+
return :turn_failed if @own_failed
|
|
211
|
+
|
|
212
|
+
@own_empty ? :empty_answer : :detached
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The prompt's `@path` images, stored where the worker reads them
|
|
216
|
+
# (AttachLink#ingest_images: the session's images/ here) and sent as
|
|
217
|
+
# refs: the Bridge never takes a path. nil (after a line saying why)
|
|
218
|
+
# when one can't be.
|
|
219
|
+
def attach_images(text)
|
|
220
|
+
paths = ImageInput.extract(text)
|
|
221
|
+
return [] if paths.empty?
|
|
222
|
+
|
|
223
|
+
@link.ingest_images(paths.map { |image| image[:path] })
|
|
224
|
+
rescue ImageStore::Error => e
|
|
225
|
+
@screen.commit("could not attach the image: #{e.message}")
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# --model (or --llm-context, --thinking) on a resumed session: run it in its
|
|
230
|
+
# worker before the first prompt goes; its :command_ran decides
|
|
231
|
+
# (#first_command_ran), and sends the next one.
|
|
232
|
+
# @return [Symbol, nil] :failed when the worker didn't take it
|
|
233
|
+
def send_first_command
|
|
234
|
+
line = @first_commands.shift
|
|
235
|
+
@first_command_line = line
|
|
236
|
+
reply = @client.post_command(line: line, client_id: @client_id)
|
|
237
|
+
if reply.status == 202
|
|
238
|
+
@first_command_id = reply.json&.fetch("command_id", nil)
|
|
239
|
+
return nil if @first_command_id
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
why =
|
|
243
|
+
if reply.status == 404 then stale_worker("run commands")
|
|
244
|
+
elsif (name = older_worker_command(reply, line)) then stale_worker("run #{name}")
|
|
245
|
+
elsif too_late?(reply) then ExitLines::LATE
|
|
246
|
+
else "the worker answered #{reply.status}"
|
|
247
|
+
end
|
|
248
|
+
@screen.commit("#{first_command_failed}: #{why}")
|
|
249
|
+
:failed
|
|
250
|
+
rescue SystemCallError, IOError => e
|
|
251
|
+
@screen.commit("#{first_command_failed}: #{worker_down(e)}")
|
|
252
|
+
:failed
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# @return [Symbol, nil] :failed unless the command went through
|
|
256
|
+
def first_command_ran(event)
|
|
257
|
+
@first_command_id = nil
|
|
258
|
+
if event[:status] == "ok"
|
|
259
|
+
return send_first_command unless @first_commands.empty?
|
|
260
|
+
|
|
261
|
+
send_first_prompt
|
|
262
|
+
return nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
@screen.commit("#{first_command_failed}: #{event[:output]}")
|
|
266
|
+
:failed
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def first_command_failed
|
|
270
|
+
flag = first_command_flag
|
|
271
|
+
flag == "--model" ? "could not switch to the --model" : "could not set the #{flag}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The launch flag the first command came from: "/model X" is --model,
|
|
275
|
+
# "/llm-context …" --llm-context, "/thinking X --yes" --thinking.
|
|
276
|
+
def first_command_flag
|
|
277
|
+
"--#{@first_command_line.to_s[%r{\A/([\w-]+)}, 1]}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# After the join, so it lands below what the session already had. Our
|
|
281
|
+
# own prompts are never echoed from events (a typed one stays on screen),
|
|
282
|
+
# so it is shown here as if typed.
|
|
283
|
+
def send_first_prompt
|
|
284
|
+
text = @first_prompt.to_s.strip
|
|
285
|
+
@first_prompt = nil
|
|
286
|
+
return if text.empty?
|
|
287
|
+
|
|
288
|
+
@screen.commit("#{paint(PROMPT, 92)}#{text}")
|
|
289
|
+
# As if typed: a session command runs as the command; a typo of one
|
|
290
|
+
# gets the hint, not a turn.
|
|
291
|
+
if command_registry.command?(text)
|
|
292
|
+
send_command(text)
|
|
293
|
+
elsif (hint = command_registry.unknown_command_hint(text))
|
|
294
|
+
@screen.commit(hint)
|
|
295
|
+
else
|
|
296
|
+
send_prompt(text)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|