samagotchi 0.103.0 → 0.104.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 +54 -1
- data/bin/chi +39 -2
- data/docs/architecture.md +36 -24
- data/docs/cli.md +86 -12
- data/docs/guardrails.md +2 -1
- data/docs/internals/context-telemetry.md +6 -2
- data/docs/internals/engine.md +4 -1
- data/docs/internals/participants.md +137 -0
- data/docs/internals/remote-sessions.md +153 -27
- data/docs/plugins.md +7 -1
- data/docs/sessions.md +4 -2
- data/docs/sub-agent.md +8 -0
- data/docs/testing.md +68 -4
- data/lib/samagotchi/bridge.rb +7 -3
- data/lib/samagotchi/bridge_client/event_stream.rb +9 -4
- data/lib/samagotchi/bridge_client.rb +4 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/chi_invocation.rb +13 -16
- data/lib/samagotchi/context_status.rb +15 -2
- data/lib/samagotchi/engine.rb +16 -6
- data/lib/samagotchi/guardrail_wiring.rb +7 -1
- data/lib/samagotchi/idle_recap.rb +1 -1
- data/lib/samagotchi/idle_scheduler.rb +34 -9
- data/lib/samagotchi/kernel_loop.rb +8 -1
- data/lib/samagotchi/launch_mode.rb +4 -0
- data/lib/samagotchi/llm/chat_loop.rb +8 -1
- data/lib/samagotchi/owner_lock.rb +65 -5
- data/lib/samagotchi/participants.rb +122 -0
- data/lib/samagotchi/remote_attach_command.rb +119 -0
- data/lib/samagotchi/run_options.rb +14 -2
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session.rb +8 -4
- data/lib/samagotchi/session_access/contract.rb +42 -4
- data/lib/samagotchi/session_access/dispatch.rb +29 -10
- data/lib/samagotchi/session_access/local.rb +52 -1
- data/lib/samagotchi/session_access/remote.rb +112 -16
- data/lib/samagotchi/session_access/web_bridge_client.rb +56 -1
- data/lib/samagotchi/session_access.rb +85 -3
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_inbox.rb +18 -9
- data/lib/samagotchi/session_manager.rb +21 -5
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +174 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +17 -4
- data/lib/samagotchi/terminal_ui/attach_link.rb +10 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +83 -20
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +5 -0
- data/lib/samagotchi/terminal_ui/remote_connector.rb +66 -0
- data/lib/samagotchi/terminal_ui/remote_link.rb +245 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +23 -0
- data/lib/samagotchi/terminal_ui.rb +4 -1
- data/lib/samagotchi/testing/waiting.rb +92 -9
- data/lib/samagotchi/tools/task_runtime.rb +17 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +187 -74
- data/lib/samagotchi/web/grants.rb +302 -0
- data/lib/samagotchi/web/participant_access.rb +286 -0
- data/lib/samagotchi/web/principal.rb +29 -0
- data/lib/samagotchi/web/server.rb +3 -1
- data/lib/samagotchi/web/session_access_routes.rb +120 -4
- data/lib/samagotchi/worker.rb +26 -5
- metadata +10 -1
|
@@ -10,6 +10,9 @@ require_relative "../context_command"
|
|
|
10
10
|
require_relative "../send_command"
|
|
11
11
|
require_relative "../launch_mode"
|
|
12
12
|
require_relative "../model_profile"
|
|
13
|
+
require_relative "../client_id"
|
|
14
|
+
require_relative "../children_status"
|
|
15
|
+
require_relative "../session_metrics"
|
|
13
16
|
|
|
14
17
|
module Samagotchi
|
|
15
18
|
module Web
|
|
@@ -234,7 +237,9 @@ module Samagotchi
|
|
|
234
237
|
# waited for up to +wait+ seconds (WAKE_MAX_WAIT, also the most). 200
|
|
235
238
|
# {session_id, woke, worker: {epoch, started_at, version, features},
|
|
236
239
|
# model_name, default_model (this machine's config, for the TUI's
|
|
237
|
-
# status row), parent_strategy, installed (the newest chi here)}
|
|
240
|
+
# status row), parent_strategy, installed (the newest chi here)}. A
|
|
241
|
+
# worker still leaving is waited for first (Local#wake: its recap's
|
|
242
|
+
# timeout and a margin, on top of +wait+); 409
|
|
238
243
|
# owned_by_tui (a chi REPL has it), 409 scratch (a leftover scratch
|
|
239
244
|
# session, as chi --attach here says); 504 worker_timeout (its Bridge
|
|
240
245
|
# didn't come in time); 404 not_found.
|
|
@@ -262,13 +267,20 @@ module Samagotchi
|
|
|
262
267
|
|
|
263
268
|
# GET /api/sessions/:id/worker: the session's worker now, never
|
|
264
269
|
# started (SessionAccess::Local#worker): a remote attach's
|
|
265
|
-
# rediscovery and its features after a restart
|
|
266
|
-
#
|
|
267
|
-
#
|
|
270
|
+
# rediscovery and its features after a restart, and before its wake
|
|
271
|
+
# whether the worker is leaving (its recap on leave: the wake waits
|
|
272
|
+
# for it, so the client says so). 200 {session_id, live, stopped,
|
|
273
|
+
# owner: worker|tui|null, leaving, leave_seconds (its leave's budget
|
|
274
|
+
# left, nil unless leaving), worker: {epoch, started_at, version,
|
|
275
|
+
# features}|null, default_model (this machine's config, as the
|
|
276
|
+
# wake's), parent_strategy}; 404 not_found.
|
|
268
277
|
def handle_worker(_req, id)
|
|
269
278
|
state = session_access.worker(id)
|
|
279
|
+
leaving = state.leaving?
|
|
270
280
|
json_response(200, { session_id: id, live: state.live?, stopped: state.stopped, owner: state.owner&.kind,
|
|
281
|
+
leaving: leaving, leave_seconds: (state.owner.leave_left.round(1) if leaving),
|
|
271
282
|
worker: state.sidecar && worker_json(state.sidecar),
|
|
283
|
+
default_model: ModelProfile.default_model_name,
|
|
272
284
|
parent_strategy: strategy_json(session_access.parent_strategy(id)) })
|
|
273
285
|
end
|
|
274
286
|
|
|
@@ -278,6 +290,110 @@ module Samagotchi
|
|
|
278
290
|
{ epoch: sidecar.epoch, started_at: sidecar.started_at, version: sidecar.version, features: sidecar.features }
|
|
279
291
|
end
|
|
280
292
|
|
|
293
|
+
# POST /api/sessions/:id/exit {client_id, delete}: a remote attached
|
|
294
|
+
# TUI's /exit (and the worker's half of /exit --delete and /archive),
|
|
295
|
+
# relayed to the live worker's exit (BridgeClient#request_exit; never
|
|
296
|
+
# wakes one). client_id is passed on only when it is an attached TUI's
|
|
297
|
+
# (tui:…): any other is dropped, and then every stream counts as a UI,
|
|
298
|
+
# which only keeps the worker up. 200 the Bridge's {status: exiting,
|
|
299
|
+
# session_id, discard}; 409 held with its {status, reason, tasks,
|
|
300
|
+
# hint}; 503 not_live, 501 not_supported (an older worker), 504
|
|
301
|
+
# worker_timeout.
|
|
302
|
+
def handle_exit(req, id)
|
|
303
|
+
body = body_hash(req) or return refusal(400, "invalid_json", "invalid JSON body")
|
|
304
|
+
@session_class.load(id, state_dir: default_state_dir)
|
|
305
|
+
client_id = body["client_id"] if body["client_id"].is_a?(String) && body["client_id"].start_with?(ClientId::TUI_PREFIX)
|
|
306
|
+
delete = body["delete"] == true
|
|
307
|
+
|
|
308
|
+
request = ->(client) { client.request_exit(client_id: client_id, delete: delete) }
|
|
309
|
+
with_message(relay(id, bridge_client(id), request, what: "the worker was not asked to exit", cant: "exit from here") do |reply|
|
|
310
|
+
case reply.status
|
|
311
|
+
when 200 then json_response(200, reply.json || { status: "exiting", session_id: id })
|
|
312
|
+
when 409
|
|
313
|
+
held = reply.json || {}
|
|
314
|
+
words = "the session's worker stays up (#{held["reason"] || "held"})"
|
|
315
|
+
json_response(409, held.merge("error" => "held", "detail" => words, "message" => words, "session_id" => id))
|
|
316
|
+
end
|
|
317
|
+
end)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# POST /api/sessions/:id/recap: the saved recap, and a new one asked for
|
|
321
|
+
# (BridgeClient#request_recap; it arrives on the stream as
|
|
322
|
+
# :recap_ready). 200 the Bridge's {enabled, saved, request,
|
|
323
|
+
# min_user_turns, session_off}; 503, 501, 504 as every relay.
|
|
324
|
+
def handle_recap(_req, id)
|
|
325
|
+
@session_class.load(id, state_dir: default_state_dir)
|
|
326
|
+
request = lambda(&:request_recap)
|
|
327
|
+
with_message(relay(id, bridge_client(id), request, what: "no recap was read", cant: "recap") do |reply|
|
|
328
|
+
json_response(200, reply.json || {}) if reply.status == 200
|
|
329
|
+
end)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# GET /api/sessions/:id/stats: the live worker's metrics for /stats
|
|
333
|
+
# (its stats route; a worker from before it answers only its state's
|
|
334
|
+
# metrics). 200 {session_id, metrics}; 503 not_live, 504
|
|
335
|
+
# worker_timeout, 502 bridge_error.
|
|
336
|
+
def handle_stats(_req, id)
|
|
337
|
+
@session_class.load(id, state_dir: default_state_dir)
|
|
338
|
+
client = bridge_client(id) or return with_message(not_live(id))
|
|
339
|
+
|
|
340
|
+
status, json = client.get("stats")
|
|
341
|
+
metrics = json["metrics"] if status == 200 && json.is_a?(Hash)
|
|
342
|
+
if status == 404 && !(json.is_a?(Hash) && json["error"] == "unknown_session")
|
|
343
|
+
status, json = client.get("state")
|
|
344
|
+
metrics = json.dig("session_state_snapshot", "metrics") if status == 200 && json.is_a?(Hash)
|
|
345
|
+
end
|
|
346
|
+
return json_response(200, { session_id: id, metrics: metrics }) if status == 200
|
|
347
|
+
return with_message(worker_timeout("the stats were not read")) if status.nil?
|
|
348
|
+
return with_message(not_live(id)) if status == 404
|
|
349
|
+
|
|
350
|
+
with_message(error_response(502, "bridge_error", "the stats were not read: the session's worker answered #{status}"))
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# GET /api/sessions/:id/children/counts: the session's delegates'
|
|
354
|
+
# counts (ChildrenStatus.counts, from their files here), a remote
|
|
355
|
+
# TUI's status row segment. 200 {running, waiting, unreported}; 404.
|
|
356
|
+
def handle_children_counts(_req, id)
|
|
357
|
+
@session_class.load(id, state_dir: default_state_dir)
|
|
358
|
+
json_response(200, ChildrenStatus.counts(id, state_dir: default_state_dir).to_h)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# A turn id as a session's prompts carry it (a uuid; any word-ish id).
|
|
362
|
+
TURN_ID = /\A[\w.:-]{1,128}\z/
|
|
363
|
+
|
|
364
|
+
# GET /api/sessions/:id/turns/:turn_id/records: a turn's saved tool
|
|
365
|
+
# records and turn record (SessionMetrics, from analytics.json here),
|
|
366
|
+
# as saved: what a remote TUI's join draws the steps' durations and a
|
|
367
|
+
# failed turn's line from. 200 {tool_records: […], turn_record: {…} |
|
|
368
|
+
# null}; 400 invalid_turn_id; 404 not_found.
|
|
369
|
+
def handle_turn_records(_req, id, turn_id)
|
|
370
|
+
return refusal(400, "invalid_turn_id", "not a turn id: #{turn_id.to_s[0, 80]}") unless TURN_ID.match?(turn_id)
|
|
371
|
+
|
|
372
|
+
@session_class.load(id, state_dir: default_state_dir)
|
|
373
|
+
dir = Session.session_dir(id, state_dir: default_state_dir)
|
|
374
|
+
json_response(200, { tool_records: SessionMetrics.saved_tool_records(dir, turn_id),
|
|
375
|
+
turn_record: SessionMetrics.saved_turn_record(dir, turn_id) })
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# {error, detail, message}: the words a remote client raises with.
|
|
379
|
+
def refusal(status, code, words, **fields)
|
|
380
|
+
json_response(status, { error: code, detail: words, message: words, **fields })
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# A relay's error answer (#relay's {error, detail}) with its message
|
|
384
|
+
# too, as a remote attach's routes answer every refusal.
|
|
385
|
+
def with_message(response)
|
|
386
|
+
status, _headers, chunks = response
|
|
387
|
+
return response if status < 400
|
|
388
|
+
|
|
389
|
+
body = JSON.parse(chunks.join)
|
|
390
|
+
return response unless body.is_a?(Hash) && body["error"] && !body.key?("message")
|
|
391
|
+
|
|
392
|
+
json_response(status, body.merge("message" => body["detail"]))
|
|
393
|
+
rescue JSON::ParserError
|
|
394
|
+
response
|
|
395
|
+
end
|
|
396
|
+
|
|
281
397
|
# POST /api/sessions/:id/notes {text, source}: a context note for the
|
|
282
398
|
# session (NoteDelivery, as chi note; source: where it came from,
|
|
283
399
|
# "cli" by default, never "broadcast"). 200 {status: "queued" (a
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -122,9 +122,14 @@ module Samagotchi
|
|
|
122
122
|
# trapped signals (SessionManager.run_session_loop); none by default
|
|
123
123
|
# @param signal_grace [Array(Numeric, Numeric, Numeric)] the grace, the
|
|
124
124
|
# short grace and the hard exit's delay (specs shorten them)
|
|
125
|
+
# @param owner_lock [OwnerLock, nil] the session's, which this process
|
|
126
|
+
# holds (SessionManager.run_session_loop): marked leaving as the Bridge
|
|
127
|
+
# stops, so an attach waits for the recap on leave (OwnerLock.await_release)
|
|
125
128
|
def initialize(session_id:, state_dir:, session_dir:, idle_exit_minutes: nil, poll_interval: nil,
|
|
126
|
-
signals: WorkerSignals::NONE, signal_grace: [SIGNAL_GRACE, SIGNAL_SHORT_GRACE, SIGNAL_HARD_EXIT]
|
|
129
|
+
signals: WorkerSignals::NONE, signal_grace: [SIGNAL_GRACE, SIGNAL_SHORT_GRACE, SIGNAL_HARD_EXIT],
|
|
130
|
+
owner_lock: nil)
|
|
127
131
|
@session_id = session_id
|
|
132
|
+
@owner_lock = owner_lock
|
|
128
133
|
@state_dir = state_dir
|
|
129
134
|
@session_dir = session_dir
|
|
130
135
|
@idle_exit_minutes = idle_exit_minutes
|
|
@@ -1245,7 +1250,8 @@ module Samagotchi
|
|
|
1245
1250
|
next false unless @idle_exit.due?
|
|
1246
1251
|
|
|
1247
1252
|
@bridge&.close_intake
|
|
1248
|
-
|
|
1253
|
+
# Not joined: a recap tick's emit waits for the event log held here.
|
|
1254
|
+
@engine.stop_idle(join: false)
|
|
1249
1255
|
log_idle_exit
|
|
1250
1256
|
true
|
|
1251
1257
|
end
|
|
@@ -1255,14 +1261,28 @@ module Samagotchi
|
|
|
1255
1261
|
# The Bridge stops once the block (which closed its intake with the
|
|
1256
1262
|
# event log held) answers true, outside the log: Bridge#stop waits for
|
|
1257
1263
|
# the requests it is answering, and one waiting on the log would hold
|
|
1258
|
-
# it for the whole grace before being killed.
|
|
1264
|
+
# it for the whole grace before being killed. The owner lock says it is
|
|
1265
|
+
# leaving first, so a client that finds no Bridge from here on waits
|
|
1266
|
+
# for the lock (OwnerLock.await_release) instead of giving up.
|
|
1259
1267
|
# @return [Boolean] what the block answered
|
|
1260
1268
|
def stop_bridge_after
|
|
1261
1269
|
leaving = yield
|
|
1262
|
-
|
|
1270
|
+
if leaving
|
|
1271
|
+
mark_leaving
|
|
1272
|
+
@bridge&.stop
|
|
1273
|
+
end
|
|
1263
1274
|
leaving
|
|
1264
1275
|
end
|
|
1265
1276
|
|
|
1277
|
+
# How long the lock stays held after the Bridge: the recap on leave's
|
|
1278
|
+
# timeout (#left), none for a restart (#left writes none). A delete
|
|
1279
|
+
# writes none either, but the bound only has to be an upper one.
|
|
1280
|
+
def mark_leaving
|
|
1281
|
+
@owner_lock&.mark_leaving(wait: @exit_restart ? 0 : @engine.recap_timeout.to_f)
|
|
1282
|
+
rescue StandardError => e
|
|
1283
|
+
Log.warn(:worker, "mark_leaving_failed", error: e.class.name, msg: e.message)
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1266
1286
|
# Bridge POST /exit, on a Bridge thread with the event log held.
|
|
1267
1287
|
# +delete+: the session is deleted after (/exit --delete), so no recap.
|
|
1268
1288
|
# +restart+: a new worker takes over (on the newest chi installed), so
|
|
@@ -1313,7 +1333,8 @@ module Samagotchi
|
|
|
1313
1333
|
end
|
|
1314
1334
|
|
|
1315
1335
|
@bridge&.close_intake
|
|
1316
|
-
|
|
1336
|
+
# Not joined: a recap tick's emit waits for the event log held here.
|
|
1337
|
+
@engine.stop_idle(join: false)
|
|
1317
1338
|
Log.info(:worker, @exit_restart ? "restart_requested" : "exit_requested", by: @exit_requested_by || "a client")
|
|
1318
1339
|
true
|
|
1319
1340
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.104.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- docs/internals/llm-context-bench.md
|
|
161
161
|
- docs/internals/llm-context-forget.md
|
|
162
162
|
- docs/internals/llm-context-live.md
|
|
163
|
+
- docs/internals/participants.md
|
|
163
164
|
- docs/internals/prompt-caching.md
|
|
164
165
|
- docs/internals/remote-sessions.md
|
|
165
166
|
- docs/internals/tool-guardrails.md
|
|
@@ -462,6 +463,7 @@ files:
|
|
|
462
463
|
- lib/samagotchi/owner_lock.rb
|
|
463
464
|
- lib/samagotchi/parent_answers_command.rb
|
|
464
465
|
- lib/samagotchi/parent_report.rb
|
|
466
|
+
- lib/samagotchi/participants.rb
|
|
465
467
|
- lib/samagotchi/paths.rb
|
|
466
468
|
- lib/samagotchi/peer_sources.rb
|
|
467
469
|
- lib/samagotchi/pending_input_queue.rb
|
|
@@ -506,6 +508,7 @@ files:
|
|
|
506
508
|
- lib/samagotchi/relay_watcher.rb
|
|
507
509
|
- lib/samagotchi/reminder_queue.rb
|
|
508
510
|
- lib/samagotchi/reminder_store.rb
|
|
511
|
+
- lib/samagotchi/remote_attach_command.rb
|
|
509
512
|
- lib/samagotchi/remote_command.rb
|
|
510
513
|
- lib/samagotchi/remotes.rb
|
|
511
514
|
- lib/samagotchi/reply_wait.rb
|
|
@@ -549,6 +552,7 @@ files:
|
|
|
549
552
|
- lib/samagotchi/session_setup.rb
|
|
550
553
|
- lib/samagotchi/sessions_command.rb
|
|
551
554
|
- lib/samagotchi/setting_explained.rb
|
|
555
|
+
- lib/samagotchi/share_command.rb
|
|
552
556
|
- lib/samagotchi/side_usage.rb
|
|
553
557
|
- lib/samagotchi/steer.rb
|
|
554
558
|
- lib/samagotchi/steer_cut.rb
|
|
@@ -576,6 +580,8 @@ files:
|
|
|
576
580
|
- lib/samagotchi/terminal_ui/question_slot.rb
|
|
577
581
|
- lib/samagotchi/terminal_ui/reline_history_hook.rb
|
|
578
582
|
- lib/samagotchi/terminal_ui/reline_seam.rb
|
|
583
|
+
- lib/samagotchi/terminal_ui/remote_connector.rb
|
|
584
|
+
- lib/samagotchi/terminal_ui/remote_link.rb
|
|
579
585
|
- lib/samagotchi/terminal_ui/repl_input.rb
|
|
580
586
|
- lib/samagotchi/terminal_ui/screen.rb
|
|
581
587
|
- lib/samagotchi/terminal_ui/status_row.rb
|
|
@@ -675,9 +681,12 @@ files:
|
|
|
675
681
|
- lib/samagotchi/web/app.rb
|
|
676
682
|
- lib/samagotchi/web/capabilities.rb
|
|
677
683
|
- lib/samagotchi/web/editor.rb
|
|
684
|
+
- lib/samagotchi/web/grants.rb
|
|
678
685
|
- lib/samagotchi/web/info_probe.rb
|
|
679
686
|
- lib/samagotchi/web/lan.rb
|
|
680
687
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
688
|
+
- lib/samagotchi/web/participant_access.rb
|
|
689
|
+
- lib/samagotchi/web/principal.rb
|
|
681
690
|
- lib/samagotchi/web/public/activity.js
|
|
682
691
|
- lib/samagotchi/web/public/annotate_actions.js
|
|
683
692
|
- lib/samagotchi/web/public/annotate_bar.js
|