samagotchi 0.149.0 → 0.151.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 +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +19 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../atomic_file"
|
|
6
6
|
require_relative "../events"
|
|
7
|
+
require_relative "card_places"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
class Bridge
|
|
@@ -22,12 +23,18 @@ module Samagotchi
|
|
|
22
23
|
# reload draws the resolved card where it was.
|
|
23
24
|
#
|
|
24
25
|
# A card with an earlier card's id replaces it where it was. Each entry
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
26
|
+
# holds the id of the turn it belongs to or follows (+turn_id+: the running
|
|
27
|
+
# turn's, else the last one's, nil before any) and lists with +turns_since+,
|
|
28
|
+
# the turns drawn after it, counted from the messages the page draws
|
|
29
|
+
# (CardPlaces, with the turn log the store keeps), and +current+ for a card
|
|
30
|
+
# of the turn running now. A card that isn't the turn's but comes while one
|
|
31
|
+
# runs (an anytime command's, such as /btw) goes after that turn: its prompt
|
|
32
|
+
# is already in the history. Listed against messages, an in-turn entry of a
|
|
33
|
+
# turn that left no opening there (failed, rolled back, stopped before the
|
|
34
|
+
# model) stays where it was as a between-turns one: a card or hook notice
|
|
35
|
+
# without its actions; its other rows (no step to sit in) aren't listed. A
|
|
36
|
+
# kept Continue or reminder turn never has an opening: its entries are
|
|
37
|
+
# +continued+ and stay rows.
|
|
31
38
|
#
|
|
32
39
|
# Notices (hook notices, load warnings, "asking again" rows), ✂ rows
|
|
33
40
|
# and cards (with questions) are capped apart, +capacity+ each, so a
|
|
@@ -36,20 +43,21 @@ module Samagotchi
|
|
|
36
43
|
# never pushed out: the oldest other card or answered question goes.
|
|
37
44
|
#
|
|
38
45
|
# With a +path+ (the Bridge's: FILE in the session's folder) the store
|
|
39
|
-
# saves its entries and turn
|
|
46
|
+
# saves its entries and turn log there on each change, and a later
|
|
40
47
|
# worker's store starts from them, so a turn's cards and notices outlive
|
|
41
48
|
# the worker that showed them; the web reads the file (.saved) for a
|
|
42
|
-
# session no worker runs.
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# an earlier worker lists with +earlier+: true (an attached TUI's resync
|
|
48
|
-
# onto a new worker doesn't print those again). An earlier card shown
|
|
49
|
+
# session no worker runs. The load warnings stay the worker's own (each
|
|
50
|
+
# start announces its own), and a question still open when the worker went
|
|
51
|
+
# waits for no one: neither is saved or seeded. Same caps as in memory. An
|
|
52
|
+
# entry from an earlier worker lists with +earlier+: true (an attached TUI's
|
|
53
|
+
# resync onto a new worker doesn't print those again). An earlier card shown
|
|
49
54
|
# during a turn (+in_turn+) comes back without its actions: they belong
|
|
50
55
|
# to a turn no worker runs any more (check-in's Nudge would steer the
|
|
51
56
|
# new worker's turn). A card shown between turns (github-pr's offer,
|
|
52
57
|
# btw, coordinator) keeps its buttons.
|
|
58
|
+
#
|
|
59
|
+
# A file from before the log (version 1) keeps its places: each entry's
|
|
60
|
+
# count becomes +base_since+, and new openings add to it (#seed_v1).
|
|
53
61
|
class CardStore
|
|
54
62
|
CAPACITY = 20
|
|
55
63
|
NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry steer_cut].freeze
|
|
@@ -58,27 +66,40 @@ module Samagotchi
|
|
|
58
66
|
LLM_CONTEXT_FIELDS = %i[type moment why freed_tokens tail_tokens staged text groups].freeze
|
|
59
67
|
FILE = "cards.json"
|
|
60
68
|
# The events after which the file is written again: the ones that
|
|
61
|
-
# change an entry or the turn
|
|
62
|
-
SAVED_ON = (%i[card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
63
|
-
question_cancelled question_relay
|
|
69
|
+
# change an entry or the turn log.
|
|
70
|
+
SAVED_ON = (%i[turn_started card hook_notice empty_answer_retry steer_cut llm_context_edited question_requested question_answered
|
|
71
|
+
question_cancelled question_relay] + Events::TURN_END).freeze
|
|
72
|
+
# What only the store reads: never listed.
|
|
73
|
+
INTERNAL_FIELDS = %i[turn_id continued base_since past_idless during after_failed command].freeze
|
|
64
74
|
|
|
65
75
|
# What a session's saved file lists, as a store started from it lists
|
|
66
76
|
# (no turn running): the web's cards for a session no worker runs.
|
|
67
77
|
# @param session_dir [String]
|
|
78
|
+
# @param messages [Array<Hash>, nil] the session's messages, for the
|
|
79
|
+
# places (#list)
|
|
68
80
|
# @return [Array<Hash>] [] without a readable file
|
|
69
|
-
def self.saved(session_dir)
|
|
70
|
-
new(path: File.join(session_dir, FILE)).list
|
|
81
|
+
def self.saved(session_dir, messages: nil)
|
|
82
|
+
new(path: File.join(session_dir, FILE)).list(messages: messages)
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
# @param path [String, nil] the file the entries are saved in and
|
|
74
86
|
# started from; nil keeps them in memory only
|
|
75
|
-
|
|
87
|
+
# @param messages [#call, nil] the session's messages now (Session#messages:
|
|
88
|
+
# a running turn leaves them alone, a change replaces the array, so any
|
|
89
|
+
# thread may read them): where an opening with no id sits against an
|
|
90
|
+
# entry, and how far the saved log must reach. Read at an idle show and a
|
|
91
|
+
# turn's start and end, never for a running turn's row.
|
|
92
|
+
def initialize(capacity: CAPACITY, path: nil, messages: nil)
|
|
76
93
|
@capacity = capacity
|
|
77
94
|
@path = path
|
|
95
|
+
@messages = messages
|
|
78
96
|
@mutex = Mutex.new
|
|
79
97
|
@entries = []
|
|
80
|
-
@
|
|
81
|
-
@
|
|
98
|
+
@turn_log = []
|
|
99
|
+
@running_turn = nil
|
|
100
|
+
@continuing = false
|
|
101
|
+
@failed_turn = nil
|
|
102
|
+
@log_start = nil
|
|
82
103
|
@iteration = nil
|
|
83
104
|
@calls = 0
|
|
84
105
|
seed
|
|
@@ -93,67 +114,160 @@ module Samagotchi
|
|
|
93
114
|
nil # never break the running turn
|
|
94
115
|
end
|
|
95
116
|
|
|
117
|
+
# @param messages [Array<Hash>, nil] the messages the page draws. Without
|
|
118
|
+
# them (a body re-read, a trace): no turns_since:, nothing to tell a turn
|
|
119
|
+
# that left nothing by.
|
|
96
120
|
# @return [Array<Hash>] oldest first: the card's (or notice's) fields
|
|
97
|
-
# plus turns_since:
|
|
98
|
-
# worker saved,
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
121
|
+
# plus current:, turns_since: with +messages+, earlier: true for one an
|
|
122
|
+
# earlier worker saved, during: true for a card that came while the
|
|
123
|
+
# running turn runs but isn't that turn's (it goes after the running
|
|
124
|
+
# turn's prompt), and after_failed: true for one shown after the last
|
|
125
|
+
# turn's prompt went back (the page draws that prompt at the end: it goes
|
|
126
|
+
# after it, as it showed live); label: for a card a command showed ("from
|
|
127
|
+
# /short"), for a page whose reload has lost the command's line
|
|
128
|
+
def list(messages: nil)
|
|
102
129
|
@mutex.synchronize do
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
current = entry[:in_turn] && @running && turns == @turns_done
|
|
106
|
-
since = @turns_done - turns - (entry[:in_turn] && !current ? 1 : 0)
|
|
107
|
-
listed = entry.except(:turns, :during).merge(turns_since: [since, 0].max, current: current ? true : false)
|
|
108
|
-
listed[:during] = true if entry[:during]
|
|
109
|
-
listed
|
|
110
|
-
end
|
|
130
|
+
places = messages && !@entries.empty? ? CardPlaces.new(@turn_log, messages) : nil
|
|
131
|
+
@entries.filter_map { |entry| listed(entry, places) }
|
|
111
132
|
end
|
|
112
133
|
end
|
|
113
134
|
|
|
114
135
|
private
|
|
115
136
|
|
|
137
|
+
def listed(entry, places)
|
|
138
|
+
current = current?(entry)
|
|
139
|
+
shown = places && left_nothing?(entry, current, places) ? between_turns(entry) : entry
|
|
140
|
+
return unless shown
|
|
141
|
+
|
|
142
|
+
out = shown.except(*INTERNAL_FIELDS)
|
|
143
|
+
out[:turns_since] = places.turns_since(entry) if places
|
|
144
|
+
out[:current] = current
|
|
145
|
+
out[:during] = true if entry[:during]
|
|
146
|
+
out[:after_failed] = true if entry[:after_failed] && entry[:turn_id] == @failed_turn
|
|
147
|
+
out[:label] = "from #{entry[:command]}" if entry[:command]
|
|
148
|
+
out
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The turn running now's own. An entry with no anchor is none's: nil ==
|
|
152
|
+
# nil would make every one the running turn's while nothing runs.
|
|
153
|
+
def current?(entry)
|
|
154
|
+
entry[:in_turn] && @running_turn && entry[:turn_id] == @running_turn ? true : false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# An in-turn entry whose turn has no opening in the messages, and was no
|
|
158
|
+
# kept Continue or reminder (those never have one).
|
|
159
|
+
def left_nothing?(entry, current, places)
|
|
160
|
+
entry[:in_turn] && !current && !entry[:continued] && entry[:turn_id] && !places.opened?(entry[:turn_id])
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Of a turn that left nothing, what stays where it was: a card without
|
|
164
|
+
# its actions (their turn is gone), a hook's notice; its other rows have
|
|
165
|
+
# no step to sit in.
|
|
166
|
+
def between_turns(entry)
|
|
167
|
+
case entry[:type]
|
|
168
|
+
when :card then entry.merge(in_turn: false, **(entry.key?(:actions) ? { actions: [] } : {}))
|
|
169
|
+
when :hook_notice then entry.except(:iteration, :calls).merge(in_turn: false)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
116
173
|
def save
|
|
117
174
|
return unless @path
|
|
118
175
|
|
|
119
176
|
entries = @entries.reject { |entry| entry[:type] == :guardrail_warning }
|
|
120
|
-
# A session that never showed one gets no file
|
|
121
|
-
# only places entries relative to each other).
|
|
177
|
+
# A session that never showed one gets no file.
|
|
122
178
|
return if entries.empty? && !File.exist?(@path)
|
|
123
179
|
|
|
124
180
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
125
|
-
|
|
181
|
+
saved = { "version" => 2, "turn_log" => pruned_log(entries), "entries" => entries }
|
|
182
|
+
saved["failed_turn"] = @failed_turn if @failed_turn
|
|
183
|
+
AtomicFile.write(@path, JSON.generate(saved))
|
|
126
184
|
rescue StandardError
|
|
127
185
|
nil # a card that can't be saved still shows live
|
|
128
186
|
end
|
|
129
187
|
|
|
130
|
-
# The
|
|
131
|
-
#
|
|
132
|
-
#
|
|
188
|
+
# The log from the opening the earliest anchor counts from. Found by
|
|
189
|
+
# walking the messages, so a running turn's saves reuse what its start
|
|
190
|
+
# found (#fold forgets it there): its rows would walk them each, on the
|
|
191
|
+
# turn's thread. Nothing in a running turn needs an earlier start: a new
|
|
192
|
+
# entry anchors to it, and an evicted one only leaves the kept part longer.
|
|
193
|
+
def pruned_log(entries)
|
|
194
|
+
@log_start = log_start(entries) unless @running_turn && @log_start
|
|
195
|
+
@turn_log.drop(@log_start)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# A turn before that opening orders nothing. 0 when an entry has no anchor,
|
|
199
|
+
# or none does yet (such an entry counts from the log's first turn), or with
|
|
200
|
+
# no messages to find that opening in.
|
|
201
|
+
def log_start(entries)
|
|
202
|
+
positions = @turn_log.each_with_index.to_h
|
|
203
|
+
anchors = entries.map { |entry| positions[entry[:turn_id]] }
|
|
204
|
+
return 0 if anchors.empty? || anchors.include?(nil) || !@messages
|
|
205
|
+
|
|
206
|
+
CardPlaces.new(@turn_log, @messages.call).log_start_for(anchors.min)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The entries an earlier worker saved (a missing or broken file: none),
|
|
210
|
+
# their types as symbols again. A card that came during that worker's
|
|
211
|
+
# last turn stays where it was, as after a failed turn, or after that
|
|
212
|
+
# turn when the worker was killed in it: the messages say which.
|
|
133
213
|
def seed
|
|
134
214
|
return unless @path && File.file?(@path)
|
|
135
215
|
|
|
136
216
|
data = JSON.parse(File.read(@path), symbolize_names: true)
|
|
137
|
-
return unless data.is_a?(Hash) && data[:
|
|
217
|
+
return unless data.is_a?(Hash) && data[:entries].is_a?(Array)
|
|
218
|
+
|
|
219
|
+
if data[:turn_log].is_a?(Array)
|
|
220
|
+
@turn_log = data[:turn_log].grep(String)
|
|
221
|
+
@failed_turn = data[:failed_turn] if data[:failed_turn].is_a?(String)
|
|
222
|
+
@entries = seeded(data[:entries])
|
|
223
|
+
elsif data[:turns].is_a?(Integer)
|
|
224
|
+
seed_v1(data)
|
|
225
|
+
end
|
|
226
|
+
rescue StandardError
|
|
227
|
+
@turn_log = []
|
|
228
|
+
@entries = []
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A file from before the turn log: each entry's place by the count it was
|
|
232
|
+
# saved with, killed turn included (nothing in it can say where a drifted
|
|
233
|
+
# card was). New openings add to it. The next save keeps +base_since+.
|
|
234
|
+
# Can go a retention period after the release that wrote version 2.
|
|
235
|
+
def seed_v1(data)
|
|
236
|
+
killed_in_turn = data[:running] == true
|
|
237
|
+
turns_done = data[:turns] + (killed_in_turn ? 1 : 0)
|
|
238
|
+
olds = data[:entries].select { |entry| entry.is_a?(Hash) && entry[:turns].is_a?(Integer) }
|
|
239
|
+
@entries = seeded(olds.map do |entry|
|
|
240
|
+
turns = entry[:during] && killed_in_turn ? turns_done : entry[:turns]
|
|
241
|
+
entry.except(:turns).merge(base_since: [turns_done - turns - (entry[:in_turn] ? 1 : 0), 0].max)
|
|
242
|
+
end)
|
|
243
|
+
end
|
|
138
244
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
next unless entry.is_a?(Hash) && entry[:type].is_a?(String) &&
|
|
245
|
+
def seeded(entries)
|
|
246
|
+
entries.filter_map do |entry|
|
|
247
|
+
next unless entry.is_a?(Hash) && entry[:type].is_a?(String) && sound_place?(entry)
|
|
142
248
|
|
|
143
|
-
entry = entry.merge(type: entry[:type].to_sym, earlier: true)
|
|
249
|
+
entry = entry.except(:during).merge(type: entry[:type].to_sym, earlier: true)
|
|
144
250
|
entry = entry.merge(actions: []) if entry[:type] == :card && entry[:in_turn] && entry.key?(:actions)
|
|
145
251
|
entry unless entry[:type] == :guardrail_warning || open_question?(entry)
|
|
146
252
|
end
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# A hand-edited or foreign file's entry goes, rather than break every later
|
|
256
|
+
# listing of the session.
|
|
257
|
+
def sound_place?(entry)
|
|
258
|
+
return false unless entry[:turn_id].nil? || entry[:turn_id].is_a?(String)
|
|
259
|
+
|
|
260
|
+
entry.values_at(:base_since, :past_idless).compact.all? { |count| count.is_a?(Integer) && !count.negative? }
|
|
151
261
|
end
|
|
152
262
|
|
|
153
263
|
def fold(event)
|
|
154
264
|
case event[:type]
|
|
155
265
|
when :turn_started
|
|
156
|
-
@
|
|
266
|
+
@turn_log << event[:turn_id] if event[:turn_id]
|
|
267
|
+
@running_turn = event[:turn_id]
|
|
268
|
+
@continuing = event[:continue] ? true : false
|
|
269
|
+
@failed_turn = nil
|
|
270
|
+
@log_start = nil
|
|
157
271
|
@iteration = nil
|
|
158
272
|
@calls = 0
|
|
159
273
|
when :generation_started
|
|
@@ -162,58 +276,74 @@ module Samagotchi
|
|
|
162
276
|
when :tool_call_started
|
|
163
277
|
@iteration = event[:iteration]
|
|
164
278
|
@calls = event[:call_index].to_i
|
|
165
|
-
|
|
166
|
-
# stayed too); a failed one rolled back leaves none.
|
|
167
|
-
when *Events::TURN_END
|
|
168
|
-
@running = false
|
|
169
|
-
kept = Events.kept_turn?(event)
|
|
170
|
-
@turns_done += 1 if kept
|
|
171
|
-
settle_during(after_turn: kept)
|
|
279
|
+
when *Events::TURN_END then end_turn(event)
|
|
172
280
|
when :card then add_card(event)
|
|
173
281
|
when :hook_notice then add_notice(event)
|
|
174
282
|
# What failed to load (guardrails, plugins): before the first turn
|
|
175
|
-
# this worker runs, where it showed live.
|
|
283
|
+
# this worker runs, where it showed live. The Engine announces it inside
|
|
284
|
+
# that turn: it anchors before the turn.
|
|
176
285
|
when :guardrail_warning then push({ type: :guardrail_warning, message: event[:message], label: event[:label] }
|
|
177
|
-
.compact.merge(in_turn: false,
|
|
178
|
-
when :empty_answer_retry then add_turn_row(event.slice(:type, :attempt, :of, :stopped_by, :malformed)) if @
|
|
179
|
-
when :steer_cut then add_turn_row(event.slice(:type, :source)) if @
|
|
180
|
-
when :llm_context_edited then add_turn_row(Marshal.load(Marshal.dump(event.slice(*LLM_CONTEXT_FIELDS)))) if @
|
|
286
|
+
.compact.merge(in_turn: false, turn_id: @running_turn ? @turn_log[-2] : @turn_log.last))
|
|
287
|
+
when :empty_answer_retry then add_turn_row(event.slice(:type, :attempt, :of, :stopped_by, :malformed)) if @running_turn
|
|
288
|
+
when :steer_cut then add_turn_row(event.slice(:type, :source)) if @running_turn
|
|
289
|
+
when :llm_context_edited then add_turn_row(Marshal.load(Marshal.dump(event.slice(*LLM_CONTEXT_FIELDS)))) if @running_turn
|
|
181
290
|
when :question_requested
|
|
182
|
-
add_turn_row({ type: :question, pending_question: Marshal.load(Marshal.dump(event[:pending_question])) }) if @
|
|
291
|
+
add_turn_row({ type: :question, pending_question: Marshal.load(Marshal.dump(event[:pending_question])) }) if @running_turn
|
|
183
292
|
when :question_answered then resolve_question(event[:id], answer: event[:answer])
|
|
184
293
|
when :question_cancelled then resolve_question(event[:id], cancelled: true, reason: event[:reason])
|
|
185
294
|
when :question_relay then mark_relay(event[:id], event[:relayed_to])
|
|
186
295
|
end
|
|
187
296
|
end
|
|
188
297
|
|
|
298
|
+
# The running turn ended. A :during card now reads as after it when its
|
|
299
|
+
# opening is in the messages, and as before the next turn when not. A
|
|
300
|
+
# failed Continue or wake left nothing either, opening or not.
|
|
301
|
+
def end_turn(event)
|
|
302
|
+
settle_past_idless
|
|
303
|
+
@entries.each do |entry|
|
|
304
|
+
entry.delete(:during)
|
|
305
|
+
entry.delete(:continued) if entry[:turn_id] == @running_turn && !Events.kept_turn?(event)
|
|
306
|
+
end
|
|
307
|
+
@failed_turn = Events.kept_turn?(event) ? nil : @running_turn
|
|
308
|
+
@running_turn = nil
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# What the running turn showed that isn't its own (an anytime command's
|
|
312
|
+
# card) goes after that turn, so after any opening with no id inside it. The
|
|
313
|
+
# end event's messages hold the turn; its own rows count such a line as usual.
|
|
314
|
+
def settle_past_idless
|
|
315
|
+
shown = @entries.select { |entry| entry[:turn_id] == @running_turn && !entry[:in_turn] }
|
|
316
|
+
return if shown.empty? || !@messages
|
|
317
|
+
|
|
318
|
+
run = CardPlaces.new(@turn_log, @messages.call).idless_after(@running_turn)
|
|
319
|
+
shown.each { |entry| entry[:past_idless] = run if run.positive? }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# The entry's anchor: the running turn, else the last one.
|
|
323
|
+
def anchor = @running_turn || @turn_log.last
|
|
324
|
+
|
|
189
325
|
def add_card(event)
|
|
190
|
-
card = event.slice(:type, :id, :source, :title, :body, :level, :actions)
|
|
326
|
+
card = event.slice(:type, :id, :source, :title, :body, :level, :actions, :command)
|
|
191
327
|
index = @entries.index { |entry| entry[:type] == :card && entry[:id] == card[:id] }
|
|
192
328
|
if index
|
|
193
|
-
# Replaced where it was: the first one's place stays
|
|
329
|
+
# Replaced where it was: the first one's place stays, and whose it is
|
|
330
|
+
# (a command's answer from another thread; a turn's proposal its own
|
|
331
|
+
# button's command replaced, which nobody typed).
|
|
194
332
|
old = @entries[index]
|
|
195
|
-
@entries[index] = card.merge(updated: true, **old.slice(:in_turn, :
|
|
333
|
+
@entries[index] = card.except(:command).merge(updated: true, **old.slice(:in_turn, :turn_id, :during, :continued, :base_since,
|
|
334
|
+
:past_idless, :after_failed, :command))
|
|
196
335
|
else
|
|
197
|
-
entry = card.merge(in_turn: event[:in_turn] ? true : false,
|
|
198
|
-
entry[:during] = true if @
|
|
336
|
+
entry = card.merge(in_turn: event[:in_turn] ? true : false, turn_id: anchor)
|
|
337
|
+
entry[:during] = true if @running_turn && !event[:in_turn]
|
|
338
|
+
entry[:continued] = true if @running_turn && event[:in_turn] && @continuing
|
|
199
339
|
push(entry)
|
|
200
340
|
end
|
|
201
341
|
end
|
|
202
342
|
|
|
203
|
-
# The turn a :during card came in has ended: it counts as after that
|
|
204
|
-
# turn when the turn is in the history.
|
|
205
|
-
def settle_during(after_turn:)
|
|
206
|
-
@entries.each do |entry|
|
|
207
|
-
next unless entry.delete(:during)
|
|
208
|
-
|
|
209
|
-
entry[:turns] = @turns_done if after_turn
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
343
|
def add_notice(event)
|
|
214
344
|
notice = event.slice(:type, :hook, :text, :level, :fallback_for)
|
|
215
|
-
if event[:between_turns] || !@
|
|
216
|
-
push(notice.merge(in_turn: false,
|
|
345
|
+
if event[:between_turns] || !@running_turn
|
|
346
|
+
push(notice.merge(in_turn: false, turn_id: anchor))
|
|
217
347
|
else
|
|
218
348
|
add_turn_row(notice)
|
|
219
349
|
end
|
|
@@ -221,7 +351,9 @@ module Samagotchi
|
|
|
221
351
|
|
|
222
352
|
# A row of the running turn's current step.
|
|
223
353
|
def add_turn_row(row)
|
|
224
|
-
|
|
354
|
+
entry = row.merge(in_turn: true, turn_id: @running_turn, iteration: @iteration, calls: @calls).compact
|
|
355
|
+
entry[:continued] = true if @continuing
|
|
356
|
+
push(entry)
|
|
225
357
|
end
|
|
226
358
|
|
|
227
359
|
def resolve_question(id, **outcome)
|
|
@@ -246,6 +378,8 @@ module Samagotchi
|
|
|
246
378
|
end
|
|
247
379
|
|
|
248
380
|
def push(entry)
|
|
381
|
+
entry[:after_failed] = true if @failed_turn && !entry[:in_turn]
|
|
382
|
+
note_past_idless(entry) unless @running_turn
|
|
249
383
|
@entries << entry
|
|
250
384
|
cap = cap_class(entry)
|
|
251
385
|
kind = @entries.select { |e| cap_class(e) == cap }
|
|
@@ -255,6 +389,14 @@ module Samagotchi
|
|
|
255
389
|
@entries.delete_at(@entries.index { |e| e.equal?(evict) }) if evict
|
|
256
390
|
end
|
|
257
391
|
|
|
392
|
+
# Shown between turns: below the openings with no id already there.
|
|
393
|
+
def note_past_idless(entry)
|
|
394
|
+
return unless @messages
|
|
395
|
+
|
|
396
|
+
run = CardPlaces.new(@turn_log, @messages.call).idless_after(entry[:turn_id])
|
|
397
|
+
entry[:past_idless] = run if run.positive?
|
|
398
|
+
end
|
|
399
|
+
|
|
258
400
|
# Which cap +entry+ counts against: :notice, :llm_context or :card.
|
|
259
401
|
def cap_class(entry)
|
|
260
402
|
return :llm_context if LLM_CONTEXT_TYPES.include?(entry[:type])
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -136,7 +136,7 @@ module Samagotchi
|
|
|
136
136
|
@accumulator = TurnAccumulator.new
|
|
137
137
|
# Saved in the session's folder: a later worker, and the web without
|
|
138
138
|
# one, show the cards where they were.
|
|
139
|
-
@cards = CardStore.new(path: File.join(session_dir, CardStore::FILE))
|
|
139
|
+
@cards = CardStore.new(path: File.join(session_dir, CardStore::FILE), messages: -> { @engine.session&.messages })
|
|
140
140
|
@pending_card = PendingCard.new(session_dir)
|
|
141
141
|
@heartbeat_interval = heartbeat_interval
|
|
142
142
|
@epoch = SecureRandom.hex(4)
|
|
@@ -315,7 +315,7 @@ module Samagotchi
|
|
|
315
315
|
guardrail_warning: @engine.guardrail_warning,
|
|
316
316
|
plugin_warning: @engine.plugin_warning,
|
|
317
317
|
init_tasks: @engine.init_tasks,
|
|
318
|
-
cards:
|
|
318
|
+
cards: cards_listing,
|
|
319
319
|
commands: command_registry.listing,
|
|
320
320
|
chi_version: Samagotchi::VERSION,
|
|
321
321
|
event_seq: seq,
|
|
@@ -339,7 +339,8 @@ module Samagotchi
|
|
|
339
339
|
# What the page re-reads at a turn's end and when a card arrives: the
|
|
340
340
|
# session state, the last answer (Engine#last_answer_message: one
|
|
341
341
|
# message, not the conversation) and the cards, as one step of the event
|
|
342
|
-
# log (one event_seq). Light
|
|
342
|
+
# log (one event_seq). Light for a long session: GET tail copies no messages,
|
|
343
|
+
# and placing the cards only walks them (about 10 ms at 40k messages).
|
|
343
344
|
# The session's folder (working_directory) rides along: what the web
|
|
344
345
|
# checks the answer's file refs against without reading the session file.
|
|
345
346
|
# @return [Hash] {session_id:, working_directory:, session_state_snapshot:, answer:, cards:, event_seq:, event_id:}
|
|
@@ -350,7 +351,7 @@ module Samagotchi
|
|
|
350
351
|
state = @engine.session_state_snapshot.merge(event_seq: seq, event_id: event_id(seq))
|
|
351
352
|
{ session_id: @session_id, working_directory: @engine.session&.working_directory,
|
|
352
353
|
session_state_snapshot: state, answer: @engine.last_answer_message(turn_id: turn_id),
|
|
353
|
-
cards:
|
|
354
|
+
cards: cards_listing, event_seq: seq, event_id: event_id(seq) }
|
|
354
355
|
end
|
|
355
356
|
end
|
|
356
357
|
|
|
@@ -372,6 +373,13 @@ module Samagotchi
|
|
|
372
373
|
|
|
373
374
|
private
|
|
374
375
|
|
|
376
|
+
# The cards, placed against the messages a snapshot's page draws (the
|
|
377
|
+
# session's own, which a running turn leaves alone). No copy: GET tail
|
|
378
|
+
# stays light.
|
|
379
|
+
def cards_listing
|
|
380
|
+
@cards.list(messages: @engine.session&.messages)
|
|
381
|
+
end
|
|
382
|
+
|
|
375
383
|
# The turn in progress (TurnAccumulator#current_turn) with the running
|
|
376
384
|
# generation's phase as generation: {thinking_only:, age:}
|
|
377
385
|
# (Engine#generation_snapshot), so a page joining mid-generation can
|
|
@@ -124,7 +124,7 @@ module Samagotchi
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
# The listing's placement fields: nothing to place in a one-turn run.
|
|
127
|
-
LISTING_FIELDS = %w[turns_since current earlier].freeze
|
|
127
|
+
LISTING_FIELDS = %w[turns_since current earlier after_failed label].freeze
|
|
128
128
|
|
|
129
129
|
# The saved cards as the web reads them (CardStore.saved), with
|
|
130
130
|
# string keys and values, as the JSON trace prints them.
|