Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
208b6cf
docs: add design spec for outbound webhooks
mirchaemanuel May 19, 2026
fe44b34
docs: add implementation plan for outbound webhooks
mirchaemanuel May 19, 2026
fb4bbcd
feat(core): add typed EventBus for in-process pub-sub
mirchaemanuel May 19, 2026
9f3bed8
fix(core): defensive copy of subscriber list in EventBus.Publish
mirchaemanuel May 19, 2026
4204186
feat(core): publish SessionEvent on activity transitions
mirchaemanuel May 19, 2026
4b3e7b3
fix(core): revert waitingSince anchor change, fix transition test
mirchaemanuel May 19, 2026
c41e008
feat(core): SessionManager.SetEventBus propagates bus to tracker
mirchaemanuel May 19, 2026
cd43c39
feat(core): add WebhookConfig type with validation
mirchaemanuel May 19, 2026
20afa71
feat(webhook): payload schema with optional API links
mirchaemanuel May 19, 2026
7d25b46
feat(webhook): event + agent filter matching
mirchaemanuel May 19, 2026
729bb7d
feat(webhook): HMAC-SHA256 signing of payloads
mirchaemanuel May 19, 2026
d4784ae
feat(webhook): dispatcher with fan-out and HTTP POST delivery
mirchaemanuel May 19, 2026
1595800
feat(webhook): retry transient failures, no retry on 4xx
mirchaemanuel May 19, 2026
5050077
test(webhook): cover HMAC header wire format end-to-end
mirchaemanuel May 19, 2026
620256c
feat(webhook): dedup duplicate transitions across managers
mirchaemanuel May 19, 2026
6fb2bdf
test(webhook): cover graceful shutdown under load
mirchaemanuel May 19, 2026
589cb00
feat(ui): accept optional EventBus for transition publishing
mirchaemanuel May 19, 2026
e87e294
feat(api): accept optional EventBus for transition publishing
mirchaemanuel May 19, 2026
ca30e10
feat(tray): start webhook dispatcher when configured
mirchaemanuel May 19, 2026
66d95a2
feat: start webhook dispatcher in main when webhooks configured
mirchaemanuel May 19, 2026
bbe253a
docs: document outbound webhooks
mirchaemanuel May 19, 2026
0786d9e
fix(webhook): correct User-Agent header and evict stale dedup entries
mirchaemanuel May 19, 2026
172f597
fix(webhook): address 5 review findings
mirchaemanuel May 19, 2026
e533653
fix(webhook): normalize wildcard API addr, document --gui --api limit…
mirchaemanuel May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Inspired by [lazygit](https://github.com/jesseduffield/lazygit), [lazyworktree](
- **[`lazyagent compact`](docs/maintenance/compact.md)** — shrink session files in place by truncating bulky tool outputs, thinking blocks, and embedded images — sessions stay resumable with the originating agent. Supports Claude Code, pi, and Codex.
- **[`lazyagent search`](docs/maintenance/search.md)** — search transcript-file agents (Claude, Codex, pi, Amp) with highlighted snippets and an incremental local index.
- **[`lazyagent limits`](docs/maintenance/limits.md)** — on-demand 5-hour and weekly rate-limit snapshot for Claude Code and Codex, with a pace indicator that flags whether you're under-, on-, or over-utilizing the window.
- **Outbound webhooks on session state transitions** — send a signed JSON payload to Slack, a custom dashboard, or a CI endpoint whenever a session goes idle, waits for input, or changes state. See [Webhooks](docs/reference/webhooks.md).

Typical savings on a year of daily use: **80+ MiB reclaimed** across a few commands, with every rewrite validated and backed up by default.

Expand Down
20 changes: 20 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ Default: `"dark"`. Supported values:

All TUI colors (panels, activity labels, help bar, overlays) are driven by the theme.

### `webhooks`

Default: `[]` (empty — no outbound webhooks). A list of HTTP endpoints that receive a POST whenever a session changes activity state. Each entry can filter by event type and agent source, and optionally sign requests with HMAC-SHA256.

```json
{
"webhooks": [
{
"name": "slack-needs-input",
"url": "https://hooks.slack.com/services/T00/B00/XXX",
"secret": "abc123",
"events": ["waiting"],
"agents": ["claude"]
}
]
}
```

See [Outbound Webhooks](webhooks.md) for the full field reference, payload schema, request headers, HMAC verification, delivery semantics, and troubleshooting tips.

## Where the config file lives

| OS | Path |
Expand Down
10 changes: 9 additions & 1 deletion docs/reference/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,17 @@ sidebar:
- ✅ Codex via the latest rollout JSONL under `~/.codex/sessions/` — no network call, fallback to older rollouts when the most recent has no `rate_limits` event yet
- ✅ Honest User-Agent (no Claude Code impersonation), graceful failure on 401/429, disclaimer in `--help` and output

## v0.10 — Outbound webhooks

- ✅ Typed `core.EventBus` for in-process pub-sub of activity transitions
- ✅ `internal/webhook/` dispatcher with async best-effort delivery
- ✅ Event + agent filters per webhook
- ✅ Optional HMAC-SHA256 signing (GitHub-style header)
- ✅ Async fan-out with bounded queue, retry on transient failures, dedup window for duplicate transitions across in-process managers
- ✅ Documentation with payload schema and verification example

## Future ideas

- ⬜ Outbound webhooks on status changes
- ⬜ Multi-machine support via shared config / remote API
- ⬜ TUI actions: kill session, attach terminal
- ⬜ Session history browser (browse past conversations)
Expand Down
120 changes: 120 additions & 0 deletions docs/reference/webhooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: "Outbound Webhooks"
description: "Send session state transitions to Slack, dashboards, or CI pipelines via HTTP POST."
sidebar:
order: 3
---

Outbound webhooks let lazyagent push a JSON payload to any HTTP endpoint whenever a session changes activity state. Common uses include posting to a Slack channel when an agent is waiting for input, feeding a custom dashboard, or triggering a CI step when a long-running session goes idle.

## Configuration

Add a `webhooks` array to `~/.config/lazyagent/config.json`:

```json
{
"webhooks": [
{
"name": "slack-needs-input",
"url": "https://hooks.slack.com/services/T00/B00/XXX",
"secret": "abc123sharedwithslack",
"events": ["waiting"],
"agents": ["claude", "codex"]
},
{
"name": "dashboard-everything",
"url": "https://my-dashboard.local/api/lazyagent"
}
]
}
```

The first entry fires only when a Claude Code or Codex session enters the `waiting` state, and signs each request with an HMAC-SHA256 header. The second entry receives every transition from every agent, unsigned.

## Field reference

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | Human-readable identifier used in log lines. |
| `url` | string | yes | Destination endpoint. `http://` and `https://` are both accepted. |
| `secret` | string | no | When set, each request carries an `X-Lazyagent-Signature` header (see [HMAC verification](#hmac-verification)). |
| `events` | string array | no | Activity kinds to deliver. Empty or absent means all events. Valid values: `idle`, `waiting`, `thinking`, `compacting`, `reading`, `writing`, `running`, `searching`, `browsing`, `spawning`. |
| `agents` | string array | no | Agent sources to deliver. Empty or absent means all agents. Valid values: `claude`, `codex`, `pi`, `cursor`, `amp`, `opencode`. |
| `enabled` | boolean | no | Defaults to `true`. Set to `false` to disable the entry without removing it. |

## Payload schema

Every delivery is an HTTP POST with a JSON body:

```json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"event": "state_transition",
"session_id": "abc123",
"agent": "claude",
"from": "idle",
"to": "waiting",
"project_path": "/Users/foo/code/bar",
"timestamp": "2026-05-19T14:30:00Z",
"api": {
"session_url": "http://127.0.0.1:7421/api/sessions/abc123"
}
}
```

The `api` object is included on a best-effort basis. It is present when the
webhook dispatcher and the API server run in the same process — typically
`--tui --api` (or `--api` alone). When the GUI tray is involved
(`--gui`, `--gui --api`, `--tui --gui --api`), the tray process owns
webhook delivery while the API server lives in the parent process, so the
two are not linked and `api` is omitted from payloads. Consumers should
treat `api` as optional and not rely on its presence.

## Request headers

| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `User-Agent` | `lazyagent/<version>` |
| `X-Lazyagent-Event` | `state_transition` |
| `X-Lazyagent-Delivery` | UUID matching the `id` field in the body |
| `X-Lazyagent-Signature` | `sha256=<hex>` (only when `secret` is configured) |

## HMAC verification

When `secret` is set, the signature is computed over the raw request body using HMAC-SHA256. Verify it on the receiving side before trusting the payload:

```python
import hmac, hashlib

secret = b"abc123sharedwithslack"
body = request.get_data()
sig = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, request.headers["X-Lazyagent-Signature"]):
abort(401)
```

Always use a constant-time comparison (`hmac.compare_digest` or equivalent) to avoid timing attacks.

## Delivery semantics

- **Asynchronous, best-effort.** Webhooks are dispatched in the background and never block session monitoring.
- **Bounded queue.** Each dispatcher holds up to 256 pending deliveries. If the queue is full, new events are dropped and a log line is emitted.
- **Retry on transient failures.** HTTP 5xx responses and network errors trigger exponential backoff: 1 s, 5 s, 30 s. Maximum 4 attempts total.
- **No retry on 4xx.** Client errors (wrong URL, bad auth, malformed payload on the consumer side) are logged with the status code and a body snippet, then discarded.
- **Dedup window.** Duplicate transitions within 2 seconds are coalesced. This prevents double-delivery when multiple in-process managers (e.g. `--tui` and `--gui` running together) each observe the same transition.
- **`api.*` URLs.** Present only when `--api` is active, the server is bound, and the dispatcher and API server share the same process; absent otherwise (see note above the payload schema).

## Troubleshooting

**`api.session_url` is missing in `--gui --api` mode.**
This is expected: the tray process delivers webhooks while the parent process runs the API server, and the two are not cross-linked. Use `--tui --api` if you need the backlink in the payload.

**I see no POSTs.**
Verify that the `webhooks` array is non-empty and well-formed JSON. lazyagent logs invalid webhook entries on startup with a line like `config: webhook "name": ...`. Also confirm the `events` and `agents` filters match what you expect.

**I see duplicate deliveries.**
Check whether you are running more than one lazyagent process simultaneously (e.g. `--tui` in one terminal and `--gui` in the background). Each process has its own dispatcher and can emit independent POSTs for the same transition. The 2-second dedup window covers duplicate detection within a single process only.

**4xx errors appear in the log.**
The consumer is rejecting the request. lazyagent does not retry 4xx responses by design — fix the consumer endpoint (URL, auth headers, expected payload shape) and the next transition will deliver cleanly.
Loading