samagotchi 0.29.0 → 0.31.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 +99 -1
- data/docs/cli.md +4 -4
- data/docs/configuration.md +4 -0
- data/docs/hooks.md +1 -1
- data/docs/internals/engine.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +11 -4
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +1 -0
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/child_reports.rb +214 -0
- data/lib/samagotchi/client.rb +35 -4
- data/lib/samagotchi/command_steps.rb +35 -15
- data/lib/samagotchi/config.rb +5 -0
- data/lib/samagotchi/engine.rb +2 -0
- data/lib/samagotchi/host_registry.rb +4 -0
- data/lib/samagotchi/idle_recap.rb +4 -0
- data/lib/samagotchi/kernel_loop.rb +63 -11
- data/lib/samagotchi/llm/chat_loop.rb +14 -7
- data/lib/samagotchi/llm/errors.rb +9 -0
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/model_list_store.rb +117 -0
- data/lib/samagotchi/model_profile.rb +43 -0
- data/lib/samagotchi/prompt_warmup.rb +30 -5
- data/lib/samagotchi/reply_wait.rb +39 -8
- data/lib/samagotchi/send_command.rb +5 -3
- data/lib/samagotchi/session.rb +31 -5
- data/lib/samagotchi/session_inbox.rb +34 -1
- data/lib/samagotchi/session_manager.rb +41 -5
- data/lib/samagotchi/session_metrics.rb +6 -3
- data/lib/samagotchi/steer.rb +39 -7
- data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +38 -8
- data/lib/samagotchi/terminal_ui.rb +44 -5
- data/lib/samagotchi/tool_activity.rb +6 -2
- data/lib/samagotchi/tool_call_parser.rb +40 -0
- data/lib/samagotchi/tool_declarations.rb +7 -7
- data/lib/samagotchi/tools/delegate.rb +26 -13
- data/lib/samagotchi/tools/delegate_cursor.rb +91 -0
- data/lib/samagotchi/tools/delegate_wait.rb +50 -31
- data/lib/samagotchi/tools/execute.rb +3 -1
- data/lib/samagotchi/tools/task_runtime.rb +21 -2
- data/lib/samagotchi/tools/task_wait.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +8 -2
- data/lib/samagotchi/web/public/activity.js +2 -1
- data/lib/samagotchi/web/public/app.js +21 -11
- data/lib/samagotchi/web/public/command_view.js +19 -5
- data/lib/samagotchi/web/public/format.js +8 -1
- data/lib/samagotchi/web/public/index.html +14 -6
- data/lib/samagotchi/web/public/notify.js +4 -2
- data/lib/samagotchi/web/public/stage_model.js +18 -0
- data/lib/samagotchi/web/public/stage_view.js +16 -11
- data/lib/samagotchi/web/public/task_stop.js +11 -2
- data/lib/samagotchi/web/public/timing.js +12 -7
- data/lib/samagotchi/web/public/turn_events.js +29 -9
- data/lib/samagotchi/web/public/turn_model.js +16 -11
- data/lib/samagotchi/web/public/turn_view.js +49 -7
- data/lib/samagotchi/worker.rb +240 -23
- metadata +4 -1
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative "continue_offer"
|
|
|
17
17
|
require_relative "iteration_limit"
|
|
18
18
|
require_relative "session_commands"
|
|
19
19
|
require_relative "model_profile"
|
|
20
|
+
require_relative "child_reports"
|
|
20
21
|
require_relative "tools/task_runtime"
|
|
21
22
|
|
|
22
23
|
module Samagotchi
|
|
@@ -45,6 +46,11 @@ module Samagotchi
|
|
|
45
46
|
# turn) is refused as busy.
|
|
46
47
|
class Worker
|
|
47
48
|
FALLBACK_TICK_SECONDS = 5
|
|
49
|
+
# A worker just started runs no wake turn for delegate reports this
|
|
50
|
+
# long: a worker spawned for a message (chi send to a stopped parent)
|
|
51
|
+
# gets it over the Bridge a moment after it starts, and that message's
|
|
52
|
+
# turn goes first (the reports join it).
|
|
53
|
+
WAKE_START_GRACE = 2.0
|
|
48
54
|
# How much of a command's output goes into its :command_ran.
|
|
49
55
|
COMMAND_OUTPUT_LIMIT = 4096
|
|
50
56
|
BUSY_OUTPUT = "busy: wait for the turn to end"
|
|
@@ -89,6 +95,25 @@ module Samagotchi
|
|
|
89
95
|
@discard = false
|
|
90
96
|
# The exit asked for is a restart (POST /exit restart: true).
|
|
91
97
|
@exit_restart = false
|
|
98
|
+
# The lines the running turn's drain merged ([prompt, origin]), for a
|
|
99
|
+
# failed prompt turn to hand back; reset as each turn begins.
|
|
100
|
+
@merged_this_turn = []
|
|
101
|
+
# A delegate child's side (ChildRing): whether the running turn is
|
|
102
|
+
# its parent's (it rings when it ends), whether that turn rang a
|
|
103
|
+
# question, and whether the next turn rings whatever its origin (it
|
|
104
|
+
# follows a question the parent was rung about: D10).
|
|
105
|
+
@reporting_turn = false
|
|
106
|
+
@asked_parent = false
|
|
107
|
+
@owes_parent = false
|
|
108
|
+
@initial_turn = false
|
|
109
|
+
# A parent's side: turns run for delegate reports since the last human
|
|
110
|
+
# input (session.max_wakes), whether a failed one paused them until
|
|
111
|
+
# then, whether the budget notice went out, and the reports a wake
|
|
112
|
+
# turn's first boundary hands over.
|
|
113
|
+
@wakes_in_a_row = 0
|
|
114
|
+
@wakes_paused = false
|
|
115
|
+
@budget_noticed = false
|
|
116
|
+
@wake_reports = nil
|
|
92
117
|
end
|
|
93
118
|
|
|
94
119
|
# Whether the session was empty as the worker left it, so the caller
|
|
@@ -96,6 +121,12 @@ module Samagotchi
|
|
|
96
121
|
# checks again then).
|
|
97
122
|
def discard? = @discard
|
|
98
123
|
|
|
124
|
+
# Whether delegate reports wait that an idle parent runs a turn for
|
|
125
|
+
# (#wake_state): SessionManager.run_session_loop starts a new worker
|
|
126
|
+
# for them after this one leaves. Reports the budget or a failure held
|
|
127
|
+
# back wait for the next human input instead.
|
|
128
|
+
def wake_due? = wake_state == :due
|
|
129
|
+
|
|
99
130
|
# What a new session starts on (and /model resets to); nil before #run.
|
|
100
131
|
attr_reader :default_model
|
|
101
132
|
|
|
@@ -105,6 +136,7 @@ module Samagotchi
|
|
|
105
136
|
# (chi stop), :crashed when the loop raised (the session is marked
|
|
106
137
|
# errored); SessionManager.run_session_loop turns it into the exit
|
|
107
138
|
def run
|
|
139
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
108
140
|
@session = Session.load(@session_id, state_dir: @state_dir)
|
|
109
141
|
drop_dead_question
|
|
110
142
|
@engine = build_engine
|
|
@@ -114,7 +146,7 @@ module Samagotchi
|
|
|
114
146
|
@engine.session = @session
|
|
115
147
|
# Input already waiting in the inbox starts the next turn at once: no
|
|
116
148
|
# turn-end warm-up then.
|
|
117
|
-
@engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? }
|
|
149
|
+
@engine.next_turn_waiting = -> { SessionInbox.find_new_input_files(@session_dir).any? || wake_due? }
|
|
118
150
|
@turn_flow = TurnFlow.new(engine: @engine)
|
|
119
151
|
# Its question's answer is queued as the matching /continue (the
|
|
120
152
|
# Bridge exists by the time anyone answers).
|
|
@@ -131,7 +163,11 @@ module Samagotchi
|
|
|
131
163
|
@turn_end_seq = 0
|
|
132
164
|
@engine.subscribe(observer: lambda { |event|
|
|
133
165
|
@turn_end_seq = event[:event_seq] if Events::TURN_END.include?(event[:type])
|
|
166
|
+
@turn_id = event[:turn_id] if event[:type] == :turn_started
|
|
167
|
+
ring_question(event) if event[:type] == :question_requested
|
|
134
168
|
})
|
|
169
|
+
# This session's own delegate children's news (rings in children/).
|
|
170
|
+
@child_reports = ChildReports.new(session_id: @session_id, state_dir: @state_dir)
|
|
135
171
|
# /model's default is the config's, as in the REPL; the Engine started
|
|
136
172
|
# on the session's model.
|
|
137
173
|
@default_model = ModelProfile.required_model_name(nil)
|
|
@@ -190,17 +226,18 @@ module Samagotchi
|
|
|
190
226
|
absorb_notes
|
|
191
227
|
|
|
192
228
|
if (prompt = take_initial_prompt)
|
|
193
|
-
|
|
229
|
+
run_initial_prompt(prompt) unless initial_command(prompt)
|
|
194
230
|
next
|
|
195
231
|
end
|
|
196
232
|
|
|
197
233
|
input_files = SessionInbox.find_new_input_files(@session_dir)
|
|
198
234
|
if input_files.empty?
|
|
235
|
+
next if run_wake_turn
|
|
199
236
|
next if run_due_reminders
|
|
200
237
|
return left(:idle_exit) if @idle_exit.due? && leave_idle
|
|
201
238
|
return left(@exit_restart ? :restart : :exit_requested) if @exit_requested && leave_on_request
|
|
202
239
|
|
|
203
|
-
@waker.wait(
|
|
240
|
+
@waker.wait(idle_wait)
|
|
204
241
|
next
|
|
205
242
|
end
|
|
206
243
|
|
|
@@ -216,6 +253,8 @@ module Samagotchi
|
|
|
216
253
|
# Its stderr is /dev/null: the log is the only trace of why.
|
|
217
254
|
Log.exception(:worker, "crashed", e)
|
|
218
255
|
Session.mark_error(@session_id, reason: e.message, state_dir: @state_dir)
|
|
256
|
+
# A delegate's parent hears of it (best effort).
|
|
257
|
+
ChildRing.ring(@session, why: "crash", state_dir: @state_dir) if @session
|
|
219
258
|
:crashed
|
|
220
259
|
ensure
|
|
221
260
|
# The anytime commands finish and the plugins' services stop (a
|
|
@@ -268,7 +307,7 @@ module Samagotchi
|
|
|
268
307
|
|
|
269
308
|
Log.info(:worker, "dead_question_dropped", id: @session.pending_question[:id])
|
|
270
309
|
@session.pending_question = nil
|
|
271
|
-
@session.save(state_dir: @state_dir)
|
|
310
|
+
save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
|
|
272
311
|
end
|
|
273
312
|
|
|
274
313
|
# spawn_session hands the first prompt over in last_prompt, but
|
|
@@ -285,7 +324,7 @@ module Samagotchi
|
|
|
285
324
|
|
|
286
325
|
prompt = @session.last_prompt
|
|
287
326
|
@session.last_prompt = ""
|
|
288
|
-
@session.save(state_dir: @state_dir)
|
|
327
|
+
save_or_log(:initial_prompt) { @session.save(state_dir: @state_dir) }
|
|
289
328
|
prompt
|
|
290
329
|
end
|
|
291
330
|
|
|
@@ -308,14 +347,16 @@ module Samagotchi
|
|
|
308
347
|
return false unless queue_as_command(prompt, nil)
|
|
309
348
|
|
|
310
349
|
@session.status = Session::STATUS_IDLE
|
|
311
|
-
save_session
|
|
350
|
+
save_or_log(:initial_command) { save_session }
|
|
312
351
|
true
|
|
313
352
|
end
|
|
314
353
|
|
|
315
354
|
# Add the queued context notes to the conversation (between turns only,
|
|
316
355
|
# on this thread), save, then delete their files: a crash before the
|
|
317
356
|
# delete leaves them claimed, and Engine#add_context_note skips a note
|
|
318
|
-
# the saved conversation already holds.
|
|
357
|
+
# the saved conversation already holds. A failed save keeps the files
|
|
358
|
+
# too: the next pass claims them again, finds them in the conversation
|
|
359
|
+
# and saves again. Not activity: a note alone
|
|
319
360
|
# neither starts a turn nor keeps an idle worker up.
|
|
320
361
|
def absorb_notes
|
|
321
362
|
files = SessionInbox.find_new_note_files(@session_dir)
|
|
@@ -326,10 +367,20 @@ module Samagotchi
|
|
|
326
367
|
note = SessionInbox.read_note(file)
|
|
327
368
|
@engine.add_context_note(@session, note) if note
|
|
328
369
|
end
|
|
329
|
-
@session.save(state_dir: @state_dir)
|
|
370
|
+
return unless save_or_log(:notes) { @session.save(state_dir: @state_dir) }
|
|
371
|
+
|
|
330
372
|
claimed.each { |file| FileUtils.rm_f(file) }
|
|
331
373
|
end
|
|
332
374
|
|
|
375
|
+
# The first prompt (spawn_session's): a delegate child's task, which is
|
|
376
|
+
# its parent's although no client sent it.
|
|
377
|
+
def run_initial_prompt(prompt)
|
|
378
|
+
@initial_turn = true
|
|
379
|
+
run_prompt(prompt, nil)
|
|
380
|
+
ensure
|
|
381
|
+
@initial_turn = false
|
|
382
|
+
end
|
|
383
|
+
|
|
333
384
|
def run_input_file(input_file)
|
|
334
385
|
claimed_file = SessionInbox.claim_input_file(input_file)
|
|
335
386
|
return unless claimed_file
|
|
@@ -352,7 +403,6 @@ module Samagotchi
|
|
|
352
403
|
user_input(origin&.dig(:client_id))
|
|
353
404
|
@continue_offer.drop(origin)
|
|
354
405
|
@turn_flow.before_prompt_turn
|
|
355
|
-
@merged_this_turn = []
|
|
356
406
|
run_engine_turn(prompt, origin: origin, max_iterations: max_iterations(no_interrupt),
|
|
357
407
|
images: images) do |result, error|
|
|
358
408
|
if error
|
|
@@ -384,6 +434,85 @@ module Samagotchi
|
|
|
384
434
|
true
|
|
385
435
|
end
|
|
386
436
|
|
|
437
|
+
# :due when an idle parent should run a turn for its delegate
|
|
438
|
+
# children's reports now (session.delegate_reports: wake): rings wait,
|
|
439
|
+
# no continue offer does (they merge into its turn), wakes aren't paused
|
|
440
|
+
# by a failed one, and the wake budget isn't spent; :budget when only
|
|
441
|
+
# the budget stops it; nil otherwise.
|
|
442
|
+
def wake_state
|
|
443
|
+
return nil unless @child_reports && ChildRing.mode == "wake"
|
|
444
|
+
return nil if @wakes_paused || @turn_flow&.awaiting_continue?
|
|
445
|
+
return nil unless @child_reports.waiting?
|
|
446
|
+
|
|
447
|
+
@wakes_in_a_row < max_wakes ? :due : :budget
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Run a turn nobody typed for the reports the rings bring (a wake turn):
|
|
451
|
+
# a continue turn (no prompt bubble) whose first boundary merges them,
|
|
452
|
+
# with origin child:<id8>. A failure goes back to the checkpoint, keeps
|
|
453
|
+
# the rings and pauses wakes until a human's input (a provider outage
|
|
454
|
+
# doesn't loop).
|
|
455
|
+
# @return [Boolean] whether a wake turn ran
|
|
456
|
+
def run_wake_turn
|
|
457
|
+
return false if wake_grace_left.positive?
|
|
458
|
+
|
|
459
|
+
state = wake_state
|
|
460
|
+
budget_notice if state == :budget
|
|
461
|
+
return false unless state == :due
|
|
462
|
+
|
|
463
|
+
reports, = take_child_reports
|
|
464
|
+
# The turn must not generate with nothing to say: the drain gets this
|
|
465
|
+
# exact list at its first boundary.
|
|
466
|
+
return false if reports.empty?
|
|
467
|
+
|
|
468
|
+
@wake_reports = reports
|
|
469
|
+
@wakes_in_a_row += 1
|
|
470
|
+
Log.info(:worker, "delegate_wake_turn", reports: reports.size, in_a_row: @wakes_in_a_row)
|
|
471
|
+
@turn_flow.before_prompt_turn
|
|
472
|
+
run_engine_turn(nil, continue: true, origin: reports.first.origin, max_iterations: IterationLimit.for) do |result, error|
|
|
473
|
+
if error
|
|
474
|
+
note = TurnNote.failed(error.respond_to?(:summary) ? error.summary : error.message)
|
|
475
|
+
@turn_flow.prompt_turn_failed(note: note)
|
|
476
|
+
@wakes_paused = true
|
|
477
|
+
else
|
|
478
|
+
@continue_offer.after_turn(result)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
true
|
|
482
|
+
ensure
|
|
483
|
+
@wake_reports = nil
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def wake_grace_left
|
|
487
|
+
WAKE_START_GRACE - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# The idle loop's sleep: the fallback tick, or less while reports wait
|
|
491
|
+
# out the start grace.
|
|
492
|
+
def idle_wait
|
|
493
|
+
left = wake_grace_left
|
|
494
|
+
left.positive? && wake_state == :due ? [left, @poll_interval].min : @poll_interval
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def max_wakes
|
|
498
|
+
value = Integer(Config.get(ChildRing::MAX_WAKES_KEY), exception: false)
|
|
499
|
+
value&.positive? ? value : ChildRing::MAX_WAKES_DEFAULT
|
|
500
|
+
rescue StandardError
|
|
501
|
+
ChildRing::MAX_WAKES_DEFAULT
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# The wake budget is spent: the parent's UIs hear once that reports
|
|
505
|
+
# wait for the next message.
|
|
506
|
+
def budget_notice
|
|
507
|
+
return if @budget_noticed
|
|
508
|
+
|
|
509
|
+
@budget_noticed = true
|
|
510
|
+
count = SessionInbox.find_ring_files(@session_dir).map { |f| SessionInbox.read_ring(f)&.dig(:child_id) }.uniq.size
|
|
511
|
+
@engine.announce(type: :hook_notice, hook: "delegate", level: :info, between_turns: true,
|
|
512
|
+
text: "#{count} delegate report#{"s" if count != 1} waiting; #{count == 1 ? "it joins" : "they join"} " \
|
|
513
|
+
"your next message (#{max_wakes} turn#{"s" if max_wakes != 1} ran for reports in a row, #{ChildRing::MAX_WAKES_KEY})")
|
|
514
|
+
end
|
|
515
|
+
|
|
387
516
|
def max_iterations(no_interrupt) = IterationLimit.for(no_interrupt: no_interrupt)
|
|
388
517
|
|
|
389
518
|
# @return [Boolean] whether any command ran
|
|
@@ -426,7 +555,7 @@ module Samagotchi
|
|
|
426
555
|
announce_command(command, status: result.status.to_s, output: result.output, changed: Array(result.changed))
|
|
427
556
|
shown.each { |event| @engine.announce(event) }
|
|
428
557
|
end
|
|
429
|
-
save_session unless Array(result.changed).empty?
|
|
558
|
+
save_or_log(:command) { save_session } unless Array(result.changed).empty?
|
|
430
559
|
user_input(command[:client_id]) if resolved
|
|
431
560
|
@continue_offer.after_command(result, resolved: resolved)
|
|
432
561
|
@continue_offer.run_continue_turn(command) if result.resume
|
|
@@ -493,6 +622,10 @@ module Samagotchi
|
|
|
493
622
|
# @yieldparam result [Object, nil] Engine#run_turn's, nil on a failure
|
|
494
623
|
# @yieldparam error [StandardError, nil]
|
|
495
624
|
def run_engine_turn(prompt, **turn_args)
|
|
625
|
+
# Every kind of turn merges steering (a reminder turn too, which may be
|
|
626
|
+
# a fresh worker's first), so the list is this turn's from the start.
|
|
627
|
+
@merged_this_turn = []
|
|
628
|
+
@reporting_turn = reports_to_parent?(turn_args[:origin])
|
|
496
629
|
begin
|
|
497
630
|
@session.status = Session::STATUS_RUNNING
|
|
498
631
|
@session.save(state_dir: @state_dir)
|
|
@@ -515,16 +648,66 @@ module Samagotchi
|
|
|
515
648
|
unless response.nil? || response.strip.empty? || @continue_offer.awaiting?
|
|
516
649
|
SessionInbox.write_output(@session_dir, response)
|
|
517
650
|
end
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
#
|
|
524
|
-
|
|
525
|
-
|
|
651
|
+
save_or_log(:turn) { save_session }
|
|
652
|
+
settle_child_reports(error)
|
|
653
|
+
ring_parent_after_turn
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# The delegate reports this turn took: a kept turn moves the cursors on
|
|
657
|
+
# and deletes their rings; a failed one (rolled back) leaves the rings
|
|
658
|
+
# for the next turn.
|
|
659
|
+
def settle_child_reports(error)
|
|
660
|
+
error ? @child_reports.release : @child_reports.commit
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# A delegate child's turn that was its parent's (#reports_to_parent?)
|
|
664
|
+
# rings the parent, after the save: the parent reads a settled child.
|
|
665
|
+
# A question it rang about that still waits makes the next turn ring
|
|
666
|
+
# too, whoever starts it (the user answering Continue on the web).
|
|
667
|
+
def ring_parent_after_turn
|
|
668
|
+
ChildRing.ring(@session, why: "turn_end", state_dir: @state_dir) if @reporting_turn
|
|
669
|
+
@owes_parent = @asked_parent && !@session.pending_question.nil?
|
|
670
|
+
@asked_parent = false
|
|
671
|
+
@reporting_turn = false
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Whether the turn starting now is a delegate child's parent's: its
|
|
675
|
+
# task (the initial prompt), a follow-up, a continue the parent
|
|
676
|
+
# answered, a reminder, anything but a human typing into the child
|
|
677
|
+
# (ArchiveStore.user_input?), and any turn after a question the parent
|
|
678
|
+
# was rung about.
|
|
679
|
+
def reports_to_parent?(origin)
|
|
680
|
+
return false unless @session.delegate?
|
|
681
|
+
|
|
682
|
+
@initial_turn || @owes_parent || !ArchiveStore.user_input?(origin&.dig(:client_id))
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# The Engine published a question (after the question desk saved it).
|
|
686
|
+
# In a turn that reports to the parent, the model's own question and
|
|
687
|
+
# the step-limit continue ring it; approvals and hooks' questions are
|
|
688
|
+
# the user's, on this session's card.
|
|
689
|
+
def ring_question(event)
|
|
690
|
+
return unless @reporting_turn
|
|
691
|
+
|
|
692
|
+
kind = event.dig(:pending_question, :kind).to_s
|
|
693
|
+
return unless ["", ContinueOffer::KIND].include?(kind)
|
|
694
|
+
|
|
695
|
+
ChildRing.ring(@session, why: "question", state_dir: @state_dir)
|
|
696
|
+
@asked_parent = true
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# A save between turns (the turn's own, a command's, the notes', the
|
|
700
|
+
# first prompt's): what it saves is in memory, so one that fails (disk
|
|
701
|
+
# full, permissions; the next save writes it) is logged and must not
|
|
702
|
+
# take the worker down with it.
|
|
703
|
+
# @param at [Symbol] which save, for the log
|
|
704
|
+
# @return [Boolean] whether it saved
|
|
705
|
+
def save_or_log(at)
|
|
706
|
+
yield
|
|
707
|
+
true
|
|
526
708
|
rescue SystemCallError, IOError => e
|
|
527
|
-
Log.exception(:worker, "save_failed", e)
|
|
709
|
+
Log.exception(:worker, "save_failed", e, at: at)
|
|
710
|
+
false
|
|
528
711
|
end
|
|
529
712
|
|
|
530
713
|
# Back to the conversation before the failed turn, as the REPL does (so
|
|
@@ -580,14 +763,42 @@ module Samagotchi
|
|
|
580
763
|
end
|
|
581
764
|
end
|
|
582
765
|
merged.each { |_prompt, origin| user_input(origin&.dig(:client_id)) }
|
|
583
|
-
|
|
584
|
-
|
|
766
|
+
# Delegate children's reports: never handed back on a failure
|
|
767
|
+
# (their rings stay for the next turn), so not in @merged_this_turn.
|
|
768
|
+
reports, mark = take_child_reports
|
|
769
|
+
unless merged.empty? && reports.empty?
|
|
770
|
+
@engine.announce(type: :input_merged, count: merged.size + reports.size,
|
|
771
|
+
origins: merged.filter_map(&:last) + reports.map(&:origin))
|
|
585
772
|
@merged_this_turn.concat(merged)
|
|
586
773
|
end
|
|
587
774
|
# Each line keeps its sender (Steer.source_for_client): a chi send or
|
|
588
775
|
# a delegating parent's line is saved and shown to the model as theirs.
|
|
589
|
-
merged.map { |prompt, origin| Steer::Line.new(text: prompt, source: Steer.source_for_client(origin&.dig(:client_id))) }
|
|
776
|
+
lines = merged.map { |prompt, origin| Steer::Line.new(text: prompt, source: Steer.source_for_client(origin&.dig(:client_id))) } +
|
|
777
|
+
reports.map(&:line)
|
|
778
|
+
# A wake turn starts with its first line, whoever sent it.
|
|
779
|
+
lines[0] = lines[0].with(mark: mark) if mark && lines.any?
|
|
780
|
+
lines
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# The reports for rings that came in since the turn last looked. A
|
|
785
|
+
# failure to read them must not take the turn's steering with it.
|
|
786
|
+
# A wake turn's first call gets the reports it was started for (their
|
|
787
|
+
# message is the turn's start: turn_start, turn_id, so a reload shows
|
|
788
|
+
# the turn as its own).
|
|
789
|
+
# @return [Array(Array<ChildReports::Report>, Hash|nil)] the reports
|
|
790
|
+
# and the mark of their message
|
|
791
|
+
def take_child_reports
|
|
792
|
+
if @wake_reports
|
|
793
|
+
reports = @wake_reports
|
|
794
|
+
@wake_reports = nil
|
|
795
|
+
return [reports, { turn_start: true, turn_id: @turn_id }.compact]
|
|
590
796
|
end
|
|
797
|
+
|
|
798
|
+
[@child_reports.take, nil]
|
|
799
|
+
rescue StandardError => e
|
|
800
|
+
Log.warn(:worker, "delegate_reports_failed", error: e.class.name, msg: e.message)
|
|
801
|
+
[[], nil]
|
|
591
802
|
end
|
|
592
803
|
|
|
593
804
|
# Check again with the event log held, which the Bridge holds while it
|
|
@@ -688,8 +899,14 @@ module Samagotchi
|
|
|
688
899
|
|
|
689
900
|
# A human's input (ArchiveStore.user_input?) un-archives the session;
|
|
690
901
|
# a delegate's, a plugin's or a reminder's doesn't.
|
|
902
|
+
# It also resets the delegate-report wakes (budget and pause).
|
|
691
903
|
def user_input(client_id)
|
|
692
|
-
|
|
904
|
+
return unless ArchiveStore.user_input?(client_id)
|
|
905
|
+
|
|
906
|
+
ArchiveStore.user_input(@session_id, state_dir: @state_dir)
|
|
907
|
+
@wakes_in_a_row = 0
|
|
908
|
+
@wakes_paused = false
|
|
909
|
+
@budget_noticed = false
|
|
693
910
|
end
|
|
694
911
|
|
|
695
912
|
def stopped_on_disk?
|
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.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -193,6 +193,7 @@ files:
|
|
|
193
193
|
- lib/samagotchi/bundles/system/memory_guide.md
|
|
194
194
|
- lib/samagotchi/bundles/system/self_map.md
|
|
195
195
|
- lib/samagotchi/cancellation_controller.rb
|
|
196
|
+
- lib/samagotchi/child_reports.rb
|
|
196
197
|
- lib/samagotchi/cli/command.rb
|
|
197
198
|
- lib/samagotchi/cli/exit.rb
|
|
198
199
|
- lib/samagotchi/cli/flags.rb
|
|
@@ -301,6 +302,7 @@ files:
|
|
|
301
302
|
- lib/samagotchi/memory_bundle/uninstaller.rb
|
|
302
303
|
- lib/samagotchi/memory_paths.rb
|
|
303
304
|
- lib/samagotchi/model_catalog.rb
|
|
305
|
+
- lib/samagotchi/model_list_store.rb
|
|
304
306
|
- lib/samagotchi/model_overlay.rb
|
|
305
307
|
- lib/samagotchi/model_profile.rb
|
|
306
308
|
- lib/samagotchi/model_ref.rb
|
|
@@ -393,6 +395,7 @@ files:
|
|
|
393
395
|
- lib/samagotchi/tools/builtins.rb
|
|
394
396
|
- lib/samagotchi/tools/cancel_reminder.rb
|
|
395
397
|
- lib/samagotchi/tools/delegate.rb
|
|
398
|
+
- lib/samagotchi/tools/delegate_cursor.rb
|
|
396
399
|
- lib/samagotchi/tools/delegate_relay.rb
|
|
397
400
|
- lib/samagotchi/tools/delegate_result.rb
|
|
398
401
|
- lib/samagotchi/tools/delegate_wait.rb
|