samagotchi 0.33.0 → 0.35.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 +91 -1
- data/README.md +6 -1
- data/bin/chi +3 -0
- data/docs/broadcast.md +129 -0
- data/docs/cli.md +8 -0
- data/docs/configuration.md +2 -0
- data/docs/context.md +5 -0
- data/docs/guardrails.md +35 -3
- data/docs/hooks.md +25 -4
- data/docs/memory.md +17 -1
- data/docs/plugins.md +78 -3
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +15 -9
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- data/lib/samagotchi/broadcast/recipients.rb +63 -0
- data/lib/samagotchi/broadcast/scope_card.rb +94 -0
- data/lib/samagotchi/broadcast/tags.rb +154 -0
- data/lib/samagotchi/broadcast_command.rb +228 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +14 -0
- data/lib/samagotchi/bundles/coordinator/plugin.rb +114 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +11 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -11
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +32 -12
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/delegated.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/children_status.rb +143 -0
- data/lib/samagotchi/config.rb +6 -0
- data/lib/samagotchi/cut_policy.rb +58 -0
- data/lib/samagotchi/engine.rb +45 -108
- data/lib/samagotchi/engine_peers.rb +30 -0
- data/lib/samagotchi/git_head.rb +24 -0
- data/lib/samagotchi/guardrail_wiring.rb +27 -2
- data/lib/samagotchi/guardrails/approvals.rb +7 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +103 -0
- data/lib/samagotchi/guardrails/context.rb +32 -0
- data/lib/samagotchi/guardrails/shared_git.rb +124 -0
- data/lib/samagotchi/guardrails/verdict.rb +4 -3
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +11 -5
- data/lib/samagotchi/kernel_loop.rb +9 -22
- data/lib/samagotchi/llm/chat_loop.rb +11 -22
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/memory_bundle/builder.rb +14 -1
- data/lib/samagotchi/memory_bundle/installer.rb +6 -2
- data/lib/samagotchi/memory_bundle/manifest.rb +26 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -1
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +13 -13
- data/lib/samagotchi/note_delivery.rb +42 -0
- data/lib/samagotchi/plugin/api.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +4 -4
- data/lib/samagotchi/plugin/sessions.rb +45 -1
- data/lib/samagotchi/process_group.rb +2 -5
- data/lib/samagotchi/session_manager.rb +4 -3
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/steer_cut.rb +125 -0
- data/lib/samagotchi/system_prompt.rb +21 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +25 -2
- data/lib/samagotchi/terminal_ui/status_row.rb +23 -2
- data/lib/samagotchi/terminal_ui.rb +14 -0
- data/lib/samagotchi/tool_declarations.rb +9 -5
- data/lib/samagotchi/tools/builtins.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +103 -14
- data/lib/samagotchi/tools/delegate_wait.rb +55 -13
- data/lib/samagotchi/tools/send_note.rb +5 -11
- data/lib/samagotchi/turn_note.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/capabilities.rb +19 -0
- data/lib/samagotchi/web/public/activity.js +26 -6
- data/lib/samagotchi/web/public/app.js +515 -288
- data/lib/samagotchi/web/public/card.js +12 -3
- data/lib/samagotchi/web/public/command_view.js +2 -3
- data/lib/samagotchi/web/public/families_view.js +171 -0
- data/lib/samagotchi/web/public/format.js +52 -0
- data/lib/samagotchi/web/public/index.html +94 -21
- data/lib/samagotchi/web/public/live_timing.js +154 -0
- data/lib/samagotchi/web/public/live_turn.js +44 -0
- data/lib/samagotchi/web/public/sessions_list.js +228 -32
- data/lib/samagotchi/web/public/strip.js +5 -2
- data/lib/samagotchi/web/public/turn_html.js +107 -0
- data/lib/samagotchi/web/public/turn_model.js +5 -4
- data/lib/samagotchi/web/public/turn_view.js +43 -129
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +173 -309
- data/lib/samagotchi/worker_inbound.rb +251 -0
- data/lib/samagotchi/worker_wakes.rb +120 -0
- metadata +24 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
require_relative "client_id"
|
|
8
|
+
require_relative "config"
|
|
9
|
+
require_relative "context_note"
|
|
10
|
+
require_relative "log"
|
|
11
|
+
require_relative "session"
|
|
12
|
+
require_relative "session_inbox"
|
|
13
|
+
|
|
14
|
+
module Samagotchi
|
|
15
|
+
# What comes into a worker's session between turns, each kind with its
|
|
16
|
+
# own save rule (the Worker runs the turns they start):
|
|
17
|
+
#
|
|
18
|
+
# - context notes (#absorb_notes): into the conversation, saved, then
|
|
19
|
+
# their files deleted;
|
|
20
|
+
# - attached context (#absorb_context): once the conversation started,
|
|
21
|
+
# saved, then the subscriptions written; a source asking to wake may
|
|
22
|
+
# start a context wake turn (a ContextWake);
|
|
23
|
+
# - the first prompt (#take_initial_prompt): taken once and saved
|
|
24
|
+
# cleared; as a command (#initial_command?) the session is saved idle;
|
|
25
|
+
# - an input file (#take_input): a Prompt for a turn, whose own save is
|
|
26
|
+
# the turn's, or a command queued.
|
|
27
|
+
class WorkerInbound
|
|
28
|
+
# One context wake per source in this long (D5): later changes inside
|
|
29
|
+
# it arrive as notes.
|
|
30
|
+
CONTEXT_WAKE_WINDOW = 600
|
|
31
|
+
|
|
32
|
+
# A prompt to run a turn for: an input file's, or the first prompt.
|
|
33
|
+
# +no_interrupt+: an offer its turn makes keeps it for its continue
|
|
34
|
+
# turn; +images+: its image refs ({file:, name:}).
|
|
35
|
+
Prompt = Data.define(:text, :origin, :no_interrupt, :images) do
|
|
36
|
+
def initialize(text:, origin: nil, no_interrupt: false, images: []) = super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A context wake turn to run: the ContextAbsorber::Delivery that woke
|
|
40
|
+
# it, whose wake note is already in the conversation as the start of
|
|
41
|
+
# the turn +turn_id+.
|
|
42
|
+
ContextWake = Data.define(:delivery, :turn_id) do
|
|
43
|
+
def name = delivery.name
|
|
44
|
+
def origin = { client_id: "#{ClientId::CONTEXT_PREFIX}#{name}" }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A save between turns (the turn's own, a command's, the notes', the
|
|
48
|
+
# first prompt's): what it saves is in memory, so one that fails (disk
|
|
49
|
+
# full, permissions; the next save writes it) is logged and must not
|
|
50
|
+
# take the worker down with it.
|
|
51
|
+
# @param at [Symbol] which save, for the log
|
|
52
|
+
# @return [Boolean] whether it saved
|
|
53
|
+
def self.save_or_log(at)
|
|
54
|
+
yield
|
|
55
|
+
true
|
|
56
|
+
rescue SystemCallError, IOError => e
|
|
57
|
+
Log.exception(:worker, "save_failed", e, at: at)
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param wakes [WorkerWakes] the wake budget a context wake asks
|
|
62
|
+
# @param awaiting_continue [#call] whether a continue offer waits
|
|
63
|
+
# @param stopped [#call] whether the session was stopped on disk
|
|
64
|
+
# @param queue_command [#call] (line, client_id) queues a session
|
|
65
|
+
# command for the loop's next pass
|
|
66
|
+
def initialize(session:, state_dir:, session_dir:, engine:, context_absorber:, wakes:, awaiting_continue:,
|
|
67
|
+
stopped:, queue_command:)
|
|
68
|
+
@session = session
|
|
69
|
+
@state_dir = state_dir
|
|
70
|
+
@session_dir = session_dir
|
|
71
|
+
@engine = engine
|
|
72
|
+
@context_absorber = context_absorber
|
|
73
|
+
@wakes = wakes
|
|
74
|
+
@awaiting_continue = awaiting_continue
|
|
75
|
+
@stopped = stopped
|
|
76
|
+
@queue_command = queue_command
|
|
77
|
+
@initial_prompt_taken = false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Add the queued context notes to the conversation (between turns only,
|
|
81
|
+
# on the loop's thread), save, then delete their files: a crash before
|
|
82
|
+
# the delete leaves them claimed, and Engine#add_context_note skips a
|
|
83
|
+
# note the saved conversation already holds. A failed save keeps the
|
|
84
|
+
# files too: the next pass claims them again, finds them in the
|
|
85
|
+
# conversation and saves again. Not activity: a note alone neither
|
|
86
|
+
# starts a turn nor keeps an idle worker up.
|
|
87
|
+
def absorb_notes
|
|
88
|
+
files = SessionInbox.find_new_note_files(@session_dir)
|
|
89
|
+
return if files.empty? || @stopped.call
|
|
90
|
+
|
|
91
|
+
claimed = files.filter_map { |file| SessionInbox.claim_note_file(file) }
|
|
92
|
+
claimed.each do |file|
|
|
93
|
+
note = SessionInbox.read_note(file)
|
|
94
|
+
@engine.add_context_note(@session, note) if note
|
|
95
|
+
end
|
|
96
|
+
return unless save(:notes)
|
|
97
|
+
|
|
98
|
+
claimed.each { |file| FileUtils.rm_f(file) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The attached context's notes (ContextAbsorber), between turns. Not
|
|
102
|
+
# into a session with no turn yet (+before_turn+: one is about to
|
|
103
|
+
# run): auto-attached context alone doesn't make a session worth
|
|
104
|
+
# keeping. Saved, then the subscriptions written (a crash between the
|
|
105
|
+
# two re-delivers; the note ids dedupe).
|
|
106
|
+
# +wake+: the session is idle with nothing queued, so an update whose
|
|
107
|
+
# source asked to wake may start a turn (#context_wake_for): its note
|
|
108
|
+
# goes in as the turn's first message.
|
|
109
|
+
# @return [ContextWake, nil] the wake turn to run now
|
|
110
|
+
# @raise [SystemCallError, IOError] the caller logs it (context_failed)
|
|
111
|
+
def absorb_context(before_turn: false, wake: false)
|
|
112
|
+
return nil unless before_turn || conversation_started?
|
|
113
|
+
return nil if @stopped.call
|
|
114
|
+
|
|
115
|
+
batch = @context_absorber.pending
|
|
116
|
+
return nil unless batch
|
|
117
|
+
|
|
118
|
+
waking = wake ? context_wake_for(batch) : nil
|
|
119
|
+
turn_id = SecureRandom.uuid if waking
|
|
120
|
+
batch.deliveries.each do |delivery|
|
|
121
|
+
next unless delivery.note
|
|
122
|
+
|
|
123
|
+
note = delivery == waking ? delivery.wake_note.merge(turn_start: true, turn_id: turn_id) : delivery.note
|
|
124
|
+
@engine.add_context_note(@session, note)
|
|
125
|
+
end
|
|
126
|
+
# Saved even when every note was there already: after a failed save
|
|
127
|
+
# they are in memory only.
|
|
128
|
+
return nil if batch.notes.any? && !save(:context)
|
|
129
|
+
|
|
130
|
+
@context_absorber.commit(waking ? woken(batch, waking) : batch)
|
|
131
|
+
waking && ContextWake.new(delivery: waking, turn_id: turn_id)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A failed wake turn kept nothing but its note: the update goes back to
|
|
135
|
+
# a plain note (no turn start, the background wording), so a reload
|
|
136
|
+
# draws no empty turn for it.
|
|
137
|
+
def unmark_wake(context_wake)
|
|
138
|
+
messages = @engine.messages_checkpoint
|
|
139
|
+
index = messages.index { |m| m[:turn_start] && m[:turn_id] == context_wake.turn_id }
|
|
140
|
+
return unless index
|
|
141
|
+
|
|
142
|
+
messages[index] = ContextNote.message(**context_wake.delivery.note)
|
|
143
|
+
@engine.rollback_to(messages)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# spawn_session hands the first prompt over in last_prompt, but
|
|
147
|
+
# last_prompt also records every later turn's prompt (and mark_error's
|
|
148
|
+
# reason), so only a session with no conversation yet has one pending; a
|
|
149
|
+
# resumed session must not replay its last turn. Taken once.
|
|
150
|
+
# @return [String, nil]
|
|
151
|
+
def take_initial_prompt
|
|
152
|
+
return nil if @initial_prompt_taken
|
|
153
|
+
|
|
154
|
+
@initial_prompt_taken = true
|
|
155
|
+
# A context note may have come before the first prompt ran.
|
|
156
|
+
return nil unless @session.messages.all? { |m| ContextNote.note?(m) } && !@session.last_prompt.to_s.strip.empty?
|
|
157
|
+
|
|
158
|
+
prompt = @session.last_prompt
|
|
159
|
+
@session.last_prompt = ""
|
|
160
|
+
save(:initial_prompt)
|
|
161
|
+
prompt
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The first prompt as a command (queued): the session was saved as
|
|
165
|
+
# running for a turn that won't run.
|
|
166
|
+
# @return [Boolean] whether it was one
|
|
167
|
+
def initial_command?(prompt)
|
|
168
|
+
return false unless queue_as_command(prompt, nil)
|
|
169
|
+
|
|
170
|
+
@session.status = Session::STATUS_IDLE
|
|
171
|
+
save(:initial_command, unless_stopped: true)
|
|
172
|
+
true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Claim +input_file+ and yield its Prompt (the turn runs in the block),
|
|
176
|
+
# deleting the claimed file after. Nothing is yielded for a file
|
|
177
|
+
# another reader claimed, an empty line or a command (queued).
|
|
178
|
+
def take_input(input_file)
|
|
179
|
+
claimed_file = SessionInbox.claim_input_file(input_file)
|
|
180
|
+
return unless claimed_file
|
|
181
|
+
|
|
182
|
+
begin
|
|
183
|
+
message, origin, no_interrupt, images = SessionInbox.read_input(claimed_file)
|
|
184
|
+
return if message.to_s.strip.empty?
|
|
185
|
+
return if Array(images).empty? && queue_as_command(message, origin)
|
|
186
|
+
|
|
187
|
+
yield Prompt.new(text: message, origin: origin, no_interrupt: !!no_interrupt, images: images || [])
|
|
188
|
+
ensure
|
|
189
|
+
FileUtils.rm_f(claimed_file)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Whether the session has had a turn: a user or assistant message.
|
|
194
|
+
def conversation_started?
|
|
195
|
+
Array(@session.messages).any? { |m| %w[user assistant model].include?((m[:role] || m["role"]).to_s) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# +unless_stopped+: not when the session was stopped meanwhile: the stop
|
|
201
|
+
# (chi stop, from another process) saved its status, and this save
|
|
202
|
+
# would write the worker's over it.
|
|
203
|
+
def save(at, unless_stopped: false)
|
|
204
|
+
self.class.save_or_log(at) do
|
|
205
|
+
@session.save(state_dir: @state_dir) unless unless_stopped && @stopped.call
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# A session command sent as a message (chi send -m "/model x", a web
|
|
210
|
+
# page's first message) runs as the command, as the Bridge does for a
|
|
211
|
+
# POST /turn; an unknown /word stays a prompt. Queued: the loop's next
|
|
212
|
+
# pass runs it.
|
|
213
|
+
# @return [Boolean] whether +text+ was one
|
|
214
|
+
def queue_as_command(text, origin)
|
|
215
|
+
return false unless @engine.command_registry.command?(text.to_s)
|
|
216
|
+
|
|
217
|
+
@queue_command.call(text, origin&.dig(:client_id))
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The update in +batch+ that may start a wake turn now: its source
|
|
222
|
+
# asked (wake: true), context.wake is on, the wake budget is open
|
|
223
|
+
# (WorkerWakes#context_open?: no continue offer, not paused, under
|
|
224
|
+
# session.max_wakes, past the start grace) and the source hasn't woken
|
|
225
|
+
# the session in the last CONTEXT_WAKE_WINDOW. The others stay plain
|
|
226
|
+
# notes. Within the start grace a change is a note: it came while the
|
|
227
|
+
# session was away.
|
|
228
|
+
def context_wake_for(batch, now: Time.now)
|
|
229
|
+
candidates = batch.deliveries.select(&:wake_note)
|
|
230
|
+
return nil if candidates.empty? || !context_wakes_on?
|
|
231
|
+
return nil unless @wakes.context_open?(awaiting_continue: @awaiting_continue.call, names: candidates.map(&:name))
|
|
232
|
+
|
|
233
|
+
candidates.find { |delivery| !woke_lately?(delivery.subscription, now) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def context_wakes_on? = Config.get("context.wake") != false
|
|
237
|
+
|
|
238
|
+
def woke_lately?(subscription, now)
|
|
239
|
+
at = subscription&.wakes_at && Time.iso8601(subscription.wakes_at)
|
|
240
|
+
at ? now - at < CONTEXT_WAKE_WINDOW : false
|
|
241
|
+
rescue ArgumentError
|
|
242
|
+
false
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# +batch+ with +waking+'s subscription recording the wake (the 10-minute window).
|
|
246
|
+
def woken(batch, waking)
|
|
247
|
+
stamped = waking.with(subscription: waking.subscription.with(wakes_at: Time.now.iso8601))
|
|
248
|
+
batch.with(deliveries: batch.deliveries.map { |delivery| delivery == waking ? stamped : delivery })
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
require_relative "log"
|
|
5
|
+
require_relative "child_reports"
|
|
6
|
+
|
|
7
|
+
module Samagotchi
|
|
8
|
+
# A worker's wake budget: turns nobody typed (a delegate report's wake
|
|
9
|
+
# turn, an attached context source's) run only past the start grace, at
|
|
10
|
+
# most session.max_wakes in a row with no human input, and not after a
|
|
11
|
+
# failed one until a human's input (a provider outage doesn't loop).
|
|
12
|
+
# Shared by both kinds of wake; the Worker asks it before each and tells
|
|
13
|
+
# it what happened. It also holds the reports a delegate wake turn was
|
|
14
|
+
# started for until the turn's first boundary takes them (#hand_over,
|
|
15
|
+
# #take_handed).
|
|
16
|
+
class WorkerWakes
|
|
17
|
+
# @param grace [Numeric] seconds after the start with no wake turn
|
|
18
|
+
# @param clock [#call] monotonic seconds
|
|
19
|
+
# @param max [#call, nil] the budget; nil reads session.max_wakes
|
|
20
|
+
def initialize(grace:, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, max: nil)
|
|
21
|
+
@grace = grace
|
|
22
|
+
@clock = clock
|
|
23
|
+
@max = max
|
|
24
|
+
@started_at = clock.call
|
|
25
|
+
@in_a_row = 0
|
|
26
|
+
@paused = false
|
|
27
|
+
@budget_noticed = false
|
|
28
|
+
@handed = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Wake turns run since the last human input.
|
|
32
|
+
attr_reader :in_a_row
|
|
33
|
+
|
|
34
|
+
def paused? = @paused
|
|
35
|
+
|
|
36
|
+
def grace_left = @grace - (@clock.call - @started_at)
|
|
37
|
+
|
|
38
|
+
def in_grace? = grace_left.positive?
|
|
39
|
+
|
|
40
|
+
def max
|
|
41
|
+
return @max.call if @max
|
|
42
|
+
|
|
43
|
+
value = Integer(Config.get(ChildRing::MAX_WAKES_KEY), exception: false)
|
|
44
|
+
value&.positive? ? value : ChildRing::MAX_WAKES_DEFAULT
|
|
45
|
+
rescue StandardError
|
|
46
|
+
ChildRing::MAX_WAKES_DEFAULT
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# For delegate reports: :due when a wake turn may run now (the block
|
|
50
|
+
# says reports wait; asked only when nothing else holds), :budget when
|
|
51
|
+
# only the budget stops it, nil otherwise. The start grace is the
|
|
52
|
+
# caller's (it shortens the idle sleep instead).
|
|
53
|
+
def delegate_state(awaiting_continue:)
|
|
54
|
+
return nil if @paused || awaiting_continue
|
|
55
|
+
return nil unless yield
|
|
56
|
+
|
|
57
|
+
@in_a_row < max ? :due : :budget
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# For an attached context source asking to wake: no continue offer,
|
|
61
|
+
# not paused, past the start grace and under the budget. +names+: the
|
|
62
|
+
# sources asking, for the log line when the budget holds them.
|
|
63
|
+
def context_open?(awaiting_continue:, names:)
|
|
64
|
+
return false if @paused || awaiting_continue || in_grace?
|
|
65
|
+
|
|
66
|
+
if @in_a_row >= max
|
|
67
|
+
Log.info(:worker, "context_wake_held", reason: "max_wakes", names: names.join(","))
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A wake turn starts. @return [Integer] the wake turns in a row now
|
|
74
|
+
def count! = @in_a_row += 1
|
|
75
|
+
|
|
76
|
+
# A wake turn failed: none starts again until a human's input.
|
|
77
|
+
def pause!
|
|
78
|
+
@paused = true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A human's input resets the budget, the pause and the budget notice.
|
|
82
|
+
def human_input!
|
|
83
|
+
@in_a_row = 0
|
|
84
|
+
@paused = false
|
|
85
|
+
@budget_noticed = false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] true the first time the budget is spent since the
|
|
89
|
+
# last human input: the UIs hear it once
|
|
90
|
+
def notice_budget!
|
|
91
|
+
return false if @budget_noticed
|
|
92
|
+
|
|
93
|
+
@budget_noticed = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The idle loop's sleep: +poll+, or less while a wake (the block says
|
|
97
|
+
# one is due) waits out the start grace.
|
|
98
|
+
def idle_wait(poll)
|
|
99
|
+
left = grace_left
|
|
100
|
+
left.positive? && yield ? [left, poll].min : poll
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The reports a delegate wake turn runs for, until its first boundary
|
|
104
|
+
# takes them (#take_handed); #drop_handed when the turn is over.
|
|
105
|
+
def hand_over(reports)
|
|
106
|
+
@handed = reports
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @return [Array, nil] the handed reports, once
|
|
110
|
+
def take_handed
|
|
111
|
+
reports = @handed
|
|
112
|
+
@handed = nil
|
|
113
|
+
reports
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def drop_handed
|
|
117
|
+
@handed = nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.35.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -123,6 +123,7 @@ files:
|
|
|
123
123
|
- README.md
|
|
124
124
|
- bin/chi
|
|
125
125
|
- docs/architecture.md
|
|
126
|
+
- docs/broadcast.md
|
|
126
127
|
- docs/cli.md
|
|
127
128
|
- docs/configuration.md
|
|
128
129
|
- docs/context.md
|
|
@@ -163,12 +164,19 @@ files:
|
|
|
163
164
|
- lib/samagotchi/bridge_client.rb
|
|
164
165
|
- lib/samagotchi/bridge_client/event_stream.rb
|
|
165
166
|
- lib/samagotchi/bridge_client/sse_parser.rb
|
|
167
|
+
- lib/samagotchi/broadcast/recipients.rb
|
|
168
|
+
- lib/samagotchi/broadcast/scope_card.rb
|
|
169
|
+
- lib/samagotchi/broadcast/tags.rb
|
|
170
|
+
- lib/samagotchi/broadcast_command.rb
|
|
166
171
|
- lib/samagotchi/bundle_command.rb
|
|
167
172
|
- lib/samagotchi/bundle_needs.rb
|
|
168
173
|
- lib/samagotchi/bundles/btw/manifest.yml
|
|
169
174
|
- lib/samagotchi/bundles/btw/plugin.rb
|
|
170
175
|
- lib/samagotchi/bundles/check-in/manifest.yml
|
|
171
176
|
- lib/samagotchi/bundles/check-in/plugin.rb
|
|
177
|
+
- lib/samagotchi/bundles/coordinator/manifest.yml
|
|
178
|
+
- lib/samagotchi/bundles/coordinator/plugin.rb
|
|
179
|
+
- lib/samagotchi/bundles/coordinator/skill_coordinator.md
|
|
172
180
|
- lib/samagotchi/bundles/core/manifest.yml
|
|
173
181
|
- lib/samagotchi/bundles/dev/manifest.yml
|
|
174
182
|
- lib/samagotchi/bundles/github-pr/manifest.yml
|
|
@@ -198,6 +206,7 @@ files:
|
|
|
198
206
|
- lib/samagotchi/bundles/system/self_map.md
|
|
199
207
|
- lib/samagotchi/cancellation_controller.rb
|
|
200
208
|
- lib/samagotchi/child_reports.rb
|
|
209
|
+
- lib/samagotchi/children_status.rb
|
|
201
210
|
- lib/samagotchi/cli/command.rb
|
|
202
211
|
- lib/samagotchi/cli/exit.rb
|
|
203
212
|
- lib/samagotchi/cli/flags.rb
|
|
@@ -220,6 +229,7 @@ files:
|
|
|
220
229
|
- lib/samagotchi/context_usage.rb
|
|
221
230
|
- lib/samagotchi/context_window.rb
|
|
222
231
|
- lib/samagotchi/continue_offer.rb
|
|
232
|
+
- lib/samagotchi/cut_policy.rb
|
|
223
233
|
- lib/samagotchi/debug_log.rb
|
|
224
234
|
- lib/samagotchi/desktop.rb
|
|
225
235
|
- lib/samagotchi/desktop/macos.rb
|
|
@@ -236,13 +246,16 @@ files:
|
|
|
236
246
|
- lib/samagotchi/edit_preview.rb
|
|
237
247
|
- lib/samagotchi/empty_answer_retry.rb
|
|
238
248
|
- lib/samagotchi/engine.rb
|
|
249
|
+
- lib/samagotchi/engine_peers.rb
|
|
239
250
|
- lib/samagotchi/events.rb
|
|
240
251
|
- lib/samagotchi/gem_update.rb
|
|
241
252
|
- lib/samagotchi/generation_phase.rb
|
|
253
|
+
- lib/samagotchi/git_head.rb
|
|
242
254
|
- lib/samagotchi/guardrail_wiring.rb
|
|
243
255
|
- lib/samagotchi/guardrails.rb
|
|
244
256
|
- lib/samagotchi/guardrails/approval.rb
|
|
245
257
|
- lib/samagotchi/guardrails/approvals.rb
|
|
258
|
+
- lib/samagotchi/guardrails/child_boundary.rb
|
|
246
259
|
- lib/samagotchi/guardrails/context.rb
|
|
247
260
|
- lib/samagotchi/guardrails/gate.rb
|
|
248
261
|
- lib/samagotchi/guardrails/load_failures.rb
|
|
@@ -255,6 +268,7 @@ files:
|
|
|
255
268
|
- lib/samagotchi/guardrails/rm_targets.rb
|
|
256
269
|
- lib/samagotchi/guardrails/rules.rb
|
|
257
270
|
- lib/samagotchi/guardrails/scratch_writes.rb
|
|
271
|
+
- lib/samagotchi/guardrails/shared_git.rb
|
|
258
272
|
- lib/samagotchi/guardrails/shell_git_dirs.rb
|
|
259
273
|
- lib/samagotchi/guardrails/shell_lex.rb
|
|
260
274
|
- lib/samagotchi/guardrails/shell_paths.rb
|
|
@@ -320,6 +334,7 @@ files:
|
|
|
320
334
|
- lib/samagotchi/models_command.rb
|
|
321
335
|
- lib/samagotchi/muted_memories.rb
|
|
322
336
|
- lib/samagotchi/note_command.rb
|
|
337
|
+
- lib/samagotchi/note_delivery.rb
|
|
323
338
|
- lib/samagotchi/output_formatter.rb
|
|
324
339
|
- lib/samagotchi/owner_lock.rb
|
|
325
340
|
- lib/samagotchi/parent_report.rb
|
|
@@ -366,6 +381,7 @@ files:
|
|
|
366
381
|
- lib/samagotchi/session_retention.rb
|
|
367
382
|
- lib/samagotchi/sessions_command.rb
|
|
368
383
|
- lib/samagotchi/steer.rb
|
|
384
|
+
- lib/samagotchi/steer_cut.rb
|
|
369
385
|
- lib/samagotchi/system_prompt.rb
|
|
370
386
|
- lib/samagotchi/terminal_ui.rb
|
|
371
387
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
@@ -447,6 +463,7 @@ files:
|
|
|
447
463
|
- lib/samagotchi/vision_support.rb
|
|
448
464
|
- lib/samagotchi/waiting_steer.rb
|
|
449
465
|
- lib/samagotchi/web/app.rb
|
|
466
|
+
- lib/samagotchi/web/capabilities.rb
|
|
450
467
|
- lib/samagotchi/web/info_probe.rb
|
|
451
468
|
- lib/samagotchi/web/lan.rb
|
|
452
469
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
@@ -467,11 +484,14 @@ files:
|
|
|
467
484
|
- lib/samagotchi/web/public/ctx.js
|
|
468
485
|
- lib/samagotchi/web/public/data.js
|
|
469
486
|
- lib/samagotchi/web/public/diff_view.js
|
|
487
|
+
- lib/samagotchi/web/public/families_view.js
|
|
470
488
|
- lib/samagotchi/web/public/format.js
|
|
471
489
|
- lib/samagotchi/web/public/hold.js
|
|
472
490
|
- lib/samagotchi/web/public/images.js
|
|
473
491
|
- lib/samagotchi/web/public/index.html
|
|
474
492
|
- lib/samagotchi/web/public/init_row.js
|
|
493
|
+
- lib/samagotchi/web/public/live_timing.js
|
|
494
|
+
- lib/samagotchi/web/public/live_turn.js
|
|
475
495
|
- lib/samagotchi/web/public/model_pick.js
|
|
476
496
|
- lib/samagotchi/web/public/model_picker.js
|
|
477
497
|
- lib/samagotchi/web/public/notifications.js
|
|
@@ -495,6 +515,7 @@ files:
|
|
|
495
515
|
- lib/samagotchi/web/public/thinking_ticker.js
|
|
496
516
|
- lib/samagotchi/web/public/timing.js
|
|
497
517
|
- lib/samagotchi/web/public/turn_events.js
|
|
518
|
+
- lib/samagotchi/web/public/turn_html.js
|
|
498
519
|
- lib/samagotchi/web/public/turn_model.js
|
|
499
520
|
- lib/samagotchi/web/public/turn_view.js
|
|
500
521
|
- lib/samagotchi/web/public/update_notice.js
|
|
@@ -505,7 +526,9 @@ files:
|
|
|
505
526
|
- lib/samagotchi/web/token.rb
|
|
506
527
|
- lib/samagotchi/worker.rb
|
|
507
528
|
- lib/samagotchi/worker_idle_exit.rb
|
|
529
|
+
- lib/samagotchi/worker_inbound.rb
|
|
508
530
|
- lib/samagotchi/worker_sidecar.rb
|
|
531
|
+
- lib/samagotchi/worker_wakes.rb
|
|
509
532
|
homepage: https://github.com/dm1try/samagotchi
|
|
510
533
|
licenses:
|
|
511
534
|
- MIT
|