samagotchi 0.68.0 → 0.70.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 +76 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +6 -1
- data/docs/configuration.md +31 -8
- data/docs/context.md +15 -6
- data/docs/guardrails.md +23 -1
- data/docs/hooks.md +5 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +25 -13
- data/docs/internals/prompt-caching.md +9 -2
- data/docs/internals/tool-guardrails.md +61 -1
- data/docs/plugins.md +66 -38
- data/docs/releasing.md +14 -6
- data/docs/sessions.md +5 -5
- data/docs/sub-agent.md +2 -1
- data/docs/testing.md +8 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +10 -8
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +4 -4
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +6 -6
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +3 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +6 -5
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +10 -8
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +4 -4
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +6 -6
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +3 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +6 -5
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +6 -21
- data/lib/samagotchi/context_absorber.rb +35 -21
- data/lib/samagotchi/engine.rb +7 -2
- data/lib/samagotchi/guardrails/targets.rb +9 -2
- data/lib/samagotchi/host_config.rb +1 -29
- data/lib/samagotchi/host_model.rb +0 -7
- data/lib/samagotchi/kernel_loop.rb +34 -7
- data/lib/samagotchi/llm/errors.rb +47 -1
- data/lib/samagotchi/llm/http.rb +20 -2
- data/lib/samagotchi/llm_context_stale.rb +8 -2
- data/lib/samagotchi/log_subscriber.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +3 -0
- data/lib/samagotchi/model_price.rb +2 -4
- data/lib/samagotchi/peer_sources.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/peer.rb +35 -16
- data/lib/samagotchi/session_metrics.rb +4 -1
- data/lib/samagotchi/system_prompt.rb +19 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -2
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_output_cut.rb +312 -0
- data/lib/samagotchi/tool_output_files.rb +73 -0
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +70 -15
- data/lib/samagotchi/toolkit.rb +27 -14
- data/lib/samagotchi/toolkit_binding.rb +8 -2
- data/lib/samagotchi/toolkit_command.rb +20 -4
- data/lib/samagotchi/tools/builtin_calls.rb +45 -1
- data/lib/samagotchi/tools/delegate.rb +4 -1
- data/lib/samagotchi/tools/delegate_wait.rb +12 -1
- data/lib/samagotchi/tools/ref_text.rb +27 -0
- data/lib/samagotchi/tools/registry.rb +95 -8
- data/lib/samagotchi/tools/task_wait.rb +4 -1
- data/lib/samagotchi/tools/tool_index.rb +223 -72
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -9
- data/lib/samagotchi/web/message_parts.rb +9 -4
- data/lib/samagotchi/web/public/activity.js +3 -1
- data/lib/samagotchi/web/public/app.js +123 -22
- data/lib/samagotchi/web/public/chains.js +19 -2
- data/lib/samagotchi/web/public/families_view.js +15 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/model_picker.js +21 -6
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +5 -2
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/toast_place.js +16 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +11 -4
- data/lib/samagotchi/web/public/turn_html.js +4 -2
- data/lib/samagotchi/worker.rb +2 -2
- data/lib/samagotchi/worker_inbound.rb +3 -0
- metadata +5 -1
data/docs/plugins.md
CHANGED
|
@@ -25,6 +25,8 @@ plugin:
|
|
|
25
25
|
file: plugin.rb
|
|
26
26
|
sha256: sha256:6a1a7022… # shasum -a 256 plugin.rb
|
|
27
27
|
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's hooks/ check it too
|
|
28
|
+
# requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
|
|
29
|
+
# to the chi being released (docs/releasing.md)
|
|
28
30
|
needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -294,6 +296,8 @@ is what `targets:` says:
|
|
|
294
296
|
(chi's `call_tool`, which calls a tool a [source](#chitool_sourcecall-acts_as-label-wait-prefix) published). Rules keyed by that tool's name match the
|
|
295
297
|
call too, as well as rules on the tool's own name. It can't be one of
|
|
296
298
|
chi's own tools or another bundle's tool: that is dropped (and logged).
|
|
299
|
+
The "another bundle's tool" drop doesn't apply to chi's `call_tool`: its
|
|
300
|
+
`acts_as` is always a tool its sources published.
|
|
297
301
|
- `args:`: the arguments it acts with (a Hash), when they aren't the call's
|
|
298
302
|
own (a dispatcher's inner arguments). A call with no `command:` or
|
|
299
303
|
`paths:` is asked about with them, and "allow this call" is keyed by them.
|
|
@@ -583,15 +587,32 @@ chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
|
|
|
583
587
|
|
|
584
588
|
Tools the model reaches through a search instead of declaring them one by
|
|
585
589
|
one: the plugin publishes them into chi's **tool index**, and chi declares
|
|
586
|
-
two tools in all, whatever the number of sources: `find_tools
|
|
587
|
-
search that answers each match's `inputSchema`) and
|
|
588
|
-
`<group>/<tool>`; its row label is the
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
590
|
+
two tools in all, whatever the number of sources: `find_tools(query,
|
|
591
|
+
group?)` (a keyword search that answers each match's `inputSchema`) and
|
|
592
|
+
`call_tool(tool, args)` (calls one as `<group>/<tool>`; its row label is the
|
|
593
|
+
first source's `label`). The index has one group of its own, **`chi`**: the
|
|
594
|
+
built-ins the session's toolkit leaves out but doesn't remove
|
|
595
|
+
([toolkits](configuration.md#toolkits)), each with its registered schema.
|
|
596
|
+
The two tools are registered once, after the plugins load, and declared per
|
|
597
|
+
toolkit: when a plugin registered a source then, or when the toolkit has a
|
|
598
|
+
reachable built-in (`full` with no source declares neither). Nothing a
|
|
599
|
+
source publishes later changes the request's tools, only what a search
|
|
600
|
+
answers, so the prompt cache keeps: the search tool's description lists the
|
|
601
|
+
source groups as they were at load, and a `chi` line first, per toolkit.
|
|
602
|
+
The mcp bundle is the only source so far (a group per server). A source's
|
|
603
|
+
group named `chi`, or named like an earlier group, is left out of search,
|
|
604
|
+
lookup and the description, with one warning per name.
|
|
605
|
+
|
|
606
|
+
**A built-in through `call_tool` runs as that built-in.** `call_tool(tool:
|
|
607
|
+
"chi/delegate", args: {…})` (or a bare `delegate` while it's reachable) is
|
|
608
|
+
turned into the `delegate` call itself before the guardrails, so rules on
|
|
609
|
+
`delegate`, approvals (keyed `delegate:…`, the same as a direct call's),
|
|
610
|
+
`before_tool_call`/`after_tool_call` hooks and the row see `delegate`, with
|
|
611
|
+
`call[:via] == "call_tool"`; the row reads "delegate (via call_tool)", live
|
|
612
|
+
and after a reload. `tool: call_tool` rules see MCP calls and misses only. A
|
|
613
|
+
`chi/` name the toolkit removes or declares, or any built-in once
|
|
614
|
+
`call_tool` itself is removed, is refused by its name before the guardrails
|
|
615
|
+
(no hook, no card).
|
|
595
616
|
|
|
596
617
|
```ruby
|
|
597
618
|
@source = chi.tool_source(label: "mcp", prefix: "mcp_",
|
|
@@ -621,7 +642,8 @@ index = Samagotchi::Tools::ToolIndex
|
|
|
621
642
|
block returns (a String, a `ToolResult` with images).
|
|
622
643
|
- **Guardrails** see a `call_tool` as the tool it calls: its `acts_as`, its
|
|
623
644
|
arguments and `"<group>: <tool>"` as the question's label
|
|
624
|
-
([acts_as:](#guardrails))
|
|
645
|
+
([acts_as:](#guardrails)), even when another bundle has a tool of that
|
|
646
|
+
name (only the core-tool drop applies). A call made before its tool is published is
|
|
625
647
|
named by `acts_as` from the names alone; a bare name with `prefix` is
|
|
626
648
|
taken as an `acts_as` as given. The `acts_as:` hook must give exactly the
|
|
627
649
|
`acts_as` the source publishes for that tool: a call whose tool isn't the
|
|
@@ -632,24 +654,24 @@ index = Samagotchi::Tools::ToolIndex
|
|
|
632
654
|
what the two tools take in every request (none when other tools had both
|
|
633
655
|
names). A `Group` in a state other than the three, holding another group's
|
|
634
656
|
tool, or a `Tool` with a blank `acts_as` raises `ArgumentError`.
|
|
635
|
-
- `find_tools` and `call_tool` aren't chi's own tools:
|
|
636
|
-
them (unless its `remove:` names
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
declares neither.
|
|
657
|
+
- `find_tools` and `call_tool` aren't chi's own tools: a toolkit that needs
|
|
658
|
+
them declares them (unless its `remove:` names one; `call_tool` stays
|
|
659
|
+
usable with `find_tools` removed). A bundle that declares tools of other
|
|
660
|
+
names itself (an mcp bundle before 0.7.0 has `find_mcp_tools` and
|
|
661
|
+
`mcp_call`) keeps them; with no source, `full` declares neither of chi's.
|
|
640
662
|
|
|
641
663
|
### `chi.peer_source(name, label:)`
|
|
642
664
|
|
|
643
665
|
Declares a context source as a peer agent's answers: the peer bundles'
|
|
644
|
-
`claude-
|
|
666
|
+
`claude-outbox` and `codex-outbox`. Such a source, when it is a pushed source
|
|
645
667
|
of the session's own, wakes the session by the peer loop's rules instead of
|
|
646
|
-
the 10-minute window: up to `context.peer_turns` wake turns in a row,
|
|
647
|
-
for an answer whose verdict is `VERDICT OK`,
|
|
648
|
-
peer and keeps the turn within the user's task (see [Waking: peer
|
|
668
|
+
the 10-minute window: up to `context.peer_turns` wake turns in a row, one
|
|
669
|
+
closing wake (outside that count) for an answer whose verdict is `VERDICT OK`,
|
|
670
|
+
and a wake line that names the peer and keeps the turn within the user's task (see [Waking: peer
|
|
649
671
|
sources](context.md#peer-sources-the-peer-loop)).
|
|
650
672
|
|
|
651
673
|
```ruby
|
|
652
|
-
chi.peer_source "codex-
|
|
674
|
+
chi.peer_source "codex-outbox", label: "Codex"
|
|
653
675
|
```
|
|
654
676
|
|
|
655
677
|
- `name`: a context source name (as `chi context add` takes it). Another
|
|
@@ -659,7 +681,7 @@ chi.peer_source "codex-review", label: "Codex"
|
|
|
659
681
|
|
|
660
682
|
The declaration lives in the session's process only, like a tool: a source
|
|
661
683
|
file can't make itself a peer's, and uninstalling the bundle ends it.
|
|
662
|
-
Subclasses of `Samagotchi::Plugin::Peer` (below) declare their `
|
|
684
|
+
Subclasses of `Samagotchi::Plugin::Peer` (below) declare their `OUTBOX`
|
|
663
685
|
source. Needs chi 0.67.0 (guard it with `chi.respond_to?(:peer_source)`).
|
|
664
686
|
|
|
665
687
|
### `chi.ctx`
|
|
@@ -1064,7 +1086,7 @@ bundles:
|
|
|
1064
1086
|
|
|
1065
1087
|
- **Tools: search, then call.** The servers' tools are kept in chi's tool index;
|
|
1066
1088
|
the model has two fixed tools:
|
|
1067
|
-
- `find_tools(query,
|
|
1089
|
+
- `find_tools(query, group?)` searches it by keywords over the
|
|
1068
1090
|
server's name, the tool's name and its description, and answers up to 5
|
|
1069
1091
|
tools, each with its name `<server>/<tool>`, its description and its
|
|
1070
1092
|
whole `inputSchema`. A word counts more the fewer tools have it (a word
|
|
@@ -1089,7 +1111,7 @@ bundles:
|
|
|
1089
1111
|
`args` are typed by the tool's
|
|
1090
1112
|
`inputSchema` (numbers and booleans given as text, JSON text for an
|
|
1091
1113
|
object). A name it doesn't know answers the closest ones (`Error: no
|
|
1092
|
-
|
|
1114
|
+
tool chrome/open_url. Closest: chrome/new_page, …`). Its row shows
|
|
1093
1115
|
`mcp chrome/take_screenshot fullPage=true`.
|
|
1094
1116
|
- A tool turn takes a few more steps than when every tool was declared
|
|
1095
1117
|
(the P1 spike: about 1.5 more on a 30-tool server), and each request is
|
|
@@ -1159,8 +1181,9 @@ bundles:
|
|
|
1159
1181
|
session" (or in this repo) allows that tool with those arguments only.
|
|
1160
1182
|
(Approvals stored before 0.6.0, under the old name, don't carry over, nor
|
|
1161
1183
|
ones stored before 0.7.0 under `mcp_call`: each MCP tool is asked about once more.)
|
|
1162
|
-
Two things see `call_tool`, not the tool: hooks (a `before_tool_call`
|
|
1163
|
-
event's `tool:` is `"call_tool"`, the tool in its args
|
|
1184
|
+
Two things see `call_tool`, not the MCP tool: hooks (a `before_tool_call`
|
|
1185
|
+
event's `tool:` is `"call_tool"`, the tool in its args; a built-in reached
|
|
1186
|
+
through `call_tool` is the exception, it fires as that built-in), and loop-guard's
|
|
1164
1187
|
`ignore_tools` (`call_tool` ignores every MCP call; calls to different
|
|
1165
1188
|
tools, or with different args, stay distinct, since the tool is in the
|
|
1166
1189
|
args).
|
|
@@ -1692,7 +1715,7 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1692
1715
|
- **Pairing is Claude's.** chi can't see Claude's inbox, so Claude attaches
|
|
1693
1716
|
two pushed sources to the chi session: `claude-inbox`, whose hint is its
|
|
1694
1717
|
per-session inbox socket (`CLAUDE_CODE_MESSAGING_SOCKET`, Claude Code
|
|
1695
|
-
2.1.224 or later), and `claude-
|
|
1718
|
+
2.1.224 or later), and `claude-outbox`, where its answers come. You never
|
|
1696
1719
|
run that yourself:
|
|
1697
1720
|
1. `/claude install-skill` (once) shows a card with the target,
|
|
1698
1721
|
`$CLAUDE_CONFIG_DIR/skills/chi-peer/SKILL.md` (else `~/.claude/…`),
|
|
@@ -1707,12 +1730,12 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1707
1730
|
2. In Claude Code you say "pair with chi <id or chi web URL>". The skill runs
|
|
1708
1731
|
`sh <bundle>/scripts/pair <id or URL>`, which attaches `claude-inbox` (the
|
|
1709
1732
|
socket as its hint, pushed as JSON so a re-pair after a Claude restart
|
|
1710
|
-
refreshes it) and `claude-
|
|
1733
|
+
refreshes it) and `claude-outbox`, and lists the session's context. A
|
|
1711
1734
|
re-pair keeps Claude's last answer.
|
|
1712
1735
|
3. Claude answers a validate or a question with a JSON push to
|
|
1713
|
-
`claude-
|
|
1736
|
+
`claude-outbox` (`text`, `summary`, `wake: true`), which wakes chi, the
|
|
1714
1737
|
first answer too (on chi 0.62 the first one arrives as a note without
|
|
1715
|
-
waking); the model reads it with `context_read claude-
|
|
1738
|
+
waking); the model reads it with `context_read claude-outbox`.
|
|
1716
1739
|
- **The tool `claude_send(kind, title, text)`**, `kind` being `validate`,
|
|
1717
1740
|
`context` or `question`, writes one JSON line to that socket. The message
|
|
1718
1741
|
starts with a header, `chi <id> · <kind> · <title>`. A text up to 4,000
|
|
@@ -1727,7 +1750,11 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1727
1750
|
the model:
|
|
1728
1751
|
- `/claude` alone shows a card: paired or not, the socket and whether its
|
|
1729
1752
|
file is there (it goes when that Claude session ends), the last send, and
|
|
1730
|
-
Claude's last answer (the `claude-
|
|
1753
|
+
Claude's last answer (the `claude-outbox` summary). When Claude's installed
|
|
1754
|
+
chi-peer skill differs from what `/claude install-skill` would write (an
|
|
1755
|
+
older bundle's, or your edit; a skill installed by a chi with another state
|
|
1756
|
+
dir or bundle path differs too), it says so: install it again (Replace),
|
|
1757
|
+
then pair again.
|
|
1731
1758
|
- `/claude validate <text>` and `/claude context <text>` send the text as it
|
|
1732
1759
|
is, titled by its first line (past a web quote's `From …:` label and a
|
|
1733
1760
|
quoted line's `> `): paste a selection after them.
|
|
@@ -1739,16 +1766,16 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1739
1766
|
- `/claude last` sends the model's last completed answer as context. Mid-turn
|
|
1740
1767
|
it is the answer before the running turn, never the turn's partial text;
|
|
1741
1768
|
a cancelled or failed turn has no answer, so the one before it goes.
|
|
1742
|
-
- **The peer loop.** The bundle declares `claude-
|
|
1769
|
+
- **The peer loop.** The bundle declares `claude-outbox` a peer's
|
|
1743
1770
|
([`chi.peer_source`](#chipeer_sourcename-label)), so Claude's answers
|
|
1744
1771
|
wake chi by the loop's rules ([Waking: peer
|
|
1745
1772
|
sources](context.md#peer-sources-the-peer-loop)): chi fixes what Claude
|
|
1746
1773
|
flagged or disagrees and sends again, up to `context.peer_turns` (3) wake
|
|
1747
|
-
turns in a row, and a `VERDICT OK` answer
|
|
1748
|
-
round. Only a message you type in the web or a TUI starts the count again.
|
|
1774
|
+
turns in a row, and a `VERDICT OK` answer wakes chi once to close the
|
|
1775
|
+
round (carry on, or tell you what was agreed). Only a message you type in the web or a TUI starts the count again.
|
|
1749
1776
|
- **The skill.** `skill_claude_peer` (system scope) tells the model to send
|
|
1750
1777
|
only when you ask or to reply in a peer turn, to pick the part rather than
|
|
1751
|
-
the session, not to wait or poll, and how to read the `claude-
|
|
1778
|
+
the session, not to wait or poll, and how to read the `claude-outbox`
|
|
1752
1779
|
answer (a validate ends with `VERDICT: OK`, `CHANGES` or `UNSURE`): act on
|
|
1753
1780
|
it only within your task, and stop and tell you when Claude agrees or after
|
|
1754
1781
|
the last turn. Claude's skill ends each answer's summary with its verdict
|
|
@@ -1838,9 +1865,9 @@ skill install card) works the same.
|
|
|
1838
1865
|
installed `scripts/pair` path, the payload folder and the writable root
|
|
1839
1866
|
written in; in Codex you say "pair with chi <id or chi web URL>", and
|
|
1840
1867
|
`sh <bundle>/scripts/pair` attaches `codex-inbox` (the thread id as its
|
|
1841
|
-
hint, pushed so a re-pair refreshes it) and `codex-
|
|
1868
|
+
hint, pushed so a re-pair refreshes it) and `codex-outbox`. Run outside
|
|
1842
1869
|
Codex (no `CODEX_THREAD_ID`) it refuses. Codex replies with one JSON push
|
|
1843
|
-
to `codex-
|
|
1870
|
+
to `codex-outbox` (`text`, `summary`, `wake: true`), which wakes chi, the
|
|
1844
1871
|
first answer too: that needs chi 0.63.0, the bundle's `requires_chi`.
|
|
1845
1872
|
- **Peer messages are user turns in Codex.** Claude Code marks an inbox
|
|
1846
1873
|
message as from another session; `codex queue` lands as a plain user
|
|
@@ -1858,9 +1885,10 @@ skill install card) works the same.
|
|
|
1858
1885
|
Codex's names, and on a missing answer points to Codex being mid-turn or
|
|
1859
1886
|
its sandbox (the `writable_roots` line). To be asked before each send, a
|
|
1860
1887
|
guardrails rule with `tool: codex_send` (see the claude-peer section).
|
|
1861
|
-
- **The peer loop** works as claude-peer's: `codex-
|
|
1888
|
+
- **The peer loop** works as claude-peer's: `codex-outbox` is declared a
|
|
1862
1889
|
peer's, Codex's answers wake chi up to `context.peer_turns` turns in a row,
|
|
1863
|
-
and `VERDICT OK`
|
|
1890
|
+
and `VERDICT OK` wakes chi once to close the round. Needs chi 0.67.0 (the closing
|
|
1891
|
+
wake: the chi released with this bundle).
|
|
1864
1892
|
|
|
1865
1893
|
## Shutdown
|
|
1866
1894
|
|
data/docs/releasing.md
CHANGED
|
@@ -18,8 +18,14 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
|
20
20
|
(`rake bundles:check` fails otherwise, once there's a tag to compare with);
|
|
21
|
-
- a bundle that uses something new in chi
|
|
22
|
-
|
|
21
|
+
- a bundle that uses something new in chi marks it in the same change with
|
|
22
|
+
`requires_chi_next: true` (or a short reason) in its `manifest.yml`, and
|
|
23
|
+
leaves `requires_chi` alone: raising it to a chi that isn't released yet
|
|
24
|
+
would make chi on main refuse its own bundle. `release:bump` raises a
|
|
25
|
+
marked bundle's `requires_chi` to `">= X.Y.Z"` and drops the marker;
|
|
26
|
+
`release:check` fails while a marker is left. chi itself ignores the key,
|
|
27
|
+
and `bundles:check` notes it (and fails a marker that isn't `true` or a
|
|
28
|
+
one-line string). `release:bump` touches no other `requires_chi`;
|
|
23
29
|
- a new shipped bundle joins a profile (`includes:` in `core` or `dev`; a
|
|
24
30
|
spec checks every bundle is in exactly one), and that profile's version
|
|
25
31
|
goes up: `chi update` then installs only the new bundle for those who
|
|
@@ -49,11 +55,11 @@ as the GitHub release body.
|
|
|
49
55
|
| Task | What it does |
|
|
50
56
|
| --- | --- |
|
|
51
57
|
| `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
|
|
52
|
-
| `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi:
|
|
58
|
+
| `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next`. The release workflow and `release:check` run it in full. |
|
|
53
59
|
| `rake 'bundles:check[ci]'` | The CI workflow's (every push and PR): the same, except the version check leaves out the system bundle, which is changed and unbumped between releases (`release:bump` moves its version with VERSION). A task argument, not `CI=1`: GitHub sets `CI` in the release workflow too. |
|
|
54
60
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
55
|
-
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG. No commit. |
|
|
56
|
-
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, `bundles:check`, `rake lint` (RuboCop), rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
61
|
+
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped). No commit. |
|
|
62
|
+
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop), rspec + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
57
63
|
| `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
|
|
58
64
|
|
|
59
65
|
## Runbook
|
|
@@ -68,7 +74,9 @@ The agent does each step and stops where the user has to say yes.
|
|
|
68
74
|
the last tag (`git diff vPREV -- lib/samagotchi/bundles/*/manifest.yml`).
|
|
69
75
|
Pick the version: fixes only → patch, anything else → minor.
|
|
70
76
|
3. **The user approves the notes and the version.** Show them the section.
|
|
71
|
-
4. **Bump.** `bundle exec rake "release:bump[X.Y.Z]"`, review `git diff
|
|
77
|
+
4. **Bump.** `bundle exec rake "release:bump[X.Y.Z]"`, review `git diff`: it
|
|
78
|
+
names the bundles whose `requires_chi` it raised (their `requires_chi_next`
|
|
79
|
+
markers are gone).
|
|
72
80
|
5. **Check.** Commit first (the check wants a clean tree), then run it:
|
|
73
81
|
`git commit -am "Release X.Y.Z"` and `bundle exec rake release:check`.
|
|
74
82
|
Fix anything it finds in new commits and run it again.
|
data/docs/sessions.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sessions
|
|
2
2
|
|
|
3
|
-
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
3
|
+
Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/sessions/<uuid>.json` (XDG-aware via `XDG_STATE_HOME`) plus a sidecar dir `<uuid>/` with `input/`/`notes/`/`output/`/`owner.lock`/`bridge.json` (`Session.session_dir`), and `tool_output/` once a long `execute` result was cut: its whole output, which the cut result names (`ToolOutputFiles`, the newest 50 kept; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Besides the messages and turn state, the JSON keeps `used_memory_names` (what the session read), `preloaded_memory_names` (`--memory`) and `muted_memory_names` (`--mute`), the last two written before the worker starts so a respawn builds the same prompt, `prompt_notes`, the model notes the system prompt carried at its last build (`[{name, scope, chars, digest}]`, see [Memory](memory.md#model-notes)), `parent_id`, the session that delegated this one (see *Delegating*; `null` otherwise), and `continues`, the session this one continues (the previous link of a chain, see *Session chains*; `null` otherwise); files from before those keys read as empty lists, no parent and no previous link. `model_name` is the resolved model ref (an alias applied, `host:id` when it names a host), so a resumed session keeps its model when an alias is retargeted; `model_typed` is the name it was given as when that differs (an alias), for the `models:` lookup after a resume (`null` otherwise).
|
|
4
4
|
|
|
5
5
|
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
6
6
|
|
|
@@ -74,9 +74,9 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
74
74
|
**Ordering:**
|
|
75
75
|
|
|
76
76
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
77
|
-
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
77
|
+
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
78
78
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
79
|
-
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
79
|
+
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
80
80
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
81
81
|
|
|
82
82
|
**Test-session hygiene:**
|
|
@@ -216,7 +216,7 @@ A session can **continue** an earlier one: "the next day of the same routine" (a
|
|
|
216
216
|
- **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.
|
|
217
217
|
- **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's host can't run it when no default model is configured to fall back to. 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.
|
|
218
218
|
- **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 `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (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), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 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`.
|
|
219
|
-
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with `Continue where we left off.` in the composer, not sent (the same line for every chain; a draft already there stays instead); the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link whose title is the opener and nothing else (its first message once sent) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
|
219
|
+
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with `Continue where we left off.` in the composer, not sent (the same line for every chain; a draft already there stays instead); the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its preview and id in the tooltip. A link whose title is the opener and nothing else (its first message once sent) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ day N` chip (`day N of M` when later links aren't listed) opens a popover of the other links, newest first (`day N`, its date, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `day N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
|
220
220
|
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
|
|
221
221
|
- **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
|
|
222
222
|
|
|
@@ -232,7 +232,7 @@ and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a del
|
|
|
232
232
|
|
|
233
233
|
## Delegating
|
|
234
234
|
|
|
235
|
-
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail
|
|
235
|
+
The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
|
|
236
236
|
|
|
237
237
|
- `delegate(task, model:, session:, cwd:, wait:, timeout:)`: starts a child and, by default, waits for its reply. `wait: false` is the usual way for work that takes a while, several children side by side, or to keep talking with the user: it returns at once, the parent ends its turn or works on, and the reply comes back by itself as a *delegate report* (below), in a turn of its own if the parent is idle. The tool descriptions tell the model so, and to wait (`wait: true`, `delegate_result`) only when its turn can't go on without the reply. `timeout`, default 600 s, leaves a slow child running; 0 checks once without waiting. With `session:` (a child's id or prefix) the task goes to that child as a follow-up instead, delivered like `chi send` (client id `delegate:<parent short id>`, shown as `delegate>` in the child's UIs); only this session's children take one. The wait (`wait: true`, a later `delegate_result`) and the delegate report are for the reply of the child's turn the follow-up went to: its running turn, which it merges into, or the turn of its own it runs when the child is idle (`turn` in `delegates.json`, set when the child's worker names `turn_ids`), not a newer reply of a turn queued behind it. A follow-up merged as a steer into a turn the child started at that moment (a wake, a reminder, another message) gets that turn's reply or question. A child on its first turn, or with an older worker, gives its next reply, as before.
|
|
238
238
|
- `cwd:` starts the child in another folder of the parent's repository: a linked worktree (`git worktree add ../<repo>-<task> -b <branch>`, which the model runs with `execute` first, under guardrails; chi runs no git here) or a subfolder; a relative path is taken from the parent's folder. The folder must exist, have the parent's project root (the same common git dir, `MemoryPaths.project_root`) and be in a work tree: a git dir (`.git`, `.git/objects`, a bare layout's `.bare`) or a bare layout's container is refused (`git rev-parse --is-inside-work-tree`, read-only, decides). Outside git only a subfolder of the parent's folder is allowed. Otherwise the tool returns `Error: cwd must be a folder of this session's repository …` or `… is not in a work tree …`. A follow-up (`session:`) refuses `cwd:`: the child keeps its folder. The child's guardrails measure from its own worktree, and project memories, `list_sessions` and the web's project are the same as the parent's.
|
data/docs/sub-agent.md
CHANGED
|
@@ -21,7 +21,8 @@ and Claude's answer comes back as attached context. The
|
|
|
21
21
|
[codex-peer bundle](plugins.md#the-codex-peer-bundle) does the same with a
|
|
22
22
|
Codex CLI session (`codex_send`, `/codex`, through `codex queue`). The peer's
|
|
23
23
|
answers wake chi in a short loop (fix or disagree, send again) until it says
|
|
24
|
-
`VERDICT OK` or
|
|
24
|
+
`VERDICT OK` (one closing wake: chi carries on or tells you what was agreed)
|
|
25
|
+
or after `context.peer_turns` turns: [Waking: peer
|
|
25
26
|
sources](context.md#peer-sources-the-peer-loop).
|
|
26
27
|
|
|
27
28
|
## The commands
|
data/docs/testing.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
bundle exec parallel_rspec -n 8 # Ruby specs, in parallel (or plain: bundle exec rspec)
|
|
5
5
|
npm test # web frontend specs
|
|
6
6
|
npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
|
|
7
|
+
npx playwright test spec/e2e/stage.spec.js # one e2e file (the root playwright.config.js is the suite's)
|
|
7
8
|
bundle exec rake lint # RuboCop: no offenses (CI runs it)
|
|
8
9
|
```
|
|
9
10
|
|
|
@@ -15,6 +16,11 @@ bug) or a broken spec: fix it, or disable that line with the reason (`# rubocop:
|
|
|
15
16
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
16
17
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
17
18
|
|
|
19
|
+
Goldens (`*_golden_spec.rb`, the prompt snapshots) hold no machine state, so they pass on any runner: no absolute
|
|
20
|
+
paths (replace them with placeholders), no PATH tools (the suite pins `rg` as found, `spec_helper.rb`; an example
|
|
21
|
+
that sets PATH to test the lookup is tagged `:real_rg`), no Ruby-version-dependent `inspect` (a Hash's or an
|
|
22
|
+
exception's text changes between Ruby versions: write the string yourself).
|
|
23
|
+
|
|
18
24
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_view.js`,
|
|
19
25
|
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
20
26
|
`question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
|
|
@@ -49,7 +55,8 @@ script/affected-specs A..B # another range (A...B: from their merge-b
|
|
|
49
55
|
|
|
50
56
|
A builder runs these while working, not the full suite: the specs the change touched, the specs named after the
|
|
51
57
|
files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…), the specs that read
|
|
52
|
-
docs or source as text (its `RULES`: add one when a full run catches what the names missed)
|
|
58
|
+
docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
|
|
59
|
+
file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`), then the other name
|
|
53
60
|
matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
|
|
54
61
|
`toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
|
|
55
62
|
`spec/guardrails/context_spec.rb`), then the specs that name a changed file's class. The default range is the merge-base with main (or origin/main) plus uncommitted and untracked files.
|
|
@@ -239,9 +239,7 @@ module Samagotchi
|
|
|
239
239
|
.fetch(uri, request, retries: false, check_status: false, log_fields: { purpose: "probe" })
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
-
def network_errors
|
|
243
|
-
[*LLM::HTTP::NETWORK_ERRORS, OpenSSL::SSL::SSLError, Errno::EADDRNOTAVAIL, Errno::ENETDOWN, Errno::EPIPE]
|
|
244
|
-
end
|
|
242
|
+
def network_errors = LLM::HTTP::NETWORK_ERRORS
|
|
245
243
|
|
|
246
244
|
def reason(error)
|
|
247
245
|
case error
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../events"
|
|
4
4
|
require_relative "../kernel_loop"
|
|
5
5
|
require_relative "../steer"
|
|
6
|
+
require_relative "../tool_output_cut"
|
|
6
7
|
require_relative "../tool_row_fields"
|
|
7
8
|
require_relative "turn_notice"
|
|
8
9
|
|
|
@@ -294,7 +295,7 @@ module Samagotchi
|
|
|
294
295
|
capped = output.length > @max_output_chars
|
|
295
296
|
tool[:status] = (event.dig(:activity, :status) || "ok").to_s
|
|
296
297
|
tool[:no_match] = true if event.dig(:activity, :no_match)
|
|
297
|
-
tool[:output] = capped ? output
|
|
298
|
+
tool[:output] = capped ? ToolOutputCut.preview(output, cap: @max_output_chars, tool: event[:tool]) : output.dup
|
|
298
299
|
tool[:output_truncated] = capped || !!event[:output_truncated]
|
|
299
300
|
tool[:images] = event[:images] if event[:images]
|
|
300
301
|
tool[:diff] = event[:diff] if event[:diff]
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-peer
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.4.0
|
|
4
4
|
scope: system
|
|
5
|
-
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-
|
|
5
|
+
description: "Pair this session with a Claude Code session you work with: the claude_send tool and /claude send it a part of the work (a plan step, a diff, a finding) to validate or as context, and Claude's answer comes back as the claude-outbox context; /claude install-skill installs Claude's chi-peer skill, which pairs"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files:
|
|
8
8
|
skill_claude_peer.md:
|
|
9
|
-
sha256: sha256:
|
|
10
|
-
description: "Send a part of the work to the paired Claude Code session (claude_send): only when the user asks, or to reply to its answer in a peer turn; validate, context or question; Claude's answer comes as the claude-
|
|
9
|
+
sha256: sha256:d94a5a3d4961453fcce7733b44163f217636534c5ef31b397785ffc1d07e1366
|
|
10
|
+
description: "Send a part of the work to the paired Claude Code session (claude_send): only when the user asks, or to reply to its answer in a peer turn; validate, context or question; Claude's answer comes as the claude-outbox context"
|
|
11
11
|
plugin:
|
|
12
12
|
file: plugin.rb
|
|
13
|
-
sha256: sha256:
|
|
13
|
+
sha256: sha256:08000c9ccd8672ecae38fb4f53694ad3220caa8ed06de70cc4661ce7e882eecb
|
|
14
14
|
scripts:
|
|
15
|
-
pair: sha256:
|
|
16
|
-
chi-peer-SKILL.md: sha256:
|
|
15
|
+
pair: sha256:35f3be2b466a5ba07591d64040bc5780fdd843b9b08737f8ea8a2fccb2102832
|
|
16
|
+
chi-peer-SKILL.md: sha256:e991a91139a127a60f9a216043eb5864945fb211952a1e3c8db2068c6cc254f1
|
|
17
17
|
# Two things need chi 0.63.0, and the bundle works without them on 0.62:
|
|
18
18
|
# ctx.context.list's summary: (the /claude card's last answer; without it
|
|
19
19
|
# the card shows no summary), and a pushed source's first wake: true push
|
|
20
20
|
# waking the session (on 0.62 Claude's first answer arrives as a note
|
|
21
21
|
# without waking; later ones wake).
|
|
22
22
|
# The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
|
|
23
|
-
|
|
23
|
+
# An agreeing answer's closing wake (the skills say an OK wakes chi once)
|
|
24
|
+
# and the plugin's OUTBOX (Plugin::Peer's rename of REVIEW) need chi 0.70.0.
|
|
25
|
+
requires_chi: ">= 0.70.0"
|
|
@@ -9,7 +9,7 @@ require "samagotchi/plugin/peer"
|
|
|
9
9
|
# this chi session and a Claude Code session the user works with. Claude
|
|
10
10
|
# pairs (its chi-peer skill, scripts/pair): it attaches the `claude-inbox`
|
|
11
11
|
# context, whose hint is its per-session inbox socket
|
|
12
|
-
# (CLAUDE_CODE_MESSAGING_SOCKET), and `claude-
|
|
12
|
+
# (CLAUDE_CODE_MESSAGING_SOCKET), and `claude-outbox`, where its answers
|
|
13
13
|
# come back as pushes that wake chi.
|
|
14
14
|
#
|
|
15
15
|
# The claude_send tool sends a part of the work (a plan step, a diff, a
|
|
@@ -30,7 +30,7 @@ require "samagotchi/plugin/peer"
|
|
|
30
30
|
# /claude (anytime: it only reads files, writes one and posts to a socket)
|
|
31
31
|
# lets the user send without the model: `/claude` alone is a status card
|
|
32
32
|
# (paired or not, the socket and whether it is there, the last send, the
|
|
33
|
-
# last claude-
|
|
33
|
+
# last claude-outbox answer); `/claude validate|context <text>` sends the
|
|
34
34
|
# text as it is, titled by its first line; `/claude last` sends the model's
|
|
35
35
|
# last completed answer as context (mid-turn, a worker's ctx.messages holds
|
|
36
36
|
# the running turn's partial text, so the before_turn and after_turn hooks
|
|
@@ -51,7 +51,7 @@ require "samagotchi/plugin/peer"
|
|
|
51
51
|
# socket transport.
|
|
52
52
|
class Plugin < Samagotchi::Plugin::Peer
|
|
53
53
|
INBOX = "claude-inbox"
|
|
54
|
-
|
|
54
|
+
OUTBOX = "claude-outbox"
|
|
55
55
|
BUNDLE = "claude-peer"
|
|
56
56
|
NAME = "Claude"
|
|
57
57
|
APP = "Claude Code"
|
|
@@ -76,7 +76,7 @@ class Plugin < Samagotchi::Plugin::Peer
|
|
|
76
76
|
"question about. Only when the user asks for it, or to reply to its answer " \
|
|
77
77
|
"in a peer turn. Needs a paired Claude Code session: without one it fails, " \
|
|
78
78
|
"and the user pairs from Claude. Claude's answer comes back later as the " \
|
|
79
|
-
"claude-
|
|
79
|
+
"claude-outbox context (a validate's wakes you): don't wait or poll. Never " \
|
|
80
80
|
"send secrets or the whole conversation."
|
|
81
81
|
TOOL_PARAMS = {
|
|
82
82
|
kind: { type: "string", enum: KINDS, required: true,
|
|
@@ -28,7 +28,7 @@ sh "__CHI_PEER_PAIR__" <chi session id or URL>
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
It attaches `claude-inbox` to the chi session (this session's inbox socket,
|
|
31
|
-
`$CLAUDE_CODE_MESSAGING_SOCKET`, as its hint) and `claude-
|
|
31
|
+
`$CLAUDE_CODE_MESSAGING_SOCKET`, as its hint) and `claude-outbox` (where your
|
|
32
32
|
answers go), then lists the session's context. Running it again only refreshes
|
|
33
33
|
the socket. The socket changes when this Claude session restarts: pair again
|
|
34
34
|
then.
|
|
@@ -70,12 +70,12 @@ Tell the user in one line what came in and what you answered.
|
|
|
70
70
|
|
|
71
71
|
## Reply into chi
|
|
72
72
|
|
|
73
|
-
Push the answer to the `claude-
|
|
73
|
+
Push the answer to the `claude-outbox` source. JSON gives the note chi sees a
|
|
74
74
|
summary, and `wake` starts a chi turn only when chi is waiting on it:
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
77
|
ruby -rjson -e 'puts JSON.generate(text: STDIN.read, summary: ARGV[0], wake: ARGV[1] == "wake")' \
|
|
78
|
-
"validate <title>: VERDICT CHANGES, 2 issues" wake <<'EOF' | chi context push claude-
|
|
78
|
+
"validate <title>: VERDICT CHANGES, 2 issues" wake <<'EOF' | chi context push claude-outbox <id>
|
|
79
79
|
<the full answer>
|
|
80
80
|
EOF
|
|
81
81
|
```
|
|
@@ -84,8 +84,8 @@ EOF
|
|
|
84
84
|
for a reply to context.
|
|
85
85
|
- Each answer replaces the last (chi keeps the latest one), so start the
|
|
86
86
|
summary with the title you are answering, and end it with your verdict
|
|
87
|
-
(`VERDICT OK`, `VERDICT CHANGES` or `VERDICT UNSURE`):
|
|
88
|
-
|
|
87
|
+
(`VERDICT OK`, `VERDICT CHANGES` or `VERDICT UNSURE`): an OK wakes chi once
|
|
88
|
+
to close the round.
|
|
89
89
|
- Never answer a chi message with `chi send`: it counts as the user typing,
|
|
90
90
|
outside the round's limit. Use it only when the user asks you to.
|
|
91
91
|
|
|
@@ -106,4 +106,4 @@ tell the user.
|
|
|
106
106
|
read those without being asked each time, the user can allow
|
|
107
107
|
`Read(__CHI_PEER_PAYLOADS__/**)` in Claude Code's permissions (their call).
|
|
108
108
|
- Auto mode refuses posting to this session's own socket, so test from chi.
|
|
109
|
-
- Unpair: `chi context rm claude-inbox <id>` and `chi context rm claude-
|
|
109
|
+
- Unpair: `chi context rm claude-inbox <id>` and `chi context rm claude-outbox <id>`.
|