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
|
@@ -142,7 +142,7 @@ module Samagotchi
|
|
|
142
142
|
:cached_sum,
|
|
143
143
|
:cache_write_sum,
|
|
144
144
|
# What the server prefilled again of the previous request's prompt
|
|
145
|
-
# (
|
|
145
|
+
# (ReprefillMeter#tokens; an LLM context edit's cache break).
|
|
146
146
|
:reprefill_sum,
|
|
147
147
|
:reasoning_sum,
|
|
148
148
|
:cost_sum,
|
|
@@ -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
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "session"
|
|
5
|
+
require_relative "owner_lock"
|
|
6
|
+
require_relative "worker_link"
|
|
7
|
+
require_relative "recap_store"
|
|
8
|
+
require_relative "session_metrics"
|
|
9
|
+
require_relative "archive_store"
|
|
10
|
+
require_relative "effective_model"
|
|
11
|
+
require_relative "host_registry"
|
|
12
|
+
|
|
13
|
+
module Samagotchi
|
|
14
|
+
# A session as the lists see it: the one row every listing builds and
|
|
15
|
+
# reads (SessionManager.session_rows, chi sessions list on both backends,
|
|
16
|
+
# the picker, list_sessions, a broadcast's recipients, a parent's
|
|
17
|
+
# children, chi web's summaries route and Web::SessionSummary). Only
|
|
18
|
+
# scalar facts that go on the wire: #to_summary_h is the summaries route's
|
|
19
|
+
# body (28 keys, this order) and .from_summary_h reads it back, so a
|
|
20
|
+
# remote row prints the same #cli_line as a local one.
|
|
21
|
+
#
|
|
22
|
+
# The liveness rule: +live+ is WorkerLink's worker_owned? (a worker holds
|
|
23
|
+
# the lock, its Bridge up or on its way); +busy+ and +waiting+ need it,
|
|
24
|
+
# +status+ is Session#displayed_status (a dead worker's "running" is idle).
|
|
25
|
+
SessionRow = Data.define(:id, :desc, :preview, :title, :cwd, :project, :updated_at, :status, :saved_status, :live,
|
|
26
|
+
:busy, :owner, :recap, :auto_title, :ctx_pct, :parent_id, :continues, :delegate, :archived,
|
|
27
|
+
:scratch, :test_run, :stopped_by, :waiting, :waiting_id, :relayed_to)
|
|
28
|
+
|
|
29
|
+
class SessionRow
|
|
30
|
+
DESC_LIMIT = 60
|
|
31
|
+
PREVIEW_LIMIT = 120
|
|
32
|
+
# Seconds a child saved "running" with no worker yet counts as one on
|
|
33
|
+
# its way (a worker boots in a few seconds): .running_or_starting?.
|
|
34
|
+
STARTING_GRACE_SECONDS = 15
|
|
35
|
+
|
|
36
|
+
# The summaries route's keys, in their order (#to_summary_h).
|
|
37
|
+
SUMMARY_KEYS = %i[id short_id desc preview title cwd project updated_at status saved_status live busy owner recap
|
|
38
|
+
auto_title ctx_pct parent_id parent_short_id continues continues_short_id delegate archived
|
|
39
|
+
scratch test_run stopped_by waiting waiting_id relayed_to].freeze
|
|
40
|
+
DERIVED_KEYS = %i[short_id parent_short_id continues_short_id].freeze
|
|
41
|
+
# `chi sessions list --format json`'s keys (of SUMMARY_KEYS: no status).
|
|
42
|
+
# owner: "worker", "tui" (a one-turn chi run: it takes no notes or
|
|
43
|
+
# messages) or nil; recap: the saved recap's first sentence, or nil;
|
|
44
|
+
# project: its git project's root, or nil; parent_id: the session that
|
|
45
|
+
# delegated it (the delegate tool), or nil; delegate: started by the
|
|
46
|
+
# delegate tool (a fork has a parent_id too); continues: the session it
|
|
47
|
+
# continues (its chain's previous link), or nil; archived: hidden from
|
|
48
|
+
# the lists (only with --archived can it be true); scratch: a `chi
|
|
49
|
+
# scratch` session; ctx_pct: how full the context was after the last
|
|
50
|
+
# turn, or nil; title: the session's own name (/title; desc shows it
|
|
51
|
+
# too), or nil; auto_title: the one its recap gave it (RecapTitle), or
|
|
52
|
+
# nil; waiting: the kind of question it waits on (question, approval,
|
|
53
|
+
# hook), or nil; waiting_id: its id, for chi answer --question;
|
|
54
|
+
# relayed_to: the parent session (short id) whose card it also waits in
|
|
55
|
+
# (the approval relay), or nil.
|
|
56
|
+
JSON_KEYS = %i[id short_id desc title auto_title cwd project updated_at live busy owner recap parent_id continues
|
|
57
|
+
delegate archived scratch ctx_pct waiting waiting_id relayed_to stopped_by].freeze
|
|
58
|
+
|
|
59
|
+
# @param session [Session] a saved session
|
|
60
|
+
# @param session_dir [String] its folder
|
|
61
|
+
# @param owner [OwnerLock::Owner, nil] the lock's holder as read
|
|
62
|
+
# (SessionManager.session_owner)
|
|
63
|
+
# @param link [WorkerLink outcome, nil] resolved with +owner+; nil:
|
|
64
|
+
# resolved here (one read of bridge.json)
|
|
65
|
+
# @param roots [Hash] ProjectScope.root_for's cache, shared by a listing
|
|
66
|
+
# @param budgets [#call] (session) -> its llm_context budget (.budgets:
|
|
67
|
+
# one HostRegistry per listing)
|
|
68
|
+
# @param saved [SessionMetrics::SavedSummary, nil, :read] the saved
|
|
69
|
+
# analytics as a caller already read it; :read reads it here
|
|
70
|
+
# @param card [RecapStore::Card, :read] the recap card, likewise
|
|
71
|
+
# @return [SessionRow]
|
|
72
|
+
def self.build(session, session_dir:, owner:, link: nil, roots: {}, budgets: SessionRow.budgets, saved: :read,
|
|
73
|
+
card: :read)
|
|
74
|
+
link ||= WorkerLink.resolve(session_dir, unlink: false, owner: owner)
|
|
75
|
+
live = link.worker_owned?
|
|
76
|
+
card = RecapStore.card(session_dir) if card == :read
|
|
77
|
+
if saved == :read
|
|
78
|
+
saved = SessionMetrics.saved_summary(session_dir, budget_tokens: -> { budgets.call(session) })
|
|
79
|
+
end
|
|
80
|
+
waiting = session.waiting_question(live: live)
|
|
81
|
+
new(id: session.id, desc: desc_of(session), preview: cut(summary_text(session), PREVIEW_LIMIT),
|
|
82
|
+
title: session.title, cwd: session.working_directory, project: session.project_root(cache: roots),
|
|
83
|
+
updated_at: session.updated_at, status: session.displayed_status(owner: owner), saved_status: session.status,
|
|
84
|
+
live: live, busy: live && session.status == Session::STATUS_RUNNING, owner: owner&.kind,
|
|
85
|
+
recap: card.preview, auto_title: card.title, ctx_pct: saved&.ctx_pct&.round(1),
|
|
86
|
+
parent_id: session.parent_id, continues: session.continues, delegate: session.delegate?,
|
|
87
|
+
archived: ArchiveStore.archived?(session_dir), scratch: session.scratch, test_run: session.test_run,
|
|
88
|
+
stopped_by: session.stopped_by, waiting: waiting&.dig(:kind), waiting_id: waiting&.dig(:id),
|
|
89
|
+
relayed_to: waiting&.dig(:relayed_to))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A row read back from #to_summary_h's hash (symbol keys; a remote's
|
|
93
|
+
# rows, Remote#symbolize'd). A key the hash lacks reads nil.
|
|
94
|
+
def self.from_summary_h(hash)
|
|
95
|
+
new(**members.to_h { |key| [key, hash[key]] })
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The budget resolver a listing shares: the process's HostRegistry
|
|
99
|
+
# (HostRegistry.current), taken on the first row that needs it (a saved
|
|
100
|
+
# context to count) and kept for the rest of the listing.
|
|
101
|
+
# @return [#call] (session) -> the session's budget in tokens or nil
|
|
102
|
+
def self.budgets
|
|
103
|
+
registry = nil
|
|
104
|
+
->(session) { EffectiveModel.session_budget(session, registry: registry ||= HostRegistry.current) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Busy (a worker runs its turn), or saved running with no worker yet
|
|
108
|
+
# for less than STARTING_GRACE_SECONDS: a worker on its way (it counts
|
|
109
|
+
# against session.max_children; Verify fresh waits for it). The
|
|
110
|
+
# displayed status shows that one as idle, so +saved_status+ is the
|
|
111
|
+
# file's.
|
|
112
|
+
def self.running_or_starting?(saved_status:, live:, busy:, updated_at:)
|
|
113
|
+
return true if busy
|
|
114
|
+
return false unless saved_status == Session::STATUS_RUNNING && !live
|
|
115
|
+
|
|
116
|
+
Time.now - Time.iso8601(updated_at.to_s) < STARTING_GRACE_SECONDS
|
|
117
|
+
rescue ArgumentError
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A session as one line of text for the lists: its title (/title), else
|
|
122
|
+
# its last prompt, else the first message it was started with (a session
|
|
123
|
+
# command like `/model x`, or a first turn not saved yet).
|
|
124
|
+
def self.summary_text(session)
|
|
125
|
+
return one_line(session.title) unless session.title.to_s.strip.empty?
|
|
126
|
+
|
|
127
|
+
one_line(session.last_prompt.to_s.strip.empty? ? session.first_preview.to_s : session.last_prompt.to_s)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A prompt as one line for a session list: whitespace collapsed, and a
|
|
131
|
+
# quoted or annotated message (ContextQuote, the web's annotations)
|
|
132
|
+
# without its "> " markers, so the words show.
|
|
133
|
+
def self.one_line(text)
|
|
134
|
+
text.to_s.gsub(/^[ \t]*(?:>[ \t]?)+/, "").gsub(/\s+/, " ").strip
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# "<cwd basename> · <last prompt, or the first preview>", one line.
|
|
138
|
+
# The flags a list line carries: a chi scratch session, a test run, an
|
|
139
|
+
# archived one, and the turn's stopper: "[looped]" for loop-guard (the
|
|
140
|
+
# bundle's thinking watch, and the word `chi sessions stats` uses for
|
|
141
|
+
# its cuts), "[stopped by X]" for any other hook (a cut with no retry
|
|
142
|
+
# left records the same stopped_by).
|
|
143
|
+
def self.flags(scratch:, test_run:, archived:, stopped_by: nil)
|
|
144
|
+
flag = if scratch
|
|
145
|
+
" [scratch]"
|
|
146
|
+
else
|
|
147
|
+
(test_run ? " [test]" : "")
|
|
148
|
+
end
|
|
149
|
+
flag += " [archived]" if archived
|
|
150
|
+
return flag if stopped_by.to_s.empty?
|
|
151
|
+
|
|
152
|
+
flag + (stopped_by.to_s == "loop-guard" ? " [looped]" : " [stopped by #{stopped_by}]")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.desc_of(session)
|
|
156
|
+
desc = [File.basename(session.working_directory.to_s), summary_text(session)].reject(&:empty?).join(" · ")
|
|
157
|
+
cut(desc, DESC_LIMIT)
|
|
158
|
+
end
|
|
159
|
+
private_class_method :desc_of
|
|
160
|
+
|
|
161
|
+
def self.cut(text, limit)
|
|
162
|
+
text.length > limit ? "#{text[0, limit - 1]}…" : text
|
|
163
|
+
end
|
|
164
|
+
private_class_method :cut
|
|
165
|
+
|
|
166
|
+
def short_id = id.to_s[0, 8]
|
|
167
|
+
def parent_short_id = parent_id&.[](0, 8)
|
|
168
|
+
def continues_short_id = continues&.[](0, 8)
|
|
169
|
+
|
|
170
|
+
def running_or_starting?
|
|
171
|
+
self.class.running_or_starting?(saved_status: saved_status, live: live, busy: busy, updated_at: updated_at)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The session as `chi sessions list` prints it, wherever it runs (the
|
|
175
|
+
# plain list, the --live/--cwd text, a remote's): its id, state (a
|
|
176
|
+
# question it waits on, else the displayed status), the context's fill,
|
|
177
|
+
# updated_at, its text (a title, else the recap's auto title or first
|
|
178
|
+
# sentence, else the last prompt; 60 characters), its flags, and the
|
|
179
|
+
# parent it was delegated from (↳) and the link it continues (↪). Reads
|
|
180
|
+
# only row fields, so a remote row prints the same line.
|
|
181
|
+
def cli_line
|
|
182
|
+
state = if relayed_to then "waiting (in parent #{relayed_to})"
|
|
183
|
+
elsif waiting then "waiting"
|
|
184
|
+
else status.to_s
|
|
185
|
+
end
|
|
186
|
+
text = (title ? preview : auto_title || recap || preview).to_s[0, DESC_LIMIT]
|
|
187
|
+
links = parent_short_id ? " ↳ #{parent_short_id}" : ""
|
|
188
|
+
links += " ↪ #{continues_short_id}" if continues_short_id
|
|
189
|
+
ctx = SessionMetrics.context_label(ctx_pct)
|
|
190
|
+
flag = self.class.flags(scratch: scratch, test_run: test_run, archived: archived, stopped_by: stopped_by)
|
|
191
|
+
"#{id} #{state.ljust(8)} #{ctx.ljust(8)} #{updated_at} #{text}#{flag}#{links}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The summaries route's row: SUMMARY_KEYS, in order.
|
|
195
|
+
# @return [Hash] symbol keys
|
|
196
|
+
def to_summary_h
|
|
197
|
+
SUMMARY_KEYS.to_h { |key| [key, public_send(key)] }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -42,24 +42,27 @@ module Samagotchi
|
|
|
42
42
|
working_set_pins: WorkingSetPin.list(working_set_pins))
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
toolkit_origin.nil? && settings.empty? && parent_answers.nil? && working_set_pins.empty?
|
|
48
|
-
end
|
|
45
|
+
# Every member at its default: none of the session's own settings.
|
|
46
|
+
def empty? = self == self.class.new
|
|
49
47
|
|
|
50
|
-
# A session's own setup, for a session that starts from it
|
|
51
|
-
#
|
|
52
|
-
#
|
|
48
|
+
# A session's own setup, for a session that starts from it: each
|
|
49
|
+
# member is the session's field of the same name (Session.new_session
|
|
50
|
+
# applies them by name), but for SessionSetup::CARRY's. Its toolkit
|
|
51
|
+
# always: one from before toolkits is full (Toolkit.of), not whatever
|
|
52
|
+
# config's default is now.
|
|
53
53
|
# @param session [Session, nil]
|
|
54
54
|
# @return [SessionSetup]
|
|
55
55
|
def self.of(session)
|
|
56
56
|
return new if session.nil?
|
|
57
57
|
|
|
58
|
-
new(
|
|
59
|
-
appended_prompt_files: session.appended_prompt_files, toolkit: Toolkit.of(session.toolkit),
|
|
60
|
-
toolkit_origin: "inherited",
|
|
61
|
-
settings: session.settings, parent_answers: session.parent_answers,
|
|
62
|
-
working_set_pins: session.working_set_pins)
|
|
58
|
+
new(**members.to_h { |member| [member, self::CARRY.fetch(member) { ->(s) { s.public_send(member) } }.call(session)] })
|
|
63
59
|
end
|
|
64
60
|
end
|
|
61
|
+
|
|
62
|
+
# How SessionSetup.of carries a member that isn't its session's field as
|
|
63
|
+
# it is: member => (session) -> value.
|
|
64
|
+
SessionSetup::CARRY = {
|
|
65
|
+
toolkit: ->(session) { Toolkit.of(session.toolkit) },
|
|
66
|
+
toolkit_origin: ->(_session) { "inherited" }
|
|
67
|
+
}.freeze
|
|
65
68
|
end
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "session"
|
|
5
|
+
require_relative "session_row"
|
|
5
6
|
require_relative "session_manager"
|
|
6
7
|
require_relative "session_retention"
|
|
7
8
|
require_relative "project_scope"
|
|
8
9
|
require_relative "analytics_file"
|
|
9
10
|
require_relative "session_metrics"
|
|
10
|
-
require_relative "recap_store"
|
|
11
11
|
require_relative "session_delete_command"
|
|
12
12
|
require_relative "session_archive_command"
|
|
13
13
|
require_relative "worker_link"
|
|
@@ -142,28 +142,6 @@ module Samagotchi
|
|
|
142
142
|
options.to_h { |key, value| [key, INTEGER_OPTIONS.include?(key) ? Integer(value, 10) : value] }
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
# The flags a list row carries: a chi scratch session, a test run, an
|
|
146
|
-
# archived one, and the turn's stopper (see #stopped_flag).
|
|
147
|
-
def session_flag(scratch:, test_run:, archived:, stopped_by: nil)
|
|
148
|
-
flag = if scratch
|
|
149
|
-
" [scratch]"
|
|
150
|
-
else
|
|
151
|
-
(test_run ? " [test]" : "")
|
|
152
|
-
end
|
|
153
|
-
flag += " [archived]" if archived
|
|
154
|
-
flag + stopped_flag(stopped_by)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# The marker for a turn a hook stopped: "[looped]" for loop-guard (the
|
|
158
|
-
# bundle's thinking watch, and the word `chi sessions stats` uses for its
|
|
159
|
-
# cuts), "[stopped by X]" for any other hook; "" otherwise. A cut with no
|
|
160
|
-
# retry left records the same stopped_by.
|
|
161
|
-
def stopped_flag(by)
|
|
162
|
-
return "" if by.to_s.empty?
|
|
163
|
-
|
|
164
|
-
by.to_s == "loop-guard" ? " [looped]" : " [stopped by #{by}]"
|
|
165
|
-
end
|
|
166
|
-
|
|
167
145
|
def list
|
|
168
146
|
format, scope, cwd = @opts.values_at(:format, :scope, :cwd)
|
|
169
147
|
unless format.nil? || %w[text json tsv].include?(format)
|
|
@@ -175,7 +153,8 @@ module Samagotchi
|
|
|
175
153
|
# chi web's; --scope=all, a folder in no repo or an explicit --cwd: every
|
|
176
154
|
# project's. Machine-local (this cwd's project): remotely every
|
|
177
155
|
# project's (D4), and --scope=project is --cwd's project there (the
|
|
178
|
-
# server finds its root). The rows are the server's
|
|
156
|
+
# server finds its root). The rows are SessionRows, the server's
|
|
157
|
+
# remotely: one line wherever the list runs (SessionRow#cli_line).
|
|
179
158
|
# The picker (chi note from a script): filters apply before the limit,
|
|
180
159
|
# and test runs stay out, unless this is one (SAMAGOTCHI_ENV=test, CI):
|
|
181
160
|
# then its own sessions are what it looks for. The plain listing shows
|
|
@@ -191,10 +170,7 @@ module Samagotchi
|
|
|
191
170
|
end
|
|
192
171
|
project = scope == "all" || cwd ? nil : Samagotchi::ProjectScope.root_for(Dir.pwd)
|
|
193
172
|
scope_note = ->(count) { "#{count} session(s) in #{File.basename(project)} (--scope=all: every project)" }
|
|
194
|
-
|
|
195
|
-
return list_summaries(project, scope_note, include_tests: include_tests) if picker
|
|
196
|
-
|
|
197
|
-
list_plain(project, scope_note)
|
|
173
|
+
list_summaries(project, scope_note, include_tests: include_tests)
|
|
198
174
|
end
|
|
199
175
|
|
|
200
176
|
def list_summaries(project, scope_note, include_tests:)
|
|
@@ -207,117 +183,23 @@ module Samagotchi
|
|
|
207
183
|
)
|
|
208
184
|
case format
|
|
209
185
|
when "json"
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
# its git project's root, or nil
|
|
213
|
-
# parent_id: the session that delegated it (the delegate tool), or nil
|
|
214
|
-
# delegate: started by the delegate tool (a fork has a parent_id too)
|
|
215
|
-
# continues: the session it continues (its chain's previous link), or nil
|
|
216
|
-
# archived: hidden from the lists (only with --archived can it be true)
|
|
217
|
-
# scratch: a `chi scratch` session (deleted when its terminal leaves,
|
|
218
|
-
# a leftover one at the next sweep)
|
|
219
|
-
# ctx_pct: how full the context was after the last turn, or nil
|
|
220
|
-
# title: the session's own name (/title; desc shows it too), or nil;
|
|
221
|
-
# auto_title: the one its recap gave it (RecapTitle), or nil
|
|
222
|
-
# waiting: the kind of question it waits on (question, approval,
|
|
223
|
-
# hook: chi answer or the web answers it), or nil; waiting_id: its
|
|
224
|
-
# id, for chi answer --question; relayed_to: the parent session (short
|
|
225
|
-
# id) whose card it also waits in (the approval relay), or nil
|
|
226
|
-
keys = %i[id short_id desc title auto_title cwd project updated_at live busy owner recap parent_id continues delegate archived scratch ctx_pct
|
|
227
|
-
waiting waiting_id relayed_to stopped_by]
|
|
228
|
-
@stdout.puts JSON.generate(summaries.map { |summary| summary.slice(*keys) })
|
|
186
|
+
# SessionRow::JSON_KEYS (its comment says what each one is).
|
|
187
|
+
@stdout.puts JSON.generate(summaries.map { |row| row.to_summary_h.slice(*Samagotchi::SessionRow::JSON_KEYS) })
|
|
229
188
|
when "tsv"
|
|
230
|
-
summaries.each { |
|
|
189
|
+
summaries.each { |row| @stdout.puts "#{row.id}\t#{row.desc}" }
|
|
231
190
|
else
|
|
232
|
-
summaries.each
|
|
233
|
-
state = if summary[:relayed_to] then "waiting (in parent #{summary[:relayed_to]})"
|
|
234
|
-
elsif summary[:waiting] then "waiting"
|
|
235
|
-
elsif summary[:busy] then "running"
|
|
236
|
-
else summary[:live] ? "live" : summary[:status].to_s
|
|
237
|
-
end
|
|
238
|
-
# The recap's auto title, else its first sentence, says more than
|
|
239
|
-
# the last prompt (text only: tsv and json keep desc for pickers);
|
|
240
|
-
# a title (in desc) says what the user named it.
|
|
241
|
-
desc = summary[:desc]
|
|
242
|
-
named = summary[:auto_title] || summary[:recap]
|
|
243
|
-
if named && !summary[:title]
|
|
244
|
-
desc = [File.basename(summary[:cwd].to_s), named].reject(&:empty?).join(" · ")
|
|
245
|
-
desc = "#{desc[0, 59]}…" if desc.length > 60
|
|
246
|
-
end
|
|
247
|
-
# A delegated session points at its parent.
|
|
248
|
-
child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
|
|
249
|
-
# A chain's link points at the one it continues.
|
|
250
|
-
child += " ↪ #{summary[:continues_short_id]}" if summary[:continues_short_id]
|
|
251
|
-
flag = session_flag(scratch: summary[:scratch], test_run: summary[:test_run], archived: summary[:archived],
|
|
252
|
-
stopped_by: summary[:stopped_by])
|
|
253
|
-
ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
|
|
254
|
-
@stdout.puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
|
|
255
|
-
end
|
|
191
|
+
summaries.each { |row| @stdout.puts row.cli_line }
|
|
256
192
|
if project
|
|
257
193
|
@stdout.puts "#{"\n" unless summaries.empty?}#{scope_note.call(summaries.size)}"
|
|
194
|
+
elsif summaries.empty?
|
|
195
|
+
@stdout.puts "No sessions."
|
|
258
196
|
else
|
|
259
|
-
@stdout.puts
|
|
197
|
+
@stdout.puts "\n#{summaries.size} session(s) (sort=#{sort || "updated_at"} order=#{order || "desc"})"
|
|
260
198
|
end
|
|
261
199
|
end
|
|
262
200
|
0
|
|
263
201
|
end
|
|
264
202
|
|
|
265
|
-
def list_plain(project, scope_note)
|
|
266
|
-
sort, order, limit, include_archived = @opts.values_at(:sort, :order, :limit, :include_archived)
|
|
267
|
-
sort ||= "updated_at"
|
|
268
|
-
order ||= "desc"
|
|
269
|
-
sessions = @access.list(sort: sort, order: order, limit: limit, project_root: project, include_archived: include_archived)
|
|
270
|
-
# The session's title, else the recap's auto title, else its first
|
|
271
|
-
# sentence, else the last prompt (or the first message, before any
|
|
272
|
-
# turn), cut as before.
|
|
273
|
-
# These rows read each session's folder here (its recap, saved
|
|
274
|
-
# context, owner): machine-local, the server's rows remotely (P2's
|
|
275
|
-
# list parity).
|
|
276
|
-
state_dir = Samagotchi::Session.default_state_dir
|
|
277
|
-
list_text = lambda do |s|
|
|
278
|
-
card = s.title ? nil : Samagotchi::RecapStore.card(Samagotchi::Session.session_dir(s.id, state_dir: state_dir))
|
|
279
|
-
(card&.title || card&.preview || Samagotchi::SessionManager.summary_text(s))[0, 60]
|
|
280
|
-
end
|
|
281
|
-
# How full the context was after the last turn: "ctx 12%", counted
|
|
282
|
-
# against the session's llm_context budget when one is set and smaller
|
|
283
|
-
# than the window, as the live meter counts it (one registry for the
|
|
284
|
-
# whole table, built when the first row has a saved context). Blank when
|
|
285
|
-
# unknown.
|
|
286
|
-
budgets = nil
|
|
287
|
-
ctx = lambda do |s|
|
|
288
|
-
Samagotchi::SessionMetrics.context_label(
|
|
289
|
-
Samagotchi::SessionMetrics.saved_context_pct(
|
|
290
|
-
Samagotchi::Session.session_dir(s.id, state_dir: state_dir),
|
|
291
|
-
budget_tokens: -> { Samagotchi::LLMContextStrategy.session_budget(s, registry: budgets ||= Samagotchi::HostRegistry.new) }
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
end
|
|
295
|
-
# A delegated session points at its parent: ↳ <parent's short id>.
|
|
296
|
-
row = lambda do |s|
|
|
297
|
-
flag = session_flag(scratch: s.scratch, test_run: s.test_run, archived: s.archived, stopped_by: s.stopped_by)
|
|
298
|
-
child = s.parent_id ? " ↳ #{s.parent_id[0, 8]}" : ""
|
|
299
|
-
child += " ↪ #{s.continues[0, 8]}" if s.continues
|
|
300
|
-
# A question waits for an answer (chi answer, the web, chi --attach).
|
|
301
|
-
live = s.pending_question && worker_owned?(s.id, state_dir)
|
|
302
|
-
waiting = s.waiting_question(live: !!live)
|
|
303
|
-
status = if waiting&.dig(:relayed_to) then "waiting (in parent #{waiting[:relayed_to]})"
|
|
304
|
-
elsif waiting then "waiting"
|
|
305
|
-
else s.status
|
|
306
|
-
end
|
|
307
|
-
"#{s.id} #{status.ljust(8)} #{ctx.call(s).ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
|
|
308
|
-
end
|
|
309
|
-
if project
|
|
310
|
-
sessions.each { |s| @stdout.puts row.call(s) }
|
|
311
|
-
@stdout.puts "#{"\n" unless sessions.empty?}#{scope_note.call(sessions.size)}"
|
|
312
|
-
elsif sessions.empty?
|
|
313
|
-
@stdout.puts "No sessions."
|
|
314
|
-
else
|
|
315
|
-
sessions.each { |s| @stdout.puts row.call(s) }
|
|
316
|
-
@stdout.puts "\n#{sessions.size} session(s) (sort=#{sort} order=#{order})"
|
|
317
|
-
end
|
|
318
|
-
0
|
|
319
|
-
end
|
|
320
|
-
|
|
321
203
|
# One session's cost, tokens and progress, for a script or an agent that
|
|
322
204
|
# drives sessions (chi send --wait): a live worker's GET stats, else the
|
|
323
205
|
# snapshot its saved analytics.json rebuilds. Never starts a worker, and
|
|
@@ -101,7 +101,7 @@ module Samagotchi
|
|
|
101
101
|
error_line("#{command_name}: #{e.message}")
|
|
102
102
|
CLI::Exit::FAILED
|
|
103
103
|
rescue ArgumentError => e
|
|
104
|
-
error_line("#{command_name}: #{e.
|
|
104
|
+
error_line("#{command_name}: #{e.message}")
|
|
105
105
|
CLI::Exit::FAILED
|
|
106
106
|
end
|
|
107
107
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "atomic_file"
|
|
7
|
+
require_relative "session"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# A continue's "starting" marker on its new link: written before the
|
|
11
|
+
# continue moves delegates there (ChildMove) and spawns the link's worker,
|
|
12
|
+
# removed by that worker once it holds the owner lock (Worker#start). While
|
|
13
|
+
# it is #fresh?, nothing else spawns a worker for the link (SpawnPart's
|
|
14
|
+
# resume_session and wake_for_report): a ring from a moved child must not
|
|
15
|
+
# start a second one. CROSS-VERSION: the file name and its {pid, at} are
|
|
16
|
+
# read and cleared by other chi versions (spec/worker_contract_spec.rb).
|
|
17
|
+
module StartingMarker
|
|
18
|
+
# <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
|
|
19
|
+
FILE = "starting"
|
|
20
|
+
# A starting marker older than this, or whose process is gone, is left
|
|
21
|
+
# over from a continue that died: it holds nothing back.
|
|
22
|
+
STALE = 60.0
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Hold off a worker for the continue's new link (#fresh?) until its
|
|
27
|
+
# own worker holds it: written before the move, removed by that worker
|
|
28
|
+
# once it has the owner lock (Worker#start).
|
|
29
|
+
def mark(session_id, state_dir:)
|
|
30
|
+
dir = Session.session_dir(session_id, state_dir: state_dir)
|
|
31
|
+
FileUtils.mkdir_p(dir)
|
|
32
|
+
AtomicFile.write(File.join(dir, FILE), JSON.generate({ "pid" => Process.pid, "at" => Time.now.iso8601(3) }))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear(session_id, state_dir:)
|
|
36
|
+
FileUtils.rm_f(File.join(Session.session_dir(session_id, state_dir: state_dir), FILE))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether a continue is moving delegates to +session_id+ right now: its
|
|
40
|
+
# marker is there, fresh, and the process that wrote it runs.
|
|
41
|
+
def fresh?(session_id, state_dir:)
|
|
42
|
+
file = File.join(Session.session_dir(session_id, state_dir: state_dir), FILE)
|
|
43
|
+
return false unless File.exist?(file)
|
|
44
|
+
|
|
45
|
+
data = JSON.parse(File.read(file))
|
|
46
|
+
return false if Time.now - Time.iso8601(data["at"].to_s) > STALE
|
|
47
|
+
|
|
48
|
+
process_alive?(data["pid"].to_i)
|
|
49
|
+
rescue JSON::ParserError, ArgumentError, SystemCallError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Signal 0 only asks whether the process exists.
|
|
54
|
+
def process_alive?(pid)
|
|
55
|
+
return false unless pid.positive?
|
|
56
|
+
|
|
57
|
+
Process.kill(0, pid)
|
|
58
|
+
true
|
|
59
|
+
rescue Errno::ESRCH
|
|
60
|
+
false
|
|
61
|
+
rescue Errno::EPERM
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -32,27 +32,20 @@ module Samagotchi
|
|
|
32
32
|
nil
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# The seconds for +
|
|
35
|
+
# The seconds for +lookup+'s model and where they came from
|
|
36
36
|
# ("SAMAGOTCHI_STEER_CUT_AFTER", "models: qwen", "hosts.splash",
|
|
37
37
|
# "steer.cut_after"; nil for the default).
|
|
38
|
-
# @param
|
|
39
|
-
# @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
|
|
40
|
-
# @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
|
|
38
|
+
# @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
|
|
41
39
|
# @return [Array(Integer, String|nil)]
|
|
42
|
-
def resolve(
|
|
43
|
-
value, origin =
|
|
40
|
+
def resolve(lookup)
|
|
41
|
+
value, origin = lookup.global("steer.cut_after")
|
|
44
42
|
return [value.to_i, ENV_KEY] if origin == :env
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
ConfigFile.model_settings
|
|
48
|
-
rescue StandardError
|
|
49
|
-
{}
|
|
50
|
-
end
|
|
51
|
-
setting = ConfigFile.model_setting(names, :steer_cut_after, models: models)
|
|
44
|
+
setting = lookup.model(:steer_cut_after)
|
|
52
45
|
return [setting.value, "models: #{setting.key}"] if setting
|
|
53
46
|
|
|
54
|
-
host =
|
|
55
|
-
return [host, "hosts.#{
|
|
47
|
+
host = lookup.host(:steer_cut_after)
|
|
48
|
+
return [host, "hosts.#{lookup.host_name}"] unless host.nil?
|
|
56
49
|
|
|
57
50
|
return [value.to_i, "steer.cut_after"] if origin == :file
|
|
58
51
|
|