diff --git a/adapters/linear/doc.go b/adapters/linear/doc.go index 9a795c9..e48eaff 100644 --- a/adapters/linear/doc.go +++ b/adapters/linear/doc.go @@ -1,7 +1,8 @@ // Package linear provides a Linear app-actor adapter for Chat SDK Go. // // The adapter participates as a Linear app-owned actor on App-Actor Client -// Credentials (a Single-Install Adapter). It supports two Linear interaction +// Credentials (a Single-Install Adapter by default; see Multi-tenant installs +// below for serving many organizations). It supports two Linear interaction // models, both reached through the small chat.Adapter interface and the opaque, // versioned Thread ID: // diff --git a/adapters/slack/doc.go b/adapters/slack/doc.go index b37547b..f1554ba 100644 --- a/adapters/slack/doc.go +++ b/adapters/slack/doc.go @@ -24,9 +24,12 @@ // // The install credential rides as the adapter-specific slack.SlackInstall payload // on chat.Install.Credential (a Platform Escape Hatch for credentials): the -// per-workspace bot token plus an optional bot user id used for tenant-correct -// self-filtering. Thread Handle reconstruction (out-of-webhook posting) resolves -// the same way, keyed by the Platform Tenant decoded from the opaque Thread ID. +// per-workspace bot token plus the bot user id used for tenant-correct +// self-filtering. The bot user id is optional in the type but should be treated +// as required: without it the bot's own posts are not filtered, and a +// subscribed thread can loop on its own replies. Thread Handle reconstruction +// (out-of-webhook posting) resolves the same way, keyed by the Platform Tenant +// decoded from the opaque Thread ID. // // # Message history (ADR 0009) // @@ -41,15 +44,17 @@ // // # Rate-limit retry (ADR 0005) // -// Outbound posts are hardened against Slack throttling by default. The adapter -// wraps its Slack Web API call site (chat.postMessage / chat.postEphemeral / -// conversations.open) with bounded retry on a Slack 429 (honoring the Retry-After -// header, in seconds) and the ratelimited API error. Retry is bounded three ways: -// an attempt cap (Options.RetryPolicy.MaxAttempts), a cumulative backoff ceiling -// (MaxElapsed), and the caller's context deadline. The single load-bearing -// invariant is that in-line synchronous retry never sleeps past the caller's -// context deadline, so retry under the default DispatchSync stays inside Slack's -// 3-second ack window and cannot trigger a platform redelivery storm. +// Outbound calls are hardened against Slack throttling by default. The adapter +// wraps every Slack Web API call (including auth.test, chat.postMessage, +// chat.postEphemeral, conversations.open, views.open, and history reads) and +// every response_url post with bounded retry on a Slack 429 (honoring the +// Retry-After header, in seconds) and the ratelimited API error. Retry is +// bounded three ways: an attempt cap (Options.RetryPolicy.MaxAttempts), a +// cumulative backoff ceiling (MaxElapsed), and the caller's context deadline. +// The single load-bearing invariant is that in-line synchronous retry never +// sleeps past the caller's context deadline, so retry under the default +// DispatchSync stays inside Slack's 3-second ack window and cannot trigger a +// platform redelivery storm. // // The RetryPolicy is per-adapter platform config in Options, never Runtime // Options. Its zero value is a conservative default that keeps MaxElapsed under diff --git a/docs/how-to/choose-a-state-backend.md b/docs/how-to/choose-a-state-backend.md index 05c3fb6..e83fd3b 100644 --- a/docs/how-to/choose-a-state-backend.md +++ b/docs/how-to/choose-a-state-backend.md @@ -45,8 +45,12 @@ import ( chatredis "github.com/coder/chat/state/redis" ) +redisOptions, err := redis.ParseURL(os.Getenv("REDIS_URL")) // e.g. redis://127.0.0.1:6379/0 +if err != nil { + return err +} redisState, err := chatredis.New(ctx, chatredis.Options{ - Client: redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")}), + Client: redis.NewClient(redisOptions), Prefix: "mybot", // see "One namespace per bot application" below }) ``` diff --git a/docs/how-to/deferred-dispatch.md b/docs/how-to/deferred-dispatch.md index 75df8a1..b64d1a0 100644 --- a/docs/how-to/deferred-dispatch.md +++ b/docs/how-to/deferred-dispatch.md @@ -46,8 +46,9 @@ bot, err := chat.New(ctx, - `Dispatch: chat.DispatchDeferred` turns on ack-then-work. The default is `chat.DispatchSync`, which runs the handler before acknowledging. -- `DetachTimeout` bounds how long a detached handler may run after the webhook - request has ended. +- `DetachTimeout` (required under `DispatchDeferred`; `DefaultRuntimeOptions()` + leaves it at zero, so `chat.New` fails until you set it) bounds how long a + detached handler may run after the webhook request has ended. - `Concurrency: chat.ConcurrencyQueue` is the natural companion: while a detached handler holds the thread lock, follow-up events on the same thread wait instead of being dropped, and only the most recent superseded follow-up diff --git a/docs/how-to/linear-agent-sessions.md b/docs/how-to/linear-agent-sessions.md index f907435..881e312 100644 --- a/docs/how-to/linear-agent-sessions.md +++ b/docs/how-to/linear-agent-sessions.md @@ -97,7 +97,6 @@ if err := la.UpdateSession(ctx, ev.Thread.ID(), linear.AgentSessionUpdateInput{ {Title: "Reproduce the bug", Status: "pending"}, {Title: "Fix and test", Status: "pending"}, }, - ReplacePlan: true, }); err != nil { return err } @@ -148,9 +147,10 @@ This check only runs when the stop event reaches your handler, and events on one thread are serialized by the thread lock — a stop arriving while a handler is still running cannot preempt it (`ConcurrencyDrop` discards it on conflict; `ConcurrencyQueue` delivers it only after the in-flight handler -returns). The ADR 0012 force/steerability hook that would allow preemption is -staged behind the deferred-dispatch coordination design work, so **Linear's -Stop control cannot cancel in-flight work through this adapter today**. What +returns). The runtime has no preemption: the ADR 0012 force/steerability hook +that would allow it is rejected for v0.x by +[ADR 0015](../adr/0015-runtime-coordination.md), so **Linear's Stop control +cannot cancel in-flight work through this adapter**. What you can do: structure long sessions as short handler turns (each turn checks `StopRequested` on the event that started it before doing more work — `confirmStop` above is exactly diff --git a/docs/linear-agent-capabilities.md b/docs/linear-agent-capabilities.md index 93487eb..6e0d99b 100644 --- a/docs/linear-agent-capabilities.md +++ b/docs/linear-agent-capabilities.md @@ -33,7 +33,7 @@ hatch rather than typed helpers. | Typed activity helpers | Supported | `PostThought`, `PostAction`, `PostElicitation`, `PostError`; `Thread.Post` creates the `response` activity. | | Agent-to-human signals | Supported | `auth` and `select` signals with metadata pass through `CreateAgentActivity` / `PostElicitation`. | | Human-to-agent stop signal | Supported | `RawMessageFrom(ev.Message)` exposes `Signal` / `StopRequested()`; see the routing caveat below. | -| Session updates | Supported | `UpdateSession` sets `externalUrls` and replaces the session plan array. | +| Session updates | Supported | `UpdateSession` replaces `externalUrls` or adjusts them with `AddExternalURLs` / `RemoveExternalURLs`, and replaces the session plan array. | | GraphQL escape hatch | Supported | `GraphQL` (single-install) and `GraphQLForTenant` (multi-tenant) reuse adapter auth and token refresh, surface GraphQL errors, and never expose tokens. | | Proactive agent session creation | Supported | `CreateSessionOnIssue` / `CreateSessionOnComment` (plus `ForTenant` variants) wrap `agentSessionCreateOnIssue` / `agentSessionCreateOnComment`; the returned `CreatedAgentSession` carries the adapter's opaque `ThreadID` ([#47](https://github.com/coder/chat/issues/47)). | | Repository suggestions | Supported | `SuggestRepositories` wraps `issueRepositorySuggestions` with typed candidates and confidence-scored results ([#48](https://github.com/coder/chat/issues/48)). | diff --git a/docs/reference.md b/docs/reference.md index 653a366..0bd3e76 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -239,10 +239,11 @@ inbound webhook request context before the platform acknowledgement. For long-running work, opt in to `DispatchDeferred` (ack-then-work, [ADR 0002](adr/0002-async-dispatch.md)): the dedupe and lock prelude runs before the acknowledgement, then the handler runs on a detached work context -with automatic lock lease renewal, bounded by `DetachTimeout`. Under deferred -dispatch `MaxDetached` bounds admitted-but-incomplete deliveries; a delivery -arriving at the bound is rejected with `chat.ErrAdmissionRejected` before -acknowledgement and before dedupe marking ([ADR 0015](adr/0015-runtime-coordination.md)). +with automatic lock lease renewal, bounded by `DetachTimeout` (which must be +positive under deferred dispatch). Under deferred dispatch `MaxDetached` +bounds admitted-but-incomplete deliveries; a delivery arriving at the bound is +rejected with `chat.ErrAdmissionRejected` before acknowledgement and before +dedupe marking ([ADR 0015](adr/0015-runtime-coordination.md)). The [deferred dispatch guide](how-to/deferred-dispatch.md) covers enabling it and writing handlers for the detached context. @@ -325,7 +326,7 @@ The runtime implements all five upstream-aligned concurrency strategies - `ConcurrencyDebounce`: each new routed event supersedes the previous waiter; only the final event in a `DebounceInterval` quiet period dispatches, and superseded events are observable. Requires deferred - dispatch. + dispatch and a `DetachTimeout` longer than `DebounceInterval`. - `ConcurrencyConcurrent`: no thread lock at all; every event dispatches in its own execution, bounded by `MaxConcurrent`. - `ConcurrencyBurst`: routed events for a scope collect for a `BurstWindow`, diff --git a/docs/tutorials/slack-bot.md b/docs/tutorials/slack-bot.md index d98b949..2c5be84 100644 --- a/docs/tutorials/slack-bot.md +++ b/docs/tutorials/slack-bot.md @@ -97,7 +97,7 @@ discover the bot's own identity. If the token is wrong you find out now, not on the first message. When the bot is up you should see a log line like: ```text -level=INFO msg=listening addr=:8080 +2026/09/23 14:00:00 INFO listening addr=:8080 ``` ## Step 5: Expose The Bot To Slack @@ -165,7 +165,7 @@ this, only mentions ever reach it: 2. In **Event Subscriptions**, add the `message.channels` bot event. 3. Reinstall the app from **OAuth & Permissions**. -(If you set up `message.im` in Step 2, you can skip this and test the +(If you subscribed to `message.im` in Step 6, you can skip this and test the follow-up flow in a direct message instead.) Then open `examples/slack-hello-world/main.go` and replace the diff --git a/examples/linear-agent-hello-world/README.md b/examples/linear-agent-hello-world/README.md index 01afa20..9fb4111 100644 --- a/examples/linear-agent-hello-world/README.md +++ b/examples/linear-agent-hello-world/README.md @@ -151,20 +151,25 @@ Expected behavior: Thinking... ``` -5. The app posts the final response: +5. The app posts a `search-codebase` action and adds a **Draft PR** external + link to the session. +6. The app posts the final response, a bold `hello from Linear app actor` + line followed by a note inviting a follow-up prompt. - ```text - hello from Linear app actor - ``` + If the prompt text is exactly `deploy`, the app instead asks a `select` + question ("Which environment should I target?", `staging` or `prod`), and + your next follow-up is read as the answer. -6. Send a follow-up prompt in the same Linear agent session. -7. The example routes it to `OnSubscribedMessage`, posts another ephemeral - thought, and replies with: +7. Send a follow-up prompt in the same Linear agent session. +8. The example routes it to `OnSubscribedMessage`, posts the ephemeral thought + `Reading your follow-up...`, and replies with: ```text Follow-up received: YOUR_MESSAGE ``` + A follow-up carrying Linear's stop signal gets a stop confirmation instead. + ## Dogfooding Evidence Before claiming a live Linear dogfood passed, capture screenshots or video of: @@ -179,7 +184,8 @@ Before claiming a live Linear dogfood passed, capture screenshots or video of: - State is in memory, so subscriptions and dedupe data are lost when the process exits. -- Use Redis or Postgres runtime state for production deployments. +- Use Redis, Postgres, or NATS JetStream runtime state for production + deployments. - Linear request signatures are verified with `LINEAR_WEBHOOK_SECRET`. - Client credentials are exchanged during adapter startup and refreshed lazily before Linear API calls. diff --git a/examples/slack-hello-world/README.md b/examples/slack-hello-world/README.md index a91dfc1..bfcba09 100644 --- a/examples/slack-hello-world/README.md +++ b/examples/slack-hello-world/README.md @@ -5,8 +5,8 @@ mentioned in a channel, or messaged directly if you enable the DM event, it replies with portable Markdown: `**hello** _world_`. This example requires `CHAT_DEMO_IN_MEMORY_STATE=1` because state is lost on -restart. Use `examples/slack-redis-state` or `examples/slack-postgres-state` for -durable Slack apps. +restart. Use `examples/slack-redis-state`, `examples/slack-postgres-state`, or +`examples/slack-nats-state` for durable Slack apps. ## Slack App Setup @@ -45,12 +45,9 @@ In **Event Subscriptions**: https://YOUR_PUBLIC_HOST/webhooks/slack ``` -3. Subscribe to these **Bot User Events**: - - ```text - app_mention - message.im - ``` +3. Subscribe to the `app_mention` **Bot User Event**. Also add `message.im` + if you want direct messages to reach the bot (it needs the `im:history` + scope). 4. Save changes. diff --git a/examples/slack-nats-state/README.md b/examples/slack-nats-state/README.md index 13f7a2d..9e4aa08 100644 --- a/examples/slack-nats-state/README.md +++ b/examples/slack-nats-state/README.md @@ -45,12 +45,9 @@ In **Event Subscriptions**: https://YOUR_PUBLIC_HOST/webhooks/slack ``` -3. Subscribe to these **Bot User Events**: - - ```text - app_mention - message.im - ``` +3. Subscribe to the `app_mention` **Bot User Event**. Also add `message.im` + if you want direct messages to reach the bot (it needs the `im:history` + scope). 4. Save changes. @@ -77,7 +74,7 @@ Treat both values like passwords. ## Run NATS -From this example directory: +From the repository root: ```sh cd examples/slack-nats-state diff --git a/examples/slack-postgres-state/README.md b/examples/slack-postgres-state/README.md index 0e12809..fd536e8 100644 --- a/examples/slack-postgres-state/README.md +++ b/examples/slack-postgres-state/README.md @@ -42,12 +42,9 @@ In **Event Subscriptions**: https://YOUR_PUBLIC_HOST/webhooks/slack ``` -3. Subscribe to these **Bot User Events**: - - ```text - app_mention - message.im - ``` +3. Subscribe to the `app_mention` **Bot User Event**. Also add `message.im` + if you want direct messages to reach the bot (it needs the `im:history` + scope). 4. Save changes. @@ -74,7 +71,7 @@ Treat both values like passwords. ## Run Postgres -From this example directory: +From the repository root: ```sh cd examples/slack-postgres-state diff --git a/examples/slack-redis-state/README.md b/examples/slack-redis-state/README.md index afd9e4d..79c0f50 100644 --- a/examples/slack-redis-state/README.md +++ b/examples/slack-redis-state/README.md @@ -42,12 +42,9 @@ In **Event Subscriptions**: https://YOUR_PUBLIC_HOST/webhooks/slack ``` -3. Subscribe to these **Bot User Events**: - - ```text - app_mention - message.im - ``` +3. Subscribe to the `app_mention` **Bot User Event**. Also add `message.im` + if you want direct messages to reach the bot (it needs the `im:history` + scope). 4. Save changes. @@ -74,7 +71,7 @@ Treat both values like passwords. ## Run Redis -From this example directory: +From the repository root: ```sh cd examples/slack-redis-state