samagotchi 0.2.0 → 0.3.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 +116 -1
- data/README.md +40 -4
- data/bin/chi +86 -19
- data/docs/cli.md +108 -5
- data/docs/configuration.md +229 -45
- data/docs/desktop.md +6 -0
- data/docs/hooks.md +126 -5
- data/docs/plugins.md +68 -2
- data/docs/releasing.md +18 -8
- data/docs/sessions.md +30 -4
- data/lib/samagotchi/answer_display.rb +95 -0
- data/lib/samagotchi/archive_store.rb +90 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
- data/lib/samagotchi/bootstrap/probe.rb +262 -0
- data/lib/samagotchi/bootstrap_command.rb +347 -0
- data/lib/samagotchi/bridge/pending_card.rb +89 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +14 -3
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +6 -2
- data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
- data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +358 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
- data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/client.rb +72 -13
- data/lib/samagotchi/config.rb +196 -36
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +13 -7
- data/lib/samagotchi/desktop/macos/Panel.swift +71 -19
- data/lib/samagotchi/empty_answer_retry.rb +43 -0
- data/lib/samagotchi/engine.rb +233 -36
- data/lib/samagotchi/guardrails/approval.rb +9 -0
- data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +24 -5
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/idle_recap.rb +5 -1
- data/lib/samagotchi/kernel_loop.rb +47 -15
- data/lib/samagotchi/llm/chat_loop.rb +59 -20
- data/lib/samagotchi/llm/errors.rb +21 -3
- data/lib/samagotchi/llm/http.rb +42 -13
- data/lib/samagotchi/llm/openai_chat.rb +12 -4
- data/lib/samagotchi/log_subscriber.rb +18 -3
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/reply_wait.rb +126 -0
- data/lib/samagotchi/sampling_settings.rb +58 -0
- data/lib/samagotchi/self_report.rb +1 -0
- data/lib/samagotchi/send_command.rb +153 -7
- data/lib/samagotchi/session.rb +52 -11
- data/lib/samagotchi/session_archive_command.rb +107 -0
- data/lib/samagotchi/session_commands.rb +11 -2
- data/lib/samagotchi/session_manager.rb +114 -9
- data/lib/samagotchi/session_metrics.rb +222 -106
- data/lib/samagotchi/steer.rb +72 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +39 -6
- data/lib/samagotchi/terminal_ui/event_renderer.rb +13 -8
- data/lib/samagotchi/terminal_ui/formatting.rb +31 -8
- data/lib/samagotchi/terminal_ui/input_support.rb +3 -0
- data/lib/samagotchi/terminal_ui.rb +77 -4
- data/lib/samagotchi/tool_activity.rb +3 -1
- data/lib/samagotchi/tools/builtins.rb +15 -4
- data/lib/samagotchi/tools/delegate_wait.rb +26 -69
- data/lib/samagotchi/tools/execute.rb +52 -14
- data/lib/samagotchi/tools/task_runtime.rb +19 -0
- data/lib/samagotchi/tools/task_wait.rb +27 -3
- data/lib/samagotchi/turn_note.rb +60 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -6
- data/lib/samagotchi/web/app.rb +88 -4
- data/lib/samagotchi/web/public/activity.js +10 -1
- data/lib/samagotchi/web/public/annotate_presets.js +26 -0
- data/lib/samagotchi/web/public/annotations.js +13 -0
- data/lib/samagotchi/web/public/app.js +437 -88
- data/lib/samagotchi/web/public/card.js +5 -3
- data/lib/samagotchi/web/public/chat_view.js +10 -1
- data/lib/samagotchi/web/public/copy.js +20 -4
- data/lib/samagotchi/web/public/ctx.js +15 -0
- data/lib/samagotchi/web/public/data.js +21 -6
- data/lib/samagotchi/web/public/format.js +9 -0
- data/lib/samagotchi/web/public/index.html +38 -2
- data/lib/samagotchi/web/public/notify.js +175 -0
- data/lib/samagotchi/web/public/question_card.js +2 -1
- data/lib/samagotchi/web/public/sessions_list.js +7 -0
- data/lib/samagotchi/web/public/timing.js +39 -14
- data/lib/samagotchi/web/public/turn_events.js +46 -0
- data/lib/samagotchi/web/public/turn_view.js +47 -7
- data/lib/samagotchi/web/server.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +24 -1
- data/lib/samagotchi/worker.rb +11 -0
- metadata +20 -1
|
@@ -51,11 +51,29 @@ export function hookNoticeLabel(hook) {
|
|
|
51
51
|
return m ? m[1] : "hook";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// The row a step gets when the loop asks again after an empty answer.
|
|
55
|
+
export function emptyRetryLine({ attempt, of } = {}) {
|
|
56
|
+
return `↻ empty answer, asking again (${attempt}/${of})`;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
// A composer line for the worker's command route rather than a prompt.
|
|
55
60
|
export function isCommandLine(text) {
|
|
56
61
|
return /^[/!]/.test(String(text || "").trim());
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
// The terminal's own commands that have a button in the web: the reply
|
|
65
|
+
// the page shows itself (they never go to the worker, which doesn't know
|
|
66
|
+
// them), or null.
|
|
67
|
+
const WEB_LOCAL_REPLIES = {
|
|
68
|
+
"/archive": "/archive: use the archive button in the session bar",
|
|
69
|
+
"/exit": "/exit: close the tab to leave; stop in the session bar stops the worker",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function webLocalReply(text) {
|
|
73
|
+
const name = String(text || "").trim().split(/\s+/)[0].toLowerCase();
|
|
74
|
+
return WEB_LOCAL_REPLIES[name] || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
// The continue card's answers, as the terminal's continue prompt reads them.
|
|
60
78
|
export function continueLine(choice, reason = "") {
|
|
61
79
|
const why = String(reason || "").trim();
|
|
@@ -80,6 +98,8 @@ export function commandView(event, myId) {
|
|
|
80
98
|
modelName: event.model_name || null,
|
|
81
99
|
anytime: event.anytime === true,
|
|
82
100
|
commandId: event.command_id || null,
|
|
101
|
+
// A card's action (Nudge…): no bubble unless it says something back.
|
|
102
|
+
hidden: event.card === true && !event.output && event.status !== "error",
|
|
83
103
|
};
|
|
84
104
|
}
|
|
85
105
|
|
|
@@ -100,6 +120,27 @@ export function restoreAction(event, { myId, sentIds, composerEmpty }) {
|
|
|
100
120
|
}, sent && composerEmpty ? event.images : null);
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
// A turn that fails fast can have its prompt_restored come over the stream
|
|
124
|
+
// before the /turn ack names its enqueued_id: `early` (a Map) keeps such a
|
|
125
|
+
// restore of this tab's own prompt by that id, and the ack takes it back
|
|
126
|
+
// (restoreOnAck), so the prompt goes into the composer the ack would clear.
|
|
127
|
+
export function keepEarlyRestore(event, early, { myId, sentIds }) {
|
|
128
|
+
const origin = event?.origin || {};
|
|
129
|
+
const id = origin.enqueued_id;
|
|
130
|
+
if (!isOwn(origin.client_id, myId) || !id || sentIds.has(id)) return false;
|
|
131
|
+
early.set(id, event);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The ack's enqueued_id (already in sentIds) was restored early: what the
|
|
136
|
+
// composer gets back (as restoreAction for an empty composer), else null.
|
|
137
|
+
export function restoreOnAck(enqueuedId, early, { myId, sentIds }) {
|
|
138
|
+
const event = enqueuedId && early.get(enqueuedId);
|
|
139
|
+
if (!event) return null;
|
|
140
|
+
early.delete(enqueuedId);
|
|
141
|
+
return restoreAction(event, { myId, sentIds, composerEmpty: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
// What an event does to the prompt bubbles, as a list of ops for app.js:
|
|
104
145
|
// add — a bubble for a prompt this tab hasn't shown (state queued /
|
|
105
146
|
// started / steered, labelled by its sender)
|
|
@@ -191,6 +232,11 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
191
232
|
closeText();
|
|
192
233
|
events.push({ type: "hook_notice", hook: part.hook, text: part.text, level: part.level });
|
|
193
234
|
break;
|
|
235
|
+
// A plugin's steer: a steer-only merge, as live.
|
|
236
|
+
case "steer":
|
|
237
|
+
closeText();
|
|
238
|
+
events.push({ type: "pending_input_merged", count: 0, content: null, steers: [{ source: part.source, text: part.text }] });
|
|
239
|
+
break;
|
|
194
240
|
default:
|
|
195
241
|
break;
|
|
196
242
|
}
|
|
@@ -13,7 +13,7 @@ import { toolName } from "./activity.js";
|
|
|
13
13
|
import { leavesBlock } from "./card.js";
|
|
14
14
|
import { activityStatusClass, activityStatusLabel, fillActivityRow } from "./chat_view.js";
|
|
15
15
|
import { copySourceAttr } from "./copy.js";
|
|
16
|
-
import { escapeHtml, messageBodyHtml, noteHtml, normalize } from "./format.js";
|
|
16
|
+
import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
|
|
17
17
|
import { shouldFollowScroll } from "./scroll.js";
|
|
18
18
|
import { hookNoticeLabel } from "./turn_events.js";
|
|
19
19
|
import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
|
|
@@ -29,6 +29,8 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
29
29
|
let blockToggled = false;
|
|
30
30
|
// gen → { el, summary, thinking, text, activity, rows: Map(key → el), toggled }
|
|
31
31
|
const genEls = new Map();
|
|
32
|
+
// Steer rows waiting for the step that answers them (steer()).
|
|
33
|
+
let pendingSteers = [];
|
|
32
34
|
const dirty = new Set();
|
|
33
35
|
let rafPending = false;
|
|
34
36
|
// The promoted answer bubble while it is held at the box's height (the
|
|
@@ -69,6 +71,9 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
69
71
|
const summary = document.createElement("summary");
|
|
70
72
|
summary.textContent = `step ${turn.gens.length}`;
|
|
71
73
|
el.appendChild(summary);
|
|
74
|
+
// A plugin's steers merged since the last step: this step answers them.
|
|
75
|
+
for (const row of pendingSteers) el.appendChild(row);
|
|
76
|
+
pendingSteers = [];
|
|
72
77
|
const text = document.createElement("div");
|
|
73
78
|
text.className = "gen-text streaming";
|
|
74
79
|
// A step after the first shows "…" until its text comes (the first one
|
|
@@ -317,7 +322,8 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
317
322
|
|
|
318
323
|
// A hook's line (hook_notice) as a row of the current step, above the row
|
|
319
324
|
// of the call it judged; false with no running turn (app.js shows the
|
|
320
|
-
// bubble then).
|
|
325
|
+
// bubble then). data.line is a whole row of chi's own (an empty-answer
|
|
326
|
+
// retry), shown as is.
|
|
321
327
|
function notice(data) {
|
|
322
328
|
const r = turn && model.notice(turn, data);
|
|
323
329
|
if (!r) return false;
|
|
@@ -325,13 +331,34 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
325
331
|
if (!r.gen.text) entry.text.textContent = "";
|
|
326
332
|
const el = document.createElement("div");
|
|
327
333
|
el.className = "hook-notice" + (data.level === "warn" ? " warn" : "");
|
|
328
|
-
el.textContent = `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
|
|
334
|
+
el.textContent = data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
|
|
329
335
|
const follow = isNearBottom();
|
|
330
336
|
activityEl(r.gen).appendChild(el);
|
|
331
337
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
332
338
|
return true;
|
|
333
339
|
}
|
|
334
340
|
|
|
341
|
+
// A plugin's steer (pending_input_merged's steers) as the first row of
|
|
342
|
+
// the step that answers it: it joined the turn after the current step's
|
|
343
|
+
// tools, and the next generation (a new, open step) reads it; the
|
|
344
|
+
// current step collapses when that one starts. Held until then (a turn
|
|
345
|
+
// that ends first puts it in its last step). False with no running turn.
|
|
346
|
+
function steer(data) {
|
|
347
|
+
if (!turn || turn.ended) return false;
|
|
348
|
+
const holder = document.createElement("div");
|
|
349
|
+
holder.innerHTML = steerRowHtml(data);
|
|
350
|
+
pendingSteers.push(holder.firstChild);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Held steer rows into the last step (the turn ends without another).
|
|
355
|
+
function flushSteers() {
|
|
356
|
+
const gen = turn.gens[turn.gens.length - 1];
|
|
357
|
+
const entry = gen && genEls.get(gen);
|
|
358
|
+
if (entry) for (const row of pendingSteers) entry.el.insertBefore(row, entry.thinkingWrap || entry.text);
|
|
359
|
+
pendingSteers = [];
|
|
360
|
+
}
|
|
361
|
+
|
|
335
362
|
// A card shown during the turn (a plugin's, app.js draws it) as a row of
|
|
336
363
|
// the current step, like a notice; false with no running turn (app.js
|
|
337
364
|
// puts it between turns then).
|
|
@@ -376,8 +403,8 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
376
403
|
// between turns.
|
|
377
404
|
const leaving = !gen.thinking && gen.tools.length === 0;
|
|
378
405
|
const plain = turn.gens.length === 1 && gen.tools.length === 0;
|
|
379
|
-
const cards = leaving || plain ? [...entry.el.querySelectorAll(".plugin-card, .hook-notice")] : [];
|
|
380
|
-
cards.forEach((el) => { if (el.classList.contains("
|
|
406
|
+
const cards = leaving || plain ? [...entry.el.querySelectorAll(".plugin-card, .hook-notice, .steer-row")] : [];
|
|
407
|
+
cards.forEach((el) => { if (!el.classList.contains("plugin-card")) el.classList.add("bubble"); });
|
|
381
408
|
if (leaving) entry.el.remove();
|
|
382
409
|
if (plain) {
|
|
383
410
|
// A plain answer: no block, the bubble where it was (as the chat
|
|
@@ -425,6 +452,7 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
425
452
|
// one shows at once.
|
|
426
453
|
const box = promoting && kind === "completed" ? genEls.get(last)?.box : null;
|
|
427
454
|
const heldHeight = box ? box.getBoundingClientRect().height : 0;
|
|
455
|
+
if (pendingSteers.length) flushSteers();
|
|
428
456
|
apply(model.turnEnded(turn));
|
|
429
457
|
if (promoting) promote(last, { heldHeight });
|
|
430
458
|
const kept = [];
|
|
@@ -453,6 +481,7 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
453
481
|
if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
|
|
454
482
|
}
|
|
455
483
|
genEls.clear();
|
|
484
|
+
pendingSteers = [];
|
|
456
485
|
dirty.clear();
|
|
457
486
|
rafPending = false;
|
|
458
487
|
}
|
|
@@ -499,6 +528,7 @@ export function createTurnView({ historyEl, appendToHistory, isNearBottom, sawTe
|
|
|
499
528
|
},
|
|
500
529
|
turnEnded,
|
|
501
530
|
notice,
|
|
531
|
+
steer,
|
|
502
532
|
card,
|
|
503
533
|
flush,
|
|
504
534
|
// The rendered answer landed (or the re-read failed): let the held
|
|
@@ -554,28 +584,38 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
554
584
|
const { body, renderedMarkdown } = messageBodyHtml(m);
|
|
555
585
|
return `<div class="bubble output${renderedMarkdown ? " markdown" : ""}"${copySourceAttr(m, renderedMarkdown)}>${body}</div>${timing}`;
|
|
556
586
|
};
|
|
587
|
+
const steerHtml = (i, bubble) => steerRowHtml({ source: items[i].source, text: items[i].content }, { bubble });
|
|
557
588
|
return turnGroups(items, timing).map((group) => {
|
|
558
589
|
if (group.kind === "note") return `<div class="bubble note">${noteHtml(items[group.i])}</div>`;
|
|
590
|
+
if (group.kind === "steer") return steerHtml(group.i, true);
|
|
559
591
|
if (group.kind === "answer") return answerHtml(items[group.answer], "");
|
|
560
592
|
const prompt = items[group.user];
|
|
561
593
|
const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}><div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
|
|
562
594
|
const [only] = group.steps;
|
|
595
|
+
const noBlock = !group.steps.length ||
|
|
596
|
+
(group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking);
|
|
563
597
|
if (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking) {
|
|
564
598
|
// A plain answer with thinking: the chat view's collapsed block, as live.
|
|
565
599
|
parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
|
|
566
600
|
} else if (group.steps.length) {
|
|
567
601
|
const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
|
|
568
|
-
const rows = gens.map((gen) => {
|
|
602
|
+
const rows = gens.map((gen, k) => {
|
|
569
603
|
// A thinking-only step: the body directly under the step's own
|
|
570
604
|
// "thinking" summary (no wrap that would repeat it), as live.
|
|
571
605
|
const body = gen.thinking ? `<div class="thinking-body">${escapeHtml(gen.thinking)}</div>` : "";
|
|
572
606
|
const thinking = body && (gen.text || gen.tools.length) ? `<details class="thinking"><summary>thinking</summary>${body}</details>` : body;
|
|
573
607
|
const text = gen.text ? `<div class="gen-text">${escapeHtml(gen.text)}</div>` : "";
|
|
608
|
+
// The steers the step answered first, where the live rows went.
|
|
609
|
+
const steers = (group.steps[k].steers || []).map((i) => steerHtml(i, false)).join("");
|
|
574
610
|
const tools = gen.tools.length ? `<div class="activity-body">${gen.tools.map((r) => reloadRowHtml(r, thumbs)).join("")}</div>` : "";
|
|
575
|
-
return `<details class="gen"><summary>${escapeHtml(model.genLabel(gen))}</summary>${thinking}${text}${tools}</details>`;
|
|
611
|
+
return `<details class="gen"><summary>${escapeHtml(model.genLabel(gen))}</summary>${steers}${thinking}${text}${tools}</details>`;
|
|
576
612
|
});
|
|
577
613
|
parts.push(`<details class="turn-work done"><summary>${escapeHtml(model.blockSummary({ gens, ended: true }))}</summary>${rows.join("")}</details>`);
|
|
578
614
|
}
|
|
615
|
+
// Steers with no step to sit in, or in a turn that shows no block: on
|
|
616
|
+
// their own after the prompt.
|
|
617
|
+
const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
|
|
618
|
+
parts.push(loose.map((i) => steerHtml(i, true)).join(""));
|
|
579
619
|
// A turn with no answer (canceled, or running: no record yet) shows its
|
|
580
620
|
// timing after its prompt and steps, where the live view leaves it.
|
|
581
621
|
if (group.answer === null) parts.push(timingHtml(group));
|
|
@@ -9,6 +9,7 @@ require "webrick"
|
|
|
9
9
|
|
|
10
10
|
require_relative "app"
|
|
11
11
|
require_relative "session_hub"
|
|
12
|
+
require_relative "../config"
|
|
12
13
|
require_relative "../project_scope"
|
|
13
14
|
require_relative "../log"
|
|
14
15
|
require_relative "../version"
|
|
@@ -51,7 +52,8 @@ module Samagotchi
|
|
|
51
52
|
# open) its page for +dir+ and leave it be; else start one.
|
|
52
53
|
# @param scope ["project", "all"] "all": the plain page, every session
|
|
53
54
|
# @return [Integer] the exit status
|
|
54
|
-
def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false, turn_view: true
|
|
55
|
+
def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false, turn_view: true,
|
|
56
|
+
annotate_presets: Config::BY_KEY["web.annotate_presets"].default)
|
|
55
57
|
host = resolve_host(host)
|
|
56
58
|
port = resolve_port(port)
|
|
57
59
|
url = scope_url(host, port, dir: dir, scope: scope)
|
|
@@ -65,7 +67,8 @@ module Samagotchi
|
|
|
65
67
|
warn in_use_message(port)
|
|
66
68
|
1
|
|
67
69
|
else
|
|
68
|
-
start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, turn_view: turn_view
|
|
70
|
+
start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, turn_view: turn_view,
|
|
71
|
+
annotate_presets: annotate_presets) ? 0 : 1
|
|
69
72
|
end
|
|
70
73
|
rescue Interrupt
|
|
71
74
|
# Ctrl-C: the server has stopped (start's ensure); one line, and the
|
|
@@ -78,13 +81,14 @@ module Samagotchi
|
|
|
78
81
|
# from; built over the app's state dir unless given
|
|
79
82
|
# @return [Boolean] false when the port was taken (said so on stderr)
|
|
80
83
|
def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
|
|
81
|
-
turn_view: true, hub: nil)
|
|
84
|
+
turn_view: true, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil)
|
|
82
85
|
port = resolve_port(port)
|
|
83
86
|
host = resolve_host(host)
|
|
84
87
|
url ||= "http://#{url_host(host)}:#{port}/"
|
|
85
88
|
|
|
86
89
|
hub ||= SessionHub.new(state_dir: state_dir || Session.default_state_dir, manager: manager || SessionManager)
|
|
87
|
-
app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, turn_view: turn_view,
|
|
90
|
+
app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, turn_view: turn_view,
|
|
91
|
+
annotate_presets: annotate_presets, hub: hub)
|
|
88
92
|
Samagotchi::Log.info(:web, "start", url: "http://#{host}:#{port}", version: Samagotchi::VERSION)
|
|
89
93
|
hub.start
|
|
90
94
|
# Said once the port is bound: a second chi web racing for it gets
|
|
@@ -244,7 +244,8 @@ module Samagotchi
|
|
|
244
244
|
if stamp != entry.stamp
|
|
245
245
|
session = Session.summary_from_file(path)
|
|
246
246
|
# Corrupt, or missing a field: not a session, as Session.list has it.
|
|
247
|
-
|
|
247
|
+
# A `chi scratch` session is never shown.
|
|
248
|
+
return drop(id) if session.nil? || session.scratch
|
|
248
249
|
|
|
249
250
|
# Looked up once per parse (a git call for a session saved before
|
|
250
251
|
# the field), never per tick.
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../session"
|
|
4
4
|
require_relative "../recap_store"
|
|
5
|
+
require_relative "../archive_store"
|
|
6
|
+
require_relative "../session_metrics"
|
|
5
7
|
require_relative "../bridge_client"
|
|
8
|
+
require_relative "../bridge/pending_card"
|
|
6
9
|
|
|
7
10
|
module Samagotchi
|
|
8
11
|
module Web
|
|
@@ -25,6 +28,7 @@ module Samagotchi
|
|
|
25
28
|
# @return [Hash] symbol keys
|
|
26
29
|
def build(session, owner:, session_dir:, status: displayed_status(session, owner: owner), root_cache: nil)
|
|
27
30
|
used = session.respond_to?(:used_memory_names) ? Array(session.used_memory_names) : []
|
|
31
|
+
up = bridge_up?(session_dir, owner)
|
|
28
32
|
{
|
|
29
33
|
id: session.id,
|
|
30
34
|
status: status,
|
|
@@ -46,13 +50,32 @@ module Samagotchi
|
|
|
46
50
|
owner: owner&.fetch("kind", nil),
|
|
47
51
|
# The saved recap's first sentence, for the session card.
|
|
48
52
|
recap: RecapStore.preview(session_dir),
|
|
53
|
+
# How full the context was after the last turn (%), or nil.
|
|
54
|
+
ctx_pct: SessionMetrics.saved_context_pct(session_dir)&.round(1),
|
|
55
|
+
# Hidden from the strip and the list unless "include archived".
|
|
56
|
+
archived: ArchiveStore.archived?(session_dir),
|
|
49
57
|
project_root: session.project_root(cache: root_cache),
|
|
50
58
|
# A worker is reachable: its Bridge sidecar is there and the
|
|
51
59
|
# owner lock is held. A sidecar a dead worker left is not up.
|
|
52
|
-
bridge_up:
|
|
60
|
+
bridge_up: up,
|
|
61
|
+
# For the tab's notifications (notify.js): an open question, the
|
|
62
|
+
# running turn's open card with actions, and how the last turn
|
|
63
|
+
# ended.
|
|
64
|
+
pending_question: pending_question_for(session),
|
|
65
|
+
pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
|
|
66
|
+
last_turn: session.respond_to?(:last_turn) ? session.last_turn : nil
|
|
53
67
|
}
|
|
54
68
|
end
|
|
55
69
|
|
|
70
|
+
# The open question as {id:, kind:} ("approval" for a guardrail's,
|
|
71
|
+
# else "question"); nil when there is none. The text stays out.
|
|
72
|
+
def pending_question_for(session)
|
|
73
|
+
pending = session.respond_to?(:pending_question) ? session.pending_question : nil
|
|
74
|
+
return nil unless pending.is_a?(Hash) && pending[:id]
|
|
75
|
+
|
|
76
|
+
{ id: pending[:id], kind: pending[:kind].to_s == "approval" ? "approval" : "question" }
|
|
77
|
+
end
|
|
78
|
+
|
|
56
79
|
# status is turn state (idle/running). The live worker's snapshot is the
|
|
57
80
|
# truth; on disk, a "running" with no live owner was left by a worker
|
|
58
81
|
# that died mid-turn.
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "turn_note"
|
|
|
7
7
|
require_relative "context_note"
|
|
8
8
|
require_relative "worker_idle_exit"
|
|
9
9
|
require_relative "session_manager"
|
|
10
|
+
require_relative "archive_store"
|
|
10
11
|
require_relative "log"
|
|
11
12
|
require_relative "turn_flow"
|
|
12
13
|
require_relative "session_commands"
|
|
@@ -286,6 +287,7 @@ module Samagotchi
|
|
|
286
287
|
# list); the Engine resets it to idle when it ends.
|
|
287
288
|
@session.status = Session::STATUS_RUNNING
|
|
288
289
|
@session.save(state_dir: @state_dir)
|
|
290
|
+
user_input(origin&.dig(:client_id))
|
|
289
291
|
drop_continue_offer(origin)
|
|
290
292
|
@turn_flow.before_prompt_turn
|
|
291
293
|
@merged_this_turn = []
|
|
@@ -381,6 +383,7 @@ module Samagotchi
|
|
|
381
383
|
shown.each { |event| @engine.announce(event) }
|
|
382
384
|
end
|
|
383
385
|
@session.save(state_dir: @state_dir) unless Array(result.changed).empty? || stopped_on_disk?
|
|
386
|
+
user_input(command[:client_id]) if resolved
|
|
384
387
|
# An offer answered without a turn ("no") is activity: the recap
|
|
385
388
|
# written at the offer (it says the turn stopped) gets rewritten.
|
|
386
389
|
@engine.record_activity if resolved && !result.resume
|
|
@@ -432,6 +435,7 @@ module Samagotchi
|
|
|
432
435
|
status: status, output: text[0, COMMAND_OUTPUT_LIMIT], changed: changed.map(&:to_s),
|
|
433
436
|
model_name: @engine.effective_model_name }
|
|
434
437
|
event[:anytime] = true if anytime
|
|
438
|
+
event[:card] = true if command[:card]
|
|
435
439
|
event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
|
|
436
440
|
@engine.announce(event)
|
|
437
441
|
end
|
|
@@ -536,6 +540,7 @@ module Samagotchi
|
|
|
536
540
|
FileUtils.rm_f(claimed_file)
|
|
537
541
|
end
|
|
538
542
|
end
|
|
543
|
+
merged.each { |_prompt, origin| user_input(origin&.dig(:client_id)) }
|
|
539
544
|
unless merged.empty?
|
|
540
545
|
@engine.announce(type: :input_merged, count: merged.size, origins: merged.filter_map(&:last))
|
|
541
546
|
@merged_this_turn.concat(merged)
|
|
@@ -628,6 +633,12 @@ module Samagotchi
|
|
|
628
633
|
Log.info(:worker, "idle_exit", idle_s: @idle_exit.idle_seconds.round)
|
|
629
634
|
end
|
|
630
635
|
|
|
636
|
+
# A human's input (ArchiveStore.user_input?) un-archives the session;
|
|
637
|
+
# a delegate's, a plugin's or a reminder's doesn't.
|
|
638
|
+
def user_input(client_id)
|
|
639
|
+
ArchiveStore.user_input(@session_id, state_dir: @state_dir) if ArchiveStore.user_input?(client_id)
|
|
640
|
+
end
|
|
641
|
+
|
|
631
642
|
def stopped_on_disk?
|
|
632
643
|
SessionManager.stopped_on_disk?(@session_id, state_dir: @state_dir)
|
|
633
644
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -123,10 +123,16 @@ files:
|
|
|
123
123
|
- docs/releasing.md
|
|
124
124
|
- docs/sessions.md
|
|
125
125
|
- lib/samagotchi.rb
|
|
126
|
+
- lib/samagotchi/answer_display.rb
|
|
127
|
+
- lib/samagotchi/archive_store.rb
|
|
128
|
+
- lib/samagotchi/bootstrap/config_writer.rb
|
|
129
|
+
- lib/samagotchi/bootstrap/probe.rb
|
|
130
|
+
- lib/samagotchi/bootstrap_command.rb
|
|
126
131
|
- lib/samagotchi/bridge.rb
|
|
127
132
|
- lib/samagotchi/bridge/bounded_queue.rb
|
|
128
133
|
- lib/samagotchi/bridge/card_store.rb
|
|
129
134
|
- lib/samagotchi/bridge/event_id.rb
|
|
135
|
+
- lib/samagotchi/bridge/pending_card.rb
|
|
130
136
|
- lib/samagotchi/bridge/ring_buffer.rb
|
|
131
137
|
- lib/samagotchi/bridge/sse_writer.rb
|
|
132
138
|
- lib/samagotchi/bridge/turn_accumulator.rb
|
|
@@ -136,6 +142,8 @@ files:
|
|
|
136
142
|
- lib/samagotchi/bundle_needs.rb
|
|
137
143
|
- lib/samagotchi/bundles/btw/manifest.yml
|
|
138
144
|
- lib/samagotchi/bundles/btw/plugin.rb
|
|
145
|
+
- lib/samagotchi/bundles/check-in/manifest.yml
|
|
146
|
+
- lib/samagotchi/bundles/check-in/plugin.rb
|
|
139
147
|
- lib/samagotchi/bundles/guardrails/guardrails.md
|
|
140
148
|
- lib/samagotchi/bundles/guardrails/guardrails/rules.yml
|
|
141
149
|
- lib/samagotchi/bundles/guardrails/manifest.yml
|
|
@@ -146,6 +154,9 @@ files:
|
|
|
146
154
|
- lib/samagotchi/bundles/loop-guard/plugin.rb
|
|
147
155
|
- lib/samagotchi/bundles/mcp/manifest.yml
|
|
148
156
|
- lib/samagotchi/bundles/mcp/plugin.rb
|
|
157
|
+
- lib/samagotchi/bundles/source-links/hooks/source_links.rb
|
|
158
|
+
- lib/samagotchi/bundles/source-links/manifest.yml
|
|
159
|
+
- lib/samagotchi/bundles/source-links/source_links.md
|
|
149
160
|
- lib/samagotchi/bundles/system/config_modification_protocol.md
|
|
150
161
|
- lib/samagotchi/bundles/system/delegated.md
|
|
151
162
|
- lib/samagotchi/bundles/system/identity.md
|
|
@@ -169,6 +180,7 @@ files:
|
|
|
169
180
|
- lib/samagotchi/desktop/macos/Info.plist.erb
|
|
170
181
|
- lib/samagotchi/desktop/macos/Panel.swift
|
|
171
182
|
- lib/samagotchi/desktop_command.rb
|
|
183
|
+
- lib/samagotchi/empty_answer_retry.rb
|
|
172
184
|
- lib/samagotchi/engine.rb
|
|
173
185
|
- lib/samagotchi/guardrails.rb
|
|
174
186
|
- lib/samagotchi/guardrails/approval.rb
|
|
@@ -178,6 +190,7 @@ files:
|
|
|
178
190
|
- lib/samagotchi/guardrails/load_failures.rb
|
|
179
191
|
- lib/samagotchi/guardrails/protected_paths.rb
|
|
180
192
|
- lib/samagotchi/guardrails/rules.rb
|
|
193
|
+
- lib/samagotchi/guardrails/scratch_writes.rb
|
|
181
194
|
- lib/samagotchi/guardrails/targets.rb
|
|
182
195
|
- lib/samagotchi/guardrails/verdict.rb
|
|
183
196
|
- lib/samagotchi/hooks.rb
|
|
@@ -239,15 +252,19 @@ files:
|
|
|
239
252
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
240
253
|
- lib/samagotchi/recap_store.rb
|
|
241
254
|
- lib/samagotchi/reminder_store.rb
|
|
255
|
+
- lib/samagotchi/reply_wait.rb
|
|
256
|
+
- lib/samagotchi/sampling_settings.rb
|
|
242
257
|
- lib/samagotchi/self_report.rb
|
|
243
258
|
- lib/samagotchi/send_command.rb
|
|
244
259
|
- lib/samagotchi/served_model.rb
|
|
245
260
|
- lib/samagotchi/session.rb
|
|
261
|
+
- lib/samagotchi/session_archive_command.rb
|
|
246
262
|
- lib/samagotchi/session_commands.rb
|
|
247
263
|
- lib/samagotchi/session_delete_command.rb
|
|
248
264
|
- lib/samagotchi/session_manager.rb
|
|
249
265
|
- lib/samagotchi/session_metrics.rb
|
|
250
266
|
- lib/samagotchi/session_observer.rb
|
|
267
|
+
- lib/samagotchi/steer.rb
|
|
251
268
|
- lib/samagotchi/terminal_ui.rb
|
|
252
269
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
253
270
|
- lib/samagotchi/terminal_ui/attached_loop.rb
|
|
@@ -311,6 +328,7 @@ files:
|
|
|
311
328
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
312
329
|
- lib/samagotchi/web/message_parts.rb
|
|
313
330
|
- lib/samagotchi/web/public/activity.js
|
|
331
|
+
- lib/samagotchi/web/public/annotate_presets.js
|
|
314
332
|
- lib/samagotchi/web/public/annotations.js
|
|
315
333
|
- lib/samagotchi/web/public/app.js
|
|
316
334
|
- lib/samagotchi/web/public/card.js
|
|
@@ -327,6 +345,7 @@ files:
|
|
|
327
345
|
- lib/samagotchi/web/public/index.html
|
|
328
346
|
- lib/samagotchi/web/public/init_row.js
|
|
329
347
|
- lib/samagotchi/web/public/model_pick.js
|
|
348
|
+
- lib/samagotchi/web/public/notify.js
|
|
330
349
|
- lib/samagotchi/web/public/question_card.js
|
|
331
350
|
- lib/samagotchi/web/public/route.js
|
|
332
351
|
- lib/samagotchi/web/public/scope.js
|