samagotchi 0.159.0 → 0.160.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 +40 -1
- data/docs/architecture.md +5 -6
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +21 -2
- data/docs/internals/attached-context.md +4 -2
- data/docs/internals/bridge.md +7 -0
- data/docs/internals/engine.md +7 -1
- data/docs/memory.md +41 -13
- data/docs/scheduling.md +16 -3
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/bridge.rb +11 -2
- data/lib/samagotchi/broadcast/scope_card.rb +2 -1
- data/lib/samagotchi/bundle_command.rb +1 -0
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +2 -2
- data/lib/samagotchi/client_id.rb +4 -0
- data/lib/samagotchi/command_intake.rb +56 -0
- data/lib/samagotchi/context_absorber.rb +6 -3
- data/lib/samagotchi/context_candidate_choice.rb +8 -5
- data/lib/samagotchi/context_candidates.rb +11 -8
- data/lib/samagotchi/context_command.rb +9 -3
- data/lib/samagotchi/context_poller.rb +5 -2
- data/lib/samagotchi/context_sources.rb +6 -4
- data/lib/samagotchi/engine/events.rb +13 -2
- data/lib/samagotchi/engine/guardrails.rb +3 -4
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/events.rb +71 -0
- data/lib/samagotchi/guardrails/approvals.rb +27 -3
- data/lib/samagotchi/hook_routes.rb +0 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +27 -6
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +7 -27
- data/lib/samagotchi/memory_bundle/file_actions.rb +3 -4
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +14 -11
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -3
- data/lib/samagotchi/memory_bundle/status.rb +2 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +2 -0
- data/lib/samagotchi/memory_orphans.rb +14 -4
- data/lib/samagotchi/memory_paths.rb +29 -6
- data/lib/samagotchi/memory_usage.rb +23 -12
- data/lib/samagotchi/plugin/attached_context.rb +3 -1
- data/lib/samagotchi/plugin/sessions.rb +4 -1
- data/lib/samagotchi/project_command.rb +58 -20
- data/lib/samagotchi/project_id.rb +102 -0
- data/lib/samagotchi/project_id_clash.rb +62 -0
- data/lib/samagotchi/project_init.rb +214 -10
- data/lib/samagotchi/project_scope.rb +11 -2
- data/lib/samagotchi/schedules.rb +99 -39
- data/lib/samagotchi/session.rb +17 -2
- data/lib/samagotchi/session_access/local.rb +8 -5
- data/lib/samagotchi/session_access/remote.rb +2 -1
- data/lib/samagotchi/session_access.rb +4 -1
- data/lib/samagotchi/session_commands.rb +7 -3
- data/lib/samagotchi/session_continue.rb +10 -6
- data/lib/samagotchi/session_manager/spawn.rb +9 -2
- data/lib/samagotchi/session_manager/worker_command.rb +7 -19
- data/lib/samagotchi/state_format.rb +118 -0
- data/lib/samagotchi/tools/context_read.rb +7 -2
- data/lib/samagotchi/tools/delegate.rb +10 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +7 -7
- data/lib/samagotchi/update_command.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +5 -4
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/worker.rb +18 -6
- data/lib/samagotchi/worker_inbound.rb +21 -2
- metadata +5 -1
|
@@ -353,7 +353,8 @@ module Samagotchi
|
|
|
353
353
|
def context_candidates(target)
|
|
354
354
|
return [] if target.project?
|
|
355
355
|
|
|
356
|
-
ContextCandidates.with_status(target.session_id, project_root: target.project_root,
|
|
356
|
+
ContextCandidates.with_status(target.session_id, project_root: target.project_root, project_id: target.project_id,
|
|
357
|
+
state_dir: @state_dir)
|
|
357
358
|
.map { |candidate, status| ContextCandidate.new(candidate: candidate, status: status) }
|
|
358
359
|
end
|
|
359
360
|
|
|
@@ -371,7 +372,9 @@ module Samagotchi
|
|
|
371
372
|
ContextSources.check_name!(name)
|
|
372
373
|
return :removed if context_location(target).remove(name)
|
|
373
374
|
|
|
374
|
-
project = !target.project? && ContextSources.project_location_for(target.project_root,
|
|
375
|
+
project = !target.project? && ContextSources.project_location_for(target.project_root,
|
|
376
|
+
project_id: target.project_id,
|
|
377
|
+
state_dir: @state_dir)
|
|
375
378
|
project&.source(name) ? :project : :missing
|
|
376
379
|
end
|
|
377
380
|
|
|
@@ -394,7 +397,7 @@ module Samagotchi
|
|
|
394
397
|
# live in.
|
|
395
398
|
def context_location(target)
|
|
396
399
|
if target.project?
|
|
397
|
-
ContextSources.project_location_for(target.project_root, state_dir: @state_dir)
|
|
400
|
+
ContextSources.project_location_for(target.project_root, project_id: target.project_id, state_dir: @state_dir)
|
|
398
401
|
else
|
|
399
402
|
ContextSources.session_location(target.session_id, state_dir: @state_dir)
|
|
400
403
|
end
|
|
@@ -534,8 +537,8 @@ module Samagotchi
|
|
|
534
537
|
end
|
|
535
538
|
|
|
536
539
|
def attached_to(target, shadowed: false)
|
|
537
|
-
ContextSources.attached(target.session_id, project_root: target.project_root,
|
|
538
|
-
shadowed: shadowed)
|
|
540
|
+
ContextSources.attached(target.session_id, project_root: target.project_root, project_id: target.project_id,
|
|
541
|
+
state_dir: @state_dir, shadowed: shadowed)
|
|
539
542
|
end
|
|
540
543
|
|
|
541
544
|
# The block's post to the session's live worker, again on a 408 (the
|
|
@@ -809,7 +809,8 @@ module Samagotchi
|
|
|
809
809
|
owner = json["owner"] && OwnerLock::Owner.new(pid: nil, kind: json["owner"].to_s, started_at: nil)
|
|
810
810
|
SessionView.new(id: json["id"], status: json["status"], title: json["title"],
|
|
811
811
|
display_title: json["display_title"] || json["title"] || json["first_preview"].to_s,
|
|
812
|
-
project_root: json["project_root"],
|
|
812
|
+
project_root: json["project_root"], project_id: json["project_id"],
|
|
813
|
+
working_directory: json["working_directory"],
|
|
813
814
|
model_name: json["model_name"], model_typed: json["model_typed"],
|
|
814
815
|
parent_answers: json["parent_answers"], owner: owner,
|
|
815
816
|
pending_question: PendingQuestion.from_file(pending))
|
|
@@ -166,7 +166,10 @@ module Samagotchi
|
|
|
166
166
|
# @!attribute owner [OwnerLock::Owner, nil] its worker or one-turn chi run
|
|
167
167
|
# @!attribute pending_question [PendingQuestion, nil] the file's
|
|
168
168
|
SessionView = Data.define(:id, :status, :title, :display_title, :project_root, :working_directory, :model_name,
|
|
169
|
-
:model_typed, :parent_answers, :owner, :pending_question)
|
|
169
|
+
:model_typed, :parent_answers, :owner, :pending_question, :project_id) do
|
|
170
|
+
def initialize(id:, status:, title:, display_title:, project_root:, working_directory:, model_name:,
|
|
171
|
+
model_typed:, parent_answers:, owner:, pending_question:, project_id: nil) = super
|
|
172
|
+
end
|
|
170
173
|
|
|
171
174
|
# What chi send --new created (Remote; Local gives the Session): its
|
|
172
175
|
# id, a model warning, the session it continues, the delegates it took
|
|
@@ -473,7 +473,8 @@ module Samagotchi
|
|
|
473
473
|
|
|
474
474
|
peers = Tools::Peers.new(session_id: session.id, cwd: session.working_directory, state_dir: @engine.peer_state_dir)
|
|
475
475
|
listing = Tools::ContextRead.call("", peers: peers)
|
|
476
|
-
candidates = ContextCandidates.listing(session.id, project_root: session.project_root,
|
|
476
|
+
candidates = ContextCandidates.listing(session.id, project_root: session.project_root, project_id: session.project_id,
|
|
477
|
+
state_dir: @engine.peer_state_dir)
|
|
477
478
|
candidates ? "#{listing}\n\n#{candidates}" : listing
|
|
478
479
|
end
|
|
479
480
|
|
|
@@ -618,9 +619,12 @@ module Samagotchi
|
|
|
618
619
|
|
|
619
620
|
def approval_line(entry)
|
|
620
621
|
what = entry["key"] ? entry["key"].tr("\n", " ") : "any call rule #{entry["rule"]} asks about"
|
|
621
|
-
where = if entry["scope"] == "session"
|
|
622
|
+
where = if entry["scope"] == "session"
|
|
623
|
+
place = entry["repo_root"] ? " in #{entry["repo_root"]}" : ""
|
|
624
|
+
"session #{entry["session_id"].to_s[0, 8]}#{place}#{" with its env:" if entry["env"]}"
|
|
622
625
|
elsif entry["repo"] then "in repo #{entry["repo"]}"
|
|
623
|
-
else
|
|
626
|
+
else
|
|
627
|
+
"in #{entry["repo_root"]}"
|
|
624
628
|
end
|
|
625
629
|
rule = entry["rule"] ? " (rule #{entry["rule"]}, #{entry["source"]})" : ""
|
|
626
630
|
"#{entry["scope"]}: #{what} — #{where}#{rule}, #{entry["created_at"]}"
|
|
@@ -91,6 +91,9 @@ module Samagotchi
|
|
|
91
91
|
# @param prompt [String, nil] its first message (nil: it starts idle)
|
|
92
92
|
# @param preview [String, nil] what the lists show before the first turn;
|
|
93
93
|
# by default the prompt, else the previous link's preview
|
|
94
|
+
# @param client_id [String, nil] who sends +prompt+ when no person does
|
|
95
|
+
# (a schedule's chain firing: ClientId.schedule), the first turn's
|
|
96
|
+
# origin (spawn_session's client_id:)
|
|
94
97
|
# @return [Session] the new link (#model_warning as spawn_session's,
|
|
95
98
|
# #moved_children: the delegates it took over, #moved_context: the
|
|
96
99
|
# attached context it took (ContextMove), #opener:
|
|
@@ -99,7 +102,8 @@ module Samagotchi
|
|
|
99
102
|
# @raise [Refused] continued already, delegates open that can't
|
|
100
103
|
# move, or its folder is gone
|
|
101
104
|
# @raise [ArchiveRefused, OwnedByTUI] as archive_session
|
|
102
|
-
def self.run(id_or_ref, prompt: nil, preview: nil, model: nil, state_dir: nil, recap_wait: RECAP_WAIT, archive_wait: 5
|
|
105
|
+
def self.run(id_or_ref, prompt: nil, preview: nil, model: nil, state_dir: nil, recap_wait: RECAP_WAIT, archive_wait: 5,
|
|
106
|
+
client_id: nil)
|
|
103
107
|
sd = state_dir || Session.default_state_dir
|
|
104
108
|
TRIES.times do |try|
|
|
105
109
|
id = SessionChain.resolve(id_or_ref, state_dir: sd)
|
|
@@ -108,7 +112,7 @@ module Samagotchi
|
|
|
108
112
|
next :moved if follow && SessionChain.next_of(id, state_dir: sd)
|
|
109
113
|
|
|
110
114
|
run_locked(id, prompt: prompt, preview: preview, model: model, state_dir: sd, recap_wait: recap_wait,
|
|
111
|
-
archive_wait: archive_wait)
|
|
115
|
+
archive_wait: archive_wait, client_id: client_id)
|
|
112
116
|
end
|
|
113
117
|
return result unless result == :moved
|
|
114
118
|
end
|
|
@@ -123,7 +127,7 @@ module Samagotchi
|
|
|
123
127
|
end
|
|
124
128
|
end
|
|
125
129
|
|
|
126
|
-
private_class_method def self.run_locked(id, prompt:, preview:, model:, state_dir:, recap_wait:, archive_wait:)
|
|
130
|
+
private_class_method def self.run_locked(id, prompt:, preview:, model:, state_dir:, recap_wait:, archive_wait:, client_id:)
|
|
127
131
|
previous = Session.load(id, state_dir: state_dir)
|
|
128
132
|
checked = check!(previous, state_dir, model: model)
|
|
129
133
|
plan = checked.plan
|
|
@@ -140,7 +144,7 @@ module Samagotchi
|
|
|
140
144
|
archived = archive.archived - already
|
|
141
145
|
begin
|
|
142
146
|
spawn_continuation(previous, prompt: prompt, preview: preview, choice: checked.choice, state_dir: state_dir,
|
|
143
|
-
move: plan.move)
|
|
147
|
+
move: plan.move, client_id: client_id)
|
|
144
148
|
rescue StandardError
|
|
145
149
|
# Nothing started: the delegates go back, what this archived goes
|
|
146
150
|
# back to the lists, and a new link saved before its worker failed
|
|
@@ -201,7 +205,7 @@ module Samagotchi
|
|
|
201
205
|
|
|
202
206
|
# The new link, holding off any worker but its own (StartingMarker.fresh?)
|
|
203
207
|
# while +move+ goes to it.
|
|
204
|
-
private_class_method def self.spawn_continuation(previous, prompt:, preview:, choice:, state_dir:, move:)
|
|
208
|
+
private_class_method def self.spawn_continuation(previous, prompt:, preview:, choice:, state_dir:, move:, client_id: nil)
|
|
205
209
|
recap = RecapStore.read(Session.session_dir(previous.id, state_dir: state_dir))
|
|
206
210
|
start = prompt.to_s.strip.empty? ? nil : prompt
|
|
207
211
|
preview = previous.first_preview if preview.to_s.strip.empty? && start.nil?
|
|
@@ -218,7 +222,7 @@ module Samagotchi
|
|
|
218
222
|
prompt: start, preview: preview, title: previous.title, working_directory: previous.working_directory,
|
|
219
223
|
model_name: choice.model, setup: SessionSetup.of(previous), review: previous.review,
|
|
220
224
|
continues: previous.id, note: SessionChain.note_text(previous, recap: recap, state_dir: state_dir),
|
|
221
|
-
note_source: SessionChain::NOTE_SOURCE, state_dir: state_dir, before_spawn: before_spawn
|
|
225
|
+
note_source: SessionChain::NOTE_SOURCE, state_dir: state_dir, before_spawn: before_spawn, client_id: client_id
|
|
222
226
|
)
|
|
223
227
|
link.moved_children = move
|
|
224
228
|
link.moved_context = moved_context
|
|
@@ -54,12 +54,16 @@ module Samagotchi
|
|
|
54
54
|
# @param scratch [Boolean] a `chi scratch` session (its terminal's
|
|
55
55
|
# AttachLauncher): its worker runs a scratch Engine and deletes it
|
|
56
56
|
# once its last client is gone
|
|
57
|
+
# @param client_id [String, nil] who sent +prompt+ when no person did
|
|
58
|
+
# (a ClientId: a delegating parent, a schedule's chain link): its
|
|
59
|
+
# first turn's origin (Session#first_turn_client_id)
|
|
57
60
|
# @return [Session] with #seed_images_dropped: refs whose file was gone,
|
|
58
61
|
# and #model_warning: an id its host's saved list doesn't have
|
|
59
62
|
def spawn_session(prompt:, mode: "assist", working_directory: nil, model_name: nil, state_dir: nil,
|
|
60
63
|
memories: [], muted_memories: [], parent_id: nil, messages: [], images_from: nil,
|
|
61
64
|
preview: nil, title: nil, delegate: false, setup: SessionSetup.new, continues: nil, note: nil,
|
|
62
|
-
note_source: "chi", before_spawn: nil, review: nil, verify_fresh: nil, scratch: false
|
|
65
|
+
note_source: "chi", before_spawn: nil, review: nil, verify_fresh: nil, scratch: false,
|
|
66
|
+
client_id: nil)
|
|
63
67
|
sd = state_dir || Session.default_state_dir
|
|
64
68
|
# The resolved ref is stored (a resumed session keeps its model when an
|
|
65
69
|
# alias is retargeted), with the name as typed beside it. A wrong host
|
|
@@ -98,7 +102,10 @@ module Samagotchi
|
|
|
98
102
|
# for it can name it (chi send --new --wait, chi web's create): the
|
|
99
103
|
# worker runs it with that origin, and its reply file and last_turn
|
|
100
104
|
# carry the id.
|
|
101
|
-
|
|
105
|
+
unless prompt.to_s.strip.empty?
|
|
106
|
+
session.first_turn_id = SecureRandom.uuid
|
|
107
|
+
session.first_turn_client_id = client_id
|
|
108
|
+
end
|
|
102
109
|
# The worker takes last_prompt and clears it, and messages are saved at
|
|
103
110
|
# the turn's end: until then this is the only preview a list has.
|
|
104
111
|
session.first_preview = Session.preview_of(preview.to_s.strip.empty? ? prompt : preview)
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
class SessionManager
|
|
5
5
|
# CROSS-VERSION CONTRACT: this boot string, the entry it names
|
|
6
|
-
# (WorkerBoot.run_session_loop(id, state_dir:)), the
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# (WorkerBoot.run_session_loop(id, state_dir:)), the env keys
|
|
7
|
+
# spawn_options sets and the input/ file format are read by a NEWER chi
|
|
8
|
+
# than the one writing them. Change them only additively
|
|
9
|
+
# (spec/worker_contract_spec.rb).
|
|
10
10
|
BOOT_FALLBACK_ENV = "SAMAGOTCHI_BOOT_FALLBACK"
|
|
11
11
|
# "1" in a worker spawn_options started: it traps signals (WorkerSignals).
|
|
12
12
|
# WorkerBoot.run_session_loop deletes it from ENV as it reads it.
|
|
@@ -28,9 +28,9 @@ module Samagotchi
|
|
|
28
28
|
|
|
29
29
|
# WorkerCommand: the worker's command line and env (CROSS-VERSION
|
|
30
30
|
# CONTRACT: spec/worker_contract_spec.rb); the worker process itself is
|
|
31
|
-
# WorkerBoot (worker_boot.rb)
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# WorkerBoot (worker_boot.rb), which the boot command loads directly.
|
|
32
|
+
# Calls no other part. Meant to be loaded through
|
|
33
|
+
# samagotchi/session_manager.
|
|
34
34
|
module WorkerCommandPart
|
|
35
35
|
# Build the opts hash passed to Process.spawn for a forked worker. The
|
|
36
36
|
# child inherits this process's ENV and reads config.yml itself (as it
|
|
@@ -94,18 +94,6 @@ module Samagotchi
|
|
|
94
94
|
opts
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
# The worker's legacy entry for boot strings written by older chi
|
|
98
|
-
# (CROSS-VERSION CONTRACT: spec/worker_contract_spec.rb): it forwards to
|
|
99
|
-
# WorkerBoot.run_session_loop, which runs the worker process. Looked up
|
|
100
|
-
# by name on purpose: a constant reference here would put SessionManager
|
|
101
|
-
# back into one cycle with Worker, Engine and Bridge (the class map).
|
|
102
|
-
# REMOVE in the next release: boot strings name WorkerBoot since this
|
|
103
|
-
# one (see ~/.claude/plans/arch-worker-boot.md).
|
|
104
|
-
def run_session_loop(session_id, state_dir: nil, **)
|
|
105
|
-
require_relative "../worker_boot"
|
|
106
|
-
Samagotchi.const_get(:WorkerBoot).run_session_loop(session_id, state_dir: state_dir, **)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
97
|
# The worker's command line. Run from an installed gem, the worker
|
|
110
98
|
# activates the newest installed samagotchi (`>= 0.a`, prereleases
|
|
111
99
|
# count), whoever spawns it: an old chi web, chi send or --attach then
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A versioned JSON state-file schema. Missing members must be required or
|
|
5
|
+
# declare the format version and default that introduced them.
|
|
6
|
+
class StateFormat
|
|
7
|
+
REQUIRED = Object.new.freeze
|
|
8
|
+
|
|
9
|
+
Member = Data.define(:name, :introduced_in, :default, :persisted) do
|
|
10
|
+
def required? = introduced_in.nil?
|
|
11
|
+
def defaulted? = !required?
|
|
12
|
+
def persisted? = persisted
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Record = Data.define(:attributes, :extra, :raw, :missing) do
|
|
16
|
+
def readable? = missing.empty?
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
return raw unless readable?
|
|
20
|
+
|
|
21
|
+
extra.transform_keys(&:to_s).merge(attributes.transform_keys(&:to_s))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Document = Data.define(:version, :extra, :entries)
|
|
26
|
+
|
|
27
|
+
class InvalidDocument < StandardError; end
|
|
28
|
+
|
|
29
|
+
def self.required(name)
|
|
30
|
+
Member.new(name: name.to_sym, introduced_in: nil, default: REQUIRED, persisted: true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.added(name, in_version:, default:)
|
|
34
|
+
Member.new(name: name.to_sym, introduced_in: in_version, default: default, persisted: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.extra(name, in_version:)
|
|
38
|
+
Member.new(name: name.to_sym, introduced_in: in_version, default: ->(_attributes) { {} }, persisted: false)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :members
|
|
42
|
+
|
|
43
|
+
def initialize(version:, members:)
|
|
44
|
+
@version = version
|
|
45
|
+
@members = members.freeze
|
|
46
|
+
@extra_member = members.find { |member| !member.persisted? }
|
|
47
|
+
raise ArgumentError, "a state format can declare only one extra member" if members.count { |member| !member.persisted? } > 1
|
|
48
|
+
raise ArgumentError, "duplicate state format members" unless members.map(&:name).uniq == members.map(&:name)
|
|
49
|
+
|
|
50
|
+
members.each do |member|
|
|
51
|
+
required = member.required? && member.default.equal?(REQUIRED)
|
|
52
|
+
defaulted = member.defaulted? && member.introduced_in.is_a?(Integer) &&
|
|
53
|
+
member.introduced_in.positive? && member.introduced_in <= version &&
|
|
54
|
+
!member.default.equal?(REQUIRED)
|
|
55
|
+
next if required || defaulted
|
|
56
|
+
|
|
57
|
+
raise ArgumentError, "#{member.name} must be required or have an introduction version and default"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def read(data, collection:)
|
|
62
|
+
raise InvalidDocument, "the state file must contain a JSON object" unless data.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
collection = collection.to_s
|
|
65
|
+
entries = Array(data[collection]).map { |raw| read_record(raw) }
|
|
66
|
+
known = %w[version updated_at] + [collection]
|
|
67
|
+
stored_version = data["version"]
|
|
68
|
+
stored_version = 0 unless stored_version.is_a?(Integer) && stored_version >= 0
|
|
69
|
+
|
|
70
|
+
Document.new(version: [@version, stored_version].max,
|
|
71
|
+
extra: data.reject { |key, _value| known.include?(key.to_s) },
|
|
72
|
+
entries: entries)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write(document, collection:, updated_at:)
|
|
76
|
+
document.extra.transform_keys(&:to_s).merge(
|
|
77
|
+
"version" => [document.version, @version].max,
|
|
78
|
+
"updated_at" => updated_at,
|
|
79
|
+
collection.to_s => document.entries.map(&:to_h)
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def read_record(raw)
|
|
86
|
+
unless raw.is_a?(Hash)
|
|
87
|
+
missing = members.select(&:required?).map(&:name)
|
|
88
|
+
return Record.new(attributes: {}, extra: {}, raw: raw, missing: missing)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
attributes = {}
|
|
92
|
+
missing = []
|
|
93
|
+
members.each do |member|
|
|
94
|
+
next unless member.persisted?
|
|
95
|
+
|
|
96
|
+
key = member.name.to_s
|
|
97
|
+
if raw.key?(key)
|
|
98
|
+
attributes[member.name] = raw[key]
|
|
99
|
+
elsif member.required?
|
|
100
|
+
missing << member.name
|
|
101
|
+
else
|
|
102
|
+
attributes[member.name] = default_value(member.default, attributes)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
known = members.select(&:persisted?).map { |member| member.name.to_s }
|
|
107
|
+
extra = raw.reject { |key, _value| known.include?(key.to_s) }
|
|
108
|
+
extra = default_value(@extra_member.default, attributes).merge(extra) if @extra_member
|
|
109
|
+
Record.new(attributes: attributes, extra: extra, raw: raw, missing: missing)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def default_value(default, attributes)
|
|
113
|
+
value = default.respond_to?(:call) ? default.call(attributes) : default
|
|
114
|
+
value = value.dup if value.is_a?(Hash) || value.is_a?(Array)
|
|
115
|
+
value
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -51,8 +51,13 @@ module Samagotchi
|
|
|
51
51
|
@own = ContextSources.session_location(peers.session_id, state_dir: @state_dir)
|
|
52
52
|
# The Engine's PeerView knows the session's project; a plain Peers
|
|
53
53
|
# (specs, a bare kernel) has only its folder.
|
|
54
|
-
root = peers.respond_to?(:project_root)
|
|
55
|
-
|
|
54
|
+
root, id = if peers.respond_to?(:project_root)
|
|
55
|
+
[peers.project_root, peers.project_id]
|
|
56
|
+
else
|
|
57
|
+
found = ProjectScope.project_for(peers.cwd)
|
|
58
|
+
[found&.root, found&.id]
|
|
59
|
+
end
|
|
60
|
+
@attached = ContextSources.attached(peers.session_id, project_root: root, project_id: id, state_dir: @state_dir)
|
|
56
61
|
@max_chars = [max_chars.to_i, MIN_CHARS].max
|
|
57
62
|
@now = now
|
|
58
63
|
# The peer bundles' declared sources (PeerSources), through the
|
|
@@ -13,6 +13,10 @@ require_relative "../child_reports"
|
|
|
13
13
|
require_relative "../worker_link"
|
|
14
14
|
|
|
15
15
|
module Samagotchi
|
|
16
|
+
# Loaded on the first task (a leaf; the bundle test kit's load budget,
|
|
17
|
+
# spec/samagotchi/testing/sandbox_spec.rb).
|
|
18
|
+
autoload :CommandIntake, File.expand_path("../command_intake", __dir__)
|
|
19
|
+
|
|
16
20
|
# Loaded on first use: this autoload keeps SessionManager and its ~80
|
|
17
21
|
# files unloaded until the delegate/session path needs them.
|
|
18
22
|
autoload :SessionManager, File.expand_path("../session_manager", __dir__)
|
|
@@ -53,6 +57,9 @@ module Samagotchi
|
|
|
53
57
|
|
|
54
58
|
task = content.to_s.strip
|
|
55
59
|
return "Error: give the task, the child's first message" if task.empty?
|
|
60
|
+
# The child's intake takes no shell line from its parent: say so
|
|
61
|
+
# now, so the model rewrites the task (a /word one is the child's text).
|
|
62
|
+
return "Error: #{CommandIntake::REFUSED}" if CommandIntake.shell_line?(task)
|
|
56
63
|
|
|
57
64
|
sd = peers.state_dir || Session.default_state_dir
|
|
58
65
|
parent = Session.load(peers.session_id, state_dir: sd)
|
|
@@ -172,7 +179,8 @@ module Samagotchi
|
|
|
172
179
|
parent_answers: parent.parent_answers)
|
|
173
180
|
child = SessionManager.spawn_session(prompt: task, working_directory: folder,
|
|
174
181
|
model_name: child_model(model, parent), memories: CHILD_MEMORIES,
|
|
175
|
-
parent_id: parent.id, delegate: true, setup: setup, state_dir: state_dir
|
|
182
|
+
parent_id: parent.id, delegate: true, setup: setup, state_dir: state_dir,
|
|
183
|
+
client_id: ClientId.delegate(parent.id))
|
|
176
184
|
DelegateWait.mark_started(parent.id, child, state_dir: state_dir)
|
|
177
185
|
# The line for a task that names a worktree while the child runs in
|
|
178
186
|
# the parent's folder (no cwd: given): the child still starts.
|
|
@@ -358,7 +366,7 @@ module Samagotchi
|
|
|
358
366
|
|
|
359
367
|
# Only a reply after this delivery counts as the follow-up's.
|
|
360
368
|
DelegateWait.mark_seen(parent.id, child, state_dir: state_dir)
|
|
361
|
-
delivered = SessionManager.deliver_turn(id, prompt: task, client_id:
|
|
369
|
+
delivered = SessionManager.deliver_turn(id, prompt: task, client_id: ClientId.delegate(parent.id),
|
|
362
370
|
state_dir: state_dir)
|
|
363
371
|
case delivered.status
|
|
364
372
|
when :accepted
|
|
@@ -89,7 +89,8 @@ module Samagotchi
|
|
|
89
89
|
def pass(session, texts, phase:, state_dir:, recorder:)
|
|
90
90
|
return if session.id.nil? || session.scratch
|
|
91
91
|
|
|
92
|
-
rows = ContextCandidates.with_status(session.id, project_root: session.project_root,
|
|
92
|
+
rows = ContextCandidates.with_status(session.id, project_root: session.project_root,
|
|
93
|
+
project_id: session.project_id, state_dir: state_dir)
|
|
93
94
|
return if rows.empty?
|
|
94
95
|
|
|
95
96
|
log = ContextCandidatesLog.log_for(state_dir)
|
|
@@ -134,6 +135,7 @@ module Samagotchi
|
|
|
134
135
|
def choice(session, log, settings, state_dir)
|
|
135
136
|
session_id = session.id
|
|
136
137
|
project_root = session.project_root
|
|
138
|
+
project_id = session.project_id
|
|
137
139
|
show = lambda do |**card|
|
|
138
140
|
current = @current_session.call
|
|
139
141
|
next nil unless current.nil? || current.id == session_id
|
|
@@ -146,6 +148,7 @@ module Samagotchi
|
|
|
146
148
|
end
|
|
147
149
|
lambda do |job, fields|
|
|
148
150
|
ContextCandidateChoice.after_judged(job: job, fields: fields, settings: settings, project_root: project_root,
|
|
151
|
+
project_id: project_id,
|
|
149
152
|
state_dir: state_dir, log: log, show_card: show)
|
|
150
153
|
end
|
|
151
154
|
end
|
|
@@ -156,12 +159,9 @@ module Samagotchi
|
|
|
156
159
|
def parent_attached(session, state_dir)
|
|
157
160
|
return nil unless session.delegate? && session.parent_id
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
session.project_root
|
|
163
|
-
end
|
|
164
|
-
ContextSources.attached(session.parent_id, project_root: parent_root, state_dir: state_dir)
|
|
162
|
+
parent = Session.exist?(session.parent_id, state_dir: state_dir) ? Session.load(session.parent_id, state_dir: state_dir) : session
|
|
163
|
+
ContextSources.attached(session.parent_id, project_root: parent.project_root, project_id: parent.project_id,
|
|
164
|
+
state_dir: state_dir)
|
|
165
165
|
.map { |attached| attached.source.title || attached.name }
|
|
166
166
|
rescue StandardError => e
|
|
167
167
|
Log.warn(:context, "context_parent_attached_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
@@ -356,7 +356,7 @@ module Samagotchi
|
|
|
356
356
|
notes = file_notes(row, update.name, update.to || update.from, update.merged, update.behind, update.trash_copy, earlier: earlier)
|
|
357
357
|
case update.status
|
|
358
358
|
when :would_update, :updated
|
|
359
|
-
notes = [adds(update.adds), *notes, replaced(update.replaced), *update.kept_own, update.note].compact
|
|
359
|
+
notes = [adds(update.added_files), adds(update.adds), *notes, replaced(update.replaced), *update.kept_own, update.note].compact
|
|
360
360
|
done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
361
361
|
when :would_merge, :merged, :up_to_date then same_version(row, [*notes, update.note, update.manifest].compact, changed: Array(update.merged).any?)
|
|
362
362
|
when :skipped then skipped(row, [update.note, *notes].join("; "))
|
|
@@ -522,7 +522,7 @@ module Samagotchi
|
|
|
522
522
|
row
|
|
523
523
|
end
|
|
524
524
|
|
|
525
|
-
#
|
|
525
|
+
# New memory files or profile members: "+ file".
|
|
526
526
|
def adds(members)
|
|
527
527
|
members.nil? || members.empty? ? nil : "+ #{members.join(", ")}"
|
|
528
528
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -1030,7 +1030,7 @@ module Samagotchi
|
|
|
1030
1030
|
|
|
1031
1031
|
# One normal form for the decision, the mkdir and the realpath (a `..` after a symlink would differ).
|
|
1032
1032
|
dir = File.expand_path(dir)
|
|
1033
|
-
outcome = ProjectInit.call(dir, home: home)
|
|
1033
|
+
outcome = ProjectInit.call(dir, home: home, state_dir: default_state_dir)
|
|
1034
1034
|
return error_response(422, outcome.reason.to_s, outcome.message) if outcome.kind == :refused
|
|
1035
1035
|
|
|
1036
1036
|
if outcome.kind == :git
|
|
@@ -1455,7 +1455,8 @@ module Samagotchi
|
|
|
1455
1455
|
body = { session_id: id, sources: rows, can_add_url: ContextProviders.any? }
|
|
1456
1456
|
# The candidates too, for chi context ls (no UI shows them yet).
|
|
1457
1457
|
if full
|
|
1458
|
-
body[:candidates] = ContextCandidates.with_status(id, project_root: session.project_root,
|
|
1458
|
+
body[:candidates] = ContextCandidates.with_status(id, project_root: session.project_root, project_id: session.project_id,
|
|
1459
|
+
state_dir: default_state_dir)
|
|
1459
1460
|
.map { |candidate, status| candidate.to_h.merge("name" => candidate.name, "status" => status) }
|
|
1460
1461
|
end
|
|
1461
1462
|
json_response(200, body)
|
|
@@ -1573,8 +1574,8 @@ module Samagotchi
|
|
|
1573
1574
|
end
|
|
1574
1575
|
|
|
1575
1576
|
def attached_context(session, shadowed: false)
|
|
1576
|
-
ContextSources.attached(session.id, project_root: session.project_root,
|
|
1577
|
-
shadowed: shadowed)
|
|
1577
|
+
ContextSources.attached(session.id, project_root: session.project_root, project_id: session.project_id,
|
|
1578
|
+
state_dir: default_state_dir, shadowed: shadowed)
|
|
1578
1579
|
end
|
|
1579
1580
|
|
|
1580
1581
|
# @return [ContextSources::Attached, nil]
|
|
@@ -101,7 +101,7 @@ module Samagotchi
|
|
|
101
101
|
brief: true,
|
|
102
102
|
session: { id: session.id, status: session.status, title: session.title,
|
|
103
103
|
display_title: session.display_title, project_root: session.project_root,
|
|
104
|
-
working_directory: session.working_directory, model_name: session.model_name,
|
|
104
|
+
project_id: session.project_id, working_directory: session.working_directory, model_name: session.model_name,
|
|
105
105
|
model_typed: session.model_typed, parent_answers: session.parent_answers,
|
|
106
106
|
owner: owner&.kind },
|
|
107
107
|
pending_question: session.pending_question&.to_file
|
|
@@ -492,8 +492,8 @@ module Samagotchi
|
|
|
492
492
|
# @raise [ArgumentError] no such session
|
|
493
493
|
def context_target(id)
|
|
494
494
|
session = session_access.show(id)
|
|
495
|
-
ContextCommand::Target.new(session_id: id, project_root: session.project_root,
|
|
496
|
-
label: id[0, 8])
|
|
495
|
+
ContextCommand::Target.new(session_id: id, project_root: session.project_root, project_id: session.project_id,
|
|
496
|
+
cwd: session.working_directory, label: id[0, 8])
|
|
497
497
|
end
|
|
498
498
|
end
|
|
499
499
|
end
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
require_relative "client_id"
|
|
7
|
+
require_relative "command_intake"
|
|
7
8
|
require_relative "config"
|
|
8
9
|
require_relative "engine"
|
|
9
10
|
require_relative "guardrails/parent_policy"
|
|
@@ -338,7 +339,7 @@ module Samagotchi
|
|
|
338
339
|
predecessor: @session.continues)
|
|
339
340
|
# Its attached context's notes (ContextSources).
|
|
340
341
|
@context_absorber = ContextAbsorber.new(session_id: @session_id, state_dir: @state_dir,
|
|
341
|
-
project_root: @session.project_root,
|
|
342
|
+
project_root: @session.project_root, project_id: @session.project_id,
|
|
342
343
|
peer_sources: @engine.peer_sources, peer_turns: -> { @wakes.peer_turns })
|
|
343
344
|
# What comes in between turns, and how each kind is saved.
|
|
344
345
|
@inbound = WorkerInbound.new(session: @session, state_dir: @state_dir, session_dir: @session_dir, engine: @engine,
|
|
@@ -468,8 +469,8 @@ module Samagotchi
|
|
|
468
469
|
# Attached context's commands, in the background; a new text wakes
|
|
469
470
|
# the loop to absorb it. Not activity: the idle exit stops it.
|
|
470
471
|
@context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
|
|
471
|
-
project_root: @session.project_root,
|
|
472
|
-
on_change: -> { @waker.wake }).start
|
|
472
|
+
project_root: @session.project_root, project_id: @session.project_id,
|
|
473
|
+
cwd: @session.working_directory, on_change: -> { @waker.wake }).start
|
|
473
474
|
@scratch_watch = start_scratch_watch if @session.scratch
|
|
474
475
|
end
|
|
475
476
|
|
|
@@ -759,14 +760,15 @@ module Samagotchi
|
|
|
759
760
|
end
|
|
760
761
|
|
|
761
762
|
# The first prompt (spawn_session's): a delegate child's task, which is
|
|
762
|
-
# its parent's
|
|
763
|
-
#
|
|
763
|
+
# its parent's. Its turn carries the id spawn_session gave it, so a
|
|
764
|
+
# wait for it (--new --wait) names it, and its sender
|
|
765
|
+
# (WorkerInbound#initial_origin).
|
|
764
766
|
# A Verify fresh child's is a Verify turn on its claim (its verdict is
|
|
765
767
|
# recorded and forwarded to the parent).
|
|
766
768
|
def run_initial_prompt(prompt)
|
|
767
769
|
@initial_turn = true
|
|
768
770
|
fresh = @session.verify_fresh
|
|
769
|
-
run_prompt(WorkerInbound::Prompt.new(text: prompt, origin:
|
|
771
|
+
run_prompt(WorkerInbound::Prompt.new(text: prompt, origin: @inbound.initial_origin,
|
|
770
772
|
anchors: { verify: fresh&.anchor }))
|
|
771
773
|
ensure
|
|
772
774
|
@initial_turn = false
|
|
@@ -1215,6 +1217,8 @@ module Samagotchi
|
|
|
1215
1217
|
# the cards and notices it showed, held: they follow its command_ran,
|
|
1216
1218
|
# as its output
|
|
1217
1219
|
def run_command_line(command)
|
|
1220
|
+
return [shell_refused(command), []] if CommandIntake.refused?(command.line, command.client_id)
|
|
1221
|
+
|
|
1218
1222
|
@engine.holding_announcements do
|
|
1219
1223
|
command_result(command) do
|
|
1220
1224
|
@commands.run(command.line, confirm: confirm?(command), as_parent: as_parent?(command), user_ui: user_ui?(command))
|
|
@@ -1222,6 +1226,14 @@ module Samagotchi
|
|
|
1222
1226
|
end
|
|
1223
1227
|
end
|
|
1224
1228
|
|
|
1229
|
+
# A shell line from a sender that may not run one (CommandIntake: a
|
|
1230
|
+
# delegate's task, a schedule's prompt, a plugin's message): not run,
|
|
1231
|
+
# its command_ran an error with the one refusal text.
|
|
1232
|
+
def shell_refused(command)
|
|
1233
|
+
Log.warn(:worker, "shell_line_refused", client_id: command.client_id)
|
|
1234
|
+
SessionCommands::Result.new(status: :error, output: CommandIntake::REFUSED, changed: [])
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1225
1237
|
# Whether +command+ asks before a costly /thinking or /reasoning-history
|
|
1226
1238
|
# change (CacheConfirm): only a person at a web tab or a TUI sees the
|
|
1227
1239
|
# question; chi send, a delegate, a plugin and a card's action apply it.
|