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
|
@@ -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
|
|
@@ -37,6 +37,17 @@ module Samagotchi
|
|
|
37
37
|
|
|
38
38
|
def detail = ack["detail"]
|
|
39
39
|
|
|
40
|
+
# How a delivery that did not go in reads, as one line: the word for it
|
|
41
|
+
# (refused: a worker older than queueing would steer the running turn,
|
|
42
|
+
# or a remote refused it as chi send refuses it here; failed: it simply
|
|
43
|
+
# did not get in) and its reason. chi send prints it, and the delegate
|
|
44
|
+
# tool's follow-up says it after "Error: session <id8>".
|
|
45
|
+
# @return [String]
|
|
46
|
+
def refusal_text
|
|
47
|
+
refused = error == Delivery::QUEUE_UNAVAILABLE["error"] || DeliverResult::REMOTE_REFUSALS.include?(error)
|
|
48
|
+
"#{refused ? "refused" : "failed"}: #{detail || "could not queue it"}"
|
|
49
|
+
end
|
|
50
|
+
|
|
40
51
|
# What the worker applied (a Bridge ack only): the delivery asked for,
|
|
41
52
|
# or next_step for a --queue that found no turn running; nil from the
|
|
42
53
|
# input file or a worker older than delivery.
|
|
@@ -49,6 +60,13 @@ module Samagotchi
|
|
|
49
60
|
def cut_after = ack["cut_after"]
|
|
50
61
|
end
|
|
51
62
|
|
|
63
|
+
# A remote's refusals of what this machine checks before sending (a
|
|
64
|
+
# parent's /continue yes, a loosening /parent-answers, images to a
|
|
65
|
+
# model that can't see them): its turn route's error codes. Named on
|
|
66
|
+
# DeliverResult, not inside its Data.define block, where a constant
|
|
67
|
+
# would land on SessionManager.
|
|
68
|
+
DeliverResult::REMOTE_REFUSALS = %w[parent_answers_refused parent_continue_refused no_vision].freeze
|
|
69
|
+
|
|
52
70
|
# The unclaimed input files +author+ wrote.
|
|
53
71
|
PARTICIPANT_QUEUE_LOCK = "participants.lock"
|
|
54
72
|
|
|
@@ -118,7 +136,10 @@ module Samagotchi
|
|
|
118
136
|
link = WorkerLink.await(session_id, session_dir: session_dir, timeout: TURN_BRIDGE_WAIT)
|
|
119
137
|
link.live? ? link.client(session_id) : nil
|
|
120
138
|
end
|
|
121
|
-
|
|
139
|
+
# Its refusals (no session, a one-turn run's, a scratch one) are
|
|
140
|
+
# resume_session's to raise, in their one wording.
|
|
141
|
+
start_for_send(manager, session_id, state_dir, before: true)
|
|
142
|
+
enqueued_id = SessionInbox.new_enqueued_id
|
|
122
143
|
if (client = bridge.call)
|
|
123
144
|
if (missing = anchors.present.find { |anchor| !worker_has?(client, anchor.class::FEATURE) })
|
|
124
145
|
return DeliverResult.new(status: :refused, code: 409, ack: missing.class::UNAVAILABLE)
|
|
@@ -130,7 +151,7 @@ module Samagotchi
|
|
|
130
151
|
return review_unavailable if Steer.participant_author?(author) && !worker_has?(client, Delivery::REVIEW_FEATURE)
|
|
131
152
|
|
|
132
153
|
begin
|
|
133
|
-
options = { prompt: prompt, client_id: client_id }
|
|
154
|
+
options = { prompt: prompt, client_id: client_id, enqueued_id: enqueued_id }
|
|
134
155
|
options[:images] = images unless images.empty?
|
|
135
156
|
# An old worker ignores the field: it cuts anyway (its own default).
|
|
136
157
|
options[:delivery] = delivery unless Delivery.next_step?(delivery)
|
|
@@ -164,12 +185,12 @@ module Samagotchi
|
|
|
164
185
|
# still holding the session with its Bridge gone (exiting) may be an
|
|
165
186
|
# older one, which would run it without the review boundary.
|
|
166
187
|
# (A one-turn chi run holding it is refused as for anyone, below.)
|
|
167
|
-
if Steer.participant_author?(author) && (holder =
|
|
188
|
+
if Steer.participant_author?(author) && (holder = delivery_presence(manager, session_id, state_dir)).owned? &&
|
|
189
|
+
!holder.tui_owned?
|
|
168
190
|
return DeliverResult.new(status: :timeout, via: :file,
|
|
169
191
|
ack: { "error" => "worker_timeout",
|
|
170
192
|
"detail" => "the session's worker is exiting, so the message was not sent; send it again" })
|
|
171
193
|
end
|
|
172
|
-
enqueued_id = SecureRandom.uuid
|
|
173
194
|
input = { prompt: prompt, client_id: client_id, enqueued_id: enqueued_id, state_dir: state_dir }
|
|
174
195
|
input[:images] = images unless images.empty?
|
|
175
196
|
input[:delivery] = delivery unless Delivery.next_step?(delivery)
|
|
@@ -189,21 +210,51 @@ module Samagotchi
|
|
|
189
210
|
end
|
|
190
211
|
return DeliverResult.new(status: :failed) unless path
|
|
191
212
|
|
|
192
|
-
owner = delivery_owner(manager, session_id, state_dir)
|
|
193
213
|
# A TUI that took the session between the resume and the write never
|
|
194
214
|
# reads input files; a later worker would replay this one.
|
|
195
|
-
if owner&.tui?
|
|
196
|
-
FileUtils.rm_f(path) if path.is_a?(String)
|
|
197
|
-
raise OwnedByTUI, session_id
|
|
198
|
-
end
|
|
199
215
|
# A worker that idle-exited since the resume never reads it either:
|
|
200
216
|
# wake a new one. (The exiting worker also looks for input it left;
|
|
201
217
|
# one the first resume spawned that is still starting is left be.)
|
|
202
|
-
manager
|
|
218
|
+
start_for_send(manager, session_id, state_dir, before: false) do
|
|
219
|
+
FileUtils.rm_f(path) if path.is_a?(String)
|
|
220
|
+
raise OwnedByTUI, session_id
|
|
221
|
+
end
|
|
203
222
|
DeliverResult.new(status: :accepted, via: :file,
|
|
204
223
|
ack: { "status" => "accepted", "enqueued_id" => enqueued_id, "session_id" => session_id })
|
|
205
224
|
end
|
|
206
225
|
|
|
226
|
+
# deliver_turn's two liveness checks, on the session inbox's send row
|
|
227
|
+
# (SessionInbox::WakePolicy: send un-stops and starts an archived
|
|
228
|
+
# link; it is pinned to the id it names): resume_session unless a
|
|
229
|
+
# worker holds the session or one is being started. Its refusals (no
|
|
230
|
+
# session, a one-turn run's, a scratch one) are resume_session's to
|
|
231
|
+
# raise, in their one wording: +before+ the write, a session it would
|
|
232
|
+
# not resume is checked for them (WakePolicy#refusal: the file read
|
|
233
|
+
# only then, resume_session reads it otherwise); after it, a one-turn
|
|
234
|
+
# run that took the session since runs the block, and a worker that
|
|
235
|
+
# is leaving (OwnerLock::Owner#leaving?: its Bridge closed, the lock
|
|
236
|
+
# held for its last work) has its input served by its own respawn
|
|
237
|
+
# (WorkerBoot's leave): a stop that landed before this send is
|
|
238
|
+
# cleared, as resume_session clears it (a send un-stops); one that
|
|
239
|
+
# lands after it still wins.
|
|
240
|
+
private def start_for_send(manager, session_id, state_dir, before:)
|
|
241
|
+
presence = delivery_presence(manager, session_id, state_dir)
|
|
242
|
+
policy = SessionInbox::WakePolicy.for(:send)
|
|
243
|
+
outcome = policy.decide(presence)
|
|
244
|
+
yield if outcome == :owned && block_given?
|
|
245
|
+
if !before && outcome == :live && presence.stop_marked? && presence.owner&.leaving?
|
|
246
|
+
Session.clear_stopped(session_id, state_dir: state_dir || Session.default_state_dir)
|
|
247
|
+
Log.info(:worker, "send_unstopped_leaving", sid: session_id[0, 8])
|
|
248
|
+
end
|
|
249
|
+
if %i[live starting].include?(outcome)
|
|
250
|
+
return unless before
|
|
251
|
+
|
|
252
|
+
load = -> { Session.load(session_id, state_dir: state_dir || Session.default_state_dir) }
|
|
253
|
+
return unless policy.refusal(presence, load: load)
|
|
254
|
+
end
|
|
255
|
+
manager.resume_session(session_id, state_dir: state_dir)
|
|
256
|
+
end
|
|
257
|
+
|
|
207
258
|
# Whether the worker +client+ talks to names +feature+: the features of
|
|
208
259
|
# the sidecar the client was made from (the Live link WorkerLink.await
|
|
209
260
|
# found), not a second read of bridge.json.
|
|
@@ -244,8 +295,11 @@ module Samagotchi
|
|
|
244
295
|
"detail" => "the session's worker did not answer, so the message was not sent" })
|
|
245
296
|
end
|
|
246
297
|
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
# The session's WorkerPresence, its owner read through +manager+ (a
|
|
299
|
+
# stand-in's #session_owner is optional: then none).
|
|
300
|
+
private def delivery_presence(manager, session_id, state_dir)
|
|
301
|
+
owner = Degrade.call(:worker, "owner_unreadable", sid: session_id) { manager.session_owner(session_id, state_dir: state_dir) }
|
|
302
|
+
WorkerPresence.read(session_id, state_dir: state_dir, owner: owner)
|
|
249
303
|
end
|
|
250
304
|
end
|
|
251
305
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../analytics_file"
|
|
4
4
|
require_relative "../session_inbox"
|
|
5
5
|
require_relative "../worker_link"
|
|
6
|
+
require_relative "../starting_marker"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
class SessionManager
|
|
@@ -12,7 +13,9 @@ module Samagotchi
|
|
|
12
13
|
# nothing writes or reads it now, but a folder from before still has one
|
|
13
14
|
# and is still empty. A turn's drawing (Bridge::TurnDrawings::FILE)
|
|
14
15
|
# exists only after a turn that kept messages: never a skeleton file.
|
|
15
|
-
|
|
16
|
+
# The starting marker is one too: a start left one behind; a fresh one
|
|
17
|
+
# keeps the session from the deleters itself.
|
|
18
|
+
EMPTY_SKELETON_FILES = ["pid", "owner.lock", WorkerLink::FILE, AnalyticsFile::NAME, StartingMarker::FILE].freeze
|
|
16
19
|
EMPTY_DIRS = [SessionInbox::INPUT_DIR, SessionInbox::NOTES_DIR, "images"].freeze
|
|
17
20
|
EMPTY_SKELETON_DIRS = (EMPTY_DIRS + [SessionInbox::OUTPUT_DIR]).freeze
|
|
18
21
|
|
|
@@ -94,7 +97,9 @@ module Samagotchi
|
|
|
94
97
|
elsif name == "cards.json"
|
|
95
98
|
cards_empty?(path)
|
|
96
99
|
else
|
|
97
|
-
|
|
100
|
+
# A starting claim's temp files a kill left (StartingMarker.claim:
|
|
101
|
+
# starting.<pid>.<hex>, starting.old.<pid>.<hex>) are the marker's.
|
|
102
|
+
EMPTY_SKELETON_FILES.include?(name) || name.start_with?("#{StartingMarker::FILE}.")
|
|
98
103
|
end
|
|
99
104
|
end
|
|
100
105
|
end
|
|
@@ -220,7 +220,8 @@ module Samagotchi
|
|
|
220
220
|
# A stop drops the reminders: a restart's hand-off no successor took
|
|
221
221
|
# yet must not bring them back on the next resume.
|
|
222
222
|
ReminderCarry.remove(Session.session_dir(session_id, state_dir: sd))
|
|
223
|
-
|
|
223
|
+
# A deleter holding the lock (chi web, the CLI) is no worker to stop.
|
|
224
|
+
pid = owner&.deleting? ? 0 : owner&.pid.to_i
|
|
224
225
|
begin
|
|
225
226
|
Process.kill("TERM", pid) if pid && pid > 0
|
|
226
227
|
rescue Errno::ESRCH
|
|
@@ -11,7 +11,9 @@ module Samagotchi
|
|
|
11
11
|
# +include_archived+: archived sessions too (Session#archived says which).
|
|
12
12
|
def list_sessions(state_dir: nil, sort: "updated_at", order: "desc", limit: nil, offset: 0, project_root: nil,
|
|
13
13
|
include_archived: false)
|
|
14
|
-
|
|
14
|
+
sd = state_dir || Session.default_state_dir
|
|
15
|
+
SessionContinue.repair_interrupted(state_dir: sd)
|
|
16
|
+
Session.list(state_dir: sd, sort: sort, order: order, limit: limit,
|
|
15
17
|
offset: offset, project_root: project_root, include_archived: include_archived)
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -34,6 +36,7 @@ module Samagotchi
|
|
|
34
36
|
def session_rows(live: false, cwd: nil, limit: nil, include_tests: true, exclude: nil, state_dir: nil,
|
|
35
37
|
project_root: nil, include_archived: false, sort: nil, order: nil)
|
|
36
38
|
sd = state_dir || Session.default_state_dir
|
|
39
|
+
SessionContinue.repair_interrupted(state_dir: sd)
|
|
37
40
|
root = cwd && folder_path(cwd)
|
|
38
41
|
roots = {}
|
|
39
42
|
# One registry per listing, built only when a row has a saved context
|