From bdc787da9d1330ab6eb38e08de3c9decf46cb1bb Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Tue, 8 Sep 2026 00:56:37 -0700 Subject: [PATCH] Verify Linear page recovery across account and delivery lifetimes --- docs/reference/linear-list-page-recovery.md | 19 ++ src/cli/cli-error.ts | 12 +- src/cli/help.ts | 17 +- src/cli/linear-list-flag-help.ts | 18 ++ src/cli/specs/linear-mcp.ts | 2 +- src/main/linear/client.test.ts | 27 +++ .../linear/linear-account-read-lifetime.ts | 15 +- src/main/linear/linear-sdk.ts | 4 +- .../linear/mcp-issue-list-acquisition.test.ts | 111 +++++++++++ .../linear/mcp-issue-list-lifetime.test.ts | 37 +++- src/main/linear/mcp-issue-list-lifetime.ts | 8 + .../linear/mcp-issue-list-metadata.test.ts | 86 +++++++++ .../linear/mcp-issue-list-page-owned.test.ts | 60 +++++- src/main/linear/mcp-issue-list-pages.ts | 103 ++++------- .../linear/mcp-issue-list-pagination.test.ts | 20 +- src/main/linear/mcp-issue-list-recovery.ts | 17 +- src/main/linear/mcp-issue-list-result.ts | 107 +++++++++++ src/main/linear/mcp-issue-list.test.ts | 4 +- src/main/linear/mcp-issue-list.ts | 1 - .../runtime-rpc-linear-page-owned.test.ts | 174 ++++++++++++++++++ src/main/ssh/linear-list-ssh-delivery.ts | 3 + .../ssh-linear-page-owned-delivery.test.ts | 158 ++++++++++++++++ src/main/ssh/ssh-remote-cli-error-response.ts | 33 ++++ src/main/ssh/ssh-remote-cli-format.ts | 2 + src/main/ssh/ssh-remote-linear-read-help.ts | 6 +- src/main/ssh/ssh-remote-orca-cli.ts | 22 +-- src/shared/fetch-response-body.ts | 8 +- src/shared/linear/list-recovery-format.ts | 22 +++ ...ar-list-page-recovery-callers.unit.test.ts | 134 ++++++++++++++ 29 files changed, 1104 insertions(+), 126 deletions(-) create mode 100644 docs/reference/linear-list-page-recovery.md create mode 100644 src/cli/linear-list-flag-help.ts create mode 100644 src/main/linear/mcp-issue-list-acquisition.test.ts create mode 100644 src/main/linear/mcp-issue-list-metadata.test.ts create mode 100644 src/main/linear/mcp-issue-list-result.ts create mode 100644 src/main/runtime/runtime-rpc-linear-page-owned.test.ts create mode 100644 src/main/ssh/ssh-linear-page-owned-delivery.test.ts create mode 100644 src/shared/linear/list-recovery-format.ts create mode 100644 tests/e2e/linear-list-page-recovery-callers.unit.test.ts diff --git a/docs/reference/linear-list-page-recovery.md b/docs/reference/linear-list-page-recovery.md new file mode 100644 index 00000000000..1ba8588509e --- /dev/null +++ b/docs/reference/linear-list-page-recovery.md @@ -0,0 +1,19 @@ +# Linear issue listing and recovery + +`orca linear list-issues` keeps the full existing issue projection and bounds each provider page before parsing. It retains one result and admits complete provider pages; a rejected page retries a smaller `first` at the same cursor. Descriptions are not clipped or replaced with references. An omitted limit still walks ordinary small results across pages; the public explicit limit remains 1–250. + +Concrete workspace calls use the existing `meta.nextCursor` / `--cursor` continuation. On capable runtimes, the CLI negotiates `linear.status.mcpListPageRecoveryVersion: 1` for `--workspace all` and returns `meta.pageRecovery.continuation`. Resume the same query with `--workspace all --page-recovery `. The row limit may change. A recovery vector cannot be combined with a concrete workspace or `--cursor`. + +An all-workspace result is a **sorted admitted batch**, not a globally ordered prefix. Each successful page or diagnosed workspace failure rotates scheduling to the next workspace. A failed or unvisited workspace prevents completion. Check `meta.hasMore`, `meta.partial`, and workspace errors; recovery can also appear in a top-level error's `data.pageRecovery`. Both JSON and human CLI output expose it. + +Old hosts receive no new parameter when their status does not advertise support. Their producer does not gain these bounds. On new hosts, old concrete callers retain v1 continuation. Old all-workspace callers retain complete small sorted results; **incomplete all-workspace calls now fail explicitly** with `linear_list_concrete_workspace_required`, including a limit 1 query matching two small rows. Restart the original query separately for each connected workspace and reconcile by `(workspace,id)`. This can change scripts' exit status and removes the former clipped preview. + +The provider body is limited to 4 MiB decoded transport bytes,250,000 structure tokens and depth 32. Invalid transport UTF-8 is rejected. Full mapped rows contribute at most 896 KiB, reserving room within a 1 MiB direct compact/pretty response for recovery and diagnostics. The complete SSH wrapper is bounded to 2,129,920 bytes before existing E2EE/frame limits. Metadata growth and final serialization are commit gates; a final-envelope failure returns the invocation's input position, since advancing over undelivered rows would lose data. + +A valid successful first 1 record exceeding the empty allowance yields `linear_list_record_too_large`. HTTP 200 body/structure overflow yields `linear_list_acquisition_too_large`; it cannot prove that an issue, rather than an unparsed GraphQL error, is oversized. Known HTTP statuses retain auth/permission/rate/provider classification. No arbitrary-size detail escape or automatic skipping is promised. + +Each listing reserves 1.125 MiB against a 32 MiB method allowance:28 accepted owners, with the 29th rejected immediately. Each listing has at most one provider page pending or active, sharing the existing four-request limiter. Queued aborts cannot later acquire a slot. Caller timeout can precede actual read/cancel settlement; provider slots and cleanup debt remain charged until settlement, and unsent results remain charged through delivery. The byte accounting includes bounded additional parsing/serialization copies; it is not an RSS ceiling. Four stuck provider slots can exhaust availability. + +Every account upsert, including test/reconnect, invalidates in-flight account reads and changes persisted credential revision. All-workspace vectors validate current roster/query/revisions; an ordinary transport reconnect with unchanged persisted revisions does not expire them. Concrete v1 has no new cross-call generation guarantee. There is no TTL or snapshot, and no global secondary tie-order guarantee. Static no-loss/progress assumes stable representable provider enumeration, admissible settlement within scheduling/attempt budgets, and callers applying returned recovery; mutable provider data remains best effort. + +SSH Linear listing runs through the existing in-process dispatcher on the account-owning runtime, preserving folder calls without Git. Its reservation follows the existing mux writer's settlement. Other commands keep host CLI passthrough. This focused routing change avoids transferring a reservation across a spawned CLI process; it does not move credentials or execution authority to the remote shell host. diff --git a/src/cli/cli-error.ts b/src/cli/cli-error.ts index aa6f2d562e2..2dad7ebef9d 100644 --- a/src/cli/cli-error.ts +++ b/src/cli/cli-error.ts @@ -1,3 +1,4 @@ +import { linearListRecoveryInstructions } from '../shared/linear/list-recovery-format' import { computerUseErrorRecoveryData } from '../shared/computer-use-error-recovery' import { matchAutomationOwnerConflict, @@ -181,11 +182,14 @@ function nextStepsFromData(data: unknown): string[] { typeof data === 'object' && Array.isArray((data as { nextSteps?: unknown }).nextSteps) ) { - return (data as { nextSteps: unknown[] }).nextSteps.filter( - (step): step is string => typeof step === 'string' - ) + return [ + ...(data as { nextSteps: unknown[] }).nextSteps.filter( + (step): step is string => typeof step === 'string' + ), + ...linearListRecoveryInstructions(data) + ] } - return [] + return linearListRecoveryInstructions(data) } function localCliErrorData(error: unknown, context: CliErrorContext): unknown { diff --git a/src/cli/help.ts b/src/cli/help.ts index 9d722388ae4..571863a75d1 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -1,3 +1,4 @@ +import { linearListFlagHelp } from './linear-list-flag-help' import type { CommandSpec } from './args' import { findCommandSpec, isCommandGroup, supportsBrowserPageFlag } from './args' import { unknownCommandData } from './command-suggestion' @@ -73,6 +74,10 @@ export function formatGroupHelp(specs: CommandSpec[], group: string): string { function formatCommandFlagHelp(flag: string, commandPath: string[]): string { const command = commandPath.join(' ') + const linearListHelp = command === 'linear list-issues' ? linearListFlagHelp(flag) : undefined + if (linearListHelp) { + return linearListHelp + } const skillsHelp = formatSkillsCommandFlagHelp(command, flag) if (skillsHelp) { return skillsHelp @@ -92,15 +97,6 @@ function formatCommandFlagHelp(flag: string, commandPath: string[]): string { if (command === 'linear search' && flag === 'workspace') { return '--workspace Connected Linear workspace id, or all' } - if (command === 'linear list-issues' && flag === 'cursor') { - return '--cursor Opaque cursor from a previous list-issues page; issued cursors bind the workspace, raw Linear cursors need --workspace' - } - if (command === 'linear list-issues' && flag === 'priority') { - return '--priority <0-4> 0=none, 1=urgent, 2=high, 3=medium, 4=low' - } - if (command === 'linear list-issues' && flag === 'limit') { - return '--limit Max issues to return; omit to return every match' - } if (command === 'artifacts list' && flag === 'cursor') { return '--cursor Opaque cursor returned by a previous artifacts page' } @@ -119,9 +115,6 @@ function formatCommandFlagHelp(flag: string, commandPath: string[]): string { if (command === 'orchestration worker-list' && flag === 'include-remote') { return '--include-remote Include connected-server worker observations' } - if (command === 'linear list-issues' && flag === 'workspace') { - return '--workspace Connected Linear workspace id, or all' - } if (command.startsWith('linear ') && flag === 'workspace') { return '--workspace Connected Linear workspace id' } diff --git a/src/cli/linear-list-flag-help.ts b/src/cli/linear-list-flag-help.ts new file mode 100644 index 00000000000..a06e5bde790 --- /dev/null +++ b/src/cli/linear-list-flag-help.ts @@ -0,0 +1,18 @@ +export function linearListFlagHelp(flag: string): string | undefined { + if (flag === 'cursor') { + return '--cursor Opaque cursor from a previous list-issues page; issued cursors bind the workspace, raw Linear cursors need --workspace' + } + if (flag === 'page-recovery') { + return '--page-recovery Resume an admitted batch with --workspace all on a capable runtime; cannot use --cursor' + } + if (flag === 'priority') { + return '--priority <0-4> 0=none, 1=urgent, 2=high, 3=medium, 4=low' + } + if (flag === 'limit') { + return '--limit Max issues to return; omit to return every match' + } + if (flag === 'workspace') { + return '--workspace Connected Linear workspace id, or all' + } + return undefined +} diff --git a/src/cli/specs/linear-mcp.ts b/src/cli/specs/linear-mcp.ts index e953a4a5701..2ac67a1225e 100644 --- a/src/cli/specs/linear-mcp.ts +++ b/src/cli/specs/linear-mcp.ts @@ -43,7 +43,7 @@ export const LINEAR_MCP_COMMAND_SPECS: CommandSpec[] = [ path: ['linear', 'list-issues'], summary: 'List Linear issues with MCP-compatible filters', usage: - 'orca linear list-issues [--team ] [--cycle ] [--label