samagotchi 0.156.0 → 0.158.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 +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../log"
|
|
7
|
+
require_relative "../message_time"
|
|
8
|
+
require_relative "../steer"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
class Bridge
|
|
12
|
+
# A finished turn as live drew it (its drawing), one JSON line per turn in
|
|
13
|
+
# the session's folder (FILE), so a reload or a reconnect draws the turn
|
|
14
|
+
# the way the live page did: the TurnAccumulator's final fold of the turn
|
|
15
|
+
# (its parts in order: steps, rows, notices, steers, merged lines,
|
|
16
|
+
# reminders, in-turn cards) and the end event's display keys (END_KEYS).
|
|
17
|
+
# Display-side only: no request, message, strategy, recap, --resume or
|
|
18
|
+
# continue reads it.
|
|
19
|
+
#
|
|
20
|
+
# A line names the turn's own messages by the range the Engine put on
|
|
21
|
+
# the end event (`kept_from`/`kept_to`: the turn's messages are
|
|
22
|
+
# `kept[from...to]`) and digests both ends (#digest). It holds while the
|
|
23
|
+
# messages still reach `to` and both ends digest the same: a rollback, a
|
|
24
|
+
# rolled-back failure or a prompt handed back drops it. Of two valid
|
|
25
|
+
# lines that overlap (a rollback, then a re-run that said the same at the
|
|
26
|
+
# same boundaries), and of several for one turn id, the newest wins
|
|
27
|
+
# (.valid). A turn with no range (an older worker, a turn that kept
|
|
28
|
+
# nothing) or an empty one writes no line, and draws as before.
|
|
29
|
+
#
|
|
30
|
+
# Written by the accumulator's on_turn_end hook (#append), inside the
|
|
31
|
+
# SessionObserver's delivery: the line is on disk before any stream gets
|
|
32
|
+
# the end frame. That departs from SessionObserver's "enqueue-only" note,
|
|
33
|
+
# as CardStore's save does: one append per turn end.
|
|
34
|
+
class TurnDrawings
|
|
35
|
+
FILE = "turn_drawings.jsonl"
|
|
36
|
+
# In the sidecar's features (Bridge::FEATURES): this worker's CardStore
|
|
37
|
+
# lists an in-turn entry's of_turn, which a page drawing the turn needs
|
|
38
|
+
# to leave that entry to the drawing. chi web draws no drawings for a
|
|
39
|
+
# live worker without it (an older one: its rows would show twice).
|
|
40
|
+
FEATURE = "turn_drawings"
|
|
41
|
+
# A show draws at most this many turns from their drawings, the newest
|
|
42
|
+
# (the payload and the page's offscreen render stay bounded in a long
|
|
43
|
+
# session); older turns draw as before, from their messages.
|
|
44
|
+
SHOWN_MAX = 100
|
|
45
|
+
VERSION = 1
|
|
46
|
+
# The end event's keys the page draws a finished turn's end from (not
|
|
47
|
+
# the answer: its text, html and refs stay the message's; not the range:
|
|
48
|
+
# the line's from/to).
|
|
49
|
+
END_KEYS = %i[type turn_id duration_ms revises verify cancellation_reason cancelled_by summary message error_class
|
|
50
|
+
kept_steps].freeze
|
|
51
|
+
# What a line keeps of the accumulator's turn.
|
|
52
|
+
DRAWING_KEYS = (%i[prompt turn_id origin continue at images parts] + Steer::AUTHOR_KEYS).uniq.freeze
|
|
53
|
+
KINDS = { "turn_completed" => "completed", "turn_canceled" => "canceled", "turn_failed" => "failed" }.freeze
|
|
54
|
+
|
|
55
|
+
# One line of the file (string-keyed drawing and end, as JSON has them).
|
|
56
|
+
Line = Data.define(:v, :turn_id, :kind, :from, :to, :from_digest, :to_digest, :drawing, :end) do
|
|
57
|
+
# @return [Line, nil] nil for a line this chi can't draw (another
|
|
58
|
+
# version, a missing field, parts that aren't rows: .rows?)
|
|
59
|
+
def self.from_h(raw)
|
|
60
|
+
return nil unless raw.is_a?(Hash) && raw["v"] == VERSION
|
|
61
|
+
return nil unless raw["turn_id"].is_a?(String) && !raw["turn_id"].empty?
|
|
62
|
+
return nil unless raw["from"].is_a?(Integer) && raw["to"].is_a?(Integer) && raw["from"] < raw["to"] && raw["from"] >= 0
|
|
63
|
+
return nil unless raw["from_digest"].is_a?(String) && raw["to_digest"].is_a?(String)
|
|
64
|
+
return nil unless raw["drawing"].is_a?(Hash) && raw["end"].is_a?(Hash)
|
|
65
|
+
return nil unless TurnDrawings.rows?(raw["drawing"]["parts"])
|
|
66
|
+
|
|
67
|
+
new(v: raw["v"], turn_id: raw["turn_id"], kind: raw["kind"].to_s, from: raw["from"], to: raw["to"],
|
|
68
|
+
from_digest: raw["from_digest"], to_digest: raw["to_digest"], drawing: raw["drawing"], end: raw["end"])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_h = super.transform_keys(&:to_s)
|
|
72
|
+
|
|
73
|
+
def range = (from...to)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param path [String, nil] the file (nil: nothing is written)
|
|
77
|
+
# @param messages [#call] the session's messages now (the Engine's,
|
|
78
|
+
# as CardStore's): the end step replaced them before the end event, so
|
|
79
|
+
# they hold the turn's range when #append runs
|
|
80
|
+
def initialize(path:, messages:)
|
|
81
|
+
@path = path
|
|
82
|
+
@messages = messages
|
|
83
|
+
@last = path ? self.class.tail_line(path) : nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The newest line this store wrote or started from (P3's snapshot).
|
|
87
|
+
# @return [Line, nil]
|
|
88
|
+
attr_reader :last
|
|
89
|
+
|
|
90
|
+
# The accumulator's hook: the turn's line, when its end event names a
|
|
91
|
+
# range. Never raises (a turn that can't be drawn still ended).
|
|
92
|
+
# @param turn [Hash] the accumulator's final fold (symbol keys)
|
|
93
|
+
# @param end_event [Hash] the end event
|
|
94
|
+
def append(turn, end_event)
|
|
95
|
+
line = line_for(turn, end_event)
|
|
96
|
+
return unless line
|
|
97
|
+
|
|
98
|
+
write(line)
|
|
99
|
+
@last = line
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
Log.warn(:bridge, "turn_drawing_failed", error: "#{e.class}: #{e.message}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def line_for(turn, end_event)
|
|
105
|
+
from = end_event[:kept_from]
|
|
106
|
+
to = end_event[:kept_to]
|
|
107
|
+
return nil unless @path && turn && from.is_a?(Integer) && to.is_a?(Integer) && from < to && from >= 0
|
|
108
|
+
|
|
109
|
+
messages = @messages.call || []
|
|
110
|
+
return nil if messages.size < to
|
|
111
|
+
|
|
112
|
+
id = end_event[:turn_id] || turn[:turn_id]
|
|
113
|
+
return nil unless id
|
|
114
|
+
|
|
115
|
+
Line.new(v: VERSION, turn_id: id.to_s, kind: KINDS[end_event[:type].to_s] || end_event[:type].to_s, from: from, to: to,
|
|
116
|
+
from_digest: self.class.digest(messages[from]), to_digest: self.class.digest(messages[to - 1]),
|
|
117
|
+
drawing: JSON.parse(JSON.generate(scrubbed(turn.slice(*DRAWING_KEYS)))),
|
|
118
|
+
end: JSON.parse(JSON.generate(scrubbed(self.class.end_of(end_event)))))
|
|
119
|
+
end
|
|
120
|
+
private :line_for
|
|
121
|
+
|
|
122
|
+
# Newline-first: a torn last line (a crash mid-write) ends before this
|
|
123
|
+
# one starts, so both parse apart; then one write.
|
|
124
|
+
def write(line)
|
|
125
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
126
|
+
File.open(@path, File::RDWR | File::CREAT | File::APPEND, 0o600) do |file|
|
|
127
|
+
size = file.size
|
|
128
|
+
torn = size.positive? && (file.pread(1, size - 1) != "\n")
|
|
129
|
+
file.write("#{"\n" if torn}#{JSON.generate(line.to_h)}\n")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
private :write
|
|
133
|
+
|
|
134
|
+
def scrubbed(value) = self.class.scrub(value)
|
|
135
|
+
private :scrubbed
|
|
136
|
+
|
|
137
|
+
class << self
|
|
138
|
+
# Whether a drawing's +parts+ are rows TurnAccumulator.replay_events
|
|
139
|
+
# can unfold: none, or a list of Hashes (a hand-edited line's may be
|
|
140
|
+
# anything, and the unfold would raise).
|
|
141
|
+
def rows?(parts) = parts.nil? || (parts.is_a?(Array) && parts.all?(Hash))
|
|
142
|
+
|
|
143
|
+
# The end event's display keys (END_KEYS), with an empty answer's
|
|
144
|
+
# notice from its summary.
|
|
145
|
+
# @return [Hash]
|
|
146
|
+
def end_of(event)
|
|
147
|
+
out = event.slice(*END_KEYS).compact
|
|
148
|
+
out[:type] = out[:type].to_s if out[:type]
|
|
149
|
+
empty = event.dig(:turn_summary, :empty_answer)
|
|
150
|
+
out[:turn_summary] = { empty_answer: empty } if empty
|
|
151
|
+
out
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A message's digest in one form for the live (symbol keys, raw
|
|
155
|
+
# strings) and the saved (string keys, scrubbed as Session#to_h does)
|
|
156
|
+
# messages: role, content and time, each read from either key.
|
|
157
|
+
# @return [String]
|
|
158
|
+
def digest(message)
|
|
159
|
+
return "" unless message.is_a?(Hash)
|
|
160
|
+
|
|
161
|
+
role = fetch(message, :role)
|
|
162
|
+
time = fetch(message, MessageTime::KEY)
|
|
163
|
+
Digest::SHA1.hexdigest(JSON.generate(scrub([role&.to_s, fetch(message, :content), time&.to_s])))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Every String in +value+ as Session#to_h saves it (UTF-8, invalid
|
|
167
|
+
# bytes as "?"); symbol keys stay symbols (JSON writes them alike).
|
|
168
|
+
def scrub(value)
|
|
169
|
+
case value
|
|
170
|
+
when String
|
|
171
|
+
text = value.encoding == Encoding::UTF_8 ? value : value.dup.force_encoding(Encoding::UTF_8)
|
|
172
|
+
text.valid_encoding? ? text : text.scrub("?")
|
|
173
|
+
when Array then value.map { |item| scrub(item) }
|
|
174
|
+
when Hash then value.to_h { |key, item| [key, scrub(item)] }
|
|
175
|
+
when Symbol then value.to_s
|
|
176
|
+
else value
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The session folder's lines, oldest first (unreadable, torn and
|
|
181
|
+
# unknown lines skipped), cached by the file's path, size and mtime
|
|
182
|
+
# (chi web reads it on each full show).
|
|
183
|
+
# @return [Array<Line>]
|
|
184
|
+
def read(session_dir)
|
|
185
|
+
path = File.join(session_dir, FILE)
|
|
186
|
+
stat = File.stat(path)
|
|
187
|
+
key = [stat.size, stat.mtime.to_r]
|
|
188
|
+
cached = cache_mutex.synchronize { cache[path] }
|
|
189
|
+
return cached[1] if cached && cached[0] == key
|
|
190
|
+
|
|
191
|
+
lines = parse(File.binread(path))
|
|
192
|
+
cache_mutex.synchronize do
|
|
193
|
+
cache.shift while cache.size >= CACHE_SIZE
|
|
194
|
+
cache[path] = [key, lines]
|
|
195
|
+
end
|
|
196
|
+
lines
|
|
197
|
+
rescue SystemCallError, IOError
|
|
198
|
+
[]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# @return [Array<Line>]
|
|
202
|
+
def parse(text)
|
|
203
|
+
text.dup.force_encoding(Encoding::UTF_8).scrub("?").each_line.filter_map do |raw|
|
|
204
|
+
Line.from_h(JSON.parse(raw))
|
|
205
|
+
rescue JSON::ParserError
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The lines that hold against +messages+ (Session messages, symbol
|
|
211
|
+
# or string keys): each one's range still in them with both ends the
|
|
212
|
+
# same; of overlapping ranges and of one turn's lines the newest
|
|
213
|
+
# (last in the file) wins. Ordered by their range.
|
|
214
|
+
# @return [Array<Line>]
|
|
215
|
+
def valid(lines, messages)
|
|
216
|
+
messages = Array(messages)
|
|
217
|
+
kept = []
|
|
218
|
+
lines.reverse_each do |line|
|
|
219
|
+
next unless holds?(line, messages)
|
|
220
|
+
next if kept.any? { |other| other.turn_id == line.turn_id || overlap?(other, line) }
|
|
221
|
+
|
|
222
|
+
kept << line
|
|
223
|
+
end
|
|
224
|
+
kept.sort_by(&:from)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The last parseable line of +path+, or nil.
|
|
228
|
+
# @return [Line, nil]
|
|
229
|
+
def tail_line(path)
|
|
230
|
+
return nil unless File.file?(path)
|
|
231
|
+
|
|
232
|
+
File.open(path, "rb") do |file|
|
|
233
|
+
size = file.size
|
|
234
|
+
chunk = file.pread([size, TAIL_BYTES].min, [size - TAIL_BYTES, 0].max)
|
|
235
|
+
parse(chunk.to_s).last
|
|
236
|
+
end
|
|
237
|
+
rescue SystemCallError, IOError
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Whether +line+ is small enough to send in a snapshot: within what
|
|
242
|
+
# #tail_line reads back after a restart (TAIL_BYTES), so a join draws
|
|
243
|
+
# a turn the same before and after one (a bigger line: rebuilt).
|
|
244
|
+
def sendable?(line) = JSON.generate(line.to_h).bytesize <= TAIL_BYTES
|
|
245
|
+
|
|
246
|
+
def holds?(line, messages)
|
|
247
|
+
messages.size >= line.to && digest(messages[line.from]) == line.from_digest &&
|
|
248
|
+
digest(messages[line.to - 1]) == line.to_digest
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def overlap?(one, other) = one.from < other.to && other.from < one.to
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def fetch(message, key)
|
|
256
|
+
message.key?(key) ? message[key] : message[key.to_s]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def cache = (@cache ||= {})
|
|
260
|
+
def cache_mutex = (@cache_mutex ||= Mutex.new)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Sessions whose parsed file chi web keeps (a long one's is tens of MB
|
|
264
|
+
# of objects; the mtime moves at every turn end anyway).
|
|
265
|
+
CACHE_SIZE = 8
|
|
266
|
+
# Read from the end at start (#last): a drawing is a few KB, its outputs cut.
|
|
267
|
+
TAIL_BYTES = 1024 * 1024
|
|
268
|
+
private_constant :CACHE_SIZE, :TAIL_BYTES
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "bridge/pending_card"
|
|
|
21
21
|
require_relative "bridge/ring_buffer"
|
|
22
22
|
require_relative "bridge/sse_writer"
|
|
23
23
|
require_relative "bridge/turn_accumulator"
|
|
24
|
+
require_relative "bridge/turn_drawings"
|
|
24
25
|
require_relative "session"
|
|
25
26
|
require_relative "engine"
|
|
26
27
|
require_relative "system_one/log"
|
|
@@ -134,7 +135,10 @@ module Samagotchi
|
|
|
134
135
|
@bind = bind
|
|
135
136
|
@port = port
|
|
136
137
|
@ring = RingBuffer.new(capacity: ring_capacity)
|
|
137
|
-
|
|
138
|
+
# A finished turn's drawing (TurnDrawings), written by the accumulator
|
|
139
|
+
# at the end event, before any stream gets it.
|
|
140
|
+
@drawings = TurnDrawings.new(path: File.join(session_dir, TurnDrawings::FILE), messages: -> { @engine.session&.messages })
|
|
141
|
+
@accumulator = TurnAccumulator.new(on_turn_end: @drawings.method(:append))
|
|
138
142
|
# Saved in the session's folder: a later worker, and the web without
|
|
139
143
|
# one, show the cards where they were.
|
|
140
144
|
@cards = CardStore.new(path: File.join(session_dir, CardStore::FILE), messages: -> { @engine.session&.messages })
|
|
@@ -300,7 +304,7 @@ module Samagotchi
|
|
|
300
304
|
# the event log: the Engine's messages (not the lagging copy on disk),
|
|
301
305
|
# the turn in progress, turns queued behind it, the idle recap since the
|
|
302
306
|
# last turn, the recap saved with the session (also from before the last
|
|
303
|
-
# turns: {text:, covered:, turns_since:, created_at:}), a pending continue offer, the guardrail and plugin load warnings,
|
|
307
|
+
# turns: {text:, covered:, turns_since:, opening_id:, created_at:}), a pending continue offer, the guardrail and plugin load warnings,
|
|
304
308
|
# the plugins' init tasks still running (Engine#init_tasks), the
|
|
305
309
|
# last cards and between-turns notices (CardStore#list), the session's commands (Commands::Registry#listing:
|
|
306
310
|
# what an attached TUI routes and completes, the web's autocomplete), and the event_seq it all covers.
|
|
@@ -315,13 +319,20 @@ module Samagotchi
|
|
|
315
319
|
# working_set: the running turn's last :working_set_updated ({items:,
|
|
316
320
|
# at:}, TurnAccumulator#working_set), which the messages lag; nil
|
|
317
321
|
# between turns and before the turn emitted one.
|
|
322
|
+
# last_turn_drawing: the last exchange's turn as live drew it
|
|
323
|
+
# ({drawing:, end:}, TurnDrawings#last, not unfolded: an attaching TUI
|
|
324
|
+
# unfolds it, TurnAccumulator.replay_events), only while it holds against
|
|
325
|
+
# these messages and ends at their end; absent otherwise (a rollback, a
|
|
326
|
+
# later Continue or note, an older worker's file).
|
|
318
327
|
# @return [Hash] {messages:, current_turn:, working_set:, queued:, queued_commands:, recap:, saved_recap:, continue_offer:, pending_question:,
|
|
319
|
-
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:}
|
|
328
|
+
# guardrail_warning:, plugin_warning:, init_tasks:, cards:, commands:, chi_version:, event_seq:, event_id:, last_turn_drawing:}
|
|
320
329
|
def snapshot
|
|
321
330
|
@engine.synchronize_events do
|
|
322
331
|
seq = @engine.event_count
|
|
332
|
+
messages = @engine.messages_checkpoint
|
|
333
|
+
drawing = last_turn_drawing(messages)
|
|
323
334
|
{
|
|
324
|
-
messages:
|
|
335
|
+
messages: messages,
|
|
325
336
|
current_turn: current_turn_with_generation,
|
|
326
337
|
working_set: @accumulator.working_set,
|
|
327
338
|
queued: @accumulator.queued,
|
|
@@ -338,10 +349,24 @@ module Samagotchi
|
|
|
338
349
|
chi_version: Samagotchi::VERSION,
|
|
339
350
|
event_seq: seq,
|
|
340
351
|
event_id: event_id(seq)
|
|
341
|
-
}
|
|
352
|
+
}.merge(drawing ? { last_turn_drawing: drawing } : {})
|
|
342
353
|
end
|
|
343
354
|
end
|
|
344
355
|
|
|
356
|
+
# The newest drawing (TurnDrawings#last) as the snapshot sends it, when
|
|
357
|
+
# it is the last exchange's: its range still holds against +messages+
|
|
358
|
+
# and ends at their end, and is no bigger than a restart reads back
|
|
359
|
+
# (TurnDrawings.sendable?). nil otherwise.
|
|
360
|
+
# @return [Hash, nil] {drawing:, end:} (string keys inside, as the file)
|
|
361
|
+
def last_turn_drawing(messages)
|
|
362
|
+
line = @drawings.last
|
|
363
|
+
return nil unless line && line.to == messages.size && TurnDrawings.holds?(line, messages)
|
|
364
|
+
return nil unless TurnDrawings.sendable?(line)
|
|
365
|
+
|
|
366
|
+
{ drawing: line.drawing, end: line.end }
|
|
367
|
+
end
|
|
368
|
+
private :last_turn_drawing
|
|
369
|
+
|
|
345
370
|
# The snapshot and the session state (status, pending question, ...) as
|
|
346
371
|
# one step of the event log, both as of the same event_seq: what a
|
|
347
372
|
# snapshot or reset frame (SSEWriter) and GET snapshot carry.
|
|
@@ -664,7 +689,7 @@ module Samagotchi
|
|
|
664
689
|
# review turn, and chi web says so (Delivery::PAIR_FEATURE).
|
|
665
690
|
FEATURES = ["restart", "task_stop", "reparent", Delivery::QUEUE_FEATURE, ReplyWait::TURN_IDS_FEATURE,
|
|
666
691
|
VerifyAnchor::FEATURE, ReviseAnchor::FEATURE,
|
|
667
|
-
SessionInbox::UNQUEUE_FEATURE, Delivery::REVIEW_FEATURE, Delivery::PAIR_FEATURE].freeze
|
|
692
|
+
SessionInbox::UNQUEUE_FEATURE, Delivery::REVIEW_FEATURE, Delivery::PAIR_FEATURE, TurnDrawings::FEATURE].freeze
|
|
668
693
|
|
|
669
694
|
# The routes, by method and what follows /session/:id/: a handler takes
|
|
670
695
|
# the session id and the request body and returns [headers, status, body].
|
|
@@ -29,12 +29,12 @@ module Samagotchi
|
|
|
29
29
|
# @return [Array<Recipient>] newest first
|
|
30
30
|
def list(state_dir:, active_hours:, include_tests: Session.test_session_env?, now: Time.now)
|
|
31
31
|
cutoff = now - (active_hours.to_f * 3600)
|
|
32
|
-
SessionManager.
|
|
33
|
-
next if row
|
|
34
|
-
next unless row
|
|
32
|
+
SessionManager.session_rows(include_tests: include_tests, state_dir: state_dir).filter_map do |row|
|
|
33
|
+
next if row.delegate || row.scratch
|
|
34
|
+
next unless row.live || row.owner == "tui" || active_since?(row, cutoff, state_dir)
|
|
35
35
|
|
|
36
|
-
Recipient.new(id: row
|
|
37
|
-
desc: row
|
|
36
|
+
Recipient.new(id: row.id, short_id: row.short_id, project: row.project, cwd: row.cwd,
|
|
37
|
+
desc: row.desc, live: row.live, owner: row.owner)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -43,11 +43,11 @@ module Samagotchi
|
|
|
43
43
|
# session with no turn recorded; it is never older than the last
|
|
44
44
|
# turn's end, so an older one rules the session out unread.
|
|
45
45
|
def active_since?(row, cutoff, state_dir)
|
|
46
|
-
return false unless (time(row
|
|
46
|
+
return false unless (time(row.updated_at) || Time.at(0)) >= cutoff
|
|
47
47
|
|
|
48
|
-
session = Session.summary_from_file(Session.session_file(row
|
|
48
|
+
session = Session.summary_from_file(Session.session_file(row.id, state_dir: state_dir))
|
|
49
49
|
ended = time(session&.last_turn&.ended_at)
|
|
50
|
-
(ended || time(row
|
|
50
|
+
(ended || time(row.updated_at)) >= cutoff
|
|
51
51
|
end
|
|
52
52
|
private_class_method :active_since?
|
|
53
53
|
|
|
@@ -29,7 +29,7 @@ module Samagotchi
|
|
|
29
29
|
# @param get [#call] a Config key → its value
|
|
30
30
|
# @return [Choice]
|
|
31
31
|
def resolve(host_registry: nil, get: ->(key) { Config.get(key) })
|
|
32
|
-
registry = host_registry || HostRegistry.
|
|
32
|
+
registry = host_registry || HostRegistry.current
|
|
33
33
|
SETTINGS.each do |model, host_ref, base_url|
|
|
34
34
|
target = IdleTarget.resolve(model: get.call(model), host_ref: get.call(host_ref), base_url: get.call(base_url),
|
|
35
35
|
host_registry: registry)
|
|
@@ -21,7 +21,7 @@ module Samagotchi
|
|
|
21
21
|
#
|
|
22
22
|
# It installs and loads the bundle in a Testing::Sandbox (temp HOME and
|
|
23
23
|
# XDG dirs), not in the user's config or state, but in this process: the
|
|
24
|
-
# plugin's
|
|
24
|
+
# plugin's own code runs as written; --fix rewrites stale
|
|
25
25
|
# sha256 lines (MemoryBundle::Shas). #run returns 0 without errors, 1
|
|
26
26
|
# with any.
|
|
27
27
|
#
|
|
@@ -34,8 +34,8 @@ module Samagotchi
|
|
|
34
34
|
|
|
35
35
|
Check the bundle at PATH (default: .) as an install would take it, one finding per line:
|
|
36
36
|
error|warn|info CODE file[:line]: message (fix: hint)
|
|
37
|
-
It installs and loads the bundle in a sandbox (temp HOME, config and state), not in yours, but its plugin
|
|
38
|
-
|
|
37
|
+
It installs and loads the bundle in a sandbox (temp HOME, config and state), not in yours, but its plugin
|
|
38
|
+
runs in this process as written: run it on code you'd install.
|
|
39
39
|
--fix rewrite stale and placeholder sha256 lines in manifest.yml
|
|
40
40
|
--set KEY=VALUE a setting for the load (config.yml bundles: <name>: KEY), repeatable
|
|
41
41
|
Exit status: 0 no errors, 1 errors, 2 usage.
|
|
@@ -81,6 +81,7 @@ module Samagotchi
|
|
|
81
81
|
# @return [Integer] exit status
|
|
82
82
|
def run
|
|
83
83
|
manifest = read_manifest
|
|
84
|
+
manifest = nil if manifest && hooks_retired?
|
|
84
85
|
if manifest
|
|
85
86
|
missing = check_files(manifest)
|
|
86
87
|
check_source_tree(manifest, missing) unless @installed
|
|
@@ -91,6 +92,7 @@ module Samagotchi
|
|
|
91
92
|
check_settings(manifest) unless @installed
|
|
92
93
|
check_install(manifest) unless missing || placeholders? || manifest.session_settings_clash
|
|
93
94
|
check_api(manifest)
|
|
95
|
+
check_fail_closed(manifest) unless @installed
|
|
94
96
|
check_boundary(manifest)
|
|
95
97
|
check_tests unless @installed
|
|
96
98
|
end
|
|
@@ -135,12 +137,27 @@ module Samagotchi
|
|
|
135
137
|
Dir.mktmpdir("chi-bundle-check-") do |tmp|
|
|
136
138
|
File.write(File.join(tmp, "manifest.yml"), MemoryBundle::Shas.parseable(@text))
|
|
137
139
|
MemoryBundle::Manifest.read(dir: tmp)
|
|
140
|
+
rescue MemoryBundle::Manifest::HooksRetired => e
|
|
141
|
+
add(:error, :"hooks-retired", "manifest.yml", e.message, line: manifest_line("hooks"), fix: hooks_fix)
|
|
142
|
+
nil
|
|
138
143
|
rescue MemoryBundle::Manifest::ValidationError => e
|
|
139
144
|
add(:error, :manifest, "manifest.yml", e.message.gsub(tmp, display(@dir)))
|
|
140
145
|
nil
|
|
141
146
|
end
|
|
142
147
|
end
|
|
143
148
|
|
|
149
|
+
# A source with hooks/*.rb (listed or not) can't be installed: the run
|
|
150
|
+
# stops there, as after a manifest error (Installer#refuse_hooks).
|
|
151
|
+
def hooks_retired?
|
|
152
|
+
files = MemoryBundle::Manifest::HooksRetired.files_in(@dir)
|
|
153
|
+
return false if files.empty?
|
|
154
|
+
|
|
155
|
+
add(:error, :"hooks-retired", "hooks/", MemoryBundle::Manifest::HooksRetired.for_files(files).message, fix: hooks_fix)
|
|
156
|
+
true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def hooks_fix = "hook the events from plugin.rb with chi.on, then remove hooks/ and the manifest's hooks:"
|
|
160
|
+
|
|
144
161
|
# The line of manifest.yml naming +file+ (its entry), or nil.
|
|
145
162
|
def manifest_line(file)
|
|
146
163
|
base = File.basename(file)
|
|
@@ -191,7 +208,7 @@ module Samagotchi
|
|
|
191
208
|
# @return [Boolean] whether a listed file is missing
|
|
192
209
|
def check_files(manifest)
|
|
193
210
|
listed = manifest.files.keys + Array(manifest.plugin && manifest.plugin[:file]) +
|
|
194
|
-
manifest.
|
|
211
|
+
manifest.scripts.keys.map { |s| File.join("scripts", s) }
|
|
195
212
|
missing = listed.reject { |rel| File.file?(path(rel)) }
|
|
196
213
|
missing.each do |rel|
|
|
197
214
|
add(:error, :"missing-file", "manifest.yml", "lists #{rel}, which isn't in the bundle", line: manifest_line(rel),
|
|
@@ -201,7 +218,7 @@ module Samagotchi
|
|
|
201
218
|
end
|
|
202
219
|
|
|
203
220
|
# What an install leaves out: a memory files: doesn't list (by
|
|
204
|
-
# MemoryFiles' rules), a plugin
|
|
221
|
+
# MemoryFiles' rules), a plugin or script the manifest doesn't name.
|
|
205
222
|
def check_unlisted(manifest)
|
|
206
223
|
memories = MemoryBundle::MemoryFiles.select(@dir, manifest)
|
|
207
224
|
loose = Dir.glob(path("*.md")).map { |f| File.basename(f) }.sort.reject do |name|
|
|
@@ -210,9 +227,6 @@ module Samagotchi
|
|
|
210
227
|
loose.each { |name| unlisted(name, "files:") }
|
|
211
228
|
plugin = manifest.plugin && manifest.plugin[:file]
|
|
212
229
|
unlisted("plugin.rb", "plugin:") if File.file?(path("plugin.rb")) && plugin != "plugin.rb"
|
|
213
|
-
Dir.glob(path("hooks/*.rb")).sort.each do |file|
|
|
214
|
-
unlisted("hooks/#{File.basename(file)}", "hooks:") unless manifest.hooks.key?(File.basename(file))
|
|
215
|
-
end
|
|
216
230
|
Dir.glob(path("scripts/*")).sort.each do |file|
|
|
217
231
|
unlisted("scripts/#{File.basename(file)}", "scripts:") unless manifest.scripts.key?(File.basename(file))
|
|
218
232
|
end
|
|
@@ -250,7 +264,7 @@ module Samagotchi
|
|
|
250
264
|
|
|
251
265
|
# ── requires_chi ──────────────────────────────────────────────────
|
|
252
266
|
|
|
253
|
-
def code?(manifest) = manifest.plugin || manifest.
|
|
267
|
+
def code?(manifest) = manifest.plugin || manifest.scripts.any?
|
|
254
268
|
|
|
255
269
|
def check_requires_chi(manifest)
|
|
256
270
|
line = manifest_line("requires_chi")
|
|
@@ -413,7 +427,7 @@ module Samagotchi
|
|
|
413
427
|
registered.tools.each { |name| add(:info, :registered, plugin, "tool #{name}") }
|
|
414
428
|
registered.hooks.each do |event, hooks|
|
|
415
429
|
hooks.each do |hook|
|
|
416
|
-
file =
|
|
430
|
+
file = plugin
|
|
417
431
|
next add(:info, :registered, file, "hook #{event} (priority #{hook.priority})") if Hooks::EVENTS.include?(event)
|
|
418
432
|
|
|
419
433
|
add(:warn, :"hook-event", file, "hook on #{event.inspect}, which chi never fires",
|
|
@@ -439,6 +453,23 @@ module Samagotchi
|
|
|
439
453
|
end
|
|
440
454
|
end
|
|
441
455
|
|
|
456
|
+
# A chi.on(..., on_error: :fail_closed) guard denies the call it raises
|
|
457
|
+
# on; only the manifest's plugin: required: true makes its failing to
|
|
458
|
+
# load deny every call (chi knows it before the plugin registers).
|
|
459
|
+
FAIL_CLOSED = /on_error:\s*(?::fail_closed\b|["']fail_closed["'])/
|
|
460
|
+
|
|
461
|
+
def check_fail_closed(manifest)
|
|
462
|
+
file = manifest.plugin && manifest.plugin[:file]
|
|
463
|
+
return if !file || manifest.plugin[:required] || !File.file?(path(file))
|
|
464
|
+
|
|
465
|
+
source = File.read(path(file))
|
|
466
|
+
return unless (index = source =~ FAIL_CLOSED)
|
|
467
|
+
|
|
468
|
+
add(:warn, :"fail-closed", file, "a fail-closed chi.on hook, but the manifest's plugin: has no required: true: " \
|
|
469
|
+
"if the plugin fails to load, tool calls still run",
|
|
470
|
+
line: source[0, index].count("\n") + 1, fix: "add required: true under plugin: in manifest.yml")
|
|
471
|
+
end
|
|
472
|
+
|
|
442
473
|
def check_boundary(manifest)
|
|
443
474
|
require_relative "../testing/boundary"
|
|
444
475
|
known = shipped? ? Testing::Boundary::EXCEPTIONS.fetch(manifest.name, {}) : {}
|
|
@@ -59,8 +59,9 @@ module Samagotchi
|
|
|
59
59
|
|
|
60
60
|
# What the child sends: an edited file, which the copy's own shas can't
|
|
61
61
|
# tell, a manifest key that doesn't parse (the copy holds the mirror's
|
|
62
|
-
# value or none: InstalledManifest#key_problems),
|
|
63
|
-
#
|
|
62
|
+
# value or none: InstalledManifest#key_problems), the class-file hooks
|
|
63
|
+
# the record still has (they don't load), and Check's warnings and
|
|
64
|
+
# errors, naming the installed bundle's folder rather than the copy's.
|
|
64
65
|
def answer_of(bundle)
|
|
65
66
|
edited = MemoryBundle::ShippedUpdate.edited_executables(bundle.name, bundle).map do |file|
|
|
66
67
|
Check::Finding.new(level: :warn, code: :edited, file: file, line: nil, fix: "chi bundle upgrade #{bundle.name}, or reinstall it",
|
|
@@ -69,6 +70,7 @@ module Samagotchi
|
|
|
69
70
|
edited += bundle.manifest.key_problems.values.map do |message|
|
|
70
71
|
Check::Finding.new(level: :error, code: :manifest, file: "manifest.yml", line: nil, message: message, fix: nil)
|
|
71
72
|
end
|
|
73
|
+
edited << retired_hooks(bundle) unless bundle.hooks.empty?
|
|
72
74
|
edited = edited.map { |finding| { level: finding.level, text: finding.to_s } }
|
|
73
75
|
installed_dir = MemoryBundle::Provenance.new(name: bundle.name).bundle_dir
|
|
74
76
|
edited + MemoryBundle::InstalledCopy.with(bundle) do |dir|
|
|
@@ -80,6 +82,15 @@ module Samagotchi
|
|
|
80
82
|
end
|
|
81
83
|
private_class_method :answer_of
|
|
82
84
|
|
|
85
|
+
# A 0.157 record's class-file hooks: they no longer load, and only an
|
|
86
|
+
# upgrade from a source that has moved them (or an uninstall) clears it.
|
|
87
|
+
def retired_hooks(bundle)
|
|
88
|
+
Check::Finding.new(level: :warn, code: :"hooks-retired", file: "hooks/", line: nil,
|
|
89
|
+
message: MemoryBundle::Manifest::HooksRetired::WORDS,
|
|
90
|
+
fix: "chi bundle upgrade #{bundle.name} once its source hooks from plugin.rb, or chi bundle uninstall #{bundle.name}")
|
|
91
|
+
end
|
|
92
|
+
private_class_method :retired_hooks
|
|
93
|
+
|
|
83
94
|
def died(why, prefix: true) = Found.new(level: "error", text: check_line(prefix ? "the check died: #{why}" : why))
|
|
84
95
|
private_class_method :died
|
|
85
96
|
|
|
@@ -163,7 +163,10 @@ module Samagotchi
|
|
|
163
163
|
reader.join(0.1) if reader.alive?
|
|
164
164
|
reader.kill if reader.alive?
|
|
165
165
|
end
|
|
166
|
-
|
|
166
|
+
# readpartial gives raw bytes: the child writes UTF-8 (the answer,
|
|
167
|
+
# the trace), so each stream is read as that, a bad byte as "?",
|
|
168
|
+
# here once, for the text and the --json report alike.
|
|
169
|
+
readers.map { |reader| reader.value.to_s.dup.force_encoding(Encoding::UTF_8).scrub("?") }
|
|
167
170
|
end
|
|
168
171
|
|
|
169
172
|
def close_streams(streams)
|
|
@@ -186,7 +186,7 @@ module Samagotchi
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
def leftover_workers
|
|
189
|
-
sessions = File.join(state_home,
|
|
189
|
+
sessions = File.join(state_home, SessionPaths::STATE_SUBDIR)
|
|
190
190
|
Dir.glob(File.join(sessions, "*", OwnerLock::FILE)).filter_map do |lock|
|
|
191
191
|
pid = OwnerLock.owner(File.dirname(lock))&.pid
|
|
192
192
|
pid if ProcessGroup.leader?(pid)
|
|
@@ -28,7 +28,7 @@ module Samagotchi
|
|
|
28
28
|
def read(exit_code:, answer:, stderr:, sandbox:, timed_out: false)
|
|
29
29
|
return empty_trace(exit_code, answer, stderr, sandbox, timed_out) unless @session_id
|
|
30
30
|
|
|
31
|
-
sessions_dir = File.join(@state_home,
|
|
31
|
+
sessions_dir = File.join(@state_home, SessionPaths::STATE_SUBDIR)
|
|
32
32
|
session_dir = Session.session_dir(@session_id, state_dir: sessions_dir)
|
|
33
33
|
session = json_file(Session.session_file(@session_id, state_dir: sessions_dir)) || {}
|
|
34
34
|
messages = Array(session["messages"]).select { |message| message.is_a?(Hash) }
|