samagotchi 0.126.0 → 0.128.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 +4 -0
- data/docs/cli.md +1 -1
- data/docs/configuration.md +2 -2
- data/docs/guardrails.md +5 -0
- data/docs/internals/participants.md +17 -7
- data/docs/sessions.md +3 -1
- data/docs/testing.md +3 -2
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +56 -0
- data/lib/samagotchi/guardrails/read_only_shell.rb +48 -1
- data/lib/samagotchi/session_manager.rb +5 -0
- data/lib/samagotchi/session_participants.rb +14 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +9 -5
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +6 -12
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +23 -35
- data/lib/samagotchi/terminal_ui/attached_loop.rb +10 -18
- data/lib/samagotchi/terminal_ui/own_turns.rb +81 -0
- data/lib/samagotchi/verify_anchor.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +12 -0
- data/lib/samagotchi/web/participant_access.rb +16 -1
- data/lib/samagotchi/web/participant_review.rb +4 -2
- data/lib/samagotchi/web/project_list.rb +52 -0
- data/lib/samagotchi/web/public/annotate_bar.js +14 -2
- data/lib/samagotchi/web/public/app.js +25 -4
- data/lib/samagotchi/web/public/composer.css +13 -1
- data/lib/samagotchi/web/public/index.html +35 -16
- data/lib/samagotchi/web/public/notify.js +4 -1
- data/lib/samagotchi/web/public/participant.html +33 -32
- data/lib/samagotchi/web/public/participant.js +16 -6
- data/lib/samagotchi/web/public/participant_model.js +3 -2
- data/lib/samagotchi/web/public/participant_names.js +2 -2
- data/lib/samagotchi/web/public/project_picker.js +115 -0
- data/lib/samagotchi/web/public/question_card.js +4 -1
- data/lib/samagotchi/web/public/restore.js +7 -4
- data/lib/samagotchi/web/public/scope.js +8 -10
- data/lib/samagotchi/web/public/send_turn.js +3 -1
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/stage_view.js +1 -1
- data/lib/samagotchi/web/public/stream_handlers.js +7 -3
- data/lib/samagotchi/web/public/verdict_card.js +18 -3
- data/lib/samagotchi/web/public/verdict_fresh.js +11 -0
- data/lib/samagotchi/web/public/verify_card.css +1 -0
- data/lib/samagotchi/web/public/verify_preset.js +6 -0
- data/lib/samagotchi/web/push/attention.rb +24 -5
- data/lib/samagotchi/web/verify_fresh_routes.rb +12 -7
- data/lib/samagotchi/worker.rb +7 -4
- metadata +4 -1
|
@@ -53,6 +53,7 @@ export function emptySessionState() {
|
|
|
53
53
|
// worker_version / worker_features): the info bar's stale-worker badge.
|
|
54
54
|
workerVersion: null,
|
|
55
55
|
workerFeatures: [],
|
|
56
|
+
bridgeUp: false,
|
|
56
57
|
// The LLM context strategy, apply rule and budget the next turn runs
|
|
57
58
|
// under, each with its source (the info bar's llm ctx chip), or null.
|
|
58
59
|
llmContext: null,
|
|
@@ -95,6 +96,7 @@ export function sessionStateFrom(data) {
|
|
|
95
96
|
// not at the session's next hub event (followSelectedSession).
|
|
96
97
|
workerVersion: s.worker_version || null,
|
|
97
98
|
workerFeatures: Array.isArray(s.worker_features) ? s.worker_features : [],
|
|
99
|
+
bridgeUp: s.bridge_up === true,
|
|
98
100
|
timing: normalizeTiming(data.timing),
|
|
99
101
|
};
|
|
100
102
|
// The saved fill, so a reloaded or stopped session shows it before its
|
|
@@ -66,7 +66,7 @@ export function createStageView({
|
|
|
66
66
|
// The dock becomes a clear column: the stage's card, then the composer's
|
|
67
67
|
// (its rows moved into .dock-card, which wears the glass and the focus ring).
|
|
68
68
|
document.body.classList.add("stage-view");
|
|
69
|
-
const card = node("div", "dock-card");
|
|
69
|
+
const card = node("div", "dock-card composer-card");
|
|
70
70
|
card.append(...dockEl.children);
|
|
71
71
|
dockEl.appendChild(card);
|
|
72
72
|
const stage = node("section", "turn-stage");
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
// guess (takeGuess: whether, and forget it); restart:
|
|
27
27
|
// close the stream and resync
|
|
28
28
|
// composer: {empty(), refill(text), setChips(chips)}
|
|
29
|
+
// restoreVerify(anchor) a restored Verify turn's claim, armed again for its resend
|
|
29
30
|
// getSession(id, opts) data.js
|
|
30
31
|
// resync(), updateInfoBar(), applyPromptOps(data), showSteers(steers),
|
|
31
32
|
// markRecapStale(), setTurnRunning(running), setLiveStatus(status),
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
import { commandView, drawCommandBubble, historyHasCommandBubble } from "./commands.js";
|
|
55
56
|
import { keepEarlyRestore, restoreAction } from "./restore.js";
|
|
56
57
|
import { PAIR, setBadgeMode } from "./participant_names.js";
|
|
58
|
+
import { isParticipantAuthor } from "./participant_model.js";
|
|
57
59
|
|
|
58
60
|
export function createStreamHandlers(deps) {
|
|
59
61
|
const {
|
|
@@ -162,12 +164,13 @@ export function createStreamHandlers(deps) {
|
|
|
162
164
|
// A participant's review turn: its id and author, for Apply at its end
|
|
163
165
|
// (turn_completed names neither). A pair turn acted itself: no Apply.
|
|
164
166
|
const author = data.author || data.origin?.author;
|
|
167
|
+
const participant = isParticipantAuthor(author);
|
|
165
168
|
const pair = data.participant_mode === PAIR;
|
|
166
|
-
liveTurn.review =
|
|
169
|
+
liveTurn.review = participant && data.turn_id && !pair ? { turn_id: data.turn_id, author, author_name: data.author_name } : null;
|
|
167
170
|
// Its prompt drawn while queued takes the turn's mode now.
|
|
168
171
|
if (data.participant_mode) setBadgeMode(promptBubble, data.participant_mode);
|
|
169
172
|
// The owner's input queues behind it (the composer's delivery line).
|
|
170
|
-
liveTurn.participantTurn =
|
|
173
|
+
liveTurn.participantTurn = participant;
|
|
171
174
|
setTurnRunning(true);
|
|
172
175
|
setLiveStatus("running");
|
|
173
176
|
liveTurn.timing.detach();
|
|
@@ -244,8 +247,9 @@ export function createStreamHandlers(deps) {
|
|
|
244
247
|
prompt_restored: async (data) => {
|
|
245
248
|
const id = selected();
|
|
246
249
|
keepEarlyRestore(data, earlyRestores, { myId: clientId, sentIds });
|
|
247
|
-
const { refill, label, images: restoredImages } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
250
|
+
const { refill, label, images: restoredImages, verify } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
248
251
|
if (refill) composer.refill(refill);
|
|
252
|
+
if (refill && verify) deps.restoreVerify?.(verify);
|
|
249
253
|
if (restoredImages?.length) composer.setChips(restoredChips(selected(), restoredImages));
|
|
250
254
|
const failedText = liveTurn.failedText;
|
|
251
255
|
liveTurn.failedText = null;
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { ownerClaimHash } from "./claim_link.js";
|
|
2
|
+
|
|
1
3
|
const TYPES = Object.freeze({
|
|
2
4
|
confirmed: { icon: "✅", label: "confirmed" },
|
|
3
5
|
refuted: { icon: "❌", label: "refuted" },
|
|
4
6
|
unclear: { icon: "❓", label: "unclear" },
|
|
5
7
|
});
|
|
6
8
|
|
|
7
|
-
export
|
|
9
|
+
export const CHECK_NOT_ON_PAGE = "That check is no longer on this page.";
|
|
10
|
+
|
|
11
|
+
export function ownerCheckHash(sessions, currentSessionId, checkTurnId) {
|
|
12
|
+
if (!Array.isArray(sessions) || typeof checkTurnId !== "string") return null;
|
|
13
|
+
const session = sessions.find((candidate) =>
|
|
14
|
+
candidate?.id !== currentSessionId
|
|
15
|
+
&& Array.isArray(candidate.verdicts)
|
|
16
|
+
&& candidate.verdicts.some((verdict) => verdict?.check_turn_id === checkTurnId));
|
|
17
|
+
return session ? ownerClaimHash(session.id, { turnId: checkTurnId }) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function placeVerdict(root, record, { who = "Owner", author = null, onMissingCheck = null } = {}) {
|
|
8
21
|
const anchor = record?.anchor;
|
|
9
22
|
const type = TYPES[record?.verdict];
|
|
10
23
|
if (!root || !type || typeof anchor?.turn_id !== "string" || typeof anchor?.quote !== "string"
|
|
@@ -47,8 +60,10 @@ export function placeVerdict(root, record, { who = "Owner", author = null } = {}
|
|
|
47
60
|
link.textContent = "View check";
|
|
48
61
|
link.addEventListener("click", (event) => {
|
|
49
62
|
event.preventDefault();
|
|
50
|
-
[...root.querySelectorAll("[data-turn-id]")]
|
|
51
|
-
|
|
63
|
+
const check = [...root.querySelectorAll("[data-turn-id]")]
|
|
64
|
+
.find((element) => element.dataset.turnId === record.check_turn_id);
|
|
65
|
+
if (check) check.scrollIntoView?.({ behavior: "smooth", block: "center" });
|
|
66
|
+
else onMissingCheck?.(record);
|
|
52
67
|
});
|
|
53
68
|
card.append(heading, quote, reason, link);
|
|
54
69
|
|
|
@@ -79,6 +79,17 @@ export function renderFresh(card, fresh = {}) {
|
|
|
79
79
|
const link = childLink(typeof run === "string" ? null : run.child_id);
|
|
80
80
|
if (link) line.append(" ", link);
|
|
81
81
|
part.append(line);
|
|
82
|
+
} else if (card.classList.contains("unclear") && fresh.unavailableReason) {
|
|
83
|
+
const button = doc.createElement("button");
|
|
84
|
+
button.type = "button";
|
|
85
|
+
button.className = "verify-fresh-btn";
|
|
86
|
+
button.textContent = "Verify fresh";
|
|
87
|
+
button.title = fresh.unavailableReason;
|
|
88
|
+
button.disabled = true;
|
|
89
|
+
const note = doc.createElement("p");
|
|
90
|
+
note.className = "verify-fresh-unavailable";
|
|
91
|
+
note.textContent = fresh.unavailableReason;
|
|
92
|
+
part.append(button, note);
|
|
82
93
|
} else if (card.classList.contains("unclear") && fresh.canStart && fresh.start) {
|
|
83
94
|
const button = doc.createElement("button");
|
|
84
95
|
button.type = "button";
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
.verify-fresh-result{font-weight:600;color:var(--text-bright)}
|
|
12
12
|
.verify-fresh-running{color:var(--text-muted);font-style:italic}
|
|
13
13
|
.verify-fresh-error{color:var(--danger-text)}
|
|
14
|
+
.verify-fresh-unavailable{color:var(--text-muted)}
|
|
14
15
|
.verify-fresh-link{color:var(--accent);font-weight:400}
|
|
15
16
|
.verify-fresh-btn{font:inherit;font-size:12px;font-weight:600;padding:3px 12px;border-radius:8px;cursor:pointer;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
16
17
|
.verify-fresh-btn:hover{border-color:var(--accent)}
|
|
@@ -12,6 +12,12 @@ export const VERIFY_PRESET = Object.freeze({
|
|
|
12
12
|
+ "End with a line `Verdict: confirmed|refuted|unclear — <one-line reason>`.",
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
export const VERIFY_UNAVAILABLE = "restart this session to use Verify";
|
|
16
|
+
|
|
17
|
+
export function verifyWorkerUnavailable(workerFeatures, bridgeUp) {
|
|
18
|
+
return bridgeUp === true && Array.isArray(workerFeatures) && !workerFeatures.includes("verify");
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
// +presets+ (annotate_presets.js parsePresets) with Verify after them.
|
|
16
22
|
export function withVerify(presets) {
|
|
17
23
|
return [...(presets || []), VERIFY_PRESET];
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../steer"
|
|
4
|
+
|
|
3
5
|
module Samagotchi
|
|
4
6
|
module Web
|
|
5
7
|
module Push
|
|
@@ -118,15 +120,31 @@ module Samagotchi
|
|
|
118
120
|
# @return [Array<String>]
|
|
119
121
|
def open_keys(by_id)
|
|
120
122
|
by_id.each_value.flat_map do |summary|
|
|
121
|
-
question = summary
|
|
122
|
-
card = summary
|
|
123
|
+
question = open_question(summary)
|
|
124
|
+
card = open_card(summary)
|
|
123
125
|
keys = []
|
|
124
|
-
keys << key(summary[:id], question[:id]) if question
|
|
125
|
-
keys << card_key(summary[:id], card[:id]) if card
|
|
126
|
+
keys << key(summary[:id], question[:id]) if question
|
|
127
|
+
keys << card_key(summary[:id], card[:id]) if card
|
|
126
128
|
keys
|
|
127
129
|
end
|
|
128
130
|
end
|
|
129
131
|
|
|
132
|
+
# Whether this summary contributes to the project's waiting count:
|
|
133
|
+
# the same open question or card #open_keys badges.
|
|
134
|
+
def waits?(summary) = !!(open_question(summary) || open_card(summary))
|
|
135
|
+
|
|
136
|
+
# The summary's unanswered question not relayed elsewhere, or nil.
|
|
137
|
+
def open_question(summary)
|
|
138
|
+
question = summary[:pending_question]
|
|
139
|
+
question if question.is_a?(Hash) && question[:id] && !question[:relayed_to]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The summary's pending card, or nil.
|
|
143
|
+
def open_card(summary)
|
|
144
|
+
card = summary[:pending_card]
|
|
145
|
+
card if card.is_a?(Hash) && card[:id]
|
|
146
|
+
end
|
|
147
|
+
|
|
130
148
|
def key(session_id, id) = "#{session_id}:#{id}"
|
|
131
149
|
def card_key(session_id, card_id) = "#{session_id}:card:#{card_id}"
|
|
132
150
|
|
|
@@ -151,7 +169,8 @@ module Samagotchi
|
|
|
151
169
|
result = verdict[:verdict].to_s
|
|
152
170
|
icon = { "confirmed" => "✅", "refuted" => "❌", "unclear" => "❓" }.fetch(result, "")
|
|
153
171
|
by = verdict[:by_name]
|
|
154
|
-
|
|
172
|
+
# An owner's verdict has no participant author (nil, "" or "owner").
|
|
173
|
+
by = Steer.participant_author?(verdict[:by]) ? "A participant" : "You" unless present?(by)
|
|
155
174
|
reason = clip(verdict[:reason].to_s, 120)
|
|
156
175
|
"#{by}: #{icon} #{result} — #{reason}"
|
|
157
176
|
end
|
|
@@ -33,14 +33,18 @@ module Samagotchi
|
|
|
33
33
|
def handle_verify_fresh(req, id, of)
|
|
34
34
|
clicker = principal(req).owner? ? nil : principal(req).grant
|
|
35
35
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
36
|
-
if clicker && (refused = participant_cap_refusal(clicker) || fresh_worker_refusal(session))
|
|
36
|
+
if clicker && (refused = participant_cap_refusal(clicker) || fresh_worker_refusal(session, participant: true))
|
|
37
37
|
return verify_fresh_refused(clicker, of, *refused)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
by = Steer.sender_author(field_of(VerifyFresh.original(session.verdicts, of), :by))
|
|
41
41
|
runner = by ? verdict_author_grant(id, by) : clicker
|
|
42
42
|
return author_link_ended(clicker, id, of, by) if by && runner.nil?
|
|
43
|
-
if runner && runner != clicker && (refused = participant_cap_refusal(runner) ||
|
|
43
|
+
if runner && runner != clicker && (refused = participant_cap_refusal(runner) ||
|
|
44
|
+
fresh_worker_refusal(session, participant: true))
|
|
45
|
+
return verify_fresh_refused(clicker, of, *refused, runner: runner)
|
|
46
|
+
end
|
|
47
|
+
if (refused = fresh_worker_refusal(session))
|
|
44
48
|
return verify_fresh_refused(clicker, of, *refused, runner: runner)
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -76,12 +80,13 @@ module Samagotchi
|
|
|
76
80
|
|
|
77
81
|
def field_of(record, key) = record && (record[key] || record[key.to_s])
|
|
78
82
|
|
|
79
|
-
# A live worker
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
def fresh_worker_refusal(session)
|
|
83
|
+
# A live worker must support VerifyFresh's parent checks. For a
|
|
84
|
+
# participant's child it must also have the review boundary.
|
|
85
|
+
def fresh_worker_refusal(session, participant: false)
|
|
83
86
|
features = ParticipantMode.live_features(session, default_state_dir)
|
|
84
|
-
return nil if features.nil?
|
|
87
|
+
return nil if features.nil?
|
|
88
|
+
return [409, VerifyAnchor::UNAVAILABLE["error"], VerifyAnchor::UNAVAILABLE["detail"]] if VerifyAnchor.worker_unavailable?(features)
|
|
89
|
+
return nil unless participant && !features.include?(Delivery::REVIEW_FEATURE)
|
|
85
90
|
|
|
86
91
|
[409, Delivery::REVIEW_UNAVAILABLE["error"], Delivery::REVIEW_UNAVAILABLE["detail"]]
|
|
87
92
|
end
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -697,7 +697,7 @@ module Samagotchi
|
|
|
697
697
|
images: prompt.images, **(prompt.verify ? { verify: prompt.verify } : {})) do |result, error|
|
|
698
698
|
if error
|
|
699
699
|
# The Engine announced :turn_failed (with the error's one line).
|
|
700
|
-
restore_failed_turn([[prompt.text, origin, prompt.images], *@merged_this_turn], error: error)
|
|
700
|
+
restore_failed_turn([[prompt.text, origin, prompt.images, prompt.verify], *@merged_this_turn], error: error)
|
|
701
701
|
else
|
|
702
702
|
@continue_offer.after_turn(result, no_interrupt: prompt.no_interrupt)
|
|
703
703
|
end
|
|
@@ -1383,8 +1383,10 @@ module Samagotchi
|
|
|
1383
1383
|
# or the restored ones, never the one without the other. A turn that
|
|
1384
1384
|
# got somewhere stays instead (TurnFlow#prompt_turn_failed), with the
|
|
1385
1385
|
# prompts it took: nothing goes back.
|
|
1386
|
-
# @param prompts [Array<Array(String, TurnOrigin|nil, Array|nil)>]
|
|
1387
|
-
# origin, images] (a web client gets its image chips
|
|
1386
|
+
# @param prompts [Array<Array(String, TurnOrigin|nil, Array|nil, VerifyAnchor|nil)>]
|
|
1387
|
+
# [prompt, origin, images, verify] (a web client gets its image chips
|
|
1388
|
+
# back, and a Verify turn's claim, so sending it again still records
|
|
1389
|
+
# a verdict)
|
|
1388
1390
|
# @param error [Exception, nil] what failed: its one line stays in the
|
|
1389
1391
|
# conversation as a turn note
|
|
1390
1392
|
# Not saved here: run_engine_turn saves after its block.
|
|
@@ -1392,9 +1394,10 @@ module Samagotchi
|
|
|
1392
1394
|
@engine.synchronize_events do
|
|
1393
1395
|
next if @turn_flow.prompt_turn_failed(error: error) == :kept
|
|
1394
1396
|
|
|
1395
|
-
prompts.each do |prompt, origin, images|
|
|
1397
|
+
prompts.each do |prompt, origin, images, verify|
|
|
1396
1398
|
restored = { type: :prompt_restored, prompt: prompt, origin: origin.wire }
|
|
1397
1399
|
restored[:images] = images unless Array(images).empty?
|
|
1400
|
+
restored[:verify] = verify.to_h if verify
|
|
1398
1401
|
@engine.announce(restored)
|
|
1399
1402
|
end
|
|
1400
1403
|
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.128.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -623,6 +623,7 @@ files:
|
|
|
623
623
|
- lib/samagotchi/terminal_ui/local_link.rb
|
|
624
624
|
- lib/samagotchi/terminal_ui/model_segments_text.rb
|
|
625
625
|
- lib/samagotchi/terminal_ui/one_shot_sink.rb
|
|
626
|
+
- lib/samagotchi/terminal_ui/own_turns.rb
|
|
626
627
|
- lib/samagotchi/terminal_ui/participant_commands.rb
|
|
627
628
|
- lib/samagotchi/terminal_ui/plain_surface.rb
|
|
628
629
|
- lib/samagotchi/terminal_ui/question_prompt.rb
|
|
@@ -752,6 +753,7 @@ files:
|
|
|
752
753
|
- lib/samagotchi/web/participants_admin.rb
|
|
753
754
|
- lib/samagotchi/web/principal.rb
|
|
754
755
|
- lib/samagotchi/web/private_files.rb
|
|
756
|
+
- lib/samagotchi/web/project_list.rb
|
|
755
757
|
- lib/samagotchi/web/proxy_gate.rb
|
|
756
758
|
- lib/samagotchi/web/public/activity.js
|
|
757
759
|
- lib/samagotchi/web/public/annotate_actions.js
|
|
@@ -822,6 +824,7 @@ files:
|
|
|
822
824
|
- lib/samagotchi/web/public/participant_names.js
|
|
823
825
|
- lib/samagotchi/web/public/participants_panel.js
|
|
824
826
|
- lib/samagotchi/web/public/plain_line.js
|
|
827
|
+
- lib/samagotchi/web/public/project_picker.js
|
|
825
828
|
- lib/samagotchi/web/public/prompt_history.js
|
|
826
829
|
- lib/samagotchi/web/public/push.js
|
|
827
830
|
- lib/samagotchi/web/public/push_devices.js
|