@@ -47,6 +47,8 @@ const backgroundFinalizers = new Set();
4747let quarantineReadTestCallCount = 0 ;
4848const RAW_TAIL_CHARS = 60_000 ;
4949const FETCH_PROVENANCE_TIPS = Symbol ( "fetchProvenanceTips" ) ;
50+ const LOCK_HISTORY_REFS = Symbol ( "lockHistoryRefs" ) ;
51+ const MAX_JSON_RPC_LINE_CHARS = 1_000_000 ;
5052const QUARANTINE_RECORD_FILE = "record.json" ;
5153const WORKSPACE_QUARANTINE_DIRECTORY = "cli-agent-bridge-quarantines" ;
5254const TEST_RUNTIME_PLATFORM = process . env . NODE_ENV === "test"
@@ -1342,10 +1344,10 @@ export function backendGitProvenanceEnvironment(tracePath, baseEnvironment = pro
13421344 delete env [ name ] ;
13431345 }
13441346 }
1345- // Trace2 identifies successful fetch/pull commands in the canonical target
1346- // repository. Git does not expose a complete, cross-version per-fetch tip
1347- // log, so their presence makes commit attribution explicitly unavailable
1348- // instead of relying on the last (overwritable) FETCH_HEAD contents .
1347+ // Trace2 identifies completed fetch/pull attempts in the canonical target
1348+ // repository. A nonzero fetch may already have updated some refs, and Git
1349+ // exposes no complete cross-version per-fetch tip log, so any such attempt
1350+ // makes attribution unavailable instead of trusting the last FETCH_HEAD.
13491351 env . GIT_TRACE2_EVENT = tracePath ;
13501352 return env ;
13511353}
@@ -1555,7 +1557,11 @@ export async function readBackendGitProvenance(provenance, worktreeRoot, options
15551557 uncertain = true ;
15561558 continue ;
15571559 }
1558- if ( session . worktree === targetWorktree && session . exitCode === 0 ) {
1560+ // A non-zero fetch can still update a subset of its destinations before a
1561+ // later ref is rejected. Completion in the target repository is therefore
1562+ // enough to make exact commit attribution unavailable; exit status cannot
1563+ // prove that the repository was left untouched.
1564+ if ( session . worktree === targetWorktree ) {
15591565 sawFetch = true ;
15601566 uncertain = true ;
15611567 }
@@ -1636,14 +1642,65 @@ export async function runGitCommand(args, {
16361642 return result ;
16371643}
16381644
1639- // A starting/running lease ref is conservatively active until its exact owner
1640- // CAS removes or transitions it. Periodic ownership probes intentionally avoid
1641- // writing heartbeat blobs, and a stale timestamp cannot prove that an escaped
1642- // worker (or a worker on another host sharing the repository) has stopped.
1645+ // Every foreign exact lease ref is conservatively active until its owner CAS
1646+ // removes it. State and wall-clock timestamps cannot prove that a worker (or a
1647+ // worker on another host sharing the repository) is outside our snapshot.
1648+
1649+ export async function readRepositoryLockActivity ( lockStoreRoot , ownLockRef , options = { } ) {
1650+ if ( ! lockStoreRoot ) return { activeRefs : 0 , historyRefs : new Map ( ) } ;
1651+ const readRefs = async ( label ) => {
1652+ const storedRefs = await runGitCommand ( [
1653+ "for-each-ref" , "--format=%(refname)%09%(objectname)" , WORKSPACE_LOCK_REF_PREFIX ,
1654+ ] , { cwd : lockStoreRoot , ...options } ) ;
1655+ const failure = snapshotFailure ( label , storedRefs ) ;
1656+ if ( failure ) throw new Error ( "git snapshot unreliable: " + failure ) ;
1657+ return String ( storedRefs . stdout ?? "" ) . split ( / \r ? \n / u) ;
1658+ } ;
1659+
1660+ // These are intentionally two ordered Git snapshots, not two views parsed
1661+ // from one for-each-ref result. A lease exists before its marker is
1662+ // published: if the history scan already sees a new marker, the later live
1663+ // scan must see its lease unless that run completed before target ref capture.
1664+ const historyRefs = new Map ( ) ;
1665+ for ( const line of await readRefs ( "git for-each-ref workspace activity history" ) ) {
1666+ if ( ! line ) continue ;
1667+ const separator = line . indexOf ( "\t" ) ;
1668+ if ( separator <= 0 ) continue ;
1669+ const ref = line . slice ( 0 , separator ) ;
1670+ const oid = line . slice ( separator + 1 ) ;
1671+ if ( ref === ownLockRef || ref === ownLockRef + ".history" ) continue ;
1672+ if ( / ^ r e f s \/ c l i - a g e n t - b r i d g e \/ w o r k s p a c e - l o c k s \/ [ 0 - 9 a - f ] { 64 } \. h i s t o r y $ / u. test ( ref ) ) {
1673+ historyRefs . set ( ref , oid ) ;
1674+ }
1675+ }
1676+ let activeRefs = 0 ;
1677+ for ( const line of await readRefs ( "git for-each-ref workspace active leases" ) ) {
1678+ if ( ! line ) continue ;
1679+ const separator = line . indexOf ( "\t" ) ;
1680+ if ( separator <= 0 ) continue ;
1681+ const ref = line . slice ( 0 , separator ) ;
1682+ if ( ref === ownLockRef ) continue ;
1683+ if ( / ^ r e f s \/ c l i - a g e n t - b r i d g e \/ w o r k s p a c e - l o c k s \/ [ 0 - 9 a - f ] { 64 } $ / u. test ( ref ) ) {
1684+ // Ref existence is the conservative signal. Malformed, idle, pending,
1685+ // or quarantined owners cannot be dismissed using a foreign host clock.
1686+ activeRefs += 1 ;
1687+ }
1688+ }
1689+ return { activeRefs, historyRefs } ;
1690+ }
16431691
16441692async function gitSnapshot ( worktreeRoot , options = { } ) {
16451693 const ownLockRef = typeof options . ownLockRef === "string" ? options . ownLockRef : null ;
16461694 const lockStoreRoot = typeof options . lockStoreRoot === "string" ? options . lockStoreRoot : null ;
1695+ const historyBaseline = options . concurrencyHistoryBaseline instanceof Map
1696+ ? options . concurrencyHistoryBaseline
1697+ : null ;
1698+ // The before vector must precede every target-repository observation. If it
1699+ // were sampled at the end, a run completing between ref capture and this
1700+ // marker could be swallowed into the baseline and misattributed later.
1701+ let lockActivity = historyBaseline === null
1702+ ? await readRepositoryLockActivity ( lockStoreRoot , ownLockRef , options )
1703+ : null ;
16471704 const jobs = [
16481705 [ "git status --short" , "status" , [ "status" , "--short" , "--untracked-files=all" , "--ignore-submodules=none" ] , false , false , true ] ,
16491706 [ "git diff --stat" , "diffStat" , [ "diff" , "--ignore-submodules=none" , "--stat" ] , false , false , true ] ,
@@ -1751,46 +1808,21 @@ async function gitSnapshot(worktreeRoot, options = {}) {
17511808 throw new Error ( "git snapshot unreliable: cannot read FETCH_HEAD: " + error . message ) ;
17521809 }
17531810 }
1754- const lockRefs = [ ] ;
1755- if ( lockStoreRoot ) {
1756- const storedRefs = await runGitCommand ( [
1757- "for-each-ref" , "--format=%(refname)%09%(objectname)" , WORKSPACE_LOCK_REF_PREFIX ,
1758- ] , { cwd : lockStoreRoot , ...options } ) ;
1759- const storedRefsFailure = snapshotFailure ( "git for-each-ref workspace lock store" , storedRefs ) ;
1760- if ( storedRefsFailure ) throw new Error ( "git snapshot unreliable: " + storedRefsFailure ) ;
1761- for ( const line of String ( storedRefs . stdout ?? "" ) . split ( / \r ? \n / u) ) {
1762- if ( ! line ) continue ;
1763- const separator = line . indexOf ( "\t" ) ;
1764- if ( separator <= 0 ) continue ;
1765- const ref = line . slice ( 0 , separator ) ;
1766- if ( ref !== ownLockRef ) lockRefs . push ( line . slice ( separator + 1 ) ) ;
1767- }
1768- }
1811+ lockActivity ??= await readRepositoryLockActivity ( lockStoreRoot , ownLockRef , options ) ;
17691812 // Linked worktrees serialize per worktree but share repository refs, so a
17701813 // commit from a parallel delegation can land between our two snapshots.
1771- // Detection combines two signals: leases that are active right now, and the
1772- // persistent run-history records completed delegations leave behind, whose
1773- // [acquiredAt, endedAt] window is checked against this snapshot's window.
1774- const windowStart = Number . isFinite ( options . concurrencyWindowStart )
1775- ? options . concurrencyWindowStart
1776- : Number . POSITIVE_INFINITY ;
1777- let concurrentDelegations = 0 ;
1778- for ( const oid of lockRefs ) {
1779- const blob = await runGitCommand ( [ "cat-file" , "blob" , oid ] , { cwd : lockStoreRoot , ...options } ) ;
1780- if ( blob . exitCode !== 0 ) continue ; // unreadable owner blob: ignore for disclosure
1781- try {
1782- const record = JSON . parse ( blob . stdout ) ;
1783- if ( Number . isFinite ( record ?. endedAt ) ) {
1784- const acquiredAt = Number . isFinite ( record . acquiredAt ) ? record . acquiredAt : record . endedAt ;
1785- if ( acquiredAt <= Date . now ( ) && record . endedAt >= windowStart ) {
1786- concurrentDelegations += 1 ;
1787- }
1788- continue ;
1814+ // Detection combines two clock-independent signals: leases that are active
1815+ // at either snapshot, and a changed persistent history-ref OID between the
1816+ // snapshots. Wall clocks from two hosts sharing a repository are not
1817+ // comparable and must never decide whether attribution is exact.
1818+ let concurrentDelegations = lockActivity . activeRefs ;
1819+ if ( historyBaseline ) {
1820+ const historyNames = new Set ( [ ...historyBaseline . keys ( ) , ...lockActivity . historyRefs . keys ( ) ] ) ;
1821+ for ( const ref of historyNames ) {
1822+ if ( historyBaseline . get ( ref ) !== lockActivity . historyRefs . get ( ref ) ) {
1823+ concurrentDelegations += 1 ;
17891824 }
1790- const active = record &&
1791- ( record . workerState === "starting" || record . workerState === "running" ) ;
1792- if ( active ) concurrentDelegations += 1 ;
1793- } catch { /* malformed owner blob: ignore for disclosure */ }
1825+ }
17941826 }
17951827 const snapshot = {
17961828 // The leading space in porcelain's first XY column is significant (for
@@ -1804,6 +1836,7 @@ async function gitSnapshot(worktreeRoot, options = {}) {
18041836 fetchHeads,
18051837 concurrentDelegations,
18061838 } ;
1839+ Object . defineProperty ( snapshot , LOCK_HISTORY_REFS , { value : lockActivity . historyRefs } ) ;
18071840 return snapshot ;
18081841}
18091842
@@ -2127,10 +2160,25 @@ export async function committedDelta(worktreeRoot, before, after, options = {})
21272160}
21282161
21292162export function backendEntryFromProbe ( name , spec , check ) {
2130- const available = check . exitCode === 0 && check . treeTerminated === true ;
2131- const probeError = check . treeTerminated !== true
2132- ? ( check . terminationError || "backend version probe process tree could not be confirmed terminated" )
2133- : ( check . errorMessage || "command not found or not executable" ) ;
2163+ const available = check . exitCode === 0 && check . treeTerminated === true && check . timedOut !== true ;
2164+ const probeStderr = tail ( String ( check . stderr ?? "" ) , 500 ) . trim ( ) ;
2165+ let probeError = "" ;
2166+ if ( check . treeTerminated !== true ) {
2167+ probeError = check . terminationError ||
2168+ "backend version probe process tree could not be confirmed terminated" ;
2169+ } else if ( check . timedOut ) {
2170+ probeError = "backend version probe timed out after " + VERSION_CHECK_TIMEOUT_MS + " ms" +
2171+ ( probeStderr ? ": " + probeStderr : "" ) ;
2172+ } else if ( typeof check . exitCode === "number" && check . exitCode !== 0 ) {
2173+ probeError = "backend version probe exited with code " + check . exitCode +
2174+ ( probeStderr ? ": " + probeStderr : "" ) ;
2175+ } else if ( check . errorMessage ) {
2176+ probeError = check . errorMessage ;
2177+ } else if ( probeStderr ) {
2178+ probeError = "backend version probe failed: " + probeStderr ;
2179+ } else {
2180+ probeError = "command not found or not executable" ;
2181+ }
21342182 return {
21352183 name,
21362184 label : typeof spec . label === "string" ? spec . label : name ,
@@ -2145,6 +2193,7 @@ export function backendEntryFromProbe(name, spec, check) {
21452193}
21462194
21472195async function listBackends ( cancel = null ) {
2196+ if ( cancel ?. cancelled ) throw new OperationCancelledError ( "list_backends cancelled by client" ) ;
21482197 if ( ! supportsReliableProcessContainment ( ) ) {
21492198 return [ {
21502199 name : "unsupported-platform" ,
@@ -2162,20 +2211,18 @@ async function listBackends(cancel = null) {
21622211 try {
21632212 backends = await loadBackends ( { cancel } ) ;
21642213 } catch ( error ) {
2165- if ( error instanceof OperationCancelledError ) return [ ] ;
21662214 throw error ;
21672215 }
21682216 const entries = [ ] ;
21692217 for ( const [ name , spec ] of Object . entries ( backends ) ) {
21702218 // A hung `--version` probe must not pin the request: the client can cancel
21712219 // the discovery call, terminating the current probe and skipping the rest.
2172- if ( cancel ?. cancelled ) break ;
2220+ if ( cancel ?. cancelled ) throw new OperationCancelledError ( "list_backends cancelled by client" ) ;
21732221 if ( ! spec || typeof spec . command !== "string" ) continue ;
21742222 let resolvedCommand ;
21752223 try {
21762224 resolvedCommand = await resolveBackendCommand ( spec . command , { cancel } ) ;
21772225 } catch ( error ) {
2178- if ( error instanceof OperationCancelledError ) break ;
21792226 throw error ;
21802227 }
21812228 if ( ! resolvedCommand ) {
@@ -2201,6 +2248,13 @@ async function listBackends(cancel = null) {
22012248 } ,
22022249 } ) ;
22032250 if ( cancel ?. controller ) cancel . controller = null ;
2251+ if ( cancel ?. cancelled ) {
2252+ if ( check . treeTerminated !== true ) {
2253+ throw new Error ( check . terminationError ||
2254+ "backend version probe process tree could not be confirmed terminated" ) ;
2255+ }
2256+ throw new OperationCancelledError ( "list_backends cancelled by client" ) ;
2257+ }
22042258 entries . push ( backendEntryFromProbe ( name , spec , check ) ) ;
22052259 }
22062260 return entries ;
@@ -2818,9 +2872,6 @@ async function delegateTask(rawArgs, cancel) {
28182872 return gitProcessQuarantine ;
28192873 } ;
28202874 const allowDirty = rawArgs . allowDirty === true ;
2821- // Attribution window for concurrency disclosure: everything between the
2822- // before-snapshot and the after-snapshot.
2823- const attributionWindowStart = Date . now ( ) ;
28242875 let before ;
28252876 try {
28262877 before = await gitSnapshot ( worktreeRoot , {
@@ -3104,7 +3155,7 @@ async function delegateTask(rawArgs, cancel) {
31043155 deadline,
31053156 ownLockRef : workspaceLease . ref ,
31063157 lockStoreRoot,
3107- concurrencyWindowStart : attributionWindowStart ,
3158+ concurrencyHistoryBaseline : before [ LOCK_HISTORY_REFS ] ,
31083159 onUnconfirmedProcessTree : quarantineGitProcessTree ,
31093160 } ) ;
31103161 Object . defineProperty ( after , FETCH_PROVENANCE_TIPS , { value : fetchProvenanceTips } ) ;
@@ -3323,6 +3374,7 @@ function installShutdownHandlers(stdin, stdout = process.stdout) {
33233374 process . once ( "exit" , ( ) => {
33243375 for ( const { cancel } of activeRequests . values ( ) ) cancel . cancel ( ) ;
33253376 } ) ;
3377+ return shutdown ;
33263378}
33273379
33283380async function handleMessage ( message ) {
@@ -3376,6 +3428,11 @@ async function handleMessage(message) {
33763428 content : [ { type : "text" , text : lines . join ( "\n" ) } ] ,
33773429 structuredContent : { backends : entries } ,
33783430 } ) ;
3431+ } catch ( error ) {
3432+ if ( error instanceof OperationCancelledError ) {
3433+ return jsonRpcError ( message . id , - 32800 , "list_backends cancelled by client" ) ;
3434+ }
3435+ throw error ;
33793436 } finally {
33803437 finishRequest ( ) ;
33813438 }
@@ -3536,7 +3593,11 @@ async function handleMessage(message) {
35363593 }
35373594}
35383595
3539- function startStdioServer ( { stdin = process . stdin , stdout = process . stdout } = { } ) {
3596+ export function startStdioServer ( {
3597+ stdin = process . stdin ,
3598+ stdout = process . stdout ,
3599+ onOversizedLine = ( ) => stdin . destroy ?. ( ) ,
3600+ } = { } ) {
35403601 stdin . setEncoding ( "utf8" ) ;
35413602 let buffer = "" ;
35423603 stdin . on ( "data" , ( chunk ) => {
@@ -3548,6 +3609,14 @@ function startStdioServer({ stdin = process.stdin, stdout = process.stdout } = {
35483609 buffer = "" ;
35493610 break ;
35503611 }
3612+ if ( newlineIndex > MAX_JSON_RPC_LINE_CHARS ) {
3613+ buffer = "" ;
3614+ stdout . write ( JSON . stringify ( jsonRpcError (
3615+ null , - 32700 , "JSON-RPC request line exceeds the configured size limit" ,
3616+ ) ) + "\n" ) ;
3617+ onOversizedLine ( ) ;
3618+ return ;
3619+ }
35513620 const line = buffer . slice ( 0 , newlineIndex ) . trim ( ) ;
35523621 buffer = buffer . slice ( newlineIndex + 1 ) ;
35533622 newlineIndex = buffer . indexOf ( "\n" ) ;
@@ -3565,10 +3634,19 @@ function startStdioServer({ stdin = process.stdin, stdout = process.stdout } = {
35653634 stdout . write ( JSON . stringify ( jsonRpcError ( null , - 32603 , error . message ) ) + "\n" ) ;
35663635 } ) ;
35673636 }
3637+ if ( buffer . length > MAX_JSON_RPC_LINE_CHARS ) {
3638+ buffer = "" ;
3639+ stdout . write ( JSON . stringify ( jsonRpcError (
3640+ null , - 32700 , "JSON-RPC request line exceeds the configured size limit" ,
3641+ ) ) + "\n" ) ;
3642+ onOversizedLine ( ) ;
3643+ }
35683644 } ) ;
35693645}
35703646
35713647if ( process . argv [ 1 ] && process . argv [ 1 ] === fileURLToPath ( import . meta. url ) ) {
3572- startStdioServer ( ) ;
3573- installShutdownHandlers ( process . stdin , process . stdout ) ;
3648+ const shutdown = installShutdownHandlers ( process . stdin , process . stdout ) ;
3649+ startStdioServer ( {
3650+ onOversizedLine : ( ) => shutdown ( 1 ) ,
3651+ } ) ;
35743652}
0 commit comments