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
|
@@ -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.displayed_status(owner: Samagotchi::SessionManager.session_owner(s.id, state_dir: state_dir))
|
|
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
|
data/lib/samagotchi/steer.rb
CHANGED
|
@@ -364,6 +364,16 @@ module Samagotchi
|
|
|
364
364
|
turn_prompt?(message) && !shell_line?(message)
|
|
365
365
|
end
|
|
366
366
|
|
|
367
|
+
# An opening's stamp: its turn_id (a prompt's, a wake note's), else its
|
|
368
|
+
# own opening_id (/continue no's reason line); nil for neither (a line
|
|
369
|
+
# an older chi wrote). Where a card (Bridge::CardPlaces) and the recap
|
|
370
|
+
# (RecapPlace) sit.
|
|
371
|
+
# @return [String, nil]
|
|
372
|
+
def opening_stamp(message)
|
|
373
|
+
id = message[:turn_id] || message["turn_id"] || message[:opening_id] || message["opening_id"]
|
|
374
|
+
id.is_a?(String) && !id.empty? ? id : nil
|
|
375
|
+
end
|
|
376
|
+
|
|
367
377
|
# Call a loop's drain: +at_answer+ goes only to a drain that takes it
|
|
368
378
|
# (the Engine's); a caller's own drain (a queue's #drain) is called bare.
|
|
369
379
|
# A failing drain drains nothing (the loop keeps going).
|
|
@@ -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] ModelSettings.all (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
|
-
ModelSettings.all
|
|
48
|
-
rescue StandardError
|
|
49
|
-
{}
|
|
50
|
-
end
|
|
51
|
-
setting = ModelSettings.find(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
|
|
|
@@ -291,7 +291,7 @@ module Samagotchi
|
|
|
291
291
|
6. Use write for full-file rewrites or creating new files.
|
|
292
292
|
|
|
293
293
|
Memory convention:
|
|
294
|
-
Project scope: one folder per git repository, shared by its worktrees and subdirectories
|
|
294
|
+
Project scope: one folder per project (a git repository, shared by its worktrees and subdirectories, or a folder marked by `chi project init` and its subfolders; its path is on the "Project memories folder" line below)
|
|
295
295
|
System scope: ~/.config/samagotchi/memories/ (cross-project)
|
|
296
296
|
memory_read accepts optional scope (project|system).
|
|
297
297
|
memory_write requires explicit scope and entry name.
|
|
@@ -623,10 +623,20 @@ module Samagotchi
|
|
|
623
623
|
File.expand_path(path)
|
|
624
624
|
end
|
|
625
625
|
|
|
626
|
+
# The project_location line over its top, for a checkout (git) or a
|
|
627
|
+
# marked project: [below the top, at the top].
|
|
628
|
+
TOP_LINES = {
|
|
629
|
+
git: ["Top of this checkout (where the project's commands run from; stay inside it):",
|
|
630
|
+
"This checkout (the project's files; its commands run here):"],
|
|
631
|
+
marker: ["Top of this project (where its commands run from; stay inside it):",
|
|
632
|
+
"This project (its files; its commands run here):"]
|
|
633
|
+
}.freeze
|
|
634
|
+
|
|
626
635
|
# Where the session runs and which project memory folder it uses. Only
|
|
627
|
-
# this checkout
|
|
628
|
-
#
|
|
629
|
-
# 2026-10-06), and in a linked worktree
|
|
636
|
+
# this checkout (or marked project, MemoryPaths.lookup) is named: its
|
|
637
|
+
# top (at its root too, so a model doesn't take the memories folder for
|
|
638
|
+
# the project and cd there, Qwen 2026-10-06), and in a linked worktree
|
|
639
|
+
# a line that work stays in it. The repository's main
|
|
630
640
|
# checkout (MemoryPaths.project_root, which scopes memories and session
|
|
631
641
|
# lists) is never shown: a model told its path went to work there
|
|
632
642
|
# (a worktree child committed onto main, 2026-10-03 and 2026-10-06).
|
|
@@ -635,16 +645,15 @@ module Samagotchi
|
|
|
635
645
|
def project_location
|
|
636
646
|
Degrade.call(:prompt, "location_failed") do
|
|
637
647
|
cwd = Dir.pwd
|
|
638
|
-
|
|
648
|
+
found = MemoryPaths.lookup(cwd)
|
|
649
|
+
top = found.top
|
|
639
650
|
lines = ["Current working directory:", cwd]
|
|
640
|
-
if top
|
|
641
|
-
|
|
642
|
-
lines << top
|
|
643
|
-
elsif top
|
|
644
|
-
lines << "This checkout (the project's files; its commands run here):"
|
|
651
|
+
if top
|
|
652
|
+
below, at = TOP_LINES.fetch(found.kind)
|
|
653
|
+
lines << (top == cwd ? at : below)
|
|
645
654
|
lines << top
|
|
646
655
|
end
|
|
647
|
-
if
|
|
656
|
+
if found.git? && found.root != top
|
|
648
657
|
lines << "This checkout is a linked git worktree: read, edit, run and commit only in it, never in the " \
|
|
649
658
|
"repository's other checkouts."
|
|
650
659
|
end
|
|
@@ -613,39 +613,19 @@ module Samagotchi
|
|
|
613
613
|
lane = :writing
|
|
614
614
|
when :generation_chunk
|
|
615
615
|
tail, lane = event[:thinking].to_s.empty? ? [event[:text], :writing] : [event[:thinking], :thinking]
|
|
616
|
-
|
|
617
|
-
running_tool = event[:tool]
|
|
618
|
-
@renderer.call(event)
|
|
619
|
-
when :tool_call_completed
|
|
620
|
-
running_tool = nil
|
|
621
|
-
replay_tool_completed(event)
|
|
622
|
-
when :merged_input then @screen.commit("input> #{event[:content]}")
|
|
623
|
-
when :reminder_injected then @screen.commit(reminder_line(event[:reminders]))
|
|
624
|
-
# The turn's start (its images), a plugin's steer, and its rows (a
|
|
625
|
-
# hook's notice, a retry's line), as they were drawn live.
|
|
626
|
-
# Questions are left out: the pending one is asked after the
|
|
627
|
-
# replay, answered ones showed no line.
|
|
616
|
+
# The turn's start (its images), as it was drawn live.
|
|
628
617
|
when :turn_started then @renderer.call(without_named_images(event, turn[:prompt]))
|
|
629
|
-
|
|
630
|
-
|
|
618
|
+
# Its rows, as a drawn turn's (JoinReplay#replay_row).
|
|
619
|
+
else
|
|
620
|
+
running_tool = event[:tool] if event[:type] == :tool_call_started
|
|
621
|
+
running_tool = nil if event[:type] == :tool_call_completed
|
|
622
|
+
@join_replay.replay_row(event)
|
|
631
623
|
end
|
|
632
624
|
end
|
|
633
625
|
@view.resume(tail: tail, lane: lane, tool: running_tool, parts: turn[:parts])
|
|
634
626
|
ask(turn[:pending_question]) if turn[:pending_question]
|
|
635
627
|
end
|
|
636
628
|
|
|
637
|
-
# A finished tool call of the joined turn: the live row (action,
|
|
638
|
-
# duration). A snapshot from an older worker has no action: its row
|
|
639
|
-
# names the tool only.
|
|
640
|
-
def replay_tool_completed(event)
|
|
641
|
-
return @renderer.call({ duration_ms: nil }.merge(event)) if event.dig(:activity, :action)
|
|
642
|
-
|
|
643
|
-
activity = EventRenderer.described(event) || {}
|
|
644
|
-
@screen.commit(snapshot_tool_line({ tool: event[:tool], params: activity[:params], status: activity[:status],
|
|
645
|
-
description: activity[:description], output_truncated: event[:output_truncated],
|
|
646
|
-
images: event[:images], diff: event[:diff] }))
|
|
647
|
-
end
|
|
648
|
-
|
|
649
629
|
def start_turn(event)
|
|
650
630
|
@running = true
|
|
651
631
|
@joined_mid_turn = false
|