samagotchi 0.153.0 → 0.154.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 +28 -1
- data/README.md +2 -3
- data/bin/chi +23 -24
- data/docs/architecture.md +30 -33
- data/docs/broadcast.md +5 -5
- data/docs/bundles.md +9 -12
- data/docs/cli.md +80 -96
- data/docs/configuration.md +27 -26
- data/docs/context.md +3 -3
- data/docs/desktop.md +1 -1
- data/docs/guardrails.md +8 -10
- data/docs/hooks.md +9 -9
- data/docs/internals/attached-context.md +2 -2
- data/docs/internals/background-tasks.md +3 -4
- data/docs/internals/block-renderers.md +4 -0
- data/docs/internals/bridge.md +3 -3
- data/docs/internals/context-telemetry.md +1 -1
- data/docs/internals/engine.md +33 -7
- data/docs/internals/objectives.md +6 -5
- data/docs/internals/participants.md +7 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/remote-sessions.md +7 -7
- data/docs/internals/web-push.md +1 -1
- data/docs/memory.md +2 -2
- data/docs/plugins.md +23 -28
- data/docs/sessions.md +24 -24
- data/docs/sub-agent.md +2 -2
- data/docs/system-one.md +1 -1
- data/docs/testing.md +16 -6
- data/lib/samagotchi/answer_text.rb +34 -0
- data/lib/samagotchi/bridge.rb +43 -32
- data/lib/samagotchi/bridge_client.rb +10 -9
- data/lib/samagotchi/broadcast/recipients.rb +2 -2
- data/lib/samagotchi/broadcast/triage.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +5 -5
- data/lib/samagotchi/bundle_command/try/command.rb +1 -1
- data/lib/samagotchi/bundle_command/try/run.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +2 -2
- data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
- data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +3 -3
- data/lib/samagotchi/commands/registry.rb +6 -6
- data/lib/samagotchi/composer_route.rb +4 -5
- data/lib/samagotchi/config.rb +14 -12
- data/lib/samagotchi/context_candidate_choice.rb +1 -1
- data/lib/samagotchi/context_candidates.rb +4 -3
- data/lib/samagotchi/continue_offer.rb +1 -1
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/degrade.rb +75 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
- data/lib/samagotchi/engine/cards.rb +87 -0
- data/lib/samagotchi/engine/context_judge.rb +138 -0
- data/lib/samagotchi/engine/events.rb +131 -0
- data/lib/samagotchi/engine/guardrails.rb +87 -0
- data/lib/samagotchi/engine/hooks.rb +119 -0
- data/lib/samagotchi/engine/messages.rb +124 -0
- data/lib/samagotchi/engine/model_settings.rb +127 -0
- data/lib/samagotchi/engine/models.rb +261 -0
- data/lib/samagotchi/engine/objectives.rb +116 -0
- data/lib/samagotchi/engine/participants.rb +86 -0
- data/lib/samagotchi/engine/plugins.rb +208 -0
- data/lib/samagotchi/engine/questions.rb +98 -0
- data/lib/samagotchi/engine/recap.rb +130 -0
- data/lib/samagotchi/engine/session.rb +131 -0
- data/lib/samagotchi/engine/side_requests.rb +135 -0
- data/lib/samagotchi/engine/steering.rb +134 -0
- data/lib/samagotchi/engine/toolkit.rb +269 -0
- data/lib/samagotchi/engine/turn_end.rb +180 -0
- data/lib/samagotchi/engine/turn_helpers.rb +160 -0
- data/lib/samagotchi/engine/verify.rb +153 -0
- data/lib/samagotchi/engine.rb +62 -2646
- data/lib/samagotchi/grants.rb +575 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -2
- data/lib/samagotchi/guardrails/context.rb +2 -3
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
- data/lib/samagotchi/hook_routes.rb +3 -3
- data/lib/samagotchi/idle_recap.rb +4 -5
- data/lib/samagotchi/idle_reminders.rb +3 -4
- data/lib/samagotchi/iteration_limit.rb +2 -2
- data/lib/samagotchi/kernel_loop.rb +10 -11
- data/lib/samagotchi/launch_mode.rb +13 -32
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/llm_context_command.rb +3 -2
- data/lib/samagotchi/log.rb +7 -7
- data/lib/samagotchi/log_line.rb +1 -1
- data/lib/samagotchi/log_path.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/note_delivery.rb +18 -8
- data/lib/samagotchi/objectives.rb +24 -3
- data/lib/samagotchi/objectives_check.rb +8 -27
- data/lib/samagotchi/objectives_command.rb +4 -69
- data/lib/samagotchi/objectives_edit.rb +87 -0
- data/lib/samagotchi/output_formatter.rb +7 -1
- data/lib/samagotchi/owner_lock.rb +7 -1
- data/lib/samagotchi/parent_answers_command.rb +5 -4
- data/lib/samagotchi/participant_exposure.rb +42 -0
- data/lib/samagotchi/participant_mode.rb +1 -1
- data/lib/samagotchi/participant_name.rb +1 -1
- data/lib/samagotchi/participants.rb +17 -17
- data/lib/samagotchi/participants_watch.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +2 -2
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/context.rb +21 -7
- data/lib/samagotchi/plugin/peer.rb +2 -1
- data/lib/samagotchi/plugin/sessions.rb +6 -4
- data/lib/samagotchi/plugin_tasks.rb +1 -2
- data/lib/samagotchi/private_files.rb +29 -0
- data/lib/samagotchi/project_env.rb +2 -2
- data/lib/samagotchi/question_desk.rb +8 -7
- data/lib/samagotchi/recap_command.rb +2 -1
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/recap_store.rb +1 -1
- data/lib/samagotchi/redact.rb +86 -0
- data/lib/samagotchi/reminder_queue.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +2 -2
- data/lib/samagotchi/review_start.rb +1 -1
- data/lib/samagotchi/run_options.rb +6 -6
- data/lib/samagotchi/self_report.rb +42 -39
- data/lib/samagotchi/send_command/busy.rb +1 -1
- data/lib/samagotchi/send_command.rb +4 -1
- data/lib/samagotchi/session.rb +11 -2
- data/lib/samagotchi/session_access/contract.rb +3 -3
- data/lib/samagotchi/session_access/local.rb +9 -9
- data/lib/samagotchi/session_access/remote.rb +17 -7
- data/lib/samagotchi/session_access.rb +4 -4
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +8 -6
- data/lib/samagotchi/session_commands.rb +45 -48
- data/lib/samagotchi/session_continue.rb +1 -1
- data/lib/samagotchi/session_delete_command.rb +4 -4
- data/lib/samagotchi/session_manager.rb +59 -36
- data/lib/samagotchi/session_metrics.rb +2 -2
- data/lib/samagotchi/session_model_settings.rb +3 -2
- data/lib/samagotchi/session_participants.rb +1 -1
- data/lib/samagotchi/session_retention.rb +3 -3
- data/lib/samagotchi/sessions_command.rb +4 -4
- data/lib/samagotchi/share_command.rb +10 -10
- data/lib/samagotchi/steer.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +102 -109
- data/lib/samagotchi/task_watch.rb +1 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
- data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
- data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
- data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
- data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
- data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
- data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
- data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
- data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
- data/lib/samagotchi/terminal_ui.rb +142 -1387
- data/lib/samagotchi/title_command.rb +2 -2
- data/lib/samagotchi/tool_runner.rb +14 -16
- data/lib/samagotchi/toolkit_command.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +3 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/objectives_tool.rb +2 -2
- data/lib/samagotchi/tools/send_note.rb +2 -1
- data/lib/samagotchi/turn_endings.rb +2 -2
- data/lib/samagotchi/turn_flow.rb +4 -5
- data/lib/samagotchi/update_hint.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +39 -50
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_review.rb +5 -5
- data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
- data/lib/samagotchi/web/participant_view.rb +2 -82
- data/lib/samagotchi/web/participants_admin.rb +1 -1
- data/lib/samagotchi/web/public/block_host.js +35 -8
- data/lib/samagotchi/web/public/block_renderers.css +9 -5
- data/lib/samagotchi/web/public/chains.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -3
- data/lib/samagotchi/web/push/store.rb +1 -1
- data/lib/samagotchi/web/session_access_routes.rb +5 -4
- data/lib/samagotchi/web/session_control.rb +12 -8
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +2 -2
- data/lib/samagotchi/web/session_show.rb +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +100 -9
- data/lib/samagotchi/worker_link.rb +3 -3
- data/lib/samagotchi/working_set_command.rb +3 -2
- metadata +29 -6
- data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
- data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
- data/lib/samagotchi/web/grants.rb +0 -575
- data/lib/samagotchi/web/participant_exposure.rb +0 -44
- data/lib/samagotchi/web/private_files.rb +0 -31
|
@@ -42,6 +42,11 @@ const NOT_AN_IMAGE = "the renderer's SVG can't be shown as an image";
|
|
|
42
42
|
// this tall and scrolls sideways instead.
|
|
43
43
|
const MIN_SHOWN_HEIGHT = 120;
|
|
44
44
|
|
|
45
|
+
// The zoom button's words: the picture fitted to the column, or at its natural size.
|
|
46
|
+
const ZOOM_IN = "Show at full size";
|
|
47
|
+
const ZOOM_OUT = "Fit to the column";
|
|
48
|
+
|
|
49
|
+
const ZOOM_ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><circle cx="7" cy="7" r="4.25" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="m10.2 10.2 3.3 3.3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
|
|
45
50
|
const ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M5.5 4 2 8l3.5 4M10.5 4 14 8l-3.5 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
46
51
|
|
|
47
52
|
// The picture's URL and size. The XML parser reads the root, as the image will: a
|
|
@@ -286,6 +291,9 @@ export function createBlockHost({
|
|
|
286
291
|
return Promise.race([image, bound]).finally(() => clearTimeout(timer));
|
|
287
292
|
}
|
|
288
293
|
|
|
294
|
+
// The picture's name says where its words are: the source <pre> is hidden, so a
|
|
295
|
+
// screen reader reaches it through the toggle. The zoom is a button too, for the
|
|
296
|
+
// keyboard (a click on the picture does the same); it shows on keyboard focus only.
|
|
289
297
|
function makeFigure(picture, themeName, lang) {
|
|
290
298
|
const figure = doc.createElement("figure");
|
|
291
299
|
figure.className = DRAWN;
|
|
@@ -294,17 +302,33 @@ export function createBlockHost({
|
|
|
294
302
|
scroll.className = "block-scroll";
|
|
295
303
|
const img = doc.createElement("img");
|
|
296
304
|
img.className = "block-img";
|
|
297
|
-
img.alt = `${lang} diagram`;
|
|
305
|
+
img.alt = `${lang} diagram, its source under the Show the source button`;
|
|
298
306
|
showPicture(img, picture);
|
|
299
307
|
scroll.append(img);
|
|
300
|
-
const
|
|
301
|
-
toggle
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
figure.append(scroll, toggle);
|
|
308
|
+
const zoom = button("block-zoom", ZOOM_ICON);
|
|
309
|
+
const toggle = button("block-toggle", ICON);
|
|
310
|
+
figure.append(scroll, zoom, toggle);
|
|
311
|
+
setZoomed(figure, false);
|
|
305
312
|
return figure;
|
|
306
313
|
}
|
|
307
314
|
|
|
315
|
+
function button(className, icon) {
|
|
316
|
+
const el = doc.createElement("button");
|
|
317
|
+
el.type = "button";
|
|
318
|
+
el.className = className;
|
|
319
|
+
litRender(icon, el);
|
|
320
|
+
return el;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function setZoomed(figure, on) {
|
|
324
|
+
figure.classList.toggle("natural", on);
|
|
325
|
+
const zoom = figure.querySelector(":scope > .block-zoom");
|
|
326
|
+
if (!zoom) return;
|
|
327
|
+
zoom.setAttribute("aria-pressed", on ? "true" : "false");
|
|
328
|
+
zoom.setAttribute("aria-label", on ? ZOOM_OUT : ZOOM_IN);
|
|
329
|
+
zoom.title = on ? ZOOM_OUT : ZOOM_IN;
|
|
330
|
+
}
|
|
331
|
+
|
|
308
332
|
function showPicture(img, { url, width, height }) {
|
|
309
333
|
if (width && height) {
|
|
310
334
|
img.setAttribute("width", width);
|
|
@@ -476,7 +500,7 @@ export function createBlockHost({
|
|
|
476
500
|
scan(root);
|
|
477
501
|
|
|
478
502
|
root.addEventListener("mousedown", (e) => {
|
|
479
|
-
if (e.target.closest?.(".block-toggle")) e.preventDefault();
|
|
503
|
+
if (e.target.closest?.(".block-toggle, .block-zoom")) e.preventDefault();
|
|
480
504
|
});
|
|
481
505
|
// Delegated, so a Revise clone's work too.
|
|
482
506
|
root.addEventListener("click", (e) => {
|
|
@@ -490,7 +514,10 @@ export function createBlockHost({
|
|
|
490
514
|
}
|
|
491
515
|
const note = e.target.closest?.(".block-note");
|
|
492
516
|
if (note) { copy(note.dataset.text); return; }
|
|
493
|
-
if (e.target.closest?.(".block-img"))
|
|
517
|
+
if (e.target.closest?.(".block-zoom, .block-img")) {
|
|
518
|
+
const figure = e.target.closest(`.${DRAWN}`);
|
|
519
|
+
setZoomed(figure, !figure.classList.contains("natural"));
|
|
520
|
+
}
|
|
494
521
|
});
|
|
495
522
|
}
|
|
496
523
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. */
|
|
1
|
+
/* A drawn fenced block (block_host.js): the picture scales to the column, a click shows its natural size in a scroller; the toggle shows the source, in the block's place. In a .code-wrap the copy button sits top-right, so the toggle sits left of it. The zoom button, the keyboard's way to the natural size, shows on keyboard focus only, left of the toggle. */
|
|
2
2
|
.block-drawn{position:relative;margin:0 0 12px;white-space:normal}
|
|
3
3
|
.code-wrap>.block-drawn{margin:0}
|
|
4
4
|
.code-wrap>.block-drawn.source{height:0}
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
.block-img{display:block;max-width:100%;max-height:50000px;object-fit:contain;height:auto;margin:0 auto;cursor:zoom-in}
|
|
9
9
|
.block-drawn.natural .block-img{max-width:50000px;margin:0;cursor:zoom-out}
|
|
10
10
|
.block-drawn.source .block-scroll{display:none}
|
|
11
|
-
.block-toggle{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
|
|
11
|
+
.block-toggle,.block-zoom{position:absolute;top:5px;right:5px;z-index:1;display:inline-flex;align-items:center;justify-content:center;height:24px;min-width:24px;padding:0 5px;border-radius:6px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-3);opacity:0;transition:opacity .12s}
|
|
12
12
|
.code-wrap>.block-drawn>.block-toggle{right:34px}
|
|
13
|
-
.block-
|
|
14
|
-
.
|
|
15
|
-
.block-
|
|
13
|
+
.block-zoom{right:34px}
|
|
14
|
+
.code-wrap>.block-drawn>.block-zoom{right:63px}
|
|
15
|
+
.block-drawn.source>.block-zoom{display:none}
|
|
16
|
+
.block-zoom:not(:focus-visible){pointer-events:none}
|
|
17
|
+
.block-toggle svg,.block-zoom svg{display:block}
|
|
18
|
+
.block-drawn:hover>.block-toggle,.code-wrap:hover>.block-drawn>.block-toggle,.block-toggle:focus-visible,.block-zoom:focus-visible,.block-drawn.source>.block-toggle{opacity:1}
|
|
19
|
+
.block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){filter:none}@media(hover:hover){.block-toggle:hover:not(:disabled),.block-zoom:hover:not(:disabled){color:var(--text-bright)}}
|
|
16
20
|
.block-drawn.source:not(:last-child){margin:0}
|
|
17
21
|
/* Its words are the attribute's, so a bubble's text stays the answer's (block_host.js fail). */
|
|
18
22
|
.block-note{margin:4px 0 12px;font-size:12px;color:var(--text-3);white-space:normal}
|
|
@@ -305,8 +305,8 @@ export function continueRefusal(error, sessions = []) {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
// The open-children refusal as the toast shows it: the open delegates the
|
|
308
|
-
// continue can't move to the next link (an older chi's worker, a
|
|
309
|
-
//
|
|
308
|
+
// continue can't move to the next link (an older chi's worker, a
|
|
309
|
+
// one-shot run), each a link to its session with what to do about it.
|
|
310
310
|
// @param hrefFor (id) → its page's href
|
|
311
311
|
export function openChildrenTemplate(prevId, children, hrefFor) {
|
|
312
312
|
const n = children.length;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// URL-safe and a space is one "+".
|
|
13
13
|
//
|
|
14
14
|
// Normalized: whitespace collapsed, and what the participant page shows
|
|
15
|
-
// instead (
|
|
15
|
+
// instead (Samagotchi::Redact): secret shapes "[redacted]" (so a link
|
|
16
16
|
// never carries one, and matches the participant's redacted text), and on
|
|
17
17
|
// the owner's page the session's folder relative ("<cwd>/x" → "x",
|
|
18
18
|
// "<cwd>" → "."), the home folder "~". Not the owner's token (Redact's
|
|
@@ -41,10 +41,10 @@ export const HIT_CLASS = "claim-hit";
|
|
|
41
41
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
42
42
|
|
|
43
43
|
export const MARK = "[redacted]";
|
|
44
|
-
//
|
|
45
|
-
// (lib/samagotchi/
|
|
44
|
+
// Samagotchi::Redact's PATTERNS and VALUES
|
|
45
|
+
// (lib/samagotchi/redact.rb), as JS: keep them in step.
|
|
46
46
|
// spec/fixtures/redact_cases.json holds cases both sides read the same
|
|
47
|
-
// (
|
|
47
|
+
// (redact_spec.rb, claim_link.test.js).
|
|
48
48
|
const SECRET_PATTERNS = [
|
|
49
49
|
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g,
|
|
50
50
|
/\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
|
|
@@ -51,7 +51,7 @@ export function composerMode({ selected, owner, running, narrow }) {
|
|
|
51
51
|
if (tui) {
|
|
52
52
|
return {
|
|
53
53
|
...base,
|
|
54
|
-
placeholder: pick("A terminal chi owns this session ·
|
|
54
|
+
placeholder: pick("A terminal chi owns this session · its one-turn run (chi --non-interactive) lets go as it ends", "A terminal chi owns this session"),
|
|
55
55
|
hint: "", button: "Send", disabled: true,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -97,13 +97,13 @@ export function deleteConfirmText(session) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// The badge on a session a process holds right now (the list's `owner`):
|
|
100
|
-
// a worker (
|
|
100
|
+
// a worker (every UI can send), or a terminal chi's one-turn run (--non-interactive).
|
|
101
101
|
export function ownerBadge(owner, id) {
|
|
102
102
|
if (owner === "worker") {
|
|
103
103
|
return { kind: "worker", text: "live", title: `A worker runs this session; a terminal joins with: chi --attach ${id}` };
|
|
104
104
|
}
|
|
105
105
|
if (owner === "tui") {
|
|
106
|
-
return { kind: "tui", text: "terminal", title: "A terminal chi
|
|
106
|
+
return { kind: "tui", text: "terminal", title: "A terminal chi runs one turn in this session (chi --non-interactive) and lets go as it ends" };
|
|
107
107
|
}
|
|
108
108
|
return null;
|
|
109
109
|
}
|
|
@@ -171,7 +171,7 @@ export function childrenSummary(parent, sessions) {
|
|
|
171
171
|
|
|
172
172
|
// Whether the info bar offers "stop": only while a worker runs the session
|
|
173
173
|
// (the list says so, or this page streams from one a send just woke). A
|
|
174
|
-
// stopped or idle-exited session has nothing to stop, and a
|
|
174
|
+
// stopped or idle-exited session has nothing to stop, and a one-turn run's is
|
|
175
175
|
// refused by the server.
|
|
176
176
|
export function canStopSession({ owner, streaming }) {
|
|
177
177
|
if (owner === "tui") return false;
|
|
@@ -7,7 +7,7 @@ require "openssl"
|
|
|
7
7
|
require "time"
|
|
8
8
|
require "uri"
|
|
9
9
|
require_relative "crypto"
|
|
10
|
-
require_relative "
|
|
10
|
+
require_relative "../../private_files"
|
|
11
11
|
require_relative "../../log"
|
|
12
12
|
require_relative "../../atomic_file"
|
|
13
13
|
require_relative "../../session"
|
|
@@ -256,7 +256,7 @@ module Samagotchi
|
|
|
256
256
|
# status row), parent_strategy, installed (the newest chi here)}. A
|
|
257
257
|
# worker still leaving is waited for first (Local#wake: its recap's
|
|
258
258
|
# timeout and a margin, on top of +wait+); 409
|
|
259
|
-
# owned_by_tui (a chi
|
|
259
|
+
# owned_by_tui (a one-turn chi run has it), 409 scratch (a leftover scratch
|
|
260
260
|
# session, as chi --attach here says); 504 worker_timeout (its Bridge
|
|
261
261
|
# didn't come in time); 409 worker_newer (a worker whose protocol is newer
|
|
262
262
|
# than this chi's, not resumed); 404 not_found.
|
|
@@ -425,7 +425,7 @@ module Samagotchi
|
|
|
425
425
|
# session (NoteDelivery, as chi note; source: where it came from,
|
|
426
426
|
# "cli" by default, never "broadcast"). 200 {status: "queued" (a
|
|
427
427
|
# worker adds it) or "waits" (the next start does), session_id,
|
|
428
|
-
# queued}; 409 owned_by_tui (status refused: a chi
|
|
428
|
+
# queued}; 409 owned_by_tui (status refused: a one-turn chi run has it); 400
|
|
429
429
|
# note_rejected (empty, or over 16 KiB).
|
|
430
430
|
def handle_notes(req, id)
|
|
431
431
|
body = body_hash(req) or return error_response(400, "invalid_json", "invalid JSON body")
|
|
@@ -438,8 +438,9 @@ module Samagotchi
|
|
|
438
438
|
session_access.show(id)
|
|
439
439
|
result = session_access.note(id, text: text, source: source.strip)
|
|
440
440
|
if result.status == :refused
|
|
441
|
-
error = SessionManager::OwnedByTUI.new(id)
|
|
442
|
-
return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0
|
|
441
|
+
error = result.scratch ? SessionManager::ScratchSession.new(id) : SessionManager::OwnedByTUI.new(id)
|
|
442
|
+
return refusal_response(409, "owned_by_tui", error, session_id: id, status: "refused", queued: 0,
|
|
443
|
+
scratch: result.scratch || nil)
|
|
443
444
|
end
|
|
444
445
|
|
|
445
446
|
json_response(200, { status: result.status.to_s, session_id: id, queued: result.queued })
|
|
@@ -16,7 +16,7 @@ module Samagotchi
|
|
|
16
16
|
# a task, dequeue a queued turn, what it could stop and stop it
|
|
17
17
|
# (handle_session_holds, handle_stop_holds — the page's Stop asks about
|
|
18
18
|
# these first), stop, restart, archive, unarchive and delete
|
|
19
|
-
# (handle_question_answer … handle_delete), with the refusals they answer (
|
|
19
|
+
# (handle_question_answer … handle_delete), with the refusals they answer (owned_by_one_shot, owned_by_tui, archive_refused). Included in App:
|
|
20
20
|
# its ROUTES name the handlers, and its helpers (json_response,
|
|
21
21
|
# error_response, parse_json, request_body, relay, not_live,
|
|
22
22
|
# bridge_error, reply_detail, worker_timeout, bridge_client,
|
|
@@ -443,7 +443,7 @@ module Samagotchi
|
|
|
443
443
|
|
|
444
444
|
# POST /api/sessions/:id/archive: hide it (and its delegates) from the
|
|
445
445
|
# lists, keep it for good. A live idle worker is stopped first; a turn
|
|
446
|
-
# running in it or a delegate, a chi
|
|
446
|
+
# running in it or a delegate, a one-turn chi run's session and a scratch one
|
|
447
447
|
# are refused (409).
|
|
448
448
|
def handle_archive(_req, id)
|
|
449
449
|
result = @manager.archive_session(id, state_dir: @state_dir, wait: STOP_WAIT_SECONDS)
|
|
@@ -451,7 +451,7 @@ module Samagotchi
|
|
|
451
451
|
json_response(200, { status: "archived", session_id: result.id, archived: result.archived,
|
|
452
452
|
stopped: result.stopped, discarded: result.discarded })
|
|
453
453
|
rescue SessionManager::OwnedByTUI => e
|
|
454
|
-
|
|
454
|
+
owned_by_one_shot(id, e)
|
|
455
455
|
rescue SessionManager::ArchiveRefused => e
|
|
456
456
|
archive_refused(e)
|
|
457
457
|
end
|
|
@@ -466,7 +466,7 @@ module Samagotchi
|
|
|
466
466
|
# DELETE /api/sessions/:id?force=true: the session for good. With
|
|
467
467
|
# force a live worker is stopped first (the page sends it: its confirm
|
|
468
468
|
# says so); without it a live worker's session is refused, 409
|
|
469
|
-
# worker_running, as chi sessions delete without --force. A chi
|
|
469
|
+
# worker_running, as chi sessions delete without --force. A one-turn chi run's
|
|
470
470
|
# session is always refused.
|
|
471
471
|
def handle_delete(req, id)
|
|
472
472
|
result = @manager.delete_session(id, state_dir: @state_dir, stop: truthy_param?(req.params["force"]),
|
|
@@ -474,7 +474,7 @@ module Samagotchi
|
|
|
474
474
|
@hub&.touch(result[:id])
|
|
475
475
|
json_response(200, { status: "deleted", session_id: result[:id], stopped: result[:stopped] })
|
|
476
476
|
rescue SessionManager::OwnedByTUI => e
|
|
477
|
-
|
|
477
|
+
owned_by_one_shot(id, e)
|
|
478
478
|
rescue SessionManager::DeleteRefused => e
|
|
479
479
|
detail = e.reason == :still_stopping ? "#{e.message}; try again in a moment" : e.message
|
|
480
480
|
refusal_response(409, e.reason.to_s, e, detail: detail, session_id: e.session_id, reason: e.reason.to_s)
|
|
@@ -482,15 +482,19 @@ module Samagotchi
|
|
|
482
482
|
|
|
483
483
|
# 409 owned_by_tui in the words a refused archive, delete or continue
|
|
484
484
|
# shows (stop, restart, a turn and a command show OwnedByTUI's own).
|
|
485
|
-
def
|
|
485
|
+
def owned_by_one_shot(id, error = nil)
|
|
486
486
|
named = SessionManager::OwnedByTUI.new(id)
|
|
487
487
|
error ||= named
|
|
488
488
|
refusal_response(409, "owned_by_tui", error, detail: named.message, session_id: error.session_id)
|
|
489
489
|
end
|
|
490
490
|
|
|
491
491
|
# 409 owned_by_tui in OwnedByTUI's own words (stop, restart, a turn, a
|
|
492
|
-
# command)
|
|
493
|
-
|
|
492
|
+
# command); scratch: true for a chi scratch session
|
|
493
|
+
# (SessionManager::ScratchSession), which a remote client raises again.
|
|
494
|
+
def owned_by_tui(error)
|
|
495
|
+
scratch = error.is_a?(SessionManager::ScratchSession) || nil
|
|
496
|
+
refusal_response(409, "owned_by_tui", error, session_id: error.session_id, scratch: scratch)
|
|
497
|
+
end
|
|
494
498
|
|
|
495
499
|
# 409 scratch or busy (every other reason: a turn, a queued prompt, an
|
|
496
500
|
# open step-limit question, in it or a delegate, +busy_id+), with the
|
|
@@ -339,7 +339,7 @@ module Samagotchi
|
|
|
339
339
|
rescue SessionManager::ArchiveRefused => e
|
|
340
340
|
[nil, archive_refused(e)]
|
|
341
341
|
rescue SessionManager::OwnedByTUI => e
|
|
342
|
-
[nil,
|
|
342
|
+
[nil, owned_by_one_shot(given, e)]
|
|
343
343
|
rescue ModelProfile::MissingModel => e
|
|
344
344
|
[nil, error_response(400, "invalid_model", e.message)]
|
|
345
345
|
rescue Session::AmbiguousId => e
|
|
@@ -41,8 +41,8 @@ module Samagotchi
|
|
|
41
41
|
# The flock leaves no file trace when its holder dies, so owners are
|
|
42
42
|
# probed: every tick for the sessions the projection believes owned
|
|
43
43
|
# (a handful; OwnerLock.owner is open + LOCK_SH|LOCK_NB + close), and
|
|
44
|
-
# all sessions this often (a
|
|
45
|
-
# without saving it has no file signal until its first save).
|
|
44
|
+
# all sessions this often (a one-shot or an older chi's REPL that
|
|
45
|
+
# opened an old session without saving it has no file signal until its first save).
|
|
46
46
|
FULL_PROBE_INTERVAL = 10.0
|
|
47
47
|
|
|
48
48
|
# What a subscriber gets: `session` with {session: summary} for an
|
|
@@ -120,8 +120,8 @@ module Samagotchi
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# The file's question only while a worker owns the session: one a
|
|
123
|
-
# dead worker saved can't be answered (its turn is gone), nor a
|
|
124
|
-
# REPL's (
|
|
123
|
+
# dead worker saved can't be answered (its turn is gone), nor a
|
|
124
|
+
# one-shot's or an older chi's REPL's (they share nothing with the web).
|
|
125
125
|
# A question between turns (the step-limit one) is the snapshot's own.
|
|
126
126
|
# Its refs (the asking reply's, QuestionAsked) are checked again.
|
|
127
127
|
def shown_pending_question(session, turn_snapshot, owner)
|
|
@@ -108,7 +108,7 @@ module Samagotchi
|
|
|
108
108
|
# For the tab's notifications (notify.js): an open question, the
|
|
109
109
|
# running turn's open card with actions, and how the last turn
|
|
110
110
|
# ended. A question saved by a worker that died is not open, nor a
|
|
111
|
-
# chi
|
|
111
|
+
# one-turn chi run's (only a worker shares it).
|
|
112
112
|
pending_question: session.waiting_question(live: !!owner&.worker?),
|
|
113
113
|
pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
|
|
114
114
|
last_turn: session.last_turn&.to_file,
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "turn_note"
|
|
|
14
14
|
require_relative "steer"
|
|
15
15
|
require_relative "turn_origin"
|
|
16
16
|
require_relative "worker_idle_exit"
|
|
17
|
+
require_relative "degrade"
|
|
17
18
|
require_relative "hold_stop"
|
|
18
19
|
require_relative "children_status"
|
|
19
20
|
require_relative "worker_wakes"
|
|
@@ -51,7 +52,7 @@ module Samagotchi
|
|
|
51
52
|
# It runs the session's Engine and Bridge, takes queued prompts from the
|
|
52
53
|
# input dir one turn at a time, and returns when nobody has used it for the
|
|
53
54
|
# idle-exit timeout. A failed turn is rolled back and its prompt handed back
|
|
54
|
-
# (:prompt_restored),
|
|
55
|
+
# (:prompt_restored), and the loop goes on. A stop marked on
|
|
55
56
|
# disk exits the process; an error outside a turn marks the session and
|
|
56
57
|
# exits with 1.
|
|
57
58
|
#
|
|
@@ -188,6 +189,10 @@ module Samagotchi
|
|
|
188
189
|
# checks again then).
|
|
189
190
|
def discard? = @discard
|
|
190
191
|
|
|
192
|
+
# Whether this worker runs a `chi scratch` session: deleted as it leaves
|
|
193
|
+
# (SessionManager.run_session_loop).
|
|
194
|
+
def scratch? = !!@session&.scratch
|
|
195
|
+
|
|
191
196
|
# Whether delegate reports wait that an idle parent runs a turn for
|
|
192
197
|
# (#wake_state): SessionManager.run_session_loop starts a new worker
|
|
193
198
|
# for them after this one leaves. Reports the budget or a failure held
|
|
@@ -201,8 +206,9 @@ module Samagotchi
|
|
|
201
206
|
# to exit (Bridge POST /exit), :restart when it asked for a restart
|
|
202
207
|
# (the caller starts a new worker), :stopped when the session was stopped
|
|
203
208
|
# (chi stop), :signal when a signal stopped it (WorkerSignals),
|
|
204
|
-
# :crashed when the loop raised (the session is marked errored)
|
|
205
|
-
#
|
|
209
|
+
# :crashed when the loop raised (the session is marked errored),
|
|
210
|
+
# :scratch_left when a scratch session's terminal is gone (the caller
|
|
211
|
+
# deletes it); SessionManager.run_session_loop turns it into the exit
|
|
206
212
|
def run
|
|
207
213
|
# The thread a signal's expired grace raises in (the main thread in a
|
|
208
214
|
# worker process).
|
|
@@ -248,6 +254,8 @@ module Samagotchi
|
|
|
248
254
|
# The session, its Engine and what runs its turns, before anyone can
|
|
249
255
|
# reach it.
|
|
250
256
|
def start
|
|
257
|
+
# A scratch worker's grace for its terminal's first stream counts from here.
|
|
258
|
+
@started_at = monotonic_now
|
|
251
259
|
# The wake turns' budget (delegate reports, attached context, peers), from now.
|
|
252
260
|
@wakes = WorkerWakes.new(grace: WAKE_START_GRACE)
|
|
253
261
|
# This worker holds the session now (a continue's starting marker held
|
|
@@ -340,7 +348,7 @@ module Samagotchi
|
|
|
340
348
|
@bridge.queue_command(line, client_id: client_id, after_file: after_file,
|
|
341
349
|
enqueued_id: enqueued_id, author: author)
|
|
342
350
|
})
|
|
343
|
-
# /model's default is the config's
|
|
351
|
+
# /model's default is the config's; the Engine started
|
|
344
352
|
# on the session's model. nil when there is no default.model.
|
|
345
353
|
@default_model = ModelProfile.default_model_name
|
|
346
354
|
@commands = SessionCommands.new(engine: @engine, turn_flow: @turn_flow,
|
|
@@ -416,6 +424,31 @@ module Samagotchi
|
|
|
416
424
|
@context_poller = ContextPoller.new(session_id: @session_id, state_dir: @state_dir,
|
|
417
425
|
project_root: @session.project_root, cwd: @session.working_directory,
|
|
418
426
|
on_change: -> { @waker.wake }).start
|
|
427
|
+
@scratch_watch = start_scratch_watch if @session.scratch
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# A scratch session's turn never runs on with no terminal: a turn
|
|
431
|
+
# waiting on a question or an approval would keep the worker up for
|
|
432
|
+
# nobody. Each tick while a turn runs and its terminal is gone
|
|
433
|
+
# (#scratch_unattended?), the question is dropped and the turn
|
|
434
|
+
# cancelled as a stop cancels it; the idle pass then leaves
|
|
435
|
+
# (:scratch_left) and the session is deleted.
|
|
436
|
+
def start_scratch_watch
|
|
437
|
+
Thread.new do
|
|
438
|
+
loop do
|
|
439
|
+
sleep(@poll_interval)
|
|
440
|
+
Degrade.call(:worker, "scratch_watch_failed") { cancel_unattended_turn }
|
|
441
|
+
end
|
|
442
|
+
end.tap { |thread| thread.report_on_exception = false }
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def cancel_unattended_turn
|
|
446
|
+
return unless @engine.turn_running? && scratch_unattended?
|
|
447
|
+
|
|
448
|
+
Log.info(:worker, "scratch_turn_cancelled", sid: @session_id)
|
|
449
|
+
@engine.cancel_question(SessionManager::STOP_CANCEL_REASON) if @engine.pending_question
|
|
450
|
+
@engine.cancel_current_turn!(SessionManager::STOP_CANCEL_REASON.to_sym)
|
|
451
|
+
@waker.wake
|
|
419
452
|
end
|
|
420
453
|
|
|
421
454
|
# One pass of the loop: the first work found runs (queued commands, a
|
|
@@ -468,6 +501,7 @@ module Samagotchi
|
|
|
468
501
|
return if run_wake_turn
|
|
469
502
|
return if run_task_wake
|
|
470
503
|
return if run_due_reminders
|
|
504
|
+
return left(:scratch_left) if scratch_unattended? && !@engine.turn_running? && leave_scratch
|
|
471
505
|
return left(:idle_exit) if @idle_exit.due? && leave_idle
|
|
472
506
|
|
|
473
507
|
leaving = @exit_requested && leave_on_request
|
|
@@ -553,6 +587,7 @@ module Samagotchi
|
|
|
553
587
|
TEARDOWN_LEAVE_WAIT = 10
|
|
554
588
|
|
|
555
589
|
def teardown
|
|
590
|
+
@scratch_watch&.kill
|
|
556
591
|
@config_restart_unsubscribe&.call
|
|
557
592
|
@context_poller&.stop
|
|
558
593
|
drop_queued_commands
|
|
@@ -571,6 +606,8 @@ module Samagotchi
|
|
|
571
606
|
engine = nil
|
|
572
607
|
engine = Samagotchi::Engine.new(
|
|
573
608
|
model_name: @session.model_name,
|
|
609
|
+
# `chi scratch`: no memory_write, no delegates, approvals in memory.
|
|
610
|
+
scratch: @session.scratch,
|
|
574
611
|
model_typed: @session.model_typed,
|
|
575
612
|
# The session's --memory and --mute lists: the same prompt on every
|
|
576
613
|
# (re)spawn.
|
|
@@ -783,7 +820,7 @@ module Samagotchi
|
|
|
783
820
|
end
|
|
784
821
|
|
|
785
822
|
# A participant's cap in dollars: their own, else the config's.
|
|
786
|
-
def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f :
|
|
823
|
+
def participant_cap(entry) = entry&.cap.is_a?(Numeric) ? entry.cap.to_f : Grants.default_cap
|
|
787
824
|
|
|
788
825
|
# A participant's cleaned name, else their id.
|
|
789
826
|
def participant_label(author, name)
|
|
@@ -830,7 +867,7 @@ module Samagotchi
|
|
|
830
867
|
nil
|
|
831
868
|
end
|
|
832
869
|
|
|
833
|
-
# A due reminder runs as a continue turn
|
|
870
|
+
# A due reminder runs as a continue turn: Engine#run_turn
|
|
834
871
|
# injects the reminders as a tail system message, and no user message is
|
|
835
872
|
# added. A prompt's turn may have injected them already (a stale latch):
|
|
836
873
|
# then there is nothing to run.
|
|
@@ -1414,7 +1451,7 @@ module Samagotchi
|
|
|
1414
1451
|
# WorkerInbound.save_or_log: a failed save between turns is logged.
|
|
1415
1452
|
def save_or_log(at, &) = WorkerInbound.save_or_log(at, &)
|
|
1416
1453
|
|
|
1417
|
-
# Back to the conversation before the failed turn
|
|
1454
|
+
# Back to the conversation before the failed turn (so
|
|
1418
1455
|
# failed prompts don't pile up as consecutive user messages), and each
|
|
1419
1456
|
# prompt it took (its own and any merged into it) goes back to its
|
|
1420
1457
|
# sender, who can send it again. The rollback and the announcements are
|
|
@@ -1559,6 +1596,57 @@ module Samagotchi
|
|
|
1559
1596
|
end
|
|
1560
1597
|
end
|
|
1561
1598
|
|
|
1599
|
+
# How long a scratch worker waits for its terminal's first stream
|
|
1600
|
+
# (AttachLauncher attaches within BRIDGE_WAIT of the spawn).
|
|
1601
|
+
SCRATCH_JOIN_GRACE = 30
|
|
1602
|
+
|
|
1603
|
+
# How long a scratch worker's terminal may have no stream open, after
|
|
1604
|
+
# it had one, before it counts as gone: twice the attached terminal's
|
|
1605
|
+
# whole reconnect ladder (TerminalUI::LocalLink#reconnect_delays), so
|
|
1606
|
+
# a reconnect gap (a dropped stream, a slow worker) never reads as a
|
|
1607
|
+
# terminal that left.
|
|
1608
|
+
SCRATCH_GONE_AFTER = 2 * BridgeClient::EventStream::REDISCOVER_DELAYS.sum
|
|
1609
|
+
|
|
1610
|
+
# Whether this is a `chi scratch` session's worker whose terminal is
|
|
1611
|
+
# gone: no stream open for SCRATCH_GONE_AFTER, after one was (a killed
|
|
1612
|
+
# TUI's closes as its writer reads the hang-up), or none came within
|
|
1613
|
+
# SCRATCH_JOIN_GRACE. Its terminal deletes it on every way out it sees;
|
|
1614
|
+
# this is for the ones it doesn't (kill -9, a closed terminal window).
|
|
1615
|
+
def scratch_unattended?
|
|
1616
|
+
return false unless @session&.scratch && @bridge
|
|
1617
|
+
|
|
1618
|
+
if @bridge.open_streams.positive?
|
|
1619
|
+
@unattended_since = nil
|
|
1620
|
+
return false
|
|
1621
|
+
end
|
|
1622
|
+
return monotonic_now - @started_at >= SCRATCH_JOIN_GRACE unless @bridge.streams_served.positive?
|
|
1623
|
+
|
|
1624
|
+
# Set by the first look (the scratch watch or the idle pass) that
|
|
1625
|
+
# finds no stream; a stream back resets it above.
|
|
1626
|
+
@unattended_since ||= monotonic_now
|
|
1627
|
+
monotonic_now - @unattended_since >= SCRATCH_GONE_AFTER
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1631
|
+
|
|
1632
|
+
# #leave_idle for a scratch worker its terminal left: checked again with
|
|
1633
|
+
# the event log held, then the intake closes. The session is deleted
|
|
1634
|
+
# once the lock is free (SessionManager.run_session_loop: :scratch_left).
|
|
1635
|
+
# @return [Boolean] false when a client came back since
|
|
1636
|
+
def leave_scratch
|
|
1637
|
+
stop_bridge_after do
|
|
1638
|
+
@engine.synchronize_events do
|
|
1639
|
+
# A turn still running is the scratch watch's to cancel first.
|
|
1640
|
+
next false unless scratch_unattended? && !@engine.turn_running?
|
|
1641
|
+
|
|
1642
|
+
@bridge&.close_intake
|
|
1643
|
+
@engine.stop_idle(join: false)
|
|
1644
|
+
Log.info(:worker, "scratch_left", served: @bridge.streams_served)
|
|
1645
|
+
true
|
|
1646
|
+
end
|
|
1647
|
+
end
|
|
1648
|
+
end
|
|
1649
|
+
|
|
1562
1650
|
# Check again with the event log held, which the Bridge holds while it
|
|
1563
1651
|
# queues a POST /turn, then close the Bridge's intake so no client can
|
|
1564
1652
|
# queue one after the check, and stop the idle jobs (reminder callback,
|
|
@@ -1694,6 +1782,9 @@ module Samagotchi
|
|
|
1694
1782
|
# @return [WorkerIdleExit::Hold, nil]
|
|
1695
1783
|
def exit_hold(client_id, kind:, stop:, ask:)
|
|
1696
1784
|
if kind == :restart
|
|
1785
|
+
# A scratch session's worker ends with its terminal: never restarted.
|
|
1786
|
+
return WorkerIdleExit::Hold.new(reason: :scratch) if @session.scratch
|
|
1787
|
+
|
|
1697
1788
|
hold = @idle_exit.hold_for_restart
|
|
1698
1789
|
return hold && WorkerIdleExit::Hold.new(reason: hold)
|
|
1699
1790
|
end
|
|
@@ -1792,7 +1883,7 @@ module Samagotchi
|
|
|
1792
1883
|
# A restart keeps even an empty session (a new worker takes it over at
|
|
1793
1884
|
# once) and leaves the recap to the conversation's next real pause.
|
|
1794
1885
|
def left(result)
|
|
1795
|
-
deleting = result == :exit_requested && @exit_deletes
|
|
1886
|
+
deleting = (result == :exit_requested && @exit_deletes) || result == :scratch_left
|
|
1796
1887
|
# The attached context goes with this process: a change that landed after
|
|
1797
1888
|
# the leaving pass looked at it has no later pass to note it, so note it
|
|
1798
1889
|
# here (no wake turn: the worker is on its way out). Not into a session
|
|
@@ -1805,7 +1896,7 @@ module Samagotchi
|
|
|
1805
1896
|
# session /exit --delete deletes next: its save could land after the
|
|
1806
1897
|
# delete and write the session back.
|
|
1807
1898
|
@task_wakes.absorb unless deleting
|
|
1808
|
-
return result if result
|
|
1899
|
+
return result if %i[restart scratch_left].include?(result)
|
|
1809
1900
|
|
|
1810
1901
|
@discard = empty_session?
|
|
1811
1902
|
write_recap_on_leave unless @discard || deleting
|
|
@@ -23,9 +23,9 @@ module Samagotchi
|
|
|
23
23
|
# past START_LIMIT without one;
|
|
24
24
|
# - Leaving: the worker closed its Bridge and holds the lock for its last work;
|
|
25
25
|
# - Skew: a worker whose sidecar speaks a newer PROTOCOL than this chi;
|
|
26
|
-
# - Stale: a sidecar, but the lock is free (or a chi
|
|
26
|
+
# - Stale: a sidecar, but the lock is free (or a one-turn chi run holds it, which
|
|
27
27
|
# writes none): its worker died;
|
|
28
|
-
# - None: no worker and no sidecar, or a chi
|
|
28
|
+
# - None: no worker and no sidecar, or a one-turn chi run.
|
|
29
29
|
# A kernel releases a dead worker's lock, so a free lock is the proof of
|
|
30
30
|
# death a TCP probe never gave (a port can be reused by another session's
|
|
31
31
|
# Bridge).
|
|
@@ -80,7 +80,7 @@ module Samagotchi
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
# No worker: no sidecar in the session folder, and no worker's lock (a
|
|
83
|
-
# chi
|
|
83
|
+
# one-turn chi run's, +owner+, is no worker's).
|
|
84
84
|
None = Data.define(:owner) do
|
|
85
85
|
def initialize(owner: nil) = super
|
|
86
86
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "working_set"
|
|
4
4
|
require_relative "working_set_steer"
|
|
5
5
|
require_relative "tools/memory"
|
|
6
|
+
require_relative "session"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# /working-set: with no arguments the session's working set (WorkingSet,
|
|
@@ -50,7 +51,7 @@ module Samagotchi
|
|
|
50
51
|
return [listing, [], :ok] if verb.nil?
|
|
51
52
|
raise ArgumentError, USAGE unless VERBS.include?(verb) && !words.empty?
|
|
52
53
|
|
|
53
|
-
session = @engine.session or raise ArgumentError,
|
|
54
|
+
session = @engine.session or raise ArgumentError, Session::NONE_YET
|
|
54
55
|
cwd = session.working_directory
|
|
55
56
|
strategy = @engine.working_set_strategy
|
|
56
57
|
messages = @engine.clone_messages(session.messages)
|
|
@@ -79,7 +80,7 @@ module Samagotchi
|
|
|
79
80
|
# The set under the strategy the next turn runs, and the pending ops.
|
|
80
81
|
def listing
|
|
81
82
|
session = @engine.session
|
|
82
|
-
return
|
|
83
|
+
return Session::NONE_YET unless session
|
|
83
84
|
|
|
84
85
|
messages = Array(session.messages)
|
|
85
86
|
items = WorkingSet.build(messages, cwd: session.working_directory, strategy: @engine.working_set_strategy,
|