samagotchi 0.96.0 → 0.98.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 +55 -1
- data/bin/chi +3 -0
- data/docs/architecture.md +12 -2
- data/docs/bundle-authoring.md +7 -4
- data/docs/cli.md +13 -4
- data/docs/configuration.md +13 -0
- data/docs/hooks.md +59 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -2
- data/docs/internals/engine.md +21 -1
- data/docs/memory.md +43 -3
- data/docs/plugins.md +8 -1
- data/docs/sessions.md +2 -2
- data/lib/samagotchi/answer_refs.rb +16 -9
- data/lib/samagotchi/bundle_command/check.rb +5 -3
- data/lib/samagotchi/bundle_command/try/run.rb +19 -4
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +39 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +75 -40
- data/lib/samagotchi/hooks/bundle_loader.rb +18 -15
- data/lib/samagotchi/hooks.rb +4 -1
- data/lib/samagotchi/llm/errors.rb +135 -3
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +28 -9
- data/lib/samagotchi/memory_bundle/manifest.rb +17 -0
- data/lib/samagotchi/memory_bundle/profile.rb +14 -4
- data/lib/samagotchi/memory_command.rb +237 -0
- data/lib/samagotchi/output_formatter.rb +8 -2
- data/lib/samagotchi/pending_question.rb +7 -3
- data/lib/samagotchi/plugin/api.rb +12 -3
- data/lib/samagotchi/question_asked.rb +83 -0
- data/lib/samagotchi/question_desk.rb +3 -2
- data/lib/samagotchi/send_command.rb +11 -11
- data/lib/samagotchi/session_inbox.rb +29 -0
- data/lib/samagotchi/session_manager.rb +11 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +1 -0
- data/lib/samagotchi/testing/bundle.rb +18 -1
- data/lib/samagotchi/tools/memory.rb +1 -1
- data/lib/samagotchi/turn_checkpoint.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +30 -9
- data/lib/samagotchi/web/public/app.js +44 -11
- data/lib/samagotchi/web/public/index.html +36 -13
- data/lib/samagotchi/web/public/info_bar_model.js +0 -2
- data/lib/samagotchi/web/public/lists_view.js +32 -8
- data/lib/samagotchi/web/public/markdown_place.js +25 -8
- data/lib/samagotchi/web/public/question_cards.js +23 -6
- data/lib/samagotchi/web/public/session_rename.js +88 -15
- data/lib/samagotchi/web/public/session_title.js +6 -0
- data/lib/samagotchi/web/public/stage_frame.js +24 -14
- data/lib/samagotchi/web/public/stage_model.js +4 -2
- data/lib/samagotchi/web/public/stage_slots.js +41 -9
- data/lib/samagotchi/web/public/stage_view.js +149 -8
- data/lib/samagotchi/web/public/stream_handlers.js +5 -1
- data/lib/samagotchi/web/public/turn_model.js +20 -0
- data/lib/samagotchi/web/public/turn_view.js +22 -9
- data/lib/samagotchi/worker.rb +19 -5
- data/lib/samagotchi/worker_idle_exit.rb +16 -8
- metadata +3 -1
|
@@ -40,6 +40,10 @@ const PENDING = ".bubble.question:not(.answered):not(.cancelled), .bubble.contin
|
|
|
40
40
|
// asks the user too, but the turn goes on: it waits only while the turn runs
|
|
41
41
|
// and never holds the hand-off (a turn cut short can leave it open).
|
|
42
42
|
const ASKS = ".plugin-card[data-in-turn] .card-action";
|
|
43
|
+
// The model's own question (question_cards.js data-kind; not an approval,
|
|
44
|
+
// the step limit, a hook's question or a delegate's relayed one): the one
|
|
45
|
+
// card whose long asking text gets the question room.
|
|
46
|
+
const ASK_CARD = '.bubble.question[data-kind="question"]:not([data-relay])';
|
|
43
47
|
|
|
44
48
|
function readCollapsed() {
|
|
45
49
|
return readStored(COLLAPSE_KEY) === "1";
|
|
@@ -113,6 +117,14 @@ export function createStageView({
|
|
|
113
117
|
chip.type = "button";
|
|
114
118
|
const chipLabel = node("span", "ts-chip-label", chip);
|
|
115
119
|
const ticks = node("span", "ts-ticks", chip);
|
|
120
|
+
// The question room's jump pill, at the scroller's bottom edge (absolute,
|
|
121
|
+
// above the chip): while the room's card is out of the stage's view it
|
|
122
|
+
// eases the stage to it (drawPill).
|
|
123
|
+
const pill = node("button", "ts-room-pill", cloud);
|
|
124
|
+
pill.type = "button";
|
|
125
|
+
pill.setAttribute("aria-label", "Go to the question");
|
|
126
|
+
pill.hidden = true;
|
|
127
|
+
pill.textContent = "❓ Answer ↓";
|
|
116
128
|
// Below the turn's card: the lines queued for after it (placeKind
|
|
117
129
|
// "queued"), each to run as a turn of its own; they hand off last.
|
|
118
130
|
const queuedSlot = node("div", "ts-queued", stage);
|
|
@@ -140,6 +152,21 @@ export function createStageView({
|
|
|
140
152
|
let revealed = false;
|
|
141
153
|
let rafPending = false;
|
|
142
154
|
let foldTimer = null;
|
|
155
|
+
// The question room (drawRoom): the pending ask card whose asking text
|
|
156
|
+
// overflowed its cap, and the turn's raised cap (stage_frame.js; kept
|
|
157
|
+
// until the next turn: the frame is grow-only).
|
|
158
|
+
let roomCard = null;
|
|
159
|
+
let roomCap = false;
|
|
160
|
+
// What the overflow was measured for ({ card, text }): measured once the
|
|
161
|
+
// asking text shows in its final form (its markdown, or the raw text when
|
|
162
|
+
// there is none: the raw text is taller, and a short question's could
|
|
163
|
+
// overflow where its markdown fits), again only for a new card or text,
|
|
164
|
+
// never at a cap's change (a phone's keyboard).
|
|
165
|
+
let measured = null;
|
|
166
|
+
// The ask card whose arrival reveal has run (no pill while the stage
|
|
167
|
+
// grows to it), and whether the room's card is in the stage's view.
|
|
168
|
+
let arrived = null;
|
|
169
|
+
let cardInSight = true;
|
|
143
170
|
|
|
144
171
|
const view = createTurnView({
|
|
145
172
|
historyEl,
|
|
@@ -181,6 +208,69 @@ export function createStageView({
|
|
|
181
208
|
return !!(extras.querySelector(PENDING) || tail.querySelector(PENDING));
|
|
182
209
|
}
|
|
183
210
|
|
|
211
|
+
// The pending card in the extras when it is the model's own question
|
|
212
|
+
// (never the tail's: a post-end card, the step limit; asking is empty then).
|
|
213
|
+
function askCard() {
|
|
214
|
+
const card = extras.querySelector(PENDING);
|
|
215
|
+
return card?.matches(ASK_CARD) ? card : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The asking text overflows its cap (+1 px of rounding), measured with
|
|
219
|
+
// the room's class off.
|
|
220
|
+
function askingOverflows() {
|
|
221
|
+
return !asking.hidden && asking.scrollHeight > asking.clientHeight + 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The question room: while a model's own question waits and its asking
|
|
225
|
+
// text didn't fit its cap, the slot loses its cap and the stage's one
|
|
226
|
+
// scroll reads the whole reply (index.html .question-room); the cap rises
|
|
227
|
+
// for the rest of the turn. The card keeps the room until it stops
|
|
228
|
+
// pending (+text+: the asking text drawn).
|
|
229
|
+
function drawRoom(blocked, text) {
|
|
230
|
+
const ask = blocked ? askCard() : null;
|
|
231
|
+
if (roomCard && ask === roomCard) return;
|
|
232
|
+
if (roomCard) {
|
|
233
|
+
cardSight.unobserve(roomCard);
|
|
234
|
+
stage.classList.remove("question-room");
|
|
235
|
+
roomCard = null;
|
|
236
|
+
}
|
|
237
|
+
if (!ask || !slotsView.askingSettled() || (measured && measured.card === ask && measured.text === text)) return;
|
|
238
|
+
measured = { card: ask, text };
|
|
239
|
+
if (!askingOverflows()) return;
|
|
240
|
+
roomCard = ask;
|
|
241
|
+
stage.classList.add("question-room");
|
|
242
|
+
cardInSight = true;
|
|
243
|
+
cardSight.observe(roomCard);
|
|
244
|
+
if (!roomCap) {
|
|
245
|
+
roomCap = true;
|
|
246
|
+
frame.fitCap();
|
|
247
|
+
}
|
|
248
|
+
// Decided after the card's arrival reveal (a slow markdown read): the
|
|
249
|
+
// reply reads from its start now, the pill only after that.
|
|
250
|
+
if (arrived === ask) {
|
|
251
|
+
arrived = null;
|
|
252
|
+
frame.afterGrowth(() => {
|
|
253
|
+
if (roomCard !== ask) return;
|
|
254
|
+
frame.reveal(extras, { fromStart: true });
|
|
255
|
+
arrived = ask;
|
|
256
|
+
drawPill();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// The pill: the room's card out of the stage's view, once its arrival
|
|
262
|
+
// reveal has run, the stage open.
|
|
263
|
+
function drawPill() {
|
|
264
|
+
pill.hidden = !(roomCard && !collapsed && arrived === roomCard && !cardInSight);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The room's card in the stage's view: ~40 px of it shows above the
|
|
268
|
+
// scroller's bottom edge (where the pill sits).
|
|
269
|
+
const cardSight = new IntersectionObserver((entries) => {
|
|
270
|
+
for (const entry of entries) if (entry.target === roomCard) cardInSight = entry.isIntersecting;
|
|
271
|
+
drawPill();
|
|
272
|
+
}, { root: scroller, rootMargin: "0px 0px -40px 0px", threshold: 0 });
|
|
273
|
+
|
|
184
274
|
function asksCard() {
|
|
185
275
|
return !endKind && !!extras.querySelector(ASKS);
|
|
186
276
|
}
|
|
@@ -214,7 +304,10 @@ export function createStageView({
|
|
|
214
304
|
// next step (held-wait, set with waiting): the card under them never
|
|
215
305
|
// moves at the answer. The waiting height is measured before the swap.
|
|
216
306
|
const wasWaiting = stage.classList.contains("waiting");
|
|
217
|
-
if (blocked && !wasWaiting)
|
|
307
|
+
if (blocked && !wasWaiting) {
|
|
308
|
+
stage.classList.add("held-wait");
|
|
309
|
+
live.style.minHeight = "";
|
|
310
|
+
}
|
|
218
311
|
if (!blocked && wasWaiting) stage.style.setProperty("--live-held", `${live.offsetHeight}px`);
|
|
219
312
|
stage.classList.toggle("plain", plain);
|
|
220
313
|
stage.classList.toggle("ended", !!endKind);
|
|
@@ -225,7 +318,9 @@ export function createStageView({
|
|
|
225
318
|
phaseEl.textContent = slots.phase;
|
|
226
319
|
|
|
227
320
|
slotsView.drawHeadline(slots.headline);
|
|
228
|
-
slotsView.drawAsking(slots.asking);
|
|
321
|
+
slotsView.drawAsking(slots.asking, slots.askingRefs);
|
|
322
|
+
drawRoom(blocked, slots.asking);
|
|
323
|
+
drawPill();
|
|
229
324
|
slotsView.drawNow(slots.tool, slots.quiet);
|
|
230
325
|
slotsView.drawTrail(slots.trail, flash);
|
|
231
326
|
slotsView.drawTicks(slots.ticks);
|
|
@@ -253,6 +348,25 @@ export function createStageView({
|
|
|
253
348
|
tail.hidden = !tail.firstChild;
|
|
254
349
|
}
|
|
255
350
|
|
|
351
|
+
// The next step: the live slots' waiting height is let go. In a turn
|
|
352
|
+
// with the question room that height was the whole reply's: it goes as
|
|
353
|
+
// far as the stage's scroll absorbs it (the scroll moves up by as much,
|
|
354
|
+
// in the same task: the card in view stays put), the rest kept as the
|
|
355
|
+
// live slots' least height until the next wait or turn.
|
|
356
|
+
function releaseHeldWait() {
|
|
357
|
+
const held = roomCap && stage.classList.contains("held-wait") && !stage.classList.contains("waiting");
|
|
358
|
+
const before = held ? live.offsetHeight : 0;
|
|
359
|
+
const top = scroller.scrollTop;
|
|
360
|
+
stage.classList.remove("held-wait");
|
|
361
|
+
stage.style.removeProperty("--live-held");
|
|
362
|
+
live.style.minHeight = "";
|
|
363
|
+
if (!held) return;
|
|
364
|
+
const after = live.offsetHeight;
|
|
365
|
+
const absorbed = Math.min(Math.max(0, before - after), top);
|
|
366
|
+
if (before - absorbed > after) live.style.minHeight = `${before - absorbed}px`;
|
|
367
|
+
if (absorbed) scroller.scrollTop = top - absorbed;
|
|
368
|
+
}
|
|
369
|
+
|
|
256
370
|
// One line in the trail for FLASH_MS, ahead of the calls (a newer flash
|
|
257
371
|
// takes its place).
|
|
258
372
|
function flashTrail(what) {
|
|
@@ -339,6 +453,13 @@ export function createStageView({
|
|
|
339
453
|
else setCloud(!cloudOpen);
|
|
340
454
|
});
|
|
341
455
|
headline.addEventListener("click", peek);
|
|
456
|
+
// To the card: its end in sight (the arrival's rule), or back up to it
|
|
457
|
+
// for a reader below it (the cloud open under the card).
|
|
458
|
+
pill.addEventListener("click", () => {
|
|
459
|
+
if (!roomCard) return;
|
|
460
|
+
if (roomCard.offsetTop < scroller.scrollTop) frame.easeStage(Math.max(0, roomCard.offsetTop - 8));
|
|
461
|
+
else frame.reveal(extras);
|
|
462
|
+
});
|
|
342
463
|
// A ref's "show in turn" (refs.js) names a row in the cloud: open the
|
|
343
464
|
// stage and the cloud to it, as a hand toggle would.
|
|
344
465
|
// A collapsed stage opens for this turn only: the kept choice
|
|
@@ -378,6 +499,7 @@ export function createStageView({
|
|
|
378
499
|
isHolding: () => holding,
|
|
379
500
|
isCollapsed: () => collapsed,
|
|
380
501
|
isCloudOpen: () => cloudOpen,
|
|
502
|
+
isRoomCap: () => roomCap,
|
|
381
503
|
});
|
|
382
504
|
|
|
383
505
|
// The in-use watch (stage_watch.js): an ended turn hands off once the
|
|
@@ -421,9 +543,16 @@ export function createStageView({
|
|
|
421
543
|
thinkingLine = null;
|
|
422
544
|
flash = null;
|
|
423
545
|
clearTimeout(flashTimer);
|
|
546
|
+
if (roomCard) cardSight.unobserve(roomCard);
|
|
547
|
+
roomCard = null;
|
|
548
|
+
roomCap = false;
|
|
549
|
+
measured = null;
|
|
550
|
+
arrived = null;
|
|
551
|
+
pill.hidden = true;
|
|
424
552
|
setCloud(false);
|
|
425
|
-
stage.classList.remove("plain", "ended", "waiting", "held-wait", "asking", "in-use");
|
|
553
|
+
stage.classList.remove("plain", "ended", "waiting", "held-wait", "asking", "in-use", "question-room");
|
|
426
554
|
stage.style.removeProperty("--live-held");
|
|
555
|
+
live.style.minHeight = "";
|
|
427
556
|
}
|
|
428
557
|
|
|
429
558
|
// The turn leaves the stage's care (the hand-off's start, a reset, a new
|
|
@@ -546,9 +675,7 @@ export function createStageView({
|
|
|
546
675
|
refresh();
|
|
547
676
|
},
|
|
548
677
|
generationStarted(data, now = null) {
|
|
549
|
-
|
|
550
|
-
stage.classList.remove("held-wait");
|
|
551
|
-
stage.style.removeProperty("--live-held");
|
|
678
|
+
releaseHeldWait();
|
|
552
679
|
view.generationStarted(data, now);
|
|
553
680
|
},
|
|
554
681
|
chunk: (data) => view.chunk(data),
|
|
@@ -600,6 +727,11 @@ export function createStageView({
|
|
|
600
727
|
},
|
|
601
728
|
containsLive: (el) => view.containsLive(el),
|
|
602
729
|
askingText: () => view.askingText(),
|
|
730
|
+
// The asking slot pairs them on its next draw.
|
|
731
|
+
questionRefs(refs) {
|
|
732
|
+
view.questionRefs(refs);
|
|
733
|
+
scheduleRefresh();
|
|
734
|
+
},
|
|
603
735
|
// The turn model and its live phase as the status row reads it (the
|
|
604
736
|
// composer's delivery line).
|
|
605
737
|
current: () => view.current(),
|
|
@@ -637,8 +769,17 @@ export function createStageView({
|
|
|
637
769
|
else scheduleRefresh();
|
|
638
770
|
// A card that waits for the user is brought into the stage's view: in
|
|
639
771
|
// a short window the prompt and headline fill it and the card sat
|
|
640
|
-
// below, out of sight (once the stage has grown for it).
|
|
641
|
-
|
|
772
|
+
// below, out of sight (once the stage has grown for it). A question
|
|
773
|
+
// with the room reads from the reply's start instead (its pill leads
|
|
774
|
+
// to the card); its pill shows from here on.
|
|
775
|
+
if (waits) {
|
|
776
|
+
frame.afterGrowth(() => {
|
|
777
|
+
const ask = askCard();
|
|
778
|
+
frame.reveal(slot, { fromStart: !!ask && ask === roomCard });
|
|
779
|
+
arrived = ask;
|
|
780
|
+
drawPill();
|
|
781
|
+
});
|
|
782
|
+
}
|
|
642
783
|
return true;
|
|
643
784
|
},
|
|
644
785
|
// Where the live timing line goes (the status row), null: the history.
|
|
@@ -332,7 +332,11 @@ export function createStreamHandlers(deps) {
|
|
|
332
332
|
},
|
|
333
333
|
question_requested: (data) => {
|
|
334
334
|
const pq = data.pending_question;
|
|
335
|
-
if (pq
|
|
335
|
+
if (!pq || !pq.id) return;
|
|
336
|
+
// A plugin's refs on the reply that asks: on its step before the card
|
|
337
|
+
// draws the asking slot.
|
|
338
|
+
if (pq.refs) turnDom.questionRefs?.(pq.refs);
|
|
339
|
+
questionCards.renderQuestion(pq);
|
|
336
340
|
},
|
|
337
341
|
question_answered: (data) => {
|
|
338
342
|
questionCards.resolveQuestion(data.id, { answer: data.answer });
|
|
@@ -174,6 +174,26 @@ export function askingText(turn) {
|
|
|
174
174
|
return gen.text.trim();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// A plugin's refs on the reply that asks (a question's refs, from its
|
|
178
|
+
// pending question): kept on the asking step's gen, the last one, while
|
|
179
|
+
// the turn goes on, for the stage's asking slot (askingRefs) and the
|
|
180
|
+
// step's closed text (turn_view.js). A non-array or an empty list keeps
|
|
181
|
+
// what the gen has. The gen, or null.
|
|
182
|
+
export function questionRefs(turn, refs) {
|
|
183
|
+
if (!turn || turn.ended || !Array.isArray(refs) || !refs.length) return null;
|
|
184
|
+
const gen = turn.gens[turn.gens.length - 1];
|
|
185
|
+
if (!gen) return null;
|
|
186
|
+
gen.refs = refs;
|
|
187
|
+
return gen;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The refs on the asking text (questionRefs), while it shows
|
|
191
|
+
// (askingText's rule); else null.
|
|
192
|
+
export function askingRefs(turn) {
|
|
193
|
+
if (!askingText(turn)) return null;
|
|
194
|
+
return turn.gens[turn.gens.length - 1].refs || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
// The turn is over (completed, canceled, failed, the worker gone).
|
|
178
198
|
export function turnEnded(turn) {
|
|
179
199
|
const closed = closeCurrent(turn);
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// chrome (CSS :has), so a plain answer streams like today's bubble, and is
|
|
22
22
|
// removed at the end: the answer bubble takes its place.
|
|
23
23
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
24
|
-
import { setAnswerRefs } from "./answer_refs.js";
|
|
24
|
+
import { answerRefsOf, setAnswerRefs } from "./answer_refs.js";
|
|
25
25
|
import { bindRowRefs } from "./refs.js";
|
|
26
26
|
import { leavesBlock } from "./card.js";
|
|
27
27
|
import { failedTurnText, normalize, sourceLabel } from "./format.js";
|
|
@@ -70,13 +70,14 @@ function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking",
|
|
|
70
70
|
// the newest last), the live "…" until the first one, or a closed step's
|
|
71
71
|
// full text. None when there is nothing to say. A closed step's text is
|
|
72
72
|
// set whole (.textContent): its refs (ref_scan.js) split it into spans
|
|
73
|
-
// that a text part's update would leave behind.
|
|
74
|
-
|
|
73
|
+
// that a text part's update would leave behind. +at+: a reloaded step's
|
|
74
|
+
// message index, for its refs (data-answer-at, as an answer's).
|
|
75
|
+
function textTemplate(text, { live, sentences, placeholder, at = null }) {
|
|
75
76
|
if (live && sentences?.length) {
|
|
76
77
|
return html`<div class="gen-text streaming"><div class="narration">${sentences.map((s) => html`<div class="sentence">${s}</div>`)}</div></div>`;
|
|
77
78
|
}
|
|
78
79
|
if (live) return placeholder ? html`<div class="gen-text streaming">…</div>` : nothing;
|
|
79
|
-
return text ? html`<div class="gen-text" .textContent=${text}></div>` : nothing;
|
|
80
|
+
return text ? html`<div class="gen-text" data-answer-at=${at ?? nothing} .textContent=${text}></div>` : nothing;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
// One step, live or reloaded. +gen+: { text, thinking, tools, answer } (a
|
|
@@ -86,16 +87,17 @@ function textTemplate(text, { live, sentences, placeholder }) {
|
|
|
86
87
|
// answers, its +items+ (tool rows, notices, cards: nodes or templates; a
|
|
87
88
|
// reload's are its rows' nodes), the thinking +body+ node, the ticker's
|
|
88
89
|
// +line+, the narration's +sentences+ and "…" +placeholder+, +on+ (the
|
|
89
|
-
// click and toggle handlers).
|
|
90
|
+
// click and toggle handlers). A reload's +at+: the step's message index
|
|
91
|
+
// (textTemplate).
|
|
90
92
|
export function stepTemplate(gen, st = {}) {
|
|
91
93
|
const { live = false, index = 0, open = live, steers = [], items = [], body = gen.thinking, line = null,
|
|
92
|
-
sentences = null, placeholder = false, on = {} } = st;
|
|
94
|
+
sentences = null, placeholder = false, on = {}, at = null } = st;
|
|
93
95
|
const text = gen.answer ? "" : gen.text || "";
|
|
94
96
|
const label = live ? { repeats: false, content: `step ${index + 1}` } : closedLabel({ ...gen, text });
|
|
95
97
|
const failed = !live && model.failedCount(gen.tools) > 0;
|
|
96
98
|
// A step left with its thinking only reads "thinking" once.
|
|
97
99
|
const wrapped = live || !!text || gen.tools.length > 0;
|
|
98
|
-
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
100
|
+
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder, at })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
// A hook's line (hook_notice) as a step's row. data.line is a whole row of
|
|
@@ -254,6 +256,10 @@ export function createTurnView({
|
|
|
254
256
|
live: !entry.closed, index: entry.index, open: entry.open, steers: entry.steers, items: entry.items,
|
|
255
257
|
body: entry.thinkingText, line: entry.line, sentences: entry.sentences, placeholder: entry.placeholder, on: entry.on,
|
|
256
258
|
}), entry.frag, { renderBefore: entry.end });
|
|
259
|
+
// A closed step that asked a question knows its refs (model.questionRefs):
|
|
260
|
+
// the page pairs them with its text at the turn's end (linkTextRefs).
|
|
261
|
+
const text = entry.closed && entry.gen.refs ? entry.el?.querySelector(":scope > .gen-text") : null;
|
|
262
|
+
if (text && !answerRefsOf(text)) setAnswerRefs(text, entry.gen.refs);
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
// A step's nodes (lit's start marker, the <details>, the end marker), to
|
|
@@ -776,6 +782,10 @@ export function createTurnView({
|
|
|
776
782
|
// The running turn's asking reply (turn_model.js askingText): what a
|
|
777
783
|
// question or approval card keeps once answered.
|
|
778
784
|
askingText: () => model.askingText(turn),
|
|
785
|
+
// A question's refs (its pending question's) on the step that asks it.
|
|
786
|
+
questionRefs(refs) {
|
|
787
|
+
if (turn) model.questionRefs(turn, refs);
|
|
788
|
+
},
|
|
779
789
|
step(i) {
|
|
780
790
|
const entry = turn && genEls.get(turn.gens[i]);
|
|
781
791
|
return entry ? { el: entry.el, summary: entry.el.querySelector(":scope > summary"), thinkingWrap: thinkingWrap(entry) } : null;
|
|
@@ -880,6 +890,8 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
880
890
|
// The steers the step answered first, where the live rows went.
|
|
881
891
|
steers: (group.steps[k].steers || []).map((i) => steerTemplate(i, false)),
|
|
882
892
|
items: gen.tools.length ? [rowNodes(gen.tools, thumbs)] : [],
|
|
893
|
+
// Its text knows its message's refs (a question's), as an answer does.
|
|
894
|
+
at: group.steps[k].i,
|
|
883
895
|
}));
|
|
884
896
|
parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
|
|
885
897
|
outlines.push(gens);
|
|
@@ -910,8 +922,9 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
910
922
|
const out = nodesOf(parts);
|
|
911
923
|
// A finished turn's block as the live end leaves it: its outline, open.
|
|
912
924
|
[...out.children].filter((el) => el.matches(".turn-work")).forEach((block, i) => outlineBlock(block, outlines[i] || []));
|
|
913
|
-
// Each answer bubble knows its message's refs
|
|
914
|
-
// page pairs them after the render), the index
|
|
925
|
+
// Each answer bubble and step text knows its message's refs
|
|
926
|
+
// (answer_refs.js; the page pairs them after the render), the index
|
|
927
|
+
// stays out of the DOM.
|
|
915
928
|
for (const bubble of out.querySelectorAll("[data-answer-at]")) {
|
|
916
929
|
setAnswerRefs(bubble, items[Number(bubble.dataset.answerAt)]?.display_refs);
|
|
917
930
|
bubble.removeAttribute("data-answer-at");
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -237,6 +237,7 @@ module Samagotchi
|
|
|
237
237
|
@config_restart_unsubscribe = ConfigFile.watch_restart_settings!(process: :worker) do |message|
|
|
238
238
|
@engine.announce(type: :hook_notice, hook: "config", text: message, level: :warn, between_turns: true)
|
|
239
239
|
end
|
|
240
|
+
prune_tombstones
|
|
240
241
|
settle_interrupted_turn
|
|
241
242
|
# Before the Bridge serves anything: a UI joining a resumed worker's
|
|
242
243
|
# stream gets the session's history and status in its snapshot, not
|
|
@@ -491,6 +492,15 @@ module Samagotchi
|
|
|
491
492
|
save_or_log(:interrupted_turn) { save_session }
|
|
492
493
|
end
|
|
493
494
|
|
|
495
|
+
# The per-input tombstones (removed, settled) older than
|
|
496
|
+
# SessionInbox::TOMBSTONE_MAX_AGE, once per worker start: they only go
|
|
497
|
+
# with the session dir otherwise.
|
|
498
|
+
def prune_tombstones
|
|
499
|
+
removed = SessionInbox.prune_removed(@session_dir)
|
|
500
|
+
settled = TurnCheckpoint.prune_settled(@session_dir)
|
|
501
|
+
Log.info(:worker, "tombstones_pruned", removed: removed, settled: settled) if (removed + settled).positive?
|
|
502
|
+
end
|
|
503
|
+
|
|
494
504
|
# A saved parent_answers that isn't a level reads as hard on every
|
|
495
505
|
# answer without a word; said once here, as the session loads.
|
|
496
506
|
def announce_session_warnings
|
|
@@ -958,6 +968,8 @@ module Samagotchi
|
|
|
958
968
|
event = { type: :command_ran, **command.event_fields, status: status, output: text[0, COMMAND_OUTPUT_LIMIT],
|
|
959
969
|
changed: changed.map(&:to_s), model_name: @engine.effective_model_name }
|
|
960
970
|
event[:anytime] = true if anytime
|
|
971
|
+
# The session's title after /title (nil: cleared), for the TUIs' row.
|
|
972
|
+
event[:title] = @engine.session&.title if changed.include?(:title)
|
|
961
973
|
# Its line was shown at its command_queued (waits: turn_end).
|
|
962
974
|
event[:queued] = true if command.waits_for_turn_end?
|
|
963
975
|
# The chips the command moved (SessionSetting#changed_by): what the next
|
|
@@ -1259,12 +1271,14 @@ module Samagotchi
|
|
|
1259
1271
|
# The Bridge serves before the idle-exit policy exists.
|
|
1260
1272
|
return WorkerIdleExit::Hold.new(reason: :starting) unless @idle_exit
|
|
1261
1273
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1274
|
+
if restart
|
|
1275
|
+
hold = @idle_exit.hold_for_restart
|
|
1276
|
+
return WorkerIdleExit::Hold.new(reason: hold) if hold
|
|
1277
|
+
else
|
|
1278
|
+
# /exit names the tasks it waits for (D3), from the one look that found them.
|
|
1279
|
+
hold = @idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT)
|
|
1280
|
+
return hold if hold
|
|
1266
1281
|
end
|
|
1267
|
-
return WorkerIdleExit::Hold.new(reason: hold) if hold
|
|
1268
1282
|
|
|
1269
1283
|
@exit_requested = true
|
|
1270
1284
|
@exit_requested_by = client_id
|
|
@@ -94,15 +94,23 @@ module Samagotchi
|
|
|
94
94
|
# still be open, untagged)
|
|
95
95
|
# @return [Symbol, nil] :turn_running, :input_queued, :continue_offered,
|
|
96
96
|
# :client_connected, :reminders or :background_tasks
|
|
97
|
-
def hold_for_request(requester:, streams: true)
|
|
98
|
-
return :turn_running if @engine.turn_running?
|
|
99
|
-
return :input_queued if @input_pending.call
|
|
100
|
-
return :continue_offered if @awaiting_continue.call
|
|
101
|
-
return :client_connected if streams && @bridge && @bridge.open_streams_except(requester).positive?
|
|
102
|
-
return :reminders if @engine.reminder_store&.any?
|
|
103
|
-
return :background_tasks if @watched_running.call.any?
|
|
97
|
+
def hold_for_request(requester:, streams: true) = request_hold(requester: requester, streams: streams)&.reason
|
|
104
98
|
|
|
105
|
-
|
|
99
|
+
# #hold_for_request as a Hold: for :background_tasks, the watched tasks
|
|
100
|
+
# still running, read once (TaskWatch#running looks at each task's
|
|
101
|
+
# process), with +hint+.
|
|
102
|
+
# @return [Hold, nil]
|
|
103
|
+
def request_hold(requester:, streams: true, hint: nil)
|
|
104
|
+
reason = if @engine.turn_running? then :turn_running
|
|
105
|
+
elsif @input_pending.call then :input_queued
|
|
106
|
+
elsif @awaiting_continue.call then :continue_offered
|
|
107
|
+
elsif streams && @bridge && @bridge.open_streams_except(requester).positive? then :client_connected
|
|
108
|
+
elsif @engine.reminder_store&.any? then :reminders
|
|
109
|
+
end
|
|
110
|
+
return Hold.new(reason: reason) if reason
|
|
111
|
+
|
|
112
|
+
running = @watched_running.call
|
|
113
|
+
running.any? ? Hold.new(reason: :background_tasks, tasks: running, hint: hint) : nil
|
|
106
114
|
end
|
|
107
115
|
|
|
108
116
|
# What keeps the worker from restarting now (a client asked: POST /exit
|
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.98.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -437,6 +437,7 @@ files:
|
|
|
437
437
|
- lib/samagotchi/memory_bundle/system_bundle.rb
|
|
438
438
|
- lib/samagotchi/memory_bundle/trash.rb
|
|
439
439
|
- lib/samagotchi/memory_bundle/uninstaller.rb
|
|
440
|
+
- lib/samagotchi/memory_command.rb
|
|
440
441
|
- lib/samagotchi/memory_paths.rb
|
|
441
442
|
- lib/samagotchi/message_parts.rb
|
|
442
443
|
- lib/samagotchi/model_catalog.rb
|
|
@@ -484,6 +485,7 @@ files:
|
|
|
484
485
|
- lib/samagotchi/prompt_note.rb
|
|
485
486
|
- lib/samagotchi/prompt_warmup.rb
|
|
486
487
|
- lib/samagotchi/question_answer.rb
|
|
488
|
+
- lib/samagotchi/question_asked.rb
|
|
487
489
|
- lib/samagotchi/question_desk.rb
|
|
488
490
|
- lib/samagotchi/queued_command.rb
|
|
489
491
|
- lib/samagotchi/reasoning_history.rb
|