@@ -46,10 +46,18 @@ const REQUEST_TIMEOUT_MS = 8000;
4646const RELATION_TYPE_BLOCKS = 'blocks' ;
4747
4848/**
49- * Page size for the children / relations connections. Bounded by
50- * ``max_sub_issues`` policy downstream; a parent with more children
51- * than this is over-cap and will be rejected before execution, so a
52- * single page is sufficient for the MVP (no cursor pagination).
49+ * Page size for the children / relations connections.
50+ *
51+ * review blocker #7: this query fetches a SINGLE page (no cursor loop). The old
52+ * comment claimed a >100-child parent is "rejected before execution", but the
53+ * Mode-A path (a human-authored sub-issue graph) has NO cap check — so an
54+ * over-size epic was SILENTLY truncated to the first 100 AND its dropped
55+ * children's dependency edges were filtered out, leaving survivors that release
56+ * out of order. Rather than silently truncate, we now detect ``hasNextPage`` on
57+ * either connection and return an explicit ``error`` (fail loud) so the operator
58+ * splits the epic instead of getting a wrong-order run. 100 is the platform
59+ * ceiling for a single Mode-A epic; a genuinely larger workload should be
60+ * multiple epics.
5361 */
5462const CONNECTION_PAGE_SIZE = 100 ;
5563
@@ -58,19 +66,22 @@ const CONNECTION_PAGE_SIZE = 100;
5866 *
5967 * For child C, ``inverseRelations`` of type ``blocks`` are relations
6068 * whose *source* issue blocks C — i.e. C's predecessors. We take the
61- * related issue id from each as a ``depends_on`` edge.
69+ * related issue id from each as a ``depends_on`` edge. ``pageInfo.hasNextPage``
70+ * on both connections lets us detect (and reject) a truncated over-size graph.
6271 */
6372const SUB_ISSUE_GRAPH_QUERY = `
6473query SubIssueGraph($issueId: String!, $first: Int!) {
6574 issue(id: $issueId) {
6675 id
6776 identifier
6877 children(first: $first) {
78+ pageInfo { hasNextPage }
6979 nodes {
7080 id
7181 identifier
7282 title
7383 inverseRelations(first: $first) {
84+ pageInfo { hasNextPage }
7485 nodes {
7586 type
7687 issue { id }
@@ -113,18 +124,22 @@ interface RawRelationNode {
113124 readonly issue ?: { readonly id ?: string } | null ;
114125}
115126
127+ interface RawPageInfo {
128+ readonly hasNextPage ?: boolean ;
129+ }
130+
116131interface RawChildNode {
117132 readonly id ?: string ;
118133 readonly identifier ?: string ;
119134 readonly title ?: string ;
120- readonly inverseRelations ?: { readonly nodes ?: readonly RawRelationNode [ ] } | null ;
135+ readonly inverseRelations ?: { readonly pageInfo ?: RawPageInfo ; readonly nodes ?: readonly RawRelationNode [ ] } | null ;
121136}
122137
123138interface RawSubIssueGraph {
124139 readonly data ?: {
125140 readonly issue ?: {
126141 readonly id ?: string ;
127- readonly children ?: { readonly nodes ?: readonly RawChildNode [ ] } | null ;
142+ readonly children ?: { readonly pageInfo ?: RawPageInfo ; readonly nodes ?: readonly RawChildNode [ ] } | null ;
128143 } | null ;
129144 } ;
130145 readonly errors ?: unknown ;
@@ -204,6 +219,33 @@ export async function fetchSubIssueGraph(
204219 return { kind : 'no_children' , parentIssueId : issue . id } ;
205220 }
206221
222+ // review blocker #7: fail LOUD on truncation instead of silently dropping
223+ // children (and their dependency edges). If Linear reports more children than
224+ // one page holds, or any child has more blockers than one page holds, we can't
225+ // build a correct DAG from a single fetch — reject so the operator splits the
226+ // epic rather than getting a silently-wrong-order run.
227+ if ( issue . children ?. pageInfo ?. hasNextPage ) {
228+ logger . warn ( 'Linear sub-issue fetch truncated — parent has more children than one page' , {
229+ parent_issue_id : issue . id , page_size : CONNECTION_PAGE_SIZE ,
230+ } ) ;
231+ return {
232+ kind : 'error' ,
233+ message : `This epic has more than ${ CONNECTION_PAGE_SIZE } sub-issues — too many for a single `
234+ + `orchestration. Split it into multiple epics of at most ${ CONNECTION_PAGE_SIZE } sub-issues each.` ,
235+ } ;
236+ }
237+ const truncatedBlockersChild = childNodes . find ( ( c ) => c . inverseRelations ?. pageInfo ?. hasNextPage ) ;
238+ if ( truncatedBlockersChild ) {
239+ logger . warn ( 'Linear sub-issue fetch truncated — a child has more blockers than one page' , {
240+ parent_issue_id : issue . id , child_id : truncatedBlockersChild . id , page_size : CONNECTION_PAGE_SIZE ,
241+ } ) ;
242+ return {
243+ kind : 'error' ,
244+ message : `A sub-issue has more than ${ CONNECTION_PAGE_SIZE } blocking relations — too many to order `
245+ + `reliably. Reduce the cross-dependencies on sub-issue ${ truncatedBlockersChild . identifier ?? truncatedBlockersChild . id } .` ,
246+ } ;
247+ }
248+
207249 // Restrict depends_on edges to ids that are themselves children of
208250 // this parent — a "blocks" relation pointing at an issue outside the
209251 // epic is not an intra-epic ordering constraint. (validateDag also
0 commit comments