samagotchi 0.153.0 → 0.154.1
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 +42 -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/bundles.md +9 -12
- data/docs/cli.md +82 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +21 -8
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +4 -2
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +31 -6
- data/lib/samagotchi/answer_text.rb +33 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +52 -32
- data/lib/samagotchi/bridge_client.rb +19 -14
- 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/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -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/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -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/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 +3 -3
- 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 +62 -2646
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- 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/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +7 -6
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +13 -13
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm/chat_loop.rb +3 -1
- data/lib/samagotchi/llm_context_command.rb +3 -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/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/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 +67 -49
- 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 +27 -7
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +13 -2
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +154 -145
- 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 +1 -11
- 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 +17 -22
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- 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 +2 -12
- data/lib/samagotchi/terminal_ui/status_row.rb +6 -14
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +15 -16
- data/lib/samagotchi/toolkit_command.rb +2 -1
- 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/send_note.rb +2 -1
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +11 -7
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -51
- data/lib/samagotchi/web/message_display.rb +7 -0
- 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/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- 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 +1 -1
- data/lib/samagotchi/web/public/format.js +4 -3
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +13 -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 +6 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +103 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- data/lib/samagotchi.rb +0 -1
- metadata +31 -7
- data/lib/samagotchi/pending_input_queue.rb +0 -55
- 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
|
@@ -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,16 +60,6 @@ module Samagotchi
|
|
|
60
60
|
@screen.commit(text)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Draw on +surface+ from now on (the REPL swaps in its live region
|
|
64
|
-
# after the view is built, and back when it ends): what the slot shows
|
|
65
|
-
# moves there.
|
|
66
|
-
def surface=(surface)
|
|
67
|
-
@lock.synchronize do
|
|
68
|
-
@screen = surface
|
|
69
|
-
redraw_status if status_text
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
63
|
# A new turn: its tally starts over too.
|
|
74
64
|
def reset_turn_feedback
|
|
75
65
|
@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)
|
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require "reline"
|
|
6
6
|
|
|
7
7
|
require_relative "../config"
|
|
8
|
+
require_relative "../degrade"
|
|
8
9
|
require_relative "../memory_paths"
|
|
9
10
|
require_relative "../prompt_history"
|
|
10
11
|
require_relative "../tools/memory"
|
|
@@ -14,7 +15,7 @@ require_relative "reline_history_hook"
|
|
|
14
15
|
|
|
15
16
|
module Samagotchi
|
|
16
17
|
class TerminalUI
|
|
17
|
-
#
|
|
18
|
+
# The attached TUI's input: the persistent prompt
|
|
18
19
|
# history, Tab completion (/commands, @path in the cwd, #memory, which
|
|
19
20
|
# inserts #name and the model reads it as typed), multiline reads at the
|
|
20
21
|
# main prompt, and a one-shot prefill of the next read (the default
|
|
@@ -29,12 +30,6 @@ module Samagotchi
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
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
33
|
# Whether a new session's first read gets the default input
|
|
39
34
|
# (SAMAGOTCHI_DEFAULT_INPUT); --no-default-input says no.
|
|
40
35
|
def default_input_wanted? = !@no_default_input
|
|
@@ -96,20 +91,18 @@ module Samagotchi
|
|
|
96
91
|
fragment = slash_fragment.to_s.strip
|
|
97
92
|
return [] unless fragment.start_with?("/")
|
|
98
93
|
|
|
99
|
-
|
|
94
|
+
# Only at the line's start: "/x" mid-line is a path, not a command.
|
|
95
|
+
Degrade.call(:attached, "completion_line_unreadable") do
|
|
100
96
|
buf = Reline.line_buffer.to_s
|
|
101
|
-
if !buf.empty? && !buf.lstrip.start_with?("/")
|
|
102
|
-
return []
|
|
103
|
-
end
|
|
104
|
-
rescue StandardError
|
|
105
|
-
nil
|
|
97
|
+
return [] if !buf.empty? && !buf.lstrip.start_with?("/")
|
|
106
98
|
end
|
|
107
99
|
|
|
108
100
|
lowered = fragment.downcase
|
|
109
101
|
return slash_commands.dup if lowered == "/"
|
|
110
102
|
|
|
111
103
|
slash_commands.select { |cmd| cmd.start_with?(lowered) }
|
|
112
|
-
rescue StandardError
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Degrade.note(:attached, "slash_completion_failed", e)
|
|
113
106
|
[]
|
|
114
107
|
end
|
|
115
108
|
|
|
@@ -140,7 +133,8 @@ module Samagotchi
|
|
|
140
133
|
relative_path = "#{relative_path}/" if File.directory?(absolute_path)
|
|
141
134
|
"#{AT_PATH_COMPLETION_PREFIX}#{relative_path}"
|
|
142
135
|
end
|
|
143
|
-
rescue StandardError
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
Degrade.note(:attached, "path_completion_failed", e)
|
|
144
138
|
[]
|
|
145
139
|
end
|
|
146
140
|
|
|
@@ -196,7 +190,8 @@ module Samagotchi
|
|
|
196
190
|
yield scope, name
|
|
197
191
|
end
|
|
198
192
|
end
|
|
199
|
-
rescue StandardError
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
Degrade.note(:attached, "memory_completion_failed", e)
|
|
200
195
|
[]
|
|
201
196
|
end
|
|
202
197
|
|
|
@@ -213,8 +208,8 @@ module Samagotchi
|
|
|
213
208
|
@history_signature = PromptHistory.signature
|
|
214
209
|
@history_seen = PromptHistory.entries
|
|
215
210
|
@history_seen.last(PromptHistory::LIMIT).each { |entry| Reline::HISTORY << entry }
|
|
216
|
-
rescue StandardError
|
|
217
|
-
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
Degrade.note(:attached, "history_load_failed", e)
|
|
218
213
|
end
|
|
219
214
|
|
|
220
215
|
# A scratch session keeps nothing, its typed lines neither.
|
|
@@ -229,8 +224,8 @@ module Samagotchi
|
|
|
229
224
|
history_lock.synchronize { forget_own_line(own) } if own
|
|
230
225
|
raise
|
|
231
226
|
end
|
|
232
|
-
rescue StandardError
|
|
233
|
-
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
Degrade.note(:attached, "history_append_failed", e)
|
|
234
229
|
end
|
|
235
230
|
|
|
236
231
|
# Before a main-prompt read, and on the first ↑ of a walk at an open
|
|
@@ -261,8 +256,8 @@ module Samagotchi
|
|
|
261
256
|
@history_signature = signature
|
|
262
257
|
@history_seen = entries
|
|
263
258
|
history_lock.synchronize { add_others_lines(fresh) }
|
|
264
|
-
rescue StandardError
|
|
265
|
-
|
|
259
|
+
rescue StandardError => e
|
|
260
|
+
Degrade.note(:attached, "history_pickup_failed", e)
|
|
266
261
|
end
|
|
267
262
|
end
|
|
268
263
|
|
|
@@ -77,8 +77,11 @@ module Samagotchi
|
|
|
77
77
|
Log.session_id = @link.client.session_id
|
|
78
78
|
Log.info(:attached, "joined", session: @link.client.session_id, messages: exchange.size,
|
|
79
79
|
notes: messages.count { |m| ContextNote.note?(m) })
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
# The last turn's own prompt (Steer.turn_prompt?, as the page's
|
|
81
|
+
# turns): not Qwen's recovery line, nor input merged into the turn.
|
|
82
|
+
last_user = exchange.rindex { |m| Steer.turn_prompt?(m) }
|
|
83
|
+
from = last_user && messages.index { |m| m.equal?(exchange[last_user]) }
|
|
84
|
+
render_join_notes(messages, from)
|
|
82
85
|
return unless last_user
|
|
83
86
|
|
|
84
87
|
@screen.commit(join_prompt_line(exchange[last_user]))
|
|
@@ -86,7 +89,6 @@ module Samagotchi
|
|
|
86
89
|
unless ImageRef.placeholder_only?(exchange[last_user][:content])
|
|
87
90
|
Array(exchange[last_user][:images]).each { |ref| @screen.commit(format_image_line(ref)) }
|
|
88
91
|
end
|
|
89
|
-
from = messages.index { |m| m.equal?(exchange[last_user]) }
|
|
90
92
|
render_join_steps(messages, from, durations: join_tool_records(exchange[last_user]))
|
|
91
93
|
record = @link.saved_turn_record(exchange[last_user][:turn_id] || exchange[last_user]["turn_id"])
|
|
92
94
|
failed = join_failed_record(record)
|
|
@@ -114,19 +116,21 @@ module Samagotchi
|
|
|
114
116
|
.find { |text| !text.empty? }
|
|
115
117
|
@screen.commit(last_lines(answer)) if answer
|
|
116
118
|
end
|
|
117
|
-
render_join_notes(messages, after_exchange: true)
|
|
119
|
+
render_join_notes(messages, from, after_exchange: true)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# What the last exchange did, as the live turn drew it: each saved
|
|
121
123
|
# call's tool row (action, params, status from its result, and its
|
|
122
124
|
# duration from +durations+, the turn's saved tool records) and a
|
|
123
|
-
# plugin's steer
|
|
124
|
-
# ticker's).
|
|
125
|
+
# plugin's steer and the user's input merged into the turn where they
|
|
126
|
+
# came. The step texts stay out, as live (the ticker's).
|
|
125
127
|
def render_join_steps(messages, from, durations: [])
|
|
126
128
|
records = durations.dup
|
|
127
129
|
messages.each_with_index.drop(from + 1).each do |m, i|
|
|
128
130
|
if Steer.steer?(m)
|
|
129
131
|
@screen.commit(format_steer_line(source: m[:source], text: m[:content]))
|
|
132
|
+
elsif Steer.input?(m) && !Steer.turn_prompt?(m)
|
|
133
|
+
@screen.commit("input> #{m[:content]}")
|
|
130
134
|
elsif %w[model assistant].include?(m[:role].to_s)
|
|
131
135
|
responses = messages.drop(i + 1).take_while { |r| r[:role].to_s == "tool_response" }
|
|
132
136
|
Array(MessageParts.for_message(m, responses)&.dig(:tools)).each do |part|
|
|
@@ -209,13 +213,13 @@ module Samagotchi
|
|
|
209
213
|
format_tool_guardrail_suffix({ decided_by: "system one", p: saved["p"] || saved[:p] })
|
|
210
214
|
end
|
|
211
215
|
|
|
212
|
-
# The context notes that came since the last prompt (
|
|
213
|
-
# session with none), after the
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return if after_exchange != !
|
|
216
|
+
# The context notes that came since the last prompt (+from+, its index
|
|
217
|
+
# in +messages+; all of them in a session with none), after the
|
|
218
|
+
# exchange the header shows.
|
|
219
|
+
def render_join_notes(messages, from, after_exchange: false)
|
|
220
|
+
return if after_exchange != !from.nil?
|
|
217
221
|
|
|
218
|
-
messages[(
|
|
222
|
+
messages[(from ? from + 1 : 0)..].select { |m| ContextNote.note?(m) }.each do |m|
|
|
219
223
|
@screen.commit(context_note_line(ContextNote.label_of(m), ContextNote.text_of(m)))
|
|
220
224
|
end
|
|
221
225
|
end
|
|
@@ -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
|