samagotchi 0.156.0 → 0.157.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 +44 -1
- data/bin/chi +1 -0
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +3 -1
- data/docs/cli.md +9 -8
- data/docs/configuration.md +36 -4
- data/docs/guardrails.md +9 -5
- data/docs/hooks.md +11 -5
- data/docs/internals/bridge.md +83 -6
- data/docs/internals/bundles.md +28 -18
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +16 -2
- 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 +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +9 -9
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +53 -2
- 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 +10 -4
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
- data/lib/samagotchi/bridge/turn_drawings.rb +265 -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 +18 -0
- 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 +5 -2
- 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/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.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_sources.rb +5 -4
- 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 +29 -3
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails_command.rb +1 -1
- 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 +71 -76
- data/lib/samagotchi/memory_bundle/builder.rb +2 -1
- 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_copy.rb +2 -1
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
- data/lib/samagotchi/memory_bundle/installer.rb +185 -169
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- 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 +15 -1
- data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
- data/lib/samagotchi/memory_bundle/status.rb +14 -20
- data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
- data/lib/samagotchi/memory_bundle/trash.rb +23 -8
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- 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 +41 -6
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -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/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 +321 -439
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +133 -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_cut_after.rb +7 -14
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/tools/delegate.rb +48 -43
- 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 +31 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- 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/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 +20 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -0,0 +1,265 @@
|
|
|
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)
|
|
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
|
+
|
|
66
|
+
new(v: raw["v"], turn_id: raw["turn_id"], kind: raw["kind"].to_s, from: raw["from"], to: raw["to"],
|
|
67
|
+
from_digest: raw["from_digest"], to_digest: raw["to_digest"], drawing: raw["drawing"], end: raw["end"])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h = super.transform_keys(&:to_s)
|
|
71
|
+
|
|
72
|
+
def range = (from...to)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param path [String, nil] the file (nil: nothing is written)
|
|
76
|
+
# @param messages [#call] the session's messages now (the Engine's,
|
|
77
|
+
# as CardStore's): the end step replaced them before the end event, so
|
|
78
|
+
# they hold the turn's range when #append runs
|
|
79
|
+
def initialize(path:, messages:)
|
|
80
|
+
@path = path
|
|
81
|
+
@messages = messages
|
|
82
|
+
@last = path ? self.class.tail_line(path) : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The newest line this store wrote or started from (P3's snapshot).
|
|
86
|
+
# @return [Line, nil]
|
|
87
|
+
attr_reader :last
|
|
88
|
+
|
|
89
|
+
# The accumulator's hook: the turn's line, when its end event names a
|
|
90
|
+
# range. Never raises (a turn that can't be drawn still ended).
|
|
91
|
+
# @param turn [Hash] the accumulator's final fold (symbol keys)
|
|
92
|
+
# @param end_event [Hash] the end event
|
|
93
|
+
def append(turn, end_event)
|
|
94
|
+
line = line_for(turn, end_event)
|
|
95
|
+
return unless line
|
|
96
|
+
|
|
97
|
+
write(line)
|
|
98
|
+
@last = line
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Log.warn(:bridge, "turn_drawing_failed", error: "#{e.class}: #{e.message}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def line_for(turn, end_event)
|
|
104
|
+
from = end_event[:kept_from]
|
|
105
|
+
to = end_event[:kept_to]
|
|
106
|
+
return nil unless @path && turn && from.is_a?(Integer) && to.is_a?(Integer) && from < to && from >= 0
|
|
107
|
+
|
|
108
|
+
messages = @messages.call || []
|
|
109
|
+
return nil if messages.size < to
|
|
110
|
+
|
|
111
|
+
id = end_event[:turn_id] || turn[:turn_id]
|
|
112
|
+
return nil unless id
|
|
113
|
+
|
|
114
|
+
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,
|
|
115
|
+
from_digest: self.class.digest(messages[from]), to_digest: self.class.digest(messages[to - 1]),
|
|
116
|
+
drawing: JSON.parse(JSON.generate(scrubbed(turn.slice(*DRAWING_KEYS)))),
|
|
117
|
+
end: JSON.parse(JSON.generate(scrubbed(self.class.end_of(end_event)))))
|
|
118
|
+
end
|
|
119
|
+
private :line_for
|
|
120
|
+
|
|
121
|
+
# Newline-first: a torn last line (a crash mid-write) ends before this
|
|
122
|
+
# one starts, so both parse apart; then one write.
|
|
123
|
+
def write(line)
|
|
124
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
125
|
+
File.open(@path, File::RDWR | File::CREAT | File::APPEND, 0o600) do |file|
|
|
126
|
+
size = file.size
|
|
127
|
+
torn = size.positive? && (file.pread(1, size - 1) != "\n")
|
|
128
|
+
file.write("#{"\n" if torn}#{JSON.generate(line.to_h)}\n")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
private :write
|
|
132
|
+
|
|
133
|
+
def scrubbed(value) = self.class.scrub(value)
|
|
134
|
+
private :scrubbed
|
|
135
|
+
|
|
136
|
+
class << self
|
|
137
|
+
# The end event's display keys (END_KEYS), with an empty answer's
|
|
138
|
+
# notice from its summary.
|
|
139
|
+
# @return [Hash]
|
|
140
|
+
def end_of(event)
|
|
141
|
+
out = event.slice(*END_KEYS).compact
|
|
142
|
+
out[:type] = out[:type].to_s if out[:type]
|
|
143
|
+
empty = event.dig(:turn_summary, :empty_answer)
|
|
144
|
+
out[:turn_summary] = { empty_answer: empty } if empty
|
|
145
|
+
out
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A message's digest in one form for the live (symbol keys, raw
|
|
149
|
+
# strings) and the saved (string keys, scrubbed as Session#to_h does)
|
|
150
|
+
# messages: role, content and time, each read from either key.
|
|
151
|
+
# @return [String]
|
|
152
|
+
def digest(message)
|
|
153
|
+
return "" unless message.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
role = fetch(message, :role)
|
|
156
|
+
time = fetch(message, MessageTime::KEY)
|
|
157
|
+
Digest::SHA1.hexdigest(JSON.generate(scrub([role&.to_s, fetch(message, :content), time&.to_s])))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Every String in +value+ as Session#to_h saves it (UTF-8, invalid
|
|
161
|
+
# bytes as "?"); symbol keys stay symbols (JSON writes them alike).
|
|
162
|
+
def scrub(value)
|
|
163
|
+
case value
|
|
164
|
+
when String
|
|
165
|
+
text = value.encoding == Encoding::UTF_8 ? value : value.dup.force_encoding(Encoding::UTF_8)
|
|
166
|
+
text.valid_encoding? ? text : text.scrub("?")
|
|
167
|
+
when Array then value.map { |item| scrub(item) }
|
|
168
|
+
when Hash then value.to_h { |key, item| [key, scrub(item)] }
|
|
169
|
+
when Symbol then value.to_s
|
|
170
|
+
else value
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The session folder's lines, oldest first (unreadable, torn and
|
|
175
|
+
# unknown lines skipped), cached by the file's path, size and mtime
|
|
176
|
+
# (chi web reads it on each full show).
|
|
177
|
+
# @return [Array<Line>]
|
|
178
|
+
def read(session_dir)
|
|
179
|
+
path = File.join(session_dir, FILE)
|
|
180
|
+
stat = File.stat(path)
|
|
181
|
+
key = [stat.size, stat.mtime.to_r]
|
|
182
|
+
cached = cache_mutex.synchronize { cache[path] }
|
|
183
|
+
return cached[1] if cached && cached[0] == key
|
|
184
|
+
|
|
185
|
+
lines = parse(File.binread(path))
|
|
186
|
+
cache_mutex.synchronize do
|
|
187
|
+
cache.shift while cache.size >= CACHE_SIZE
|
|
188
|
+
cache[path] = [key, lines]
|
|
189
|
+
end
|
|
190
|
+
lines
|
|
191
|
+
rescue SystemCallError, IOError
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @return [Array<Line>]
|
|
196
|
+
def parse(text)
|
|
197
|
+
text.dup.force_encoding(Encoding::UTF_8).scrub("?").each_line.filter_map do |raw|
|
|
198
|
+
Line.from_h(JSON.parse(raw))
|
|
199
|
+
rescue JSON::ParserError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The lines that hold against +messages+ (Session messages, symbol
|
|
205
|
+
# or string keys): each one's range still in them with both ends the
|
|
206
|
+
# same; of overlapping ranges and of one turn's lines the newest
|
|
207
|
+
# (last in the file) wins. Ordered by their range.
|
|
208
|
+
# @return [Array<Line>]
|
|
209
|
+
def valid(lines, messages)
|
|
210
|
+
messages = Array(messages)
|
|
211
|
+
kept = []
|
|
212
|
+
lines.reverse_each do |line|
|
|
213
|
+
next unless holds?(line, messages)
|
|
214
|
+
next if kept.any? { |other| other.turn_id == line.turn_id || overlap?(other, line) }
|
|
215
|
+
|
|
216
|
+
kept << line
|
|
217
|
+
end
|
|
218
|
+
kept.sort_by(&:from)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The last parseable line of +path+, or nil.
|
|
222
|
+
# @return [Line, nil]
|
|
223
|
+
def tail_line(path)
|
|
224
|
+
return nil unless File.file?(path)
|
|
225
|
+
|
|
226
|
+
File.open(path, "rb") do |file|
|
|
227
|
+
size = file.size
|
|
228
|
+
chunk = file.pread([size, TAIL_BYTES].min, [size - TAIL_BYTES, 0].max)
|
|
229
|
+
parse(chunk.to_s).last
|
|
230
|
+
end
|
|
231
|
+
rescue SystemCallError, IOError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Whether +line+ is small enough to send in a snapshot: within what
|
|
236
|
+
# #tail_line reads back after a restart (TAIL_BYTES), so a join draws
|
|
237
|
+
# a turn the same before and after one (a bigger line: rebuilt).
|
|
238
|
+
def sendable?(line) = JSON.generate(line.to_h).bytesize <= TAIL_BYTES
|
|
239
|
+
|
|
240
|
+
def holds?(line, messages)
|
|
241
|
+
messages.size >= line.to && digest(messages[line.from]) == line.from_digest &&
|
|
242
|
+
digest(messages[line.to - 1]) == line.to_digest
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def overlap?(one, other) = one.from < other.to && other.from < one.to
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
def fetch(message, key)
|
|
250
|
+
message.key?(key) ? message[key] : message[key.to_s]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def cache = (@cache ||= {})
|
|
254
|
+
def cache_mutex = (@cache_mutex ||= Mutex.new)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Sessions whose parsed file chi web keeps (a long one's is tens of MB
|
|
258
|
+
# of objects; the mtime moves at every turn end anyway).
|
|
259
|
+
CACHE_SIZE = 8
|
|
260
|
+
# Read from the end at start (#last): a drawing is a few KB, its outputs cut.
|
|
261
|
+
TAIL_BYTES = 1024 * 1024
|
|
262
|
+
private_constant :CACHE_SIZE, :TAIL_BYTES
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
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)
|
|
@@ -91,6 +91,7 @@ module Samagotchi
|
|
|
91
91
|
check_settings(manifest) unless @installed
|
|
92
92
|
check_install(manifest) unless missing || placeholders? || manifest.session_settings_clash
|
|
93
93
|
check_api(manifest)
|
|
94
|
+
check_fail_closed(manifest) unless @installed
|
|
94
95
|
check_boundary(manifest)
|
|
95
96
|
check_tests unless @installed
|
|
96
97
|
end
|
|
@@ -439,6 +440,23 @@ module Samagotchi
|
|
|
439
440
|
end
|
|
440
441
|
end
|
|
441
442
|
|
|
443
|
+
# A chi.on(..., on_error: :fail_closed) guard denies the call it raises
|
|
444
|
+
# on; only the manifest's plugin: required: true makes its failing to
|
|
445
|
+
# load deny every call (chi knows it before the plugin registers).
|
|
446
|
+
FAIL_CLOSED = /on_error:\s*(?::fail_closed\b|["']fail_closed["'])/
|
|
447
|
+
|
|
448
|
+
def check_fail_closed(manifest)
|
|
449
|
+
file = manifest.plugin && manifest.plugin[:file]
|
|
450
|
+
return if !file || manifest.plugin[:required] || !File.file?(path(file))
|
|
451
|
+
|
|
452
|
+
source = File.read(path(file))
|
|
453
|
+
return unless (index = source =~ FAIL_CLOSED)
|
|
454
|
+
|
|
455
|
+
add(:warn, :"fail-closed", file, "a fail-closed chi.on hook, but the manifest's plugin: has no required: true: " \
|
|
456
|
+
"if the plugin fails to load, tool calls still run",
|
|
457
|
+
line: source[0, index].count("\n") + 1, fix: "add required: true under plugin: in manifest.yml")
|
|
458
|
+
end
|
|
459
|
+
|
|
442
460
|
def check_boundary(manifest)
|
|
443
461
|
require_relative "../testing/boundary"
|
|
444
462
|
known = shipped? ? Testing::Boundary::EXCEPTIONS.fetch(manifest.name, {}) : {}
|
|
@@ -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) }
|
|
@@ -639,9 +639,12 @@ module Samagotchi
|
|
|
639
639
|
0
|
|
640
640
|
end
|
|
641
641
|
|
|
642
|
+
# The base and the file; a marked one also the bundle's text it stays
|
|
643
|
+
# behind (its source-less FileState says which).
|
|
642
644
|
def diff_memory(prov, target_dir, key)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
+
state = Samagotchi::MemoryBundle::FileState.of(key, record: prov.record, provenance: prov, target_dir: target_dir, source_dir: nil)
|
|
646
|
+
print_base_and_current(key, state.paths[:base], state.paths[:current])
|
|
647
|
+
print_incoming(prov, key) if state.marked?
|
|
645
648
|
end
|
|
646
649
|
|
|
647
650
|
# The bundle's text a file stays behind (Provenance#incoming_path), so the
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: known-names
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.2.0
|
|
4
4
|
requires_chi: ">= 0.150.0"
|
|
5
5
|
scope: system
|
|
6
6
|
description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
|
|
7
7
|
trust_level: reviewed
|
|
8
8
|
files:
|
|
9
9
|
known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
event: before_tool_call
|
|
14
|
-
on_error: log
|
|
15
|
-
priority: 50
|
|
10
|
+
plugin:
|
|
11
|
+
file: plugin.rb
|
|
12
|
+
sha256: sha256:d140612e6c34a9d2d205bf818a2a0797ad1e133829481f354aaa9e56698017e3
|
|
16
13
|
# config.yml bundles: known-names: (another key warns; an older chi ignores this)
|
|
17
14
|
settings: [names, ignore, mode, derive, min_length, max_distance]
|
|
@@ -243,3 +243,20 @@ class KnownNames
|
|
|
243
243
|
Array(value).map { |name| name.to_s.strip }.reject(&:empty?)
|
|
244
244
|
end
|
|
245
245
|
end
|
|
246
|
+
|
|
247
|
+
# What chi loads: one KnownNames, built once from config.yml
|
|
248
|
+
# `bundles: known-names:` when the plugin loads, on before_tool_call at
|
|
249
|
+
# priority 50. A raise in the handler is logged and the call goes on
|
|
250
|
+
# (chi.on's log policy); KnownNames#call turns its own errors into a
|
|
251
|
+
# notice first.
|
|
252
|
+
class Plugin
|
|
253
|
+
PRIORITY = 50
|
|
254
|
+
|
|
255
|
+
def initialize(settings = {})
|
|
256
|
+
@names = KnownNames.new(settings)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def register(chi)
|
|
260
|
+
chi.on(:before_tool_call, priority: PRIORITY) { |event, _ctx| @names.call(event) }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: source-links
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.7.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, refs in a waiting question's reply, and a one-line note after the turn (note false turns it off); sources are configured patterns
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
requires_chi: ">= 0.150.0"
|
|
8
8
|
files:
|
|
9
9
|
source_links.md: sha256:ace01a0f7c308a0ae54ece714c31c0e6d7a49c372d98111030e6d268320bed3a
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
events: [after_turn, question_asked]
|
|
14
|
-
on_error: log
|
|
15
|
-
priority: 90
|
|
10
|
+
plugin:
|
|
11
|
+
file: plugin.rb
|
|
12
|
+
sha256: sha256:b4a777d8b6135f80e248dd21b5370c91dfcf2aea17057734e77241eb9cc4afe4
|
|
16
13
|
# config.yml bundles: source-links: (another key warns; an older chi ignores this)
|
|
17
14
|
settings: [sources, max, note]
|
|
@@ -59,10 +59,10 @@ require "samagotchi/ref"
|
|
|
59
59
|
# action, "Open in <name>", the ref's URL. A URL that isn't http(s) stays a
|
|
60
60
|
# markdown link only. The web's ref menu shows the action.
|
|
61
61
|
#
|
|
62
|
-
# The hook is on :question_asked too (
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
62
|
+
# The hook is on :question_asked too (Plugin below hooks both events): while
|
|
63
|
+
# a question of the model's (or an approval of its call) waits, the refs in
|
|
64
|
+
# the reply that asks it, and in the question's text and options, are refs
|
|
65
|
+
# on that reply, the same as an answer's. The web pairs
|
|
66
66
|
# them in the stage's asking slot, on the question's card and on that step
|
|
67
67
|
# in the history (a ref only in the question's text pairs there alone). No
|
|
68
68
|
# links in the text and no note line for it: the turn's end has those.
|
|
@@ -657,3 +657,22 @@ class SourceLinks
|
|
|
657
657
|
value.to_s.gsub(/[^A-Za-z0-9\-._~]/) { |c| c.bytes.map { |b| format("%%%02X", b) }.join }
|
|
658
658
|
end
|
|
659
659
|
end
|
|
660
|
+
|
|
661
|
+
# What chi loads: one SourceLinks, built once from config.yml
|
|
662
|
+
# `bundles: source-links:` when the plugin loads, on after_turn and
|
|
663
|
+
# question_asked at priority 90 (its state, the remotes it asked git for,
|
|
664
|
+
# is shared). A raise in the handler is logged and the turn goes on
|
|
665
|
+
# (chi.on's log policy).
|
|
666
|
+
class Plugin
|
|
667
|
+
PRIORITY = 90
|
|
668
|
+
|
|
669
|
+
def initialize(settings = {})
|
|
670
|
+
@links = SourceLinks.new(settings)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def register(chi)
|
|
674
|
+
%i[after_turn question_asked].each do |event|
|
|
675
|
+
chi.on(event, priority: PRIORITY) { |payload, _ctx| @links.call(payload) }
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.157.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:709bd52710bf6c50334a89a10742ea1554c5985cc2d7c4e7571e526cc4479526
|
|
9
9
|
config_modification_protocol.md: sha256:25ed2d4901f00eeea226db5eb2b75a05d6cdee4015ac8daa4bed624f745c3922
|
|
10
|
-
memory_guide.md: sha256:
|
|
10
|
+
memory_guide.md: sha256:fe4dbb25ce4c9f9b51507a6130fa7bf992a2cc41af2453830eacd27c09a65d1e
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -114,7 +114,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
114
114
|
| Command | Purpose |
|
|
115
115
|
|---------|---------|
|
|
116
116
|
| `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). It also installs a bundle whose `requires_chi` this chi doesn't meet: without `--force` that install is refused (`NAME can't be installed: it requires chi …`). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
|
|
117
|
-
| `upgrade <source\|name> [--force] [--dry-run] [--agent]` | A bare `name` is an installed bundle: chi upgrades it from the source its record keeps (its git URL with the `#ref` it recorded, its folder, or the archive it came from) — a dev install is refused, and a record whose source is gone says so and asks for the path or URL. Otherwise 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#
|
|
117
|
+
| `upgrade <source\|name> [--force] [--dry-run] [--agent]` | A bare `name` is an installed bundle: chi upgrades it from the source its record keeps (its git URL with the `#ref` it recorded, its folder, or the archive it came from) — a dev install is refused, and a record whose source is gone says so and asks for the path or URL. Otherwise 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#decision` (the file's `FileState` and its `FileActions` row, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
|
|
118
118
|
| `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
|
|
119
119
|
| `status [<bundle>]` | Provenance + per-file `ok|modified|missing|unreadable|no-index` vs stored checksum and base snapshot, and what stays behind or merged at a chi start; `issues=` counts files behind, missing or unreadable, not edits; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
|
|
120
120
|
| `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
|
|
@@ -60,23 +60,23 @@ module Samagotchi
|
|
|
60
60
|
move = []
|
|
61
61
|
refuse = []
|
|
62
62
|
SessionManager.children_of(prev_id, state_dir: state_dir).each do |row|
|
|
63
|
-
subtree = [row
|
|
63
|
+
subtree = [row.id, *SessionManager.descendant_ids(row.id, state_dir)]
|
|
64
64
|
next if (subtree & open_ids).empty?
|
|
65
65
|
|
|
66
|
-
if row
|
|
66
|
+
if row.archived
|
|
67
67
|
# An open delegate below an archived one: the archive's cascade
|
|
68
68
|
# would take it, the move doesn't (only unarchived ones move).
|
|
69
69
|
refuse.concat(open.select { |c| subtree.include?(c.id) }.map do |c|
|
|
70
|
-
c.with(why: "under archived #{row
|
|
70
|
+
c.with(why: "under archived #{row.short_id}; unarchive that one or archive this one")
|
|
71
71
|
end)
|
|
72
72
|
next
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
why = unmovable(row
|
|
75
|
+
why = unmovable(row.id, state_dir: state_dir)
|
|
76
76
|
if why
|
|
77
|
-
refuse << SessionContinue::OpenChild.new(id: row
|
|
77
|
+
refuse << SessionContinue::OpenChild.new(id: row.id, short_id: row.short_id, why: why)
|
|
78
78
|
else
|
|
79
|
-
move << row
|
|
79
|
+
move << row.id
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
Plan.new(move: move, refuse: refuse)
|
|
@@ -335,8 +335,8 @@ module Samagotchi
|
|
|
335
335
|
before = kept ? kept.after : current
|
|
336
336
|
# A follow-up's turn (DelegateCursor#turn) is reported by its own
|
|
337
337
|
# reply, as delegate_result takes it, not a newer queued turn's.
|
|
338
|
-
wait = ReplyWait.call(child_id, state_dir: @state_dir,
|
|
339
|
-
timeout: 0
|
|
338
|
+
wait = ReplyWait.call(child_id, state_dir: @state_dir, **ReplyWait::Target.from_cursor(before, child: child).to_h,
|
|
339
|
+
timeout: 0)
|
|
340
340
|
return nil unless reportable?(wait)
|
|
341
341
|
|
|
342
342
|
claim_id = before.report_claim&.id || SecureRandom.hex(16)
|