samagotchi 0.102.0 → 0.103.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 +40 -1
- data/bin/chi +23 -191
- data/docs/architecture.md +54 -0
- data/docs/cli.md +114 -7
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +2 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/remote-sessions.md +303 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +26 -2
- data/docs/sessions.md +12 -5
- data/docs/sub-agent.md +8 -0
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +24 -4
- data/lib/samagotchi/bridge_client/event_stream.rb +81 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +22 -38
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +30 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +49 -4
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +7 -4
- data/lib/samagotchi/bundles/guardrails/manifest.yml +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +43 -0
- data/lib/samagotchi/cli/command.rb +3 -2
- data/lib/samagotchi/cli/flags.rb +5 -0
- data/lib/samagotchi/cli/parent_wait.rb +22 -13
- data/lib/samagotchi/context_command.rb +80 -73
- data/lib/samagotchi/engine.rb +12 -8
- data/lib/samagotchi/idle_recap.rb +7 -1
- data/lib/samagotchi/kernel_loop.rb +13 -1
- data/lib/samagotchi/launch_mode.rb +22 -0
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +233 -0
- data/lib/samagotchi/send_command/busy.rb +58 -0
- data/lib/samagotchi/send_command/parent_gate.rb +23 -10
- data/lib/samagotchi/send_command/wait_target.rb +16 -3
- data/lib/samagotchi/send_command.rb +173 -108
- data/lib/samagotchi/session.rb +30 -5
- data/lib/samagotchi/session_access/contract.rb +129 -0
- data/lib/samagotchi/session_access/dispatch.rb +133 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +349 -0
- data/lib/samagotchi/session_access/remote.rb +638 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +181 -0
- data/lib/samagotchi/session_access.rb +141 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_manager.rb +7 -2
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +51 -15
- data/lib/samagotchi/terminal_ui/attach_link.rb +104 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +80 -75
- data/lib/samagotchi/terminal_ui/local_link.rb +130 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +5 -3
- data/lib/samagotchi/terminal_ui.rb +2 -2
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +360 -69
- data/lib/samagotchi/web/public/data.js +3 -2
- data/lib/samagotchi/web/public/index.html +20 -4
- data/lib/samagotchi/web/public/stage_frame.js +7 -2
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/stream_handlers.js +3 -1
- data/lib/samagotchi/web/public/working_set.js +159 -28
- data/lib/samagotchi/web/public/working_set_rail.js +39 -12
- data/lib/samagotchi/web/public/working_set_view.js +91 -33
- data/lib/samagotchi/web/session_access_routes.rb +343 -0
- data/lib/samagotchi/worker_sidecar.rb +12 -5
- data/lib/samagotchi/working_set.rb +88 -14
- data/lib/samagotchi/working_set_command.rb +133 -30
- data/lib/samagotchi/working_set_pin.rb +34 -0
- data/lib/samagotchi/working_set_steer.rb +191 -27
- metadata +19 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../session"
|
|
4
|
+
require_relative "../session_access"
|
|
5
|
+
require_relative "../reply_wait"
|
|
6
|
+
require_relative "../bridge_client"
|
|
7
|
+
require_relative "../parent_report"
|
|
8
|
+
require_relative "../note_delivery"
|
|
9
|
+
require_relative "../image_store"
|
|
10
|
+
require_relative "../context_sources"
|
|
11
|
+
require_relative "../send_command/busy"
|
|
12
|
+
require_relative "../chi_invocation"
|
|
13
|
+
require_relative "../worker_sidecar"
|
|
14
|
+
require_relative "../launch_mode"
|
|
15
|
+
|
|
16
|
+
module Samagotchi
|
|
17
|
+
# Loaded on first use: SessionManager and its ~80 files only when a
|
|
18
|
+
# command reaches it (chi context never does).
|
|
19
|
+
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
20
|
+
|
|
21
|
+
module SessionAccess
|
|
22
|
+
# This machine's sessions: the calls the session commands made before
|
|
23
|
+
# the seam, moved here unchanged. The errors are the callees' own
|
|
24
|
+
# (ArgumentError and Session::AmbiguousId for an id, SessionManager's
|
|
25
|
+
# refusals), which the commands word.
|
|
26
|
+
class Local
|
|
27
|
+
# @param state_dir [String, nil] the sessions' folder; nil: the
|
|
28
|
+
# callees' own default (Session.default_state_dir), and no
|
|
29
|
+
# state_dir: is passed on (chi sessions stop/restart/list)
|
|
30
|
+
def initialize(state_dir: nil)
|
|
31
|
+
@state_dir = state_dir
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def remote? = false
|
|
35
|
+
|
|
36
|
+
# The printed commands' spelling: this machine's chi.
|
|
37
|
+
# @return [ChiInvocation]
|
|
38
|
+
def invocation = ChiInvocation::HERE
|
|
39
|
+
|
|
40
|
+
# --- ids and sessions
|
|
41
|
+
|
|
42
|
+
# The full id of +given+ (an id or a unique prefix); the session
|
|
43
|
+
# need not exist (Session.resolve_id).
|
|
44
|
+
# @raise [Session::AmbiguousId]
|
|
45
|
+
def resolve_id(given) = Session.resolve_id(given, **dir)
|
|
46
|
+
|
|
47
|
+
# The full id of +given+, of a session that exists.
|
|
48
|
+
# @raise [ArgumentError] no such session (Session::AmbiguousId: a
|
|
49
|
+
# prefix of several)
|
|
50
|
+
def resolve(given)
|
|
51
|
+
id = resolve_id(given)
|
|
52
|
+
show(id)
|
|
53
|
+
id
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Session]
|
|
57
|
+
# @raise [ArgumentError] no such session
|
|
58
|
+
def show(id) = Session.load(id, **dir)
|
|
59
|
+
|
|
60
|
+
# @return [OwnerLock::Owner, nil] the worker or chi REPL that owns it
|
|
61
|
+
def owner(id) = SessionManager.session_owner(id, **dir)
|
|
62
|
+
|
|
63
|
+
# SessionManager.list_sessions: Session objects, for chi sessions list.
|
|
64
|
+
def list(**filters) = SessionManager.list_sessions(**filters, **dir)
|
|
65
|
+
|
|
66
|
+
# SessionManager.session_summaries: the summary hashes (chi sessions
|
|
67
|
+
# list --live/--format, chi note --all).
|
|
68
|
+
def summaries(**filters) = SessionManager.session_summaries(**filters, **dir)
|
|
69
|
+
|
|
70
|
+
# The session's worker for an attach: the live one as is, else
|
|
71
|
+
# resumed and waited for up to +wait+ seconds (chi --attach on this
|
|
72
|
+
# machine; chi web's POST …/wake for a remote one).
|
|
73
|
+
# @return [Woken] its sidecar nil when the Bridge didn't come in time
|
|
74
|
+
# @raise [ArgumentError] no such session
|
|
75
|
+
# @raise [Refused] code scratch: a leftover chi scratch session
|
|
76
|
+
# @raise [SessionManager::OwnedByTUI] a chi REPL has it
|
|
77
|
+
def wake(id, wait:)
|
|
78
|
+
session = show(id)
|
|
79
|
+
raise Refused.new(LaunchMode::SCRATCH_LEFTOVER, code: "scratch") if session.scratch
|
|
80
|
+
|
|
81
|
+
live = WorkerSidecar.live(session_dir(id), unlink: true)
|
|
82
|
+
return Woken.new(woke: false, sidecar: live) if live
|
|
83
|
+
|
|
84
|
+
SessionManager.resume_session(id, **dir)
|
|
85
|
+
found = BridgeClient.wait_for(id, session_dir: session_dir(id), timeout: wait)
|
|
86
|
+
Woken.new(woke: true, sidecar: found && WorkerSidecar.read(session_dir(id)))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The session's worker now, never started (chi web's GET …/worker).
|
|
90
|
+
# @return [WorkerState]
|
|
91
|
+
# @raise [ArgumentError] no such session
|
|
92
|
+
def worker(id)
|
|
93
|
+
show(id)
|
|
94
|
+
WorkerState.new(sidecar: WorkerSidecar.live(session_dir(id), unlink: true),
|
|
95
|
+
stopped: Session.stopped_marker?(id, state_dir: state_dir!), owner: owner(id))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# --- create and send
|
|
99
|
+
|
|
100
|
+
# @param spec [NewSession]
|
|
101
|
+
# @return [Session] the session created (its id, model_warning,
|
|
102
|
+
# continues, moved_children)
|
|
103
|
+
# @raise [SessionContinue::Refused, SessionManager::ArchiveRefused,
|
|
104
|
+
# SessionManager::OwnedByTUI] refused; StandardError: could not start
|
|
105
|
+
def create(spec)
|
|
106
|
+
start = spec.preview ? { prompt: spec.prompt, preview: spec.preview } : { prompt: spec.prompt }
|
|
107
|
+
if spec.continues
|
|
108
|
+
return SessionManager.continue_session(spec.continues, **start, model: spec.model_name, **dir)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
SessionManager.spawn_session(**start, working_directory: spec.working_directory, model_name: spec.model_name,
|
|
112
|
+
setup: spec.setup, **dir)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Whether a just-created session's worker came up (its Bridge
|
|
116
|
+
# answers) within +timeout+ seconds.
|
|
117
|
+
def await_worker(id, timeout:)
|
|
118
|
+
BridgeClient.wait_for(id, session_dir: session_dir(id), timeout: timeout)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Copies of +refs+ (ImageStore refs stored in +from+) in the
|
|
122
|
+
# session's folder, as the refs a turn takes.
|
|
123
|
+
# @return [Array<Hash>] {file:, name:} each
|
|
124
|
+
def upload_images(id, refs, from:)
|
|
125
|
+
to = session_dir(id)
|
|
126
|
+
refs.map do |ref|
|
|
127
|
+
ImageStore.copy_file(ref, from: from, to: to)
|
|
128
|
+
{ file: ref[:file], name: ref[:name] }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [SessionManager::DeliverResult]
|
|
133
|
+
# @raise [SessionManager::OwnedByTUI] a chi REPL owns it
|
|
134
|
+
def deliver_turn(id, prompt:, client_id:, images:, delivery: nil)
|
|
135
|
+
SessionManager.deliver_turn(id, prompt: prompt, client_id: client_id, images: images, delivery: delivery, **dir)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [NoteDelivery::Result]
|
|
139
|
+
def note(id, text:, source:)
|
|
140
|
+
NoteDelivery.deliver(id, text: text, source: source, state_dir: state_dir!)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# --- waiting for the reply
|
|
144
|
+
|
|
145
|
+
# What a wait needs from before anything is sent (WaitStart).
|
|
146
|
+
# @param session [Session, nil] the session as the caller just read
|
|
147
|
+
# it (its checks and the baseline then see the same state); nil
|
|
148
|
+
# reads it here
|
|
149
|
+
# @param question_id [String, nil, :pending] the question the
|
|
150
|
+
# baseline counts as already reported (ReplyWait.baseline_of);
|
|
151
|
+
# :pending, the one pending now
|
|
152
|
+
# @param turn [String, nil] a turn asked for (--turn): known_turn
|
|
153
|
+
# says whether it can be waited for
|
|
154
|
+
# @param created [Boolean] +session+ was just created: no reply, no
|
|
155
|
+
# question and no ended turn yet, nothing else read (read later, as
|
|
156
|
+
# chi web's prepare route does, its first turn may have ended: that
|
|
157
|
+
# end is the answer, not the baseline's)
|
|
158
|
+
# @return [WaitStart]
|
|
159
|
+
def prepare_wait(id, session: nil, question_id: :pending, turn: nil, created: false)
|
|
160
|
+
session ||= show(id)
|
|
161
|
+
if created
|
|
162
|
+
return WaitStart.new(cursor: nil, baseline: ReplyWait::Baseline.new(messages: session.messages.size),
|
|
163
|
+
own_turn: nil, known_turn: nil, running: false, live: false, settled: false,
|
|
164
|
+
last_turn_id: nil)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
cursor = ReplyWait.newest_reply(id, state_dir: state_dir!)
|
|
168
|
+
baseline = question_id == :pending ? ReplyWait.baseline_of(session) : ReplyWait.baseline_of(session, question_id: question_id)
|
|
169
|
+
session_dir = session_dir(id)
|
|
170
|
+
running = session.status == Session::STATUS_RUNNING
|
|
171
|
+
WaitStart.new(
|
|
172
|
+
cursor: cursor, baseline: baseline, own_turn: ReplyWait.own_turn(session, session_dir: session_dir),
|
|
173
|
+
known_turn: (ReplyWait.known_turn?(session, session_dir: session_dir, turn: turn) if turn),
|
|
174
|
+
running: running, live: running || !owner(id).nil?,
|
|
175
|
+
settled: session.status == Session::STATUS_IDLE && !session.last_turn.nil? && session.pending_question.nil?,
|
|
176
|
+
last_turn_id: session.last_turn&.enqueued_id
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Why chi send --wait may not send into the session now (a turn runs,
|
|
181
|
+
# or a question waits on it with --queue), or nil.
|
|
182
|
+
# @param session [Session, nil] the session as the caller just read
|
|
183
|
+
# it; nil reads it here
|
|
184
|
+
# @return [SendCommand::Busy, nil]
|
|
185
|
+
def busy(id, queue:, session: nil)
|
|
186
|
+
session ||= show(id)
|
|
187
|
+
owner = owner(id) if session.status == Session::STATUS_RUNNING
|
|
188
|
+
SendCommand::Busy.for(session, owner: owner, queue: queue)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# ReplyWait.call: blocks until the reply, a question, the turn's end
|
|
192
|
+
# or +timeout+.
|
|
193
|
+
# @return [ReplyWait::Result]
|
|
194
|
+
# @raise [ArgumentError] the session is gone
|
|
195
|
+
def wait_reply(id, **)
|
|
196
|
+
ReplyWait.call(id, state_dir: state_dir!, **)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# What a parent may pick on the session's questions: this machine's
|
|
200
|
+
# guardrails.parent_answers tightened by the session's own
|
|
201
|
+
# (ParentReport.strategy_for).
|
|
202
|
+
def parent_strategy(id) = ParentReport.strategy_for(id, state_dir: state_dir!)
|
|
203
|
+
|
|
204
|
+
# --- questions
|
|
205
|
+
|
|
206
|
+
# The answer posted to the session's worker (its Bridge), once more
|
|
207
|
+
# after a 408.
|
|
208
|
+
# @return [BridgeClient::Response, nil] the Bridge's reply; nil when
|
|
209
|
+
# no worker runs it
|
|
210
|
+
# @raise [SystemCallError, IOError, SocketError] the worker went away
|
|
211
|
+
def answer(id, question_id:, selected:, freeform:, client_id:)
|
|
212
|
+
on_worker(id) { |client| client.answer(id: question_id, selected: selected, freeform: freeform, client_id: client_id) }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The question dismissed on the session's worker; as #answer.
|
|
216
|
+
def dismiss(id, question_id:)
|
|
217
|
+
on_worker(id) { |client| client.dismiss_question(id: question_id) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# --- sessions
|
|
221
|
+
|
|
222
|
+
# @return [Boolean] whether the worker released the session
|
|
223
|
+
def stop(id, wait:) = SessionManager.stop_session(id, wait: wait, **dir)
|
|
224
|
+
|
|
225
|
+
# @return [SessionManager::RestartResult]
|
|
226
|
+
def restart(id) = SessionManager.restart_session(id, **dir)
|
|
227
|
+
|
|
228
|
+
# @return [Hash] SessionManager.archive_session's
|
|
229
|
+
def archive(id) = SessionManager.archive_session(id, **dir)
|
|
230
|
+
|
|
231
|
+
# @return [Hash] SessionManager.unarchive_session's
|
|
232
|
+
def unarchive(id) = SessionManager.unarchive_session(id, **dir)
|
|
233
|
+
|
|
234
|
+
# @param force [Boolean] stop a live worker first (else refused)
|
|
235
|
+
# @return [Hash] SessionManager.delete_session's
|
|
236
|
+
def delete(id, force:) = SessionManager.delete_session(id, stop: force, **dir)
|
|
237
|
+
|
|
238
|
+
# --- context (a session's sources, or a project's: local only)
|
|
239
|
+
#
|
|
240
|
+
# +target+ is chi context's Target (session_id, project_root; a
|
|
241
|
+
# project's when session_id is nil).
|
|
242
|
+
|
|
243
|
+
# Attaches +source+ to the target's own sources.
|
|
244
|
+
def context_add(target, source)
|
|
245
|
+
location = context_location(target)
|
|
246
|
+
location.add(source.with(scope: location.scope))
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# New text for the target's --push source +name+.
|
|
251
|
+
# @param fetched [ContextSources::Fetched]
|
|
252
|
+
# @return [ContextPush]
|
|
253
|
+
def context_push(target, name, fetched)
|
|
254
|
+
attached = context_attached(target, name) or return ContextPush.new(status: :missing)
|
|
255
|
+
return ContextPush.new(status: :not_push) unless attached.source.push?
|
|
256
|
+
|
|
257
|
+
before, written = attached.location.locked(name) do
|
|
258
|
+
[attached.location.snapshot(name).revision, attached.location.record_text(name, fetched)]
|
|
259
|
+
end
|
|
260
|
+
ContextPush.new(status: :pushed, before: before, revision: written.revision)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The target's sources: a session's own, then its project's (shadowed
|
|
264
|
+
# ones too, marked), muted and unread; a project's own.
|
|
265
|
+
# @return [Array<ContextEntry>]
|
|
266
|
+
def context_entries(target)
|
|
267
|
+
if target.project?
|
|
268
|
+
location = context_location(target)
|
|
269
|
+
return location.sources.map { |source| ContextEntry.new(source: source, snapshot: location.snapshot(source.name)) }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
own = context_location(target)
|
|
273
|
+
subs = own.subscriptions
|
|
274
|
+
attached_to(target, shadowed: true).map do |a|
|
|
275
|
+
snapshot = a.snapshot
|
|
276
|
+
read = subs[a.name]&.read
|
|
277
|
+
ContextEntry.new(source: a.source, snapshot: snapshot, muted: own.muted?(a.name), shadowed: a.shadowed,
|
|
278
|
+
unread: snapshot.text? && read != snapshot.revision)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The source +name+ as the target sees it, with its snapshot.
|
|
283
|
+
# @return [ContextEntry, nil]
|
|
284
|
+
def context_entry(target, name)
|
|
285
|
+
attached = context_attached(target, name) or return nil
|
|
286
|
+
ContextEntry.new(source: attached.source, snapshot: attached.snapshot)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Detaches the target's own source +name+.
|
|
290
|
+
# @return [Symbol] :removed, :project (a session's project has it:
|
|
291
|
+
# rm --project, or mute it) or :missing
|
|
292
|
+
def context_remove(target, name)
|
|
293
|
+
ContextSources.check_name!(name)
|
|
294
|
+
return :removed if context_location(target).remove(name)
|
|
295
|
+
|
|
296
|
+
project = !target.project? && ContextSources.project_location_for(target.project_root, state_dir: @state_dir)
|
|
297
|
+
project&.source(name) ? :project : :missing
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Local only (chi context refresh, mute, unmute, which work on the
|
|
301
|
+
# files here; Contract: local_only): the source +name+ as the target
|
|
302
|
+
# sees it, with the folder it lives in.
|
|
303
|
+
# @return [ContextSources::Attached, nil]
|
|
304
|
+
def context_attached(target, name)
|
|
305
|
+
ContextSources.check_name!(name)
|
|
306
|
+
if target.project?
|
|
307
|
+
location = context_location(target)
|
|
308
|
+
source = location.source(name)
|
|
309
|
+
return source && ContextSources::Attached.new(source: source, location: location, shadowed: false)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
attached_to(target).find { |a| a.name == name }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Local only, as #context_attached: the folder a target's own sources
|
|
316
|
+
# live in.
|
|
317
|
+
def context_location(target)
|
|
318
|
+
if target.project?
|
|
319
|
+
ContextSources.project_location_for(target.project_root, state_dir: @state_dir)
|
|
320
|
+
else
|
|
321
|
+
ContextSources.session_location(target.session_id, state_dir: @state_dir)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
def dir = @state_dir ? { state_dir: @state_dir } : {}
|
|
328
|
+
def state_dir! = @state_dir || Session.default_state_dir
|
|
329
|
+
def session_dir(id) = Session.session_dir(id, state_dir: state_dir!)
|
|
330
|
+
|
|
331
|
+
def attached_to(target, shadowed: false)
|
|
332
|
+
ContextSources.attached(target.session_id, project_root: target.project_root, state_dir: @state_dir,
|
|
333
|
+
shadowed: shadowed)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# The block's post to the session's live worker, again on a 408 (the
|
|
337
|
+
# worker didn't take it in time).
|
|
338
|
+
def on_worker(id)
|
|
339
|
+
client = BridgeClient.discover(id, session_dir: session_dir(id)) or return nil
|
|
340
|
+
reply = nil
|
|
341
|
+
2.times do
|
|
342
|
+
reply = yield client
|
|
343
|
+
break unless reply.status == 408
|
|
344
|
+
end
|
|
345
|
+
reply
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|