sessionphone 0.3.0__tar.gz → 0.3.2__tar.gz

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.
Files changed (66) hide show
  1. {sessionphone-0.3.0 → sessionphone-0.3.2}/PKG-INFO +669 -106
  2. {sessionphone-0.3.0 → sessionphone-0.3.2}/README.md +668 -105
  3. {sessionphone-0.3.0 → sessionphone-0.3.2}/pyproject.toml +1 -1
  4. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/account.py +103 -48
  5. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/avatar.py +1 -1
  6. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/bot.py +364 -55
  7. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/calls.py +397 -65
  8. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/dc_env.py +235 -55
  9. sessionphone-0.3.2/src/sessionphone/bridge/floor.py +75 -0
  10. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/heard_sink.py +2 -2
  11. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/listalert.py +3 -3
  12. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/mouthleg.py +7 -2
  13. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/pidfile.py +49 -1
  14. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/seatmail.py +109 -7
  15. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/spool.py +96 -11
  16. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/xdc_feed.py +6 -3
  17. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/channels.py +208 -27
  18. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/cli.py +373 -53
  19. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/hearing.py +224 -32
  20. sessionphone-0.3.2/src/sessionphone/keygate.py +489 -0
  21. sessionphone-0.3.2/src/sessionphone/migrate.py +342 -0
  22. sessionphone-0.3.2/src/sessionphone/paths.py +248 -0
  23. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/reach.py +107 -15
  24. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/seat.py +25 -4
  25. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/service.py +214 -33
  26. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/settings.py +63 -5
  27. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/call.py +138 -14
  28. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/server.py +2 -2
  29. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/turns.py +138 -65
  30. {sessionphone-0.3.0 → sessionphone-0.3.2}/wheel-bin/dc-env +46 -14
  31. {sessionphone-0.3.0 → sessionphone-0.3.2}/wheel-bin/dc-say +22 -7
  32. sessionphone-0.3.0/src/sessionphone/paths.py +0 -132
  33. {sessionphone-0.3.0 → sessionphone-0.3.2}/LICENSE +0 -0
  34. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/__init__.py +0 -0
  35. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/apps/__init__.py +0 -0
  36. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/apps/guide-trail/icon.png +0 -0
  37. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/apps/guide-trail/index.html +0 -0
  38. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/apps/guide-trail/manifest.toml +0 -0
  39. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/__init__.py +0 -0
  40. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/ack.py +0 -0
  41. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/earleg.py +0 -0
  42. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/echo.py +0 -0
  43. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/firstcall.py +0 -0
  44. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/recording.py +0 -0
  45. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/retract.py +0 -0
  46. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/stt.py +0 -0
  47. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/bridge/xdc_rt.py +0 -0
  48. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/displayname.py +0 -0
  49. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/imgtext.py +0 -0
  50. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/imgtext.swift +0 -0
  51. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/jobs.py +0 -0
  52. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/media.py +0 -0
  53. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/noise.py +0 -0
  54. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/pages/qr-page.html +0 -0
  55. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/procname.py +0 -0
  56. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/py.typed +0 -0
  57. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/qrprint.py +0 -0
  58. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/voicemsg.py +0 -0
  59. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/__init__.py +0 -0
  60. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/enrol_qr.py +0 -0
  61. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/names.py +0 -0
  62. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/page/index.html +0 -0
  63. {sessionphone-0.3.0 → sessionphone-0.3.2}/src/sessionphone/web/passkeys.py +0 -0
  64. {sessionphone-0.3.0 → sessionphone-0.3.2}/wheel-bin/dc-answer +0 -0
  65. {sessionphone-0.3.0 → sessionphone-0.3.2}/wheel-bin/dc-hangup +0 -0
  66. {sessionphone-0.3.0 → sessionphone-0.3.2}/wheel-bin/dc-xdc-realtime +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sessionphone
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Ring your Claude Code session from your phone over Delta Chat
5
5
  Author: Mike Bailey
6
6
  Author-email: Mike Bailey <mike@sessionkit.ai>
@@ -48,9 +48,23 @@ Then say "set up Delta". The skill (`skills/sessionphone/SKILL.md`) walks
48
48
  the agent through it: it makes its own Delta Chat profile
49
49
  (`sessionphone setup`), shows you a QR code (`sessionphone qr`), links your
50
50
  phone (`sessionphone link`), starts the bot (`sessionphone bot install`) and
51
- rings you. "Status" at the end of this page says what works and what does
51
+ rings you. Before your phone is linked, `setup` checks the speech-to-text
52
+ server the bot will hear you through, and when it does not answer, says what
53
+ to install. "Status" at the end of this page says what works and what does
52
54
  not.
53
55
 
56
+ **Where Delta text lands.** The bot mails each Delta message, and each call
57
+ turn, to its agent's box, named after the agent: box `claude`, unless
58
+ `DC_AGENT` names another agent or the session running install was started as
59
+ one ([Which agent install picks](#the-bots-seat)); install's `box:` line names
60
+ it. A Claude session hears that box only when it
61
+ was started as that agent (`session start claude`); a plain `claude` gets no
62
+ copy, and nothing tells it so. `bot install` asks sessionmail whether the
63
+ session running it hears the box, and says so on its `hear:` line. Set
64
+ `DC_MAILTO` to send the text to another box instead: install again with it
65
+ set, and the `hear:` line then tells you about that box.
66
+ [Hearing Delta text in Claude](#hearing-delta-text-in-claude) has the rest.
67
+
54
68
  ## Installation
55
69
 
56
70
  ```bash
@@ -103,10 +117,21 @@ macOS 14 or later, read from `uv.lock`.
103
117
  - **sessionkit:** the kit asks for `sessionphone[media]` (0.2.1 or later),
104
118
  so a kit install already has the pack, and `bot install` fetches nothing.
105
119
 
120
+ ## Requirements
121
+
122
+ - [x] macOS (tested on macOS 27, Apple silicon)
123
+ - [ ] Linux
124
+ - [ ] WSL
125
+ - [ ] Windows
126
+
127
+ What sessionphone depends on, what needs each one, and how to upgrade it:
128
+ [dependencies][dependencies].
129
+
106
130
  ## Usage
107
131
 
108
132
  `sessionphone --help` lists the verbs. The skill in
109
- `skills/sessionphone/SKILL.md` is the map for the agent.
133
+ `skills/sessionphone/SKILL.md` is the map for the agent, and the pages it
134
+ links for the long reference are in `docs/`.
110
135
 
111
136
  ### The bot's picture: `sessionphone avatar`
112
137
 
@@ -156,8 +181,9 @@ install` line that changes it. `DC_NAME` always wins.
156
181
  agent's name to a file, `name-replaced`, under the root. The bot reads it at
157
182
  its start and removes it once the name is settled. Nothing older is
158
183
  remembered. `DC_NAME` is saved in `phone.json` like every `DC_*`.
159
- `DC_AGENT_NAME` is a different setting: the name the bot says in greetings
160
- and puts in its mail's From.
184
+ The bot greets a call by its display name, the name people see in their
185
+ chat list. `DC_AGENT_NAME` is a different setting: the agent's name, which the
186
+ bot puts in its mail's From. Set, it is also the name in the greetings.
161
187
 
162
188
  ### The invite, as a QR on the terminal: `sessionphone invite`
163
189
 
@@ -232,7 +258,7 @@ The mail's shape:
232
258
 
233
259
  | Field | Value |
234
260
  |---|---|
235
- | To | `DC_MAILTO`, else `<agent>@<host>` |
261
+ | To | `DC_MAILTO`, else `<agent>`, a bare box name: sessionmail adds this host |
236
262
  | From | `<Name>'s Delta Chat bridge <<agent>-chat@...>`. The seat is `<agent>-chat`, the local part of `DC_ADDR`; sessionmail sets the domain |
237
263
  | Subject | `[<room> <chat_id>] <tail>`. `<room>` is the chat name cut to 14 characters, after `DC_PROFILE` when one is set. `<tail>` is `<sender>: <text>` or the kind's own words (`REQUEST ...`). ASCII, 69 characters at most. A backtick becomes `'` and `$(` becomes `$ (`, because `message send` refuses a subject that could execute |
238
264
  | Body | the text, with any quoted (`>`) lines, then a `---` footer: `chat:`, `from:` and how to answer. The mail leg stops reading aloud at `---` |
@@ -247,10 +273,18 @@ rule, a page rule):
247
273
  | `X-Delta-Chat-Profile` | the profile, when not the default | every mail |
248
274
  | `X-Delta-Chat-Kind` | `message`, `request`, `reaction`, `webxdc` or `call` | M1-M5 |
249
275
  | `X-Delta-Chat-Msg-Id` | the Delta message id, which `dc-react --msg` takes. For a reaction, the message reacted to | M1-M3 only |
250
- | `X-Delta-Chat-Sender` | the Delta contact's **address**: the key Delta verified. The display name is only a claim | M1-M3. A tap has no sender |
251
- | `X-Delta-Chat-Channel` | the channel NAME the chat is listed as in `channels.txt` | M1-M4, only when the chat is listed |
276
+ | `X-Delta-Chat-Sender` | the address the sender's message carries, **as a label for people**. It is not the sender's key, and it is not an identity: the display name is only a claim, and so is the address. Use `X-Delta-Chat-Channel` to know who it is | M1-M3. A tap has no sender |
277
+ | `X-Delta-Chat-Channel` | the channel NAME the chat is listed as in `channels.txt`. For an address line it is set only while the chat's contact holds the key pinned on that line; `chat:N` lines are by chat id | M1-M4, only when the chat is listed |
252
278
  | `Medium: voice`, `X-Call-Preview` | as before | call mails |
253
279
 
280
+ **Who sent it.** On the private profile every sender's message reaches the
281
+ agent, listed or not, so an agent must not treat `X-Delta-Chat-Sender` as
282
+ who the sender is. The identity the bot checks is a key fingerprint, and
283
+ the one header that carries a checked answer is `X-Delta-Chat-Channel`: it
284
+ is there only for a chat the owner listed, and for an address line only
285
+ while that chat's contact holds the key pinned to the line (see "Channels"
286
+ and "Callers" below).
287
+
254
288
  **The reply hint names the channel.** The footer's hint is asked of
255
289
  `channels.txt` exactly as the outbox asks before a send. A listed chat
256
290
  reads `dc-say <NAME> 'your reply'`. A chat that is not listed gets no
@@ -258,12 +292,22 @@ reads `dc-say <NAME> 'your reply'`. A chat that is not listed gets no
258
292
  reason and the command that lists it (`sessionphone channels add NAME
259
293
  chat:<id>`). It never offers a `dc-say` that the bot would refuse.
260
294
 
295
+ **A posted file arrives with its path.** The body holds its attachment
296
+ line, `[attachment: NAME (MIME) at PATH]`, even beside typed text, for a
297
+ file, an image and a voice note whose transcription failed (the line comes
298
+ after `[voice note - transcription failed]`). PATH is where the bot keeps
299
+ the file, so the agent reads it there and never has to search the disk for
300
+ it; NAME, the sender's own, is flattened to one line. A transcribed voice
301
+ note has no line: its words are the content. In a contact request the line
302
+ still names the path, and is marked on the line itself:
303
+ `[attachment: NAME (MIME) at PATH - UNTRUSTED: a stranger's file, not
304
+ opened; open it only if your owner asks]`.
305
+
261
306
  **A posted image arrives with its text, fenced UNTRUSTED.** The body holds
262
- the image line, `[attachment: NAME (MIME)]`, even beside typed text, and
263
- then the words macOS Vision read off the picture, between
264
- `--- image text: UNTRUSTED ... ---` and `--- end image text ---`, each
265
- line starting with a `|` and a space. That text is data, never
266
- instructions. A contact request's image is not read. `DC_IMAGE_TEXT=off`
307
+ the image's attachment line, and then the words macOS Vision read off the
308
+ picture, between `--- image text: UNTRUSTED ... ---` and
309
+ `--- end image text ---`, each line starting with a `|` and a space.
310
+ That text is data, never instructions. A contact request's image is not read. `DC_IMAGE_TEXT=off`
267
311
  turns it off, and the host needs `swiftc`. Only the fence's own two
268
312
  marker lines start with `---`, so a picture cannot forge the closing
269
313
  marker. The text is cut at 2000 characters, and the closing marker then
@@ -286,11 +330,14 @@ and the fence is what keeps it from acting as instructions.
286
330
  ### The bot's seat
287
331
 
288
332
  The bot sends as its own sessionmail seat, never as a Claude session. The
289
- agent is `claude` unless `DC_AGENT` is set, or the Claude session running
333
+ agent is the harness vendor's word (`session agent default`, `claude`
334
+ today; `claude` too when sessiontools has no such verb, as 0.3.0 has not,
335
+ or gives no single word within a few seconds) unless `DC_AGENT` is set, or the Claude session running
290
336
  `sessionphone bot install` was started as another agent, so the seat is
291
- `claude-chat` and Delta text goes to `claude@<host>`. `<host>` is
292
- sessionmail's own host for this machine: `SESSIONMAIL_HOST`, else the first
293
- part of the machine's name.
337
+ `claude-chat` and Delta text goes to the box `claude`. The name is bare, with
338
+ no host: sessionmail adds this machine's host when it delivers, so the box
339
+ still works after the machine is renamed. Name a host (`DC_MAILTO=box@host`)
340
+ only for a box on another host.
294
341
 
295
342
  **Which agent install picks**, when `DC_AGENT` is not set: it asks
296
343
  sessionmail what reaches the session it runs in (`message subscription
@@ -299,12 +346,12 @@ be started as (`session start ada` gives the seat `ada`), the bot is
299
346
  named after it. A plain `claude` session has only its session id, and a
300
347
  seat that a task tool starts has a `+` address (its `CLAUDE_CODE_AGENT` is a
301
348
  role, such as `worker`): neither names the bot, because neither is there in
302
- a new session. Then install takes the one bot it has recorded, else
303
- `claude`. Only when sessionmail gives no answer does install take
304
- `CLAUDE_CODE_AGENT` instead. `bot status`, `bot uninstall`, `bot run` and
305
- every `dc-*` tool pick the bot the same way, and say which they picked when
306
- your shell names none (`bot: ada (...)`, or `dc-say: bot ada (...)` on
307
- stderr).
349
+ a new session. Then install takes the one bot it has recorded, else the
350
+ harness vendor's word, else `claude`. Only when sessionmail gives no
351
+ answer does install take `CLAUDE_CODE_AGENT` instead. `bot status`,
352
+ `bot uninstall`, `bot run` and every `dc-*` tool pick the bot the same
353
+ way, and say which they picked when your shell names none
354
+ (`bot: ada (...)`, or `dc-say: bot ada (...)` on stderr).
308
355
 
309
356
  **One bot to a root.** A root is one Delta Chat account, so two bots on it
310
357
  is a broken state: the old one keeps mailing its own box, and its calls and
@@ -313,7 +360,7 @@ the same root (its `SESSIONPHONE_ROOT`, `DC_ROOT` or `DC_ACCOUNTS`), as
313
360
  after a rename, install refuses (exit 2), writes nothing, and names it:
314
361
 
315
362
  ```text
316
- sessionphone: ~/.sessionkit/phone/settings/phone.json records bot claude on the same root as bot ada (~/.sessionphone). ... To replace it: `sessionphone bot install --replace claude` ...
363
+ sessionphone: ~/.sessionkit/phone/settings/phone.json records bot claude on the same root as bot ada (~/.sessionkit/phone/state). ... To replace it: `sessionphone bot install --replace claude` ...
317
364
  ```
318
365
 
319
366
  `--replace claude` retires that bot: its launchd job is booted out, its
@@ -440,7 +487,7 @@ needed. If you want one, set it in sessionmail's settings key
440
487
  | Setting | What it is, and what the child `message send` gets |
441
488
  |---|---|
442
489
  | `DC_ADDR` | the bot's own From seat, `<agent>-chat`; its local part is the seat the key signs as. The child gets `TM_AGENT_ADDRESS` and `EMAIL` = that local part, so an `EMAIL` inherited from whoever ran install never becomes the From |
443
- | `DC_MAILTO` | where Delta text goes: the agent's box, `<agent>@<host>` by default |
490
+ | `DC_MAILTO` | where Delta text goes: the agent's box, the bare `<agent>` by default; kept as given, so `box@host` names a box on another host |
444
491
  | `DC_SIGNER_SOCK` | `CLAUDE_SESSION_AGENT_SOCK`: the seat agent's socket (`SEAT_SOCKET`) |
445
492
  | `DC_SIGNER_PUB` | `CLAUDE_SESSION_PUB`: the seat's `.pub` (`SEAT_PUB`), or a copy of it kept apart from the key |
446
493
  | `DC_MESSAGE` | the `message` binary, resolved to an absolute path at start (launchd's `PATH` is short). Else `message` on `PATH` |
@@ -451,9 +498,42 @@ Do not set `DC_ADDR` to the agent's box. The key is the bot's seat, so the
451
498
  bot refuses: the signer and the From would be different seats. To send Delta
452
499
  text somewhere else, set `DC_MAILTO`.
453
500
 
501
+ ### Moving the root with bot migrate
502
+
503
+ The bot's root, which holds its Delta Chat account and its **keys**, was
504
+ the old `~/.sessionphone`, and is now `~/.sessionkit/phone/state/`. A Mac that has the
505
+ old root and not the new one keeps using the old one, and each verb says so
506
+ once on stderr; nothing moves by itself. A root you set (`SESSIONPHONE_ROOT`,
507
+ `DC_ROOT`, or the one `bot install` keeps in `phone.json`) is never moved.
508
+ To move the default root, once on each Mac:
509
+
510
+ ```bash
511
+ sessionphone bot migrate --dry-run # checks, says each step, changes nothing
512
+ sessionphone bot migrate
513
+ ```
514
+
515
+ Do it with care, the root is where the keys live:
516
+
517
+ - **Stop the bot first** (`sessionphone bot uninstall`). `migrate` refuses
518
+ (exit 2, nothing moved) while a bot runs on the old root (its `.bot.lock`
519
+ is held, or its `bot.pid` names a running bot) or a call is live. It also
520
+ refuses when the new root is already there, when the old path is a link
521
+ somewhere else, or when the two are on different filesystems.
522
+ - **The keys move by rename**, never copy then delete: the account keeps its
523
+ files, its inodes and its mode 0700.
524
+ - **The old path is left as a symlink** to the new root, so anything still
525
+ holding the old name keeps working. `migrate` and `bot uninstall` never
526
+ delete the old root.
527
+ - **The launchd plists** whose root is the old one are re-rendered to name the
528
+ new one; any other plist is left as it is. launchd reads a plist when it
529
+ loads the job, so run `sessionphone bot install` again to start the bot on
530
+ the new path.
531
+ - **`--dry-run` shows where the account will open** (the `account:` line) and
532
+ every step it would take, and changes nothing. Read it before the real run.
533
+
454
534
  ### Hearing Delta text in Claude
455
535
 
456
- Delta text lands in the agent's box, `claude@<host>`. Mail to that box
536
+ Delta text lands in the agent's box, `claude`. Mail to that box
457
537
  reaches a session only through the pager's doorbell, and the doorbell rings
458
538
  only for a session started as that agent. `bot install` (and
459
539
  `sessionphone setup`) asks sessionmail what reaches the session it runs in
@@ -464,17 +544,20 @@ When the session will not hear it, the line says so first, and it is the
464
544
  first line `bot install` prints, ahead of `installed:`:
465
545
 
466
546
  ```text
467
- hear: this session will NOT hear Delta messages or calls: they go to box <agent>, and this session hears only its own session box. Ways out: start Claude with `session start <agent>` (only a session started as <agent> hears that box, today and in later sessions); or install again with DC_AGENT=NAME (the agent a session is started as) or DC_MAILTO=ADDR (a box a session hears); or `sessionphone bot install --hear-here`, which lets this session hear box <agent> until this session ends.
547
+ hear: this session will NOT hear Delta messages or calls: they go to box <agent>, and this session hears only its own session box. Ways out: start Claude with `session start <agent>` (only a session started as <agent> hears that box, today and in later sessions); or install again with DC_AGENT=NAME (the agent a session is started as) or DC_MAILTO=BOX (a box a session hears); or `sessionphone bot install --hear-here`, which lets this session hear box <agent> until this session ends.
468
548
  ```
469
549
 
470
550
  This is a warning, not a refusal: the bot is installed either way, and the
471
551
  exit code is 0. When the session does hear the box, the line says
472
552
  `this session hears box <agent> (its seat <agent>): Delta messages and calls
473
- reach it, while it runs`. From a shell that is not a Claude session (a
474
- Terminal), nothing can be measured, so the line gives the advice:
553
+ reach it, while it runs`. When `DC_MAILTO` names the session's own box, its
554
+ session id, the line says `(its own session box)` instead. That box lasts
555
+ only as long as the session: a later session has another id, and does not
556
+ hear it. From a shell that is not a Claude session (a Terminal), nothing can
557
+ be measured, so the line gives the advice:
475
558
 
476
559
  ```text
477
- hear: To hear Delta messages, start Claude with: session start <agent> (it runs `claude --agent <agent>`: only a session started as <agent> hears the box; a plain `claude` gets no copy of its mail, and nothing says so). To send Delta text to another box, install again with DC_AGENT=NAME (the agent a session is started as) or DC_MAILTO=ADDR (a box a session hears).
560
+ hear: To hear Delta messages, start Claude with: session start <agent> (it runs `claude --agent <agent>`: only a session started as <agent> hears the box; a plain `claude` gets no copy of its mail, and nothing says so). To send Delta text to another box, install again with DC_AGENT=NAME (the agent a session is started as) or DC_MAILTO=BOX (a box a session hears).
478
561
  ```
479
562
 
480
563
  The bot gives the same advice at every start, on the `agent box:` line of
@@ -492,11 +575,23 @@ installed it.
492
575
  Install never does this unless asked. Its `hear-here:` line, before
493
576
  `hear:`, says what it did, or why not. When it did not take, the session
494
577
  still will not hear the box, so the warning leads and `hear-here:` follows it.
578
+ - When the bot's name came from the default (no `DC_AGENT`, no seat, no
579
+ bot recorded) and nothing hears its box, the `hear:` line is followed by
580
+ one more line, which names the fix:
581
+
582
+ ```text
583
+ default: no seat owns box claude: the bot's name came from the default, so nothing hears its mail yet. Start a bare `session start` first; it becomes claude.
584
+ ```
585
+
586
+ Install asks sessionmail who hears the box (`message subscription list
587
+ <agent>`, read only). It prints the line only when the answer is plainly
588
+ "nobody": no such box, or `0 hear it` with no live listener. When this
589
+ session hears the box, or the answer cannot be read, there is no line.
495
590
  - Two windows started as `session start <agent>` share one box, and both get
496
591
  each Delta message. The second launch needs `--allow-second-holder`;
497
592
  without it, it exits 3.
498
593
  - A Claude session started any other way (plain `claude`) does not hear
499
- `<agent>@<host>`: it gets no copy of the mail, and nothing tells it so.
594
+ the box `<agent>`: it gets no copy of the mail, and nothing tells it so.
500
595
  - When sessionmail cannot be asked (an older one without `message
501
596
  subscription list`), the line gives the advice and says that nothing was
502
597
  measured. It never says the session hears the box.
@@ -585,7 +680,10 @@ A send that `message send` refuses is never lost:
585
680
  - **Retried:** `identity`, `mta`, `undeliverable`, and the bot's own `exec`
586
681
  (the binary could not run) and `timeout` (no answer in 30 s).
587
682
  - **Backoff:** each mail keeps its own attempt count and next try. The wait
588
- starts at 60 s and doubles to at most 3600 s.
683
+ starts at 60 s and doubles to at most 3600 s. A call turn (a mail whose
684
+ Message-ID starts `call-turn-`, from a Delta call or a web call) is first
685
+ retried at 5 s, because the caller is still on the line, then at 60 s and
686
+ on: 5, 60, 120 ... 3600 s.
589
687
  - **Order:** the outbox loop tries every due mail, oldest first, and a mail
590
688
  that keeps failing never blocks the ones behind it.
591
689
  - **The exception: the MTA itself is down.** A pass stops at the first
@@ -594,7 +692,7 @@ A send that `message send` refuses is never lost:
594
692
  timeout at most.
595
693
  - **A possible duplicate.** Kind `mta` with `sent: unknown` may have gone.
596
694
  It is retried anyway, and logged `sent=unknown`, because a duplicate beats
597
- a loss.
695
+ a loss. Not a call mail: its box is checked first (below).
598
696
 
599
697
  **What goes to `undelivered/dead/`.** Nothing is ever deleted from `dead/`:
600
698
 
@@ -612,14 +710,33 @@ BRIDGE MAIL FAILED <error>; NOT KEPT, the mail is lost: <why>
612
710
  BRIDGE MAIL RETRY <n> FAILED <error>; next try in <s>s: <path>
613
711
  BRIDGE MAIL HELD <n> more not tried this pass: <name> got kind <kind>, ...
614
712
  BRIDGE MAIL SENT on try <n> (first failed HH:MM:SS, last sent=<sent>): <name> <Message-ID>
713
+ BRIDGE MAIL ALREADY THERE on try <n>: <Message-ID> is in <box>'s box; not sent again: <name>
615
714
  BRIDGE MAIL DEAD <path>: <why>
616
715
  ```
617
716
 
618
717
  `NOT KEPT` means the spool itself could not be written (a full disk, say).
619
718
  `dc-live` prints `undelivered mail to the agent: N waiting, M dead` on
620
719
  stderr whenever either count is not zero. `bin/bridge-check` reports the
621
- same two counts, and they never fail its gate. The channels alert keeps its
622
- own retry, and the call mails are not spooled (part 2b).
720
+ same two counts, and they never fail its gate. `sessionphone bot status`
721
+ prints `undelivered: N waiting, M dead (<dir>)`, from the spool its installed
722
+ job names (`DC_UNDELIVERED`, else `undelivered` in the bot's home). The
723
+ channels alert keeps its own retry.
724
+
725
+ The call mails are spooled too, all but a call's preview: the turn
726
+ supersedes it and carries its whole text. A call mail goes under a
727
+ Message-ID the bridge made, and sessionmail has no duplicate check: a send
728
+ under an id already in the box lands twice. So before the spool retries a
729
+ mail that carries its own `--message-id`, it reads the target box for that
730
+ id, read-only (`message path <box>`, then the header of each file in `new/`
731
+ and `cur/`):
732
+
733
+ - The id is there: the mail went. The record is removed and nothing is sent
734
+ (`BRIDGE MAIL ALREADY THERE`).
735
+ - The id is not there: the mail is retried.
736
+ - The box cannot be checked from here (a box on another host, behind an
737
+ MTA): the mail is retried only if every try so far said `sent=no`. One
738
+ that may have gone (`sent=unknown`) goes to `dead/` instead, and is
739
+ counted there. It is never sent twice.
623
740
 
624
741
  ### App taps: `DC_XDC_IN` is the integration point
625
742
 
@@ -782,7 +899,8 @@ log (`heard_<day>.jsonl`, in `$EARS_HEARD_DIR`, else
782
899
  | F3 | `kind: saying` by this agent (`DC_AGENT`) | `{"lines": [{"who": "agent", "name": DC_AGENT_NAME, "text": TEXT}]}` |
783
900
 
784
901
  Call lines (`source: call`), other agents' sayings and noise turns are not
785
- fed. Text is one line, at most 4000 characters.
902
+ fed. Text is one line, at most 4000 characters. That cut is display only: no
903
+ agent reads this feed, and the heard log it reads keeps every word.
786
904
 
787
905
  - **Two consents.** The msg's chat must pass the channels list, as every
788
906
  send does, **and** be listed in `DC_XDC_FEED_CHATS` (comma-separated chat
@@ -882,12 +1000,19 @@ message send DC_MAILTO -s SUBJECT -f - --raw --no-shorten --json \
882
1000
  call's Reply-To.
883
1001
  - **`-r <root>`** writes In-Reply-To and References. Cc, Supersedes and the
884
1002
  ids are never raw `--header`s: `message send` owns them.
1003
+ - **A turn arrives whole.** The body of a turn mail and of a preview carries
1004
+ every word heard, however long the turn, flattened to one line and never
1005
+ cut. The heard log's turn and partial lines, and the call's transcript,
1006
+ hold the whole text too. Only display is cut: the Subject (the first 44
1007
+ characters of the turn, 34 of a preview) and the bridge's own log lines.
885
1008
  - **The From** keeps its display name and the seat, `<agent>-chat`. Its
886
1009
  domain is sessionmail's, as for every other mail.
887
- - **A call mail that fails is logged, never kept.** sessionmail has no
888
- duplicate check, so a retried call mail could land twice under one id.
889
- The Delta call path makes one try. A web call's turn is tried again only
890
- when nothing was sent, for a reason in the world, not in the mail.
1010
+ - **A call mail that fails is kept and retried**, all but a preview (see
1011
+ the undelivered spool above). sessionmail has no duplicate check, so the
1012
+ spool reads the box for the mail's id before each retry, and never sends
1013
+ it twice. The Delta call path makes one try itself; the spool makes the
1014
+ rest. A web call's turn is the same: one try, then the web server's own
1015
+ spool (see [Web call turns](#web-call-turns-mailed-to-the-seat)).
891
1016
  - **The bot refuses to start (exit 2)** when the installed `message` has no
892
1017
  `--message-id`. At start it runs one probe that sends nothing,
893
1018
  `message send probe@invalid -s probe --no-body --message-id '<x>' --json`,
@@ -907,14 +1032,15 @@ an agent that could write the outbox could message any chat on the owner's
907
1032
  Delta account. It cannot now. The bot sends **only** to a chat on its
908
1033
  allow-list, `channels.txt`, which the owner edits.
909
1034
 
910
- **The file.** `channels.txt` in the bot's root (`~/.sessionphone/channels.txt`,
1035
+ **The file.** `channels.txt` in the bot's root (`~/.sessionkit/phone/state/channels.txt`,
911
1036
  or `$DC_ROOT/channels.txt`); `DC_CHANNELS_FILE` overrides the path. Keep it 0600.
912
1037
  One entry per line, `#` starts a comment:
913
1038
 
914
1039
  ```text
915
- # NAME TARGET # note
916
- owner owner@relay.example # owner, joined 2030-06-09
917
- family chat:42 # the family group
1040
+ # sessionphone keys v1: an address line without key: is refused until pinned
1041
+ # NAME TARGET KEY # note
1042
+ owner owner@relay.example key:4E4D1E1E160E2C8D034A91AFDCF4D1301E82234F # owner, joined 2030-06-09
1043
+ family chat:42 # the family group
918
1044
  ```
919
1045
 
920
1046
  - **NAME** is lowercase `[a-z0-9_-]+`, unique, may not start with `-`, and
@@ -923,9 +1049,30 @@ family chat:42 # the family group
923
1049
  - **TARGET** is an address, meaning the 1:1 chat with that contact (never a
924
1050
  group they are in), or `chat:N`, one chat by id, the only way to list a
925
1051
  group.
926
- - `sessionphone channels` lists it; `sessionphone channels add NAME TARGET
927
- [--note TEXT]` adds a line. You can edit the file by hand. The bot re-reads
928
- it on every job, so an edit needs no restart, and it logs what changed.
1052
+ - **An address line is bound to a key.** `key:FPR` is the fingerprint of the
1053
+ key the contact holds: 40 hex digits (or 64 for a newer key), as the phone
1054
+ shows it under the contact's profile, Encryption. Spaces and colons in what
1055
+ you type are dropped. The bot sends to, and on the public profile takes in,
1056
+ that contact's 1:1 chat only while the contact holds that key. **The key is
1057
+ the identity; the address is the label.** If the contact's address changes
1058
+ but the key is the same, the chat still passes and the bot logs `now
1059
+ writes`. A different key at the same address does not pass. `chat:N` lines
1060
+ take no key (a chat id already belongs to one contact), and a `key:` on one
1061
+ is a bad line.
1062
+ - **An address line with no `key:` is refused** until a key is pinned to it.
1063
+ The refusal names the command: `sessionphone channels pin NAME`.
1064
+ - `sessionphone channels` lists it, each address line with its key or
1065
+ `UNPINNED`. `sessionphone channels add NAME TARGET [--key FPR] [--note
1066
+ TEXT]` adds a line, and `sessionphone channels pin NAME [--key FPR]` pins
1067
+ (or re-pins) a listed one. With `--key` they only write the file, so they
1068
+ work while the bot runs. Without `--key` they open the account to find the
1069
+ one key contact with that address, so the bot must be stopped; they pin only
1070
+ when exactly one key contact has the address, print its fingerprint to
1071
+ compare with the phone, and otherwise write nothing and say why. You can
1072
+ edit the file by hand. The bot re-reads it on every job, so an edit needs
1073
+ no restart, and it logs what changed, a key change included.
1074
+ - **A key that rotates is a new contact.** When someone's key changes, their
1075
+ chats are refused until you pin the new key.
929
1076
 
930
1077
  **A tool takes a NAME where it takes a chat.** `dc-say owner 'hi'`,
931
1078
  `dc-call family`, `dc-send-file`, `dc-invite`, `dc-hangup`, `dc-answer`. A
@@ -959,7 +1106,7 @@ variable that turned the gate off is exactly what an agent would flip.
959
1106
  **When the list refuses every send, the bot mails its agent once.** At
960
1107
  start, and once a second whether or not a job came in, the bot reads its
961
1108
  list. If it is missing, unreadable, or writable by group or world, the bot
962
- sends ONE mail to the agent's box (`DC_MAILTO`, else `<agent>@<host>`) by
1109
+ sends ONE mail to the agent's box (`DC_MAILTO`, else `<agent>`) by
963
1110
  `message send`, signed by the bot's seat, never by Delta, so the closed
964
1111
  list cannot block it. The subject
965
1112
  reads `[channels] REFUSING EVERY SEND: no channels.txt`, and the body names
@@ -994,10 +1141,12 @@ resend; the message says so). Before, it said `queued` and exited 0.
994
1141
  only a number: a NAME cannot resolve, so it is refused.
995
1142
 
996
1143
  **The list is seeded for you.** `sessionphone link` lists the first joiner as
997
- `owner` before it greets them, so even that first send goes to a listed chat.
1144
+ `owner`, bound to the key of the contact that joined by the invite, before it
1145
+ greets them, so even that first send goes to a listed chat.
998
1146
  `sessionphone bot install --owner ADDR` seeds `owner` if the list has no
999
- channel yet; without `--owner` it takes the callers file's owner line. ADDR
1000
- is the owner's Delta Chat address, as their Delta Chat profile shows it, not
1147
+ channel yet, with the key from `--owner-key FPR` or the one the callers file
1148
+ already pins to ADDR; without `--owner` it takes the callers file's owner
1149
+ line, key and all. ADDR is the owner's Delta Chat address, as their Delta Chat profile shows it, not
1001
1150
  their mail address. If there is neither, install refuses (exit 2, before it
1002
1151
  touches launchd), names the file, and gives the two ways forward: link the
1003
1152
  phone (the first to join becomes the owner), or `--owner` with the owner's
@@ -1046,6 +1195,34 @@ at its next job. Not governed: mail to a guest (`dc-invite ... ADDRESS` mails
1046
1195
  the guest, which is not a Delta send), and call signalling and voice into a
1047
1196
  call that already passed a gate.
1048
1197
 
1198
+ **Before you upgrade, check the owner line.** In `channels.txt` the owner
1199
+ line is the one named `owner`, or one whose note starts `owner`. `link`,
1200
+ `bot install` and `channels seed` write it named `owner`. If the owner's line
1201
+ has another NAME and no such note, add `# owner` to it before you upgrade.
1202
+ Otherwise every send to the owner is refused until a key is pinned by hand.
1203
+ The list must be 0600, and a list shared by two bots needs the same care as
1204
+ a shared `callers.txt`: see "Before you upgrade, check each list" under
1205
+ Callers below.
1206
+
1207
+ **Upgrading a list that has address lines.** A `channels.txt` from an older
1208
+ version has address lines with no key. The first time the new bot starts it
1209
+ migrates the file once (a marker line on top says it has run): it pins the
1210
+ `owner` line to the one key contact with that address, keeps the original as
1211
+ `channels.txt.pre-keys` (0600, written once), and logs a summary, `keys:
1212
+ <file>: N pinned, N unpinned, N ambiguous`. Every other address line stays
1213
+ unpinned, because nobody was there to compare its key. Each unpinned or
1214
+ ambiguous line is logged, `UNPINNED` or `AMBIGUOUS`, with the command that
1215
+ fixes it; if no address line is pinned the log says so in a `WARNING`. What
1216
+ you see is a refused send, `no key is pinned to it`, until you run
1217
+ `sessionphone channels pin NAME`, which prints the key it found so you can
1218
+ compare it with the phone. **An ambiguous line** is one where two or more key
1219
+ contacts share the address (the log lists each contact id and fingerprint). The
1220
+ bot pins none of them. Choose the key the phone shows for the person you mean
1221
+ and run `sessionphone channels pin NAME --key FPR`; a line with no key
1222
+ contact yet is pinned the same way once they have written to the bot, or by
1223
+ `--key` straight away. Later starts do not pin anything: only the commands
1224
+ above do.
1225
+
1049
1226
  **Upgrading breaks every send until the list exists.** See the CHANGELOG:
1050
1227
  write `channels.txt` first. The upgrade step, per bot root, with that bot's
1051
1228
  `DC_*` in the shell: `sessionphone channels seed` (or `sessionphone bot
@@ -1071,6 +1248,170 @@ the first (`KeyError: 'chat_id'`) and silently sends the second as a plain
1071
1248
  file. One restart writes the stamp. A program that writes its own jobs into
1072
1249
  the outbox is not checked: only the tools are.
1073
1250
 
1251
+ ## Callers: who may ring the bot, bound to a key
1252
+
1253
+ The bot answers a call only from a contact that holds a key the owner
1254
+ approved. The list is `callers.txt` in the bot's root (`DC_CALLERS_FILE`
1255
+ overrides the path), 0600, one line per caller, `#` starts a comment:
1256
+
1257
+ ```text
1258
+ # sessionphone keys v1: a line without key: is refused until pinned
1259
+ owner@relay.example key:4E4D1E1E160E2C8D034A91AFDCF4D1301E82234F # owner, joined 2030-06-09
1260
+ bob@relay.example # no key yet: refused
1261
+ ```
1262
+
1263
+ - **The key is the identity; the address is the label.** `key:FPR` is the
1264
+ fingerprint of the key the caller's contact holds: 40 hex digits (64 for a
1265
+ newer key), as the phone shows it under the contact's profile, Encryption.
1266
+ Case, spaces and colons in what you type do not matter. A call passes when
1267
+ its contact holds that key. If the contact's address has changed, the call
1268
+ still passes and the log says `listed as X, now writes Y`. A call from a
1269
+ contact that holds a different key is refused, whatever address it
1270
+ carries, and so is a call from a contact with no key.
1271
+ - **A line with no `key:` is refused**, and the log names the command:
1272
+ `sessionphone callers pin ADDR --key FPR`. The first line is a marker
1273
+ comment; it only records that the one-time upgrade below has run.
1274
+ - **`sessionphone link`** writes the owner bound to the key of the contact
1275
+ that joined by the invite, the one moment the bot can trust.
1276
+ `sessionphone bot install --owner ADDR --owner-key FPR` writes the same
1277
+ line from what you type. `--owner` alone on a list with no marker writes an
1278
+ address-only line, which the first start pins (below). On a list that has
1279
+ the marker, install refuses (exit 2), writes nothing and names `--owner-key`.
1280
+ - **`sessionphone callers`** lists each line with its key or `UNPINNED`.
1281
+ `sessionphone callers add ADDR [--key FPR] [--note TEXT]` approves a caller,
1282
+ and `sessionphone callers pin ADDR [--key FPR]` pins, or re-pins, a listed
1283
+ one. With `--key` they only write the file, so they work while the bot
1284
+ runs. Without `--key` they open the account and pin only when exactly one
1285
+ key contact has ADDR, which needs the bot stopped; they print the
1286
+ fingerprint they found so you can compare it with the phone, and otherwise
1287
+ write nothing, exit 1, and say why (`--key` is the way out).
1288
+ - **They touch the bot's own file and account.** `link`, `callers`,
1289
+ `callers add` and `callers pin` resolve the callers file and the account
1290
+ exactly as the bot does, from the shell's environment: `DC_CALLERS_FILE`,
1291
+ else `<home>/callers.txt`, and `DC_ACCOUNTS`, else `<home>/accounts`, where
1292
+ `<home>` is `DC_ROOT` (else `SESSIONPHONE_ROOT`, else the kept root) plus
1293
+ `/<DC_PROFILE>` when one is set. Run them with the bot's settings in front
1294
+ (`DC_PROFILE=public sessionphone callers pin ADDR --key FPR`) to reach that
1295
+ bot. Each says which file it read or wrote (`callers: <file>`, or `in
1296
+ <file>` on its result line), and `link` and a pin without `--key` say which
1297
+ account they opened (`account: <folder>`). The bot's own hints (no owner,
1298
+ `DC_CALLERS` ignored) name the command with the settings it needs.
1299
+ A named profile reads its own `<root>/<profile>/callers.txt`, so an edit
1300
+ to the root's `callers.txt` does not reach it: see "A profile's own
1301
+ callers file and list" in `docs/settings.md`.
1302
+ - **Reading is unchanged.** Every other reader of the file still takes the
1303
+ first word of a line as the address. An older sessionphone reading a keyed
1304
+ file finds no address it recognises and refuses the line: it never passes a
1305
+ caller the new bot would refuse.
1306
+ - **A key that rotates is a new contact.** When someone's key changes, their
1307
+ calls are refused until you pin the new key.
1308
+
1309
+ **On 0.3.0, upgrade now.** Its caller check trusted the sender's address,
1310
+ which could be faked. Upgrade to 0.3.1 after the checks below; until you
1311
+ can, stop the bot.
1312
+
1313
+ **Before you upgrade, check each list.** The first start pins only the owner
1314
+ line, and only in a list it can migrate. Three things lock the owner
1315
+ out. Fix each one before the upgrade:
1316
+
1317
+ - **The owner line needs its note.** The migration pins only the line whose
1318
+ note starts `owner`. `sessionphone link` writes that note, and so does
1319
+ `bot install --owner`. A line written by hand, or by `callers add` with no
1320
+ `--note`, has none. Then after the upgrade nobody can ring until a key is
1321
+ pinned by hand. Add `# owner` to the owner's line before you upgrade:
1322
+
1323
+ ```text
1324
+ owner@relay.example # owner
1325
+ ```
1326
+
1327
+ - **The list must be 0600.** The migration skips a list that group or
1328
+ others can write (`mode & 022`, such as 0664). It adds no marker, and every
1329
+ address line in it stays refused, the owner's included. The log says
1330
+ `NOT migrated: mode 0664 lets others write it`. 0.3.0 never checked the
1331
+ mode of `callers.txt`, so a loose one that rings today stops ringing after
1332
+ the upgrade. Check the mode, on macOS:
1333
+
1334
+ ```bash
1335
+ stat -f '%Lp %N' callers.txt channels.txt
1336
+ ```
1337
+
1338
+ On Linux:
1339
+
1340
+ ```bash
1341
+ stat -c '%a %n' callers.txt channels.txt
1342
+ ```
1343
+
1344
+ Then make both lists 0600 before you upgrade:
1345
+
1346
+ ```bash
1347
+ chmod 600 callers.txt channels.txt
1348
+ ```
1349
+
1350
+ - **One `callers.txt` shared by two bots** (both name it in
1351
+ `DC_CALLERS_FILE`). The first bot to start migrates it, counting key
1352
+ contacts in its own account only. The second sees the marker and skips the
1353
+ file, with no log line. So an owner line that only the second bot's account
1354
+ knows stays unpinned, and that owner cannot ring the second bot. There is
1355
+ no lock between the two bots, so do not rely on which starts first. Give
1356
+ each bot its own file. Or, with both bots stopped and already upgraded,
1357
+ write each owner's `key:FPR` on its line yourself, before the first start:
1358
+
1359
+ ```text
1360
+ owner@relay.example key:4E4D1E1E160E2C8D034A91AFDCF4D1301E82234F # owner
1361
+ ```
1362
+
1363
+ Write a key only after the upgrade. An older version reads a keyed line as
1364
+ no address it knows, and refuses the owner until the new bot runs.
1365
+
1366
+ **Which lines will stay unpinned.** Read the list itself; that changes
1367
+ nothing:
1368
+
1369
+ ```bash
1370
+ cat callers.txt channels.txt
1371
+ ```
1372
+
1373
+ At the first start:
1374
+
1375
+ - a line that already has `key:` is unchanged;
1376
+ - the owner line is pinned when exactly one key contact in this bot's
1377
+ account has its address. With none it stays unpinned, and with two or more
1378
+ it is ambiguous;
1379
+ - every other address line stays unpinned, whatever the count.
1380
+
1381
+ **Upgrading a list from an older version.** The first time the new bot
1382
+ starts it migrates `callers.txt` once, so the owner is not locked out:
1383
+
1384
+ 1. It keeps the original as `callers.txt.pre-keys` (0600, written once, never
1385
+ overwritten). Copying it back is the rollback.
1386
+ 2. It pins the **owner line** (the one whose note starts `owner`, as `link`
1387
+ writes it) when exactly one key contact has that address, and adds the
1388
+ marker line on top. Comments, notes and order are kept.
1389
+ 3. It logs one summary, `keys: <file>: N pinned, N unpinned, N ambiguous`,
1390
+ and one line per entry it did not pin. Every other address stays
1391
+ unpinned, because nobody was there to compare its key.
1392
+ 4. A list it cannot write, or one group or others can write, is left exactly
1393
+ as it was, with no marker, and the log says why; its address-only lines
1394
+ stay refused.
1395
+
1396
+ After that first start nothing pins on its own: the marker means it has run.
1397
+ Only `link`, `callers add`, `callers pin` and `bot install --owner-key` write
1398
+ a key, because a key chosen at the moment a call arrives would be the
1399
+ caller's own choice.
1400
+
1401
+ **What you see, and how to fix it.** A caller whose line is not pinned is
1402
+ refused, with `listed, but no key is pinned to it`. If no line is pinned the
1403
+ start log says `NOBODY may ring this agent`. Chat is not gated by this list,
1404
+ so you can still reach your agent while you pin. For each line:
1405
+
1406
+ | the log says | meaning | do |
1407
+ |---|---|---|
1408
+ | `UNPINNED ADDR: no key contact with this address yet` | nobody with that address has written to the bot | `callers pin ADDR --key FPR` with the key their phone shows |
1409
+ | `UNPINNED ADDR: one key contact has this address (ID FPR), not pinned unattended` | one candidate, but only the owner line is pinned unattended | compare `FPR` with their phone, then `callers pin ADDR --key FPR` |
1410
+ | `AMBIGUOUS ADDR: N key contacts share this address (ID FPR, ID FPR, ...)` | two or more keys write from that address; the bot will not choose | ask the person which key is theirs (their phone shows it), then `callers pin ADDR --key FPR` with that one. Do not pin one you cannot tell apart |
1411
+
1412
+ The same marker, `.pre-keys` backup and log lines apply to `channels.txt`
1413
+ (above).
1414
+
1074
1415
  ## Voice messages: `dc-send-file --voice-message`
1075
1416
 
1076
1417
  ```text
@@ -1129,7 +1470,7 @@ still logs `SENT FILE`.
1129
1470
  ### For an outbox: the `voice_message` job key
1130
1471
 
1131
1472
  A program that drops its own jobs in the bot's outbox (`$DC_ROOT/outbox`,
1132
- by default `~/.sessionphone/outbox`) asks for a voice message with one key
1473
+ by default `~/.sessionkit/phone/state/outbox`) asks for a voice message with one key
1133
1474
  on the file job:
1134
1475
 
1135
1476
  ```json
@@ -1269,12 +1610,17 @@ only in `bot.log`, as `REFUSED delete ...` or `REFUSED send to chat C: REASON
1269
1610
  ## Watching a call: `dc-log` and `dc-log-web`
1270
1611
 
1271
1612
  `dc-log` shows the bot's log as a conversation, both ways (`-f` follows it).
1272
- It reads `DC_LOG`, else `<root>/bot.log`. The root is `DC_ROOT`, else
1273
- `SESSIONPHONE_ROOT`, else the root `bot install` keeps for the bot it
1274
- would pick, else `~/.sessionphone`: the same contract every `dc-*` tool
1275
- reads from `bin/dc-env`. Its first line names the file it read. If
1276
- `DC_LOG` is unset and only the old default `<root>/bot/bot.log` exists, it
1277
- reads that file and says so on stderr.
1613
+ It reads `DC_LOG`, else the bot's log,
1614
+ `~/.sessionkit/phone/log/<agent>/bot.log` (`<agent>/<profile>/bot.log` when
1615
+ `DC_PROFILE` names a profile), for the bot every `dc-*` tool picks: the
1616
+ same contract every `dc-*` tool reads from `bin/dc-env`. Its first line
1617
+ names the file it read. A bot that has not started on this version yet
1618
+ still writes `<root>/bot.log` (`<root>/<profile>/bot.log` for a named
1619
+ profile); while that is the only one there, `dc-log` reads it. The root
1620
+ is `DC_ROOT`, else `SESSIONPHONE_ROOT`, else the root `bot install` keeps
1621
+ for the bot it would pick, else `~/.sessionkit/phone/state`. If `DC_LOG` is unset and
1622
+ only the older default `<root>/bot/bot.log` exists, it reads that file and
1623
+ says so on stderr.
1278
1624
 
1279
1625
  `dc-log-web` renders the same view as an HTML page for a phone, newest at the
1280
1626
  top, rewritten every 2 s (`--once` writes it once and prints its path).
@@ -1293,8 +1639,8 @@ Whether it keeps any audio is the recording switch's to say, and it is off
1293
1639
  by default: see [Call recording](#call-recording-off-unless-it-is-turned-on).
1294
1640
  The far party's audio is named for the role: `caller-NNN.wav`, one per
1295
1641
  chunk sent to transcription, or, with `DC_EARS=1`, `caller-call.wav` for the
1296
- whole call and `caller-call.json` for its clock. Calls recorded by older
1297
- versions used `mike-*` for the same files and keep those names.
1642
+ whole call and `caller-call.json` for its clock. Older versions used
1643
+ another name for the same files, and calls they recorded keep those names.
1298
1644
  `dc-call-mix` reads `full-in.wav` and `full-out.wav`, which are kept only
1299
1645
  while recording is on.
1300
1646
 
@@ -1405,6 +1751,13 @@ The chat line follows the call's recording switch:
1405
1751
  - The spoken notice is never said on a call that is not kept. If speech fails,
1406
1752
  `bot.log` says so and the greeting goes on; the chat line, when it is on,
1407
1753
  still tells the caller.
1754
+ - If neither the spoken notice nor the chat line reaches the caller on a kept
1755
+ call (speech fails or is cut short, and the chat line is off or could not be
1756
+ sent), the notice is posted once as chat text before the greeting, and
1757
+ `bot.log` says so in one line: `recording notice: ...; delivered as text`.
1758
+ A chat that `channels.txt` refuses is not retried, and a chat line not
1759
+ confirmed within 5 seconds is not sent again; `bot.log` says the caller was
1760
+ not told and the call is still kept.
1408
1761
  - The chat line goes through the profile's `channels.txt` like every in-call
1409
1762
  post. Each call logs what it did:
1410
1763
  `CALL recording notice: chat line on (default), spoken notice on (default)`.
@@ -1429,12 +1782,14 @@ the caller said was mailed, whether anyone answered it, what the bridge did
1429
1782
  while it waited, and why a call never connected. Other docs link this
1430
1783
  section as `README.md#call-troubleshooting`, so keep the heading.
1431
1784
 
1432
- **Where the log is.** It is `bot.log` in the bot's root
1433
- (`~/.sessionphone/bot.log`, or `$DC_ROOT/bot.log`). `DC_LOG` overrides the
1434
- path. Each line starts with the date and time, which the examples below
1435
- leave out. Every line the call code writes starts `CALL`. While a call is
1436
- live, each one ends `[call <id>]`, where `<id>` is the call's Delta message
1437
- id:
1785
+ **Where the log is.** It is `bot.log` in the bot's own log folder,
1786
+ `~/.sessionkit/phone/log/<agent>/` (`<agent>/<profile>/` for a named profile).
1787
+ `DC_LOG` overrides the path, and `sessionphone bot status` names the file.
1788
+ A bot that has not started on this version yet still writes
1789
+ `<root>/bot.log` (`<root>/<profile>/bot.log` for a named profile). Each
1790
+ line starts with the date and time, which the examples below leave out.
1791
+ Every line the call code writes starts `CALL`. While a call is live, each
1792
+ one ends `[call <id>]`, where `<id>` is the call's Delta message id:
1438
1793
 
1439
1794
  ```text
1440
1795
  CALL TURN <n> mailed to <box>: msg <msgid>, taken=pending [call <id>]
@@ -1444,8 +1799,8 @@ So `grep '\[call <id>\]' bot.log` gives you one call. The verdicts written at
1444
1799
  hang-up come after the tag is cleared, so find those by `TURN <n>`.
1445
1800
 
1446
1801
  `dc-log` shows a call as a conversation, and reads the same file: `DC_LOG`,
1447
- else `<root>/bot.log` (see
1448
- [Watching a call](#watching-a-call-dc-log-and-dc-log-web)). If only the old
1802
+ else the bot's log above (see
1803
+ [Watching a call](#watching-a-call-dc-log-and-dc-log-web)). If only the older
1449
1804
  default `<root>/bot/bot.log` is there, it reads that and says so on stderr.
1450
1805
 
1451
1806
  ```sh
@@ -1453,18 +1808,31 @@ dc-log -f
1453
1808
  ```
1454
1809
 
1455
1810
  Each call also gets a directory, `<root>/calls/<YYYYmmdd-HHMMSS>-chat<chat_id>-msg<id>/`.
1456
- It holds `transcript.md` (the bridge's own words are under `bridge:`), and,
1457
- only while recording is on, `full-in.wav` and `full-out.wav` (see
1811
+ It holds `transcript.md`, every line in time order under who spoke it. The
1812
+ bot's own words, such as its greeting, are under the bot's name; its holding
1813
+ line and its "couldn't reach the agent" line are under `bridge:`. Only
1814
+ while recording is on, it holds `full-in.wav` and `full-out.wav` (see
1458
1815
  [Call recording](#call-recording-off-unless-it-is-turned-on)). It also holds `offer.sdp` and
1459
1816
  `answer.sdp`, which contain network addresses. `bot.log` does not: no call
1460
1817
  line carries an IP address, a port, or the relay's TURN username or
1461
1818
  credential.
1462
1819
 
1820
+ **A call that is over, still reported live.** The bot writes
1821
+ `<home>/call-live.json` (`DC_LIVE`) when a call connects, and removes it when
1822
+ the call ends; `dc-live` and `dc-say --call` read it. When launchd stops the
1823
+ bot (SIGTERM: a bootout, or an install over a running bot), the bot ends each
1824
+ call it holds, so the phone hangs up too, and removes the file before it
1825
+ exits. A bot that died with no chance to (a SIGKILL, a crash) can still leave
1826
+ the file. The next bot removes it at start, once it holds the account, and
1827
+ says so once in `bot.log`. Until then, `dc-live` does not believe a file whose
1828
+ call began before the running bot started: it says no call is live (exit 1,
1829
+ and the reason on stderr), and `--id` prints nothing.
1830
+
1463
1831
  ### Where a turn is mailed
1464
1832
 
1465
1833
  A turn is what the caller says between pauses. Each one is mailed:
1466
1834
 
1467
- - **To** the agent's box, `DC_MAILTO`, else `<agent>@<host>`.
1835
+ - **To** the agent's box, `DC_MAILTO`, else the bare `<agent>`.
1468
1836
  - **Cc** the call's guests: `call-guests.txt` in the root, plus anyone added
1469
1837
  with `dc-invite`.
1470
1838
  - **Threaded** under the call's root mail, whose subject is
@@ -1481,14 +1849,25 @@ CALL TURN <n> mailed to <box>: msg <msgid>, taken=pending
1481
1849
  CALL TURN <n> mailed to <box> cc <guest>,<guest>: msg <msgid>, taken=pending
1482
1850
  ```
1483
1851
 
1484
- A turn the mail system refused was never sent:
1852
+ A turn the mail system refused was not sent then. It is kept in the
1853
+ undelivered spool and retried, never twice under its id:
1485
1854
 
1486
1855
  ```text
1487
- BRIDGE MAIL FAILED rc=<rc> kind=<kind>: ...; a call mail, not kept
1856
+ BRIDGE MAIL FAILED rc=<rc> kind=<kind>: ..., kept at <path>
1488
1857
  CALL TURN <n> NOT mailed to <box>: rc=<rc> kind=<kind>: ..., taken=no
1489
1858
  CALL TURN <n> NOT mailed to <box>: <ExcType>: ..., taken=no
1490
1859
  ```
1491
1860
 
1861
+ On a live call (voicemail too), the caller is told: the call says "I
1862
+ couldn't reach the agent." once for that turn. A turn is one mail however
1863
+ many chunks it joined, so the line is never said per chunk, and the spool's
1864
+ later retries say nothing. A call that is closing says nothing. The log
1865
+ line:
1866
+
1867
+ ```text
1868
+ CALL UNDELIVERED box=<box> turn=<n>: said on the call
1869
+ ```
1870
+
1492
1871
  ### Did anyone take it?
1493
1872
 
1494
1873
  The bridge cannot see the agent's mailbox. The box may be on another host,
@@ -1538,7 +1917,8 @@ CALL NO-REPLY box=<box> turn=<n> after=<w>s: caller talking, no holding line
1538
1917
  first. The window starts at the `mailed to` line. A newer turn replaces the
1539
1918
  timer, so two quick turns get one holding line. A reply after the window
1540
1919
  still plays. Voicemail never gets the holding line, and neither does a turn
1541
- whose mail failed. The holding line is never mailed.
1920
+ whose mail failed: that turn gets "I couldn't reach the agent." instead (see
1921
+ above). The holding line is never mailed.
1542
1922
 
1543
1923
  ### The first call: a wait while the speech models download
1544
1924
 
@@ -1583,13 +1963,39 @@ small mono Opus with its tags and chapters stripped:
1583
1963
 
1584
1964
  ```bash
1585
1965
  ffmpeg -i <your file> -map_metadata -1 -map_chapters -1 -ac 1 -ar 48000 \
1586
- -c:a libopus -b:a 32k ~/.sessionphone/wait-music.opus
1966
+ -c:a libopus -b:a 32k ~/.sessionkit/phone/state/wait-music.opus
1587
1967
  ```
1588
1968
 
1589
1969
  Put it in the bot's root, or point `DC_WAIT_MUSIC` at it and run
1590
1970
  `sessionphone bot install` again. The bot decodes it once and reads it again
1591
1971
  when the file changes.
1592
1972
 
1973
+ ### A missed call
1974
+
1975
+ The bot logs every ring the moment it sees it, before it decides whether to
1976
+ answer, refuse or ignore the call. A ring the bot answers then gets its
1977
+ own first line, once the call's session has started:
1978
+
1979
+ ```text
1980
+ CALL EVENT IncomingCall msg <id> chat <chat_id> video=<False|True>
1981
+ CALL INCOMING msg <id> chat <chat_id> ...
1982
+ ```
1983
+
1984
+ Neither line carries the caller's address. When the phone shows a missed
1985
+ call, look for the call's `msg` id:
1986
+
1987
+ - **No `CALL EVENT ...` line, and a `RECV` line for that chat reading
1988
+ `Missed call`**: the call reached the bot only after the phone had stopped
1989
+ ringing. Delta Chat delivered it late, as a missed call, so the bot never
1990
+ saw it ring. Look at delivery: the bot's connection to its relay, and
1991
+ whether the bot was running.
1992
+ - **A `CALL EVENT ...` line, then a `REFUSED` or `CALL event ignored ...`
1993
+ line**: the bot heard the ring and did not answer, for the reason that
1994
+ line gives.
1995
+ - **A `CALL EVENT ...` line and no `CALL INCOMING ...` line**: the bot heard
1996
+ the ring, but the call's session stalled before its first line. That is a
1997
+ bug in the bot: report it with the `bot.log` lines around the ring.
1998
+
1593
1999
  ### A call that never connects
1594
2000
 
1595
2001
  ```text
@@ -1635,6 +2041,38 @@ from the call goes in, and `bot.log` says so once:
1635
2041
  CALL heard: chat <chat_id> is not in DC_HEARD_CHATS ...
1636
2042
  ```
1637
2043
 
2044
+ ### What a `dc-say` ack says about the call
2045
+
2046
+ When a call is live in the chat, a `dc-say` is also spoken into the call.
2047
+ Its ack (the line `dc-say` prints) ends with what happened to the speech:
2048
+
2049
+ | ack ends with | means |
2050
+ |---|---|
2051
+ | `; spoken into the call` | every sentence was rendered and handed to the call |
2052
+ | `; NOT spoken: tts failed: <reason>` | the speech failed, for example `No such file or directory: 'ffmpeg'` or the speech server down; the words are in the chat as text |
2053
+ | `; NOT spoken in full: <k> of <n> sentences failed, tts failed: <reason>` | some sentences were said, some were not |
2054
+ | `; NOT spoken in full: cut off on the call` | a barge-in cut the line before all of it was said |
2055
+ | `; queued to speak` | the speech was still rendering when the ack was due |
2056
+ | `; NOT spoken (chat only)` | the line went to the chat only and was never sent to the speech: `dc-say --text-only` |
2057
+
2058
+ **`dc-say --text-only CHAT "text"` posts to the chat and never speaks it**,
2059
+ even with a call live there. Use it for anything addressed to the chat and
2060
+ not to the person on the call: a report, a log, a status. A plain `dc-say`
2061
+ (no flag) is read aloud into a live call, so a long report sent that way is
2062
+ read out into their call. `--text-only` sets `"no_speak": true` on the job;
2063
+ without the flag the job has no such key. It contradicts `--call` (say this
2064
+ into the call), so both together exit 1 and nothing is queued.
2065
+
2066
+ The bot waits up to 3 s for the speech before it writes the ack. Those 3 s
2067
+ are shared by every speaking `dc-say` the bot takes in one pass of its outbox,
2068
+ so a burst of them is acked within about 3 s in all, not 3 s each. A long line,
2069
+ a speech server's first render, or a line late in a burst can take longer, and
2070
+ then the ack says `queued to speak`. If that line then fails, the bot mails the agent one line
2071
+ that starts `NOT spoken` and names the chat, the chat message and the reason.
2072
+ Nothing is mailed when it is spoken. The mail goes to the bridge's agent
2073
+ (`DC_MAILTO`), also for a `dc-say --who NAME`. An ack never says `spoken`
2074
+ before the speech was rendered.
2075
+
1638
2076
  ## Mail into a call: who is known
1639
2077
 
1640
2078
  Mail threaded to a live call is read aloud into it, and posted in the call's
@@ -1739,11 +2177,37 @@ arrives while another is playing waits behind it and then plays whole. Lines
1739
2177
  that wait play in the order they arrived. One speaker's sentence is never
1740
2178
  cut into another's.
1741
2179
 
2180
+ A call's opening goes first: the recording notice (when it is said) and then
2181
+ the greeting play before any line that arrived while the call was still
2182
+ connecting. A line that waited for them then plays after them, once, under
2183
+ the floor rules below, its wait counted from the end of the greeting.
2184
+
1742
2185
  A barge-in, or the mouth's own cut, stops the line that is playing and drops
1743
2186
  every line waiting behind it, whoever's they are. A reply that is cut says
1744
2187
  no more of its sentences. A mouth that stops before its line ends is a cut.
1745
2188
  Sounds are not lines: they play over whatever is playing, at once.
1746
2189
 
2190
+ ### A reply waits for the caller to finish
2191
+
2192
+ A reply does not start while the caller is speaking. A reply is a mouth
2193
+ line, a `dc-say` or a mail into the call. When it is next to play, it waits
2194
+ until the caller has been quiet for `DC_FLOOR_GAP` seconds (0.8 by default),
2195
+ then plays. It waits `DC_FLOOR_CAP` seconds at most (6 by default), so noise
2196
+ on the line cannot keep the agent silent. Lines still play one at a time and
2197
+ in the order they arrived; a reply that waits holds the lines behind it. The
2198
+ bridge's own lines, such as the greeting, the holding line and the notices,
2199
+ do not wait. `DC_FLOOR_GAP=0` turns this off. At start, the bot logs the
2200
+ setting, and each reply that waited logs one line as it starts:
2201
+
2202
+ ```text
2203
+ CALL floor control: a reply waits for <gap>s of caller quiet, <cap>s at most
2204
+ CALL floor control: off (a reply may start while the caller speaks)
2205
+ CALL FLOOR <who> line held <s>s, released by quiet
2206
+ CALL FLOOR <who> line held <s>s, released by cap
2207
+ ```
2208
+
2209
+ A reply that never had to wait logs no `FLOOR` line.
2210
+
1747
2211
  ## A browser tab as a call: `sessionphone web`
1748
2212
 
1749
2213
  `sessionphone web` serves one page and its WebSocket, so a browser tab
@@ -1792,7 +2256,8 @@ sessionphone web enrol # a fresh one-time enrol code and its QR, besid
1792
2256
  mails each finished turn once. See [Web call turns](#web-call-turns-mailed-to-the-seat).
1793
2257
  - **`--legs` on a live root refuses** (exit 2) unless `--demo` is given. A
1794
2258
  live root is the default root, the one `$SESSIONPHONE_ROOT` names,
1795
- `~/.sessionphone`, or any `~/.*/delta-chat`: a keeper picks up any leg
2259
+ `~/.sessionkit/phone/state` (or the old `~/.sessionphone`, until
2260
+ [`bot migrate`](#moving-the-root-with-bot-migrate) moves it), or any `~/.*/delta-chat`: a keeper picks up any leg
1796
2261
  socket there within seconds. `--legs --demo` on the default root is the
1797
2262
  DEMO, a live seat on the line, and it waits for its GO.
1798
2263
  - **One live call per server.** A second caller gets `busy`.
@@ -1804,7 +2269,7 @@ sessionphone web enrol # a fresh one-time enrol code and its QR, besid
1804
2269
  signs in.
1805
2270
 
1806
2271
  State lives in `ROOT/web/` (mode 0700): `passkeys.json` and the enrol code.
1807
- `ROOT` is `--root`, else `$SESSIONPHONE_ROOT`, else `~/.sessionphone`.
2272
+ `ROOT` is `--root`, else `$SESSIONPHONE_ROOT`, else `~/.sessionkit/phone/state`.
1808
2273
 
1809
2274
  ### Web call turns: mailed to the seat
1810
2275
 
@@ -1822,8 +2287,9 @@ above): without one it exits 2 before any of this.
1822
2287
  the `host` in sessionmail's `mail.json`, else the first part of this
1823
2288
  machine's name. A reverse-DNS name (`….ip6.arpa`) or an unrelated DNS
1824
2289
  name is never used.
1825
- - **To** is the seat: `--mail-to ADDR`, else `$DC_MAILTO`, else
1826
- `$DC_AGENT@HOST`. There is no further fallback.
2290
+ - **To** is the seat: `--mail-to BOX`, else `$DC_MAILTO`, else the bare
2291
+ `$DC_AGENT`. There is no further fallback. A bare box is on this host
2292
+ (sessionmail adds the host); `box@host` is kept as given.
1827
2293
  - **Reply-To** is the call's box, `calls+web-<N>@HOST`. A plain reply is
1828
2294
  spoken into the call.
1829
2295
  - **From** is the web seat, the local part of `$DC_ADDR`, else
@@ -1836,7 +2302,8 @@ above): without one it exits 2 before any of this.
1836
2302
  `X-Delta-Chat-Kind: call` and `Medium: voice`. There is no
1837
2303
  `X-Delta-Chat-Id`, because a web call has no chat.
1838
2304
  - **The subject** is `[web <N>] <caller>: <words>`, and the body says
1839
- `from: <caller>`. The caller is `--caller-name`, else `$DC_CALLER_NAME`,
2305
+ `from: <caller>`. The body carries the whole turn, never cut; only the
2306
+ subject's words are cut, for display. The caller is `--caller-name`, else `$DC_CALLER_NAME`,
1840
2307
  else `owner`.
1841
2308
  - **The body** ends with how to answer: `mouth say --device call:N "text"`,
1842
2309
  or reply to the mail.
@@ -1855,9 +2322,9 @@ above): without one it exits 2 before any of this.
1855
2322
  exits 2 before anything is served.
1856
2323
  - **The start lines** name the To, the From, the Reply-To, whether the
1857
2324
  turns are signed and why (for example `unsigned (carrier drop: ...)`),
1858
- the seat's box (`turns box:`) and the heard log. A heard dir that is not
1859
- there is marked `(missing)`: no turn reaches the seat until the ears
1860
- write there.
2325
+ the seat's box (`turns box:`), the heard log, and the kept turns
2326
+ (`turns kept:`). A heard dir that is not there is marked `(missing)`: no
2327
+ turn reaches the seat until the ears write there.
1861
2328
  - **`--legs` refuses to start** (exit 2, nothing made on disk) for the
1862
2329
  seat in these cases:
1863
2330
  - **No seat is named.** `CLAUDE_CODE_AGENT` is not taken: it names the
@@ -1877,12 +2344,53 @@ above): without one it exits 2 before any of this.
1877
2344
  - A turn is mailed only if its `seq` is above the last one decided.
1878
2345
  - The Message-ID is fixed, `<call-turn-web-<N>-<seq>@HOST>`, so a replay
1879
2346
  is the same message. Its HOST is sessionmail's, not `DC_HOST`.
1880
- - A send that sent nothing, refused for a reason in the world (`exec`,
1881
- `identity`, `mta` or `undeliverable`), is tried 3 times with the same
1882
- id. A refusal of the mail itself (`subject`, `message-id`, ...) is not
1883
- tried again. A send that may have gone (it timed out, or the mail system
1884
- failed after taking it) is never tried again, and the log says
1885
- `sent=unknown`.
2347
+ - Each turn is tried ONCE by the call. A refused turn is kept and
2348
+ retried by the server's own spool, never twice under its id, while
2349
+ `--legs` runs on that root (below).
2350
+ - **A refused turn is kept, not lost.** It goes to the web server's own
2351
+ undelivered spool, `ROOT/web/undelivered/`, in the bot spool's record
2352
+ shape and under the turn's own Message-ID. It is never the bot's spool:
2353
+ `DC_UNDELIVERED` does not move it, and the retry goes From the web seat,
2354
+ as the first try did, never the bot's.
2355
+ - The server drains it about once a second. A kept turn is first retried
2356
+ after 5 s, then after 60 s, doubling to at most an hour, and goes to
2357
+ `dead/` 24 h after its first failure.
2358
+ - Before each retry the spool reads the seat's box for the turn's id.
2359
+ Already there: nothing is sent. Not there: it is sent once more under
2360
+ the same id. The box cannot be checked from here and the turn may have
2361
+ gone (`sent=unknown`): it goes to `dead/`, never sent twice.
2362
+ - A refusal of the mail itself (`subject`, `message-id`, ...) goes to
2363
+ `dead/` at once: counted, never retried.
2364
+ - **A web turn is retried only while a `sessionphone web --legs` server
2365
+ runs on that root.** Nothing else drains `ROOT/web/undelivered/`. A
2366
+ later start drains what was left, and a turn past 24 h goes to `dead/`.
2367
+ - **The count is in the server's own start line and log only.**
2368
+ `dc-live`, `bin/bridge-check` and `sessionphone bot status` count the
2369
+ bot's spool. They find it from the bot's settings, and a web server's
2370
+ `--root` is not among them: it has no plist and no pid file.
2371
+
2372
+ ```text
2373
+ turns kept: <n> waiting, <m> dead (<root>/web/undelivered)
2374
+ TURN call:<N> seq <S> NOT mailed to <box>: rc=<rc> kind=<kind>: ..., taken=no
2375
+ TURN call:<N> seq <S> NOT confirmed to <box>: rc=<rc> kind=<kind> sent=unknown: ... <id>
2376
+ BRIDGE MAIL FAILED rc=<rc> kind=<kind>: ..., kept at <path>
2377
+ BRIDGE MAIL SENT on try <n> (first failed <time>, last sent=<sent>): <file> <id>
2378
+ BRIDGE MAIL ALREADY THERE on try <n>: <id> is in <box>'s box; not sent again: <file>
2379
+ ```
2380
+ - **The caller is told.** On a live call the caller hears "I couldn't
2381
+ reach the agent." once for each turn that did not go, as on a Delta call.
2382
+ The server has no voice of its own: it runs `mouth say --device call:N`,
2383
+ the mouth speaks into the call, and the call's echo guard knows the line,
2384
+ so it is never mailed back as a turn. The spool's later retries say
2385
+ nothing, and a call that has ended says nothing. With no `mouth` on the
2386
+ PATH the line is not said, and the log says so:
2387
+
2388
+ ```text
2389
+ UNDELIVERED call:<N> seq <S>: said on the call
2390
+ UNDELIVERED call:<N> seq <S>: not said: the call has ended
2391
+ UNDELIVERED call:<N> seq <S>: not said: no mouth on PATH
2392
+ UNDELIVERED call:<N> seq <S>: not said: mouth exited <rc>: <its first line>
2393
+ ```
1886
2394
  - **Echo is dropped.** A turn that is the call's own speech heard back is
1887
2395
  never mailed. The log names the call, the seq and the score:
1888
2396
  `ECHO call:N seq S: ... | dropped: <text>`. The heard log keeps the turn.
@@ -1894,8 +2402,8 @@ above): without one it exits 2 before any of this.
1894
2402
  - **The mail is M5: signed, by `message send --message-id`.** It goes
1895
2403
  through the same one sender as the Delta call mails (see
1896
2404
  [call mails](#call-mails-signed-on-the-bots-own-ids)).
1897
- - Without `--legs` (echo mode) nothing is mailed, and the two flags do
1898
- nothing.
2405
+ - Without `--legs` (echo mode) nothing is mailed, kept or said, and the
2406
+ two flags do nothing.
1899
2407
 
1900
2408
  ### Try it
1901
2409
 
@@ -1971,9 +2479,9 @@ given in the phone tool's host settings file,
1971
2479
 
1972
2480
  | What | Where it lives |
1973
2481
  | --- | --- |
1974
- | the root, mode 0700: account, outbox, call dirs | `$SESSIONPHONE_ROOT`, else the one `bot install` keeps, else `~/.sessionphone`; the bot's own can be `$DC_ROOT` |
1975
- | who may ring the bot | `<root>/callers.txt`, or `$DC_CALLERS_FILE` |
1976
- | where the bot may send | `<root>/channels.txt`, or `$DC_CHANNELS_FILE` |
2482
+ | the root, mode 0700: account, outbox, call dirs | `$SESSIONPHONE_ROOT`, else the one `bot install` keeps, else `~/.sessionkit/phone/state`; the bot's own can be `$DC_ROOT` |
2483
+ | who may ring the bot, each caller bound to a key | `<root>/callers.txt`, or `$DC_CALLERS_FILE` |
2484
+ | where the bot may send, each address bound to a key | `<root>/channels.txt`, or `$DC_CHANNELS_FILE` |
1977
2485
  | call recording: `record`, `record_days` and the two notices | `~/.sessionkit/voice/settings/ears.json`; `$EARS_CONF` moves it only for a bot run by hand (the launchd job does not carry it, so it reads `~/.sessionkit/voice/settings/ears.json`) |
1978
2486
  | the bot's launchd job | `~/Library/LaunchAgents/dev.sessionkit.phone.bot.<agent>.plist` |
1979
2487
  | what `bot install` remembers, one map per bot | `~/.sessionkit/phone/settings/phone.json`, then `phone.d/*.json` |
@@ -2015,22 +2523,45 @@ Every key, with its type and default:
2015
2523
 
2016
2524
  ## Logs
2017
2525
 
2526
+ Both of a bot's logs are in its own folder, `~/.sessionkit/phone/log/<agent>/`,
2527
+ and a named profile's in `~/.sessionkit/phone/log/<agent>/<profile>/`, so two
2528
+ bots or two profiles never write one file. The folders are private (0700),
2529
+ made by `bot install` and by the bot's first line.
2530
+
2018
2531
  | Log | Where | Follow it |
2019
2532
  | --- | --- | --- |
2020
- | what the bot did: chats, calls, sends and refusals | `~/.sessionphone/bot.log`, or `$DC_LOG` | `tail -f ~/.sessionphone/bot.log` |
2533
+ | what the bot did: chats, calls, sends and refusals | `~/.sessionkit/phone/log/<agent>/bot.log`, or `$DC_LOG` | `tail -f ~/.sessionkit/phone/log/<agent>/bot.log` |
2021
2534
  | a call, as a conversation | read from the bot's log | `dc-log -f`, or `dc-log-web` for a phone page |
2022
- | launchd's capture of the bot's output: the same lines, plus why it would not start, and any traceback | `~/.sessionphone/bot.launchd.log` (in `$SESSIONPHONE_ROOT`) | `tail -f ~/.sessionphone/bot.launchd.log` |
2023
- | each call's transcript, written at hang-up | `<root>/calls/<call>/transcript.md` | `ls -t ~/.sessionphone/calls` for the newest |
2535
+ | launchd's capture of the bot's output: the same lines, plus why it would not start, and any traceback | `~/.sessionkit/phone/log/<agent>/bot.launchd.log` | `tail -f ~/.sessionkit/phone/log/<agent>/bot.launchd.log` |
2536
+ | each call's transcript, written at hang-up | `<root>/calls/<call>/transcript.md` | `ls -t ~/.sessionkit/phone/state/calls` for the newest |
2024
2537
  | the web call's auth and connection lines, as JSON | the terminal that runs `sessionphone web` | it is in the foreground |
2025
2538
 
2026
- - One call's lines: `grep '\[call <id>\]' ~/.sessionphone/bot.log`. See
2027
- [Call troubleshooting](#call-troubleshooting).
2539
+ - One call's lines: `grep '\[call <id>\]' ~/.sessionkit/phone/log/<agent>/bot.log`.
2540
+ See [Call troubleshooting](#call-troubleshooting).
2541
+ - **A bot installed before the logs moved.** launchd keeps writing
2542
+ `<root>/bot.launchd.log`, the path its plist names, until `bot install`
2543
+ runs again. The bot writes its own log in the new folder from its first
2544
+ start on this version; until then it writes `<root>/bot.log`, and while
2545
+ that is the only one there, `bot status`, `dc-log` and the `dc-*` tools
2546
+ read it. Nothing moves or deletes an old log: the old files stay where
2547
+ they were, for you to keep or remove.
2548
+ - `bot uninstall` removes the job and its plist, and no log.
2028
2549
  - When the bot will not start, read `bot.launchd.log` first: a refusal at
2029
2550
  start is written there, and not always to `bot.log`.
2030
2551
  - **Health:** `sessionphone bot status`. It names the job (and, when your
2031
2552
  shell names no bot, which bot and where from), says whether the bot is
2032
2553
  running (with its pid) and where its log is, and exits 0 only when the
2033
- bot is running.
2554
+ bot is running. With the job installed, its `hearing:` line says, in one
2555
+ line, whether the speech-to-text server at the job's `DC_STT_URL` answers
2556
+ a half-second test clip. The check is capped at 5 s, so a server still
2557
+ loading its model reads `not confirmed`, and a `NO` names the URL that did
2558
+ not answer and points at `sessionphone bot install`, which says how to fix
2559
+ it. It never changes the exit code. Its `speaking:` line says whether
2560
+ the bot can speak its replies (the ffmpeg `bot install` looks for, judged
2561
+ on the job's environment). A `NO` there never changes the exit code.
2562
+ Its `undelivered:` line counts the mail to the agent waiting in the bot's
2563
+ spool and dead there, call turns included; it never changes the exit
2564
+ code either.
2034
2565
  - **Is a bot running for this root?** `dc-live` asks the bot's own pid file,
2035
2566
  `<root>/bot.pid` (`DC_PID_FILE`). The bot writes it once it holds the
2036
2567
  account: its pid, its start time and command line as `ps` gives them, and
@@ -2043,6 +2574,16 @@ Every key, with its type and default:
2043
2574
  `sessionphone bot run` counts. For a launchd bot, the pid `bot status`
2044
2575
  reads from launchd and the pid in `bot.pid` are the same process. A bot
2045
2576
  started before its version wrote `bot.pid` has none: restart it.
2577
+ - **A live call holds back install and uninstall.** `bot install` and
2578
+ `bot uninstall` stop the bot, and a call cannot outlive it. While a call is
2579
+ up they refuse, exit 2, with `a call is live; hang up or pass --force`,
2580
+ then the call as `dc-live` names it. Nothing is written or booted out.
2581
+ Hang up and run it again, or pass `--force` to restart the bot anyway: the
2582
+ call drops. A call counts only while the bot that wrote `call-live.json`
2583
+ still runs, judged as `dc-live` judges it, and only when the call began
2584
+ after that bot started. A file left by a bot stopped mid-call is no call.
2585
+ The files are the ones the installed job's plist names, so a reinstall
2586
+ that changes the root still sees the call on the old one.
2046
2587
 
2047
2588
  ## Development
2048
2589
 
@@ -2058,9 +2599,11 @@ Test on a fresh macOS in a tart VM, not on the machine you develop on.
2058
2599
 
2059
2600
  ## Status
2060
2601
 
2061
- sessionphone runs today, on macOS. Its version is in `pyproject.toml`, and
2062
- what changed since the last release is under `[Unreleased]` in
2063
- `CHANGELOG.md`.
2602
+ sessionphone runs today, on macOS. Its version is in `pyproject.toml`.
2603
+ Each change writes its changelog entry to `changelog.d/`, one file each, and
2604
+ the release moves them into `CHANGELOG.md`. `make changelog` prints what
2605
+ changed since the last release, as the next release will write it under
2606
+ `[Unreleased]`.
2064
2607
 
2065
2608
  **What works on master.** `uv run pytest -q` passes. The tests named below
2066
2609
  use fakes: no real account, relay, launchd job or speech server.
@@ -2085,13 +2628,16 @@ use fakes: no real account, relay, launchd job or speech server.
2085
2628
  listed in `channels.txt` (`tests/test_seatmail.py`,
2086
2629
  `tests/test_contract_headers.py`, `tests/test_channels.py`,
2087
2630
  `tests/test_dc_channels.py`).
2088
- - **Calls.** Only approved callers may ring. A call is answered at once, or
2631
+ - **Calls.** Only approved callers may ring: a contact that holds the key
2632
+ pinned to a line of `callers.txt`. A call is answered at once, or
2089
2633
  rings first with `DC_ANSWER=ring` and falls back to voicemail. What the
2090
2634
  caller says is mailed to the agent turn by turn. With `DC_NO_REPLY_S` set
2091
2635
  (off by default), a turn with no reply hears the holding line
2092
2636
  (`tests/bridge/test_callers.py`,
2093
2637
  `tests/bridge/test_ringfirst.py`, `tests/bridge/test_call_turn_log.py`,
2094
2638
  `tests/bridge/test_noreply.py`).
2639
+ A reply waits for the caller to finish speaking
2640
+ (`tests/bridge/test_floor.py`).
2095
2641
  - **Voice messages** with `dc-send-file --voice-message`
2096
2642
  (`tests/test_voicemsg.py`).
2097
2643
  - **A browser tab as a call**, `sessionphone web` (`tests/web/`). The
@@ -2119,8 +2665,24 @@ default, and without ears a call is still heard and answered.
2119
2665
  - **macOS only.** `bot install` writes a launchd job. There is no systemd
2120
2666
  unit, and the `say` fallback is macOS's.
2121
2667
  - **No hearing without an STT server.** With nothing at `DC_STT_URL`, a call
2122
- connects but the agent does not hear the caller. `bot install` ends with a
2123
- `hearing:` line that says so (`tests/test_hearing.py`).
2668
+ connects but the agent does not hear the caller. `sessionphone setup` asks
2669
+ that server before the phone is linked, while there is time to install
2670
+ it: its last lines, `hearing:`, say whether it answered a half-second test
2671
+ clip and, when not, name the install step (under Session Kit,
2672
+ `ears services install --only speech`; standalone, VoiceMode's). Its exit
2673
+ code does not change. `bot install` ends with the same `hearing:` check, and
2674
+ `bot status` gives it in one line (`tests/test_hearing.py`,
2675
+ `tests/test_stt_before_link.py`).
2676
+ - **No spoken replies without ffmpeg.** When the speech server answers, the
2677
+ bot turns each spoken reply into call audio with `ffmpeg`. With none,
2678
+ every spoken reply fails and goes to the caller as chat text. The bot
2679
+ looks for it in `DC_FFMPEG`, then on the job's `PATH`, then at
2680
+ `/opt/homebrew/bin/ffmpeg` and `/usr/local/bin/ffmpeg`. `bot install`
2681
+ ends with a `speaking:` line that says whether it found one, and when it
2682
+ did not, gives the fix (`brew install ffmpeg`, then `bot install` again,
2683
+ so the running bot picks it up). `bot status` says the same.
2684
+ `DC_TTS=apple` speaks by macOS `say` and needs no ffmpeg
2685
+ (`tests/test_speaking.py`).
2124
2686
  - **The bot needs sessionmail, and a key only on an MTA.** On the mail drop,
2125
2687
  sessionmail's default, it sends unsigned and needs no seat step. On an MTA
2126
2688
  it must sign: give it a key with `sessionphone bot install --registry DIR`
@@ -2130,3 +2692,4 @@ default, and without ears a call is still heard and answered.
2130
2692
  it on a fresh macOS in a tart VM.
2131
2693
 
2132
2694
  [bot-mail-signing]: docs/bot-mail-signing.md
2695
+ [dependencies]: docs/dependencies.md