samagotchi 0.156.0 → 0.158.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -26,7 +26,7 @@ module Samagotchi
|
|
|
26
26
|
# bin/chi subcommands that act on this machine: never sessions. chi
|
|
27
27
|
# remote (this machine's list of remotes) runs here under a remote too
|
|
28
28
|
# (Dispatch); the others are refused there.
|
|
29
|
-
MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit judgments remote home].freeze
|
|
29
|
+
MACHINE = %w[bootstrap update models desktop memory bundle guardrails toolkit judgments remote home project].freeze
|
|
30
30
|
|
|
31
31
|
# "command", "command sub" or either with a flag (its long name) →
|
|
32
32
|
# SUPPORTED or LOCAL_ONLY. A LOCAL_ONLY command or subcommand lists
|
|
@@ -81,12 +81,11 @@ module Samagotchi
|
|
|
81
81
|
# @return [OwnerLock::Owner, nil] the worker or one-turn chi run that owns it
|
|
82
82
|
def owner(id) = SessionManager.session_owner(id, **dir)
|
|
83
83
|
|
|
84
|
-
# SessionManager.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
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)
|
|
90
89
|
|
|
91
90
|
# The session's worker for an attach: the live one as is, else
|
|
92
91
|
# resumed and waited for up to +wait+ seconds (chi --attach on this
|
|
@@ -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"
|
|
@@ -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
|
|
@@ -0,0 +1,134 @@
|
|
|
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+; +unread+ and +keep+ give
|
|
19
|
+
# it the Unread rule (see .level).
|
|
20
|
+
def self.plain(key, default, cast: nil, write: nil, fresh: default, optional: false, unread: nil, keep: nil)
|
|
21
|
+
new(key: key, attr: key.to_sym, from: nil, default: default, fresh: fresh, optional: optional, cast: cast,
|
|
22
|
+
write: write, unread: unread, keep: keep)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# rubocop:disable Performance/RedundantBlockCall
|
|
26
|
+
# A value object's field (nil for none): a value that is already one
|
|
27
|
+
# is taken as it is, nil is nil (the type isn't loaded for it: an
|
|
28
|
+
# autoloaded one stays unloaded), anything else is read with the type's
|
|
29
|
+
# +reader+ (from_file, of); the file gets its #to_file, through
|
|
30
|
+
# .scrub_utf8 with +scrub+. +type+ is a block, so the row names its type
|
|
31
|
+
# without loading it.
|
|
32
|
+
def self.value(key, reader, optional: false, scrub: false, &type)
|
|
33
|
+
cast = lambda do |value|
|
|
34
|
+
next nil if value.nil?
|
|
35
|
+
|
|
36
|
+
klass = type.call
|
|
37
|
+
value.is_a?(klass) ? value : klass.public_send(reader, value)
|
|
38
|
+
end
|
|
39
|
+
write = scrub ? ->(value) { value && scrub_utf8(value.to_file) } : ->(value) { value&.to_file }
|
|
40
|
+
new(key: key, attr: key.to_sym, from: nil, default: nil, fresh: nil, optional: optional, cast: cast,
|
|
41
|
+
write: write, unread: nil, keep: nil)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A list of value objects ([] for none): read with the type's .list, each
|
|
45
|
+
# written with its #to_file.
|
|
46
|
+
def self.list(key, &type)
|
|
47
|
+
new(key: key, attr: key.to_sym, from: nil, default: [], fresh: [], optional: false,
|
|
48
|
+
cast: ->(value) { type.call.list(value) }, write: ->(values) { values.map(&:to_file) }, unread: nil,
|
|
49
|
+
keep: nil)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A session's own level (nil for none), with the Unread rule: a value
|
|
53
|
+
# that isn't one reads as nil, and the session keeps it as written (a
|
|
54
|
+
# newer chi's level, a hand edit) and saves it back so, until it sets
|
|
55
|
+
# its own. A level is the type's .session_level of the value; a String
|
|
56
|
+
# that doesn't read as one, other than the +unset+ word (any case and
|
|
57
|
+
# spaces), is the one kept. With +merge_unread+ the field is a value
|
|
58
|
+
# object of several parts (LLMContextOverride: .from_file, .unread):
|
|
59
|
+
# the parts it couldn't read are kept, and the file gets them under the
|
|
60
|
+
# parts it has (nil when there is none).
|
|
61
|
+
def self.level(key, unset: nil, merge_unread: false, &type)
|
|
62
|
+
if merge_unread
|
|
63
|
+
cast = ->(value) { type.call.from_file(value) }
|
|
64
|
+
unread = ->(value) { type.call.unread(value) }
|
|
65
|
+
write = ->(value) { value&.to_file }
|
|
66
|
+
keep = lambda do |written, kept|
|
|
67
|
+
saved = kept.merge(written || {})
|
|
68
|
+
saved.empty? ? nil : saved
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
cast = ->(value) { type.call.session_level(value) }
|
|
72
|
+
unread = lambda do |value|
|
|
73
|
+
value if value.is_a?(String) && type.call.session_level(value).nil? && !value.strip.casecmp?(unset)
|
|
74
|
+
end
|
|
75
|
+
write = ->(value) { value&.to_s }
|
|
76
|
+
keep = ->(written, kept) { written || kept }
|
|
77
|
+
end
|
|
78
|
+
new(key: key, attr: key.to_sym, from: nil, default: nil, fresh: nil, optional: false, cast: cast, write: write,
|
|
79
|
+
unread: unread, keep: keep)
|
|
80
|
+
end
|
|
81
|
+
# rubocop:enable Performance/RedundantBlockCall
|
|
82
|
+
|
|
83
|
+
# A key only the file has: +write+ takes Session's +from+ attribute.
|
|
84
|
+
def self.file_only(key, default, from:, write:)
|
|
85
|
+
new(key: key, attr: nil, from: from, default: default, fresh: nil, optional: false, cast: nil, write: write,
|
|
86
|
+
unread: nil, keep: nil)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def required? = default.equal?(SessionField::REQUIRED)
|
|
90
|
+
|
|
91
|
+
def fresh_required? = fresh.equal?(SessionField::REQUIRED)
|
|
92
|
+
|
|
93
|
+
# A new Session's value when it isn't given: a fresh copy of a list or
|
|
94
|
+
# a Hash, never one shared with another session.
|
|
95
|
+
def fresh_value = fresh.is_a?(Array) || fresh.is_a?(Hash) ? fresh.dup : fresh
|
|
96
|
+
|
|
97
|
+
# The instance variable #to_h reads (never the reader: project_root's
|
|
98
|
+
# and verdicts' have logic of their own).
|
|
99
|
+
def ivar = :"@#{attr || from}"
|
|
100
|
+
|
|
101
|
+
# +value+ as the attribute holds it.
|
|
102
|
+
def cast_value(value) = cast ? cast.call(value) : value
|
|
103
|
+
|
|
104
|
+
# The attribute's +value+ as the file holds it, with what the session
|
|
105
|
+
# kept of a value it couldn't read (+kept+, .level's).
|
|
106
|
+
def dump(value, kept = nil)
|
|
107
|
+
written = write ? write.call(value) : value
|
|
108
|
+
keep ? keep.call(written, kept) : written
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The last guard before JSON.generate, which raises on bytes that
|
|
112
|
+
# aren't UTF-8: a save that raised would lose the whole conversation.
|
|
113
|
+
# ToolRunner already scrubs tool output; this covers any other source.
|
|
114
|
+
def self.scrub_utf8(obj)
|
|
115
|
+
case obj
|
|
116
|
+
when String
|
|
117
|
+
str = obj.encoding == Encoding::UTF_8 ? obj : obj.dup.force_encoding(Encoding::UTF_8)
|
|
118
|
+
str.valid_encoding? ? str : str.scrub("?")
|
|
119
|
+
when Array then obj.map { |element| scrub_utf8(element) }
|
|
120
|
+
when Hash then obj.to_h { |key, value| [key, scrub_utf8(value)] }
|
|
121
|
+
else obj
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A Hash with String keys (one level).
|
|
126
|
+
def self.stringify_keys(hash)
|
|
127
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A field a session file must have (KeyError without it, ArgumentError
|
|
132
|
+
# from Session.new).
|
|
133
|
+
SessionField::REQUIRED = Object.new.freeze
|
|
134
|
+
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
|
|
@@ -10,7 +10,8 @@ module Samagotchi
|
|
|
10
10
|
# other entry (or a file in one of the EMPTY_DIRS) is something the
|
|
11
11
|
# session keeps. "pid" is the pid file workers wrote until 2026-10-01:
|
|
12
12
|
# nothing writes or reads it now, but a folder from before still has one
|
|
13
|
-
# and is still empty.
|
|
13
|
+
# and is still empty. A turn's drawing (Bridge::TurnDrawings::FILE)
|
|
14
|
+
# exists only after a turn that kept messages: never a skeleton file.
|
|
14
15
|
EMPTY_SKELETON_FILES = ["pid", "owner.lock", WorkerLink::FILE, AnalyticsFile::NAME].freeze
|
|
15
16
|
EMPTY_DIRS = [SessionInbox::INPUT_DIR, SessionInbox::NOTES_DIR, "images"].freeze
|
|
16
17
|
EMPTY_SKELETON_DIRS = (EMPTY_DIRS + [SessionInbox::OUTPUT_DIR]).freeze
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
class SessionManager
|
|
5
|
-
SUMMARY_DESC_LIMIT = 60
|
|
6
|
-
|
|
7
|
-
SUMMARY_PREVIEW_LIMIT = 120
|
|
8
|
-
|
|
9
5
|
# Listing: read-only queries over the saved sessions (the lists, their
|
|
10
6
|
# summary rows, a parent's delegates, a session's replies). Calls the
|
|
11
7
|
# facade's session_owner. Meant to be loaded through samagotchi/session_manager.
|
|
@@ -23,10 +19,10 @@ module Samagotchi
|
|
|
23
19
|
# `chi sessions list --live --format json|tsv` (chi note from a script)
|
|
24
20
|
# and the agent's list_sessions tool; both pass the current project as
|
|
25
21
|
# +project_root+ unless asked for every project.
|
|
26
|
-
# @param live [Boolean] only sessions a worker owns now (
|
|
27
|
-
# not the saved status, which a dead
|
|
28
|
-
# session a one-shot (or an older
|
|
29
|
-
# can't take notes
|
|
22
|
+
# @param live [Boolean] only sessions a worker owns now (WorkerLink's
|
|
23
|
+
# worker_owned?: the owner lock, not the saved status, which a dead
|
|
24
|
+
# worker leaves at "running"); a session a one-shot (or an older
|
|
25
|
+
# chi's REPL) owns is left out: it can't take notes
|
|
30
26
|
# @param cwd [String, nil] only sessions in this folder or below it
|
|
31
27
|
# @param project_root [String, nil] only this project's sessions
|
|
32
28
|
# (Session#project_root)
|
|
@@ -34,76 +30,45 @@ module Samagotchi
|
|
|
34
30
|
# @param include_tests [Boolean] false leaves out test runs
|
|
35
31
|
# @param exclude [String, nil] a session id to leave out (the asker)
|
|
36
32
|
# @param include_archived [Boolean] archived sessions too
|
|
37
|
-
# @return [Array<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# waiting_id:, stopped_by:}; status = Session#displayed_status (a dead
|
|
41
|
-
# worker's "running" is idle, as chi web shows it), saved_status = the
|
|
42
|
-
# file's (a "running" with no owner yet is also a worker on its way:
|
|
43
|
-
# Tools::Delegate.running?), busy =
|
|
44
|
-
# live with a turn running, delegate = started by the delegate tool
|
|
45
|
-
# (Session#delegate?; a fork has a parent_id but isn't one), continues = the session it continues (the
|
|
46
|
-
# previous link of its chain, Session#continues), recap = the saved recap's first sentence, auto_title =
|
|
47
|
-
# the auto title its reply led with (RecapTitle) or nil, project =
|
|
48
|
-
# Session#project_root, waiting = the kind of question it waits on
|
|
49
|
-
# ("question", "approval", "hook"; Session#waiting_question) or nil,
|
|
50
|
-
# waiting_id = that question's id or nil, relayed_to = the parent
|
|
51
|
-
# whose card it waits in too (the approval relay), its short id, or nil,
|
|
52
|
-
# stopped_by = the hook that stopped the last turn (Session#stopped_by)
|
|
53
|
-
def session_summaries(live: false, cwd: nil, limit: nil, include_tests: true, exclude: nil, state_dir: nil,
|
|
54
|
-
project_root: nil, include_archived: false, sort: nil, order: nil)
|
|
33
|
+
# @return [Array<SessionRow>] see SessionRow for the fields
|
|
34
|
+
def session_rows(live: false, cwd: nil, limit: nil, include_tests: true, exclude: nil, state_dir: nil,
|
|
35
|
+
project_root: nil, include_archived: false, sort: nil, order: nil)
|
|
55
36
|
sd = state_dir || Session.default_state_dir
|
|
56
37
|
root = cwd && folder_path(cwd)
|
|
57
38
|
roots = {}
|
|
58
39
|
# One registry per listing, built only when a row has a saved context
|
|
59
40
|
# to count: its session's llm_context budget is what ctx_pct counts
|
|
60
41
|
# against (SessionMetrics.saved_context_pct).
|
|
61
|
-
budgets =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
budgets = SessionRow.budgets
|
|
43
|
+
rows = Session.list(state_dir: sd, sort: sort || "updated_at", order: order || "desc", project_root: project_root,
|
|
44
|
+
include_archived: include_archived).lazy
|
|
45
|
+
.reject { |s| (!include_tests && s.test_run) || s.id == exclude }
|
|
46
|
+
.select { |s| root.nil? || in_folder?(s.working_directory, root) }
|
|
47
|
+
.filter_map do |s|
|
|
48
|
+
dir = Session.session_dir(s.id, state_dir: sd)
|
|
67
49
|
owner = session_owner(s.id, state_dir: sd)
|
|
68
|
-
|
|
69
|
-
next if live && !
|
|
50
|
+
link = WorkerLink.resolve(dir, unlink: false, owner: owner)
|
|
51
|
+
next if live && !link.worker_owned?
|
|
70
52
|
|
|
71
|
-
|
|
72
|
-
project: s.project_root(cache: roots), updated_at: s.updated_at, status: s.displayed_status(owner: owner), saved_status: s.status, live: owned, busy: owned && s.status == Session::STATUS_RUNNING,
|
|
73
|
-
owner: owner&.kind, **recap_fields(Session.session_dir(s.id, state_dir: sd)),
|
|
74
|
-
ctx_pct: SessionMetrics.saved_context_pct(Session.session_dir(s.id, state_dir: sd),
|
|
75
|
-
budget_tokens: -> { budgets.call(s) })&.round(1),
|
|
76
|
-
parent_id: s.parent_id, parent_short_id: s.parent_id&.[](0, 8), continues: s.continues,
|
|
77
|
-
continues_short_id: s.continues&.[](0, 8), delegate: s.delegate?, archived: s.archived,
|
|
78
|
-
scratch: s.scratch, test_run: s.test_run, stopped_by: s.stopped_by }.merge(waiting_fields(s.waiting_question(live: owned)))
|
|
53
|
+
SessionRow.build(s, session_dir: dir, owner: owner, link: link, roots: roots, budgets: budgets)
|
|
79
54
|
end
|
|
80
|
-
(limit ?
|
|
55
|
+
(limit ? rows.first(limit) : rows.to_a)
|
|
81
56
|
end
|
|
82
57
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# One HostRegistry for a whole listing, built on the first row that
|
|
90
|
-
# needs it (a saved context to count) and kept for the rest: resolving a
|
|
91
|
-
# session's llm_context budget reads config.yml (mtime-cached), so not
|
|
92
|
-
# one registry per row.
|
|
93
|
-
# @return [#call] (session) -> the session's budget in tokens or nil
|
|
94
|
-
private def listing_budgets
|
|
95
|
-
registry = nil
|
|
96
|
-
->(session) { LLMContextStrategy.session_budget(session, registry: registry ||= HostRegistry.new) }
|
|
97
|
-
end
|
|
58
|
+
# .session_rows as the summaries route's hashes (SessionRow#to_summary_h,
|
|
59
|
+
# its 28 keys in order): chi web's GET /api/sessions/summaries.
|
|
60
|
+
# @return [Array<Hash>]
|
|
61
|
+
def session_summaries(**filters) = session_rows(**filters).map(&:to_summary_h)
|
|
98
62
|
|
|
99
63
|
# The sessions delegated by +parent_id+ (the `delegate` tool), newest
|
|
100
|
-
# first, as .
|
|
64
|
+
# first, as .session_rows. A running one (busy) counts against
|
|
101
65
|
# session.max_children. Archived ones too.
|
|
66
|
+
# @return [Array<SessionRow>]
|
|
102
67
|
def children_of(parent_id, state_dir: nil)
|
|
103
68
|
return [] if parent_id.to_s.empty?
|
|
104
69
|
|
|
105
|
-
|
|
106
|
-
.select { |s| s
|
|
70
|
+
session_rows(state_dir: state_dir, include_tests: true, include_archived: true)
|
|
71
|
+
.select { |s| s.parent_id == parent_id.to_s }
|
|
107
72
|
end
|
|
108
73
|
|
|
109
74
|
# Children, their children, …: a delegated session doesn't delegate
|
|
@@ -113,51 +78,15 @@ module Samagotchi
|
|
|
113
78
|
queue = [id]
|
|
114
79
|
until queue.empty?
|
|
115
80
|
children_of(queue.shift, state_dir: state_dir).each do |child|
|
|
116
|
-
next if seen.include?(child
|
|
81
|
+
next if seen.include?(child.id) || except.include?(child.id)
|
|
117
82
|
|
|
118
|
-
seen << child
|
|
119
|
-
queue << child
|
|
83
|
+
seen << child.id
|
|
84
|
+
queue << child.id
|
|
120
85
|
end
|
|
121
86
|
end
|
|
122
87
|
seen.drop(1)
|
|
123
88
|
end
|
|
124
89
|
|
|
125
|
-
# "<cwd basename> · <last prompt, or the first preview>", one line.
|
|
126
|
-
private def summary_desc(session)
|
|
127
|
-
desc = [File.basename(session.working_directory.to_s), summary_text(session)].reject(&:empty?).join(" · ")
|
|
128
|
-
cut(desc, SUMMARY_DESC_LIMIT)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# A session row's recap: and auto_title: (RecapStore.card, one read).
|
|
132
|
-
private def recap_fields(session_dir)
|
|
133
|
-
card = RecapStore.card(session_dir)
|
|
134
|
-
{ recap: card.preview, auto_title: card.title }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
private def summary_preview(session)
|
|
138
|
-
cut(summary_text(session), SUMMARY_PREVIEW_LIMIT)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# A session as one line of text for the lists: its title (/title), else
|
|
142
|
-
# its last prompt, else the first message it was started with (a session
|
|
143
|
-
# command like `/model x`, or a first turn not saved yet).
|
|
144
|
-
def summary_text(session)
|
|
145
|
-
return one_line(session.title) unless session.title.to_s.strip.empty?
|
|
146
|
-
|
|
147
|
-
one_line(session.last_prompt.to_s.strip.empty? ? session.first_preview.to_s : session.last_prompt.to_s)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# A prompt as one line for a session list: whitespace collapsed, and a
|
|
151
|
-
# quoted or annotated message (ContextQuote, the web's annotations)
|
|
152
|
-
# without its "> " markers, so the words show.
|
|
153
|
-
def one_line(text)
|
|
154
|
-
text.to_s.gsub(/^[ \t]*(?:>[ \t]?)+/, "").gsub(/\s+/, " ").strip
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
private def cut(text, limit)
|
|
158
|
-
text.length > limit ? "#{text[0, limit - 1]}…" : text
|
|
159
|
-
end
|
|
160
|
-
|
|
161
90
|
private def folder_path(path)
|
|
162
91
|
File.realpath(path)
|
|
163
92
|
rescue SystemCallError
|
|
@@ -26,7 +26,7 @@ require_relative "participant_messages"
|
|
|
26
26
|
require_relative "session"
|
|
27
27
|
require_relative "session_inbox"
|
|
28
28
|
require_relative "session_metrics"
|
|
29
|
-
require_relative "
|
|
29
|
+
require_relative "effective_model"
|
|
30
30
|
require_relative "session_setup"
|
|
31
31
|
require_relative "host_registry"
|
|
32
32
|
require_relative "utf8_default"
|
|
@@ -48,6 +48,7 @@ require_relative "model_profile"
|
|
|
48
48
|
require_relative "installed_gem"
|
|
49
49
|
require_relative "installed_versions"
|
|
50
50
|
require_relative "recap_store"
|
|
51
|
+
require_relative "session_row"
|
|
51
52
|
require_relative "reminder_carry"
|
|
52
53
|
require_relative "archive_store"
|
|
53
54
|
require_relative "continue_offer"
|
|
@@ -1112,19 +1112,22 @@ module Samagotchi
|
|
|
1112
1112
|
if guardrail.is_a?(Hash) && guardrail[:decided_by] == "system one"
|
|
1113
1113
|
active[:guardrail] = { rule: guardrail[:rule], p: guardrail[:p], judgment_id: guardrail[:judgment_id] }.compact
|
|
1114
1114
|
end
|
|
1115
|
-
finish_tool_record(active, status.empty? ? "ok" : status, waited_ms: event[:waited_ms].to_i
|
|
1115
|
+
finish_tool_record(active, status.empty? ? "ok" : status, waited_ms: event[:waited_ms].to_i,
|
|
1116
|
+
duration_ms: event[:duration_ms])
|
|
1116
1117
|
end
|
|
1117
1118
|
end
|
|
1118
1119
|
|
|
1119
|
-
# Caller holds the mutex.
|
|
1120
|
-
|
|
1120
|
+
# Caller holds the mutex. +duration_ms+: the event's (ToolRunner's,
|
|
1121
|
+
# the figure every UI shows live); without one (a test kernel's
|
|
1122
|
+
# events), timed here.
|
|
1123
|
+
def finish_tool_record(active, status, waited_ms: 0, duration_ms: nil)
|
|
1121
1124
|
record = active.slice(
|
|
1122
1125
|
:id, :turn_id, :iteration, :call_index, :tool, :called_as, :via, :no_match, :guardrail, :started_at
|
|
1123
1126
|
).merge(
|
|
1124
1127
|
status: status,
|
|
1125
1128
|
completed_at: now.iso8601(3),
|
|
1126
1129
|
# Less a guardrail approval wait (ToolRunner's waited_ms).
|
|
1127
|
-
duration_ms: [elapsed_ms(active[:started_monotonic]) - waited_ms, 0].max
|
|
1130
|
+
duration_ms: duration_ms.is_a?(Integer) ? duration_ms : [elapsed_ms(active[:started_monotonic]) - waited_ms, 0].max
|
|
1128
1131
|
)
|
|
1129
1132
|
@tool_records << record
|
|
1130
1133
|
count_tool(record)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "paths"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# Where a session's files are and which ids name one: the sessions dir,
|
|
7
|
+
# a session's <id>.json and <id>/ dir, and the id rule every id from
|
|
8
|
+
# outside meets before it becomes a path. A leaf (Paths only), so a file
|
|
9
|
+
# that needs a session's path needs no Session: Session extends it, so
|
|
10
|
+
# Session.session_dir and the rest answer the same. `extend self`, not
|
|
11
|
+
# module_function: Session's copies must stay public.
|
|
12
|
+
module SessionPaths
|
|
13
|
+
extend self # rubocop:disable Style/ModuleFunction -- module_function would make Session's copies private
|
|
14
|
+
|
|
15
|
+
STATE_SUBDIR = File.join("samagotchi", "sessions")
|
|
16
|
+
FILE_EXT = ".json"
|
|
17
|
+
|
|
18
|
+
# An id that is not a session id: it could name a path outside the
|
|
19
|
+
# sessions dir ("../x", "/etc", "a/b", a NUL), or is empty.
|
|
20
|
+
class InvalidId < ArgumentError; end
|
|
21
|
+
|
|
22
|
+
# Ids are SecureRandom.uuid; anything made of letters, digits, "-" and
|
|
23
|
+
# "_" (up to 128, not starting with "-") is taken, so a UUID prefix the
|
|
24
|
+
# CLI resolves passes too. This is the one check every id from outside
|
|
25
|
+
# (the bridge, the web routes, chi send/note, delegate) meets before it
|
|
26
|
+
# becomes a path: session_dir and the session file path refuse others.
|
|
27
|
+
VALID_ID = /\A[A-Za-z0-9_][A-Za-z0-9_-]{0,127}\z/
|
|
28
|
+
|
|
29
|
+
def valid_id?(session_id)
|
|
30
|
+
session_id.is_a?(String) && VALID_ID.match?(session_id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @raise [InvalidId] unless valid_id?(session_id)
|
|
34
|
+
def check_id!(session_id)
|
|
35
|
+
return session_id if valid_id?(session_id)
|
|
36
|
+
|
|
37
|
+
raise InvalidId, "invalid session id: #{session_id.to_s[0, 80].inspect}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Directory for a specific session (holds IPC files alongside session.json).
|
|
41
|
+
# @raise [InvalidId] for an id that isn't one (valid_id?)
|
|
42
|
+
def session_dir(session_id, state_dir: default_state_dir)
|
|
43
|
+
File.join(state_dir, check_id!(session_id))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A session's <id>.json.
|
|
47
|
+
# @raise [InvalidId] for an id that isn't one (valid_id?)
|
|
48
|
+
def session_file(session_id, state_dir: default_state_dir)
|
|
49
|
+
File.join(state_dir, "#{check_id!(session_id)}#{FILE_EXT}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# XDG-aware sessions directory.
|
|
53
|
+
def default_state_dir(env: ENV)
|
|
54
|
+
File.join(Paths.state_home(env: env), STATE_SUBDIR)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -78,7 +78,7 @@ module Samagotchi
|
|
|
78
78
|
Dir.children(state_dir).filter_map do |name|
|
|
79
79
|
dir = File.join(state_dir, name)
|
|
80
80
|
next unless Session.valid_id?(name) && File.directory?(dir)
|
|
81
|
-
next if File.exist?(File.join(state_dir, "#{name}#{
|
|
81
|
+
next if File.exist?(File.join(state_dir, "#{name}#{SessionPaths::FILE_EXT}"))
|
|
82
82
|
next unless Time.now - File.mtime(dir) > EMPTY_GRACE_SECONDS && SessionManager.empty_session_dir?(dir)
|
|
83
83
|
next if SessionManager.session_owner(name, state_dir: state_dir)
|
|
84
84
|
|
|
@@ -203,7 +203,7 @@ module Samagotchi
|
|
|
203
203
|
passed_over = 0
|
|
204
204
|
|
|
205
205
|
all.each_with_index do |session, idx|
|
|
206
|
-
path = File.join(state_dir, "#{session.id}#{
|
|
206
|
+
path = File.join(state_dir, "#{session.id}#{SessionPaths::FILE_EXT}")
|
|
207
207
|
# Only when json present
|
|
208
208
|
unless File.exist?(path)
|
|
209
209
|
skipped << session.id
|