samagotchi 0.150.0 → 0.152.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 +50 -1
- data/docs/architecture.md +2 -2
- data/docs/bundle-authoring.md +5 -5
- data/docs/cli.md +32 -7
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +4 -0
- data/docs/internals/block-renderers.md +98 -0
- data/docs/internals/bridge.md +53 -17
- data/docs/internals/bundles.md +49 -14
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/web-frontend.md +2 -2
- data/docs/memory.md +7 -2
- data/docs/plugins.md +91 -14
- data/docs/releasing.md +7 -2
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/block_renderers.rb +116 -0
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +219 -122
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +47 -6
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/memory_bundle/asset_installer.rb +18 -9
- data/lib/samagotchi/memory_bundle/builder.rb +39 -31
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -43
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -14
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +91 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +166 -5
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +220 -19
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -3
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +24 -13
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +2 -0
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +9 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +13 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
autoload :Log, File.expand_path("log", __dir__)
|
|
5
|
+
autoload :VERSION, File.expand_path("version", __dir__)
|
|
6
|
+
module MemoryBundle
|
|
7
|
+
autoload :Manifest, File.expand_path("memory_bundle/manifest", __dir__)
|
|
8
|
+
autoload :Provenance, File.expand_path("memory_bundle/provenance", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Declarative block renderers (docs/plugins.md, docs/internals/block-renderers.md):
|
|
12
|
+
# a bundle's manifest maps a fenced block's language to the scripts/ files the
|
|
13
|
+
# owner's page loads to draw it:
|
|
14
|
+
#
|
|
15
|
+
# block_renderers:
|
|
16
|
+
# - lang: mermaid
|
|
17
|
+
# load: [mermaid.tiny.js, mermaid_renderer.js]
|
|
18
|
+
#
|
|
19
|
+
# JS draws, in a sandboxed frame; Ruby only routes and serves, so chi web
|
|
20
|
+
# (which runs no plugins) can answer, and a reloaded page draws after the
|
|
21
|
+
# worker has gone. The installer records them in the bundle's provenance;
|
|
22
|
+
# .for_installed reads them from there.
|
|
23
|
+
module BlockRenderers
|
|
24
|
+
# A renderer as the manifest declares it.
|
|
25
|
+
Renderer = Data.define(:lang, :load) do
|
|
26
|
+
def to_h = { "lang" => lang, "load" => load }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# One the page may use: the bundle that won the language, and its files.
|
|
30
|
+
Installed = Data.define(:lang, :bundle, :files)
|
|
31
|
+
|
|
32
|
+
class Invalid < ArgumentError; end
|
|
33
|
+
|
|
34
|
+
# A language a block can carry: a fence's first word, downcased
|
|
35
|
+
# (MarkdownRenderer sets it as data-lang under this same pattern).
|
|
36
|
+
LANG = /\A[a-z0-9_+-]{1,32}\z/
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The manifest's block_renderers: list.
|
|
41
|
+
# @return [Array<Renderer>] [] when absent
|
|
42
|
+
# @raise [Invalid]
|
|
43
|
+
def parse_list(raw)
|
|
44
|
+
return [] if raw.nil?
|
|
45
|
+
raise Invalid, "block_renderers: must be a list of mappings" unless raw.is_a?(Array)
|
|
46
|
+
|
|
47
|
+
renderers = raw.map { |item| parse(item) }
|
|
48
|
+
twice = renderers.map(&:lang).tally.select { |_lang, count| count > 1 }.keys
|
|
49
|
+
raise Invalid, "block_renderers: #{twice.join(", ")} listed more than once" unless twice.empty?
|
|
50
|
+
|
|
51
|
+
renderers
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @raise [Invalid]
|
|
55
|
+
def parse(item)
|
|
56
|
+
raise Invalid, "block_renderers: each item must be a mapping, not #{item.inspect}" unless item.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
data = item.transform_keys(&:to_s)
|
|
59
|
+
lang = data["lang"]
|
|
60
|
+
unless lang.is_a?(String) && lang.match?(LANG)
|
|
61
|
+
raise Invalid, "block_renderers: lang #{lang.inspect} isn't a fence's language (lowercase letters, digits, _ + -; at most 32)"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
load = data["load"]
|
|
65
|
+
unless load.is_a?(Array) && !load.empty? && load.all? { |file| file.is_a?(String) && file.match?(MemoryBundle::Manifest::SCRIPT_FILE) }
|
|
66
|
+
raise Invalid, "block_renderers: #{lang}'s load must be a non-empty list of file names in scripts/, not #{load.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Renderer.new(lang: lang, load: load)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# What the installed bundles draw: the first bundle by name wins a
|
|
73
|
+
# language. A bundle whose requires_chi this chi doesn't meet, or whose list
|
|
74
|
+
# doesn't parse (logged), is skipped; a record that doesn't read lists none.
|
|
75
|
+
# @return [Array<Installed>]
|
|
76
|
+
def for_installed
|
|
77
|
+
taken = {}
|
|
78
|
+
MemoryBundle::Provenance.each_installed do |bundle, record|
|
|
79
|
+
manifest = record.manifest
|
|
80
|
+
next if manifest.requires_chi_failure(VERSION)
|
|
81
|
+
|
|
82
|
+
renderers(bundle, manifest).each do |renderer|
|
|
83
|
+
taken[renderer.lang] ||= Installed.new(lang: renderer.lang, bundle: bundle, files: renderer.load)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
taken.values
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The installed path of +file+, once a renderer .for_installed lists loads
|
|
90
|
+
# it from +bundle+ (neither name is ever joined into a path before that:
|
|
91
|
+
# they come from the URL) and it is as installed. This code runs in the
|
|
92
|
+
# owner's browser, so it is checked as a plugin's load and a provider's
|
|
93
|
+
# script are; a --dev install's links are not.
|
|
94
|
+
# @return [String, nil] nil when no such renderer loads such a file
|
|
95
|
+
# @raise [Invalid] the file is gone or changed
|
|
96
|
+
def script_path(bundle, file)
|
|
97
|
+
return nil unless for_installed.any? { |renderer| renderer.bundle == bundle && renderer.files.include?(file) }
|
|
98
|
+
|
|
99
|
+
provenance = MemoryBundle::Provenance.new(name: bundle)
|
|
100
|
+
path = File.join(provenance.scripts_dir, file)
|
|
101
|
+
recorded = provenance.record&.scripts&.fetch(file, nil)
|
|
102
|
+
return path if File.file?(path) && MemoryBundle::Provenance.sha_matches?(path, recorded)
|
|
103
|
+
|
|
104
|
+
raise Invalid, "bundle #{bundle}'s scripts/#{file} differs from the installed one (reinstall the bundle)"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Its parsed renderers; a list that doesn't parse is none (logged).
|
|
108
|
+
def renderers(bundle, manifest)
|
|
109
|
+
if (problem = manifest.problems["block_renderers"])
|
|
110
|
+
Log.warn(:web, "block_renderers_unreadable", bundle: bundle, msg: problem)
|
|
111
|
+
end
|
|
112
|
+
manifest.block_renderers
|
|
113
|
+
end
|
|
114
|
+
private_class_method :renderers
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../steer"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
class Bridge
|
|
7
|
+
# Where a CardStore's entries sit in a session's messages: the turns the
|
|
8
|
+
# page draws after each. The messages count, with the rule the page and
|
|
9
|
+
# the recap share (Steer.turn_opening?); the store's turn log only orders
|
|
10
|
+
# the ids. So a turn with no opening (a reminder, a Continue, a failed or
|
|
11
|
+
# rolled-back one, a prompt stopped before the model) counts nothing, and
|
|
12
|
+
# an opening with no id (/continue no's reason, Qwen's recovery line) counts
|
|
13
|
+
# as it does on the page. Nothing orders such an opening against an entry, so
|
|
14
|
+
# an entry says how many it came after (+past_idless+, #idless_after). Pure:
|
|
15
|
+
# no Engine, no file.
|
|
16
|
+
class CardPlaces
|
|
17
|
+
# @param turn_log [Array<String>] the turn ids, oldest first
|
|
18
|
+
# @param messages [Array<Hash>] the messages the page draws
|
|
19
|
+
def initialize(turn_log, messages)
|
|
20
|
+
positions = turn_log.each_with_index.to_h
|
|
21
|
+
@ids = Array(messages).select { |message| Steer.turn_opening?(message) }.map { |message| message[:turn_id] || message["turn_id"] }
|
|
22
|
+
@openings = @ids.size
|
|
23
|
+
@opened = @ids.compact.to_set
|
|
24
|
+
@places = @ids.map { |id| positions[id] }
|
|
25
|
+
index_by_position(@places, turn_log.size)
|
|
26
|
+
@positions = positions
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The openings after the last one whose turn is at or before the entry's
|
|
30
|
+
# in the log (none: from the first one whose turn is later; none: 0),
|
|
31
|
+
# less the ones with no id it came after. An entry from an older file
|
|
32
|
+
# (+base_since+) has no id: all the log's turns are later, on top of what
|
|
33
|
+
# it had counted.
|
|
34
|
+
# @param entry [Hash] a store's entry: :turn_id, :base_since
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
def turns_since(entry)
|
|
37
|
+
position = @positions.fetch(entry[:turn_id], -1)
|
|
38
|
+
at_or_before = position >= 0 ? @last_at_or_before[position] : nil
|
|
39
|
+
since = if at_or_before
|
|
40
|
+
@openings - at_or_before - 1 - [entry[:past_idless].to_i, idless_run(at_or_before)].min
|
|
41
|
+
else
|
|
42
|
+
later = @first_after[position + 1]
|
|
43
|
+
later ? @openings - later : 0
|
|
44
|
+
end
|
|
45
|
+
entry[:base_since].to_i + since
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The openings with no id right after the one +turn_id+ counts from: what
|
|
49
|
+
# an entry shown now has come after. A rollback can only shorten the run, so
|
|
50
|
+
# a count read back against it never eats a later turn.
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
def idless_after(turn_id)
|
|
53
|
+
position = @positions.fetch(turn_id, -1)
|
|
54
|
+
at_or_before = position >= 0 ? @last_at_or_before[position] : nil
|
|
55
|
+
at_or_before ? idless_run(at_or_before) : 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Where a log may start and still read an entry at +position+ the same: at
|
|
59
|
+
# the opening that entry counts from, or at the entry's turn with none.
|
|
60
|
+
# @return [Integer] a log position
|
|
61
|
+
def log_start_for(position)
|
|
62
|
+
at_or_before = @last_at_or_before[position]
|
|
63
|
+
at_or_before ? @places[at_or_before] : position
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Boolean] whether that turn has an opening in the messages
|
|
67
|
+
def opened?(turn_id)
|
|
68
|
+
@opened.include?(turn_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def idless_run(opening)
|
|
74
|
+
@ids.drop(opening + 1).take_while(&:nil?).size
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# For each log position: the last opening at or before it, and the first
|
|
78
|
+
# one at or after it. One pass each, for the store's many entries.
|
|
79
|
+
def index_by_position(places, size)
|
|
80
|
+
@last_at_or_before = Array.new(size)
|
|
81
|
+
@first_after = Array.new(size + 1)
|
|
82
|
+
places.each_with_index do |place, opening|
|
|
83
|
+
next unless place
|
|
84
|
+
|
|
85
|
+
@last_at_or_before[place] = opening
|
|
86
|
+
@first_after[place] ||= opening
|
|
87
|
+
end
|
|
88
|
+
(1...size).each { |i| @last_at_or_before[i] = [@last_at_or_before[i - 1], @last_at_or_before[i]].compact.max }
|
|
89
|
+
(size - 1).downto(0) { |i| @first_after[i] = [@first_after[i], @first_after[i + 1]].compact.min }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|