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
|
@@ -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),
|
|
@@ -338,7 +338,7 @@ module Samagotchi
|
|
|
338
338
|
when :stream_closed
|
|
339
339
|
@view.finish_thinking_spinner
|
|
340
340
|
@screen.commit("Lost the session's worker (#{event[:reason]}). " \
|
|
341
|
-
"Resume it with:
|
|
341
|
+
"Resume it with: #{via.command("--shared --resume #{@client.session_id}")}")
|
|
342
342
|
return :closed
|
|
343
343
|
else @renderer.call(event)
|
|
344
344
|
end
|
|
@@ -349,27 +349,30 @@ module Samagotchi
|
|
|
349
349
|
|
|
350
350
|
# The newest chi installed, looked at once, at the join.
|
|
351
351
|
def installed_version
|
|
352
|
-
@installed_version
|
|
352
|
+
@link.installed_version
|
|
353
353
|
rescue StandardError
|
|
354
354
|
nil
|
|
355
355
|
end
|
|
356
356
|
|
|
357
|
+
# How the commands this TUI prints are spelled (AttachLink#via).
|
|
358
|
+
def via = @link.via
|
|
359
|
+
|
|
357
360
|
# The session's live worker for the stream to follow (EventStream's
|
|
358
|
-
# rediscover): nil while there is none yet, :gone once
|
|
359
|
-
#
|
|
361
|
+
# rediscover): nil while there is none yet, :gone once no worker is
|
|
362
|
+
# coming (AttachLink#live_worker). The reason isn't passed on: the
|
|
363
|
+
# stream closes as "unreachable", the words a stopped session has
|
|
364
|
+
# always had here.
|
|
360
365
|
def live_worker
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
BridgeClient.discover(id, session_dir: Session.session_dir(id, state_dir: @state_dir), host: @client.host)
|
|
366
|
+
found, _reason = @link.live_worker
|
|
367
|
+
found
|
|
365
368
|
end
|
|
366
369
|
|
|
367
370
|
# The stream moved to a new worker (a restart): requests go there too.
|
|
368
371
|
def switch_worker(event)
|
|
369
|
-
@client =
|
|
372
|
+
@client = @link.switched(event)
|
|
370
373
|
@worker_changed = true
|
|
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))
|
|
373
376
|
nil
|
|
374
377
|
end
|
|
375
378
|
|
|
@@ -390,7 +393,7 @@ module Samagotchi
|
|
|
390
393
|
|
|
391
394
|
# @return [Symbol, nil] :detach to end the loop
|
|
392
395
|
def submit(line)
|
|
393
|
-
return detach("Detached; the session keeps running. Re-attach with:
|
|
396
|
+
return detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if line.nil?
|
|
394
397
|
|
|
395
398
|
text = line.strip
|
|
396
399
|
if @question
|
|
@@ -455,7 +458,7 @@ module Samagotchi
|
|
|
455
458
|
# @return [nil]
|
|
456
459
|
def submit_delivery(route)
|
|
457
460
|
return @screen.commit(STEER_CHOICE_USAGE) if route.kind == :usage
|
|
458
|
-
return @screen.commit(
|
|
461
|
+
return @screen.commit(queue_needs_newer_worker) if Delivery.queue?(route.delivery) && !worker_queues?
|
|
459
462
|
|
|
460
463
|
# To an idle session it is an ordinary prompt: no delivery to send.
|
|
461
464
|
send_prompt(route.text, delivery: route.delivery)
|
|
@@ -465,10 +468,7 @@ module Samagotchi
|
|
|
465
468
|
# Whether the attached worker runs delivery: queue (its sidecar names
|
|
466
469
|
# Delivery::QUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
467
470
|
# 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
|
|
471
|
+
def worker_queues? = @link.queues?
|
|
472
472
|
|
|
473
473
|
# @return [Symbol] :detach
|
|
474
474
|
def detach(line)
|
|
@@ -502,12 +502,13 @@ module Samagotchi
|
|
|
502
502
|
# An empty session: the worker deletes it as it leaves.
|
|
503
503
|
return detach(EMPTY_EXIT) if discards?(reply)
|
|
504
504
|
|
|
505
|
-
@delete_session
|
|
505
|
+
@link.delete_session
|
|
506
506
|
detach("Detached; deleted session #{id}.")
|
|
507
507
|
rescue SystemCallError, IOError => e
|
|
508
508
|
detach("#{exit_failed_line(e.message)} Not deleted.")
|
|
509
509
|
rescue SessionManager::DeleteRefused, SessionManager::OwnedByTUI, ArgumentError => e
|
|
510
|
-
detach("Detached; the worker is stopping, but session #{id} was not deleted (#{e.message}):
|
|
510
|
+
detach("Detached; the worker is stopping, but session #{id} was not deleted (#{e.message}): " \
|
|
511
|
+
"#{via.command("sessions delete #{id}")}")
|
|
511
512
|
end
|
|
512
513
|
|
|
513
514
|
# /archive during a turn (D2): it waits for the turn's end and the
|
|
@@ -540,14 +541,14 @@ module Samagotchi
|
|
|
540
541
|
return detach(EMPTY_EXIT) if reply.status == 200 && discards?(reply)
|
|
541
542
|
return detach("#{exit_line(reply)} Not archived.") unless [200, 409].include?(reply.status)
|
|
542
543
|
|
|
543
|
-
result = @archive_session
|
|
544
|
+
result = @link.archive_session
|
|
544
545
|
return detach("Detached; the session was empty, so it is discarded.") if result[:discarded].include?(id)
|
|
545
546
|
|
|
546
|
-
detach("Detached; archived session #{id}.
|
|
547
|
+
detach("Detached; archived session #{id}. #{via.command("sessions list --archived")} finds it.")
|
|
547
548
|
rescue SystemCallError, IOError => e
|
|
548
549
|
detach("#{exit_failed_line(e.message)} Not archived.")
|
|
549
550
|
rescue SessionManager::ArchiveRefused, SessionManager::OwnedByTUI, ArgumentError => e
|
|
550
|
-
detach("Detached; session #{id} was not archived (#{e.message}). Re-attach with:
|
|
551
|
+
detach("Detached; session #{id} was not archived (#{e.message}). Re-attach with: #{via.attach(id)}")
|
|
551
552
|
end
|
|
552
553
|
|
|
553
554
|
def exit_line(reply)
|
|
@@ -557,16 +558,18 @@ module Samagotchi
|
|
|
557
558
|
when 200
|
|
558
559
|
return EMPTY_EXIT if discards?(reply)
|
|
559
560
|
|
|
560
|
-
"Detached; the session's worker is stopping. Resume with:
|
|
561
|
+
"Detached; the session's worker is stopping. Resume with: #{via.resume(id)}"
|
|
561
562
|
when 409
|
|
562
563
|
reason = reply.json&.fetch("reason", nil).to_s
|
|
563
564
|
"Detached; the session keeps running (#{held_words(reason, reply.json&.fetch("tasks", nil))}). " \
|
|
564
|
-
"Re-attach with:
|
|
565
|
+
"Re-attach with: #{via.attach(id)}"
|
|
565
566
|
when 0 then exit_failed_line("no reply")
|
|
566
567
|
when 408 then exit_failed_line(LATE)
|
|
567
568
|
else
|
|
568
569
|
# A worker from before the route answers the Bridge's plain 404.
|
|
569
|
-
|
|
570
|
+
if error == "not_found"
|
|
571
|
+
return "Detached; this worker runs an older chi and can't be stopped from here: #{via.command("sessions stop #{id}")}"
|
|
572
|
+
end
|
|
570
573
|
|
|
571
574
|
exit_failed_line([reply.status, reply.json&.fetch("detail", nil) || error].compact.join(" "))
|
|
572
575
|
end
|
|
@@ -584,7 +587,7 @@ module Samagotchi
|
|
|
584
587
|
def discards?(reply) = reply.json&.fetch("discard", nil) == true
|
|
585
588
|
|
|
586
589
|
def exit_failed_line(why)
|
|
587
|
-
"Detached (could not ask the worker to stop: #{why}). Re-attach with:
|
|
590
|
+
"Detached (could not ask the worker to stop: #{why}). Re-attach with: #{via.attach(@client.session_id)}"
|
|
588
591
|
end
|
|
589
592
|
|
|
590
593
|
# The worker says what became of it (:continue_resolved); an invalid
|
|
@@ -639,27 +642,31 @@ module Samagotchi
|
|
|
639
642
|
|
|
640
643
|
QUEUED_NOTE = "(queued: runs after this turn)"
|
|
641
644
|
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
645
|
NOTHING_QUEUED = "(nothing queued: /queue TEXT queues a message for after the running turn)"
|
|
646
646
|
UNQUEUE_REFUSED = { "already_started" => "(not removed: it already started)",
|
|
647
647
|
"unknown_enqueued_id" => "(not removed: it is already gone)",
|
|
648
648
|
"not_queued" => "(not removed: it goes in at the next step)" }.freeze
|
|
649
649
|
|
|
650
|
+
# /queue to a worker from before queueing, which would steer it.
|
|
651
|
+
def queue_needs_newer_worker
|
|
652
|
+
"(/queue needs a newer worker: restart it with #{via.command("sessions restart")}, then send again)"
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def unqueue_needs_newer_worker = "(/unqueue needs a newer worker: restart it with #{via.command("sessions restart")})"
|
|
656
|
+
|
|
650
657
|
# /unqueue: take back the newest prompt this run queued for after the
|
|
651
658
|
# turn, before it runs (POST turn/dequeue). Its :turn_dequeued comes
|
|
652
659
|
# to every UI; here the reply says what became of it. A worker whose
|
|
653
660
|
# sidecar lacks "unqueue" is not asked.
|
|
654
661
|
def unqueue_newest
|
|
655
|
-
return @screen.commit(
|
|
662
|
+
return @screen.commit(unqueue_needs_newer_worker) unless worker_unqueues?
|
|
656
663
|
|
|
657
664
|
id = @my_queued.last
|
|
658
665
|
return @screen.commit(NOTHING_QUEUED) unless id
|
|
659
666
|
|
|
660
667
|
reply = @client.dequeue(enqueued_id: id, client_id: @client_id)
|
|
661
668
|
error = reply.json&.fetch("error", nil)
|
|
662
|
-
return @screen.commit(
|
|
669
|
+
return @screen.commit(unqueue_needs_newer_worker) if reply.status == 404 && error == "not_found"
|
|
663
670
|
|
|
664
671
|
# Removed, started or gone: no longer this run's to take back.
|
|
665
672
|
@my_queued.delete(id) if [200, 404, 409].include?(reply.status)
|
|
@@ -689,10 +696,7 @@ module Samagotchi
|
|
|
689
696
|
# Whether the attached worker takes queued messages back (its sidecar
|
|
690
697
|
# names SessionInbox::UNQUEUE_FEATURE). No sidecar to read: true, its Bridge
|
|
691
698
|
# 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
|
|
699
|
+
def worker_unqueues? = @link.unqueues?
|
|
696
700
|
|
|
697
701
|
# A command queued for after the running turn (command_queued with
|
|
698
702
|
# waits): its line now (another client's) and a note; its command_ran
|
|
@@ -861,7 +865,7 @@ module Samagotchi
|
|
|
861
865
|
# Read after its deadline and dropped (BridgeClient::DEADLINE_SHARE).
|
|
862
866
|
return @screen.commit("could not send the prompt (#{LATE})") if detail == "deadline_passed"
|
|
863
867
|
# A worker from 0.51 to 0.63 refuses a queue (nothing was sent).
|
|
864
|
-
return @screen.commit(
|
|
868
|
+
return @screen.commit(queue_needs_newer_worker) if detail == "delivery_unavailable"
|
|
865
869
|
|
|
866
870
|
@screen.commit("could not send the prompt (#{[reply.status, detail].compact.join(" ")})")
|
|
867
871
|
rescue SystemCallError, IOError => e
|
|
@@ -905,7 +909,7 @@ module Samagotchi
|
|
|
905
909
|
|
|
906
910
|
# The input ended and nothing of ours is left to wait for.
|
|
907
911
|
def end_of_input
|
|
908
|
-
detach("Detached; the session keeps running. Re-attach with:
|
|
912
|
+
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @input_ended
|
|
909
913
|
return :turn_failed if @own_failed
|
|
910
914
|
|
|
911
915
|
@own_empty ? :empty_answer : :detached
|
|
@@ -913,30 +917,26 @@ module Samagotchi
|
|
|
913
917
|
|
|
914
918
|
def unanswered_question(pending)
|
|
915
919
|
@unanswered = pending
|
|
916
|
-
detach("A question waits for an answer:
|
|
920
|
+
detach("A question waits for an answer: #{via.attach(@client.session_id)}")
|
|
917
921
|
:unanswered
|
|
918
922
|
end
|
|
919
923
|
|
|
920
|
-
# The prompt's `@path` images, stored
|
|
921
|
-
# (
|
|
922
|
-
# never takes a path. nil (after a line saying why)
|
|
924
|
+
# The prompt's `@path` images, stored where the worker reads them
|
|
925
|
+
# (AttachLink#ingest_images: the session's images/ here) and sent as
|
|
926
|
+
# refs: the Bridge never takes a path. nil (after a line saying why)
|
|
927
|
+
# when one can't be.
|
|
923
928
|
def attach_images(text)
|
|
924
929
|
paths = ImageInput.extract(text)
|
|
925
930
|
return [] if paths.empty?
|
|
926
931
|
|
|
927
|
-
|
|
928
|
-
paths.map do |image|
|
|
929
|
-
ref = ImageStore.ingest(session_dir, path: image[:path])
|
|
930
|
-
{ file: ref[:file], name: ref[:name] }
|
|
931
|
-
end
|
|
932
|
+
@link.ingest_images(paths.map { |image| image[:path] })
|
|
932
933
|
rescue ImageStore::Error => e
|
|
933
934
|
@screen.commit("could not attach the image: #{e.message}")
|
|
934
935
|
nil
|
|
935
936
|
end
|
|
936
937
|
|
|
937
938
|
def show_stats
|
|
938
|
-
|
|
939
|
-
metrics = @client.get_json("stats")&.dig("metrics") || @client.get_json("state")&.dig("session_state_snapshot", "metrics")
|
|
939
|
+
metrics = @link.stats_metrics
|
|
940
940
|
@screen.commit(metrics ? format_session_metrics(EventRenderer.deep_symbolize_keys(metrics)) : "(no metrics: the worker did not answer)")
|
|
941
941
|
end
|
|
942
942
|
|
|
@@ -955,6 +955,16 @@ module Samagotchi
|
|
|
955
955
|
# The attached TUI's commands, for Tab.
|
|
956
956
|
def slash_commands = command_registry.completions(:attached)
|
|
957
957
|
|
|
958
|
+
# @path and #memory candidates, as the link offers them
|
|
959
|
+
# (AttachLink#completions).
|
|
960
|
+
def build_project_path_completion_candidates(path_fragment)
|
|
961
|
+
@link.completions.paths ? super : []
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
def build_memory_completion_candidates(memory_fragment)
|
|
965
|
+
@link.completions.memories ? super : []
|
|
966
|
+
end
|
|
967
|
+
|
|
958
968
|
# The session's commands as its worker's snapshot names them (its
|
|
959
969
|
# plugins' too); the built-ins until one arrives, or from a worker too
|
|
960
970
|
# old to name them. An unknown /foo is a prompt, as in the REPL.
|
|
@@ -1072,7 +1082,7 @@ module Samagotchi
|
|
|
1072
1082
|
end
|
|
1073
1083
|
|
|
1074
1084
|
def stale_worker(cant)
|
|
1075
|
-
BridgeClient.stale_worker_message(@client.session_id, cant: cant)
|
|
1085
|
+
BridgeClient.stale_worker_message(@client.session_id, cant: cant, via: via)
|
|
1076
1086
|
end
|
|
1077
1087
|
|
|
1078
1088
|
# The worker rolled a failed turn back and handed its prompt back: ours
|
|
@@ -1208,7 +1218,7 @@ module Samagotchi
|
|
|
1208
1218
|
@screen.commit("guardrails> #{snapshot[:guardrail_warning]}") if snapshot[:guardrail_warning]
|
|
1209
1219
|
@screen.commit("plugins> #{snapshot[:plugin_warning]}") if snapshot[:plugin_warning]
|
|
1210
1220
|
version_line = VersionLines.at_attach(worker: snapshot[:chi_version], installed: installed_version,
|
|
1211
|
-
session_id: @client.session_id)
|
|
1221
|
+
session_id: @client.session_id, via: via)
|
|
1212
1222
|
@screen.commit(paint(version_line, 2)) if version_line
|
|
1213
1223
|
end
|
|
1214
1224
|
if snapshot[:commands]
|
|
@@ -1381,13 +1391,11 @@ module Samagotchi
|
|
|
1381
1391
|
end
|
|
1382
1392
|
end
|
|
1383
1393
|
|
|
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.
|
|
1394
|
+
# The prompt's turn's saved tool records (AttachLink: SessionMetrics,
|
|
1395
|
+
# in this machine's state dir, as #attach_images): none for a prompt
|
|
1396
|
+
# with no turn id (an older session) or a session with no analytics.
|
|
1387
1397
|
def join_tool_records(prompt)
|
|
1388
|
-
|
|
1389
|
-
rescue Session::InvalidId
|
|
1390
|
-
[]
|
|
1398
|
+
@link.saved_tool_records(prompt[:turn_id] || prompt["turn_id"])
|
|
1391
1399
|
end
|
|
1392
1400
|
|
|
1393
1401
|
# The turn's saved record when it failed with the fields its line is
|
|
@@ -1395,16 +1403,13 @@ module Samagotchi
|
|
|
1395
1403
|
# canceled or older turn — or one saved before the failure field —
|
|
1396
1404
|
# draws its answer as before.
|
|
1397
1405
|
def join_failed_record(prompt)
|
|
1398
|
-
record =
|
|
1399
|
-
prompt[:turn_id] || prompt["turn_id"])
|
|
1406
|
+
record = @link.saved_turn_record(prompt[:turn_id] || prompt["turn_id"])
|
|
1400
1407
|
return nil unless record && record["status"] == "failed" && record["failure"].is_a?(Hash)
|
|
1401
1408
|
|
|
1402
1409
|
failure = record["failure"]
|
|
1403
1410
|
return nil if failure["summary"].to_s.empty? && failure["message"].to_s.empty?
|
|
1404
1411
|
|
|
1405
1412
|
record
|
|
1406
|
-
rescue Session::InvalidId
|
|
1407
|
-
nil
|
|
1408
1413
|
end
|
|
1409
1414
|
|
|
1410
1415
|
# The saved record of +part+'s call (its duration and status): the
|
|
@@ -1631,14 +1636,14 @@ module Samagotchi
|
|
|
1631
1636
|
def refresh_children
|
|
1632
1637
|
return unless @status.enabled?
|
|
1633
1638
|
|
|
1634
|
-
@status.update(children: @children_counts
|
|
1639
|
+
@status.update(children: @link.children_counts)
|
|
1635
1640
|
rescue StandardError => e
|
|
1636
1641
|
Log.debug(:attached, "children_counts_failed", error: e.class.name)
|
|
1637
1642
|
end
|
|
1638
1643
|
|
|
1639
1644
|
# The config's default model, as the worker's /model names it.
|
|
1640
1645
|
def default_model_name
|
|
1641
|
-
@default_model_name ||=
|
|
1646
|
+
@default_model_name ||= @link.default_model_name
|
|
1642
1647
|
end
|
|
1643
1648
|
|
|
1644
1649
|
def reminder_origin?(origin) = origin[:client_id].to_s.start_with?(ClientId::SYSTEM_PREFIX)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "attach_link"
|
|
4
|
+
require_relative "../session"
|
|
5
|
+
require_relative "../bridge_client"
|
|
6
|
+
require_relative "../worker_sidecar"
|
|
7
|
+
require_relative "../delivery"
|
|
8
|
+
require_relative "../session_inbox"
|
|
9
|
+
require_relative "../image_store"
|
|
10
|
+
require_relative "../session_metrics"
|
|
11
|
+
require_relative "../children_status"
|
|
12
|
+
require_relative "../session_manager"
|
|
13
|
+
require_relative "../installed_versions"
|
|
14
|
+
require_relative "../model_profile"
|
|
15
|
+
require_relative "../parent_report"
|
|
16
|
+
require_relative "../chi_invocation"
|
|
17
|
+
|
|
18
|
+
module Samagotchi
|
|
19
|
+
class TerminalUI
|
|
20
|
+
# The AttachLink of a session on this machine: its worker's Bridge
|
|
21
|
+
# (found through the sidecar in the session folder) and the session's
|
|
22
|
+
# files under +state_dir+.
|
|
23
|
+
class LocalLink
|
|
24
|
+
include AttachLink
|
|
25
|
+
|
|
26
|
+
# How long /exit --delete and /archive wait for the worker to let go
|
|
27
|
+
# (AttachedLoop's defaults too).
|
|
28
|
+
DELETE_WAIT = 10
|
|
29
|
+
|
|
30
|
+
attr_reader :client
|
|
31
|
+
|
|
32
|
+
# @param client [BridgeClient] the live worker's
|
|
33
|
+
# @param state_dir [String] the state dir the worker was found in
|
|
34
|
+
# @param delete_session [#call] session id -> deletes it
|
|
35
|
+
# @param archive_session [#call] session id -> archives it
|
|
36
|
+
# (SessionManager.archive_session's result)
|
|
37
|
+
# @param installed_version [#call] -> the newest chi installed
|
|
38
|
+
# @param children_counts [#call] session id -> ChildrenStatus::Counts
|
|
39
|
+
def initialize(client:, state_dir: Session.default_state_dir,
|
|
40
|
+
delete_session: ->(id) { SessionManager.delete_session(id, stop: true, wait: DELETE_WAIT) },
|
|
41
|
+
archive_session: ->(id) { SessionManager.archive_session(id, wait: DELETE_WAIT) },
|
|
42
|
+
installed_version: -> { InstalledVersions.new.newest },
|
|
43
|
+
children_counts: ->(id) { ChildrenStatus.counts(id, state_dir: state_dir) })
|
|
44
|
+
@client = client
|
|
45
|
+
@state_dir = state_dir
|
|
46
|
+
@delete_session = delete_session
|
|
47
|
+
@archive_session = archive_session
|
|
48
|
+
@installed_version = installed_version
|
|
49
|
+
@children_counts = children_counts
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The sidecar's worker, nil while none runs; gone once the session was
|
|
53
|
+
# stopped (no worker is coming): its stop marker in +state_dir+.
|
|
54
|
+
def live_worker
|
|
55
|
+
id = @client.session_id
|
|
56
|
+
return [:gone, "stopped"] if Session.stopped_marker?(id, state_dir: @state_dir)
|
|
57
|
+
|
|
58
|
+
[BridgeClient.discover(id, session_dir: session_dir, host: @client.host), nil]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A new worker's Bridge, on the port its event names.
|
|
62
|
+
def switched(event)
|
|
63
|
+
@client = BridgeClient.new(session_id: @client.session_id, port: AttachLink.event_port(event), host: @client.host)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# No sidecar to read: true, its Bridge answers (a P0-P2 worker refuses
|
|
67
|
+
# a queue).
|
|
68
|
+
def queues? = feature?(Delivery::QUEUE_FEATURE)
|
|
69
|
+
|
|
70
|
+
# No sidecar to read: true, its Bridge answers (an older one's 404
|
|
71
|
+
# not_found says so).
|
|
72
|
+
def unqueues? = feature?(SessionInbox::UNQUEUE_FEATURE)
|
|
73
|
+
|
|
74
|
+
# The worker's stats; a worker from before the stats route answers
|
|
75
|
+
# only /state.
|
|
76
|
+
def stats_metrics
|
|
77
|
+
@client.get_json("stats")&.dig("metrics") || @client.get_json("state")&.dig("session_state_snapshot", "metrics")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Stored in the session's images/ here (the worker's state dir).
|
|
81
|
+
def ingest_images(paths)
|
|
82
|
+
paths.map do |path|
|
|
83
|
+
ref = ImageStore.ingest(session_dir, path: path)
|
|
84
|
+
{ file: ref[:file], name: ref[:name] }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# SessionMetrics, in this machine's state dir: none for a prompt with
|
|
89
|
+
# no turn id (an older session) or a session with no analytics.
|
|
90
|
+
def saved_tool_records(turn_id)
|
|
91
|
+
SessionMetrics.saved_tool_records(session_dir, turn_id)
|
|
92
|
+
rescue Session::InvalidId
|
|
93
|
+
[]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def saved_turn_record(turn_id)
|
|
97
|
+
SessionMetrics.saved_turn_record(session_dir, turn_id)
|
|
98
|
+
rescue Session::InvalidId
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def children_counts = @children_counts.call(@client.session_id)
|
|
103
|
+
|
|
104
|
+
def delete_session = @delete_session.call(@client.session_id)
|
|
105
|
+
|
|
106
|
+
def archive_session = @archive_session.call(@client.session_id)
|
|
107
|
+
|
|
108
|
+
def installed_version = @installed_version.call
|
|
109
|
+
|
|
110
|
+
def default_model_name = ModelProfile.default_model_name
|
|
111
|
+
|
|
112
|
+
def parent_strategy = ParentReport.strategy_for(@client.session_id, state_dir: @state_dir)
|
|
113
|
+
|
|
114
|
+
def completions = Completions::ALL
|
|
115
|
+
|
|
116
|
+
def reconnect_delays = BridgeClient::EventStream::REDISCOVER_DELAYS
|
|
117
|
+
|
|
118
|
+
def via = ChiInvocation::HERE
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def session_dir = Session.session_dir(@client.session_id, state_dir: @state_dir)
|
|
123
|
+
|
|
124
|
+
def feature?(name)
|
|
125
|
+
sidecar = WorkerSidecar.read(session_dir)
|
|
126
|
+
sidecar.nil? || sidecar.features.include?(name)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../installed_versions"
|
|
4
4
|
require_relative "../version"
|
|
5
|
+
require_relative "../chi_invocation"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
7
8
|
class TerminalUI
|
|
@@ -15,8 +16,9 @@ module Samagotchi
|
|
|
15
16
|
# @param worker [String, nil] the worker's chi_version (nil: a worker
|
|
16
17
|
# from before it said so; nothing is said then)
|
|
17
18
|
# @param installed [String, nil] InstalledVersions#newest
|
|
19
|
+
# @param via [ChiInvocation] how the commands in it are spelled
|
|
18
20
|
# @return [String, nil] one "chi> ..." line, or nil when all is current
|
|
19
|
-
def at_attach(worker:, installed:, session_id:, terminal: VERSION)
|
|
21
|
+
def at_attach(worker:, installed:, session_id:, terminal: VERSION, via: ChiInvocation::HERE)
|
|
20
22
|
return nil if worker.to_s.empty?
|
|
21
23
|
|
|
22
24
|
newest = [installed, terminal].select { |v| Gem::Version.correct?(v.to_s) && !v.to_s.empty? }
|
|
@@ -26,8 +28,8 @@ module Samagotchi
|
|
|
26
28
|
return nil unless worker_old || terminal_old
|
|
27
29
|
|
|
28
30
|
id = session_id.to_s[0, 8]
|
|
29
|
-
restart = "
|
|
30
|
-
attach = "/detach, then
|
|
31
|
+
restart = via.command("sessions restart #{id}")
|
|
32
|
+
attach = "/detach, then #{via.attach(id)}"
|
|
31
33
|
if worker_old && terminal_old
|
|
32
34
|
"chi> chi #{newest} is installed; this session's worker runs #{worker} and this terminal #{terminal}. " \
|
|
33
35
|
"#{restart} moves the worker; #{attach} for the terminal."
|
|
@@ -890,8 +890,8 @@ module Samagotchi
|
|
|
890
890
|
show_command_result(command)
|
|
891
891
|
refresh_status_row(clear_unknown_context: true) if command.changed.include?(:model)
|
|
892
892
|
refresh_status_row if command.changed.include?(:title)
|
|
893
|
-
# /recap
|
|
894
|
-
save_session(session) if command.changed.
|
|
893
|
+
# /recap and /title don't save themselves (a worker saves a changed command).
|
|
894
|
+
save_session(session) if command.changed.intersect?(%i[recap title])
|
|
895
895
|
persist_recent_history(input) if command.shell
|
|
896
896
|
return
|
|
897
897
|
end
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
# /title, the session's own name (Session#title): with no argument, the
|
|
5
5
|
# title and what shows without one; `/title TEXT` names the session
|
|
6
|
-
# (whitespace collapsed, cut at 80 characters as a preview is)
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
6
|
+
# (whitespace collapsed, cut at 80 characters as a preview is);
|
|
7
|
+
# `/title --clear` drops it, and the lists show the first message again.
|
|
8
|
+
# Anyone who may send the session a command may name it (a parent agent
|
|
9
|
+
# its delegates too). It doesn't save: its host saves a changed result at
|
|
10
|
+
# once (Worker, the REPL). It runs mid-turn as well (anytime): the
|
|
11
|
+
# worker's save takes the session's lock and writes a running turn's
|
|
12
|
+
# checkpoint, as a turn's own saves do.
|
|
12
13
|
class TitleCommand
|
|
13
14
|
NAME = "/title"
|
|
14
15
|
CLEAR = "--clear"
|
|
@@ -17,10 +18,8 @@ module Samagotchi
|
|
|
17
18
|
LIMIT = 80
|
|
18
19
|
|
|
19
20
|
# @param engine [Engine] #session
|
|
20
|
-
|
|
21
|
-
def initialize(engine:, save:)
|
|
21
|
+
def initialize(engine:)
|
|
22
22
|
@engine = engine
|
|
23
|
-
@save = save
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
# @param args [String] what follows /title
|
|
@@ -42,9 +41,8 @@ module Samagotchi
|
|
|
42
41
|
session.title = title
|
|
43
42
|
# The command itself as the session's first line (chi send --new
|
|
44
43
|
# "/title X", a web first message) became its preview: it goes, and
|
|
45
|
-
# the save derives the preview from the first user message again.
|
|
44
|
+
# the host's save derives the preview from the first user message again.
|
|
46
45
|
session.first_preview = "" if session.first_preview.to_s.start_with?(NAME)
|
|
47
|
-
@save.call(session)
|
|
48
46
|
[listing(session), true, :ok]
|
|
49
47
|
end
|
|
50
48
|
|