# Lifecycle events

Lifecycle events are notifications: *"you were just installed"*, *"a chat just started"*. Unlike middleware there
is nothing to transform or veto — your handler runs, returns nothing, and the platform moves on. They are where a
plugin sets itself up, heals itself, and cleans up.

```ts
globalThis.events.<event> = async (context) => {
  // do the work; return nothing
};
```

Assigning the handler at the top level is all it takes — the event is discovered at publish time like any other
capability.

## The six events

| Event | Fires | Sent to | `host.session` |
| --- | --- | --- | --- |
| `pluginInstalled` | your plugin was installed on an account (also on **Re-run setup**) | your plugin only | none — there is no chat |
| `pluginUpdated` | the install moved to a different version (auto-update, accepted update, policy change, a new draft) | your plugin only | none |
| `pluginUninstalled` | your plugin was uninstalled | your plugin only | none |
| `sessionStarted` | a new interactive chat was created | every plugin on the account that handles it | that chat |
| `sessionCleared` | a chat was cleared in place with `/new` | every plugin on the account that handles it | that (now empty) chat |
| `sessionDeleted` | a chat was deleted | every plugin on the account that handles it | none usable — the chat is already gone |

The session events fire for **interactive** chats only — not for scheduled runs, subagent sessions or
agent-to-agent threads.

### Contexts

```ts
pluginInstalled:   { installationId: string; pluginSlug: string; versionId: string; semVer: string }
pluginUpdated:     { installationId: string; previousVersionId: string; newVersionId: string;
                     previousSemVer: string; newSemVer: string }
pluginUninstalled: { installationId: string; pluginSlug: string }
sessionStarted:    { sessionId: string; agentId: string; sessionKind: string }   // "interactive" today
sessionCleared:    { sessionId: string; agentId: string }
sessionDeleted:    { sessionId: string; agentId: string }                         // the only record left
```

## Delivery: at least once, never blocking

- Each event is written to a durable outbox right after the change that caused it is saved, then delivered by a
  background worker. If AgentParley restarts after that, the event is still delivered.
- A handler that throws, times out or runs out of memory is logged and **skipped**; it is not run again for that
  event. Catch errors you can recover from inside the handler, and make the next event (`sessionStarted`,
  **Re-run setup**) able to finish the job.
- If AgentParley itself can't deliver the event (the plugin runtime is unreachable, a database error), the delivery is
  retried with exponential backoff (30 s base, up to 8 attempts).
- The action that caused the event is never held up by your handler: the install completes, the chat opens.

**At least once** means your handler can run more than once for the same occurrence (a redelivery after such an
infrastructure failure). Write every handler to be **idempotent** — running it twice must leave the same result as
running it once. For crews that's built in: `host.agents.create` is get-or-create by `key`.

## What a handler can reach

The full host surface your permissions earn: `host.fetch`, `host.settings`, `host.crypto`, `host.log`,
`host.agents.*`, `host.cronJobs.*` and `host.models.list()` (these work without a chat), `host.oauth`, `host.llm`
and `host.decide`. The session events additionally get `host.session.*` for their chat, `host.session.llm` (the
chat's agent model) and `host.workspace.*` (the chat's agent's files).

Wall clock: 30 s per delivery attempt.

## Patterns

### Provision and heal a crew

The **Oh My OpenAgents** crew plugin uses three events to keep its seven agents correct — created on install,
re-asserted after every update, and re-created if the user deleted one, the next time any chat starts:

```ts
// infrastructure/plugins/oh-my-openagents/index.ts
globalThis.events.pluginInstalled = async () => {
  await reconcileCrew();
};
globalThis.events.sessionStarted = async () => {
  await reconcileCrew();
};
globalThis.events.pluginUpdated = async () => {
  await reconcileCrew();
};
```

`reconcileCrew` lists the plugin's agents, creates missing ones by key, updates drifted ones and deletes retired
ones. See [Agent crews](plugin-agent-crews.md#self-healing-crews).

If the account didn't have room for every agent on install (plan limit), the owner can upgrade and press
**Re-run setup** on the installed plugin's page — that sends `pluginInstalled` again.

### Greet a new chat

Written for these docs — post a first message into every new interactive chat:

```ts
globalThis.events.sessionStarted = async () => {
  await host.session!.message.post({ text: "Tip: type /orchestrate <goal> to put the whole crew on it." });
};
```

Remember this can run twice; a greeting posted twice is harmless, but a counter incremented twice is not.

### Clean up external state

`pluginUninstalled` is where to revoke a webhook you registered with a vendor or delete a remote record. Your
plugin's agents, channels, live connections and stored secrets are removed by the platform itself on uninstall —
you don't need to delete them.

## Events vs. the session-clear middleware

`sessionClearing` (middleware) fires **before** `/new` deletes anything, so you can still read the transcript.
`sessionCleared` (event) fires **after**, durably. Use the hook to read, the event to react.
