samagotchi 0.102.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 +93 -1
- data/bin/chi +60 -191
- data/docs/architecture.md +67 -1
- data/docs/cli.md +190 -9
- data/docs/configuration.md +7 -1
- data/docs/guardrails.md +4 -2
- data/docs/internals/context-telemetry.md +6 -2
- data/docs/internals/engine.md +5 -2
- data/docs/internals/llm-context-forget.md +11 -1
- data/docs/internals/participants.md +137 -0
- data/docs/internals/remote-sessions.md +429 -0
- data/docs/internals/working-set.md +95 -11
- data/docs/plugins.md +33 -3
- data/docs/sessions.md +16 -7
- data/docs/sub-agent.md +16 -0
- data/docs/testing.md +68 -4
- data/lib/samagotchi/answer_command.rb +27 -25
- data/lib/samagotchi/bridge.rb +31 -7
- data/lib/samagotchi/bridge_client/event_stream.rb +86 -12
- data/lib/samagotchi/bridge_client/stream_request.rb +77 -0
- data/lib/samagotchi/bridge_client.rb +26 -39
- 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 +40 -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/context_status.rb +15 -2
- data/lib/samagotchi/engine.rb +28 -14
- data/lib/samagotchi/guardrail_wiring.rb +7 -1
- data/lib/samagotchi/idle_recap.rb +8 -2
- data/lib/samagotchi/idle_scheduler.rb +34 -9
- data/lib/samagotchi/kernel_loop.rb +21 -2
- data/lib/samagotchi/launch_mode.rb +26 -0
- data/lib/samagotchi/llm/chat_loop.rb +8 -1
- data/lib/samagotchi/llm_context_forget.rb +13 -6
- data/lib/samagotchi/note_command.rb +8 -7
- data/lib/samagotchi/owner_lock.rb +65 -5
- data/lib/samagotchi/parent_report.rb +55 -43
- data/lib/samagotchi/participants.rb +122 -0
- data/lib/samagotchi/remote_attach_command.rb +119 -0
- data/lib/samagotchi/remote_command.rb +170 -0
- data/lib/samagotchi/remotes.rb +130 -0
- data/lib/samagotchi/run_options.rb +245 -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 +38 -9
- data/lib/samagotchi/session_access/contract.rb +167 -0
- data/lib/samagotchi/session_access/dispatch.rb +152 -0
- data/lib/samagotchi/session_access/http_transport.rb +109 -0
- data/lib/samagotchi/session_access/local.rb +400 -0
- data/lib/samagotchi/session_access/remote.rb +734 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +236 -0
- data/lib/samagotchi/session_access.rb +223 -0
- data/lib/samagotchi/session_archive_command.rb +9 -5
- data/lib/samagotchi/session_commands.rb +20 -15
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +8 -4
- data/lib/samagotchi/session_inbox.rb +18 -9
- data/lib/samagotchi/session_manager.rb +27 -6
- data/lib/samagotchi/session_setup.rb +11 -6
- data/lib/samagotchi/sessions_command.rb +36 -14
- data/lib/samagotchi/share_command.rb +174 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +66 -17
- data/lib/samagotchi/terminal_ui/attach_link.rb +114 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +155 -87
- data/lib/samagotchi/terminal_ui/formatting.rb +13 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +135 -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 +28 -3
- data/lib/samagotchi/terminal_ui.rb +6 -3
- data/lib/samagotchi/testing/waiting.rb +92 -9
- data/lib/samagotchi/title_command.rb +9 -11
- data/lib/samagotchi/tools/task_runtime.rb +17 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +530 -126
- 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/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/server.rb +3 -1
- data/lib/samagotchi/web/session_access_routes.rb +459 -0
- data/lib/samagotchi/worker.rb +26 -5
- 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 +28 -1
|
@@ -14,6 +14,7 @@ require_relative "line_reader"
|
|
|
14
14
|
require_relative "question_prompt"
|
|
15
15
|
require_relative "reline_seam"
|
|
16
16
|
require_relative "version_lines"
|
|
17
|
+
require_relative "local_link"
|
|
17
18
|
require_relative "../bridge/turn_accumulator"
|
|
18
19
|
require_relative "../bridge_client"
|
|
19
20
|
require_relative "../delivery"
|
|
@@ -45,8 +46,6 @@ module Samagotchi
|
|
|
45
46
|
include InputSupport
|
|
46
47
|
|
|
47
48
|
PROMPT = "> "
|
|
48
|
-
# How long /exit --delete waits for the worker to let go.
|
|
49
|
-
DELETE_WAIT = 10
|
|
50
49
|
# A second Ctrl-C at an empty idle prompt within this many seconds detaches.
|
|
51
50
|
DETACH_WINDOW = 2.0
|
|
52
51
|
DETACH_HINT = "Ctrl-D to detach, /exit stops the worker"
|
|
@@ -84,7 +83,8 @@ module Samagotchi
|
|
|
84
83
|
# waiting when it ended :unanswered
|
|
85
84
|
attr_reader :unanswered
|
|
86
85
|
|
|
87
|
-
# @param client [BridgeClient]
|
|
86
|
+
# @param client [BridgeClient] the session's on this machine (a
|
|
87
|
+
# LocalLink is made of it and the keywords below), or nil with +link+
|
|
88
88
|
# @param screen [Surface] with #synchronize and #columns (a Screen, or a
|
|
89
89
|
# PlainSurface when the terminal can't show a live region)
|
|
90
90
|
# @param client_id [String] this UI's id in the events ("tui:<pid>")
|
|
@@ -108,19 +108,20 @@ module Samagotchi
|
|
|
108
108
|
# ChildrenStatus::Counts, the status row's children segment
|
|
109
109
|
# @param state_dir [String] the state dir the worker was found in: its
|
|
110
110
|
# session folder (@path images, saved records) is under it
|
|
111
|
-
|
|
111
|
+
# @param link [AttachLink, nil] where the session is, instead of
|
|
112
|
+
# +client+ and the keywords that make a LocalLink
|
|
113
|
+
def initialize(screen:, client_id:, client: nil, first_prompt: nil, first_command: nil, no_interrupt: false,
|
|
112
114
|
default_input: false, wait_at_eof: false, as_parent: false,
|
|
113
115
|
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
|
|
114
|
-
delete_session: ->(id) { SessionManager.delete_session(id, stop: true, wait: DELETE_WAIT) },
|
|
115
|
-
archive_session: ->(id) { SessionManager.archive_session(id, wait: DELETE_WAIT) },
|
|
116
|
+
delete_session: ->(id) { SessionManager.delete_session(id, stop: true, wait: LocalLink::DELETE_WAIT) },
|
|
117
|
+
archive_session: ->(id) { SessionManager.archive_session(id, wait: LocalLink::DELETE_WAIT) },
|
|
116
118
|
installed_version: -> { InstalledVersions.new.newest },
|
|
117
119
|
state_dir: Session.default_state_dir,
|
|
118
|
-
children_counts: ->(id) { ChildrenStatus.counts(id, state_dir: state_dir) })
|
|
119
|
-
@
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
@
|
|
123
|
-
@archive_session = archive_session
|
|
120
|
+
children_counts: ->(id) { ChildrenStatus.counts(id, state_dir: state_dir) }, link: nil)
|
|
121
|
+
@link = link || LocalLink.new(client: client, state_dir: state_dir, delete_session: delete_session,
|
|
122
|
+
archive_session: archive_session, installed_version: installed_version,
|
|
123
|
+
children_counts: children_counts)
|
|
124
|
+
@client = @link.client
|
|
124
125
|
@screen = screen
|
|
125
126
|
@client_id = client_id
|
|
126
127
|
# A parent agent drives this attach (AttachLauncher:
|
|
@@ -170,7 +171,6 @@ module Samagotchi
|
|
|
170
171
|
@continue_offer = nil
|
|
171
172
|
# The status row: the worker's model, ctx, the session's memories.
|
|
172
173
|
@status = StatusRow.new(screen)
|
|
173
|
-
@children_counts = children_counts
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def running? = @running
|
|
@@ -197,7 +197,7 @@ module Samagotchi
|
|
|
197
197
|
# A new worker (a restart) is followed, not lost: its sidecar is
|
|
198
198
|
# looked for whenever the stream drops.
|
|
199
199
|
stream = @client.follow(client_id: @client_id, rediscover: method(:live_worker),
|
|
200
|
-
reconnect_delays:
|
|
200
|
+
reconnect_delays: @link.reconnect_delays) do |event|
|
|
201
201
|
queue << [:event, event]
|
|
202
202
|
end
|
|
203
203
|
@reader = LineReader.new(queue, prompt: method(:prompt_text), read: input || method(:read_input_line),
|
|
@@ -335,10 +335,13 @@ module Samagotchi
|
|
|
335
335
|
@status.take_event(event)
|
|
336
336
|
@renderer.call(event)
|
|
337
337
|
when :worker_changed then switch_worker(event)
|
|
338
|
+
# A stream through chi web (never a Bridge here): a reconnect failed,
|
|
339
|
+
# and the next frame came.
|
|
340
|
+
when :stream_retrying then @screen.commit("(lost the connection to #{via.remote || "the worker"}; reconnecting…)")
|
|
341
|
+
when :stream_resumed then @screen.commit("(reconnected)")
|
|
338
342
|
when :stream_closed
|
|
339
343
|
@view.finish_thinking_spinner
|
|
340
|
-
@screen.commit(
|
|
341
|
-
"Resume it with: chi --shared --resume #{@client.session_id}")
|
|
344
|
+
@screen.commit(closed_line(event[:reason].to_s))
|
|
342
345
|
return :closed
|
|
343
346
|
else @renderer.call(event)
|
|
344
347
|
end
|
|
@@ -349,30 +352,56 @@ module Samagotchi
|
|
|
349
352
|
|
|
350
353
|
# The newest chi installed, looked at once, at the join.
|
|
351
354
|
def installed_version
|
|
352
|
-
@installed_version
|
|
355
|
+
@link.installed_version
|
|
353
356
|
rescue StandardError
|
|
354
357
|
nil
|
|
355
358
|
end
|
|
356
359
|
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
# stopped (no worker is coming).
|
|
360
|
-
def live_worker
|
|
361
|
-
id = @client.session_id
|
|
362
|
-
return :gone if Session.stopped_marker?(id)
|
|
360
|
+
# How the commands this TUI prints are spelled (AttachLink#via).
|
|
361
|
+
def via = @link.via
|
|
363
362
|
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
# The session's live worker for the stream to follow (EventStream's
|
|
364
|
+
# rediscover), with its reason (AttachLink#live_worker's pair): the
|
|
365
|
+
# stream's closing event carries the last one (#closed_line).
|
|
366
|
+
def live_worker = @link.live_worker
|
|
366
367
|
|
|
367
368
|
# The stream moved to a new worker (a restart): requests go there too.
|
|
368
369
|
def switch_worker(event)
|
|
369
|
-
@client =
|
|
370
|
+
@client = @link.switched(event)
|
|
370
371
|
@worker_changed = true
|
|
372
|
+
# The new worker's machine may name another default model.
|
|
373
|
+
@default_model_name = nil
|
|
371
374
|
@view.finish_thinking_spinner
|
|
372
|
-
Log.info(:attached, "worker_changed", port: event[:
|
|
375
|
+
Log.info(:attached, "worker_changed", port: AttachLink.event_port(event), epoch: event[:epoch])
|
|
373
376
|
nil
|
|
374
377
|
end
|
|
375
378
|
|
|
379
|
+
# The stream ended for good: why, and what brings the session back.
|
|
380
|
+
def closed_line(reason)
|
|
381
|
+
id = @client.session_id
|
|
382
|
+
return remote_closed_line(reason, id) if via.remote?
|
|
383
|
+
|
|
384
|
+
# Here a stopped session has always read as unreachable.
|
|
385
|
+
reason = "unreachable" if reason == "stopped"
|
|
386
|
+
"Lost the session's worker (#{reason}). Resume it with: #{via.command("--shared --resume #{id}")}"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Each reason in its own words (the remote's name, its chi web's
|
|
390
|
+
# failure as AttachLink#failure gave it).
|
|
391
|
+
def remote_closed_line(reason, id)
|
|
392
|
+
name = via.remote
|
|
393
|
+
failure = @link.failure
|
|
394
|
+
case reason
|
|
395
|
+
when "stopped" then "The session was stopped on #{name}. Resume it with: #{via.resume(id)}"
|
|
396
|
+
when "auth_failed", "too_old" then "Lost the session: #{failure || reason}"
|
|
397
|
+
when "worker_gone" then "The session's worker on #{name} is gone. Resume it with: #{via.resume(id)}"
|
|
398
|
+
when "unknown_session" then "The session is gone from #{name} (deleted)."
|
|
399
|
+
else
|
|
400
|
+
cause = failure&.sub(/\Aremote \S+ \([^)]*\): /, "")
|
|
401
|
+
"Lost the connection to #{name}#{" (#{cause})" if cause}. Re-attach with: #{via.attach(id)}"
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
376
405
|
def next_item(queue)
|
|
377
406
|
queue.pop
|
|
378
407
|
rescue Interrupt
|
|
@@ -390,7 +419,7 @@ module Samagotchi
|
|
|
390
419
|
|
|
391
420
|
# @return [Symbol, nil] :detach to end the loop
|
|
392
421
|
def submit(line)
|
|
393
|
-
return detach("Detached; the session keeps running. Re-attach with:
|
|
422
|
+
return detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if line.nil?
|
|
394
423
|
|
|
395
424
|
text = line.strip
|
|
396
425
|
if @question
|
|
@@ -401,6 +430,31 @@ module Samagotchi
|
|
|
401
430
|
return answer_question(text)
|
|
402
431
|
end
|
|
403
432
|
|
|
433
|
+
text, kept = split_prefilled(text)
|
|
434
|
+
submitted = submit_line(text)
|
|
435
|
+
# The removed text the command was typed after, back in the input
|
|
436
|
+
# (unless an /unqueue put its own there, or the loop ends).
|
|
437
|
+
@prefilled = kept if kept && submitted != :detach && @prefilled.nil? && @reader&.prefill(kept)
|
|
438
|
+
submitted
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# A line typed after an /unqueue's text (#unqueued) without clearing
|
|
442
|
+
# it, the text untouched and then a command ("fix it/unqueue"): the
|
|
443
|
+
# command alone, and the text to put back. Any other line as is.
|
|
444
|
+
# @return [Array(String, String|nil)]
|
|
445
|
+
def split_prefilled(text)
|
|
446
|
+
kept = @prefilled
|
|
447
|
+
@prefilled = nil
|
|
448
|
+
return [text, nil] unless kept && text.length > kept.length && text.start_with?(kept)
|
|
449
|
+
|
|
450
|
+
rest = text[kept.length..].strip
|
|
451
|
+
route = ComposerRoute.call(rest, running: @running, registry: command_registry)
|
|
452
|
+
command = route.choice || %i[local command usage].include?(route.kind)
|
|
453
|
+
command ? [rest, kept] : [text, nil]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# @return [Symbol, nil] :detach to end the loop
|
|
457
|
+
def submit_line(text)
|
|
404
458
|
# /cut TEXT and /queue TEXT are this UI's own deliveries, read before
|
|
405
459
|
# every command check (ComposerRoute; SessionCommands registers them
|
|
406
460
|
# for Tab and /help only). A plain line sends no delivery: it goes in
|
|
@@ -455,20 +509,18 @@ module Samagotchi
|
|
|
455
509
|
# @return [nil]
|
|
456
510
|
def submit_delivery(route)
|
|
457
511
|
return @screen.commit(STEER_CHOICE_USAGE) if route.kind == :usage
|
|
458
|
-
return @screen.commit(
|
|
512
|
+
return @screen.commit(queue_needs_newer_worker) if Delivery.queue?(route.delivery) && !worker_queues?
|
|
459
513
|
|
|
460
|
-
# To an idle session it is an ordinary prompt: no delivery to send
|
|
461
|
-
|
|
514
|
+
# To an idle session it is an ordinary prompt: no delivery to send,
|
|
515
|
+
# and its ack says it was sent now (#sent_note).
|
|
516
|
+
send_prompt(route.text, delivery: route.delivery, choice: route.choice)
|
|
462
517
|
nil
|
|
463
518
|
end
|
|
464
519
|
|
|
465
520
|
# Whether the attached worker runs delivery: queue (its sidecar names
|
|
466
521
|
# Delivery::QUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
467
522
|
# answers (a P0-P2 worker refuses a queue: #send_prompt).
|
|
468
|
-
def worker_queues?
|
|
469
|
-
sidecar = WorkerSidecar.read(Session.session_dir(@client.session_id, state_dir: @state_dir))
|
|
470
|
-
sidecar.nil? || sidecar.features.include?(Delivery::QUEUE_FEATURE)
|
|
471
|
-
end
|
|
523
|
+
def worker_queues? = @link.queues?
|
|
472
524
|
|
|
473
525
|
# @return [Symbol] :detach
|
|
474
526
|
def detach(line)
|
|
@@ -502,12 +554,13 @@ module Samagotchi
|
|
|
502
554
|
# An empty session: the worker deletes it as it leaves.
|
|
503
555
|
return detach(EMPTY_EXIT) if discards?(reply)
|
|
504
556
|
|
|
505
|
-
@delete_session
|
|
557
|
+
@link.delete_session
|
|
506
558
|
detach("Detached; deleted session #{id}.")
|
|
507
559
|
rescue SystemCallError, IOError => e
|
|
508
560
|
detach("#{exit_failed_line(e.message)} Not deleted.")
|
|
509
561
|
rescue SessionManager::DeleteRefused, SessionManager::OwnedByTUI, ArgumentError => e
|
|
510
|
-
detach("Detached; the worker is stopping, but session #{id} was not deleted (#{e.message}):
|
|
562
|
+
detach("Detached; the worker is stopping, but session #{id} was not deleted (#{e.message}): " \
|
|
563
|
+
"#{via.command("sessions delete #{id}")}")
|
|
511
564
|
end
|
|
512
565
|
|
|
513
566
|
# /archive during a turn (D2): it waits for the turn's end and the
|
|
@@ -540,14 +593,14 @@ module Samagotchi
|
|
|
540
593
|
return detach(EMPTY_EXIT) if reply.status == 200 && discards?(reply)
|
|
541
594
|
return detach("#{exit_line(reply)} Not archived.") unless [200, 409].include?(reply.status)
|
|
542
595
|
|
|
543
|
-
result = @archive_session
|
|
596
|
+
result = @link.archive_session
|
|
544
597
|
return detach("Detached; the session was empty, so it is discarded.") if result[:discarded].include?(id)
|
|
545
598
|
|
|
546
|
-
detach("Detached; archived session #{id}.
|
|
599
|
+
detach("Detached; archived session #{id}. #{via.command("sessions list --archived")} finds it.")
|
|
547
600
|
rescue SystemCallError, IOError => e
|
|
548
601
|
detach("#{exit_failed_line(e.message)} Not archived.")
|
|
549
602
|
rescue SessionManager::ArchiveRefused, SessionManager::OwnedByTUI, ArgumentError => e
|
|
550
|
-
detach("Detached; session #{id} was not archived (#{e.message}). Re-attach with:
|
|
603
|
+
detach("Detached; session #{id} was not archived (#{e.message}). Re-attach with: #{via.attach(id)}")
|
|
551
604
|
end
|
|
552
605
|
|
|
553
606
|
def exit_line(reply)
|
|
@@ -557,16 +610,18 @@ module Samagotchi
|
|
|
557
610
|
when 200
|
|
558
611
|
return EMPTY_EXIT if discards?(reply)
|
|
559
612
|
|
|
560
|
-
"Detached; the session's worker is stopping. Resume with:
|
|
613
|
+
"Detached; the session's worker is stopping. Resume with: #{via.resume(id)}"
|
|
561
614
|
when 409
|
|
562
615
|
reason = reply.json&.fetch("reason", nil).to_s
|
|
563
616
|
"Detached; the session keeps running (#{held_words(reason, reply.json&.fetch("tasks", nil))}). " \
|
|
564
|
-
"Re-attach with:
|
|
617
|
+
"Re-attach with: #{via.attach(id)}"
|
|
565
618
|
when 0 then exit_failed_line("no reply")
|
|
566
619
|
when 408 then exit_failed_line(LATE)
|
|
567
620
|
else
|
|
568
621
|
# A worker from before the route answers the Bridge's plain 404.
|
|
569
|
-
|
|
622
|
+
if error == "not_found"
|
|
623
|
+
return "Detached; this worker runs an older chi and can't be stopped from here: #{via.command("sessions stop #{id}")}"
|
|
624
|
+
end
|
|
570
625
|
|
|
571
626
|
exit_failed_line([reply.status, reply.json&.fetch("detail", nil) || error].compact.join(" "))
|
|
572
627
|
end
|
|
@@ -584,7 +639,7 @@ module Samagotchi
|
|
|
584
639
|
def discards?(reply) = reply.json&.fetch("discard", nil) == true
|
|
585
640
|
|
|
586
641
|
def exit_failed_line(why)
|
|
587
|
-
"Detached (could not ask the worker to stop: #{why}). Re-attach with:
|
|
642
|
+
"Detached (could not ask the worker to stop: #{why}). Re-attach with: #{via.attach(@client.session_id)}"
|
|
588
643
|
end
|
|
589
644
|
|
|
590
645
|
# The worker says what became of it (:continue_resolved); an invalid
|
|
@@ -631,6 +686,9 @@ module Samagotchi
|
|
|
631
686
|
# Why a Bridge request raised, for the line that says what didn't go
|
|
632
687
|
# through: a stopped (or wedged) worker times out, a gone one refuses.
|
|
633
688
|
def worker_down(error)
|
|
689
|
+
# Through chi web: the message names the remote and what failed.
|
|
690
|
+
return error.message if via.remote?
|
|
691
|
+
|
|
634
692
|
head, detail = error.message.split(" - ", 2)
|
|
635
693
|
return "worker not answering: #{(detail || head).sub(/\Abridge \S+: /, "")}" if error.is_a?(Errno::ETIMEDOUT)
|
|
636
694
|
|
|
@@ -639,27 +697,31 @@ module Samagotchi
|
|
|
639
697
|
|
|
640
698
|
QUEUED_NOTE = "(queued: runs after this turn)"
|
|
641
699
|
QUEUE_RUNS_NOW = "(the turn had ended: runs as the next turn)"
|
|
642
|
-
# /queue to a worker from before queueing, which would steer it.
|
|
643
|
-
QUEUE_NEEDS_NEWER_WORKER = "(/queue needs a newer worker: restart it with chi sessions restart, then send again)"
|
|
644
|
-
UNQUEUE_NEEDS_NEWER_WORKER = "(/unqueue needs a newer worker: restart it with chi sessions restart)"
|
|
645
700
|
NOTHING_QUEUED = "(nothing queued: /queue TEXT queues a message for after the running turn)"
|
|
646
701
|
UNQUEUE_REFUSED = { "already_started" => "(not removed: it already started)",
|
|
647
702
|
"unknown_enqueued_id" => "(not removed: it is already gone)",
|
|
648
703
|
"not_queued" => "(not removed: it goes in at the next step)" }.freeze
|
|
649
704
|
|
|
705
|
+
# /queue to a worker from before queueing, which would steer it.
|
|
706
|
+
def queue_needs_newer_worker
|
|
707
|
+
"(/queue needs a newer worker: restart it with #{via.command("sessions restart")}, then send again)"
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def unqueue_needs_newer_worker = "(/unqueue needs a newer worker: restart it with #{via.command("sessions restart")})"
|
|
711
|
+
|
|
650
712
|
# /unqueue: take back the newest prompt this run queued for after the
|
|
651
713
|
# turn, before it runs (POST turn/dequeue). Its :turn_dequeued comes
|
|
652
714
|
# to every UI; here the reply says what became of it. A worker whose
|
|
653
715
|
# sidecar lacks "unqueue" is not asked.
|
|
654
716
|
def unqueue_newest
|
|
655
|
-
return @screen.commit(
|
|
717
|
+
return @screen.commit(unqueue_needs_newer_worker) unless worker_unqueues?
|
|
656
718
|
|
|
657
719
|
id = @my_queued.last
|
|
658
720
|
return @screen.commit(NOTHING_QUEUED) unless id
|
|
659
721
|
|
|
660
722
|
reply = @client.dequeue(enqueued_id: id, client_id: @client_id)
|
|
661
723
|
error = reply.json&.fetch("error", nil)
|
|
662
|
-
return @screen.commit(
|
|
724
|
+
return @screen.commit(unqueue_needs_newer_worker) if reply.status == 404 && error == "not_found"
|
|
663
725
|
|
|
664
726
|
# Removed, started or gone: no longer this run's to take back.
|
|
665
727
|
@my_queued.delete(id) if [200, 404, 409].include?(reply.status)
|
|
@@ -678,7 +740,9 @@ module Samagotchi
|
|
|
678
740
|
@screen.commit("removed: #{first_line_clip(prompt)}")
|
|
679
741
|
return if @wait_at_eof || prompt.strip.empty?
|
|
680
742
|
|
|
681
|
-
@
|
|
743
|
+
return @prefilled = prompt.strip if @reader&.prefill(prompt)
|
|
744
|
+
|
|
745
|
+
@screen.commit("(the removed message is in the input history: ↑)")
|
|
682
746
|
end
|
|
683
747
|
|
|
684
748
|
def first_line_clip(text, max = 60)
|
|
@@ -689,10 +753,7 @@ module Samagotchi
|
|
|
689
753
|
# Whether the attached worker takes queued messages back (its sidecar
|
|
690
754
|
# names SessionInbox::UNQUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
691
755
|
# answers (an older one's 404 not_found says so).
|
|
692
|
-
def worker_unqueues?
|
|
693
|
-
sidecar = WorkerSidecar.read(Session.session_dir(@client.session_id, state_dir: @state_dir))
|
|
694
|
-
sidecar.nil? || sidecar.features.include?(SessionInbox::UNQUEUE_FEATURE)
|
|
695
|
-
end
|
|
756
|
+
def worker_unqueues? = @link.unqueues?
|
|
696
757
|
|
|
697
758
|
# A command queued for after the running turn (command_queued with
|
|
698
759
|
# waits): its line now (another client's) and a note; its command_ran
|
|
@@ -803,11 +864,13 @@ module Samagotchi
|
|
|
803
864
|
# from the ack (the worker knows whether cutting is on for its model);
|
|
804
865
|
# a queued one waits for the turn's end (the ack says queue).
|
|
805
866
|
# An idle session's prompt simply runs, and a prompt with images runs
|
|
806
|
-
# as its own next turn (never merging mid-turn), so nothing is said
|
|
867
|
+
# as its own next turn (never merging mid-turn), so nothing is said;
|
|
868
|
+
# one typed as /cut or /queue says it was sent now (+choice+).
|
|
807
869
|
# @param ack [Hash, nil] the Bridge's 202 body
|
|
808
870
|
# @param delivery [String, nil] what was asked for
|
|
809
871
|
# @return [String, nil]
|
|
810
|
-
def sent_note(ack, delivery: nil, images: [], running: false)
|
|
872
|
+
def sent_note(ack, delivery: nil, images: [], running: false, choice: nil)
|
|
873
|
+
return STEER_CHOICE_IDLE if choice && !running
|
|
811
874
|
return nil unless running && ack.is_a?(Hash)
|
|
812
875
|
# The Bridge acks queue only while a turn ran at enqueue; a /queue
|
|
813
876
|
# it acks next_step found the turn over: it is the next turn.
|
|
@@ -834,7 +897,8 @@ module Samagotchi
|
|
|
834
897
|
@client_id
|
|
835
898
|
end
|
|
836
899
|
|
|
837
|
-
|
|
900
|
+
# @param choice [Symbol, nil] :cut or :queue when typed with that word
|
|
901
|
+
def send_prompt(text, delivery: nil, choice: nil)
|
|
838
902
|
persist_recent_history(text)
|
|
839
903
|
images = attach_images(text)
|
|
840
904
|
return own_prompt_failed if images.nil?
|
|
@@ -850,7 +914,7 @@ module Samagotchi
|
|
|
850
914
|
@sent_ids << enqueued_id if enqueued_id
|
|
851
915
|
@open_ids << enqueued_id if enqueued_id && @wait_at_eof
|
|
852
916
|
@my_queued << enqueued_id if enqueued_id && reply.json&.fetch("delivery", nil) == Delivery::QUEUE
|
|
853
|
-
note = sent_note(reply.json, delivery: delivery, images: images, running: running)
|
|
917
|
+
note = sent_note(reply.json, delivery: delivery, images: images, running: running, choice: choice)
|
|
854
918
|
@screen.commit(note) if note
|
|
855
919
|
return
|
|
856
920
|
end
|
|
@@ -861,7 +925,7 @@ module Samagotchi
|
|
|
861
925
|
# Read after its deadline and dropped (BridgeClient::DEADLINE_SHARE).
|
|
862
926
|
return @screen.commit("could not send the prompt (#{LATE})") if detail == "deadline_passed"
|
|
863
927
|
# A worker from 0.51 to 0.63 refuses a queue (nothing was sent).
|
|
864
|
-
return @screen.commit(
|
|
928
|
+
return @screen.commit(queue_needs_newer_worker) if detail == "delivery_unavailable"
|
|
865
929
|
|
|
866
930
|
@screen.commit("could not send the prompt (#{[reply.status, detail].compact.join(" ")})")
|
|
867
931
|
rescue SystemCallError, IOError => e
|
|
@@ -905,7 +969,7 @@ module Samagotchi
|
|
|
905
969
|
|
|
906
970
|
# The input ended and nothing of ours is left to wait for.
|
|
907
971
|
def end_of_input
|
|
908
|
-
detach("Detached; the session keeps running. Re-attach with:
|
|
972
|
+
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @input_ended
|
|
909
973
|
return :turn_failed if @own_failed
|
|
910
974
|
|
|
911
975
|
@own_empty ? :empty_answer : :detached
|
|
@@ -913,31 +977,30 @@ module Samagotchi
|
|
|
913
977
|
|
|
914
978
|
def unanswered_question(pending)
|
|
915
979
|
@unanswered = pending
|
|
916
|
-
detach("A question waits for an answer:
|
|
980
|
+
detach("A question waits for an answer: #{via.attach(@client.session_id)}")
|
|
917
981
|
:unanswered
|
|
918
982
|
end
|
|
919
983
|
|
|
920
|
-
# The prompt's `@path` images, stored
|
|
921
|
-
# (
|
|
922
|
-
# never takes a path. nil (after a line saying why)
|
|
984
|
+
# The prompt's `@path` images, stored where the worker reads them
|
|
985
|
+
# (AttachLink#ingest_images: the session's images/ here) and sent as
|
|
986
|
+
# refs: the Bridge never takes a path. nil (after a line saying why)
|
|
987
|
+
# when one can't be.
|
|
923
988
|
def attach_images(text)
|
|
924
989
|
paths = ImageInput.extract(text)
|
|
925
990
|
return [] if paths.empty?
|
|
926
991
|
|
|
927
|
-
|
|
928
|
-
paths.map do |image|
|
|
929
|
-
ref = ImageStore.ingest(session_dir, path: image[:path])
|
|
930
|
-
{ file: ref[:file], name: ref[:name] }
|
|
931
|
-
end
|
|
992
|
+
@link.ingest_images(paths.map { |image| image[:path] })
|
|
932
993
|
rescue ImageStore::Error => e
|
|
933
994
|
@screen.commit("could not attach the image: #{e.message}")
|
|
934
995
|
nil
|
|
935
996
|
end
|
|
936
997
|
|
|
937
998
|
def show_stats
|
|
938
|
-
|
|
939
|
-
metrics = @client.get_json("stats")&.dig("metrics") || @client.get_json("state")&.dig("session_state_snapshot", "metrics")
|
|
999
|
+
metrics = @link.stats_metrics
|
|
940
1000
|
@screen.commit(metrics ? format_session_metrics(EventRenderer.deep_symbolize_keys(metrics)) : "(no metrics: the worker did not answer)")
|
|
1001
|
+
rescue SystemCallError, IOError => e
|
|
1002
|
+
# Through chi web: the message names the remote and what failed.
|
|
1003
|
+
@screen.commit("(no metrics: #{via.remote? ? e.message : "the worker did not answer"})")
|
|
941
1004
|
end
|
|
942
1005
|
|
|
943
1006
|
# One read on the reader thread: the REPL's multiline read with Tab
|
|
@@ -955,6 +1018,16 @@ module Samagotchi
|
|
|
955
1018
|
# The attached TUI's commands, for Tab.
|
|
956
1019
|
def slash_commands = command_registry.completions(:attached)
|
|
957
1020
|
|
|
1021
|
+
# @path and #memory candidates, as the link offers them
|
|
1022
|
+
# (AttachLink#completions).
|
|
1023
|
+
def build_project_path_completion_candidates(path_fragment)
|
|
1024
|
+
@link.completions.paths ? super : []
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def build_memory_completion_candidates(memory_fragment)
|
|
1028
|
+
@link.completions.memories ? super : []
|
|
1029
|
+
end
|
|
1030
|
+
|
|
958
1031
|
# The session's commands as its worker's snapshot names them (its
|
|
959
1032
|
# plugins' too); the built-ins until one arrives, or from a worker too
|
|
960
1033
|
# old to name them. An unknown /foo is a prompt, as in the REPL.
|
|
@@ -970,8 +1043,8 @@ module Samagotchi
|
|
|
970
1043
|
@screen.commit(recap_command_text(enabled: answer["enabled"], saved: answer["saved"],
|
|
971
1044
|
request: answer["request"], min_user_turns: answer["min_user_turns"],
|
|
972
1045
|
session_off: answer["session_off"] == true))
|
|
973
|
-
rescue SystemCallError, IOError
|
|
974
|
-
@screen.commit("(no recap: the worker did not answer)")
|
|
1046
|
+
rescue SystemCallError, IOError => e
|
|
1047
|
+
@screen.commit("(no recap: #{via.remote? ? e.message : "the worker did not answer"})")
|
|
975
1048
|
end
|
|
976
1049
|
|
|
977
1050
|
def prompt_text
|
|
@@ -1072,7 +1145,7 @@ module Samagotchi
|
|
|
1072
1145
|
end
|
|
1073
1146
|
|
|
1074
1147
|
def stale_worker(cant)
|
|
1075
|
-
BridgeClient.stale_worker_message(@client.session_id, cant: cant)
|
|
1148
|
+
BridgeClient.stale_worker_message(@client.session_id, cant: cant, via: via)
|
|
1076
1149
|
end
|
|
1077
1150
|
|
|
1078
1151
|
# The worker rolled a failed turn back and handed its prompt back: ours
|
|
@@ -1208,7 +1281,7 @@ module Samagotchi
|
|
|
1208
1281
|
@screen.commit("guardrails> #{snapshot[:guardrail_warning]}") if snapshot[:guardrail_warning]
|
|
1209
1282
|
@screen.commit("plugins> #{snapshot[:plugin_warning]}") if snapshot[:plugin_warning]
|
|
1210
1283
|
version_line = VersionLines.at_attach(worker: snapshot[:chi_version], installed: installed_version,
|
|
1211
|
-
session_id: @client.session_id)
|
|
1284
|
+
session_id: @client.session_id, via: via)
|
|
1212
1285
|
@screen.commit(paint(version_line, 2)) if version_line
|
|
1213
1286
|
end
|
|
1214
1287
|
if snapshot[:commands]
|
|
@@ -1381,13 +1454,11 @@ module Samagotchi
|
|
|
1381
1454
|
end
|
|
1382
1455
|
end
|
|
1383
1456
|
|
|
1384
|
-
# The prompt's turn's saved tool records (SessionMetrics,
|
|
1385
|
-
# machine's state dir, as #attach_images): none for a prompt
|
|
1386
|
-
# turn id (an older session) or a session with no analytics.
|
|
1457
|
+
# The prompt's turn's saved tool records (AttachLink: SessionMetrics,
|
|
1458
|
+
# in this machine's state dir, as #attach_images): none for a prompt
|
|
1459
|
+
# with no turn id (an older session) or a session with no analytics.
|
|
1387
1460
|
def join_tool_records(prompt)
|
|
1388
|
-
|
|
1389
|
-
rescue Session::InvalidId
|
|
1390
|
-
[]
|
|
1461
|
+
@link.saved_tool_records(prompt[:turn_id] || prompt["turn_id"])
|
|
1391
1462
|
end
|
|
1392
1463
|
|
|
1393
1464
|
# The turn's saved record when it failed with the fields its line is
|
|
@@ -1395,16 +1466,13 @@ module Samagotchi
|
|
|
1395
1466
|
# canceled or older turn — or one saved before the failure field —
|
|
1396
1467
|
# draws its answer as before.
|
|
1397
1468
|
def join_failed_record(prompt)
|
|
1398
|
-
record =
|
|
1399
|
-
prompt[:turn_id] || prompt["turn_id"])
|
|
1469
|
+
record = @link.saved_turn_record(prompt[:turn_id] || prompt["turn_id"])
|
|
1400
1470
|
return nil unless record && record["status"] == "failed" && record["failure"].is_a?(Hash)
|
|
1401
1471
|
|
|
1402
1472
|
failure = record["failure"]
|
|
1403
1473
|
return nil if failure["summary"].to_s.empty? && failure["message"].to_s.empty?
|
|
1404
1474
|
|
|
1405
1475
|
record
|
|
1406
|
-
rescue Session::InvalidId
|
|
1407
|
-
nil
|
|
1408
1476
|
end
|
|
1409
1477
|
|
|
1410
1478
|
# The saved record of +part+'s call (its duration and status): the
|
|
@@ -1631,14 +1699,14 @@ module Samagotchi
|
|
|
1631
1699
|
def refresh_children
|
|
1632
1700
|
return unless @status.enabled?
|
|
1633
1701
|
|
|
1634
|
-
@status.update(children: @children_counts
|
|
1702
|
+
@status.update(children: @link.children_counts)
|
|
1635
1703
|
rescue StandardError => e
|
|
1636
1704
|
Log.debug(:attached, "children_counts_failed", error: e.class.name)
|
|
1637
1705
|
end
|
|
1638
1706
|
|
|
1639
1707
|
# The config's default model, as the worker's /model names it.
|
|
1640
1708
|
def default_model_name
|
|
1641
|
-
@default_model_name ||=
|
|
1709
|
+
@default_model_name ||= @link.default_model_name
|
|
1642
1710
|
end
|
|
1643
1711
|
|
|
1644
1712
|
def reminder_origin?(origin) = origin[:client_id].to_s.start_with?(ClientId::SYSTEM_PREFIX)
|
|
@@ -118,6 +118,9 @@ module Samagotchi
|
|
|
118
118
|
CUT_OFF_NOTE = "(cutting is off; goes in at the next step)"
|
|
119
119
|
# A bare /cut or /queue (ComposerRoute's :usage).
|
|
120
120
|
STEER_CHOICE_USAGE = "(/cut and /queue need a message: /cut TEXT, /queue TEXT)"
|
|
121
|
+
# A /cut TEXT or /queue TEXT with no turn running: the text is sent
|
|
122
|
+
# at once, a normal turn (ComposerRoute routes it on as a line).
|
|
123
|
+
STEER_CHOICE_IDLE = "(nothing is running: sent now)"
|
|
121
124
|
|
|
122
125
|
# @param outcome [Symbol] SteerCut#cut_for_steer's answer: :now, :waits or :off
|
|
123
126
|
# @param after [Integer, nil] the run's cut wait (the model's own
|
|
@@ -559,11 +562,20 @@ module Samagotchi
|
|
|
559
562
|
"#{label}: #{visible.join(", ")}#{suffix}"
|
|
560
563
|
end
|
|
561
564
|
|
|
565
|
+
# +text+ cut to +width+ with an ellipsis, at a word (or a " | "
|
|
566
|
+
# between segments) when one ends in the second half, so the row never
|
|
567
|
+
# stops mid-word (a plain surface prints it as is).
|
|
562
568
|
def cap_preview_text(text, width)
|
|
563
569
|
return "" if width <= 0
|
|
564
570
|
|
|
565
571
|
value = text.to_s
|
|
566
|
-
value.length
|
|
572
|
+
return value if value.length <= width
|
|
573
|
+
return value[0, width] if width < 2
|
|
574
|
+
|
|
575
|
+
cut = value[0, width - 1]
|
|
576
|
+
space = cut.rindex(" ")
|
|
577
|
+
cut = cut[0, space] if space && space >= width / 2
|
|
578
|
+
"#{cut.sub(/[\s,;:|·-]+\z/, "")}…"
|
|
567
579
|
end
|
|
568
580
|
|
|
569
581
|
public
|