samagotchi 0.160.0 → 0.161.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 +86 -1
- data/docs/architecture.md +7 -3
- data/docs/cli.md +6 -4
- data/docs/context.md +10 -1
- data/docs/guardrails.md +12 -3
- data/docs/internals/bridge.md +21 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +4 -2
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +37 -5
- data/docs/sessions.md +14 -11
- data/docs/testing.md +9 -1
- data/lib/samagotchi/bridge/card_store.rb +31 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
- data/lib/samagotchi/bridge/sse_writer.rb +8 -5
- data/lib/samagotchi/bridge.rb +92 -42
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +56 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +2 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/engine/events.rb +12 -0
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/targets.rb +25 -6
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +59 -12
- data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
- data/lib/samagotchi/memory_bundle/installer.rb +79 -12
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +2 -1
- data/lib/samagotchi/memory_bundle/status.rb +1 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +27 -13
- data/lib/samagotchi/memory_orphans.rb +40 -9
- data/lib/samagotchi/note_command.rb +9 -1
- data/lib/samagotchi/note_delivery.rb +34 -24
- data/lib/samagotchi/note_move.rb +205 -0
- data/lib/samagotchi/objectives_command.rb +3 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/project_init.rb +3 -3
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +286 -48
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +42 -5
- data/lib/samagotchi/session_access/local.rb +45 -17
- data/lib/samagotchi/session_access/remote.rb +11 -4
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +16 -3
- data/lib/samagotchi/session_continue.rb +155 -21
- data/lib/samagotchi/session_delete_command.rb +3 -1
- data/lib/samagotchi/session_inbox.rb +41 -1
- data/lib/samagotchi/session_inbox_delivery.rb +254 -0
- data/lib/samagotchi/session_manager/delivery.rb +66 -12
- data/lib/samagotchi/session_manager/emptiness.rb +7 -2
- data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +4 -1
- data/lib/samagotchi/session_manager/removal.rb +130 -26
- data/lib/samagotchi/session_manager/spawn.rb +164 -104
- data/lib/samagotchi/session_manager.rb +1 -0
- data/lib/samagotchi/session_retention.rb +53 -23
- data/lib/samagotchi/session_setup.rb +27 -7
- data/lib/samagotchi/starting_marker.rb +93 -24
- data/lib/samagotchi/state_format.rb +18 -7
- data/lib/samagotchi/system_one/runs.rb +6 -1
- data/lib/samagotchi/system_one/runs_store.rb +18 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
- data/lib/samagotchi/terminal_ui.rb +2 -3
- data/lib/samagotchi/tools/delegate.rb +9 -4
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +7 -2
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +1 -1
- data/lib/samagotchi/web/public/app.js +2 -7
- data/lib/samagotchi/web/public/chains.js +4 -4
- data/lib/samagotchi/web/public/data.js +18 -2
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/session_fields.js +3 -2
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/update_notice.js +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +12 -8
- data/lib/samagotchi/web/session_control.rb +9 -3
- data/lib/samagotchi/web/session_create.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +50 -5
- data/lib/samagotchi/web/session_summary.rb +5 -2
- data/lib/samagotchi/worker.rb +45 -7
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +41 -8
- data/lib/samagotchi/worker_link.rb +4 -1
- data/lib/samagotchi/worker_presence.rb +106 -0
- data/lib/samagotchi/worker_sidecar.rb +3 -4
- metadata +5 -1
|
@@ -6,6 +6,7 @@ require "time"
|
|
|
6
6
|
|
|
7
7
|
require_relative "../session"
|
|
8
8
|
require_relative "../session_manager"
|
|
9
|
+
require_relative "../session_continue"
|
|
9
10
|
require_relative "../schedules"
|
|
10
11
|
require_relative "../bridge_client"
|
|
11
12
|
require_relative "../worker_link"
|
|
@@ -45,6 +46,7 @@ module Samagotchi
|
|
|
45
46
|
# all sessions this often (a one-shot or an older chi's REPL that
|
|
46
47
|
# opened an old session without saving it has no file signal until its first save).
|
|
47
48
|
FULL_PROBE_INTERVAL = 10.0
|
|
49
|
+
SCHEDULE_TICK_STOP_WAIT = 5.0
|
|
48
50
|
|
|
49
51
|
# What a subscriber gets: `session` with {session: summary} for an
|
|
50
52
|
# upsert, `session_gone` with {id:} for a removal, `chi` with
|
|
@@ -107,6 +109,8 @@ module Samagotchi
|
|
|
107
109
|
@root_cache = {} # folder → project root, for sessions saved before project_root
|
|
108
110
|
@next_full_probe = nil
|
|
109
111
|
@next_schedule_tick = nil
|
|
112
|
+
@schedule_tick_in_flight = false
|
|
113
|
+
@schedule_tick_thread = nil
|
|
110
114
|
@wakeup = Queue.new # stop wakes the tick thread out of its sleep
|
|
111
115
|
@thread = nil
|
|
112
116
|
@stopped = false
|
|
@@ -129,6 +133,8 @@ module Samagotchi
|
|
|
129
133
|
thread = @thread
|
|
130
134
|
@thread = nil
|
|
131
135
|
thread&.join(@scan_interval + 1.0)
|
|
136
|
+
schedule_thread = @monitor.synchronize { @schedule_tick_thread }
|
|
137
|
+
schedule_thread&.join(SCHEDULE_TICK_STOP_WAIT)
|
|
132
138
|
nil
|
|
133
139
|
end
|
|
134
140
|
|
|
@@ -143,6 +149,7 @@ module Samagotchi
|
|
|
143
149
|
def scan
|
|
144
150
|
@monitor.synchronize do
|
|
145
151
|
full = full_probe_due?
|
|
152
|
+
SessionContinue.repair_interrupted(state_dir: @state_dir)
|
|
146
153
|
scan_files
|
|
147
154
|
@sessions.each_key { |id| refresh(id, probe: full) }
|
|
148
155
|
if full
|
|
@@ -320,20 +327,42 @@ module Samagotchi
|
|
|
320
327
|
|
|
321
328
|
# chi web can be the scheduler when the user opts in (schedule.auto_tick):
|
|
322
329
|
# the same tick cron runs, gated by schedule.tick_interval_minutes and
|
|
323
|
-
# sharing schedules.lock, so cron and the hub never both fire.
|
|
330
|
+
# sharing schedules.lock, so cron and the hub never both fire. The scan
|
|
331
|
+
# loop only launches one tick at a time; it does not wait for a firing.
|
|
324
332
|
def schedule_tick
|
|
325
333
|
return unless Config.get("schedule.auto_tick")
|
|
326
334
|
|
|
327
335
|
now = @now.call
|
|
328
|
-
|
|
336
|
+
@monitor.synchronize do
|
|
337
|
+
return if @schedule_tick_in_flight
|
|
338
|
+
return if @next_schedule_tick && now < @next_schedule_tick
|
|
329
339
|
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
@next_schedule_tick = now + (Samagotchi::Schedules.tick_interval_minutes * 60)
|
|
341
|
+
@schedule_tick_in_flight = true
|
|
342
|
+
@schedule_tick_thread = Thread.new { run_schedule_tick }
|
|
343
|
+
end
|
|
332
344
|
rescue StandardError => e
|
|
345
|
+
@monitor.synchronize do
|
|
346
|
+
if @schedule_tick_in_flight && @schedule_tick_thread.nil?
|
|
347
|
+
@schedule_tick_in_flight = false
|
|
348
|
+
@next_schedule_tick = nil
|
|
349
|
+
end
|
|
350
|
+
end
|
|
333
351
|
Log.warn(:web, "schedule_tick_failed", error: e.class.name, msg: e.message)
|
|
334
352
|
nil
|
|
335
353
|
end
|
|
336
354
|
|
|
355
|
+
def run_schedule_tick
|
|
356
|
+
Samagotchi::Schedules.new(state_dir: @state_dir).tick
|
|
357
|
+
rescue StandardError => e
|
|
358
|
+
Log.warn(:web, "schedule_tick_failed", error: e.class.name, msg: e.message)
|
|
359
|
+
ensure
|
|
360
|
+
@monitor.synchronize do
|
|
361
|
+
@schedule_tick_in_flight = false
|
|
362
|
+
@schedule_tick_thread = nil
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
337
366
|
# A change (the first look too, unless it finds nothing) is a `chi`
|
|
338
367
|
# event: chi web's terminal line and the open tabs hear it.
|
|
339
368
|
def check_installed
|
|
@@ -342,10 +371,25 @@ module Samagotchi
|
|
|
342
371
|
|
|
343
372
|
@installed = newest
|
|
344
373
|
emit("chi", chi_state)
|
|
374
|
+
refresh_worker_compat
|
|
345
375
|
rescue StandardError => e
|
|
346
376
|
Log.warn(:web, "installed_check_failed", error: e.class.name, msg: e.message)
|
|
347
377
|
end
|
|
348
378
|
|
|
379
|
+
def refresh_worker_compat
|
|
380
|
+
# An installed-version change can stale a worker without changing its files.
|
|
381
|
+
@sessions.each_value do |entry|
|
|
382
|
+
summary = entry.summary
|
|
383
|
+
next unless summary&.dig(:worker_version)
|
|
384
|
+
|
|
385
|
+
compatibility = Compat.verdict(worker_version: summary[:worker_version], version: @installed || VERSION).to_s
|
|
386
|
+
next if summary[:worker_compat] == compatibility
|
|
387
|
+
|
|
388
|
+
entry.summary = summary.merge(worker_compat: compatibility)
|
|
389
|
+
emit("session", session: entry.summary)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
349
393
|
# The state dir's files: a new id is parsed and emitted, an id gone
|
|
350
394
|
# from disk is dropped, a changed file is re-parsed. Skipped when the
|
|
351
395
|
# dir's mtime says nothing was renamed in or unlinked since.
|
|
@@ -414,7 +458,8 @@ module Samagotchi
|
|
|
414
458
|
|
|
415
459
|
entry.owner = owner
|
|
416
460
|
entry.live = live
|
|
417
|
-
summary = SessionSummary.build(entry.session, owner: owner, session_dir: dir, registry: hub_registry
|
|
461
|
+
summary = SessionSummary.build(entry.session, owner: owner, session_dir: dir, registry: hub_registry,
|
|
462
|
+
compat_version: @installed || VERSION)
|
|
418
463
|
# A new worker (pid) or a new Bridge for the same session is an
|
|
419
464
|
# event for the page even when the summary reads the same: it
|
|
420
465
|
# reconnects its stream on it.
|
|
@@ -11,6 +11,7 @@ require_relative "../effective_model"
|
|
|
11
11
|
require_relative "../host_registry"
|
|
12
12
|
require_relative "../bridge_client"
|
|
13
13
|
require_relative "../worker_link"
|
|
14
|
+
require_relative "../compat"
|
|
14
15
|
require_relative "../session_row"
|
|
15
16
|
require_relative "../bridge/pending_card"
|
|
16
17
|
|
|
@@ -26,7 +27,7 @@ module Samagotchi
|
|
|
26
27
|
WEB_KEYS = %i[id status mode model_name working_directory created_at updated_at last_prompt short_id test_run
|
|
27
28
|
used_memory_names preloaded_memory_names muted_memory_names prompt_notes parent_id delegate
|
|
28
29
|
continues review first_preview title owner recap auto_title ctx_pct tokens memory_index side
|
|
29
|
-
archived project_root bridge_up worker_version worker_features pending_question pending_card
|
|
30
|
+
archived project_root bridge_up worker_version worker_features worker_compat pending_question pending_card
|
|
30
31
|
last_turn verdicts verify_fresh last_verdict].freeze
|
|
31
32
|
|
|
32
33
|
module_function
|
|
@@ -45,9 +46,10 @@ module Samagotchi
|
|
|
45
46
|
# the smaller of that budget and the window, as the live meter
|
|
46
47
|
# does). nil: one is built here; a caller that builds many summaries
|
|
47
48
|
# (the session hub) keeps one and passes it.
|
|
49
|
+
# @param compat_version [String] the newest chi the worker badge compares with
|
|
48
50
|
# @return [Hash] symbol keys
|
|
49
51
|
def build(session, owner:, session_dir:, status: displayed_status(session, owner: owner), root_cache: nil,
|
|
50
|
-
registry: nil)
|
|
52
|
+
registry: nil, compat_version: VERSION)
|
|
51
53
|
link = WorkerLink.resolve(session_dir, unlink: false, owner: owner)
|
|
52
54
|
up = link.live?
|
|
53
55
|
sidecar = up ? link.sidecar : nil
|
|
@@ -116,6 +118,7 @@ module Samagotchi
|
|
|
116
118
|
# nil and [] without one): the page's stale-worker badge.
|
|
117
119
|
worker_version: sidecar&.version,
|
|
118
120
|
worker_features: sidecar ? sidecar.features : [],
|
|
121
|
+
worker_compat: sidecar && Compat.verdict(worker_version: sidecar.version, version: compat_version).to_s,
|
|
119
122
|
# For the tab's notifications (notify.js): an open question, the
|
|
120
123
|
# running turn's open card with actions, and how the last turn
|
|
121
124
|
# ended. A question saved by a worker that died is not open, nor a
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -286,6 +286,7 @@ module Samagotchi
|
|
|
286
286
|
end
|
|
287
287
|
prune_tombstones
|
|
288
288
|
settle_interrupted_turn
|
|
289
|
+
settle_stale_status
|
|
289
290
|
# Before the Bridge serves anything: a UI joining a resumed worker's
|
|
290
291
|
# stream gets the session's history and status in its snapshot, not
|
|
291
292
|
# an empty session until the first turn.
|
|
@@ -350,6 +351,9 @@ module Samagotchi
|
|
|
350
351
|
@bridge.queue_command(line, client_id: client_id, after_file: after_file,
|
|
351
352
|
enqueued_id: enqueued_id, author: author)
|
|
352
353
|
})
|
|
354
|
+
# Notes its predecessor got after a continue that died before its
|
|
355
|
+
# move, or from a writer that knew no head (ChildMove.adopt's twin).
|
|
356
|
+
@inbound.adopt_notes
|
|
353
357
|
# /model's default is the config's; the Engine started
|
|
354
358
|
# on the session's model. nil when there is no default.model.
|
|
355
359
|
@default_model = ModelProfile.default_model_name
|
|
@@ -695,6 +699,31 @@ module Samagotchi
|
|
|
695
699
|
save_or_log(:interrupted_turn) { save_session }
|
|
696
700
|
end
|
|
697
701
|
|
|
702
|
+
# No turn runs yet: a status a turn would be behind is from before this
|
|
703
|
+
# worker, and goes idle under the lock (resume_session writes nothing to
|
|
704
|
+
# the file, which this worker owns): a stopped one with no stop marker
|
|
705
|
+
# (Session.load reads it stopped while the marker is there; a stop that
|
|
706
|
+
# landed before an earlier worker took the lock saved it so), an error,
|
|
707
|
+
# a running whose worker died. Not a new session's running whose first
|
|
708
|
+
# prompt waits (WorkerInbound.initial_prompt_pending?).
|
|
709
|
+
STALE_STATUSES = [Session::STATUS_STOPPED, Session::STATUS_ERROR, Session::STATUS_RUNNING].freeze
|
|
710
|
+
|
|
711
|
+
#
|
|
712
|
+
# Saved whatever the status: the save stamps updated_at, which a resume
|
|
713
|
+
# used to move (an old session woken with no turn, an attach or a web
|
|
714
|
+
# wake, stays young for the sweep and moves up the lists). Not a
|
|
715
|
+
# stopped one: this worker leaves at once, and would write "stopped".
|
|
716
|
+
def settle_stale_status
|
|
717
|
+
return if Session.stopped_marker?(@session_id, state_dir: @state_dir)
|
|
718
|
+
|
|
719
|
+
status = @session.status
|
|
720
|
+
if STALE_STATUSES.include?(status) && !(status == Session::STATUS_RUNNING && WorkerInbound.initial_prompt_pending?(@session))
|
|
721
|
+
@session.status = Session::STATUS_IDLE
|
|
722
|
+
Log.info(:worker, "stale_status_settled", status: status)
|
|
723
|
+
end
|
|
724
|
+
save_or_log(:worker_start) { save_session }
|
|
725
|
+
end
|
|
726
|
+
|
|
698
727
|
# The per-input tombstones (removed, settled, merged) older than
|
|
699
728
|
# SessionInbox::TOMBSTONE_MAX_AGE, once per worker start: they only go
|
|
700
729
|
# with the session dir otherwise.
|
|
@@ -1217,8 +1246,6 @@ module Samagotchi
|
|
|
1217
1246
|
# the cards and notices it showed, held: they follow its command_ran,
|
|
1218
1247
|
# as its output
|
|
1219
1248
|
def run_command_line(command)
|
|
1220
|
-
return [shell_refused(command), []] if CommandIntake.refused?(command.line, command.client_id)
|
|
1221
|
-
|
|
1222
1249
|
@engine.holding_announcements do
|
|
1223
1250
|
command_result(command) do
|
|
1224
1251
|
@commands.run(command.line, confirm: confirm?(command), as_parent: as_parent?(command), user_ui: user_ui?(command))
|
|
@@ -1254,6 +1281,8 @@ module Samagotchi
|
|
|
1254
1281
|
# command for the line, is an error Result saying so.
|
|
1255
1282
|
# @return [SessionCommands::Result]
|
|
1256
1283
|
def command_result(command)
|
|
1284
|
+
return shell_refused(command) if CommandIntake.refused?(command.line, command.client_id)
|
|
1285
|
+
|
|
1257
1286
|
yield || SessionCommands::Result.new(status: :error, output: "not a session command", changed: [])
|
|
1258
1287
|
rescue StandardError => e
|
|
1259
1288
|
SessionCommands::Result.new(status: :error, output: "#{command.line.split.first}: #{e.message}", changed: [])
|
|
@@ -1589,11 +1618,20 @@ module Samagotchi
|
|
|
1589
1618
|
|
|
1590
1619
|
begin
|
|
1591
1620
|
input = SessionInbox.read_input(claimed_file)
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1621
|
+
enqueued_id = input.origin.enqueued_id
|
|
1622
|
+
if SessionInbox.input_taken?(@session_dir, @session, enqueued_id) || !@engine.claim_enqueued_id(enqueued_id)
|
|
1623
|
+
Log.info(:worker, "duplicate_input_dropped", enqueued_id: enqueued_id)
|
|
1624
|
+
next
|
|
1625
|
+
end
|
|
1626
|
+
begin
|
|
1627
|
+
prompt = input.prompt.to_s.strip
|
|
1628
|
+
# Which turn took it, on disk before its file goes (a wait for
|
|
1629
|
+
# it reads this, not an inference: ReplyWait::Poll#follow_merge).
|
|
1630
|
+
SessionInbox.mark_merged(@session_dir, enqueued_id, into: @turn_enqueued_id) unless prompt.empty?
|
|
1631
|
+
prompt.empty? ? nil : [prompt, input.origin]
|
|
1632
|
+
ensure
|
|
1633
|
+
@engine.finish_enqueued_id(enqueued_id)
|
|
1634
|
+
end
|
|
1597
1635
|
ensure
|
|
1598
1636
|
FileUtils.rm_f(claimed_file)
|
|
1599
1637
|
end
|
|
@@ -93,7 +93,7 @@ module Samagotchi
|
|
|
93
93
|
# wakes a new worker, as after an idle exit.
|
|
94
94
|
exit(0) if result == :stopped
|
|
95
95
|
if result == :signal
|
|
96
|
-
|
|
96
|
+
respawn(session_id, state_dir: sd, session_dir: session_dir, result: result) unless SessionInbox.find_new_input_files(session_dir).empty?
|
|
97
97
|
exit(0)
|
|
98
98
|
end
|
|
99
99
|
exit(1) if result == :crashed
|
|
@@ -103,21 +103,31 @@ module Samagotchi
|
|
|
103
103
|
# A restart starts its successor here, on the newest chi installed
|
|
104
104
|
# (worker_command), once this one's lock is free.
|
|
105
105
|
# Delegate reports that rang while it left wake one too.
|
|
106
|
-
if result == :restart
|
|
107
|
-
|
|
108
|
-
# the stop), and the reminders go with it.
|
|
109
|
-
ReminderCarry.remove(session_dir)
|
|
110
|
-
Log.info(:worker, "restart_dropped", why: :stopped)
|
|
111
|
-
elsif result == :restart ||
|
|
112
|
-
(%i[idle_exit exit_requested].include?(result) &&
|
|
106
|
+
if result == :restart ||
|
|
107
|
+
(%i[idle_exit exit_requested].include?(result) &&
|
|
113
108
|
(!SessionInbox.find_new_input_files(session_dir).empty? || worker.wake_due?))
|
|
114
|
-
|
|
109
|
+
respawn(session_id, state_dir: sd, session_dir: session_dir, result: result)
|
|
115
110
|
elsif worker.discard?
|
|
116
111
|
discard_left_session(session_id, state_dir: sd, default_model: worker.default_model)
|
|
117
112
|
end
|
|
118
113
|
result
|
|
119
114
|
end
|
|
120
115
|
|
|
116
|
+
# Start this session's next worker as this one leaves (+result+), on
|
|
117
|
+
# the session inbox's leave row (SessionInbox::WakePolicy), read on a
|
|
118
|
+
# fresh WorkerPresence now the lock is free: a stop that landed as it
|
|
119
|
+
# left wins for every way out (resume_session would clear it; a
|
|
120
|
+
# restart's reminders go with it), and a worker that took the session
|
|
121
|
+
# since, or one being started, gets no second one. What waits (input,
|
|
122
|
+
# rings) stays on disk for the next start.
|
|
123
|
+
private def respawn(session_id, state_dir:, session_dir:, result:)
|
|
124
|
+
decided = SessionInbox::WakePolicy.for(:leave).decide(WorkerPresence.read(session_id, state_dir: state_dir))
|
|
125
|
+
return SessionManager.resume_session(session_id, state_dir: state_dir) if decided == :start
|
|
126
|
+
|
|
127
|
+
ReminderCarry.remove(session_dir) if result == :restart && decided == :stopped
|
|
128
|
+
Log.info(:worker, result == :restart ? "restart_dropped" : "respawn_dropped", why: decided, reason: result)
|
|
129
|
+
end
|
|
130
|
+
|
|
121
131
|
# A spawned worker leaves its spawner's process tree: a killer that
|
|
122
132
|
# walks the tree by parent pid (a terminal closing, a tool's teardown
|
|
123
133
|
# of a `chi send --wait` it ran in the background) no longer reaches it.
|
|
@@ -125,8 +135,9 @@ module Samagotchi
|
|
|
125
135
|
# forks the worker, which starts a session of its own (setsid: no
|
|
126
136
|
# controlling terminal, no SIGHUP from one) and returns to run the loop;
|
|
127
137
|
# this one waits until the worker holds the session's OwnerLock, or has
|
|
128
|
-
# exited, or WORKER_START_GRACE passed, and exits:
|
|
129
|
-
#
|
|
138
|
+
# exited, or WORKER_START_GRACE passed, and exits: this process (the
|
|
139
|
+
# pid the session's starting marker names, StartingMarker) lives
|
|
140
|
+
# exactly while the worker starts (WorkerPresence#starting?). The
|
|
130
141
|
# worker is reparented to init (launchd), which reaps it.
|
|
131
142
|
private def detach_from_spawner(session_dir)
|
|
132
143
|
worker = Process.fork
|
|
@@ -10,6 +10,7 @@ require_relative "command_intake"
|
|
|
10
10
|
require_relative "config"
|
|
11
11
|
require_relative "context_note"
|
|
12
12
|
require_relative "log"
|
|
13
|
+
require_relative "note_move"
|
|
13
14
|
require_relative "objectives"
|
|
14
15
|
require_relative "session"
|
|
15
16
|
require_relative "session_inbox"
|
|
@@ -108,6 +109,21 @@ module Samagotchi
|
|
|
108
109
|
@queue_command = queue_command
|
|
109
110
|
@initial_prompt_taken = false
|
|
110
111
|
@context_wake_waiting = false
|
|
112
|
+
# The link this one continues, when its notes may still come here
|
|
113
|
+
# (NoteMove.adoption): one listing, at the start.
|
|
114
|
+
@note_adoption = NoteMove.adoption(session, state_dir: state_dir)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Take the notes written to the link this session continues since it
|
|
118
|
+
# was continued (NoteMove::Adoption): a writer that resolved the old id
|
|
119
|
+
# just before this link was saved, an older chi's. Each pass, before
|
|
120
|
+
# the notes are read; a failure is logged, the pass goes on.
|
|
121
|
+
# @return [Array<String>] the note files adopted
|
|
122
|
+
def adopt_notes
|
|
123
|
+
@note_adoption ? @note_adoption.call : []
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
Log.warn(:worker, "notes_adopt_failed", sid: @session.id[0, 8], error: e.class.name, msg: e.message)
|
|
126
|
+
[]
|
|
111
127
|
end
|
|
112
128
|
|
|
113
129
|
# Add the queued context notes to the conversation (between turns only,
|
|
@@ -119,7 +135,9 @@ module Samagotchi
|
|
|
119
135
|
# starts a turn nor keeps an idle worker up. Of the objectives notes in
|
|
120
136
|
# a batch only the newest goes in: each is a whole list, so the older
|
|
121
137
|
# ones (edits made during a turn) would only be sent for nothing.
|
|
138
|
+
# The predecessor's late notes come first (#adopt_notes).
|
|
122
139
|
def absorb_notes
|
|
140
|
+
adopt_notes
|
|
123
141
|
files = SessionInbox.find_new_note_files(@session_dir)
|
|
124
142
|
return if files.empty? || @stopped.call
|
|
125
143
|
|
|
@@ -197,14 +215,20 @@ module Samagotchi
|
|
|
197
215
|
# spawn_session hands the first prompt over in last_prompt, but
|
|
198
216
|
# last_prompt also records every later turn's prompt (and mark_error's
|
|
199
217
|
# reason), so only a session with no conversation yet has one pending; a
|
|
200
|
-
# resumed session must not replay its last turn.
|
|
218
|
+
# resumed session must not replay its last turn. A context note may
|
|
219
|
+
# have come before the first prompt ran.
|
|
220
|
+
# @param session [Session]
|
|
221
|
+
def self.initial_prompt_pending?(session)
|
|
222
|
+
session.messages.all? { |m| ContextNote.note?(m) } && !session.last_prompt.to_s.strip.empty?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# The first prompt (.initial_prompt_pending?), taken once.
|
|
201
226
|
# @return [String, nil]
|
|
202
227
|
def take_initial_prompt
|
|
203
228
|
return nil if @initial_prompt_taken
|
|
204
229
|
|
|
205
230
|
@initial_prompt_taken = true
|
|
206
|
-
|
|
207
|
-
return nil unless @session.messages.all? { |m| ContextNote.note?(m) } && !@session.last_prompt.to_s.strip.empty?
|
|
231
|
+
return nil unless self.class.initial_prompt_pending?(@session)
|
|
208
232
|
|
|
209
233
|
prompt = @session.last_prompt
|
|
210
234
|
@session.last_prompt = ""
|
|
@@ -245,11 +269,20 @@ module Samagotchi
|
|
|
245
269
|
|
|
246
270
|
begin
|
|
247
271
|
input = SessionInbox.read_input(claimed_file)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
272
|
+
enqueued_id = input.origin.enqueued_id
|
|
273
|
+
if SessionInbox.input_taken?(@session_dir, @session, enqueued_id) || !@engine.claim_enqueued_id(enqueued_id)
|
|
274
|
+
Log.info(:worker, "duplicate_input_dropped", enqueued_id: enqueued_id)
|
|
275
|
+
return
|
|
276
|
+
end
|
|
277
|
+
begin
|
|
278
|
+
return if input.prompt.to_s.strip.empty?
|
|
279
|
+
return if input.images.empty? && queue_as_command(input.prompt, input.origin, File.basename(input_file))
|
|
280
|
+
|
|
281
|
+
yield Prompt.new(text: input.prompt, origin: input.origin, no_interrupt: input.no_interrupt, images: input.images,
|
|
282
|
+
delivery: input.delivery, anchors: input.anchors)
|
|
283
|
+
ensure
|
|
284
|
+
@engine.finish_enqueued_id(enqueued_id)
|
|
285
|
+
end
|
|
253
286
|
ensure
|
|
254
287
|
FileUtils.rm_f(claimed_file)
|
|
255
288
|
end
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "time"
|
|
5
5
|
require_relative "owner_lock"
|
|
6
6
|
require_relative "version"
|
|
7
|
+
require_relative "compat"
|
|
7
8
|
require_relative "worker_sidecar"
|
|
8
9
|
require_relative "bridge_client"
|
|
9
10
|
require_relative "chi_invocation"
|
|
@@ -137,7 +138,9 @@ module Samagotchi
|
|
|
137
138
|
return Starting.new(owner: owner, stuck: false) unless owner.worker?
|
|
138
139
|
return Leaving.new(owner: owner) if owner.leaving?
|
|
139
140
|
return Starting.new(owner: owner, stuck: stuck?(owner)) unless ours?(sidecar, owner)
|
|
140
|
-
|
|
141
|
+
if Compat.verdict(worker_version: sidecar.version, worker_protocol: sidecar.protocol, protocol: PROTOCOL) == :refused
|
|
142
|
+
return Skew.new(sidecar: sidecar, owner: owner, reason: :protocol)
|
|
143
|
+
end
|
|
141
144
|
|
|
142
145
|
Live.new(sidecar: sidecar, owner: owner)
|
|
143
146
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "session"
|
|
4
|
+
require_relative "owner_lock"
|
|
5
|
+
require_relative "worker_link"
|
|
6
|
+
require_relative "starting_marker"
|
|
7
|
+
require_relative "archive_store"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# Whether a session's worker is there, coming or held off, read once for
|
|
11
|
+
# one decision: the one reader of the liveness facts that the deciders
|
|
12
|
+
# ("start a worker for this session?", "is one up?") combine. Each fact is
|
|
13
|
+
# read from disk by WorkerPresence.read; the predicates are named after what
|
|
14
|
+
# the deciders ask, and each decider keeps its own subset and order (the
|
|
15
|
+
# session inbox's WakePolicy is the table of most of them).
|
|
16
|
+
#
|
|
17
|
+
# "Stopped" is #stop_marked?, the `<id>/stopped` marker (a `chi sessions
|
|
18
|
+
# stop`; a resume removes it): no decider reads the session file's status
|
|
19
|
+
# (turn state). Nor is the file read here: the one decider that needs it
|
|
20
|
+
# (a scratch session, WakePolicy#decide) reads it last, itself.
|
|
21
|
+
#
|
|
22
|
+
# @!attribute session_id [String]
|
|
23
|
+
# @!attribute exists [Boolean] its session file is there (Session.exist?)
|
|
24
|
+
# @!attribute owner [OwnerLock::Owner, nil] the owner lock's holder: a
|
|
25
|
+
# worker, a one-turn chi run (kind tui) or nil when free
|
|
26
|
+
# @!attribute stop_marked [Boolean] the `stopped` marker (Session.stopped_marker?)
|
|
27
|
+
# @!attribute starting_marked [Boolean] the `starting` marker, fresh
|
|
28
|
+
# (StartingMarker.fresh?): a worker is being started for it (every spawn
|
|
29
|
+
# writes it, until the worker holds the lock)
|
|
30
|
+
# @!attribute archived [Boolean] ArchiveStore.archived?
|
|
31
|
+
# @!attribute link [WorkerLink::Live, WorkerLink::Starting, WorkerLink::Leaving,
|
|
32
|
+
# WorkerLink::None, WorkerLink::Stale, WorkerLink::Skew, nil] the Bridge
|
|
33
|
+
# outcome, when read (+link:+), else nil
|
|
34
|
+
WorkerPresence = Data.define(:session_id, :exists, :owner, :stop_marked, :starting_marked, :archived, :link) do
|
|
35
|
+
# Read the facts for +session_id+ now.
|
|
36
|
+
# @param state_dir [String, nil] nil: Session.default_state_dir
|
|
37
|
+
# @param owner [OwnerLock::Owner, nil, :read, #call] the lock's holder as
|
|
38
|
+
# the caller already read it (SessionManager.session_owner, a stand-in
|
|
39
|
+
# manager's), or a reader of it called only for a valid id; :read reads
|
|
40
|
+
# it here (OwnerLock.owner)
|
|
41
|
+
# @param link [nil, :look, :unlink] also resolve the WorkerLink:
|
|
42
|
+
# :look leaves a stale sidecar, :unlink removes it (WorkerLink.resolve,
|
|
43
|
+
# given the owner read here or by the caller: one moment's snapshot)
|
|
44
|
+
# @return [WorkerPresence]
|
|
45
|
+
def self.read(session_id, state_dir: nil, owner: :read, link: nil)
|
|
46
|
+
return absent(session_id, link: link) unless Session.valid_id?(session_id)
|
|
47
|
+
|
|
48
|
+
owner = owner.call if owner.respond_to?(:call)
|
|
49
|
+
sd = state_dir || Session.default_state_dir
|
|
50
|
+
dir = Session.session_dir(session_id, state_dir: sd)
|
|
51
|
+
owner = OwnerLock.owner(dir) if owner == :read
|
|
52
|
+
new(session_id: session_id,
|
|
53
|
+
exists: Session.exist?(session_id, state_dir: sd),
|
|
54
|
+
owner: owner,
|
|
55
|
+
stop_marked: Session.stopped_marker?(session_id, state_dir: sd),
|
|
56
|
+
starting_marked: StartingMarker.fresh?(session_id, state_dir: sd),
|
|
57
|
+
archived: ArchiveStore.archived?(dir),
|
|
58
|
+
link: link && WorkerLink.resolve(dir, unlink: link == :unlink, owner: owner))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# An id that isn't one names no session: nothing on disk is read
|
|
62
|
+
# (Session.session_dir would raise), and a link asked for is None.
|
|
63
|
+
# @return [WorkerPresence]
|
|
64
|
+
def self.absent(session_id, link: nil)
|
|
65
|
+
new(session_id: session_id, exists: false, owner: nil, stop_marked: false, starting_marked: false,
|
|
66
|
+
archived: false, link: link && WorkerLink::None.new)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialize(session_id:, exists:, owner:, stop_marked:, starting_marked:, archived:, link: nil) = super
|
|
70
|
+
|
|
71
|
+
def exists? = exists
|
|
72
|
+
def archived? = archived
|
|
73
|
+
|
|
74
|
+
# Someone holds the owner lock: a worker or a one-turn chi run.
|
|
75
|
+
def owned? = !owner.nil?
|
|
76
|
+
|
|
77
|
+
# A one-turn chi run (`chi -p X --non-interactive`, kind tui) holds it:
|
|
78
|
+
# nothing else may resume, stop or delete it.
|
|
79
|
+
def tui_owned? = owner&.tui? == true
|
|
80
|
+
|
|
81
|
+
# The `<id>/stopped` marker: a user's stop, not the turn's status.
|
|
82
|
+
def stop_marked? = stop_marked
|
|
83
|
+
|
|
84
|
+
# A worker is being started for it: the fresh `starting` marker
|
|
85
|
+
# (StartingMarker) a spawn writes until its worker holds the lock.
|
|
86
|
+
def starting? = starting_marked
|
|
87
|
+
|
|
88
|
+
# A worker being started (#starting?, from any process) may not hold the
|
|
89
|
+
# lock yet, and no stop came since: a stop cancels the hold-off (a
|
|
90
|
+
# resume after it clears the stop and spawns; the starting worker and
|
|
91
|
+
# the new one race the lock, one wins). Of resumes after one stop, one
|
|
92
|
+
# spawns: resume_session's claim takes over only a start marked before
|
|
93
|
+
# the stop.
|
|
94
|
+
def start_pending? = starting? && !stop_marked?
|
|
95
|
+
|
|
96
|
+
# resume_session's hold-off, after the owner: a start pending.
|
|
97
|
+
def resume_held_off? = start_pending?
|
|
98
|
+
|
|
99
|
+
# The Bridge answers (WorkerLink::Live). False when the link wasn't read.
|
|
100
|
+
def live? = link&.live? == true
|
|
101
|
+
|
|
102
|
+
# A worker holds the lock, Bridge or not (Live, Starting, Leaving, Skew).
|
|
103
|
+
# False when the link wasn't read.
|
|
104
|
+
def worker_owned? = link&.worker_owned? == true
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -8,8 +8,7 @@ module Samagotchi
|
|
|
8
8
|
# A worker's bridge.json in its session directory: how clients reach its
|
|
9
9
|
# Bridge (port, bind), when it started (a new worker behind the same
|
|
10
10
|
# "worker" owner), the chi it runs (chi update, the web's stale-worker
|
|
11
|
-
# badge),
|
|
12
|
-
# what else it can do (Bridge::FEATURES), and its Bridge's epoch (the
|
|
11
|
+
# badge), what else it can do (Bridge::FEATURES), and its Bridge's epoch (the
|
|
13
12
|
# one worker identity: the epoch in every event id, so a remote client
|
|
14
13
|
# tells a restarted worker apart; nil from a worker before it), and its
|
|
15
14
|
# Bridge's secret (every request names it in X-Chi-Bridge, Bridge#secret_ok?;
|
|
@@ -57,12 +56,12 @@ module Samagotchi
|
|
|
57
56
|
|
|
58
57
|
# Written whole or not at all, readable by this user only (0600 from the
|
|
59
58
|
# moment the file exists, AtomicFile: no other account learns the
|
|
60
|
-
# worker's port or its secret from it);
|
|
59
|
+
# worker's port or its secret from it); epoch, secret, pid
|
|
61
60
|
# and protocol only when they name one.
|
|
62
61
|
def write(session_dir)
|
|
63
62
|
FileUtils.mkdir_p(session_dir)
|
|
64
63
|
record = to_h.transform_keys(&:to_s)
|
|
65
|
-
record.delete("input_format")
|
|
64
|
+
record.delete("input_format")
|
|
66
65
|
record.delete("epoch") if epoch.nil?
|
|
67
66
|
record.delete("secret") if secret.nil?
|
|
68
67
|
record.delete("pid") if pid.nil?
|
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.161.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -317,6 +317,7 @@ files:
|
|
|
317
317
|
- lib/samagotchi/command_intake.rb
|
|
318
318
|
- lib/samagotchi/command_steps.rb
|
|
319
319
|
- lib/samagotchi/commands/registry.rb
|
|
320
|
+
- lib/samagotchi/compat.rb
|
|
320
321
|
- lib/samagotchi/composer_route.rb
|
|
321
322
|
- lib/samagotchi/config.rb
|
|
322
323
|
- lib/samagotchi/config/restart_watch.rb
|
|
@@ -556,6 +557,7 @@ files:
|
|
|
556
557
|
- lib/samagotchi/muted_memories.rb
|
|
557
558
|
- lib/samagotchi/note_command.rb
|
|
558
559
|
- lib/samagotchi/note_delivery.rb
|
|
560
|
+
- lib/samagotchi/note_move.rb
|
|
559
561
|
- lib/samagotchi/objectives.rb
|
|
560
562
|
- lib/samagotchi/objectives_audit.rb
|
|
561
563
|
- lib/samagotchi/objectives_check.rb
|
|
@@ -668,6 +670,7 @@ files:
|
|
|
668
670
|
- lib/samagotchi/session_delete_command.rb
|
|
669
671
|
- lib/samagotchi/session_field.rb
|
|
670
672
|
- lib/samagotchi/session_inbox.rb
|
|
673
|
+
- lib/samagotchi/session_inbox_delivery.rb
|
|
671
674
|
- lib/samagotchi/session_manager.rb
|
|
672
675
|
- lib/samagotchi/session_manager/delivery.rb
|
|
673
676
|
- lib/samagotchi/session_manager/emptiness.rb
|
|
@@ -1113,6 +1116,7 @@ files:
|
|
|
1113
1116
|
- lib/samagotchi/worker_idle_exit.rb
|
|
1114
1117
|
- lib/samagotchi/worker_inbound.rb
|
|
1115
1118
|
- lib/samagotchi/worker_link.rb
|
|
1119
|
+
- lib/samagotchi/worker_presence.rb
|
|
1116
1120
|
- lib/samagotchi/worker_sidecar.rb
|
|
1117
1121
|
- lib/samagotchi/worker_signals.rb
|
|
1118
1122
|
- lib/samagotchi/worker_task_wakes.rb
|