@messenger-agent/claude-agent 0.41.1-alpha.0 → 0.42.0
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/routes/chat.js +7 -7
- package/package.json +2 -2
package/dist/routes/chat.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{randomUUID as
|
|
2
|
-
`).trim();if(!r)throw new he(400,{message:"No supported message content found"});return r}function
|
|
1
|
+
import{randomUUID as Xe}from"node:crypto";import{existsSync as wt,mkdirSync as yt}from"node:fs";import{join as vt}from"node:path";import{Hono as kt}from"hono";import{HTTPException as he}from"hono/http-exception";import{zValidator as gt}from"@hono/zod-validator";import{streamSSE as It}from"hono/streaming";import{loadClaudePlatformInstructions as Ct}from"../platform-instructions.js";import{logger as f}from"@messenger-agent/shared/logger";import{appConfig as m}from"../config.js";import{resolveAgentWorkdir as Tt,resolveAgentWorkspaceRoot as ht}from"@messenger-agent/shared/agent-workspaces";import{isManagedTaskToolName as Ke}from"@messenger-agent/shared/managed-task-tools";import{saveUploadedFilePart as _t}from"@messenger-agent/shared/uploaded-files";import{WakeupIdempotencyStore as bt}from"@messenger-agent/shared/wakeup-idempotency";import{answerPendingApproval as Qe,answerPendingAsk as Je,deletePendingClientTool as ze,findPendingClientToolByConversation as xt,listPendingClientTools as Ge,loadPendingClientTool as St,savePendingAsk as At,savePendingApproval as Mt,updatePendingAskConversation as Et,waitPendingApproval as Pt,waitPendingAsk as Dt}from"../pending-asks.js";import{AgentAnswerToolPartSchema as Nt,ChatBodySchema as Ot}from"../schemas.js";import{createTaskAggregator as Rt}from"../task-aggregator.js";import{createManagedTaskMcpServer as jt,MANAGED_TASK_MCP_SERVER_NAME as Ve}from"../managed-task-mcp.js";import{createScheduleMcpServer as qt,SCHEDULE_MCP_SERVER_NAME as Bt}from"../schedule-mcp.js";import{claudeExecutionPermissionMode as Ut,claudePermissionOptions as Lt}from"../permission.js";import{claudeContextSnapshot as $t,claudeTokenUsage as Ft}from"../token-usage.js";import{SessionManager as Wt}from"../session-manager.js";import{getLlmProxyBinding as Ht,moveLlmProxyBinding as Xt,setLlmProxyBinding as Kt}from"../llm-proxy-bindings.js";const se=new kt;let Qt;function _e(){return Qt??=new bt(vt(m.dataDir,"wakeup-idempotency.sqlite"))}const J=new Wt(()=>m.claude.sessionIdleTimeoutMs??36e5),Jt=Ct(void 0,m.schedule?.enabled===!0),z=new Set;function Ye(){return{...m.claude.apiKey?{ANTHROPIC_API_KEY:m.claude.apiKey,ANTHROPIC_AUTH_TOKEN:""}:null,...m.claude.baseUrl?{ANTHROPIC_BASE_URL:m.claude.baseUrl}:null}}function zt(){const e=Ye();return Object.keys(e).length>0?{env:e}:void 0}function Gt(e){if(Ke(e))return!0;const n=`mcp__${Ve}__`;return e.startsWith(n)&&Ke(e.slice(n.length))}function Vt(e){if(!e||typeof e!="object"||Array.isArray(e))return;const n=e.structuredContent;return n&&typeof n=="object"&&!Array.isArray(n)?n:void 0}function Yt(){const e=new Set(Ge().map(n=>n.conversationId));return{active:z.size,waiting:e.size}}class F extends Error{status;constructor(n,r){super(r),this.status=n,this.name="ChatRequestError"}}const P=new Map;function Ze(e){return e?.trim().toLowerCase()==="plan"?"plan":"default"}function Zt(e){return{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${e}`,param:"conversation_id"}}function C(e){return!!(e&&typeof e=="object")}function en(e){return e.type==="file"&&"mediaType"in e&&typeof e.mediaType=="string"&&"url"in e&&typeof e.url=="string"&&(!("filename"in e)||e.filename===void 0||typeof e.filename=="string")}async function tn(e){if(e.type==="text"&&typeof e.text=="string")return e.text;if(en(e))return _t(e,m.fileUploads.tempDir);if(e.type!=="step-start")throw new he(400,{message:`Unsupported message part type: ${e.type}`})}async function nn(e){const r=(await Promise.all(e.map(tn))).filter(l=>!!(l&&l.trim().length>0)).join(`
|
|
2
|
+
`).trim();if(!r)throw new he(400,{message:"No supported message content found"});return r}function on(e){for(const n of e){const r=Nt.safeParse(n);if(r.success)return r.data}}function be(e){if(typeof e=="string")return e;if(C(e)){if(typeof e.answer=="string")return e.answer;if(typeof e.text=="string")return e.text;if(C(e.answers))return Object.values(e.answers).map(n=>String(n)).join(`
|
|
3
3
|
`);if(Array.isArray(e.answers))return e.answers.map(n=>{if(typeof n=="string")return n;if(C(n)){if(typeof n.answer=="string")return n.answer;if(typeof n.text=="string")return n.text}return JSON.stringify(n)}).join(`
|
|
4
|
-
`)}return JSON.stringify(e)}function
|
|
5
|
-
`)}return be(e)}function
|
|
6
|
-
`)}function fn(e){return!!(e&&e.state==="output-available"&&(e.toolName==="AskUserQuestion"||e.toolName==="ask_user_question"))}function mn(e){return!!(e&&e.state==="approval-responded")}async function wn(e,n){return e?e.state==="approval-responded"?e.approval.reason??"":Ye(e.output):en(n)}function yn(e,n){return It(m,e,n)}function vn(e){return e==="AskUserQuestion"}function kn(e){return e==="ExitPlanMode"}function gn(e){return e==="Agent"||e==="Task"}function In(e){const n="subagent_type"in e?e.subagent_type:void 0,r="parent_tool_use_id"in e?e.parent_tool_use_id:void 0;return!!n||!!r}function Cn(e){return"session_id"in e?e.session_id:void 0}function Tn(e){return e.subtype==="success"&&e.origin?.kind==="task-notification"&&e.num_turns===0&&e.result===""&&e.usage.input_tokens===0&&e.usage.output_tokens===0&&e.usage.cache_creation_input_tokens===0&&e.usage.cache_read_input_tokens===0&&Object.keys(e.modelUsage).length===0}function b(e,n){return e?.get(n)??void 0}function hn(e){const n=b(e,"X-Matrix-Room-Id")?.trim(),r=b(e,"X-Matrix-Thread-ID")?.trim();return n&&r?`${n}/${r}`:void 0}function _n(e){const n=b(e,"X-Matrix-Room-Id")?.trim(),r=b(e,"X-Matrix-Event-ID")?.trim(),l=b(e,"X-Matrix-Thread-ID")?.trim();if(!(!n||!r))return{roomId:n,eventId:r,...l?{threadRootEventId:l}:null}}se.post("/v1/chat/:conversation_id/cancel",e=>{const n=e.req.param("conversation_id");return bn(n)?e.json({success:!0}):e.json({error:Vt(n)},404)});function bn(e){return J.cancel(e)}se.get("/admin/status",e=>{const n=Je().map(l=>({conversationId:l.conversationId,toolCallId:l.toolCallId,toolName:l.toolName,kind:l.kind,approveId:l.kind==="approval"?l.approveId:void 0,since:l.createdAt,agentMode:l.agentMode,model:l.model})),r=Gt();return e.json({activeTaskCount:r.active,waitingClientToolCount:n.length,clientToolWaits:n})});async function tt({body:e,headers:n}){const r=b(n,"X-Matrix-Workdir"),l=b(n,"X-Matrix-Workspace-ID"),p=hn(n),V=_n(n);let A,Y;try{A=yn(l,r),Y=Ct(m,l)}catch(x){throw new F(400,x instanceof Error?x.message:"Invalid workspace")}if(r&&!ft(A))throw new F(400,`Workdir not found: ${A}`);if(mt(A,{recursive:!0}),e.fork&&!("sessionId"in e.fork))throw new F(400,"Claude fork requires sessionId and messageId");const Z=e.fork&&"sessionId"in e.fork?e.fork:void 0,k=tn(e.message.parts),i=k&&(fn(k)||mn(k))?bt(k.toolCallId):e.conversationId?_t(e.conversationId):void 0,g=i?P.get(i.conversationId):void 0,M=await wn(k,e.message.parts),v=k?void 0:i,W=i&&!g?i.conversationId:e.conversationId,le=b(n,"X-Elevo-LLM-Binding")?.trim()||void 0;let D=Z?void 0:J.get(W);const de=D?.llmProxyBinding??Ft(W);if(le&&de&&le!==de)throw new F(409,"LLM proxy binding does not match the existing conversation");const N=de??le;if(D&&N&&!D.llmProxyBinding){if(D.hasActiveOwner())throw new F(409,"Cannot attach an LLM proxy binding while the conversation is active");D.abort(),D=void 0}const Se=e.model||m.claude.defaultModel,Ae=e.effort,ee=i&&!g?i.workdir:A,q=i?.kind==="approval"&&xe(i,k,M).approved?"default":i?.agentMode?Ve(i.agentMode):Ve(b(n,"X-Matrix-Agent-Mode")),U=i&&!g?i.model??Se:Se,H=i&&!g?i.effort??Ae:Ae;return{run:async(x,X)=>{if(i&&g&&!v){const t={};z.add(t);let a=!1,s,o;const w=new Promise(y=>{o=y});let u=0;const _=async()=>{a||(a=!0,u+=1,s&&(clearTimeout(s),s=void 0),await x.writeDone(),o())},j=()=>{const y=++u;a||(s=setTimeout(()=>{s=void 0,!(a||y!==u)&&x.writeData({type:"heartbeat"}).catch(()=>{}).finally(()=>{!a&&y===u&&j()})},6e4))},ie=async y=>{s&&clearTimeout(s),await x.writeData(y),j()},Fe=async y=>{a||await ie(y)};try{if(await g.attach({writeData:Fe,writeDone:_}),j(),i.kind==="ask_user")Ke(i.toolCallId,sn(i.input,k,M));else{const y=xe(i,k,M);y.approved&&await J.get(i.conversationId)?.setPermissionMode(qt()),Xe(i.toolCallId,y)}await Promise.race([g.done,w]),a||await _()}catch(y){f.error("Claude live ask continuation failed:",y),await Fe({type:"error",errorText:y instanceof Error?y.message:String(y)}),await _()}finally{u+=1,s&&clearTimeout(s),z.delete(t)}return}const Me={};z.add(Me);const te=W??We();let c=te,Ee=!1,Pe=!1,ue=!1,B=new Set,L=new Set;const ce=new Map,pe=new Map,fe=new Map;let me=!1,we=!1,ye,ve="stop",ke,ge,$,T;const O=Nt();let ne=!1,nt=0,R=!1;const Ie=new Set;let I=!1,oe,Ce=!1,E,K;const ot=new Promise(t=>{K=t});let De;const at=new Promise(t=>{De=t}),Ne=()=>{T=setTimeout(()=>{f.debug("Claude chat heartbeat",{conversationId:c,paused:R,downstreamClosed:I}),d({type:"heartbeat"}).catch(()=>{})},6e4)},rt=async t=>{T&&clearTimeout(T),await x.writeData(t),I||Ne()};oe={writeData:async t=>{I||await rt(t)},writeDone:async()=>{I||(I=!0,T&&(clearTimeout(T),T=void 0),await x.writeDone())}};const d=async t=>{I||await oe.writeData(t)},Oe=async()=>{I||(await oe.writeDone(),I=!0)},it=async()=>{$||($={activityId:We()},await d({type:"data",kind:"compact",data:{phase:"start",activityId:$.activityId}}))},Te=async(t,a={})=>{if(!$)return;const s=$;$=void 0,await d({type:"data",kind:"compact",data:{phase:"end",activityId:s.activityId,status:t,...a}})},Q=async t=>{if(!(ue||I)){ue=!0;for(const a of B)await d({type:"text-end",id:a});B=new Set;for(const a of L)await d({type:"reasoning-end",id:a});L=new Set,Pe&&(t==="stop"&&ke&&await d({type:"data",kind:"token_usage",data:ke}),await d({type:"finish-step"})),await d({type:"finish",finishReason:t}),await Oe()}},st=async()=>{for(const t of B)await d({type:"text-end",id:t});B=new Set},lt=async()=>{for(const t of L)await d({type:"reasoning-end",id:t});L=new Set},ae=async()=>{await st(),await lt()},S=async()=>{if(!ne)return;ne=!1,O.clearDirty(),await ae();const t=`todo-${te}-${nt++}`;await d({type:"tool-input-available",toolCallId:t,toolName:"TodoWrite",input:O.snapshot()}),await d({type:"tool-output-available",toolCallId:t,output:"Task updated"})},Re=async(t,a,s,o)=>{if(Ie.has(t)){f.warn("Claude client tool pause already registered",{conversationId:c,toolCallId:t,toolName:a});return}f.info("Claude client tool pause requested",{conversationId:c,toolCallId:t,toolName:a,hasApproval:!!o,...G(s)}),Ie.add(t),R=!0,X?.removeEventListener("abort",re),o&&(f.info("Claude emitting tool approval request",{conversationId:c,toolCallId:t,approveId:o.approveId,...G(o.input)}),await d({type:"tool-approval-request",toolCallId:t,approveId:o.approveId,...un(o.input)})),await Oe(),f.info("Claude client tool stream paused",{conversationId:c,toolCallId:t,toolName:a,timeoutMs:m.claude.askTimeoutMs}),E=setTimeout(()=>{f.warn(`Claude ${a} timed out after ${m.claude.askTimeoutMs}ms`,{conversationId:c,toolCallId:t}),P.delete(c),h.abort(),K()},m.claude.askTimeoutMs),E.unref?.(),De()},je=async t=>{B.has(t)||(B.add(t),await d({type:"text-start",id:t}))},qe=async t=>{L.has(t)||(L.add(t),await d({type:"reasoning-start",id:t}))},dt=t=>{if(t===c)return;const a=c;c=t,J.bind(h,t),Wt(a,t,N),At(a,t);const s=P.get(a);s&&(P.delete(a),P.set(t,s))},Ue=async t=>{Ee||(Ee=!0,dt(t),Ce||(Ce=!0,P.set(t,{attach:async a=>{E&&(clearTimeout(E),E=void 0),oe=a,I=!1,R=!1,f.info("Claude live turn sink attached",{conversationId:c})},done:ot,dispose:()=>{E&&(clearTimeout(E),E=void 0)}})),await d({type:"start",messageMetadata:{conversationId:t,threadId:t}}),await d({type:"start-step"}),Pe=!0)},Be=()=>{Ce&&(P.get(c)?.dispose(),P.delete(c));for(const t of Ie)Qe(t,{rejectWait:!0})},ut=async()=>(await Q(ve),Be(),K(),!0);Ne();const ct=async(t,a,s)=>{const{toolUseID:o}=s;if(f.info("Claude canUseTool invoked",{conversationId:c,toolName:t,toolCallId:o,agentMode:q,...G(a)}),vn(t)){xt({conversationId:c,toolCallId:o,workdir:ee,model:U,effort:H,agentMode:q,toolName:"AskUserQuestion",input:a}),await Re(o,"AskUserQuestion",a);const w=await Et(o);return w?"interrupted"in w?{behavior:"deny",message:"Question interrupted by text response",interrupt:!0,toolUseID:o}:{behavior:"allow",updatedInput:{...a,questions:Ze(a),answers:w.answers},toolUseID:o}:{behavior:"deny",message:`${t} requires user input; session will resume after user responds.`,toolUseID:o}}if(kn(t)){const w=St({conversationId:c,toolCallId:o,workdir:ee,model:U,effort:H,agentMode:q,toolName:"ExitPlanMode",input:a});f.info("Claude ExitPlanMode approval persisted",{conversationId:c,toolCallId:o,approveId:w.approveId,...G(a)}),await Re(o,"ExitPlanMode",a,w);const u=await Mt(o);return f.info("Claude ExitPlanMode approval received",{conversationId:c,toolCallId:o,approved:u&&"approved"in u?u.approved:void 0,interrupted:!!(u&&"interrupted"in u)}),u&&"interrupted"in u?{behavior:"deny",message:"Approval interrupted by text response",interrupt:!0,toolUseID:o,decisionClassification:"user_reject"}:u?.approved?{behavior:"allow",updatedInput:{...a,approval:u},toolUseID:o}:{behavior:"deny",message:u?.reason||"Denied by user",toolUseID:o,decisionClassification:"user_reject"}}return gn(t)?{behavior:"allow",updatedInput:{...a,model:U},toolUseID:o}:{behavior:"allow",toolUseID:o}},pt=async t=>{if(In(t))return!1;const a=Cn(t);if(a&&(await Ue(a),O.setSessionId(a)),R)return!1;switch(t.type){case"system":return t.subtype==="status"?(t.status==="compacting"&&await it(),t.compact_result==="success"?await Te("completed"):t.compact_result==="failed"&&await Te("failed",{error:t.compact_error})):t.subtype==="compact_boundary"&&await Te("completed",{trigger:t.compact_metadata.trigger,preTokens:t.compact_metadata.pre_tokens,postTokens:t.compact_metadata.post_tokens,durationMs:t.compact_metadata.duration_ms}),!1;case"stream_event":{const s=t.event;switch(s.type){case"message_start":{ye=s.message.id,me=!1,we=!1;break}case"content_block_start":{const o=`${ye??t.session_id}:${s.index}`;s.content_block.type==="text"?(await S(),ce.set(o,t.uuid),await je(t.uuid)):s.content_block.type==="thinking"&&(await S(),pe.set(o,t.uuid),await qe(t.uuid));break}case"content_block_delta":{const o=s.delta,w=`${ye??t.session_id}:${s.index}`;if(o.type==="text_delta"){await S(),me=!0;const u=ce.get(w);if(!u){f.warn("Ignoring Claude text delta without content_block_start",{conversationId:c,blockKey:w});break}await d({type:"text-delta",id:u,delta:o.text})}else if(o.type==="thinking_delta"){await S(),we=!0;const u=pe.get(w);if(!u){f.warn("Ignoring Claude reasoning delta without content_block_start",{conversationId:c,blockKey:w});break}await d({type:"reasoning-delta",id:u,delta:o.thinking})}break}case"content_block_stop":break}return!1}case"assistant":{ge=Bt(t.message)??ge,t.session_id&&t.uuid&&await d({type:"data",kind:"agent_session",data:{agentType:"claude",sessionId:t.session_id,messageId:t.uuid}});const s=t.message.content;if(typeof s=="string")break;for(const o of s)switch(o.type){case"thinking":{if(we||!o.thinking)break;await S(),await qe(t.uuid),await d({type:"reasoning-delta",id:t.uuid,delta:o.thinking});break}case"text":{if(me||!o.text)break;await S(),await je(t.uuid),await d({type:"text-delta",id:t.uuid,delta:o.text});break}case"tool_use":{if(await ae(),fe.set(o.id,o.name),(o.name==="EnterPlanMode"||o.name==="ExitPlanMode")&&f.info("Claude plan tool_use received",{conversationId:c,toolCallId:o.id,toolName:o.name,...G(o.input)}),O.isTaskTool(o.name)){O.applyToolUse(o.id,o.name,o.input)&&(ne=!0);break}await S(),await ae(),await d({type:"tool-input-available",toolCallId:o.id,toolName:o.name,input:o.input});break}}return await ae(),ce.clear(),pe.clear(),!1}case"user":{const s=t.message.content;if(typeof s=="string")break;const o=s.filter(u=>u.type==="tool_result"),w=o.length===1?zt(t.tool_use_result):void 0;for(const u of o){const _=u.tool_use_id,j=u.content;if(O.isTaskToolCall(_)){O.applyToolResult(_,j)&&(ne=!0);continue}if(await S(),u.is_error===!0)await d({type:"tool-output-error",toolCallId:_,errorText:nn(j)});else{const ie=fe.get(_);await d({type:"tool-output-available",toolCallId:_,output:ie&&Jt(ie)&&w!==void 0?w:j})}fe.delete(_)}return!1}case"result":{if(Tn(t))return!1;ve=t.subtype==="success"||h.wasCancelled?"stop":"error";const s=Lt(t,ge);return ke=ve==="stop"?s:void 0,await S(),t.subtype!=="success"&&!h.wasCancelled&&await d({type:"error",errorText:t.errors?.filter(o=>o.trim()).join(`
|
|
7
|
-
`)||`Claude Code failed: ${t.subtype}`}),
|
|
8
|
-
`)}:null,CLAUDE_AGENT_SDK_CLIENT_APP:"coding-agent/claude-agent"}})});
|
|
4
|
+
`)}return JSON.stringify(e)}function et(e){if(C(e)&&C(e.answers)){const n=Object.entries(e.answers).map(([r,l])=>`- ${r}:${String(l)}`);if(n.length>0)return["User answered previous question:",...n].join(`
|
|
5
|
+
`)}return be(e)}function an(e){return typeof e=="string"?e:JSON.stringify(e)}function rn(e,n){return C(e)&&typeof e.question=="string"?e.question:`Question ${n+1}`}function tt(e){return C(e)&&Array.isArray(e.questions)?e.questions:[]}function sn(e,n){const r=tt(e);return C(n)&&C(n.answers)?Object.fromEntries(Object.entries(n.answers).map(([l,p])=>[l,String(p)])):r.length>0?Object.fromEntries(r.map((l,p)=>[rn(l,p),be(n)])):{answer:be(n)}}function ln(e,n,r){return n&&n.state==="output-available"?n.output:{answers:r}}function dn(e,n,r){return n&&n.state==="output-available"?{answers:sn(e,n.output),output:n.output}:{answers:r}}function un(e,n){return!e||e.state!=="output-available"?`User answered previous question: ${n}`:et(e.output)}function cn(e){if(C(e)){if(typeof e.planContent=="string"&&e.planContent)return e.planContent;if(typeof e.plan=="string")return e.plan}return JSON.stringify(e)}function pn(e){if(!C(e))return{};const n=typeof e.planContent=="string"&&e.planContent?e.planContent:typeof e.plan=="string"&&e.plan?e.plan:void 0,r=typeof e.planFilePath=="string"&&e.planFilePath?e.planFilePath:void 0;return{...n?{plan:n}:null,...r?{planFilePath:r}:null}}function G(e){return C(e)?{inputKeys:Object.keys(e),planContentLength:typeof e.planContent=="string"?e.planContent.length:void 0,hasPlan:typeof e.plan=="string",hasPlanFilePath:typeof e.planFilePath=="string"}:{inputType:typeof e}}function fn(e){return{approved:!1,reason:e.trim()||void 0}}function nt(e,n,r){return n&&n.state==="approval-responded"?n.approval:{id:e.approveId,...fn(r)}}function xe(e,n,r){const l=nt(e,n,r);if(l.id!==e.approveId)throw new he(400,{message:"Tool approval id does not match pending approval"});return{approved:l.approved,reason:l.reason}}function mn(e,n,r){const l=xe(e,n,r),p=[l.approved?`User approved tool ${e.toolName} plan:`:`User rejected tool ${e.toolName} plan:`,cn(e.input)];return l.reason&&p.push(`Reason: ${l.reason}`),p.join(`
|
|
6
|
+
`)}function wn(e){return!!(e&&e.state==="output-available"&&(e.toolName==="AskUserQuestion"||e.toolName==="ask_user_question"))}function yn(e){return!!(e&&e.state==="approval-responded")}async function vn(e,n){return e?e.state==="approval-responded"?e.approval.reason??"":et(e.output):nn(n)}function kn(e,n){return Tt(m,e,n)}function gn(e){return e==="AskUserQuestion"}function In(e){return e==="ExitPlanMode"}function Cn(e){return e==="Agent"||e==="Task"}function Tn(e){const n="subagent_type"in e?e.subagent_type:void 0,r="parent_tool_use_id"in e?e.parent_tool_use_id:void 0;return!!n||!!r}function hn(e){return"session_id"in e?e.session_id:void 0}function _n(e){return e.subtype==="success"&&e.origin?.kind==="task-notification"&&e.num_turns===0&&e.result===""&&e.usage.input_tokens===0&&e.usage.output_tokens===0&&e.usage.cache_creation_input_tokens===0&&e.usage.cache_read_input_tokens===0&&Object.keys(e.modelUsage).length===0}function b(e,n){return e?.get(n)??void 0}function bn(e){const n=b(e,"X-Matrix-Room-Id")?.trim(),r=b(e,"X-Matrix-Thread-ID")?.trim();return n&&r?`${n}/${r}`:void 0}function xn(e){const n=b(e,"X-Matrix-Room-Id")?.trim(),r=b(e,"X-Matrix-Event-ID")?.trim(),l=b(e,"X-Matrix-Thread-ID")?.trim();if(!(!n||!r))return{roomId:n,eventId:r,...l?{threadRootEventId:l}:null}}se.post("/v1/chat/:conversation_id/cancel",e=>{const n=e.req.param("conversation_id");return Sn(n)?e.json({success:!0}):e.json({error:Zt(n)},404)});function Sn(e){return J.cancel(e)}se.get("/admin/status",e=>{const n=Ge().map(l=>({conversationId:l.conversationId,toolCallId:l.toolCallId,toolName:l.toolName,kind:l.kind,approveId:l.kind==="approval"?l.approveId:void 0,since:l.createdAt,agentMode:l.agentMode,model:l.model})),r=Yt();return e.json({activeTaskCount:r.active,waitingClientToolCount:n.length,clientToolWaits:n})});async function ot({body:e,headers:n}){const r=b(n,"X-Matrix-Workdir"),l=b(n,"X-Matrix-Workspace-ID"),p=bn(n),V=xn(n);let A,Y;try{A=kn(l,r),Y=ht(m,l)}catch(x){throw new F(400,x instanceof Error?x.message:"Invalid workspace")}if(r&&!wt(A))throw new F(400,`Workdir not found: ${A}`);if(yt(A,{recursive:!0}),e.fork&&!("sessionId"in e.fork))throw new F(400,"Claude fork requires sessionId and messageId");const Z=e.fork&&"sessionId"in e.fork?e.fork:void 0,k=on(e.message.parts),i=k&&(wn(k)||yn(k))?St(k.toolCallId):e.conversationId?xt(e.conversationId):void 0,g=i?P.get(i.conversationId):void 0,M=await vn(k,e.message.parts),v=k?void 0:i,W=i&&!g?i.conversationId:e.conversationId,le=b(n,"X-Elevo-LLM-Binding")?.trim()||void 0;let D=Z?void 0:J.get(W);const de=D?.llmProxyBinding??Ht(W);if(le&&de&&le!==de)throw new F(409,"LLM proxy binding does not match the existing conversation");const N=de??le;if(D&&N&&!D.llmProxyBinding){if(D.hasActiveOwner())throw new F(409,"Cannot attach an LLM proxy binding while the conversation is active");D.abort(),D=void 0}const Se=e.model||m.claude.defaultModel,Ae=e.effort,ee=i&&!g?i.workdir:A,q=i?.kind==="approval"&&xe(i,k,M).approved?"default":i?.agentMode?Ze(i.agentMode):Ze(b(n,"X-Matrix-Agent-Mode")),B=i&&!g?i.model??Se:Se,H=i&&!g?i.effort??Ae:Ae;return{run:async(x,X)=>{if(i&&g&&!v){const t={};z.add(t);let a=!1,s,o;const w=new Promise(y=>{o=y});let u=0;const _=async()=>{a||(a=!0,u+=1,s&&(clearTimeout(s),s=void 0),await x.writeDone(),o())},j=()=>{const y=++u;a||(s=setTimeout(()=>{s=void 0,!(a||y!==u)&&x.writeData({type:"heartbeat"}).catch(()=>{}).finally(()=>{!a&&y===u&&j()})},6e4))},ie=async y=>{s&&clearTimeout(s),await x.writeData(y),j()},He=async y=>{a||await ie(y)};try{if(await g.attach({writeData:He,writeDone:_}),j(),i.kind==="ask_user")Je(i.toolCallId,dn(i.input,k,M));else{const y=xe(i,k,M);y.approved&&await J.get(i.conversationId)?.setPermissionMode(Ut()),Qe(i.toolCallId,y)}await Promise.race([g.done,w]),a||await _()}catch(y){f.error("Claude live ask continuation failed:",y),await He({type:"error",errorText:y instanceof Error?y.message:String(y)}),await _()}finally{u+=1,s&&clearTimeout(s),z.delete(t)}return}const Me={};z.add(Me);const te=W??Xe();let c=te,Ee=!1,Pe=!1,ue=!1,U=new Set,L=new Set;const ce=new Map,pe=new Map,fe=new Map;let me=!1,we=!1,ye,ve="stop",ke,ge,$,De=new Set,Ne=!1,T;const O=Rt();let ne=!1,at=0,R=!1;const Ie=new Set;let I=!1,oe,Ce=!1,E,K;const rt=new Promise(t=>{K=t});let Oe;const it=new Promise(t=>{Oe=t}),Re=()=>{T=setTimeout(()=>{f.debug("Claude chat heartbeat",{conversationId:c,paused:R,downstreamClosed:I}),d({type:"heartbeat"}).catch(()=>{})},6e4)},st=async t=>{T&&clearTimeout(T),await x.writeData(t),I||Re()};oe={writeData:async t=>{I||await st(t)},writeDone:async()=>{I||(I=!0,T&&(clearTimeout(T),T=void 0),await x.writeDone())}};const d=async t=>{I||await oe.writeData(t)},je=async()=>{I||(await oe.writeDone(),I=!0)},lt=async()=>{$||($={activityId:Xe()},await d({type:"data",kind:"compact",data:{phase:"start",activityId:$.activityId}}))},Te=async(t,a={})=>{if(!$)return;const s=$;$=void 0,await d({type:"data",kind:"compact",data:{phase:"end",activityId:s.activityId,status:t,...a}})},Q=async t=>{if(!(ue||I)){ue=!0;for(const a of U)await d({type:"text-end",id:a});U=new Set;for(const a of L)await d({type:"reasoning-end",id:a});L=new Set,Pe&&(t==="stop"&&ke&&await d({type:"data",kind:"token_usage",data:ke}),await d({type:"finish-step"})),await d({type:"finish",finishReason:t}),await je()}},dt=async()=>{for(const t of U)await d({type:"text-end",id:t});U=new Set},ut=async()=>{for(const t of L)await d({type:"reasoning-end",id:t});L=new Set},ae=async()=>{await dt(),await ut()},S=async()=>{if(!ne)return;ne=!1,O.clearDirty(),await ae();const t=`todo-${te}-${at++}`;await d({type:"tool-input-available",toolCallId:t,toolName:"TodoWrite",input:O.snapshot()}),await d({type:"tool-output-available",toolCallId:t,output:"Task updated"})},qe=async(t,a,s,o)=>{if(Ie.has(t)){f.warn("Claude client tool pause already registered",{conversationId:c,toolCallId:t,toolName:a});return}f.info("Claude client tool pause requested",{conversationId:c,toolCallId:t,toolName:a,hasApproval:!!o,...G(s)}),Ie.add(t),R=!0,X?.removeEventListener("abort",re),o&&(f.info("Claude emitting tool approval request",{conversationId:c,toolCallId:t,approveId:o.approveId,...G(o.input)}),await d({type:"tool-approval-request",toolCallId:t,approveId:o.approveId,...pn(o.input)})),await je(),f.info("Claude client tool stream paused",{conversationId:c,toolCallId:t,toolName:a,timeoutMs:m.claude.askTimeoutMs}),E=setTimeout(()=>{f.warn(`Claude ${a} timed out after ${m.claude.askTimeoutMs}ms`,{conversationId:c,toolCallId:t}),P.delete(c),h.abort(),K()},m.claude.askTimeoutMs),E.unref?.(),Oe()},Be=async t=>{U.has(t)||(U.add(t),await d({type:"text-start",id:t}))},Ue=async t=>{L.has(t)||(L.add(t),await d({type:"reasoning-start",id:t}))},ct=t=>{if(t===c)return;const a=c;c=t,J.bind(h,t),Xt(a,t,N),Et(a,t);const s=P.get(a);s&&(P.delete(a),P.set(t,s))},Le=async t=>{Ee||(Ee=!0,ct(t),Ce||(Ce=!0,P.set(t,{attach:async a=>{E&&(clearTimeout(E),E=void 0),oe=a,I=!1,R=!1,f.info("Claude live turn sink attached",{conversationId:c})},done:rt,dispose:()=>{E&&(clearTimeout(E),E=void 0)}})),await d({type:"start",messageMetadata:{conversationId:t,threadId:t}}),await d({type:"start-step"}),Pe=!0)},$e=()=>{Ce&&(P.get(c)?.dispose(),P.delete(c));for(const t of Ie)ze(t,{rejectWait:!0})},pt=async()=>(await Q(ve),$e(),K(),!0);Re();const ft=async(t,a,s)=>{const{toolUseID:o}=s;if(f.info("Claude canUseTool invoked",{conversationId:c,toolName:t,toolCallId:o,agentMode:q,...G(a)}),gn(t)){At({conversationId:c,toolCallId:o,workdir:ee,model:B,effort:H,agentMode:q,toolName:"AskUserQuestion",input:a}),await qe(o,"AskUserQuestion",a);const w=await Dt(o);return w?"interrupted"in w?{behavior:"deny",message:"Question interrupted by text response",interrupt:!0,toolUseID:o}:{behavior:"allow",updatedInput:{...a,questions:tt(a),answers:w.answers},toolUseID:o}:{behavior:"deny",message:`${t} requires user input; session will resume after user responds.`,toolUseID:o}}if(In(t)){const w=Mt({conversationId:c,toolCallId:o,workdir:ee,model:B,effort:H,agentMode:q,toolName:"ExitPlanMode",input:a});f.info("Claude ExitPlanMode approval persisted",{conversationId:c,toolCallId:o,approveId:w.approveId,...G(a)}),await qe(o,"ExitPlanMode",a,w);const u=await Pt(o);return f.info("Claude ExitPlanMode approval received",{conversationId:c,toolCallId:o,approved:u&&"approved"in u?u.approved:void 0,interrupted:!!(u&&"interrupted"in u)}),u&&"interrupted"in u?{behavior:"deny",message:"Approval interrupted by text response",interrupt:!0,toolUseID:o,decisionClassification:"user_reject"}:u?.approved?{behavior:"allow",updatedInput:{...a,approval:u},toolUseID:o}:{behavior:"deny",message:u?.reason||"Denied by user",toolUseID:o,decisionClassification:"user_reject"}}return Cn(t)?{behavior:"allow",updatedInput:{...a,model:B},toolUseID:o}:{behavior:"allow",toolUseID:o}},mt=async t=>{if(Tn(t))return!1;const a=hn(t);if(a&&(await Le(a),O.setSessionId(a)),t.type==="system"&&t.subtype==="background_tasks_changed")return De=new Set(t.tasks.map(s=>s.task_id)),!1;if(R)return!1;switch(t.type){case"system":return t.subtype==="status"?(t.status==="compacting"&&await lt(),t.compact_result==="success"?await Te("completed"):t.compact_result==="failed"&&await Te("failed",{error:t.compact_error})):t.subtype==="compact_boundary"&&await Te("completed",{trigger:t.compact_metadata.trigger,preTokens:t.compact_metadata.pre_tokens,postTokens:t.compact_metadata.post_tokens,durationMs:t.compact_metadata.duration_ms}),!1;case"stream_event":{const s=t.event;switch(s.type){case"message_start":{ye=s.message.id,me=!1,we=!1;break}case"content_block_start":{const o=`${ye??t.session_id}:${s.index}`;s.content_block.type==="text"?(await S(),ce.set(o,t.uuid),await Be(t.uuid)):s.content_block.type==="thinking"&&(await S(),pe.set(o,t.uuid),await Ue(t.uuid));break}case"content_block_delta":{const o=s.delta,w=`${ye??t.session_id}:${s.index}`;if(o.type==="text_delta"){await S(),me=!0;const u=ce.get(w);if(!u){f.warn("Ignoring Claude text delta without content_block_start",{conversationId:c,blockKey:w});break}await d({type:"text-delta",id:u,delta:o.text})}else if(o.type==="thinking_delta"){await S(),we=!0;const u=pe.get(w);if(!u){f.warn("Ignoring Claude reasoning delta without content_block_start",{conversationId:c,blockKey:w});break}await d({type:"reasoning-delta",id:u,delta:o.thinking})}break}case"content_block_stop":break}return!1}case"assistant":{ge=$t(t.message)??ge,t.session_id&&t.uuid&&await d({type:"data",kind:"agent_session",data:{agentType:"claude",sessionId:t.session_id,messageId:t.uuid}});const s=t.message.content;if(typeof s=="string")break;for(const o of s)switch(o.type){case"thinking":{if(we||!o.thinking)break;await S(),await Ue(t.uuid),await d({type:"reasoning-delta",id:t.uuid,delta:o.thinking});break}case"text":{if(me||!o.text)break;await S(),await Be(t.uuid),await d({type:"text-delta",id:t.uuid,delta:o.text});break}case"tool_use":{if(await ae(),fe.set(o.id,o.name),(o.name==="EnterPlanMode"||o.name==="ExitPlanMode")&&f.info("Claude plan tool_use received",{conversationId:c,toolCallId:o.id,toolName:o.name,...G(o.input)}),O.isTaskTool(o.name)){O.applyToolUse(o.id,o.name,o.input)&&(ne=!0);break}await S(),await ae(),await d({type:"tool-input-available",toolCallId:o.id,toolName:o.name,input:o.input});break}}return await ae(),ce.clear(),pe.clear(),!1}case"user":{const s=t.message.content;if(typeof s=="string")break;const o=s.filter(u=>u.type==="tool_result"),w=o.length===1?Vt(t.tool_use_result):void 0;for(const u of o){const _=u.tool_use_id,j=u.content;if(O.isTaskToolCall(_)){O.applyToolResult(_,j)&&(ne=!0);continue}if(await S(),u.is_error===!0)await d({type:"tool-output-error",toolCallId:_,errorText:an(j)});else{const ie=fe.get(_);await d({type:"tool-output-available",toolCallId:_,output:ie&&Gt(ie)&&w!==void 0?w:j})}fe.delete(_)}return!1}case"result":{if(De.size>0)return t.origin?.kind!=="task-notification"&&(Ne=!0),!1;if(_n(t)&&!Ne)return!1;ve=t.subtype==="success"||h.wasCancelled?"stop":"error";const s=Ft(t,ge);return ke=ve==="stop"?s:void 0,await S(),t.subtype!=="success"&&!h.wasCancelled&&await d({type:"error",errorText:t.errors?.filter(o=>o.trim()).join(`
|
|
7
|
+
`)||`Claude Code failed: ${t.subtype}`}),pt()}}return!1},Fe={threadRef:p,matrixEvent:V,llmProxyBinding:N},h=D??J.create({conversationId:te,workdir:ee,model:B,effort:H,agentMode:q,managedTaskContext:Fe,llmProxyBinding:N,buildOptions:(t,a,s)=>({abortController:t,cwd:ee,includePartialMessages:!0,model:B,effort:H,maxTurns:m.claude.maxTurns,settings:zt(),systemPrompt:{type:"preset",preset:"claude_code",append:Jt},...Lt(q),tools:{type:"preset",preset:"claude_code"},mcpServers:{[Ve]:jt({workspaceRoot:Y,context:s}),...m.schedule?.enabled===!0?{[Bt]:qt({config:m.schedule,context:s})}:null},disallowedTools:["EnterWorktree","ExitWorktree"],...Z?{resume:Z.sessionId,resumeSessionAt:Z.messageId,forkSession:!0}:W?{resume:W}:null,canUseTool:a,env:{...process.env,...Ye(),...N?{ANTHROPIC_CUSTOM_HEADERS:[process.env.ANTHROPIC_CUSTOM_HEADERS,`X-Elevo-LLM-Binding: ${N}`].filter(Boolean).join(`
|
|
8
|
+
`)}:null,CLAUDE_AGENT_SDK_CLIENT_APP:"coding-agent/claude-agent"}})});Kt(te,N);const re=()=>h.abort();let We=!1;try{v&&g&&(f.info("Claude resuming interrupted client tool",{conversationId:c,toolCallId:v.toolCallId,toolName:v.toolName,kind:v.kind}),await g.attach({writeData:async()=>{},writeDone:async()=>{}}),v.kind==="ask_user"?Je(v.toolCallId,{interrupted:!0}):Qe(v.toolCallId,{interrupted:!0}),await g.done),i&&(!g||v)&&(f.info("Claude emitting resumed client tool result",{conversationId:c,toolCallId:i.toolCallId,toolName:i.toolName,kind:i.kind,interrupted:!!v}),i.kind==="ask_user"?await d(v?{type:"tool-output-error",toolCallId:i.toolCallId,toolName:i.toolName,errorText:"Question interrupted by text response"}:{type:"tool-output-available",toolCallId:i.toolCallId,output:ln(i,k,M)}):await d(v?{type:"tool-output-error",toolCallId:i.toolCallId,toolName:i.toolName,errorText:"Approval interrupted by text response"}:{type:"tool-output-available",toolCallId:i.toolCallId,output:nt(i,k,M)}),(!v||!g)&&ze(i.toolCallId));const t=i&&!g&&!v?i.kind==="ask_user"?un(k,M):mn(i,k,M):M,a=await h.reserveTurn(t,mt,ft,{model:B,effort:H,agentMode:q,managedTaskContext:Fe});if(f.info("Claude turn reservation completed",{conversationId:c,kind:a.kind,resumedToolCallId:i?.toolCallId,resumedToolName:i?.toolName}),a.kind==="interjection"){T&&clearTimeout(T),await x.writeDone();return}We=!0,X?.aborted&&re(),X?.addEventListener("abort",re,{once:!0}),await Promise.race([a.done,it]),!R&&!ue&&!I&&await Q(h.wasCancelled||!h.isDead?"stop":"error")}catch(t){I?(f.error("Claude stream failed after downstream closed:",t),K()):(await Le(c),X?.aborted||h.wasCancelled?await Q("stop"):h.isDead?await Q("error"):(f.error("Claude stream failed:",t),await d({type:"error",errorText:t instanceof Error?t.message:String(t)}),await Q("error")))}finally{We&&X?.removeEventListener("abort",re),T&&clearTimeout(T),z.delete(Me),R||$e(),R||K()}}}}async function Yn(e,n){await(await ot(e)).run(n,e.signal)}se.post("/v1/chat/stream",gt("json",Ot),async e=>{const n=b(e.req.raw.headers,"X-Elevo-Wakeup-Key")?.trim()||void 0;let r;if(n){const p=_e().claim(n);if(p.status!=="claimed")return e.header("X-Elevo-Wakeup-Duplicate","true"),e.json({error:"Scheduled wakeup was already accepted"},409);r=p.token}let l;try{l=await ot({body:e.req.valid("json"),headers:e.req.raw.headers})}catch(p){if(n&&r&&_e().release(n,r),p instanceof F)return e.json({error:p.message},p.status);throw p}return It(e,async p=>{let V=!1;const A=()=>{!n||!r||V||(V=!0,_e().markStarted(n,r))};await l.run({writeData:async Y=>{A(),await p.writeSSE({data:JSON.stringify(Y)})},writeDone:async()=>{A(),await p.writeSSE({data:"[DONE]"})}},e.req.raw.signal)})});var Zn=se;export{F as ChatRequestError,Sn as cancelClaudeConversation,Yt as claudeActivitySnapshot,Zn as default,Yn as handleClaudeChatStream,ot as prepareClaudeChatStream};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@messenger-agent/claude-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"ws": "^8.21.0",
|
|
20
20
|
"yaml": "^2.9.0",
|
|
21
21
|
"zod": "^4.4.3",
|
|
22
|
-
"@messenger-agent/shared": "0.
|
|
22
|
+
"@messenger-agent/shared": "0.42.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/ws": "^8.18.1"
|