Skip to content

feat(linear): run native agent sessions through the shared dispatcher - #1625

Draft
matanyall wants to merge 46 commits into
mainfrom
codex/linear-agent
Draft

matanyall wants to merge 46 commits into
mainfrom
codex/linear-agent

Conversation

@matanyall

@matanyall matanyall commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Delegations and mentions can enter Switchboard's shared dispatcher through native Linear agent sessions, with edge acknowledgements, follow-ups, progress, clarification, replies, private file uploads and incoming files staged into workspaces, issue actions, a delegated queue, authorized stop controls and durable recovery. This remains a draft under #1624: project/document session origins are implemented, while native child sessions remain issue-only and installation and end-to-end acceptance still need live proof.

Tour

  1. Behavior and proof and the full acceptance ledger.
  2. OAuth and rotating credentials, signed intake, SQLite delivery leases and the authenticated bridge.
  3. Session identity, native conversation output, consumer, recovery and lifecycle cancellation.
  4. Private inbound files and shared file classification. New prompts load files before execution begins; history restores distinct files newest first, including the initiating comment.
  5. Worker boundary, startup and resume wiring, and operator setup.

Decisions

  • Hard Stop now reaches admitted incoming-file copies and workspace pulls. The Worker enables incoming request signals and explicitly forwards the signal to its Durable Object. A direct workerd/R2 test aborted an 8 MiB transfer and verified no completed object after the transfer would otherwise have finished. Wrangler’s development proxy drops client disconnects, so stopping the bot through that proxy can still leave an unused edge copy; this is an explicit local acceptance gap. Stop now durably cancels earlier unbegun requests, including inline file hydration, before looking up active runs. Shared stop policy limits this to the requester or an operator’s authorized channel; late file responses cannot pass the invalidated admission lease. A Stop racing an unbound dispatch is also stored on the delivery; if dispatch then defers without effects, the queue completes the cancelled request instead of making it runnable again. Run registration now awaits the channel’s durable delivery binding before workspace staging, executor attach, command execution or coordinator creation. A stopped unbound delivery refuses that binding; unavailable admission stops the local run. Child and coordinator wrappers preserve the admission promise. Settled cancellation completes only the original delivery lease, preventing an unnecessary uncertain-recovery reply without consuming another worker’s claim.

  • A child question is an unfinished task. The run tools expose awaiting_input, return the untrusted question immediately, and follow the same child thread after a human reply. Conductors ask for the missing information and link to the child session. Automatic child-to-parent notifications retain the channel's requester-isolation rule. Coordinator record reads now withhold earlier PR/review artifacts for a question, and the workflow persists an input-wait phase with durable sleeps. Human replies now retain the original coding/review preset, requester and credential, unit branch or PR, round key and base, and remaining wall-clock budget. Fresh permission checks precede contract reconstruction. The coordinator follows the continuation run for later briefs and includes earlier question costs; missing prices remain unknown. Unavailable coordinator state defers before admission; unavailable continuation history retries. Stopping a waiting question now persists through the shared run store before native completion, survives late record writes and restart, and prevents a coordinator from acting on old PR/review artifacts. Waiting consumes the unit wall-clock budget and ends at its deadline. Coordinator point reads require the durable terminal record, retrying missing or unavailable history instead of losing a question or stop marker. Live acceptance remains open. The integration follows main’s shared coding/review unit thread, retaining legacy review-thread compatibility, typed refusal records, model-card resolution and the latest harness end-of-run recovery. Linear acknowledgements remain nonterminal.

  • Child work opens a native session on an app-owned root comment on the same issue. The edge records a durable creation intent, rechecks the human requester and reconciles uncertain session mutations by observation; the native creation webhook does not dispatch a second run. Coordinator thread opens carry stable keys and check channel access on rebuilt handles. The shared dispatcher remains responsible for child authorization and execution. The design and failure boundary are recorded in decision 0061.

  • Local development can separate the edge origin (LINEAR_BRIDGE_URL) from the bot’s run-page origin (PUBLIC_BASE_URL); combined installations retain the public-origin fallback. Origins fail fast on unsafe transport or URL credentials. Session links allow HTTPS and HTTP loopback. OAuth still derives its callback from the edge’s own public origin.

  • Private file reads recheck the requesting human and match URLs against current session/issue context at the edge. Downloads use only uploads.linear.app, reject redirects, preserve credential-file exclusions and enforce streamed byte/count limits. OAuth tokens stay at the edge. Large and binary files now use the channel copy capability and the shared workspace staging path. The edge rechecks human access and current session context, binds the object key to the session and streams through a fixed-length pipe into R2; credentials and bytes stay out of the bot. Size, count and configured per-message budgets apply, and unknown lengths remain explicitly unread. The bridge allows a batch deadline while preserving individual request deadlines.

  • request_input records a typed question for the end of the turn. It persists across restart, skips automatic PR/review publication and memory reflection, shuts down the model process, and delivers a native elicitation. The completed turn carries awaitingInput; it does not complete the Linear session. Final verdict, description and re-review turns can also ask; their questions suppress further automatic turns and publication. Stops and failures override a question. An opening elicitation retains issue context even when delegation produced no user activity.

  • Commands, queued prompts and restored runs check the requester’s current Linear team access before history or model work. Denied restored rows close interrupted; temporary access lookup failures leave queued/reclaimed work available for retry. Project and document sessions resolve visibility from their current origin: project team membership, or the document’s issue/team/project owner. Project team pagination, guest access and restricted parent teams are checked against fresh facts. Permission contractions recheck active non-issue sessions. Unsupported origins are explicitly refused, while Stop remains available for previously active work. Context references do not grant access, and a source comment contributes text only when it belongs to the same origin. An unread follow-up handed on after a failed run reports a lookup failure and asks for resend.

  • OAuth credentials stay in the edge Durable Object. The container receives only an internal bridge bearer; each API operation checks current session access and app ownership.

  • Created sessions receive a durable, idempotent native acknowledgement at the edge before dispatch, independent of container startup.

  • Issue tools bind the resolved requester outside model arguments. The edge refreshes Linear membership, respects guest public-team access and inherited restricted-team access, then asks the shared policy table. Updates preserve assignee/delegate; child creation never silently starts another agent run. A different person’s prompt waits for the active session run to finish and is then dispatched with that person’s own authority. Unknown owners are also deferred; the original requester can still steer their active run.

  • Native outbound files use private Linear storage and single-file signed PUT tickets. The shared upload seam preserves Slack’s default POST and keeps OAuth tokens out of the executor.

  • Signed event identities and SQLite leases survive container replacement. Entry into dispatch is recorded before execution, including commands without run records. Recovery looks for durable request/inbox/history evidence; uncertain effects produce an interruption error instead of blind repetition.

  • Follow-ups wait for the prior turn’s admission; only the same requester may steer the running agent. Fenced deferral preserves prompt order, while Stop can bypass an unstarted prompt. Different sessions run concurrently. Native Stop permits self-cancellation through the shared policy without operator rights and can close an authorized current waiting question; denied or stale Stop events leave the session unchanged; stopping another person’s work needs run-write permission and visibility; signed lifecycle events can withdraw access and cancel work without granting authority.

  • A delayed revocation cannot delete a newer installation. HTTP webhook acceptance uses Linear's required 200. A separate localhost Worker entry permits OAuth and intake testing without Docker. Linear-only startup no longer requires Slack credentials; partial Slack configuration still fails fast.

Validation

  • Project/document origins: regression tests cover context and channel identity, current visibility, paginated project teams, document ownership, source-comment isolation, file authorization, permission contraction, unsupported origins and Stop. All six adapter GraphQL queries validate against the current official Linear SDK schema. These tests do not establish that native Linear mention events arrive in the expected shape; that requires the live installation.

  • Admission cancellation: new tests reproduce Stop arriving after dispatch entry but before run registration. They cover both explicit Stop and admission transport failure before agent file copies, executor attachment, commands and coordinator creation; stopped request outcomes and durable command/ship statuses; and completion fencing after another consumer acquires the lease.

  • The current head passes the full root gate: 10,786 tests in 551 files, typecheck, lint, formatting, consistency and compiled-output checks; 4,912 proof references resolve. The bot Worker gate passes 43 tests. Current remote CI passes all active jobs on the pushed head.

  • Queued Stop: 156 focused Linear tests pass, including consumer/policy integration, migration of an existing queue, and cancellation surviving a deferral, retry and a new lease. A real local Worker using SQLite and the authenticated bridge cancelled a claimed request, refused its late begin/retry, retained another requester and a newer request, and retained the cancellation across code reload. A second runtime check migrated the existing SQLite queue, recorded Stop while dispatch was begun but unbound, reloaded the Worker, rejected a stale deferral and completed the cancelled request on the valid deferral with no replay. These use fixture events and do not establish a live Linear round trip.

  • File staging: 844 focused Linear, dispatcher, coordinator, harness, artifact-key and staging tests passed; subsequent targeted tests also cover cancelling a download when storage fails early. A real local Worker streamed an 8 MiB archive through FixedLengthStream into local R2, refused an inactive requester (403), rejected a truncated body (503), and left no object for the failed copy. The private receipt records expected metadata and stored size. These use fixture upstream responses; they do not establish a live Linear installation.

  • Durable waiting cancellation passed 254 focused tests and the full state Worker suite: 148 tests in 9 files. A real Worker route test verifies atomic cancellation, stale-stop rejection, idempotence and preservation against late writes; file-store tests also cover a record/index interruption.

  • Coordinator regressions cover question deadlines after state restoration, finishing without another child or merge, and retrying unavailable durable history instead of exposing earlier PR artifacts.

  • Native child creation and inline private-file reads also ran in real local Workers with fixture upstream responses, including lost responses, restart recovery, access refusal and bridge authentication.

  • Local services are restored and the saved run page returns HTTP 200. Earlier local bot builds completed real model requests. The OAuth edge returns 503 until the app credentials are loaded; the temporary webhook tunnel needs replacing.

  • No real Linear OAuth installation, Linear end-to-end round trip, live Slack compatibility smoke, or production deployment has occurred. The acceptance ledger remains open and the PR remains draft.

Comment thread src/config.test.ts Fixed
@matanyall matanyall changed the title feat(linear): persist app installations and incoming agent events feat(linear): run native agent sessions through the shared dispatcher Sep 17, 2026

@coreplane-switchboard coreplane-switchboard Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: The Linear consumer is not drained with the process, so routine shutdown can turn an accepted request into an unrecoverable interrupted delivery.

Warning

Changes requested · head b0a3c0b · 1 finding: 1 major

Severity Finding Where
major F1 Drain active Linear deliveries before process exit src/index.ts:1444
Full review

F1 (major, high confidence): linearConsumer.stop() prevents new claims but does not await active deliveries, and stillHere() excludes linearConsumer.pending. If shutdown lands after inbox.begin() but before dispatch binds a run, the process can exit immediately. The replacement sees delivery.begun, finds no durable run, and reports the request as interrupted instead of executing it. Include active Linear deliveries in the drain accounting or await linearConsumer.settled() before exit, with coverage for shutdown between begin and run binding.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants