samagotchi 0.159.0 → 0.161.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 +125 -1
- data/docs/architecture.md +12 -9
- data/docs/cli.md +7 -5
- data/docs/context.md +10 -1
- data/docs/guardrails.md +33 -5
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +28 -6
- data/docs/internals/bundles.md +24 -5
- data/docs/internals/engine.md +7 -1
- data/docs/internals/objectives.md +8 -2
- data/docs/internals/remote-sessions.md +2 -1
- data/docs/memory.md +43 -13
- data/docs/plugins.md +13 -4
- data/docs/releasing.md +7 -4
- data/docs/scheduling.md +53 -8
- data/docs/sessions.md +17 -12
- data/docs/testing.md +9 -1
- data/lib/samagotchi/bridge/card_store.rb +31 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +11 -1
- data/lib/samagotchi/bridge/sse_writer.rb +8 -5
- data/lib/samagotchi/bridge.rb +103 -44
- data/lib/samagotchi/bridge_client.rb +9 -6
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/broadcast_command.rb +24 -8
- data/lib/samagotchi/bundle_command.rb +57 -6
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/child_reports.rb +29 -23
- data/lib/samagotchi/client_id.rb +6 -0
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/compat.rb +32 -0
- data/lib/samagotchi/context_absorber.rb +6 -3
- data/lib/samagotchi/context_candidate_choice.rb +8 -5
- data/lib/samagotchi/context_candidates.rb +11 -8
- data/lib/samagotchi/context_candidates_log.rb +10 -1
- data/lib/samagotchi/context_command.rb +10 -4
- data/lib/samagotchi/context_move.rb +6 -1
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +25 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/steering.rb +2 -2
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/events.rb +71 -0
- data/lib/samagotchi/guardrails/approvals.rb +27 -3
- data/lib/samagotchi/guardrails/child_boundary.rb +37 -4
- data/lib/samagotchi/guardrails/protected_paths.rb +8 -5
- data/lib/samagotchi/guardrails/targets.rb +25 -6
- data/lib/samagotchi/hook_routes.rb +0 -1
- data/lib/samagotchi/live_versions.rb +7 -2
- data/lib/samagotchi/memory_bundle/asset_installer.rb +86 -18
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
- data/lib/samagotchi/memory_bundle/file_state.rb +11 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +7 -4
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +93 -23
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +158 -6
- data/lib/samagotchi/memory_bundle/shipped_update.rb +7 -4
- data/lib/samagotchi/memory_bundle/status.rb +3 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +29 -13
- data/lib/samagotchi/memory_orphans.rb +54 -13
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- data/lib/samagotchi/note_command.rb +9 -1
- data/lib/samagotchi/note_delivery.rb +34 -24
- data/lib/samagotchi/note_move.rb +205 -0
- data/lib/samagotchi/objectives_command.rb +3 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/plugin/attached_context.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +4 -1
- data/lib/samagotchi/project_command.rb +58 -20
- data/lib/samagotchi/project_id.rb +102 -0
- data/lib/samagotchi/project_id_clash.rb +62 -0
- data/lib/samagotchi/project_init.rb +214 -10
- data/lib/samagotchi/project_scope.rb +11 -2
- data/lib/samagotchi/review_start.rb +5 -3
- data/lib/samagotchi/schedule_command.rb +12 -0
- data/lib/samagotchi/schedules.rb +374 -76
- data/lib/samagotchi/send_command.rb +2 -15
- data/lib/samagotchi/session.rb +59 -7
- data/lib/samagotchi/session_access/local.rb +53 -22
- data/lib/samagotchi/session_access/remote.rb +13 -5
- data/lib/samagotchi/session_access/web_bridge_client.rb +2 -2
- data/lib/samagotchi/session_access.rb +20 -4
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +162 -24
- data/lib/samagotchi/session_delete_command.rb +3 -1
- data/lib/samagotchi/session_inbox.rb +41 -1
- data/lib/samagotchi/session_inbox_delivery.rb +254 -0
- data/lib/samagotchi/session_manager/delivery.rb +66 -12
- data/lib/samagotchi/session_manager/emptiness.rb +7 -2
- data/lib/samagotchi/session_manager/lifecycle.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +4 -1
- data/lib/samagotchi/session_manager/removal.rb +130 -26
- data/lib/samagotchi/session_manager/spawn.rb +167 -100
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- data/lib/samagotchi/session_manager.rb +1 -0
- data/lib/samagotchi/session_retention.rb +53 -23
- data/lib/samagotchi/session_setup.rb +27 -7
- data/lib/samagotchi/starting_marker.rb +93 -24
- data/lib/samagotchi/state_format.rb +129 -0
- data/lib/samagotchi/system_one/runs.rb +6 -1
- data/lib/samagotchi/system_one/runs_store.rb +18 -14
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +7 -4
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +5 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +5 -0
- data/lib/samagotchi/terminal_ui/version_lines.rb +4 -2
- data/lib/samagotchi/terminal_ui.rb +2 -3
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +19 -6
- data/lib/samagotchi/tools/schedule_task.rb +5 -3
- data/lib/samagotchi/tools/send_note.rb +10 -4
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/turn_state.rb +108 -4
- data/lib/samagotchi/update_command.rb +9 -4
- data/lib/samagotchi/verify_fresh.rb +27 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -5
- data/lib/samagotchi/web/public/app.js +2 -7
- data/lib/samagotchi/web/public/chains.js +4 -4
- data/lib/samagotchi/web/public/data.js +18 -2
- data/lib/samagotchi/web/public/info_bar_model.js +1 -1
- data/lib/samagotchi/web/public/session_fields.js +3 -2
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/update_notice.js +2 -2
- data/lib/samagotchi/web/session_access_routes.rb +15 -11
- data/lib/samagotchi/web/session_control.rb +9 -3
- data/lib/samagotchi/web/session_create.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +50 -5
- data/lib/samagotchi/web/session_summary.rb +5 -2
- data/lib/samagotchi/worker.rb +61 -11
- data/lib/samagotchi/worker_boot.rb +22 -11
- data/lib/samagotchi/worker_inbound.rb +62 -10
- data/lib/samagotchi/worker_link.rb +4 -1
- data/lib/samagotchi/worker_presence.rb +106 -0
- data/lib/samagotchi/worker_sidecar.rb +3 -4
- metadata +9 -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 "../state_format"
|
|
7
8
|
require_relative "card_places"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
@@ -62,6 +63,11 @@ module Samagotchi
|
|
|
62
63
|
# count becomes +base_since+, and new openings add to it (#seed_v1).
|
|
63
64
|
class CardStore
|
|
64
65
|
CAPACITY = 20
|
|
66
|
+
FORMAT_VERSION = 2
|
|
67
|
+
FORMAT = StateFormat.new(
|
|
68
|
+
version: FORMAT_VERSION,
|
|
69
|
+
members: [StateFormat.required(:type), StateFormat.extra(:extra, in_version: FORMAT_VERSION)]
|
|
70
|
+
)
|
|
65
71
|
NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry steer_cut].freeze
|
|
66
72
|
# Capped apart from the notices.
|
|
67
73
|
LLM_CONTEXT_TYPES = %i[llm_context_edited].freeze
|
|
@@ -98,6 +104,8 @@ module Samagotchi
|
|
|
98
104
|
@mutex = Mutex.new
|
|
99
105
|
@entries = []
|
|
100
106
|
@turn_log = []
|
|
107
|
+
@file_extra = {}
|
|
108
|
+
@format_read_only = false
|
|
101
109
|
@running_turn = nil
|
|
102
110
|
@continuing = false
|
|
103
111
|
@failed_turn = nil
|
|
@@ -180,13 +188,14 @@ module Samagotchi
|
|
|
180
188
|
|
|
181
189
|
def save
|
|
182
190
|
return unless @path
|
|
191
|
+
return if @format_read_only
|
|
183
192
|
|
|
184
193
|
entries = @entries.reject { |entry| entry[:type] == :guardrail_warning }
|
|
185
194
|
# A session that never showed one gets no file.
|
|
186
195
|
return if entries.empty? && !File.exist?(@path)
|
|
187
196
|
|
|
188
197
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
189
|
-
saved =
|
|
198
|
+
saved = @file_extra.merge("version" => FORMAT_VERSION, "turn_log" => pruned_log(entries), "entries" => entries)
|
|
190
199
|
saved["failed_turn"] = @failed_turn if @failed_turn
|
|
191
200
|
AtomicFile.write(@path, JSON.generate(saved))
|
|
192
201
|
rescue StandardError
|
|
@@ -222,15 +231,34 @@ module Samagotchi
|
|
|
222
231
|
return unless @path && File.file?(@path)
|
|
223
232
|
|
|
224
233
|
data = JSON.parse(File.read(@path), symbolize_names: true)
|
|
225
|
-
return unless data.is_a?(Hash)
|
|
234
|
+
return unless data.is_a?(Hash)
|
|
235
|
+
|
|
236
|
+
format_data = data.transform_keys(&:to_s)
|
|
237
|
+
format_data["entries"] = if data[:entries].is_a?(Array)
|
|
238
|
+
data[:entries].map { |entry| entry.is_a?(Hash) ? entry.transform_keys(&:to_s) : entry }
|
|
239
|
+
else
|
|
240
|
+
data[:entries]
|
|
241
|
+
end
|
|
242
|
+
document = FORMAT.read(format_data, collection: "entries")
|
|
243
|
+
if document.version > FORMAT_VERSION
|
|
244
|
+
raise StateFormat::InvalidDocument,
|
|
245
|
+
"cards.json has format version #{document.version}, newer than supported #{FORMAT_VERSION}; update chi before writing it"
|
|
246
|
+
end
|
|
247
|
+
@file_extra = document.extra.except("version", "entries", "turns", "turn_log", "failed_turn")
|
|
248
|
+
return unless data[:entries].is_a?(Array)
|
|
226
249
|
|
|
227
250
|
if data[:turn_log].is_a?(Array)
|
|
228
251
|
@turn_log = data[:turn_log].grep(String)
|
|
229
252
|
@failed_turn = data[:failed_turn] if data[:failed_turn].is_a?(String)
|
|
230
|
-
@entries = seeded(
|
|
253
|
+
@entries = seeded(document.entries.map { |entry| entry.to_h.transform_keys(&:to_sym) })
|
|
231
254
|
elsif data[:turns].is_a?(Integer)
|
|
232
255
|
seed_v1(data)
|
|
233
256
|
end
|
|
257
|
+
rescue StateFormat::InvalidDocument => e
|
|
258
|
+
@format_read_only = true
|
|
259
|
+
@turn_log = []
|
|
260
|
+
@entries = []
|
|
261
|
+
Log.warn(:bridge, "card_store_unreadable", error: e.message)
|
|
234
262
|
rescue StandardError
|
|
235
263
|
@turn_log = []
|
|
236
264
|
@entries = []
|
|
@@ -18,13 +18,17 @@ module Samagotchi
|
|
|
18
18
|
@capacity = [1, capacity].max
|
|
19
19
|
@mutex = Monitor.new
|
|
20
20
|
@events = [] # Array<{seq:, data:}>
|
|
21
|
+
@dropped = false
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Append +data+ keyed by +seq+ (drop oldest beyond capacity).
|
|
24
25
|
def push(seq:, data:)
|
|
25
26
|
@mutex.synchronize do
|
|
26
27
|
@events << { seq: seq, data: data }
|
|
27
|
-
|
|
28
|
+
while @events.size > @capacity
|
|
29
|
+
@events.shift
|
|
30
|
+
@dropped = true
|
|
31
|
+
end
|
|
28
32
|
nil
|
|
29
33
|
end
|
|
30
34
|
end
|
|
@@ -45,6 +49,12 @@ module Samagotchi
|
|
|
45
49
|
@mutex.synchronize { @events.first&.[](:seq) }
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
# @return [Boolean] whether a push has ever evicted an event: the window
|
|
53
|
+
# no longer starts where the capture did.
|
|
54
|
+
def dropped?
|
|
55
|
+
@mutex.synchronize { @dropped }
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
def empty?
|
|
49
59
|
@mutex.synchronize { @events.empty? }
|
|
50
60
|
end
|
|
@@ -161,16 +161,19 @@ module Samagotchi
|
|
|
161
161
|
def replay!(io, snapshot_seq)
|
|
162
162
|
from_seq = @high_water
|
|
163
163
|
oldest = @ring.oldest_seq
|
|
164
|
-
if @foreign_cursor || from_seq > snapshot_seq || (
|
|
164
|
+
if @foreign_cursor || from_seq > snapshot_seq || (oldest && from_seq < oldest - 1 && (from_seq.positive? || @ring.dropped?))
|
|
165
165
|
# Cursor is from another worker (epoch), ahead of the engine (a
|
|
166
166
|
# worker restarted, plain cursor) or behind the
|
|
167
167
|
# ring's buffered window (overflow): the server cannot replay the
|
|
168
168
|
# gap, so it emits a reset marker and the client re-syncs. A cursor
|
|
169
169
|
# at oldest - 1 has seen everything before the window, so it
|
|
170
|
-
# replays. A cold
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
170
|
+
# replays. A cold connect (from_seq == 0) is behind the window too
|
|
171
|
+
# once the ring has evicted an event (it would start mid-turn), so
|
|
172
|
+
# it resets; on a ring that never evicted (its first seq may be
|
|
173
|
+
# above 1: events before the Bridge started) it replays the whole
|
|
174
|
+
# window. A fully caught-up cursor (from_seq == snapshot_seq)
|
|
175
|
+
# is NOT a reset case — it gets an empty replay and holds for live
|
|
176
|
+
# events.
|
|
174
177
|
emit_reset(io)
|
|
175
178
|
else
|
|
176
179
|
@ring.events_in_range(after_seq: from_seq, to_seq: snapshot_seq).each { |record| write_event(io, record[:data]) }
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "openssl"
|
|
|
9
9
|
require "time"
|
|
10
10
|
require_relative "atomic_file"
|
|
11
11
|
require_relative "bridge_secret"
|
|
12
|
+
require_relative "command_intake"
|
|
12
13
|
require_relative "degrade"
|
|
13
14
|
require_relative "delivery"
|
|
14
15
|
require_relative "session_inbox"
|
|
@@ -159,6 +160,9 @@ module Samagotchi
|
|
|
159
160
|
@connection_threads = []
|
|
160
161
|
@stopped = false
|
|
161
162
|
@mutex = Monitor.new
|
|
163
|
+
@post_turn_mutex = Mutex.new
|
|
164
|
+
@enqueued_requests = {}
|
|
165
|
+
@enqueued_request_order = []
|
|
162
166
|
@refused_log_mutex = Mutex.new
|
|
163
167
|
@refused_logged_at = {}
|
|
164
168
|
@open_streams = 0
|
|
@@ -766,14 +770,23 @@ module Samagotchi
|
|
|
766
770
|
def handle_cancel(_session_id, body)
|
|
767
771
|
# Optional reason from JSON body
|
|
768
772
|
reason = :manual
|
|
773
|
+
turn_id = nil
|
|
769
774
|
if body && !body.strip.empty?
|
|
770
775
|
parsed = parse_json(body)
|
|
771
776
|
r = parsed.is_a?(Hash) ? (fetched(parsed, "reason") || fetched(parsed, "cancellation_reason")) : nil
|
|
772
777
|
reason = CANCEL_REASONS.include?(r.to_s.strip) ? r.to_s.strip.to_sym : :manual
|
|
778
|
+
turn_id = fetched(parsed, "turn_id").to_s.strip if parsed.is_a?(Hash)
|
|
779
|
+
turn_id = nil if turn_id && turn_id.empty?
|
|
773
780
|
end
|
|
774
781
|
|
|
775
782
|
if @engine.turn_running? && @engine.active_cancel_controller
|
|
776
|
-
|
|
783
|
+
# With a turn_id the engine compares it to the running turn's under
|
|
784
|
+
# its lock; clients that send none (older ones) cancel whichever runs.
|
|
785
|
+
ok = turn_id ? @engine.cancel_current_turn!(reason, turn_id: turn_id) : @engine.cancel_current_turn!(reason)
|
|
786
|
+
if ok == :turn_mismatch
|
|
787
|
+
return [{}, 409, { error: "turn_mismatch", detail: "the running turn is not the one named", session_id: @session_id }]
|
|
788
|
+
end
|
|
789
|
+
|
|
777
790
|
return [{}, 202, { status: "cancel_requested", session_id: @session_id, reason: reason.to_s }] if ok
|
|
778
791
|
|
|
779
792
|
[{}, 409, { error: "cancel_failed", detail: "could not cancel", session_id: @session_id }]
|
|
@@ -1195,34 +1208,60 @@ module Samagotchi
|
|
|
1195
1208
|
# A session command sent as a message (chi send -m "/model x", chi -p,
|
|
1196
1209
|
# a UI whose command list lags) runs as the command, as typed in a
|
|
1197
1210
|
# TUI; an unknown /word stays a prompt. Not with images: those are
|
|
1198
|
-
# for the model.
|
|
1211
|
+
# for the model. Only from a sender that may send commands
|
|
1212
|
+
# (CommandIntake.allowed?: a person, chi answer): a delegate's, a
|
|
1213
|
+
# schedule's or a plugin's /word line is a prompt, its shell line
|
|
1214
|
+
# refused (403, nothing queued).
|
|
1199
1215
|
if images.empty? && @on_command && command_registry.command?(prompt.to_s)
|
|
1200
|
-
|
|
1216
|
+
if CommandIntake.allowed?(client_id)
|
|
1217
|
+
return queue_command_request(sid, prompt.to_s.strip, client_id: client_id, deadline: deadline, author: author)
|
|
1218
|
+
elsif CommandIntake.shell_line?(prompt)
|
|
1219
|
+
Log.warn(:bridge, "shell_line_refused", client_id: client_id)
|
|
1220
|
+
return [{}, 403, { error: CommandIntake::REFUSED_ERROR, detail: CommandIntake::REFUSED }]
|
|
1221
|
+
end
|
|
1201
1222
|
end
|
|
1202
1223
|
|
|
1203
|
-
enqueued_id =
|
|
1224
|
+
enqueued_id = fetched(parsed, "enqueued_id")
|
|
1225
|
+
enqueued_id = SessionInbox.new_enqueued_id unless SessionInbox.valid_enqueued_id?(enqueued_id)
|
|
1226
|
+
enqueue_post_turn(sid: sid, prompt: prompt, client_id: client_id, no_interrupt: no_interrupt, author: author,
|
|
1227
|
+
anchors: anchors, images: images, delivery: delivery, deadline: deadline, pending_limit: pending_limit,
|
|
1228
|
+
enqueued_id: enqueued_id)
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
def enqueue_post_turn(sid:, prompt:, client_id:, no_interrupt:, author:, anchors:, images:, delivery:, deadline:,
|
|
1232
|
+
pending_limit:, enqueued_id:)
|
|
1204
1233
|
# Before it is queued: a drain after this may have taken it (#cut_for_steer).
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1234
|
+
@post_turn_mutex.synchronize do
|
|
1235
|
+
epoch = @engine.input_epoch
|
|
1236
|
+
# Whether a queue waits for a running turn: read with the write under
|
|
1237
|
+
# the event log, so no :turn_started or :turn_completed falls between.
|
|
1238
|
+
# To an idle worker the queued file simply runs as the next turn.
|
|
1239
|
+
queued_behind_turn = false
|
|
1240
|
+
# Write and announce with the event log held: the worker can't emit
|
|
1241
|
+
# this turn's :turn_started (or merge it mid-turn) before
|
|
1242
|
+
# :turn_enqueued, and a failed write announces nothing.
|
|
1243
|
+
enqueued = @engine.synchronize_events do
|
|
1244
|
+
raise IntakeClosed if stopped?
|
|
1245
|
+
|
|
1246
|
+
existing = @enqueued_requests[enqueued_id]
|
|
1247
|
+
if existing
|
|
1248
|
+
next(existing[:prompt] == prompt.to_s ? :duplicate : :conflict)
|
|
1249
|
+
end
|
|
1250
|
+
next :duplicate if @engine.enqueued_id?(enqueued_id)
|
|
1251
|
+
next :expired if expired?("turn_expired", deadline, sid: sid, client_id: client_id)
|
|
1252
|
+
next :too_many if pending_limit && pending_turns(author) >= pending_limit
|
|
1253
|
+
next :duplicate unless @engine.reserve_enqueued_id(enqueued_id)
|
|
1254
|
+
|
|
1255
|
+
# The owner's input never goes into a participant's read-only
|
|
1256
|
+
# review turn (a steer, a cut): it queues behind it as the owner's
|
|
1257
|
+
# own turn, and the ack and :turn_enqueued say so.
|
|
1258
|
+
delivery = Delivery::QUEUE if @accumulator.participant_turn?
|
|
1259
|
+
queued_behind_turn = Delivery.queue?(delivery) && !@accumulator.current_turn.nil?
|
|
1260
|
+
unless enqueue_turn(prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
1261
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery, author: author, anchors: anchors)
|
|
1262
|
+
@engine.forget_enqueued_id(enqueued_id)
|
|
1263
|
+
next false
|
|
1264
|
+
end
|
|
1226
1265
|
|
|
1227
1266
|
enqueued_event = { type: :turn_enqueued, enqueued_id: enqueued_id, client_id: client_id, prompt: prompt.to_s }
|
|
1228
1267
|
enqueued_event[:images] = images unless images.empty?
|
|
@@ -1230,26 +1269,43 @@ module Samagotchi
|
|
|
1230
1269
|
enqueued_event[:author] = author if author
|
|
1231
1270
|
@engine.announce(enqueued_event)
|
|
1232
1271
|
@on_input&.call
|
|
1272
|
+
true
|
|
1273
|
+
end
|
|
1274
|
+
if enqueued == :duplicate
|
|
1275
|
+
ack = @enqueued_requests.dig(enqueued_id, :ack) ||
|
|
1276
|
+
{ status: "accepted", enqueued_id: enqueued_id, session_id: @session_id }
|
|
1277
|
+
return [{}, 202, ack]
|
|
1278
|
+
end
|
|
1279
|
+
if enqueued == :conflict
|
|
1280
|
+
return [{}, 409, { error: "enqueued_id_conflict",
|
|
1281
|
+
detail: "enqueued_id was already accepted for a different prompt" }]
|
|
1233
1282
|
end
|
|
1283
|
+
return deadline_passed("turn") if enqueued == :expired
|
|
1284
|
+
return [{}, 429, { error: "too_many_turns", detail: ParticipantMessages::TOO_MANY_TURNS }] if enqueued == :too_many
|
|
1285
|
+
return [{}, 500, { error: "enqueue_failed", detail: "could not write turn input" }] unless enqueued
|
|
1286
|
+
|
|
1287
|
+
# Queued (outside the event lock): a message sent as +cut+ cuts a
|
|
1288
|
+
# running generation that has only been thinking for long, unless a
|
|
1289
|
+
# plugin or a client chi doesn't know sent it. An image input never
|
|
1290
|
+
# merges mid-turn, so it never cuts. Nothing else cuts: a message
|
|
1291
|
+
# that says nothing (an older client, the web composer) goes in at
|
|
1292
|
+
# the running turn's next step.
|
|
1293
|
+
cut = nil
|
|
1294
|
+
cut = @engine.cut_for_steer(Steer.source_for_client(client_id), epoch: epoch) if images.empty? && Delivery.cut?(delivery)
|
|
1295
|
+
# A queue to an idle worker is a turn now: the ack says next_step.
|
|
1296
|
+
ack_delivery = Delivery.queue?(delivery) && !queued_behind_turn ? Delivery::NEXT_STEP : delivery
|
|
1297
|
+
ack = { status: "accepted", enqueued_id: enqueued_id, session_id: @session_id, delivery: ack_delivery }
|
|
1298
|
+
ack[:cut] = cut.to_s if cut
|
|
1299
|
+
ack[:cut_after] = @engine.steer_cut_after if cut == :waits
|
|
1300
|
+
remember_enqueued_request(enqueued_id, prompt, ack)
|
|
1301
|
+
[{}, 202, ack]
|
|
1234
1302
|
end
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
# plugin or a client chi doesn't know sent it. An image input never
|
|
1242
|
-
# merges mid-turn, so it never cuts. Nothing else cuts: a message
|
|
1243
|
-
# that says nothing (an older client, the web composer) goes in at
|
|
1244
|
-
# the running turn's next step.
|
|
1245
|
-
cut = nil
|
|
1246
|
-
cut = @engine.cut_for_steer(Steer.source_for_client(client_id), epoch: epoch) if images.empty? && Delivery.cut?(delivery)
|
|
1247
|
-
# A queue to an idle worker is a turn now: the ack says next_step.
|
|
1248
|
-
ack_delivery = Delivery.queue?(delivery) && !queued_behind_turn ? Delivery::NEXT_STEP : delivery
|
|
1249
|
-
ack = { status: "accepted", enqueued_id: enqueued_id, session_id: @session_id, delivery: ack_delivery }
|
|
1250
|
-
ack[:cut] = cut.to_s if cut
|
|
1251
|
-
ack[:cut_after] = @engine.steer_cut_after if cut == :waits
|
|
1252
|
-
[{}, 202, ack]
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
def remember_enqueued_request(id, prompt, ack)
|
|
1306
|
+
@enqueued_requests[id] = { prompt: prompt.to_s, ack: ack }
|
|
1307
|
+
@enqueued_request_order << id
|
|
1308
|
+
@enqueued_requests.delete(@enqueued_request_order.shift) while @enqueued_requests.size > TurnState::ENQUEUED_ID_HISTORY
|
|
1253
1309
|
end
|
|
1254
1310
|
|
|
1255
1311
|
# POST /session/:id/turn/dequeue {enqueued_id, client_id?, deadline?}:
|
|
@@ -1281,7 +1337,10 @@ module Samagotchi
|
|
|
1281
1337
|
next [:expired, nil] if expired?("dequeue_expired", deadline, sid: session_id, enqueued_id: id)
|
|
1282
1338
|
|
|
1283
1339
|
SessionInbox.take_queued_input(session_dir, id).tap do |status, _|
|
|
1284
|
-
|
|
1340
|
+
if status == :removed
|
|
1341
|
+
@engine.drop_enqueued_id(id)
|
|
1342
|
+
@engine.announce(type: :turn_dequeued, enqueued_id: id, client_id: client_id)
|
|
1343
|
+
end
|
|
1285
1344
|
end
|
|
1286
1345
|
end
|
|
1287
1346
|
dequeue_reply(taken, id, input)
|
|
@@ -8,6 +8,7 @@ require_relative "degrade"
|
|
|
8
8
|
require_relative "worker_sidecar"
|
|
9
9
|
require_relative "bridge_secret"
|
|
10
10
|
require_relative "delivery"
|
|
11
|
+
require_relative "session_inbox"
|
|
11
12
|
require_relative "chi_invocation"
|
|
12
13
|
require_relative "bridge_client/stream_open"
|
|
13
14
|
require_relative "bridge_client/sse_parser"
|
|
@@ -197,9 +198,10 @@ module Samagotchi
|
|
|
197
198
|
# many of their turns may be running or waiting (429 too_many_turns past it)
|
|
198
199
|
# @param anchors [TurnAnchors, Hash, nil] a Verify turn's claim, a Revise
|
|
199
200
|
# turn's draft: their own body keys ("verify", "revises")
|
|
200
|
-
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil, delivery: nil, author: nil,
|
|
201
|
-
anchors: nil)
|
|
202
|
-
body = { session_id: @session_id, prompt: prompt, client_id: client_id
|
|
201
|
+
def post_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: nil, delivery: nil, author: nil,
|
|
202
|
+
pending_limit: nil, anchors: nil)
|
|
203
|
+
body = { session_id: @session_id, prompt: prompt, client_id: client_id,
|
|
204
|
+
enqueued_id: enqueued_id || SessionInbox.new_enqueued_id }
|
|
203
205
|
body[:no_interrupt] = true if no_interrupt
|
|
204
206
|
body[:author] = author if author
|
|
205
207
|
body[:pending_limit] = pending_limit if author && pending_limit
|
|
@@ -246,10 +248,11 @@ module Samagotchi
|
|
|
246
248
|
end
|
|
247
249
|
|
|
248
250
|
# POST /session/:id/cancel. 202 = requested (body names the reason the
|
|
249
|
-
# Bridge took), 409 = no active turn
|
|
251
|
+
# Bridge took), 409 = no active turn, or (error "turn_mismatch") not the
|
|
252
|
+
# +turn_id+ named: nothing was cancelled.
|
|
250
253
|
# @return [Response]
|
|
251
|
-
def cancel(reason:)
|
|
252
|
-
post("cancel", { reason: reason }, read_body: true)
|
|
254
|
+
def cancel(reason:, turn_id: nil)
|
|
255
|
+
post("cancel", { reason: reason, turn_id: turn_id }.compact, read_body: true)
|
|
253
256
|
end
|
|
254
257
|
|
|
255
258
|
# POST /session/:id/tasks/stop: stop a background task this session's
|
|
@@ -46,7 +46,8 @@ module Samagotchi
|
|
|
46
46
|
session = Session.load(recipient.id, state_dir: state_dir)
|
|
47
47
|
prompts = user_texts(session.messages)
|
|
48
48
|
branch = GitHead.branch(session.working_directory)
|
|
49
|
-
sources = ContextSources.attached(session.id, project_root: session.project_root,
|
|
49
|
+
sources = ContextSources.attached(session.id, project_root: session.project_root, project_id: session.project_id,
|
|
50
|
+
state_dir: state_dir)
|
|
50
51
|
.map { |attached| [attached.name, attached.source.hint] }
|
|
51
52
|
ScopeCard.new(id: session.id, project: recipient.project && File.basename(recipient.project),
|
|
52
53
|
folder: folder(session.working_directory, recipient.project), branch: branch,
|
|
@@ -249,13 +249,20 @@ module Samagotchi
|
|
|
249
249
|
# +decisions+, prints a line per recipient and the summary, and logs it.
|
|
250
250
|
def deliver_all(broadcast, decisions, cards)
|
|
251
251
|
ok = true
|
|
252
|
+
# Two links of one chain are one session: the note goes once (Head).
|
|
253
|
+
got = {}
|
|
252
254
|
lines = decisions.map do |d|
|
|
253
255
|
next [d, "skipped", d.verdict.reason] unless d.deliver?
|
|
254
256
|
|
|
257
|
+
head = SessionInbox::Head.resolve(:note, d.recipient.id, state_dir: @state_dir)
|
|
258
|
+
next [d, "skipped", "#{head.id[0, 8]}, its chain's newest link, got it already"] if got[head.id]
|
|
259
|
+
|
|
255
260
|
result = NoteDelivery.deliver(d.recipient.id, text: body(broadcast.text, d.verdict), source: SOURCE,
|
|
256
|
-
state_dir: @state_dir)
|
|
261
|
+
state_dir: @state_dir, resolved: head)
|
|
257
262
|
next [d, "skipped", OwnerLock::TUI_BUSY] unless result.delivered?
|
|
258
263
|
|
|
264
|
+
got[result.id] = true
|
|
265
|
+
|
|
259
266
|
[d, "delivered", "#{d.verdict.reason}#{"; waits for its next start" if result.status == :waits}"]
|
|
260
267
|
rescue SessionInbox::NoteRejected, SystemCallError => e
|
|
261
268
|
ok = false
|
|
@@ -333,7 +340,9 @@ module Samagotchi
|
|
|
333
340
|
return 1
|
|
334
341
|
end
|
|
335
342
|
ids = sessions.map { |given| resolve_session(given) }
|
|
336
|
-
|
|
343
|
+
# Two links of one chain are one session: the note goes once (Head).
|
|
344
|
+
results = ids.compact.map { |id| SessionInbox::Head.resolve(:note, id, state_dir: @state_dir) }
|
|
345
|
+
.uniq(&:id).map { |head| deliver_anyway(record, head) }
|
|
337
346
|
unless results.empty?
|
|
338
347
|
begin
|
|
339
348
|
@log.append_correction(id: record.id, at: @now, sessions: results)
|
|
@@ -359,22 +368,29 @@ module Samagotchi
|
|
|
359
368
|
nil
|
|
360
369
|
end
|
|
361
370
|
|
|
362
|
-
# @param
|
|
371
|
+
# @param head [SessionInbox::Resolved] the session named (+asked+, a full
|
|
372
|
+
# id: #resolve_session) and the link that gets it (a continued one's
|
|
373
|
+
# chain's newest)
|
|
363
374
|
# @return [Broadcast::TriageLog::Delivery]
|
|
364
|
-
def deliver_anyway(record,
|
|
365
|
-
|
|
375
|
+
def deliver_anyway(record, head)
|
|
376
|
+
id = head.asked
|
|
377
|
+
if record.delivered?(id) || record.delivered?(head.id)
|
|
366
378
|
@stdout.puts(line_for(id, "skipped", "it got #{record.id} already"))
|
|
367
379
|
return Broadcast::TriageLog::Delivery.new(session: id, result: "had it")
|
|
368
380
|
end
|
|
369
381
|
|
|
370
|
-
result = NoteDelivery.deliver(id, text: passed_on_body(record), source: SOURCE, state_dir: @state_dir
|
|
382
|
+
result = NoteDelivery.deliver(id, text: passed_on_body(record), source: SOURCE, state_dir: @state_dir,
|
|
383
|
+
resolved: head)
|
|
371
384
|
unless result.delivered?
|
|
372
385
|
@stdout.puts(line_for(id, "skipped", OwnerLock::TUI_BUSY))
|
|
373
386
|
return Broadcast::TriageLog::Delivery.new(session: id, result: "refused")
|
|
374
387
|
end
|
|
375
388
|
|
|
376
|
-
|
|
377
|
-
|
|
389
|
+
how = "by hand#{"; to #{result.id[0, 8]}, its chain's newest link" if result.continued?}" \
|
|
390
|
+
"#{"; waits for its next start" if result.status == :waits}"
|
|
391
|
+
@stdout.puts(line_for(id, "delivered", how))
|
|
392
|
+
# Logged under the link that has it, so the next deliver finds it.
|
|
393
|
+
Broadcast::TriageLog::Delivery.new(session: result.id, result: "delivered")
|
|
378
394
|
rescue SessionInbox::NoteRejected, SystemCallError => e
|
|
379
395
|
@stdout.puts(line_for(id, "failed", e.message))
|
|
380
396
|
Broadcast::TriageLog::Delivery.new(session: id, result: "failed")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "cli/flags"
|
|
4
4
|
require_relative "cli/exit"
|
|
5
|
+
require_relative "live_versions"
|
|
5
6
|
require_relative "memory_bundle"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
@@ -17,6 +18,7 @@ module Samagotchi
|
|
|
17
18
|
Source is the path/URL to the bundle (dir/zip/tar.gz/git).
|
|
18
19
|
--force overwrites existing entries (one that differs is copied to the trash first); default skips them.
|
|
19
20
|
A bundle whose requires_chi this chi doesn't meet is refused; --force installs it anyway.
|
|
21
|
+
So is a memory, plugin or script whose bytes disagree with its manifest sha256; --force installs it too.
|
|
20
22
|
|
|
21
23
|
upgrade <source|name> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
|
|
22
24
|
Upgrade a bundle (3-way: a file you didn't edit updates; one you edited takes a text
|
|
@@ -24,6 +26,7 @@ module Samagotchi
|
|
|
24
26
|
A bare NAME upgrades the bundle installed under that name from the source it came from.
|
|
25
27
|
--dry-run shows what would change. --agent launches interactive session on conflict.
|
|
26
28
|
A bundle whose requires_chi this chi doesn't meet is refused; --force installs it anyway.
|
|
29
|
+
So is a file whose bytes disagree with its manifest sha256; --force installs it too.
|
|
27
30
|
|
|
28
31
|
uninstall <bundle> [--scope system|project] [--force]
|
|
29
32
|
Remove a bundle and its index entries (--force moves a locally edited file to the trash).
|
|
@@ -73,8 +76,8 @@ module Samagotchi
|
|
|
73
76
|
Install a memory bundle from a directory, zip, tar archive, or git URL,
|
|
74
77
|
or a bundle shipped with chi by name (see: chi bundle list).
|
|
75
78
|
--force overwrites existing entries (one that differs is copied to the trash first); default skips them.
|
|
76
|
-
It also installs a bundle whose requires_chi this chi doesn't meet
|
|
77
|
-
refused, and the bundle is not written anywhere.
|
|
79
|
+
It also installs a bundle whose requires_chi this chi doesn't meet and a file whose bytes disagree with
|
|
80
|
+
its manifest sha256: without --force both are refused, and the bundle is not written anywhere.
|
|
78
81
|
install --dev PATH
|
|
79
82
|
Install the bundle in the local directory PATH as a link to it: its plugin, rules and scripts
|
|
80
83
|
are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
|
|
@@ -83,8 +86,9 @@ module Samagotchi
|
|
|
83
86
|
TEXT
|
|
84
87
|
UPGRADE_HELP = <<~TEXT
|
|
85
88
|
Usage: chi bundle upgrade <source|name> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
|
|
86
|
-
--force takes the bundle's file over an edited one,
|
|
87
|
-
doesn't meet (without it
|
|
89
|
+
--force takes the bundle's file over an edited one, installs a bundle whose requires_chi this chi
|
|
90
|
+
doesn't meet, and installs a file whose bytes disagree with its manifest sha256 (without it each is
|
|
91
|
+
refused, nothing written).
|
|
88
92
|
TEXT
|
|
89
93
|
UNINSTALL_HELP = "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
|
|
90
94
|
BUILD_HELP = <<~TEXT
|
|
@@ -246,7 +250,12 @@ module Samagotchi
|
|
|
246
250
|
end
|
|
247
251
|
end
|
|
248
252
|
installer = installer_for(expanded_source, bundle_name, prepared: prepared, scope: scope, force: force, upgrade: true, dry_run: dry_run)
|
|
249
|
-
|
|
253
|
+
# The record and the manifest as they were/are, for the hint below: it
|
|
254
|
+
# reads which plugin the run touched.
|
|
255
|
+
before = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).record
|
|
256
|
+
manifest = nil
|
|
257
|
+
status = run_installer(installer, failed: "Upgrade") do |run_manifest|
|
|
258
|
+
manifest = run_manifest
|
|
250
259
|
print_plugin(manifest, plugin: !dry_run)
|
|
251
260
|
next dry_run_done if dry_run
|
|
252
261
|
|
|
@@ -256,6 +265,8 @@ module Samagotchi
|
|
|
256
265
|
|
|
257
266
|
print_provenance(bundle_name, manifest)
|
|
258
267
|
end
|
|
268
|
+
restart_hint(before, manifest, installer) if !dry_run && status != 1
|
|
269
|
+
status
|
|
259
270
|
end
|
|
260
271
|
|
|
261
272
|
# The source normalized once (a git clone, a zip or tar extract) and the
|
|
@@ -266,6 +277,7 @@ module Samagotchi
|
|
|
266
277
|
# @return [Array(MemoryBundle::PreparedSource, String), nil]
|
|
267
278
|
def prepare_or_fail(expanded_source, word)
|
|
268
279
|
prepared = Samagotchi::MemoryBundle::PreparedSource.prepare(expanded_source)
|
|
280
|
+
Samagotchi::MemoryBundle::Manifest::HooksRetired.refuse_source_files(prepared.dir)
|
|
269
281
|
named = [prepared, Samagotchi::MemoryBundle::Manifest.read(dir: prepared.dir).name]
|
|
270
282
|
rescue Samagotchi::MemoryBundle::SourceNormalizer::UnknownSourceError,
|
|
271
283
|
Samagotchi::MemoryBundle::Manifest::ValidationError => e
|
|
@@ -299,7 +311,10 @@ module Samagotchi
|
|
|
299
311
|
end
|
|
300
312
|
|
|
301
313
|
def installer_for(source, name, scope:, force:, **)
|
|
302
|
-
|
|
314
|
+
# A bundle the user installs: a memory, plugin or script whose bytes
|
|
315
|
+
# disagree with its manifest sha is refused, not warned (--force past it).
|
|
316
|
+
Samagotchi::MemoryBundle::Installer.new(source: source, name: name, scope: scope, force: force, strict: true,
|
|
317
|
+
refuse_mismatch: true, **)
|
|
303
318
|
end
|
|
304
319
|
|
|
305
320
|
# Runs +installer+, prints its summary, then yields the manifest.
|
|
@@ -339,6 +354,41 @@ module Samagotchi
|
|
|
339
354
|
0
|
|
340
355
|
end
|
|
341
356
|
|
|
357
|
+
# A run wrote or took away a file (Installer#results' words).
|
|
358
|
+
CHANGED_WORDS = %i[installed updated merged removed restored].freeze
|
|
359
|
+
|
|
360
|
+
# Only a bundle's plugin stays in a worker's memory: its rules reload by
|
|
361
|
+
# their stamp (GuardrailWiring#rules_stamp), its scripts run from the files
|
|
362
|
+
# the install replaced, and chi no longer loads class-file hooks. So an
|
|
363
|
+
# upgrade or uninstall that changed the plugin -- or brought one in --
|
|
364
|
+
# leaves the old code in every session already up: one line, after the
|
|
365
|
+
# result, with how many sessions and the command for each. Nothing when the
|
|
366
|
+
# plugin is unchanged or there never was one, nothing with no worker that
|
|
367
|
+
# can be restarted.
|
|
368
|
+
# @param before [InstalledBundle, nil] the bundle's record as it was before
|
|
369
|
+
# the run (an uninstall deletes it, so take it first)
|
|
370
|
+
# @param manifest [MemoryBundle::Manifest, nil] the manifest this run
|
|
371
|
+
# installed; nil for an uninstall
|
|
372
|
+
# @param installer [Installer, nil] the run, for what it wrote to the plugin
|
|
373
|
+
def restart_hint(before, manifest = nil, installer = nil)
|
|
374
|
+
return if before.nil? || before.error?
|
|
375
|
+
|
|
376
|
+
plugin_files = [before.plugin&.file, manifest&.plugin&.dig(:file)].compact.uniq
|
|
377
|
+
changed = if installer
|
|
378
|
+
plugin_files.any? { |file| CHANGED_WORDS.include?(installer.results[file]&.word) }
|
|
379
|
+
else
|
|
380
|
+
!before.plugin.nil?
|
|
381
|
+
end
|
|
382
|
+
return unless changed
|
|
383
|
+
|
|
384
|
+
ids = Samagotchi::LiveVersions.workers.select(&:restart).map { |worker| worker.session_id.to_s[0, 8] }
|
|
385
|
+
return if ids.empty?
|
|
386
|
+
|
|
387
|
+
commands = ids.map { |id| "chi sessions restart #{id}" }
|
|
388
|
+
@stdout.puts "#{ids.size} session#{"s" unless ids.size == 1} " \
|
|
389
|
+
"#{ids.size == 1 ? "runs" : "run"} the old code: #{commands.join(", ")}"
|
|
390
|
+
end
|
|
391
|
+
|
|
342
392
|
# An upgrade that kept edited files: launch an agent to merge them
|
|
343
393
|
# (--agent, or yes on a terminal), or say how to take the bundle's.
|
|
344
394
|
# An upgrade that kept edited files exits 2, as a usage error does
|
|
@@ -426,6 +476,7 @@ module Samagotchi
|
|
|
426
476
|
@stdout.puts uninstaller.trash_copy_line if uninstaller.trash_copy_line
|
|
427
477
|
@stdout.puts "Removed: #{uninstaller.removed_files.join(", ")}" unless uninstaller.removed_files.empty?
|
|
428
478
|
uninstaller.warnings.each { |w| @stdout.puts w }
|
|
479
|
+
restart_hint(record)
|
|
429
480
|
0
|
|
430
481
|
rescue Samagotchi::MemoryBundle::Uninstaller::UninstallError => e
|
|
431
482
|
@stderr.puts "Uninstall failed: #{e.message}"; 1
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.161.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:750822b3488b302c6e32638e6224f270d8e06b50ea85cd6e0bb5a6ff12eed1d8
|
|
9
9
|
config_modification_protocol.md: sha256:44cf7bdac8e526805bff36b76b54fefecd41db30ca525c2ed661d78fe9155dd8
|
|
10
|
-
memory_guide.md: sha256:
|
|
10
|
+
memory_guide.md: sha256:bdd34f34063df1b4ae74f7cda7c31080986d155e535ee4526d673c70edac8292
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -13,7 +13,7 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
|
|
|
13
13
|
| Scope | Path | Use for |
|
|
14
14
|
|-------|------|---------|
|
|
15
15
|
| `system` | `$XDG_CONFIG_HOME/samagotchi/memories/`, default `~/.config/samagotchi/memories/` (`MemoryPaths.system_dir`) | User-wide preferences, identity, cross-project knowledge |
|
|
16
|
-
| `project` | `<system dir>/projects/<
|
|
16
|
+
| `project` | `<system dir>/projects/<key>/` (`MemoryPaths.project_dir`: a git project's `<basename>_<hash>`, `basename(root)` + 8-char `MD5(root)`, root = `MemoryPaths.project_root`; a marked project's id from its `.chi/project.yml`, `<basename>-<8 hex>`, kept when the folder moves) | Repo-specific conventions, workflow, stack decisions |
|
|
17
17
|
|
|
18
18
|
- The project root is the git repository: its common git dir, which every linked worktree shares. All worktrees and subdirectories of one repository share one project folder; the system prompt shows that folder (not the root: in a linked worktree, work stays in the worktree). A separate clone is a different project. Outside a repository, a folder marked by `chi project init` (its `.chi/project.yml`) is the project for it and every subfolder; outside both, the root is the working directory.
|
|
19
19
|
- `index.md` lives in each scope dir and contains auto-managed lines like `- **name** · scope · date · bytes — description`; a memory a bundle installed adds `· from <bundle>` before the description. Your free-form sections in `index.md` are preserved but managed lines are owned by `memory_write` (and refreshed when `write`/`edit` change a memory's file).
|
|
@@ -122,7 +122,7 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
|
|
|
122
122
|
| `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and an installed bundle's plugin, rules and scripts. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
|
|
123
123
|
|
|
124
124
|
**Scope resolution for install/build:**
|
|
125
|
-
- CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<
|
|
125
|
+
- CLI `--scope` wins over `manifest.yml` `scope`. Default is `system` if none given (`Installer#run`). For `project`, target is the project folder `<system dir>/projects/<key>` of the project (`MemoryPaths.scope_dir`).
|
|
126
126
|
|
|
127
127
|
**Example flows:**
|
|
128
128
|
```bash
|