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
|
@@ -13,7 +13,8 @@ module Samagotchi
|
|
|
13
13
|
# the summaries the page's trackAttention keeps (seeded by the
|
|
14
14
|
# subscribe's snapshot, so what was open before never pushes), turns
|
|
15
15
|
# each new attention (Attention.for: a question, an approval, a
|
|
16
|
-
# step-limit continue, a card with actions, a failed or done turn
|
|
16
|
+
# step-limit continue, a card with actions, a failed or done turn, a
|
|
17
|
+
# recorded Verify verdict)
|
|
17
18
|
# into a show, and hands it to its own sender thread through a Queue;
|
|
18
19
|
# one answered anywhere (Attention.closed_keys) only cancels its
|
|
19
20
|
# show's hold.
|
|
@@ -43,7 +44,7 @@ module Samagotchi
|
|
|
43
44
|
HOLD = 3.0
|
|
44
45
|
# Waiting kinds: kept 12 h for an offline phone, delivered at once.
|
|
45
46
|
SHOW_TTL = 12 * 3600
|
|
46
|
-
# A failed or
|
|
47
|
+
# A failed, done or verdict turn: stale soon, no need to wake the phone.
|
|
47
48
|
EVENT_TTL = 600
|
|
48
49
|
# A newly available chi is useful for a day while the phone is away.
|
|
49
50
|
UPDATE_TTL = 24 * 3600
|
|
@@ -280,7 +281,7 @@ module Samagotchi
|
|
|
280
281
|
retries.each { |again| send_retry(again, open) if still_due?(again.show, open) }
|
|
281
282
|
end
|
|
282
283
|
|
|
283
|
-
# A waiting show while its key is open; a failed or
|
|
284
|
+
# A waiting show while its key is open; a failed, done or verdict turn's as
|
|
284
285
|
# is (nothing to answer).
|
|
285
286
|
def still_due?(show, open) = !Attention.waiting?(show.attention.reason) || open.include?(show.attention.key)
|
|
286
287
|
|
|
@@ -196,9 +196,11 @@ module Samagotchi
|
|
|
196
196
|
|
|
197
197
|
# no_interrupt: an attached TUI's --no-interrupt (chi --remote --attach).
|
|
198
198
|
# author: the principal's (nil for the owner), never the body's.
|
|
199
|
+
verify = VerifyAnchor.from(body["verify"])
|
|
199
200
|
result = SessionManager.deliver_turn(id, prompt: prompt.to_s, client_id: client_id, images: images,
|
|
200
201
|
delivery: delivery, no_interrupt: body["no_interrupt"] == true,
|
|
201
202
|
author: principal(req).author,
|
|
203
|
+
verify: verify,
|
|
202
204
|
state_dir: @state_dir, manager: @manager,
|
|
203
205
|
bridge: -> { live_bridge_client(id) })
|
|
204
206
|
case result.status
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../session"
|
|
4
|
+
require_relative "../steer"
|
|
5
|
+
require_relative "../participants"
|
|
6
|
+
require_relative "../verify_fresh"
|
|
4
7
|
require_relative "../recap_store"
|
|
5
8
|
require_relative "../archive_store"
|
|
6
9
|
require_relative "../session_metrics"
|
|
@@ -108,10 +111,31 @@ module Samagotchi
|
|
|
108
111
|
# chi REPL's (only a worker shares it).
|
|
109
112
|
pending_question: session.waiting_question(live: !!owner&.worker?),
|
|
110
113
|
pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
|
|
111
|
-
last_turn: session.last_turn&.to_file
|
|
114
|
+
last_turn: session.last_turn&.to_file,
|
|
115
|
+
# Its Verify verdicts, the fresh ones its worker hasn't taken yet
|
|
116
|
+
# too (VerifyFresh.verdicts_of), and the fresh checks running now
|
|
117
|
+
# ({of, child_id}: the card's "verifying fresh" and its link).
|
|
118
|
+
verdicts: VerifyFresh.verdicts_of(session, session_dir),
|
|
119
|
+
verify_fresh: VerifyFresh.running(session.id, state_dir: File.dirname(session_dir))
|
|
120
|
+
.map { |row| { of: row[:of], child_id: row[:child_id] } },
|
|
121
|
+
last_verdict: last_verdict(session, session_dir)
|
|
112
122
|
}
|
|
113
123
|
end
|
|
114
124
|
|
|
125
|
+
def last_verdict(session, session_dir)
|
|
126
|
+
raw = Array(session.verdicts).last
|
|
127
|
+
return unless raw.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
verdict = raw.to_h { |key, value| [key.to_sym, value] }
|
|
130
|
+
author = verdict[:by]
|
|
131
|
+
return verdict unless Steer.participant_author?(author)
|
|
132
|
+
|
|
133
|
+
participant = Participants.read(session.id, state_dir: File.dirname(session_dir))
|
|
134
|
+
.find { |entry| entry.id == Steer.participant_id(author) }
|
|
135
|
+
participant ? verdict.merge(by_name: participant.name) : verdict
|
|
136
|
+
end
|
|
137
|
+
private_class_method :last_verdict
|
|
138
|
+
|
|
115
139
|
# The budget resolver SessionMetrics.saved_summary takes: a callable,
|
|
116
140
|
# so the session's llm_context budget (config + the registry's index,
|
|
117
141
|
# no network) is resolved only for a session that has a saved context
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../delivery"
|
|
4
|
+
require_relative "../participant_mode"
|
|
5
|
+
require_relative "../participant_messages"
|
|
6
|
+
require_relative "../participants"
|
|
7
|
+
require_relative "../session"
|
|
8
|
+
require_relative "../steer"
|
|
9
|
+
require_relative "../verify_fresh"
|
|
10
|
+
|
|
11
|
+
module Samagotchi
|
|
12
|
+
module Web
|
|
13
|
+
# POST /api/sessions/:id/verdicts/:check_turn_id/fresh, mixed into App:
|
|
14
|
+
# Verify fresh (VerifyFresh) for the owner and for a participant. A
|
|
15
|
+
# participant's is refused as their turn would be (their cap, counted
|
|
16
|
+
# with their fresh children's spend; a worker that predates
|
|
17
|
+
# participants' turns) and audited like one (participants.log,
|
|
18
|
+
# verify_fresh); their child runs under the boundary their grant has
|
|
19
|
+
# here now (ParticipantMode.live_for), saved on the child. See
|
|
20
|
+
# docs/internals/participants.md (Verify fresh).
|
|
21
|
+
module VerifyFreshRoutes
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Whose child it is (D6, the security review's MED-2): a verdict a
|
|
25
|
+
# participant's turn recorded is re-checked AS that participant
|
|
26
|
+
# (their author, their grant's mode, their cap), whoever clicks: its
|
|
27
|
+
# quote is their text, never run with the owner's power. Their link
|
|
28
|
+
# must still be live (else 409 author_link_ended). An owner's verdict
|
|
29
|
+
# is re-checked as whoever clicks: the owner's child for the owner, the
|
|
30
|
+
# clicking participant's for a participant. A participant who clicks
|
|
31
|
+
# passes their own turn's refusals first.
|
|
32
|
+
# @return 202 {of, running: true} (+child_id+ for the owner only)
|
|
33
|
+
def handle_verify_fresh(req, id, of)
|
|
34
|
+
clicker = principal(req).owner? ? nil : principal(req).grant
|
|
35
|
+
session = @session_class.load(id, state_dir: default_state_dir)
|
|
36
|
+
if clicker && (refused = participant_cap_refusal(clicker) || fresh_worker_refusal(session))
|
|
37
|
+
return verify_fresh_refused(clicker, of, *refused)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
by = Steer.sender_author(field_of(VerifyFresh.original(session.verdicts, of), :by))
|
|
41
|
+
runner = by ? verdict_author_grant(id, by) : clicker
|
|
42
|
+
return author_link_ended(clicker, id, of, by) if by && runner.nil?
|
|
43
|
+
if runner && runner != clicker && (refused = participant_cap_refusal(runner) || fresh_worker_refusal(session))
|
|
44
|
+
return verify_fresh_refused(clicker, of, *refused, runner: runner)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
mode = runner && ParticipantMode.live_for(session, runner, state_dir: default_state_dir)
|
|
48
|
+
child = VerifyFresh.start(id, of: of, author: runner&.author, mode: mode, state_dir: default_state_dir,
|
|
49
|
+
spawn: fresh_spawn)
|
|
50
|
+
audit_verify_fresh(clicker, of, status: "accepted", child: child.id, mode: mode&.to_s, runner: runner)
|
|
51
|
+
json_response(202, clicker ? { of: of, running: true } : { of: of, running: true, child_id: child.id })
|
|
52
|
+
rescue VerifyFresh::Refused => e
|
|
53
|
+
verify_fresh_refused(clicker, of, e.status, e.code, e.message, runner: runner)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The live grant of the participant +author+ ("p:<id>") in session
|
|
57
|
+
# +id+, nil when it is gone, revoked or expired.
|
|
58
|
+
def verdict_author_grant(id, author)
|
|
59
|
+
grant = @grants.find(Steer.participant_id(author))
|
|
60
|
+
grant if grant && grant.session_id == id && grant.active?(@grants.now)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 409 author_link_ended: the verdict's participant can't be charged or
|
|
64
|
+
# bounded any more, so no child runs for it.
|
|
65
|
+
def author_link_ended(clicker, id, of, author)
|
|
66
|
+
pid = Steer.participant_id(author)
|
|
67
|
+
name = Participants.read(id, state_dir: default_state_dir).find { |entry| entry.id == pid }&.name
|
|
68
|
+
name = pid if name.to_s.strip.empty?
|
|
69
|
+
detail = format(AUTHOR_LINK_ENDED, name: name)
|
|
70
|
+
audit_verify_fresh(clicker, of, status: "refused", reason: "author_link_ended", participant_id: pid) if clicker
|
|
71
|
+
error_response(409, "author_link_ended", detail)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
AUTHOR_LINK_ENDED = "%<name>s's link has ended: their verdict can't be checked again as theirs " \
|
|
75
|
+
"(re-invite them, or verify the claim yourself)"
|
|
76
|
+
|
|
77
|
+
def field_of(record, key) = record && (record[key] || record[key.to_s])
|
|
78
|
+
|
|
79
|
+
# A live worker of the session that predates participants' turns runs
|
|
80
|
+
# no participant's turn (Delivery::REVIEW_UNAVAILABLE): no fresh
|
|
81
|
+
# child for one either. nil to go on.
|
|
82
|
+
def fresh_worker_refusal(session)
|
|
83
|
+
features = ParticipantMode.live_features(session, default_state_dir)
|
|
84
|
+
return nil if features.nil? || features.include?(Delivery::REVIEW_FEATURE)
|
|
85
|
+
|
|
86
|
+
[409, Delivery::REVIEW_UNAVAILABLE["error"], Delivery::REVIEW_UNAVAILABLE["detail"]]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def verify_fresh_refused(clicker, of, status, code, detail, runner: nil)
|
|
90
|
+
audit_verify_fresh(clicker, of, status: "refused", reason: code, runner: runner)
|
|
91
|
+
error_response(status, code, detail)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Audited in the participants' trail when a participant clicked or the
|
|
95
|
+
# child runs as one: +clicker+ the participant who asked (nil: the
|
|
96
|
+
# owner), +runner+ whose child it is.
|
|
97
|
+
def audit_verify_fresh(clicker, of, status:, runner: nil, participant_id: nil, **fields)
|
|
98
|
+
subject = runner || clicker
|
|
99
|
+
return if subject.nil? && participant_id.nil?
|
|
100
|
+
|
|
101
|
+
Participants.log(subject&.session_id || clicker.session_id, :verify_fresh, state_dir: default_state_dir,
|
|
102
|
+
participant: subject&.id || participant_id,
|
|
103
|
+
asked_by: clicker ? clicker.id : "owner",
|
|
104
|
+
of: of.to_s[0, 64], status: status, **fields.compact)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def fresh_spawn = ->(**options) { @manager.spawn_session(**options) }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "worker_signals"
|
|
|
21
21
|
require_relative "worker_task_wakes"
|
|
22
22
|
require_relative "session_manager"
|
|
23
23
|
require_relative "archive_store"
|
|
24
|
+
require_relative "reminder_carry"
|
|
24
25
|
require_relative "log"
|
|
25
26
|
require_relative "turn_flow"
|
|
26
27
|
require_relative "turn_checkpoint"
|
|
@@ -32,6 +33,7 @@ require_relative "session_setting"
|
|
|
32
33
|
require_relative "queued_command"
|
|
33
34
|
require_relative "model_profile"
|
|
34
35
|
require_relative "child_reports"
|
|
36
|
+
require_relative "verify_fresh"
|
|
35
37
|
require_relative "child_move"
|
|
36
38
|
require_relative "context_absorber"
|
|
37
39
|
require_relative "context_poller"
|
|
@@ -248,7 +250,11 @@ module Samagotchi
|
|
|
248
250
|
drop_dead_question
|
|
249
251
|
# The running turn, on disk at its iteration boundaries.
|
|
250
252
|
@checkpoint = TurnCheckpoint.new(session: @session, state_dir: @state_dir)
|
|
253
|
+
# A restart's hand-off, checked before the Engine's build (its window
|
|
254
|
+
# runs from the leave) and restored into it after.
|
|
255
|
+
carried = take_carried_reminders
|
|
251
256
|
@engine = build_engine
|
|
257
|
+
restore_carried_reminders(carried)
|
|
252
258
|
@config_restart_unsubscribe = ConfigFile.watch_restart_settings!(process: :worker) do |message|
|
|
253
259
|
@engine.announce(type: :hook_notice, hook: "config", text: message, level: :warn, between_turns: true)
|
|
254
260
|
end
|
|
@@ -418,6 +424,7 @@ module Samagotchi
|
|
|
418
424
|
# A change that asks to wake may start a turn here (C4), but not
|
|
419
425
|
# ahead of an exit or restart a client asked for: it is a note then.
|
|
420
426
|
@inbound.absorb_notes
|
|
427
|
+
absorb_fresh_verdicts
|
|
421
428
|
return if absorb_context(wake: nothing_queued? && !@exit_requested)
|
|
422
429
|
|
|
423
430
|
if (prompt = @inbound.take_initial_prompt)
|
|
@@ -446,7 +453,9 @@ module Samagotchi
|
|
|
446
453
|
return if run_task_wake
|
|
447
454
|
return if run_due_reminders
|
|
448
455
|
return left(:idle_exit) if @idle_exit.due? && leave_idle
|
|
449
|
-
|
|
456
|
+
|
|
457
|
+
leaving = @exit_requested && leave_on_request
|
|
458
|
+
return left(leaving) if leaving
|
|
450
459
|
|
|
451
460
|
watch_participants
|
|
452
461
|
@waker.wait(idle_wait)
|
|
@@ -652,9 +661,13 @@ module Samagotchi
|
|
|
652
661
|
# The first prompt (spawn_session's): a delegate child's task, which is
|
|
653
662
|
# its parent's although no client sent it. Its turn carries the id
|
|
654
663
|
# spawn_session gave it, so a wait for it (--new --wait) names it.
|
|
664
|
+
# A Verify fresh child's is a Verify turn on its claim (its verdict is
|
|
665
|
+
# recorded and forwarded to the parent).
|
|
655
666
|
def run_initial_prompt(prompt)
|
|
656
667
|
@initial_turn = true
|
|
657
|
-
|
|
668
|
+
fresh = @session.verify_fresh
|
|
669
|
+
run_prompt(WorkerInbound::Prompt.new(text: prompt, origin: TurnOrigin.new(enqueued_id: @session.first_turn_id),
|
|
670
|
+
verify: fresh&.anchor))
|
|
658
671
|
ensure
|
|
659
672
|
@initial_turn = false
|
|
660
673
|
end
|
|
@@ -665,7 +678,7 @@ module Samagotchi
|
|
|
665
678
|
|
|
666
679
|
# @param prompt [WorkerInbound::Prompt]
|
|
667
680
|
def run_prompt(prompt)
|
|
668
|
-
origin = prompt.origin
|
|
681
|
+
origin = fresh_origin(prompt.origin)
|
|
669
682
|
# A participant's turn queued before their link ended or their cap was
|
|
670
683
|
# reached doesn't start (chi web refuses new ones; this is the
|
|
671
684
|
# worker's check).
|
|
@@ -681,7 +694,7 @@ module Samagotchi
|
|
|
681
694
|
@continue_offer.drop(origin)
|
|
682
695
|
@turn_flow.before_prompt_turn
|
|
683
696
|
run_engine_turn(prompt.text, origin: origin, max_iterations: max_iterations(prompt.no_interrupt),
|
|
684
|
-
images: prompt.images) do |result, error|
|
|
697
|
+
images: prompt.images, **(prompt.verify ? { verify: prompt.verify } : {})) do |result, error|
|
|
685
698
|
if error
|
|
686
699
|
# The Engine announced :turn_failed (with the error's one line).
|
|
687
700
|
restore_failed_turn([[prompt.text, origin, prompt.images], *@merged_this_turn], error: error)
|
|
@@ -692,6 +705,13 @@ module Samagotchi
|
|
|
692
705
|
participant_refusal(origin.author)
|
|
693
706
|
end
|
|
694
707
|
|
|
708
|
+
# +origin+ as this session's turns run it: a participant's Verify fresh
|
|
709
|
+
# child's are all theirs (VerifyFreshOrigin#origin_for).
|
|
710
|
+
def fresh_origin(origin)
|
|
711
|
+
fresh = @session&.verify_fresh
|
|
712
|
+
fresh ? fresh.origin_for(origin) : TurnOrigin.of(origin)
|
|
713
|
+
end
|
|
714
|
+
|
|
695
715
|
# Why the participant +author+ ("p:<id>") may run no more turns, nil
|
|
696
716
|
# while they may (and for the owner): "revoked" when participants.json
|
|
697
717
|
# has no active entry for them (revoked, expired, never invited),
|
|
@@ -701,11 +721,11 @@ module Samagotchi
|
|
|
701
721
|
def participant_refusal(author)
|
|
702
722
|
author = Steer.sender_author(author) or return nil
|
|
703
723
|
|
|
704
|
-
entry =
|
|
724
|
+
entry = participant_entry(author)
|
|
705
725
|
return "revoked" unless entry&.active?(Time.now)
|
|
706
726
|
|
|
707
727
|
cap = participant_cap(entry)
|
|
708
|
-
return nil if
|
|
728
|
+
return nil if participant_spend(author) < cap
|
|
709
729
|
|
|
710
730
|
cap_notice(author, entry, cap)
|
|
711
731
|
"cap_reached"
|
|
@@ -718,13 +738,30 @@ module Samagotchi
|
|
|
718
738
|
def participant_turn_stop(author)
|
|
719
739
|
reason = participant_refusal(author) or return nil
|
|
720
740
|
|
|
721
|
-
entry =
|
|
741
|
+
entry = participant_entry(author)
|
|
722
742
|
name = participant_label(author, entry&.name)
|
|
723
743
|
return format(REVOKED_STOP, name: name) if reason == "revoked"
|
|
724
744
|
|
|
725
745
|
format(CAP_STOP, name: name, cap: participant_cap(entry))
|
|
726
746
|
end
|
|
727
747
|
|
|
748
|
+
# +author+'s participants.json entry: this session's, a Verify fresh
|
|
749
|
+
# child's parent's (Session#participants_source_id); nil for none.
|
|
750
|
+
def participant_entry(author)
|
|
751
|
+
Participants.read(participants_id, state_dir: @state_dir).find { |e| Steer.author_for(e.id) == author }
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# The session whose participants.json and audit trail this session's
|
|
755
|
+
# participants are in.
|
|
756
|
+
def participants_id = @session&.participants_source_id || @session_id
|
|
757
|
+
|
|
758
|
+
# What +author+ spent against their cap: this session's metrics (live)
|
|
759
|
+
# and, through Verify fresh, the parent's or the children's saved ones
|
|
760
|
+
# (VerifyFresh.related_spend).
|
|
761
|
+
def participant_spend(author)
|
|
762
|
+
@engine.metrics.spend(author) + VerifyFresh.related_spend(@session, author, state_dir: @state_dir)
|
|
763
|
+
end
|
|
764
|
+
|
|
728
765
|
# A participant's cap in dollars: their own, else the config's.
|
|
729
766
|
def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f : Web::Grants.default_cap
|
|
730
767
|
|
|
@@ -746,7 +783,7 @@ module Samagotchi
|
|
|
746
783
|
id = Steer.participant_id(author)
|
|
747
784
|
text = ParticipantMode.for_turn(@session, entry) == ParticipantMode::PAIR ? PAIR_CAP_NOTICE : CAP_NOTICE
|
|
748
785
|
@engine.announce(type: :hook_notice, hook: Engine::PARTICIPANTS_HOOK, level: :warn, between_turns: true,
|
|
749
|
-
text: format(text, name: participant_label(author, entry.name), cap: cap, session:
|
|
786
|
+
text: format(text, name: participant_label(author, entry.name), cap: cap, session: participants_id[0, 8], id: id))
|
|
750
787
|
end
|
|
751
788
|
|
|
752
789
|
CAP_NOTICE = "%<name>s reached their $%<cap>.2f review cap: their comments are refused until you raise it " \
|
|
@@ -767,7 +804,7 @@ module Samagotchi
|
|
|
767
804
|
# @param origin [TurnOrigin] a participant's
|
|
768
805
|
def refuse_participant_turn(origin, reason)
|
|
769
806
|
TurnCheckpoint.record_refused(@session_dir, origin.enqueued_id, reason: reason, author: origin.participant_author)
|
|
770
|
-
Participants.log(
|
|
807
|
+
Participants.log(participants_id, :turn, state_dir: @state_dir, participant: origin.participant_id,
|
|
771
808
|
enqueued_id: origin.enqueued_id, status: "refused", reason: reason)
|
|
772
809
|
@engine.announce(type: :turn_dequeued, enqueued_id: origin.enqueued_id, client_id: origin.client_id)
|
|
773
810
|
nil
|
|
@@ -1189,7 +1226,9 @@ module Samagotchi
|
|
|
1189
1226
|
# Every kind of turn merges steering (a reminder turn too, which may be
|
|
1190
1227
|
# a fresh worker's first), so the list is this turn's from the start.
|
|
1191
1228
|
@merged_this_turn = []
|
|
1192
|
-
origin =
|
|
1229
|
+
origin = fresh_origin(turn_args[:origin])
|
|
1230
|
+
turn_args = turn_args.merge(origin: origin)
|
|
1231
|
+
verdicts_before = @session.verdicts.size
|
|
1193
1232
|
@reporting_turn = reports_to_parent?(origin)
|
|
1194
1233
|
# A participant's review turn merges no input (#pending_input_drain).
|
|
1195
1234
|
@participant_turn = origin.participant?
|
|
@@ -1234,6 +1273,33 @@ module Samagotchi
|
|
|
1234
1273
|
# turn that failed (kept with its failed note), read them.
|
|
1235
1274
|
settle_child_reports(rolled_back: @turn_flow.restores != restores || merges_lost?(error))
|
|
1236
1275
|
ring_parent_after_turn
|
|
1276
|
+
forward_fresh_verdicts(verdicts_before)
|
|
1277
|
+
end
|
|
1278
|
+
|
|
1279
|
+
# A Verify fresh child's verdicts this turn recorded go to its parent
|
|
1280
|
+
# (VerifyFresh.forward), after the save: the parent's card shows them.
|
|
1281
|
+
# A fresh child never rings (ChildRing.ring): nothing enters the
|
|
1282
|
+
# parent's conversation.
|
|
1283
|
+
def forward_fresh_verdicts(before)
|
|
1284
|
+
return unless @session.verify_fresh
|
|
1285
|
+
|
|
1286
|
+
added = @session.verdicts.drop(before)
|
|
1287
|
+
VerifyFresh.forward(@session, added, state_dir: @state_dir) unless added.empty?
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
# The parent's side, between turns: the verdicts its Verify fresh
|
|
1291
|
+
# children forwarded join the session's (saved, then their files go)
|
|
1292
|
+
# and the UIs hear each (:verdict_recorded).
|
|
1293
|
+
def absorb_fresh_verdicts
|
|
1294
|
+
added, files = VerifyFresh.take(@session, @session_dir)
|
|
1295
|
+
return if files.empty?
|
|
1296
|
+
|
|
1297
|
+
return unless save_or_log(:fresh_verdicts) { save_session }
|
|
1298
|
+
|
|
1299
|
+
FileUtils.rm_f(files)
|
|
1300
|
+
added.each { |record| @engine.announce(type: :verdict_recorded, verdict: record) }
|
|
1301
|
+
rescue StandardError => e
|
|
1302
|
+
Log.warn(:worker, "fresh_verdicts_failed", error: e.class.name, msg: e.message)
|
|
1237
1303
|
end
|
|
1238
1304
|
|
|
1239
1305
|
# The delegate reports this turn took: a kept turn moves the cursors on
|
|
@@ -1514,6 +1580,11 @@ module Samagotchi
|
|
|
1514
1580
|
def exit_request(client_id, delete: false, restart: false, stop: false, ask: true)
|
|
1515
1581
|
# The Bridge serves before the idle-exit policy exists.
|
|
1516
1582
|
return WorkerIdleExit::Hold.new(reason: :starting) unless @idle_exit
|
|
1583
|
+
# One exit is already taken: the first one decides how the worker
|
|
1584
|
+
# leaves (a later stop-all can't turn a restart, whose reminders go to
|
|
1585
|
+
# the next worker, into a plain exit). Held at the leave, it is
|
|
1586
|
+
# forgotten and the next request counts again.
|
|
1587
|
+
return nil if @exit_requested
|
|
1517
1588
|
|
|
1518
1589
|
if restart
|
|
1519
1590
|
hold = @idle_exit.hold_for_restart
|
|
@@ -1551,7 +1622,8 @@ module Samagotchi
|
|
|
1551
1622
|
# intake and stop the idle jobs (the Bridge stops after the log). Other clients' streams no longer hold: the asker's
|
|
1552
1623
|
# may not have closed yet, and a UI joining now finds the worker gone
|
|
1553
1624
|
# (:stream_closed), the window #leave_idle has too.
|
|
1554
|
-
# @return [
|
|
1625
|
+
# @return [Symbol, false] how it leaves (:restart or :exit_requested,
|
|
1626
|
+
# decided under the lock), false when something came in since the request
|
|
1555
1627
|
def leave_on_request
|
|
1556
1628
|
stop_bridge_after do
|
|
1557
1629
|
@engine.synchronize_events do
|
|
@@ -1566,16 +1638,57 @@ module Samagotchi
|
|
|
1566
1638
|
Log.info(:worker, "exit_held", reason: hold)
|
|
1567
1639
|
next false
|
|
1568
1640
|
end
|
|
1641
|
+
next false if @exit_restart && !carry_reminders
|
|
1569
1642
|
|
|
1570
1643
|
@bridge&.close_intake
|
|
1571
1644
|
# Not joined: a recap tick's emit waits for the event log held here.
|
|
1572
1645
|
@engine.stop_idle(join: false)
|
|
1573
1646
|
Log.info(:worker, @exit_restart ? "restart_requested" : "exit_requested", by: @exit_requested_by || "a client")
|
|
1574
|
-
|
|
1647
|
+
@exit_restart ? :restart : :exit_requested
|
|
1575
1648
|
end
|
|
1576
1649
|
end
|
|
1577
1650
|
end
|
|
1578
1651
|
|
|
1652
|
+
# A restart hands the reminders to the next worker (ReminderCarry), while
|
|
1653
|
+
# the event log is held: no turn can register or fire one from here. A
|
|
1654
|
+
# write that fails keeps this worker up with them rather than losing them.
|
|
1655
|
+
# A session stopped meanwhile drops them (run_session_loop then starts no
|
|
1656
|
+
# successor): nothing is written.
|
|
1657
|
+
# @return [Boolean] false when the write failed
|
|
1658
|
+
def carry_reminders
|
|
1659
|
+
if stopped_on_disk?
|
|
1660
|
+
ReminderCarry.remove(@session_dir)
|
|
1661
|
+
else
|
|
1662
|
+
ReminderCarry.write(@session_dir, session_id: @session_id, store: @engine.reminder_store)
|
|
1663
|
+
end
|
|
1664
|
+
true
|
|
1665
|
+
rescue StandardError => e
|
|
1666
|
+
@exit_requested = nil
|
|
1667
|
+
@exit_restart = false
|
|
1668
|
+
Log.warn(:worker, "reminders_carry_failed", error: e.class.name, msg: e.message)
|
|
1669
|
+
false
|
|
1670
|
+
end
|
|
1671
|
+
|
|
1672
|
+
# The reminders a restart handed over (#carry_reminders), read and
|
|
1673
|
+
# deleted; a stopped session drops them.
|
|
1674
|
+
# @return [Array<Hash>, nil] ReminderStore#export's entries
|
|
1675
|
+
def take_carried_reminders
|
|
1676
|
+
ReminderCarry.take(@session_dir, session_id: @session_id, stopped: stopped_on_disk?)
|
|
1677
|
+
rescue StandardError => e
|
|
1678
|
+
Log.warn(:worker, "reminders_carry_take_failed", error: e.class.name, msg: e.message)
|
|
1679
|
+
nil
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
# Into the new Engine's store, before the idle scheduler starts polling it.
|
|
1683
|
+
def restore_carried_reminders(entries)
|
|
1684
|
+
return unless entries
|
|
1685
|
+
|
|
1686
|
+
names = @engine.reminder_store.restore(entries)
|
|
1687
|
+
Log.info(:worker, "reminders_carried", count: names.size)
|
|
1688
|
+
rescue StandardError => e
|
|
1689
|
+
Log.warn(:worker, "reminders_carry_restore_failed", error: e.class.name, msg: e.message)
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1579
1692
|
# @return [Symbol] +result+, with #discard? decided and, for a session
|
|
1580
1693
|
# kept, its recap written: the Bridge is closed and the idle jobs are
|
|
1581
1694
|
# stopped by now, and nothing waits on this (the TUI has detached). A
|
|
@@ -11,7 +11,8 @@ module Samagotchi
|
|
|
11
11
|
# * a client holds the Bridge's stream (an attached TUI, even an idle one,
|
|
12
12
|
# or a web tab);
|
|
13
13
|
# * a reminder is registered. Reminders live only in the Engine and repeat
|
|
14
|
-
# until the agent cancels them, so an exit would drop them for good
|
|
14
|
+
# until the agent cancels them, so an exit would drop them for good (a
|
|
15
|
+
# restart hands them to the next worker instead: ReminderCarry);
|
|
15
16
|
# * a continue offer waits for an answer; it too lives only in memory;
|
|
16
17
|
# * a background task the worker watches for a finish wake (TaskWatch)
|
|
17
18
|
# still runs, or finished and waits for the next pass's wake: its end
|
|
@@ -167,18 +168,18 @@ module Samagotchi
|
|
|
167
168
|
# approval waiting (a turn's), a /btw (or another anytime command)
|
|
168
169
|
# running, a delegate's approval relay this worker still answers for,
|
|
169
170
|
# background tasks this conversation started (their output and end are
|
|
170
|
-
# this worker's to report).
|
|
171
|
-
#
|
|
172
|
-
#
|
|
171
|
+
# this worker's to report). Reminders don't hold: the worker writes them
|
|
172
|
+
# for its successor as it leaves (Worker#leave_on_request, ReminderCarry).
|
|
173
|
+
# Clients' streams don't hold either: a web tab reconnects on the new
|
|
174
|
+
# worker's bridge_up, an attached terminal finds the new worker's Bridge.
|
|
173
175
|
# @return [Symbol, nil] :turn_running, :input_queued, :continue_offered,
|
|
174
|
-
# :question_pending, :
|
|
176
|
+
# :question_pending, :command_running, :relays_open or
|
|
175
177
|
# :background_tasks
|
|
176
178
|
def hold_for_restart
|
|
177
179
|
return :turn_running if @engine.turn_running?
|
|
178
180
|
return :input_queued if @input_pending.call
|
|
179
181
|
return :continue_offered if @awaiting_continue.call
|
|
180
182
|
return :question_pending if @engine.pending_question
|
|
181
|
-
return :reminders if @engine.reminder_store&.any?
|
|
182
183
|
return :command_running if @engine.anytime_running?
|
|
183
184
|
return :relays_open if @engine.relay_desk.active?
|
|
184
185
|
return :background_tasks if @running_tasks.call.any?
|
|
@@ -40,9 +40,9 @@ module Samagotchi
|
|
|
40
40
|
# turn; +images+: its image refs ({file:, name:}); +delivery+: the wire
|
|
41
41
|
# value the sender asked for ("queue", or nil for the default
|
|
42
42
|
# next_step); +origin+: a TurnOrigin (a Hash is read as one).
|
|
43
|
-
Prompt = Data.define(:text, :origin, :no_interrupt, :images, :delivery) do
|
|
44
|
-
def initialize(text:, origin: nil, no_interrupt: false, images: [], delivery: nil)
|
|
45
|
-
super(text:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:)
|
|
43
|
+
Prompt = Data.define(:text, :origin, :no_interrupt, :images, :delivery, :verify) do
|
|
44
|
+
def initialize(text:, origin: nil, no_interrupt: false, images: [], delivery: nil, verify: nil)
|
|
45
|
+
super(text:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:, verify: VerifyAnchor.from(verify))
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -215,7 +215,7 @@ module Samagotchi
|
|
|
215
215
|
return if input.images.empty? && queue_as_command(input.prompt, input.origin, File.basename(input_file))
|
|
216
216
|
|
|
217
217
|
yield Prompt.new(text: input.prompt, origin: input.origin, no_interrupt: input.no_interrupt, images: input.images,
|
|
218
|
-
delivery: input.delivery)
|
|
218
|
+
delivery: input.delivery, verify: input.verify)
|
|
219
219
|
ensure
|
|
220
220
|
FileUtils.rm_f(claimed_file)
|
|
221
221
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.126.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -528,6 +528,7 @@ files:
|
|
|
528
528
|
- lib/samagotchi/relay_desk.rb
|
|
529
529
|
- lib/samagotchi/relay_verifier.rb
|
|
530
530
|
- lib/samagotchi/relay_watcher.rb
|
|
531
|
+
- lib/samagotchi/reminder_carry.rb
|
|
531
532
|
- lib/samagotchi/reminder_queue.rb
|
|
532
533
|
- lib/samagotchi/reminder_store.rb
|
|
533
534
|
- lib/samagotchi/remote_attach_command.rb
|
|
@@ -603,13 +604,20 @@ files:
|
|
|
603
604
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
604
605
|
- lib/samagotchi/terminal_ui/attach_link.rb
|
|
605
606
|
- lib/samagotchi/terminal_ui/attached_loop.rb
|
|
607
|
+
- lib/samagotchi/terminal_ui/attached_loop/composer.rb
|
|
608
|
+
- lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb
|
|
609
|
+
- lib/samagotchi/terminal_ui/attached_loop/questions.rb
|
|
610
|
+
- lib/samagotchi/terminal_ui/attached_loop/queueing.rb
|
|
611
|
+
- lib/samagotchi/terminal_ui/attached_loop/sending.rb
|
|
606
612
|
- lib/samagotchi/terminal_ui/attached_view.rb
|
|
607
613
|
- lib/samagotchi/terminal_ui/between_turns.rb
|
|
608
614
|
- lib/samagotchi/terminal_ui/event_renderer.rb
|
|
615
|
+
- lib/samagotchi/terminal_ui/exit_lines.rb
|
|
609
616
|
- lib/samagotchi/terminal_ui/formatting.rb
|
|
610
617
|
- lib/samagotchi/terminal_ui/gap_clock.rb
|
|
611
618
|
- lib/samagotchi/terminal_ui/image_input.rb
|
|
612
619
|
- lib/samagotchi/terminal_ui/input_support.rb
|
|
620
|
+
- lib/samagotchi/terminal_ui/join_replay.rb
|
|
613
621
|
- lib/samagotchi/terminal_ui/line_reader.rb
|
|
614
622
|
- lib/samagotchi/terminal_ui/live_region.rb
|
|
615
623
|
- lib/samagotchi/terminal_ui/local_link.rb
|
|
@@ -719,6 +727,10 @@ files:
|
|
|
719
727
|
- lib/samagotchi/update_hint.rb
|
|
720
728
|
- lib/samagotchi/used_memories.rb
|
|
721
729
|
- lib/samagotchi/utf8_default.rb
|
|
730
|
+
- lib/samagotchi/verdict.rb
|
|
731
|
+
- lib/samagotchi/verify_anchor.rb
|
|
732
|
+
- lib/samagotchi/verify_fresh.rb
|
|
733
|
+
- lib/samagotchi/verify_fresh_origin.rb
|
|
722
734
|
- lib/samagotchi/version.rb
|
|
723
735
|
- lib/samagotchi/vision_context.rb
|
|
724
736
|
- lib/samagotchi/vision_support.rb
|
|
@@ -757,6 +769,7 @@ files:
|
|
|
757
769
|
- lib/samagotchi/web/public/chains.js
|
|
758
770
|
- lib/samagotchi/web/public/chip_popover.js
|
|
759
771
|
- lib/samagotchi/web/public/chunk_router.js
|
|
772
|
+
- lib/samagotchi/web/public/claim_link.js
|
|
760
773
|
- lib/samagotchi/web/public/command_complete.js
|
|
761
774
|
- lib/samagotchi/web/public/command_list.js
|
|
762
775
|
- lib/samagotchi/web/public/command_view.js
|
|
@@ -871,6 +884,10 @@ files:
|
|
|
871
884
|
- lib/samagotchi/web/public/vendor/directives/repeat.js
|
|
872
885
|
- lib/samagotchi/web/public/vendor/directives/unsafe-html.js
|
|
873
886
|
- lib/samagotchi/web/public/vendor/lit-html.js
|
|
887
|
+
- lib/samagotchi/web/public/verdict_card.js
|
|
888
|
+
- lib/samagotchi/web/public/verdict_fresh.js
|
|
889
|
+
- lib/samagotchi/web/public/verify_card.css
|
|
890
|
+
- lib/samagotchi/web/public/verify_preset.js
|
|
874
891
|
- lib/samagotchi/web/public/working_set.js
|
|
875
892
|
- lib/samagotchi/web/public/working_set_rail.js
|
|
876
893
|
- lib/samagotchi/web/public/working_set_view.js
|
|
@@ -897,6 +914,7 @@ files:
|
|
|
897
914
|
- lib/samagotchi/web/tunnel.rb
|
|
898
915
|
- lib/samagotchi/web/tunnel/tailscale.rb
|
|
899
916
|
- lib/samagotchi/web/update_check.rb
|
|
917
|
+
- lib/samagotchi/web/verify_fresh_routes.rb
|
|
900
918
|
- lib/samagotchi/worker.rb
|
|
901
919
|
- lib/samagotchi/worker_idle_exit.rb
|
|
902
920
|
- lib/samagotchi/worker_inbound.rb
|