samagotchi 0.140.0 → 0.141.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74f14fd45c9fe7ee5c7ff6a7169a49b983309d7ab37a4af0530de063eb64a0ef
4
- data.tar.gz: b7674ded1f9a41c094e0daf2220c2836d66e7df8f880d1cdecee4da566ff5b7b
3
+ metadata.gz: 4d6c521351f86a38243b9757eff32df1745e88d2be1158684ba029e85f16b9e1
4
+ data.tar.gz: 99c0f5f7b2356a58a36f4d38330a17d95decdc64446e26ee55f38d3b5794b3ba
5
5
  SHA512:
6
- metadata.gz: 877da3c7a446f711d3fe42e3f3388109f9c4f2cedde21f41292a48ea829904101982e68b697a5f6bf0729119df9c528361af4965e8e2c805357ddb49e19f6fe0
7
- data.tar.gz: 7e77427d1782222f5876527e2095fc53e7e8d35c1c7b8d3a484c9bbb4640f482656d70fb9b948cc541a6b5c843c9764fd179131972786a3c7ed5f55971b67a64
6
+ metadata.gz: 7b735c84997b1a4c035c6d3c0f7baf6035f4f29bd400e6bce116ecaf5fe06c0d2b304fe19a8447a523833bf71cfe9829193d6c8fcb136f42e7a5f900e4f7a30f
7
+ data.tar.gz: 0705cdf1abb1716a40f38c396b020442b44c2d985911f4d945362f0d2a83bde088c0699f660c8dce791edd666b83529e7d991e642703ceb023558caaa128c394
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.141.0] - 2026-10-31
11
+
12
+ ### Added
13
+
14
+ - Movable context: a continue (`chi send --new --continues`, the web's Continue →) and a bundle's fork take the
15
+ session's attached context with them, with a note in the new session naming what moved and what stayed. A command
16
+ source moves, a pushed one stays; `chi context add --movable / --no-movable` (and `ctx.context.attach(movable:)`)
17
+ say it outright, and `chi context ls` marks one that stays. A push into a continued session is refused with the
18
+ next link's id. See docs/context.md (Movable context).
19
+ - A paired Claude Code or Codex session follows a continue: the new session asks it once to pair again with the new
20
+ id (a `moved` message to the inbox the old session had), and a reply pushed to the old id is refused with the new
21
+ one, so the peer pairs again then (claude-peer 0.4.6, codex-peer 0.3.5: install the chi-peer skill again with
22
+ `/claude install-skill` or `/codex install-skill`). Bundles get `ctx.continues` and `ctx.context.left_behind`. See
23
+ docs/context.md (Peers and a continue).
24
+
25
+ Update with `chi update` (claude-peer 0.4.6, codex-peer 0.3.5).
26
+
10
27
  ## [0.140.0] - 2026-10-31
11
28
 
12
29
  ### Added
@@ -5362,7 +5379,8 @@ and long-lived sessions.
5362
5379
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5363
5380
  hotkey panel that send selected text or the clipboard to your sessions.
5364
5381
 
5365
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.140.0...HEAD
5382
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.141.0...HEAD
5383
+ [0.141.0]: https://github.com/dm1try/samagotchi/compare/v0.140.0...v0.141.0
5366
5384
  [0.140.0]: https://github.com/dm1try/samagotchi/compare/v0.139.1...v0.140.0
5367
5385
  [0.139.1]: https://github.com/dm1try/samagotchi/compare/v0.139.0...v0.139.1
5368
5386
  [0.139.0]: https://github.com/dm1try/samagotchi/compare/v0.138.0...v0.139.0
data/docs/architecture.md CHANGED
@@ -719,8 +719,8 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
719
719
  | `POST /api/sessions/:id/wait/prepare` | owner | `question_id` (absent: the pending one; `null`: none), `turn`, `created`, `busy` (+ `queue`) | 200 `{session_id, cursor, baseline: {messages, question_id, last_turn}, own_turn, known_turn, running, live, settled, last_turn_id}` (`WaitStart`) | 409 `busy`, `waiting` (`SendCommand::Busy`'s words, and its fields: `question`, `parent_strategy`) |
720
720
  | `POST /api/sessions/:id/wait` | owner | `cursor`, `baseline`, `turn`, `queued`, `ended_before`, `owner_grace`, `timeout` (capped at 30 s) | 200: `ReplyWait::Result`'s fields (`status`: `done`, `waiting_for_answer`, `no_reply`, `error`, `stopped`, `worker_gone`, or `timeout`: ask again; `text`, `file`, `question` as `PendingQuestion#to_file`, `outcome`, `limit`, `error_kind`, `retryable`, `cancel_reason`, `stopped_by`, `kept_steps`, `turn_id`, `origin`; never the session) and `parent_strategy` `{name, allowed, origin}` (`ParentReport.strategy_for`, this server's config.yml) | 400 `invalid_wait`; 503 `server_stopping` |
721
721
  | `POST /api/sessions/:id/notes` | owner | `text`, `source` (default `cli`, never `broadcast`) | 200 `{status: queued \| waits, session_id, queued}` (`NoteDelivery`) | 409 `owned_by_tui` (`status: refused`); 400 `note_rejected`, `missing_fields`, `invalid_source` |
722
- | `POST /api/sessions/:id/context` (more bodies) | owner | `{name, push: true, why, hint}` or `{name, cmd, every, why, hint}` beside the page's `{url, why, hint}`; `added_by` `cli`/`agent` (else `web`) | 201 `{status: attached, name}` | 400 `invalid_source`; 409 `exists` |
723
- | `POST /api/sessions/:id/context/:name/push` | owner | `text`, `summary`, `wake`, `hint` (a source's JSON output, checked the same way) | 200 `{status: pushed, name, before, revision, changed}` | 404 `not_found`; 409 `not_push`; 400 `invalid_text` |
722
+ | `POST /api/sessions/:id/context` (more bodies) | owner | `{name, push: true, why, hint}` or `{name, cmd, every, why, hint}` beside the page's `{url, why, hint}`; `added_by` `cli`/`agent` (else `web`); `movable` (any body) | 201 `{status: attached, name, movable}` | 400 `invalid_source`; 409 `exists` |
723
+ | `POST /api/sessions/:id/context/:name/push` | owner | `text`, `summary`, `wake`, `hint` (a source's JSON output, checked the same way) | 200 `{status: pushed, name, before, revision, changed}` | 404 `not_found`; 409 `not_push`; 410 `context_continued` (`next_id`); 400 `invalid_text` |
724
724
  | `DELETE /api/sessions/:id/context/:name?own=1` | owner | | 200 `{status: detached}`: the session's own only | 409 `project_source` (chi context rm's words); 404 |
725
725
  | `POST /api/sessions/:id/stop` | owner | `wait?` (seconds, capped at `SessionManager::STOP_WAIT`, 10: `chi --remote NAME sessions stop` sends it) | 200 `{status: stopped, session_id, released}`: `released`, whether the worker let go within the wait (without one `STOP_WAIT_SECONDS`, 2 s: the page's Stop) | 409 `owned_by_tui` |
726
726
  | `DELETE /api/sessions/:id?force=` | owner | `force=true` stops a live worker first (the page sends it) | as before | 409 `worker_running` without it |
data/docs/context.md CHANGED
@@ -89,8 +89,8 @@ web) without touching the other sessions.
89
89
  ## The commands
90
90
 
91
91
  ```
92
- chi context add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
93
- chi context add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows
92
+ chi context add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET
93
+ chi context add URL [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET a URL an installed bundle's provider knows
94
94
  chi context push NAME [-m TEXT] [TARGET] new text for a --push source (stdin without -m)
95
95
  chi context ls [TARGET] [--format json]
96
96
  chi context show NAME [--json] [TARGET]
@@ -313,6 +313,68 @@ lower.) Every other source keeps the line above. `context.peer_turns: 0` turns
313
313
  the loop off: a peer source then wakes like any other. `context_read` names a
314
314
  peer source in its header (`Peer: your peer agent Codex's answers …`).
315
315
 
316
+ ## Movable context
317
+
318
+ A session's own sources go with it to the next session of its work: the next
319
+ link of a [chain](sessions.md#session-chains) (`chi send --new --continues`,
320
+ the web's Continue →) and a bundle's fork
321
+ ([`ctx.sessions.fork`](plugins.md#other-sessions-ctxsessions)). A topic switch goes
322
+ to a fresh session with its own context, never through a continue.
323
+
324
+ Each source says whether it moves:
325
+
326
+ - **A command source moves** (`--cmd`, a URL's source): chi runs its command
327
+ itself, in the new session too.
328
+ - **A pushed source stays** (`--push`): its writer (`pbpaste | chi context
329
+ push notes <id>`, a peer agent) holds the old session's id.
330
+ - `chi context add … --movable` / `--no-movable` (either kind) says it
331
+ outright: `--movable` on a pushed source means its writer finds the new id
332
+ itself. A bundle passes `movable:` to `ctx.context.attach`.
333
+
334
+ What moves is copied before the new session's worker starts: the source, its
335
+ last text (so its first note quotes it without waiting for a fetch, and its
336
+ interval holds), and the session's choices: every removed (declined) and
337
+ muted marker. Candidates don't move. The new link gets the usual *Attached*
338
+ note for each source; a fork starts where its seed's notes left off, so a
339
+ source that changed since gets an *Updated* note. A second chi note, after
340
+ the chain's, names what moved and what stayed:
341
+
342
+ > Context moved with this session: pr-412, ci. Left with 3f2a9c1e: notes (pushed).
343
+
344
+ `chi send --new --continues` and the web's Continue toast say the same in a
345
+ few words, and `chi context ls` marks a session's source that stays
346
+ (`· stays`; `--format json` has the derived `movable`). Project sources are
347
+ shared already, and a review fork (`chi review --from`) and a delegate take none.
348
+
349
+ A push into a continued session is refused, nothing written, with the next
350
+ link's id (`chi context push` exits 1; chi web answers 410
351
+ `context_continued`):
352
+
353
+ ```
354
+ chi context push: 3f2a9c1e was continued as 7b1d…: notes stays with the session it was attached to; attach it to 7b1d… (or pair again with 7b1d…) and push there
355
+ ```
356
+
357
+ An archived session that nobody continued takes pushes as before.
358
+
359
+ ### Peers and a continue
360
+
361
+ A peer's pairing ([claude-peer, codex-peer](plugins.md#the-claude-peer-bundle):
362
+ `<peer>-inbox` and `<peer>-outbox`, both pushed) stays with the old session,
363
+ and the peer pairs again with the new one. The continue keeps what stayed in
364
+ the new session (`left.json` beside its sources: each name, its last pushed
365
+ hint, the old id; `ctx.context.left_behind`), and the new session's worker
366
+ posts the peer one `moved` message to the inbox left behind, once:
367
+ `chi <old> · moved · continued as <new id>`. The peer's chi-peer skill pairs
368
+ again with the new id, the same way it paired before. Without an inbox (an
369
+ outbox-only pairing through a remote), or when the message didn't arrive, the
370
+ peer's next push to the old id is refused with the new id, and the skill pairs
371
+ again and pushes there. The second note says which, not whether it worked:
372
+
373
+ > Left with 3f2a9c1e: claude-inbox, claude-outbox (Claude's pairing: chi asks Claude to pair again with this session).
374
+
375
+ or, for an outbox alone, `codex-outbox (Codex pairs again when it next
376
+ answers)`. A fork takes no pairing and posts nothing.
377
+
316
378
  ## Candidates
317
379
 
318
380
  A bundle can **propose** a source instead of attaching it
@@ -359,8 +421,8 @@ review session refuses them); an unknown name, or one attached or declined
359
421
  already, is an error that says so. They are what the judge's card's buttons
360
422
  run (see [On mode](#on-mode)).
361
423
 
362
- Forks and continues start without candidates: the bundle proposes again in
363
- the new session.
424
+ Forks and continues take no candidates: the bundle proposes again in the
425
+ new session (see [Movable context](#movable-context) for the sources).
364
426
 
365
427
  ### The judge
366
428
 
@@ -132,7 +132,10 @@ the session commands*. Keep that table in step with `SessionAccessRoutes`.
132
132
  server's `ack`, which `SendCommand` prints as `<id> refused: <detail>` (`refused_word`, `REMOTE_REFUSALS`), exit 1.
133
133
  A per-method mapping covers the rest (`Remote#deliver_turn`, `#note`, `#context_push`, `#context_remove`,
134
134
  `#bridge_reply` for answer/dismiss: chi web's 503 `not_live` or 404 `not_found` → nil, i.e. no worker; 504 → the
135
- Bridge's 408; 501 → its 404). Anything unmapped becomes `RemoteFailed` "remote NAME (URL) answered STATUS code:
135
+ Bridge's 408; 501 → its 404). `#context_push` maps 409 to `:not_push` and 410 `context_continued` (a continued
136
+ session, its `next_id`) to `:continued`: a new refusal there gets a status and code of its own, as an older client
137
+ reads every 409 as `not_push` and `continued` as a continue's refusal. `#context_add` sends `movable` and reports it
138
+ dropped when the 201 doesn't echo it (an older chi web; `chi context add` warns). Anything unmapped becomes `RemoteFailed` "remote NAME (URL) answered STATUS code:
136
139
  message".
137
140
  - **Transport.** `HTTPTransport#call` turns socket errors into `RemoteFailed` "remote studio (http://…): connection
138
141
  refused" (also: no answer within 5 s, a read or write timeout, can't find host, host unreachable, TLS, a broken
data/docs/plugins.md CHANGED
@@ -822,6 +822,7 @@ full label, `plugin.rb (bundle my-bundle)`.
822
822
  | `ctx.delegate?` | whether the session is a delegate child: a task another session handed over |
823
823
  | `ctx.review?` | whether the session is a review session (`chi review`): one its participants read, not the owner's working session |
824
824
  | `ctx.frontend` | what runs the session, read now: `:repl` (the REPL, `-p` without `--non-interactive`), `:one_shot` (`-p --non-interactive`: one turn, then exit) or `:worker` (a session worker: the web, an attached TUI). Only a worker's answers reach the web while it runs; a REPL's show there once the session is opened. Until the host sets it (while the plugins load, inside `Engine.new`) it reads `:one_shot`, so read it in hooks and init tasks, not in `register` |
825
+ | `ctx.continues` | the id of the session this one continues (a [continue](sessions.md#session-chains)'s new link), else nil |
825
826
  | `ctx.fork?` | whether the session is a fork (`ctx.sessions.fork`): it has a parent but isn't a delegate child, and started from a conversation, so its first prompt isn't its first user message |
826
827
  | `ctx.model` | the model the session runs on now: its resolved ref (`host:id`), right after `/model` too |
827
828
  | `ctx.model_key` | that model's memory overlay key (`<name>.<key>.md`, what `memory_write current_model_only` writes) |
@@ -1079,6 +1080,8 @@ ctx.sessions.review(subjects: ["docs/plan.md"], seed: handoff) # => a review se
1079
1080
  `/title` of its own (see [Session titles](sessions.md#session-titles)).
1080
1081
  - An image a message names is copied into the child. One whose file is
1081
1082
  gone is dropped, with `[image x.png was not copied]` in its message.
1083
+ - This session's movable attached context goes too, with a note naming it
1084
+ (see [Movable context](context.md#movable-context)).
1082
1085
  - A child chi won't start raises `Error` with chi's reason: the session's
1083
1086
  model is gone from the config, or its toolkit is a config kit with an
1084
1087
  invalid `groups:` (`toolkit.kits`, `Toolkit.check_new!`).
@@ -1137,6 +1140,7 @@ ctx.context.decline(url: "https://github.com/acme/app/pull/42") # => "pr-42"
1137
1140
  ctx.context.propose(url: "https://github.com/acme/app/pull/42", title: "#42 Make the thing", mentions: ["#42", "feat/x"])
1138
1141
  ctx.context.candidates # => [#<data Candidate name="pr-42", url="https://…", title="#42 Make the thing", …>]
1139
1142
  ctx.context.withdraw("pr-42") # => true
1143
+ ctx.context.left_behind # => [#<data Left name="claude-inbox", hint="/tmp/…sock", from="3f2a9c1e-…">]
1140
1144
  ```
1141
1145
 
1142
1146
  [Attached context](context.md) for the session the plugin runs in: chi runs
@@ -1152,7 +1156,9 @@ gets a note when its text changes.
1152
1156
  user adds it (`ctx.context.declined?(name, url: nil)` says so, by the
1153
1157
  name or by the URL). `force: true` is for the user's own explicit choice
1154
1158
  (a click on the plugin's card): it attaches anyway and clears the
1155
- declined marker, by the name and by the URL.
1159
+ declined marker, by the name and by the URL. `movable: true|false` says
1160
+ whether a continue or a fork takes the source to the new session (by
1161
+ default a command source does; see [Movable context](context.md#movable-context)).
1156
1162
  - `decline(url:)` or `decline(name:)` marks the source declined in this
1157
1163
  session, as the user's removal does: `attach` skips it from then on. With
1158
1164
  `url:`, the name and hint come from the bundle's provider (`name:`
@@ -1179,6 +1185,12 @@ gets a note when its text changes.
1179
1185
  markers. `withdraw(name)` takes a candidate back (true when there was
1180
1186
  one); a source attached from it stays attached. Neither an attach nor a
1181
1187
  decline removes a candidate; deleting the session does.
1188
+ - `left_behind` (read-only) lists what the continue that started this
1189
+ session left with the previous link ([Movable
1190
+ context](context.md#movable-context)): `ContextSources::Left` values with
1191
+ `name`, `hint` (the last pushed one, else the source's) and `from` (the
1192
+ previous link's id); `[]` for a session that isn't a continue's. The peer
1193
+ bundles read it to ask their peer to pair again (below).
1182
1194
  - The source is the session's (not the project's), marked
1183
1195
  `added_by: plugin:<bundle>`. It is plugin code, so it isn't asked about as
1184
1196
  the agent's `chi context add --cmd` is.
@@ -2375,6 +2387,27 @@ Claude pairs; without one the bundle costs one tool declaration.
2375
2387
  the remote machine can send into this Claude session (sshd makes it the
2376
2388
  user's, mode 0600 by default): fine on your own machine or pod, not a shared
2377
2389
  host. Claude's own skill has the same under "chi on another machine over SSH".
2390
+ - **A continue.** The pairing's two sources are pushed, so they stay with
2391
+ the old session when you continue it ([Movable
2392
+ context](context.md#peers-and-a-continue)). The new session's worker posts
2393
+ Claude one message, from a quiet init task, to the socket the old
2394
+ `claude-inbox` last had:
2395
+
2396
+ ```
2397
+ chi <old> · moved · continued as <new id>
2398
+ This chi session was continued as <new id>. Pair with <new id> the way you paired with <old>, then answer there (chi context push claude-outbox <new id>).
2399
+ ```
2400
+
2401
+ Claude's skill pairs again with the new id the same way it paired before
2402
+ (the same `CHI_REMOTE` and `--inbox-socket`). It is posted once (a marker
2403
+ under the bundle's `sessions/<id>/`, so a worker restart doesn't post it
2404
+ again); a post that fails (that Claude session ended) is logged and
2405
+ dropped. An outbox-only pairing has no inbox to post to: Claude's next
2406
+ answer to the old id is refused with the new one (`<old> was continued as
2407
+ <id>`), and the skill pairs again and pushes the answer there. The new
2408
+ session's note says which: `Left with 3f2a9c1e: claude-inbox, claude-outbox
2409
+ (Claude's pairing: chi asks Claude to pair again with this session).`, or
2410
+ `(Claude pairs again when it next answers)`.
2378
2411
  - **Asking first.** The tool has no `targets:`, so guardrails match it by name
2379
2412
  only: to be asked before each send, add a rule ([Rules in
2380
2413
  config.yml](guardrails.md#rules-in-configyml)):
@@ -2498,6 +2531,12 @@ skill install card) works the same.
2498
2531
  peer's, Codex's answers wake chi up to `context.peer_turns` turns in a row,
2499
2532
  and `VERDICT OK` wakes chi once to close the round. Needs chi 0.67.0 (the closing
2500
2533
  wake: the chi released with this bundle).
2534
+ - **A continue** works as claude-peer's: the new session queues Codex the
2535
+ `moved` message on the thread the old `codex-inbox` named, once, and
2536
+ Codex's skill pairs again with the new id (the same `CHI_REMOTE`, if any).
2537
+ A pairing through a remote has no inbox: Codex pairs again when its next
2538
+ push is refused. A closed Codex session still takes the message; it waits
2539
+ for `codex resume` there.
2501
2540
 
2502
2541
  ## The ruby bundle
2503
2542
 
data/docs/sessions.md CHANGED
@@ -239,7 +239,7 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
239
239
  - `--wait ID` with no message (no `-m`, nothing piped) sends nothing: it waits for the session's next reply and prints and exits as above. With a turn running, that turn's reply (its `interrupted_turn` mark names it), not a turn's queued behind it; with the step-limit question standing, the reply of the turn its Continue runs; with an idle session whose last turn already ended, that ended turn is the answer: its reply, or its end when it left no reply — a turn that finished in the gap between waits is reported, not waited for. With none of those (or a worker whose `bridge.json` lacks `turn_ids`), the next reply of any turn. A running session is fine (that is the point), so after exit 3, 4 or 130 an agent waits again this way while the user answers in the web or `chi --attach`; the question pending when it starts doesn't count again. An idle session with no worker waits until something wakes one. With `--new`, or two ids, it is a usage error (exit 2).
240
240
  - `--set KEY=VALUE` (with `--new`, repeatable) gives the new session a setting of its own, as `chi --set` does (see *Session settings* below): a core key a session may set (`--set recap.enabled=false`) or a bundle's `BUNDLE.KEY` (`chi send --new --set cost-cap.cap=3 -m "…"` starts it with a $3 cap). A key no session sets (`guardrails.mode`), a bundle or key the installed bundles don't offer, or a value of another type than the key takes (`recap.enabled=maybe`, `cost-cap.cap=five`) is a usage error (exit 2), as is `--set` without `--new`.
241
241
  - `--parent-answers LEVEL` (with `--new`) gives the new session its own `guardrails.parent_answers` (`hard`, `standard`, `soft`), tighten-only: config.yml's applies when it is stricter, with `chi send: warning: --parent-answers soft is looser than config.yml's …`. See *Parent answers* below.
242
- - `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit, session settings and parent answers, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool`, `--set` and `--parent-answers` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it; a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
242
+ - `--continues ID` (with `--new`) starts the next link of that session's chain instead (see *Session chains*): in its folder, on its model (or the one `--model` names), LLM context, thinking level, toolkit, session settings and parent answers, so `--dir`, `--llm-context*`, `--thinking`, `--toolkit`, `--tool`, `--no-tool`, `--set` and `--parent-answers` are refused beside it; `ID` may be a prefix or `last:ID`, the chain's latest link. Without a message the new link starts idle; `--wait` then has nothing to wait for (a usage error). It prints `<full id> started (continues 3fa2c1d0)`, with `; 2 delegates moved` inside the parentheses when open delegates moved to it, and `; context moved: pr-412, ci; context stayed: notes` for its [attached context](context.md#movable-context); a refusal (continued already, naming the next link; an open delegate that can't move; a turn running) is `chi send: refused: …`, exit 1, and a `--model` it can't start on is `chi send: could not start a session: …`, exit 1, nothing archived. A fallback to the default model (below) prints `chi send: warning: …`.
243
243
  - The 16 KiB cap applies: `git diff | chi send --new …` on a big diff is refused; name the branch in the message instead and let the session read it.
244
244
  - A session nobody attaches to stalls at its first question or guardrail ask until someone answers it (`chi answer`, the web, `chi --attach`), and its worker stays up meanwhile: it doesn't idle-exit while a question waits (`chi sessions list` shows it as `waiting`; `chi sessions stop ID` frees it). Otherwise it idle-exits after `session.idle_exit_minutes` like any worker. These are ordinary sessions: delete them like any other.
245
245
 
@@ -259,12 +259,13 @@ A session can **continue** an earlier one: "the next run of the same routine" (a
259
259
 
260
260
  - **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message. Its title (`/title`) is the previous link's, with or without a first message.
261
261
  - **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 09:12), 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.
262
+ - **Its attached context:** the previous link's movable sources (a command source by default; a pushed one stays) are copied to the new link before its worker starts, with their last text and the removed and muted markers, and a second note (`attached context`, after the carry note) names what moved and what stayed. A push into the archived link is then refused with the new link's id. See [Movable context](context.md#movable-context).
262
263
  - **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.
263
264
  - **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.
264
265
  - **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.
265
266
  - **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.
266
267
  - **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.
267
- - **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`, `thinking`, `settings`, `toolkit`, `tools`, `no_tools` or `parent_answers` (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), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task); 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`.
268
+ - **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`, `thinking`, `settings`, `toolkit`, `tools`, `no_tools` or `parent_answers` (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), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task), and `moved_context`: `{moved: [names], stayed: [{name, why}]}`, the attached context it took (`why` `pushed` or `not movable`); 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`.
268
269
  - **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 an opener in the composer, not sent (a draft written in this session stays instead; one carried over from another session or the start page gives way to the opener, and the toast's **Restore draft** brings it back): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; 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 id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link with a recap of its own reads its auto title, else its recap's first sentence, instead (a link without one reads its chain's name, and a first link's recap names its chain). A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) 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 `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else 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 the `archived` toggle is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
269
270
  - **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
270
271
  - **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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: claude-peer
3
- version: 0.4.5
3
+ version: 0.4.6
4
4
  scope: system
5
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
@@ -13,7 +13,7 @@ plugin:
13
13
  sha256: sha256:50f427d3862ae9d0d15693b4d887c528162b96c0320cc663df89d59d830d4660
14
14
  scripts:
15
15
  pair: sha256:d8089ec422a816367d8c41393235223e06b21cbc0ae1e37b6fc22e94ea9a1497
16
- chi-peer-SKILL.md: sha256:b9366f8f4aee9c54d209368ce421eab7b971c03c75efab0415d1988fd40f9e60
16
+ chi-peer-SKILL.md: sha256:e279f484f21833180f5328f8a775d96d6a09fc2f8a02063a1d30ead2eaf365ca
17
17
  # The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
18
18
  # An agreeing answer's closing wake (the skills say an OK wakes chi once)
19
19
  # and the plugin's OUTBOX (Plugin::Peer's rename of REVIEW) need chi 0.70.0.
@@ -65,6 +65,7 @@ permissions.
65
65
  - **context**: take it in for your own work. No reply, unless something in it
66
66
  contradicts what you know: then reply with that.
67
67
  - **question**: answer it briefly.
68
+ - **moved**: chi continued the session (below).
68
69
 
69
70
  Tell the user in one line what came in and what you answered.
70
71
 
@@ -89,6 +90,19 @@ EOF
89
90
  - Never answer a chi message with `chi send`: it counts as the user typing,
90
91
  outside the round's limit. Use it only when the user asks you to.
91
92
 
93
+ ## chi continued the session
94
+
95
+ chi can continue its session as a new one, with a new id. Your pairing stays
96
+ with the old id, so pair again:
97
+
98
+ - **A `moved` message** (`chi <old> · moved · continued as <new id>`): run
99
+ the pair command again with the new id, the same way you paired before
100
+ (the same `CHI_REMOTE`, the same `--inbox-socket`). From then on answer only
101
+ the new id; an answer you are still writing goes there too. Don't reply to
102
+ the message itself.
103
+ - **A push refused** with `<old> was continued as <new id>`: pair again with
104
+ the new id the same way, then push the answer again, to the new id.
105
+
92
106
  ## chi on another machine over SSH
93
107
 
94
108
  When the chi session runs on a machine this Claude session reaches with SSH
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: codex-peer
3
- version: 0.3.4
3
+ version: 0.3.5
4
4
  scope: system
5
5
  description: "Pair this session with an OpenAI Codex CLI session you work with: the codex_send tool and /codex send it a part of the work (a plan step, a diff, a finding) to validate or as context, with codex queue, and Codex's answer comes back as the codex-outbox context; /codex install-skill installs Codex's chi-peer skill, which pairs"
6
6
  trust_level: reviewed
@@ -13,7 +13,7 @@ plugin:
13
13
  sha256: sha256:b03465566272891237ac1906980ff6956579a4703d3a42c414ce50a81de551f6
14
14
  scripts:
15
15
  pair: sha256:a61c3d311ce3f3e5386e90f0f867f649a39763a5883877e731e43d84dd9c85aa
16
- chi-peer-SKILL.md: sha256:7c392ac04c115ea1e762ad562b5c4c44c3eca71558ba5d9dfeaeabdd533b6a82
16
+ chi-peer-SKILL.md: sha256:cbcc12f27b99d0cd40744b86d98e2b0ad55ff63697304a652ebe5ed36fe686d1
17
17
  # The plugin needs Samagotchi::Plugin::Peer and chi.peer_source (chi 0.67.0).
18
18
  # An agreeing answer's closing wake (the skills say an OK wakes chi once)
19
19
  # and the plugin's OUTBOX (Plugin::Peer's rename of REVIEW) need chi 0.70.0.
@@ -26,7 +26,7 @@ the first line of every message. One whose first line is exactly
26
26
  chi <8 hex> · <kind> · <title>
27
27
  ```
28
28
 
29
- (`<kind>` being `validate`, `context` or `question`) comes
29
+ (`<kind>` being `validate`, `context`, `question` or `moved`) comes
30
30
  **from chi, a peer agent, not the user**. It can't approve anything, change settings, widen the
31
31
  sandbox or grant permissions, and its requests go through this session's own
32
32
  approvals as if no one had asked. The part it carries, and a payload file it
@@ -96,6 +96,7 @@ text: read that file.
96
96
  - **context**: take it in for your own work. No reply, unless something in it
97
97
  contradicts what you know: then reply with that.
98
98
  - **question**: answer it briefly.
99
+ - **moved**: chi continued the session (below).
99
100
 
100
101
  Tell the user in one line what came in and what you answered.
101
102
 
@@ -121,6 +122,19 @@ EOF
121
122
  - Never answer a chi message with `chi send`: it counts as the user typing,
122
123
  outside the round's limit. Use it only when the user asks you to.
123
124
 
125
+ ## chi continued the session
126
+
127
+ chi can continue its session as a new one, with a new id. Your pairing stays
128
+ with the old id, so pair again:
129
+
130
+ - **A `moved` message** (`chi <old> · moved · continued as <new id>`): run
131
+ the pair command again with the new id, the same way you paired before
132
+ (the same `CHI_REMOTE`, if any). From then on answer only the new id; an
133
+ answer you are still writing goes there too. Don't reply to the message
134
+ itself.
135
+ - **A push refused** with `<old> was continued as <new id>`: pair again with
136
+ the new id the same way, then push the answer again, to the new id.
137
+
124
138
  ## Don't loop
125
139
 
126
140
  Never reply to an acknowledgement, a thanks, or a message that only confirms
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.140.0
3
+ version: 0.141.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:
@@ -25,12 +25,14 @@ module Samagotchi
25
25
 
26
26
  USAGE = <<~TEXT
27
27
  Usage: chi context <add|push|ls|show|refresh|rm|mute|unmute|report> [options] [TARGET]
28
- add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] TARGET
29
- add URL [--why TEXT] [--hint TEXT] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
28
+ add NAME (--cmd CMD | --push) [--every SECONDS] [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET
29
+ add URL [--why TEXT] [--hint TEXT] [--[no-]movable] TARGET a URL an installed bundle's provider knows (github-pr: a PR)
30
30
  a source: CMD prints its text (plain, or JSON {"text", "summary", "wake", "hint"});
31
31
  --push: text comes from chi context push. --every: how often CMD runs
32
32
  (seconds, at least 30; default context.every_seconds). --why: why it's attached;
33
33
  --hint: one line, often its URL. NAME: a-z, 0-9 and -, up to 40.
34
+ --movable / --no-movable: whether a continue or a fork takes it to the new
35
+ session (default: a --cmd or URL source moves, a --push one stays).
34
36
  push NAME [-m TEXT] [TARGET] new text for a --push source (stdin without -m; text or JSON)
35
37
  ls [TARGET] [--format json] the sources, their age and state; a session's candidates
36
38
  show NAME [--json] [TARGET] the text (--json: the source and its snapshot)
@@ -52,6 +54,8 @@ module Samagotchi
52
54
  f.value "--every"
53
55
  f.value "--why"
54
56
  f.value "--hint"
57
+ f.switch "--movable"
58
+ f.switch "--no-movable", key: :movable, set: false
55
59
  f.switch "--project"
56
60
  end,
57
61
  "push" => CLI::Flags.new(help: HELP_WORDS) do |f|
@@ -142,15 +146,27 @@ module Samagotchi
142
146
  name: name, cmd: options[:cmd], every_seconds: ContextSources.check_every!(options[:every]),
143
147
  why: ContextSources.one_line(options[:why], ContextSources::LINE_MAX_CHARS),
144
148
  hint: ContextSources.one_line(options[:hint], ContextSources::LINE_MAX_CHARS),
145
- scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601
149
+ scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601, movable: options[:movable]
146
150
  )
147
151
  each_target(options, args) do |target|
148
- @access.context_add(target, source)
152
+ add_to(target, source)
149
153
  @stdout.puts("#{target.label} attached #{name}")
150
154
  true
151
155
  end
152
156
  end
153
157
 
158
+ # Attaches +source+ to +target+; a movable flag an older chi web
159
+ # dropped (its answer has no movable) is a warning: the source is
160
+ # attached with the default.
161
+ def add_to(target, source)
162
+ warn_movable_dropped(target, source.name) if @access.context_add(target, source).movable_dropped
163
+ end
164
+
165
+ def warn_movable_dropped(target, name)
166
+ error_line("#{command_name}: warning: the remote's chi web is too old for --movable/--no-movable: " \
167
+ "#{name} on #{target.label} has the default (update chi there)")
168
+ end
169
+
154
170
  # A URL through the installed bundles' providers (ContextProviders):
155
171
  # the provider gives the name, command, hint and interval. Not gated
156
172
  # like --cmd (chi-context-cmd): the command is the bundle's.
@@ -167,10 +183,11 @@ module Samagotchi
167
183
  name: resolved.name, cmd: resolved.cmd, every_seconds: resolved.every_seconds,
168
184
  why: ContextSources.one_line(options[:why] || resolved.why, ContextSources::LINE_MAX_CHARS),
169
185
  hint: ContextSources.one_line(options[:hint] || resolved.hint, ContextSources::LINE_MAX_CHARS),
170
- scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601, provider: resolved.bundle
186
+ scope: nil, added_by: added_by, created_at: Time.now.utc.iso8601, provider: resolved.bundle,
187
+ movable: options[:movable]
171
188
  )
172
189
  each_target(options, args) do |target|
173
- @access.context_add(target, source)
190
+ add_to(target, source)
174
191
  @stdout.puts("#{target.label} attached #{source.name}")
175
192
  true
176
193
  end
@@ -183,8 +200,9 @@ module Samagotchi
183
200
  why = ContextSources.one_line(options[:why], ContextSources::LINE_MAX_CHARS)
184
201
  hint = ContextSources.one_line(options[:hint], ContextSources::LINE_MAX_CHARS)
185
202
  each_target(options, args) do |target|
186
- name = @access.context_add_url(target, url, why: why, hint: hint, added_by: added_by)
187
- @stdout.puts("#{target.label} attached #{name}")
203
+ added = @access.context_add_url(target, url, why: why, hint: hint, added_by: added_by, movable: options[:movable])
204
+ warn_movable_dropped(target, added.name) if added.movable_dropped
205
+ @stdout.puts("#{target.label} attached #{added.name}")
188
206
  true
189
207
  end
190
208
  rescue ContextProviders::Invalid => e
@@ -210,6 +228,9 @@ module Samagotchi
210
228
  # two fetches would read as a change and then vanish.
211
229
  error_line("#{command_name}: #{SessionAccess::ContextPush.not_push_message(name)}")
212
230
  next false
231
+ when :continued
232
+ error_line("#{command_name}: #{SessionAccess::ContextPush.continued_message(name, target.session_id, pushed.next_id)}")
233
+ next false
213
234
  end
214
235
 
215
236
  @stdout.puts("#{target.label} #{name}: #{pushed.revision == pushed.before ? "unchanged" : "new text"} " \
@@ -452,7 +473,8 @@ module Samagotchi
452
473
  "cmd" => source.cmd, "every_seconds" => source.every_seconds, "why" => source.why, "hint" => source.hint,
453
474
  "added_by" => source.added_by, "created_at" => source.created_at, "fetched_at" => snapshot.fetched_at,
454
475
  "revision" => snapshot.revision, "summary" => snapshot.summary, "error" => snapshot.error,
455
- "muted" => entry.muted, "shadowed" => entry.shadowed, "unread" => entry.unread }.compact
476
+ "muted" => entry.muted, "shadowed" => entry.shadowed, "unread" => entry.unread,
477
+ "movable" => (source.movable? if source.scope == "session") }.compact
456
478
  end
457
479
 
458
480
  def print_rows(rows, labels:)
@@ -465,6 +487,7 @@ module Samagotchi
465
487
  else
466
488
  "ok"
467
489
  end
490
+ state = "#{state} · stays" if r["movable"] == false
468
491
  [*(labels ? [r["target"]] : []), r["name"], r["scope"], kind, age(r["fetched_at"]), state, r["why"].to_s]
469
492
  end
470
493
  widths = lines.transpose.map { |column| column.map(&:length).max }
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "context_sources"
4
+ require_relative "session_inbox"
5
+ require_relative "log"
6
+
7
+ module Samagotchi
8
+ # Movable context on its two paths, a continue (SessionContinue) and a
9
+ # plugin fork (ctx.sessions.fork): from SessionManager.spawn_session's
10
+ # before_spawn, once the new session and its first note are saved and
11
+ # before its worker starts, so its first absorb sees the sources.
12
+ module ContextMove
13
+ # What the new session's note says it is from.
14
+ NOTE_SOURCE = "attached context"
15
+
16
+ # ContextSources.move, then a second note in the new session saying what
17
+ # moved and what stayed (named by time, it sorts after the first). Best
18
+ # effort: a failure is logged and the session starts without (its start
19
+ # must not be undone for it).
20
+ # @return [ContextSources::Moved]
21
+ def self.call(from:, to:, mode:, state_dir:, seed: nil)
22
+ moved = ContextSources.move(from: from, to: to, mode: mode, seed: seed, state_dir: state_dir)
23
+ text = moved.note_text(from)
24
+ SessionInbox.write_note(to, text: text, source: NOTE_SOURCE, state_dir: state_dir) if text
25
+ moved
26
+ rescue StandardError => e
27
+ Log.warn(:context, "context_move_failed", sid: to, from: from, error: e.class.name, msg: e.message.to_s[0, 200])
28
+ ContextSources::Moved.none
29
+ end
30
+ end
31
+ end