v0.7.0 to v0.8.0
83 added, 152 removed. Audit A to A.
---
name: notification-manual-channel-model
description: >
- Nested notification-manual reference for LingTai's notification filesystem
- channel protocol, allowlist, payload envelopes and instructions, nudge routing,
- kernel sync, voluntary check behavior, and canonical producer state versus
- notification mirrors. Read after notification-manual when interpreting,
- producing, or debugging notification payloads; skip for dismissal policy.
- version: 0.7.0
+ Notification payload, mirror, allowlist, hook, delivery, delay, and block-cap
+ reference. Read after notification-manual when interpreting current channel
+ state or diagnosing delivery; dismissal policy belongs to dismissal-safety.
+ version: 0.8.0
tags: [lingtai, notifications, channels, protocol, sync, delay, alarm, nudge, hooks, whitelist]
- last_changed_at: "2026-09-06T00:00:00Z"
+ last_changed_at: "2026-09-09T05:30:00Z"
related_files:
- src/lingtai/tools/notification/manual/SKILL.md
- src/lingtai/tools/notification/schema.py
- src/lingtai/kernel/notification_store/__init__.py
maintenance: |
- Tracks the notification channel-model/protocol topic it documents; update when that integration changes.
+ Keep this reference as the sole owner of channel protocol, delivery, hook,
+ delay, and block-cap detail; update its parent route when paths change.
---
# Notification Channel Model
- ## Files and allowlist
-
- A channel is the filename stem in `.notification/<channel>.json`:
-
- - `.notification/email.json` becomes `_meta.agent_meta.notifications.attention.email`;
- - `.notification/system.json` becomes `_meta.agent_meta.notifications.attention.system`;
- - `.notification/mcp.telegram.json` becomes
- `_meta.agent_meta.notifications.attention["mcp.telegram"]`;
- - `.notification/goal.json` becomes `_meta.agent_meta.notifications.attention.goal`.
+ ## Current channel surface
- The kernel accepts built-in channels including `email`, `system`, `soul`,
- `nudge`, `post-molt`, `tool_loop_guard`, `bash`, `btw`, `cron`, `molt`, `goal`,
- `daemon`, and `delay-alarm`; MCP bridge channels use the `mcp.` prefix. The
- **effective allowlist** is `static ∪ mcp.* ∪ the agent's own registered hook
- channels`, and is **per-agent, not process-global**: a hook channel is allowed
- only for the agent whose workdir registered it. External hooks register
- manifests via `notification(action='add', ...)`, which appends to
- `.notification/hooks.json` and allowlists the manifest's `channel`.
+ A channel is the filename stem in `.notification/<channel>.json`; for example,
+ `system.json` projects to `attention.system` and `mcp.telegram.json` to
+ `attention["mcp.telegram"]`. Ordinary channel files are current mirrors. Daemon is a logical aggregate from Store-owned `.notification/daemon/<id>.json` mini-files; sibling `daemon.json` is only a non-authoritative compatibility report. Use Core/tool reads and dismissal, not direct edits.
+ Delivered metadata can be historical, so never treat an old attention snapshot as
+ canonical producer state.
- Unknown JSON filenames are ignored by collection, and kernel publish/dismiss
- helpers reject names outside the effective allowlist, so arbitrary workdir files
- cannot enter the model-visible lane. A blocked present channel is observable:
- the kernel emits one deduplicated `notification_hook` system warn-and-flag event
- (`ref_id: blocked_channel:<channel>`) per workdir and channel until registration.
- The scan skips kernel-private dotfiles (for example `.nudge_state.json`),
- non-`.json` entries, and syntactically invalid stems because they cannot become
- channels.
+ The effective allowlist is the static built-ins + `mcp.*` + hook channels
+ registered by **this agent's workdir**. It is per-agent, not process-global.
+ Unknown JSON names, non-JSON entries, invalid stems, and kernel-private dotfiles
+ are ignored. A present but unregistered channel emits one deduplicated
+ `notification_hook` warn-and-flag event with `ref_id:
+ blocked_channel:<channel>` until registration.
- `nudge` is the formal channel for throttled checks: runtime update checks publish
- `data.nudges[]` entries with `kind: kernel_version`, and source-freshness checks
- may publish `kind: source_drift`; the latter stays local and never enters
- release-migration routing. Runtime update, configuration, and refresh policy is
- owned by `../../../system-manual/reference/runtime-update-checks/SKILL.md`.
+ Built-ins include `email`, `system`, `soul`, `nudge`, `post-molt`, `tool_loop_guard`,
+ `bash`, `btw`, `cron`, `molt`, `goal`, `daemon`, and `delay-alarm`. Runtime/configuration nudges use `nudge`; its System owner routes `kernel_version` to update checks. `source_drift` stays local, never release-migration routing. See `../../../system-manual/reference/runtime-update-checks/SKILL.md`.
- ## Envelope and producer instructions
+ ## Payload and producer ownership
- Producer helpers write the current channel surface as a standard envelope:
+ A producer writes the current channel as an envelope such as:
```json
{
"header": "1 system notification",
"icon": "🔔",
"priority": "normal",
"published_at": "2026-06-10T00:00:00Z",
- "instructions": "Optional agent-facing handling guidance.",
+ "instructions": "Optional producer-owned handling guidance.",
"data": {"events": []}
}
```
- `instructions` is an optional field inside one channel payload, not a channel
- name. It tells the agent how that producer expects the event to be handled or
- cleared. Producers own that directive because only they know whether the file is
- a disposable output, a mirror over canonical state, a coalesced event summary,
- or protected source of truth.
-
- External producers that can write the workdir may publish the same envelope to an
- allowlisted `mcp.<server>.json` path. They must use atomic sibling-temp
- replacement so readers never observe a partial JSON file.
-
- ## Voluntary check and model-visible delivery
-
- `check` returns a dict placeholder that the turn-loop post-hook stamps with the
- canonical live payload; the handler assembles no second channel representation
- and writes no notification state.
-
- When notifications arrive while an agent is IDLE or ASLEEP, the kernel can
- synthesize the same `notification(action='check')` tool-call/result shape—same
- `action`/`input`/`reasoning` envelope, indistinguishable from a read you issued
- yourself—and wake the agent. During ACTIVE work the post-hook moves the single
- live payload onto a suitable dict-shaped tool result only on first appearance,
- material change, or a deliberate check. Delivery fingerprints and the live
- holder belong to kernel synchronization, not to the `manual` action.
-
- ## Consumer delay filtering and expiry recovery
-
- The `daemon` target is masked, not filtered: its payload, byte version, and
- bounded summary stay visible while its attention entry collapses to a constant
- token, so daemon arrivals stay readable but do not wake until delay expires.
- Independent channels continue to wake.
-
- `notification(action='delay', input={'channel': ..., 'seconds': 0 or a live configured cap})`
- is not a producer operation. Its private `.notification/.delay_state.json`
- record causes the coherent consumer snapshot, delivery fingerprint, synthetic
- wake, and voluntary `check` projection to omit exactly one allowed target while
- it is live; the target file continues receiving and retaining producer updates.
- A nonzero delay replaces the prior one, and `seconds: 0` cancels only the
- matching target and makes it visible again. The target file is never rewritten.
-
- The finite nonzero cap is read live from
- `LINGTAI_NOTIFICATION_DELAY_MAX_SECONDS`; a missing, blank, non-numeric,
- non-positive, or non-finite value falls back to `600` with the existing bounded
- diagnostic. The process timer is only a prompt path. Every coherent sync also
- recovers a persisted overdue delay: recovery stops filtering and publishes one
- high-priority `delay-alarm` mirror in the same read/wake cycle. Its state uses
- the established native notification mutation lock plus atomic sibling
- replacement, and a stable request id makes a stale callback or restart retry
- overwrite the same latest-only alarm rather than append a duplicate.
+ `instructions` is inside the payload, not a channel. Read it before choosing a
+ verb: the producer knows whether the file is disposable output, a mirror over
+ canonical state, a coalesced event summary, or protected source of truth. A
+ notification clear changes only this mirror. It must not mark mail read, change a
+ goal, consume an MCP queue, or mutate any other producer-owned record. A producer with canonical state should register a generic-dismiss guard and teach its owner verb in `instructions`; hook registration only widens the allowlist, not dismissal policy. External
+ writers use atomic sibling-temp replacement so readers never see partial JSON.
- The alarm contains only byte-level change comparison plus producer-reported or
- retained-event measurements; these values are not claimed to be exact totals for
- overwrite or capped payloads. `delay-alarm` is a built-in mirror consumers may
- dismiss, but it cannot itself be delayed. Malformed or unreadable delay state
- fails open to visible target delivery. A `daemon` delay masks only its attention
- token; payload, delivered version, and bounded summary remain readable.
+ ## Delivery and voluntary `check`
- ## Canonical producer state versus mirror
+ `check` returns a placeholder; the turn loop stamps the one live payload onto that
+ same result. IDLE/ASLEEP wake delivery uses the same shape. During ACTIVE work,
+ the current payload is copied to every eligible final ToolResultBlock, even when
+ unchanged; only the newest emission is current and older holders are historical
+ traces. Delivery signatures are bookkeeping, not an attachment gate. Fingerprints
+ and the live holder belong to kernel sync, not `manual` or this handler.
- A generic channel clear changes only the `.notification/<channel>.json` surface.
- It does not mark an email read, change a goal, consume an MCP source queue, or
- mutate any other producer-owned state. A producer whose notification is a mirror
- over canonical state must register a generic-dismiss guard and teach the
- producer-specific verb in `instructions`.
+ ## Consumer delay and expiry
- This separation is deliberate: the filesystem protocol gives the kernel one
- current high-attention surface, while canonical state remains under the
- producer's own schema and lifecycle.
+ `delay` is a consumer filter, never a producer operation. One allowed target is
+ recorded in private `.notification/.delay_state.json`; the target file keeps
+ receiving updates. A nonzero request replaces the prior live delay, and `seconds:
+ 0` cancels only the matching target and makes it visible again. The finite cap is
+ read live from `LINGTAI_NOTIFICATION_DELAY_MAX_SECONDS`; blank, non-numeric,
+ non-positive, or non-finite values fall back to `600`.
- ## Hook registration workflow
+ For ordinary targets, coherent snapshots, voluntary checks, and wake delivery
+ omit only the delayed target. A `daemon` delay masks attention only: its payload,
+ byte version, and bounded summary remain readable, while `daemon:delayed=1`
+ prevents daemon arrivals from moving the wake fingerprint. Other channels,
+ including registered hooks, continue to wake. `delay-alarm` cannot itself be
+ delayed.
- An external hook follows this sequence:
+ Expiry or recovery stops filtering and publishes one high-priority latest-only
+ `delay-alarm` mirror with target, requested/actual duration, byte-level
+ changed/no-change, and only conservative producer-reported or retained-event
+ statistics. State is atomically replaced under the established Store mutation
+ lock; a stable request id makes timer/restart races overwrite one alarm rather
+ than duplicate it. Malformed or unreadable delay state fails open to target
+ visibility, never silence.
- 1. Write a watcher that publishes the standard envelope atomically to an
- allowlisted `.notification/<channel>.json` path.
- 2. Call `notification(action='add', input={...})` with `name`, `channel`,
- `source`, `description`, `how_to_modify`, and `how_to_cancel`; optional
- `version` defaults to `1.0.0`, and `instructions` teaches handling.
- 3. Read the resulting channel with `check`, then use the producer instruction or
- narrowest dismiss action. `drop` revokes registration but does not kill the
- process.
+ ## Hooks and allowlist lifecycle
- `list` returns manifests in registry order. `edit` changes named fields and
- revalidates channel uniqueness; a null-only edit is a no-op. Corrupt or
- unreadable `hooks.json` makes `list`, `add`, `drop`, and `edit` return a bounded
- load-failed result. Built-in channels and Store-reserved stems (`hooks` and
- `large_result_acks`) cannot be registered.
+ 1. Write a watcher that atomically publishes the envelope to an allowlisted
+ `.notification/<channel>.json` path.
+ 2. Before starting that watcher, register it with `notification(action='add', input={...})`, supplying `name`,
+ `channel`, `source`, `description`, `how_to_modify`, and `how_to_cancel`;
+ `version` defaults to `1.0.0`, and `instructions` is optional.
+ 3. Read with `check`, then follow the producer instruction or narrowest dismiss.
- A blocked present JSON channel emits one deduplicated
- `notification_hook` event with `ref_id: blocked_channel:<channel>` per workdir and
- channel until registration. Kernel-private dotfiles, non-JSON entries, and
- invalid stems are skipped by this scan.
+ `list` preserves registry order. `edit` revalidates channel uniqueness; a null-
+ only edit is a no-op. `drop` revokes the channel but never stops its process.
+ Corrupt or unreadable `.notification/hooks.json` gives `list`, `add`, `drop`, and
+ `edit` a bounded load-failed result. Built-in channels and Store-reserved stems
+ `hooks` and `large_result_acks` cannot be registered.
- ## Block-cap and settings detail
+ ## Block cap and settings
- The persistent block (`notification_persistent`) and transient attention lane
- share `LINGTAI_NOTIFICATION_MAX_CHARS` (default `10000`, clamped to `2048..10000`).
- Resolution is live environment, then valid closed System-v2
- `notification_max_chars`, then default. Invalid or malformed values contribute
- no layer value. At or under the cap, serialization is byte-identical. Above it,
- the full persistent payload is atomically spilled to
- `logs/notification-overflow-<ts>.json`; the attention lane uses a
+ `notification.max_chars` is one shared live cap (`10000`, clamped to `2048..10000`)
+ for persistent and attention lanes. Metadata files are not channels. Resolution is environment
+ `LINGTAI_NOTIFICATION_MAX_CHARS`, then valid System-v2
+ `notification_max_chars`, then default. At or under cap, serialization is
+ byte-identical. Above cap, persistent output spills to
+ `logs/notification-overflow-<ts>.json`; attention output uses the
content-addressed `notification-attention-overflow-<digest8>.json` (with a
- collision suffix), and both model copies compact while preserving routing ids.
-
- Heavy free text is reduced in stages; if an id-only copy cannot fit, a marker-only
- envelope retains the exact spill basename, and a failed spill points back to the
- producer tool. The cap is a context-size control, not delivery accounting.
-
- The SHOW row `notification.max_chars` reports that same effective clamped value.
- To change it, obtain owner approval and use the existing environment launcher or
- closed System-v2 procedure; an environment/launcher change needs the authorized
- refresh or relaunch, while the file layer is hot-read. SHOW itself never writes,
- refreshes, adds an `init.json` field, or creates a Notification settings file.
-
- ## Footprint
+ collision suffix). Both model copies compact while preserving routing ids. If an id-only copy still cannot fit, the marker-only envelope may omit its long `path` (`path_omitted=true`) while retaining the exact basename in `spill_file`. Read the full spill before acting; if `spill_failed`, use the producer tool for full content. This is context-size control, not
+ access or delivery accounting.
- The protocol footprint is `.notification/<channel>.json` plus kernel-owned
- notification metadata such as legacy acknowledgement state
- (`.notification/large_result_acks.json`), the hook-manifest registry
- (`.notification/hooks.json`, a single non-channel file invisible to collection),
- and consumer-delay state (`.notification/.delay_state.json`, a private dotfile
- invisible to collection). Inspect it read-only before diagnosing a producer.
- Never delete the directory or bulk-remove files—doing so bypasses the guards and
- stale checks that only the producer verb or an atomic notification action honor.
+ The SHOW row reports the same effective clamped value and never writes, refreshes,
+ adds `init.json`, or creates a settings file. The notification footprint also
+ includes kernel-owned `large_result_acks.json`, `hooks.json`, and the private
+ delay state. Inspect read-only; never delete the directory or bulk-remove files,
+ because producer guards and stale checks live in the atomic actions.