samagotchi 0.155.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 +57 -1
- data/bin/chi +2 -1
- data/docs/architecture.md +22 -13
- 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/anthropic.md +1 -1
- data/docs/internals/attached-context.md +3 -2
- data/docs/internals/background-tasks.md +1 -1
- 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 +641 -937
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +2 -2
- data/docs/internals/participants.md +3 -2
- data/docs/internals/prompt-caching.md +4 -1
- data/docs/internals/remote-sessions.md +8 -3
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +2 -2
- data/docs/memory.md +13 -2
- data/docs/plugins.md +35 -2
- data/docs/sessions.md +11 -11
- data/docs/sub-agent.md +17 -2
- data/docs/system-one.md +3 -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/config_writer.rb +1 -1
- 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/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +18 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
- 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/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/child_move.rb +13 -57
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +9 -23
- data/lib/samagotchi/config.rb +20 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_sources.rb +5 -4
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/context_judge.rb +19 -11
- data/lib/samagotchi/engine/messages.rb +1 -1
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +29 -11
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine/side_requests.rb +21 -11
- data/lib/samagotchi/engine/toolkit.rb +10 -9
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +7 -7
- data/lib/samagotchi/engine/verify.rb +1 -1
- data/lib/samagotchi/engine.rb +43 -12
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +6 -3
- data/lib/samagotchi/guardrails/targets.rb +24 -9
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +58 -46
- data/lib/samagotchi/kernel_loop.rb +53 -124
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/chat_loop.rb +40 -21
- data/lib/samagotchi/llm/native_backend.rb +1 -1
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +29 -67
- data/lib/samagotchi/llm_context_view.rb +9 -0
- 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_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +7 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +2 -3
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +41 -6
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/loader.rb +33 -10
- data/lib/samagotchi/plugin/sessions.rb +3 -6
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +27 -0
- data/lib/samagotchi/reminder_carry.rb +1 -1
- 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/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +36 -37
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +370 -445
- data/lib/samagotchi/session_access/local.rb +12 -11
- data/lib/samagotchi/session_access/remote.rb +22 -6
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_field.rb +133 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +139 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +118 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +28 -1541
- data/lib/samagotchi/session_metrics.rb +8 -5
- 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/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- 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 +5 -7
- data/lib/samagotchi/testing/install.rb +2 -1
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +10 -4
- data/lib/samagotchi/tool_declarations.rb +2 -2
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +25 -5
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +55 -45
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +5 -5
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/tools/registry.rb +28 -11
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +52 -51
- data/lib/samagotchi/tools/tool_surface.rb +22 -8
- data/lib/samagotchi/turn_context.rb +2 -1
- data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
- data/lib/samagotchi/turn_stages/objectives.rb +56 -52
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
- data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
- data/lib/samagotchi/turn_stages/warmup.rb +14 -15
- 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 +45 -28
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +3 -3
- 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 +6 -6
- data/lib/samagotchi/web/session_control.rb +4 -4
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +67 -5
- data/lib/samagotchi/web/session_summary.rb +39 -28
- data/lib/samagotchi/worker.rb +83 -32
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_link.rb +3 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +32 -4
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -33,6 +33,8 @@ module Samagotchi
|
|
|
33
33
|
# Loaded on first use: SessionManager and its ~80 files only when a
|
|
34
34
|
# command reaches it (chi context never does).
|
|
35
35
|
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
36
|
+
# A continue (#create with continues); it requires session_manager.
|
|
37
|
+
autoload :SessionContinue, File.expand_path("../session_continue", __dir__)
|
|
36
38
|
|
|
37
39
|
module SessionAccess
|
|
38
40
|
# This machine's sessions: the calls the session commands made before
|
|
@@ -63,11 +65,11 @@ module Samagotchi
|
|
|
63
65
|
# @raise [Session::AmbiguousId]
|
|
64
66
|
def resolve_id(given) = Session.resolve_id(given, **dir)
|
|
65
67
|
|
|
66
|
-
# The full id of +given+, of a session that exists.
|
|
67
|
-
# @raise [
|
|
68
|
+
# The full id of +given+, of a session that exists (and loads).
|
|
69
|
+
# @raise [Session::NotFound] no such session (Session::AmbiguousId: a
|
|
68
70
|
# prefix of several)
|
|
69
71
|
def resolve(given)
|
|
70
|
-
id =
|
|
72
|
+
id = Session.resolve!(given, **dir)
|
|
71
73
|
show(id)
|
|
72
74
|
id
|
|
73
75
|
end
|
|
@@ -79,12 +81,11 @@ module Samagotchi
|
|
|
79
81
|
# @return [OwnerLock::Owner, nil] the worker or one-turn chi run that owns it
|
|
80
82
|
def owner(id) = SessionManager.session_owner(id, **dir)
|
|
81
83
|
|
|
82
|
-
# SessionManager.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
def summaries(**filters) = SessionManager.session_summaries(**filters, **dir)
|
|
84
|
+
# SessionManager.session_rows: the SessionRows chi sessions list
|
|
85
|
+
# prints and chi note --all reaches (chi web's summaries route answers
|
|
86
|
+
# their #to_summary_h).
|
|
87
|
+
# @return [Array<SessionRow>]
|
|
88
|
+
def summaries(**filters) = SessionManager.session_rows(**filters, **dir)
|
|
88
89
|
|
|
89
90
|
# The session's worker for an attach: the live one as is, else
|
|
90
91
|
# resumed and waited for up to +wait+ seconds (chi --attach on this
|
|
@@ -140,7 +141,7 @@ module Samagotchi
|
|
|
140
141
|
|
|
141
142
|
start = spec.preview ? { prompt: spec.prompt, preview: spec.preview } : { prompt: spec.prompt }
|
|
142
143
|
if spec.continues
|
|
143
|
-
return
|
|
144
|
+
return SessionContinue.run(spec.continues, **start, model: spec.model_name, **dir)
|
|
144
145
|
end
|
|
145
146
|
|
|
146
147
|
SessionManager.spawn_session(**start, working_directory: spec.working_directory, model_name: spec.model_name,
|
|
@@ -269,7 +270,7 @@ module Samagotchi
|
|
|
269
270
|
# @return [Boolean] whether the worker released the session
|
|
270
271
|
def stop(id, wait:) = SessionManager.stop_session(id, wait: wait, **dir)
|
|
271
272
|
|
|
272
|
-
# @return [SessionManager::
|
|
273
|
+
# @return [SessionManager::Restarted]
|
|
273
274
|
def restart(id) = SessionManager.restart_session(id, **dir)
|
|
274
275
|
|
|
275
276
|
# @return [SessionManager::ArchiveResult]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../session"
|
|
4
|
+
require_relative "../session_row"
|
|
4
5
|
require_relative "../session_access"
|
|
5
6
|
require_relative "../remotes"
|
|
6
7
|
require_relative "../reply_wait"
|
|
@@ -137,11 +138,11 @@ module Samagotchi
|
|
|
137
138
|
end
|
|
138
139
|
end
|
|
139
140
|
|
|
140
|
-
# @raise [
|
|
141
|
+
# @raise [Session::NotFound] no such session (Session::AmbiguousId: a
|
|
141
142
|
# prefix of several)
|
|
142
143
|
def resolve(given)
|
|
143
144
|
id = resolve_id(given)
|
|
144
|
-
raise
|
|
145
|
+
raise Session::NotFound, given if id == given.to_s && !exists?(id)
|
|
145
146
|
|
|
146
147
|
id
|
|
147
148
|
end
|
|
@@ -158,8 +159,9 @@ module Samagotchi
|
|
|
158
159
|
# @return [OwnerLock::Owner, nil]
|
|
159
160
|
def owner(id) = show(id).owner
|
|
160
161
|
|
|
161
|
-
# The rows chi sessions list prints
|
|
162
|
-
#
|
|
162
|
+
# The rows chi sessions list prints: chi web's summaries
|
|
163
|
+
# (SessionRow#to_summary_h) read back as SessionRows.
|
|
164
|
+
# @return [Array<SessionRow>]
|
|
163
165
|
def summaries(live: false, cwd: nil, limit: nil, include_tests: true, project_root: nil, include_archived: false,
|
|
164
166
|
sort: nil, order: nil)
|
|
165
167
|
query = { live: (1 if live), cwd: cwd, limit: limit, tests: (0 unless include_tests), dir: project_root,
|
|
@@ -167,7 +169,7 @@ module Samagotchi
|
|
|
167
169
|
reply = request(:get, "/api/sessions/summaries", query: query)
|
|
168
170
|
raise_refusal(reply) unless reply.status == 200
|
|
169
171
|
|
|
170
|
-
Array(reply.json).map { |row| symbolize(row) }
|
|
172
|
+
Array(reply.json).map { |row| SessionRow.from_summary_h(symbolize(row)) }
|
|
171
173
|
end
|
|
172
174
|
|
|
173
175
|
# --- create and send
|
|
@@ -704,6 +706,20 @@ module Samagotchi
|
|
|
704
706
|
reply.json["detail"].to_s.start_with?("no installed bundle resolves")
|
|
705
707
|
end
|
|
706
708
|
|
|
709
|
+
# chi web's 404 not_found detail for a session it doesn't have:
|
|
710
|
+
# "no session X" (Session::NotFound), or "Session not found: X" from
|
|
711
|
+
# chi web 0.155.0 and older (compat N-1: the same line either way).
|
|
712
|
+
SESSION_NOT_FOUND = /\A(?:no session |Session not found: )(.+)\z/m
|
|
713
|
+
|
|
714
|
+
# Session::NotFound for a missing session's 404, else an
|
|
715
|
+
# ArgumentError with chi web's words (another route's 404). chi web
|
|
716
|
+
# never answers Session::NONE_YET ("no session yet") as a 404
|
|
717
|
+
# not_found; were it to, the pattern would read it as NotFound("yet").
|
|
718
|
+
def not_found_error(detail)
|
|
719
|
+
given = detail[SESSION_NOT_FOUND, 1]
|
|
720
|
+
given ? Session::NotFound.new(given) : ArgumentError.new(detail)
|
|
721
|
+
end
|
|
722
|
+
|
|
707
723
|
# Raises what Local's callee raises for this refusal (chi web sends
|
|
708
724
|
# the fields each class is built from; see docs/architecture.md, chi
|
|
709
725
|
# web's routes for the session commands), else RemoteFailed.
|
|
@@ -714,7 +730,7 @@ module Samagotchi
|
|
|
714
730
|
sid = json["session_id"]
|
|
715
731
|
reason = json["reason"]&.to_sym
|
|
716
732
|
case error
|
|
717
|
-
when "not_found" then raise
|
|
733
|
+
when "not_found" then raise not_found_error(message.to_s)
|
|
718
734
|
when "ambiguous_id" then raise Session::AmbiguousId.new(message, ids: Array(json["ids"]))
|
|
719
735
|
when "owned_by_tui"
|
|
720
736
|
raise SessionManager::ScratchSession, sid if json["scratch"] == true
|
|
@@ -82,7 +82,7 @@ module Samagotchi
|
|
|
82
82
|
@stdout.puts("#{short} refused: #{e.message}")
|
|
83
83
|
false
|
|
84
84
|
rescue ArgumentError => e
|
|
85
|
-
error_line("chi sessions archive: #{e.
|
|
85
|
+
error_line("chi sessions archive: #{e.message}")
|
|
86
86
|
false
|
|
87
87
|
rescue SystemCallError => e
|
|
88
88
|
@stdout.puts("#{short} failed: #{e.message}")
|
|
@@ -96,7 +96,7 @@ module Samagotchi
|
|
|
96
96
|
@stdout.puts(result[:unarchived].empty? ? "#{short} not archived" : "#{short} unarchived")
|
|
97
97
|
true
|
|
98
98
|
rescue ArgumentError => e
|
|
99
|
-
error_line("chi sessions unarchive: #{e.
|
|
99
|
+
error_line("chi sessions unarchive: #{e.message}")
|
|
100
100
|
false
|
|
101
101
|
rescue SystemCallError => e
|
|
102
102
|
@stdout.puts("#{short} failed: #{e.message}")
|
|
@@ -15,7 +15,7 @@ module Samagotchi
|
|
|
15
15
|
# "the next run of the same routine". The chain is derived, never stored:
|
|
16
16
|
# follow +continues+ back for the earlier links, forward (the session whose
|
|
17
17
|
# +continues+ names this one) for the later ones. A link is continued at
|
|
18
|
-
# most once (
|
|
18
|
+
# most once (SessionContinue.run refuses a second), so the
|
|
19
19
|
# chain's latest link is the one nobody continues.
|
|
20
20
|
module SessionChain
|
|
21
21
|
# `--continues last:<id>`: the latest link of the chain <id> is in.
|
|
@@ -62,15 +62,16 @@ module Samagotchi
|
|
|
62
62
|
# A session to continue as the CLI and the web name it: an id, a unique
|
|
63
63
|
# prefix, or `last:<id or prefix>` for that chain's latest link.
|
|
64
64
|
# @return [String] the full id of an existing session
|
|
65
|
-
# @raise [
|
|
65
|
+
# @raise [Session::NotFound] none such (Session::AmbiguousId for a prefix of several)
|
|
66
66
|
def resolve(ref, state_dir:)
|
|
67
67
|
given = ref.to_s.strip
|
|
68
68
|
last = given.start_with?(LAST_PREFIX)
|
|
69
69
|
given = given.delete_prefix(LAST_PREFIX) if last
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
id = begin
|
|
71
|
+
Session.resolve!(given, state_dir: state_dir)
|
|
72
|
+
rescue Session::NotFound
|
|
73
|
+
raise Session::NotFound, ref # as given, last: and all
|
|
74
|
+
end
|
|
74
75
|
|
|
75
76
|
last ? latest(id, state_dir: state_dir) : id
|
|
76
77
|
end
|
|
@@ -7,6 +7,7 @@ require_relative "session_inbox"
|
|
|
7
7
|
require_relative "session_chain"
|
|
8
8
|
require_relative "objectives"
|
|
9
9
|
require_relative "child_move"
|
|
10
|
+
require_relative "starting_marker"
|
|
10
11
|
require_relative "context_move"
|
|
11
12
|
require_relative "archive_store"
|
|
12
13
|
require_relative "recap_store"
|
|
@@ -17,9 +18,9 @@ require_relative "log"
|
|
|
17
18
|
require_relative "session_manager"
|
|
18
19
|
|
|
19
20
|
module Samagotchi
|
|
20
|
-
# The next link of a session chain
|
|
21
|
-
# Continue →
|
|
22
|
-
#
|
|
21
|
+
# The next link of a session chain: chi send --new --continues and web
|
|
22
|
+
# Continue → run .run. The archive, spawn, stop and wake it builds on are
|
|
23
|
+
# SessionManager's.
|
|
23
24
|
module SessionContinue
|
|
24
25
|
# A continue that can't happen (#run). reason: :continued
|
|
25
26
|
# (another session continues it already: +ids+ names it), :open_children
|
|
@@ -198,7 +199,7 @@ module Samagotchi
|
|
|
198
199
|
Log.warn(:worker, "continue_wake_failed", sid: id, error: e.class.name, msg: e.message)
|
|
199
200
|
end
|
|
200
201
|
|
|
201
|
-
# The new link, holding off any worker but its own (
|
|
202
|
+
# The new link, holding off any worker but its own (StartingMarker.fresh?)
|
|
202
203
|
# while +move+ goes to it.
|
|
203
204
|
private_class_method def self.spawn_continuation(previous, prompt:, preview:, choice:, state_dir:, move:)
|
|
204
205
|
recap = RecapStore.read(Session.session_dir(previous.id, state_dir: state_dir))
|
|
@@ -206,7 +207,7 @@ module Samagotchi
|
|
|
206
207
|
preview = previous.first_preview if preview.to_s.strip.empty? && start.nil?
|
|
207
208
|
moved_context = nil
|
|
208
209
|
before_spawn = lambda do |link|
|
|
209
|
-
|
|
210
|
+
StartingMarker.mark(link.id, state_dir: state_dir)
|
|
210
211
|
ChildMove.apply(move, from: previous.id, to: link.id, state_dir: state_dir)
|
|
211
212
|
# Its movable context too, with a note saying what moved and stayed.
|
|
212
213
|
moved_context = ContextMove.call(from: previous.id, to: link.id, mode: :continue, state_dir: state_dir)
|
|
@@ -76,7 +76,7 @@ module Samagotchi
|
|
|
76
76
|
@stdout.puts("#{short} refused: #{line}")
|
|
77
77
|
false
|
|
78
78
|
rescue ArgumentError => e
|
|
79
|
-
error_line("chi sessions delete: #{e.
|
|
79
|
+
error_line("chi sessions delete: #{e.message}")
|
|
80
80
|
false
|
|
81
81
|
rescue SystemCallError => e
|
|
82
82
|
@stdout.puts("#{short} failed: #{e.message}")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# One key of a session file (a Session::FIELDS row): +key+ the file's
|
|
5
|
+
# key, +attr+ Session's attribute and keyword (nil for a key only the
|
|
6
|
+
# file has: the settings pair, written from Session's +from+), +default+
|
|
7
|
+
# the value of a file without the key (REQUIRED for one it must have),
|
|
8
|
+
# +fresh+ a new Session's when it isn't given (it differs only for
|
|
9
|
+
# metadata_version and messages), +optional+ a key written only when set,
|
|
10
|
+
# +cast+ what a value is read as (Session.new, .from_h and the writers),
|
|
11
|
+
# +write+ what the file gets from the attribute (the value as is without
|
|
12
|
+
# one). +unread+ and +keep+ are the Unread rule's (.level): what of a
|
|
13
|
+
# value the field couldn't read the session keeps, and how the file gets
|
|
14
|
+
# it back. A leaf: the types a row names are named in session.rb, in a
|
|
15
|
+
# block its lambdas call (`type.call`, not `yield`: never while the
|
|
16
|
+
# constructor runs, so Performance/RedundantBlockCall is off for them).
|
|
17
|
+
SessionField = Data.define(:key, :attr, :from, :default, :fresh, :optional, :cast, :write, :unread, :keep) do
|
|
18
|
+
# A field read as given, or through +cast+.
|
|
19
|
+
def self.plain(key, default, cast: nil, write: nil, fresh: default, optional: false)
|
|
20
|
+
new(key: key, attr: key.to_sym, from: nil, default: default, fresh: fresh, optional: optional, cast: cast,
|
|
21
|
+
write: write, unread: nil, keep: nil)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop:disable Performance/RedundantBlockCall
|
|
25
|
+
# A value object's field (nil for none): a value that is already one
|
|
26
|
+
# is taken as it is, nil is nil (the type isn't loaded for it: an
|
|
27
|
+
# autoloaded one stays unloaded), anything else is read with the type's
|
|
28
|
+
# +reader+ (from_file, of); the file gets its #to_file, through
|
|
29
|
+
# .scrub_utf8 with +scrub+. +type+ is a block, so the row names its type
|
|
30
|
+
# without loading it.
|
|
31
|
+
def self.value(key, reader, optional: false, scrub: false, &type)
|
|
32
|
+
cast = lambda do |value|
|
|
33
|
+
next nil if value.nil?
|
|
34
|
+
|
|
35
|
+
klass = type.call
|
|
36
|
+
value.is_a?(klass) ? value : klass.public_send(reader, value)
|
|
37
|
+
end
|
|
38
|
+
write = scrub ? ->(value) { value && scrub_utf8(value.to_file) } : ->(value) { value&.to_file }
|
|
39
|
+
new(key: key, attr: key.to_sym, from: nil, default: nil, fresh: nil, optional: optional, cast: cast,
|
|
40
|
+
write: write, unread: nil, keep: nil)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A list of value objects ([] for none): read with the type's .list, each
|
|
44
|
+
# written with its #to_file.
|
|
45
|
+
def self.list(key, &type)
|
|
46
|
+
new(key: key, attr: key.to_sym, from: nil, default: [], fresh: [], optional: false,
|
|
47
|
+
cast: ->(value) { type.call.list(value) }, write: ->(values) { values.map(&:to_file) }, unread: nil,
|
|
48
|
+
keep: nil)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A session's own level (nil for none), with the Unread rule: a value
|
|
52
|
+
# that isn't one reads as nil, and the session keeps it as written (a
|
|
53
|
+
# newer chi's level, a hand edit) and saves it back so, until it sets
|
|
54
|
+
# its own. A level is the type's .session_level of the value; a String
|
|
55
|
+
# that doesn't read as one, other than the +unset+ word (any case and
|
|
56
|
+
# spaces), is the one kept. With +merge_unread+ the field is a value
|
|
57
|
+
# object of several parts (LLMContextOverride: .from_file, .unread):
|
|
58
|
+
# the parts it couldn't read are kept, and the file gets them under the
|
|
59
|
+
# parts it has (nil when there is none).
|
|
60
|
+
def self.level(key, unset: nil, merge_unread: false, &type)
|
|
61
|
+
if merge_unread
|
|
62
|
+
cast = ->(value) { type.call.from_file(value) }
|
|
63
|
+
unread = ->(value) { type.call.unread(value) }
|
|
64
|
+
write = ->(value) { value&.to_file }
|
|
65
|
+
keep = lambda do |written, kept|
|
|
66
|
+
saved = kept.merge(written || {})
|
|
67
|
+
saved.empty? ? nil : saved
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
cast = ->(value) { type.call.session_level(value) }
|
|
71
|
+
unread = lambda do |value|
|
|
72
|
+
value if value.is_a?(String) && type.call.session_level(value).nil? && !value.strip.casecmp?(unset)
|
|
73
|
+
end
|
|
74
|
+
write = ->(value) { value&.to_s }
|
|
75
|
+
keep = ->(written, kept) { written || kept }
|
|
76
|
+
end
|
|
77
|
+
new(key: key, attr: key.to_sym, from: nil, default: nil, fresh: nil, optional: false, cast: cast, write: write,
|
|
78
|
+
unread: unread, keep: keep)
|
|
79
|
+
end
|
|
80
|
+
# rubocop:enable Performance/RedundantBlockCall
|
|
81
|
+
|
|
82
|
+
# A key only the file has: +write+ takes Session's +from+ attribute.
|
|
83
|
+
def self.file_only(key, default, from:, write:)
|
|
84
|
+
new(key: key, attr: nil, from: from, default: default, fresh: nil, optional: false, cast: nil, write: write,
|
|
85
|
+
unread: nil, keep: nil)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def required? = default.equal?(SessionField::REQUIRED)
|
|
89
|
+
|
|
90
|
+
def fresh_required? = fresh.equal?(SessionField::REQUIRED)
|
|
91
|
+
|
|
92
|
+
# A new Session's value when it isn't given: a fresh copy of a list or
|
|
93
|
+
# a Hash, never one shared with another session.
|
|
94
|
+
def fresh_value = fresh.is_a?(Array) || fresh.is_a?(Hash) ? fresh.dup : fresh
|
|
95
|
+
|
|
96
|
+
# The instance variable #to_h reads (never the reader: project_root's
|
|
97
|
+
# and verdicts' have logic of their own).
|
|
98
|
+
def ivar = :"@#{attr || from}"
|
|
99
|
+
|
|
100
|
+
# +value+ as the attribute holds it.
|
|
101
|
+
def load(value) = cast ? cast.call(value) : value
|
|
102
|
+
|
|
103
|
+
# The attribute's +value+ as the file holds it, with what the session
|
|
104
|
+
# kept of a value it couldn't read (+kept+, .level's).
|
|
105
|
+
def dump(value, kept = nil)
|
|
106
|
+
written = write ? write.call(value) : value
|
|
107
|
+
keep ? keep.call(written, kept) : written
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The last guard before JSON.generate, which raises on bytes that
|
|
111
|
+
# aren't UTF-8: a save that raised would lose the whole conversation.
|
|
112
|
+
# ToolRunner already scrubs tool output; this covers any other source.
|
|
113
|
+
def self.scrub_utf8(obj)
|
|
114
|
+
case obj
|
|
115
|
+
when String
|
|
116
|
+
str = obj.encoding == Encoding::UTF_8 ? obj : obj.dup.force_encoding(Encoding::UTF_8)
|
|
117
|
+
str.valid_encoding? ? str : str.scrub("?")
|
|
118
|
+
when Array then obj.map { |element| scrub_utf8(element) }
|
|
119
|
+
when Hash then obj.to_h { |key, value| [key, scrub_utf8(value)] }
|
|
120
|
+
else obj
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A Hash with String keys (one level).
|
|
125
|
+
def self.stringify_keys(hash)
|
|
126
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A field a session file must have (KeyError without it, ArgumentError
|
|
131
|
+
# from Session.new).
|
|
132
|
+
SessionField::REQUIRED = Object.new.freeze
|
|
133
|
+
end
|
|
@@ -442,7 +442,7 @@ module Samagotchi
|
|
|
442
442
|
private_class_method :unmark_removed
|
|
443
443
|
|
|
444
444
|
# How old a per-input tombstone (input/removed/, TurnCheckpoint's
|
|
445
|
-
# input/settled/) gets before a worker's start prunes it (#prune_old).
|
|
445
|
+
# input/settled/, a merge record in input/merged/) gets before a worker's start prunes it (#prune_old).
|
|
446
446
|
# A wait reads one at its next poll (ReplyWait::POLL_INTERVAL) and a
|
|
447
447
|
# later `chi send --wait --turn` reads a settled one at its start, so
|
|
448
448
|
# only a wait whose process slept (a closed laptop) or one started long
|
|
@@ -470,6 +470,54 @@ module Samagotchi
|
|
|
470
470
|
# The removed tombstones past +max_age+ (#prune_old).
|
|
471
471
|
def self.prune_removed(session_dir, max_age: TOMBSTONE_MAX_AGE) = prune_old(session_dir, REMOVED_DIR, max_age: max_age)
|
|
472
472
|
|
|
473
|
+
# Where a running turn's drain records an input it merged as a steer:
|
|
474
|
+
# input/merged/<enqueued_id> = {"into": the turn's enqueued_id, "at"}.
|
|
475
|
+
# Its own directory: a file in input/settled/ reads as worker_stopped to
|
|
476
|
+
# a chi before it, and no *.json glob sees a subdirectory.
|
|
477
|
+
MERGED_DIR = "merged"
|
|
478
|
+
|
|
479
|
+
# Records that the input +id+ went into the turn +into+ (both
|
|
480
|
+
# enqueued_ids) as a steer, before the drain deletes its claimed file
|
|
481
|
+
# (Worker#pending_input_drain): a wait for it (ReplyWait's turn:) then
|
|
482
|
+
# waits for that turn (Poll#follow_merge). Nothing for an id that
|
|
483
|
+
# can't name a file, nor for a turn with none; a failed write only
|
|
484
|
+
# logs (the wait infers the merge, as for a worker before this).
|
|
485
|
+
def self.mark_merged(session_dir, id, into:, now: Time.now)
|
|
486
|
+
return unless ENQUEUED_ID_NAME.match?(id.to_s) && ENQUEUED_ID_NAME.match?(into.to_s)
|
|
487
|
+
|
|
488
|
+
dir = File.join(session_dir, INPUT_DIR, MERGED_DIR)
|
|
489
|
+
FileUtils.mkdir_p(dir)
|
|
490
|
+
AtomicFile.write(File.join(dir, id), JSON.generate("into" => into, "at" => now.iso8601(3)))
|
|
491
|
+
rescue SystemCallError, IOError => e
|
|
492
|
+
Log.warn(:worker, "merge_record_write_failed", error: e.class.name, msg: e.message, enqueued_id: id)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# A merge record (#mark_merged): the enqueued_id of the turn an input
|
|
496
|
+
# went into, and when (a Time, nil when unreadable).
|
|
497
|
+
Merge = Data.define(:into, :at)
|
|
498
|
+
|
|
499
|
+
# @return [Merge, nil] the turn the input +id+ was merged into
|
|
500
|
+
# (#mark_merged), nil without a readable record
|
|
501
|
+
def self.merged_into(session_dir, id)
|
|
502
|
+
return nil unless ENQUEUED_ID_NAME.match?(id.to_s)
|
|
503
|
+
|
|
504
|
+
raw = JSON.parse(File.read(File.join(session_dir, INPUT_DIR, MERGED_DIR, id.to_s)))
|
|
505
|
+
into = raw["into"]
|
|
506
|
+
return nil unless ENQUEUED_ID_NAME.match?(into.to_s)
|
|
507
|
+
|
|
508
|
+
at = begin
|
|
509
|
+
Time.iso8601(raw["at"].to_s)
|
|
510
|
+
rescue ArgumentError
|
|
511
|
+
nil
|
|
512
|
+
end
|
|
513
|
+
Merge.new(into: into, at: at)
|
|
514
|
+
rescue JSON::ParserError, SystemCallError, TypeError, NoMethodError
|
|
515
|
+
nil
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# The merge records past +max_age+ (#prune_old).
|
|
519
|
+
def self.prune_merged(session_dir, max_age: TOMBSTONE_MAX_AGE) = prune_old(session_dir, MERGED_DIR, max_age: max_age)
|
|
520
|
+
|
|
473
521
|
# @return [Boolean] whether the queued input +enqueued_id+ was removed
|
|
474
522
|
# before it ran (#take_queued_input's tombstone), or is being removed:
|
|
475
523
|
# the tombstone goes first, so while its claimed file is still there
|