samagotchi 0.52.0 → 0.54.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 +58 -1
- data/README.md +1 -1
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +1 -2
- data/docs/context.md +104 -9
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +73 -0
- data/docs/internals/context-telemetry.md +8 -5
- data/docs/internals/engine.md +16 -7
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/releasing.md +6 -3
- data/docs/sessions.md +1 -1
- data/docs/testing.md +5 -3
- data/lib/samagotchi/analytics_file.rb +35 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bundle_command.rb +35 -44
- data/lib/samagotchi/bundle_needs.rb +4 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +6 -4
- data/lib/samagotchi/config.rb +0 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_providers.rb +11 -14
- data/lib/samagotchi/context_status.rb +10 -18
- data/lib/samagotchi/engine.rb +132 -135
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -8
- data/lib/samagotchi/host_registry.rb +13 -0
- data/lib/samagotchi/kernel_loop.rb +5 -3
- data/lib/samagotchi/llm/chat_loop.rb +5 -4
- data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
- data/lib/samagotchi/memory_bundle/builder.rb +184 -190
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +51 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
- data/lib/samagotchi/memory_bundle/installer.rb +252 -488
- data/lib/samagotchi/memory_bundle/listing.rb +6 -6
- data/lib/samagotchi/memory_bundle/manifest.rb +9 -29
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
- data/lib/samagotchi/memory_bundle/profile.rb +21 -21
- data/lib/samagotchi/memory_bundle/provenance.rb +46 -57
- data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
- data/lib/samagotchi/memory_bundle/status.rb +22 -24
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
- data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
- data/lib/samagotchi/plugin/loader.rb +8 -8
- data/lib/samagotchi/self_report.rb +2 -2
- data/lib/samagotchi/send_command.rb +4 -28
- data/lib/samagotchi/session_commands.rb +32 -4
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +45 -257
- data/lib/samagotchi/session_metrics.rb +18 -21
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -1
- data/lib/samagotchi/terminal_ui.rb +25 -3
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -8
- data/lib/samagotchi/web/public/activity.js +2 -0
- data/lib/samagotchi/web/public/app.js +6 -8
- data/lib/samagotchi/web/public/command_complete.js +7 -2
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/index.html +4 -2
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +5 -3
- data/lib/samagotchi/web/public/thinking_chip.js +31 -21
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_html.js +10 -6
- metadata +9 -1
|
@@ -5,7 +5,7 @@ web/SSE clients) as a `:context_status` stream event. The telemetry itself is
|
|
|
5
5
|
not injected into the model's conversation; what the model gets is one short
|
|
6
6
|
line, as a tail system message (`kind: context`), when usage rises into a bucket
|
|
7
7
|
whose guidance asks it to change how it works (from the second threshold, 40%
|
|
8
|
-
by default, up; never on a fall
|
|
8
|
+
by default, up; never on a fall, and not again for a bucket a
|
|
9
9
|
resumed session's line already names):
|
|
10
10
|
|
|
11
11
|
`[CONTEXT: about 55% of the context window is in use (estimated; bucket=40plus). context moderate — prefer targeted and range reads over full-file dumps]`
|
|
@@ -19,8 +19,12 @@ carries a `status` string using this prefix:
|
|
|
19
19
|
|
|
20
20
|
Emission behavior:
|
|
21
21
|
|
|
22
|
-
-
|
|
23
|
-
|
|
22
|
+
- The estimate is emitted on every request (both loops, before each request),
|
|
23
|
+
so the web and the attached TUI get a fresh number each time.
|
|
24
|
+
- The model's own `[CONTEXT: …]` line is still gated: it is left only on a rise
|
|
25
|
+
into a guidance bucket (from the second threshold up), never on a fall or a
|
|
26
|
+
cadence tick, and not again for a bucket a resumed session's line already
|
|
27
|
+
names.
|
|
24
28
|
- This is warn-only behavior (no automatic history truncation).
|
|
25
29
|
- When the model server reports real `usage` fields in the stream payload, the
|
|
26
30
|
telemetry uses those actual token counts (prefixed `src=server`) instead of the
|
|
@@ -55,5 +59,4 @@ Configuration:
|
|
|
55
59
|
- `SAMAGOTCHI_CONTEXT_STATUS` (`true` by default): set to `false` or `0` to disable telemetry.
|
|
56
60
|
- `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` / `context.window_tokens`: context window size for when the server doesn't report one. chi asks llama.cpp for its real window first (`/props`, the per-slot `n_ctx`); this setting only fills in when it can't (mlx, oMLX, server down), and 256000 is the last resort.
|
|
57
61
|
- `SAMAGOTCHI_CONTEXT_CHARS_PER_TOKEN` (default `4.0`): heuristic ratio for char-to-token estimation.
|
|
58
|
-
- `SAMAGOTCHI_CONTEXT_STATUS_THRESHOLDS` (default `20,40,60,80`): comma-separated threshold percentages.
|
|
59
|
-
- `SAMAGOTCHI_CONTEXT_STATUS_CADENCE` (default `0`): emit every N rounds in addition to threshold crossings.
|
|
62
|
+
- `SAMAGOTCHI_CONTEXT_STATUS_THRESHOLDS` (default `20,40,60,80`): comma-separated threshold percentages. The buckets' guidance still gates the model's own `[CONTEXT: …]` line; nothing else reads `context.status_cadence` (it was removed once the event streamed on every request).
|
data/docs/internals/engine.md
CHANGED
|
@@ -212,6 +212,13 @@ One iteration with tool calls:
|
|
|
212
212
|
than that line). The model (both loops), the event and the after_tool_call hook get that text.
|
|
213
213
|
- Emits `:tool_call_completed` {iteration, call_count, call_index, tool, output, output_truncated, activity,
|
|
214
214
|
images, diff, waited_ms, view}, then fires `:after_tool_call` {iteration, tool, output, status}.
|
|
215
|
+
- The activity's status (`ToolActivity#tool_activity_status`): `error` for an `Error:` result and for an
|
|
216
|
+
execute that exited non-zero, except a grep-family search that found nothing (`ToolActivity.no_match?`:
|
|
217
|
+
exit 1, no stderr block, the last step is grep/egrep/fgrep/rg/ag/pgrep, no `&&` after a step that can fail
|
|
218
|
+
silently or sends output to /dev/null, no background `&`, no earlier exit/return or `set -e`; when unsure,
|
|
219
|
+
not a no-match). That one stays `ok` with `no_match: true` on the activity, which SessionMetrics keeps on the tool
|
|
220
|
+
record and the Bridge's TurnAccumulator on the part, for the web row's "no match" word. The model's text is
|
|
221
|
+
unchanged.
|
|
215
222
|
|
|
216
223
|
A denied call gets denial text in place of its output; the rest of the batch still runs. After a hook's
|
|
217
224
|
`stop_turn`, the gate denies every remaining call of the batch.
|
|
@@ -292,7 +299,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
292
299
|
|---|---|---|
|
|
293
300
|
| `:turn_started` | `prepare_turn` | `{session_id, prompt, turn_id, continue?, images?}` |
|
|
294
301
|
| `:reminder_injected` | due reminders went into the turn | `{reminders}` |
|
|
295
|
-
| `:context_status` | the
|
|
302
|
+
| `:context_status` | every request, the current estimate before it | `{iteration, ...}` |
|
|
296
303
|
| `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source}` (native adds `profile`, `profile_source`) |
|
|
297
304
|
| `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` |
|
|
298
305
|
| `:generation_retrying` | the transport retries the request, or the chat loop asks again for a step whose stream dropped mid-answer (`restarted: true`: what the step streamed is void, and the web's live step, the TUI's activity line, the Bridge's TurnAccumulator and the stream hooks' watch start it over) | `{iteration, attempt, max_retries, next_delay, error_class, error_message, status, restarted?}` |
|
|
@@ -331,14 +338,14 @@ Engine initialization (in this order, among other things):
|
|
|
331
338
|
@host_registry, @effective_model_name; @client = @host_registry.resolve(...).client
|
|
332
339
|
@given_profile = profile or nil (resolved on first need, so building an Engine makes no network call)
|
|
333
340
|
@guardrail_wiring, @question_desk
|
|
334
|
-
@hooks =
|
|
335
|
-
@tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins)
|
|
336
|
-
@kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
|
|
341
|
+
@extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
|
|
342
|
+
@tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins)
|
|
343
|
+
wire_kernel: @kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
|
|
337
344
|
@kernel.guardrail_gate = @guardrail_wiring.gate; @kernel.warmup = PromptWarmup.new
|
|
338
345
|
@hooks.runtime = hook_runtime
|
|
339
346
|
@native_backend = LLM::NativeBackend.new(kernel: @kernel)
|
|
340
347
|
@prompt_builder = SystemPrompt.new(...)
|
|
341
|
-
@session_observer with the metrics, the log subscriber and the thinking tails subscribed
|
|
348
|
+
@session_observer with the metrics, the log subscriber and the thinking tails subscribed (subscribe_session_observers)
|
|
342
349
|
|
|
343
350
|
Model switching, Engine#switch_model!(model_name, persist_default:, typed:):
|
|
344
351
|
→ checks the host (ModelProfile.check_host!), sets @effective_model_name
|
|
@@ -430,8 +437,9 @@ Both loops:
|
|
|
430
437
|
with its own nudge; when it is used up, the turn ends cancelled (`:hook`). A steer's cut asks again with the
|
|
431
438
|
steer and spends nothing.
|
|
432
439
|
- **Steering**: queued input goes in at iteration boundaries (`Steer.inject!`, `:pending_input_merged`).
|
|
433
|
-
- **Context status**: an estimate of the window's fill before each request
|
|
434
|
-
|
|
440
|
+
- **Context status**: an estimate of the window's fill before each request,
|
|
441
|
+
emitted on every request (`:context_status`) so the UI always has a current
|
|
442
|
+
number, with a guidance line on the tail only when it rises far enough.
|
|
435
443
|
- **Cancel**: text streamed before a cancel stays, marked `[interrupted]`.
|
|
436
444
|
- **The same events**: generation events, tool dispatch and tool call events, and the before/after generation
|
|
437
445
|
and tool call hooks.
|
|
@@ -476,6 +484,7 @@ Paths are under `lib/samagotchi/`.
|
|
|
476
484
|
| `tools/registry.rb` | The tools a session offers: schemas, handlers, order |
|
|
477
485
|
| `system_prompt.rb` | System prompt construction (stable part, volatile tail) |
|
|
478
486
|
| `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
|
|
487
|
+
| `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
|
|
479
488
|
| `guardrail_wiring.rb`, `guardrails/gate.rb` | The gate's context and approvals; the verdict per call |
|
|
480
489
|
| `events.rb` | Event type sets shared by the Worker, the Bridge, the TUI and the web |
|
|
481
490
|
| `cancellation_controller.rb` | A turn's cancel signal, and a generation's child controller |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Read Tool Size Guardrails
|
|
4
4
|
|
|
5
|
-
The `read` tool
|
|
5
|
+
The `read` tool applies adaptive limits to avoid accidental context exhaustion
|
|
6
6
|
when opening very large files (for example, VCR cassettes).
|
|
7
7
|
|
|
8
8
|
Behavior:
|
|
@@ -11,7 +11,8 @@ Behavior:
|
|
|
11
11
|
- Large files: return a head+tail preview plus truncation metadata.
|
|
12
12
|
- Extremely large files: return an error indicating the hard size limit.
|
|
13
13
|
|
|
14
|
-
Configuration
|
|
14
|
+
Configuration (each is also a config.yml key, `read.truncate_at_bytes` and so on, and a CLI
|
|
15
|
+
flag such as `--read-truncate-at-bytes`; see docs/configuration.md):
|
|
15
16
|
|
|
16
17
|
- `SAMAGOTCHI_READ_TRUNCATE_AT_BYTES` (default `65536`): files above this size return a preview instead of full content.
|
|
17
18
|
- `SAMAGOTCHI_READ_PREVIEW_BYTES` (default `12288`): total preview budget split across head and tail.
|
|
@@ -23,8 +24,8 @@ Optional preview telemetry:
|
|
|
23
24
|
|
|
24
25
|
Telemetry uses existing context estimation settings:
|
|
25
26
|
|
|
26
|
-
- `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS`
|
|
27
|
-
- `SAMAGOTCHI_CONTEXT_CHARS_PER_TOKEN`
|
|
27
|
+
- `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` (`context.window_tokens`)
|
|
28
|
+
- `SAMAGOTCHI_CONTEXT_CHARS_PER_TOKEN` (`context.chars_per_token`)
|
|
28
29
|
|
|
29
30
|
## Execute Tool Output Guardrails
|
|
30
31
|
|
|
@@ -33,13 +34,14 @@ The `execute` tool applies the same guardrail model to command output:
|
|
|
33
34
|
- Small stdout/stderr: returned in full.
|
|
34
35
|
- Large stdout/stderr: returned as head+tail previews with truncation metadata.
|
|
35
36
|
|
|
36
|
-
Configuration:
|
|
37
|
+
Configuration (config.yml `execute.*` keys and CLI flags too, as for `read`):
|
|
37
38
|
|
|
38
39
|
- `SAMAGOTCHI_EXECUTE_TRUNCATE_AT_BYTES` (default `65536`): output above this size is truncated.
|
|
39
40
|
- `SAMAGOTCHI_EXECUTE_PREVIEW_BYTES` (default `12288`): total preview budget split across head and tail.
|
|
40
41
|
- `SAMAGOTCHI_EXECUTE_TELEMETRY_THRESHOLD_PCT` (default `80`): include estimated output token impact only when threshold is crossed.
|
|
42
|
+
- `SAMAGOTCHI_EXECUTE_TIMEOUT_SEC` (`execute.timeout_sec`, default `120`): a command running longer is stopped and answers `Error: command timed out after …s`.
|
|
41
43
|
|
|
42
44
|
Implementation note:
|
|
43
45
|
|
|
44
|
-
- Shared logic lives in `lib/samagotchi/tools/output_guardrails.rb` and is used by
|
|
46
|
+
- Shared logic lives in `lib/samagotchi/tools/output_guardrails.rb` and is used by `read` and `execute`, and by `edit` (its env switches) and `delegate_wait` (the head+tail preview of a child's long reply).
|
|
45
47
|
- Additional tools that can emit large payloads should rely on this shared helper for consistent behavior.
|
data/docs/plugins.md
CHANGED
|
@@ -1210,7 +1210,8 @@ A plain read keeps no version.
|
|
|
1210
1210
|
**The nudge** (`nudge: true`). Some models, finding a skill's step broken,
|
|
1211
1211
|
skip it and go on without fixing the skill. In a turn that read a skill
|
|
1212
1212
|
(`memory_read` of a `skill_*` name, or `read` of its file), the first failing
|
|
1213
|
-
tool call after it (an `execute` that exited non-zero, a
|
|
1213
|
+
tool call after it (an `execute` that exited non-zero, except a grep-family
|
|
1214
|
+
search that found nothing, a tool error) steers
|
|
1214
1215
|
the model once (a call a guardrail or the user denied doesn't count as a
|
|
1215
1216
|
failed step): *"A step of skill release failed. Find out why before skipping
|
|
1216
1217
|
it; if the skill is out of date, fix it now: edit the step that changed in
|
data/docs/releasing.md
CHANGED
|
@@ -79,9 +79,12 @@ The agent does each step and stops where the user has to say yes.
|
|
|
79
79
|
8. **The user approves the `release` environment** in the Actions run
|
|
80
80
|
(Review deployments → Approve).
|
|
81
81
|
9. **Watch the run.** `gh run watch` (or `gh run list --workflow release.yml`).
|
|
82
|
-
It
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
It doesn't run the suites again: it checks that the CI workflow succeeded
|
|
83
|
+
on the tagged commit as a push to main (waiting while that run is still
|
|
84
|
+
going, stopping if it failed or never ran) and runs the full
|
|
85
|
+
`bundles:check`. Then it checks that the tag is `v` + VERSION and on main,
|
|
86
|
+
builds the gem, pushes it with a trusted-publishing token and creates the
|
|
87
|
+
GitHub release with the notes and the .gem attached.
|
|
85
88
|
10. **Verify the published gem** in a clean GEM_HOME:
|
|
86
89
|
```sh
|
|
87
90
|
tmp=$(mktemp -d)
|
data/docs/sessions.md
CHANGED
|
@@ -204,7 +204,7 @@ A session can **continue** an earlier one: "the next day of the same routine" (a
|
|
|
204
204
|
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Never more than the recap: no transcript. An idle new link holding only the note is kept, not discarded as empty.
|
|
205
205
|
- **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
|
|
206
206
|
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a plain REPL's session is refused. Its finished delegates are archived with it, as any archive does.
|
|
207
|
-
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`
|
|
207
|
+
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`SessionContinue::Refused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that is open in a chi REPL (close it there). An older `chi web` or CLI still running shows a moved delegate under the archived link until it is restarted.
|
|
208
208
|
- **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
|
|
209
209
|
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model names a host config.yml no longer has. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
|
|
210
210
|
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and not `dir`, `model` or `llm_context` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a model whose host is gone; 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
|
data/docs/testing.md
CHANGED
|
@@ -15,9 +15,11 @@ bug) or a broken spec: fix it, or disable that line with the reason (`# rubocop:
|
|
|
15
15
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
16
16
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
17
17
|
|
|
18
|
-
The web specs run in plain node. One that renders with lit-html (`turn_view.js`, `
|
|
19
|
-
`import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
|
|
20
|
-
file that loads it). The shim's `htmlOf(nodes)` and `canon(html)` compare markup
|
|
18
|
+
The web specs run in plain node. One that renders with lit-html (`turn_view.js`, `thinking_chip.js`,
|
|
19
|
+
`vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
|
|
20
|
+
never shipped; about 0.3 s per spec file that loads it). The shim's `htmlOf(nodes)` and `canon(html)` compare markup
|
|
21
|
+
without lit's marker comments. happy-dom 20 picks the wrong option of a `<select>` whose options lit inserted with
|
|
22
|
+
`?selected`: read the option marked `selected` (`option[selected]`), not `select.value`; the browser follows the mark.
|
|
21
23
|
|
|
22
24
|
## Worktrees
|
|
23
25
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# A session's analytics.json: the SessionMetrics snapshot with every turn
|
|
7
|
+
# and tool record, saved next to the session file (<session dir>/
|
|
8
|
+
# analytics.json). SessionMetrics#persist writes it; the session lists,
|
|
9
|
+
# `chi sessions stats`, the web's timing and a collector loading an earlier
|
|
10
|
+
# process's records read it. The records stay string-keyed hashes as
|
|
11
|
+
# saved: their keys grow over versions and every reader treats each one
|
|
12
|
+
# as optional.
|
|
13
|
+
module AnalyticsFile
|
|
14
|
+
NAME = "analytics.json"
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @param session_dir [String]
|
|
19
|
+
# @return [String]
|
|
20
|
+
def path(session_dir)
|
|
21
|
+
File.join(session_dir, NAME)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The parsed file, string-keyed; nil when it is missing, unreadable,
|
|
25
|
+
# not JSON or not an object (an older or broken file reads as none).
|
|
26
|
+
# @param session_dir [String]
|
|
27
|
+
# @return [Hash, nil]
|
|
28
|
+
def read(session_dir)
|
|
29
|
+
data = JSON.parse(File.read(path(session_dir)))
|
|
30
|
+
data.is_a?(Hash) ? data : nil
|
|
31
|
+
rescue JSON::ParserError, SystemCallError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -169,6 +169,7 @@ module Samagotchi
|
|
|
169
169
|
|
|
170
170
|
activity = { tool: part[:tool], status: part[:status], params: part[:params], **title }
|
|
171
171
|
activity[:action] = part[:action] if part[:action]
|
|
172
|
+
activity[:no_match] = true if part[:no_match]
|
|
172
173
|
completed = { type: :tool_call_completed, **call, output: part[:output], output_truncated: !!part[:output_truncated],
|
|
173
174
|
activity: activity }
|
|
174
175
|
completed[:duration_ms] = part[:duration_ms] unless part[:duration_ms].nil?
|
|
@@ -288,6 +289,7 @@ module Samagotchi
|
|
|
288
289
|
output = event[:output].to_s
|
|
289
290
|
capped = output.length > @max_output_chars
|
|
290
291
|
tool[:status] = (event.dig(:activity, :status) || "ok").to_s
|
|
292
|
+
tool[:no_match] = true if event.dig(:activity, :no_match)
|
|
291
293
|
tool[:output] = capped ? output[0, @max_output_chars] : output.dup
|
|
292
294
|
tool[:output_truncated] = capped || !!event[:output_truncated]
|
|
293
295
|
tool[:images] = event[:images] if event[:images]
|
|
@@ -293,8 +293,8 @@ module Samagotchi
|
|
|
293
293
|
@stderr.puts "Usage: chi bundle uninstall <bundle> [--scope system|project] [--force]"
|
|
294
294
|
return CLI::Exit::USAGE
|
|
295
295
|
end
|
|
296
|
-
|
|
297
|
-
return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name,
|
|
296
|
+
record = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).record
|
|
297
|
+
return uninstall_profile(bundle_name, force: force) if Samagotchi::MemoryBundle::Profile.installed_meta?(bundle_name, record)
|
|
298
298
|
|
|
299
299
|
uninstaller = Samagotchi::MemoryBundle::Uninstaller.new(name: bundle_name, scope: scope, force: force)
|
|
300
300
|
begin
|
|
@@ -370,7 +370,7 @@ module Samagotchi
|
|
|
370
370
|
@stdout.puts "Bundle '#{name}' not installed."
|
|
371
371
|
return 0
|
|
372
372
|
end
|
|
373
|
-
@stdout.puts "Bundle: #{name} v#{st[:provenance]
|
|
373
|
+
@stdout.puts "Bundle: #{name} v#{st[:provenance].version} scope=#{status_scope(st)} installed=#{st[:provenance].installed_at}"
|
|
374
374
|
if st[:scope_error]
|
|
375
375
|
@stdout.puts "Target: (unknown scope: this chi can't resolve it; upgrade chi or reinstall the bundle)"
|
|
376
376
|
else
|
|
@@ -380,7 +380,7 @@ module Samagotchi
|
|
|
380
380
|
next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
|
|
381
381
|
|
|
382
382
|
mods = []
|
|
383
|
-
mods << "conflict (kept your edits over v#{st[:provenance]
|
|
383
|
+
mods << "conflict (kept your edits over v#{st[:provenance].version}: chi bundle diff #{name} #{k})" if info[:conflict]
|
|
384
384
|
mods << "modified" if info[:modified]
|
|
385
385
|
mods << "missing" if info[:missing]
|
|
386
386
|
mods << "no-index" unless info[:index_present] || info[:overlay]
|
|
@@ -389,23 +389,19 @@ module Samagotchi
|
|
|
389
389
|
@stdout.puts " #{k}: #{label}"
|
|
390
390
|
end
|
|
391
391
|
# Hooks
|
|
392
|
-
hooks = st[:provenance]
|
|
393
|
-
trust = st[:provenance]
|
|
394
|
-
commit = st[:provenance]
|
|
392
|
+
hooks = st[:provenance].hooks
|
|
393
|
+
trust = st[:provenance].trust_level || "experimental"
|
|
394
|
+
commit = st[:provenance].source_commit
|
|
395
395
|
@stdout.puts " trust_level: #{trust}" if hooks.any? || trust
|
|
396
396
|
@stdout.puts " source_commit: #{commit}" if commit
|
|
397
397
|
if hooks.any?
|
|
398
398
|
@stdout.puts " Hooks (#{hooks.size}):"
|
|
399
|
-
hooks.each do |hk,
|
|
400
|
-
|
|
401
|
-
event = m["event"] || ""
|
|
402
|
-
on_error = m["on_error"] || "skip"
|
|
403
|
-
prio = m["priority"] || 100
|
|
404
|
-
hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk.to_s)
|
|
399
|
+
hooks.each do |hk, hook|
|
|
400
|
+
hook_path = File.join(Samagotchi::MemoryBundle::Provenance.new(name: name).hooks_dir, hk)
|
|
405
401
|
exists = File.exist?(hook_path) ? "ok" : "missing"
|
|
406
|
-
@stdout.puts " #{hk}: event=#{event} on_error=#{on_error} priority=#{
|
|
402
|
+
@stdout.puts " #{hk}: event=#{hook.event} on_error=#{hook.on_error} priority=#{hook.priority} [#{exists}]"
|
|
407
403
|
end
|
|
408
|
-
@stdout.puts " requires_chi: #{st[:provenance]
|
|
404
|
+
@stdout.puts " requires_chi: #{st[:provenance].requires_chi}" if st[:hooks_requires_failure]
|
|
409
405
|
@stdout.puts " not loaded: #{st[:hooks_requires_failure]}" if st[:hooks_requires_failure]
|
|
410
406
|
end
|
|
411
407
|
if (plugin = st[:plugin])
|
|
@@ -415,28 +411,28 @@ module Samagotchi
|
|
|
415
411
|
@stdout.puts " not loaded: #{plugin[:requires_failure]}" if plugin[:requires_failure]
|
|
416
412
|
end
|
|
417
413
|
st[:needs].each { |need| @stdout.puts " #{Samagotchi::MemoryBundle::Status.need_line(need)}" }
|
|
418
|
-
(st[:provenance]
|
|
419
|
-
state = Samagotchi::MemoryBundle::Provenance.new(name: member
|
|
414
|
+
(st[:provenance].includes || []).each do |member|
|
|
415
|
+
state = Samagotchi::MemoryBundle::Provenance.new(name: member).installed? ? "installed" : "not installed: chi bundle install #{member}"
|
|
420
416
|
@stdout.puts " includes #{member} [#{state}]"
|
|
421
417
|
end
|
|
422
418
|
else
|
|
423
419
|
bundles = Samagotchi::MemoryBundle::Provenance.each_installed.to_a
|
|
424
420
|
@stdout.puts "No installed bundles." if bundles.empty?
|
|
425
|
-
bundles.each do |bname,
|
|
426
|
-
next @stdout.puts(" #{bname} (manifest.json unreadable)") if
|
|
421
|
+
bundles.each do |bname, record|
|
|
422
|
+
next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
|
|
427
423
|
|
|
428
424
|
st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
|
|
429
425
|
mods = st[:files].values.count { |v| v[:modified] || v[:missing] }
|
|
430
|
-
hooks_count =
|
|
426
|
+
hooks_count = st[:provenance].hooks.size
|
|
431
427
|
hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
|
|
432
428
|
plugin = st[:plugin]
|
|
433
429
|
mods += 1 if plugin && (plugin[:state] != "ok" || plugin[:requires_failure])
|
|
434
430
|
mods += 1 if st[:hooks_requires_failure]
|
|
435
431
|
mods += 1 if st[:scope_error]
|
|
436
432
|
plugin_info = plugin ? " plugin=#{plugin[:file]}" : ""
|
|
437
|
-
includes = st[:provenance]
|
|
433
|
+
includes = st[:provenance].includes
|
|
438
434
|
includes_info = includes ? " includes=#{includes.join(",")}" : ""
|
|
439
|
-
@stdout.puts " #{bname} v#{st[:provenance]
|
|
435
|
+
@stdout.puts " #{bname} v#{st[:provenance].version} scope=#{status_scope(st)} files=#{st[:files].size}#{hook_info}#{plugin_info}#{includes_info} issues=#{mods}"
|
|
440
436
|
end
|
|
441
437
|
end
|
|
442
438
|
0
|
|
@@ -453,19 +449,19 @@ module Samagotchi
|
|
|
453
449
|
@stderr.puts "Usage: chi bundle diff <bundle> [file]"; return CLI::Exit::USAGE
|
|
454
450
|
end
|
|
455
451
|
prov = Samagotchi::MemoryBundle::Provenance.new(name: bname)
|
|
456
|
-
|
|
457
|
-
unless
|
|
452
|
+
record = prov.record
|
|
453
|
+
unless record
|
|
458
454
|
@stderr.puts "Bundle '#{bname}' not installed"; return 1
|
|
459
455
|
end
|
|
460
|
-
scope =
|
|
456
|
+
scope = record.effective_scope
|
|
461
457
|
begin
|
|
462
458
|
target_dir = Samagotchi::MemoryPaths.scope_dir!(scope)
|
|
463
459
|
rescue ArgumentError => e
|
|
464
460
|
@stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
|
|
465
461
|
end
|
|
466
|
-
files =
|
|
467
|
-
hooks =
|
|
468
|
-
plugin_path = prov.plugin_path(
|
|
462
|
+
files = record.files
|
|
463
|
+
hooks = record.hooks
|
|
464
|
+
plugin_path = prov.plugin_path(record)
|
|
469
465
|
plugin_file = plugin_path && File.basename(plugin_path)
|
|
470
466
|
show_plugin = lambda do
|
|
471
467
|
@stdout.puts "=== plugin/#{plugin_file} ==="
|
|
@@ -474,11 +470,11 @@ module Samagotchi
|
|
|
474
470
|
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
475
471
|
@stdout.puts "--- current (on-disk) ---"
|
|
476
472
|
@stdout.puts File.exist?(plugin_path) ? File.read(plugin_path) : "(missing)"
|
|
477
|
-
@stdout.puts "--- metadata: sha256=#{
|
|
473
|
+
@stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
|
|
478
474
|
end
|
|
479
475
|
if file_arg
|
|
480
476
|
# Try file first, then hook
|
|
481
|
-
if
|
|
477
|
+
if record.owns?(file_arg)
|
|
482
478
|
base = prov.base_path(file_arg)
|
|
483
479
|
cur = File.join(target_dir, file_arg)
|
|
484
480
|
@stdout.puts "=== #{file_arg} ==="
|
|
@@ -486,7 +482,7 @@ module Samagotchi
|
|
|
486
482
|
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
487
483
|
@stdout.puts "--- current (on-disk) ---"
|
|
488
484
|
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
489
|
-
elsif hooks.key?(file_arg
|
|
485
|
+
elsif hooks.key?(file_arg)
|
|
490
486
|
base = prov.base_path(file_arg)
|
|
491
487
|
cur = File.join(prov.hooks_dir, file_arg)
|
|
492
488
|
@stdout.puts "=== hooks/#{file_arg} ==="
|
|
@@ -495,11 +491,7 @@ module Samagotchi
|
|
|
495
491
|
@stdout.puts "--- current (on-disk) ---"
|
|
496
492
|
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
497
493
|
# Show hook metadata
|
|
498
|
-
|
|
499
|
-
if meta
|
|
500
|
-
m = meta.transform_keys(&:to_s)
|
|
501
|
-
@stdout.puts "--- metadata: event=#{m["event"]} on_error=#{m["on_error"]} priority=#{m["priority"]} sha256=#{m["sha256"]}"
|
|
502
|
-
end
|
|
494
|
+
@stdout.puts hook_metadata_line(hooks[file_arg])
|
|
503
495
|
elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
|
|
504
496
|
show_plugin.call
|
|
505
497
|
else
|
|
@@ -508,8 +500,7 @@ module Samagotchi
|
|
|
508
500
|
end
|
|
509
501
|
@stdout.puts ""
|
|
510
502
|
else
|
|
511
|
-
|
|
512
|
-
keys.each do |k|
|
|
503
|
+
files.each_key do |k|
|
|
513
504
|
base = prov.base_path(k)
|
|
514
505
|
cur = File.join(target_dir, k)
|
|
515
506
|
@stdout.puts "=== #{k} ==="
|
|
@@ -520,7 +511,7 @@ module Samagotchi
|
|
|
520
511
|
@stdout.puts ""
|
|
521
512
|
end
|
|
522
513
|
# Hooks diff
|
|
523
|
-
hooks.
|
|
514
|
+
hooks.each do |k, hook|
|
|
524
515
|
base = prov.base_path(k)
|
|
525
516
|
cur = File.join(prov.hooks_dir, k)
|
|
526
517
|
@stdout.puts "=== hooks/#{k} ==="
|
|
@@ -528,11 +519,7 @@ module Samagotchi
|
|
|
528
519
|
@stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
|
|
529
520
|
@stdout.puts "--- current (on-disk) ---"
|
|
530
521
|
@stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
|
|
531
|
-
|
|
532
|
-
if meta
|
|
533
|
-
m = meta.transform_keys(&:to_s)
|
|
534
|
-
@stdout.puts "--- metadata: event=#{m["event"]} on_error=#{m["on_error"]} priority=#{m["priority"]} sha256=#{m["sha256"]}"
|
|
535
|
-
end
|
|
522
|
+
@stdout.puts hook_metadata_line(hook)
|
|
536
523
|
@stdout.puts ""
|
|
537
524
|
end
|
|
538
525
|
if plugin_file
|
|
@@ -586,6 +573,10 @@ module Samagotchi
|
|
|
586
573
|
"includes=#{here.join(",")}#{" left out=#{gone.join(",")}" unless gone.empty?}"
|
|
587
574
|
end
|
|
588
575
|
|
|
576
|
+
def hook_metadata_line(hook)
|
|
577
|
+
"--- metadata: event=#{hook.event} on_error=#{hook.on_error} priority=#{hook.priority} sha256=#{hook.sha256}"
|
|
578
|
+
end
|
|
579
|
+
|
|
589
580
|
def build(rest)
|
|
590
581
|
parsed = parse_flags(BUILD_FLAGS, rest, "build", help: BUILD_HELP)
|
|
591
582
|
return parsed if parsed.is_a?(Integer)
|
|
@@ -65,19 +65,18 @@ module Samagotchi
|
|
|
65
65
|
# entry name → marker, for the installed bundles of +scope+ with a
|
|
66
66
|
# missing need. A manifest.json or needs: that doesn't parse is skipped.
|
|
67
67
|
def self.entry_markers(scope, path:, bundles_dir:)
|
|
68
|
-
MemoryBundle::Provenance.each_installed(dir: bundles_dir).each_with_object({}) do |(_name,
|
|
69
|
-
next if
|
|
70
|
-
next unless data[:needs].is_a?(Array) && !data[:needs].empty?
|
|
68
|
+
MemoryBundle::Provenance.each_installed(dir: bundles_dir).each_with_object({}) do |(_name, bundle), acc|
|
|
69
|
+
next if bundle.error? || bundle.effective_scope != scope.to_s || !bundle.needs?
|
|
71
70
|
|
|
72
71
|
needs = begin
|
|
73
|
-
MemoryBundle::Manifest.parse_needs(
|
|
72
|
+
MemoryBundle::Manifest.parse_needs(bundle.needs)
|
|
74
73
|
rescue MemoryBundle::Manifest::ValidationError
|
|
75
74
|
next
|
|
76
75
|
end
|
|
77
76
|
marker = marker(missing(needs, path: path))
|
|
78
77
|
next unless marker
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
bundle.files.each_key do |key|
|
|
81
80
|
name = MutedMemories.normalize(key)
|
|
82
81
|
acc[name] ||= marker if name
|
|
83
82
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skills
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.7
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Skills (skill_<name> memories, the steps of a task done together): /skill save, list, show and diff; older versions kept with a short diff line on every update; a nudge when a followed skill's step fails"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:42f535eb1dd616bdf2e027031538c9419767c531e2b52e2baa8f1cc83accaba7
|
|
10
10
|
requires_chi: ">= 0.11.0"
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
# fixing the skill: in a turn that read a skill (memory_read of a skill_*
|
|
26
26
|
# name, or a read of its file), the first failing tool call after it (an
|
|
27
27
|
# execute with "exit: N", N ≠ 0, or with no exit line an Error: line near
|
|
28
|
-
# the top; any tool's "[tool] Error: …"
|
|
28
|
+
# the top; any tool's "[tool] Error: …"; not a grep that found nothing,
|
|
29
|
+
# which core counts ok) steers the model once to find out
|
|
29
30
|
# why and fix the skill, unless a skill read was changed already (by any
|
|
30
31
|
# tool, see above). A call a guardrail or the user denied (its first line
|
|
31
32
|
# "[tool] Error: blocked by guardrail: …", "… denied by guardrail (…): …" or
|
|
@@ -18,7 +18,7 @@ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/confi
|
|
|
18
18
|
|
|
19
19
|
Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`); leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
|
|
20
20
|
|
|
21
|
-
**Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds
|
|
21
|
+
**Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds`, `memory.index_warn_tokens`, `thinking.turn_preamble`, `default.max_tokens`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
|
|
22
22
|
|
|
23
23
|
Example `config.yml` (new nested form, preferred):
|
|
24
24
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.54.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:dfc19266259a9af1e0d769b7e0f67996b740b0c9921fdf3dadad973f6e5c144a
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:45064ac6b19863e1646d15891d0e9cc7a8f10ab981a0d25d6e13891591538b8a
|
|
10
10
|
memory_guide.md: sha256:15cd3b24eda853cc458371c6c75809a2d4b5352311d075aaa4384926b5221c27
|
|
11
11
|
delegated.md: sha256:aedaa1e756a244904c5560fff0424e06fb468bbdcb0358bccefe321d67cd2614
|
|
@@ -14,6 +14,8 @@ module Samagotchi
|
|
|
14
14
|
# Loaded on first use: session_manager requires terminal_ui, which
|
|
15
15
|
# requires KernelLoop and so the tools (a require cycle otherwise).
|
|
16
16
|
autoload :SessionManager, File.expand_path("session_manager", __dir__)
|
|
17
|
+
# SessionContinue::OpenChild, the rows a plan refuses: loaded on first use too.
|
|
18
|
+
autoload :SessionContinue, File.expand_path("session_continue", __dir__)
|
|
17
19
|
|
|
18
20
|
# A continue moving the previous link's open delegates to the chain's new
|
|
19
21
|
# link (SessionManager.continue_session), instead of archiving them with
|
|
@@ -45,7 +47,7 @@ module Samagotchi
|
|
|
45
47
|
# What a continue does with the previous link's children: +move+ the
|
|
46
48
|
# direct children whose subtree has an open node (they go to the new
|
|
47
49
|
# link, their subtrees with them), +refuse+ the open ones it can't move
|
|
48
|
-
# (
|
|
50
|
+
# (SessionContinue::OpenChild, why: an older chi worker or a chi REPL).
|
|
49
51
|
Plan = Data.define(:move, :refuse)
|
|
50
52
|
# A move under way: from the session, the children moved, and those
|
|
51
53
|
# already sent their note (so a move finished again notes no one twice).
|
|
@@ -55,8 +57,8 @@ module Samagotchi
|
|
|
55
57
|
|
|
56
58
|
module_function
|
|
57
59
|
|
|
58
|
-
# @param open [Array<
|
|
59
|
-
# nodes, at any depth (
|
|
60
|
+
# @param open [Array<SessionContinue::OpenChild>] the previous link's open
|
|
61
|
+
# nodes, at any depth (SessionContinue.open_children)
|
|
60
62
|
# @return [Plan]
|
|
61
63
|
def plan(prev_id, open:, state_dir:)
|
|
62
64
|
open_ids = open.map(&:id)
|
|
@@ -77,7 +79,7 @@ module Samagotchi
|
|
|
77
79
|
|
|
78
80
|
why = unmovable(row[:id], state_dir: state_dir)
|
|
79
81
|
if why
|
|
80
|
-
refuse <<
|
|
82
|
+
refuse << SessionContinue::OpenChild.new(id: row[:id], short_id: row[:id][0, 8], why: why)
|
|
81
83
|
else
|
|
82
84
|
move << row[:id]
|
|
83
85
|
end
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -156,7 +156,6 @@ module Samagotchi
|
|
|
156
156
|
# (MemoryBundle::IndexSize); 0: no note. `memory:` is its own section, not the `memories:` preload list.
|
|
157
157
|
Entry.new(key: "memory.index_warn_tokens", yaml_path: %w[memory index_warn_tokens], type: :integer, default: 2500, expose: %i[env config cli]),
|
|
158
158
|
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
159
|
-
Entry.new(key: "context.status_cadence", yaml_path: %w[context status_cadence], type: :integer, default: 0, expose: %i[env config cli]),
|
|
160
159
|
# Attached context (ContextSources): how often a source's command runs when it names no --every (seconds; less
|
|
161
160
|
# than 30 counts as 30).
|
|
162
161
|
Entry.new(key: "context.every_seconds", yaml_path: %w[context every_seconds], type: :integer, default: 300, expose: %i[env config]),
|
|
@@ -22,7 +22,7 @@ module Samagotchi
|
|
|
22
22
|
USAGE = <<~TEXT
|
|
23
23
|
Usage: chi context <add|push|ls|show|refresh|rm|mute|unmute> [options] [TARGET]
|
|
24
24
|
add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
|
|
25
|
-
add URL [--why TEXT] TARGET
|
|
25
|
+
add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
|
|
26
26
|
a source: CMD prints its text (plain, or JSON {"text", "summary", "wake", "hint"});
|
|
27
27
|
--push: text comes from chi context push. --every: how often CMD runs
|
|
28
28
|
(seconds, at least 30; default context.every_seconds). --why: why it's attached;
|