@bastani/atomic 0.9.19-alpha.6 → 0.9.19-alpha.8

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 (119) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  7. package/dist/builtin/web-access/index.bundle.mjs +22 -3
  8. package/dist/builtin/web-access/package.json +1 -1
  9. package/dist/builtin/workflows/CHANGELOG.md +19 -0
  10. package/dist/builtin/workflows/README.md +1 -1
  11. package/dist/builtin/workflows/builtin/adversarial-verification.js +2 -2
  12. package/dist/builtin/workflows/builtin/{chunk-vhmnzdj9.js → chunk-1jth021m.js} +1 -1
  13. package/dist/builtin/workflows/builtin/{chunk-p8jjwfg6.js → chunk-2377r57g.js} +4 -2
  14. package/dist/builtin/workflows/builtin/{chunk-hbnwv56f.js → chunk-31j2gs7h.js} +2 -2
  15. package/dist/builtin/workflows/builtin/{chunk-n8zysf1t.js → chunk-ngkqkzej.js} +1 -1
  16. package/dist/builtin/workflows/builtin/{chunk-1nbq96fh.js → chunk-nkrafh9s.js} +1 -1
  17. package/dist/builtin/workflows/builtin/{chunk-1kx4aggw.js → chunk-wf741bv3.js} +2 -2
  18. package/dist/builtin/workflows/builtin/{chunk-t6x2d78s.js → chunk-z29qbehr.js} +1 -1
  19. package/dist/builtin/workflows/builtin/generate-and-filter.js +2 -2
  20. package/dist/builtin/workflows/builtin/goal.js +3 -3
  21. package/dist/builtin/workflows/builtin/index.js +7 -7
  22. package/dist/builtin/workflows/builtin/ralph.js +3 -3
  23. package/dist/builtin/workflows/builtin/tournament.js +2 -2
  24. package/dist/builtin/workflows/package.json +1 -1
  25. package/dist/builtin/workflows/src/extension/index.bundle.mjs +590 -256
  26. package/dist/builtin/workflows/src/index.js +67 -19
  27. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +2 -0
  28. package/dist/modes/interactive/components/task-live-transcript.d.ts +1 -1
  29. package/dist/modes/interactive/components/task-live-transcript.d.ts.map +1 -1
  30. package/dist/modes/interactive/components/task-live-transcript.js +19 -11
  31. package/dist/modes/interactive/components/task-live-transcript.js.map +1 -1
  32. package/docs/background-tasks.md +1 -1
  33. package/docs/build.md +51 -0
  34. package/docs/changelog.mdx +8 -8
  35. package/docs/compaction/reference.md +267 -0
  36. package/docs/compaction.md +36 -219
  37. package/docs/computer-use.md +35 -0
  38. package/docs/containerization.md +2 -0
  39. package/docs/custom-provider/api-reference.md +145 -0
  40. package/docs/custom-provider/oauth.md +131 -0
  41. package/docs/custom-provider/override.md +34 -0
  42. package/docs/custom-provider/registration.md +163 -0
  43. package/docs/custom-provider/streaming.md +214 -0
  44. package/docs/custom-provider.md +43 -607
  45. package/docs/development.md +76 -0
  46. package/docs/docs.json +199 -77
  47. package/docs/environment-variables.md +1 -1
  48. package/docs/extensions/api-reference.md +1001 -0
  49. package/docs/extensions/authoring.md +676 -0
  50. package/docs/extensions/events.md +793 -0
  51. package/docs/extensions/examples.md +94 -0
  52. package/docs/extensions/ui.md +470 -0
  53. package/docs/extensions.md +270 -2768
  54. package/docs/getting-started/authentication.md +55 -0
  55. package/docs/getting-started/first-session.md +172 -0
  56. package/docs/getting-started/installation.md +154 -0
  57. package/docs/getting-started/project-instructions.md +43 -0
  58. package/docs/guides/configuration.md +65 -0
  59. package/docs/guides/intercom.md +24 -0
  60. package/docs/guides/non-interactive.md +33 -0
  61. package/docs/guides/subagents.md +24 -0
  62. package/docs/guides/workflows.md +27 -0
  63. package/docs/guides.md +41 -0
  64. package/docs/index.md +13 -45
  65. package/docs/intercom/operations.md +178 -0
  66. package/docs/intercom/reference.md +146 -0
  67. package/docs/intercom.md +54 -259
  68. package/docs/json.md +17 -0
  69. package/docs/llama-cpp.md +5 -0
  70. package/docs/models/artificial-analysis-index.md +89 -0
  71. package/docs/models/evals.md +2 -2
  72. package/docs/models/model-selection.md +3 -3
  73. package/docs/models/pareto-efficiency.md +2 -2
  74. package/docs/models/reference.md +601 -0
  75. package/docs/models.md +45 -550
  76. package/docs/packages/authoring.md +88 -0
  77. package/docs/packages/reference.md +43 -0
  78. package/docs/packages.md +28 -107
  79. package/docs/programmatic.md +29 -0
  80. package/docs/prompt-templates.md +11 -0
  81. package/docs/providers/reference.md +23 -0
  82. package/docs/providers.md +10 -15
  83. package/docs/quickstart.md +44 -238
  84. package/docs/reference/cli.md +252 -0
  85. package/docs/reference.md +52 -0
  86. package/docs/rpc/examples.md +64 -0
  87. package/docs/rpc/extension-ui.md +198 -0
  88. package/docs/rpc/protocol.md +1341 -0
  89. package/docs/rpc.md +109 -1346
  90. package/docs/sdk/reference.md +1090 -0
  91. package/docs/sdk.md +56 -1002
  92. package/docs/session-format.md +2 -0
  93. package/docs/sessions.md +6 -10
  94. package/docs/settings.md +6 -45
  95. package/docs/skills/authoring.md +95 -0
  96. package/docs/skills/reference.md +60 -0
  97. package/docs/skills.md +20 -122
  98. package/docs/subagents/authoring.md +37 -0
  99. package/docs/subagents/reference.md +60 -0
  100. package/docs/subagents.md +39 -95
  101. package/docs/terminal-setup.md +1 -1
  102. package/docs/themes/reference.md +176 -0
  103. package/docs/themes.md +21 -135
  104. package/docs/tmux.md +5 -0
  105. package/docs/tools/edit.md +5 -0
  106. package/docs/tools.md +5 -0
  107. package/docs/tui/reference.md +343 -0
  108. package/docs/tui.md +23 -286
  109. package/docs/usage.md +26 -209
  110. package/docs/web-access.md +2 -0
  111. package/docs/windows.md +2 -1
  112. package/docs/workflows/api-reference.md +11 -4
  113. package/docs/workflows/authoring.md +4 -2
  114. package/docs/workflows/builtins.md +2 -2
  115. package/docs/workflows/operations.md +15 -10
  116. package/docs/workflows/reliable-design.md +21 -3
  117. package/docs/workflows/verification.md +8 -0
  118. package/npm-shrinkwrap.json +32 -32
  119. package/package.json +3 -3
@@ -0,0 +1,793 @@
1
+ ---
2
+ title: Extension events
3
+ description: Every extension event, its payload, and its return contract.
4
+ ---
5
+
6
+ # Extension events
7
+
8
+ ## Events
9
+
10
+ ### Lifecycle Overview
11
+
12
+ Interactive trust-gated startup first emits `session_start` and `resources_discover` for the permitted trust-safe extensions, then resolves `project_trust`. After authorization, newly loaded extensions receive `session_start`; resource discovery runs again against the completed set. Existing reporters keep their session and do not receive a second `session_start`. Noninteractive startup resolves trust before the ordinary session lifecycle.
13
+
14
+ ```
15
+ Atomic starts
16
+
17
+ ├─► session_start / resources_discover (trust-safe interactive bootstrap, when needed)
18
+ ├─► project_trust (user/global and CLI extensions only, before project resources load)
19
+ ├─► session_start { reason: "startup" } (extensions not already started)
20
+ └─► resources_discover { reason: "startup" }
21
+
22
+
23
+ user sends prompt ─────────────────────────────────────────┐
24
+ │ │
25
+ ├─► (extension commands checked first, bypass if found) │
26
+ ├─► input (can intercept, transform, or handle) │
27
+ ├─► (skill/template expansion if not handled) │
28
+ ├─► before_agent_start (can inject message, modify system prompt)
29
+ ├─► agent_start │
30
+ ├─► message_start / message_update / message_end │
31
+ │ │
32
+ │ ┌─── turn (repeats while LLM calls tools) ───┐ │
33
+ │ │ │ │
34
+ │ ├─► turn_start │ │
35
+ │ ├─► context (can modify messages) │ │
36
+ │ ├─► before_provider_request (can inspect or replace payload)
37
+ │ ├─► after_provider_response (status + headers, before stream consume)
38
+ │ │ │ │
39
+ │ │ LLM responds, may call tools: │ │
40
+ │ │ ├─► tool_execution_start │ │
41
+ │ │ ├─► tool_call (can block) │ │
42
+ │ │ ├─► tool_execution_update │ │
43
+ │ │ ├─► tool_result (can modify) │ │
44
+ │ │ └─► tool_execution_end │ │
45
+ │ │ │ │
46
+ │ ├─► turn_end │ │
47
+ │ └─► post-tool threshold preflight │ │
48
+ │ (may compact before the next provider request) │
49
+ └─► agent_end │
50
+
51
+ user sends another prompt ◄────────────────────────────────┘
52
+
53
+ /new (new session) or /resume (switch session)
54
+ ├─► session_before_switch (can cancel)
55
+ ├─► session_shutdown
56
+ ├─► session_start { reason: "new" | "resume", previousSessionFile? }
57
+ └─► resources_discover { reason: "startup" }
58
+
59
+ /fork or /clone
60
+ ├─► session_before_fork (can cancel)
61
+ ├─► session_shutdown
62
+ ├─► session_start { reason: "fork", previousSessionFile }
63
+ └─► resources_discover { reason: "startup" }
64
+
65
+ /compact or auto-compaction
66
+ ├─► compaction_start / compaction_end (verbatim line-compaction status)
67
+ ├─► session_before_compact (can cancel or provide compactedText)
68
+ ├─► session_compact (after the compaction boundary is persisted)
69
+ └─► session_compact_failed (failure or cancellation)
70
+
71
+ /tree navigation
72
+ ├─► session_before_tree (can cancel or customize)
73
+ └─► session_tree
74
+
75
+ /model or CTRL+P (model selection/cycling)
76
+ ├─► thinking_level_select (if model change changes/clamps thinking level)
77
+ └─► model_select
78
+
79
+ thinking level changes (settings, keybinding, pi.setThinkingLevel())
80
+ └─► thinking_level_select
81
+
82
+ exit (CTRL+C, CTRL+D, SIGHUP, SIGTERM)
83
+ └─► session_shutdown
84
+ ```
85
+
86
+ ### Startup Events
87
+
88
+ #### project_trust
89
+
90
+ Fired before Atomic decides whether to trust a project with dynamic configs (`.atomic`, legacy `.pi`, or `.agents/skills`). It runs during startup and when session replacement (for example `/resume`) enters a cwd whose trust has not been resolved in the current process. Only user/global extensions and CLI `-e` extensions participate; project-local extensions are not loaded until after trust is resolved.
91
+
92
+ ```typescript
93
+ pi.on("project_trust", async (event, ctx) => {
94
+ // event.cwd - current working directory
95
+ // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
96
+ if (ctx.hasUI && await ctx.ui.confirm("Trust project?", event.cwd)) {
97
+ return { trusted: "yes", remember: true };
98
+ }
99
+ return { trusted: "undecided" };
100
+ });
101
+ ```
102
+
103
+ A `project_trust` handler must return `{ trusted: "yes" | "no" | "undecided" }`. A user/global or CLI extension that returns `"yes"` or `"no"` owns the decision; the first yes/no decision wins and suppresses the built-in trust prompt. Use `remember: true` to persist a yes/no decision; otherwise it applies only to the current process. Return `"undecided"` to let later handlers or the built-in trust flow decide. Check `ctx.hasUI` before prompting. If no handler returns yes/no, normal trust resolution continues: saved `trust.json` decisions apply first, then `defaultProjectTrust` controls whether Atomic asks, trusts, or declines by default.
104
+
105
+ ### Resource Events
106
+
107
+ #### resources_discover
108
+
109
+ Fired after `session_start` so extensions can contribute additional skill, prompt, and theme paths.
110
+ The startup path uses `reason: "startup"`. Reload uses `reason: "reload"`.
111
+
112
+ ```typescript
113
+ pi.on("resources_discover", async (event, _ctx) => {
114
+ // event.cwd - current working directory
115
+ // event.reason - "startup" | "reload"
116
+ return {
117
+ skillPaths: ["/path/to/skills"],
118
+ promptPaths: ["/path/to/prompts"],
119
+ themePaths: ["/path/to/themes"],
120
+ };
121
+ });
122
+ ```
123
+
124
+ ### Session Events
125
+
126
+ See [Session Format](/session-format) for session storage internals and the SessionManager API.
127
+
128
+ #### session_start
129
+
130
+ Fired when a session is started, loaded, or reloaded.
131
+
132
+ ```typescript
133
+ pi.on("session_start", async (event, ctx) => {
134
+ // event.reason - "startup" | "reload" | "new" | "resume" | "fork"
135
+ // event.previousSessionFile - present for "new", "resume", and "fork"
136
+ ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
137
+ });
138
+ ```
139
+
140
+ #### session_info_changed
141
+
142
+ Fired when the current session display name is set via `/name`, RPC, or `pi.setSessionName()`.
143
+
144
+ ```typescript
145
+ pi.on("session_info_changed", async (event, ctx) => {
146
+ // event.name - current normalized name, or undefined if cleared
147
+ ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
148
+ });
149
+ ```
150
+
151
+ #### session_before_switch
152
+
153
+ Fired before starting a new session (`/new`) or switching sessions (`/resume`).
154
+
155
+ ```typescript
156
+ pi.on("session_before_switch", async (event, ctx) => {
157
+ // event.reason - "new" or "resume"
158
+ // event.targetSessionFile - session we're switching to (only for "resume")
159
+
160
+ if (event.reason === "new") {
161
+ const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
162
+ if (!ok) return { cancel: true };
163
+ }
164
+ });
165
+ ```
166
+
167
+ After a successful switch or new-session action, Atomic emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "new" | "resume"` and `previousSessionFile`.
168
+ Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
169
+
170
+ #### session_before_fork
171
+
172
+ Fired when forking via `/fork` or cloning via `/clone`.
173
+
174
+ ```typescript
175
+ pi.on("session_before_fork", async (event, ctx) => {
176
+ // event.entryId - ID of the selected entry
177
+ // event.position - "before" for /fork, "at" for /clone
178
+ return { cancel: true }; // Cancel fork/clone
179
+ // OR
180
+ return { skipConversationRestore: true }; // Reserved for future conversation restore control
181
+ });
182
+ ```
183
+
184
+ After a successful fork or clone, Atomic emits `session_shutdown` for the old extension instance, reloads and rebinds extensions for the new session, then emits `session_start` with `reason: "fork"` and `previousSessionFile`.
185
+ Do cleanup work in `session_shutdown`, then reestablish any in-memory state in `session_start`.
186
+
187
+ #### session_before_compact / session_compact / session_compact_failed
188
+
189
+ Fired by `/compact` and auto-compaction, including a threshold crossing detected after tool results enter the prospective next-turn context. Atomic prepares the complete active transcript except for the exact newest `preserve_recent` context-visible messages. Extensions may cancel or provide a complete, non-empty `compactedText` replacement for that region; they cannot move `firstKeptEntryId`. The override is persisted verbatim and works without provider credentials. A successful post-tool compaction returns its rebuilt context directly to the already-active Pi loop; it does not start a separate continuation. Cancellation or failure prevents that loop's follow-up provider request.
190
+
191
+ ```typescript
192
+ pi.on("session_before_compact", async (event) => {
193
+ const { preparation, branchEntries, parameters, reason, signal } = event;
194
+
195
+ // preparation.region.lines - unnumbered compactable transcript lines
196
+ // preparation.firstKeptEntryId - fixed start of the exact tail, or null when the tail is empty
197
+ // preparation.tokensBefore - whole-context token estimate
198
+ // parameters - compression_ratio, preserve_recent, query
199
+ // branchEntries - raw entries on the active branch
200
+ // reason - "manual" | "threshold" | "overflow"
201
+ // preparation is a deep-frozen clone
202
+
203
+ if (signal.aborted) return { cancel: true };
204
+
205
+ // Cancel compaction:
206
+ return { cancel: true };
207
+
208
+ // Or replace only the prepared region. Whitespace-only text is rejected.
209
+ return {
210
+ compactedText: preparation.region.lines.slice(0, 40).join("\n"),
211
+ };
212
+ });
213
+
214
+ pi.on("session_compact", async (event) => {
215
+ // event.result - VerbatimCompactionResult (text, boundary, stats, parameters, rung)
216
+ // event.compactionEntry - saved CompactionEntry with strategy "verbatim-lines"
217
+ // event.fromExtension - true when session_before_compact provided compactedText
218
+ // Observe-only: errors are isolated after persistence.
219
+ });
220
+
221
+ pi.on("session_compact_failed", async (event) => {
222
+ // event.reason - "manual" | "threshold" | "overflow"
223
+ // event.errorMessage - absent for cancellation
224
+ // event.aborted / event.willRetry - terminal state
225
+ // event.fromExtension - whether extension-provided text was active
226
+ });
227
+ ```
228
+
229
+ #### session_before_tree / session_tree
230
+
231
+ Fired on `/tree` navigation. See [Sessions](/sessions) for tree navigation concepts.
232
+
233
+ ```typescript
234
+ pi.on("session_before_tree", async (event, ctx) => {
235
+ const { preparation, signal } = event;
236
+ return { cancel: true };
237
+ // OR provide custom summary:
238
+ return { summary: { summary: "...", details: {} } };
239
+ });
240
+
241
+ pi.on("session_tree", async (event, ctx) => {
242
+ // event.newLeafId, oldLeafId, summaryEntry, fromExtension
243
+ });
244
+ ```
245
+
246
+ #### session_shutdown
247
+
248
+ Fired before a started session runtime is torn down. Use this to clean up resources opened from `session_start` or other session-scoped hooks.
249
+
250
+ ```typescript
251
+ pi.on("session_shutdown", async (event, ctx) => {
252
+ // event.reason - "quit" | "reload" | "new" | "resume" | "fork"
253
+ // event.targetSessionFile - destination session for session replacement flows
254
+ // Cleanup, save state, etc.
255
+ });
256
+ ```
257
+
258
+ ### Agent Events
259
+
260
+ #### before_agent_start
261
+
262
+ Fired after user submits prompt, before agent loop. Can inject a message and/or modify the system prompt.
263
+
264
+ ```typescript
265
+ pi.on("before_agent_start", async (event, ctx) => {
266
+ // event.prompt - user's prompt text
267
+ // event.images - attached images (if any)
268
+ // event.systemPrompt - current chained system prompt for this handler
269
+ // (includes changes from earlier before_agent_start handlers)
270
+ // event.systemPromptOptions - structured options used to build the system prompt
271
+ // .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
272
+ // .selectedTools - tools currently active in the prompt
273
+ // .toolSnippets - one-line descriptions for each tool
274
+ // .promptGuidelines - custom guideline bullets
275
+ // .appendSystemPrompt - text from --append-system-prompt flags
276
+ // .cwd - working directory
277
+ // .contextFiles - AGENTS.md files and other loaded context files
278
+ // .skills - loaded skills
279
+
280
+ return {
281
+ // Inject a persistent message (stored in session, sent to LLM)
282
+ message: {
283
+ customType: "my-extension",
284
+ content: "Additional context for the LLM",
285
+ display: true,
286
+ },
287
+ // Replace the system prompt for this turn (chained across extensions)
288
+ systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
289
+ };
290
+ });
291
+ ```
292
+
293
+ The `systemPromptOptions` field gives extensions access to the same structured data Atomic uses to build the system prompt. This lets you inspect what Atomic has loaded — custom prompts, guidelines, tool snippets, context files, skills — without re-discovering resources or re-parsing flags. Use it when your extension needs to make deep, informed changes to the system prompt while respecting user-provided configuration.
294
+
295
+ Inside `before_agent_start`, `event.systemPrompt` and `ctx.getSystemPrompt()` both reflect the chained system prompt as of the current handler. Later `before_agent_start` handlers can still modify it again.
296
+
297
+ #### agent_start / agent_end / agent_settled
298
+
299
+ `agent_start` begins a low-level run. `agent_end` fires when that run ends, but Atomic may still retry, compact and retry, or deliver queued follow-ups. Use `agent_settled` when a status integration needs to know Atomic has no automatic continuation left, including a chain of repeated output-cap continuations. Silence during a provider request or between these runs is not settlement.
300
+
301
+ ```typescript
302
+ pi.on("agent_start", async (_event, ctx) => {});
303
+ pi.on("agent_end", async (event, ctx) => {
304
+ // event.messages - messages from this low-level run
305
+ });
306
+ pi.on("agent_settled", async (_event, ctx) => {
307
+ // ctx.isIdle() is true unless another extension started a run.
308
+ });
309
+ ```
310
+
311
+ #### ui_prompt_start / ui_prompt_end
312
+
313
+ These notification-only events wrap blocking user-facing prompts. Each event has `reason: "ui_prompt" | "project_trust"`, the prompt `kind`, and the prompt `title` when available. Host and status integrations can use the pair to distinguish waiting for the user from active work.
314
+
315
+ - `ui_prompt`: extension prompts opened through `ctx.ui.select()`, `ctx.ui.confirm()`, `ctx.ui.input()`, `ctx.ui.editor()`, and `ctx.ui.custom()`.
316
+ Custom inspection/navigation components can pass `{ purpose: "navigation" }` to omit their own prompt span. The default remains `"prompt"`. Nested approval calls still emit events; mounting or hiding the workflow graph is not itself an approval.
317
+ - `project_trust`: interactive startup and resume trust dialogs (including trust-hook `select`, `confirm`, and `input` dialogs and borrowed extension-source authorization), plus the built-in `/trust` selector. In isolated interactive mode, the engine owns startup/resume decisions and uses the host UI; host-owned `/trust` notifications are forwarded to the engine. If the current engine has not bound yet, the transport retains the start and end in order until it binds, even if the selector closes first. Separate completed dialogs retain separate lifecycle pairs when delivered together. This does not delay the trust decision; retiring that engine discards its pending notifications.
318
+
319
+ Startup first loads only permitted user/global, builtin, and explicitly authorized CLI extensions, and binds them to a real session before asking for trust. Existing handlers receive the live `ExtensionContext` while the dialog is waiting: `ctx.cwd`, `ctx.sessionManager`, and other session APIs are available. Approval completes resources in that same session without rerunning safe extension factories or their `session_start` handlers. Newly authorized project extensions receive `session_start` only after loading; they do not receive historical prompt events. Untrusted project and borrowed project-local code is never loaded just to observe a prompt. Silent saved/default/CLI policy decisions and noninteractive startup emit no artificial waits.
320
+
321
+ Interactive resume trust dialogs use the outgoing session's live extension context. Destination validation and `session_before_switch` cancellation precede trust preparation; failed preparation leaves that session active. Project resources load only when the prepared replacement continues after shutdown. Attaching subscribers does not replay earlier notifications.
322
+
323
+ Atomic coalesces nested or overlapping prompts, including mixed reasons, into one shared outer span. The end event retains the original outer prompt's reason, kind, and title and fires after every prompt in the span settles, including rejected promises and synchronous failures. Cancelling or disposing the `/trust` selector ends its wait. Rebinding the host UI context closes an active span before a prompt from the new context can begin. Notifications are not replayed to a replacement engine if the engine exits while a host selector is open.
324
+
325
+ At session replacement, Atomic waits up to 1,000 ms for a snapshot of pending prompt notification deliveries before shutdown. Prompt display and answers never await observers. Start and end dispatch independently, invoking each observer in notification order without awaiting other observers; an earlier slow observer cannot make later subscribers receive an end before its start. An observer's own asynchronous start and end work can overlap, so update lifecycle state before awaiting unrelated work. If an observer hangs, Atomic warns and continues replacement; its context is not guaranteed to remain valid after that finite boundary.
326
+
327
+ Handlers run best-effort from the microtask queue. Atomic does not await them before opening or closing the prompt, so notifications do not block the UI.
328
+
329
+ ```typescript
330
+ pi.on("ui_prompt_start", (event) => {
331
+ // event.reason - "ui_prompt" | "project_trust"
332
+ // event.kind - "select" | "confirm" | "input" | "editor" | "custom"
333
+ // event.title - prompt title when available
334
+ });
335
+
336
+ pi.on("ui_prompt_end", (event) => {
337
+ // Atomic is no longer waiting on this outer prompt span.
338
+ });
339
+ ```
340
+
341
+ #### turn_start / turn_end
342
+
343
+ Fired for each turn (one LLM response + tool calls).
344
+
345
+ ```typescript
346
+ pi.on("turn_start", async (event, ctx) => {
347
+ // event.turnIndex, event.timestamp
348
+ });
349
+
350
+ pi.on("turn_end", async (event, ctx) => {
351
+ // event.turnIndex, event.message, event.toolResults
352
+ });
353
+ ```
354
+
355
+ #### message_start / message_update / message_end
356
+
357
+ Fired for message lifecycle updates.
358
+
359
+ - `message_start` and `message_end` fire for user, assistant, and toolResult messages.
360
+ - `message_update` fires for assistant streaming updates.
361
+ - `message_end` handlers can return `{ message }` to replace the finalized message. The replacement must keep the same `role`.
362
+
363
+ ```typescript
364
+ pi.on("message_start", async (event, ctx) => {
365
+ // event.message
366
+ });
367
+
368
+ pi.on("message_update", async (event, ctx) => {
369
+ // event.assistantMessageEvent (token-by-token delta; no cumulative message)
370
+ });
371
+
372
+ pi.on("message_end", async (event, ctx) => {
373
+ if (event.message.role !== "assistant") return;
374
+
375
+ return {
376
+ message: {
377
+ ...event.message,
378
+ usage: {
379
+ ...event.message.usage,
380
+ cost: {
381
+ ...event.message.usage.cost,
382
+ total: 0.123,
383
+ },
384
+ },
385
+ },
386
+ };
387
+ });
388
+ ```
389
+
390
+ #### tool_execution_start / tool_execution_update / tool_execution_end
391
+
392
+ Fired for tool execution lifecycle updates.
393
+
394
+ In parallel tool mode:
395
+ - `tool_execution_start` is emitted in assistant source order during the preflight phase
396
+ - `tool_execution_update` events may interleave across tools
397
+ - `tool_execution_end` is emitted in tool completion order after each tool is finalized
398
+ - final `toolResult` message events are still emitted later in assistant source order
399
+
400
+ ```typescript
401
+ pi.on("tool_execution_start", async (event, ctx) => {
402
+ // event.toolCallId, event.toolName, event.args
403
+ });
404
+
405
+ pi.on("tool_execution_update", async (event, ctx) => {
406
+ // event.toolCallId, event.toolName, event.args, event.partialResult
407
+ });
408
+
409
+ pi.on("tool_execution_end", async (event, ctx) => {
410
+ // event.toolCallId, event.toolName, event.result, event.isError
411
+ });
412
+ ```
413
+
414
+ #### context
415
+
416
+ Fired before each LLM call. Modify messages non-destructively. See [Session Format](/session-format) for message types. When tool output crosses the buffered compaction threshold, the post-tool compaction preflight finishes before this hook runs for the follow-up call, so `event.messages` contains the rebuilt compacted context.
417
+
418
+ ```typescript
419
+ pi.on("context", async (event, ctx) => {
420
+ // event.messages - deep copy, safe to modify
421
+ const filtered = event.messages.filter(m => !shouldPrune(m));
422
+ return { messages: filtered };
423
+ });
424
+ ```
425
+
426
+ #### before_provider_headers
427
+
428
+ Fires after outgoing HTTP headers are assembled. Mutate `event.headers` to add, override, or remove headers. The event also identifies the provider and model.
429
+
430
+ ```typescript
431
+ pi.on("before_provider_headers", (event, ctx) => {
432
+ event.headers["x-session-id"] = ctx.sessionManager.getSessionId();
433
+ delete event.headers["x-remove-me"];
434
+ });
435
+ ```
436
+
437
+ #### before_provider_request
438
+
439
+ Fired after the provider-specific payload is built, right before the request is sent. Handlers run in extension load order. Returning `undefined` keeps the payload unchanged. Returning any other value replaces the payload for later handlers and for the actual request.
440
+
441
+ This hook can rewrite provider-level system instructions or remove them entirely. Those payload-level changes are not reflected by `ctx.getSystemPrompt()`, which reports Atomic's system prompt string rather than the final serialized provider payload.
442
+
443
+ ```typescript
444
+ pi.on("before_provider_request", (event, ctx) => {
445
+ console.log(JSON.stringify(event.payload, null, 2));
446
+
447
+ // Optional: replace payload
448
+ // return { ...event.payload, temperature: 0 };
449
+ });
450
+ ```
451
+
452
+ This is mainly useful for debugging provider serialization and cache behavior.
453
+
454
+ #### after_provider_response
455
+
456
+ Fired after an HTTP response is received and before its stream body is consumed. Handlers run in extension load order.
457
+
458
+ ```typescript
459
+ pi.on("after_provider_response", (event, ctx) => {
460
+ // event.status - HTTP status code
461
+ // event.headers - normalized response headers
462
+ if (event.status === 429) {
463
+ console.log("rate limited", event.headers["retry-after"]);
464
+ }
465
+ });
466
+ ```
467
+
468
+ Header availability depends on provider and transport. Providers that abstract HTTP responses may not expose headers.
469
+
470
+ ### Model Events
471
+
472
+ #### model_select
473
+
474
+ Fired when the model changes via `/model` command, model cycling (`CTRL+P`), or session restore.
475
+
476
+ ```typescript
477
+ pi.on("model_select", async (event, ctx) => {
478
+ // event.model - newly selected model
479
+ // event.previousModel - previous model (undefined if first selection)
480
+ // event.source - "set" | "cycle" | "restore"
481
+
482
+ const prev = event.previousModel
483
+ ? `${event.previousModel.provider}/${event.previousModel.id}`
484
+ : "none";
485
+ const next = `${event.model.provider}/${event.model.id}`;
486
+
487
+ ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
488
+ });
489
+ ```
490
+
491
+ Use this to update UI elements (status bars, footers) or perform model-specific initialization when the active model changes.
492
+
493
+ #### thinking_level_select
494
+
495
+ Fired when the thinking level changes. This is notification-only; handler return values are ignored.
496
+
497
+ ```typescript
498
+ pi.on("thinking_level_select", async (event, ctx) => {
499
+ // event.level - newly selected thinking level
500
+ // event.previousLevel - previous thinking level
501
+
502
+ ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
503
+ });
504
+ ```
505
+
506
+ Use this to update extension UI when `pi.setThinkingLevel()`, model changes, or built-in thinking-level controls change the active thinking level.
507
+
508
+ ### Tool Events
509
+
510
+ #### tool_call
511
+
512
+ Fired after `tool_execution_start`, before the tool executes. **Can block.** Use `isToolCallEventType` to narrow and get typed inputs.
513
+
514
+ Before `tool_call` runs, Atomic waits for previously emitted Agent events to finish draining through `AgentSession`. This means `ctx.sessionManager` is up to date through the current assistant tool-calling message.
515
+
516
+ In the default parallel tool execution mode, sibling tool calls from the same assistant message are preflighted sequentially, then executed concurrently. `tool_call` is not guaranteed to see sibling tool results from that same assistant message in `ctx.sessionManager`.
517
+
518
+ `event.input` is mutable. Mutate it in place to patch tool arguments before execution.
519
+
520
+ Behavior guarantees:
521
+ - Mutations to `event.input` affect the actual tool execution
522
+ - Later `tool_call` handlers see mutations made by earlier handlers
523
+ - No re-validation is performed after your mutation
524
+ - Return values from `tool_call` control blocking via `{ block: true, reason?: string, terminate?: boolean }`
525
+ - `terminate` only applies to a blocked call; the agent stops early only when every finalized result in the batch is terminating
526
+ - `terminate` applies only to a blocked call; the agent stops early only when every finalized result in the batch is terminating
527
+
528
+ ```typescript
529
+ import { isToolCallEventType } from "@bastani/atomic";
530
+
531
+ pi.on("tool_call", async (event, ctx) => {
532
+ // event.toolName - "bash", "powershell", "read", "write", "edit", "find", "search", etc.
533
+ // event.toolCallId
534
+ // event.input - tool parameters (mutable)
535
+
536
+ // Built-in tools: no type params needed
537
+ if (isToolCallEventType("bash", event)) {
538
+ // event.input is { command: string; timeout?: number }
539
+ event.input.command = `source ~/.profile\n${event.input.command}`;
540
+
541
+ if (event.input.command.includes("rm -rf")) {
542
+ return { block: true, reason: "Dangerous command", terminate: true };
543
+ }
544
+ }
545
+
546
+ if (isToolCallEventType("powershell", event)) {
547
+ // event.input is typed as PowerShellToolInput
548
+ event.input.command = `$ErrorActionPreference = "Stop"\n${event.input.command}`;
549
+ }
550
+
551
+ if (isToolCallEventType("read", event)) {
552
+ // event.input is { path: string }
553
+ console.log(`Reading: ${event.input.path}`);
554
+ }
555
+
556
+ if (isToolCallEventType("search", event)) {
557
+ // event.input is typed as SearchToolInput
558
+ event.input.paths ??= ".";
559
+ }
560
+ });
561
+ ```
562
+
563
+ #### Typing custom tool input
564
+
565
+ Custom tools should export their input type:
566
+
567
+ ```typescript
568
+ // my-extension.ts
569
+ export type MyToolInput = Static<typeof myToolSchema>;
570
+ ```
571
+
572
+ Use `isToolCallEventType` with explicit type parameters:
573
+
574
+ ```typescript
575
+ import { isToolCallEventType } from "@bastani/atomic";
576
+ import type { MyToolInput } from "my-extension";
577
+
578
+ pi.on("tool_call", (event) => {
579
+ if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
580
+ event.input.action; // typed
581
+ }
582
+ });
583
+ ```
584
+
585
+ #### tool_result
586
+
587
+ Fired after tool execution finishes and before `tool_execution_end` plus the final tool result message events are emitted. **Can modify result.**
588
+
589
+ In parallel tool mode, `tool_result` and `tool_execution_end` may interleave in tool completion order, while final `toolResult` message events are still emitted later in assistant source order.
590
+
591
+ `tool_result` handlers chain like middleware:
592
+ - Handlers run in extension load order
593
+ - Each handler sees the latest result after previous handler changes
594
+ - Handlers can return partial patches (`content`, `details`, or `isError`); omitted fields keep their current values
595
+
596
+ After all handlers finish, Atomic normalizes image blocks returned by the tool or inserted by a handler according to `images.autoResize` before saving the result to history. If image processing fails, the original image remains in the result.
597
+
598
+ Use `ctx.signal` for nested async work inside the handler. This lets Escape cancel model calls, `fetch()`, and other abort-aware operations started by the extension.
599
+
600
+ ```typescript
601
+ import { isBashToolResult, isPowerShellToolResult, isSearchToolResult } from "@bastani/atomic";
602
+
603
+ pi.on("tool_result", async (event, ctx) => {
604
+ // event.toolName, event.toolCallId, event.input
605
+ // event.content, event.details, event.isError
606
+
607
+ if (isBashToolResult(event)) {
608
+ // event.details is typed as BashToolDetails
609
+ }
610
+
611
+ if (isPowerShellToolResult(event)) {
612
+ // event.details is typed as PowerShellToolDetails | undefined
613
+ }
614
+
615
+ if (isSearchToolResult(event)) {
616
+ // event.details is typed as SearchToolDetails | undefined
617
+ }
618
+
619
+ const response = await fetch("https://example.com/summarize", {
620
+ method: "POST",
621
+ body: JSON.stringify({ content: event.content }),
622
+ signal: ctx.signal,
623
+ });
624
+
625
+ // Modify result:
626
+ return { content: [...], details: {...}, isError: false };
627
+ });
628
+ ```
629
+
630
+ ### User Bash Events
631
+
632
+ #### user_bash
633
+
634
+ Fired when user executes `!` or `!!` commands. **Can intercept.**
635
+
636
+ ```typescript
637
+ import { createLocalBashOperations } from "@bastani/atomic";
638
+
639
+ pi.on("user_bash", (event, ctx) => {
640
+ // event.command - the bash command
641
+ // event.excludeFromContext - true if !! prefix
642
+ // event.cwd - working directory
643
+
644
+ // Option 1: Provide custom operations (e.g., SSH)
645
+ return { operations: remoteBashOps };
646
+
647
+ // Option 2: Wrap atomic's built-in local bash backend
648
+ const local = createLocalBashOperations();
649
+ return {
650
+ operations: {
651
+ exec(command, cwd, options) {
652
+ return local.exec(`source ~/.profile\n${command}`, cwd, options);
653
+ }
654
+ }
655
+ };
656
+
657
+ // Option 3: Full replacement - return result directly
658
+ return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
659
+ });
660
+ ```
661
+
662
+ ### Input Events
663
+
664
+ #### input
665
+
666
+ Fired when user input is received, after extension commands are checked but before skill and template expansion. The event sees the raw input text, so `/skill:foo` and `/template` are not yet expanded.
667
+
668
+ Direct `session.steer()` and `session.followUp()` calls also run input handlers before skill/template expansion and queue admission. A handled input is not queued; transformed text and images are queued instead. Their optional third argument sets `source`, defaulting to `interactive`; RPC queue commands use `rpc`.
669
+
670
+ **Processing order:**
671
+ 1. Extension commands (`/cmd`) checked first - if found, handler runs and input event is skipped
672
+ 2. `input` event fires - can intercept, transform, or handle
673
+ 3. If not handled: skill commands (`/skill:name`) expanded to skill content
674
+ 4. If not handled: prompt templates (`/template`) expanded to template content
675
+ 5. Agent processing begins (`before_agent_start`, etc.)
676
+
677
+ ```typescript
678
+ pi.on("input", async (event, ctx) => {
679
+ // event.text - raw input (before skill/template expansion)
680
+ // event.images - attached images, if any
681
+ // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
682
+
683
+ // Transform: rewrite input before expansion
684
+ if (event.text.startsWith("?quick "))
685
+ return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
686
+
687
+ // Handle: respond without LLM (extension shows its own feedback)
688
+ if (event.text === "ping") {
689
+ ctx.ui.notify("pong", "info");
690
+ return { action: "handled" };
691
+ }
692
+
693
+ // Route by source: skip processing for extension-injected messages
694
+ if (event.source === "extension") return { action: "continue" };
695
+
696
+ // Intercept skill commands before expansion
697
+ if (event.text.startsWith("/skill:")) {
698
+ // Could transform, block, or let pass through
699
+ }
700
+
701
+ return { action: "continue" }; // Default: pass through to expansion
702
+ });
703
+ ```
704
+
705
+ **Results:**
706
+ - `continue` - pass through unchanged (default if handler returns nothing)
707
+ - `transform` - modify text/images, then continue to expansion
708
+ - `handled` - skip agent entirely (first handler to return this wins)
709
+
710
+ Transforms chain across handlers. See [input-transform.ts](https://github.com/bastani-inc/atomic/blob/main/packages/coding-agent/examples/extensions/input-transform.ts) and [input-transform-streaming.ts](https://github.com/bastani-inc/atomic/blob/main/packages/coding-agent/examples/extensions/input-transform-streaming.ts) for `streamingBehavior`-aware routing.
711
+
712
+ ## Workflow activity and lifecycle hooks
713
+
714
+ The host exposes typed workflow observation contracts. A workflow provider must register and publish activity; these APIs alone do not connect the workflow scheduler. Without a publisher snapshot, availability is `unavailable`, not an empty ready state.
715
+
716
+ The workflows package also contains a pure root-activity projector. It combines a store snapshot with runtime ownership of executing stages and tools, retries, stopping runs, and acknowledged failures. Nested runs fold into one root summary; historical `running` status alone never counts as execution. Runnable stage handoffs remain `working`, while a stage parked on its own prompt contributes attention rather than execution. Independent work keeps the root `working` with `needsAttention: true`. With no work progressing, human waits and unresolved failures are `blocked`; paused runs are `idle` with reason `paused`, and completed or intentionally stopped runs are `idle` with reason `quiescent`.
717
+
718
+ Live executor ownership also keeps a running root `working` with reason `automatic_continuation` after its nodes settle and before author code admits the next node. This requires all stages to be completed or skipped and all tools completed, with no prompt, active block, or stop. It does not add to the execution count. Historical snapshots without live ownership, paused runs, and parked nodes do not qualify.
719
+
720
+ Stopping a child suppresses handoffs only in that child's subtree, not in its parent or sibling runs. With no other active waits, a paused stage keeps the root `idle` with reason `paused` after independent execution finishes, even when the stored run status remains `running`. Its retained prompt does not request attention until the stage resumes; independent active waits still do. The projector does not change stored run or stage outcomes.
721
+
722
+ Stopping ownership follows each run's `parentRunId` chain and then its root identity, even when a named ancestor's snapshot is absent. The root reports `working` with reason `stopping` only when all executing contributions are draining under a stop and no unaffected retry or handoff can progress. Independent work retains the usual `retrying`, `executing`, or `automatic_continuation` reason; a stopped run with no execution left does not select the reason. Removing history does not release runtime execution or stop ownership.
723
+
724
+ The projection module's `workflowActivityNodeKey(runId, nodeId)` helper builds `${runId}:${nodeId}` keys for `executingStageIds`, `executingToolNodeIds`, and `retryingStageIds`. The first colon separates the runtime UUID run ID from the node ID, which may contain colons. Bare node IDs do not establish ownership: two runs can contain the same tool hash. `stoppingRunIds` and `acknowledgedFailureRunIds` use plain run IDs.
725
+
726
+ The projector and its ownership-key helper are internal to the workflows package, not exports of the supported `@bastani/atomic/workflows` SDK. Extension consumers use `ctx.observeWorkflowActivity` rather than importing the projector.
727
+
728
+ The workflows extension registers a publisher on activation and publishes this activity stream for its owning session: root snapshots and changes, plus `workflow_lifecycle`, `workflow_stage_completed`, and `workflow_heartbeat` hooks (the runtime state table is in [`workflows/operations.md`](/workflows/operations#workflow-activity-for-extensions)). It does not change chat notifications. The built-in [Herdr reporter](/herdr) consumes this stream to reflect workflow execution and human-input waits in the owning pane.
729
+
730
+
731
+ | Hook | Payload and semantics |
732
+ | --- | --- |
733
+ | `workflow_lifecycle` | `WorkflowLifecycleEvent`: run, stage, tool, or prompt target with typed status, optional previous status, event identity, cursor, ownership, timestamps, and `live` or `replay` delivery. Run targets may carry a control `action`, distinct from its eventual outcome. |
734
+ | `workflow_activity_changed` | `WorkflowActivityChangedEvent`: full root replacement and the same cursor as the observer's `changed` frame. No initial snapshot guarantee. |
735
+ | `workflow_stage_completed` | `WorkflowStageCompletedEvent`: the lifecycle envelope with a stage target whose status is `completed`. Shares the lifecycle event ID and cursor. Failed, skipped, cancelled, and killed outcomes do not produce this hook. |
736
+ | `workflow_heartbeat` | `WorkflowHeartbeatEvent`: run/root/owner identity, `scheduledAt`, and `intervalMinutes`. Observation only, with no scheduler or cadence change. |
737
+
738
+ Run control actions describe the caller's request: an already-aborted caller signal still emits `kill` after run registration, and a whole-run pause at a task-result checkpoint emits `pause` while graceful suspension retains the paused outcome and `exitReason: "quit"`. A control event alone does not mean execution has drained.
739
+
740
+ Use `ctx.observeWorkflowActivity` for status consumers. Registration captures a snapshot atomically with attaching the observer. Delivery is asynchronous, snapshot first, then FIFO updates. Each callback finishes before the next callback for that observer starts; a slow observer does not delay the publisher or other observers.
741
+
742
+ ```typescript
743
+ import type { ExtensionAPI, WorkflowActivitySubscription, WorkflowRootActivity } from "@bastani/atomic";
744
+
745
+ export default function (pi: ExtensionAPI) {
746
+ let lease: WorkflowActivitySubscription | undefined;
747
+ const roots = new Map<string, WorkflowRootActivity>();
748
+
749
+ pi.on("session_start", (_event, ctx) => {
750
+ lease?.dispose();
751
+ lease = ctx.observeWorkflowActivity((frame) => {
752
+ if (frame.kind === "snapshot") {
753
+ roots.clear();
754
+ if (frame.availability !== "ready") {
755
+ // Unknown activity must not be interpreted as idle.
756
+ return;
757
+ }
758
+ for (const root of frame.roots) roots.set(root.rootRunId, root);
759
+ } else if (frame.kind === "changed") {
760
+ roots.set(frame.root.rootRunId, frame.root);
761
+ } else {
762
+ roots.delete(frame.rootRunId);
763
+ }
764
+ });
765
+ });
766
+ pi.on("session_shutdown", () => lease?.dispose());
767
+ pi.on("workflow_stage_completed", (event) => {
768
+ // Canonical nested identity, not the display name.
769
+ console.log(event.eventId, event.target.stageId, event.delivery);
770
+ });
771
+ }
772
+ ```
773
+
774
+ Frames are ordinary objects with a `{ epoch: string, revision: number }` cursor. Revisions increase within an epoch; lifecycle publication may leave gaps between activity revisions. A new publisher starts a new epoch and an `unavailable` snapshot. Never compare revision numbers across epochs. A `ready` snapshot has a `roots` array, including an empty array when known empty. `recovering` and `unavailable` snapshots omit `roots`. Subsequent snapshots replace all prior knowledge. Changes replace a complete root, not increment counters. Removals contain `rootRunId`. Root summaries include `state`, `reason`, execution/wait counts, and `needsAttention`.
775
+
776
+ Providers call `pi.registerWorkflowActivityPublisher()` and retain its returned `WorkflowActivityPublisher`. Its methods are `publishSnapshot({ availability: "ready", roots })`, `publishSnapshot({ availability: "recovering" | "unavailable" })`, `publishChanged(root)`, `publishRemoved(rootRunId)`, `publishLifecycle(event)`, `publishHeartbeat(event)`, and `dispose()`. Lifecycle input includes `type: "workflow_lifecycle"` and the envelope except `cursor`, which the host supplies. Heartbeat input includes `type: "workflow_heartbeat"`. IDs, names, timestamps, zero counts and optional attribution are preserved. Roots are keyed by `rootRunId`; duplicate snapshot IDs use the last value at the first insertion position. Removing an absent ID is permitted. Changes do not turn an unknown source into `ready`; publish a snapshot to establish readiness.
777
+
778
+ Workflow hooks published during extension factory initialization are retained until the runner binds dispatch, then delivered asynchronously in publication order before later live publications. Buffered events keep their original payloads and cursors; publisher retirement and runner disposal fence them just like live events.
779
+
780
+ Observation leases and publisher disposal are idempotent. Runner retirement on reload disposes every observer and fences publishers. Already-running callbacks cannot be cancelled, but no queued observer callbacks run after disposal. Runner retirement, publisher disposal, and publisher replacement also fence every workflow hook handler that has not started, including later handlers in the same or another extension when a previous handler is awaiting. Already-published activity frames remain ordered before the new source snapshot. Activity recovery never synthesizes lifecycle completions; explicit lifecycle replay retains the supplied event ID and `delivery: "replay"`.
781
+
782
+ The host hub retains at most 256 diagnostics, available through its host-side `diagnostics()` inspection API. These are diagnostic records, not thrown observation errors:
783
+
784
+ - `ObserverDisposed`: an observation lease was retired.
785
+ - `SourceRecovering`: the provider is hydrating state.
786
+ - `SourceUnavailable`: no current source snapshot is known.
787
+ - `ObserverDeliveryFailed`: a callback threw or rejected; other observers and publication continue.
788
+ - `ObserverOverflow`: a per-observer queue reached its 256-frame limit. Pending frames are cleared and a fresh snapshot replaces them, invalidating continuity instead of silently losing updates.
789
+ - `PublisherFenced`: a disposed or superseded publisher attempted publication.
790
+
791
+ ## Next steps
792
+
793
+ Continue with [extension UI](/extensions/ui) to add user interaction. Look up the context available to event handlers in the [Extension API reference](/extensions/api-reference#extensioncontext).