sessionmail 0.2.0__tar.gz → 0.3.0__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 (89) hide show
  1. {sessionmail-0.2.0 → sessionmail-0.3.0}/.claude-plugin/plugin.json +1 -1
  2. {sessionmail-0.2.0 → sessionmail-0.3.0}/PKG-INFO +4 -12
  3. {sessionmail-0.2.0 → sessionmail-0.3.0}/README.md +3 -11
  4. {sessionmail-0.2.0 → sessionmail-0.3.0}/hooks/mail-guard/README.md +3 -3
  5. {sessionmail-0.2.0 → sessionmail-0.3.0}/pyproject.toml +6 -9
  6. {sessionmail-0.2.0 → sessionmail-0.3.0}/scripts/seat-agent/README.md +12 -4
  7. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_birth.py +28 -5
  8. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_boxes.py +1 -1
  9. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_drop.py +14 -7
  10. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_init.py +1 -1
  11. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_injectlog.py +1 -1
  12. sessionmail-0.3.0/src/sessionmail/_launchd.py +430 -0
  13. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_magicdns.py +52 -4
  14. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_mhs.py +20 -2
  15. sessionmail-0.3.0/src/sessionmail/_pull_agent.py +315 -0
  16. sessionmail-0.3.0/src/sessionmail/_puller.py +1029 -0
  17. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_render.py +6 -3
  18. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_seat.py +63 -1
  19. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_sender.py +1 -1
  20. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_settings.py +22 -0
  21. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_setup.py +72 -4
  22. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_sig.py +13 -5
  23. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_voice.py +1 -1
  24. sessionmail-0.3.0/src/sessionmail/arm.txt +1 -0
  25. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mail.schema.json +144 -0
  26. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/message.py +44 -19
  27. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/mail-ext.lib.sh +1 -1
  28. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/mhs-thread +2 -2
  29. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/session-mail-run +11 -8
  30. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/session-mail-verify +2 -2
  31. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/notmuch/post-new +8 -8
  32. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/pager.py +27 -17
  33. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/postfix/vmailbox-gen +1 -1
  34. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/rules.py +2 -2
  35. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/seat/seat-agent +4 -4
  36. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/seat/seat-agent.plist.template +4 -3
  37. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/seat_init.py +14 -9
  38. {sessionmail-0.2.0 → sessionmail-0.3.0}/tests/fixtures/sig_v2/README.md +1 -1
  39. {sessionmail-0.2.0 → sessionmail-0.3.0}/tests/fixtures/sm827_honest/README.md +4 -0
  40. sessionmail-0.2.0/src/sessionmail/arm.txt +0 -1
  41. {sessionmail-0.2.0 → sessionmail-0.3.0}/.gitignore +0 -0
  42. {sessionmail-0.2.0 → sessionmail-0.3.0}/LICENSE +0 -0
  43. {sessionmail-0.2.0 → sessionmail-0.3.0}/plugins/pager-monitor/README.md +0 -0
  44. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/__init__.py +0 -0
  45. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_archive.py +0 -0
  46. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_arm.py +0 -0
  47. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_detect.py +0 -0
  48. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_detect_imap.py +0 -0
  49. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_generator.py +0 -0
  50. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_headers.py +0 -0
  51. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_help.py +0 -0
  52. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_index.py +0 -0
  53. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_layer.py +0 -0
  54. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_maildir.py +0 -0
  55. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_mcp.py +0 -0
  56. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_metrics.py +0 -0
  57. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_migrate.py +0 -0
  58. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_notmuch.py +0 -0
  59. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_operator.py +0 -0
  60. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_parsers.py +0 -0
  61. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_pull_imap.py +0 -0
  62. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_reach.py +0 -0
  63. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_record.py +0 -0
  64. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_root.py +0 -0
  65. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_say.py +0 -0
  66. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_send.py +0 -0
  67. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_shorten.py +0 -0
  68. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_stub.py +0 -0
  69. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_subscribe.py +0 -0
  70. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_subscription.py +0 -0
  71. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_tokens.py +0 -0
  72. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_views.py +0 -0
  73. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/_why.py +0 -0
  74. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/hook_lines.toml +0 -0
  75. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/humans +0 -0
  76. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/memory.py +0 -0
  77. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/mail-box.lib.sh +0 -0
  78. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/session-mail-cat +0 -0
  79. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/session-mail-ls +0 -0
  80. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/session-mail-tree +0 -0
  81. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/mhs/session-mail-wake +0 -0
  82. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/notmuch/notmuch-config.sessionkit.template +0 -0
  83. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/notmuch/notmuch-config.store.template +0 -0
  84. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/notmuch/store-index +0 -0
  85. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/postfix/delivery-check +0 -0
  86. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/postfix/magicdns-suffix +0 -0
  87. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/postfix/nm-deliver +0 -0
  88. {sessionmail-0.2.0 → sessionmail-0.3.0}/src/sessionmail/settings.template.json +0 -0
  89. {sessionmail-0.2.0 → sessionmail-0.3.0}/tests/fixtures/sm827_hint_root/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessionmail",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Mail for coding agents: message moves messages, pager alerts, memory is the filesystem view - three commands over one maildir.",
5
5
  "author": {
6
6
  "name": "Mike Bailey",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sessionmail
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: sessionmail - mail for coding agents: message, pager and memory over one maildir.
5
5
  Project-URL: Homepage, https://github.com/sessionkit/sessionmail
6
6
  Project-URL: Repository, https://github.com/sessionkit/sessionmail
@@ -104,16 +104,6 @@ message setup # again: it links the notmuch hook
104
104
  - **Nothing runs `notmuch new` for you.** `message mark` and `memory`
105
105
  read an empty index until something does: launchd, cron, or by hand.
106
106
 
107
- **Status.** `sessionmail` 0.0.1 on PyPI is a name reservation, not this
108
- code - a 1.3 KB placeholder with no dependencies.
109
- Until 0.2.0 ships, install from source instead of the `uv tool install
110
- sessionmail` line above:
111
-
112
- ```sh
113
- uv tool install git+https://github.com/sessionkit/sessionmail # from the repo
114
- make install # from a clone
115
- ```
116
-
117
107
  `message`, `pager` and `memory` all run; the specs, in the git repository
118
108
  (the release archive leaves them out), say what each verb is meant to do
119
109
  and `make progress` counts how much of it is built.
@@ -188,7 +178,8 @@ Everything else is in `docs/`, one subject per file:
188
178
  - [Gating on `message verify --json`][verify-json-verdict]: the verdict as
189
179
  one JSON object.
190
180
  - Delivery: [where mail comes from][mail-delivery-paths], [postfix
191
- configuration][postfix-configuration], [the file drop][file-drop].
181
+ configuration][postfix-configuration], [the file drop][file-drop],
182
+ [pulling a mail host's boxes][pull].
192
183
  - [Configuration][configuration]: the files you set up, by component;
193
184
  [settings][settings], the keys of `mail.json`.
194
185
  - Index: [notmuch][], [notmuch settings][notmuch-settings], [the post-new
@@ -210,6 +201,7 @@ Everything else is in `docs/`, one subject per file:
210
201
  [configuration]: docs/configuration.md
211
202
  [settings]: docs/settings.md
212
203
  [file-drop]: docs/file-drop.md
204
+ [pull]: docs/pull.md
213
205
  [notmuch]: docs/notmuch.md
214
206
  [notmuch-settings]: docs/notmuch-settings.md
215
207
  [post-new-hook]: docs/notmuch-settings.md#the-post-new-hook
@@ -85,16 +85,6 @@ message setup # again: it links the notmuch hook
85
85
  - **Nothing runs `notmuch new` for you.** `message mark` and `memory`
86
86
  read an empty index until something does: launchd, cron, or by hand.
87
87
 
88
- **Status.** `sessionmail` 0.0.1 on PyPI is a name reservation, not this
89
- code - a 1.3 KB placeholder with no dependencies.
90
- Until 0.2.0 ships, install from source instead of the `uv tool install
91
- sessionmail` line above:
92
-
93
- ```sh
94
- uv tool install git+https://github.com/sessionkit/sessionmail # from the repo
95
- make install # from a clone
96
- ```
97
-
98
88
  `message`, `pager` and `memory` all run; the specs, in the git repository
99
89
  (the release archive leaves them out), say what each verb is meant to do
100
90
  and `make progress` counts how much of it is built.
@@ -169,7 +159,8 @@ Everything else is in `docs/`, one subject per file:
169
159
  - [Gating on `message verify --json`][verify-json-verdict]: the verdict as
170
160
  one JSON object.
171
161
  - Delivery: [where mail comes from][mail-delivery-paths], [postfix
172
- configuration][postfix-configuration], [the file drop][file-drop].
162
+ configuration][postfix-configuration], [the file drop][file-drop],
163
+ [pulling a mail host's boxes][pull].
173
164
  - [Configuration][configuration]: the files you set up, by component;
174
165
  [settings][settings], the keys of `mail.json`.
175
166
  - Index: [notmuch][], [notmuch settings][notmuch-settings], [the post-new
@@ -191,6 +182,7 @@ Everything else is in `docs/`, one subject per file:
191
182
  [configuration]: docs/configuration.md
192
183
  [settings]: docs/settings.md
193
184
  [file-drop]: docs/file-drop.md
185
+ [pull]: docs/pull.md
194
186
  [notmuch]: docs/notmuch.md
195
187
  [notmuch-settings]: docs/notmuch-settings.md
196
188
  [post-new-hook]: docs/notmuch-settings.md#the-post-new-hook
@@ -29,7 +29,7 @@ script `session-mail-send`:
29
29
  3. **a heredoc or pipe feeding the send with no `--body-file`** — the send does
30
30
  not read stdin, so that delivers an *empty* message at exit 0.
31
31
 
32
- `session mail send` is **retired** (2030-09-30): it now only prints where its
32
+ `session mail send` is **retired** (2030-12-08): it now only prints where its
33
33
  verbs went. The rules above still match it, but every deny sends the caller to
34
34
  the live door, never back to the retired `--body-file` or MCP `mail_send`
35
35
  forms.
@@ -118,11 +118,11 @@ the rest still judged; click refuses it in its own words). Commands the guard
118
118
  does not match (`message list --anything`, a `grep` for the phrase) never
119
119
  reach this rule.
120
120
 
121
- Measured on 33,302 real agent Bash commands (2030-09-03): 1,645 denied, every
121
+ Measured on 33,302 real agent Bash commands (2030-06-09): 1,645 denied, every
122
122
  one of them a genuine inline-body or damaged-subject send; **zero** denials of
123
123
  a command that merely mentioned the phrase. Scanning cost ~16µs per command (0.54s for the whole corpus); the hook process itself, python startup included, runs in ~15ms.
124
124
 
125
- Re-measured with the `message send` rule (2030-09-30), 30 days of transcripts:
125
+ Re-measured with the `message send` rule (2030-12-08), 30 days of transcripts:
126
126
  168,906 Bash commands, 5,143 of them `message send` invocations. The session
127
127
  rules' decisions were identical to the pre-move hook's (1,104 denials, zero
128
128
  moved). `message send` denials: 2, both a real `$( )` in a subject; zero among
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sessionmail"
7
- dynamic = ["version"]
7
+ version = "0.3.0"
8
8
  description = "sessionmail - mail for coding agents: message, pager and memory over one maildir."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.15"
@@ -50,14 +50,11 @@ Homepage = "https://github.com/sessionkit/sessionmail"
50
50
  Repository = "https://github.com/sessionkit/sessionmail"
51
51
 
52
52
  # ── Version: one source of truth ──────────────────────────────────────
53
- # .claude-plugin/plugin.json holds the version and scripts/release.py is
54
- # its single writer (it stamps plugin.json and marketplace.json). The
55
- # build READS it from there; hatchling's regex version source is why
56
- # hatchling is the backend (session's precedent, same reasoning).
57
- [tool.hatch.version]
58
- source = "regex"
59
- path = ".claude-plugin/plugin.json"
60
- pattern = '"version"\s*:\s*"(?P<version>[^"]+)"'
53
+ # The ONE version is `version` in [project] above, static. scripts/release.py
54
+ # is its single writer (through `uv version`, which keeps uv.lock in step)
55
+ # and mirrors it into .claude-plugin/plugin.json and marketplace.json. This
56
+ # is the family rule (the kit's docs/family-release.md): the version lives
57
+ # in pyproject.toml and the release mirrors it.
61
58
 
62
59
  # ── Package data: the postfix map generator, and the arm instruction ──
63
60
  #
@@ -9,6 +9,8 @@ through `message`, which keeps the package at three commands:
9
9
 
10
10
  message seat init --registry DIR ADDRESS mint the seat's key (was scripts/seat-init)
11
11
  message seat agent ADDRESS [SOCKET] run its agent (was scripts/seat-agent/seat-agent)
12
+ message seat agent --install ADDRESS [SOCKET] render and load the LaunchAgent
13
+ message seat agent --uninstall ADDRESS unload it and remove its plist
12
14
  message seat agent --template print the LaunchAgent template
13
15
 
14
16
  The `seat` noun is hidden from `message --help` and from the MCP tools; `message
@@ -48,8 +50,14 @@ seat --help` answers when typed.
48
50
 
49
51
  ## The template
50
52
 
53
+ `message seat agent --install ADDRESS [SOCKET]` renders it, writes it into
54
+ `~/Library/LaunchAgents`, loads it with `launchctl bootstrap`, and checks that the agent
55
+ answers. `--uninstall ADDRESS` boots it out and removes the plist. Their contract
56
+ (output, exits, the `Capability: install` marker) is in
57
+ [`docs/message-seat.md`](../../docs/message-seat.md).
58
+
51
59
  `message seat agent --template` prints it, and nothing else: it never renders it, loads
52
- it or runs launchctl. Render it by replacing each `@PLACEHOLDER@`, then lint it:
60
+ it or runs launchctl. To render it by hand, replace each `@PLACEHOLDER@`, then lint it:
53
61
 
54
62
  message seat agent --template |
55
63
  sed -e 's|@ADDRESS@|bot.alice|g' -e "s|@MESSAGE@|$(command -v message)|g" \
@@ -68,9 +76,9 @@ ProgramArguments renders to `message seat agent ADDRESS SOCKET`. The placeholder
68
76
  On a host that runs `message` from an editable checkout, merging there changes the
69
77
  seat's agent at its next start.
70
78
 
71
- **Installing it is not done here.** Writing the plist into `~/Library/LaunchAgents` and
72
- running `launchctl bootstrap` is the host operator's act, by hand. No
73
- test and no make target in this repo does either.
79
+ **No test and no make target loads a job on the real launchd.** The suite runs
80
+ `--install` against a fake `launchctl` (`SESSIONMAIL_LAUNCHCTL`). A live install is the
81
+ host operator's act: `message seat agent --install`, or the hand render above.
74
82
 
75
83
  ## The adapter's env (an example; the adapter owns it)
76
84
 
@@ -68,6 +68,19 @@ SUBSCRIBE_VAR = "SESSIONMAIL_SUBSCRIBE"
68
68
  #: one: the detached generator's line.
69
69
  BORN_FILE = "born"
70
70
 
71
+ #: What a generator line says on a mail-drop host, suffix or none: the drop
72
+ #: builds no maps, so no generator runs there (`_seat._run_generator`'s own
73
+ #: words for it; `tests/test_birth.py` holds the two equal).
74
+ DROP_REGEN = "generator not run: the carrier is the mail drop; it reads the maps per send"
75
+
76
+
77
+ def _on_drop(env: Mapping[str, str] | None) -> bool:
78
+ """Whether the carrier is the mail drop. Decides what a generator line
79
+ SAYS, never what runs."""
80
+ from . import _settings
81
+ return _settings.carrier(env).kind == "drop"
82
+
83
+
71
84
  #: Who births when the caller does not say: the subscription journal's `argv`.
72
85
  VIA_LISTEN = "pager listen"
73
86
 
@@ -350,13 +363,19 @@ def ensure_session_box(session: str, seat: "Seat | None", *,
350
363
  f"{stale}, left by an earlier run of this session; its row "
351
364
  f"goes at the next regen")
352
365
  if has_session and seat is not None:
353
- live, why = _seat.doorbell(seat.address, session, env)
366
+ if detach and _settings.carrier(env).kind == "drop":
367
+ # The drop's doorbell runs the generator (`_drop.prepare`), and a
368
+ # detaching call never waits on it; the listen asks instead.
369
+ live, why = None, "not measured at session start; the first pager listen measures it"
370
+ else:
371
+ live, why = _seat.doorbell(seat.address, session, env)
354
372
  regen_said = ""
355
373
  if (birth.born or live is False) and detach:
356
374
  _detach(birth, session, env, root, record_source)
357
375
  regen_said = birth.regen_line
358
376
  elif (birth.born or live is False) and birth.suffix_unknown:
359
- regen_said = "generator NOT run (MagicDNS suffix unknown)"
377
+ regen_said = (DROP_REGEN if _on_drop(env)
378
+ else "generator NOT run (MagicDNS suffix unknown)")
360
379
  elif birth.born or live is False:
361
380
  regen_said = _seat.regen_maps(root, env=env)
362
381
  birth.regen, birth.regen_line = "ran", regen_said
@@ -384,7 +403,8 @@ def ensure_session_box(session: str, seat: "Seat | None", *,
384
403
  # included: the whole generator, detached, routes the boxes too.
385
404
  _detach(birth, session, env, root, record_source)
386
405
  elif owed and birth.suffix_unknown:
387
- birth.sub_lines.append(" map : NOT rebuilt - MagicDNS suffix unknown; the "
406
+ birth.sub_lines.append(f" map : {DROP_REGEN}" if _on_drop(env) else
407
+ " map : NOT rebuilt - MagicDNS suffix unknown; the "
388
408
  "subscriptions stand, and the next rebuild with a suffix "
389
409
  "routes them")
390
410
  elif owed:
@@ -429,8 +449,11 @@ def _main(argv: list[str] | None = None) -> int:
429
449
  # The hook did not ask for the suffix (it costs the hook's
430
450
  # budget), so the child asks, and never runs the generator without
431
451
  # one: the record's line 2 says so instead.
452
+ # On a mail-drop host no generator runs either way, so the line
453
+ # names the carrier, not the suffix.
432
454
  suffix, why = _magicdns.lookup()
433
455
  line = (_seat.regen_maps(Path(args.regen)) if suffix is not None
456
+ else DROP_REGEN if _on_drop(None)
434
457
  else f"generator NOT run (MagicDNS suffix unknown: {why})")
435
458
  except Exception as exc: # noqa: BLE001 - regen_maps never raises; a child that dies says why
436
459
  line = f"generator failed in the background: {exc}"
@@ -443,8 +466,8 @@ def _main(argv: list[str] | None = None) -> int:
443
466
  return 0
444
467
 
445
468
 
446
- __all__ = ["BORN_FILE", "SUBSCRIBE_VAR", "Birth", "BornRecord", "birth_subscriptions", "born_record",
447
- "detach_regen", "ensure_session_box", "read_born"]
469
+ __all__ = ["BORN_FILE", "DROP_REGEN", "SUBSCRIBE_VAR", "Birth", "BornRecord", "birth_subscriptions",
470
+ "born_record", "detach_regen", "ensure_session_box", "read_born"]
448
471
 
449
472
 
450
473
  if __name__ == "__main__":
@@ -248,7 +248,7 @@ def live_sessions() -> set[str]:
248
248
  """The sessions this host counts live: a harness sidecar
249
249
  (`sessiontools.sessions`) whose pid is alive - `session list`'s rule,
250
250
  which the generator applies itself, from the same sidecars and `ps`
251
- (`live_sessions_json`, since master 2030-09-30). Its procStart check is
251
+ (`live_sessions_json`, since master 2030-12-08). Its procStart check is
252
252
  not repeated here: a recycled pid can at worst name one extra subscriber
253
253
  in a listing, and routes nothing. `sessiontools` also skips a sidecar
254
254
  with no `status`/`updatedAt`, which the generator does not need; every
@@ -132,6 +132,11 @@ class Maps:
132
132
  #: The domain a bare From is qualified with (`origin`); set by the
133
133
  #: sender's drop pre-flight, empty when nothing asked.
134
134
  origin: str = ""
135
+ #: Why the generator refused the alias print (`generator exit N: its
136
+ #: last stderr line`), set by `prepare`; empty when it printed. The
137
+ #: alias map is then empty, and a reader that must tell "no row" from
138
+ #: "no map" (the doorbell) reads this.
139
+ alias_refused: str = ""
135
140
 
136
141
 
137
142
  def selected(env: Mapping[str, str]) -> bool:
@@ -183,16 +188,18 @@ def origin(env: Mapping[str, str], host: str, notes: list[str] | None = None) ->
183
188
  - Else the FQDN from the ONE suffix definition (`_magicdns`), the form
184
189
  postfix writes when `myorigin = $myhostname`, and a form
185
190
  `_voice.host_forms` accepts.
186
- - Else the short host, said in NOTES: the real generator refuses with
187
- no suffix before the drop gets here, so only a generator that asks
188
- for none (a test's fake) reaches it."""
191
+ - Else the short host. A host with no tailnet gets here on every send
192
+ (the generator's no-fqdn rows), and there the short host is right, so
193
+ nothing is said (`_magicdns.drop_without_tailnet`). Any other host
194
+ (a malformed operator suffix, or a tailscale that is there but does
195
+ not answer) is said in NOTES."""
189
196
  given = (env.get(ORIGIN_VAR) or "").strip() or _settings.file_text("postfix.origin", env)
190
197
  if given:
191
198
  return given
192
199
  found, _why = _magicdns.lookup(env)
193
200
  if found:
194
201
  return f"{host}.{found}"
195
- if notes is not None:
202
+ if notes is not None and not _magicdns.drop_without_tailnet(env):
196
203
  notes.append(f"⚠ {_magicdns.UNKNOWN} - a bare From is qualified with the short "
197
204
  f"host only (@{host})")
198
205
  return host
@@ -308,9 +315,9 @@ def prepare(env: Mapping[str, str]) -> Maps:
308
315
  if done.returncode == 0:
309
316
  maps.alias = _rows(done.stdout)
310
317
  else:
311
- maps.notes.append(f"⚠ no alias map (generator exit {done.returncode}: "
312
- f"{_last(done.stderr)}) - seat mail lands in the seat's own box "
313
- "only; no session box gets its doorbell copy")
318
+ maps.alias_refused = f"generator exit {done.returncode}: {_last(done.stderr)}"
319
+ maps.notes.append(f"⚠ no alias map ({maps.alias_refused}) - seat mail lands in the "
320
+ "seat's own box only; no session box gets its doorbell copy")
314
321
  return maps
315
322
 
316
323
 
@@ -480,7 +480,7 @@ def probe(key, env: Mapping[str, str]) -> bool:
480
480
  signer = SIGNER or _sender.sign_wire
481
481
  verify = VERIFY or _root.ssh_verify
482
482
  wire = (f"From: {key.principal}@probe.invalid\nTo: {key.principal}@probe.invalid\n"
483
- f"Subject: [ROOT] signing probe\nDate: Tue, 01 Jan 2030 00:00:00 +0000\n"
483
+ f"Subject: [ROOT] signing probe\nDate: Sun, 09 Jun 2030 00:00:00 +0000\n"
484
484
  f"Message-ID: <probe@probe.invalid>\nMIME-Version: 1.0\n"
485
485
  f"Content-Type: text/plain; charset=utf-8\n\n"
486
486
  f"memory init: a signing probe, never sent\n").encode("utf-8")
@@ -130,7 +130,7 @@ def tool_use_ids(payload: Mapping[str, Any]) -> list[str]:
130
130
  """The tool calls a hook run answers: `tool_calls[].tool_use_id`, then a
131
131
  top-level `tool_use_id` (PreToolUse), the missing ones dropped; `[]`
132
132
  when there are none. The inner key is Claude Code's hooks reference,
133
- "PostToolBatch input" (code.claude.com/docs/en/hooks, read 2030-10-16):
133
+ "PostToolBatch input" (code.claude.com/docs/en/hooks, read 2030-06-09):
134
134
  each `tool_calls` element carries `tool_name`, `tool_input`,
135
135
  `tool_use_id` and `tool_response`. Never anything else of the call:
136
136
  its input and response are not ours to log."""