samagotchi 0.152.0 → 0.154.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 +45 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundle-authoring.md +1 -1
- data/docs/bundles.md +93 -12
- data/docs/cli.md +83 -99
- data/docs/configuration.md +46 -40
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +15 -15
- data/docs/hooks.md +13 -12
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +5 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +42 -12
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +8 -8
- data/docs/internals/prompt-caching.md +8 -4
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +20 -17
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +52 -43
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +25 -25
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +18 -8
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +63 -2642
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +106 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +36 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -13,14 +13,19 @@ require_relative "../launch_mode"
|
|
|
13
13
|
require_relative "live_region"
|
|
14
14
|
require_relative "plain_surface"
|
|
15
15
|
require_relative "attached_loop"
|
|
16
|
+
require_relative "log_tail"
|
|
17
|
+
require_relative "formatting"
|
|
16
18
|
|
|
17
19
|
module Samagotchi
|
|
18
20
|
class TerminalUI
|
|
19
|
-
# `chi --attach ID` and `chi --
|
|
21
|
+
# `chi`, `chi --attach ID` and `chi --resume ID`: find or start the
|
|
20
22
|
# session's worker, then run the TUI as a client of its Bridge
|
|
21
23
|
# (AttachedLoop). It takes no OwnerLock and builds no Engine: the worker
|
|
22
24
|
# owns the session, and any number of UIs can attach to it.
|
|
23
25
|
module AttachLauncher
|
|
26
|
+
# #paint, for the -v records.
|
|
27
|
+
extend Formatting
|
|
28
|
+
|
|
24
29
|
# Why attaching failed, worded for the terminal.
|
|
25
30
|
class Error < StandardError; end
|
|
26
31
|
|
|
@@ -28,6 +33,13 @@ module Samagotchi
|
|
|
28
33
|
# A connector that answered no session (its own failures raise Error
|
|
29
34
|
# with their words).
|
|
30
35
|
NOTHING_CONNECTED = "could not attach: no session was found or started"
|
|
36
|
+
# `chi scratch`'s first line, and its last once the session is gone.
|
|
37
|
+
SCRATCH_BANNER = "Scratch session: nothing is kept, it is deleted when you leave."
|
|
38
|
+
SCRATCH_DELETED = "Scratch session deleted."
|
|
39
|
+
# chi -v on a worker that was running before this launch (%s: the
|
|
40
|
+
# debug log): its level is its own.
|
|
41
|
+
VERBOSE_RUNNING = "(--verbose: this worker was already running and logs at the level it started with; " \
|
|
42
|
+
"its records show here only if it writes to %s: one chi web started may log elsewhere)"
|
|
31
43
|
|
|
32
44
|
module_function
|
|
33
45
|
|
|
@@ -62,6 +74,13 @@ module Samagotchi
|
|
|
62
74
|
# own guardrails.parent_answers; an existing session keeps its own
|
|
63
75
|
# (never sent as a /parent-answers command: that would be a
|
|
64
76
|
# loosening route from a command line)
|
|
77
|
+
# @param scratch [Boolean] `chi scratch`: a new scratch session (its
|
|
78
|
+
# worker's Engine saves nothing), stopped and deleted however the
|
|
79
|
+
# attach ends; its worker deletes it too once no client is left
|
|
80
|
+
# @param verbose [Boolean] -v: the session's debug records in the
|
|
81
|
+
# debug log (Log.path), shown above the live region as they come
|
|
82
|
+
# (LogTail); a worker this launch spawns logs at debug (bin/chi's
|
|
83
|
+
# log.level override reaches it through Config.cli_env)
|
|
65
84
|
# @param state_dir [String, nil] where the sessions are (the default
|
|
66
85
|
# state dir by default): the worker's, for the attached loop too
|
|
67
86
|
# @param connector [#connect, nil] finds or starts the session
|
|
@@ -73,19 +92,23 @@ module Samagotchi
|
|
|
73
92
|
# :failed when the --model switch didn't go through, or (input from a
|
|
74
93
|
# pipe) :turn_failed / :empty_answer / :removed / :unanswered
|
|
75
94
|
# (AttachedLoop#run)
|
|
76
|
-
def run(attach: nil,
|
|
95
|
+
def run(attach: nil, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
|
|
77
96
|
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
78
|
-
toolkit_flags: [], settings: nil, parent_answers: nil,
|
|
97
|
+
toolkit_flags: [], settings: nil, parent_answers: nil, scratch: false, verbose: false, state_dir: nil,
|
|
98
|
+
connector: nil)
|
|
79
99
|
state_dir ||= Session.default_state_dir
|
|
100
|
+
running_before = verbose && !connector && worker_running?(attach || resume, state_dir)
|
|
80
101
|
link = connector && (connector.connect || raise(Error, NOTHING_CONNECTED))
|
|
81
102
|
client = if connector
|
|
82
103
|
link.client
|
|
83
104
|
else
|
|
84
|
-
connect(attach: attach,
|
|
105
|
+
connect(attach: attach, resume: resume, model: model, state_dir: state_dir,
|
|
85
106
|
memories: memories, muted_memories: muted_memories, llm_context: llm_context,
|
|
86
107
|
thinking: thinking, appended_prompt_files: appended_prompt_files, toolkit: toolkit,
|
|
87
|
-
toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers
|
|
108
|
+
toolkit_flags: toolkit_flags, settings: settings, parent_answers: parent_answers,
|
|
109
|
+
scratch: scratch)
|
|
88
110
|
end
|
|
111
|
+
$stdout.puts(SCRATCH_BANNER) if scratch
|
|
89
112
|
commands = []
|
|
90
113
|
# --yes on /model and /thinking: typing the flag is the choice (no
|
|
91
114
|
# confirm step); its applied reply ends the launch's command.
|
|
@@ -93,11 +116,13 @@ module Samagotchi
|
|
|
93
116
|
commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
|
|
94
117
|
commands << "#{ThinkingCommand::NAME} #{thinking} --yes" if (attach || resume) && thinking
|
|
95
118
|
first_command = commands.size > 1 ? commands : commands.first
|
|
119
|
+
$stderr.puts(format(VERBOSE_RUNNING, Log.path)) if running_before
|
|
96
120
|
surface = open_surface
|
|
121
|
+
tail = (start_log_tail(surface, client.session_id) if verbose && Log.path)
|
|
97
122
|
options = { screen: surface, client_id: client_id(connector), first_prompt: prompt,
|
|
98
123
|
first_command: first_command, no_interrupt: no_interrupt,
|
|
99
124
|
default_input: default_input && !prompt && !attach && !resume,
|
|
100
|
-
wait_at_eof: !$stdin.tty?,
|
|
125
|
+
wait_at_eof: !$stdin.tty?, scratch: scratch,
|
|
101
126
|
as_parent: Guardrails::ParentApprovals.parent_process? }
|
|
102
127
|
attached = if connector
|
|
103
128
|
AttachedLoop.new(link: link, **options)
|
|
@@ -107,6 +132,7 @@ module Samagotchi
|
|
|
107
132
|
ended = begin
|
|
108
133
|
attached.run
|
|
109
134
|
ensure
|
|
135
|
+
tail&.stop
|
|
110
136
|
close_surface(surface)
|
|
111
137
|
end
|
|
112
138
|
if ended == :unanswered && attached.unanswered
|
|
@@ -119,6 +145,34 @@ module Samagotchi
|
|
|
119
145
|
ended
|
|
120
146
|
ensure
|
|
121
147
|
link&.close
|
|
148
|
+
# Every way out (Ctrl-D, /exit, the worker gone, an error, Ctrl-C or
|
|
149
|
+
# a signal): only kill -9 leaves it to its worker (and the sweep).
|
|
150
|
+
end_scratch(client.session_id, state_dir: state_dir, quiet: !$!.nil?) if scratch && client
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Whether +session_id+'s worker runs now (nil: a new session).
|
|
154
|
+
def worker_running?(session_id, state_dir)
|
|
155
|
+
return false unless session_id && Session.valid_id?(session_id)
|
|
156
|
+
|
|
157
|
+
WorkerLink.resolve(Session.session_dir(session_id, state_dir: state_dir), unlink: false).live?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The session's debug records, dim, above the live region.
|
|
161
|
+
def start_log_tail(surface, session_id)
|
|
162
|
+
LogTail.new(path: Log.path, session_id: session_id, on_record: lambda { |record|
|
|
163
|
+
surface.commit(paint(LogLine.format(record).chomp, 2))
|
|
164
|
+
}).start
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Stop the scratch session's worker and delete the session; its worker
|
|
168
|
+
# may have deleted it first (it found no client left).
|
|
169
|
+
def end_scratch(session_id, state_dir:, quiet: false, out: $stdout, err: $stderr)
|
|
170
|
+
SessionManager.delete_session(session_id, state_dir: state_dir, stop: true, wait: LocalLink::DELETE_WAIT)
|
|
171
|
+
out.puts(SCRATCH_DELETED) unless quiet
|
|
172
|
+
rescue ArgumentError
|
|
173
|
+
out.puts(SCRATCH_DELETED) unless quiet
|
|
174
|
+
rescue SessionManager::DeleteRefused, SystemCallError => e
|
|
175
|
+
err.puts("Scratch session #{session_id} was not deleted (#{e.message}): chi sessions delete #{session_id}")
|
|
122
176
|
end
|
|
123
177
|
|
|
124
178
|
# This TUI's client id: tui:PID, or the connector's own (a remote one
|
|
@@ -185,10 +239,11 @@ module Samagotchi
|
|
|
185
239
|
|
|
186
240
|
# @return [BridgeClient] a client for the live Bridge of the session
|
|
187
241
|
# @raise [Error]
|
|
188
|
-
def connect(attach: nil,
|
|
242
|
+
def connect(attach: nil, resume: nil, model: nil, state_dir: nil, wait: BRIDGE_WAIT,
|
|
189
243
|
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
190
|
-
toolkit_flags: [], settings: nil, parent_answers: nil, err: $stderr)
|
|
244
|
+
toolkit_flags: [], settings: nil, parent_answers: nil, scratch: false, err: $stderr)
|
|
191
245
|
sd = state_dir || Session.default_state_dir
|
|
246
|
+
connected = false
|
|
192
247
|
if attach || resume
|
|
193
248
|
warn_memory_flags_ignored(attach || resume, memories, muted_memories, err, appended_prompt_files: appended_prompt_files,
|
|
194
249
|
toolkit_flags: toolkit_flags)
|
|
@@ -211,14 +266,21 @@ module Samagotchi
|
|
|
211
266
|
thinking: Thinking.session_level(thinking),
|
|
212
267
|
appended_prompt_files: appended_prompt_files,
|
|
213
268
|
toolkit: toolkit, settings: settings,
|
|
214
|
-
parent_answers: parent_answers)
|
|
269
|
+
parent_answers: parent_answers),
|
|
270
|
+
scratch: scratch)
|
|
215
271
|
end
|
|
216
272
|
link = WorkerLink.await(session.id, session_dir: Session.session_dir(session.id, state_dir: sd), timeout: wait)
|
|
217
|
-
|
|
273
|
+
raise(Error, format(LaunchMode::BRIDGE_LATE, session.id)) unless link.live?
|
|
274
|
+
|
|
275
|
+
connected = true
|
|
276
|
+
link.client(session.id)
|
|
218
277
|
rescue SessionManager::OwnedByTUI => e
|
|
219
278
|
raise Error, e.message
|
|
220
279
|
rescue ArgumentError => e
|
|
221
280
|
raise Error, e.message
|
|
281
|
+
ensure
|
|
282
|
+
# A scratch session whose Bridge never came (or Ctrl-C meanwhile).
|
|
283
|
+
end_scratch(session.id, state_dir: sd, quiet: true, err: err) if scratch && session && !connected
|
|
222
284
|
end
|
|
223
285
|
|
|
224
286
|
# The session's previous worker may still be leaving: its Bridge
|
|
@@ -12,10 +12,11 @@ module Samagotchi
|
|
|
12
12
|
# Input composition and Ctrl-C handling for the attached loop.
|
|
13
13
|
# Reads @client, @clock, @command_registry, @continue_offer, @exit_ask,
|
|
14
14
|
# @gaps, @link, @prefilled, @question, @queue, @queued_ids, @reader,
|
|
15
|
-
# @running, @screen; writes @prefilled, @last_idle_interrupt_at.
|
|
15
|
+
# @running, @scratch, @screen; writes @prefilled, @last_idle_interrupt_at.
|
|
16
16
|
# Calls the loop helpers #answer_continue, #answer_exit_ask, #answer_question,
|
|
17
17
|
# #archive_after_turn, #command_registry, #detach, #echo_answer, #exit_and_archive,
|
|
18
|
-
# #
|
|
18
|
+
# #end_scratch, #exit_and_delete, #exit_worker, #refuse_scratch_archive, #gap_divider,
|
|
19
|
+
# #participant_commands, #persist_recent_history,
|
|
19
20
|
# #queue_input_prefill, #read_prompt_line, #send_command, #send_prompt,
|
|
20
21
|
# #show_recap, #show_stats, #submit_delivery, #unqueue_newest, #via, and
|
|
21
22
|
# #worker_down.
|
|
@@ -46,6 +47,7 @@ module Samagotchi
|
|
|
46
47
|
return answer_exit_ask(line)
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
return end_scratch if line.nil? && @scratch
|
|
49
51
|
return detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if line.nil?
|
|
50
52
|
|
|
51
53
|
text = line.strip
|
|
@@ -104,11 +106,12 @@ module Samagotchi
|
|
|
104
106
|
route = ComposerRoute.call(text, running: @running, registry: command_registry)
|
|
105
107
|
return submit_delivery(route) if route.choice
|
|
106
108
|
|
|
107
|
-
# The terminal's own commands (SessionCommands registers them
|
|
108
|
-
# REPL reads the same words). Before the continue offer: the
|
|
109
|
+
# The terminal's own commands (SessionCommands registers them). Before the continue offer: the
|
|
109
110
|
# leaving ones are no answer to it.
|
|
110
111
|
local = route.local
|
|
111
112
|
return submit(nil) if local == :detach
|
|
113
|
+
return end_scratch if local == :exit && @scratch
|
|
114
|
+
return refuse_scratch_archive if local == :archive && @scratch
|
|
112
115
|
return SessionCommands.delete_on_exit?(text) ? exit_and_delete : exit_worker if local == :exit
|
|
113
116
|
return archive_after_turn if local == :archive && (@running || !@queued_ids.empty?)
|
|
114
117
|
return exit_and_archive if local == :archive
|
|
@@ -130,7 +133,7 @@ module Samagotchi
|
|
|
130
133
|
elsif %i[share participants].include?(local)
|
|
131
134
|
@screen.commit(participant_commands.run(text))
|
|
132
135
|
elsif route.kind == :command
|
|
133
|
-
# The history keeps !cmds
|
|
136
|
+
# The history keeps !cmds (prompts: #send_prompt).
|
|
134
137
|
persist_recent_history(text) if PromptHistory.shell_line?(text)
|
|
135
138
|
send_command(text)
|
|
136
139
|
elsif route.kind == :typo
|
|
@@ -142,7 +145,7 @@ module Samagotchi
|
|
|
142
145
|
nil
|
|
143
146
|
end
|
|
144
147
|
|
|
145
|
-
# One read on the reader thread:
|
|
148
|
+
# One read on the reader thread: a multiline read with Tab
|
|
146
149
|
# completion at the main prompt, a plain line for a question's choice
|
|
147
150
|
# or the continue offer's answer.
|
|
148
151
|
def read_input_line(prompt, prefill)
|
|
@@ -169,7 +172,7 @@ module Samagotchi
|
|
|
169
172
|
|
|
170
173
|
# The session's commands as its worker's snapshot names them (its
|
|
171
174
|
# plugins' too); the built-ins until one arrives, or from a worker too
|
|
172
|
-
# old to name them. An unknown /foo is a prompt
|
|
175
|
+
# old to name them. An unknown /foo is a prompt.
|
|
173
176
|
def command_registry = @command_registry || SessionCommands.builtin_registry
|
|
174
177
|
|
|
175
178
|
# Ctrl-C cancels the running turn (whoever started it). At an idle
|
|
@@ -197,13 +200,13 @@ module Samagotchi
|
|
|
197
200
|
return submit(nil) if @last_idle_interrupt_at && now - @last_idle_interrupt_at <= DETACH_WINDOW
|
|
198
201
|
|
|
199
202
|
@last_idle_interrupt_at = now
|
|
200
|
-
@screen.commit(DETACH_HINT)
|
|
203
|
+
@screen.commit(@scratch ? SCRATCH_DETACH_HINT : DETACH_HINT)
|
|
201
204
|
nil
|
|
202
205
|
end
|
|
203
206
|
|
|
204
207
|
# On the reader thread, from Reline's Ctrl-C: what the line held, and
|
|
205
208
|
# when. During a turn the press goes to the loop (it cancels the turn)
|
|
206
|
-
# and the read goes on with the typed text still in it
|
|
209
|
+
# and the read goes on with the typed text still in it;
|
|
207
210
|
# otherwise it is noted for the Interrupt the LineReader queues next.
|
|
208
211
|
# @return [Boolean] true: handled, the read goes on; false: Reline ends
|
|
209
212
|
# the read with Interrupt
|
|
@@ -19,6 +19,22 @@ module Samagotchi
|
|
|
19
19
|
:detach
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# A scratch session's way out (Ctrl-D, /detach, /exit, a second
|
|
23
|
+
# Ctrl-C): the loop ends with no line; its AttachLauncher stops the
|
|
24
|
+
# worker and deletes the session, and says so.
|
|
25
|
+
# @return [Symbol] :detach
|
|
26
|
+
def end_scratch
|
|
27
|
+
@view.finish_thinking_spinner
|
|
28
|
+
:detach
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# /archive in a scratch session: nothing to archive (ArchiveRefused's words).
|
|
32
|
+
# @return [nil]
|
|
33
|
+
def refuse_scratch_archive
|
|
34
|
+
@screen.commit(SessionManager::ArchiveRefused.new(@client.session_id, :scratch).message)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
22
38
|
# Detach, and ask the worker to exit now. The worker decides: it stays
|
|
23
39
|
# up while anything still needs it (a turn, queued input, a continue
|
|
24
40
|
# offer, another UI, reminders), and the line says which. When the only
|
|
@@ -27,8 +27,7 @@ module Samagotchi
|
|
|
27
27
|
nil
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
# A bare answer at the continue prompt
|
|
31
|
-
# one is yes).
|
|
30
|
+
# A bare answer at the continue prompt (an empty one is yes).
|
|
32
31
|
def continue_line(text)
|
|
33
32
|
text.empty? ? SessionCommands::CONTINUE_COMMAND : "#{SessionCommands::CONTINUE_COMMAND} #{text}"
|
|
34
33
|
end
|
|
@@ -149,7 +148,7 @@ module Samagotchi
|
|
|
149
148
|
nil
|
|
150
149
|
end
|
|
151
150
|
|
|
152
|
-
# An empty answer dismisses the question
|
|
151
|
+
# An empty answer dismisses the question: the tool
|
|
153
152
|
# returns unanswered (an approval: denied) and the turn goes on. The
|
|
154
153
|
# :question_cancelled every UI gets finds it already closed here.
|
|
155
154
|
def dismiss_question
|
|
@@ -52,7 +52,7 @@ module Samagotchi
|
|
|
52
52
|
# #handle_event turns the worker's event stream into terminal output:
|
|
53
53
|
# the snapshot a join starts with (the last exchange, queued prompts,
|
|
54
54
|
# the turn so far), prompts other clients sent, and each turn through the
|
|
55
|
-
#
|
|
55
|
+
# EventRenderer, drawn by an AttachedView.
|
|
56
56
|
class AttachedLoop
|
|
57
57
|
include Formatting
|
|
58
58
|
include InputSupport
|
|
@@ -63,9 +63,14 @@ module Samagotchi
|
|
|
63
63
|
include Sending
|
|
64
64
|
|
|
65
65
|
PROMPT = "> "
|
|
66
|
+
# The prompt while a question or a continue offer waits for its answer
|
|
67
|
+
# (its choices are in the notes slot).
|
|
68
|
+
QUESTION_PROMPT = "? "
|
|
66
69
|
# A second Ctrl-C at an empty idle prompt within this many seconds detaches.
|
|
67
70
|
DETACH_WINDOW = 2.0
|
|
68
71
|
DETACH_HINT = "Ctrl-D to detach, /exit stops the worker"
|
|
72
|
+
# A `chi scratch` session's: every way out ends it.
|
|
73
|
+
SCRATCH_DETACH_HINT = "Ctrl-D or /exit ends the scratch session (nothing is kept)"
|
|
69
74
|
attr_reader :client_id
|
|
70
75
|
|
|
71
76
|
# @return [String, nil] the model the worker's turns run on, as the
|
|
@@ -92,7 +97,7 @@ module Samagotchi
|
|
|
92
97
|
# through, the launch stops
|
|
93
98
|
# @param no_interrupt [Boolean] post every turn with no_interrupt
|
|
94
99
|
# @param default_input [Boolean] type SAMAGOTCHI_DEFAULT_INPUT into
|
|
95
|
-
# the first read (a new session with no -p
|
|
100
|
+
# the first read (a new session with no -p)
|
|
96
101
|
# @param clock [#call] monotonic seconds (the Ctrl-C detach window)
|
|
97
102
|
# @param now [#call] the wall clock, a Time (the gap dividers)
|
|
98
103
|
# @param delete_session [#call] session id -> deletes it (/exit --delete)
|
|
@@ -107,8 +112,11 @@ module Samagotchi
|
|
|
107
112
|
# session folder (@path images, saved records) is under it
|
|
108
113
|
# @param link [AttachLink, nil] where the session is, instead of
|
|
109
114
|
# +client+ and the keywords that make a LocalLink
|
|
115
|
+
# @param scratch [Boolean] a `chi scratch` session: Ctrl-D, /detach,
|
|
116
|
+
# /exit and a second Ctrl-C all end it (its AttachLauncher deletes
|
|
117
|
+
# it), /archive is refused, and typed lines stay out of the history
|
|
110
118
|
def initialize(screen:, client_id:, client: nil, first_prompt: nil, first_command: nil, no_interrupt: false,
|
|
111
|
-
default_input: false, wait_at_eof: false, as_parent: false,
|
|
119
|
+
default_input: false, wait_at_eof: false, as_parent: false, scratch: false,
|
|
112
120
|
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, now: -> { MessageTime.now },
|
|
113
121
|
delete_session: ->(id) { SessionManager.delete_session(id, stop: true, wait: LocalLink::DELETE_WAIT) },
|
|
114
122
|
archive_session: ->(id) { SessionManager.archive_session(id, wait: LocalLink::DELETE_WAIT) },
|
|
@@ -119,6 +127,7 @@ module Samagotchi
|
|
|
119
127
|
archive_session: archive_session, installed_version: installed_version,
|
|
120
128
|
children_counts: children_counts)
|
|
121
129
|
@client = @link.client
|
|
130
|
+
@scratch = scratch
|
|
122
131
|
@screen = screen
|
|
123
132
|
@client_id = client_id
|
|
124
133
|
# A parent agent drives this attach (AttachLauncher:
|
|
@@ -285,7 +294,7 @@ module Samagotchi
|
|
|
285
294
|
@gaps.touch
|
|
286
295
|
refresh_children
|
|
287
296
|
own_turn_ended(event)
|
|
288
|
-
# The renderer says how it ended (
|
|
297
|
+
# The renderer says how it ended (a one-shot's words too).
|
|
289
298
|
when :turn_canceled, :turn_failed
|
|
290
299
|
@status.take_event(event)
|
|
291
300
|
@renderer.call(event)
|
|
@@ -413,7 +422,7 @@ module Samagotchi
|
|
|
413
422
|
|
|
414
423
|
# Here a stopped session has always read as unreachable.
|
|
415
424
|
reason = "unreachable" if reason == "stopped"
|
|
416
|
-
"Lost the session's worker (#{reason}). Resume it with: #{via.command("--
|
|
425
|
+
"Lost the session's worker (#{reason}). Resume it with: #{via.command("--resume #{id}")}"
|
|
417
426
|
end
|
|
418
427
|
|
|
419
428
|
# Each reason in its own words (the remote's name, its chi web's
|
|
@@ -478,7 +487,7 @@ module Samagotchi
|
|
|
478
487
|
end
|
|
479
488
|
|
|
480
489
|
# The saved recap, and a new one asked for at once (it arrives as
|
|
481
|
-
# :recap_ready),
|
|
490
|
+
# :recap_ready), in Formatting's /recap words.
|
|
482
491
|
def show_recap
|
|
483
492
|
reply = @client.request_recap
|
|
484
493
|
answer = reply.status == 200 ? reply.json : nil
|
|
@@ -504,11 +513,11 @@ module Samagotchi
|
|
|
504
513
|
end
|
|
505
514
|
|
|
506
515
|
# The worker rolled a failed turn back and handed its prompt back: ours
|
|
507
|
-
# goes back into the input
|
|
516
|
+
# goes back into the input for a retry.
|
|
508
517
|
def restore_prompt(event)
|
|
509
518
|
origin = origin_of(event[:origin])
|
|
510
519
|
return unless own?(origin.client_id) && @own_turns.sent?(origin.enqueued_id)
|
|
511
|
-
# Lines from a pipe: nobody to retry it
|
|
520
|
+
# Lines from a pipe: nobody to retry it.
|
|
512
521
|
return if @wait_at_eof
|
|
513
522
|
|
|
514
523
|
if @reader&.prefill(event[:prompt].to_s)
|
|
@@ -10,7 +10,7 @@ require_relative "thinking_line"
|
|
|
10
10
|
|
|
11
11
|
module Samagotchi
|
|
12
12
|
class TerminalUI
|
|
13
|
-
# EventRenderer's view in
|
|
13
|
+
# EventRenderer's view in the attached TUI, drawn
|
|
14
14
|
# on a Screen: the thinking feedback is the activity slot, one row right
|
|
15
15
|
# above the prompt (spinner frame, then the running tool or the newest
|
|
16
16
|
# sentence of the model's thinking or text, ThinkingLine), and every
|
|
@@ -60,8 +60,7 @@ module Samagotchi
|
|
|
60
60
|
@screen.commit(text)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Draw on +surface+ from now on (the REPL
|
|
64
|
-
# after the view is built, and back when it ends): what the slot shows
|
|
63
|
+
# Draw on +surface+ from now on (no caller since the REPL went): what the slot shows
|
|
65
64
|
# moves there.
|
|
66
65
|
def surface=(surface)
|
|
67
66
|
@lock.synchronize do
|
|
@@ -8,9 +8,8 @@ module Samagotchi
|
|
|
8
8
|
# Renders one UI's view of Engine turn events: the thinking spinner while
|
|
9
9
|
# the model generates, a `tool>` line per completed tool call, and the
|
|
10
10
|
# answer (with any tool activity not already shown) from :turn_completed's
|
|
11
|
-
# turn_summary, and how a turn ended. It
|
|
12
|
-
#
|
|
13
|
-
# Bridge.
|
|
11
|
+
# turn_summary, and how a turn ended. It renders what attached mode gets
|
|
12
|
+
# over the Bridge.
|
|
14
13
|
#
|
|
15
14
|
# Drawing is delegated to a +view+ (an AttachedView in both TUIs). The
|
|
16
15
|
# renderer keeps
|
|
@@ -158,8 +157,7 @@ module Samagotchi
|
|
|
158
157
|
end
|
|
159
158
|
|
|
160
159
|
# Start a turn's bookkeeping and reset the view's per-turn feedback.
|
|
161
|
-
# Also called
|
|
162
|
-
# Engine#run_turn (so no :turn_started).
|
|
160
|
+
# Also called from #call on a :turn_started.
|
|
163
161
|
def begin_turn
|
|
164
162
|
@streamed_tool_activity = Hash.new(0)
|
|
165
163
|
@tool_started_at = {}
|
|
@@ -17,14 +17,14 @@ require_relative "model_segments_text"
|
|
|
17
17
|
|
|
18
18
|
module Samagotchi
|
|
19
19
|
class TerminalUI
|
|
20
|
-
# Line formatting shared by the
|
|
20
|
+
# Line formatting shared by the attached view and a one-shot: colour, the
|
|
21
21
|
# `tool>` line, elapsed durations. Pure apart from reading whether
|
|
22
22
|
# $stdout is a colour terminal.
|
|
23
23
|
module Formatting
|
|
24
24
|
include ModelSegmentsText
|
|
25
25
|
|
|
26
|
-
# The /stats text for a metrics snapshot, for the
|
|
27
|
-
#
|
|
26
|
+
# The /stats text for a metrics snapshot, for the attached view (an
|
|
27
|
+
# instance includes the module) and for `chi sessions stats`
|
|
28
28
|
# (no UI: the same text from the CLI).
|
|
29
29
|
# @return [String] "(no metrics yet)" without a snapshot
|
|
30
30
|
def self.format_session_metrics(snapshot)
|
|
@@ -420,7 +420,7 @@ module Samagotchi
|
|
|
420
420
|
text.strip.empty? ? [] : lines
|
|
421
421
|
end
|
|
422
422
|
|
|
423
|
-
# ── Turn ends: one set of words for
|
|
423
|
+
# ── Turn ends: one set of words for a one-shot, attached mode and the
|
|
424
424
|
# web (timing.js cancelLineText, format.js failedTurnText) ──────────
|
|
425
425
|
|
|
426
426
|
# A cancel's reason as the UIs name it (anything else as it is);
|
|
@@ -552,7 +552,7 @@ module Samagotchi
|
|
|
552
552
|
# Off by the session's own recap.enabled (/recap off, --set recap.enabled=false).
|
|
553
553
|
RECAP_SESSION_OFF_TEXT = "recap is off for this session (/recap on turns it on, /recap default goes back to config.yml)"
|
|
554
554
|
|
|
555
|
-
# What /recap says (
|
|
555
|
+
# What /recap says (attached mode's words): the saved
|
|
556
556
|
# recap, then what came of asking for a new one.
|
|
557
557
|
# @param saved [Hash, nil] {text:, turns_since:}
|
|
558
558
|
# @param request [Symbol, String, nil] IdleRecap#request_now's answer
|
|
@@ -573,7 +573,7 @@ module Samagotchi
|
|
|
573
573
|
lines.compact.join("\n")
|
|
574
574
|
end
|
|
575
575
|
|
|
576
|
-
# ── The status line (
|
|
576
|
+
# ── The status line (attached mode's idle one) ───────────────────────
|
|
577
577
|
|
|
578
578
|
# status.line: off hides it.
|
|
579
579
|
STATUS_LINE_OFF = "off"
|
|
@@ -662,7 +662,7 @@ module Samagotchi
|
|
|
662
662
|
end
|
|
663
663
|
|
|
664
664
|
# Render the analytics snapshot as a compact, user-facing report. Raw event
|
|
665
|
-
# logs (debug-only) are intentionally excluded; this surface is for
|
|
665
|
+
# logs (debug-only) are intentionally excluded; this surface is for /stats
|
|
666
666
|
# and `chi sessions stats` (the module function above).
|
|
667
667
|
def format_session_metrics(snapshot)
|
|
668
668
|
return "(no metrics yet)" unless snapshot.is_a?(Hash)
|
|
@@ -14,7 +14,7 @@ require_relative "reline_history_hook"
|
|
|
14
14
|
|
|
15
15
|
module Samagotchi
|
|
16
16
|
class TerminalUI
|
|
17
|
-
#
|
|
17
|
+
# The attached TUI's input: the persistent prompt
|
|
18
18
|
# history, Tab completion (/commands, @path in the cwd, #memory, which
|
|
19
19
|
# inserts #name and the model reads it as typed), multiline reads at the
|
|
20
20
|
# main prompt, and a one-shot prefill of the next read (the default
|
|
@@ -29,12 +29,6 @@ module Samagotchi
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
-
# The /commands Tab offers: the session's registry, the REPL's own
|
|
33
|
-
# among them.
|
|
34
|
-
def slash_commands = command_registry.completions(:repl)
|
|
35
|
-
|
|
36
|
-
def command_registry = @commands&.registry || SessionCommands.builtin_registry
|
|
37
|
-
|
|
38
32
|
# Whether a new session's first read gets the default input
|
|
39
33
|
# (SAMAGOTCHI_DEFAULT_INPUT); --no-default-input says no.
|
|
40
34
|
def default_input_wanted? = !@no_default_input
|
|
@@ -6,8 +6,8 @@ module Samagotchi
|
|
|
6
6
|
class TerminalUI
|
|
7
7
|
# Reads input lines on its own thread, so output keeps rendering while
|
|
8
8
|
# the user types, and hands each line (nil = Ctrl-D) or Ctrl-C to a sink
|
|
9
|
-
# (anything with #<<, such as a Thread::Queue); the attached loop
|
|
10
|
-
#
|
|
9
|
+
# (anything with #<<, such as a Thread::Queue); the attached loop reads
|
|
10
|
+
# this way. #reprompt makes it drop the open read and start
|
|
11
11
|
# again with the current prompt (a question opened or closed); #prefill
|
|
12
12
|
# starts it again with text already typed in (a failed prompt).
|
|
13
13
|
class LineReader
|
|
@@ -7,8 +7,8 @@ require_relative "reline_seam"
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
class TerminalUI
|
|
9
9
|
# Opens and closes a live region: a Screen with Reline drawing into it
|
|
10
|
-
# through RelineSeam.
|
|
11
|
-
#
|
|
10
|
+
# through RelineSeam. Attached mode uses one when the terminal can show
|
|
11
|
+
# it, and falls back to a PlainSurface otherwise.
|
|
12
12
|
module LiveRegion
|
|
13
13
|
module_function
|
|
14
14
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../log"
|
|
4
|
+
require_relative "../log_line"
|
|
5
|
+
require_relative "../degrade"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
class TerminalUI
|
|
9
|
+
# `chi -v` attached: the debug log's new records of one session (its
|
|
10
|
+
# sid), followed from where the file ends as it starts, each handed to
|
|
11
|
+
# +on_record+ (AttachLauncher commits it above the live region). The
|
|
12
|
+
# worker writes to the file this chi spawned it with
|
|
13
|
+
# (SAMAGOTCHI_LOG_FILE); a rotation (DebugLog renames the file to .1) is
|
|
14
|
+
# seen as another inode or a shorter file, and the new one is read from
|
|
15
|
+
# its start.
|
|
16
|
+
class LogTail
|
|
17
|
+
POLL_INTERVAL = 0.2
|
|
18
|
+
|
|
19
|
+
# @param path [String] the debug log (Log.path)
|
|
20
|
+
# @param session_id [String] whose records are shown (its first
|
|
21
|
+
# Log::SID_LENGTH chars are the records' sid)
|
|
22
|
+
# @param on_record [#call] LogLine::Record ->, on the tail's thread
|
|
23
|
+
def initialize(path:, session_id:, on_record:, poll_interval: POLL_INTERVAL)
|
|
24
|
+
@path = path
|
|
25
|
+
@sid = session_id.to_s[0, Log::SID_LENGTH]
|
|
26
|
+
@on_record = on_record
|
|
27
|
+
@poll_interval = poll_interval
|
|
28
|
+
@io = nil
|
|
29
|
+
@ino = nil
|
|
30
|
+
@buffer = +""
|
|
31
|
+
@record = nil
|
|
32
|
+
@stopping = false
|
|
33
|
+
@wake = Thread::Queue.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Start following from the file's current end.
|
|
37
|
+
# @return [self]
|
|
38
|
+
def start
|
|
39
|
+
open_at(:end)
|
|
40
|
+
@thread = Thread.new { follow }
|
|
41
|
+
@thread.report_on_exception = false
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# How long #stop waits for the record being handed on.
|
|
46
|
+
STOP_WAIT = 1
|
|
47
|
+
|
|
48
|
+
# The records written since the last look are shown first. The tail
|
|
49
|
+
# ends after the look it is in (a record half committed is finished);
|
|
50
|
+
# one stuck past STOP_WAIT is killed.
|
|
51
|
+
def stop
|
|
52
|
+
if @thread
|
|
53
|
+
@stopping = true
|
|
54
|
+
@wake << :stop
|
|
55
|
+
@thread.kill unless @thread.join(STOP_WAIT)
|
|
56
|
+
Degrade.call(:attached, "log_tail_failed") { poll }
|
|
57
|
+
end
|
|
58
|
+
@io&.close
|
|
59
|
+
@io = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# One look: the lines written since the last (tests call it directly).
|
|
63
|
+
def poll
|
|
64
|
+
reopen_if_rotated
|
|
65
|
+
return unless @io
|
|
66
|
+
|
|
67
|
+
drain
|
|
68
|
+
take_lines
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def follow
|
|
74
|
+
until @stopping
|
|
75
|
+
Degrade.call(:attached, "log_tail_failed") { poll }
|
|
76
|
+
@wake.pop(timeout: @poll_interval)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# What the open file holds past the last read, into the buffer.
|
|
81
|
+
def drain
|
|
82
|
+
chunk = @io.read_nonblock(1 << 16, exception: false)
|
|
83
|
+
while chunk.is_a?(String)
|
|
84
|
+
@buffer << chunk
|
|
85
|
+
chunk = @io.read_nonblock(1 << 16, exception: false)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def open_at(where)
|
|
90
|
+
@io = File.open(@path, "rb")
|
|
91
|
+
@ino = @io.stat.ino
|
|
92
|
+
@io.seek(0, IO::SEEK_END) if where == :end
|
|
93
|
+
rescue SystemCallError
|
|
94
|
+
@io = nil # not there yet: the next look opens it
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def reopen_if_rotated
|
|
98
|
+
return open_at(:start) unless @io
|
|
99
|
+
|
|
100
|
+
stat = File.stat(@path)
|
|
101
|
+
return if stat.ino == @ino && stat.size >= @io.pos
|
|
102
|
+
|
|
103
|
+
# Renamed: what the worker wrote to it since the last look is still
|
|
104
|
+
# there, read before the new file (a truncated one has nothing left).
|
|
105
|
+
drain if stat.ino != @ino
|
|
106
|
+
@io.close
|
|
107
|
+
open_at(:start)
|
|
108
|
+
rescue SystemCallError
|
|
109
|
+
nil # renamed and not written again yet
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Whole lines only; a payload line joins the record above it. DebugLog
|
|
113
|
+
# writes a record and its payload in one write, so a look ends on a
|
|
114
|
+
# whole record: each one is handed on by the end of the look.
|
|
115
|
+
def take_lines
|
|
116
|
+
while (cut = @buffer.index("\n"))
|
|
117
|
+
line = LogLine.clean(@buffer.slice!(0, cut + 1)).chomp
|
|
118
|
+
if LogLine.payload_line?(line)
|
|
119
|
+
@record.payload = [@record.payload, line.delete_prefix(LogLine::INDENT)].compact.join("\n") if @record
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
flush
|
|
123
|
+
@record = LogLine.parse(line)
|
|
124
|
+
end
|
|
125
|
+
flush
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def flush
|
|
129
|
+
record = @record
|
|
130
|
+
@record = nil
|
|
131
|
+
@on_record.call(record) if record && record.sid == @sid
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|