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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require_relative "atomic_file"
|
|
4
7
|
require_relative "session"
|
|
5
8
|
require_relative "session_setup"
|
|
6
9
|
require_relative "session_inbox"
|
|
@@ -9,6 +12,7 @@ require_relative "objectives"
|
|
|
9
12
|
require_relative "child_move"
|
|
10
13
|
require_relative "starting_marker"
|
|
11
14
|
require_relative "context_move"
|
|
15
|
+
require_relative "note_move"
|
|
12
16
|
require_relative "archive_store"
|
|
13
17
|
require_relative "recap_store"
|
|
14
18
|
require_relative "bridge_client"
|
|
@@ -56,6 +60,8 @@ module Samagotchi
|
|
|
56
60
|
RECAP_WAIT = 30.0
|
|
57
61
|
# In the previous link's folder: one continue of a session at a time.
|
|
58
62
|
LOCK = "continue.lock"
|
|
63
|
+
# A durable record in the previous link's folder, written before archive.
|
|
64
|
+
CONTINUE_INTENT = "continue.json"
|
|
59
65
|
# How often a last:<id> continue looks again when another continue
|
|
60
66
|
# moved the chain's end while it waited for the lock.
|
|
61
67
|
TRIES = 3
|
|
@@ -67,6 +73,29 @@ module Samagotchi
|
|
|
67
73
|
# (ChildMove::Plan) and the new link's model (ModelChoice).
|
|
68
74
|
Checked = Data.define(:plan, :choice)
|
|
69
75
|
|
|
76
|
+
# Repair an interrupted continue before showing sessions or starting
|
|
77
|
+
# another continue. An active continue holds LOCK, so its intent is left
|
|
78
|
+
# alone; after a crash, an unsaved successor means the previous link is
|
|
79
|
+
# unarchived.
|
|
80
|
+
def self.repair_interrupted(state_dir:)
|
|
81
|
+
return unless Dir.exist?(state_dir)
|
|
82
|
+
|
|
83
|
+
paths = Dir.glob(File.join(state_dir, "*", CONTINUE_INTENT))
|
|
84
|
+
return if paths.empty?
|
|
85
|
+
|
|
86
|
+
sessions = Session.list(state_dir: state_dir, include_archived: true)
|
|
87
|
+
by_id = sessions.to_h { |session| [session.id, session] }
|
|
88
|
+
paths.each do |path|
|
|
89
|
+
id = File.basename(File.dirname(path))
|
|
90
|
+
session = by_id[id]
|
|
91
|
+
if session
|
|
92
|
+
repair_intent(session, state_dir, path, sessions)
|
|
93
|
+
else
|
|
94
|
+
quarantine_intent(path, id, message: "session file could not be loaded")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
70
99
|
# Start the next link of a chain: a new session that continues
|
|
71
100
|
# +id_or_ref+ (Session#continues) in its folder, on its model (the name
|
|
72
101
|
# it was typed as, so an alias holds) and with its own setup (SessionSetup),
|
|
@@ -91,6 +120,9 @@ module Samagotchi
|
|
|
91
120
|
# @param prompt [String, nil] its first message (nil: it starts idle)
|
|
92
121
|
# @param preview [String, nil] what the lists show before the first turn;
|
|
93
122
|
# by default the prompt, else the previous link's preview
|
|
123
|
+
# @param client_id [String, nil] who sends +prompt+ when no person does
|
|
124
|
+
# (a schedule's chain firing: ClientId.schedule), the first turn's
|
|
125
|
+
# origin (spawn_session's client_id:)
|
|
94
126
|
# @return [Session] the new link (#model_warning as spawn_session's,
|
|
95
127
|
# #moved_children: the delegates it took over, #moved_context: the
|
|
96
128
|
# attached context it took (ContextMove), #opener:
|
|
@@ -99,8 +131,10 @@ module Samagotchi
|
|
|
99
131
|
# @raise [Refused] continued already, delegates open that can't
|
|
100
132
|
# move, or its folder is gone
|
|
101
133
|
# @raise [ArchiveRefused, OwnedByTUI] as archive_session
|
|
102
|
-
def self.run(id_or_ref, prompt: nil, preview: nil, model: nil, state_dir: nil, recap_wait: RECAP_WAIT, archive_wait: 5
|
|
134
|
+
def self.run(id_or_ref, prompt: nil, preview: nil, model: nil, state_dir: nil, recap_wait: RECAP_WAIT, archive_wait: 5,
|
|
135
|
+
client_id: nil)
|
|
103
136
|
sd = state_dir || Session.default_state_dir
|
|
137
|
+
repair_interrupted(state_dir: sd)
|
|
104
138
|
TRIES.times do |try|
|
|
105
139
|
id = SessionChain.resolve(id_or_ref, state_dir: sd)
|
|
106
140
|
result = with_lock(id, sd) do
|
|
@@ -108,7 +142,7 @@ module Samagotchi
|
|
|
108
142
|
next :moved if follow && SessionChain.next_of(id, state_dir: sd)
|
|
109
143
|
|
|
110
144
|
run_locked(id, prompt: prompt, preview: preview, model: model, state_dir: sd, recap_wait: recap_wait,
|
|
111
|
-
archive_wait: archive_wait)
|
|
145
|
+
archive_wait: archive_wait, client_id: client_id)
|
|
112
146
|
end
|
|
113
147
|
return result unless result == :moved
|
|
114
148
|
end
|
|
@@ -123,7 +157,7 @@ module Samagotchi
|
|
|
123
157
|
end
|
|
124
158
|
end
|
|
125
159
|
|
|
126
|
-
private_class_method def self.run_locked(id, prompt:, preview:, model:, state_dir:, recap_wait:, archive_wait:)
|
|
160
|
+
private_class_method def self.run_locked(id, prompt:, preview:, model:, state_dir:, recap_wait:, archive_wait:, client_id:)
|
|
127
161
|
previous = Session.load(id, state_dir: state_dir)
|
|
128
162
|
checked = check!(previous, state_dir, model: model)
|
|
129
163
|
plan = checked.plan
|
|
@@ -136,24 +170,100 @@ module Samagotchi
|
|
|
136
170
|
already = [id, *SessionManager.descendant_ids(id, state_dir, except: plan.move)].select do |sid|
|
|
137
171
|
ArchiveStore.archived?(Session.session_dir(sid, state_dir: state_dir))
|
|
138
172
|
end
|
|
173
|
+
successor_id = SecureRandom.uuid
|
|
174
|
+
intent_path = continue_intent_path(id, state_dir)
|
|
175
|
+
intent_data = { successor_id: successor_id, previously_archived: already }
|
|
176
|
+
AtomicFile.write(intent_path, "#{JSON.generate(intent_data)}\n", perm: 0o600)
|
|
139
177
|
archive = SessionManager.archive_session(id, state_dir: state_dir, wait: archive_wait, except: plan.move)
|
|
140
178
|
archived = archive.archived - already
|
|
179
|
+
intent_data[:archived_ids] = archived
|
|
180
|
+
AtomicFile.write(intent_path, "#{JSON.generate(intent_data)}\n", perm: 0o600)
|
|
181
|
+
# The notes the new link took from this one (NoteMove.move).
|
|
182
|
+
notes = []
|
|
141
183
|
begin
|
|
142
|
-
spawn_continuation(previous, prompt: prompt, preview: preview, choice: checked.choice, state_dir: state_dir,
|
|
143
|
-
|
|
144
|
-
rescue StandardError
|
|
184
|
+
link = spawn_continuation(previous, prompt: prompt, preview: preview, choice: checked.choice, state_dir: state_dir,
|
|
185
|
+
move: plan.move, client_id: client_id, notes: notes, session_id: successor_id)
|
|
186
|
+
rescue StandardError, Interrupt
|
|
145
187
|
# Nothing started: the delegates go back, what this archived goes
|
|
146
188
|
# back to the lists, and a new link saved before its worker failed
|
|
147
189
|
# to spawn goes (left, it would hold the chain: "continued already",
|
|
148
|
-
# last: to it).
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
discard_failed_link(id, state_dir)
|
|
152
|
-
ensure
|
|
153
|
-
archived.each { |sid| ArchiveStore.unarchive(sid, state_dir: state_dir) }
|
|
154
|
-
wake_for_rings(id, state_dir) unless plan.move.empty?
|
|
190
|
+
# last: to it), its notes given back first.
|
|
191
|
+
undo_failed_continue(id, state_dir, moved: plan.move, archived: archived) do
|
|
192
|
+
discard_failed_link(id, state_dir, notes: notes)
|
|
155
193
|
end
|
|
194
|
+
FileUtils.rm_f(intent_path)
|
|
156
195
|
raise
|
|
196
|
+
else
|
|
197
|
+
FileUtils.rm_f(intent_path)
|
|
198
|
+
link
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private_class_method def self.continue_intent_path(id, state_dir)
|
|
203
|
+
File.join(Session.session_dir(id, state_dir: state_dir), CONTINUE_INTENT)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private_class_method def self.repair_intent(session, state_dir, path, sessions)
|
|
207
|
+
lock_path = File.join(Session.session_dir(session.id, state_dir: state_dir), LOCK)
|
|
208
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o600) do |lock|
|
|
209
|
+
next unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
210
|
+
next unless File.file?(path)
|
|
211
|
+
|
|
212
|
+
intent = JSON.parse(File.read(path))
|
|
213
|
+
intent = {} unless intent.is_a?(Hash)
|
|
214
|
+
successor_id = intent["successor_id"]
|
|
215
|
+
successor_path = Session.session_file(successor_id, state_dir: state_dir) if Session.valid_id?(successor_id)
|
|
216
|
+
successor = JSON.parse(File.read(successor_path)) if successor_path && File.file?(successor_path)
|
|
217
|
+
unless successor.is_a?(Hash) && successor["continues"] == session.id
|
|
218
|
+
previously_archived = Array(intent["previously_archived"]).select { |id| Session.valid_id?(id) }
|
|
219
|
+
archived = if intent["archived_ids"].is_a?(Array)
|
|
220
|
+
intent["archived_ids"].select { |id| Session.valid_id?(id) }
|
|
221
|
+
else
|
|
222
|
+
archived_descendants(session.id, sessions, state_dir) - previously_archived
|
|
223
|
+
end
|
|
224
|
+
undo_failed_continue(session.id, state_dir, moved: [], archived: archived)
|
|
225
|
+
end
|
|
226
|
+
FileUtils.rm_f(path)
|
|
227
|
+
end
|
|
228
|
+
rescue JSON::ParserError => e
|
|
229
|
+
quarantine_intent(path, session.id, error: e)
|
|
230
|
+
rescue Errno::ENOENT
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private_class_method def self.quarantine_intent(path, session_id, error: nil, message: nil)
|
|
235
|
+
File.rename(path, "#{path}.bad")
|
|
236
|
+
Log.warn(:worker, "continue_repair_failed", sid: session_id, path: path,
|
|
237
|
+
error: error&.class&.name || "SessionUnavailable",
|
|
238
|
+
msg: (message || error.message).to_s[0, 200])
|
|
239
|
+
rescue Errno::ENOENT
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private_class_method def self.archived_descendants(id, sessions, state_dir)
|
|
244
|
+
children = sessions.group_by(&:parent_id)
|
|
245
|
+
queue = [id]
|
|
246
|
+
descendants = []
|
|
247
|
+
until queue.empty?
|
|
248
|
+
children.fetch(queue.shift, []).each do |child|
|
|
249
|
+
next if descendants.include?(child.id)
|
|
250
|
+
|
|
251
|
+
descendants << child.id
|
|
252
|
+
queue << child.id
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
[id, *descendants].select do |session_id|
|
|
256
|
+
ArchiveStore.archived?(Session.session_dir(session_id, state_dir: state_dir))
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private_class_method def self.undo_failed_continue(id, state_dir, moved:, archived:)
|
|
261
|
+
undo_move(id, state_dir, moved: moved)
|
|
262
|
+
begin
|
|
263
|
+
yield if block_given?
|
|
264
|
+
ensure
|
|
265
|
+
archived.each { |sid| ArchiveStore.unarchive(sid, state_dir: state_dir) }
|
|
266
|
+
wake_for_rings(id, state_dir) unless moved.empty?
|
|
157
267
|
end
|
|
158
268
|
end
|
|
159
269
|
|
|
@@ -171,10 +281,16 @@ module Samagotchi
|
|
|
171
281
|
# The link a failed #run saved (no other continues +id+:
|
|
172
282
|
# check! made sure under the lock): its worker stopped if one
|
|
173
283
|
# started, then the rings a moved delegate wrote into it just before
|
|
174
|
-
# the undo go back to +id
|
|
175
|
-
#
|
|
176
|
-
# and
|
|
177
|
-
|
|
284
|
+
# the undo go back to +id+. Then its session file goes first, so the
|
|
285
|
+
# link is out of the chain (a note to +id+ stays there:
|
|
286
|
+
# SessionInbox::Head) and a writer that resolved it before finds it gone,
|
|
287
|
+
# then the notes it moved there (+notes+) go back with any sent to it
|
|
288
|
+
# meanwhile (NoteMove.give_back), then the link's folder goes, and one
|
|
289
|
+
# last look gives back a note an older chi wrote after the delete
|
|
290
|
+
# (its write_note recreates the folder). A worker that outlives the
|
|
291
|
+
# stop is raised (SessionManager::DeleteRefused): the link stays, and
|
|
292
|
+
# the caller hears it.
|
|
293
|
+
private_class_method def self.discard_failed_link(id, state_dir, notes:)
|
|
178
294
|
orphan = SessionChain.next_of(id, state_dir: state_dir) or return
|
|
179
295
|
if SessionManager.refuse_tui!(orphan, state_dir: state_dir) &&
|
|
180
296
|
!SessionManager.stop_session(orphan, state_dir: state_dir, wait: SessionManager::STOP_WAIT)
|
|
@@ -182,13 +298,26 @@ module Samagotchi
|
|
|
182
298
|
end
|
|
183
299
|
|
|
184
300
|
ChildMove.return_rings(from: orphan, to: id, state_dir: state_dir)
|
|
185
|
-
|
|
301
|
+
FileUtils.rm_f(Session.session_file(orphan, state_dir: state_dir))
|
|
302
|
+
NoteMove.give_back(from: orphan, to: id, moved: notes, state_dir: state_dir)
|
|
303
|
+
SessionManager.delete_session(orphan, state_dir: state_dir, allow_scheduled: true)
|
|
304
|
+
give_back_strays(orphan, id, state_dir)
|
|
186
305
|
rescue SessionManager::DeleteRefused
|
|
187
306
|
raise
|
|
188
307
|
rescue StandardError => e
|
|
189
308
|
Log.warn(:worker, "continue_cleanup_failed", sid: id, error: e.class.name, msg: e.message)
|
|
190
309
|
end
|
|
191
310
|
|
|
311
|
+
# A failed link's folder an older chi's note brought back after its
|
|
312
|
+
# delete: the note goes back to +id+, the folder goes again.
|
|
313
|
+
private_class_method def self.give_back_strays(orphan, id, state_dir)
|
|
314
|
+
dir = Session.session_dir(orphan, state_dir: state_dir)
|
|
315
|
+
return unless Dir.exist?(dir) && !Session.exist?(orphan, state_dir: state_dir)
|
|
316
|
+
|
|
317
|
+
NoteMove.give_back(from: orphan, to: id, moved: [], state_dir: state_dir)
|
|
318
|
+
FileUtils.rm_rf(dir)
|
|
319
|
+
end
|
|
320
|
+
|
|
192
321
|
# A failed continue's previous link, back in the lists, with rings that
|
|
193
322
|
# came in meanwhile: wake it for them, as the ring's own wake would have.
|
|
194
323
|
private_class_method def self.wake_for_rings(id, state_dir)
|
|
@@ -199,26 +328,35 @@ module Samagotchi
|
|
|
199
328
|
Log.warn(:worker, "continue_wake_failed", sid: id, error: e.class.name, msg: e.message)
|
|
200
329
|
end
|
|
201
330
|
|
|
202
|
-
# The new link, holding off any worker but its own
|
|
203
|
-
#
|
|
204
|
-
|
|
331
|
+
# The new link, holding off any worker but its own while +move+ goes to
|
|
332
|
+
# it (spawn_session marks it starting before its file is saved:
|
|
333
|
+
# StartingMarker.fresh?).
|
|
334
|
+
# @param notes [Array<String>] gets the names of the notes moved to it
|
|
335
|
+
private_class_method def self.spawn_continuation(previous, prompt:, preview:, choice:, state_dir:, move:, notes:,
|
|
336
|
+
session_id:, client_id: nil)
|
|
205
337
|
recap = RecapStore.read(Session.session_dir(previous.id, state_dir: state_dir))
|
|
206
338
|
start = prompt.to_s.strip.empty? ? nil : prompt
|
|
207
339
|
preview = previous.first_preview if preview.to_s.strip.empty? && start.nil?
|
|
208
340
|
moved_context = nil
|
|
209
341
|
before_spawn = lambda do |link|
|
|
210
|
-
StartingMarker.mark(link.id, state_dir: state_dir)
|
|
211
342
|
ChildMove.apply(move, from: previous.id, to: link.id, state_dir: state_dir)
|
|
212
343
|
# Its movable context too, with a note saying what moved and stayed.
|
|
213
344
|
moved_context = ContextMove.call(from: previous.id, to: link.id, mode: :continue, state_dir: state_dir)
|
|
214
|
-
# Its objectives
|
|
345
|
+
# Its objectives: the list reads as the latest word.
|
|
215
346
|
Objectives.carry(previous, to: link.id, state_dir: state_dir)
|
|
347
|
+
# Its waiting notes last: the carry above read a pending /objectives
|
|
348
|
+
# edit from them first. They keep their names, so they still sort
|
|
349
|
+
# ahead of the link's own notes. A note sent to the previous link
|
|
350
|
+
# from here on goes to this one (SessionInbox::Head), and a
|
|
351
|
+
# straggler is adopted by its worker (NoteMove.adoption).
|
|
352
|
+
notes.concat(NoteMove.move(from: previous.id, to: link.id, state_dir: state_dir))
|
|
216
353
|
end
|
|
217
354
|
link = SessionManager.spawn_session(
|
|
218
355
|
prompt: start, preview: preview, title: previous.title, working_directory: previous.working_directory,
|
|
219
356
|
model_name: choice.model, setup: SessionSetup.of(previous), review: previous.review,
|
|
220
357
|
continues: previous.id, note: SessionChain.note_text(previous, recap: recap, state_dir: state_dir),
|
|
221
|
-
note_source: SessionChain::NOTE_SOURCE, state_dir: state_dir, before_spawn: before_spawn
|
|
358
|
+
note_source: SessionChain::NOTE_SOURCE, state_dir: state_dir, before_spawn: before_spawn, client_id: client_id,
|
|
359
|
+
session_id: session_id
|
|
222
360
|
)
|
|
223
361
|
link.moved_children = move
|
|
224
362
|
link.moved_context = moved_context
|
|
@@ -70,7 +70,9 @@ module Samagotchi
|
|
|
70
70
|
@stdout.puts("#{short} refused: it is #{OwnerLock::TUI_BUSY}; let it finish first")
|
|
71
71
|
false
|
|
72
72
|
rescue SessionManager::DeleteRefused => e
|
|
73
|
-
line =
|
|
73
|
+
line = case e.reason
|
|
74
|
+
when :still_stopping then "its worker is still shutting down; try again in a moment"
|
|
75
|
+
when :scheduled then e.message
|
|
74
76
|
else "its worker is running (--force stops it first)"
|
|
75
77
|
end
|
|
76
78
|
@stdout.puts("#{short} refused: #{line}")
|
|
@@ -44,6 +44,20 @@ module Samagotchi
|
|
|
44
44
|
# NOTE_MAX_BYTES.
|
|
45
45
|
class NoteRejected < ArgumentError; end
|
|
46
46
|
|
|
47
|
+
# The one delivery path (session_inbox_delivery.rb), loaded on first
|
|
48
|
+
# use: the bundle test kit's load budget stays as it was.
|
|
49
|
+
delivery = File.expand_path("session_inbox_delivery", __dir__)
|
|
50
|
+
%i[Dispatch Delivered Head Resolved WakePolicy].each { |name| autoload name, delivery }
|
|
51
|
+
|
|
52
|
+
# A delivery of +kind+ to session +target+ (Dispatch.deliver: resolve,
|
|
53
|
+
# refuse, the block's write, one presence read, the WakePolicy's wake).
|
|
54
|
+
# @return [Delivered]
|
|
55
|
+
def self.deliver(kind, target, **, &) = Dispatch.deliver(kind, target, **, &)
|
|
56
|
+
|
|
57
|
+
# The wake half of #deliver alone (Dispatch.wake).
|
|
58
|
+
# @return [Delivered]
|
|
59
|
+
def self.wake(kind, target, **) = Dispatch.wake(kind, target, **)
|
|
60
|
+
|
|
47
61
|
# Queue a turn's input file: JSON with the prompt and the sender's ids.
|
|
48
62
|
# +delivery+ is only written when it is "queue": a step-boundary
|
|
49
63
|
# message is the default an older worker assumes, and a cut is done by
|
|
@@ -89,11 +103,17 @@ module Samagotchi
|
|
|
89
103
|
# @param now [Time] the time the file is named and stamped by (a spec's clock)
|
|
90
104
|
# @return [String] the note file's path
|
|
91
105
|
# @raise [NoteRejected] for an empty note or one over 16 KiB (never cut)
|
|
106
|
+
# @raise [Session::NotFound] for a session that is gone: no file and no
|
|
107
|
+
# folder (deleted since the writer resolved it), which a note would
|
|
108
|
+
# otherwise bring back as a stray folder (as Session#save skips one)
|
|
92
109
|
def self.write_note(session_id, text:, source: "cli", from_session: nil, from_cwd: nil, state_dir: nil,
|
|
93
110
|
objectives: nil, now: Time.now)
|
|
94
111
|
body = checked_text(text)
|
|
95
112
|
sd = state_dir || Session.default_state_dir
|
|
96
|
-
|
|
113
|
+
session_dir = Session.session_dir(session_id, state_dir: sd)
|
|
114
|
+
raise Session::NotFound, session_id unless Dir.exist?(session_dir) || Session.exist?(session_id, state_dir: sd)
|
|
115
|
+
|
|
116
|
+
notes_dir = File.join(session_dir, NOTES_DIR)
|
|
97
117
|
FileUtils.mkdir_p(notes_dir)
|
|
98
118
|
|
|
99
119
|
# The random part keeps two writers in one nanosecond apart; the
|
|
@@ -203,6 +223,11 @@ module Samagotchi
|
|
|
203
223
|
# An id that is safe in a file name (a Bridge's or deliver_turn's
|
|
204
224
|
# uuid); anything else leaves the name bare.
|
|
205
225
|
ENQUEUED_ID_NAME = /\A[A-Za-z0-9_-]{1,64}\z/
|
|
226
|
+
ENQUEUED_ID_UUID = /\A[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\z/i
|
|
227
|
+
|
|
228
|
+
def self.new_enqueued_id = SecureRandom.uuid
|
|
229
|
+
|
|
230
|
+
def self.valid_enqueued_id?(id) = id.is_a?(String) && ENQUEUED_ID_UUID.match?(id)
|
|
206
231
|
|
|
207
232
|
def self.output_name(timestamp, enqueued_id)
|
|
208
233
|
id = enqueued_id.to_s
|
|
@@ -221,6 +246,21 @@ module Samagotchi
|
|
|
221
246
|
Dir.children(dir).select { |f| f.end_with?(suffix) }.max
|
|
222
247
|
end
|
|
223
248
|
|
|
249
|
+
# Evidence that a prior worker took this input: a saved or interrupted
|
|
250
|
+
# turn, its reply, or a mid-turn merge record. A restarted worker's
|
|
251
|
+
# in-memory id ledger cannot cover these deliveries.
|
|
252
|
+
def self.input_taken?(session_dir, session, enqueued_id)
|
|
253
|
+
id = enqueued_id.to_s
|
|
254
|
+
return false if id.empty?
|
|
255
|
+
return true if [session.last_turn&.enqueued_id, session.interrupted_turn&.enqueued_id].include?(id)
|
|
256
|
+
return true if reply_for(session_dir, id)
|
|
257
|
+
|
|
258
|
+
require_relative "turn_checkpoint"
|
|
259
|
+
return true if TurnCheckpoint.settled_turn(session_dir, id)
|
|
260
|
+
|
|
261
|
+
!merged_into(session_dir, id).nil?
|
|
262
|
+
end
|
|
263
|
+
|
|
224
264
|
# The output files' text, oldest first; with +since_time+ only files
|
|
225
265
|
# modified after it.
|
|
226
266
|
def self.read_outputs(session_dir, since_time: nil)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "session"
|
|
4
|
+
require_relative "session_inbox"
|
|
5
|
+
require_relative "archive_store"
|
|
6
|
+
require_relative "worker_presence"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
# Loaded on first use: this autoload keeps SessionManager and its ~80
|
|
10
|
+
# files unloaded until a delivery starts a worker.
|
|
11
|
+
autoload :SessionManager, File.expand_path("session_manager", __dir__)
|
|
12
|
+
|
|
13
|
+
# The session inbox's one delivery path (SessionInbox.deliver): which link
|
|
14
|
+
# a delivery goes to (Head), whether it is refused, its write (the kind's
|
|
15
|
+
# own writer, unchanged), one read of the worker's presence after it, and
|
|
16
|
+
# what that means for a worker (WakePolicy). Loaded on first use, from
|
|
17
|
+
# session_inbox.rb's autoloads: the bundle test kit's load budget
|
|
18
|
+
# (spec/samagotchi/testing/sandbox_spec.rb) stays as it was.
|
|
19
|
+
module SessionInbox
|
|
20
|
+
# Where a delivery for +asked+ goes: +id+, reached +via+ :pinned (the
|
|
21
|
+
# kind never follows a chain), :same (followed, nothing to follow) or
|
|
22
|
+
# :head (a later link of +asked+'s chain).
|
|
23
|
+
Resolved = Data.define(:id, :asked, :via)
|
|
24
|
+
|
|
25
|
+
# A delivery's outcome.
|
|
26
|
+
# @!attribute kind [Symbol] the WakePolicy row
|
|
27
|
+
# @!attribute id [String] the session whose worker the outcome is about
|
|
28
|
+
# (a ring's file stays with +asked+: WakePolicy#resolves :reader)
|
|
29
|
+
# @!attribute asked [String] the id the caller named
|
|
30
|
+
# @!attribute via [Symbol] Resolved#via
|
|
31
|
+
# @!attribute status [Symbol] :refused (nothing written), :queued (a
|
|
32
|
+
# worker sees it: one runs, is starting or was started) or :waits (no
|
|
33
|
+
# worker comes: it waits for the next start)
|
|
34
|
+
# @!attribute wake [Symbol] WakePolicy#decide's outcome, :woken for a
|
|
35
|
+
# start made, or the refusal (:gone, :owned: a one-turn chi run's,
|
|
36
|
+
# :scratch)
|
|
37
|
+
# @!attribute queued [Integer, nil] notes waiting (a note kind's), nil else
|
|
38
|
+
Delivered = Data.define(:kind, :id, :asked, :via, :status, :wake, :queued) do
|
|
39
|
+
def initialize(kind:, id:, asked:, via:, status:, wake:, queued: nil) = super
|
|
40
|
+
|
|
41
|
+
def refused? = status == :refused
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# What a delivery of one kind may do to a session's worker: one frozen
|
|
45
|
+
# row per kind (ROWS), decided on WorkerPresence's predicates alone, in
|
|
46
|
+
# one order for every kind (#decide).
|
|
47
|
+
# @!attribute kind [Symbol] :send, :note, :push, :ring, :fresh_verdict or
|
|
48
|
+
# :leave (WorkerBoot's respawn, not a delivery)
|
|
49
|
+
# @!attribute resolves [Symbol] which link it goes to (Head.resolve):
|
|
50
|
+
# :pinned the id named; :head the chain's newest link from it; :reader
|
|
51
|
+
# the link that reads the named one's file (a ring: the file stays with
|
|
52
|
+
# the parent parent.json names, ChildReports reads its predecessor's
|
|
53
|
+
# rings, so the wake goes one hop at most)
|
|
54
|
+
# @!attribute starts [Boolean] it may start a worker
|
|
55
|
+
# @!attribute unstops [Boolean] it starts a stopped session (a resume
|
|
56
|
+
# clears the stop): send only
|
|
57
|
+
# @!attribute starts_archived [Boolean] it starts an archived link
|
|
58
|
+
# (input from a person un-archives it): send only
|
|
59
|
+
# @!attribute refuse_tui [Boolean] a one-turn chi run's session refuses it
|
|
60
|
+
# @!attribute refuse_scratch [Boolean] a chi scratch session refuses it
|
|
61
|
+
WakePolicy = Data.define(:kind, :resolves, :starts, :unstops, :starts_archived, :refuse_tui, :refuse_scratch) do
|
|
62
|
+
# @return [WakePolicy]
|
|
63
|
+
# @raise [ArgumentError] no such kind
|
|
64
|
+
def self.for(kind) = WakePolicy::ROWS.fetch(kind) { raise ArgumentError, "no inbox kind #{kind.inspect}" }
|
|
65
|
+
|
|
66
|
+
# Why the delivery is refused before anything is written, else nil:
|
|
67
|
+
# :gone (no session), :owned (a one-turn chi run's), :scratch.
|
|
68
|
+
# @param load [#call, nil] the session, read only when the scratch
|
|
69
|
+
# flag decides (nil: not a scratch session)
|
|
70
|
+
# @return [Symbol, nil]
|
|
71
|
+
def refusal(presence, load: nil)
|
|
72
|
+
return :gone unless presence.exists?
|
|
73
|
+
return :owned if refuse_tui && presence.tui_owned?
|
|
74
|
+
|
|
75
|
+
:scratch if refuse_scratch && scratch?(load)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# What the delivery means for the session's worker, in this order:
|
|
79
|
+
# :gone, :owned (a one-turn chi run's), :live (a worker holds it: it
|
|
80
|
+
# sees the delivery), :stopped (the stop marker: the only "stopped",
|
|
81
|
+
# the file's status never decides), :archived, :starting (a worker is
|
|
82
|
+
# being started: it reads the store on its first pass), :none (the
|
|
83
|
+
# kind never starts one), :off (+wake+ false: none was asked),
|
|
84
|
+
# :scratch (its terminal's alone; the file is read last, only here),
|
|
85
|
+
# :start (start one).
|
|
86
|
+
# @param wake [Boolean] the delivery asks for a worker (a ring in wake
|
|
87
|
+
# mode, a push with wake)
|
|
88
|
+
# @return [Symbol]
|
|
89
|
+
def decide(presence, wake: true, load: nil)
|
|
90
|
+
return :gone unless presence.exists?
|
|
91
|
+
return :owned if presence.tui_owned?
|
|
92
|
+
return :live if presence.owned?
|
|
93
|
+
return :stopped if presence.stop_marked? && !unstops
|
|
94
|
+
return :archived if presence.archived? && !starts_archived
|
|
95
|
+
# A stop cancels a start's hold-off for a kind that clears it
|
|
96
|
+
# (WorkerPresence#start_pending?).
|
|
97
|
+
return :starting if unstops ? presence.start_pending? : presence.starting?
|
|
98
|
+
return :none unless starts
|
|
99
|
+
return :off unless wake
|
|
100
|
+
return :scratch if scratch?(load)
|
|
101
|
+
|
|
102
|
+
:start
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private def scratch?(load) = load ? load.call.scratch == true : false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
WakePolicy::ROWS = {
|
|
109
|
+
send: WakePolicy.new(kind: :send, resolves: :pinned, starts: true, unstops: true, starts_archived: true,
|
|
110
|
+
refuse_tui: true, refuse_scratch: true),
|
|
111
|
+
note: WakePolicy.new(kind: :note, resolves: :head, starts: false, unstops: false, starts_archived: false,
|
|
112
|
+
refuse_tui: true, refuse_scratch: true),
|
|
113
|
+
push: WakePolicy.new(kind: :push, resolves: :pinned, starts: true, unstops: false, starts_archived: false,
|
|
114
|
+
refuse_tui: false, refuse_scratch: false),
|
|
115
|
+
ring: WakePolicy.new(kind: :ring, resolves: :reader, starts: true, unstops: false, starts_archived: false,
|
|
116
|
+
refuse_tui: false, refuse_scratch: false),
|
|
117
|
+
fresh_verdict: WakePolicy.new(kind: :fresh_verdict, resolves: :pinned, starts: false, unstops: false,
|
|
118
|
+
starts_archived: false, refuse_tui: false, refuse_scratch: false),
|
|
119
|
+
# Not a delivery: a worker's own leave starting its successor for
|
|
120
|
+
# what waits (input, rings) or a restart (WorkerBoot, after the
|
|
121
|
+
# lock's release). A stop wins; an archived session's input still
|
|
122
|
+
# starts it, as a send's would have.
|
|
123
|
+
leave: WakePolicy.new(kind: :leave, resolves: :pinned, starts: true, unstops: false, starts_archived: true,
|
|
124
|
+
refuse_tui: false, refuse_scratch: false)
|
|
125
|
+
}.freeze
|
|
126
|
+
|
|
127
|
+
# Which link of a chain a delivery goes to.
|
|
128
|
+
module Head
|
|
129
|
+
module_function
|
|
130
|
+
|
|
131
|
+
# +id+ for a pinned kind, or for an id not archived (the cheap check
|
|
132
|
+
# first: only an archived id is walked, with one Session.list).
|
|
133
|
+
# Else the walk forward over archived links that have a successor:
|
|
134
|
+
# to the first link not archived, or the last (a loop a hand-edited
|
|
135
|
+
# file could make ends where it started); a :reader kind goes one hop
|
|
136
|
+
# at most.
|
|
137
|
+
# @return [Resolved]
|
|
138
|
+
def resolve(kind, id, state_dir:)
|
|
139
|
+
how = WakePolicy.for(kind).resolves
|
|
140
|
+
return Resolved.new(id: id, asked: id, via: :pinned) if how == :pinned
|
|
141
|
+
|
|
142
|
+
same = Resolved.new(id: id, asked: id, via: :same)
|
|
143
|
+
return same unless archived?(id, state_dir)
|
|
144
|
+
|
|
145
|
+
nexts = successors(state_dir)
|
|
146
|
+
seen = [id]
|
|
147
|
+
while (following = nexts[seen.last]) && !seen.include?(following)
|
|
148
|
+
seen << following
|
|
149
|
+
break if how == :reader || !archived?(following, state_dir)
|
|
150
|
+
end
|
|
151
|
+
seen.size == 1 ? same : Resolved.new(id: seen.last, asked: id, via: :head)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def archived?(id, state_dir)
|
|
155
|
+
Session.valid_id?(id) && ArchiveStore.archived?(Session.session_dir(id, state_dir: state_dir))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# id → the session that continues it (SessionChain.next_of's: the
|
|
159
|
+
# earliest one, archived ones too).
|
|
160
|
+
def successors(state_dir)
|
|
161
|
+
Session.list(state_dir: state_dir, sort: "created_at", order: "asc", include_archived: true)
|
|
162
|
+
.each_with_object({}) { |s, nexts| nexts[s.continues] ||= s.id if s.continues }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# SessionInbox.deliver and SessionInbox.wake.
|
|
167
|
+
module Dispatch
|
|
168
|
+
# What a start each kind makes is logged as (SessionManager.start_for_inbox).
|
|
169
|
+
START_EVENTS = { ring: :delegate_woke_parent, push: :context_woke_session }.freeze
|
|
170
|
+
|
|
171
|
+
module_function
|
|
172
|
+
|
|
173
|
+
# Deliver one +kind+ of thing to session +target+, in this order:
|
|
174
|
+
# resolve the link (Head.resolve), refuse (WakePolicy#refusal: before
|
|
175
|
+
# any write), write (the block: the kind's own writer, given the id
|
|
176
|
+
# it writes under), read the presence once, then decide and apply
|
|
177
|
+
# (#wake).
|
|
178
|
+
# @param wake [Boolean] the delivery may ask for a worker; the block's
|
|
179
|
+
# value says whether it does (truthy: asks)
|
|
180
|
+
# @param pin [Boolean] write under +target+ itself, whatever the kind's
|
|
181
|
+
# row resolves (a writer whose text is +target+'s own: an
|
|
182
|
+
# /objectives list built from its conversation)
|
|
183
|
+
# @param resolved [Resolved, nil] the link, as the caller resolved it
|
|
184
|
+
# already (Head.resolve for this kind): not walked again
|
|
185
|
+
# @param owner [#call, nil] (id) the owner lock's holder (a stand-in
|
|
186
|
+
# manager's session_owner); nil: SessionManager.session_owner
|
|
187
|
+
# @param start [#call, nil] (id) starts a worker, returns :woken (or
|
|
188
|
+
# why not); nil: SessionManager.start_for_inbox
|
|
189
|
+
# @yieldparam id [String] the session it is written under
|
|
190
|
+
# @yieldreturn [Boolean] whether it asks for a worker
|
|
191
|
+
# @return [Delivered]
|
|
192
|
+
def deliver(kind, target, state_dir: nil, wake: true, pin: false, resolved: nil, owner: nil, start: nil)
|
|
193
|
+
sd = state_dir || Session.default_state_dir
|
|
194
|
+
policy = WakePolicy.for(kind)
|
|
195
|
+
resolved = Resolved.new(id: target, asked: target, via: :pinned) if pin
|
|
196
|
+
resolved ||= Head.resolve(kind, target, state_dir: sd)
|
|
197
|
+
at = policy.resolves == :reader ? resolved.asked : resolved.id
|
|
198
|
+
if (why = refusal(policy, at, sd, owner))
|
|
199
|
+
return Delivered.new(kind: kind, id: resolved.id, asked: resolved.asked, via: resolved.via, status: :refused,
|
|
200
|
+
wake: why)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
asks = yield(at)
|
|
204
|
+
apply(policy, resolved, sd, wake: wake && asks ? true : false, owner: owner, start: start)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The wake half of #deliver alone, for a delivery already on disk (a
|
|
208
|
+
# failed continue's previous link with rings that came meanwhile).
|
|
209
|
+
# @return [Delivered]
|
|
210
|
+
def wake(kind, target, state_dir: nil, owner: nil, start: nil)
|
|
211
|
+
sd = state_dir || Session.default_state_dir
|
|
212
|
+
apply(WakePolicy.for(kind), Head.resolve(kind, target, state_dir: sd), sd, wake: true, owner: owner,
|
|
213
|
+
start: start)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def refusal(policy, id, state_dir, owner)
|
|
217
|
+
return :gone unless Session.exist?(id, state_dir: state_dir)
|
|
218
|
+
return nil unless policy.refuse_tui || policy.refuse_scratch
|
|
219
|
+
|
|
220
|
+
policy.refusal(presence(id, state_dir, owner), load: -> { Session.load(id, state_dir: state_dir) })
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def apply(policy, resolved, state_dir, wake:, owner:, start:)
|
|
224
|
+
id = resolved.id
|
|
225
|
+
outcome = policy.decide(presence(id, state_dir, owner), wake: wake, load: -> { Session.load(id, state_dir: state_dir) })
|
|
226
|
+
if outcome == :start
|
|
227
|
+
outcome = if start
|
|
228
|
+
start.call(id)
|
|
229
|
+
else
|
|
230
|
+
SessionManager.start_for_inbox(id, state_dir: state_dir,
|
|
231
|
+
event: START_EVENTS.fetch(policy.kind))
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
status = %i[live woken starting].include?(outcome) ? :queued : :waits
|
|
235
|
+
Delivered.new(kind: policy.kind, id: id, asked: resolved.asked, via: resolved.via, status: status,
|
|
236
|
+
wake: outcome, queued: notes_waiting(policy, status, id, state_dir))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# A note's count of the notes waiting for a start (0 when a worker
|
|
240
|
+
# sees them); nil for the other kinds.
|
|
241
|
+
def notes_waiting(policy, status, id, state_dir)
|
|
242
|
+
return nil unless policy.kind == :note
|
|
243
|
+
return 0 unless status == :waits
|
|
244
|
+
|
|
245
|
+
SessionInbox.find_new_note_files(Session.session_dir(id, state_dir: state_dir)).size
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def presence(id, state_dir, owner)
|
|
249
|
+
reader = owner ? -> { owner.call(id) } : -> { SessionManager.session_owner(id, state_dir: state_dir) }
|
|
250
|
+
WorkerPresence.read(id, state_dir: state_dir, owner: reader)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|