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
|
@@ -86,11 +86,16 @@ module Samagotchi
|
|
|
86
86
|
state: state_of(session, row, reply), waiting: row[:waiting], live: !!row[:live],
|
|
87
87
|
delegate: session.delegate?, cwd: session.working_directory, branch: GitHead.branch(session.working_directory),
|
|
88
88
|
last_reply: reply && first_line(session.id, reply, state_dir: state_dir),
|
|
89
|
-
last_reply_at: reply && reply_time(reply), reported: !reply.nil? && cursor
|
|
89
|
+
last_reply_at: reply && reply_time(reply), reported: !reply.nil? && reported?(session, cursor, reply),
|
|
90
90
|
updated_at: session.updated_at, archived: !!row[:archived])
|
|
91
91
|
end
|
|
92
92
|
private_class_method :child
|
|
93
93
|
|
|
94
|
+
# A reply the parent was given; a Verify fresh child's always counts
|
|
95
|
+
# (its verdict goes to the parent's card, never a report).
|
|
96
|
+
def reported?(session, cursor, reply) = !session.verify_fresh.nil? || cursor.reply_file == reply
|
|
97
|
+
private_class_method :reported?
|
|
98
|
+
|
|
94
99
|
def state_of(session, row, reply)
|
|
95
100
|
return "waiting" if row[:waiting]
|
|
96
101
|
return "running" if Tools::Delegate.running?(row)
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -15,6 +15,8 @@ require_relative "steer"
|
|
|
15
15
|
require_relative "turn_note"
|
|
16
16
|
require_relative "turn_endings"
|
|
17
17
|
require_relative "turn_origin"
|
|
18
|
+
require_relative "verify_anchor"
|
|
19
|
+
require_relative "verdict"
|
|
18
20
|
require_relative "model_profile"
|
|
19
21
|
require_relative "effective_model"
|
|
20
22
|
require_relative "session_setting"
|
|
@@ -901,10 +903,12 @@ module Samagotchi
|
|
|
901
903
|
|
|
902
904
|
# Event types #announce accepts: facts about the session's input queue,
|
|
903
905
|
# a failed turn's prompt handed back and the continue offer, which live
|
|
904
|
-
# UIs need in the event log, emitted outside any turn's stream
|
|
906
|
+
# UIs need in the event log, emitted outside any turn's stream; a
|
|
907
|
+
# Verify fresh child's verdict landing on its card (:verdict_recorded).
|
|
905
908
|
ANNOUNCEABLE_EVENTS = %i[turn_enqueued turn_dequeued input_merged prompt_restored continue_offered continue_resolved
|
|
906
909
|
command_queued command_ran context_added card hook_notice guardrail_warning
|
|
907
|
-
plugin_init_started plugin_init_finished working_set_updated participants_changed
|
|
910
|
+
plugin_init_started plugin_init_finished working_set_updated participants_changed
|
|
911
|
+
verdict_recorded].freeze
|
|
908
912
|
|
|
909
913
|
# Put a transport-level event into the ordered event log. Unlike turn
|
|
910
914
|
# events it reaches only persistent observers (no turn sink, no memory
|
|
@@ -1589,7 +1593,10 @@ module Samagotchi
|
|
|
1589
1593
|
seconds = turn.elapsed
|
|
1590
1594
|
synchronize_events do
|
|
1591
1595
|
kept, event = yield(seconds)
|
|
1596
|
+
stamp_verify_anchor(kept, turn)
|
|
1592
1597
|
replace_session_messages(turn.session, kept) if kept
|
|
1598
|
+
verdict = record_verdict(turn, outcome)
|
|
1599
|
+
event[:verdict] = verdict if verdict
|
|
1593
1600
|
emit_working_set(turn.on_event, turn.session, turn.session.messages)
|
|
1594
1601
|
turn.session.status = Session::STATUS_IDLE
|
|
1595
1602
|
record_last_turn(turn.session, outcome, seconds, turn.origin, event, limit: exhausted ? turn.limit : nil)
|
|
@@ -1602,6 +1609,31 @@ module Samagotchi
|
|
|
1602
1609
|
end
|
|
1603
1610
|
private :end_turn
|
|
1604
1611
|
|
|
1612
|
+
def stamp_verify_anchor(messages, turn)
|
|
1613
|
+
return unless turn.verify && messages.is_a?(Array)
|
|
1614
|
+
|
|
1615
|
+
opening = messages.reverse.find do |message|
|
|
1616
|
+
(message[:role] || message["role"]).to_s == "user" &&
|
|
1617
|
+
(message[:turn_id] || message["turn_id"]).to_s == turn.id
|
|
1618
|
+
end
|
|
1619
|
+
opening[:verify] = turn.verify.to_h if opening
|
|
1620
|
+
end
|
|
1621
|
+
private :stamp_verify_anchor
|
|
1622
|
+
|
|
1623
|
+
def record_verdict(turn, outcome)
|
|
1624
|
+
return unless outcome == "completed" && turn.verify
|
|
1625
|
+
|
|
1626
|
+
answer = AnswerTail.find(turn.session.messages, turn_id: turn.id)
|
|
1627
|
+
parsed = Verdict.parse(answer && (answer[:content] || answer["content"]))
|
|
1628
|
+
return unless parsed
|
|
1629
|
+
|
|
1630
|
+
record = { anchor: turn.verify.to_h, verdict: parsed.verdict, reason: parsed.reason,
|
|
1631
|
+
by: turn.origin.author, check_turn_id: turn.id, at: Time.now.iso8601(3) }
|
|
1632
|
+
turn.session.verdicts = turn.session.verdicts + [record]
|
|
1633
|
+
record
|
|
1634
|
+
end
|
|
1635
|
+
private :record_verdict
|
|
1636
|
+
|
|
1605
1637
|
def save_quietly(session)
|
|
1606
1638
|
session.save(state_dir: session_state_dir)
|
|
1607
1639
|
rescue StandardError => e
|
|
@@ -2011,7 +2043,7 @@ module Samagotchi
|
|
|
2011
2043
|
# +wall+: the wall time it started (MessageTime.now, once): the
|
|
2012
2044
|
# :turn_started event's at and its user message's (and reminders') stamp.
|
|
2013
2045
|
Turn = Struct.new(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
|
|
2014
|
-
:settings, :id, :limit, :resumed, :resume_from, :wall) do
|
|
2046
|
+
:settings, :id, :limit, :resumed, :resume_from, :wall, :verify) do
|
|
2015
2047
|
def resumed? = resumed ? true : false
|
|
2016
2048
|
|
|
2017
2049
|
# The turn's boundary events carry the origin only when it names a
|
|
@@ -2083,9 +2115,9 @@ module Samagotchi
|
|
|
2083
2115
|
# re-raises; a provider error adds error_kind:, retryable:, host: and a
|
|
2084
2116
|
# one-line summary:.
|
|
2085
2117
|
def run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, max_tool_output_chars: nil, pending_input: nil, continue: false,
|
|
2086
|
-
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil)
|
|
2118
|
+
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil, verify: nil)
|
|
2087
2119
|
turn = begin_turn(session, prompt, on_event: on_event, cancel_controller: cancel_controller, origin: origin,
|
|
2088
|
-
continue: continue, id: id)
|
|
2120
|
+
continue: continue, id: id, verify: verify)
|
|
2089
2121
|
if continue
|
|
2090
2122
|
turn.resumed = resumed
|
|
2091
2123
|
turn.resume_from = resume_from
|
|
@@ -2133,8 +2165,11 @@ module Samagotchi
|
|
|
2133
2165
|
# The turn's state before anything can fail it: the session running,
|
|
2134
2166
|
# the turn flag, its cancel controller and sink. The cross-thread state
|
|
2135
2167
|
# is set here and cleared in #release_turn only.
|
|
2136
|
-
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil)
|
|
2168
|
+
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil)
|
|
2137
2169
|
origin = TurnOrigin.of(origin)
|
|
2170
|
+
# A participant's Verify fresh child: every turn is theirs (their
|
|
2171
|
+
# author, boundary and cap), whoever started it.
|
|
2172
|
+
origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
2138
2173
|
# Track the active session for recap and status snapshot.
|
|
2139
2174
|
@session = session
|
|
2140
2175
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -2157,7 +2192,7 @@ module Samagotchi
|
|
|
2157
2192
|
|
|
2158
2193
|
Turn.new(session: session, prompt: continue ? nil : prompt, continue: continue, on_event: on_event,
|
|
2159
2194
|
controller: effective_controller, origin: origin, started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
2160
|
-
id: id || SecureRandom.uuid, wall: MessageTime.now)
|
|
2195
|
+
id: id || SecureRandom.uuid, wall: MessageTime.now, verify: VerifyAnchor.from(verify))
|
|
2161
2196
|
end
|
|
2162
2197
|
private :begin_turn
|
|
2163
2198
|
|
|
@@ -2360,6 +2395,7 @@ module Samagotchi
|
|
|
2360
2395
|
unless turn.continue
|
|
2361
2396
|
user_message = { role: "user", content: turn.prompt, turn_id: turn.id, **author_fields(turn.origin.author),
|
|
2362
2397
|
**participant_mode_fields(turn.origin.author), MessageTime::KEY => turn.at }
|
|
2398
|
+
user_message[:verify] = turn.verify.to_h if turn.verify
|
|
2363
2399
|
user_message[:images] = image_refs unless image_refs.empty?
|
|
2364
2400
|
turn.messages << user_message
|
|
2365
2401
|
session.last_prompt = turn.prompt
|
|
@@ -109,9 +109,18 @@ module Samagotchi
|
|
|
109
109
|
@git = Guardrails::GitInfo.new
|
|
110
110
|
author = turn_author
|
|
111
111
|
@turn_entry = author && @participant.call(author)
|
|
112
|
-
@turn_mode = author &&
|
|
112
|
+
@turn_mode = author && participant_turn_mode(@session_lookup.call, @turn_entry)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# A participant's turn's mode: in a Verify fresh child the one saved
|
|
116
|
+
# on it at spawn (VerifyFreshOrigin#turn_mode: :review unless saved
|
|
117
|
+
# :pair), never looked up again; elsewhere ParticipantMode.for_turn.
|
|
118
|
+
def participant_turn_mode(session, entry)
|
|
119
|
+
fresh = session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
120
|
+
fresh ? fresh.turn_mode : ParticipantMode.for_turn(session, entry)
|
|
121
|
+
end
|
|
122
|
+
private :participant_turn_mode
|
|
123
|
+
|
|
115
124
|
# The running participant turn's mode (ParticipantMode.for_turn: :pair
|
|
116
125
|
# only in a working session for a participant invited to act), decided
|
|
117
126
|
# at its start; nil for the owner's turns. The boundary in #checks, the
|
|
@@ -447,6 +447,7 @@ module Samagotchi
|
|
|
447
447
|
message[:images] = entry[:images] if entry[:images].is_a?(Array) && !entry[:images].empty?
|
|
448
448
|
message[:turn_id] = entry[:turn_id] if entry[:turn_id]
|
|
449
449
|
message[:turn_start] = true if entry[:turn_start] == true
|
|
450
|
+
message[:verify] = entry[:verify] if entry[:verify].is_a?(Hash)
|
|
450
451
|
message[:thinking] = entry[:thinking] if entry[:thinking].is_a?(String) && !entry[:thinking].empty?
|
|
451
452
|
message[:anthropic_content] = entry[:anthropic_content] if entry[:anthropic_content].is_a?(Array)
|
|
452
453
|
ToolResponse::SAVED_KEYS.each { |key| message[key] = entry[key] if entry[key] }
|
|
@@ -119,7 +119,7 @@ module Samagotchi
|
|
|
119
119
|
# How long #stop waits for +stopped+ after the KILL by default.
|
|
120
120
|
KILL_WAIT_SEC = 1.0
|
|
121
121
|
|
|
122
|
-
# TERMs the group, waits up to +grace+ for +stopped+, then KILLs it and
|
|
122
|
+
# TERMs (and CONTs) the group, waits up to +grace+ for +stopped+, then KILLs it and
|
|
123
123
|
# waits up to +kill_wait+ for +stopped+ again (a KILL is delivered, not
|
|
124
124
|
# done, when kill returns).
|
|
125
125
|
#
|
|
@@ -146,6 +146,15 @@ module Samagotchi
|
|
|
146
146
|
extra = walk ? descendant_groups(pgid) : []
|
|
147
147
|
signal(pgid, "TERM", **signal_options)
|
|
148
148
|
extra.each { |group| signal(group, "TERM") }
|
|
149
|
+
# A stopped process (SIGSTOP, Ctrl-Z) holds the TERM pending, its
|
|
150
|
+
# handler unrun, until it is continued: CONT it after the TERM, so it
|
|
151
|
+
# resumes with the TERM already waiting (a CONT first would let it run
|
|
152
|
+
# unsignalled, to be stopped again) and stops within the grace, not by
|
|
153
|
+
# the KILL. A running process ignores the CONT. Always quiet: a group
|
|
154
|
+
# the TERM already ended may be zombies only by now, which macOS
|
|
155
|
+
# refuses a signal with EPERM.
|
|
156
|
+
signal(pgid, "CONT", **signal_options, quiet: QUIET)
|
|
157
|
+
extra.each { |group| signal(group, "CONT") }
|
|
149
158
|
# +stopped+ is asked until it says true once (it may reap: a second
|
|
150
159
|
# wait on a reaped pid raises ECHILD), then only the extra groups.
|
|
151
160
|
done = false
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "atomic_file"
|
|
7
|
+
require_relative "log"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# A restart's hand-off of the session's reminders, which otherwise live only
|
|
11
|
+
# in the worker's Engine: the leaving worker writes `<id>/reminders.json`
|
|
12
|
+
# (Worker#leave_on_request, as it takes a restart) and its successor takes
|
|
13
|
+
# it as it starts (Worker#start): read and deleted before its Engine is
|
|
14
|
+
# built, restored into the Engine's store after. Only an asked
|
|
15
|
+
# restart writes one: a stop, an idle exit, a crash and a fallback restart
|
|
16
|
+
# (a TERM) drop the reminders as before, and a stop or an archive removes a
|
|
17
|
+
# file left behind (SessionManager.stop_session, .archive_session); a
|
|
18
|
+
# restart a stop landed on starts no successor (.run_session_loop). A file is honoured only by the same
|
|
19
|
+
# session, within WINDOW of its writing (a successor starts in seconds), so
|
|
20
|
+
# one no successor read can't bring reminders back a day later.
|
|
21
|
+
module ReminderCarry
|
|
22
|
+
FILE = "reminders.json"
|
|
23
|
+
# Seconds a hand-off stays good after it was written.
|
|
24
|
+
WINDOW = 120
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def path(session_dir) = File.join(session_dir, FILE)
|
|
29
|
+
|
|
30
|
+
# Write +store+'s reminders for the successor; no reminders, no file.
|
|
31
|
+
# @return [Integer] how many were written
|
|
32
|
+
# @raise [SystemCallError] when the write fails (the caller stays up)
|
|
33
|
+
def write(session_dir, session_id:, store:, wall: Time.now)
|
|
34
|
+
entries = store.export(wall: wall)
|
|
35
|
+
if entries.empty?
|
|
36
|
+
remove(session_dir)
|
|
37
|
+
return 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
body = { "session_id" => session_id, "written_at" => wall.getutc.iso8601(3), "reminders" => entries }
|
|
41
|
+
AtomicFile.write(path(session_dir), "#{JSON.generate(body)}\n", perm: 0o600)
|
|
42
|
+
entries.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Read the hand-off and delete it. A file of another session, outside
|
|
46
|
+
# WINDOW or unreadable, or one found as a stopped session starts, is
|
|
47
|
+
# deleted unused (log reminders_carry_dropped; at warn unless stopped).
|
|
48
|
+
# @return [Array<Hash>, nil] the entries for ReminderStore#restore, nil
|
|
49
|
+
# when there is none to restore
|
|
50
|
+
def take(session_dir, session_id:, stopped: false, wall: Time.now)
|
|
51
|
+
file = path(session_dir)
|
|
52
|
+
return nil unless File.exist?(file)
|
|
53
|
+
|
|
54
|
+
data = read(file)
|
|
55
|
+
remove(session_dir)
|
|
56
|
+
why = drop_reason(data, session_id, stopped, wall)
|
|
57
|
+
return data["reminders"] unless why
|
|
58
|
+
|
|
59
|
+
if why == :stopped
|
|
60
|
+
Log.info(:worker, "reminders_carry_dropped", why: why)
|
|
61
|
+
else
|
|
62
|
+
Log.warn(:worker, "reminders_carry_dropped", why: why)
|
|
63
|
+
end
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [true]
|
|
68
|
+
def remove(session_dir)
|
|
69
|
+
FileUtils.rm_f(path(session_dir))
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def read(file)
|
|
74
|
+
data = JSON.parse(File.read(file))
|
|
75
|
+
data.is_a?(Hash) ? data : nil
|
|
76
|
+
rescue JSON::ParserError, SystemCallError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def drop_reason(data, session_id, stopped, wall)
|
|
81
|
+
return :unreadable unless data
|
|
82
|
+
return :stopped if stopped
|
|
83
|
+
return :other_session unless data["session_id"] == session_id
|
|
84
|
+
|
|
85
|
+
written = Time.iso8601(data["written_at"].to_s)
|
|
86
|
+
:stale if (wall - written).abs > WINDOW
|
|
87
|
+
rescue ArgumentError
|
|
88
|
+
:stale
|
|
89
|
+
end
|
|
90
|
+
private_class_method :read, :drop_reason
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "monitor"
|
|
4
4
|
require "securerandom"
|
|
5
|
+
require "time"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
7
8
|
# Thread-safe store for periodic reminders registered by the agent.
|
|
@@ -122,10 +123,58 @@ module Samagotchi
|
|
|
122
123
|
end
|
|
123
124
|
end
|
|
124
125
|
|
|
126
|
+
# The reminders as a restart hands them to the next worker (ReminderCarry):
|
|
127
|
+
# each one's id, name, description and interval, and its next fire as
|
|
128
|
+
# wall-clock ISO time, since the monotonic clock doesn't cross processes.
|
|
129
|
+
# @return [Array<Hash{String => Object}>]
|
|
130
|
+
def export(wall: Time.now, mono: Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
131
|
+
@mutex.synchronize do
|
|
132
|
+
@reminders.each_value.map do |r|
|
|
133
|
+
{ "id" => r[:id], "name" => r[:name], "description" => r[:description],
|
|
134
|
+
"interval_minutes" => r[:interval_minutes],
|
|
135
|
+
"next_fire_at" => (wall + (r[:next_fire_at] - mono)).utc.iso8601(3) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Take back what #export wrote in another process. A reminder keeps its
|
|
141
|
+
# id and schedule: one overdue is due now (once, no catch-up), one further
|
|
142
|
+
# ahead than its interval (a clock that jumped) is due one interval from
|
|
143
|
+
# now. An entry #register would refuse, or with no time, is skipped.
|
|
144
|
+
# @param entries [Array<Hash>] #export's, parsed from JSON
|
|
145
|
+
# @return [Array<String>] the names restored
|
|
146
|
+
def restore(entries, wall: Time.now, mono: Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
147
|
+
return [] unless entries.is_a?(Array)
|
|
148
|
+
|
|
149
|
+
restored = entries.filter_map { |entry| restored_entry(entry, wall, mono) }
|
|
150
|
+
@mutex.synchronize { restored.each { |r| @reminders[r[:name]] = r } }
|
|
151
|
+
restored.map { |r| r[:name] }
|
|
152
|
+
end
|
|
153
|
+
|
|
125
154
|
# Clear all reminders.
|
|
126
155
|
# Called when the session ends or the engine is reset.
|
|
127
156
|
def clear_all
|
|
128
157
|
@mutex.synchronize { @reminders.clear }
|
|
129
158
|
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def restored_entry(entry, wall, mono)
|
|
163
|
+
return nil unless entry.is_a?(Hash)
|
|
164
|
+
|
|
165
|
+
name = entry["name"].to_s.strip
|
|
166
|
+
description = entry["description"].to_s.strip
|
|
167
|
+
interval = Integer(entry["interval_minutes"], exception: false)
|
|
168
|
+
return nil if name.empty? || description.empty? || interval.nil? || !(1..1440).cover?(interval)
|
|
169
|
+
|
|
170
|
+
next_wall = Time.iso8601(entry["next_fire_at"].to_s)
|
|
171
|
+
period = interval * 60
|
|
172
|
+
next_fire = mono + (next_wall - wall).clamp(0, period)
|
|
173
|
+
id = entry["id"].is_a?(String) && !entry["id"].empty? ? entry["id"] : SecureRandom.hex(6)
|
|
174
|
+
{ id: id, name: name, description: description, interval_minutes: interval,
|
|
175
|
+
last_fire_at: next_fire - period, next_fire_at: next_fire }
|
|
176
|
+
rescue ArgumentError
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
130
179
|
end
|
|
131
180
|
end
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -20,6 +20,10 @@ require_relative "project_scope"
|
|
|
20
20
|
require_relative "guardrails/parent_levels"
|
|
21
21
|
|
|
22
22
|
module Samagotchi
|
|
23
|
+
# Loaded on first use (a Verify fresh child): keeps the bundle test kit's
|
|
24
|
+
# load budget (spec/samagotchi/testing/sandbox_spec.rb).
|
|
25
|
+
autoload :VerifyFreshOrigin, File.expand_path("verify_fresh_origin", __dir__)
|
|
26
|
+
|
|
23
27
|
# archive_store requires this file.
|
|
24
28
|
autoload :ArchiveStore, File.expand_path("archive_store", __dir__)
|
|
25
29
|
|
|
@@ -134,6 +138,14 @@ module Samagotchi
|
|
|
134
138
|
@last_turn = LastTurn.from_file(value)
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
# Verdict cards recorded by completed Verify turns, in append order.
|
|
142
|
+
# A missing field stays missing until the first verdict is recorded.
|
|
143
|
+
def verdicts = @verdicts || []
|
|
144
|
+
|
|
145
|
+
def verdicts=(values)
|
|
146
|
+
@verdicts = values.nil? ? nil : Array(values)
|
|
147
|
+
end
|
|
148
|
+
|
|
137
149
|
# The mark of a turn a worker checkpointed and never finished
|
|
138
150
|
# (InterruptedTurn), as loaded; the next worker settles it and sets it
|
|
139
151
|
# to nil. While a turn runs, #turn_checkpoint's marker is saved instead.
|
|
@@ -280,6 +292,20 @@ module Samagotchi
|
|
|
280
292
|
# Whether this is a review session (chi review, the web's Start review).
|
|
281
293
|
def review? = !@review.nil?
|
|
282
294
|
|
|
295
|
+
# A Verify fresh child's requester, rules and claim (VerifyFreshOrigin),
|
|
296
|
+
# nil for any other session. Set once, at spawn (VerifyFresh.start).
|
|
297
|
+
attr_reader :verify_fresh
|
|
298
|
+
|
|
299
|
+
# @param value [VerifyFreshOrigin, Hash, nil] the session file's form too
|
|
300
|
+
def verify_fresh=(value)
|
|
301
|
+
@verify_fresh = value.nil? ? nil : VerifyFreshOrigin.of(value)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# The session whose participants.json names this session's
|
|
305
|
+
# participants: a Verify fresh child's parent (its requester's grant is
|
|
306
|
+
# the parent's), else the session itself.
|
|
307
|
+
def participants_source_id = @verify_fresh && @parent_id ? @parent_id : @id
|
|
308
|
+
|
|
283
309
|
# How many tool outputs its history holds: its tool messages
|
|
284
310
|
# (tool_response, a chat loop's tool; a native step's one message holds
|
|
285
311
|
# every call's output and counts once).
|
|
@@ -333,7 +359,7 @@ module Samagotchi
|
|
|
333
359
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
334
360
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
335
361
|
toolkit: nil, toolkit_origin: "chosen", settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil,
|
|
336
|
-
working_set_pins: [], review: nil)
|
|
362
|
+
working_set_pins: [], review: nil, verdicts: nil, verify_fresh: nil)
|
|
337
363
|
@id = id
|
|
338
364
|
@metadata_version = metadata_version
|
|
339
365
|
@mode = mode
|
|
@@ -370,6 +396,8 @@ module Samagotchi
|
|
|
370
396
|
self.parent_answers = parent_answers
|
|
371
397
|
self.working_set_pins = working_set_pins
|
|
372
398
|
self.review = review
|
|
399
|
+
self.verdicts = verdicts
|
|
400
|
+
self.verify_fresh = verify_fresh
|
|
373
401
|
self.interrupted_turn = interrupted_turn
|
|
374
402
|
@turn_checkpoint = nil
|
|
375
403
|
@archived = false
|
|
@@ -539,6 +567,7 @@ module Samagotchi
|
|
|
539
567
|
"parent_id" => nil,
|
|
540
568
|
"scratch" => false,
|
|
541
569
|
"last_turn" => nil,
|
|
570
|
+
"verdicts" => nil,
|
|
542
571
|
"delegate" => false,
|
|
543
572
|
"llm_context" => nil,
|
|
544
573
|
"continues" => nil,
|
|
@@ -553,12 +582,13 @@ module Samagotchi
|
|
|
553
582
|
"parent_answers" => nil,
|
|
554
583
|
"working_set_pins" => [],
|
|
555
584
|
"review" => nil,
|
|
556
|
-
"first_turn_id" => nil
|
|
585
|
+
"first_turn_id" => nil,
|
|
586
|
+
"verify_fresh" => nil
|
|
557
587
|
}.freeze
|
|
558
588
|
|
|
559
589
|
# FIELDS a session file has only when set: a session without them saves
|
|
560
590
|
# as it did before they existed.
|
|
561
|
-
OPTIONAL_FIELDS = %w[review first_turn_id].freeze
|
|
591
|
+
OPTIONAL_FIELDS = %w[review first_turn_id verdicts verify_fresh].freeze
|
|
562
592
|
|
|
563
593
|
# A session from a parsed session file. +messages+ false leaves the
|
|
564
594
|
# conversation out (the lists' lightweight summaries).
|
|
@@ -712,6 +742,13 @@ module Samagotchi
|
|
|
712
742
|
when "last_turn" then @last_turn&.to_file
|
|
713
743
|
when "working_set_pins" then @working_set_pins.map(&:to_file)
|
|
714
744
|
when "review" then @review&.to_file
|
|
745
|
+
when "verify_fresh" then @verify_fresh&.to_file
|
|
746
|
+
when "verdicts"
|
|
747
|
+
@verdicts&.map do |record|
|
|
748
|
+
scrub_utf8(record.to_h do |key, entry|
|
|
749
|
+
[key.to_s, entry.is_a?(Hash) ? stringify_message_keys(entry) : entry]
|
|
750
|
+
end)
|
|
751
|
+
end
|
|
715
752
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names", "appended_prompt_files"
|
|
716
753
|
Array(instance_variable_get(:"@#{key}"))
|
|
717
754
|
else instance_variable_get(:"@#{key}")
|
|
@@ -12,6 +12,10 @@ require_relative "steer"
|
|
|
12
12
|
require_relative "turn_origin"
|
|
13
13
|
|
|
14
14
|
module Samagotchi
|
|
15
|
+
# Loaded on first use (a Verify turn's input): keeps the bundle test kit's
|
|
16
|
+
# load budget (spec/samagotchi/testing/sandbox_spec.rb).
|
|
17
|
+
autoload :VerifyAnchor, File.expand_path("verify_anchor", __dir__)
|
|
18
|
+
|
|
15
19
|
# A session directory's file inbox and outbox (see SessionManager for the
|
|
16
20
|
# layout): input/ holds one JSON file per queued turn, notes/ the context
|
|
17
21
|
# notes a worker adds between turns, output/ one text file per reply,
|
|
@@ -53,7 +57,7 @@ module Samagotchi
|
|
|
53
57
|
# @param now [Time] the time the file is named by (a spec's clock)
|
|
54
58
|
# @return [String, false] the input file's path, or false
|
|
55
59
|
def self.write_input(session_dir, prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [],
|
|
56
|
-
delivery: nil, author: nil, now: Time.now)
|
|
60
|
+
delivery: nil, author: nil, verify: nil, now: Time.now)
|
|
57
61
|
images = Array(images)
|
|
58
62
|
input_dir = File.join(session_dir, INPUT_DIR)
|
|
59
63
|
FileUtils.mkdir_p(input_dir)
|
|
@@ -63,7 +67,7 @@ module Samagotchi
|
|
|
63
67
|
"no_interrupt" => (no_interrupt ? true : nil),
|
|
64
68
|
"delivery" => (Delivery.queue?(delivery) ? Delivery::QUEUE : nil),
|
|
65
69
|
"images" => (images.empty? ? nil : images.map { |image| image.transform_keys(&:to_s) }),
|
|
66
|
-
"author" => author }.compact
|
|
70
|
+
"author" => author, "verify" => VerifyAnchor.from(verify)&.to_file }.compact
|
|
67
71
|
AtomicFile.write(path, JSON.generate(record))
|
|
68
72
|
path
|
|
69
73
|
rescue StandardError
|
|
@@ -228,9 +232,9 @@ module Samagotchi
|
|
|
228
232
|
# (+no_interrupt+, --no-interrupt), its +images+ ({file:, name:} refs),
|
|
229
233
|
# and its +delivery+ ("queue" or nil; a file without the key is one an
|
|
230
234
|
# older chi wrote, so nil).
|
|
231
|
-
Input = Data.define(:prompt, :origin, :no_interrupt, :images, :delivery) do
|
|
232
|
-
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
233
|
-
super(prompt:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:)
|
|
235
|
+
Input = Data.define(:prompt, :origin, :no_interrupt, :images, :delivery, :verify) do
|
|
236
|
+
def initialize(prompt:, origin: nil, no_interrupt: false, images: [], delivery: nil, verify: nil)
|
|
237
|
+
super(prompt:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:, verify: verify && VerifyAnchor.from(verify))
|
|
234
238
|
end
|
|
235
239
|
end
|
|
236
240
|
# A file that doesn't parse, or isn't a JSON object: no prompt.
|
|
@@ -246,7 +250,7 @@ module Samagotchi
|
|
|
246
250
|
origin = TurnOrigin.from(data.slice("client_id", "enqueued_id", "author"))
|
|
247
251
|
images = Array(data["images"]).select { |image| image.is_a?(Hash) }.map { |image| image.transform_keys(&:to_sym) }
|
|
248
252
|
Input.new(prompt: data["prompt"].to_s, origin: origin, no_interrupt: data["no_interrupt"] == true,
|
|
249
|
-
images: images, delivery: Delivery.queue?(data["delivery"]) ? Delivery::QUEUE : nil)
|
|
253
|
+
images: images, delivery: Delivery.queue?(data["delivery"]) ? Delivery::QUEUE : nil, verify: data["verify"])
|
|
250
254
|
rescue JSON::ParserError
|
|
251
255
|
UNREADABLE_INPUT
|
|
252
256
|
end
|
|
@@ -278,9 +282,11 @@ module Samagotchi
|
|
|
278
282
|
true
|
|
279
283
|
end
|
|
280
284
|
|
|
281
|
-
# A parsed input file's #waits_for_turn_end?.
|
|
285
|
+
# A parsed input file's #waits_for_turn_end?. A Verify turn waits too:
|
|
286
|
+
# merged into a running turn as a steer, its anchor would be lost.
|
|
282
287
|
def self.turn_end_data?(data)
|
|
283
|
-
Array(data["images"]).any? || Delivery.queue?(data["delivery"]) || Steer.participant_author?(data["author"])
|
|
288
|
+
Array(data["images"]).any? || Delivery.queue?(data["delivery"]) || Steer.participant_author?(data["author"]) ||
|
|
289
|
+
data["verify"].is_a?(Hash)
|
|
284
290
|
end
|
|
285
291
|
|
|
286
292
|
# How the unclaimed input file carrying +enqueued_id+ waits: :turn_end
|
|
@@ -36,6 +36,7 @@ require_relative "model_profile"
|
|
|
36
36
|
require_relative "installed_gem"
|
|
37
37
|
require_relative "installed_versions"
|
|
38
38
|
require_relative "recap_store"
|
|
39
|
+
require_relative "reminder_carry"
|
|
39
40
|
require_relative "archive_store"
|
|
40
41
|
require_relative "continue_offer"
|
|
41
42
|
require_relative "image_store"
|
|
@@ -163,6 +164,8 @@ module Samagotchi
|
|
|
163
164
|
# turn sees it
|
|
164
165
|
# @param review [SessionReview, nil] a review session's (ReviewStart; a
|
|
165
166
|
# continue keeps its previous link's)
|
|
167
|
+
# @param verify_fresh [VerifyFreshOrigin, nil] a Verify fresh child's
|
|
168
|
+
# requester, rules and claim (VerifyFresh.start)
|
|
166
169
|
# @param before_spawn [#call, nil] (session) runs once the session and
|
|
167
170
|
# its note are saved, before its worker spawns
|
|
168
171
|
# @return [Session] with #seed_images_dropped: refs whose file was gone,
|
|
@@ -170,7 +173,7 @@ module Samagotchi
|
|
|
170
173
|
def self.spawn_session(prompt:, mode: "assist", working_directory: nil, model_name: nil, state_dir: nil,
|
|
171
174
|
memories: [], muted_memories: [], parent_id: nil, messages: [], images_from: nil,
|
|
172
175
|
preview: nil, title: nil, delegate: false, setup: SessionSetup.new, continues: nil, note: nil,
|
|
173
|
-
note_source: "chi", before_spawn: nil, review: nil)
|
|
176
|
+
note_source: "chi", before_spawn: nil, review: nil, verify_fresh: nil)
|
|
174
177
|
sd = state_dir || Session.default_state_dir
|
|
175
178
|
# The resolved ref is stored (a resumed session keeps its model when an
|
|
176
179
|
# alias is retargeted), with the name as typed beside it. A wrong host
|
|
@@ -197,6 +200,8 @@ module Samagotchi
|
|
|
197
200
|
)
|
|
198
201
|
session.toolkit = Toolkit.configured(delegate: delegate) if session.toolkit.nil?
|
|
199
202
|
session.toolkit_origin = setup.toolkit_origin || "default"
|
|
203
|
+
# Saved before the worker starts: its first turn is already the requester's.
|
|
204
|
+
session.verify_fresh = verify_fresh
|
|
200
205
|
session.model_warning = model_warning
|
|
201
206
|
# With no prompt there is no first turn to run (an attaching UI sends
|
|
202
207
|
# the prompts), so the session starts idle.
|
|
@@ -402,6 +407,9 @@ module Samagotchi
|
|
|
402
407
|
stopped = tree.select { |sid| owners[sid] }
|
|
403
408
|
stopped.each { |sid| stop_session(sid, state_dir: sd, wait: wait) }
|
|
404
409
|
archived, discarded = tree.partition { |sid| ArchiveStore.archive(sid, state_dir: sd) }
|
|
410
|
+
# A restart's reminders hand-off no successor took (the stop above
|
|
411
|
+
# removes a live one's): archived sessions keep no reminders.
|
|
412
|
+
archived.each { |sid| ReminderCarry.remove(Session.session_dir(sid, state_dir: sd)) }
|
|
405
413
|
# D8: an archive ends the review (unarchiving doesn't restore it).
|
|
406
414
|
Participants.end_sessions(archived, by: "archive", state_dir: sd)
|
|
407
415
|
{ id: id, archived: archived, stopped: stopped, discarded: discarded }
|
|
@@ -663,7 +671,8 @@ module Samagotchi
|
|
|
663
671
|
input_queued: "a prompt is queued",
|
|
664
672
|
continue_offered: "the step-limit question waits for an answer",
|
|
665
673
|
question_pending: "a question or approval waits for an answer",
|
|
666
|
-
|
|
674
|
+
# Only a worker from before ReminderCarry still holds a restart for its reminders.
|
|
675
|
+
reminders: "it has reminders, which its worker's older chi can't carry over a restart",
|
|
667
676
|
command_running: "a /btw (or another command) is still running",
|
|
668
677
|
relays_open: "a delegate's approval relayed here is open or just answered",
|
|
669
678
|
background_tasks: "background tasks it started are still running"
|
|
@@ -822,7 +831,7 @@ module Samagotchi
|
|
|
822
831
|
end
|
|
823
832
|
private_class_method :running_created_task?
|
|
824
833
|
|
|
825
|
-
# Stop a session by sending TERM to its process.
|
|
834
|
+
# Stop a session by sending TERM (then CONT, for a stopped one) to its process.
|
|
826
835
|
# With +wait+ (seconds), also wait for the owner to let go of the session,
|
|
827
836
|
# so a resume right after spawns a fresh worker instead of finding the
|
|
828
837
|
# dying one.
|
|
@@ -838,15 +847,29 @@ module Samagotchi
|
|
|
838
847
|
cancel_running_turn(session_id, state_dir: sd)
|
|
839
848
|
# Mark first: a worker that has not taken the lock yet sees it and exits.
|
|
840
849
|
Session.mark_stopped(session_id, state_dir: sd)
|
|
850
|
+
# A stop drops the reminders: a restart's hand-off no successor took
|
|
851
|
+
# yet must not bring them back on the next resume.
|
|
852
|
+
ReminderCarry.remove(Session.session_dir(session_id, state_dir: sd))
|
|
841
853
|
pid = owner&.pid.to_i
|
|
842
854
|
begin
|
|
843
855
|
Process.kill("TERM", pid) if pid && pid > 0
|
|
844
856
|
rescue Errno::ESRCH
|
|
845
857
|
nil # already exited
|
|
846
858
|
end
|
|
859
|
+
continue_stopped(pid)
|
|
847
860
|
wait_for_owner_release(session_id, timeout: wait, state_dir: sd) if wait
|
|
848
861
|
end
|
|
849
862
|
|
|
863
|
+
# A stopped worker (SIGSTOP, Ctrl-Z) runs its TERM handler only once
|
|
864
|
+
# continued: CONT it after the TERM (as ProcessGroup.stop does), so it
|
|
865
|
+
# leaves now rather than outliving the stop's wait. Best effort: one the
|
|
866
|
+
# TERM ended may be gone already.
|
|
867
|
+
private_class_method def self.continue_stopped(pid)
|
|
868
|
+
Process.kill("CONT", pid) if pid && pid > 0
|
|
869
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
870
|
+
nil
|
|
871
|
+
end
|
|
872
|
+
|
|
850
873
|
STOP_CANCEL_WAIT = 3.0
|
|
851
874
|
# How long a stop from the CLI (chi sessions stop, delete, a continue's
|
|
852
875
|
# orphan) waits for the worker to let go of the session, here or
|
|
@@ -1137,8 +1160,13 @@ module Samagotchi
|
|
|
1137
1160
|
# A restart starts its successor here, on the newest chi installed
|
|
1138
1161
|
# (worker_command), once this one's lock is free.
|
|
1139
1162
|
# Delegate reports that rang while it left wake one too.
|
|
1140
|
-
if result == :restart
|
|
1141
|
-
|
|
1163
|
+
if result == :restart && Session.stopped_marker?(session_id, state_dir: sd)
|
|
1164
|
+
# A stop landed as it left: no successor (resume_session would clear
|
|
1165
|
+
# the stop), and the reminders go with it.
|
|
1166
|
+
ReminderCarry.remove(session_dir)
|
|
1167
|
+
Log.info(:worker, "restart_dropped", why: :stopped)
|
|
1168
|
+
elsif result == :restart ||
|
|
1169
|
+
(%i[idle_exit exit_requested].include?(result) &&
|
|
1142
1170
|
(!SessionInbox.find_new_input_files(session_dir).empty? || worker.wake_due?))
|
|
1143
1171
|
resume_session(session_id, state_dir: sd)
|
|
1144
1172
|
elsif worker.discard?
|
|
@@ -1286,10 +1314,10 @@ module Samagotchi
|
|
|
1286
1314
|
# session's images/
|
|
1287
1315
|
# @return [String, false] the input file's path, or false.
|
|
1288
1316
|
def self.write_turn_input(session_id, prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, state_dir: nil,
|
|
1289
|
-
images: [], delivery: nil, author: nil)
|
|
1317
|
+
images: [], delivery: nil, author: nil, verify: nil)
|
|
1290
1318
|
session_dir = Session.session_dir(session_id, state_dir: state_dir || Session.default_state_dir)
|
|
1291
1319
|
SessionInbox.write_input(session_dir, prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
1292
|
-
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author)
|
|
1320
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author, verify: verify)
|
|
1293
1321
|
end
|
|
1294
1322
|
|
|
1295
1323
|
# How long a turn waits for the Bridge of a worker a resume just spawned.
|
|
@@ -1371,7 +1399,8 @@ module Samagotchi
|
|
|
1371
1399
|
# @raise [OwnedByTUI] a chi REPL owns the session
|
|
1372
1400
|
# @raise [ArgumentError] no such session
|
|
1373
1401
|
def self.deliver_turn(session_id, prompt:, client_id: nil, images: [], delivery: nil, no_interrupt: false,
|
|
1374
|
-
author: nil, pending_limit: nil, state_dir: nil, manager: self, bridge: nil)
|
|
1402
|
+
author: nil, pending_limit: nil, verify: nil, state_dir: nil, manager: self, bridge: nil)
|
|
1403
|
+
verify = VerifyAnchor.from(verify)
|
|
1375
1404
|
session_dir = Session.session_dir(session_id, state_dir: state_dir || Session.default_state_dir)
|
|
1376
1405
|
bridge ||= lambda do
|
|
1377
1406
|
link = WorkerLink.await(session_id, session_dir: session_dir, timeout: TURN_BRIDGE_WAIT)
|
|
@@ -1393,6 +1422,7 @@ module Samagotchi
|
|
|
1393
1422
|
options[:no_interrupt] = true if no_interrupt
|
|
1394
1423
|
options[:author] = author if author
|
|
1395
1424
|
options[:pending_limit] = pending_limit if author && pending_limit
|
|
1425
|
+
options[:verify] = verify if verify
|
|
1396
1426
|
reply = client.post_turn(**options)
|
|
1397
1427
|
ack = reply.json
|
|
1398
1428
|
return DeliverResult.new(status: :accepted, via: :bridge, ack: ack) if reply.status == 202 && ack.is_a?(Hash)
|
|
@@ -1430,6 +1460,7 @@ module Samagotchi
|
|
|
1430
1460
|
input[:delivery] = delivery unless Delivery.next_step?(delivery)
|
|
1431
1461
|
input[:no_interrupt] = true if no_interrupt
|
|
1432
1462
|
input[:author] = author if author
|
|
1463
|
+
input[:verify] = verify if verify
|
|
1433
1464
|
path = participant_queue_lock(session_dir, author && pending_limit) do
|
|
1434
1465
|
# No worker: the participant's waiting files are all their turns,
|
|
1435
1466
|
# counted and written under one lock (the Bridge counts under its).
|