@yuandc/aica 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/client/acp-client.js +1 -1
- package/dist/acp/client/acp-process.js +1 -1
- package/dist/acp/client/acp-runtime-pool.js +1 -1
- package/dist/acp/providers/codex/codex-process.js +1 -1
- package/dist/acp/providers/codex-acp/adapter.js +4 -4
- package/dist/acp/providers/codex-acp/provider.js +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/core/codex-provider-runtime-config.js +1 -0
- package/dist/core/heartbeat.js +3 -3
- package/dist/core/job-worker.js +2 -2
- package/dist/core/machine-filesystem-worker.js +4 -4
- package/dist/version.js +1 -0
- package/dist/worker-cli.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{JsonLineRpcClient as n}from"./json-rpc.js";class
|
|
1
|
+
import{JsonLineRpcClient as n}from"./json-rpc.js";class p{onSessionUpdate;onClientRequest;rpc;constructor(e,s,o){this.onSessionUpdate=s,this.onClientRequest=o,this.rpc=new n(e,{peerName:"ACP server",includeJsonRpc:!0,onNotification:(r,i,t)=>{r==="session/update"&&this.onSessionUpdate(i,t)},onRequest:async(r,i,t)=>{if(this.onClientRequest)return this.onClientRequest(r,i,t);throw new Error(`Unsupported ACP client request: ${r}`)}})}initialize(e=6e4){return this.rpc.request("initialize",{protocolVersion:1,clientCapabilities:{fs:{readTextFile:!1,writeTextFile:!1},terminal:!1},clientInfo:{name:"aca",version:"0.1.0"}},e)}configureRuntime(e,s=3e4){return this.rpc.request("aca/runtime_config",e,s)}newSession(e,s=12e4){return this.rpc.request("session/new",{cwd:e.cwd,mcpServers:e.mcpServers??[],...e.config?{config:e.config}:{}},s)}resumeSession(e,s=12e4){return this.rpc.request("session/resume",{sessionId:e.sessionId,cwd:e.cwd,mcpServers:e.mcpServers??[],...e.config?{config:e.config}:{}},s)}loadSession(e,s=12e4){return this.rpc.request("session/load",{sessionId:e.sessionId,cwd:e.cwd,mcpServers:e.mcpServers??[],...e.config?{config:e.config}:{}},s)}closeSession(e,s=3e4){return this.rpc.request("session/close",{sessionId:e},s)}listSessions(e={},s=6e4){return this.rpc.request("session/list",e,s)}deleteSession(e,s=6e4){return this.rpc.request("session/delete",{sessionId:e},s)}forkSession(e,s=12e4){return this.rpc.request("session/fork",{sessionId:e.sessionId,cwd:e.cwd,...e.mcpServers?{mcpServers:e.mcpServers}:{},...e.config?{config:e.config}:{}},s)}setSessionConfigOption(e,s=6e4){return this.rpc.request("session/set_config_option",e,s)}setSessionMode(e,s=6e4){return this.rpc.request("session/set_mode",e,s)}prompt(e,s){return this.rpc.request("session/prompt",e,s)}compactSession(e,s=600*1e3){return this.rpc.request("session/compact",e,s)}cancel(e){this.rpc.notify("session/cancel",{sessionId:e})}authenticate(e,s=12e4){return this.rpc.request("authenticate",e,s)}logout(e={},s=6e4){return this.rpc.request("logout",e,s)}listProviders(e={},s=6e4){return this.rpc.request("providers/list",e,s)}setProvider(e,s=6e4){return this.rpc.request("providers/set",e,s)}disableProvider(e,s=6e4){return this.rpc.request("providers/disable",e,s)}}export{p as AcpClient};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
1
|
+
import{spawn as s}from"node:child_process";import{acpChildEnvironment as u}from"../../core/process-identity.js";function a(e,t,n,r={}){const i=s(e,t,{cwd:n,stdio:["pipe","pipe","pipe"],env:u(e,t,r)});return{command:e,args:t,child:i}}async function f(e){e.exitCode!==null||e.signalCode!==null||(e.stdin.end(),!await o(e,2e3)&&(e.kill("SIGTERM"),!await o(e,1e3)&&(e.kill("SIGKILL"),await o(e,1e3))))}function o(e,t){return e.exitCode!==null||e.signalCode!==null?Promise.resolve(!0):new Promise(n=>{const r=setTimeout(()=>n(!1),t);r.unref(),e.once("close",()=>{clearTimeout(r),n(!0)})})}export{f as closeAcpProcess,a as startAcpProcess};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import c from"node:fs";import{randomUUID as
|
|
1
|
+
import c from"node:fs";import{randomUUID as m}from"node:crypto";import{AcpClient as p}from"./acp-client.js";import{closeAcpProcess as h,startAcpProcess as v}from"./acp-process.js";const f=300*1e3,I=60*1e3,w=4;class A{runtimes=new Map;idleTtlMs;sweepIntervalMs;maxIdleRuntimes;sweepTimer=null;disposeAllPromise=null;constructor(e={}){this.idleTtlMs=Math.max(1e3,e.idleTtlMs??E()),this.sweepIntervalMs=Math.max(1e3,e.sweepIntervalMs??R()),this.maxIdleRuntimes=Math.max(0,e.maxIdleRuntimes??S())}async acquire(e){this.disposeAllPromise&&await this.disposeAllPromise;const i=c.realpathSync(e.cwd);this.ensureSweep();const s=e.runtimeIdentity||"default",r=e.providerSessionId?d(e.providerId,i,e.providerSessionId,s):null,n=r?this.runtimes.get(r):null;n&&!this.isRuntimeAlive(n)&&await this.disposeEntry(n).catch(()=>{});const o=r?this.runtimes.get(r):null;if(o)return this.activateEntry(o,e),this.createLease(o);const u=e.providerSessionId||`pending:${m()}`,l=d(e.providerId,i,u,s),a=this.createEntry({...e,cwd:i,key:l,providerSessionId:e.providerSessionId??null,runtimeIdentity:s});return this.runtimes.set(l,a),this.createLease(a)}disposeAll(){if(this.disposeAllPromise)return this.disposeAllPromise;const e=this.performDisposeAll();return this.disposeAllPromise=e,e.finally(()=>{this.disposeAllPromise===e&&(this.disposeAllPromise=null)}),e}async performDisposeAll(){this.sweepTimer&&(clearInterval(this.sweepTimer),this.sweepTimer=null);const e=[...this.runtimes.values()];await Promise.all(e.map(i=>this.disposeEntry(i).catch(()=>{})))}async sweepIdle(e=Date.now()){const i=[...this.runtimes.values()];for(const s of i)s.inUse||s.disposed||e-s.lastUsedAtMs<this.idleTtlMs||await this.disposeEntry(s).catch(()=>{});await this.trimIdleRuntimes()}async retireProviderRuntimes(e){const i=[...this.runtimes.values()].filter(s=>s.providerId===e&&!s.disposed);for(const s of i)s.retired=!0;return await Promise.all(i.filter(s=>!s.inUse).map(s=>this.disposeEntry(s).catch(()=>{}))),i.length}stats(){const e=[...this.runtimes.values()].filter(i=>!i.disposed);return{total:e.length,inUse:e.filter(i=>i.inUse).length,idle:e.filter(i=>!i.inUse).length,pids:e.map(i=>i.started.child.pid).filter(i=>Number.isInteger(i))}}createEntry(e){const i=v(e.command,e.args,e.cwd,e.env),s={key:e.key,providerId:e.providerId,cwd:e.cwd,providerSessionId:e.providerSessionId,runtimeIdentity:e.runtimeIdentity,started:i,client:null,initialized:!1,sessionEstablished:!1,runtimeConfigured:!1,inUse:!0,retired:!1,disposed:!1,disposePromise:null,lastUsedAtMs:Date.now(),sessionResponse:null,activeHandlers:{onSessionUpdate:e.onSessionUpdate,onClientRequest:e.onClientRequest}};return s.client=new p(i.child,(r,n)=>s.activeHandlers?.onSessionUpdate(r,n),async(r,n,o)=>{if(!s.activeHandlers?.onClientRequest)throw new Error(`Unsupported ACP client request: ${r}`);return s.activeHandlers.onClientRequest(r,n,o)}),i.child.once("close",()=>{this.runtimes.get(s.key)===s&&this.runtimes.delete(s.key),s.disposed=!0,s.inUse=!1,s.activeHandlers=null}),s}activateEntry(e,i){if(e.inUse)throw new Error(`ACP runtime is already in use: provider=${e.providerId} session=${e.providerSessionId??"unknown"}`);e.inUse=!0,e.activeHandlers={onSessionUpdate:i.onSessionUpdate,onClientRequest:i.onClientRequest}}createLease(e){let i=!1;return{get client(){return e.client},get started(){return e.started},get cwd(){return e.cwd},get providerSessionId(){return e.providerSessionId},get sessionResponse(){return e.sessionResponse},get initialized(){return e.initialized},get sessionEstablished(){return e.sessionEstablished},get runtimeConfigured(){return e.runtimeConfigured},markInitialized:()=>{e.initialized=!0},markRuntimeConfigured:()=>{e.runtimeConfigured=!0},setProviderSession:s=>{e.providerSessionId=s.providerSessionId,e.sessionEstablished=!0,"sessionResponse"in s&&(e.sessionResponse=s.sessionResponse??null),this.rekeyEntry(e,d(e.providerId,e.cwd,s.providerSessionId,e.runtimeIdentity))},setSessionResponse:s=>{e.sessionResponse=s},release:()=>{i||(i=!0,e.inUse=!1,e.activeHandlers=null,e.lastUsedAtMs=Date.now(),e.retired?this.disposeEntry(e):this.trimIdleRuntimes())},dispose:async()=>{i=!0,await this.disposeEntry(e)}}}rekeyEntry(e,i){if(e.key===i)return;const s=this.runtimes.get(i);if(s&&s!==e){if(s.inUse)throw new Error(`ACP runtime key is already active: provider=${s.providerId} session=${s.providerSessionId??"unknown"}`);this.disposeEntry(s)}this.runtimes.get(e.key)===e&&this.runtimes.delete(e.key),e.key=i,this.runtimes.set(i,e)}async disposeEntry(e){if(e.disposePromise)return e.disposePromise;if(e.disposed)return;const i=this.performDisposeEntry(e);return e.disposePromise=i,i}async performDisposeEntry(e){e.disposed=!0,e.inUse=!1,e.activeHandlers=null,this.runtimes.get(e.key)===e&&this.runtimes.delete(e.key),e.providerSessionId&&await e.client.closeSession(e.providerSessionId,5e3).catch(()=>{}),await h(e.started.child)}async trimIdleRuntimes(){const e=[...this.runtimes.values()].filter(s=>!s.inUse&&!s.disposed).sort((s,r)=>s.lastUsedAtMs-r.lastUsedAtMs),i=Math.max(0,e.length-this.maxIdleRuntimes);for(const s of e.slice(0,i))await this.disposeEntry(s).catch(()=>{})}isRuntimeAlive(e){return!e.disposed&&e.started.child.exitCode===null&&e.started.child.signalCode===null}ensureSweep(){this.sweepTimer||(this.sweepTimer=setInterval(()=>{this.sweepIdle()},this.sweepIntervalMs),this.sweepTimer.unref())}}const _=new A;function d(t,e,i,s){return`${t}\0${e}\0${i}\0${s}`}function E(){const t=Number.parseInt(process.env.ACA_ACP_RUNTIME_IDLE_TTL_MS??"",10);return Number.isInteger(t)&&t>=1e3?t:f}function R(){const t=Number.parseInt(process.env.ACA_ACP_RUNTIME_SWEEP_INTERVAL_MS??"",10);return Number.isInteger(t)&&t>=1e3?t:I}function S(){const t=Number.parseInt(process.env.ACA_ACP_RUNTIME_MAX_IDLE??"",10);return Number.isInteger(t)&&t>=0?Math.min(t,32):w}export{A as AcpRuntimePool,_ as defaultAcpRuntimePool};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
1
|
+
import{spawn as s}from"node:child_process";import{acpChildEnvironment as t}from"../../../core/process-identity.js";import{resolveCodexPath as n}from"./codex-path.js";import{resolveCodexPath as v}from"./codex-path.js";function m(e,o=null){const r=n(),a=d(o),p=s(r,a,{cwd:e,env:t(r,a,{CODEX_PATH:r,...o?{ACA_CODEX_PROVIDER_KEY:o.apiKey}:{}}),stdio:["pipe","pipe","pipe"]});return p.stdout.setEncoding("utf8"),p.stderr.setEncoding("utf8"),{command:r,args:a,child:p}}function d(e){return e?["-c",'model_provider="aca_managed"',"-c",'model_providers.aca_managed.name="ACA Managed"',"-c",`model_providers.aca_managed.base_url=${JSON.stringify(e.baseUrl)}`,"-c",'model_providers.aca_managed.env_key="ACA_CODEX_PROVIDER_KEY"',"-c",'model_providers.aca_managed.wire_api="responses"',"-c","model_providers.aca_managed.requires_openai_auth=false","app-server","--stdio"]:["app-server","--stdio"]}export{d as codexAppServerArgs,v as resolveCodexPath,m as startCodexAppServer};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{JsonLineRpcClient as P}from"../../client/json-rpc.js";import{startCodexAppServer as q}from"../codex/codex-process.js";import{CodexConnectionRecoveryTracker as
|
|
2
|
-
`);this.lineBuffer=t.pop()??"";for(const s of t){const r=s.trim();if(!r)continue;let o;try{o=JSON.parse(r)}catch{continue}this.handleMessage(o)}}async handleMessage(e){if("id"in e&&("result"in e||"error"in e)&&typeof e.method!="string"){this.handleParentResponse(e);return}const t=typeof e.method=="string"?e.method:"",s=e.id;if(t){if(s===void 0){await this.handleNotification(t,u(e.params));return}try{const r=await this.handleRequest(t,u(e.params));this.write({jsonrpc:"2.0",id:s,result:r})}catch(r){this.write({jsonrpc:"2.0",id:s,error:{code:-32603,message:r instanceof Error?r.message:String(r)}})}}}async handleNotification(e,t){e==="session/cancel"&&await this.cancel(t)}async handleRequest(e,t){switch(e){case"initialize":return this.initializeResponse();case"session/new":return this.newSession(t);case"session/resume":case"session/load":return this.resumeSession(t);case"session/fork":return this.forkSession(t);case"session/prompt":return this.prompt(t);case"session/compact":return this.manualCompact(t);case"session/cancel":return await this.cancel(t),{};case"session/close":return await this.closeSession(t),{};case"session/set_config_option":return this.setConfigOption(t);case"session/set_mode":return this.setMode(t);case"fs/read_text_file":case"fs/write_text_file":case"terminal/create":case"terminal/output":case"terminal/wait_for_exit":case"terminal/kill":case"terminal/release":return{};default:throw new Error(`Unsupported ACP method: ${e}`)}}initializeResponse(){return{protocolVersion:1,agentCapabilities:{loadSession:!0,sessionCapabilities:{resume:!0,close:!0}}}}async newSession(e){const t=a(e,"cwd")||process.cwd();this.applyInitialConfig(e),await this.ensureCodex(t);const s=await this.requireRpc().request("thread/start",{cwd:t,approvalsReviewer:"user",...this.codexConfig(),ephemeral:!1,threadSource:"aca-codex-acp"},6e4),r=C(s);if(!r)throw new Error("Codex thread/start did not return thread id");return this.currentSessionId=r,this.currentCwd=t,this.captureThreadMetadata(s),this.sessionResponse(r)}async resumeSession(e){const t=a(e,"sessionId");if(!t)throw new Error("sessionId is required");const s=a(e,"cwd")||this.currentCwd;this.applyInitialConfig(e),await this.ensureCodex(s);const r=await this.requireRpc().request("thread/resume",{threadId:t,cwd:s,approvalsReviewer:"user",...this.codexConfig()},6e4);return this.currentSessionId=C(r)||t,this.currentCwd=s,this.captureThreadMetadata(r),this.sessionResponse(this.currentSessionId)}async forkSession(e){const t=a(e,"sessionId");if(!t)throw new Error("sessionId is required");const s=a(e,"cwd")||this.currentCwd;this.applyInitialConfig(e),await this.ensureCodex(s);const r=await this.requireRpc().request("thread/fork",{threadId:t,cwd:s,approvalsReviewer:"user",ephemeral:!1,threadSource:"aca-codex-acp-fork",...this.codexConfig()},12e4),o=C(r);if(!o)throw new Error("Codex thread/fork did not return thread id");return this.currentSessionId=o,this.currentCwd=s,this.captureThreadMetadata(r),this.sessionResponse(o)}async prompt(e){const t=a(e,"sessionId")||this.currentSessionId;let s=this.resolveSessionAlias(t);if(!s)throw new Error("sessionId is required");const r=Array.isArray(e.prompt)?e.prompt:[],o=this.requireRpc();this.currentSessionId=s,this.promptActive=!0,this.currentTurnId=null,this.currentPromptText=D(r),this.currentAssistantText="",this.currentAffectedFiles.clear(),this.connectionRecovery.reset(),this.agentMessagePhases.clear(),this.planDeltas.reset();let i;try{i=await o.request("turn/start",{threadId:s,clientUserMessageId:`aca-acp-${Date.now()}-${Math.random().toString(16).slice(2)}`,input:$(r),cwd:this.currentCwd,...O(this.config)},R());const c=String(i?.turn?.id??"");this.currentTurnId=c||null;const d=c?await this.waitForTurnCompleted(c,R()):i?.turn;this.threadTurnCount+=1,this.previousUserText=this.currentPromptText,this.previousAssistantText=this.currentAssistantText,this.previousAffectedFiles.clear();for(const l of this.currentAffectedFiles)this.previousAffectedFiles.add(l);return{stopReason:"end_turn",sessionId:this.currentSessionId,turn:d,_meta:{acaContextUsage:L(this.latestTokenUsage)}}}catch(c){const d=this.connectionRecovery.fail(c);throw d&&this.notifySessionUpdate(d),c}finally{this.promptActive=!1,this.currentTurnId=null}}async cancel(e){const t=a(e,"sessionId")||this.currentSessionId;if(!(!t||!this.rpc)&&(await this.rpc.request("turn/interrupt",{threadId:t}).catch(()=>{}),this.currentTurnId)){const s=this.pendingTurnCompletions.get(this.currentTurnId);s&&(this.pendingTurnCompletions.delete(this.currentTurnId),clearTimeout(s.timer),s.reject(new Error("Codex prompt cancelled")))}}async manualCompact(e){const t=a(e,"sessionId")||this.currentSessionId,s=this.resolveSessionAlias(t);if(!s)throw new Error("sessionId is required");if(this.promptActive||this.maintenanceActive)throw new Error("Codex session is busy");this.currentSessionId=s;const r=S(),o=await this.maintainContext("manual",{userRequest:this.previousUserText,assistantResponse:this.previousAssistantText,affectedFiles:[...this.previousAffectedFiles]},{force:!0,allowRollover:e.allowRollover===!0,compactTimeoutMs:z(e,r.compactTimeoutMs)}),i=String(o.action||"");if(i==="compact_failed"||i==="rollover_failed"){const c=String(o.compactError||"Codex 上下文压缩未完成");throw new Error(`Codex 上下文压缩失败:${c}`)}return{sessionId:this.currentSessionId,maintenance:o,contextUsage:W(o)}}async closeSession(e){await this.shutdown()}async ensureCodex(e){if(this.rpc&&this.started&&this.started.child.exitCode===null&&this.started.child.signalCode===null)return;if(this.codexStartupPromise){await this.codexStartupPromise;return}const t=(async()=>{if(this.rpc&&this.started&&this.started.child.exitCode===null&&this.started.child.signalCode===null)return;const s=q(e),r=new P(s.child,{peerName:"Codex app-server",onNotification:(o,i)=>this.handleCodexNotification(o,i),onRequest:async(o,i)=>this.handleCodexRequest(o,i)});this.started=s,this.rpc=r,this.initialized=!1,await r.request("initialize",{clientInfo:{name:"aca-codex-acp",version:"0.1.0"},capabilities:{experimentalApi:!0,requestAttestation:!1}},6e4),this.initialized=!0})();this.codexStartupPromise=t;try{await t}catch(s){throw this.rpc&&this.started&&!this.initialized&&await T(this.started.child,1e3),this.rpc=null,this.started=null,s}finally{this.codexStartupPromise===t&&(this.codexStartupPromise=null)}}handleCodexNotification(e,t){if(this.captureContextNotification(e,t),this.maintenanceActive||!this.promptActive&&e!=="turn/completed")return;this.rpc?.refreshPendingRequestTimeout("turn/start"),this.captureAgentMessagePhase(e,t);const s=this.connectionRecovery.beforeNotification(e,t);e==="turn/completed"&&this.resolveTurnCompleted(t);const r=k(e,t,{agentMessagePhases:this.agentMessagePhases,planDeltas:this.planDeltas});for(const o of[...s,...r])this.captureUpdateEvidence(o),this.notifySessionUpdate(o)}notifySessionUpdate(e){this.notify("session/update",{sessionId:this.currentSessionId,update:e})}async handleCodexRequest(e,t){if(b(e))return E(e,t,async s=>{const r=await this.requestParent("session/request_permission",{sessionId:this.currentSessionId,request:{...s,params:t,method:e}}),o=u(r),i=u(o.outcome);return i.outcome==="selected"&&typeof i.optionId=="string"?{outcome:{outcome:"selected",optionId:i.optionId}}:{outcome:{outcome:"cancelled"}}});if(G(e)){const s=await this.requestParent("elicitation/create",{sessionId:this.currentSessionId,request:{...u(t),method:e}}),r=u(s),o=u(r.outcome);return o.outcome==="submitted"?{outcome:{outcome:"submitted",value:typeof o.value=="string"?o.value:""}}:{outcome:{outcome:"cancelled"}}}return{}}captureAgentMessagePhase(e,t){if(e!=="item/started"&&e!=="item/completed")return;const s=h(t,"item");if(a(s,"type")!=="agentMessage")return;const r=a(s,"id");r&&this.agentMessagePhases.set(r,a(s,"phase"))}waitForTurnCompleted(e,t){const s=this.completedTurns.get(e);return s?Promise.resolve(s):new Promise((r,o)=>{const i=setTimeout(()=>{this.pendingTurnCompletions.delete(e),o(new Error("Codex app-server request timed out while waiting for turn/completed"))},t);i.unref(),this.pendingTurnCompletions.set(e,{resolve:r,reject:o,timer:i})})}resolveTurnCompleted(e){const t=h(e,"turn"),s=a(t,"id");if(!s)return;this.completedTurns.set(s,t);const r=this.pendingTurnCompletions.get(s);if(r){if(this.pendingTurnCompletions.delete(s),clearTimeout(r.timer),a(t,"status")==="failed"){r.reject(new Error(`Codex turn failed: ${JSON.stringify(t.error??{})}`));return}r.resolve(t)}}async shutdown(){return this.shutdownPromise?this.shutdownPromise:(this.shutdownPromise=this.performShutdown(),this.shutdownPromise)}async performShutdown(){this.pendingCompaction&&(clearTimeout(this.pendingCompaction.timer),this.pendingCompaction.reject(new Error("Codex ACP adapter closed")),this.pendingCompaction=null);for(const t of this.pendingParentRequests.values())clearTimeout(t.timer),t.reject(new Error("Codex ACP adapter closed"));this.pendingParentRequests.clear();for(const t of this.pendingTurnCompletions.values())clearTimeout(t.timer),t.reject(new Error("Codex ACP adapter closed"));this.pendingTurnCompletions.clear();const e=this.started?.child;!e||e.exitCode!==null||e.signalCode!==null||(e.stdin.end(),!await T(e,2e3)&&(e.kill("SIGTERM"),!await T(e,1e3)&&(e.kill("SIGKILL"),await T(e,1e3))))}sessionResponse(e){return{sessionId:e,modes:{currentModeId:"workspace-write",availableModes:[{id:"read-only",name:"Read Only"},{id:"workspace-write",name:"Workspace Write"},{id:"danger-full-access",name:"Full Access"}]},configOptions:[{id:"model",category:"model",type:"string"},{id:"approvalPolicy",category:"approval",type:"string"},{id:"sandbox",category:"mode",type:"string"}]}}requireRpc(){if(!this.rpc)throw new Error("Codex app-server is not started");return this.rpc}notify(e,t){this.write({jsonrpc:"2.0",method:e,params:t})}requestParent(e,t,s=600*1e3){const r=this.nextParentRequestId++;return new Promise((o,i)=>{const c=setTimeout(()=>{this.pendingParentRequests.delete(r),i(new Error(`ACP client request timed out: ${e}`))},s);c.unref(),this.pendingParentRequests.set(r,{resolve:o,reject:i,timer:c}),this.write({jsonrpc:"2.0",id:r,method:e,params:t})})}handleParentResponse(e){const t=Number(e.id),s=this.pendingParentRequests.get(t);if(s){if(this.pendingParentRequests.delete(t),clearTimeout(s.timer),"error"in e){const r=u(e.error);s.reject(new Error(a(r,"message")||"ACP client request failed"));return}s.resolve(e.result)}}setConfigOption(e){const t=a(e,"configId"),s=e.value;if(!t)throw new Error("configId is required");if(typeof s!="string"&&typeof s!="boolean")throw new Error("config value must be string or boolean");return this.config[t]=s,this.notify("session/update",{sessionId:this.currentSessionId,update:{sessionUpdate:"config_option_update",configId:t,value:s}}),{}}setMode(e){const t=a(e,"modeId");if(!t)throw new Error("modeId is required");return this.config.sandbox=t,this.notify("session/update",{sessionId:this.currentSessionId,update:{sessionUpdate:"current_mode_update",modeId:t}}),{}}applyInitialConfig(e){const t=h(e,"config");for(const[s,r]of Object.entries(t))(typeof r=="string"||typeof r=="boolean")&&(this.config[s]=r)}codexConfig(){return{...typeof this.config.model=="string"&&this.config.model?{model:this.config.model}:{},approvalPolicy:typeof this.config.approvalPolicy=="string"?this.config.approvalPolicy:"on-request",sandbox:typeof this.config.sandbox=="string"?this.config.sandbox:"workspace-write"}}captureContextNotification(e,t){if(e==="thread/tokenUsage/updated"&&(this.latestTokenUsage=F(t)??this.latestTokenUsage),e==="thread/compacted"&&this.pendingCompaction&&this.resolveCompaction(),e==="item/completed"){const s=h(t,"item"),r=a(u(t),"threadId");a(s,"type")==="contextCompaction"&&(!r||r===this.currentSessionId)&&this.resolveCompaction(),a(s,"type")==="agentMessage"&&a(s,"phase")==="final_answer"&&(this.currentAssistantText=f(s)||this.currentAssistantText)}}resolveCompaction(){if(!this.pendingCompaction)return;const e=this.pendingCompaction;this.pendingCompaction=null,clearTimeout(e.timer),e.resolve()}captureUpdateEvidence(e){String(e.sessionUpdate||"")==="agent_message_chunk"&&(this.currentAssistantText+=f(e.content)),x(e,this.currentAffectedFiles)}captureThreadMetadata(e){const t=h(e,"thread");this.currentRolloutPath=a(t,"path")||null,this.threadTurnCount=Array.isArray(t.turns)?t.turns.length:0;const s=B(t.turns);s.userRequest&&(this.previousUserText=s.userRequest),s.assistantResponse&&(this.previousAssistantText=s.assistantResponse),this.previousAffectedFiles.clear();for(const r of s.affectedFiles)this.previousAffectedFiles.add(r);this.latestTokenUsage=null}async maintainContext(e,t,s={}){const r=S(),o=this.contextState(),i=I(o,r);if(!i.compact&&!s.force||!this.currentSessionId)return this.maintenanceResult({stage:e,action:"none",reasons:i.reasons,beforeState:o});const c=this.currentSessionId;this.notifyContextStatus("正在整理会话上下文",A(o)),this.maintenanceActive=!0;let d=!1,l="",g=o,m=i,v=!1,y="";try{const p=o.usage?.observedAtMs??0;await this.compactThread(c,s.compactTimeoutMs??r.compactTimeoutMs),await this.waitForTokenUsageAfter(p,2e3)}catch(p){d=!0,l=p instanceof Error?p.message:String(p)}try{if(g=this.contextState(),m=I(g,r,{compactAttempted:!0,compactFailed:d}),m.rollover&&s.allowRollover!==!1){this.notifyContextStatus("正在创建轻量会话",m.reasons.join(", "));try{await this.rotateThread(c,t),v=!0}catch(p){y=p instanceof Error?p.message:String(p),this.notifyContextStatus("轻量会话创建失败",y)}}else d||this.notifyContextStatus("会话上下文已压缩",A(g))}finally{this.maintenanceActive=!1}return this.maintenanceResult({action:v?"rollover":y?"rollover_failed":d?"compact_failed":"compact",stage:e,reasons:m.reasons,beforeState:o,afterState:g,compactError:[l,y].filter(Boolean).join("; "),previousThreadId:c,rolloverRecommended:m.rollover&&s.allowRollover===!1})}compactThread(e,t){return this.pendingCompaction?Promise.reject(new Error("Codex context compaction is already running")):new Promise((s,r)=>{const o=setTimeout(()=>{this.pendingCompaction?.timer===o&&(this.pendingCompaction=null),r(new Error("Codex thread compaction timed out"))},t);o.unref(),this.pendingCompaction={resolve:s,reject:r,timer:o},this.requireRpc().request("thread/compact/start",{threadId:e},t).catch(i=>{this.pendingCompaction?.timer===o&&(this.pendingCompaction=null,clearTimeout(o),r(i instanceof Error?i:new Error(String(i))))})})}async rotateThread(e,t){const s=U({previousThreadId:e,cwd:this.currentCwd,userRequest:t.userRequest,assistantResponse:t.assistantResponse,affectedFiles:t.affectedFiles}),r=await this.requireRpc().request("thread/start",{cwd:this.currentCwd,approvalsReviewer:"user",...this.codexConfig(),developerInstructions:s,ephemeral:!1,threadSource:"aca-codex-acp-rollover"},6e4),o=C(r);if(!o)throw new Error("Codex rollover thread/start did not return thread id");this.sessionAliases.set(e,o),this.currentSessionId=o,this.captureThreadMetadata(r)}async waitForTokenUsageAfter(e,t){const s=Date.now()+t;for(;Date.now()<s;){if((this.latestTokenUsage?.observedAtMs??0)>e)return;await J(50)}}resolveSessionAlias(e){if(!e)return null;let t=e;const s=new Set;for(;this.sessionAliases.has(t)&&!s.has(t);)s.add(t),t=this.sessionAliases.get(t)??t;return t}contextState(){return{usage:this.latestTokenUsage,rolloutBytes:j(this.currentRolloutPath),turnCount:this.threadTurnCount}}maintenanceResult(e){return{stage:e.stage,action:e.action,reasons:e.reasons,previousThreadId:e.previousThreadId??null,sessionId:this.currentSessionId,before:e.beforeState,after:e.afterState??null,compactError:e.compactError||null,rolloverRecommended:e.rolloverRecommended===!0}}notifyContextStatus(e,t){this.notify("session/update",{sessionId:this.currentSessionId,update:{sessionUpdate:"aca_context_maintenance",label:e,detail:t}})}write(e){process.stdout.write(`${JSON.stringify(e)}
|
|
3
|
-
`)}}function
|
|
1
|
+
import{JsonLineRpcClient as P}from"../../client/json-rpc.js";import{startCodexAppServer as q}from"../codex/codex-process.js";import{CodexConnectionRecoveryTracker as b}from"../codex/connection-events.js";import{codexAcpSessionUpdatesFromNotification as _}from"../codex/events.js";import{CodexPlanDeltaState as k}from"../codex/plan-events.js";import{isCodexApprovalRequest as M,resolveCodexApprovalRequest as E}from"../codex/permissions.js";import{buildCodexContinuityInstructions as U,codexTokenUsageFromNotification as F,decideCodexContextMaintenance as I,readCodexContextMaintenancePolicy as S,rolloutFileSize as N}from"./context-maintenance.js";class j{lineBuffer="";started=null;rpc=null;codexStartupPromise=null;initialized=!1;currentSessionId=null;currentCwd=process.cwd();promptActive=!1;currentTurnId=null;nextParentRequestId=1;pendingParentRequests=new Map;completedTurns=new Map;pendingTurnCompletions=new Map;agentMessagePhases=new Map;planDeltas=new k;config={};sessionAliases=new Map;currentAffectedFiles=new Set;connectionRecovery=new b;latestTokenUsage=null;currentRolloutPath=null;threadTurnCount=0;currentPromptText="";currentAssistantText="";previousUserText="";previousAssistantText="";previousAffectedFiles=new Set;maintenanceActive=!1;pendingCompaction=null;shutdownPromise=null;managedProvider=null;constructor(){process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>this.handleInput(String(t)));const e=()=>{this.shutdown().finally(()=>process.exit(0))};process.stdin.once("end",e),process.stdin.once("close",e),process.stdin.once("error",e),process.once("SIGTERM",()=>{this.shutdown().finally(()=>process.exit(0))}),process.once("SIGINT",()=>{this.shutdown().finally(()=>process.exit(130))})}handleInput(e){this.lineBuffer+=e;const t=this.lineBuffer.split(`
|
|
2
|
+
`);this.lineBuffer=t.pop()??"";for(const s of t){const r=s.trim();if(!r)continue;let o;try{o=JSON.parse(r)}catch{continue}this.handleMessage(o)}}async handleMessage(e){if("id"in e&&("result"in e||"error"in e)&&typeof e.method!="string"){this.handleParentResponse(e);return}const t=typeof e.method=="string"?e.method:"",s=e.id;if(t){if(s===void 0){await this.handleNotification(t,u(e.params));return}try{const r=await this.handleRequest(t,u(e.params));this.write({jsonrpc:"2.0",id:s,result:r})}catch(r){this.write({jsonrpc:"2.0",id:s,error:{code:-32603,message:r instanceof Error?r.message:String(r)}})}}}async handleNotification(e,t){e==="session/cancel"&&await this.cancel(t)}async handleRequest(e,t){switch(e){case"initialize":return this.initializeResponse();case"aca/runtime_config":return this.applyRuntimeConfig(t);case"session/new":return this.newSession(t);case"session/resume":case"session/load":return this.resumeSession(t);case"session/fork":return this.forkSession(t);case"session/prompt":return this.prompt(t);case"session/compact":return this.manualCompact(t);case"session/cancel":return await this.cancel(t),{};case"session/close":return await this.closeSession(t),{};case"session/set_config_option":return this.setConfigOption(t);case"session/set_mode":return this.setMode(t);case"fs/read_text_file":case"fs/write_text_file":case"terminal/create":case"terminal/output":case"terminal/wait_for_exit":case"terminal/kill":case"terminal/release":return{};default:throw new Error(`Unsupported ACP method: ${e}`)}}initializeResponse(){return{protocolVersion:1,agentCapabilities:{loadSession:!0,sessionCapabilities:{resume:!0,close:!0}}}}applyRuntimeConfig(e){if(this.started)throw new Error("Codex runtime config must be applied before app-server starts");const t=u(e.codexProvider);if(Object.keys(t).length===0)return this.managedProvider=null,{configured:!1};const s=a(t,"baseUrl"),r=a(t,"apiKey"),o=Number(t.revision||0);if(!s||!r||!Number.isSafeInteger(o)||o<1)throw new Error("Invalid Codex managed provider runtime config");return this.managedProvider={baseUrl:s,apiKey:r,revision:o},{configured:!0,revision:o}}async newSession(e){const t=a(e,"cwd")||process.cwd();this.applyInitialConfig(e),await this.ensureCodex(t);const s=await this.requireRpc().request("thread/start",{cwd:t,approvalsReviewer:"user",...this.codexConfig(),ephemeral:!1,threadSource:"aca-codex-acp"},6e4),r=C(s);if(!r)throw new Error("Codex thread/start did not return thread id");return this.currentSessionId=r,this.currentCwd=t,this.captureThreadMetadata(s),this.sessionResponse(r)}async resumeSession(e){const t=a(e,"sessionId");if(!t)throw new Error("sessionId is required");const s=a(e,"cwd")||this.currentCwd;this.applyInitialConfig(e),await this.ensureCodex(s);const r=await this.requireRpc().request("thread/resume",{threadId:t,cwd:s,approvalsReviewer:"user",...this.codexConfig()},6e4);return this.currentSessionId=C(r)||t,this.currentCwd=s,this.captureThreadMetadata(r),this.sessionResponse(this.currentSessionId)}async forkSession(e){const t=a(e,"sessionId");if(!t)throw new Error("sessionId is required");const s=a(e,"cwd")||this.currentCwd;this.applyInitialConfig(e),await this.ensureCodex(s);const r=await this.requireRpc().request("thread/fork",{threadId:t,cwd:s,approvalsReviewer:"user",ephemeral:!1,threadSource:"aca-codex-acp-fork",...this.codexConfig()},12e4),o=C(r);if(!o)throw new Error("Codex thread/fork did not return thread id");return this.currentSessionId=o,this.currentCwd=s,this.captureThreadMetadata(r),this.sessionResponse(o)}async prompt(e){const t=a(e,"sessionId")||this.currentSessionId;let s=this.resolveSessionAlias(t);if(!s)throw new Error("sessionId is required");const r=Array.isArray(e.prompt)?e.prompt:[],o=this.requireRpc();this.currentSessionId=s,this.promptActive=!0,this.currentTurnId=null,this.currentPromptText=$(r),this.currentAssistantText="",this.currentAffectedFiles.clear(),this.connectionRecovery.reset(),this.agentMessagePhases.clear(),this.planDeltas.reset();let i;try{i=await o.request("turn/start",{threadId:s,clientUserMessageId:`aca-acp-${Date.now()}-${Math.random().toString(16).slice(2)}`,input:O(r),cwd:this.currentCwd,...D(this.config)},R());const c=String(i?.turn?.id??"");this.currentTurnId=c||null;const d=c?await this.waitForTurnCompleted(c,R()):i?.turn;this.threadTurnCount+=1,this.previousUserText=this.currentPromptText,this.previousAssistantText=this.currentAssistantText,this.previousAffectedFiles.clear();for(const l of this.currentAffectedFiles)this.previousAffectedFiles.add(l);return{stopReason:"end_turn",sessionId:this.currentSessionId,turn:d,_meta:{acaContextUsage:L(this.latestTokenUsage)}}}catch(c){const d=this.connectionRecovery.fail(c);throw d&&this.notifySessionUpdate(d),c}finally{this.promptActive=!1,this.currentTurnId=null}}async cancel(e){const t=a(e,"sessionId")||this.currentSessionId;if(!(!t||!this.rpc)&&(await this.rpc.request("turn/interrupt",{threadId:t}).catch(()=>{}),this.currentTurnId)){const s=this.pendingTurnCompletions.get(this.currentTurnId);s&&(this.pendingTurnCompletions.delete(this.currentTurnId),clearTimeout(s.timer),s.reject(new Error("Codex prompt cancelled")))}}async manualCompact(e){const t=a(e,"sessionId")||this.currentSessionId,s=this.resolveSessionAlias(t);if(!s)throw new Error("sessionId is required");if(this.promptActive||this.maintenanceActive)throw new Error("Codex session is busy");this.currentSessionId=s;const r=S(),o=await this.maintainContext("manual",{userRequest:this.previousUserText,assistantResponse:this.previousAssistantText,affectedFiles:[...this.previousAffectedFiles]},{force:!0,allowRollover:e.allowRollover===!0,compactTimeoutMs:z(e,r.compactTimeoutMs)}),i=String(o.action||"");if(i==="compact_failed"||i==="rollover_failed"){const c=String(o.compactError||"Codex 上下文压缩未完成");throw new Error(`Codex 上下文压缩失败:${c}`)}return{sessionId:this.currentSessionId,maintenance:o,contextUsage:W(o)}}async closeSession(e){await this.shutdown()}async ensureCodex(e){if(this.rpc&&this.started&&this.started.child.exitCode===null&&this.started.child.signalCode===null)return;if(this.codexStartupPromise){await this.codexStartupPromise;return}const t=(async()=>{if(this.rpc&&this.started&&this.started.child.exitCode===null&&this.started.child.signalCode===null)return;const s=q(e,this.managedProvider),r=new P(s.child,{peerName:"Codex app-server",onNotification:(o,i)=>this.handleCodexNotification(o,i),onRequest:async(o,i)=>this.handleCodexRequest(o,i)});this.started=s,this.rpc=r,this.initialized=!1,await r.request("initialize",{clientInfo:{name:"aca-codex-acp",version:"0.1.0"},capabilities:{experimentalApi:!0,requestAttestation:!1}},6e4),this.initialized=!0})();this.codexStartupPromise=t;try{await t}catch(s){throw this.rpc&&this.started&&!this.initialized&&await T(this.started.child,1e3),this.rpc=null,this.started=null,s}finally{this.codexStartupPromise===t&&(this.codexStartupPromise=null)}}handleCodexNotification(e,t){if(this.captureContextNotification(e,t),this.maintenanceActive||!this.promptActive&&e!=="turn/completed")return;this.rpc?.refreshPendingRequestTimeout("turn/start"),this.captureAgentMessagePhase(e,t);const s=this.connectionRecovery.beforeNotification(e,t);e==="turn/completed"&&this.resolveTurnCompleted(t);const r=_(e,t,{agentMessagePhases:this.agentMessagePhases,planDeltas:this.planDeltas});for(const o of[...s,...r])this.captureUpdateEvidence(o),this.notifySessionUpdate(o)}notifySessionUpdate(e){this.notify("session/update",{sessionId:this.currentSessionId,update:e})}async handleCodexRequest(e,t){if(M(e))return E(e,t,async s=>{const r=await this.requestParent("session/request_permission",{sessionId:this.currentSessionId,request:{...s,params:t,method:e}}),o=u(r),i=u(o.outcome);return i.outcome==="selected"&&typeof i.optionId=="string"?{outcome:{outcome:"selected",optionId:i.optionId}}:{outcome:{outcome:"cancelled"}}});if(G(e)){const s=await this.requestParent("elicitation/create",{sessionId:this.currentSessionId,request:{...u(t),method:e}}),r=u(s),o=u(r.outcome);return o.outcome==="submitted"?{outcome:{outcome:"submitted",value:typeof o.value=="string"?o.value:""}}:{outcome:{outcome:"cancelled"}}}return{}}captureAgentMessagePhase(e,t){if(e!=="item/started"&&e!=="item/completed")return;const s=h(t,"item");if(a(s,"type")!=="agentMessage")return;const r=a(s,"id");r&&this.agentMessagePhases.set(r,a(s,"phase"))}waitForTurnCompleted(e,t){const s=this.completedTurns.get(e);return s?Promise.resolve(s):new Promise((r,o)=>{const i=setTimeout(()=>{this.pendingTurnCompletions.delete(e),o(new Error("Codex app-server request timed out while waiting for turn/completed"))},t);i.unref(),this.pendingTurnCompletions.set(e,{resolve:r,reject:o,timer:i})})}resolveTurnCompleted(e){const t=h(e,"turn"),s=a(t,"id");if(!s)return;this.completedTurns.set(s,t);const r=this.pendingTurnCompletions.get(s);if(r){if(this.pendingTurnCompletions.delete(s),clearTimeout(r.timer),a(t,"status")==="failed"){r.reject(new Error(`Codex turn failed: ${JSON.stringify(t.error??{})}`));return}r.resolve(t)}}async shutdown(){return this.shutdownPromise?this.shutdownPromise:(this.shutdownPromise=this.performShutdown(),this.shutdownPromise)}async performShutdown(){this.pendingCompaction&&(clearTimeout(this.pendingCompaction.timer),this.pendingCompaction.reject(new Error("Codex ACP adapter closed")),this.pendingCompaction=null);for(const t of this.pendingParentRequests.values())clearTimeout(t.timer),t.reject(new Error("Codex ACP adapter closed"));this.pendingParentRequests.clear();for(const t of this.pendingTurnCompletions.values())clearTimeout(t.timer),t.reject(new Error("Codex ACP adapter closed"));this.pendingTurnCompletions.clear();const e=this.started?.child;!e||e.exitCode!==null||e.signalCode!==null||(e.stdin.end(),!await T(e,2e3)&&(e.kill("SIGTERM"),!await T(e,1e3)&&(e.kill("SIGKILL"),await T(e,1e3))))}sessionResponse(e){return{sessionId:e,modes:{currentModeId:"workspace-write",availableModes:[{id:"read-only",name:"Read Only"},{id:"workspace-write",name:"Workspace Write"},{id:"danger-full-access",name:"Full Access"}]},configOptions:[{id:"model",category:"model",type:"string"},{id:"approvalPolicy",category:"approval",type:"string"},{id:"sandbox",category:"mode",type:"string"}]}}requireRpc(){if(!this.rpc)throw new Error("Codex app-server is not started");return this.rpc}notify(e,t){this.write({jsonrpc:"2.0",method:e,params:t})}requestParent(e,t,s=600*1e3){const r=this.nextParentRequestId++;return new Promise((o,i)=>{const c=setTimeout(()=>{this.pendingParentRequests.delete(r),i(new Error(`ACP client request timed out: ${e}`))},s);c.unref(),this.pendingParentRequests.set(r,{resolve:o,reject:i,timer:c}),this.write({jsonrpc:"2.0",id:r,method:e,params:t})})}handleParentResponse(e){const t=Number(e.id),s=this.pendingParentRequests.get(t);if(s){if(this.pendingParentRequests.delete(t),clearTimeout(s.timer),"error"in e){const r=u(e.error);s.reject(new Error(a(r,"message")||"ACP client request failed"));return}s.resolve(e.result)}}setConfigOption(e){const t=a(e,"configId"),s=e.value;if(!t)throw new Error("configId is required");if(typeof s!="string"&&typeof s!="boolean")throw new Error("config value must be string or boolean");return this.config[t]=s,this.notify("session/update",{sessionId:this.currentSessionId,update:{sessionUpdate:"config_option_update",configId:t,value:s}}),{}}setMode(e){const t=a(e,"modeId");if(!t)throw new Error("modeId is required");return this.config.sandbox=t,this.notify("session/update",{sessionId:this.currentSessionId,update:{sessionUpdate:"current_mode_update",modeId:t}}),{}}applyInitialConfig(e){const t=h(e,"config");for(const[s,r]of Object.entries(t))(typeof r=="string"||typeof r=="boolean")&&(this.config[s]=r)}codexConfig(){return{...typeof this.config.model=="string"&&this.config.model?{model:this.config.model}:{},approvalPolicy:typeof this.config.approvalPolicy=="string"?this.config.approvalPolicy:"on-request",sandbox:typeof this.config.sandbox=="string"?this.config.sandbox:"workspace-write"}}captureContextNotification(e,t){if(e==="thread/tokenUsage/updated"&&(this.latestTokenUsage=F(t)??this.latestTokenUsage),e==="thread/compacted"&&this.pendingCompaction&&this.resolveCompaction(),e==="item/completed"){const s=h(t,"item"),r=a(u(t),"threadId");a(s,"type")==="contextCompaction"&&(!r||r===this.currentSessionId)&&this.resolveCompaction(),a(s,"type")==="agentMessage"&&a(s,"phase")==="final_answer"&&(this.currentAssistantText=f(s)||this.currentAssistantText)}}resolveCompaction(){if(!this.pendingCompaction)return;const e=this.pendingCompaction;this.pendingCompaction=null,clearTimeout(e.timer),e.resolve()}captureUpdateEvidence(e){String(e.sessionUpdate||"")==="agent_message_chunk"&&(this.currentAssistantText+=f(e.content)),x(e,this.currentAffectedFiles)}captureThreadMetadata(e){const t=h(e,"thread");this.currentRolloutPath=a(t,"path")||null,this.threadTurnCount=Array.isArray(t.turns)?t.turns.length:0;const s=B(t.turns);s.userRequest&&(this.previousUserText=s.userRequest),s.assistantResponse&&(this.previousAssistantText=s.assistantResponse),this.previousAffectedFiles.clear();for(const r of s.affectedFiles)this.previousAffectedFiles.add(r);this.latestTokenUsage=null}async maintainContext(e,t,s={}){const r=S(),o=this.contextState(),i=I(o,r);if(!i.compact&&!s.force||!this.currentSessionId)return this.maintenanceResult({stage:e,action:"none",reasons:i.reasons,beforeState:o});const c=this.currentSessionId;this.notifyContextStatus("正在整理会话上下文",A(o)),this.maintenanceActive=!0;let d=!1,l="",g=o,m=i,v=!1,y="";try{const p=o.usage?.observedAtMs??0;await this.compactThread(c,s.compactTimeoutMs??r.compactTimeoutMs),await this.waitForTokenUsageAfter(p,2e3)}catch(p){d=!0,l=p instanceof Error?p.message:String(p)}try{if(g=this.contextState(),m=I(g,r,{compactAttempted:!0,compactFailed:d}),m.rollover&&s.allowRollover!==!1){this.notifyContextStatus("正在创建轻量会话",m.reasons.join(", "));try{await this.rotateThread(c,t),v=!0}catch(p){y=p instanceof Error?p.message:String(p),this.notifyContextStatus("轻量会话创建失败",y)}}else d||this.notifyContextStatus("会话上下文已压缩",A(g))}finally{this.maintenanceActive=!1}return this.maintenanceResult({action:v?"rollover":y?"rollover_failed":d?"compact_failed":"compact",stage:e,reasons:m.reasons,beforeState:o,afterState:g,compactError:[l,y].filter(Boolean).join("; "),previousThreadId:c,rolloverRecommended:m.rollover&&s.allowRollover===!1})}compactThread(e,t){return this.pendingCompaction?Promise.reject(new Error("Codex context compaction is already running")):new Promise((s,r)=>{const o=setTimeout(()=>{this.pendingCompaction?.timer===o&&(this.pendingCompaction=null),r(new Error("Codex thread compaction timed out"))},t);o.unref(),this.pendingCompaction={resolve:s,reject:r,timer:o},this.requireRpc().request("thread/compact/start",{threadId:e},t).catch(i=>{this.pendingCompaction?.timer===o&&(this.pendingCompaction=null,clearTimeout(o),r(i instanceof Error?i:new Error(String(i))))})})}async rotateThread(e,t){const s=U({previousThreadId:e,cwd:this.currentCwd,userRequest:t.userRequest,assistantResponse:t.assistantResponse,affectedFiles:t.affectedFiles}),r=await this.requireRpc().request("thread/start",{cwd:this.currentCwd,approvalsReviewer:"user",...this.codexConfig(),developerInstructions:s,ephemeral:!1,threadSource:"aca-codex-acp-rollover"},6e4),o=C(r);if(!o)throw new Error("Codex rollover thread/start did not return thread id");this.sessionAliases.set(e,o),this.currentSessionId=o,this.captureThreadMetadata(r)}async waitForTokenUsageAfter(e,t){const s=Date.now()+t;for(;Date.now()<s;){if((this.latestTokenUsage?.observedAtMs??0)>e)return;await J(50)}}resolveSessionAlias(e){if(!e)return null;let t=e;const s=new Set;for(;this.sessionAliases.has(t)&&!s.has(t);)s.add(t),t=this.sessionAliases.get(t)??t;return t}contextState(){return{usage:this.latestTokenUsage,rolloutBytes:N(this.currentRolloutPath),turnCount:this.threadTurnCount}}maintenanceResult(e){return{stage:e.stage,action:e.action,reasons:e.reasons,previousThreadId:e.previousThreadId??null,sessionId:this.currentSessionId,before:e.beforeState,after:e.afterState??null,compactError:e.compactError||null,rolloverRecommended:e.rolloverRecommended===!0}}notifyContextStatus(e,t){this.notify("session/update",{sessionId:this.currentSessionId,update:{sessionUpdate:"aca_context_maintenance",label:e,detail:t}})}write(e){process.stdout.write(`${JSON.stringify(e)}
|
|
3
|
+
`)}}function O(n){return n.map(e=>{const t=u(e);if(t.type==="image"){const s=a(t,"uri");if(s)return{type:"localImage",path:s};const r=a(t,"data");return{type:"image",url:`data:${a(t,"mimeType")||"image/png"};base64,${r}`}}return{type:"text",text:a(t,"text"),text_elements:[]}})}function $(n){return n.map(e=>a(u(e),"text")).filter(Boolean).join(`
|
|
4
4
|
|
|
5
|
-
`)}function
|
|
5
|
+
`)}function D(n){const e={};typeof n.model=="string"&&n.model&&(e.model=n.model),typeof n.approvalPolicy=="string"&&n.approvalPolicy&&(e.approvalPolicy=n.approvalPolicy),typeof n.sandbox=="string"&&n.sandbox&&(e.sandbox=n.sandbox);const t=n.reasoning_effort??n.reasoningEffort??n.model_reasoning_effort??n.modelReasoningEffort??n.effort;return typeof t=="string"&&t&&(e.effort=t),e}function C(n){const e=h(n,"thread");return a(e,"id")||null}function h(n,e){const t=u(n);return u(t[e])}function u(n){return n&&typeof n=="object"&&!Array.isArray(n)?n:{}}function f(n){if(typeof n=="string")return n;if(Array.isArray(n))return n.map(f).filter(Boolean).join("");if(!n||typeof n!="object")return"";const e=u(n);for(const t of["text","message","content","output_text"]){const s=f(e[t]);if(s)return s}return""}function x(n,e){if(Array.isArray(n)){for(const o of n)x(o,e);return}const t=u(n),s=a(t,"type").toLowerCase(),r=a(t,"path");if(r&&["diff","resource_link","content"].includes(s)&&e.add(r),Array.isArray(t.locations))for(const o of t.locations){const i=a(u(o),"path");i&&e.add(i)}for(const o of[t.content,t.update,t.rawInput,t.rawOutput])o&&o!==n&&x(o,e)}function B(n){if(!Array.isArray(n)||n.length===0)return{userRequest:"",assistantResponse:"",affectedFiles:[]};const e=n.at(-1),t=new Set;x(e,t);let s="",r="";const o=i=>{if(Array.isArray(i)){for(const l of i)o(l);return}if(!i||typeof i!="object")return;const c=u(i),d=a(c,"type").toLowerCase();d==="usermessage"&&(s=f(c)||s),d==="agentmessage"&&(r=f(c)||r);for(const l of["items","content","input"])c[l]&&o(c[l])};return o(e),{userRequest:s,assistantResponse:r,affectedFiles:[...t]}}function z(n,e){const t=w(n,"timeoutMs");return!Number.isFinite(t)||t<=0?e:Math.max(3e4,Math.min(59e4,t-1e4))}function W(n){const e=String(n.action||"none"),t=u(n.after),s=u(n.before),r=u(t.usage),o=u(s.usage),i=Object.keys(r).length>0?r:o,c=w(i,"contextWindow");if(e==="rollover")return{usedTokens:0,inputTokens:0,contextWindow:c,ratio:0,observedAtMs:Date.now(),state:"new_thread",maintenanceAction:e};if(Object.keys(i).length===0)return null;const d=w(i,"totalTokens"),l=w(i,"inputTokens");return{usedTokens:d||l,inputTokens:l,contextWindow:c,ratio:c>0?(d||l)/c:0,observedAtMs:w(i,"observedAtMs")||Date.now(),state:e==="compact"?"compacted":"active",maintenanceAction:e}}function L(n){return n?{usedTokens:n.totalTokens||n.inputTokens,inputTokens:n.inputTokens,contextWindow:n.contextWindow,ratio:n.contextWindow>0?(n.totalTokens||n.inputTokens)/n.contextWindow:0,observedAtMs:n.observedAtMs,state:"active",maintenanceAction:null}:null}function A(n){const e=n.usage?`${n.usage.inputTokens} tokens`:"token unknown",t=n.rolloutBytes>0?`${Math.round(n.rolloutBytes/1024/1024)}MB rollout`:"rollout unknown";return`${e}, ${t}, ${n.turnCount} turns`}function w(n,e){const t=u(n)[e];return typeof t=="number"&&Number.isFinite(t)?t:0}function a(n,e){const s=u(n)[e];return typeof s=="string"?s:""}function G(n){const e=n.toLowerCase();return e.includes("elicitation")||e.includes("requestuserinput")||e.includes("request_user_input")||e.includes("input/request")}function R(){const n=Number.parseInt(process.env.ACA_ACP_PROMPT_TIMEOUT_MS??"",10);return Number.isInteger(n)&&n>=3e4?n:360*60*1e3}function T(n,e){return n.exitCode!==null||n.signalCode!==null?Promise.resolve(!0):new Promise(t=>{const s=setTimeout(()=>t(!1),e);s.unref(),n.once("close",()=>{clearTimeout(s),t(!0)})})}function J(n){return new Promise(e=>setTimeout(e,n))}function K(){new j}const V=process.argv[1]||"";/(?:^|[\\/])adapter\.(?:js|ts)$/.test(V)&&K();export{K as runCodexAcpAdapter};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import d from"node:fs";import{runAbortableRequest as S}from"../../client/abortable-request.js";import{BoundedItemBuffer as A}from"../../client/bounded-buffer.js";import{promptBlocksToAcpContent as v}from"../../client/acp-content.js";import{eventFromAcpSessionUpdate as x,statusFromAcpSessionUpdate as P}from"../../client/acp-events.js";import{defaultAcpRuntimePool as w}from"../../client/acp-runtime-pool.js";import{resolveCodexAcpLaunch as g}from"./launch.js";import{CodexPromptPerformanceTracker as k,readCodexContextMaintenancePolicy as M}from"./context-maintenance.js";const R={sessionResume:!0,imageInput:!0,fileAttachment:!0,filesystem:!0,terminal:!0,permissionRequest:!0,configOptions:!0,usage:!0,contextUsage:!0,contextCompaction:!0,plan:!0,diff:!0};class W{id="codex-acp";name="Codex ACP";capabilities=R;async createSession(e){return new T(e.cwd,e.providerSessionId??null,e.timeoutMs)}async forkSession(e){if(!d.existsSync(e.cwd)||!d.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const s=e.sourceProviderSessionId.trim(),t=await w.acquire({providerId:"codex-acp",cwd:e.cwd,providerSessionId:s,...g(),onSessionUpdate:()=>{}});try{t.initialized||(await t.client.initialize(6e4),t.markInitialized()),e.onStatus?.({phase:"forking",label:"正在分叉 Codex 会话",detail:s,updateType:"session/fork",atMs:Date.now()});const r=await t.client.forkSession({sessionId:s,cwd:e.cwd,config:y(e)},e.timeoutMs??12e4),n=p(r);if(!n)throw new Error("Codex ACP session/fork did not return sessionId");return{providerSessionId:n}}finally{t.release()}}}class T{cwd;providerSessionId;defaultTimeoutMs;lease=null;sessionResponse=null;statusCallback=null;updateCallback=null;permissionCallback=null;userInputCallback=null;abortHandler=null;statusTimeline=[];updates=new A(16);content="";performanceTracker=null;constructor(e,s,t=U()){this.cwd=e,this.providerSessionId=s,this.defaultTimeoutMs=t}async sendPrompt(e){if(!d.existsSync(e.cwd)||!d.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const s=Date.now();this.statusCallback=e.onStatus??null,this.updateCallback=e.onUpdate??null,this.permissionCallback=e.onPermissionRequest??null,this.userInputCallback=e.onUserInputRequest??null,this.attachAbortSignal(e.signal),this.content="",this.updates.clear(),this.statusTimeline.length=0,this.performanceTracker=new k(s);let t=null,r=null,n=this.providerSessionId,i,c=null;try{this.emitStatus("initializing","Codex ACP 启动中","codex-acp");const a=await this.acquireRuntime(e.cwd);this.lease=a,t=a.client,r=a.started,await this.initialize(a),n=await this.establishSession(a,e),await this.applyConfig(t,n,e),this.emitStatus("thinking","思考中"),c=this.startSlowStatusTimer(),i=await this.promptWithAbort(t,{sessionId:n,prompt:v(e.prompt,e.promptBlocks),timeoutMs:e.timeoutMs??this.defaultTimeoutMs,signal:e.signal});const u=C(i);u&&u!==n&&(n=u,this.providerSessionId=u,a.setProviderSession({providerSessionId:u,sessionResponse:this.sessionResponse}))}finally{c&&clearInterval(c),this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}return this.emitStatus("completed","已完成"),{providerSessionId:n,cliType:"builtin",agentType:"codex",effectiveConfig:{model:e.model??null,mode:e.mode??null,configOptionValues:e.configOptionValues??null,command:r.command,processPid:r.child.pid??null,provider:"codex-acp",protocol:"acp"},command:r.command,args:r.args,cwd:e.cwd,exitCode:r.child.exitCode,signal:r.child.signalCode,durationMs:Date.now()-s,stdout:t.rpc.stdout,stderr:t.rpc.stderr,content:this.content.trim(),updateCount:this.updates.count(),updates:this.updates.snapshot(),statusTimeline:[...this.statusTimeline],performance:this.performanceTracker?.snapshot(),contextUsage:D(i),promptResponse:i}}async compactContext(e){if(!d.existsSync(e.cwd)||!d.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const s=Date.now();this.statusCallback=e.onStatus??null,this.updateCallback=e.onUpdate??null,this.attachAbortSignal(e.signal);let t=this.providerSessionId,r;try{this.emitStatus("initializing","Codex ACP 启动中","codex-acp");const i=await this.acquireRuntime(e.cwd);this.lease=i,await this.initialize(i),t=await this.establishSession(i,e),await this.applyConfig(i.client,t,e);const c=O(e.timeoutMs);r=await this.compactWithAbort(i.client,{sessionId:t,allowRollover:e.allowRollover===!0,timeoutMs:c,signal:e.signal});const a=C(r);a&&a!==t&&(t=a,this.providerSessionId=a,i.setProviderSession({providerSessionId:a,sessionResponse:this.sessionResponse}))}catch(i){if(this.lease&&q(i,e.signal)){const c=this.lease;this.lease=null,await c.dispose().catch(()=>{})}throw this.emitStatus("failed","上下文压缩失败",i instanceof Error?i.message:String(i)),i}finally{this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}const n=l(r)??{};return{providerSessionId:t||this.providerSessionId||"",contextUsage:I(n.contextUsage),maintenance:l(n.maintenance)??{},durationMs:Date.now()-s}}async cancel(){this.providerSessionId&&this.lease?.client.cancel(this.providerSessionId)}async close(){this.statusCallback=null,this.updateCallback=null,this.permissionCallback=null,this.userInputCallback=null,this.abortHandler&&(this.abortHandler=null),this.releaseLease()}async acquireRuntime(e){const s=g();return w.acquire({providerId:"codex-acp",cwd:e,providerSessionId:this.providerSessionId,command:s.command,args:s.args,onSessionUpdate:t=>this.handleSessionUpdate(t),onClientRequest:async(t,r)=>{if(t==="session/request_permission")return this.requestPermission(r);if(t==="elicitation/create"||t==="session/request_user_input")return this.requestUserInput(r);throw new Error(`Unsupported Codex ACP client request: ${t}`)}})}async initialize(e){e.initialized||(await e.client.initialize(6e4),e.markInitialized())}async establishSession(e,s){const t=e.client,r=y(s);if(e.providerSessionId&&e.sessionEstablished)return this.providerSessionId=e.providerSessionId,this.sessionResponse=e.sessionResponse,e.providerSessionId;if(this.providerSessionId)try{this.emitStatus("resuming","Codex ACP 会话恢复中","session/load");const c=await t.loadSession({sessionId:this.providerSessionId,cwd:s.cwd,config:r},6e4);this.sessionResponse=l(c);const a=p(c)||this.providerSessionId;return this.providerSessionId=a,e.setProviderSession({providerSessionId:a,sessionResponse:this.sessionResponse}),a}catch(c){if(s.requireSessionResume){const a=c instanceof Error?c.message:String(c);throw new Error(`无法恢复导入的 Codex ACP Session ${this.providerSessionId}:${a}`)}this.providerSessionId=null}this.emitStatus("acp","Codex ACP 会话创建中","session/new");const n=await t.newSession({cwd:s.cwd,config:r},12e4);this.sessionResponse=l(n);const i=p(n);if(!i)throw new Error("Codex ACP session/new did not return sessionId");return this.providerSessionId=i,e.setProviderSession({providerSessionId:i,sessionResponse:this.sessionResponse}),i}async applyConfig(e,s,t){t.model&&await e.setSessionConfigOption({sessionId:s,configId:"model",value:t.model},6e4).catch(()=>{});const r=f(t.mode);r&&await e.setSessionConfigOption({sessionId:s,configId:"approvalPolicy",value:r},6e4).catch(()=>{});const n=h(t.mode);n&&await e.setSessionConfigOption({sessionId:s,configId:"sandbox",value:n},6e4).catch(()=>{});for(const[i,c]of Object.entries(t.configOptionValues??{}))["model","mode","sandbox","approvalPolicy"].includes(i)||await e.setSessionConfigOption({sessionId:s,configId:i,value:c},6e4).catch(()=>{})}handleSessionUpdate(e){this.performanceTracker?.recordEvent(),this.updates.push({method:"session/update",params:e});const s=x(e);s&&(s.type==="agent_message_chunk"&&(this.content+=s.text??""),this.updateCallback?.(s));const t=P(e);t&&this.emitStatus(t.phase,t.label,t.detail,t.updateType)}startSlowStatusTimer(){const e=M().slowEventThresholdMs,s=setInterval(()=>{const t=this.performanceTracker,r=Date.now();if(!t?.shouldReportSlow(r,e))return;const n=t.snapshot(r),i=Math.max(1,Math.round(n.silentForMs/6e4));this.emitStatus("thinking","模型长时间处理中",`已 ${i} 分钟没有收到新的 ACP 事件`,"aca.performance.slow")},Math.min(3e4,Math.max(1e3,Math.floor(e/4))));return s.unref(),s}attachAbortSignal(e){if(e){if(this.abortHandler=()=>{this.cancel()},e.aborted)throw new Error("Codex ACP prompt cancelled");e.addEventListener("abort",this.abortHandler,{once:!0})}}async promptWithAbort(e,s){return S({signal:s.signal,cancelledMessage:"Codex ACP prompt cancelled",startRequest:()=>e.prompt({sessionId:s.sessionId,prompt:s.prompt},s.timeoutMs),onAbort:async()=>{e.cancel(s.sessionId),await this.lease?.dispose()}})}async compactWithAbort(e,s){return S({signal:s.signal,cancelledMessage:"Codex ACP 上下文压缩已取消",startRequest:()=>e.compactSession({sessionId:s.sessionId,allowRollover:s.allowRollover,timeoutMs:s.timeoutMs},s.timeoutMs),onAbort:async()=>{e.cancel(s.sessionId),await this.lease?.dispose()}})}async requestPermission(e){if(!this.permissionCallback)return{outcome:{outcome:"cancelled"}};const s=e&&typeof e=="object"&&!Array.isArray(e)?e:{},t=s.request&&typeof s.request=="object"&&!Array.isArray(s.request)?s.request:s,n=(Array.isArray(t.options)?t.options:[]).map((i,c)=>{const a=i&&typeof i=="object"&&!Array.isArray(i)?i:{};return{optionId:String(a.optionId??a.id??c),kind:String(a.kind??"choice"),...typeof a.label=="string"?{label:a.label}:{}}});return this.emitStatus("requestPermission","等待授权",_(t),"session/request_permission"),this.permissionCallback({requestId:String(t.requestId??t.id??`codex-acp-permission-${Date.now()}`),params:e,options:n})}async requestUserInput(e){if(!this.userInputCallback)return{outcome:{outcome:"cancelled"}};const s=e&&typeof e=="object"&&!Array.isArray(e)?e:{},t=s.request&&typeof s.request=="object"&&!Array.isArray(s.request)?s.request:s;return this.emitStatus("requestPermission","等待输入",E(t),"elicitation/create"),this.userInputCallback({requestId:String(t.requestId??t.id??`codex-acp-input-${Date.now()}`),prompt:String(t.prompt??t.message??t.question??""),params:e,...typeof t.defaultValue=="string"?{defaultValue:t.defaultValue}:{}})}emitStatus(e,s,t,r){const n={phase:e,label:s,...t?{detail:t}:{},...r?{updateType:r}:{},atMs:Date.now()},i=this.statusTimeline[this.statusTimeline.length-1];i&&i.phase===n.phase&&i.label===n.label&&i.detail===n.detail&&i.updateType===n.updateType||(this.statusTimeline.push(n),this.statusCallback?.(n))}releaseLease(){this.lease?.release(),this.lease=null}}function q(o,e){if(e?.aborted)return!0;const s=o instanceof Error?o.message:String(o||"");return/compact.*timed out|request timed out.*compact|adapter closed|aborted|cancelled/i.test(s)}function p(o){if(!o||typeof o!="object"||Array.isArray(o))return null;const e=o.sessionId;return typeof e=="string"&&e.trim()?e.trim():null}function l(o){return o&&typeof o=="object"&&!Array.isArray(o)?o:null}function h(o){const e=String(o||"").trim();return e?e==="read-only"?"read-only":["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"danger-full-access":"workspace-write":null}function f(o){const e=String(o||"").trim();return e?["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"never":e==="acceptEdits"||e==="accept-edits"?"on-failure":"on-request":null}function y(o){return{...o.model?{model:o.model}:{},...f(o.mode)?{approvalPolicy:f(o.mode)}:{},...h(o.mode)?{sandbox:h(o.mode)}:{},...o.configOptionValues??{}}}function _(o){const e=o.params&&typeof o.params=="object"&&!Array.isArray(o.params)?o.params:o;for(const s of["command","cwd","reason","grantRoot","method"]){const t=e[s];if(typeof t=="string"&&t.trim())return b(t)}}function E(o){for(const e of["prompt","message","question","title"]){const s=o[e];if(typeof s=="string"&&s.trim())return b(s)}}function b(o){const e=o.replace(/\s+/g," ").trim();return e.length>120?`${e.slice(0,117)}...`:e}function U(){const o=Number.parseInt(process.env.ACA_CODEX_ACP_PROMPT_TIMEOUT_MS??process.env.ACA_ACP_PROMPT_TIMEOUT_MS??"",10);return Number.isInteger(o)&&o>=3e4?o:360*60*1e3}function O(o){const e=Number.isInteger(o)?Number(o):Number.parseInt(process.env.ACA_CODEX_ACP_COMPACT_TIMEOUT_MS??"",10);return Number.isFinite(e)&&e>=3e4?Math.min(e,6e5):12e4}function C(o){if(!o||typeof o!="object"||Array.isArray(o))return null;const e=o;return typeof e.sessionId=="string"&&e.sessionId.trim()?e.sessionId:null}function D(o){const e=l(o),s=l(e?._meta);return I(s?.acaContextUsage)}function I(o){const e=l(o);if(!e)return null;const s=m(e.contextWindow),t=m(e.usedTokens),r=m(e.inputTokens),n=String(e.state||"unknown"),i=["active","compacted","new_thread"].includes(n)?n:"unknown";return{usedTokens:t,inputTokens:r,contextWindow:s,ratio:s>0?Math.max(0,m(e.ratio)||t/s):0,observedAtMs:m(e.observedAtMs)||Date.now(),state:i,maintenanceAction:typeof e.maintenanceAction=="string"?e.maintenanceAction:null}}function m(o){return typeof o=="number"&&Number.isFinite(o)&&o>=0?o:0}export{W as CodexAcpProvider,R as codexAcpCapabilities};
|
|
1
|
+
import d from"node:fs";import{runAbortableRequest as w}from"../../client/abortable-request.js";import{BoundedItemBuffer as P}from"../../client/bounded-buffer.js";import{promptBlocksToAcpContent as k}from"../../client/acp-content.js";import{eventFromAcpSessionUpdate as R,statusFromAcpSessionUpdate as M}from"../../client/acp-events.js";import{defaultAcpRuntimePool as S}from"../../client/acp-runtime-pool.js";import{resolveCodexAcpLaunch as y}from"./launch.js";import{CodexPromptPerformanceTracker as T,readCodexContextMaintenancePolicy as _}from"./context-maintenance.js";import{ensureCodexProviderRuntimeConfig as g}from"../../../core/codex-provider-runtime-config.js";const q={sessionResume:!0,imageInput:!0,fileAttachment:!0,filesystem:!0,terminal:!0,permissionRequest:!0,configOptions:!0,usage:!0,contextUsage:!0,contextCompaction:!0,plan:!0,diff:!0};class Y{id="codex-acp";name="Codex ACP";capabilities=q;async createSession(e){return new E(e.cwd,e.providerSessionId??null,e.timeoutMs)}async forkSession(e){if(!d.existsSync(e.cwd)||!d.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const s=e.sourceProviderSessionId.trim(),o=await g(),r=await S.acquire({providerId:"codex-acp",cwd:e.cwd,providerSessionId:s,runtimeIdentity:o?.runtimeIdentity||"default",env:b(o),...y(),onSessionUpdate:()=>{}});try{await I(r,o),r.initialized||(await r.client.initialize(6e4),r.markInitialized()),e.onStatus?.({phase:"forking",label:"正在分叉 Codex 会话",detail:s,updateType:"session/fork",atMs:Date.now()});const n=await r.client.forkSession({sessionId:s,cwd:e.cwd,config:C(e)},e.timeoutMs??12e4),i=p(n);if(!i)throw new Error("Codex ACP session/fork did not return sessionId");return{providerSessionId:i}}finally{r.release()}}}class E{cwd;providerSessionId;defaultTimeoutMs;lease=null;sessionResponse=null;statusCallback=null;updateCallback=null;permissionCallback=null;userInputCallback=null;abortHandler=null;statusTimeline=[];updates=new P(16);content="";performanceTracker=null;constructor(e,s,o=j()){this.cwd=e,this.providerSessionId=s,this.defaultTimeoutMs=o}async sendPrompt(e){if(!d.existsSync(e.cwd)||!d.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const s=Date.now();this.statusCallback=e.onStatus??null,this.updateCallback=e.onUpdate??null,this.permissionCallback=e.onPermissionRequest??null,this.userInputCallback=e.onUserInputRequest??null,this.attachAbortSignal(e.signal),this.content="",this.updates.clear(),this.statusTimeline.length=0,this.performanceTracker=new T(s);let o=null,r=null,n=this.providerSessionId,i,c=null;try{this.emitStatus("initializing","Codex ACP 启动中","codex-acp");const a=await this.acquireRuntime(e.cwd);this.lease=a,o=a.client,r=a.started,await this.initialize(a),n=await this.establishSession(a,e),await this.applyConfig(o,n,e),this.emitStatus("thinking","思考中"),c=this.startSlowStatusTimer(),i=await this.promptWithAbort(o,{sessionId:n,prompt:k(e.prompt,e.promptBlocks),timeoutMs:e.timeoutMs??this.defaultTimeoutMs,signal:e.signal});const u=A(i);u&&u!==n&&(n=u,this.providerSessionId=u,a.setProviderSession({providerSessionId:u,sessionResponse:this.sessionResponse}))}finally{c&&clearInterval(c),this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}return this.emitStatus("completed","已完成"),{providerSessionId:n,cliType:"builtin",agentType:"codex",effectiveConfig:{model:e.model??null,mode:e.mode??null,configOptionValues:e.configOptionValues??null,command:r.command,processPid:r.child.pid??null,provider:"codex-acp",protocol:"acp"},command:r.command,args:r.args,cwd:e.cwd,exitCode:r.child.exitCode,signal:r.child.signalCode,durationMs:Date.now()-s,stdout:o.rpc.stdout,stderr:o.rpc.stderr,content:this.content.trim(),updateCount:this.updates.count(),updates:this.updates.snapshot(),statusTimeline:[...this.statusTimeline],performance:this.performanceTracker?.snapshot(),contextUsage:H(i),promptResponse:i}}async compactContext(e){if(!d.existsSync(e.cwd)||!d.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const s=Date.now();this.statusCallback=e.onStatus??null,this.updateCallback=e.onUpdate??null,this.attachAbortSignal(e.signal);let o=this.providerSessionId,r;try{this.emitStatus("initializing","Codex ACP 启动中","codex-acp");const i=await this.acquireRuntime(e.cwd);this.lease=i,await this.initialize(i),o=await this.establishSession(i,e),await this.applyConfig(i.client,o,e);const c=z(e.timeoutMs);r=await this.compactWithAbort(i.client,{sessionId:o,allowRollover:e.allowRollover===!0,timeoutMs:c,signal:e.signal});const a=A(r);a&&a!==o&&(o=a,this.providerSessionId=a,i.setProviderSession({providerSessionId:a,sessionResponse:this.sessionResponse}))}catch(i){if(this.lease&&U(i,e.signal)){const c=this.lease;this.lease=null,await c.dispose().catch(()=>{})}throw this.emitStatus("failed","上下文压缩失败",i instanceof Error?i.message:String(i)),i}finally{this.abortHandler&&e.signal&&(e.signal.removeEventListener("abort",this.abortHandler),this.abortHandler=null),this.releaseLease()}const n=l(r)??{};return{providerSessionId:o||this.providerSessionId||"",contextUsage:x(n.contextUsage),maintenance:l(n.maintenance)??{},durationMs:Date.now()-s}}async cancel(){this.providerSessionId&&this.lease?.client.cancel(this.providerSessionId)}async close(){this.statusCallback=null,this.updateCallback=null,this.permissionCallback=null,this.userInputCallback=null,this.abortHandler&&(this.abortHandler=null),this.releaseLease()}async acquireRuntime(e){const s=y(),o=await g(),r=await S.acquire({providerId:"codex-acp",cwd:e,providerSessionId:this.providerSessionId,runtimeIdentity:o?.runtimeIdentity||"default",env:b(o),command:s.command,args:s.args,onSessionUpdate:n=>this.handleSessionUpdate(n),onClientRequest:async(n,i)=>{if(n==="session/request_permission")return this.requestPermission(i);if(n==="elicitation/create"||n==="session/request_user_input")return this.requestUserInput(i);throw new Error(`Unsupported Codex ACP client request: ${n}`)}});try{return await I(r,o),r}catch(n){throw await r.dispose().catch(()=>{}),n}}async initialize(e){e.initialized||(await e.client.initialize(6e4),e.markInitialized())}async establishSession(e,s){const o=e.client,r=C(s);if(e.providerSessionId&&e.sessionEstablished)return this.providerSessionId=e.providerSessionId,this.sessionResponse=e.sessionResponse,e.providerSessionId;if(this.providerSessionId)try{this.emitStatus("resuming","Codex ACP 会话恢复中","session/load");const c=await o.loadSession({sessionId:this.providerSessionId,cwd:s.cwd,config:r},6e4);this.sessionResponse=l(c);const a=p(c)||this.providerSessionId;return this.providerSessionId=a,e.setProviderSession({providerSessionId:a,sessionResponse:this.sessionResponse}),a}catch(c){if(s.requireSessionResume){const a=c instanceof Error?c.message:String(c);throw new Error(`无法恢复导入的 Codex ACP Session ${this.providerSessionId}:${a}`)}this.providerSessionId=null}this.emitStatus("acp","Codex ACP 会话创建中","session/new");const n=await o.newSession({cwd:s.cwd,config:r},12e4);this.sessionResponse=l(n);const i=p(n);if(!i)throw new Error("Codex ACP session/new did not return sessionId");return this.providerSessionId=i,e.setProviderSession({providerSessionId:i,sessionResponse:this.sessionResponse}),i}async applyConfig(e,s,o){o.model&&await e.setSessionConfigOption({sessionId:s,configId:"model",value:o.model},6e4).catch(()=>{});const r=f(o.mode);r&&await e.setSessionConfigOption({sessionId:s,configId:"approvalPolicy",value:r},6e4).catch(()=>{});const n=h(o.mode);n&&await e.setSessionConfigOption({sessionId:s,configId:"sandbox",value:n},6e4).catch(()=>{});for(const[i,c]of Object.entries(o.configOptionValues??{}))["model","mode","sandbox","approvalPolicy"].includes(i)||await e.setSessionConfigOption({sessionId:s,configId:i,value:c},6e4).catch(()=>{})}handleSessionUpdate(e){this.performanceTracker?.recordEvent(),this.updates.push({method:"session/update",params:e});const s=R(e);s&&(s.type==="agent_message_chunk"&&(this.content+=s.text??""),this.updateCallback?.(s));const o=M(e);o&&this.emitStatus(o.phase,o.label,o.detail,o.updateType)}startSlowStatusTimer(){const e=_().slowEventThresholdMs,s=setInterval(()=>{const o=this.performanceTracker,r=Date.now();if(!o?.shouldReportSlow(r,e))return;const n=o.snapshot(r),i=Math.max(1,Math.round(n.silentForMs/6e4));this.emitStatus("thinking","模型长时间处理中",`已 ${i} 分钟没有收到新的 ACP 事件`,"aca.performance.slow")},Math.min(3e4,Math.max(1e3,Math.floor(e/4))));return s.unref(),s}attachAbortSignal(e){if(e){if(this.abortHandler=()=>{this.cancel()},e.aborted)throw new Error("Codex ACP prompt cancelled");e.addEventListener("abort",this.abortHandler,{once:!0})}}async promptWithAbort(e,s){return w({signal:s.signal,cancelledMessage:"Codex ACP prompt cancelled",startRequest:()=>e.prompt({sessionId:s.sessionId,prompt:s.prompt},s.timeoutMs),onAbort:async()=>{e.cancel(s.sessionId),await this.lease?.dispose()}})}async compactWithAbort(e,s){return w({signal:s.signal,cancelledMessage:"Codex ACP 上下文压缩已取消",startRequest:()=>e.compactSession({sessionId:s.sessionId,allowRollover:s.allowRollover,timeoutMs:s.timeoutMs},s.timeoutMs),onAbort:async()=>{e.cancel(s.sessionId),await this.lease?.dispose()}})}async requestPermission(e){if(!this.permissionCallback)return{outcome:{outcome:"cancelled"}};const s=e&&typeof e=="object"&&!Array.isArray(e)?e:{},o=s.request&&typeof s.request=="object"&&!Array.isArray(s.request)?s.request:s,n=(Array.isArray(o.options)?o.options:[]).map((i,c)=>{const a=i&&typeof i=="object"&&!Array.isArray(i)?i:{};return{optionId:String(a.optionId??a.id??c),kind:String(a.kind??"choice"),...typeof a.label=="string"?{label:a.label}:{}}});return this.emitStatus("requestPermission","等待授权",O(o),"session/request_permission"),this.permissionCallback({requestId:String(o.requestId??o.id??`codex-acp-permission-${Date.now()}`),params:e,options:n})}async requestUserInput(e){if(!this.userInputCallback)return{outcome:{outcome:"cancelled"}};const s=e&&typeof e=="object"&&!Array.isArray(e)?e:{},o=s.request&&typeof s.request=="object"&&!Array.isArray(s.request)?s.request:s;return this.emitStatus("requestPermission","等待输入",D(o),"elicitation/create"),this.userInputCallback({requestId:String(o.requestId??o.id??`codex-acp-input-${Date.now()}`),prompt:String(o.prompt??o.message??o.question??""),params:e,...typeof o.defaultValue=="string"?{defaultValue:o.defaultValue}:{}})}emitStatus(e,s,o,r){const n={phase:e,label:s,...o?{detail:o}:{},...r?{updateType:r}:{},atMs:Date.now()},i=this.statusTimeline[this.statusTimeline.length-1];i&&i.phase===n.phase&&i.label===n.label&&i.detail===n.detail&&i.updateType===n.updateType||(this.statusTimeline.push(n),this.statusCallback?.(n))}releaseLease(){this.lease?.release(),this.lease=null}}async function I(t,e){t.runtimeConfigured||(await t.client.configureRuntime({codexProvider:e?{baseUrl:e.baseUrl,apiKey:e.apiKey,revision:e.revision}:null}),t.markRuntimeConfigured())}function b(t){return t?{OPENAI_API_KEY:t.apiKey,OPENAI_BASE_URL:t.baseUrl}:{}}function U(t,e){if(e?.aborted)return!0;const s=t instanceof Error?t.message:String(t||"");return/compact.*timed out|request timed out.*compact|adapter closed|aborted|cancelled/i.test(s)}function p(t){if(!t||typeof t!="object"||Array.isArray(t))return null;const e=t.sessionId;return typeof e=="string"&&e.trim()?e.trim():null}function l(t){return t&&typeof t=="object"&&!Array.isArray(t)?t:null}function h(t){const e=String(t||"").trim();return e?e==="read-only"?"read-only":["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"danger-full-access":"workspace-write":null}function f(t){const e=String(t||"").trim();return e?["agent-full-access","full-access","bypassPermissions","danger-full-access"].includes(e)?"never":e==="acceptEdits"||e==="accept-edits"?"on-failure":"on-request":null}function C(t){return{...t.model?{model:t.model}:{},...f(t.mode)?{approvalPolicy:f(t.mode)}:{},...h(t.mode)?{sandbox:h(t.mode)}:{},...t.configOptionValues??{}}}function O(t){const e=t.params&&typeof t.params=="object"&&!Array.isArray(t.params)?t.params:t;for(const s of["command","cwd","reason","grantRoot","method"]){const o=e[s];if(typeof o=="string"&&o.trim())return v(o)}}function D(t){for(const e of["prompt","message","question","title"]){const s=t[e];if(typeof s=="string"&&s.trim())return v(s)}}function v(t){const e=t.replace(/\s+/g," ").trim();return e.length>120?`${e.slice(0,117)}...`:e}function j(){const t=Number.parseInt(process.env.ACA_CODEX_ACP_PROMPT_TIMEOUT_MS??process.env.ACA_ACP_PROMPT_TIMEOUT_MS??"",10);return Number.isInteger(t)&&t>=3e4?t:360*60*1e3}function z(t){const e=Number.isInteger(t)?Number(t):Number.parseInt(process.env.ACA_CODEX_ACP_COMPACT_TIMEOUT_MS??"",10);return Number.isFinite(e)&&e>=3e4?Math.min(e,6e5):12e4}function A(t){if(!t||typeof t!="object"||Array.isArray(t))return null;const e=t;return typeof e.sessionId=="string"&&e.sessionId.trim()?e.sessionId:null}function H(t){const e=l(t),s=l(e?._meta);return x(s?.acaContextUsage)}function x(t){const e=l(t);if(!e)return null;const s=m(e.contextWindow),o=m(e.usedTokens),r=m(e.inputTokens),n=String(e.state||"unknown"),i=["active","compacted","new_thread"].includes(n)?n:"unknown";return{usedTokens:o,inputTokens:r,contextWindow:s,ratio:s>0?Math.max(0,m(e.ratio)||o/s):0,observedAtMs:m(e.observedAtMs)||Date.now(),state:i,maintenanceAction:typeof e.maintenanceAction=="string"?e.maintenanceAction:null}}function m(t){return typeof t=="number"&&Number.isFinite(t)&&t>=0?t:0}export{Y as CodexAcpProvider,q as codexAcpCapabilities};
|
package/dist/commands/start.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Command as D}from"commander";import _ from"node:fs";import k from"node:os";import
|
|
1
|
+
import{Command as D}from"commander";import _ from"node:fs";import k from"node:os";import p from"node:path";import i from"node:process";import{randomUUID as x}from"node:crypto";import{Logger as W}from"../core/logger.js";import{loadCredentials as j,saveApiKeyCredential as N}from"../core/auth.js";import{getRuntimeStatePath as O}from"../core/paths.js";import{writeJsonFile as L}from"../core/fs.js";import{HEARTBEAT_INTERVAL_MS as M,startHeartbeatLoop as U}from"../core/heartbeat.js";import{startJobWorkerLoop as F}from"../core/job-worker.js";import{startFileTransferWorkerLoop as H}from"../core/file-transfer-worker.js";import{startMachineFilesystemWorkerLoop as K}from"../core/machine-filesystem-worker.js";import{cleanupOrphanedAcpProcesses as V}from"../core/acp-orphan-cleanup.js";import{defaultAcpRuntimePool as S}from"../acp/client/acp-runtime-pool.js";import{isProcessAlive as u,readPidFile as h,writePidFile as B,removePidFile as y}from"../core/process.js";import{applyRuntimePathOptions as C}from"../core/runtime-options.js";import{loadAcaConfig as P,updateAcaConfig as b}from"../core/aca-config.js";import{acaServerRequest as G}from"../core/aca-server-client.js";import{restoreProjectsAfterLogin as J}from"../core/project-recovery.js";import{syncCodexProviderRuntimeConfig as q}from"../core/codex-provider-runtime-config.js";import{ACA_WORKER_INSTANCE_ENV as E,ACA_WORKER_PID_ENV as z,ACA_WORKER_START_TICKS_ENV as Q,readProcessStartTicks as X}from"../core/process-identity.js";import{printStartTips as Y,spawnDaemon as Z}from"./daemon.js";function Re(){return new D("start").description("Start agent service in native mode").option("--aica-home <path>","Runtime home directory (defaults to ~/.aica or AICA_HOME)").option("--aica-config <path>","Config file path (defaults to <aica-home>/config.json)").option("--aca-home <path>","Deprecated alias for --aica-home").option("--aca-config <path>","Deprecated alias for --aica-config").option("--server <url>","ACA Server URL override").option("--machine-id <id>","Machine ID override for this runtime").option("--machine-name <name>","Machine name to register (defaults to hostname)").option("--cli-types <types...>","Specify CLI types to register, `claude` or `codex`").option("--debug","enable debug output").option("--heartbeat-log","output current timestamp every 5 seconds").option("--auth <api-key>","Use a CLI API key for non-interactive authentication").option("--token <enrollment-token>","Use a one-time Web-generated token to register this machine").option("-b, --background","在后台启动 Worker").action(async e=>{if(e.background){await ee(e);return}await ie(e)})}async function ee(e){C(e);const n=h();if(n&&u(n))throw new Error(`Worker 已在后台运行,PID ${n}。`);n&&y();const o=Z(te());if(o.unref(),!o.pid)throw new Error("后台 Worker 启动失败:未获得进程 PID。");if(!await re(o.pid,3e3)&&!u(o.pid))throw new Error(`后台 Worker 启动失败,请检查 ${p.join(i.env.AICA_HOME||i.env.ACA_HOME||p.join(k.homedir(),".aica"),"logs")}。`);console.log(`Worker 已在后台启动,PID ${o.pid}。`),Y()}function te(){const e=i.argv.findIndex((n,o)=>o>=2&&n==="start");return e<0?[]:i.argv.slice(e+1).filter(n=>n!=="--background"&&n!=="-b")}async function re(e,n){const o=Date.now();for(;Date.now()-o<n;){if(h()===e)return!0;if(!u(e))return!1;await new Promise(t=>setTimeout(t,50))}return h()===e}async function ie(e){C(e);const n=i.env[E]||x(),o=X(i.pid)||"";i.env[E]=n,i.env[z]=String(i.pid),i.env[Q]=o;const t=new W("start",e.debug?"debug":"info"),$=P(),g={...e.server?.trim()?{serverUrl:e.server.trim()}:{},...e.machineId?{machineId:e.machineId.trim()}:{},...e.machineName?{machineName:e.machineName.trim()}:{}};let s=Object.keys(g).length>0?b(g):$;const m=e.machineName?.trim()||s.machineName||k.hostname();if(e.token?.trim()){const r=await G("POST","/api/auth/machine-enroll",{token:e.token.trim(),machineId:s.machineId,machineName:m},{serverUrl:s.serverUrl});s=b({machineId:r.machineId,machineName:m,token:r.token,username:r.username}),t.info(`Machine enrollment completed for user ${r.username}`);try{const a=await J();a.restored>0&&t.info(`restored ${a.restored} local project(s) after machine enrollment`),a.unavailable>0&&t.warn(`skipped ${a.unavailable} unavailable project path(s) during machine enrollment recovery`),s=P()}catch(a){t.warn(`project recovery after machine enrollment failed: ${a instanceof Error?a.message:String(a)}`)}}const R=e.auth?N(e.auth,m):j();B(i.pid),T({pid:i.pid,workerInstanceId:n,workerStartTicks:o,phase:"running",startupStage:"local-compatible",machineId:s.machineId||R?.machine?.machineId||"local-machine",machineName:m,cliTypes:e.cliTypes??ne(),startedAt:new Date().toISOString(),heartbeatIntervalMs:M,issues:[{severity:"info",message:"aca is running in aca-server compatibility mode; ACP execution happens on this machine."}]}),t.info(`Starting aca local-compatible service on machine ${m}`),t.info(`PID ${i.pid}`);const f=await V();f.terminated>0&&t.warn(`terminated ${f.terminated} orphaned ACP process(es): ${f.pids.join(",")}`),q().then(r=>t.info(r?`Codex Provider runtime config loaded revision=${r.revision}`:"Codex Provider runtime config is not configured")).catch(r=>t.warn(`Codex Provider runtime config sync failed: ${r instanceof Error?r.message:String(r)}`));let v=!1,l,w,I,A;const d=async r=>{if(v)return;v=!0,t.info("Shutting down"),l&&clearInterval(l),I?.stop(),A?.stop(),await w?.stop().catch(c=>t.warn(`job worker shutdown failed: ${c instanceof Error?c.message:String(c)}`));const a=S.stats();await S.disposeAll().catch(c=>t.warn(`ACP runtime shutdown failed: ${c instanceof Error?c.message:String(c)}`)),t.info(`ACP runtimes stopped total=${a.total} inUse=${a.inUse} idle=${a.idle}`),T({pid:i.pid,phase:"stopped",stoppedAt:new Date().toISOString(),issues:[]}),y(),i.exit(r)};i.once("SIGINT",()=>{d(130)}),i.once("SIGTERM",()=>{d(0)}),i.once("uncaughtException",r=>{t.error(`uncaught exception: ${r.stack||r.message}`),d(1)}),i.once("unhandledRejection",r=>{const a=r instanceof Error?r.stack||r.message:String(r);t.error(`unhandled rejection: ${a}`),d(1)}),e.heartbeatLog&&setInterval(()=>t.info(`heartbeat: ${new Date().toISOString()}`),5e3),l=U(t),w=F(t),I=H(t),A=K(t),await new Promise(()=>{})}function ne(){return["codex","claude"].filter(e=>oe(e))}function oe(e){const n=i.env.PATH||"";for(const o of n.split(":"))try{if(o&&_.existsSync(p.join(o,e)))return!0}catch{}return!1}function T(e){L(O(),e)}export{Re as createStartCommand};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{acaServerRequest as c}from"./aca-server-client.js";let i=!1,r=null,t=null;async function s(){if(t)return t;const e=(async()=>{const n=await c("GET","/api/client/codex-provider-config",void 0,{timeoutMs:15e3});return r=f(n.item),i=!0,r})();t=e;try{return await e}finally{t===e&&(t=null)}}async function d(){return i?r:process.env.ACA_WORKER_INSTANCE_ID?s():null}async function m(e){if(!Number.isSafeInteger(e)||Number(e)<0)return d();const n=r?.revision??0;return i&&n===e||!process.env.ACA_WORKER_INSTANCE_ID?r:s()}function l(){return r}function p(){i=!1,r=null,t=null}function C(e){return r=f(e),i=!0,r}function f(e){if(!e)return null;const n=typeof e.baseUrl=="string"?e.baseUrl.trim():"",u=typeof e.apiKey=="string"?e.apiKey.trim():"",o=Number(e.revision||0);if(!n||!u||!Number.isSafeInteger(o)||o<1)throw new Error("Server 返回的 Codex Provider 配置无效");return{baseUrl:n,apiKey:u,revision:o,updatedAtMs:Math.max(0,Number(e.updatedAtMs||0)),runtimeIdentity:`machine-codex-provider:${o}:${n}`}}export{C as applyCodexProviderRuntimeConfigForTests,d as ensureCodexProviderRuntimeConfig,m as ensureCodexProviderRuntimeRevision,l as getCodexProviderRuntimeConfig,p as resetCodexProviderRuntimeConfigForTests,s as syncCodexProviderRuntimeConfig};
|
package/dist/core/heartbeat.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{execFileSync as g}from"node:child_process";import m from"node:os";import{readJsonFile as I,writeJsonFile as v}from"./fs.js";import{getRuntimeStatePath as y}from"./paths.js";import{acaServerRequest as x}from"./aca-server-client.js";import{loadAcaConfig as C}from"./aca-config.js";import{providerHealthSnapshot as w}from"./provider-health.js";import{readCodexCapabilities as O}from"./codex-capabilities.js";const b=3e4,
|
|
2
|
-
`));if(!r)continue;const l=M(r.name,r.id,a)??a;o.push({modelId:a,displayName:
|
|
3
|
-
`));if(!r)continue;const l=M(r.name,r.id,a)??a;o.push({modelId:a,displayName:
|
|
1
|
+
import{execFileSync as g}from"node:child_process";import m from"node:os";import{readJsonFile as I,writeJsonFile as v}from"./fs.js";import{getRuntimeStatePath as y}from"./paths.js";import{acaServerRequest as x}from"./aca-server-client.js";import{loadAcaConfig as C}from"./aca-config.js";import{providerHealthSnapshot as w}from"./provider-health.js";import{readCodexCapabilities as O}from"./codex-capabilities.js";import{AICA_VERSION as _}from"../version.js";const b=3e4,k=360*60*1e3,E=360*60*1e3;let p=null,u=null;async function S(e=C(),t={}){return e.token?await x("POST","/api/heartbeat",{machineId:e.machineId,machineName:e.machineName,machineInfo:{platform:m.platform(),architecture:m.arch(),osRelease:m.release(),acaVersion:N(),hostname:m.hostname()},workspaceId:e.defaultWorkspaceId,agentConfigs:t.includeProviderDetails===!1?[]:await D(e),projects:e.projects.map(n=>({projectId:n.projectId,workspaceId:n.workspaceId,name:n.name,rootPath:n.rootPath}))}):{ok:!1,skipped:!0,reason:"not_logged_in"}}async function D(e){const t=w(),o=await O(),n=o?.capabilities??V();return[{agentId:`${e.machineId}:builtin:codex`,workspaceId:e.defaultWorkspaceId,name:"Codex",agentType:"codex",cliType:"builtin",machineId:e.machineId,model:o?.defaultModel||"gpt-5.5",mode:"agent-full-access",raw:{providerStatus:t.codex,capabilities:n}},{agentId:`${e.machineId}:builtin:mimo`,workspaceId:e.defaultWorkspaceId,name:"MimoCode",agentType:"mimo",cliType:"builtin",machineId:e.machineId,model:"mimo/mimo-auto",mode:"agent-full-access",raw:{providerStatus:t.mimo,capabilities:j()}},{agentId:`${e.machineId}:builtin:opencode`,workspaceId:e.defaultWorkspaceId,name:"OpenCode",agentType:"opencode",cliType:"builtin",machineId:e.machineId,model:process.env.ACA_OPENCODE_DEFAULT_MODEL||null,mode:"agent-full-access",raw:{providerStatus:t.opencode,capabilities:T()}}]}function N(e=process.env){return e.ACA_VERSION?.trim()||_}function V(){const e=[{id:"read-only",name:"Read-only",description:"Requires approval to edit files and run commands."},{id:"agent",name:"Agent",description:"Read and edit files, and run commands."},{id:"agent-full-access",name:"Agent (full access)",description:"Edit files and run commands with full access."}],t=[{modelId:"gpt-5.5",name:"gpt-5.5",description:"Latest frontier Codex model"},{modelId:"gpt-5.4",name:"gpt-5.4",description:"Frontier Codex model"},{modelId:"gpt-5.4-mini",name:"gpt-5.4-mini",description:"Smaller, faster Codex model"}],o=[{value:"low",name:"Low",description:"Fastest responses"},{value:"medium",name:"Medium",description:"Balanced reasoning"},{value:"high",name:"High",description:"More reasoning for difficult tasks"},{value:"xhigh",name:"X High",description:"Extra reasoning for complex tasks"}];return{source:"aca-static",fetchedAt:Date.now(),contextUsage:!0,contextCompaction:!0,modes:e,models:t,configOptions:[{id:"mode",name:"Mode",category:"mode",type:"select",currentValue:"agent-full-access",options:e.map(n=>({value:n.id,name:n.name,description:n.description}))},{id:"model",name:"Model",category:"model",type:"select",currentValue:"gpt-5.5",options:t.map(n=>({value:n.modelId,name:n.name,description:n.description}))},{id:"reasoning_effort",name:"Reasoning effort",category:"thought_level",type:"select",currentValue:"medium",options:o}]}}function R(){const e=[{id:"agent",name:"Agent",description:"Run MimoCode with normal workspace permissions."},{id:"agent-full-access",name:"Agent (full access)",description:"Run MimoCode and auto-approve supported local permissions."}],t=[{modelId:"mimo/mimo-auto",name:"MiMo Auto (free)",description:"MiMo Auto free model"},{modelId:"xiaomi/mimo-v2.5",name:"mimo-v2.5",description:"MimoCode v2.5"},{modelId:"xiaomi/mimo-v2.5-pro",name:"mimo-v2.5-pro",description:"MimoCode v2.5 Pro"},{modelId:"xiaomi/mimo-v2.5-pro-ultraspeed",name:"mimo-v2.5-pro-ultraspeed",description:"MimoCode v2.5 Pro UltraSpeed"}],o=[{value:"minimal",name:"Minimal",description:"Fastest responses"},{value:"high",name:"High",description:"More reasoning for difficult tasks"},{value:"max",name:"Max",description:"Maximum reasoning when supported"}];return{source:"aca-static",fetchedAt:Date.now(),contextUsage:!0,contextCompaction:!1,modes:e,models:t,configOptions:[{id:"mode",name:"Mode",category:"mode",type:"select",currentValue:"agent-full-access",options:e.map(n=>({value:n.id,name:n.name,description:n.description}))},{id:"model",name:"Model",category:"model",type:"select",currentValue:"mimo/mimo-auto",options:t.map(n=>({value:n.modelId,name:n.name,description:n.description}))},{id:"variant",name:"Variant",category:"thought_level",type:"select",currentValue:"high",options:o}]}}function j(){const e=Date.now();if(p&&p.expiresAtMs>e)return p.capabilities;const t=W()??R();return p={expiresAtMs:e+k,capabilities:t},t}function H(){const e=[{id:"plan",name:"Plan",description:"Inspect the project and propose changes."},{id:"build",name:"Build",description:"Read and edit files, and run commands when approved."},{id:"agent-full-access",name:"Agent (full access)",description:"Run OpenCode with ACA full-access mode mapping."}],t=[{modelId:"",name:"Default",description:"Use the model selected by OpenCode configuration."}];return{source:"aca-static",fetchedAt:Date.now(),contextUsage:!0,contextCompaction:!1,modes:e,models:t,configOptions:[{id:"mode",name:"Mode",category:"mode",type:"select",currentValue:"agent-full-access",options:e.map(o=>({value:o.id,name:o.name,description:o.description}))},{id:"model",name:"Model",category:"model",type:"text",currentValue:process.env.ACA_OPENCODE_DEFAULT_MODEL||"",options:t.map(o=>({value:o.modelId,name:o.name,description:o.description}))}]}}function T(){const e=Date.now();if(u&&u.expiresAtMs>e)return u.capabilities;const t=$()??H();return u={expiresAtMs:e+E,capabilities:t},t}function $(){try{const e=process.env.ACA_OPENCODE_COMMAND||"opencode",t=g(e,["models","--verbose"],{encoding:"utf8",timeout:15e3,maxBuffer:8*1024*1024,stdio:["ignore","pipe","pipe"]}),o=L(t);if(!o.length)return null;const n=[{id:"plan",name:"Plan",description:"Inspect the project and propose changes."},{id:"build",name:"Build",description:"Read and edit files, and run commands when approved."},{id:"agent-full-access",name:"Agent (full access)",description:"Run OpenCode with ACA full-access mode mapping."}],a=F(o);return{source:"opencode-cli",fetchedAt:Date.now(),contextUsage:!0,contextCompaction:!1,modes:n,models:o.map(i=>({modelId:i.modelId,name:i.displayName,description:i.description,metadata:i.metadata})),configOptions:[{id:"mode",name:"Mode",category:"mode",type:"select",currentValue:"agent-full-access",options:n.map(i=>({value:i.id,name:i.name,description:i.description}))},{id:"model",name:"Model",category:"model",type:"select",currentValue:a,options:o.map(i=>({value:i.modelId,name:i.displayName,description:i.description}))}]}}catch{return null}}function L(e){const t=e.split(/\r?\n/),o=[];let n=0;for(;n<t.length;){const a=t[n]?.trim()??"";if(n+=1,!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(a))continue;const i=[];let s=0,c=!1;for(;n<t.length;){const d=t[n]??"";if(n+=1,!(!c&&!d.trim().startsWith("{"))&&(c=!0,i.push(d),s+=f(d,"{")-f(d,"}"),c&&s<=0))break}const r=A(i.join(`
|
|
2
|
+
`));if(!r)continue;const l=M(r.name,r.id,a)??a;o.push({modelId:a,displayName:P(l,r),description:B(r),metadata:r})}return o}function F(e){const t=process.env.ACA_OPENCODE_DEFAULT_MODEL?.trim();return t&&e.some(o=>o.modelId===t)?t:e.find(o=>o.modelId==="opencode/big-pickle")?.modelId||e.find(o=>/free$/i.test(o.modelId))?.modelId||e[0]?.modelId||""}function P(e,t){return U(t)&&!/\bfree\b/i.test(e)?`${e} (free)`:e}function U(e){const t=e.cost;if(!t||typeof t!="object"||Array.isArray(t))return!1;const o=t,n=o.cache&&typeof o.cache=="object"&&!Array.isArray(o.cache)?o.cache:{};return[o.input,o.output,n.read,n.write].every(a=>Number(a??0)===0)}function B(e){const t=e.limit&&typeof e.limit=="object"&&!Array.isArray(e.limit)?e.limit:{},o=Number(t.context??0),n=Number(t.output??0);return o>0&&n>0?`Context ${o}, output ${n}`:o>0?`Context ${o}`:"OpenCode model"}function W(){try{const e=process.env.ACA_MIMO_COMMAND||"mimo",t=g(e,["models","--verbose"],{encoding:"utf8",timeout:15e3,maxBuffer:2*1024*1024,stdio:["ignore","pipe","pipe"]}),o=z(t);if(!o.length)return null;const n=[{id:"agent",name:"Agent",description:"Run MimoCode with normal workspace permissions."},{id:"agent-full-access",name:"Agent (full access)",description:"Run MimoCode and auto-approve supported local permissions."}],a=J(o);return{source:"mimo-cli",fetchedAt:Date.now(),contextUsage:!0,contextCompaction:!1,modes:n,models:o.map(i=>({modelId:i.modelId,name:i.displayName,description:i.description,metadata:i.metadata})),configOptions:[{id:"mode",name:"Mode",category:"mode",type:"select",currentValue:"agent-full-access",options:n.map(i=>({value:i.id,name:i.name,description:i.description}))},{id:"model",name:"Model",category:"model",type:"select",currentValue:o.some(i=>i.modelId==="mimo/mimo-auto")?"mimo/mimo-auto":o[0]?.modelId,options:o.map(i=>({value:i.modelId,name:i.displayName,description:i.description}))},{id:"variant",name:"Variant",category:"thought_level",type:"select",currentValue:a.some(i=>i.value==="high")?"high":a[0]?.value,options:a}]}}catch{return null}}function z(e){const t=e.split(/\r?\n/),o=[];let n=0;for(;n<t.length;){const a=t[n]?.trim()??"";if(n+=1,!/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(a))continue;const i=[];let s=0,c=!1;for(;n<t.length;){const d=t[n]??"";if(n+=1,!(!c&&!d.trim().startsWith("{"))&&(c=!0,i.push(d),s+=f(d,"{")-f(d,"}"),c&&s<=0))break}const r=A(i.join(`
|
|
3
|
+
`));if(!r)continue;const l=M(r.name,r.id,a)??a;o.push({modelId:a,displayName:q(l,r),description:G(r),metadata:r})}return o}function J(e){const t=new Set;for(const a of e){const i=a.metadata.variants;if(!(!i||typeof i!="object"||Array.isArray(i)))for(const s of Object.keys(i))t.add(s)}const o=["minimal","low","medium","high","max"].filter(a=>t.has(a));return(o.length?o:["minimal","high","max"]).map(a=>({value:a,name:K(a),description:a==="high"?"More reasoning for difficult tasks":a==="max"?"Maximum reasoning when supported":"Fastest responses"}))}function q(e,t){return X(t)&&!/\bfree\b/i.test(e)?`${e} (free)`:e}function X(e){const t=e.cost;if(!t||typeof t!="object"||Array.isArray(t))return!1;const o=t,n=o.cache&&typeof o.cache=="object"&&!Array.isArray(o.cache)?o.cache:{};return[o.input,o.output,n.read,n.write].every(a=>Number(a??0)===0)}function G(e){const t=e.limit&&typeof e.limit=="object"&&!Array.isArray(e.limit)?e.limit:{},o=Number(t.context??0);return o>0?`Context ${o}`:"MimoCode model"}function A(e){try{const t=JSON.parse(e);return t&&typeof t=="object"&&!Array.isArray(t)?t:null}catch{return null}}function M(...e){for(const t of e)if(typeof t=="string"&&t.trim())return t.trim()}function K(e){return e.slice(0,1).toUpperCase()+e.slice(1)}function f(e,t){let o=0;for(const n of e)n===t&&(o+=1);return o}function re(e){let t=!1;const o=async(i,s="heartbeat")=>{const c=new Date().toISOString();try{const r=await S(C(),{includeProviderDetails:i});if(r.skipped){h({lastHeartbeatAt:c,lastHeartbeatStatus:"skipped",lastHeartbeatReason:r.reason??"unknown"}),e.debug(`${s} skipped: ${r.reason??"unknown"}`);return}h({lastHeartbeatAt:c,lastHeartbeatStatus:"ok",lastHeartbeatProjectCount:r.projectCount??null,presenceExpiresAtMs:r.presenceExpiresAtMs??null}),e.debug(`${s} ok projects=${r.projectCount??0}`)}catch(r){const l=r instanceof Error?r.message:String(r);h({lastHeartbeatAt:c,lastHeartbeatStatus:"error",lastHeartbeatError:l}),e.warn(`${s} failed: ${l}`)}},n=async()=>{if(!t){t=!0;try{await o(!0)}finally{t=!1}}};return(async()=>{if(!t){t=!0;try{await o(!1,"initial presence heartbeat"),await o(!0)}finally{t=!1}}})(),setInterval(()=>{n()},b)}function h(e){const t=I(y())??{};v(y(),{...t,heartbeatIntervalMs:b,...e})}export{b as HEARTBEAT_INTERVAL_MS,N as reportedAicaVersion,S as sendHeartbeat,re as startHeartbeatLoop};
|
package/dist/core/job-worker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import H from"node:crypto";import $ from"node:fs";import j from"node:path";import{compactAgentContext as G,forkAgentSession as K,sendAgentPrompt as X}from"../acp/agent.js";import{acaServerDownload as Z,acaServerRequest as y}from"./aca-server-client.js";import{loadAcaConfig as Q}from"./aca-config.js";import{prepareLocalWorktree as Y}from"./worktree.js";import{expandWorkerRuntimePath as ee}from"./worker-runtime-paths.js";import{collectAffectedFiles as O,extractAffectedFilePathsFromAcpRaw as te}from"./affected-files.js";import{resolveJobPermissionPolicy as ne}from"./job-permission-policy.js";import{appendCoalescedAcpEvent as oe}from"./acp-event-coalescer.js";import{shouldUploadAcpEvent as ae}from"./acp-event-upload-filter.js";import{userFacingAcpError as ie}from"../acp/error-display.js";const re=1500,F=5e3,U=1e3,se=200,ce=64,z=900,le=1e4,ue=0,de=12e3;function pe(e){return ee(e)}function me(e){const t=e.project_metadata;return t&&typeof t=="object"&&!Array.isArray(t)&&t.kind==="chat"?!0:typeof e.project_root_path=="string"&&(e.project_root_path.startsWith("~/.aica/chats/")||e.project_root_path.startsWith("~/.aca/chats/")||/(^|\/)\.aica\/chats\//.test(e.project_root_path)||/(^|\/)\.aca\/chats\//.test(e.project_root_path))}function nt(e,t={}){let n=!1,o=null,a=null,r=0,s=0;const u=new Set,i=new Map,d=new Map,_=async()=>{if(n||Date.now()<s)return;const w=we();if(B(u.size,w))try{const m=t.loadConfig?.()??Q();if(!m.token)return;for(;!n&&B(u.size,w);){const g=t.claimNextJob?await t.claimNextJob(m,[...i.keys()]):await y("POST","/api/client/jobs/claim",{machineId:m.machineId,workspaceId:m.defaultWorkspaceId,activeSessionIds:[...i.keys()]});if(r=0,s=0,!g.item)break;const h=g.item;if(n){await(t.cancelClaimedJob?t.cancelClaimedJob(h,m):x(h.job_id,m.machineId,"cancelled")).catch(()=>{});break}const f=h.session_id,P=i.get(f);u.add(h.job_id);const v=new AbortController;let p;p=(async()=>{P&&(e.warn(`claimed job=${h.job_id} for already active session=${f}; waiting for the previous run to release its ACP runtime`),await fe(P,h,m,v)),!v.signal.aborted&&await(t.runJob??ye)(h,m,e,v)})().catch(I=>{e.warn(`job ${h.job_id} failed outside handler: ${I instanceof Error?I.message:String(I)}`)}).finally(()=>{u.delete(h.job_id),i.get(f)===p&&i.delete(f),d.delete(h.job_id)}),i.set(f,p),d.set(h.job_id,{controller:v,promise:p})}}catch(m){if(!n){r+=1;const g=Be(r);s=Date.now()+g,e.warn(`job worker failed, retry in ${g}ms: ${m instanceof Error?m.message:String(m)}`)}}},A=()=>{if(n||o)return;const w=_();o=w,w.finally(()=>{o===w&&(o=null)})};A();const l=setInterval(()=>{A()},t.claimIntervalMs??re);return{timer:l,stop:()=>a||(a=(async()=>{if(n)return;n=!0,clearInterval(l);for(const m of d.values())m.controller.abort();await o?.catch(()=>{});for(const m of d.values())m.controller.abort();const w=[...d.values()].map(m=>m.promise);w.length!==0&&await Promise.race([Promise.allSettled(w).then(()=>{}),M(de)])})(),a)}}async function fe(e,t,n,o){let a=!1;const s=setInterval(()=>{a||o.signal.aborted||(a=!0,Promise.allSettled([y("POST",`/api/client/jobs/${encodeURIComponent(t.job_id)}/heartbeat`,{machineId:n.machineId}),W(t.job_id,o)]).finally(()=>{a=!1}))},F);try{await Promise.race([e,he(o.signal)])}finally{clearInterval(s)}}function he(e){return e.aborted?Promise.resolve():new Promise(t=>e.addEventListener("abort",()=>t(),{once:!0}))}function we(){const e=process.env.ACA_MAX_CONCURRENT_JOBS||process.env.ACA_MAX_CONCURRENT_PROJECT_JOBS||"",t=Number.parseInt(e,10);return!e.trim()||!Number.isFinite(t)||t<0?ue:t===0?0:Math.max(1,Math.min(t,8))}function B(e,t){return t===0||e<t}function be(){const e=Number.parseInt(process.env.ACA_ACP_EVENT_BATCH_INTERVAL_MS||"",10);return Number.isFinite(e)?Math.max(40,Math.min(e,2e3)):se}async function ye(e,t,n,o){const a=e.job_id,r=e.session_id,s=e.workspace_id,u=e.project_id??null,i=e.input??{},d=String(i.control?.type||"").toLowerCase();if(!d&&!i.message?.trim()&&(i.attachments??[]).length===0){await x(a,t.machineId,"job input missing message");return}const _=t.projects.find(p=>p.projectId===u),A=pe(_?.rootPath||e.project_root_path||"");if(!A){await x(a,t.machineId,"job project root is unknown");return}me(e)&&$.mkdirSync(A,{recursive:!0}),n.info(`claimed job=${a} session=${r} project=${_?.name??u??"unknown"}`);let l=!1,w=!1;const m=setInterval(()=>{l||(l=!0,y("POST",`/api/client/jobs/${encodeURIComponent(a)}/heartbeat`,{machineId:t.machineId}).catch(()=>{}).finally(()=>{l=!1})),w||(w=!0,W(a,o).catch(()=>{}).finally(()=>{w=!1}))},F);let g=0;const h=Ae(a,t.machineId),f=Ce(a,t.machineId),P=new Set;let v=null;try{await f.sendNow({phase:"initializing",label:"准备工作区",detail:e.worktree_id?"worktree":"project root"}).catch(()=>{});const p=Y({projectId:u,sessionId:r,projectRoot:A,worktree:e});v=p,await Ne(a,t.machineId,p);const I=Se(i.acpRun);if(d==="compact"){if(!e.acp_session_id)throw new Error("Current session does not have an ACP context to compact");const c=await G({agentType:k(I.agentType,"codex"),cliType:k(I.cliType,"builtin"),acpSessionId:e.acp_session_id,cwd:p.cwd,model:i.model,mode:i.mode,configOptionValues:i.configOptionValues,timeoutMs:i.timeoutMs,allowRollover:i.control?.allowRollover===!0,signal:o.signal,onStatus:S=>{f.report({phase:S.phase,label:S.label,detail:S.detail,updateType:S.updateType,raw:S})}});await f.flush(),await y("POST",`/api/client/jobs/${encodeURIComponent(a)}/complete`,{machineId:t.machineId,acpSessionId:c.acpSessionId,suppressTurn:!0,result:{ok:!0,control:{type:"compact",allowRollover:i.control?.allowRollover===!0},acpSessionId:c.acpSessionId,durationMs:c.durationMs,usage:c.contextUsage,maintenance:c.maintenance,worktree:R(p)}}),n.info(`completed context compact job=${a}`);return}if(d==="fork"){const c=String(i.control?.sourceAcpSessionId||"").trim();if(!c)throw new Error("Source session does not have a Codex ACP context to fork");await f.sendNow({phase:"forking",label:"正在分叉 Codex 会话",detail:i.control?.sourceSessionId?`源会话 ${i.control.sourceSessionId}`:void 0}).catch(()=>{});const S=await K({agentType:k(I.agentType,"codex"),cliType:k(I.cliType,"builtin"),sourceProviderSessionId:c,cwd:p.cwd,model:i.model,mode:i.mode,configOptionValues:i.configOptionValues,timeoutMs:i.timeoutMs,signal:o.signal,onStatus:C=>{f.report({phase:C.phase,label:C.label,detail:C.detail,updateType:C.updateType,raw:C})}});await f.flush(),await y("POST",`/api/client/jobs/${encodeURIComponent(a)}/complete`,{machineId:t.machineId,acpSessionId:S.acpSessionId,suppressTurn:!0,result:{ok:!0,control:{type:"fork",sourceSessionId:i.control?.sourceSessionId||null},acpSessionId:S.acpSessionId,worktree:R(p)}}),n.info(`completed Codex session fork job=${a}`);return}const T=await xe({cwd:p.cwd,sessionId:r,jobId:a,attachments:i.attachments??[]});T.length>0&&(n.info(`materialized ${T.length} attachment(s) for job=${a}`),await f.sendNow({phase:"initializing",label:"附件已写入本机",detail:T.map(c=>c.relativePath).join(", "),raw:{attachments:L(T)}}).catch(()=>{}));const N=ke(i,T),q=Re(N,T),b=await X({agentType:k(I.agentType,"codex"),cliType:k(I.cliType,"builtin"),acpSessionId:e.acp_session_id??null,requireSessionResume:Ie(e.session_raw),prompt:N,promptBlocks:q,cwd:p.cwd,model:i.model,mode:i.mode,configOptionValues:i.configOptionValues,canExecuteTools:i.canExecuteTools,timeoutMs:i.timeoutMs,contextWindow:ge(I.contextWindow),signal:o.signal,onStatus:c=>{f.report({phase:c.phase,label:c.label,detail:c.detail,updateType:c.updateType,raw:c})},onUpdate:c=>{g+=1;for(const C of te(c.raw))P.add(C);const S={turnId:e.user_turn_id??null,eventType:c.type,label:c.label,text:c.text,status:c.status,toolCallId:c.toolCallId,raw:Pe(c.raw,g)};ae(S)&&h.push(S)},onPermissionRequest:c=>{const S=ne(i,c);return S?Promise.resolve(S):Oe(a,t.machineId,c,o.signal)},onUserInputRequest:c=>Fe(a,t.machineId,c,o.signal)});await h.flush(),await f.flush();const D=O({cwd:p.cwd,evidencePaths:P}),E=_e(b.promptResponse);if(!(Te(E)||!!b.content)){await x(a,t.machineId,`${b.agentType} stopped: ${E??"unknown"}`,{acpSessionId:b.acpSessionId,stopReason:E,exitCode:b.exitCode,stderr:Je(b.stderr,4e3),worktree:R(p)});return}await y("POST",`/api/client/jobs/${encodeURIComponent(a)}/complete`,{machineId:t.machineId,acpSessionId:b.acpSessionId,assistantText:b.content,result:{ok:!0,acpSessionId:b.acpSessionId,stopReason:E,durationMs:b.durationMs,acpRun:i.acpRun??null,effectiveConfig:b.effectiveConfig??null,performance:b.performance??null,usage:b.contextUsage??null,updateCount:b.updateCount,statusTimeline:b.statusTimeline,worktree:R(p),attachments:L(T),files:D}}),n.info(`completed job=${a}`)}catch(p){if(ze(o.signal)){h.discard(),f.discard(),n.warn(`stopped local job=${a} because ACA Server already finalized it`);return}await h.flush(),await f.flush();const I=o.signal.aborted?"cancelled":p instanceof Error?p.message:String(p),T=v?O({cwd:v.cwd,evidencePaths:P}):[];await x(a,t.machineId,ie(I),{rawError:I,files:T})}finally{await h.flush(),await f.flush(),clearInterval(m)}}function Ie(e){if(!e||typeof e!="object"||Array.isArray(e))return!1;const t=e.importedProviderSession;return!!(t&&typeof t=="object"&&!Array.isArray(t)&&t.strictResume===!0)}function Se(e){return!e||typeof e!="object"||Array.isArray(e)?{}:e}function ge(e){return typeof e=="number"&&Number.isFinite(e)&&e>0?e:void 0}function k(...e){for(const t of e)if(typeof t=="string"&&t.trim())return t.trim();return""}function _e(e){if(!e||typeof e!="object"||Array.isArray(e))return;const t=e;if(typeof t.stopReason=="string")return t.stopReason;if(typeof t.reason=="string")return t.reason;const n=t.part;if(n&&typeof n=="object"&&!Array.isArray(n)&&typeof n.reason=="string")return n.reason}function Te(e){return e?["end_turn","stop","completed","complete"].includes(e):!1}function Ae(e,t){let n=[],o=null,a=Promise.resolve(),r=!1;const s=async d=>{d.length!==0&&await y("POST",`/api/client/jobs/${encodeURIComponent(e)}/events/batch`,{machineId:t,events:d})},u=()=>{if(o&&(clearTimeout(o),o=null),r)return n=[],a;const d=n;return n=[],a=a.then(()=>s(d)).catch(()=>{}),a},i=()=>{o||(o=setTimeout(()=>{u()},be()),o.unref())};return{push(d){if(!r){if(oe(n,d),ve(d)||n.length>=ce){u();return}i()}},flush:u,discard(){r=!0,n=[],o&&clearTimeout(o),o=null}}}function ve(e){return e.eventType==="tool_call"?!0:e.eventType!=="tool_call_update"?!1:["completed","failed","cancelled"].includes(String(e.status||"").toLowerCase())}function Ce(e,t){let n=null,o=0,a="",r=null,s=Promise.resolve(),u=!1;const i=l=>JSON.stringify({phase:l.phase??"",label:l.label??"",detail:l.detail??"",updateType:l.updateType??""}),d=async l=>{await y("POST",`/api/client/jobs/${encodeURIComponent(e)}/presence`,{machineId:t,phase:l.phase,label:l.label,detail:l.detail,updateType:l.updateType,raw:l.raw}),o=Date.now(),a=i(l)},_=()=>{if(r&&(clearTimeout(r),r=null),u)return n=null,s;if(!n)return s;const l=n;return n=null,s=s.then(()=>d(l)).catch(()=>{}),s},A=l=>{r||(r=setTimeout(()=>{_()},l),r.unref())};return{report(l){if(u)return;const w=Date.now(),g=i(l)!==a,h=w-o>=le;if(!g&&!h)return;n=l;const f=w-o;if(f>=z||o===0){_();return}A(z-f)},sendNow(l){return u?s:(n=l,_())},flush:_,discard(){u=!0,n=null,r&&clearTimeout(r),r=null}}}function Pe(e,t){return e&&typeof e=="object"&&!Array.isArray(e)?{...e,acaSequence:t}:{value:e,acaSequence:t}}function ke(e,t=[]){const n=e?.message?.trim()??"";if(t.length===0)return n;const a=["用户随消息上传了以下附件,aca 已将附件写入本机工作区。需要读取附件时请直接使用下面的 localPath;图片附件也已作为 ACP image block 随本轮请求传入。",...t.map((r,s)=>`${s+1}. ${r.name} (${r.kind}, ${r.mimeType}, ${Me(r.size)})
|
|
1
|
+
import H from"node:crypto";import j from"node:fs";import $ from"node:path";import{compactAgentContext as G,forkAgentSession as K,sendAgentPrompt as X}from"../acp/agent.js";import{acaServerDownload as Z,acaServerRequest as I}from"./aca-server-client.js";import{loadAcaConfig as Q}from"./aca-config.js";import{prepareLocalWorktree as Y}from"./worktree.js";import{expandWorkerRuntimePath as ee}from"./worker-runtime-paths.js";import{collectAffectedFiles as O,extractAffectedFilePathsFromAcpRaw as te}from"./affected-files.js";import{resolveJobPermissionPolicy as ne}from"./job-permission-policy.js";import{appendCoalescedAcpEvent as oe}from"./acp-event-coalescer.js";import{shouldUploadAcpEvent as ae}from"./acp-event-upload-filter.js";import{userFacingAcpError as ie}from"../acp/error-display.js";import{ensureCodexProviderRuntimeRevision as re}from"./codex-provider-runtime-config.js";const se=1500,F=5e3,z=1e3,ce=200,le=64,U=900,ue=1e4,de=0,pe=12e3;function me(e){return ee(e)}function fe(e){const t=e.project_metadata;return t&&typeof t=="object"&&!Array.isArray(t)&&t.kind==="chat"?!0:typeof e.project_root_path=="string"&&(e.project_root_path.startsWith("~/.aica/chats/")||e.project_root_path.startsWith("~/.aca/chats/")||/(^|\/)\.aica\/chats\//.test(e.project_root_path)||/(^|\/)\.aca\/chats\//.test(e.project_root_path))}function it(e,t={}){let n=!1,o=null,a=null,r=0,s=0;const u=new Set,i=new Map,d=new Map,_=async()=>{if(n||Date.now()<s)return;const w=be();if(B(u.size,w))try{const m=t.loadConfig?.()??Q();if(!m.token)return;for(;!n&&B(u.size,w);){const g=t.claimNextJob?await t.claimNextJob(m,[...i.keys()]):await I("POST","/api/client/jobs/claim",{machineId:m.machineId,workspaceId:m.defaultWorkspaceId,activeSessionIds:[...i.keys()]});if(r=0,s=0,!g.item)break;const h=g.item;if(n){await(t.cancelClaimedJob?t.cancelClaimedJob(h,m):R(h.job_id,m.machineId,"cancelled")).catch(()=>{});break}const f=h.session_id,P=i.get(f);u.add(h.job_id);const A=new AbortController;let p;p=(async()=>{P&&(e.warn(`claimed job=${h.job_id} for already active session=${f}; waiting for the previous run to release its ACP runtime`),await he(P,h,m,A)),!A.signal.aborted&&await(t.runJob??Ie)(h,m,e,A)})().catch(b=>{e.warn(`job ${h.job_id} failed outside handler: ${b instanceof Error?b.message:String(b)}`)}).finally(()=>{u.delete(h.job_id),i.get(f)===p&&i.delete(f),d.delete(h.job_id)}),i.set(f,p),d.set(h.job_id,{controller:A,promise:p})}}catch(m){if(!n){r+=1;const g=Le(r);s=Date.now()+g,e.warn(`job worker failed, retry in ${g}ms: ${m instanceof Error?m.message:String(m)}`)}}},v=()=>{if(n||o)return;const w=_();o=w,w.finally(()=>{o===w&&(o=null)})};v();const l=setInterval(()=>{v()},t.claimIntervalMs??se);return{timer:l,stop:()=>a||(a=(async()=>{if(n)return;n=!0,clearInterval(l);for(const m of d.values())m.controller.abort();await o?.catch(()=>{});for(const m of d.values())m.controller.abort();const w=[...d.values()].map(m=>m.promise);w.length!==0&&await Promise.race([Promise.allSettled(w).then(()=>{}),M(pe)])})(),a)}}async function he(e,t,n,o){let a=!1;const s=setInterval(()=>{a||o.signal.aborted||(a=!0,Promise.allSettled([I("POST",`/api/client/jobs/${encodeURIComponent(t.job_id)}/heartbeat`,{machineId:n.machineId}),W(t.job_id,o)]).finally(()=>{a=!1}))},F);try{await Promise.race([e,we(o.signal)])}finally{clearInterval(s)}}function we(e){return e.aborted?Promise.resolve():new Promise(t=>e.addEventListener("abort",()=>t(),{once:!0}))}function be(){const e=process.env.ACA_MAX_CONCURRENT_JOBS||process.env.ACA_MAX_CONCURRENT_PROJECT_JOBS||"",t=Number.parseInt(e,10);return!e.trim()||!Number.isFinite(t)||t<0?de:t===0?0:Math.max(1,Math.min(t,8))}function B(e,t){return t===0||e<t}function ye(){const e=Number.parseInt(process.env.ACA_ACP_EVENT_BATCH_INTERVAL_MS||"",10);return Number.isFinite(e)?Math.max(40,Math.min(e,2e3)):ce}async function Ie(e,t,n,o){const a=e.job_id,r=e.session_id,s=e.workspace_id,u=e.project_id??null,i=e.input??{},d=String(i.control?.type||"").toLowerCase();if(!d&&!i.message?.trim()&&(i.attachments??[]).length===0){await R(a,t.machineId,"job input missing message");return}const _=t.projects.find(p=>p.projectId===u),v=me(_?.rootPath||e.project_root_path||"");if(!v){await R(a,t.machineId,"job project root is unknown");return}fe(e)&&j.mkdirSync(v,{recursive:!0}),n.info(`claimed job=${a} session=${r} project=${_?.name??u??"unknown"}`);let l=!1,w=!1;const m=setInterval(()=>{l||(l=!0,I("POST",`/api/client/jobs/${encodeURIComponent(a)}/heartbeat`,{machineId:t.machineId}).catch(()=>{}).finally(()=>{l=!1})),w||(w=!0,W(a,o).catch(()=>{}).finally(()=>{w=!1}))},F);let g=0;const h=Ce(a,t.machineId),f=xe(a,t.machineId),P=new Set;let A=null;try{await f.sendNow({phase:"initializing",label:"准备工作区",detail:e.worktree_id?"worktree":"project root"}).catch(()=>{});const p=Y({projectId:u,sessionId:r,projectRoot:v,worktree:e});A=p,await Fe(a,t.machineId,p);const b=ge(i.acpRun);if(_e(b.agentType)&&await re(e.codex_provider_revision),d==="compact"){if(!e.acp_session_id)throw new Error("Current session does not have an ACP context to compact");const c=await G({agentType:x(b.agentType,"codex"),cliType:x(b.cliType,"builtin"),acpSessionId:e.acp_session_id,cwd:p.cwd,model:i.model,mode:i.mode,configOptionValues:i.configOptionValues,timeoutMs:i.timeoutMs,allowRollover:i.control?.allowRollover===!0,signal:o.signal,onStatus:S=>{f.report({phase:S.phase,label:S.label,detail:S.detail,updateType:S.updateType,raw:S})}});await f.flush(),await I("POST",`/api/client/jobs/${encodeURIComponent(a)}/complete`,{machineId:t.machineId,acpSessionId:c.acpSessionId,suppressTurn:!0,result:{ok:!0,control:{type:"compact",allowRollover:i.control?.allowRollover===!0},acpSessionId:c.acpSessionId,durationMs:c.durationMs,usage:c.contextUsage,maintenance:c.maintenance,worktree:k(p)}}),n.info(`completed context compact job=${a}`);return}if(d==="fork"){const c=String(i.control?.sourceAcpSessionId||"").trim();if(!c)throw new Error("Source session does not have a Codex ACP context to fork");await f.sendNow({phase:"forking",label:"正在分叉 Codex 会话",detail:i.control?.sourceSessionId?`源会话 ${i.control.sourceSessionId}`:void 0}).catch(()=>{});const S=await K({agentType:x(b.agentType,"codex"),cliType:x(b.cliType,"builtin"),sourceProviderSessionId:c,cwd:p.cwd,model:i.model,mode:i.mode,configOptionValues:i.configOptionValues,timeoutMs:i.timeoutMs,signal:o.signal,onStatus:C=>{f.report({phase:C.phase,label:C.label,detail:C.detail,updateType:C.updateType,raw:C})}});await f.flush(),await I("POST",`/api/client/jobs/${encodeURIComponent(a)}/complete`,{machineId:t.machineId,acpSessionId:S.acpSessionId,suppressTurn:!0,result:{ok:!0,control:{type:"fork",sourceSessionId:i.control?.sourceSessionId||null},acpSessionId:S.acpSessionId,worktree:k(p)}}),n.info(`completed Codex session fork job=${a}`);return}const T=await $e({cwd:p.cwd,sessionId:r,jobId:a,attachments:i.attachments??[]});T.length>0&&(n.info(`materialized ${T.length} attachment(s) for job=${a}`),await f.sendNow({phase:"initializing",label:"附件已写入本机",detail:T.map(c=>c.relativePath).join(", "),raw:{attachments:L(T)}}).catch(()=>{}));const N=Re(i,T),q=Ee(N,T),y=await X({agentType:x(b.agentType,"codex"),cliType:x(b.cliType,"builtin"),acpSessionId:e.acp_session_id??null,requireSessionResume:Se(e.session_raw),prompt:N,promptBlocks:q,cwd:p.cwd,model:i.model,mode:i.mode,configOptionValues:i.configOptionValues,canExecuteTools:i.canExecuteTools,timeoutMs:i.timeoutMs,contextWindow:Te(b.contextWindow),signal:o.signal,onStatus:c=>{f.report({phase:c.phase,label:c.label,detail:c.detail,updateType:c.updateType,raw:c})},onUpdate:c=>{g+=1;for(const C of te(c.raw))P.add(C);const S={turnId:e.user_turn_id??null,eventType:c.type,label:c.label,text:c.text,status:c.status,toolCallId:c.toolCallId,raw:ke(c.raw,g)};ae(S)&&h.push(S)},onPermissionRequest:c=>{const S=ne(i,c);return S?Promise.resolve(S):ze(a,t.machineId,c,o.signal)},onUserInputRequest:c=>Ue(a,t.machineId,c,o.signal)});await h.flush(),await f.flush();const D=O({cwd:p.cwd,evidencePaths:P}),E=ve(y.promptResponse);if(!(Ae(E)||!!y.content)){await R(a,t.machineId,`${y.agentType} stopped: ${E??"unknown"}`,{acpSessionId:y.acpSessionId,stopReason:E,exitCode:y.exitCode,stderr:We(y.stderr,4e3),worktree:k(p)});return}await I("POST",`/api/client/jobs/${encodeURIComponent(a)}/complete`,{machineId:t.machineId,acpSessionId:y.acpSessionId,assistantText:y.content,result:{ok:!0,acpSessionId:y.acpSessionId,stopReason:E,durationMs:y.durationMs,acpRun:i.acpRun??null,effectiveConfig:y.effectiveConfig??null,performance:y.performance??null,usage:y.contextUsage??null,updateCount:y.updateCount,statusTimeline:y.statusTimeline,worktree:k(p),attachments:L(T),files:D}}),n.info(`completed job=${a}`)}catch(p){if(Je(o.signal)){h.discard(),f.discard(),n.warn(`stopped local job=${a} because ACA Server already finalized it`);return}await h.flush(),await f.flush();const b=o.signal.aborted?"cancelled":p instanceof Error?p.message:String(p),T=A?O({cwd:A.cwd,evidencePaths:P}):[];await R(a,t.machineId,ie(b),{rawError:b,files:T})}finally{await h.flush(),await f.flush(),clearInterval(m)}}function Se(e){if(!e||typeof e!="object"||Array.isArray(e))return!1;const t=e.importedProviderSession;return!!(t&&typeof t=="object"&&!Array.isArray(t)&&t.strictResume===!0)}function ge(e){return!e||typeof e!="object"||Array.isArray(e)?{}:e}function _e(e){const t=String(e||"codex").toLowerCase().replace(/[^a-z0-9]+/g,"");return t==="codex"||t==="codexacp"||t==="codexstandard"}function Te(e){return typeof e=="number"&&Number.isFinite(e)&&e>0?e:void 0}function x(...e){for(const t of e)if(typeof t=="string"&&t.trim())return t.trim();return""}function ve(e){if(!e||typeof e!="object"||Array.isArray(e))return;const t=e;if(typeof t.stopReason=="string")return t.stopReason;if(typeof t.reason=="string")return t.reason;const n=t.part;if(n&&typeof n=="object"&&!Array.isArray(n)&&typeof n.reason=="string")return n.reason}function Ae(e){return e?["end_turn","stop","completed","complete"].includes(e):!1}function Ce(e,t){let n=[],o=null,a=Promise.resolve(),r=!1;const s=async d=>{d.length!==0&&await I("POST",`/api/client/jobs/${encodeURIComponent(e)}/events/batch`,{machineId:t,events:d})},u=()=>{if(o&&(clearTimeout(o),o=null),r)return n=[],a;const d=n;return n=[],a=a.then(()=>s(d)).catch(()=>{}),a},i=()=>{o||(o=setTimeout(()=>{u()},ye()),o.unref())};return{push(d){if(!r){if(oe(n,d),Pe(d)||n.length>=le){u();return}i()}},flush:u,discard(){r=!0,n=[],o&&clearTimeout(o),o=null}}}function Pe(e){return e.eventType==="tool_call"?!0:e.eventType!=="tool_call_update"?!1:["completed","failed","cancelled"].includes(String(e.status||"").toLowerCase())}function xe(e,t){let n=null,o=0,a="",r=null,s=Promise.resolve(),u=!1;const i=l=>JSON.stringify({phase:l.phase??"",label:l.label??"",detail:l.detail??"",updateType:l.updateType??""}),d=async l=>{await I("POST",`/api/client/jobs/${encodeURIComponent(e)}/presence`,{machineId:t,phase:l.phase,label:l.label,detail:l.detail,updateType:l.updateType,raw:l.raw}),o=Date.now(),a=i(l)},_=()=>{if(r&&(clearTimeout(r),r=null),u)return n=null,s;if(!n)return s;const l=n;return n=null,s=s.then(()=>d(l)).catch(()=>{}),s},v=l=>{r||(r=setTimeout(()=>{_()},l),r.unref())};return{report(l){if(u)return;const w=Date.now(),g=i(l)!==a,h=w-o>=ue;if(!g&&!h)return;n=l;const f=w-o;if(f>=U||o===0){_();return}v(U-f)},sendNow(l){return u?s:(n=l,_())},flush:_,discard(){u=!0,n=null,r&&clearTimeout(r),r=null}}}function ke(e,t){return e&&typeof e=="object"&&!Array.isArray(e)?{...e,acaSequence:t}:{value:e,acaSequence:t}}function Re(e,t=[]){const n=e?.message?.trim()??"";if(t.length===0)return n;const a=["用户随消息上传了以下附件,aca 已将附件写入本机工作区。需要读取附件时请直接使用下面的 localPath;图片附件也已作为 ACP image block 随本轮请求传入。",...t.map((r,s)=>`${s+1}. ${r.name} (${r.kind}, ${r.mimeType}, ${Oe(r.size)})
|
|
2
2
|
localPath: ${r.path}
|
|
3
3
|
workspacePath: ${r.relativePath}`)].join(`
|
|
4
4
|
`);return n?`${n}
|
|
5
5
|
|
|
6
|
-
${a}`:a}function
|
|
6
|
+
${a}`:a}function Ee(e,t){const n=[{type:"text",text:e}];for(const o of t)o.kind!=="image"||!o.mimeType.startsWith("image/")||n.push({type:"image",data:o.base64,mimeType:o.mimeType,uri:o.path});return n}async function $e(e){if(e.attachments.length===0)return[];const t=$.join(e.cwd,".aca","attachments",J(e.sessionId),J(e.jobId));j.mkdirSync(t,{recursive:!0});const n=[];for(const[o,a]of e.attachments.entries()){const r=a.attachmentId?{mimeType:a.mimeType||"application/octet-stream",bytes:await Z(`/api/client/attachments/${encodeURIComponent(a.attachmentId)}`)}:je(a.dataUrl??""),s=a.mimeType||r.mimeType||"application/octet-stream",u=a.kind==="image"||s.startsWith("image/")?"image":"file",i=Me(a.name||`attachment-${o+1}`,s),d=$.join(t,`${String(o+1).padStart(2,"0")}-${i}`);j.writeFileSync(d,r.bytes),n.push({name:a.name||i,mimeType:s,size:r.bytes.byteLength,kind:u,path:d,relativePath:$.relative(e.cwd,d),base64:r.bytes.toString("base64"),sha256:H.createHash("sha256").update(r.bytes).digest("hex")})}return n}function je(e){const t=e.indexOf(",");if(!e.startsWith("data:")||t<0)throw new Error("attachment dataUrl is not a data URL");const n=e.slice(5,t),o=e.slice(t+1),a=n.split(";").filter(Boolean),r=a[0]?.includes("/")?a[0]:"application/octet-stream";if(!a.includes("base64"))throw new Error("attachment dataUrl is not base64 encoded");return{mimeType:r,bytes:Buffer.from(o,"base64")}}function Me(e,t){const n=$.basename(e).replace(/[<>:"/\\|?*\x00-\x1F]/g,"_").trim(),o=`attachment${Ne(t)}`;return(!n||n==="."||n===".."?o:n).slice(0,180)}function Ne(e){switch(e.toLowerCase()){case"image/png":return".png";case"image/jpeg":return".jpg";case"image/webp":return".webp";case"image/gif":return".gif";case"text/plain":return".txt";case"application/json":return".json";case"text/markdown":case"text/x-markdown":return".md";case"application/pdf":return".pdf";case"application/msword":return".doc";case"application/vnd.openxmlformats-officedocument.wordprocessingml.document":return".docx";case"application/vnd.ms-excel":return".xls";case"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":return".xlsx";case"application/vnd.ms-powerpoint":return".ppt";case"application/vnd.openxmlformats-officedocument.presentationml.presentation":return".pptx";case"application/zip":return".zip";case"application/x-7z-compressed":return".7z";case"application/x-rar-compressed":case"application/vnd.rar":return".rar";case"application/gzip":return".gz";case"application/x-tar":return".tar";default:return""}}function J(e){return e.replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,80)||"item"}function L(e){return e.map(t=>({name:t.name,kind:t.kind,mimeType:t.mimeType,size:t.size,path:t.path,relativePath:t.relativePath,sha256:t.sha256}))}function Oe(e){if(!Number.isFinite(e)||e<=0)return"0 B";const t=["B","KB","MB","GB"];let n=e,o=0;for(;n>=1024&&o<t.length-1;)n/=1024,o+=1;return`${n>=10||o===0?n.toFixed(0):n.toFixed(1)} ${t[o]}`}async function Fe(e,t,n){!n.usedWorktree||!n.worktreeId||await I("POST",`/api/client/jobs/${encodeURIComponent(e)}/worktree`,{machineId:t,baseCommit:n.baseCommit??null,worktreePath:n.cwd,status:n.status??"active",raw:n.raw??k(n)})}function k(e){return{usedWorktree:e.usedWorktree,worktreeId:e.worktreeId??null,cwd:e.cwd,status:e.status??null,baseCommit:e.baseCommit??null,branchName:e.branchName??null,baseBranch:e.baseBranch??null}}async function ze(e,t,n,o){const r=(await I("POST",`/api/client/jobs/${encodeURIComponent(e)}/permissions`,{machineId:t,request:n})).item.permission_id;for(;!o.aborted;){await M(z);const s=await I("GET",`/api/client/permissions/${encodeURIComponent(r)}`);if(s.item.status==="resolved"&&s.item.response){const u=s.item.response.outcome;return u.outcome==="selected"?{outcome:u}:{outcome:{outcome:"cancelled"}}}}return{outcome:{outcome:"cancelled"}}}async function Ue(e,t,n,o){const r=(await I("POST",`/api/client/jobs/${encodeURIComponent(e)}/permissions`,{machineId:t,request:{kind:"elicitation",...n}})).item.permission_id;for(;!o.aborted;){await M(z);const s=await I("GET",`/api/client/permissions/${encodeURIComponent(r)}`);if(s.item.status==="resolved"&&s.item.response){const u=s.item.response.outcome;return u.outcome==="submitted"?{outcome:u}:{outcome:{outcome:"cancelled"}}}}return{outcome:{outcome:"cancelled"}}}async function W(e,t){if(t.signal.aborted)return;const n=await I("GET",`/api/client/jobs/${encodeURIComponent(e)}`);if(Number(n.item.cancel_requested??0)===1){t.abort();return}const o=Be(n.item);o&&t.abort(new V(e,o))}function Be(e){const t=String(e.status||"").toLowerCase();return t&&!["claimed","running"].includes(t)?t:null}class V extends Error{constructor(t,n){super(`ACA Server already finalized job=${t} status=${n}`),this.name="ServerFinalizedJobError"}}function Je(e){return e.aborted&&e.reason instanceof V}async function R(e,t,n,o){await I("POST",`/api/client/jobs/${encodeURIComponent(e)}/fail`,{machineId:t,error:n,result:o})}function M(e){return new Promise(t=>setTimeout(t,e))}function Le(e){return Math.min(1e4,500*2**Math.min(5,Math.max(0,e-1)))+Math.floor(Math.random()*250)}function We(e,t){return e.length<=t?e:`${e.slice(0,t)}...`}export{Be as serverFinalizedJobStatus,it as startJobWorkerLoop};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{execFileSync as g}from"node:child_process";import{randomUUID as R}from"node:crypto";import l from"node:fs";import T from"node:os";import c from"node:path";import{acaServerDownloadToFile as Q,acaServerRequest as b}from"./aca-server-client.js";import{loadAcaConfig as Z,upsertLocalProject as q}from"./aca-config.js";import{startLongPollWorker as tt}from"./long-poll-worker.js";import{expandWorkerRuntimePath as et}from"./worker-runtime-paths.js";const F=25e3,rt=F+1e4,x=300,ot=3e4;let E=null;function zt(t){return tt({run:async()=>{const e=Z();if(!e.token)throw new Error("Worker 尚未配置认证令牌");const r=await b("GET",`/api/client/machine-operations/claim?machineId=${encodeURIComponent(e.machineId)}&waitMs=${F}`,void 0,{timeoutMs:rt});if(r.item)try{const o=await nt(r.item);await b("POST",`/api/client/machine-operations/${encodeURIComponent(r.item.requestId)}/complete`,{result:o})}catch(o){const n=o instanceof Error?o.message:String(o);t.warn(`machine filesystem request failed request=${r.item.requestId}: ${n}`),await b("POST",`/api/client/machine-operations/${encodeURIComponent(r.item.requestId)}/fail`,{message:n}).catch(()=>{})}},onError:(e,r)=>t.warn(`machine filesystem worker failed, retry in ${r}ms: ${e instanceof Error?e.message:String(e)}`)})}async function nt(t){if(t.operation==="filesystem_roots")return Et();if(t.operation==="list_directory")return _t({directoryPath:String(t.payload.path||""),includeHidden:t.payload.includeHidden===!0});if(t.operation==="register_project")return Dt(t.payload);if(t.operation==="list_project_directory")return at(t.payload);if(t.operation==="upload_project_file")return ct(t.payload);if(t.operation==="get_project_git_status")return O(t.payload);if(t.operation==="get_project_git_diff")return dt(t.payload);if(t.operation==="get_project_git_log")return mt(t.payload);if(t.operation==="get_project_git_commit")return pt(t.payload);if(t.operation==="get_worktree_status")return j(t.payload);if(t.operation==="commit_worktree")return it(t.payload);if(t.operation==="cleanup_worktree")return st(t.payload);throw new Error("不支持的 Machine 文件系统操作。 ")}function j(t){const e=D(String(t.worktreePath||""));if(!l.existsSync(e))return{exists:!1,isRepository:!1,dirty:!1,changedFiles:0,ahead:0};const r=m(e);if(!w(r))return{exists:!0,isRepository:!1,dirty:yt(r),changedFiles:null,ahead:0};const o=O({rootPath:r}),n=String(t.baseCommit||"").trim(),s=n&&/^[0-9a-f]{7,64}$/i.test(n)?gt(r,n):0;return{exists:!0,isRepository:!0,dirty:Array.isArray(o.files)&&o.files.length>0,changedFiles:Array.isArray(o.files)?o.files.length:0,branch:o.branch,ahead:s,files:Array.isArray(o.files)?o.files.slice(0,50):[],truncated:o.truncated===!0}}function it(t){const e=m(String(t.worktreePath||"")),r=String(t.message||"").trim();if(!r)throw new Error("提交说明不能为空。");if(!w(e))throw new Error("当前 Worktree 不是 Git 仓库,无法提交。");g("git",["-C",e,"add","-A"],{stdio:"pipe",timeout:3e4});try{return g("git",["-C",e,"diff","--cached","--quiet"],{stdio:"pipe",timeout:1e4}),{committed:!1,reason:"no_changes",...j(t)}}catch(o){if(Number(o.status)!==1)throw o}return g("git",["-C",e,"commit","-m",r],{stdio:"pipe",timeout:6e4}),{committed:!0,commit:f(e,["rev-parse","HEAD"],8*1024).trim(),...j(t)}}function st(t){const e=D(String(t.worktreePath||"")),r=m(String(t.projectRoot||""));if(p(e)===p(r))throw new Error("不能清理项目主目录。");if(!l.existsSync(e))return{removed:!1,reason:"not_found"};const o=m(e),n=String(t.branchName||"").trim(),s=w(o)&&w(r);if(s){if(g("git",["-C",r,"worktree","remove","--force",o],{stdio:"pipe",timeout:6e4}),t.deleteBranch===!0&&n)try{g("git",["-C",r,"branch","-D",n],{stdio:"pipe",timeout:3e4})}catch(i){const a=String(i.stderr||i);if(!/not found|not exist/i.test(a))throw i}}else l.rmSync(o,{recursive:!0,force:!0,maxRetries:2,retryDelay:200});return{removed:!0,branchDeleted:!!(s&&t.deleteBranch===!0&&n)}}function at(t){const e=m(String(t.rootPath||"")),r=P(String(t.relativePath||"")),o=N(e,r,!0),n=Math.max(0,Number(t.offset||0)),s=Math.min(300,Math.max(20,Number(t.limit||200))),i=l.readdirSync(o,{withFileTypes:!0}).filter(u=>!u.isSymbolicLink()).map(u=>({name:u.name,path:K(e,c.join(o,u.name)),type:u.isDirectory()?"directory":u.isFile()?"file":"other",hidden:u.name.startsWith("."),extension:u.isFile()?c.extname(u.name).slice(1).toLowerCase():""})).filter(u=>u.type!=="other").sort((u,d)=>u.type!==d.type?u.type==="directory"?-1:1:u.name.localeCompare(d.name,void 0,{numeric:!0,sensitivity:"base"})),a=i.slice(n,n+s);return{path:r,rootKind:t.rootKind==="worktree"?"worktree":"project",items:a,total:i.length,nextOffset:n+a.length<i.length?n+a.length:null}}async function ct(t){const e=String(t.uploadId||"").trim();if(!e)throw new Error("项目上传任务缺少 uploadId。");const r=m(String(t.rootPath||"")),o=P(String(t.relativeDirectory||"")),n=N(r,o,!0),s=I(String(t.fileName||"")),i=Number(t.sizeBytes),a=String(t.sha256||"").trim().toLowerCase();if(!Number.isSafeInteger(i)||i<0)throw new Error("项目上传文件大小无效。");if(!/^[0-9a-f]{64}$/.test(a))throw new Error("项目上传文件摘要无效。");const u=c.join(n,`.aca-upload-${R()}.tmp`);try{const d=await Q(`/api/client/project-uploads/${encodeURIComponent(e)}`,u,{maxBytes:i,timeoutMs:3e5});if(d.sizeBytes!==i)throw new Error("项目上传文件大小校验失败。");if(d.sha256.toLowerCase()!==a)throw new Error("项目上传文件完整性校验失败。");const h=lt(r,o,s,u);return{name:c.basename(h),path:K(r,h),directory:o,size:d.sizeBytes,sha256:d.sha256,renamed:c.basename(h)!==s,rootKind:t.rootKind==="worktree"?"worktree":"project"}}finally{l.rmSync(u,{force:!0})}}function lt(t,e,r,o){const n=m(t),s=P(e),i=N(n,s,!0),a=I(r);if(l.realpathSync.native(c.dirname(o))!==i)throw new Error("项目上传临时文件不在目标目录中。");const d=l.lstatSync(o);if(!d.isFile()||d.isSymbolicLink())throw new Error("项目上传临时文件无效。");for(let h=1;h<=1e4;h+=1){const J=h===1?a:ut(a,h),S=c.join(i,J);try{return l.linkSync(o,S),l.chmodSync(S,420),l.rmSync(o,{force:!0}),S}catch(v){if(L(v))continue;if(!ft(v))throw v;try{return l.copyFileSync(o,S,l.constants.COPYFILE_EXCL),l.chmodSync(S,420),l.rmSync(o,{force:!0}),S}catch(C){if(L(C))continue;throw C}}}throw new Error("同名文件过多,无法生成可用文件名。")}function I(t){const e=t.trim();if(!e||e==="."||e===".."||/[\\/\x00-\x1F]/.test(e))throw new Error("项目上传文件名无效。");if(process.platform==="win32"&&(/[<>:"|?*]/.test(e)||/[. ]$/.test(e)))throw new Error("项目上传文件名在 Windows 上无效。");return e.slice(0,180)}function ut(t,e){const r=c.extname(t);return`${r?t.slice(0,-r.length):t} (${e})${r}`}function L(t){return!!(t&&typeof t=="object"&&t.code==="EEXIST")}function ft(t){const e=t&&typeof t=="object"?t.code:"";return["EPERM","EACCES","EXDEV","ENOTSUP","EOPNOTSUPP"].includes(String(e||""))}const M=500,_=2*1024*1024,y=2*1024*1024;function O(t){const e=m(String(t.rootPath||""));if(!w(e))return{isRepository:!1,branch:null,files:[],ahead:0,behind:0};const o=f(e,["status","--porcelain=v1","-z","--untracked-files=all","--no-renames"],_).split("\0").filter(Boolean).slice(0,M),n=o.map(a=>wt(a)).filter(a=>!!a),s=A(f(e,["diff","--no-ext-diff","--numstat","-z","--no-renames","HEAD","--","."],_));for(const a of n){const u=s.get(a.path);if(u)a.additions=u.additions,a.deletions=u.deletions;else if(a.status==="untracked"){const d=G(e,a.path);a.additions=d.additions,a.deletions=d.deletions}}const i=xt(e);return{isRepository:!0,branch:bt(e),files:n,ahead:i.ahead,behind:i.behind,truncated:o.length>=M}}function dt(t){const e=m(String(t.rootPath||"")),r=P(String(t.relativePath||""));if(!r)throw new Error("Diff 文件路径不能为空。");if(!w(e))return{isRepository:!1,path:r,diff:"",additions:0,deletions:0};const o=W(e,r),n=St(e,r),s=n?f(e,["diff","--no-ext-diff","--unified=80","HEAD","--",r],y):B(e,["diff","--no-ext-diff","--no-index","--unified=80",process.platform==="win32"?"NUL":"/dev/null",o],y),i=n?A(f(e,["diff","--no-ext-diff","--numstat","-z","--no-renames","HEAD","--",r],32*1024)).get(r):G(e,r);return{isRepository:!0,path:r,diff:U(s),additions:i?.additions??0,deletions:i?.deletions??0,truncated:s.length>y}}function mt(t){const e=m(String(t.rootPath||""));if(!w(e))return{isRepository:!1,items:[]};if(!Pt(e))return{isRepository:!0,items:[]};const r=Math.min(200,Math.max(20,Number(t.limit||100)));return{isRepository:!0,items:f(e,["log",`--max-count=${r}`,"--date=iso-strict","--pretty=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%D%x1f%s%x1f%b%x1e"],_).split("").map(i=>ht(i)).filter(i=>!!i),limit:r}}function pt(t){const e=m(String(t.rootPath||"")),r=String(t.commit||"").trim();if(!/^[0-9a-f]{7,64}$/i.test(r))throw new Error("Git 提交标识无效。");if(!w(e))return{isRepository:!1,commit:r,diff:"",additions:0,deletions:0};f(e,["cat-file","-e",`${r}^{commit}`],8*1024);const o=f(e,["show","--no-ext-diff","--no-color","--format=","--stat","--patch","--unified=40",r],y),n=[...A(f(e,["show","--no-ext-diff","--numstat","-z","--no-renames","--format=",r],_)).values()];return{isRepository:!0,commit:r,diff:U(o),additions:n.reduce((s,i)=>s+(i.additions||0),0),deletions:n.reduce((s,i)=>s+(i.deletions||0),0),changedFiles:n.length,truncated:o.length>y}}function ht(t){const e=t.replace(/^\s+|\s+$/g,"");if(!e)return null;const r=e.split(""),o=String(r[0]||"").trim();return o?{hash:o,shortHash:String(r[1]||o.slice(0,8)).trim(),authorName:String(r[2]||"").trim(),authorEmail:String(r[3]||"").trim(),authoredAt:String(r[4]||"").trim(),refs:String(r[5]||"").trim(),subject:String(r[6]||"").trim(),body:r.slice(7).join("").trim()}:null}function wt(t){if(t.length<4)return null;const e=t[0]||" ",r=t[1]||" ",o=P(t.slice(3));return o?{path:o,status:e==="?"&&r==="?"?"untracked":e==="U"||r==="U"?"conflicted":e==="D"||r==="D"?"deleted":e==="A"||r==="A"?"added":e==="R"||r==="R"?"renamed":"modified",indexStatus:e,worktreeStatus:r,staged:e!==" "&&e!=="?",additions:null,deletions:null}:null}function A(t){const e=new Map;for(const r of t.split("\0")){if(!r)continue;const o=H(r);if(!o||!o.path)continue;const n=P(o.path);n&&e.set(n,{additions:o.additions,deletions:o.deletions})}return e}function G(t,e){const r=W(t,e),n=B(t,["diff","--no-ext-diff","--no-index","--numstat","-z","--",process.platform==="win32"?"NUL":"/dev/null",r],32*1024).split("\0",1)[0]||"",s=H(n);return s?{additions:s.additions,deletions:s.deletions}:{additions:null,deletions:null}}function H(t){const e=t.indexOf(" "),r=e<0?-1:t.indexOf(" ",e+1);if(e<0||r<0)return null;const o=t.slice(0,e),n=t.slice(e+1,r);return{additions:/^\d+$/.test(o)?Number(o):null,deletions:/^\d+$/.test(n)?Number(n):null,path:t.slice(r+1)}}function w(t){try{return f(t,["rev-parse","--is-inside-work-tree"],8*1024).trim()==="true"}catch(e){if(String(e.code||"")==="ENOENT")throw new Error("设备未安装 Git。");return!1}}function gt(t,e){try{const r=f(t,["rev-list","--count",`${e}..HEAD`],8192).trim(),o=Number(r);return Number.isSafeInteger(o)&&o>0?o:0}catch{return 0}}function yt(t){try{return l.readdirSync(t).length>0}catch{return!1}}function St(t,e){try{return f(t,["ls-files","--error-unmatch","--",e],8*1024),!0}catch{return!1}}function Pt(t){try{return f(t,["rev-parse","--verify","HEAD"],8*1024),!0}catch{return!1}}function bt(t){try{return f(t,["symbolic-ref","--quiet","--short","HEAD"],8*1024).trim()||null}catch{try{return`HEAD (${f(t,["rev-parse","--short","HEAD"],8*1024).trim()})`}catch{return null}}}function xt(t){try{const e=f(t,["rev-list","--left-right","--count","HEAD...@{upstream}"],8192).trim().split(/\s+/).map(Number);return{ahead:Number.isFinite(e[0])?e[0]:0,behind:Number.isFinite(e[1])?e[1]:0}}catch{return{ahead:0,behind:0}}}function W(t,e){const r=c.resolve(t,...e.split("/")),o=c.relative(t,r);if(o.startsWith("..")||c.isAbsolute(o))throw new Error("Diff 路径超出项目范围。");return r}function f(t,e,r){return g("git",["-C",t,...e],{encoding:"utf8",timeout:2e4,maxBuffer:r,stdio:["ignore","pipe","pipe"]})}function B(t,e,r){try{return f(t,e,r)}catch(o){const n=String(o.stdout||"");if(n)return n;if(Number(o.status)===1)return"";throw o}}function U(t){return t.length<=y?t:`${t.slice(0,y)}
|
|
1
|
+
import{execFileSync as g}from"node:child_process";import{randomUUID as N}from"node:crypto";import u from"node:fs";import T from"node:os";import c from"node:path";import{acaServerDownloadToFile as Q,acaServerRequest as x}from"./aca-server-client.js";import{loadAcaConfig as Z,upsertLocalProject as q}from"./aca-config.js";import{startLongPollWorker as tt}from"./long-poll-worker.js";import{expandWorkerRuntimePath as et}from"./worker-runtime-paths.js";import{syncCodexProviderRuntimeConfig as rt}from"./codex-provider-runtime-config.js";import{defaultAcpRuntimePool as ot}from"../acp/client/acp-runtime-pool.js";const F=25e3,nt=F+1e4,b=300,it=3e4;let _=null;function Yt(t){return tt({run:async()=>{const e=Z();if(!e.token)throw new Error("Worker 尚未配置认证令牌");const r=await x("GET",`/api/client/machine-operations/claim?machineId=${encodeURIComponent(e.machineId)}&waitMs=${F}`,void 0,{timeoutMs:nt});if(r.item)try{const o=await st(r.item);await x("POST",`/api/client/machine-operations/${encodeURIComponent(r.item.requestId)}/complete`,{result:o})}catch(o){const n=o instanceof Error?o.message:String(o);t.warn(`machine filesystem request failed request=${r.item.requestId}: ${n}`),await x("POST",`/api/client/machine-operations/${encodeURIComponent(r.item.requestId)}/fail`,{message:n}).catch(()=>{})}},onError:(e,r)=>t.warn(`machine filesystem worker failed, retry in ${r}ms: ${e instanceof Error?e.message:String(e)}`)})}async function st(t){if(t.operation==="sync_codex_provider_config"){const e=await rt(),r=await ot.retireProviderRuntimes("codex-acp");return{configured:!!e,revision:e?.revision??null,retiredRuntimes:r}}if(t.operation==="filesystem_roots")return vt();if(t.operation==="list_directory")return jt({directoryPath:String(t.payload.path||""),includeHidden:t.payload.includeHidden===!0});if(t.operation==="register_project")return Dt(t.payload);if(t.operation==="list_project_directory")return ut(t.payload);if(t.operation==="upload_project_file")return lt(t.payload);if(t.operation==="get_project_git_status")return O(t.payload);if(t.operation==="get_project_git_diff")return pt(t.payload);if(t.operation==="get_project_git_log")return ht(t.payload);if(t.operation==="get_project_git_commit")return wt(t.payload);if(t.operation==="get_worktree_status")return j(t.payload);if(t.operation==="commit_worktree")return at(t.payload);if(t.operation==="cleanup_worktree")return ct(t.payload);throw new Error("不支持的 Machine 文件系统操作。 ")}function j(t){const e=C(String(t.worktreePath||""));if(!u.existsSync(e))return{exists:!1,isRepository:!1,dirty:!1,changedFiles:0,ahead:0};const r=m(e);if(!w(r))return{exists:!0,isRepository:!1,dirty:Pt(r),changedFiles:null,ahead:0};const o=O({rootPath:r}),n=String(t.baseCommit||"").trim(),s=n&&/^[0-9a-f]{7,64}$/i.test(n)?St(r,n):0;return{exists:!0,isRepository:!0,dirty:Array.isArray(o.files)&&o.files.length>0,changedFiles:Array.isArray(o.files)?o.files.length:0,branch:o.branch,ahead:s,files:Array.isArray(o.files)?o.files.slice(0,50):[],truncated:o.truncated===!0}}function at(t){const e=m(String(t.worktreePath||"")),r=String(t.message||"").trim();if(!r)throw new Error("提交说明不能为空。");if(!w(e))throw new Error("当前 Worktree 不是 Git 仓库,无法提交。");g("git",["-C",e,"add","-A"],{stdio:"pipe",timeout:3e4});try{return g("git",["-C",e,"diff","--cached","--quiet"],{stdio:"pipe",timeout:1e4}),{committed:!1,reason:"no_changes",...j(t)}}catch(o){if(Number(o.status)!==1)throw o}return g("git",["-C",e,"commit","-m",r],{stdio:"pipe",timeout:6e4}),{committed:!0,commit:f(e,["rev-parse","HEAD"],8*1024).trim(),...j(t)}}function ct(t){const e=C(String(t.worktreePath||"")),r=m(String(t.projectRoot||""));if(p(e)===p(r))throw new Error("不能清理项目主目录。");if(!u.existsSync(e))return{removed:!1,reason:"not_found"};const o=m(e),n=String(t.branchName||"").trim(),s=w(o)&&w(r);if(s){if(g("git",["-C",r,"worktree","remove","--force",o],{stdio:"pipe",timeout:6e4}),t.deleteBranch===!0&&n)try{g("git",["-C",r,"branch","-D",n],{stdio:"pipe",timeout:3e4})}catch(i){const a=String(i.stderr||i);if(!/not found|not exist/i.test(a))throw i}}else u.rmSync(o,{recursive:!0,force:!0,maxRetries:2,retryDelay:200});return{removed:!0,branchDeleted:!!(s&&t.deleteBranch===!0&&n)}}function ut(t){const e=m(String(t.rootPath||"")),r=P(String(t.relativePath||"")),o=R(e,r,!0),n=Math.max(0,Number(t.offset||0)),s=Math.min(300,Math.max(20,Number(t.limit||200))),i=u.readdirSync(o,{withFileTypes:!0}).filter(l=>!l.isSymbolicLink()).map(l=>({name:l.name,path:K(e,c.join(o,l.name)),type:l.isDirectory()?"directory":l.isFile()?"file":"other",hidden:l.name.startsWith("."),extension:l.isFile()?c.extname(l.name).slice(1).toLowerCase():""})).filter(l=>l.type!=="other").sort((l,d)=>l.type!==d.type?l.type==="directory"?-1:1:l.name.localeCompare(d.name,void 0,{numeric:!0,sensitivity:"base"})),a=i.slice(n,n+s);return{path:r,rootKind:t.rootKind==="worktree"?"worktree":"project",items:a,total:i.length,nextOffset:n+a.length<i.length?n+a.length:null}}async function lt(t){const e=String(t.uploadId||"").trim();if(!e)throw new Error("项目上传任务缺少 uploadId。");const r=m(String(t.rootPath||"")),o=P(String(t.relativeDirectory||"")),n=R(r,o,!0),s=I(String(t.fileName||"")),i=Number(t.sizeBytes),a=String(t.sha256||"").trim().toLowerCase();if(!Number.isSafeInteger(i)||i<0)throw new Error("项目上传文件大小无效。");if(!/^[0-9a-f]{64}$/.test(a))throw new Error("项目上传文件摘要无效。");const l=c.join(n,`.aca-upload-${N()}.tmp`);try{const d=await Q(`/api/client/project-uploads/${encodeURIComponent(e)}`,l,{maxBytes:i,timeoutMs:3e5});if(d.sizeBytes!==i)throw new Error("项目上传文件大小校验失败。");if(d.sha256.toLowerCase()!==a)throw new Error("项目上传文件完整性校验失败。");const h=ft(r,o,s,l);return{name:c.basename(h),path:K(r,h),directory:o,size:d.sizeBytes,sha256:d.sha256,renamed:c.basename(h)!==s,rootKind:t.rootKind==="worktree"?"worktree":"project"}}finally{u.rmSync(l,{force:!0})}}function ft(t,e,r,o){const n=m(t),s=P(e),i=R(n,s,!0),a=I(r);if(u.realpathSync.native(c.dirname(o))!==i)throw new Error("项目上传临时文件不在目标目录中。");const d=u.lstatSync(o);if(!d.isFile()||d.isSymbolicLink())throw new Error("项目上传临时文件无效。");for(let h=1;h<=1e4;h+=1){const J=h===1?a:dt(a,h),S=c.join(i,J);try{return u.linkSync(o,S),u.chmodSync(S,420),u.rmSync(o,{force:!0}),S}catch(v){if(L(v))continue;if(!mt(v))throw v;try{return u.copyFileSync(o,S,u.constants.COPYFILE_EXCL),u.chmodSync(S,420),u.rmSync(o,{force:!0}),S}catch(D){if(L(D))continue;throw D}}}throw new Error("同名文件过多,无法生成可用文件名。")}function I(t){const e=t.trim();if(!e||e==="."||e===".."||/[\\/\x00-\x1F]/.test(e))throw new Error("项目上传文件名无效。");if(process.platform==="win32"&&(/[<>:"|?*]/.test(e)||/[. ]$/.test(e)))throw new Error("项目上传文件名在 Windows 上无效。");return e.slice(0,180)}function dt(t,e){const r=c.extname(t);return`${r?t.slice(0,-r.length):t} (${e})${r}`}function L(t){return!!(t&&typeof t=="object"&&t.code==="EEXIST")}function mt(t){const e=t&&typeof t=="object"?t.code:"";return["EPERM","EACCES","EXDEV","ENOTSUP","EOPNOTSUPP"].includes(String(e||""))}const M=500,E=2*1024*1024,y=2*1024*1024;function O(t){const e=m(String(t.rootPath||""));if(!w(e))return{isRepository:!1,branch:null,files:[],ahead:0,behind:0};const o=f(e,["status","--porcelain=v1","-z","--untracked-files=all","--no-renames"],E).split("\0").filter(Boolean).slice(0,M),n=o.map(a=>yt(a)).filter(a=>!!a),s=A(f(e,["diff","--no-ext-diff","--numstat","-z","--no-renames","HEAD","--","."],E));for(const a of n){const l=s.get(a.path);if(l)a.additions=l.additions,a.deletions=l.deletions;else if(a.status==="untracked"){const d=G(e,a.path);a.additions=d.additions,a.deletions=d.deletions}}const i=Et(e);return{isRepository:!0,branch:_t(e),files:n,ahead:i.ahead,behind:i.behind,truncated:o.length>=M}}function pt(t){const e=m(String(t.rootPath||"")),r=P(String(t.relativePath||""));if(!r)throw new Error("Diff 文件路径不能为空。");if(!w(e))return{isRepository:!1,path:r,diff:"",additions:0,deletions:0};const o=B(e,r),n=xt(e,r),s=n?f(e,["diff","--no-ext-diff","--unified=80","HEAD","--",r],y):W(e,["diff","--no-ext-diff","--no-index","--unified=80",process.platform==="win32"?"NUL":"/dev/null",o],y),i=n?A(f(e,["diff","--no-ext-diff","--numstat","-z","--no-renames","HEAD","--",r],32*1024)).get(r):G(e,r);return{isRepository:!0,path:r,diff:U(s),additions:i?.additions??0,deletions:i?.deletions??0,truncated:s.length>y}}function ht(t){const e=m(String(t.rootPath||""));if(!w(e))return{isRepository:!1,items:[]};if(!bt(e))return{isRepository:!0,items:[]};const r=Math.min(200,Math.max(20,Number(t.limit||100)));return{isRepository:!0,items:f(e,["log",`--max-count=${r}`,"--date=iso-strict","--pretty=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%D%x1f%s%x1f%b%x1e"],E).split("").map(i=>gt(i)).filter(i=>!!i),limit:r}}function wt(t){const e=m(String(t.rootPath||"")),r=String(t.commit||"").trim();if(!/^[0-9a-f]{7,64}$/i.test(r))throw new Error("Git 提交标识无效。");if(!w(e))return{isRepository:!1,commit:r,diff:"",additions:0,deletions:0};f(e,["cat-file","-e",`${r}^{commit}`],8*1024);const o=f(e,["show","--no-ext-diff","--no-color","--format=","--stat","--patch","--unified=40",r],y),n=[...A(f(e,["show","--no-ext-diff","--numstat","-z","--no-renames","--format=",r],E)).values()];return{isRepository:!0,commit:r,diff:U(o),additions:n.reduce((s,i)=>s+(i.additions||0),0),deletions:n.reduce((s,i)=>s+(i.deletions||0),0),changedFiles:n.length,truncated:o.length>y}}function gt(t){const e=t.replace(/^\s+|\s+$/g,"");if(!e)return null;const r=e.split(""),o=String(r[0]||"").trim();return o?{hash:o,shortHash:String(r[1]||o.slice(0,8)).trim(),authorName:String(r[2]||"").trim(),authorEmail:String(r[3]||"").trim(),authoredAt:String(r[4]||"").trim(),refs:String(r[5]||"").trim(),subject:String(r[6]||"").trim(),body:r.slice(7).join("").trim()}:null}function yt(t){if(t.length<4)return null;const e=t[0]||" ",r=t[1]||" ",o=P(t.slice(3));return o?{path:o,status:e==="?"&&r==="?"?"untracked":e==="U"||r==="U"?"conflicted":e==="D"||r==="D"?"deleted":e==="A"||r==="A"?"added":e==="R"||r==="R"?"renamed":"modified",indexStatus:e,worktreeStatus:r,staged:e!==" "&&e!=="?",additions:null,deletions:null}:null}function A(t){const e=new Map;for(const r of t.split("\0")){if(!r)continue;const o=H(r);if(!o||!o.path)continue;const n=P(o.path);n&&e.set(n,{additions:o.additions,deletions:o.deletions})}return e}function G(t,e){const r=B(t,e),n=W(t,["diff","--no-ext-diff","--no-index","--numstat","-z","--",process.platform==="win32"?"NUL":"/dev/null",r],32*1024).split("\0",1)[0]||"",s=H(n);return s?{additions:s.additions,deletions:s.deletions}:{additions:null,deletions:null}}function H(t){const e=t.indexOf(" "),r=e<0?-1:t.indexOf(" ",e+1);if(e<0||r<0)return null;const o=t.slice(0,e),n=t.slice(e+1,r);return{additions:/^\d+$/.test(o)?Number(o):null,deletions:/^\d+$/.test(n)?Number(n):null,path:t.slice(r+1)}}function w(t){try{return f(t,["rev-parse","--is-inside-work-tree"],8*1024).trim()==="true"}catch(e){if(String(e.code||"")==="ENOENT")throw new Error("设备未安装 Git。");return!1}}function St(t,e){try{const r=f(t,["rev-list","--count",`${e}..HEAD`],8192).trim(),o=Number(r);return Number.isSafeInteger(o)&&o>0?o:0}catch{return 0}}function Pt(t){try{return u.readdirSync(t).length>0}catch{return!1}}function xt(t,e){try{return f(t,["ls-files","--error-unmatch","--",e],8*1024),!0}catch{return!1}}function bt(t){try{return f(t,["rev-parse","--verify","HEAD"],8*1024),!0}catch{return!1}}function _t(t){try{return f(t,["symbolic-ref","--quiet","--short","HEAD"],8*1024).trim()||null}catch{try{return`HEAD (${f(t,["rev-parse","--short","HEAD"],8*1024).trim()})`}catch{return null}}}function Et(t){try{const e=f(t,["rev-list","--left-right","--count","HEAD...@{upstream}"],8192).trim().split(/\s+/).map(Number);return{ahead:Number.isFinite(e[0])?e[0]:0,behind:Number.isFinite(e[1])?e[1]:0}}catch{return{ahead:0,behind:0}}}function B(t,e){const r=c.resolve(t,...e.split("/")),o=c.relative(t,r);if(o.startsWith("..")||c.isAbsolute(o))throw new Error("Diff 路径超出项目范围。");return r}function f(t,e,r){return g("git",["-C",t,...e],{encoding:"utf8",timeout:2e4,maxBuffer:r,stdio:["ignore","pipe","pipe"]})}function W(t,e,r){try{return f(t,e,r)}catch(o){const n=String(o.stdout||"");if(n)return n;if(Number(o.status)===1)return"";throw o}}function U(t){return t.length<=y?t:`${t.slice(0,y)}
|
|
2
2
|
|
|
3
|
-
[Diff 内容过大,已截断]`}function
|
|
4
|
-
`),o=Y(V(r));for(const n of o){const s=String(n.Root||"");if(!s)continue;const i=Number(n.DriveType||0),a=String(n.Name||s.replace(/[\\/]+$/,"")),
|
|
5
|
-
`),n=Y(V(o,{ACA_MACHINE_FS_PATH:r})),s=n.map(i=>{const a=String(i.Attributes||"");return{name:String(i.Name||""),path:String(i.FullName||""),kind:"directory",hidden:/Hidden|System/i.test(a),hasChildren:!0,reparsePoint:/ReparsePoint/i.test(a)}}).filter(i=>i.name&&i.path&&!i.reparsePoint&&(e||!i.hidden)).slice(0,
|
|
3
|
+
[Diff 内容过大,已截断]`}function vt(){const t=process.platform,e=k(t);return{platform:t,roots:e}}function jt(t){if(!t.directoryPath.trim())throw new Error("目录路径不能为空。");return process.platform==="win32"?Ct(t.directoryPath,!!t.includeHidden):$t(t.directoryPath,!!t.includeHidden)}function p(t,e=process.platform){const r=e==="win32"?c.win32:c.posix,o=r.normalize(t),n=r.parse(o).root,s=o.length>n.length?o.replace(/[\\/]+$/,""):o;return e==="win32"?s.toLocaleLowerCase("en-US"):s}function At(){const t=u.realpathSync(T.homedir()),e=[{id:"home",label:"用户目录",path:t,kind:"home",available:!0}];return t!==c.parse(t).root&&e.push({id:"root",label:"文件系统",path:"/",kind:"root",available:!0}),e}function k(t){if(_?.platform===t&&_.expiresAtMs>Date.now())return _.roots;const e=t==="win32"?kt():At();return _={platform:t,roots:e,expiresAtMs:Date.now()+it},e}function kt(){const t=u.realpathSync.native(T.homedir()),e=new Map;e.set(p(t,"win32"),{id:"home",label:"用户目录",path:t,kind:"home",available:!0});try{const r=["$items = Get-CimInstance Win32_LogicalDisk | ForEach-Object {"," [PSCustomObject]@{ Name = $_.DeviceID; Root = ($_.DeviceID + '\\\\'); DriveType = $_.DriveType; VolumeName = $_.VolumeName }","}","@($items) | ConvertTo-Json -Compress"].join(`
|
|
4
|
+
`),o=Y(V(r));for(const n of o){const s=String(n.Root||"");if(!s)continue;const i=Number(n.DriveType||0),a=String(n.Name||s.replace(/[\\/]+$/,"")),l=String(n.VolumeName||"").trim();e.set(p(s,"win32"),{id:`drive-${a.replace(/[^a-z0-9]+/gi,"").toLowerCase()}`,label:l?`${l} (${a})`:Lt(a,i),path:s,kind:It(i),available:!0})}}catch{const r=c.win32.parse(t).root;e.set(p(r,"win32"),{id:"drive-system",label:`系统盘 (${r.replace(/\\$/,"")})`,path:r,kind:"fixed",available:!0})}return[...e.values()]}function $t(t,e){const r=$(t),n=u.readdirSync(r,{withFileTypes:!0}).filter(i=>i.isDirectory()&&!i.isSymbolicLink()&&(e||!i.name.startsWith("."))),s=n.slice(0,b).map(i=>({name:i.name,path:c.join(r,i.name),kind:"directory",hidden:i.name.startsWith("."),hasChildren:!0}));return s.sort((i,a)=>i.name.localeCompare(a.name,void 0,{numeric:!0,sensitivity:"base"})),z(r,s,n.length>b,"linux")}function Ct(t,e){const r=$(t),o=["$items = Get-ChildItem -LiteralPath $env:ACA_MACHINE_FS_PATH -Directory -Force -ErrorAction Stop | ForEach-Object {"," [PSCustomObject]@{ Name = $_.Name; FullName = $_.FullName; Attributes = $_.Attributes.ToString() }","}","@($items) | ConvertTo-Json -Compress"].join(`
|
|
5
|
+
`),n=Y(V(o,{ACA_MACHINE_FS_PATH:r})),s=n.map(i=>{const a=String(i.Attributes||"");return{name:String(i.Name||""),path:String(i.FullName||""),kind:"directory",hidden:/Hidden|System/i.test(a),hasChildren:!0,reparsePoint:/ReparsePoint/i.test(a)}}).filter(i=>i.name&&i.path&&!i.reparsePoint&&(e||!i.hidden)).slice(0,b).sort((i,a)=>i.name.localeCompare(a.name,void 0,{numeric:!0,sensitivity:"base"}));return z(r,s,n.length>b,"win32")}function z(t,e,r,o){const n=o==="win32"?c.win32:c.posix,s=n.parse(t).root,i=p(t,o)===p(s,o)?null:n.dirname(t);return{platform:o,path:t,parentPath:i,breadcrumbs:Rt(t,o),readable:X(t,u.constants.R_OK),writable:X(t,u.constants.W_OK),items:e,truncated:r}}function Rt(t,e){const r=e==="win32"?c.win32:c.posix,o=r.parse(t),n=t.slice(o.root.length).split(/[\\/]+/).filter(Boolean),s=[{label:o.root.replace(/[\\/]+$/,"")||o.root,path:o.root}];let i=o.root;for(const a of n)i=r.join(i,a),s.push({label:a,path:i});return s}async function Dt(t){const e=String(t.workspaceId||"").trim(),r=String(t.name||"").trim(),o=t.projectType==="chat_room"?"chat_room":"project";if(!e||!r)throw new Error("项目名称和工作区不能为空。");const n=$(String(t.path||""));u.accessSync(n,u.constants.R_OK|u.constants.W_OK);const i=(await x("GET",`/api/projects?workspaceId=${encodeURIComponent(e)}`)).items.find(l=>p(l.root_path)===p(n)),a=i||(await x("POST","/api/projects",{projectId:N(),workspaceId:e,name:r,rootPath:n,source:"aica",projectType:o})).item;return q(Nt(a)),{project:a,duplicate:!!i}}function Nt(t){return{projectId:t.project_id,workspaceId:t.workspace_id,name:t.name,rootPath:t.root_path}}function $(t){if(!t.trim()||!c.isAbsolute(t))throw new Error("必须选择绝对目录路径。");if(Tt(t),!u.statSync(t).isDirectory())throw new Error("选择的路径不是目录。");const r=u.realpathSync.native(t);return Ft(r),r}function m(t){const e=C(t),r=u.realpathSync.native(e);if(!u.statSync(r).isDirectory())throw new Error("项目根目录不存在。");return r}function C(t){const e=et(t);if(!e||!c.isAbsolute(e))throw new Error("项目根目录无效。");return c.resolve(e)}function P(t){const e=t.replace(/\\/g,"/").replace(/^\/+|\/+$/g,"");if(!e)return"";if(e.includes("\0")||e.split("/").some(r=>!r||r==="."||r===".."))throw new Error("项目相对路径无效。");return e}function R(t,e,r){const o=e?c.join(t,...e.split("/")):t,n=u.realpathSync.native(o),s=c.relative(t,n);if(s.startsWith("..")||c.isAbsolute(s))throw new Error("目录超出项目范围。");const i=u.statSync(n);if(r&&!i.isDirectory())throw new Error("请求路径不是目录。");return n}function K(t,e){return c.relative(t,e).split(c.sep).join("/")}function Tt(t){const e=process.platform==="win32"?c.win32:c.posix;if(!k(process.platform).some(o=>{const n=String(o.path||"");if(!n)return!1;const s=e.relative(n,t);return s===""||!s.startsWith("..")&&!e.isAbsolute(s)}))throw new Error("目录不在允许浏览的位置内。")}function Ft(t){const e=process.platform==="win32"?c.win32:c.posix;if(!k(process.platform).some(o=>{const n=String(o.path||"");if(!n)return!1;const s=e.relative(n,t);return s===""||!s.startsWith("..")&&!e.isAbsolute(s)}))throw new Error("目录不在允许浏览的位置内。")}function X(t,e){try{return u.accessSync(t,e),!0}catch{return!1}}function V(t,e={}){return g("powershell.exe",["-NoLogo","-NoProfile","-NonInteractive","-Command",t],{encoding:"utf8",windowsHide:!0,timeout:8e3,maxBuffer:4*1024*1024,env:{...process.env,...e}}).replace(/^\uFEFF/,"").trim()}function Y(t){if(!t)return[];const e=JSON.parse(t);return Array.isArray(e)?e.filter(r=>!!(r&&typeof r=="object")):e&&typeof e=="object"?[e]:[]}function It(t){return t===2?"removable":t===3?"fixed":t===4?"network":t===5?"optical":"drive"}function Lt(t,e){return e===2?`移动磁盘 (${t})`:e===4?`网络驱动器 (${t})`:e===5?`光驱 (${t})`:`本地磁盘 (${t})`}export{ct as cleanupWorktree,ft as commitProjectUploadTempFile,at as commitWorktree,p as comparableMachinePath,wt as getProjectGitCommit,pt as getProjectGitDiff,ht as getProjectGitLog,O as getProjectGitStatus,j as getWorktreeStatus,jt as listMachineDirectory,vt as listMachineFilesystemRoots,ut as listProjectDirectory,Yt as startMachineFilesystemWorkerLoop,lt as uploadProjectFile};
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o="0.1.27";export{o as AICA_VERSION};
|
package/dist/worker-cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
2
|
+
import{Command as m}from"commander";import a from"node:path";import{createStartCommand as n}from"./commands/start.js";import{createDaemonCommand as t}from"./commands/daemon.js";import{createWorkerAuthCommand as s}from"./commands/worker-auth.js";import{createWorkerCodexCommand as c}from"./commands/worker-codex.js";import{createWorkerProjectCommand as i}from"./commands/worker-project.js";import{DEFAULT_ACA_SERVER_URL as d}from"./core/aca-config.js";import{AICA_VERSION as p}from"./version.js";process.env.ACA_DEFAULT_SERVER_URL||=d;async function C(r=process.argv){const o=new m;o.name("aica").description("AICA remote Agent worker").version(p).enablePositionalOptions(),o.addCommand(n()),o.addCommand(t()),o.addCommand(s()),o.addCommand(i()),o.addCommand(c()),await o.parseAsync(r)}const A=process.argv[1]||"",e=a.basename(A).replace(/\.(?:js|ts|cjs|mjs)$/i,"");(e==="worker-cli"||e==="aica"||e==="aca")&&C().catch(r=>{const o=r instanceof Error?r.message:String(r);console.error(o),process.exitCode=1});export{C as runWorkerCli};
|