@messenger-agent/codex-agent 0.27.0 → 0.29.0-alpha.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.
@@ -1 +1 @@
1
- import l from"node:path";import{mkdir as f,writeFile as y}from"node:fs/promises";import{generateImage as T}from"ai";import{createOpenAI as w}from"@ai-sdk/openai";import{Agent as I,fetch as x}from"undici";import{z as r}from"zod";import{executeManagedTaskTool as S,isManagedTaskToolName as b,MANAGED_TASK_TOOL_DEFINITIONS as A}from"@messenger-agent/shared/managed-task-tools";import{AgentFileError as k,resolveAgentFilePath as v}from"@messenger-agent/shared/agent-files";import{executeScheduleTool as E,SCHEDULE_TOOL_DEFINITION as c,ScheduleToolError as _}from"@messenger-agent/shared/schedule-tools";const p=1200*1e3,M=new I({headersTimeout:p,bodyTimeout:p}),R=(t,e)=>x(t,{...e,dispatcher:M}),d=r.object({prompt:r.string().describe("The text prompt describing the image to generate"),store_path:r.string().describe("Absolute file path (including filename) to save the image. If omitted, saves to the current working directory with an auto-generated name.").optional(),size:r.enum(["1024x1024","1536x1024","1024x1536"]).describe("Image size. Leave it undefined to use the auto size.").optional()}),m=r.object({path:r.string().describe("File to send. Absolute paths use the system path; relative paths resolve from the active workspace.")}),N=r.toJSONSchema(d),O=r.toJSONSchema(m);function g(t){return{contentItems:[{type:"inputText",text:JSON.stringify(t)}],success:!0}}const a=new Map;function F(t,e){const o=e.threadRef?.trim();a.set(t,{workspaceRoot:e.workspaceRoot,...o?{threadRef:o}:null,...e.matrixEvent?{matrixEvent:e.matrixEvent}:null,...e.llmProxyBinding?{llmProxyBinding:e.llmProxyBinding}:null})}function u(t,e){const o=a.get(t);if(o)for(const n of e)n&&n!==t&&F(n,o)}function $(t,e){if(t!=="item/started"&&t!=="item/completed"||!e||typeof e!="object")return;const o=e,n=o.item;if(!n||typeof n!="object")return;const i=n;if(i.type==="subAgentActivity"&&i.kind==="started"&&typeof o.threadId=="string"&&typeof i.agentThreadId=="string"){u(o.threadId,[i.agentThreadId]);return}i.type==="collabAgentToolCall"&&i.tool==="spawnAgent"&&typeof i.senderThreadId=="string"&&Array.isArray(i.receiverThreadIds)&&u(i.senderThreadId,i.receiverThreadIds.filter(s=>typeof s=="string"))}async function h(t){const e=a.get(t);if(e)return e.workspaceRoot;const{sessionManager:o}=await import("./codex.js"),n=o.workdirFor(t);if(!n)throw new Error(`No workspace context for thread ${t}; the session may not exist in this process`);return n}async function L(t){const{appConfig:e}=await import("./config.js");if(!e.openai.apiKey)throw new Error("Missing openai.api_key in LLM config");return{apiKey:e.openai.apiKey,baseURL:e.openai.baseUrl,imageModel:e.openai.imageModel,llmProxyBinding:a.get(t)?.llmProxyBinding}}async function C(){const{appConfig:t}=await import("./config.js");return t.schedule}function H(t=!1){const e=[{name:"generate_image",description:"Generate an image from a text prompt. The image is saved to the specified path (absolute path including filename) or the current working directory if not specified. Returns the saved file path.",inputSchema:N},{name:"send_file",description:"Send any readable file to the client. Absolute paths use the system path; relative paths resolve from the active workspace. The agent runtime captures this tool call and performs the actual send.",inputSchema:O},...A.map(o=>({name:o.name,description:o.description,inputSchema:o.inputSchema}))];return t&&e.push({name:c.name,description:c.description,inputSchema:c.inputSchema}),e}async function V(t){try{if(t.tool==="generate_image"){const e=d.parse(t.arguments),o=await L(t.threadId),n=w({apiKey:o.apiKey,baseURL:o.baseURL,fetch:R,...o.llmProxyBinding?{headers:{"X-Elevo-LLM-Binding":o.llmProxyBinding}}:null}),{image:i}=await T({model:n.image(o.imageModel),prompt:e.prompt,size:e.size,maxRetries:0,abortSignal:AbortSignal.timeout(p)}),s=e.store_path?l.resolve(e.store_path):l.join(process.cwd(),`generated_${Date.now()}.png`);return await f(l.dirname(s),{recursive:!0}),await y(s,Buffer.from(i.uint8Array)),{contentItems:[{type:"inputText",text:s}],success:!0}}if(t.tool==="send_file"){const e=m.parse(t.arguments),o=await v(e.path,await h(t.threadId));return{contentItems:[{type:"inputText",text:JSON.stringify({path:o})}],success:!0}}if(b(t.tool)){const e=await S(await h(t.threadId),t.tool,t.arguments,a.get(t.threadId));return g(e)}if(t.tool===c.name)return g(await E(await C(),a.get(t.threadId),t.arguments));throw new Error(`Unknown dynamic tool: ${t.tool}`)}catch(e){const o=e instanceof k&&e.code==="INVALID_PATH"||e instanceof Error?e.message:String(e),n=e instanceof _?{error:{code:e.code,message:e.message,retryable:e.retryable,currentRevision:e.currentRevision}}:o;return{contentItems:[{type:"inputText",text:typeof n=="string"?n:JSON.stringify(n)}],success:!1}}}export{H as dynamicToolSpecs,V as handleDynamicToolCall,u as inheritManagedTaskToolContext,$ as inheritManagedTaskToolContextFromNotification,F as setManagedTaskToolContext};
1
+ import l from"node:path";import{mkdir as f,writeFile as y}from"node:fs/promises";import{generateImage as T}from"ai";import{createOpenAI as w}from"@ai-sdk/openai";import{Agent as I,fetch as x}from"undici";import{z as r}from"zod";import{executeManagedTaskTool as S,isManagedTaskToolName as b,MANAGED_TASK_TOOL_DEFINITIONS as A}from"@messenger-agent/shared/managed-task-tools";import{AgentFileError as k,resolveAgentFilePath as v}from"@messenger-agent/shared/agent-files";import{executeScheduleTool as E,SCHEDULE_TOOL_DEFINITION as c,ScheduleToolError as _}from"@messenger-agent/shared/schedule-tools";const d=1200*1e3,M=new I({headersTimeout:d,bodyTimeout:d}),R=(t,e)=>x(t,{...e,dispatcher:M}),p=r.object({prompt:r.string().describe("The text prompt describing the image to generate"),store_path:r.string().describe("Absolute file path (including filename) to save the image. If omitted, saves to the current working directory with an auto-generated name.").optional(),size:r.enum(["1024x1024","1536x1024","1024x1536"]).describe("Image size. Leave it undefined to use the auto size.").optional()}),m=r.object({path:r.string().describe("File to send. Absolute paths use the system path; relative paths resolve from the active workspace.")}),N=r.toJSONSchema(p),O=r.toJSONSchema(m);function g(t){return{contentItems:[{type:"inputText",text:JSON.stringify(t)}],success:!0}}const a=new Map;function F(t,e){const o=e.threadRef?.trim();a.set(t,{workspaceRoot:e.workspaceRoot,...o?{threadRef:o}:null,...e.matrixEvent?{matrixEvent:e.matrixEvent}:null,...e.llmProxyBinding?{llmProxyBinding:e.llmProxyBinding}:null})}function u(t,e){const o=a.get(t);if(o)for(const n of e)n&&n!==t&&F(n,o)}function $(t,e){if(t!=="item/started"&&t!=="item/completed"||!e||typeof e!="object")return;const o=e,n=o.item;if(!n||typeof n!="object")return;const i=n;if(i.type==="subAgentActivity"&&i.kind==="started"&&typeof o.threadId=="string"&&typeof i.agentThreadId=="string"){u(o.threadId,[i.agentThreadId]);return}i.type==="collabAgentToolCall"&&i.tool==="spawnAgent"&&typeof i.senderThreadId=="string"&&Array.isArray(i.receiverThreadIds)&&u(i.senderThreadId,i.receiverThreadIds.filter(s=>typeof s=="string"))}async function h(t){const e=a.get(t);if(e)return e.workspaceRoot;const{sessionManager:o}=await import("./codex.js"),n=o.workdirFor(t);if(!n)throw new Error(`No workspace context for thread ${t}; the session may not exist in this process`);return n}async function L(t){const{appConfig:e}=await import("./config.js");if(!e.openai.apiKey)throw new Error("Missing openai.api_key in LLM config");return{apiKey:e.openai.apiKey,baseURL:e.openai.baseUrl,imageModel:e.openai.imageModel,llmProxyBinding:a.get(t)?.llmProxyBinding}}async function C(){const{appConfig:t}=await import("./config.js");return t.schedule}function H(t=!1){const e=[{name:"generate_image",description:"Generate an image from a text prompt. The image is saved to the specified path (absolute path including filename) or the current working directory if not specified. Returns the saved file path.",inputSchema:N},{name:"send_file",description:"Send any readable file to the client. Absolute paths use the system path; relative paths resolve from the active workspace. The agent runtime captures this tool call and performs the actual send.",inputSchema:O},...A.map(o=>({name:o.name,description:o.description,inputSchema:o.inputSchema}))];return t&&e.push({name:c.name,description:c.description,inputSchema:c.inputSchema}),e}async function V(t){try{if(t.tool==="generate_image"){const e=p.parse(t.arguments),o=await L(t.threadId),n=w({apiKey:o.apiKey,baseURL:o.baseURL,fetch:R,...o.llmProxyBinding?{headers:{"X-Elevo-LLM-Binding":o.llmProxyBinding}}:null}),{image:i}=await T({model:n.image(o.imageModel),prompt:e.prompt,size:e.size,maxRetries:0,abortSignal:AbortSignal.timeout(d)}),s=e.store_path?l.resolve(e.store_path):l.join(process.cwd(),`generated_${Date.now()}.png`);return await f(l.dirname(s),{recursive:!0}),await y(s,Buffer.from(i.uint8Array)),{contentItems:[{type:"inputText",text:s}],success:!0}}if(t.tool==="send_file"){const e=m.parse(t.arguments),o=await v(e.path,await h(t.threadId));return{contentItems:[{type:"inputText",text:JSON.stringify({path:o})}],success:!0}}if(b(t.tool)){const e=await S(await h(t.threadId),t.tool,t.arguments,a.get(t.threadId));return g(e)}if(t.tool===c.name)return g(await E(await C(),a.get(t.threadId),t.arguments,fetch,t.callId));throw new Error(`Unknown dynamic tool: ${t.tool}`)}catch(e){const o=e instanceof k&&e.code==="INVALID_PATH"||e instanceof Error?e.message:String(e),n=e instanceof _?{error:{code:e.code,message:e.message,retryable:e.retryable,currentRevision:e.currentRevision}}:o;return{contentItems:[{type:"inputText",text:typeof n=="string"?n:JSON.stringify(n)}],success:!1}}}export{H as dynamicToolSpecs,V as handleDynamicToolCall,u as inheritManagedTaskToolContext,$ as inheritManagedTaskToolContextFromNotification,F as setManagedTaskToolContext};
@@ -1,4 +1,4 @@
1
- import{existsSync as c,readFileSync as d}from"node:fs";import{fileURLToPath as o}from"node:url";import{logger as i}from"@messenger-agent/shared/logger";const m=[o(new URL("./assets/codex-home/AGENTS.md",import.meta.url)),o(new URL("../../../assets/codex-home/AGENTS.md",import.meta.url)),"/app/assets/codex-home/AGENTS.md"],s="## Codex Managed Task Tools\n\nWhen using managed task tools in Codex, call these tool names directly: `search_managed_tasks`, `get_managed_task`, `create_managed_task`, `update_managed_task`, `apply_managed_task_patch`, and `delete_managed_task`.\n",l="## Schedule Tool\n\nUse the single `schedule` tool to create, list, inspect, update, pause, resume, or cancel schedules in the current trusted Matrix context. The action is selected inside `input.action`; never ask for or invent a Room ID, actor ID, source event, or service credential. After create or update succeeds, repeat the service's normalized rule, absolute next run time, IANA timezone, message, and mention MXIDs to the user so relative times and recurrence are unambiguous.\n";function h(n,r=!1){const e=r?`
2
- ${l}`:"",t=[n??process.env.CODEX_AGENTS_TEMPLATE_PATH,...m].find(a=>!!(a&&c(a)));return t?`${d(t,"utf-8").trimEnd()}
1
+ import{existsSync as c,readFileSync as i}from"node:fs";import{fileURLToPath as a}from"node:url";import{logger as d}from"@messenger-agent/shared/logger";const l=[a(new URL("./assets/codex-home/AGENTS.md",import.meta.url)),a(new URL("../../../assets/codex-home/AGENTS.md",import.meta.url)),"/app/assets/codex-home/AGENTS.md"],n="## Codex Managed Task Tools\n\nWhen using managed task tools in Codex, call these tool names directly: `search_managed_tasks`, `get_managed_task`, `create_managed_task`, `update_managed_task`, `apply_managed_task_patch`, and `delete_managed_task`.\n",m="## Schedule Tool\n\nUse the single `schedule` tool to prepare create/update proposals or list, inspect, pause, resume, and cancel schedules in the current trusted Matrix context. The action is selected inside `input.action`; never ask for or invent a Room ID, actor ID, source event, or service credential. A successful create/update result is a proposal awaiting one explicit Messenger confirmation, not an active schedule. Summarize it, but do not ask for a chat reply or invoke another confirmation tool.\n";function h(s,r=!1){const e=r?`
2
+ ${m}`:"",t=[s??process.env.CODEX_AGENTS_TEMPLATE_PATH,...l].find(o=>!!(o&&c(o)));return t?`${i(t,"utf-8").trimEnd()}
3
3
 
4
- ${s}${e}`:(i.warn("Codex platform instructions template not found"),`${s}${e}`)}export{h as loadCodexPlatformInstructions};
4
+ ${n}${e}`:(d.warn("Codex platform instructions template not found"),`${n}${e}`)}export{h as loadCodexPlatformInstructions};
@@ -1,2 +1,2 @@
1
- import{Hono as Jt}from"hono";import{HTTPException as q}from"hono/http-exception";import{zValidator as Kt}from"@hono/zod-validator";import{streamSSE as Qt}from"hono/streaming";import{randomUUID as Vt}from"node:crypto";import{sessionManager as j,WorkdirNotFoundError as Gt}from"../codex.js";import{logger as B}from"@messenger-agent/shared/logger";import{ChatBodySchema as Yt,RequestUserInputToolPartSchema as Zt}from"../schemas.js";import{appConfig as xt}from"../config.js";import{setManagedTaskToolContext as te}from"../dynamic-tools.js";import{resolveAgentWorkspaceRoot as ee}from"@messenger-agent/shared/agent-workspaces";import{saveUploadedFilePart as ne}from"@messenger-agent/shared/uploaded-files";import{commandExecutionItem as bt,addTokenUsage as ae,displayCommand as re,dynamicToolCallItem as Tt,dynamicToolCallOutput as oe,fileChangeItem as St,itemText as Et,mcpToolCallItem as kt,mcpToolCallOutput as ie,toUsage as se,turnOptions as de,webSearchItem as Rt,webSearchPayload as _t}from"./shared.js";const z=new Jt,m=new Map,H=new Set,ot=new Map,R=new Map;function Le(){const t=new Set([...R.values()].map(e=>e.conversationId).filter(e=>!!e));return{active:H.size,waiting:t.size}}function ue(t){return{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}}function le(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Thread-ID")?.trim();return e&&r?`${e}/${r}`:void 0}function ce(t){const e=v(t,"X-Matrix-Room-Id")?.trim(),r=v(t,"X-Matrix-Event-ID")?.trim(),o=v(t,"X-Matrix-Thread-ID")?.trim();if(!(!e||!r))return{roomId:e,eventId:r,...o?{threadRootEventId:o}:null}}class D extends Error{status;constructor(e,r){super(r),this.status=e,this.name="ChatRequestError"}}function v(t,e){return t?.get(e)??void 0}async function Ot(t){const e=m.get(t);if(!e)return!1;for(e.cancelRequested=!0,e.turnId||e.owner?.abortController.abort();;){const r=e.transition;if(await r,r===e.transition)break}return e.thread&&e.turnId&&(e.interrupt||(e.interrupt=e.thread.interrupt(e.turnId).finally(()=>{e.interrupt=void 0})),await e.interrupt),e.owner?.abortController.abort(),e.resolveOwnerReady?.(),m.get(t)===e&&m.delete(t),!0}function Mt(t){let e=m.get(t);e||(e={conversationId:t,generation:0,cancelRequested:!1,transition:Promise.resolve()},m.set(t,e));const r=++e.generation,o=new AbortController,i=!e.owner;return i&&(e.owner={generation:r,abortController:o},e.ownerReady=new Promise(d=>{e.resolveOwnerReady=d})),{state:e,generation:r,abortController:o,ownsOutput:i}}function N(t){return m.get(t.state.conversationId)===t.state&&t.state.owner?.generation===t.generation}async function J(t,e){const r=t.transition;let o;const i=new Promise(d=>{o=d});t.transition=r.then(()=>i),await r;try{return await e()}finally{o()}}async function pe(t,e,r,o){!t.ownsOutput&&!t.state.turnId&&await t.state.ownerReady;try{return await J(t.state,async()=>{const{state:i}=t;if(m.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");i.thread=e;const d=i.turnId?await e.steerStreamed(r,i.turnId,{signal:t.abortController.signal}):await e.runStreamed(r,{...o,signal:t.abortController.signal});return i.turnId=d.turnId,d})}finally{t.ownsOutput&&t.state.resolveOwnerReady?.()}}async function fe(t,e,r){return J(t.state,async()=>{const{state:o}=t;if(!N(t)||o.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");o.thread=e;const i=await r();return o.turnId=i.turnId,o.resolveOwnerReady?.(),i})}async function Pt(t,e,r,o){return J(t.state,async()=>{const{state:i}=t;if(m.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");i.thread=e;const d=await e.submitPendingUserInput(r,o);return d&&(i.turnId=d),d})}async function me(t,e,r,o,i){return J(t.state,async()=>{const{state:d}=t;if(m.get(d.conversationId)!==d||d.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");d.thread=e,await e.interruptPendingUserInput(r.turnId,r.toolCallId),d.turnId=void 0;const T=await e.runStreamed(o,{...i,signal:t.abortController.signal});return d.turnId=T.turnId,T})}function qt(t,e,r){t.state.thread=e,t.state.turnId=r}function Dt(t){const{state:e}=t;N(t)&&(e.owner=void 0,e.resolveOwnerReady?.(),e.turnId=void 0,m.get(e.conversationId)===e&&m.delete(e.conversationId))}function Nt(t){N(t)&&(t.state.owner=void 0,t.state.resolveOwnerReady?.(),!t.state.turnId&&m.get(t.state.conversationId)===t.state&&m.delete(t.state.conversationId))}function we(t){N(t)&&(Nt(t),t.state.resolveOwnerReady?.(),!t.state.turnId&&m.get(t.state.conversationId)===t.state&&m.delete(t.state.conversationId))}function K(t,e){return`${t??"unknown"}:${e}`}function ye(t){if(t)return[...R.values()].find(e=>e.conversationId===t)}function Ut(t,e){return t??e}function ge(t){return t.type==="file"&&"mediaType"in t&&typeof t.mediaType=="string"&&"url"in t&&typeof t.url=="string"&&(!("filename"in t)||t.filename===void 0||typeof t.filename=="string")}async function Ie(t){if(t.type==="text"&&typeof t.text=="string")return t.text;if(ge(t))return ne(t,xt.fileUploads.tempDir);throw new q(400,{message:`Unsupported message part type: ${t.type}`})}async function he(t){const r=(await Promise.all(t.map(Ie))).filter(o=>!!(o&&o.trim().length>0)).join(`
2
- `).trim();if(!r)throw new q(400,{message:"No supported message content found"});return r}function Ce(t){if(!t.some(o=>o.type==="tool"))return;if(t.length!==1)throw new q(400,{message:"When a tool part is present, the message must contain exactly one part"});const r=Zt.safeParse(t[0]);if(!r.success)throw new q(400,{message:`Invalid requestUserInput tool part: ${r.error.message}`});return{itemId:r.data.toolCallId,response:r.data.output,sourceTurnId:r.data.sourceTurnId}}function w(t,e,r,o){const i=[e,r,o].filter(Boolean).join(":"),d=t.get(i);if(d)return d;const T=Vt();return t.set(i,T),T}function ve(t,e){return t&&t.length>0?t:e}function xe(t){B.debug("Received chat stream event:",t)}function be(t){if(t instanceof Gt||t&&typeof t=="object"&&"path"in t&&typeof t.path=="string")return t.path;if(t instanceof Error&&t.message.startsWith("Workdir not found: "))return t.message.slice(19)}z.post("/v1/chat/:conversation_id/cancel",async t=>{const e=t.req.param("conversation_id");try{return await Ot(e)?t.json({success:!0}):t.json({error:ue(e)},404)}catch(r){const o=r instanceof Error?r.message:"Failed to interrupt Codex conversation";return B.error(`Failed to cancel Codex conversation ${e}:`,r),t.json({error:{type:"server_error",code:"cancel_failed",message:o}},500)}}),z.get("/admin/status",t=>t.json({activeTaskCount:H.size,waitingClientToolCount:R.size,clientToolWaits:[...R.values()].map(({conversationId:e,toolCallId:r,toolName:o,since:i})=>({conversationId:e,toolCallId:r,toolName:o,since:i}))}));async function Lt({body:t,headers:e}){const{conversationId:r,fork:o,model:i,effort:d,message:T}=t,Q=v(e,"X-Matrix-Bot-Name"),V=v(e,"X-Matrix-Workdir"),U=v(e,"X-Matrix-Workspace-ID"),At=v(e,"X-Matrix-Agent-Mode"),G=v(e,"X-Elevo-LLM-Binding")?.trim()||void 0,Y=o?void 0:j.bindingFor(r);if(G&&Y&&G!==Y)throw new D(409,"LLM proxy binding does not match the existing conversation");const L=Y??G,Wt=le(e),$t=ce(e),Z=Ce(T.parts),tt=Z?void 0:await he(T.parts),it=Z?void 0:ye(r),c=Z,_=tt&&it?it:void 0;let A=!o&&r?Mt(r):void 0;const Ft=(()=>{try{return ee(xt,U)}catch(S){throw new D(400,S instanceof Error?S.message:"Invalid workspace")}})(),{thread:y,isNewSession:Xt}=await(async()=>{try{if(o){if(!("threadId"in o))throw new D(400,"Codex fork requires threadId and turnId");return await j.fork(o,Q,V,U,i,"danger-full-access",L)}return await j.getOrCreate(r,Q,V,U,i,"danger-full-access",L)}catch(S){A&&we(A);const O=be(S);throw O?new D(400,`Workdir not found: ${O}`):S}})();A??=Mt(y.id),te(y.id,{workspaceRoot:Ft,threadRef:Wt,matrixEvent:$t,llmProxyBinding:L});const st=de(At,i,d);return{run:async(S,O)=>{const p=A,W=p.abortController;H.add(W);const et=()=>{N(p)&&Ot(p.state.conversationId).catch(a=>B.error("Failed to cancel disconnected Codex stream:",a))};O?.aborted&&et(),O?.addEventListener("abort",et,{once:!0});let x=!!c,dt=!!c,ut=!1,l,I=y.id,h,$=!1,P;const lt=new Set,ct=new Set,F=new Set,f=new Map,pt=new Set,jt=new Set,ft=new Set;let mt,nt,wt;const yt=()=>{P=setTimeout(()=>{s({type:"heartbeat"}).catch(()=>{})},6e4)},gt=async a=>{P&&clearTimeout(P),a.data==="[DONE]"?await S.writeDone():await S.writeData(JSON.parse(String(a.data))),yt()},s=async a=>{await gt({data:JSON.stringify(a)})},X=async()=>{await gt({data:"[DONE]"})},Bt=async()=>{const a=Ut(l,y.id),n=ot.get(a);n&&(ot.delete(a),await s({type:"data",kind:"diff",data:{diff:n}}))},It=async(a,n)=>{if(a.type!=="contextCompaction")return!1;if(n==="end"&&!pt.has(a.id))return!0;const u=n==="start"?pt:jt;return u.has(a.id)||(u.add(a.id),await s({type:"data",kind:"compact",data:n==="start"?{phase:n,activityId:a.id}:{phase:n,activityId:a.id,status:"completed"}})),!0},at=async a=>{if(a.type!=="subAgentActivity")return!1;if(ft.has(a.id))return!0;ft.add(a.id);const n=w(f,l,a.id),u={activity:a.kind,agentThreadId:a.agentThreadId,...a.agentPath?{agentPath:a.agentPath}:null};return await s({type:"tool-input-available",toolCallId:n,toolName:`subagent_${a.kind}`,...a.agentPath?{title:a.agentPath}:null,input:u}),await s(a.kind==="interrupted"?{type:"tool-output-error",toolCallId:n,errorText:"Subagent interrupted"}:{type:"tool-output-available",toolCallId:n,output:u}),!0};yt();const ht=a=>{l!==a&&(l=a,Xt&&j.persist(a,Q,V,U,L))},E=async(a,n)=>{x||(x=!0,ht(a),await s({type:"start",messageMetadata:{conversationId:a,threadId:a,...n?{turnId:n}:null}}),await s({type:"start-step"}),dt=!0)},rt=async a=>{ut||(ut=!0,dt&&(await Bt(),a==="stop"&&nt&&await s({type:"data",kind:"token_usage",data:nt}),await s({type:"finish-step"})),await s({type:"finish",finishReason:a}),await X())},Ct=async a=>{if(lt.has(a.id))return;lt.add(a.id);const n=a.type==="plan"?{type:"plan"}:void 0;await s({type:"text-start",id:a.id,...n?{textMetadata:n}:{}})},vt=async a=>{ct.has(a.id)||(ct.add(a.id),await s({type:"reasoning-start",id:a.id}))},zt=async a=>{const n=bt(a);if(n)return await s({type:"tool-input-available",toolCallId:w(f,l,a.id),toolName:"bash",input:re(n.command)}),!0;const u=St(a);if(u){for(const[b,C]of u.changes.entries())await s({type:"tool-input-available",toolCallId:w(f,l,a.id,String(b)),toolName:C.kind.type==="update"?"Edit":C.kind.type==="add"?"Add":"Delete",title:C.path,input:C});return!0}const g=kt(a);if(g)return await s({type:"tool-input-available",toolCallId:w(f,l,a.id),toolName:g.tool,input:g.arguments}),!0;const k=Tt(a);if(k)return await s({type:"tool-input-available",toolCallId:w(f,l,a.id),toolName:k.tool,input:k.arguments}),!0;const M=Rt(a);return M?(await s({type:"tool-input-available",toolCallId:w(f,l,a.id),toolName:"web_search",title:M.query,input:_t(M)}),!0):!1},Ht=async a=>{const n=bt(a);if(n){const b=n.status==="failed"||n.status==="declined";return await s(b?{type:"tool-output-error",toolCallId:w(f,l,a.id),errorText:ve(n.aggregatedOutput,"Command execution failed")}:{type:"tool-output-available",toolCallId:w(f,l,a.id),output:n.aggregatedOutput??""}),!0}const u=St(a);if(u){const b=u.status==="failed"||u.status==="declined";for(const[C]of u.changes.entries())await s(b?{type:"tool-output-error",toolCallId:w(f,l,a.id,String(C)),errorText:u.status}:{type:"tool-output-available",toolCallId:w(f,l,a.id,String(C)),output:u.status,preliminary:!1});return!0}const g=kt(a);if(g){const b=g.status==="failed";return await s(b?{type:"tool-output-error",toolCallId:w(f,l,a.id),errorText:g.error?.message??"MCP tool call failed"}:{type:"tool-output-available",toolCallId:w(f,l,a.id),output:ie(g)}),!0}const k=Tt(a);if(k){const b=k.status==="failed"||k.success===!1,C=oe(k);return await s(b?{type:"tool-output-error",toolCallId:w(f,l,a.id),errorText:C||"Dynamic tool call failed"}:{type:"tool-output-available",toolCallId:w(f,l,a.id),output:C}),!0}const M=Rt(a);return M?(await s({type:"tool-output-available",toolCallId:w(f,l,a.id),output:_t(M)}),!0):!1};try{if(c&&!p.ownsOutput){await Pt(p,y,c.itemId,c.response),R.delete(K(r,c.itemId)),await X();return}const a=_?await me(p,y,_,tt,st):c?await fe(p,y,()=>y.continueWithUserInput(c.itemId,c.response,{signal:W.signal,sourceTurnId:c.sourceTurnId})):await pe(p,y,tt,st);if(c?R.delete(K(r,c.itemId)):_&&R.delete(K(r,_.toolCallId)),qt(p,y,a.turnId),!p.ownsOutput){await X();return}c?(await s({type:"data",kind:"agent_session",data:{agentType:"codex",threadId:y.id,turnId:a.turnId}}),await s({type:"tool-output-available",toolCallId:c.itemId,toolName:"request_user_input",output:c.response})):_&&await s({type:"tool-output-error",toolCallId:_.toolCallId,toolName:"request_user_input",errorText:"Question interrupted by text response"});for await(const n of a.events)if(n.type!=="item.delta"&&xe(n),n.type==="thread.started")I=n.thread_id,ht(I);else if(n.type==="turn.started")h=n.turn_id,qt(p,y,h),await E(I,h);else if(n.type==="item.started"){if(x||await E(I,h),await It(n.item,"start")||await at(n.item)||await zt(n.item))continue;n.item.type==="agentMessage"||n.item.type==="plan"?await Ct(n.item):n.item.type==="reasoning"&&await vt(n.item)}else if(n.type==="item.delta")x||await E(I,h),n.item_type==="agentMessage"||n.item_type==="plan"?(F.add(n.item_id),await s({type:"text-delta",id:n.item_id,delta:n.delta})):n.item_type==="reasoning"&&(F.add(n.item_id),await s({type:"reasoning-delta",id:n.item_id,delta:n.delta}));else if(n.type==="item.updated"){if(await at(n.item))continue}else if(n.type==="item.completed"){if(x||await E(I,h),await It(n.item,"end")||await at(n.item)||await Ht(n.item))continue;if(n.item.type==="agentMessage"||n.item.type==="plan"){await Ct(n.item);const u=Et(n.item)??"";u&&!F.has(n.item.id)&&await s({type:"text-delta",id:n.item.id,delta:u}),await s({type:"text-end",id:n.item.id})}else if(n.item.type==="reasoning"){await vt(n.item);const u=Et(n.item)??"";u&&!F.has(n.item.id)&&await s({type:"reasoning-delta",id:n.item.id,delta:u}),await s({type:"reasoning-end",id:n.item.id})}}else if(n.type==="item.tool.requestUserInput"){if(x||await E(I,h),n.params.isBlocking===!1){if(!await Pt(p,y,n.params.itemId,{answers:{}}))throw new Error(`Non-blocking user input request is no longer pending: ${n.params.itemId}`);B.info(`Auto-resolved non-blocking request_user_input ${n.params.itemId} for turn ${n.params.turnId}`);continue}R.set(K(l,n.params.itemId),{conversationId:l,toolCallId:n.params.itemId,toolName:"request_user_input",turnId:n.params.turnId,since:new Date().toISOString()}),await s({type:"tool-input-available",toolCallId:n.params.itemId,toolName:"request_user_input",input:n.params}),await X();return}else if(n.type==="turn.diff.updated")x||await E(I,h),ot.set(Ut(l,y.id),n.diff);else if(n.type==="thread.tokenUsage.updated"){const u=se(n.usage);if(u){const g=ae(wt,u.last);g&&(wt=g,nt={...u,last:g})}}else if(n.type==="turn.failed"){mt!==n.error.message&&await s({type:"error",errorText:n.error.message}),await rt("error"),$=!0,Dt(p);return}else n.type==="error"&&(mt=n.message,await s({type:"error",errorText:n.message}));$=!W.signal.aborted,$&&Dt(p),x||await E(I,h),await rt("stop")}catch(a){const n=a instanceof Error?a.message:"Chat stream failed";x||await E(I,h),await s({type:"error",errorText:n}),await rt("error")}finally{P&&clearTimeout(P),O?.removeEventListener("abort",et),H.delete(W),$||Nt(p)}}}}async function Ae({body:t,headers:e,signal:r},o){await(await Lt({body:t,headers:e})).run(o,r)}z.post("/v1/chat/stream",Kt("json",Yt),async t=>{const e=t.req.valid("json"),r=await(async()=>{try{return await Lt({body:e,headers:t.req.raw.headers})}catch(o){throw o instanceof D?new q(o.status,{message:o.message}):o}})();return Qt(t,async o=>{const i=async d=>{await o.writeSSE(d)};await r.run({writeData:async d=>{await i({data:JSON.stringify(d)})},writeDone:async()=>{await i({data:"[DONE]"})}},t.req.raw.signal)})});var We=z;export{D as ChatRequestError,Ot as cancelCodexConversation,Le as codexActivitySnapshot,We as default,Ae as handleCodexChatStream,Lt as prepareCodexChatStream};
1
+ import{Hono as Jt}from"hono";import{HTTPException as U}from"hono/http-exception";import{zValidator as Qt}from"@hono/zod-validator";import{streamSSE as Vt}from"hono/streaming";import{randomUUID as Gt}from"node:crypto";import{join as Yt}from"node:path";import{sessionManager as z,WorkdirNotFoundError as Zt}from"../codex.js";import{logger as H}from"@messenger-agent/shared/logger";import{ChatBodySchema as te,RequestUserInputToolPartSchema as ee}from"../schemas.js";import{appConfig as ot}from"../config.js";import{setManagedTaskToolContext as ae}from"../dynamic-tools.js";import{resolveAgentWorkspaceRoot as ne}from"@messenger-agent/shared/agent-workspaces";import{saveUploadedFilePart as re}from"@messenger-agent/shared/uploaded-files";import{WakeupIdempotencyStore as oe}from"@messenger-agent/shared/wakeup-idempotency";import{commandExecutionItem as Tt,addTokenUsage as ie,displayCommand as se,dynamicToolCallItem as St,dynamicToolCallOutput as de,fileChangeItem as kt,itemText as Et,mcpToolCallItem as Rt,mcpToolCallOutput as ue,toUsage as le,turnOptions as ce,webSearchItem as _t,webSearchPayload as Ot}from"./shared.js";const K=new Jt;let pe;function it(){return pe??=new oe(Yt(ot.dataDir,"wakeup-idempotency.sqlite"))}const m=new Map,J=new Set,st=new Map,_=new Map;function je(){const t=new Set([..._.values()].map(e=>e.conversationId).filter(e=>!!e));return{active:J.size,waiting:t.size}}function fe(t){return{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}}function me(t){const e=h(t,"X-Matrix-Room-Id")?.trim(),r=h(t,"X-Matrix-Thread-ID")?.trim();return e&&r?`${e}/${r}`:void 0}function we(t){const e=h(t,"X-Matrix-Room-Id")?.trim(),r=h(t,"X-Matrix-Event-ID")?.trim(),o=h(t,"X-Matrix-Thread-ID")?.trim();if(!(!e||!r))return{roomId:e,eventId:r,...o?{threadRootEventId:o}:null}}class W extends Error{status;constructor(e,r){super(r),this.status=e,this.name="ChatRequestError"}}function h(t,e){return t?.get(e)??void 0}async function Mt(t){const e=m.get(t);if(!e)return!1;for(e.cancelRequested=!0,e.turnId||e.owner?.abortController.abort();;){const r=e.transition;if(await r,r===e.transition)break}return e.thread&&e.turnId&&(e.interrupt||(e.interrupt=e.thread.interrupt(e.turnId).finally(()=>{e.interrupt=void 0})),await e.interrupt),e.owner?.abortController.abort(),e.resolveOwnerReady?.(),m.get(t)===e&&m.delete(t),!0}function Pt(t){let e=m.get(t);e||(e={conversationId:t,generation:0,cancelRequested:!1,transition:Promise.resolve()},m.set(t,e));const r=++e.generation,o=new AbortController,i=!e.owner;return i&&(e.owner={generation:r,abortController:o},e.ownerReady=new Promise(d=>{e.resolveOwnerReady=d})),{state:e,generation:r,abortController:o,ownsOutput:i}}function L(t){return m.get(t.state.conversationId)===t.state&&t.state.owner?.generation===t.generation}async function Q(t,e){const r=t.transition;let o;const i=new Promise(d=>{o=d});t.transition=r.then(()=>i),await r;try{return await e()}finally{o()}}async function ye(t,e,r,o){!t.ownsOutput&&!t.state.turnId&&await t.state.ownerReady;try{return await Q(t.state,async()=>{const{state:i}=t;if(m.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");i.thread=e;const d=i.turnId?await e.steerStreamed(r,i.turnId,{signal:t.abortController.signal}):await e.runStreamed(r,{...o,signal:t.abortController.signal});return i.turnId=d.turnId,d})}finally{t.ownsOutput&&t.state.resolveOwnerReady?.()}}async function ge(t,e,r){return Q(t.state,async()=>{const{state:o}=t;if(!L(t)||o.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");o.thread=e;const i=await r();return o.turnId=i.turnId,o.resolveOwnerReady?.(),i})}async function qt(t,e,r,o){return Q(t.state,async()=>{const{state:i}=t;if(m.get(i.conversationId)!==i||i.cancelRequested)throw new DOMException("Chat stream was cancelled before user input was submitted","AbortError");i.thread=e;const d=await e.submitPendingUserInput(r,o);return d&&(i.turnId=d),d})}async function Ie(t,e,r,o,i){return Q(t.state,async()=>{const{state:d}=t;if(m.get(d.conversationId)!==d||d.cancelRequested)throw new DOMException("Chat stream was cancelled before the turn started","AbortError");d.thread=e,await e.interruptPendingUserInput(r.turnId,r.toolCallId),d.turnId=void 0;const I=await e.runStreamed(o,{...i,signal:t.abortController.signal});return d.turnId=I.turnId,I})}function Dt(t,e,r){t.state.thread=e,t.state.turnId=r}function Nt(t){const{state:e}=t;L(t)&&(e.owner=void 0,e.resolveOwnerReady?.(),e.turnId=void 0,m.get(e.conversationId)===e&&m.delete(e.conversationId))}function Ut(t){L(t)&&(t.state.owner=void 0,t.state.resolveOwnerReady?.(),!t.state.turnId&&m.get(t.state.conversationId)===t.state&&m.delete(t.state.conversationId))}function he(t){L(t)&&(Ut(t),t.state.resolveOwnerReady?.(),!t.state.turnId&&m.get(t.state.conversationId)===t.state&&m.delete(t.state.conversationId))}function V(t,e){return`${t??"unknown"}:${e}`}function Ce(t){if(t)return[..._.values()].find(e=>e.conversationId===t)}function Wt(t,e){return t??e}function ve(t){return t.type==="file"&&"mediaType"in t&&typeof t.mediaType=="string"&&"url"in t&&typeof t.url=="string"&&(!("filename"in t)||t.filename===void 0||typeof t.filename=="string")}async function xe(t){if(t.type==="text"&&typeof t.text=="string")return t.text;if(ve(t))return re(t,ot.fileUploads.tempDir);throw new U(400,{message:`Unsupported message part type: ${t.type}`})}async function be(t){const r=(await Promise.all(t.map(xe))).filter(o=>!!(o&&o.trim().length>0)).join(`
2
+ `).trim();if(!r)throw new U(400,{message:"No supported message content found"});return r}function Te(t){if(!t.some(o=>o.type==="tool"))return;if(t.length!==1)throw new U(400,{message:"When a tool part is present, the message must contain exactly one part"});const r=ee.safeParse(t[0]);if(!r.success)throw new U(400,{message:`Invalid requestUserInput tool part: ${r.error.message}`});return{itemId:r.data.toolCallId,response:r.data.output,sourceTurnId:r.data.sourceTurnId}}function w(t,e,r,o){const i=[e,r,o].filter(Boolean).join(":"),d=t.get(i);if(d)return d;const I=Gt();return t.set(i,I),I}function Se(t,e){return t&&t.length>0?t:e}function ke(t){H.debug("Received chat stream event:",t)}function Ee(t){if(t instanceof Zt||t&&typeof t=="object"&&"path"in t&&typeof t.path=="string")return t.path;if(t instanceof Error&&t.message.startsWith("Workdir not found: "))return t.message.slice(19)}K.post("/v1/chat/:conversation_id/cancel",async t=>{const e=t.req.param("conversation_id");try{return await Mt(e)?t.json({success:!0}):t.json({error:fe(e)},404)}catch(r){const o=r instanceof Error?r.message:"Failed to interrupt Codex conversation";return H.error(`Failed to cancel Codex conversation ${e}:`,r),t.json({error:{type:"server_error",code:"cancel_failed",message:o}},500)}}),K.get("/admin/status",t=>t.json({activeTaskCount:J.size,waitingClientToolCount:_.size,clientToolWaits:[..._.values()].map(({conversationId:e,toolCallId:r,toolName:o,since:i})=>({conversationId:e,toolCallId:r,toolName:o,since:i}))}));async function Lt({body:t,headers:e}){const{conversationId:r,fork:o,model:i,effort:d,message:I}=t,O=h(e,"X-Matrix-Bot-Name"),M=h(e,"X-Matrix-Workdir"),S=h(e,"X-Matrix-Workspace-ID"),At=h(e,"X-Matrix-Agent-Mode"),G=h(e,"X-Elevo-LLM-Binding")?.trim()||void 0,Y=o?void 0:z.bindingFor(r);if(G&&Y&&G!==Y)throw new W(409,"LLM proxy binding does not match the existing conversation");const A=Y??G,$t=me(e),Ft=we(e),Z=Te(I.parts),tt=Z?void 0:await be(I.parts),dt=Z?void 0:Ce(r),c=Z,P=tt&&dt?dt:void 0;let $=!o&&r?Pt(r):void 0;const Xt=(()=>{try{return ne(ot,S)}catch(k){throw new W(400,k instanceof Error?k.message:"Invalid workspace")}})(),{thread:y,isNewSession:jt}=await(async()=>{try{if(o){if(!("threadId"in o))throw new W(400,"Codex fork requires threadId and turnId");return await z.fork(o,O,M,S,i,"danger-full-access",A)}return await z.getOrCreate(r,O,M,S,i,"danger-full-access",A)}catch(k){$&&he($);const q=Ee(k);throw q?new W(400,`Workdir not found: ${q}`):k}})();$??=Pt(y.id),ae(y.id,{workspaceRoot:Xt,threadRef:$t,matrixEvent:Ft,llmProxyBinding:A});const ut=ce(At,i,d);return{run:async(k,q)=>{const p=$,F=p.abortController;J.add(F);const et=()=>{L(p)&&Mt(p.state.conversationId).catch(n=>H.error("Failed to cancel disconnected Codex stream:",n))};q?.aborted&&et(),q?.addEventListener("abort",et,{once:!0});let b=!!c,lt=!!c,ct=!1,l,C=y.id,v,X=!1,N;const pt=new Set,ft=new Set,j=new Set,f=new Map,mt=new Set,Bt=new Set,wt=new Set;let yt,at,gt;const It=()=>{N=setTimeout(()=>{s({type:"heartbeat"}).catch(()=>{})},6e4)},ht=async n=>{N&&clearTimeout(N),n.data==="[DONE]"?await k.writeDone():await k.writeData(JSON.parse(String(n.data))),It()},s=async n=>{await ht({data:JSON.stringify(n)})},B=async()=>{await ht({data:"[DONE]"})},zt=async()=>{const n=Wt(l,y.id),a=st.get(n);a&&(st.delete(n),await s({type:"data",kind:"diff",data:{diff:a}}))},Ct=async(n,a)=>{if(n.type!=="contextCompaction")return!1;if(a==="end"&&!mt.has(n.id))return!0;const u=a==="start"?mt:Bt;return u.has(n.id)||(u.add(n.id),await s({type:"data",kind:"compact",data:a==="start"?{phase:a,activityId:n.id}:{phase:a,activityId:n.id,status:"completed"}})),!0},nt=async n=>{if(n.type!=="subAgentActivity")return!1;if(wt.has(n.id))return!0;wt.add(n.id);const a=w(f,l,n.id),u={activity:n.kind,agentThreadId:n.agentThreadId,...n.agentPath?{agentPath:n.agentPath}:null};return await s({type:"tool-input-available",toolCallId:a,toolName:`subagent_${n.kind}`,...n.agentPath?{title:n.agentPath}:null,input:u}),await s(n.kind==="interrupted"?{type:"tool-output-error",toolCallId:a,errorText:"Subagent interrupted"}:{type:"tool-output-available",toolCallId:a,output:u}),!0};It();const vt=n=>{l!==n&&(l=n,jt&&z.persist(n,O,M,S,A))},E=async(n,a)=>{b||(b=!0,vt(n),await s({type:"start",messageMetadata:{conversationId:n,threadId:n,...a?{turnId:a}:null}}),await s({type:"start-step"}),lt=!0)},rt=async n=>{ct||(ct=!0,lt&&(await zt(),n==="stop"&&at&&await s({type:"data",kind:"token_usage",data:at}),await s({type:"finish-step"})),await s({type:"finish",finishReason:n}),await B())},xt=async n=>{if(pt.has(n.id))return;pt.add(n.id);const a=n.type==="plan"?{type:"plan"}:void 0;await s({type:"text-start",id:n.id,...a?{textMetadata:a}:{}})},bt=async n=>{ft.has(n.id)||(ft.add(n.id),await s({type:"reasoning-start",id:n.id}))},Ht=async n=>{const a=Tt(n);if(a)return await s({type:"tool-input-available",toolCallId:w(f,l,n.id),toolName:"bash",input:se(a.command)}),!0;const u=kt(n);if(u){for(const[T,x]of u.changes.entries())await s({type:"tool-input-available",toolCallId:w(f,l,n.id,String(T)),toolName:x.kind.type==="update"?"Edit":x.kind.type==="add"?"Add":"Delete",title:x.path,input:x});return!0}const g=Rt(n);if(g)return await s({type:"tool-input-available",toolCallId:w(f,l,n.id),toolName:g.tool,input:g.arguments}),!0;const R=St(n);if(R)return await s({type:"tool-input-available",toolCallId:w(f,l,n.id),toolName:R.tool,input:R.arguments}),!0;const D=_t(n);return D?(await s({type:"tool-input-available",toolCallId:w(f,l,n.id),toolName:"web_search",title:D.query,input:Ot(D)}),!0):!1},Kt=async n=>{const a=Tt(n);if(a){const T=a.status==="failed"||a.status==="declined";return await s(T?{type:"tool-output-error",toolCallId:w(f,l,n.id),errorText:Se(a.aggregatedOutput,"Command execution failed")}:{type:"tool-output-available",toolCallId:w(f,l,n.id),output:a.aggregatedOutput??""}),!0}const u=kt(n);if(u){const T=u.status==="failed"||u.status==="declined";for(const[x]of u.changes.entries())await s(T?{type:"tool-output-error",toolCallId:w(f,l,n.id,String(x)),errorText:u.status}:{type:"tool-output-available",toolCallId:w(f,l,n.id,String(x)),output:u.status,preliminary:!1});return!0}const g=Rt(n);if(g){const T=g.status==="failed";return await s(T?{type:"tool-output-error",toolCallId:w(f,l,n.id),errorText:g.error?.message??"MCP tool call failed"}:{type:"tool-output-available",toolCallId:w(f,l,n.id),output:ue(g)}),!0}const R=St(n);if(R){const T=R.status==="failed"||R.success===!1,x=de(R);return await s(T?{type:"tool-output-error",toolCallId:w(f,l,n.id),errorText:x||"Dynamic tool call failed"}:{type:"tool-output-available",toolCallId:w(f,l,n.id),output:x}),!0}const D=_t(n);return D?(await s({type:"tool-output-available",toolCallId:w(f,l,n.id),output:Ot(D)}),!0):!1};try{if(c&&!p.ownsOutput){await qt(p,y,c.itemId,c.response),_.delete(V(r,c.itemId)),await B();return}const n=P?await Ie(p,y,P,tt,ut):c?await ge(p,y,()=>y.continueWithUserInput(c.itemId,c.response,{signal:F.signal,sourceTurnId:c.sourceTurnId})):await ye(p,y,tt,ut);if(c?_.delete(V(r,c.itemId)):P&&_.delete(V(r,P.toolCallId)),Dt(p,y,n.turnId),!p.ownsOutput){await B();return}c?(await s({type:"data",kind:"agent_session",data:{agentType:"codex",threadId:y.id,turnId:n.turnId}}),await s({type:"tool-output-available",toolCallId:c.itemId,toolName:"request_user_input",output:c.response})):P&&await s({type:"tool-output-error",toolCallId:P.toolCallId,toolName:"request_user_input",errorText:"Question interrupted by text response"});for await(const a of n.events)if(a.type!=="item.delta"&&ke(a),a.type==="thread.started")C=a.thread_id,vt(C);else if(a.type==="turn.started")v=a.turn_id,Dt(p,y,v),await E(C,v);else if(a.type==="item.started"){if(b||await E(C,v),await Ct(a.item,"start")||await nt(a.item)||await Ht(a.item))continue;a.item.type==="agentMessage"||a.item.type==="plan"?await xt(a.item):a.item.type==="reasoning"&&await bt(a.item)}else if(a.type==="item.delta")b||await E(C,v),a.item_type==="agentMessage"||a.item_type==="plan"?(j.add(a.item_id),await s({type:"text-delta",id:a.item_id,delta:a.delta})):a.item_type==="reasoning"&&(j.add(a.item_id),await s({type:"reasoning-delta",id:a.item_id,delta:a.delta}));else if(a.type==="item.updated"){if(await nt(a.item))continue}else if(a.type==="item.completed"){if(b||await E(C,v),await Ct(a.item,"end")||await nt(a.item)||await Kt(a.item))continue;if(a.item.type==="agentMessage"||a.item.type==="plan"){await xt(a.item);const u=Et(a.item)??"";u&&!j.has(a.item.id)&&await s({type:"text-delta",id:a.item.id,delta:u}),await s({type:"text-end",id:a.item.id})}else if(a.item.type==="reasoning"){await bt(a.item);const u=Et(a.item)??"";u&&!j.has(a.item.id)&&await s({type:"reasoning-delta",id:a.item.id,delta:u}),await s({type:"reasoning-end",id:a.item.id})}}else if(a.type==="item.tool.requestUserInput"){if(b||await E(C,v),a.params.isBlocking===!1){if(!await qt(p,y,a.params.itemId,{answers:{}}))throw new Error(`Non-blocking user input request is no longer pending: ${a.params.itemId}`);H.info(`Auto-resolved non-blocking request_user_input ${a.params.itemId} for turn ${a.params.turnId}`);continue}_.set(V(l,a.params.itemId),{conversationId:l,toolCallId:a.params.itemId,toolName:"request_user_input",turnId:a.params.turnId,since:new Date().toISOString()}),await s({type:"tool-input-available",toolCallId:a.params.itemId,toolName:"request_user_input",input:a.params}),await B();return}else if(a.type==="turn.diff.updated")b||await E(C,v),st.set(Wt(l,y.id),a.diff);else if(a.type==="thread.tokenUsage.updated"){const u=le(a.usage);if(u){const g=ie(gt,u.last);g&&(gt=g,at={...u,last:g})}}else if(a.type==="turn.failed"){yt!==a.error.message&&await s({type:"error",errorText:a.error.message}),await rt("error"),X=!0,Nt(p);return}else a.type==="error"&&(yt=a.message,await s({type:"error",errorText:a.message}));X=!F.signal.aborted,X&&Nt(p),b||await E(C,v),await rt("stop")}catch(n){const a=n instanceof Error?n.message:"Chat stream failed";b||await E(C,v),await s({type:"error",errorText:a}),await rt("error")}finally{N&&clearTimeout(N),q?.removeEventListener("abort",et),J.delete(F),X||Ut(p)}}}}async function Be({body:t,headers:e,signal:r},o){await(await Lt({body:t,headers:e})).run(o,r)}K.post("/v1/chat/stream",Qt("json",te),async t=>{const e=t.req.valid("json"),r=h(t.req.raw.headers,"X-Elevo-Wakeup-Key")?.trim()||void 0;let o;if(r){const d=it().claim(r);if(d.status!=="claimed")return t.header("X-Elevo-Wakeup-Duplicate","true"),t.json({error:"Scheduled wakeup was already accepted"},409);o=d.token}let i;try{i=await Lt({body:e,headers:t.req.raw.headers})}catch(d){throw r&&o&&it().release(r,o),d instanceof W?new U(d.status,{message:d.message}):d}return Vt(t,async d=>{let I=!1;const O=()=>{!r||!o||I||(I=!0,it().markStarted(r,o))},M=async S=>{await d.writeSSE(S)};await i.run({writeData:async S=>{O(),await M({data:JSON.stringify(S)})},writeDone:async()=>{O(),await M({data:"[DONE]"})}},t.req.raw.signal)})});var ze=K;export{W as ChatRequestError,Mt as cancelCodexConversation,je as codexActivitySnapshot,ze as default,Be as handleCodexChatStream,Lt as prepareCodexChatStream};
@@ -1 +1 @@
1
- import{createRequire as p}from"node:module";import h from"ws";import{ChatBodySchema as y}from"./schemas.js";import{appConfig as C}from"./config.js";import{cancelCodexConversation as b,ChatRequestError as g,handleCodexChatStream as m}from"./routes/chat.js";import{logger as l}from"@messenger-agent/shared/logger";import{sendHttpTunnelResponse as f}from"@messenger-agent/shared/tunnel-http";import{CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE as w,CODING_AGENT_TUNNEL_PROTOCOL_VERSION as v}from"@messenger-agent/shared/tunnel-protocol";const T=p(import.meta.url)("../package.json").version;function _(r){return new Promise(e=>setTimeout(e,r))}function k(r){try{const e=JSON.parse(r.toString());return!e||typeof e!="object"||!("type"in e)?void 0:e}catch{return}}function I(r){const e=new Headers;for(const[t,d]of Object.entries(r??{}))e.set(t,d);return e}function O(r){if(r.method==="POST")try{const e=new URL(r.path,"http://coding-agent-tunnel.local").pathname,t=/^\/v1\/chat\/([^/]+)\/cancel$/.exec(e);return t?decodeURIComponent(t[1]):void 0}catch{return}}class q{config;stopped=!1;socket;running=new Map;constructor(e){this.config=e}async start(){if(!this.config.enabled)return;if(!this.config.serverUrl||!this.config.tunnelId||!this.config.token)throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");let e=this.config.reconnectInitialMs;for(;!this.stopped;){try{await this.connectOnce(),e=this.config.reconnectInitialMs}catch(t){if(this.stopped)break;l.warn("[TunnelClient] websocket tunnel connection failed:",t)}this.stopped||(await _(e),e=Math.min(e*2,this.config.reconnectMaxMs))}}stop(){this.stopped=!0;for(const e of this.running.values())e.abortController.abort();this.running.clear(),this.socket?.close()}connectOnce(){return new Promise((e,t)=>{const d={Authorization:`Bearer ${this.config.token}`},i=new h(this.config.serverUrl,{headers:d});this.socket=i;let n,a=!1;const s=()=>{n&&clearInterval(n),this.socket===i&&(this.socket=void 0);for(const c of this.running.values())c.abortController.abort(new Error("Tunnel connection closed"));this.running.clear()};i.on("open",()=>{this.send({type:"hello",protocolVersion:v,tunnelId:this.config.tunnelId,agentType:"codex",clientVersion:T}),n=setInterval(()=>{this.send({type:"heartbeat"})},this.config.heartbeatIntervalMs),l.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`)}),i.on("message",c=>{const o=k(c);if(!o){l.warn("[TunnelClient] received invalid websocket message");return}if(o.type!=="heartbeat"){if(o.type==="cancel"){this.running.get(o.id)?.abortController.abort();return}o.type==="request"&&this.handleRequest(o).catch(u=>{l.error(`[TunnelClient] request ${o.id} failed:`,u)})}}),i.once("error",c=>{a||(a=!0,s(),t(c))}),i.once("close",(c,o)=>{s(),c===w&&(this.stopped=!0,l.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${o.toString()}`)),a||(a=!0,e())})})}send(e){const t=this.socket;!t||t.readyState!==h.OPEN||t.send(JSON.stringify(e))}async handleRequest(e){const t=O(e);if(t){let n;try{n=await b(t)}catch(s){const c=s instanceof Error?s.message:"Failed to interrupt Codex conversation",o=Response.json({error:{type:"server_error",code:"cancel_failed",message:c}},{status:500});await f(e.id,o,u=>this.send(u));return}const a=Response.json(n?{success:!0}:{error:{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}},{status:n?200:404});await f(e.id,a,s=>this.send(s));return}if(e.method!=="POST"||e.path!=="/v1/chat/stream"){this.send({type:"response.error",id:e.id,code:"unsupported_request",errorText:`Unsupported tunnel request: ${e.method} ${e.path}`});return}const d=y.safeParse(e.body);if(!d.success){this.send({type:"response.error",id:e.id,code:"invalid_request",errorText:`Invalid chat request body: ${d.error.message}`});return}const i=new AbortController;this.running.set(e.id,{abortController:i}),this.send({type:"response.start",id:e.id,mode:"sse"});try{await m({body:d.data,headers:I(e.headers),signal:i.signal},{writeData:async n=>this.send({type:"response.chunk",id:e.id,data:n}),writeDone:async()=>this.send({type:"response.end",id:e.id})})}catch(n){const a=n instanceof Error?n.message:"Tunnel request failed",s=n instanceof g?`http_${n.status}`:"request_failed";this.send({type:"response.error",id:e.id,code:s,errorText:a})}finally{this.running.delete(e.id)}}}function P(r=C.tunnel){if(!r.enabled)return;const e=new q(r);return e.start().catch(t=>{l.error("[TunnelClient] stopped unexpectedly:",t)}),e}export{q as CodexTunnelClient,P as startCodexTunnelClient};
1
+ import{createRequire as p}from"node:module";import h from"ws";import{ChatBodySchema as y}from"./schemas.js";import{appConfig as C}from"./config.js";import{cancelCodexConversation as m,ChatRequestError as b,handleCodexChatStream as g}from"./routes/chat.js";import{logger as l}from"@messenger-agent/shared/logger";import{reconnectDelayWithJitter as w}from"@messenger-agent/shared/reconnect-backoff";import{sendHttpTunnelResponse as f}from"@messenger-agent/shared/tunnel-http";import{CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE as v,CODING_AGENT_TUNNEL_PROTOCOL_VERSION as T}from"@messenger-agent/shared/tunnel-protocol";const _=p(import.meta.url)("../package.json").version;function k(r){return new Promise(e=>setTimeout(e,r))}function I(r){try{const e=JSON.parse(r.toString());return!e||typeof e!="object"||!("type"in e)?void 0:e}catch{return}}function O(r){const e=new Headers;for(const[t,d]of Object.entries(r??{}))e.set(t,d);return e}function q(r){if(r.method==="POST")try{const e=new URL(r.path,"http://coding-agent-tunnel.local").pathname,t=/^\/v1\/chat\/([^/]+)\/cancel$/.exec(e);return t?decodeURIComponent(t[1]):void 0}catch{return}}class x{config;stopped=!1;socket;running=new Map;constructor(e){this.config=e}async start(){if(!this.config.enabled)return;if(!this.config.serverUrl||!this.config.tunnelId||!this.config.token)throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");let e=this.config.reconnectInitialMs;for(;!this.stopped;){try{await this.connectOnce(),e=this.config.reconnectInitialMs}catch(t){if(this.stopped)break;l.warn("[TunnelClient] websocket tunnel connection failed:",t)}this.stopped||(await k(w(e)),e=Math.min(e*2,this.config.reconnectMaxMs))}}stop(){this.stopped=!0;for(const e of this.running.values())e.abortController.abort();this.running.clear(),this.socket?.close()}connectOnce(){return new Promise((e,t)=>{const d={Authorization:`Bearer ${this.config.token}`},i=new h(this.config.serverUrl,{headers:d});this.socket=i;let n,a=!1;const s=()=>{n&&clearInterval(n),this.socket===i&&(this.socket=void 0);for(const c of this.running.values())c.abortController.abort(new Error("Tunnel connection closed"));this.running.clear()};i.on("open",()=>{this.send({type:"hello",protocolVersion:T,tunnelId:this.config.tunnelId,agentType:"codex",clientVersion:_}),n=setInterval(()=>{this.send({type:"heartbeat"})},this.config.heartbeatIntervalMs),l.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`)}),i.on("message",c=>{const o=I(c);if(!o){l.warn("[TunnelClient] received invalid websocket message");return}if(o.type!=="heartbeat"){if(o.type==="cancel"){this.running.get(o.id)?.abortController.abort();return}o.type==="request"&&this.handleRequest(o).catch(u=>{l.error(`[TunnelClient] request ${o.id} failed:`,u)})}}),i.once("error",c=>{a||(a=!0,s(),t(c))}),i.once("close",(c,o)=>{s(),c===v&&(this.stopped=!0,l.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${o.toString()}`)),a||(a=!0,e())})})}send(e){const t=this.socket;!t||t.readyState!==h.OPEN||t.send(JSON.stringify(e))}async handleRequest(e){const t=q(e);if(t){let n;try{n=await m(t)}catch(s){const c=s instanceof Error?s.message:"Failed to interrupt Codex conversation",o=Response.json({error:{type:"server_error",code:"cancel_failed",message:c}},{status:500});await f(e.id,o,u=>this.send(u));return}const a=Response.json(n?{success:!0}:{error:{type:"not_found_error",code:"conversation_not_found",message:`Conversation not found or already completed: ${t}`,param:"conversation_id"}},{status:n?200:404});await f(e.id,a,s=>this.send(s));return}if(e.method!=="POST"||e.path!=="/v1/chat/stream"){this.send({type:"response.error",id:e.id,code:"unsupported_request",errorText:`Unsupported tunnel request: ${e.method} ${e.path}`});return}const d=y.safeParse(e.body);if(!d.success){this.send({type:"response.error",id:e.id,code:"invalid_request",errorText:`Invalid chat request body: ${d.error.message}`});return}const i=new AbortController;this.running.set(e.id,{abortController:i}),this.send({type:"response.start",id:e.id,mode:"sse"});try{await g({body:d.data,headers:O(e.headers),signal:i.signal},{writeData:async n=>this.send({type:"response.chunk",id:e.id,data:n}),writeDone:async()=>this.send({type:"response.end",id:e.id})})}catch(n){const a=n instanceof Error?n.message:"Tunnel request failed",s=n instanceof b?`http_${n.status}`:"request_failed";this.send({type:"response.error",id:e.id,code:s,errorText:a})}finally{this.running.delete(e.id)}}}function j(r=C.tunnel){if(!r.enabled)return;const e=new x(r);return e.start().catch(t=>{l.error("[TunnelClient] stopped unexpectedly:",t)}),e}export{x as CodexTunnelClient,j as startCodexTunnelClient};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@messenger-agent/codex-agent",
3
- "version": "0.27.0",
3
+ "version": "0.29.0-alpha.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "ws": "^8.21.0",
23
23
  "yaml": "^2.9.0",
24
24
  "zod": "^4.4.3",
25
- "@messenger-agent/shared": "0.27.0"
25
+ "@messenger-agent/shared": "0.29.0-alpha.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/ws": "^8.18.1"