samagotchi 0.159.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 +125 -1
- data/docs/architecture.md +12 -9
- data/docs/cli.md +7 -5
- data/docs/context.md +10 -1
- data/docs/guardrails.md +33 -5
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +28 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/engine.md +7 -1
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +43 -13
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +53 -8
- data/docs/sessions.md +17 -12
- 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 +103 -44
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +57 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +6 -0
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_absorber.rb +6 -3
- data/lib/samagotchi/context_candidate_choice.rb +8 -5
- data/lib/samagotchi/context_candidates.rb +11 -8
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +10 -4
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +25 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/events.rb +71 -0
- data/lib/samagotchi/guardrails/approvals.rb +27 -3
- 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/hook_routes.rb +0 -1
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +86 -18
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
- 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/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +93 -23
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +7 -4
- data/lib/samagotchi/memory_bundle/status.rb +3 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +29 -13
- data/lib/samagotchi/memory_orphans.rb +54 -13
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- 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/plugin/attached_context.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +4 -1
- data/lib/samagotchi/project_command.rb +58 -20
- data/lib/samagotchi/project_id.rb +102 -0
- data/lib/samagotchi/project_id_clash.rb +62 -0
- data/lib/samagotchi/project_init.rb +214 -10
- data/lib/samagotchi/project_scope.rb +11 -2
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +374 -76
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +59 -7
- data/lib/samagotchi/session_access/local.rb +53 -22
- data/lib/samagotchi/session_access/remote.rb +13 -5
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +20 -4
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +162 -24
- 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 +167 -100
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- 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 +129 -0
- 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/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +19 -6
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +9 -4
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -5
- 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 +15 -11
- 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 +61 -11
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +62 -10
- 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 +9 -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
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
require_relative "client_id"
|
|
7
|
+
require_relative "command_intake"
|
|
7
8
|
require_relative "config"
|
|
8
9
|
require_relative "engine"
|
|
9
10
|
require_relative "guardrails/parent_policy"
|
|
@@ -285,6 +286,7 @@ module Samagotchi
|
|
|
285
286
|
end
|
|
286
287
|
prune_tombstones
|
|
287
288
|
settle_interrupted_turn
|
|
289
|
+
settle_stale_status
|
|
288
290
|
# Before the Bridge serves anything: a UI joining a resumed worker's
|
|
289
291
|
# stream gets the session's history and status in its snapshot, not
|
|
290
292
|
# an empty session until the first turn.
|
|
@@ -338,7 +340,7 @@ module Samagotchi
|
|
|
338
340
|
predecessor: @session.continues)
|
|
339
341
|
# Its attached context's notes (ContextSources).
|
|
340
342
|
@context_absorber = ContextAbsorber.new(session_id: @session_id, state_dir: @state_dir,
|
|
341
|
-
project_root: @session.project_root,
|
|
343
|
+
project_root: @session.project_root, project_id: @session.project_id,
|
|
342
344
|
peer_sources: @engine.peer_sources, peer_turns: -> { @wakes.peer_turns })
|
|
343
345
|
# What comes in between turns, and how each kind is saved.
|
|
344
346
|
@inbound = WorkerInbound.new(session: @session, state_dir: @state_dir, session_dir: @session_dir, engine: @engine,
|
|
@@ -349,6 +351,9 @@ module Samagotchi
|
|
|
349
351
|
@bridge.queue_command(line, client_id: client_id, after_file: after_file,
|
|
350
352
|
enqueued_id: enqueued_id, author: author)
|
|
351
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
|
|
352
357
|
# /model's default is the config's; the Engine started
|
|
353
358
|
# on the session's model. nil when there is no default.model.
|
|
354
359
|
@default_model = ModelProfile.default_model_name
|
|
@@ -468,8 +473,8 @@ module Samagotchi
|
|
|
468
473
|
# Attached context's commands, in the background; a new text wakes
|
|
469
474
|
# the loop to absorb it. Not activity: the idle exit stops it.
|
|
470
475
|
@context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
|
|
471
|
-
project_root: @session.project_root,
|
|
472
|
-
on_change: -> { @waker.wake }).start
|
|
476
|
+
project_root: @session.project_root, project_id: @session.project_id,
|
|
477
|
+
cwd: @session.working_directory, on_change: -> { @waker.wake }).start
|
|
473
478
|
@scratch_watch = start_scratch_watch if @session.scratch
|
|
474
479
|
end
|
|
475
480
|
|
|
@@ -694,6 +699,31 @@ module Samagotchi
|
|
|
694
699
|
save_or_log(:interrupted_turn) { save_session }
|
|
695
700
|
end
|
|
696
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
|
+
|
|
697
727
|
# The per-input tombstones (removed, settled, merged) older than
|
|
698
728
|
# SessionInbox::TOMBSTONE_MAX_AGE, once per worker start: they only go
|
|
699
729
|
# with the session dir otherwise.
|
|
@@ -759,14 +789,15 @@ module Samagotchi
|
|
|
759
789
|
end
|
|
760
790
|
|
|
761
791
|
# The first prompt (spawn_session's): a delegate child's task, which is
|
|
762
|
-
# its parent's
|
|
763
|
-
#
|
|
792
|
+
# its parent's. Its turn carries the id spawn_session gave it, so a
|
|
793
|
+
# wait for it (--new --wait) names it, and its sender
|
|
794
|
+
# (WorkerInbound#initial_origin).
|
|
764
795
|
# A Verify fresh child's is a Verify turn on its claim (its verdict is
|
|
765
796
|
# recorded and forwarded to the parent).
|
|
766
797
|
def run_initial_prompt(prompt)
|
|
767
798
|
@initial_turn = true
|
|
768
799
|
fresh = @session.verify_fresh
|
|
769
|
-
run_prompt(WorkerInbound::Prompt.new(text: prompt, origin:
|
|
800
|
+
run_prompt(WorkerInbound::Prompt.new(text: prompt, origin: @inbound.initial_origin,
|
|
770
801
|
anchors: { verify: fresh&.anchor }))
|
|
771
802
|
ensure
|
|
772
803
|
@initial_turn = false
|
|
@@ -1222,6 +1253,14 @@ module Samagotchi
|
|
|
1222
1253
|
end
|
|
1223
1254
|
end
|
|
1224
1255
|
|
|
1256
|
+
# A shell line from a sender that may not run one (CommandIntake: a
|
|
1257
|
+
# delegate's task, a schedule's prompt, a plugin's message): not run,
|
|
1258
|
+
# its command_ran an error with the one refusal text.
|
|
1259
|
+
def shell_refused(command)
|
|
1260
|
+
Log.warn(:worker, "shell_line_refused", client_id: command.client_id)
|
|
1261
|
+
SessionCommands::Result.new(status: :error, output: CommandIntake::REFUSED, changed: [])
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1225
1264
|
# Whether +command+ asks before a costly /thinking or /reasoning-history
|
|
1226
1265
|
# change (CacheConfirm): only a person at a web tab or a TUI sees the
|
|
1227
1266
|
# question; chi send, a delegate, a plugin and a card's action apply it.
|
|
@@ -1242,6 +1281,8 @@ module Samagotchi
|
|
|
1242
1281
|
# command for the line, is an error Result saying so.
|
|
1243
1282
|
# @return [SessionCommands::Result]
|
|
1244
1283
|
def command_result(command)
|
|
1284
|
+
return shell_refused(command) if CommandIntake.refused?(command.line, command.client_id)
|
|
1285
|
+
|
|
1245
1286
|
yield || SessionCommands::Result.new(status: :error, output: "not a session command", changed: [])
|
|
1246
1287
|
rescue StandardError => e
|
|
1247
1288
|
SessionCommands::Result.new(status: :error, output: "#{command.line.split.first}: #{e.message}", changed: [])
|
|
@@ -1577,11 +1618,20 @@ module Samagotchi
|
|
|
1577
1618
|
|
|
1578
1619
|
begin
|
|
1579
1620
|
input = SessionInbox.read_input(claimed_file)
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
|
1585
1635
|
ensure
|
|
1586
1636
|
FileUtils.rm_f(claimed_file)
|
|
1587
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
|
|
@@ -6,9 +6,11 @@ require "time"
|
|
|
6
6
|
|
|
7
7
|
require_relative "child_reports"
|
|
8
8
|
require_relative "client_id"
|
|
9
|
+
require_relative "command_intake"
|
|
9
10
|
require_relative "config"
|
|
10
11
|
require_relative "context_note"
|
|
11
12
|
require_relative "log"
|
|
13
|
+
require_relative "note_move"
|
|
12
14
|
require_relative "objectives"
|
|
13
15
|
require_relative "session"
|
|
14
16
|
require_relative "session_inbox"
|
|
@@ -107,6 +109,21 @@ module Samagotchi
|
|
|
107
109
|
@queue_command = queue_command
|
|
108
110
|
@initial_prompt_taken = false
|
|
109
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
|
+
[]
|
|
110
127
|
end
|
|
111
128
|
|
|
112
129
|
# Add the queued context notes to the conversation (between turns only,
|
|
@@ -118,7 +135,9 @@ module Samagotchi
|
|
|
118
135
|
# starts a turn nor keeps an idle worker up. Of the objectives notes in
|
|
119
136
|
# a batch only the newest goes in: each is a whole list, so the older
|
|
120
137
|
# ones (edits made during a turn) would only be sent for nothing.
|
|
138
|
+
# The predecessor's late notes come first (#adopt_notes).
|
|
121
139
|
def absorb_notes
|
|
140
|
+
adopt_notes
|
|
122
141
|
files = SessionInbox.find_new_note_files(@session_dir)
|
|
123
142
|
return if files.empty? || @stopped.call
|
|
124
143
|
|
|
@@ -196,14 +215,20 @@ module Samagotchi
|
|
|
196
215
|
# spawn_session hands the first prompt over in last_prompt, but
|
|
197
216
|
# last_prompt also records every later turn's prompt (and mark_error's
|
|
198
217
|
# reason), so only a session with no conversation yet has one pending; a
|
|
199
|
-
# 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.
|
|
200
226
|
# @return [String, nil]
|
|
201
227
|
def take_initial_prompt
|
|
202
228
|
return nil if @initial_prompt_taken
|
|
203
229
|
|
|
204
230
|
@initial_prompt_taken = true
|
|
205
|
-
|
|
206
|
-
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)
|
|
207
232
|
|
|
208
233
|
prompt = @session.last_prompt
|
|
209
234
|
@session.last_prompt = ""
|
|
@@ -211,11 +236,24 @@ module Samagotchi
|
|
|
211
236
|
prompt
|
|
212
237
|
end
|
|
213
238
|
|
|
239
|
+
# The first prompt's origin: the turn id spawn_session gave it (a wait
|
|
240
|
+
# for it, --new --wait, names it) and who sent it when no person did
|
|
241
|
+
# (Session#first_turn_client_id: a delegating parent, a schedule). A
|
|
242
|
+
# delegate child's task is its parent's even without the field (one
|
|
243
|
+
# spawned by an older chi, or by a caller that names no sender). NONE
|
|
244
|
+
# for a person's session from before either field.
|
|
245
|
+
# @return [TurnOrigin]
|
|
246
|
+
def initial_origin
|
|
247
|
+
client_id = @session.first_turn_client_id
|
|
248
|
+
client_id ||= ClientId.delegate(@session.parent_id) if @session.delegate? && @session.parent_id
|
|
249
|
+
TurnOrigin.of(client_id: client_id, enqueued_id: @session.first_turn_id)
|
|
250
|
+
end
|
|
251
|
+
|
|
214
252
|
# The first prompt as a command (queued): the session was saved as
|
|
215
253
|
# running for a turn that won't run.
|
|
216
254
|
# @return [Boolean] whether it was one
|
|
217
255
|
def initial_command?(prompt)
|
|
218
|
-
return false unless queue_as_command(prompt,
|
|
256
|
+
return false unless queue_as_command(prompt, initial_origin)
|
|
219
257
|
|
|
220
258
|
@session.status = Session::STATUS_IDLE
|
|
221
259
|
save(:initial_command, unless_stopped: true)
|
|
@@ -231,11 +269,20 @@ module Samagotchi
|
|
|
231
269
|
|
|
232
270
|
begin
|
|
233
271
|
input = SessionInbox.read_input(claimed_file)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
239
286
|
ensure
|
|
240
287
|
FileUtils.rm_f(claimed_file)
|
|
241
288
|
end
|
|
@@ -264,7 +311,11 @@ module Samagotchi
|
|
|
264
311
|
# POST /turn; an unknown /word stays a prompt. Queued: the loop's next
|
|
265
312
|
# pass runs it. A participant's is refused (dropped, with its audit
|
|
266
313
|
# line): commands and shell lines are the owner's (chi web and the
|
|
267
|
-
# Bridge refuse them first; this is the worker's own layer).
|
|
314
|
+
# Bridge refuse them first; this is the worker's own layer). From a
|
|
315
|
+
# sender that may not send commands (CommandIntake.allowed?: a
|
|
316
|
+
# delegate, a schedule, a plugin...) a /word line is a prompt, and a
|
|
317
|
+
# shell line is queued for Worker#run_command_line to refuse, so its
|
|
318
|
+
# sender and the UIs see the refusal as the command's error.
|
|
268
319
|
# @return [Boolean] whether +text+ was one
|
|
269
320
|
def queue_as_command(text, origin, after_file = nil)
|
|
270
321
|
return false unless @engine.command_registry.command?(text.to_s)
|
|
@@ -275,6 +326,7 @@ module Samagotchi
|
|
|
275
326
|
what: "command", reason: "not_permitted")
|
|
276
327
|
return true
|
|
277
328
|
end
|
|
329
|
+
return false unless CommandIntake.allowed?(origin.client_id) || CommandIntake.shell_line?(text)
|
|
278
330
|
|
|
279
331
|
@queue_command.call(text, origin.client_id, after_file, origin.enqueued_id)
|
|
280
332
|
true
|
|
@@ -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?
|