samagotchi 0.140.0 → 0.141.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 +19 -1
- data/docs/architecture.md +2 -2
- data/docs/context.md +66 -4
- data/docs/internals/remote-sessions.md +4 -1
- data/docs/plugins.md +40 -1
- data/docs/sessions.md +3 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +14 -0
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +15 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/context_command.rb +32 -9
- data/lib/samagotchi/context_move.rb +31 -0
- data/lib/samagotchi/context_sources.rb +248 -10
- data/lib/samagotchi/plugin/attached_context.rb +14 -2
- data/lib/samagotchi/plugin/context.rb +5 -0
- data/lib/samagotchi/plugin/peer.rb +53 -0
- data/lib/samagotchi/plugin/sessions.rb +9 -2
- data/lib/samagotchi/send_command.rb +3 -1
- data/lib/samagotchi/session.rb +4 -0
- data/lib/samagotchi/session_access/contract.rb +2 -0
- data/lib/samagotchi/session_access/local.rb +24 -3
- data/lib/samagotchi/session_access/remote.rb +19 -8
- data/lib/samagotchi/session_access.rb +26 -6
- data/lib/samagotchi/session_continue.rb +9 -2
- data/lib/samagotchi/testing/boundary.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -8
- data/lib/samagotchi/web/public/chains.js +10 -2
- data/lib/samagotchi/web/session_access_routes.rb +7 -2
- data/lib/samagotchi/web/session_create.rb +1 -0
- metadata +2 -1
|
@@ -173,9 +173,12 @@ module Samagotchi
|
|
|
173
173
|
# over, the server's word on a --parent-answers looser than its
|
|
174
174
|
# config.yml's (capped), and the id the prompt it started with gets for
|
|
175
175
|
# its first turn (nil for one created idle).
|
|
176
|
-
|
|
176
|
+
# +moved_context+: the attached context a continue took
|
|
177
|
+
# (ContextSources::Moved; nil from an older chi web).
|
|
178
|
+
Created = Data.define(:id, :model_warning, :continues, :moved_children, :parent_answers_warning, :first_turn_id,
|
|
179
|
+
:moved_context) do
|
|
177
180
|
def initialize(id:, model_warning: nil, continues: nil, moved_children: [], parent_answers_warning: nil,
|
|
178
|
-
first_turn_id: nil) = super
|
|
181
|
+
first_turn_id: nil, moved_context: nil) = super
|
|
179
182
|
end
|
|
180
183
|
|
|
181
184
|
# A session's worker as an attach finds it (Local#worker, chi web's GET
|
|
@@ -255,16 +258,33 @@ module Samagotchi
|
|
|
255
258
|
# "declined".
|
|
256
259
|
ContextCandidate = Data.define(:candidate, :status)
|
|
257
260
|
|
|
261
|
+
# chi context add's outcome: the source's +name+, and
|
|
262
|
+
# +movable_dropped+ when a movable flag was given and the remote's chi
|
|
263
|
+
# web didn't keep it (its answer has no movable: older than the flag).
|
|
264
|
+
ContextAdded = Data.define(:name, :movable_dropped) do
|
|
265
|
+
def initialize(name:, movable_dropped: false) = super
|
|
266
|
+
end
|
|
267
|
+
|
|
258
268
|
# chi context push's outcome: :pushed (+before+ the revision it had,
|
|
259
|
-
# +revision+ the new one), :missing (no such source)
|
|
260
|
-
# command source, whose text is its command's)
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
# +revision+ the new one), :missing (no such source), :not_push (a
|
|
270
|
+
# command source, whose text is its command's) or :continued (the
|
|
271
|
+
# session was continued, as +next_id+: nothing was written).
|
|
272
|
+
ContextPush = Data.define(:status, :before, :revision, :next_id) do
|
|
273
|
+
def initialize(status:, before: nil, revision: nil, next_id: nil) = super
|
|
263
274
|
|
|
264
275
|
# Why a push to +name+ was refused as :not_push: a command source's
|
|
265
276
|
# text is its command's, and a push between two fetches would read
|
|
266
277
|
# as a change and then vanish.
|
|
267
278
|
def self.not_push_message(name) = "#{name} runs a command, which writes its text: push goes to a --push source"
|
|
279
|
+
|
|
280
|
+
# Why a push to +name+ in session +id+ was refused as :continued: its
|
|
281
|
+
# writer holds the old id, and nothing reads a continued session.
|
|
282
|
+
# Generic (the CLI and chi web load no plugins): a peer's skill owns
|
|
283
|
+
# its re-pair.
|
|
284
|
+
def self.continued_message(name, id, next_id)
|
|
285
|
+
"#{id.to_s[0, 8]} was continued as #{next_id}: #{name} stays with the session it was attached to; " \
|
|
286
|
+
"attach it to #{next_id} (or pair again with #{next_id}) and push there"
|
|
287
|
+
end
|
|
268
288
|
end
|
|
269
289
|
end
|
|
270
290
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "session_setup"
|
|
|
6
6
|
require_relative "session_inbox"
|
|
7
7
|
require_relative "session_chain"
|
|
8
8
|
require_relative "child_move"
|
|
9
|
+
require_relative "context_move"
|
|
9
10
|
require_relative "archive_store"
|
|
10
11
|
require_relative "recap_store"
|
|
11
12
|
require_relative "bridge_client"
|
|
@@ -76,7 +77,8 @@ module Samagotchi
|
|
|
76
77
|
# running, a prompt queued or the step-limit question refuse; an idle
|
|
77
78
|
# worker is stopped), so a refusal starts nothing. Its open delegates
|
|
78
79
|
# (running, waiting, live, an unreported reply, at any depth below them)
|
|
79
|
-
# move to the new link instead (ChildMove): their reports reach it.
|
|
80
|
+
# move to the new link instead (ChildMove): their reports reach it. So
|
|
81
|
+
# does its movable attached context (ContextMove).
|
|
80
82
|
# Before that its live worker is asked for a recap and given up to
|
|
81
83
|
# +recap_wait+ seconds to write it.
|
|
82
84
|
# One continue of a session at a time (a lock in its folder): a double
|
|
@@ -87,7 +89,8 @@ module Samagotchi
|
|
|
87
89
|
# @param preview [String, nil] what the lists show before the first turn;
|
|
88
90
|
# by default the prompt, else the previous link's preview
|
|
89
91
|
# @return [Session] the new link (#model_warning as spawn_session's,
|
|
90
|
-
# #moved_children: the delegates it took over, #
|
|
92
|
+
# #moved_children: the delegates it took over, #moved_context: the
|
|
93
|
+
# attached context it took (ContextMove), #opener:
|
|
91
94
|
# SessionChain.opener_for the previous link)
|
|
92
95
|
# @raise [ArgumentError] no such session (Session::AmbiguousId for a prefix of several)
|
|
93
96
|
# @raise [Refused] continued already, delegates open that can't
|
|
@@ -199,9 +202,12 @@ module Samagotchi
|
|
|
199
202
|
recap = RecapStore.read(Session.session_dir(previous.id, state_dir: state_dir))
|
|
200
203
|
start = prompt.to_s.strip.empty? ? nil : prompt
|
|
201
204
|
preview = previous.first_preview if preview.to_s.strip.empty? && start.nil?
|
|
205
|
+
moved_context = nil
|
|
202
206
|
before_spawn = lambda do |link|
|
|
203
207
|
ChildMove.mark_starting(link.id, state_dir: state_dir)
|
|
204
208
|
ChildMove.apply(move, from: previous.id, to: link.id, state_dir: state_dir)
|
|
209
|
+
# Its movable context too, with a note saying what moved and stayed.
|
|
210
|
+
moved_context = ContextMove.call(from: previous.id, to: link.id, mode: :continue, state_dir: state_dir)
|
|
205
211
|
end
|
|
206
212
|
link = SessionManager.spawn_session(
|
|
207
213
|
prompt: start, preview: preview, title: previous.title, working_directory: previous.working_directory,
|
|
@@ -210,6 +216,7 @@ module Samagotchi
|
|
|
210
216
|
note_source: SessionChain::NOTE_SOURCE, state_dir: state_dir, before_spawn: before_spawn
|
|
211
217
|
)
|
|
212
218
|
link.moved_children = move
|
|
219
|
+
link.moved_context = moved_context
|
|
213
220
|
link.opener = SessionChain.opener_for(previous)
|
|
214
221
|
# The fallback's warning first: spawn_session set its own.
|
|
215
222
|
warnings = [choice.warning, link.model_warning].compact
|
|
@@ -69,7 +69,11 @@ module Samagotchi
|
|
|
69
69
|
'require "samagotchi/guardrails/context"' => "for GitInfo: a ctx helper for the checkout's files could replace it",
|
|
70
70
|
"Samagotchi::Guardrails::GitInfo.new" => "the work tree's top for the AGENTS.md chain (ctx.repo_root covers the top only)"
|
|
71
71
|
},
|
|
72
|
-
|
|
72
|
+
# Both bundles' goldens live in claude-peer's spec/.
|
|
73
|
+
"claude-peer" => PEER_SPEC.merge(
|
|
74
|
+
"Samagotchi::ContextSources.move" => "spec: a continue leaving the pairing behind, as SessionContinue " \
|
|
75
|
+
"moves it: needs a kit helper that continues a session"
|
|
76
|
+
),
|
|
73
77
|
"codex-peer" => PEER_SPEC,
|
|
74
78
|
"coordinator" => {
|
|
75
79
|
"Samagotchi::MemoryPaths.scope_dir" => "where the coordinator's memories live: needs a ctx memory-path API",
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -1428,13 +1428,18 @@ module Samagotchi
|
|
|
1428
1428
|
# +url+ (the page's only kind). As chi context add: {name, push: true,
|
|
1429
1429
|
# why, hint}, a source chi context push fills, or {name, cmd, every,
|
|
1430
1430
|
# why, hint}, a command chi runs every so often. added_by: "cli" or
|
|
1431
|
-
# "agent" (chi context's), else "web".
|
|
1432
|
-
#
|
|
1433
|
-
#
|
|
1431
|
+
# "agent" (chi context's), else "web". movable (any kind; optional):
|
|
1432
|
+
# whether a continue or a fork takes it (Source#movable?). 201
|
|
1433
|
+
# {status: "attached", name, movable: the source's, derived when not
|
|
1434
|
+
# given}; 400 invalid_json or invalid_source (a bad name, every or
|
|
1435
|
+
# movable), 422 not_a_url or no_provider, 409 exists.
|
|
1434
1436
|
def handle_context_add(req, id)
|
|
1435
1437
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
1436
1438
|
body = parse_json(request_body(req))
|
|
1437
1439
|
return error_response(400, "invalid_json", CONTEXT_ADD_BODIES) unless body.is_a?(Hash)
|
|
1440
|
+
unless [nil, true, false].include?(body["movable"])
|
|
1441
|
+
return error_response(400, "invalid_source", "movable is true or false")
|
|
1442
|
+
end
|
|
1438
1443
|
return context_add_named(id, body) if body.key?("name")
|
|
1439
1444
|
return error_response(400, "invalid_json", CONTEXT_ADD_BODIES) unless body["url"].is_a?(String)
|
|
1440
1445
|
|
|
@@ -1445,8 +1450,8 @@ module Samagotchi
|
|
|
1445
1450
|
return error_response(422, "no_provider", "no installed bundle resolves #{url[0, 200]}") unless resolved
|
|
1446
1451
|
|
|
1447
1452
|
own = ContextSources.session_location(session.id, state_dir: default_state_dir)
|
|
1448
|
-
own.add(web_source(resolved, body, own.scope))
|
|
1449
|
-
json_response(201, { status: "attached", name: resolved.name })
|
|
1453
|
+
source = own.add(web_source(resolved, body, own.scope))
|
|
1454
|
+
json_response(201, { status: "attached", name: resolved.name, movable: source.movable? })
|
|
1450
1455
|
rescue ContextProviders::Invalid => e
|
|
1451
1456
|
error_response(422, "no_provider", e.message)
|
|
1452
1457
|
rescue ContextSources::Invalid => e
|
|
@@ -1469,12 +1474,13 @@ module Samagotchi
|
|
|
1469
1474
|
ContextSources.check_name!(name)
|
|
1470
1475
|
ContextSources::Source.new(name: name, cmd: cmd, every_seconds: ContextSources.check_every!(every),
|
|
1471
1476
|
why: context_line(body["why"]), hint: context_line(body["hint"]), scope: nil,
|
|
1472
|
-
added_by: context_added_by(body), created_at: Time.now.utc.iso8601
|
|
1477
|
+
added_by: context_added_by(body), created_at: Time.now.utc.iso8601,
|
|
1478
|
+
movable: body["movable"])
|
|
1473
1479
|
rescue ContextSources::Invalid => e
|
|
1474
1480
|
return error_response(400, "invalid_source", e.message)
|
|
1475
1481
|
end
|
|
1476
1482
|
session_access.context_add(context_target(id), source)
|
|
1477
|
-
json_response(201, { status: "attached", name: name })
|
|
1483
|
+
json_response(201, { status: "attached", name: name, movable: source.movable? })
|
|
1478
1484
|
end
|
|
1479
1485
|
|
|
1480
1486
|
def context_line(value) = ContextSources.one_line(value.is_a?(String) ? value : nil, ContextSources::LINE_MAX_CHARS)
|
|
@@ -1485,7 +1491,7 @@ module Samagotchi
|
|
|
1485
1491
|
why: context_line(body["why"]) || resolved.why,
|
|
1486
1492
|
hint: context_line(body["hint"]) || resolved.hint, scope: scope,
|
|
1487
1493
|
added_by: context_added_by(body), created_at: Time.now.utc.iso8601,
|
|
1488
|
-
provider: resolved.bundle)
|
|
1494
|
+
provider: resolved.bundle, movable: body["movable"])
|
|
1489
1495
|
end
|
|
1490
1496
|
|
|
1491
1497
|
# DELETE /api/sessions/:id/context/:name: the session's own source is
|
|
@@ -318,14 +318,22 @@ export function openChildrenTemplate(prevId, children, hrefFor) {
|
|
|
318
318
|
|
|
319
319
|
// The toast after a continue: the new link, the previous one archived (when
|
|
320
320
|
// the page shows the new one), how many open delegates moved to it (the
|
|
321
|
-
// answer's moved),
|
|
322
|
-
//
|
|
321
|
+
// answer's moved), the attached context that moved and stayed (its
|
|
322
|
+
// moved_context; none from an older chi web), and the model fallback's
|
|
323
|
+
// warning (model_warning: the previous link's host is gone, the link starts
|
|
324
|
+
// on the default).
|
|
323
325
|
export function continuedText(prevId, created, { opened = true } = {}) {
|
|
324
326
|
const short = String(prevId).slice(0, 8);
|
|
325
327
|
const moved = Array.isArray(created?.moved) ? created.moved.length : 0;
|
|
326
328
|
const parts = [`Continued ${short} in ${String(created?.id || "").slice(0, 8)}`];
|
|
327
329
|
if (opened) parts.push(`${short} is archived`);
|
|
328
330
|
if (moved) parts.push(`${moved} open ${moved === 1 ? "delegate" : "delegates"} moved`);
|
|
331
|
+
const context = created?.moved_context;
|
|
332
|
+
const names = (list) => (Array.isArray(list) ? list : []).map((item) => (typeof item === "string" ? item : item?.name)).filter(Boolean);
|
|
333
|
+
const contextMoved = names(context?.moved);
|
|
334
|
+
const contextStayed = names(context?.stayed);
|
|
335
|
+
if (contextMoved.length) parts.push(`context moved: ${contextMoved.join(", ")}`);
|
|
336
|
+
if (contextStayed.length) parts.push(`context stayed with ${short}: ${contextStayed.join(", ")}`);
|
|
329
337
|
const warning = typeof created?.model_warning === "string" ? created.model_warning.trim() : "";
|
|
330
338
|
if (warning) parts.push(warning);
|
|
331
339
|
return parts.join("; ");
|
|
@@ -450,8 +450,10 @@ module Samagotchi
|
|
|
450
450
|
# hint}: new text for the session's --push source (chi context push;
|
|
451
451
|
# the fields as a source's JSON output gives them, checked the same
|
|
452
452
|
# way). 200 {status: "pushed", name, before, revision, changed}; 404
|
|
453
|
-
# context_missing (no such source); 409 not_push (a command source);
|
|
454
|
-
#
|
|
453
|
+
# context_missing (no such source); 409 not_push (a command source); 410
|
|
454
|
+
# context_continued (the session was continued, as next_id: never 409,
|
|
455
|
+
# which an older Remote reads as not_push, nor "continued", which it
|
|
456
|
+
# reads as a continue's refusal); 400 invalid_text.
|
|
455
457
|
def handle_context_push(req, id, name)
|
|
456
458
|
body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
|
|
457
459
|
return error_response(400, "missing_fields", "text is required") unless body["text"].is_a?(String)
|
|
@@ -465,6 +467,9 @@ module Samagotchi
|
|
|
465
467
|
case pushed.status
|
|
466
468
|
when :missing then error_response(404, "context_missing", "no source #{name[0, 40]}")
|
|
467
469
|
when :not_push then error_response(409, "not_push", SessionAccess::ContextPush.not_push_message(name))
|
|
470
|
+
when :continued
|
|
471
|
+
json_response(410, { error: "context_continued", next_id: pushed.next_id,
|
|
472
|
+
detail: SessionAccess::ContextPush.continued_message(name, id, pushed.next_id) })
|
|
468
473
|
else json_response(200, { status: "pushed", name: name, before: pushed.before, revision: pushed.revision,
|
|
469
474
|
changed: pushed.before != pushed.revision })
|
|
470
475
|
end
|
|
@@ -180,6 +180,7 @@ module Samagotchi
|
|
|
180
180
|
json_response(201, session_to_json(session).merge(bridge_port: port, moved: moved,
|
|
181
181
|
model_warning: session.model_warning,
|
|
182
182
|
opener: session.opener,
|
|
183
|
+
moved_context: session.moved_context&.to_h,
|
|
183
184
|
first_turn_id: session.first_turn_id))
|
|
184
185
|
end
|
|
185
186
|
|
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.141.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -304,6 +304,7 @@ files:
|
|
|
304
304
|
- lib/samagotchi/context_command.rb
|
|
305
305
|
- lib/samagotchi/context_fetch.rb
|
|
306
306
|
- lib/samagotchi/context_judge.rb
|
|
307
|
+
- lib/samagotchi/context_move.rb
|
|
307
308
|
- lib/samagotchi/context_note.rb
|
|
308
309
|
- lib/samagotchi/context_poller.rb
|
|
309
310
|
- lib/samagotchi/context_providers.rb
|