From df1956141722e240183d3f9a4e18fa2a56ff4f98 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 29 Oct 2026 20:49:45 +0000 Subject: [PATCH 1/5] docs(examples): fix stale tutorial steps, reply text, and Draft PR wording The state READMEs now point at the tutorial sections that hold the tunnel and the channels:history setup, and show the reply as the Markdown that main.go posts. The Linear README says UpdateSession replaces the external links. --- examples/linear-agent-hello-world/README.md | 4 ++-- examples/slack-nats-state/README.md | 15 +++++++++------ examples/slack-postgres-state/README.md | 15 +++++++++------ examples/slack-redis-state/README.md | 15 +++++++++------ 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/examples/linear-agent-hello-world/README.md b/examples/linear-agent-hello-world/README.md index e24a65f..d995738 100644 --- a/examples/linear-agent-hello-world/README.md +++ b/examples/linear-agent-hello-world/README.md @@ -112,8 +112,8 @@ Expected behavior: Thinking... ``` -5. The app posts a `search-codebase` action and adds a **Draft PR** external - link to the session. +5. The app posts a `search-codebase` action and sets the session's external + links to one **Draft PR** link, which replaces any existing links. 6. The app posts the final response, a bold `hello from Linear app actor` line followed by a note inviting a follow-up prompt. diff --git a/examples/slack-nats-state/README.md b/examples/slack-nats-state/README.md index 7919d00..4235135 100644 --- a/examples/slack-nats-state/README.md +++ b/examples/slack-nats-state/README.md @@ -13,10 +13,12 @@ or newer). ## Slack App Setup Set up a Slack app as in the [tutorial](../../docs/tutorials/slack-bot.md): -Steps 2–3 create the app and collect credentials, Step 5 exposes your -machine, and Step 6 subscribes to events. This example also replies to follow-up messages in -subscribed threads, so add Step 8's `channels:history` scope and -`message.channels` event too. +Steps 2–3 create the app and collect credentials, and +[Use HTTP Instead Of Socket Mode](../../docs/tutorials/slack-bot.md#use-http-instead-of-socket-mode) +exposes your machine and sets the request URL. This example also replies to +follow-up messages in subscribed threads, so add the `channels:history` scope +and `message.channels` event from +[Step 6](../../docs/tutorials/slack-bot.md#step-6-make-it-yours) too. | Setting | Value | | --- | --- | @@ -82,10 +84,11 @@ In Slack, mention the bot in a channel where it is present: @your-bot hello ``` -The bot replies in the same thread and subscribes the thread: +The bot replies in the same thread with portable Markdown and subscribes the +thread: ```text -hello world from NATS state. This thread is now subscribed. +**hello** _world_ from NATS state. This thread is now subscribed. ``` Send another message in the same Slack thread. The bot should route it as a diff --git a/examples/slack-postgres-state/README.md b/examples/slack-postgres-state/README.md index 9f0f654..65c4b43 100644 --- a/examples/slack-postgres-state/README.md +++ b/examples/slack-postgres-state/README.md @@ -10,10 +10,12 @@ are routed through Postgres-backed state. ## Slack App Setup Set up a Slack app as in the [tutorial](../../docs/tutorials/slack-bot.md): -Steps 2–3 create the app and collect credentials, Step 5 exposes your -machine, and Step 6 subscribes to events. This example also replies to follow-up messages in -subscribed threads, so add Step 8's `channels:history` scope and -`message.channels` event too. +Steps 2–3 create the app and collect credentials, and +[Use HTTP Instead Of Socket Mode](../../docs/tutorials/slack-bot.md#use-http-instead-of-socket-mode) +exposes your machine and sets the request URL. This example also replies to +follow-up messages in subscribed threads, so add the `channels:history` scope +and `message.channels` event from +[Step 6](../../docs/tutorials/slack-bot.md#step-6-make-it-yours) too. | Setting | Value | | --- | --- | @@ -79,10 +81,11 @@ In Slack, mention the bot in a channel where it is present: @your-bot hello ``` -The bot replies in the same thread and subscribes the thread: +The bot replies in the same thread with portable Markdown and subscribes the +thread: ```text -hello world from Postgres state. This thread is now subscribed. +**hello** _world_ from Postgres state. This thread is now subscribed. ``` Send another message in the same Slack thread. The bot should route it as a diff --git a/examples/slack-redis-state/README.md b/examples/slack-redis-state/README.md index 77943ec..2e21b24 100644 --- a/examples/slack-redis-state/README.md +++ b/examples/slack-redis-state/README.md @@ -10,10 +10,12 @@ routed through Redis-backed state. ## Slack App Setup Set up a Slack app as in the [tutorial](../../docs/tutorials/slack-bot.md): -Steps 2–3 create the app and collect credentials, Step 5 exposes your -machine, and Step 6 subscribes to events. This example also replies to follow-up messages in -subscribed threads, so add Step 8's `channels:history` scope and -`message.channels` event too. +Steps 2–3 create the app and collect credentials, and +[Use HTTP Instead Of Socket Mode](../../docs/tutorials/slack-bot.md#use-http-instead-of-socket-mode) +exposes your machine and sets the request URL. This example also replies to +follow-up messages in subscribed threads, so add the `channels:history` scope +and `message.channels` event from +[Step 6](../../docs/tutorials/slack-bot.md#step-6-make-it-yours) too. | Setting | Value | | --- | --- | @@ -79,10 +81,11 @@ In Slack, mention the bot in a channel where it is present: @your-bot hello ``` -The bot replies in the same thread and subscribes the thread: +The bot replies in the same thread with portable Markdown and subscribes the +thread: ```text -hello world from Redis state. This thread is now subscribed. +**hello** _world_ from Redis state. This thread is now subscribed. ``` Send another message in the same Slack thread. The bot should route it as a From f911bb862e8b8be4e9bd5cff1360fa0f376e4f43 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 29 Oct 2026 20:49:45 +0000 Subject: [PATCH 2/5] docs: correct deferred-dispatch, ack-window, and first-thought comments DispatchDeferred starts the handler before it returns to the adapter, so the acknowledgement and the handler run at the same time. The retry deadline check and MaxElapsed bound only the backoff sleep, not the HTTP attempts. newFirstThoughtDeadline falls back to now() for a bad timestamp. --- README.md | 10 +++++----- adapters/linear/raw.go | 4 ++-- adapters/slack/doc.go | 6 ++++-- doc.go | 12 +++++++----- internal/ratelimit/ratelimit.go | 4 ++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 536c772..0f8f6e4 100644 --- a/README.md +++ b/README.md @@ -121,11 +121,11 @@ go get github.com/coder/chat/state/nats runs Socket Mode, `Chat.Webhook` serves HTTP, and a bot can use both. See [receiving deliveries](docs/reference.md#receiving-deliveries). - **Ack-then-work dispatch.** Handlers run synchronously by default; opt in - to `DispatchDeferred` to run them after the event is acknowledged, with - lock renewal and an admission cap. Five concurrency strategies (drop, - queue, debounce, concurrent, burst) decide what happens when events - overlap on one thread; debounce and burst require deferred dispatch — - [defer long-running work](docs/how-to/deferred-dispatch.md). + to `DispatchDeferred` to acknowledge the event without waiting for the + handler, with lock renewal and an admission cap. Five concurrency + strategies (drop, queue, debounce, concurrent, burst) decide what happens + when events overlap on one thread; debounce and burst require deferred + dispatch — [defer long-running work](docs/how-to/deferred-dispatch.md). - **Slash commands and interactive components.** Commands and button clicks have their own hooks; Block Kit and modals go through typed adapter access — [slash commands](docs/how-to/slash-commands.md), diff --git a/adapters/linear/raw.go b/adapters/linear/raw.go index adc453c..5af29e3 100644 --- a/adapters/linear/raw.go +++ b/adapters/linear/raw.go @@ -85,8 +85,8 @@ func RawMessageFrom(m *chat.Message) (*RawMessage, bool) { } // newFirstThoughtDeadline parses the session-created timestamp and computes the -// first-thought deadline. It returns nil when the timestamp is absent or -// unparseable so the surface stays best-effort. +// first-thought deadline. When the timestamp is absent or unparseable, it uses +// now() as the creation time. It returns nil only when now is also nil. func newFirstThoughtDeadline(createdAt string, now func() time.Time) *FirstThoughtDeadline { created, ok := parseLinearTime(createdAt) if !ok { diff --git a/adapters/slack/doc.go b/adapters/slack/doc.go index 64490ff..f9bf24a 100644 --- a/adapters/slack/doc.go +++ b/adapters/slack/doc.go @@ -79,8 +79,10 @@ // 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. +// context deadline. The deadline check and MaxElapsed bound only the sleep +// between attempts, not the HTTP attempts. Retry under the default DispatchSync +// stays inside Slack's 3-second ack window only when the request context or +// Options.Client bounds those attempts. // // The RetryPolicy is per-adapter platform config in Options, never Runtime // Options. RetryPolicy and RateLimited are type aliases of slackapi.RetryPolicy diff --git a/doc.go b/doc.go index 8695cbf..1f19f68 100644 --- a/doc.go +++ b/doc.go @@ -77,11 +77,13 @@ // - [DispatchSync], the default, runs the handler on the webhook request // context and acknowledges after it returns. Use it for fast handlers. // - [DispatchDeferred] (ack-then-work) runs dedupe before the -// acknowledgement, then runs the handler on a detached context bounded by -// [RuntimeOptions].DetachTimeout, which must be positive. Under every -// strategy except [ConcurrencyConcurrent], which takes no lock, the -// runtime renews the thread lock while the handler runs; if the lease is -// lost, it cancels the handler's context with [ErrPreempted] as the cause. +// acknowledgement, then starts the handler on a detached context bounded +// by [RuntimeOptions].DetachTimeout, which must be positive, and returns +// to the adapter without waiting for it. The acknowledgement and the +// handler run at the same time. Under every strategy except +// [ConcurrencyConcurrent], which takes no lock, the runtime renews the +// thread lock while the handler runs; if the lease is lost, it cancels +// the handler's context with [ErrPreempted] as the cause. // // In both modes an accepted event is acknowledged even when its handler // returns an error; the error is logged and observed, not retried. diff --git a/internal/ratelimit/ratelimit.go b/internal/ratelimit/ratelimit.go index 9c5e469..0a6dfe7 100644 --- a/internal/ratelimit/ratelimit.go +++ b/internal/ratelimit/ratelimit.go @@ -53,8 +53,8 @@ func BackoffDelay(base, maxDelay time.Duration, attempt int, retryAfter time.Dur } // SleepCtx sleeps for d but returns the context error if the context ends first, -// so retry never outlasts the caller's context (and thus never blows the platform -// ack window). +// so a backoff sleep never outlasts the caller's context. It does not bound the +// HTTP attempts between sleeps. func SleepCtx(ctx context.Context, d time.Duration) error { if d <= 0 { return ctx.Err() From 9a8af9b447347506d73095917c28f494d876f8be Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 29 Oct 2026 20:49:45 +0000 Subject: [PATCH 3/5] docs(readme): cite Chat.prelude and Chat.queuePollInterval Chat.prelude routes commands and interactions. Chat.queuePollInterval holds the ThreadLockTTL/20 value and its clamp. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f8f6e4..33cae0f 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ works through these steps [runtime.go:Chat.dispatch, runtime.go:Chat.prelude]: 6. **Route.** Commands go to `OnCommand` and interactions to `OnInteraction`. A message goes to `OnSubscribedMessage` when its thread is subscribed, else to `OnNewMention` when it is a mention or a direct message - [runtime.go:Chat.route]. + [runtime.go:Chat.prelude, runtime.go:Chat.route]. 7. **Mark.** When the prelude knows the outcome (dropped on a lock conflict, ignored, or routed), `State.MarkEvent` records the event ID for `DedupeTTL`. A duplicate stops here [runtime.go:Chat.prelude, @@ -301,7 +301,7 @@ Invariants: not cancelled [runtime.go:Chat.dispatchSync]. - Lock waiters poll `State.AcquireLock` every `ThreadLockTTL/20`, clamped to 1ms–1s, because `State` does not signal a release - [runtime.go:Chat.pollForLock]. + [runtime.go:Chat.pollForLock, runtime.go:Chat.queuePollInterval]. - `Shutdown` closes admission, cancels deferred work, waits for it to drain, stops listeners and waits for them, shuts down the adapters, and then shuts down `State`. `ctx` bounds the two waits. If `ctx` ends first, From 505ceb5561395cb5b2d2bec1ff374aef6408bd44 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 29 Oct 2026 20:52:22 +0000 Subject: [PATCH 4/5] docs: say the deferred handler can run during or after the ack Queue, debounce, and burst make the handler wait, so the ack and the handler do not always run at the same time. Name the caller's context the same way in the Slack retry paragraph, and drop the SleepCtx sentence that described its callers. --- adapters/slack/doc.go | 4 ++-- doc.go | 8 ++++---- internal/ratelimit/ratelimit.go | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/adapters/slack/doc.go b/adapters/slack/doc.go index f9bf24a..7f21c68 100644 --- a/adapters/slack/doc.go +++ b/adapters/slack/doc.go @@ -81,8 +81,8 @@ // invariant is that in-line synchronous retry never sleeps past the caller's // context deadline. The deadline check and MaxElapsed bound only the sleep // between attempts, not the HTTP attempts. Retry under the default DispatchSync -// stays inside Slack's 3-second ack window only when the request context or -// Options.Client bounds those attempts. +// stays inside Slack's 3-second ack window only when the caller's context +// deadline or an Options.Client timeout also limits those attempts. // // The RetryPolicy is per-adapter platform config in Options, never Runtime // Options. RetryPolicy and RateLimited are type aliases of slackapi.RetryPolicy diff --git a/doc.go b/doc.go index 1f19f68..bcf48b6 100644 --- a/doc.go +++ b/doc.go @@ -77,10 +77,10 @@ // - [DispatchSync], the default, runs the handler on the webhook request // context and acknowledges after it returns. Use it for fast handlers. // - [DispatchDeferred] (ack-then-work) runs dedupe before the -// acknowledgement, then starts the handler on a detached context bounded -// by [RuntimeOptions].DetachTimeout, which must be positive, and returns -// to the adapter without waiting for it. The acknowledgement and the -// handler run at the same time. Under every strategy except +// acknowledgement and returns to the adapter without waiting for the +// handler. The handler runs on a detached context bounded by +// [RuntimeOptions].DetachTimeout, which must be positive, and can act +// before the acknowledgement is written. Under every strategy except // [ConcurrencyConcurrent], which takes no lock, the runtime renews the // thread lock while the handler runs; if the lease is lost, it cancels // the handler's context with [ErrPreempted] as the cause. diff --git a/internal/ratelimit/ratelimit.go b/internal/ratelimit/ratelimit.go index 0a6dfe7..315172f 100644 --- a/internal/ratelimit/ratelimit.go +++ b/internal/ratelimit/ratelimit.go @@ -53,8 +53,7 @@ func BackoffDelay(base, maxDelay time.Duration, attempt int, retryAfter time.Dur } // SleepCtx sleeps for d but returns the context error if the context ends first, -// so a backoff sleep never outlasts the caller's context. It does not bound the -// HTTP attempts between sleeps. +// so a backoff sleep never outlasts the caller's context. func SleepCtx(ctx context.Context, d time.Duration) error { if d <= 0 { return ctx.Err() From 2d9a60c80d4b85cf1a58786528452b0733b29096 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 29 Oct 2026 21:02:53 +0000 Subject: [PATCH 5/5] docs: drop remaining ack-window and after-ack handler claims The retry deadline check and MaxElapsed bound only the backoff sleep, and DispatchDeferred acknowledges without waiting for the handler. Correct the copies of the old claims in CONTEXT.md, docs/, the Slack retry comments, the ratelimit README, and the Linear example. --- CONTEXT.md | 8 ++++---- adapters/slack/slack.go | 6 +++--- adapters/slack/slackapi/retry.go | 16 +++++++++------- docs/how-to/deferred-dispatch.md | 4 ++-- docs/reference.md | 12 +++++++----- examples/linear-agent-hello-world/main.go | 4 ++-- internal/ratelimit/README.md | 2 +- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index d38e663..ec02230 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -191,15 +191,15 @@ The `context.Context` used for runtime dispatch and handler execution. _Avoid_: Background work context **Dispatch Mode**: -Whether the routed handler runs before or after the adapter acknowledges the platform: synchronous by default, or deferred so the platform is acknowledged first. +Whether the adapter acknowledges the platform after the routed handler returns or without waiting for it: synchronous by default, or deferred. _Avoid_: Concurrency Strategy, async flag **Ack-Then-Work**: -The deferred-dispatch contract where the adapter acknowledges within the platform deadline after the synchronous prelude, then the handler runs on the **Detached Work Context**. +The deferred-dispatch contract where the adapter acknowledges within the platform deadline after the synchronous prelude, without waiting for the handler, which runs on the **Detached Work Context**. _Avoid_: Synchronous handler, fire-and-forget goroutine **Detached Work Context**: -A runtime-derived context that outlives the inbound request or **Delivery** so a deferred handler keeps working after acknowledgement; bounded by a detach timeout and cancelled by **Runtime Shutdown**. +A runtime-derived context that outlives the inbound request or **Delivery** so a deferred handler keeps working after the adapter acknowledges; bounded by a detach timeout and cancelled by **Runtime Shutdown**. _Avoid_: Request context, context.Background() **Admission Bound**: @@ -450,7 +450,7 @@ _Avoid_: Full platform schema, strict external SDK model - A **Lock Conflict** is acknowledged to the platform by default and recorded as unhandled runtime contention. - An adapter accepts and normalizes a **Delivery**, but **Runtime Dispatch** decides which handler runs. - The **Linear App-Actor Slice** preserves synchronous **Runtime Dispatch** for MVP; long-running Linear agent work should post an early **Agent Activity Thought** and enqueue follow-up work in application code. -- **Runtime Dispatch** defaults to synchronous (**Dispatch Mode** sync); a deferred **Dispatch Mode** (**Ack-Then-Work**) is available opt-in and runs the handler on the **Detached Work Context** after acknowledgement. +- **Runtime Dispatch** defaults to synchronous (**Dispatch Mode** sync); a deferred **Dispatch Mode** (**Ack-Then-Work**) is available opt-in, runs the handler on the **Detached Work Context**, and acknowledges without waiting for the handler. - The synchronous **Dispatch Context** comes from the inbound webhook request, or from the handler context of a **Delivery** that a **Listener** receives; under deferred **Dispatch Mode** the **Thread Lock** is held across the detached work and refreshed before its lease expires. - Runtime locks must be released when **Dispatch Context** is cancelled or handler execution exits. - **Runtime State** mutations performed during synchronous dispatch respect the caller's **Dispatch Context** and fail when it is cancelled. diff --git a/adapters/slack/slack.go b/adapters/slack/slack.go index 6d40d5d..2edde91 100644 --- a/adapters/slack/slack.go +++ b/adapters/slack/slack.go @@ -48,9 +48,9 @@ type Options struct { // it is adapter-owned wiring, not on the core Adapter interface. Nil is a no-op. Observer chat.Observer // RetryPolicy bounds outbound Slack rate-limit retry/backoff (ADR 0005). The - // zero value applies a conservative default that stays well under Slack's - // 3-second ack window; MaxAttempts: 1 disables retry. It is per-adapter - // platform config, never Runtime Options. + // zero value applies a conservative default that caps the total backoff sleep + // at 2 seconds but does not bound the HTTP attempts; MaxAttempts: 1 disables + // retry. It is per-adapter platform config, never Runtime Options. RetryPolicy RetryPolicy } diff --git a/adapters/slack/slackapi/retry.go b/adapters/slack/slackapi/retry.go index 8f77c98..37c4ecf 100644 --- a/adapters/slack/slackapi/retry.go +++ b/adapters/slack/slackapi/retry.go @@ -16,13 +16,15 @@ import ( // RetryPolicy bounds outbound Slack rate-limit retry/backoff (ADR 0005). Retry is // bounded by attempt count and cumulative elapsed backoff, honors Slack's -// Retry-After header, and never sleeps past the caller's context deadline so -// in-line synchronous retry cannot outlive the platform ack window (Slack's -// 3-second budget). The zero value applies a conservative default; MaxAttempts: 1 -// disables retry for callers that want raw single-shot behavior. A Retry-After -// longer than MaxDelay is capped at MaxDelay, so the retry comes earlier than -// Slack asked. Callers that must honor long Retry-After values should raise -// MaxDelay and MaxElapsed. +// Retry-After header, and never sleeps past the caller's context deadline. The +// deadline check and MaxElapsed bound only the sleep between attempts, not the +// HTTP attempts, so in-line synchronous retry stays inside Slack's 3-second ack +// window only when the caller's context deadline or an Options.HTTPClient +// timeout also limits those attempts. The zero value applies a conservative +// default; MaxAttempts: 1 disables retry for callers that want raw single-shot +// behavior. A Retry-After longer than MaxDelay is capped at MaxDelay, so the +// retry comes earlier than Slack asked. Callers that must honor long +// Retry-After values should raise MaxDelay and MaxElapsed. type RetryPolicy struct { // MaxAttempts is the total number of attempts including the first. Zero applies // a conservative default; 1 disables retry. diff --git a/docs/how-to/deferred-dispatch.md b/docs/how-to/deferred-dispatch.md index 8440627..297e548 100644 --- a/docs/how-to/deferred-dispatch.md +++ b/docs/how-to/deferred-dispatch.md @@ -31,7 +31,7 @@ Start from `chat.DefaultRuntimeOptions()` so you keep the required | Option | Default | What it does | | --- | --- | --- | | `Dispatch` | `DispatchSync` | `DispatchDeferred` turns on ack-then-work. | -| `DetachTimeout` | `0` | How long a handler may run after the acknowledgement. Required under deferred dispatch: `chat.New` fails while it is zero. | +| `DetachTimeout` | `0` | The timeout of the detached context that each deferred handler runs on. Required under deferred dispatch: `chat.New` fails while it is zero. | | `Concurrency` | `ConcurrencyDrop` | What happens to an event that arrives while a handler holds the thread lock. See [Pick a concurrency strategy](#pick-a-concurrency-strategy). | | `MaxDetached` | `1024` | The cap on deferred work in flight. Must be positive under deferred dispatch. See [Handle overload](#handle-overload). | @@ -77,7 +77,7 @@ Follow three rules: - **Use the `ctx` you are given for every call.** It carries the `DetachTimeout` deadline and the runtime's cancellation signal. -- **Expect no platform retry.** An error returned after the acknowledgement +- **Expect no platform retry.** An error that a deferred handler returns is logged and observed, but the platform never redelivers the event. If the work must not be lost, make it idempotent and put it on your own queue. - **Expect shutdown to abort work.** `Shutdown(ctx)` cancels the detached diff --git a/docs/reference.md b/docs/reference.md index 90f2f7a..8ca88f5 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -316,9 +316,10 @@ inbound webhook request context, or on the delivery context of a listener, 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` (which must be -positive under deferred dispatch). Under deferred dispatch `MaxDetached` +before the acknowledgement, and the adapter acknowledges without waiting for +the handler, which runs on a detached work context 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)). After @@ -525,8 +526,9 @@ if ok { issue-comment threads from the root comment and its replies, with the same newest-first ordering and `Before` cursor semantics, clamped to Linear's maximum page size. -- Long fetches belong after the acknowledgement, under deferred dispatch; the - runtime never fetches history on the inbound request path. +- Long fetches belong in a deferred handler, which does not delay the + acknowledgement; the runtime never fetches history on the inbound request + path. This deliberately diverges from Vercel Chat SDK's end-to-end stored-history model: persistence of conversation content stays an application concern. diff --git a/examples/linear-agent-hello-world/main.go b/examples/linear-agent-hello-world/main.go index 1f1de60..bb0d81b 100644 --- a/examples/linear-agent-hello-world/main.go +++ b/examples/linear-agent-hello-world/main.go @@ -141,8 +141,8 @@ func newMentionHandler(linearAccess linearAgentAccess, pending *pendingSelection } // Ack-Then-Work: post a first thought inside the ~10s first-thought window, - // then do the rest. Under DispatchDeferred this whole handler already runs on - // the Detached Work Context after the webhook is acknowledged. + // then do the rest. Under DispatchDeferred this whole handler runs on the + // Detached Work Context, and the webhook is acknowledged without waiting for it. _, _ = linearAccess.PostThought(ctx, ev.Thread.ID(), "Thinking...") // Show native tool-call progress as an action. diff --git a/internal/ratelimit/README.md b/internal/ratelimit/README.md index bbc4547..193587f 100644 --- a/internal/ratelimit/README.md +++ b/internal/ratelimit/README.md @@ -34,7 +34,7 @@ These packages import it: - A shifted delay that is non-positive (shift overflow) or above `maxDelay` becomes `maxDelay`. `[ratelimit.go:BackoffDelay]` - The result is `min(max(delay, retryAfter), maxDelay)`. A platform `retryAfter` can raise the delay but never above `maxDelay`. `[ratelimit.go:BackoffDelay]` - `d <= 0` returns `ctx.Err()` at once without waiting. `[ratelimit.go:SleepCtx]` -- The sleep never outlasts the caller's context. The doc comment says this keeps retry inside the platform ack window. That holds only when the caller's context deadline fits that window. `[ratelimit.go:SleepCtx]` +- The sleep never outlasts the caller's context. `[ratelimit.go:SleepCtx]` ## Side Effects