samagotchi 0.154.1 → 0.156.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 +29 -1
- data/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +166 -98
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// A chi-ui picks (docs/internals/inline-widgets.md): rows of things to decide,
|
|
2
|
+
// each with its options as chips, the recommended one marked and picked to
|
|
3
|
+
// start with, a "+ note" per row, and one "Fill reply" that puts the picks in
|
|
4
|
+
// the composer as plain text ("1: fix now (today) · D4: B"). It never sends:
|
|
5
|
+
// the composer text is the widget's only way out, and the owner reads it
|
|
6
|
+
// before sending. Ported from eval 0013's viewer.
|
|
7
|
+
//
|
|
8
|
+
// The data is P1a's normalised JSON (InlineWidgets). Its strings are the
|
|
9
|
+
// model's: they go in as textContent or attributes only, never markup. The
|
|
10
|
+
// picks live here, in the widget: a rewrite of the bubble starts over.
|
|
11
|
+
//
|
|
12
|
+
// A module of its own: static JS is cached at unversioned URLs.
|
|
13
|
+
|
|
14
|
+
// What a row starts with: its recommended option picked, no note.
|
|
15
|
+
export function initialPicks(rows) {
|
|
16
|
+
return (rows || []).map((r) => ({ pick: r.recommended != null ? String(r.recommended) : null, note: "" }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The reply the picks make: "id: option (note)" per picked row, " · " between
|
|
20
|
+
// them; a row with no pick is left out.
|
|
21
|
+
export function replyText(rows, picks) {
|
|
22
|
+
return (rows || [])
|
|
23
|
+
.map((r, k) => {
|
|
24
|
+
const { pick, note } = picks[k] || {};
|
|
25
|
+
if (!pick) return null;
|
|
26
|
+
const said = String(note ?? "").replace(/\s+/g, " ").trim();
|
|
27
|
+
return `${r.id ?? k + 1}: ${pick}${said ? ` (${said})` : ""}`;
|
|
28
|
+
})
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(" · ");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The composer's text with +reply+: after a draft on a line of its own, the
|
|
34
|
+
// draft kept as typed.
|
|
35
|
+
export function withDraft(draft, reply) {
|
|
36
|
+
const typed = String(draft ?? "").trimEnd();
|
|
37
|
+
return typed ? `${typed}\n${reply}` : reply;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function el(doc, tag, className, text) {
|
|
41
|
+
const node = doc.createElement(tag);
|
|
42
|
+
if (className) node.className = className;
|
|
43
|
+
if (text != null) node.textContent = String(text);
|
|
44
|
+
return node;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function button(doc, className, text) {
|
|
48
|
+
const node = el(doc, "button", className, text);
|
|
49
|
+
node.type = "button";
|
|
50
|
+
return node;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The picks as an element, for a widget host's shadow root.
|
|
54
|
+
// @param fill (reply) => puts +reply+ in the composer (never sends)
|
|
55
|
+
export function drawPicks(picks, { fill, doc = document }) {
|
|
56
|
+
const rows = picks.rows || [];
|
|
57
|
+
const state = initialPicks(rows);
|
|
58
|
+
|
|
59
|
+
const box = el(doc, "div", "w picks");
|
|
60
|
+
box.setAttribute("role", "group");
|
|
61
|
+
box.setAttribute("aria-label", `Picks: ${picks.title || "choices"}`);
|
|
62
|
+
const head = el(doc, "div", "w-head");
|
|
63
|
+
const count = el(doc, "span", "w-count");
|
|
64
|
+
head.append(el(doc, "span", "w-kind", "Picks"), el(doc, "span", "w-title", picks.title || ""), count);
|
|
65
|
+
const body = el(doc, "div", "w-body");
|
|
66
|
+
box.append(head, body);
|
|
67
|
+
|
|
68
|
+
const list = el(doc, "div", "wp-list");
|
|
69
|
+
const chips = rows.map((r, k) => {
|
|
70
|
+
const id = String(r.id ?? k + 1);
|
|
71
|
+
const row = el(doc, "div", "wp-row");
|
|
72
|
+
const text = el(doc, "div", "wp-text");
|
|
73
|
+
text.append(el(doc, "div", null, r.text || ""));
|
|
74
|
+
if (r.why) text.append(el(doc, "div", "wp-why", r.why));
|
|
75
|
+
const add = button(doc, "wp-addnote", "+ note");
|
|
76
|
+
add.addEventListener("click", () => {
|
|
77
|
+
const input = el(doc, "input", "wp-note");
|
|
78
|
+
input.type = "text";
|
|
79
|
+
input.placeholder = "a word for this row";
|
|
80
|
+
input.setAttribute("aria-label", `Note for ${id}`);
|
|
81
|
+
input.addEventListener("input", () => { state[k].note = input.value; });
|
|
82
|
+
add.replaceWith(input);
|
|
83
|
+
input.focus();
|
|
84
|
+
});
|
|
85
|
+
text.append(add);
|
|
86
|
+
const opts = el(doc, "div", "wp-opts");
|
|
87
|
+
opts.setAttribute("role", "group");
|
|
88
|
+
opts.setAttribute("aria-label", `Options for ${id}`);
|
|
89
|
+
const own = (r.options || []).map((option) => {
|
|
90
|
+
const o = String(option);
|
|
91
|
+
const b = button(doc, "wp-opt", o);
|
|
92
|
+
if (r.recommended != null && String(r.recommended) === o) {
|
|
93
|
+
b.append(el(doc, "span", "rec", "rec"));
|
|
94
|
+
b.title = "recommended";
|
|
95
|
+
}
|
|
96
|
+
b.addEventListener("click", () => {
|
|
97
|
+
state[k].pick = state[k].pick === o ? null : o;
|
|
98
|
+
paint();
|
|
99
|
+
});
|
|
100
|
+
opts.append(b);
|
|
101
|
+
return [o, b];
|
|
102
|
+
});
|
|
103
|
+
row.append(el(doc, "span", "wp-id", id), text, opts);
|
|
104
|
+
list.append(row);
|
|
105
|
+
return own;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const foot = el(doc, "div", "wp-foot");
|
|
109
|
+
const fillBtn = button(doc, "wp-fill", "Fill reply");
|
|
110
|
+
fillBtn.title = "Put the picks in the composer; you send it";
|
|
111
|
+
foot.append(fillBtn, el(doc, "span", "hint", "fills the composer, never sends"));
|
|
112
|
+
body.append(list, foot);
|
|
113
|
+
|
|
114
|
+
fillBtn.addEventListener("click", () => {
|
|
115
|
+
const reply = replyText(rows, state);
|
|
116
|
+
if (reply) fill(reply);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
function paint() {
|
|
120
|
+
chips.forEach((own, k) => {
|
|
121
|
+
for (const [o, b] of own) b.setAttribute("aria-pressed", String(state[k].pick === o));
|
|
122
|
+
});
|
|
123
|
+
const picked = state.filter((s) => s.pick).length;
|
|
124
|
+
count.textContent = `${picked} of ${rows.length} picked`;
|
|
125
|
+
fillBtn.disabled = picked === 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
paint();
|
|
129
|
+
return box;
|
|
130
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// A chi-ui walk (docs/internals/inline-widgets.md): a scenario stepped through
|
|
2
|
+
// one step at a time. On top a swimlane, one column per actor and one row per
|
|
3
|
+
// step (the current, the seen and a case's target marked; a click jumps); under
|
|
4
|
+
// it the step's card (the actor, what it does, "You see", the dashed "Behind
|
|
5
|
+
// it"); a fork asks "Which case?" instead of Next; Back, Next and the path
|
|
6
|
+
// taken, and the arrow keys while the widget has the focus. Ported from eval
|
|
7
|
+
// 0013's viewer.
|
|
8
|
+
//
|
|
9
|
+
// The data is P1a's normalised JSON (InlineWidgets): ids are strings, every
|
|
10
|
+
// next and goto names a step, round 1's branches are cases. Its strings are
|
|
11
|
+
// the model's: they go in as textContent or attributes only, never markup.
|
|
12
|
+
// The position lives here, in the widget: a rewrite of the bubble starts over.
|
|
13
|
+
//
|
|
14
|
+
// A module of its own: static JS is cached at unversioned URLs.
|
|
15
|
+
|
|
16
|
+
// A case that is no condition: round 1 wrote the usual path as "always".
|
|
17
|
+
export const ALWAYS = /^(always|otherwise|then|next)$/i;
|
|
18
|
+
export const USUAL = "otherwise (the usual path)";
|
|
19
|
+
// A step's `next: end` (InlineWidgets::END_NEXT): the walk ends there, a
|
|
20
|
+
// branch that ends early.
|
|
21
|
+
export const END = "end";
|
|
22
|
+
|
|
23
|
+
const isAlways = (when) => ALWAYS.test(String(when ?? "").trim());
|
|
24
|
+
|
|
25
|
+
// Where each step of +walk+ goes, by index.
|
|
26
|
+
// - nextOf(k): the step after k when it isn't a fork: its next, else a case
|
|
27
|
+
// that is no condition, else the step below; null at the end, and at a
|
|
28
|
+
// step whose next is `end`.
|
|
29
|
+
// - casesOf(k): a fork's cases, [{when, to}] (to null when the goto names no
|
|
30
|
+
// step), or null when k is no fork. A case that is no condition reads as
|
|
31
|
+
// the usual path, and a step that names a next as well offers it first when
|
|
32
|
+
// no case goes there (round 1's next + branches).
|
|
33
|
+
export function walkRoutes(walk) {
|
|
34
|
+
const steps = walk?.steps || [];
|
|
35
|
+
const byId = new Map(steps.map((s, k) => [String(s.id), k]));
|
|
36
|
+
const idx = (id) => (id == null ? null : byId.get(String(id)) ?? null);
|
|
37
|
+
const casesIn = (k) => (Array.isArray(steps[k]?.cases) ? steps[k].cases : []);
|
|
38
|
+
const isFork = (k) => casesIn(k).some((c) => !isAlways(c.when));
|
|
39
|
+
|
|
40
|
+
function nextOf(k) {
|
|
41
|
+
const step = steps[k];
|
|
42
|
+
if (!step || step.next === END) return null;
|
|
43
|
+
const named = idx(step.next);
|
|
44
|
+
if (named != null) return named;
|
|
45
|
+
const always = casesIn(k).find((c) => isAlways(c.when) && idx(c.goto) != null);
|
|
46
|
+
if (always && !isFork(k)) return idx(always.goto);
|
|
47
|
+
return k + 1 < steps.length ? k + 1 : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function casesOf(k) {
|
|
51
|
+
if (!isFork(k)) return null;
|
|
52
|
+
const cases = casesIn(k).map((c) => ({ when: isAlways(c.when) ? USUAL : String(c.when ?? ""), to: idx(c.goto) }));
|
|
53
|
+
const named = idx(steps[k].next);
|
|
54
|
+
if (named != null && !cases.some((c) => c.to === named)) cases.unshift({ when: USUAL, to: named });
|
|
55
|
+
return cases;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { steps, nextOf, casesOf };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The swimlane's cells: one per step, in its actor's column (1-based, after
|
|
62
|
+
// the number column) and its own row (after the actors' row), with a tag that
|
|
63
|
+
// says where it leads when that isn't the step below: "N cases", "→ n", "end".
|
|
64
|
+
export function swimlaneCells(walk, routes = walkRoutes(walk)) {
|
|
65
|
+
const actors = (walk?.actors || []).map(String);
|
|
66
|
+
const { steps, nextOf, casesOf } = routes;
|
|
67
|
+
return steps.map((s, k) => {
|
|
68
|
+
const cases = casesOf(k);
|
|
69
|
+
const next = nextOf(k);
|
|
70
|
+
let tag = null;
|
|
71
|
+
if (cases) tag = { text: `${cases.length} cases`, fork: true };
|
|
72
|
+
else if (next != null && next !== k + 1) tag = { text: `→ ${next + 1}`, fork: false };
|
|
73
|
+
else if (next == null && k < steps.length - 1) tag = { text: "end", fork: false };
|
|
74
|
+
return { k, column: Math.max(0, actors.indexOf(String(s.actor))) + 2, row: k + 2, text: String(s.does || s.sees || ""), tag };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Where the walker is: the path taken (step indexes) and the steps seen.
|
|
79
|
+
// next() at the end starts over.
|
|
80
|
+
export function createWalker(routes) {
|
|
81
|
+
let path = [0];
|
|
82
|
+
const seen = new Set([0]);
|
|
83
|
+
return {
|
|
84
|
+
get at() { return path[path.length - 1]; },
|
|
85
|
+
get path() { return [...path]; },
|
|
86
|
+
seen: (k) => seen.has(k),
|
|
87
|
+
go(k) {
|
|
88
|
+
if (k == null || !routes.steps[k] || k === this.at) return;
|
|
89
|
+
path.push(k);
|
|
90
|
+
seen.add(k);
|
|
91
|
+
},
|
|
92
|
+
back() { if (path.length > 1) path.pop(); },
|
|
93
|
+
next() {
|
|
94
|
+
const n = routes.nextOf(this.at);
|
|
95
|
+
if (n != null) { this.go(n); return; }
|
|
96
|
+
path = [0];
|
|
97
|
+
seen.clear();
|
|
98
|
+
seen.add(0);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The path's words under the card, empty before the first move.
|
|
104
|
+
export function trailText(path) {
|
|
105
|
+
return path.length > 1 ? `path ${path.map((k) => k + 1).join(" → ")}` : "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function el(doc, tag, className, text) {
|
|
109
|
+
const node = doc.createElement(tag);
|
|
110
|
+
if (className) node.className = className;
|
|
111
|
+
if (text != null) node.textContent = String(text);
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function button(doc, className, text) {
|
|
116
|
+
const node = el(doc, "button", className, text);
|
|
117
|
+
node.type = "button";
|
|
118
|
+
return node;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The walk as an element, for a widget host's shadow root.
|
|
122
|
+
export function drawWalk(walk, doc = document) {
|
|
123
|
+
const routes = walkRoutes(walk);
|
|
124
|
+
const { steps, nextOf, casesOf } = routes;
|
|
125
|
+
const actors = (walk.actors || []).map(String);
|
|
126
|
+
const walker = createWalker(routes);
|
|
127
|
+
|
|
128
|
+
const box = el(doc, "div", "w walk");
|
|
129
|
+
box.tabIndex = 0;
|
|
130
|
+
box.setAttribute("role", "group");
|
|
131
|
+
box.setAttribute("aria-label", `Walk: ${walk.title || "a scenario"}`);
|
|
132
|
+
const head = el(doc, "div", "w-head");
|
|
133
|
+
const count = el(doc, "span", "w-count");
|
|
134
|
+
head.append(el(doc, "span", "w-kind", "Walk"), el(doc, "span", "w-title", walk.title || ""), count);
|
|
135
|
+
const body = el(doc, "div", "w-body");
|
|
136
|
+
box.append(head, body);
|
|
137
|
+
|
|
138
|
+
const lane = el(doc, "div", "ww-swim");
|
|
139
|
+
lane.style.setProperty("--actors", String(actors.length));
|
|
140
|
+
lane.append(el(doc, "span"));
|
|
141
|
+
for (const actor of actors) lane.append(el(doc, "span", "ww-swim-actor", actor));
|
|
142
|
+
const cells = swimlaneCells(walk, routes).map((cell) => {
|
|
143
|
+
const num = el(doc, "span", "ww-swim-num", cell.k + 1);
|
|
144
|
+
num.style.gridRow = String(cell.row);
|
|
145
|
+
const node = button(doc, "ww-cell");
|
|
146
|
+
node.style.gridColumn = String(cell.column);
|
|
147
|
+
node.style.gridRow = String(cell.row);
|
|
148
|
+
node.append(el(doc, "span", "ww-cell-t", cell.text));
|
|
149
|
+
if (cell.tag) node.append(el(doc, "span", cell.tag.fork ? "ww-tag fork" : "ww-tag", cell.tag.text));
|
|
150
|
+
const s = steps[cell.k];
|
|
151
|
+
node.title = `${s.does || ""}${s.does && s.sees ? "\n" : ""}${s.sees ? `sees: ${s.sees}` : ""}`;
|
|
152
|
+
node.setAttribute("aria-label", `Step ${cell.k + 1}, ${s.actor}: ${cell.text}`);
|
|
153
|
+
node.addEventListener("click", () => { walker.go(cell.k); render(); });
|
|
154
|
+
lane.append(num, node);
|
|
155
|
+
return node;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const card = el(doc, "div", "ww-step");
|
|
159
|
+
card.setAttribute("aria-live", "polite");
|
|
160
|
+
const nav = el(doc, "div", "ww-nav");
|
|
161
|
+
const prev = button(doc, "btn", "← Back");
|
|
162
|
+
const next = button(doc, "btn primary", "Next →");
|
|
163
|
+
const trail = el(doc, "span", "ww-trail");
|
|
164
|
+
nav.append(prev, trail, next);
|
|
165
|
+
body.append(lane, card, nav);
|
|
166
|
+
|
|
167
|
+
prev.addEventListener("click", () => { walker.back(); render(); });
|
|
168
|
+
next.addEventListener("click", () => { walker.next(); render(); });
|
|
169
|
+
box.addEventListener("keydown", (e) => {
|
|
170
|
+
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
171
|
+
if (e.key === "ArrowRight" && !next.hidden) { e.preventDefault(); next.click(); }
|
|
172
|
+
if (e.key === "ArrowLeft" && !prev.disabled) { e.preventDefault(); prev.click(); }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const peek = (k, on) => cells[k]?.classList.toggle("peek", on);
|
|
176
|
+
|
|
177
|
+
function pane(className, label, text) {
|
|
178
|
+
const node = el(doc, "div", `ww-pane ${className}`);
|
|
179
|
+
node.append(el(doc, "span", "eyebrow", label), el(doc, "span", null, text));
|
|
180
|
+
return node;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A keyboard move can remove, hide or disable the focused button (a case,
|
|
184
|
+
// Next at a fork, Back at the start): the focus stays in the widget.
|
|
185
|
+
function render() {
|
|
186
|
+
const focused = box.getRootNode()?.activeElement;
|
|
187
|
+
const hadFocus = !!focused && box.contains(focused);
|
|
188
|
+
const k = walker.at;
|
|
189
|
+
const s = steps[k] || {};
|
|
190
|
+
card.replaceChildren();
|
|
191
|
+
const top = el(doc, "div", "ww-step-top");
|
|
192
|
+
top.append(el(doc, "span", "ww-actor", s.actor || ""), el(doc, "span", "ww-stepno", `${k + 1} / ${steps.length}`));
|
|
193
|
+
card.append(top);
|
|
194
|
+
if (s.does) card.append(el(doc, "div", "ww-does", s.does));
|
|
195
|
+
if (s.sees) card.append(pane("ww-sees", "You see", s.sees));
|
|
196
|
+
if (s.behind) card.append(pane("ww-behind", "Behind it", s.behind));
|
|
197
|
+
const cases = casesOf(k);
|
|
198
|
+
if (cases) {
|
|
199
|
+
const fork = el(doc, "div", "ww-fork");
|
|
200
|
+
fork.append(el(doc, "span", "eyebrow", "Which case?"));
|
|
201
|
+
for (const c of cases) {
|
|
202
|
+
const b = button(doc, "ww-case");
|
|
203
|
+
b.append(el(doc, "span", null, c.when), el(doc, "span", "ww-case-to", c.to != null ? `step ${c.to + 1} →` : "?"));
|
|
204
|
+
b.disabled = c.to == null;
|
|
205
|
+
b.addEventListener("click", () => { walker.go(c.to); render(); });
|
|
206
|
+
for (const [type, on] of [["mouseenter", true], ["mouseleave", false], ["focus", true], ["blur", false]]) {
|
|
207
|
+
b.addEventListener(type, () => peek(c.to, on));
|
|
208
|
+
}
|
|
209
|
+
fork.append(b);
|
|
210
|
+
}
|
|
211
|
+
card.append(fork);
|
|
212
|
+
}
|
|
213
|
+
cells.forEach((cell, j) => {
|
|
214
|
+
cell.classList.toggle("cur", j === k);
|
|
215
|
+
cell.classList.toggle("seen", walker.seen(j) && j !== k);
|
|
216
|
+
cell.classList.remove("peek");
|
|
217
|
+
if (j === k) cell.setAttribute("aria-current", "step");
|
|
218
|
+
else cell.removeAttribute("aria-current");
|
|
219
|
+
});
|
|
220
|
+
count.textContent = `step ${k + 1} of ${steps.length}`;
|
|
221
|
+
prev.disabled = walker.path.length < 2;
|
|
222
|
+
next.hidden = !!cases;
|
|
223
|
+
next.textContent = nextOf(k) == null ? "Start over" : "Next →";
|
|
224
|
+
trail.textContent = trailText(walker.path);
|
|
225
|
+
if (hadFocus && (!focused.isConnected || focused.hidden || focused.disabled)) box.focus({ preventScroll: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
render();
|
|
229
|
+
return box;
|
|
230
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* A chi-ui widget (inline_widgets.js), inside its host's shadow root: chi's :root tokens (tokens.css) reach it as inherited custom properties, so light and dark follow the page. Ported from eval 0013's viewer. At a phone's width the swimlane scrolls sideways inside the widget; the page never does. */
|
|
2
|
+
:host{display:block;min-width:0}
|
|
3
|
+
*{box-sizing:border-box}
|
|
4
|
+
[hidden]{display:none!important}
|
|
5
|
+
button{font:inherit;color:inherit;cursor:pointer;margin:0}
|
|
6
|
+
button:disabled{cursor:default}
|
|
7
|
+
button:focus-visible,input:focus-visible,.w:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
8
|
+
.eyebrow{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}
|
|
9
|
+
|
|
10
|
+
/* The frame both share. The head leaves room on the right for the figure's source toggle and a code block's copy button (block_renderers.css). */
|
|
11
|
+
.w{border:1px solid var(--accent-border);border-radius:10px;background:var(--bg-inset);overflow:hidden;font-size:14px;line-height:1.5;color:var(--text-2)}
|
|
12
|
+
.w-head{display:flex;gap:8px;align-items:baseline;padding:8px 68px 8px 12px;border-bottom:1px solid var(--border);flex-wrap:wrap}
|
|
13
|
+
.w-kind{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);font-weight:600}
|
|
14
|
+
.w-title{font-weight:600;color:var(--text-bright);min-width:0;overflow-wrap:anywhere}
|
|
15
|
+
.w-count{margin-left:auto;color:var(--text-muted);font-size:12px;font-variant-numeric:tabular-nums}
|
|
16
|
+
.w-body{padding:12px;display:grid;gap:12px;min-width:0}
|
|
17
|
+
|
|
18
|
+
/* walk */
|
|
19
|
+
.ww-swim{display:grid;grid-template-columns:22px repeat(var(--actors,2),minmax(84px,1fr));gap:3px 6px;align-items:stretch;font-size:12px;overflow-x:auto;padding-bottom:2px}
|
|
20
|
+
.ww-swim-actor{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);text-align:center;padding-bottom:2px;border-bottom:1px solid var(--border-strong);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
21
|
+
.ww-swim-num{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);font-size:11px;align-self:center;text-align:right}
|
|
22
|
+
.ww-cell{display:flex;gap:6px;align-items:center;justify-content:space-between;text-align:left;min-width:0;border:1px solid var(--border);background:var(--bg-panel);border-radius:6px;padding:3px 8px;color:var(--text-3);transition:background .15s,border-color .15s}
|
|
23
|
+
.ww-cell:hover{border-color:var(--border-strong)}
|
|
24
|
+
.ww-cell-t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
25
|
+
.ww-cell.seen{border-color:var(--accent-border);color:var(--text-2)}
|
|
26
|
+
.ww-cell.cur{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}
|
|
27
|
+
.ww-cell.peek{border-color:var(--warn);box-shadow:0 0 0 2px var(--warn-bg)}
|
|
28
|
+
.ww-tag{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--text-faint)}
|
|
29
|
+
.ww-tag.fork{color:var(--warn)}
|
|
30
|
+
.ww-cell.cur .ww-tag{color:var(--on-solid);opacity:.85}
|
|
31
|
+
.ww-step{display:grid;gap:8px;min-height:120px;border-top:1px solid var(--border);padding-top:12px;align-content:start}
|
|
32
|
+
.ww-step-top{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
|
|
33
|
+
.ww-actor{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--accent);font-size:12px}
|
|
34
|
+
.ww-stepno{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);font-size:11px}
|
|
35
|
+
.ww-does{font-size:16px;color:var(--text-bright);font-weight:500;text-wrap:pretty;overflow-wrap:anywhere}
|
|
36
|
+
.ww-pane{border-radius:8px;padding:8px 10px;display:grid;gap:2px;overflow-wrap:anywhere}
|
|
37
|
+
.ww-sees{background:var(--bg-panel);border:1px solid var(--border);color:var(--text)}
|
|
38
|
+
.ww-behind{background:transparent;border:1px dashed var(--border-strong);color:var(--text-3);font-size:13px}
|
|
39
|
+
.ww-fork{display:grid;gap:6px}
|
|
40
|
+
.ww-case{display:flex;gap:10px;justify-content:space-between;align-items:center;text-align:left;border:1px solid var(--border-strong);background:var(--bg-panel);border-radius:8px;padding:7px 10px;overflow-wrap:anywhere}
|
|
41
|
+
.ww-case:hover:not(:disabled),.ww-case:focus-visible{border-color:var(--warn)}
|
|
42
|
+
.ww-case:disabled{opacity:.5}
|
|
43
|
+
.ww-case-to{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--warn)}
|
|
44
|
+
.ww-nav{display:flex;gap:10px;align-items:center;justify-content:space-between}
|
|
45
|
+
.ww-nav .btn{flex:none;border:1px solid var(--border-strong);background:var(--bg-panel);border-radius:8px;padding:4px 12px}
|
|
46
|
+
.ww-nav .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}
|
|
47
|
+
.ww-nav .btn:disabled{opacity:.4}
|
|
48
|
+
.ww-trail{font-size:12px;color:var(--text-faint);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere;text-align:center;flex:1;min-width:0}
|
|
49
|
+
@media (prefers-reduced-motion:reduce){.ww-cell{transition:none}}
|
|
50
|
+
|
|
51
|
+
/* picks */
|
|
52
|
+
.wp-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:start;padding:8px 0;border-bottom:1px solid var(--border)}
|
|
53
|
+
.wp-row:first-child{padding-top:0}
|
|
54
|
+
.wp-row:last-child{border-bottom:0}
|
|
55
|
+
.wp-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--text-muted);padding-top:2px;min-width:24px}
|
|
56
|
+
.wp-text{color:var(--text);min-width:0;overflow-wrap:anywhere}
|
|
57
|
+
.wp-why{color:var(--text-muted);font-size:12px}
|
|
58
|
+
.wp-note{display:block;margin-top:4px;width:100%;background:var(--bg-panel);border:1px solid var(--border-strong);border-radius:6px;color:var(--text);padding:3px 6px;font:inherit;font-size:12px}
|
|
59
|
+
.wp-opts{display:flex;flex-wrap:wrap;gap:4px;justify-content:flex-end;max-width:320px}
|
|
60
|
+
.wp-opt{border:1px solid var(--border-strong);background:var(--bg-panel);border-radius:6px;padding:2px 10px;font-size:12px;overflow-wrap:anywhere;text-align:left}
|
|
61
|
+
.wp-opt:hover{border-color:var(--accent-border)}
|
|
62
|
+
.wp-opt[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}
|
|
63
|
+
.wp-opt .rec{font-size:9px;letter-spacing:.06em;text-transform:uppercase;margin-left:5px;opacity:.75}
|
|
64
|
+
.wp-addnote{background:none;border:0;color:var(--text-faint);font-size:12px;padding:2px 0}
|
|
65
|
+
.wp-addnote:hover{color:var(--text-2)}
|
|
66
|
+
.wp-foot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
67
|
+
.wp-fill{background:var(--accent);border:1px solid var(--accent);color:var(--on-solid);border-radius:8px;padding:4px 14px}
|
|
68
|
+
.wp-fill:disabled{opacity:.4}
|
|
69
|
+
.hint{color:var(--text-faint);font-size:12px}
|
|
70
|
+
@media (max-width:560px){.wp-row{grid-template-columns:auto minmax(0,1fr)}.wp-opts{grid-column:2;justify-content:flex-start;max-width:none}}
|
|
@@ -78,14 +78,14 @@ module Samagotchi
|
|
|
78
78
|
# 404 not_found; 400 ambiguous_id with ids (the sessions it names).
|
|
79
79
|
def handle_resolve(req)
|
|
80
80
|
given = req.params["id"].to_s
|
|
81
|
-
return
|
|
81
|
+
return session_not_found(given) unless Session.valid_id?(given)
|
|
82
82
|
return participant_resolve(principal(req).grant, given) unless principal(req).owner?
|
|
83
83
|
|
|
84
84
|
json_response(200, { id: session_access.resolve(given) })
|
|
85
85
|
rescue Session::AmbiguousId => e
|
|
86
86
|
refusal_response(400, "ambiguous_id", e, ids: e.ids)
|
|
87
|
-
rescue ArgumentError
|
|
88
|
-
|
|
87
|
+
rescue ArgumentError # NotFound, a file that won't load
|
|
88
|
+
session_not_found(given)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# GET /api/sessions/:id?brief=1: the session as a session command
|
|
@@ -47,7 +47,7 @@ module Samagotchi
|
|
|
47
47
|
|
|
48
48
|
def handle_question_answer(req, id)
|
|
49
49
|
session = @session_class.load(id, state_dir: default_state_dir) rescue nil
|
|
50
|
-
return
|
|
50
|
+
return session_not_found(id) unless session
|
|
51
51
|
|
|
52
52
|
body = parse_json(request_body(req))
|
|
53
53
|
return error_response(400, "invalid_json", "invalid JSON body") unless body.is_a?(Hash)
|
|
@@ -239,7 +239,7 @@ module Samagotchi
|
|
|
239
239
|
return [] if raw.nil?
|
|
240
240
|
|
|
241
241
|
session_dir = image_session_dir(id)
|
|
242
|
-
return
|
|
242
|
+
return Session::NotFound.new(id).message unless session_dir
|
|
243
243
|
|
|
244
244
|
ImageStore.check_refs(session_dir, raw)
|
|
245
245
|
end
|
|
@@ -259,7 +259,7 @@ module Samagotchi
|
|
|
259
259
|
# (converted, downscaled); answers the ref the turn then names.
|
|
260
260
|
def handle_image_upload(req, id)
|
|
261
261
|
session_dir = image_session_dir(id)
|
|
262
|
-
return
|
|
262
|
+
return session_not_found(id) unless session_dir
|
|
263
263
|
|
|
264
264
|
length = req.content_length.to_i
|
|
265
265
|
if length > MAX_IMAGE_UPLOAD_BYTES
|
|
@@ -140,7 +140,7 @@ module Samagotchi
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
# POST /api/sessions with continues: <id, prefix or last:id>: the next
|
|
143
|
-
# link of that session's chain (
|
|
143
|
+
# link of that session's chain (SessionContinue.run), in its
|
|
144
144
|
# folder and with its own setup (SessionSetup: llm_context, thinking
|
|
145
145
|
# and reasoning_history levels, settings), so dir, llm_context, thinking and settings are
|
|
146
146
|
# refused beside it (a create takes no reasoning_history). It runs on
|
|
@@ -328,11 +328,11 @@ module Samagotchi
|
|
|
328
328
|
[level, nil]
|
|
329
329
|
end
|
|
330
330
|
|
|
331
|
-
#
|
|
331
|
+
# SessionContinue.run, its refusals as responses: [session,
|
|
332
332
|
# nil] or [nil, the response]. Only its own errors: what fails after
|
|
333
333
|
# the new link started is not a refusal of it.
|
|
334
334
|
def continue_from(given, prompt:, preview:, model:)
|
|
335
|
-
[
|
|
335
|
+
[SessionContinue.run(given, prompt: prompt, preview: preview, model: model, state_dir: @state_dir), nil]
|
|
336
336
|
rescue SessionContinue::Refused => e
|
|
337
337
|
extra = e.reason == :continued ? { next_id: e.ids.first } : {}
|
|
338
338
|
[nil, refusal_response(409, e.reason.to_s, e, session_id: e.session_id, reason: e.reason.to_s, ids: e.ids, **extra)]
|
|
@@ -412,7 +412,7 @@ module Samagotchi
|
|
|
412
412
|
# read is mtime-cached), so a host added or a window changed while
|
|
413
413
|
# chi web runs is picked up on the next refresh of a card.
|
|
414
414
|
def hub_registry
|
|
415
|
-
hosts =
|
|
415
|
+
hosts = HostConfig.all
|
|
416
416
|
if @hub_registry.nil? || hosts != @hub_registry_hosts
|
|
417
417
|
@hub_registry = HostRegistry.new(hosts_config: hosts)
|
|
418
418
|
@hub_registry_hosts = hosts
|
|
@@ -184,7 +184,7 @@ module Samagotchi
|
|
|
184
184
|
# cards from its light GET tail, else the saved ones; the session file
|
|
185
185
|
# is only stat'ed (the 404), never parsed.
|
|
186
186
|
def handle_cards_read(id)
|
|
187
|
-
return
|
|
187
|
+
return session_not_found(id) unless session_exists?(id)
|
|
188
188
|
|
|
189
189
|
tail = live_tail(id)
|
|
190
190
|
json_response(200, { cards: cards_for_display(live_or_saved(tail, "cards") { saved_cards(id) }) })
|
|
@@ -201,7 +201,7 @@ module Samagotchi
|
|
|
201
201
|
# are the newest turn's (#recent_timing), which the page merges by id
|
|
202
202
|
# (the page still sends ?recent=1, which is accepted and not needed).
|
|
203
203
|
def handle_tail_read(req, id)
|
|
204
|
-
return
|
|
204
|
+
return session_not_found(id) unless session_exists?(id)
|
|
205
205
|
|
|
206
206
|
turn_id = req.params["turn_id"].to_s.strip
|
|
207
207
|
turn_id = nil if turn_id.empty?
|
|
@@ -259,7 +259,7 @@ module Samagotchi
|
|
|
259
259
|
# the live ones) and turn_count, when the page's merge comes up short.
|
|
260
260
|
# The worker's GET state, never its messages.
|
|
261
261
|
def handle_timing_read(id)
|
|
262
|
-
return
|
|
262
|
+
return session_not_found(id) unless session_exists?(id)
|
|
263
263
|
|
|
264
264
|
state = bridge_get_json(id, "state")
|
|
265
265
|
json_response(200, { timing: timing_payload(id, live_metrics: state && state["session_state_snapshot"]&.fetch("metrics", nil)) })
|
|
@@ -146,13 +146,12 @@ module Samagotchi
|
|
|
146
146
|
private_class_method :budget_resolver
|
|
147
147
|
|
|
148
148
|
# status is turn state (idle/running). The live worker's snapshot is the
|
|
149
|
-
# truth; on disk, a "running" with no live
|
|
150
|
-
# that died mid-turn.
|
|
149
|
+
# truth; on disk, Session#displayed_status (a "running" with no live
|
|
150
|
+
# owner was left by a worker that died mid-turn).
|
|
151
151
|
def displayed_status(session, snapshot = nil, owner:)
|
|
152
152
|
return snapshot["status"] if snapshot.is_a?(Hash) && snapshot["status"]
|
|
153
|
-
return session.status unless session.status == Session::STATUS_RUNNING
|
|
154
153
|
|
|
155
|
-
|
|
154
|
+
session.displayed_status(owner: owner)
|
|
156
155
|
end
|
|
157
156
|
|
|
158
157
|
def first_preview_for(session)
|