feat(linear): v1.1 polish — pre-container feedback, state-on-start, sweep, nits#87
feat(linear): v1.1 polish — pre-container feedback, state-on-start, sweep, nits#87isadeks wants to merge 8 commits into
Conversation
Closes the silent-drop UX gap that appeared whenever a Linear-triggered task
was rejected before the agent container started — the user would apply the
trigger label, see nothing happen, and have no way to know why. Reactions
and progress comments are emitted by the agent container; nothing fired
until that point, so all upstream rejections were invisible on the Linear
side.
This commit wires a best-effort GraphQL feedback path covering all six
distinct rejection points:
In `linear-webhook-processor.ts` (pre-`createTaskCore`):
1. Issue has no projectId → "isn't in a project" comment
2. Project not onboarded / removed → "isn't onboarded; admin can run
`bgagent linear onboard-project`" comment
3. Webhook missing organization or actor → diagnostic comment
4. Linear actor has no linked platform user → "v1 only the API-token
owner can submit; multi-user OAuth is on the v3 roadmap" comment
5. `createTaskCore` returns non-201 → message branched on status:
guardrail/validation block surfaces the user-facing error string;
503 prompts the user to re-apply the label; other 4xx/5xx falls
through to a generic message.
In `orchestrate-task.ts` (post-201, in admission control):
6. User concurrency cap rejection → "concurrency limit; wait for one
to finish, then re-apply the label" comment.
All five processor paths and the orchestrator path call a shared helper,
`reportIssueFailure(secretArn, issueId, message)`, that runs the comment
and ❌ reaction in parallel via `Promise.allSettled`. The helper:
- Reuses the existing 5-minute `getLinearSecret` cache from
`linear-verify.ts` (no extra Secrets Manager hits on warm Lambdas).
- Swallows network, auth, and GraphQL errors with WARN logs — Linear
feedback is advisory and must never gate the rejection path.
- Posts to Linear's hosted GraphQL endpoint; mutation shapes match
`agent/src/linear_reactions.py` (`commentCreate`, `reactionCreate`).
CDK plumbing:
- `linear-integration.ts` — wires `LINEAR_API_TOKEN_SECRET_ARN` into
the webhook processor and grants read on the existing
`LinearIntegration.apiTokenSecret`.
- `agent.ts` — grants the same secret to `orchestrator.fn` and
populates the env var. The grant is unconditional; the orchestrator
only invokes the helper when `task.channel_source === 'linear'`.
The non-Linear case is a hard no-op at the call site — `notifyLinear-
OnConcurrencyCap` early-returns on `channel_source !== 'linear'`, and the
processor only handles Linear payloads. Slack/API/webhook tasks are
unaffected.
Tests (28 new; 1240 → 1268, all green):
- `cdk/test/handlers/shared/linear-feedback.test.ts` (13 tests):
mutation shape, auth header, error swallowing in 4 distinct failure
modes (secret-resolution null, non-2xx, GraphQL `errors`, network
throw), `Promise.allSettled` partial-success semantics.
- `cdk/test/handlers/linear-webhook-processor.test.ts` (10 new tests
in a `user-visible feedback` describe block): one assertion per
rejection path + happy-path-doesn't-fire + filter-rejection-doesn't-
fire (the latter is intentional UX — the processor sees many events
that aren't tasks, and dropping a comment on each would be noisy).
- `cdk/test/handlers/orchestrate-task-feedback.test.ts` (5 tests):
new file; covers `notifyLinearOnConcurrencyCap` directly with
`withDurableExecution` mocked. Asserts the linear path fires; the
api/webhook/slack paths no-op; missing metadata, missing env, and
undefined `channel_metadata` all no-op cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nits Wraps the v1.1 polish theme from PR aws-samples#87. Five small additions, all agent-side or docs: State-on-start (the user-visible one): - prompt_builder._channel_prompt_addendum now instructs the agent to transition the originating Linear issue to `In Progress` (or `Todo` fallback) at agent-start, mirroring the existing `In Review` chain fired at PR-open. Closes the gap where the issue stayed at `Backlog` during real agent work — only the 👀 reaction and "🤖 Starting" comment signaled progress, while humans-using-Linear expect the state column to reflect "being worked." Skips if the issue is already in `In Progress` or any later state; doesn't loop on list_issue_statuses. Alain aws-samples#63 review nits (4 small surgical changes): - linear_reactions.py: auth-failure circuit breaker. Track consecutive 401/403s; after 3 strikes, log ERROR once and short-circuit all later _graphql calls (return None) until the container restarts. Resets on any 2xx response. Replaces the prior behaviour where revoked tokens flooded CloudWatch with WARNs and wasted Linear API quota indefinitely. - pipeline.py: declare `linear_eyes_reaction_id: str | None = None` explicitly before the try block instead of relying on `locals().get("linear_eyes_reaction_id")` in the crash handler. Functionally identical; survives refactors and reads cleanly. - config.py::resolve_linear_api_token: narrow `except Exception` to `(BotoCoreError, ClientError)` from botocore.exceptions. Switch `print()` to `shell.log("WARN", ...)` so warnings join the structured log stream the rest of the agent uses. - LINEAR_SETUP_GUIDE.md + cli/src/commands/linear.ts: stop telling users to run `bgagent linear link <code>` when auto-link fails — the code generator is a v3 feature that doesn't ship in v1, so the suggestion was misleading. Replaced with explicit admin-assisted fallback (DynamoDB put-item with steps to find workspaceId, viewerId, Cognito sub) and a clear "this command exists but is non-functional in v1" note. Tests: 532 agent + 1268 cdk + 196 cli, all green. Deployed to backgroundagent-dev. Smoke-tested 👀-on-start (156ms, agent unblocked) in the prior commit; state-on-start smoke is the next manual step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Whitespace-only changes flagged by CI's self-mutation guard. No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review SummaryGreat PR — the architecture is sound, test coverage is thorough (28 new tests), and the "best-effort, never gate the pipeline" philosophy is consistently applied. A few items to address before merge: Must fix1. Thread-safety of circuit breaker globals (
→ Add a 2. If → Wrap in a defensive try-catch: try {
await notifyLinearOnConcurrencyCap(task);
} catch (feedbackErr) {
logger.warn('Linear concurrency-cap feedback failed (non-fatal)', { error: ... });
}Should fix3.
→ Add 4. Daemon thread crash silence ( If → Wrap the thread target in a top-level try-except that logs via 5. Sweep delete counter inaccuracy (
→ Nice to have (non-blocking)
Overall: solid work. The two "must fix" items are small changes that prevent real (if unlikely) failure modes. Happy to re-review once addressed. |
- linear_reactions: guard auth-circuit globals with `_auth_state_lock` so the daemon sweep thread and the main thread can't race the read-modify-write on `_consecutive_auth_failures` / `_auth_circuit_open`. - linear_reactions: wrap the daemon sweep target in `_sweep_stale_reactions_safe` so an unexpected exception logs at ERROR instead of dying silently (stderr from a daemon thread doesn't reliably reach CloudWatch). - linear_reactions: only increment the sweep delete counter when `_graphql(_DELETE_MUTATION, ...)` actually returns a non-None response — previously the summary log overstated success. - config: hoist `import boto3` out of the catch-narrowed try/except so an `ImportError` (boto3 missing from the image) degrades to a WARN log instead of crashing the agent. - orchestrate-task: wrap `notifyLinearOnConcurrencyCap` in a defensive try/catch — durable-execution retries the entire admission-control step on throw, which would re-fire `failTask` + `emitTaskEvent` and produce duplicate events. - tests: 1 new throw-propagation test for `notifyLinearOnConcurrencyCap`, 3 new tests for `resolve_linear_api_token` (cached env, no-arn, ImportError fallback). Auto-reset fixture in `test_linear_reactions.py` now also resets the circuit-breaker globals between tests so future cases don't leak state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- linear_reactions: log a single DEBUG line when the auth circuit breaker short-circuits a call, so the path isn't zero-trace once open. - config: split the `(BotoCoreError, ClientError)` catch so `AccessDeniedException` logs at ERROR instead of WARN — IAM misconfig is persistent and should page someone, not blend into transient warnings. Also drop the personal name from the inline reference to the aws-samples#63 review. - linear-webhook-processor: tighten `buildCreateTaskFailureMessage` param types to `number` / `string` (no `| undefined`) — the only caller passes `APIGatewayProxyResult` fields which are always defined. Removes dead fallback-to-`'unknown'` branches. - test_config: 2 new tests covering the split exception path (AccessDenied → ERROR; ResourceNotFound → WARN). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review @krokoko. All 9 items addressed across two commits — split must/should-fix from nice-to-have so the load-bearing changes are isolated. Must-fix (
|
Summary
Wraps the v1.1 polish theme for the Linear integration, addressing the silent-drop gaps and PR #63 review recommendations in a single PR. Six rejection paths now produce user-visible feedback; the issue state transitions on agent-start (not just at PR-open); a stale-reaction sweep keeps re-runs clean; and four small review nits are addressed.
What changed
1. Pre-container failure feedback (5 paths in processor + 1 in orchestrator)
Linear-triggered tasks that fail before the agent container starts now produce a Linear comment + ❌ reaction within ~20s of label-apply. Previously, six distinct rejection points all silently dropped:
projectIdbgagent linear onboard-project"createTaskCorereturns non-201All six call sites use a shared helper
cdk/src/handlers/shared/linear-feedback.ts—reportIssueFailure(secretArn, issueId, message)posts the comment and reaction in parallel viaPromise.allSettled. Reuses the existing 5-minutegetLinearSecretcache.2. State transition at agent-start
Mirrors the existing
In Reviewtransition that fires at PR-open. The prompt addendum's step 1 now instructs the agent to transitionBacklog/Todo→In Progressbefore doing repo work, so Linear's state column reflects "being worked" during the minutes between label-apply and PR-open. Falls back toTodoifIn Progressdoesn't exist; skips if the issue is already inIn Progressor any later state. Doesn't loop onlist_issue_statuses.3. Stale-reaction sweep
Re-runs (label removed and re-applied; or pre-container ❌ followed by a successful retry) used to leave prior 👀/✅/❌ accumulated next to the new run's reaction. Added
_sweep_stale_reactions(issue_id)toagent/src/linear_reactions.py:exclude_id=rid).Smoke-tested on
backgroundagent-dev:react_task_startedtotal = 156ms; sweep done in background at +303ms; agent started at +3ms afterreact_task_startedexit.4. PR #63 review nits (4)
linear_reactions.py— track consecutive 401/403s; after 3 strikes, ERROR once and short-circuit until container restart. Replaces the prior behaviour where revoked tokens flooded CloudWatch with WARNs forever.linear_eyes_reaction_id: str | None = Noneinit inpipeline.pyinstead oflocals().get(...)in the crash handler.except Exceptioninresolve_linear_api_tokento(BotoCoreError, ClientError). Switchprint()→shell.log("WARN", ...).bgagent linear setupauto-link failures. Replaces misleading "runbgagent linear link <code>" suggestion (the command is non-functional in v1) with explicit DynamoDB put-item steps and a clear v3-OAuth note.Plumbing
linear-integration.ts— wiresLINEAR_API_TOKEN_SECRET_ARNinto the webhook processor + grants read onLinearIntegration.apiTokenSecret.agent.ts— same wiring fororchestrator.fn(declared afterLinearIntegrationso done in the stack rather than as a prop).LinearIntegrationWebhookProcessorFnandTaskOrchestratorOrchestratorFnboth carry the env var and IAM policy.Test plan
Unit tests — 1240 baseline → 1268 CDK, 526 → 532 agent, 196 CLI, all green:
cdk/test/handlers/shared/linear-feedback.test.ts(new, 13 tests) — mutation shape, auth header, error swallowing in 4 distinct failure modes,Promise.allSettledpartial-success semantics.cdk/test/handlers/linear-webhook-processor.test.ts— extended with auser-visible feedbackdescribe block, 10 new tests: one assertion per rejection path + happy-path-doesn't-fire + filter-rejection-doesn't-fire (latter is intentional UX).cdk/test/handlers/orchestrate-task-feedback.test.ts(new, 5 tests) — coversnotifyLinearOnConcurrencyCapwithwithDurableExecutionmocked.agent/tests/test_linear_reactions.py— extended with 5 sweep tests covering scoping rules (viewer-owned bgagent emojis only; preserves human reactions even with same emoji; preserves bot reactions of other emojis; sweep failure doesn't block 👀; viewer-id cached across calls).Lint + synth + agent quality: clean.
Smoke tested on backgroundagent-dev:
eyes-visible at +156msconfirmed via runtime container logsSmoke tests not run for paths 3 (defensive, no natural reproducer) and 5b (503, no natural reproducer) — covered by unit tests. Path 6 (concurrency cap) requires 4 simultaneous tasks to reproduce; covered by unit tests.
Reviewer notes
linear_*_msg_tsfields on TaskRecord — all per-issue state stays in Linear, accessed at runtime.TaskEventsTable2-reader-per-shard limit (closed by feat(fanout): migrate SlackNotifyFn to FanOutConsumer subscriber (#64) #79 anyway).