beeos-claw 0.5.54 → 0.5.55
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/im-inbound-handler.js +1 -1
- package/dist/im-runtime-dispatch-session-guard.test.js +1 -1
- package/dist/im-runtime-dispatch.js +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/im-reply-sink.js +1 -1
- package/dist/runtime/prompt-run-store.js +1 -1
- package/dist/runtime/prompt-run-store.test.js +1 -1
- package/package.json +1 -1
- package/dist/runtime/openclaw-process-ownership-hook-registrar.js +0 -1
- package/dist/runtime/openclaw-process-ownership-hook-registrar.test.js +0 -1
- package/dist/runtime/openclaw-process-ownership-hook.js +0 -1
- package/dist/runtime/openclaw-process-ownership-hook.test.js +0 -1
- package/dist/runtime/process-session-terminator.js +0 -1
- package/dist/runtime/process-session-terminator.test.js +0 -1
- package/dist/runtime/run-process-lifecycle-coordinator.js +0 -1
- package/dist/runtime/run-process-lifecycle-coordinator.test.js +0 -1
- package/dist/runtime/run-process-ownership-registry.js +0 -1
- package/dist/runtime/run-process-ownership-registry.test.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",SEEN_MESSAGE_IDS_MAX=256,resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>{if("string"==typeof e.conversationId&&e.conversationId)return e.conversationId;const n=e.content??{};return"string"==typeof n.conversation_id&&n.conversation_id?n.conversation_id:"string"==typeof n.channel_id&&n.channel_id?n.channel_id:""};export const createImInboundHandler=e=>{const{runtime:n,pluginConfig:s,client:
|
|
1
|
+
import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",SEEN_MESSAGE_IDS_MAX=256,resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>{if("string"==typeof e.conversationId&&e.conversationId)return e.conversationId;const n=e.content??{};return"string"==typeof n.conversation_id&&n.conversation_id?n.conversation_id:"string"==typeof n.channel_id&&n.channel_id?n.channel_id:""};export const createImInboundHandler=e=>{const{runtime:n,pluginConfig:s,client:t,localServer:o,promptRunStore:a,sessionWriteGate:i,sessionWriteReleaseCoordinator:r,logger:c,writeObs:d,principalIdOverride:p,promptTimeoutMs:l,dispatch:m=dispatchImInboundPrompt}=e,g=new Map;return{onChatMessage:async e=>{const I=extractChatPrompt(e,{source:"chat_message",localAgentId:o.getLocalAgentId()});if(!I)return;if(!I.channelId||!I.messageId)return void c.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${I.sessionKey}) — drop`);const h=promptRunKey(I.channelId,I.messageId);if(g.has(h))return void c.info?.(`[beeos-claw] duplicate chat_message replay dropped (message_id=${I.messageId}, channelId=${I.channelId})`);if(g.size>=256){const e=g.keys().next().value;void 0!==e&&g.delete(e)}g.set(h,!0);const _=p||o.getDefaultPlatformAgent()?.instanceId||"";if(!_)return void c.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${I.sessionKey})`);const u=(e.content??{}).metadata??void 0,y="string"==typeof u?.delivery_principal?u.delivery_principal:"";if(y&&y!==_)return void c.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${y}, selfPrincipalId=${_}, message_id=${I.messageId})`);o.ensureImInboundSession(I.sessionKey,I.channelId,I.messageId);const f=resolveOrigin(u?.protocol);c.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${I.sessionKey}, channelId=${I.channelId}, origin=${f})`);const v=e=>{d({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await m({runtime:n,pluginConfig:s,prompt:{channelId:I.channelId,inboundMessageId:I.messageId,selfPrincipalId:_,dispatchInput:{message:I.message,attachments:I.files},sessionKey:I.sessionKey},client:t,logger:c,promptRunStore:a,sessionWriteGate:i,sessionWriteReleaseCoordinator:r,origin:f,promptTimeoutMs:l,onLifecycleEvent:v})}catch(e){c.error?.(`[beeos-claw] chat_message dispatch failed channelId=${I.channelId} in_reply_to=${I.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:e=>{const n=e.content??{},s="string"==typeof n.target_message_id?n.target_message_id:"",t="string"==typeof n.reason?n.reason:"unknown";if(!s)return void c.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${t})`);const o=conversationIdFromMessage(e),i=`chat_cancel:${t}`,r=o?a.abort(promptRunKey(o,s),i):a.abortByInboundMessageId(s,i);r.ok?c.info?.(`[beeos-claw] chat_cancel aborted run (target_message_id=${s}, requestId=${r.requestId}, channelId=${r.channelId}, reason=${t})`):c.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${s}, reason=${t}, why=${r.reason})`)}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mkdirSync,mkdtempSync,rmSync,unlinkSync,writeFileSync}from"node:fs";import{tmpdir}from"node:os";import{join}from"node:path";import{afterEach,describe,expect,it,vi}from"vitest";import{SessionWriteGate}from"./acp-gateway/session-write-gate.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{OpenClawSessionWriteQuiescence}from"./runtime/openclaw-session-write-quiescence.js";import{SessionWriteGateReleaseCoordinator}from"./runtime/session-write-gate-release-coordinator.js";import{RunProcessOwnershipRegistry}from"./runtime/run-process-ownership-registry.js";import{ProcessSessionTerminator}from"./runtime/process-session-terminator.js";import{RunProcessLifecycleCoordinator}from"./runtime/run-process-lifecycle-coordinator.js";import{createImDispatchFixture,createRuntime,deferred}from"./test-fixtures/im-runtime.fixture.js";const SESSION_LOCK_DIAGNOSTIC=["Agent failed before reply: session file locked (timeout 120000ms):","pid=357 /home/node/.openclaw/agents/main/sessions/channel-test.jsonl.lock","gatewayToken=super-secret-token"].join(" "),asOutcome=async e=>{try{return{value:await e}}catch(e){return{error:e}}};describe("dispatchImInboundPrompt Message Service session coordination",()=>{const e=[];afterEach(()=>{for(const t of e.splice(0))rmSync(t,{recursive:!0,force:!0})}),it("releases the cancelled session before a follow-up Message Service turn starts",async()=>{const e=createImDispatchFixture(),t=deferred(),s=deferred(),n=createRuntime(async({abortSignal:e,dispatcher:n})=>(t.resolve(),await new Promise((t,n)=>{e.addEventListener("abort",()=>{s.promise.then(()=>{n(new Error(String(e.reason)))})},{once:!0})}),n.sendFinalReply({text:"unreachable"}),{counts:{tool:0,block:0,final:1}})),o=dispatchImInboundPrompt(e.args(n));await t.promise,e.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request"),s.resolve(),await expect(o).rejects.toThrow(/chat_cancel:user_request/),expect(e.client.streams[0]?.terminal).toMatchObject({state:"cancelled",stopReason:"user_stop"});const r=createImDispatchFixture(),a=createRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"follow-up completed"}),{counts:{tool:0,block:0,final:1}})),i=await dispatchImInboundPrompt(r.args(a,{prompt:{channelId:"channel-test",inboundMessageId:"message-after-cancel",selfPrincipalId:"agent-test",sessionKey:"agent:main:im:channel-test",dispatchInput:{message:"follow-up"}}}));expect(i).toBe(!0),expect(r.client.streams[0]?.terminal).toEqual({state:"completed",stopReason:"end_turn",body:"follow-up completed"})}),it("redacts raw OpenClaw session-lock diagnostics from the Message Service reply",async()=>{const e=createImDispatchFixture(),t=createRuntime(async()=>{throw new Error(SESSION_LOCK_DIAGNOSTIC)});await expect(dispatchImInboundPrompt(e.args(t))).rejects.toThrow(SESSION_LOCK_DIAGNOSTIC);const s=classifyAgentRuntimeFailure({raw:SESSION_LOCK_DIAGNOSTIC,source:"live_gateway"}).publicMessage,n=e.client.streams[0]?.terminal;expect(n).toMatchObject({state:"failed",stopReason:"error",body:s}),expect(n?.body).not.toContain(".jsonl.lock"),expect(n?.body).not.toContain("super-secret-token"),expect(e.events.find(e=>"dispatch_error"===e.type)).toMatchObject({error:"session_file_locked"})}),it("terminalizes a gateway-injected lock final without ever writing the diagnostic body",async()=>{const e=createImDispatchFixture(),t=createRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:SESSION_LOCK_DIAGNOSTIC}),{counts:{tool:0,block:0,final:1}}));await expect(dispatchImInboundPrompt(e.args(t))).rejects.toThrow(/session file locked/i),expect(e.client.streams[0]?.terminal).toMatchObject({state:"failed",stopReason:"error",body:"The previous run was interrupted before it completed."}),expect(e.client.streams[0]?.body).not.toContain(".jsonl.lock"),expect(e.client.streams[0]?.body).not.toContain("super-secret-token")}),it("binds onAgentRunStart to the active prompt owner before cancellation",async()=>{const e=createImDispatchFixture(),t=deferred(),s=createRuntime(async({replyOptions:e,abortSignal:s})=>(e.onAgentRunStart?.("openclaw-run-1"),t.resolve(),await new Promise((e,t)=>{s.addEventListener("abort",()=>{t(new Error(String(s.reason)))},{once:!0})}),{counts:{tool:0,block:0,final:0}})),n=asOutcome(dispatchImInboundPrompt(e.args(s)));await t.promise,expect(e.promptRunStore.snapshot()[0]?.[1]).toMatchObject({runtimeRunId:"openclaw-run-1",sessionKey:"agent:main:im:channel-test"}),expect(e.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request")).toMatchObject({runtimeRunId:"openclaw-run-1"}),expect(await n).toHaveProperty("error")}),it("waits for exact owned-process termination before committing user cancellation",async()=>{const e=createImDispatchFixture(),t=vi.fn(async e=>{e.args.action;return{ok:!0,toolName:"process",output:{details:{status:"failed",sessionId:"process-session-90"}}}}),s=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:t})),n=deferred(),o=createRuntime(async({replyOptions:e,abortSignal:t})=>(e.onAgentRunStart?.("runtime-run-cancel"),await s.afterToolCall({runId:"runtime-run-cancel",sessionKey:"agent:main:im:channel-test",toolName:"exec",params:{command:"sleep 90"},result:{details:{status:"running",sessionId:"process-session-90"}}}),n.resolve(),await new Promise((e,s)=>{t.addEventListener("abort",()=>{s(new Error(String(t.reason)))},{once:!0})}),{counts:{tool:1,block:0,final:0}})),r=asOutcome(dispatchImInboundPrompt(e.args(o,{runProcessLifecycleCoordinator:s})));await n.promise,e.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request"),expect(await r).toHaveProperty("error"),expect(t.mock.calls.map(([e])=>e.args.action)).toEqual(["kill","poll"]),expect(e.client.streams[0]?.terminal).toMatchObject({state:"cancelled",stopReason:"user_stop"})}),it("does not report cancellation when owned-process termination is unverified",async()=>{const e=createImDispatchFixture(),t=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:async e=>"kill"===e.args.action?{ok:!0,output:{details:{status:"failed"}}}:{ok:!0,output:{details:{status:"running",sessionId:"process-session-90"}}}},{verificationAttempts:1})),s=deferred(),n=createRuntime(async({replyOptions:e,abortSignal:n})=>(e.onAgentRunStart?.("runtime-run-unverified"),await t.afterToolCall({runId:"runtime-run-unverified",sessionKey:"agent:main:im:channel-test",toolName:"exec",params:{command:"sleep 90"},result:{details:{status:"running",sessionId:"process-session-90"}}}),s.resolve(),await new Promise((e,t)=>{n.addEventListener("abort",()=>{t(new Error(String(n.reason)))},{once:!0})}),{counts:{tool:1,block:0,final:0}})),o=asOutcome(dispatchImInboundPrompt(e.args(n,{runProcessLifecycleCoordinator:t})));await s.promise,e.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request"),expect(await o).toHaveProperty("error"),expect(e.client.streams[0]?.terminal).toEqual({state:"failed",stopReason:"error",body:"The task could not be stopped cleanly. Please retry."}),expect(e.events.find(e=>"dispatch_error"===e.type)).toMatchObject({error:"process_termination_unverified"})}),it("does not report normal completion when owned-process cleanup is unverified",async()=>{const e=createImDispatchFixture(),t=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:async e=>"kill"===e.args.action?{ok:!0,output:{}}:{ok:!0,output:{details:{status:"running",sessionId:"process-session-yield"}}}},{verificationAttempts:1})),s=createRuntime(async({replyOptions:e,dispatcher:s})=>(e.onAgentRunStart?.("runtime-run-success-unverified"),await t.afterToolCall({runId:"runtime-run-success-unverified",sessionKey:"agent:main:im:channel-test",toolName:"exec",params:{command:"sleep 90"},result:{details:{status:"running",sessionId:"process-session-yield"}}}),s.sendFinalReply({text:"runtime returned"}),{counts:{tool:1,block:0,final:1}}));await expect(dispatchImInboundPrompt(e.args(s,{runProcessLifecycleCoordinator:t}))).resolves.toBe(!1),expect(e.client.streams[0]?.terminal).toEqual({state:"failed",stopReason:"error",body:"The task could not be stopped cleanly. Please retry."}),expect(e.events.find(e=>"dispatch_complete"===e.type)).toBeUndefined()}),it("completes normally after process polling releases naturally exited exec ownership",async()=>{const e=createImDispatchFixture(),t=vi.fn(async()=>{throw new Error("normal completion must not invoke process termination")}),s=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:t})),n=createRuntime(async({replyOptions:e,dispatcher:t})=>{const n="runtime-run-natural-completion",o="agent:main:im:channel-test";return e.onAgentRunStart?.(n),await s.afterToolCall({runId:n,sessionKey:o,toolName:"exec",params:{command:"sleep 90"},result:{details:{status:"running",sessionId:"process-session-natural"}}}),await s.afterToolCall({runId:n,sessionKey:o,toolName:"process",params:{action:"poll",sessionId:"process-session-natural"},result:{details:{status:"completed",sessionId:"process-session-natural",exitCode:0}}}),t.sendFinalReply({text:"completed naturally"}),{counts:{tool:2,block:0,final:1}}});await expect(dispatchImInboundPrompt(e.args(n,{runProcessLifecycleCoordinator:s}))).resolves.toBe(!0),expect(t).not.toHaveBeenCalled(),expect(e.client.streams[0]?.terminal).toEqual({state:"completed",stopReason:"end_turn",body:"completed naturally"})}),it("releases the gate immediately when runtime dispatch throws synchronously",async()=>{const e=new SessionWriteGate,t=new SessionWriteGateReleaseCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{observationTimeoutMs:1}),s=createImDispatchFixture({sessionWriteGate:e,sessionWriteReleaseCoordinator:t}),n={channel:{reply:{dispatchReplyFromConfig:()=>{throw new Error("runtime rejected before writer start")}}}};await expect(dispatchImInboundPrompt(s.args(n))).rejects.toThrow("runtime rejected before writer start"),expect(e.snapshot("agent:main:im:channel-test").active).toBeUndefined();const o=createImDispatchFixture({sessionWriteGate:e,sessionWriteReleaseCoordinator:t}),r=createRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"accepted"}),{counts:{tool:0,block:0,final:1}}));await expect(dispatchImInboundPrompt(o.args(r))).resolves.toBe(!0)}),it("returns a busy reply without invoking the runtime for a concurrent same-session envelope",async()=>{const e=new SessionWriteGate,t={sessionWriteGate:e,sessionWriteReleaseCoordinator:new SessionWriteGateReleaseCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{})},s=new Set,n=deferred(),o=deferred(),r=[],a=createRuntime(async({invocation:e,abortSignal:t})=>{const a=e.ctx.SessionKey;if(r.push(a),s.has(a))throw new Error(SESSION_LOCK_DIAGNOSTIC);return s.add(a),n.resolve(),await new Promise((e,n)=>{t.addEventListener("abort",()=>{o.promise.then(()=>{s.delete(a),n(new Error(String(t.reason)))})},{once:!0})}),{counts:{tool:0,block:0,final:0}}}),i=createImDispatchFixture(t),c=dispatchImInboundPrompt(i.args(a));await n.promise;const l=createImDispatchFixture(t),p=asOutcome(dispatchImInboundPrompt(l.args(a,{prompt:{channelId:"channel-test",inboundMessageId:"message-concurrent",selfPrincipalId:"agent-test",sessionKey:"agent:main:im:channel-test",dispatchInput:{message:"concurrent prompt"}}}))),m=await p;i.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request"),o.resolve(),await asOutcome(c),expect(m).toEqual({value:!1}),expect(r).toEqual(["agent:main:im:channel-test"]),expect(l.client.streams[0]?.terminal).toMatchObject({state:"failed",body:expect.stringMatching(/still stopping/i)})}),it("keeps a non-contended Message Service stream on the normal completion path",async()=>{const e=createImDispatchFixture(),t=createRuntime(async({dispatcher:e,replyOptions:t})=>(t.onPartialReply?.({text:"normal"}),e.sendFinalReply({text:"normal completion"}),{counts:{tool:0,block:0,final:1}}));await expect(dispatchImInboundPrompt(e.args(t))).resolves.toBe(!0),expect(e.client.streams[0]?.terminal).toEqual({state:"completed",stopReason:"end_turn",body:"normal completion"}),expect(e.events.map(e=>e.type)).toEqual(["dispatch_start","dispatch_complete"])}),it("keeps the shared gate closed until the cancelled JSONL writer releases its lock",async()=>{const t="agent:main:im:locked-conversation",s=mkdtempSync(join(tmpdir(),"beeos-im-lock-"));e.push(s);const n=join(s,"agents","main","sessions");mkdirSync(n,{recursive:!0});const o=join(n,"locked-session.jsonl"),r=`${o}.lock`;writeFileSync(o,""),writeFileSync(join(n,"sessions.json"),JSON.stringify({[t]:{sessionId:"locked-session",sessionFile:o}}));const a=new SessionWriteGate,i=new OpenClawSessionWriteQuiescence({},{stateDir:s,pollIntervalMs:5,quietConfirmationMs:5}),c={sessionWriteGate:a,sessionWriteReleaseCoordinator:new SessionWriteGateReleaseCoordinator(a,i,{},{observationTimeoutMs:100,retryDelayMs:5})},l=deferred();let p=0;const m=createRuntime(async({abortSignal:e,dispatcher:t})=>(p+=1,1===p&&(l.resolve(),await new Promise((t,s)=>{e.addEventListener("abort",()=>{s(new Error(String(e.reason)))},{once:!0})})),t.sendFinalReply({text:"after lock released"}),{counts:{tool:0,block:0,final:1}})),u=createImDispatchFixture(c),d=asOutcome(dispatchImInboundPrompt(u.args(m,{prompt:{channelId:"channel-test",inboundMessageId:"message-before-cancel",selfPrincipalId:"agent-test",sessionKey:t,dispatchInput:{message:"start long run"}}})));await l.promise,writeFileSync(r,"331"),u.promptRunStore.abort(promptRunKey("channel-test","message-before-cancel"),"chat_cancel:user_request");const h=createImDispatchFixture(c);await expect(dispatchImInboundPrompt(h.args(m,{prompt:{channelId:"channel-test",inboundMessageId:"message-while-locked",selfPrincipalId:"agent-test",sessionKey:t,dispatchInput:{message:"must not enter runtime"}}}))).resolves.toBe(!1),expect(p).toBe(1),unlinkSync(r),expect(await d).toHaveProperty("error");const y=createImDispatchFixture(c);await expect(dispatchImInboundPrompt(y.args(m,{prompt:{channelId:"channel-test",inboundMessageId:"message-after-unlock",selfPrincipalId:"agent-test",sessionKey:t,dispatchInput:{message:"now safe"}}}))).resolves.toBe(!0),expect(p).toBe(2)})});
|
|
1
|
+
import{mkdirSync,mkdtempSync,rmSync,unlinkSync,writeFileSync}from"node:fs";import{tmpdir}from"node:os";import{join}from"node:path";import{afterEach,describe,expect,it}from"vitest";import{SessionWriteGate}from"./acp-gateway/session-write-gate.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{OpenClawSessionWriteQuiescence}from"./runtime/openclaw-session-write-quiescence.js";import{SessionWriteGateReleaseCoordinator}from"./runtime/session-write-gate-release-coordinator.js";import{createImDispatchFixture,createRuntime,deferred}from"./test-fixtures/im-runtime.fixture.js";const SESSION_LOCK_DIAGNOSTIC=["Agent failed before reply: session file locked (timeout 120000ms):","pid=357 /home/node/.openclaw/agents/main/sessions/channel-test.jsonl.lock","gatewayToken=super-secret-token"].join(" "),asOutcome=async e=>{try{return{value:await e}}catch(e){return{error:e}}};describe("dispatchImInboundPrompt Message Service session coordination",()=>{const e=[];afterEach(()=>{for(const t of e.splice(0))rmSync(t,{recursive:!0,force:!0})}),it("releases the cancelled session before a follow-up Message Service turn starts",async()=>{const e=createImDispatchFixture(),t=deferred(),s=deferred(),n=createRuntime(async({abortSignal:e,dispatcher:n})=>(t.resolve(),await new Promise((t,n)=>{e.addEventListener("abort",()=>{s.promise.then(()=>{n(new Error(String(e.reason)))})},{once:!0})}),n.sendFinalReply({text:"unreachable"}),{counts:{tool:0,block:0,final:1}})),o=dispatchImInboundPrompt(e.args(n));await t.promise,e.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request"),s.resolve(),await expect(o).rejects.toThrow(/chat_cancel:user_request/),expect(e.client.streams[0]?.terminal).toMatchObject({state:"cancelled",stopReason:"user_stop"});const a=createImDispatchFixture(),r=createRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"follow-up completed"}),{counts:{tool:0,block:0,final:1}})),i=await dispatchImInboundPrompt(a.args(r,{prompt:{channelId:"channel-test",inboundMessageId:"message-after-cancel",selfPrincipalId:"agent-test",sessionKey:"agent:main:im:channel-test",dispatchInput:{message:"follow-up"}}}));expect(i).toBe(!0),expect(a.client.streams[0]?.terminal).toEqual({state:"completed",stopReason:"end_turn",body:"follow-up completed"})}),it("redacts raw OpenClaw session-lock diagnostics from the Message Service reply",async()=>{const e=createImDispatchFixture(),t=createRuntime(async()=>{throw new Error(SESSION_LOCK_DIAGNOSTIC)});await expect(dispatchImInboundPrompt(e.args(t))).rejects.toThrow(SESSION_LOCK_DIAGNOSTIC);const s=classifyAgentRuntimeFailure({raw:SESSION_LOCK_DIAGNOSTIC,source:"live_gateway"}).publicMessage,n=e.client.streams[0]?.terminal;expect(n).toMatchObject({state:"failed",stopReason:"error",body:s}),expect(n?.body).not.toContain(".jsonl.lock"),expect(n?.body).not.toContain("super-secret-token"),expect(e.events.find(e=>"dispatch_error"===e.type)).toMatchObject({error:"session_file_locked"})}),it("terminalizes a gateway-injected lock final without ever writing the diagnostic body",async()=>{const e=createImDispatchFixture(),t=createRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:SESSION_LOCK_DIAGNOSTIC}),{counts:{tool:0,block:0,final:1}}));await expect(dispatchImInboundPrompt(e.args(t))).rejects.toThrow(/session file locked/i),expect(e.client.streams[0]?.terminal).toMatchObject({state:"failed",stopReason:"error",body:"The previous run was interrupted before it completed."}),expect(e.client.streams[0]?.body).not.toContain(".jsonl.lock"),expect(e.client.streams[0]?.body).not.toContain("super-secret-token")}),it("releases the gate immediately when runtime dispatch throws synchronously",async()=>{const e=new SessionWriteGate,t=new SessionWriteGateReleaseCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{observationTimeoutMs:1}),s=createImDispatchFixture({sessionWriteGate:e,sessionWriteReleaseCoordinator:t}),n={channel:{reply:{dispatchReplyFromConfig:()=>{throw new Error("runtime rejected before writer start")}}}};await expect(dispatchImInboundPrompt(s.args(n))).rejects.toThrow("runtime rejected before writer start"),expect(e.snapshot("agent:main:im:channel-test").active).toBeUndefined();const o=createImDispatchFixture({sessionWriteGate:e,sessionWriteReleaseCoordinator:t}),a=createRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"accepted"}),{counts:{tool:0,block:0,final:1}}));await expect(dispatchImInboundPrompt(o.args(a))).resolves.toBe(!0)}),it("returns a busy reply without invoking the runtime for a concurrent same-session envelope",async()=>{const e=new SessionWriteGate,t={sessionWriteGate:e,sessionWriteReleaseCoordinator:new SessionWriteGateReleaseCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{})},s=new Set,n=deferred(),o=deferred(),a=[],r=createRuntime(async({invocation:e,abortSignal:t})=>{const r=e.ctx.SessionKey;if(a.push(r),s.has(r))throw new Error(SESSION_LOCK_DIAGNOSTIC);return s.add(r),n.resolve(),await new Promise((e,n)=>{t.addEventListener("abort",()=>{o.promise.then(()=>{s.delete(r),n(new Error(String(t.reason)))})},{once:!0})}),{counts:{tool:0,block:0,final:0}}}),i=createImDispatchFixture(t),c=dispatchImInboundPrompt(i.args(r));await n.promise;const l=createImDispatchFixture(t),p=asOutcome(dispatchImInboundPrompt(l.args(r,{prompt:{channelId:"channel-test",inboundMessageId:"message-concurrent",selfPrincipalId:"agent-test",sessionKey:"agent:main:im:channel-test",dispatchInput:{message:"concurrent prompt"}}}))),m=await p;i.promptRunStore.abort(promptRunKey("channel-test","message-test"),"chat_cancel:user_request"),o.resolve(),await asOutcome(c),expect(m).toEqual({value:!1}),expect(a).toEqual(["agent:main:im:channel-test"]),expect(l.client.streams[0]?.terminal).toMatchObject({state:"failed",body:expect.stringMatching(/still stopping/i)})}),it("keeps a non-contended Message Service stream on the normal completion path",async()=>{const e=createImDispatchFixture(),t=createRuntime(async({dispatcher:e,replyOptions:t})=>(t.onPartialReply?.({text:"normal"}),e.sendFinalReply({text:"normal completion"}),{counts:{tool:0,block:0,final:1}}));await expect(dispatchImInboundPrompt(e.args(t))).resolves.toBe(!0),expect(e.client.streams[0]?.terminal).toEqual({state:"completed",stopReason:"end_turn",body:"normal completion"}),expect(e.events.map(e=>e.type)).toEqual(["dispatch_start","dispatch_complete"])}),it("keeps the shared gate closed until the cancelled JSONL writer releases its lock",async()=>{const t="agent:main:im:locked-conversation",s=mkdtempSync(join(tmpdir(),"beeos-im-lock-"));e.push(s);const n=join(s,"agents","main","sessions");mkdirSync(n,{recursive:!0});const o=join(n,"locked-session.jsonl"),a=`${o}.lock`;writeFileSync(o,""),writeFileSync(join(n,"sessions.json"),JSON.stringify({[t]:{sessionId:"locked-session",sessionFile:o}}));const r=new SessionWriteGate,i=new OpenClawSessionWriteQuiescence({},{stateDir:s,pollIntervalMs:5,quietConfirmationMs:5}),c={sessionWriteGate:r,sessionWriteReleaseCoordinator:new SessionWriteGateReleaseCoordinator(r,i,{},{observationTimeoutMs:100,retryDelayMs:5})},l=deferred();let p=0;const m=createRuntime(async({abortSignal:e,dispatcher:t})=>(p+=1,1===p&&(l.resolve(),await new Promise((t,s)=>{e.addEventListener("abort",()=>{s(new Error(String(e.reason)))},{once:!0})})),t.sendFinalReply({text:"after lock released"}),{counts:{tool:0,block:0,final:1}})),d=createImDispatchFixture(c),u=asOutcome(dispatchImInboundPrompt(d.args(m,{prompt:{channelId:"channel-test",inboundMessageId:"message-before-cancel",selfPrincipalId:"agent-test",sessionKey:t,dispatchInput:{message:"start long run"}}})));await l.promise,writeFileSync(a,"331"),d.promptRunStore.abort(promptRunKey("channel-test","message-before-cancel"),"chat_cancel:user_request");const h=createImDispatchFixture(c);await expect(dispatchImInboundPrompt(h.args(m,{prompt:{channelId:"channel-test",inboundMessageId:"message-while-locked",selfPrincipalId:"agent-test",sessionKey:t,dispatchInput:{message:"must not enter runtime"}}}))).resolves.toBe(!1),expect(p).toBe(1),unlinkSync(a),expect(await u).toHaveProperty("error");const I=createImDispatchFixture(c);await expect(dispatchImInboundPrompt(I.args(m,{prompt:{channelId:"channel-test",inboundMessageId:"message-after-unlock",selfPrincipalId:"agent-test",sessionKey:t,dispatchInput:{message:"now safe"}}}))).resolves.toBe(!0),expect(p).toBe(2)})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{interceptAgentRuntimeFailures,messageStopReasonForRuntimeFailure,SESSION_BUSY_PUBLIC_MESSAGE}from"./runtime/im-runtime-failure-policy.js";const
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{interceptAgentRuntimeFailures,messageStopReasonForRuntimeFailure,SESSION_BUSY_PUBLIC_MESSAGE}from"./runtime/im-runtime-failure-policy.js";const normalizePromptTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},deterministicMessageId=(e,n,r,a)=>`${e}_${createHash("sha256").update(`${n}:${r}:${a}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:r,client:a,logger:t,promptRunStore:i,sessionWriteGate:s,sessionWriteReleaseCoordinator:o}=e;if(!hasRuntimeReplyApi(n))return t.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${r.channelId} in_reply_to=${r.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:r.channelId,inboundMessageId:r.inboundMessageId}),!1;const d=`im_req_${randomUUID().replace(/-/g,"")}`,l=Date.now(),c=e.origin??"im",u={protocol:"im",requestId:d,sessionId:r.sessionKey??"agent:main",sourceMessageId:r.inboundMessageId,chatId:r.channelId,origin:c},m=normalizePromptTimeoutMs(e.promptTimeoutMs),p=promptRunKey(r.channelId,r.inboundMessageId),I=r.sessionKey??"agent:main",g=new AbortController;if(!i.register(p,{requestId:d,channelId:r.channelId,abortController:g,startedAt:l,sessionKey:I}))return t.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${r.inboundMessageId} requestId=${d} channel=${r.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const h=a.messages.startStream({conversationId:r.channelId,id:deterministicMessageId("asst",r.channelId,r.inboundMessageId,r.selfPrincipalId),replyTo:e.inReplyTo??r.inboundMessageId,type:"agent_reply"},{onError:(e,n)=>{const a="terminal"===n?t.warn:t.debug;a?.(`[im] runtime stream ${n} PATCH error requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${e.message}`)}},{sender:r.selfPrincipalId});let y,f=!1;try{f=!0===(await h.opened()).idempotent}catch(e){t.debug?.(`[im] runtime reply stream open probe failed requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}; continuing so terminal fallback can land an error row`)}if(f)return t.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:r.channelId,inboundMessageId:r.inboundMessageId}),i.unregister(p),!1;const $=new SerializedCallbackQueue,b=createImReplySink({stream:h,openReasoningStream:()=>(y=a.messages.startStream({conversationId:r.channelId,id:deterministicMessageId("rsn",r.channelId,r.inboundMessageId,r.selfPrincipalId),replyTo:e.inReplyTo??r.inboundMessageId,type:"agent_reasoning"},{onError:(e,n)=>{const a="terminal"===n?t.warn:t.debug;a?.(`[im] runtime reasoning stream ${n} PATCH error requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${e.message}`)}},{sender:r.selfPrincipalId}),y),abortSignal:g.signal,callbackQueue:$,logger:t,requestId:d,channelId:r.channelId,inboundMessageId:r.inboundMessageId,origin:c}),_=interceptAgentRuntimeFailures(b,"im_runtime",e=>t.warn?.(`[im] runtime failure reply suppressed requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId} error_code=${e.errorCode}`)),M=_.sink,w=async(e,n)=>{if(!y)return!1;try{return"cancel"===e?await y.cancel({stopReason:"user_stop"}):"fail"===e?await y.fail(n?{stopReason:n}:void 0):await y.finalize({stopReason:"end_turn"}),!1}catch(e){return t.warn?.(`[im] runtime reasoning stream terminal write failed requestId=${d} channel=${r.channelId}: ${formatErrMessage(e)}`),!0}};let R,S,E=!1;const q=async e=>{M.dispatcher.markComplete(e);try{await M.dispatcher.waitForIdle()}catch{}},v=()=>{if(!g.signal.aborted)return;const e=g.signal.reason;throw e instanceof Error?e:new Error(String(e??"runtime dispatch aborted"))},k=s.tryStartInteractive({sessionKey:I,label:"im_chat_message"});if(!k.ok){try{await h.fail({body:SESSION_BUSY_PUBLIC_MESSAGE,stopReason:"error"})}catch(e){t.warn?.(`[im] session busy terminal write failed channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}`)}return t.warn?.(`[im] session write gate busy sessionKey=${I} channel=${r.channelId} in_reply_to=${r.inboundMessageId} active_operation=${k.activeOperationId??"unknown"}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:"session_busy",cause:"runtime_error"}),i.unregister(p),!1}const C=k.token;try{t.info?.(`[im] runtime dispatching requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId} origin=${c}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:d,channelId:r.channelId,inboundMessageId:r.inboundMessageId});const a=dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:r.dispatchInput,replyRoute:u,replySinks:{im:M},onDispatchInvoked:()=>{E=!0}}),s=void 0!==m?Promise.race([a,new Promise((e,n)=>{R=setTimeout(()=>{S=new Error(`IM runtime dispatch timed out after ${m}ms`),t.warn?.(`[im] runtime dispatch timeout requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId} timeout_ms=${m}`),g.abort(S),n(S)},m)})]):a,o=await s.finally(()=>{R&&(clearTimeout(R),R=void 0)}),I=_.detectedFailure();if(I)throw new Error(I.diagnosticMessage);if(v(),await q("success"),v(),!i.claimTerminal(p))throw v(),new Error("terminal outcome already claimed");const y=await w("complete");let f=!1;try{await h.finalize({stopReason:"end_turn"})}catch(e){f=!0,t.warn?.(`[im] runtime reply stream finalize failed requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}`)}return t.info?.(`[im] runtime dispatch completed requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId} counts=${JSON.stringify(o.counts)} elapsed_ms=${Date.now()-l}`),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:d,channelId:r.channelId,inboundMessageId:r.inboundMessageId,counts:o.counts,...f?{terminalWriteFailed:!0}:{},...y?{reasoningTerminalWriteFailed:!0}:{}}),!0}catch(n){await q("failure");const a=g.signal.aborted,s=a?g.signal.reason:void 0,o=a&&"string"==typeof s&&s.startsWith("chat_cancel"),l=void 0!==S;let c="runtime_error";if(l?c="timeout":a&&(c="abort"),!i.claimTerminal(p))throw t.warn?.(`[im] terminal write skipped because another outcome already committed requestId=${d} channel=${r.channelId}`),n;const u=_.detectedFailure()??classifyAgentRuntimeFailure({raw:n,source:"im_runtime",runtimeKind:"openclaw"});o?await w("cancel"):l?await w("fail","timeout"):await w("fail");try{o?await h.cancel({stopReason:"user_stop"}):await h.fail({body:u.publicMessage,stopReason:messageStopReasonForRuntimeFailure(u)})}catch(e){t.warn?.(`[im] runtime stream terminal write failed requestId=${d} channel=${r.channelId}: ${formatErrMessage(e)}`)}throw t.warn?.(`[im] runtime dispatch failed requestId=${d} channel=${r.channelId} in_reply_to=${r.inboundMessageId} cause=${c} dispatchInvoked=${E} error_code=${u.errorCode}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:u.errorCode,cause:c}),n}finally{i.unregister(p),E?await o.releaseAfterQuiescence(C):o.releaseImmediately(C)}};
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{hasRuntimeReplyApi}from"./runtime/reply-router.js";import{PromptRunStore}from"./runtime/prompt-run-store.js";import{SessionWriteGate}from"./acp-gateway/session-write-gate.js";import{OpenClawSessionWriteQuiescence}from"./runtime/openclaw-session-write-quiescence.js";import{SessionWriteGateReleaseCoordinator}from"./runtime/session-write-gate-release-coordinator.js";import{RunProcessOwnershipRegistry}from"./runtime/run-process-ownership-registry.js";import{ProcessSessionTerminator}from"./runtime/process-session-terminator.js";import{RunProcessLifecycleCoordinator}from"./runtime/run-process-lifecycle-coordinator.js";import{OpenClawProcessOwnershipHookRegistrar}from"./runtime/openclaw-process-ownership-hook-registrar.js";import{createImInboundHandler}from"./im-inbound-handler.js";import{MessageServiceRecoveryScanner}from"./im-recovery-scanner.js";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{BEEOS_LIST_FILES_TOOL_NAME,BEEOS_LIST_FILES_TOOL_SCHEMA,BEEOS_LIST_FILES_TOOL_DESCRIPTION,executeListFiles}from"./files/list-tool.js";import{BEEOS_FETCH_FILE_TOOL_NAME,BEEOS_FETCH_FILE_TOOL_SCHEMA,BEEOS_FETCH_FILE_TOOL_DESCRIPTION,executeFetchFile}from"./files/fetch-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{BEEOS_FILE_RESOURCE_ROUTING_RULE}from"./files/beeos-file-uri.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";import{registerArouterMediaProviders}from"./media/index.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null;const sharedSessionWriteGate=new SessionWriteGate,sharedPromptRunStore=new PromptRunStore,sharedRunProcessOwnershipRegistry=new RunProcessOwnershipRegistry;let sharedRunProcessLifecycleCoordinator,sharedSessionWriteReleaseCoordinator;const processOwnershipHookRegistrar=new OpenClawProcessOwnershipHookRegistrar;let _initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async r=>{const{createPrivateKey:o,sign:t}=await import("node:crypto"),a=o({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(t(null,Buffer.from(r),a))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,r,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] unhandledRejection:",o),r({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:t,error:{code:"unhandled_rejection",message:t}})}),process.on("uncaughtException",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] uncaughtException:",o),r({component:"plugin",domain:"fatal",name:"uncaught_exception",severity:"error",summary:t,error:{code:"uncaught_exception",message:t}})}))}function initializePlugin(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const t=o.config,a=buildThirdPartyChannelSet(r);a.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...a].join(", ")}`);const s=t.log.enabled?createObsEventWriter({dir:t.log.dir,redaction:{gatewayToken:t.gateway.token},logger:e.logger}):()=>{};s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:t,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),t.keyFile)try{agentKeys=loadAgentKeyPair(t.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&t.agentGatewayUrl&&(setUploadDeps({gatewayUrl:t.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:t,writeObs:s,thirdPartyChannels:a,openclawConfig:r}}function registerUploadTools(e,r,o){const{cfg:t,writeObs:a}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const n=await executeUpload(o,t,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${n.ok}`),a({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:r,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,r.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,r){handleStatusRequest(e,r,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}}),e.registerTool({name:BEEOS_LIST_FILES_TOOL_NAME,description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,parameters:BEEOS_LIST_FILES_TOOL_SCHEMA,async execute(r,o,s){e.logger.debug?.(`[beeos_list_files] toolCallId=${r} params=${JSON.stringify(o)}`);const n=await executeListFiles(o,t,agentKeys,s);return a({component:"file-list-tool",domain:"tool",name:n.ok?"list_files.success":"list_files.failure",severity:n.ok?"info":"warn",payload:{toolCallId:r,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),n}}),e.registerTool({name:BEEOS_FETCH_FILE_TOOL_NAME,description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,parameters:BEEOS_FETCH_FILE_TOOL_SCHEMA,async execute(r,o,s){e.logger.debug?.(`[beeos_fetch_file] toolCallId=${r} params=${JSON.stringify(o)}`);const n=await executeFetchFile(o,t,agentKeys,{signal:s,resolvePath:e.resolvePath});return a({component:"file-fetch-tool",domain:"tool",name:n.ok?"fetch_file.success":"fetch_file.failure",severity:n.ok?"info":"warn",payload:{toolCallId:r,ok:n.ok,path:n.path,size:n.size,error:n.error}}),n}})}function registerServices(e,r,o){const{cfg:t,writeObs:a,thirdPartyChannels:s}=r,n=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedSessionWriteReleaseCoordinator??=new SessionWriteGateReleaseCoordinator(sharedSessionWriteGate,new OpenClawSessionWriteQuiescence(e.logger,{agentId:t.gateway.agentId||"main"}),e.logger);const l=sharedSessionWriteReleaseCoordinator;sharedAcpServer||(sharedAcpServer=new ACPServer(t,e.logger,a,n,i,s,sharedSessionWriteGate,l));const c=sharedAcpServer;o.current=c;const g=sharedPromptRunStore;sharedRunProcessLifecycleCoordinator??=new RunProcessLifecycleCoordinator(sharedRunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:e=>c.gatewayRequest("tools.invoke",e)}));const d=sharedRunProcessLifecycleCoordinator;if("function"==typeof e.on)try{processOwnershipHookRegistrar.register(e,d,e.logger,e=>g.runtimeRunIdForSessionKey(e))}catch(r){e.logger.debug?.("[beeos-claw] after_tool_call hook unavailable: %s",r instanceof Error?r.message:String(r))}e.registerService({id:"beeos-acp",async start(){const r=Date.now(),o=_pluginRegisteredAt?r-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${o}ms, bridge.url=${t.bridge.url||"(empty)"}`),!t.bridge.url&&t.agentGatewayUrl&&n){const r=Date.now(),o=await discoverBridgeFromGW(t.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-r}ms result=${o?.url??"null"}`),o)t.bridge.url=o.url,writeDiscoveryCache({bridge:{url:o.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${o.url}`);else{const r=readDiscoveryCache();r?.bridge?.url?(t.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await c.start(t.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-r}ms)`)},async stop(){await c.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const{enabled:p,url:u,principalId:f,a2aEnabled:m}=t.messageBus;let y;if(t.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);y=createTokenProvider({agentGatewayUrl:t.agentGatewayUrl,identity:e})}const w=p&&(y||t.messageBus.serviceUrl),b=w?c.getMessageClient():null;if(w&&b){b.on("disconnect",r=>{e.logger.warn?.("[beeos-claw] message bus disconnected (code=%s, reason=%s) — SDK will auto-reconnect",r?.code??"?",r?.reason??"?")}),b.on("error",r=>{e.logger.warn?.("[beeos-claw] message bus error: %s",r instanceof Error?r.message:String(r))});let r=f||process.env.BEEOS_PRINCIPAL_ID||"";const o={serviceUrl:t.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:u,tokenProvider:y,logger:e.logger},s=new Map;let n,i=!1;const p=async()=>{if(r||!y)return r;try{const o=await y("");r=o.identityId||"",r&&e.logger.info?.("[beeos-claw] Message bus principal resolved from Agent Gateway token: %s",r)}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve message bus principal from Agent Gateway token: %s",r instanceof Error?r.message:String(r))}return r},m=()=>{if(r)return[{localAgentId:c.getLocalAgentId(),principalId:r}];const e=c.getPlatformAgentMappings().map(({localAgentId:e,platformAgent:r})=>({localAgentId:e,principalId:r.instanceId||""})).filter(e=>e.principalId);if(e.length>0)return e;const o=c.getDefaultPlatformAgent()?.instanceId||"";return o?[{localAgentId:c.getLocalAgentId(),principalId:o}]:[]},w=e=>({getLocalAgentId:()=>e.localAgentId,getDefaultPlatformAgent:()=>({instanceId:e.principalId}),ensureImInboundSession:(e,r,o)=>{c.ensureImInboundSession(e,r,o)}}),_=async(r,n)=>{if(s.has(r.principalId))return;const i=n?b:new MessageClient(o);n&&c.setMessageClient(o,r.principalId);const p=createImInboundHandler({runtime:e.runtime,pluginConfig:e.pluginConfig,client:i,localServer:w(r),promptRunStore:g,sessionWriteGate:sharedSessionWriteGate,sessionWriteReleaseCoordinator:l,runProcessLifecycleCoordinator:d,logger:e.logger,writeObs:a,principalIdOverride:r.principalId,promptTimeoutMs:t.promptTimeoutMs}),u=new MessageServiceRecoveryScanner({client:i,imInbound:p,logger:e.logger});i.on("connect",()=>{u.trigger("reconnect"),e.logger.info?.("[beeos-claw] MessageClient connected (localAgentId=%s, principalId=%s, via=%s)",r.localAgentId,r.principalId,y?"gateway":"direct")}),i.on("message",r=>{(async()=>{"chat_message"===r.type?await p.onChatMessage(r):"chat_cancel"===r.type&&p.onChatCancel(r)})().catch(r=>{e.logger.warn?.(`[beeos-claw] message handler failed: ${r instanceof Error?r.message:String(r)}`)})}),s.set(r.principalId,{client:i,scanner:u,primary:n});try{await i.connect({identityId:r.principalId}),u.start(r.principalId)}catch(e){throw s.delete(r.principalId),u.stop(),n||await i.disconnect().catch(()=>{}),e}},h=async r=>{if(i)if(n)await n;else{n=(async()=>{const o=m();if(0===o.length)return void e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled");const t=new Set(o.map(e=>e.principalId));let a=!1;for(const[o,n]of s)t.has(o)||(n.primary&&(a=!0),n.scanner.stop(),await n.client.disconnect(),s.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected stale identity principalId=%s reason=%s",o,r));if(a&&s.size>0)for(const[o,t]of s)t.scanner.stop(),await t.client.disconnect(),s.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected for primary rebalance principalId=%s reason=%s",o,r);let n=[...s.values()].some(e=>e.primary);for(const e of o){const r=!n;await _(e,r),r&&(n=!0)}})();try{await n}finally{n=void 0}}};e.registerService({id:"beeos-message-bus",async start(){if(i=!0,c.onAgentMappingUpdated(()=>{h("agent-mapping-updated").catch(r=>{e.logger.error?.("[beeos-claw] Message bus mapping sync failed:",r)})}),await p(),!r)try{await c.waitForAgentMapping()}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}await h("startup")},async stop(){i=!1;const r=[...s.values()];s.clear();for(const e of r)e.scanner.stop(),await e.client.disconnect();e.logger.info?.("[beeos-claw] MessageClient disconnected")}})}else p?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",p);if(!1!==m&&t.agentGatewayUrl){const r=e=>c.getGatewaySessionKeyForToolCall(e),o=e=>c.getMentionsForToolCall(e),a=(e,r)=>{c.broadcastNotification(e,r)},s=new TaskHandleRegistry;registerA2ATools(e,{agentGatewayUrl:t.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},s,r,o,a,()=>c?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(t.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const r=sharedCanvasState;if(t.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=t.agentGatewayUrl,r=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}const a=a=>{const s=new CanvasRelayClient({canvasRelayUrl:t.canvasRelayUrl,agentKeys:agentKeys,agentId:a.id,displayName:a.displayName,avatarUrl:a.avatarUrl,tokenProvider:o,onReconnect:()=>c.onRelayReconnected(),logger:e.logger});s.connect(),r.setRelayClient(s),r.setAgentId(a.id),c.wireRelayActionHandler(),c.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${a.id}, name=${a.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await c.waitForAgentMapping();const r=c.getDefaultPlatformAgent();if(r?.id)return void a(r);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),c.onceAgentMappingReady(()=>{const r=c.getDefaultPlatformAgent();if(r?.id)try{a(r)}catch(r){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${r}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>a(e),isCanvasActive:e=>!!e&&c.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>c.getSessionIdForToolCall(e),getPlatformAgentId:e=>c.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>c.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>c.waitForAgentMapping()},n=new FormSchemaRegistry,i=buildCronSchemas(s,{capMs:t.cronMaxDurationCapMs});n.register(i.create),n.register(i.update);const l=new ResourceDescriptorRegistry;l.register(CRON_RESOURCE);const g=e=>async(r,o,t)=>(c.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),c.setCanvasState(r,{schemaRegistry:n,descriptorRegistry:l});const d=c.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:n,builder:d,logger:e.logger}))});const p=c.getResourceRenderer();if(p){generateActionTools(l,e,async(e,r)=>c.gatewayRequest(e,r),p,e.logger)}const u=`\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\n- ${BEEOS_FILE_RESOURCE_ROUTING_RULE}\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.`,f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),m=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(y.size>=m&&!y.has(r.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(s,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!r?.sessionKey)return{appendSystemContext:u};return c.isCanvasActiveForGatewayKey(r.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&c.setPendingToolCallSessionKey(r.sessionKey),!f.has(e.toolName))return;const o=r?.sessionKey?y.get(r.sessionKey):void 0;return isThirdPartyChannel(s,{sessionKey:r?.sessionKey,channelId:o})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:r?.sessionKey?c.isCanvasActiveForGatewayKey(r.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const _=new TerminalManager,h=createTerminalWebSocketService({config:{port:t.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:t.terminalWsAllowedOrigins},terminalMgr:_,logger:e.logger,writeObs:a});if(t.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){h.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${t.terminalWsPort}`)},stop(){h.stop(),_.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const o=mirrorOpenClawConfig(r.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{if(!hasRuntimeReplyApi(e.runtime))throw e.logger.error?.("[beeos-claw] FATAL: OpenClaw runtime.channel.reply.dispatchReplyFromConfig not exposed. Upgrade OpenClaw or revert beeos-claw."),new Error("openclaw_runtime_reply_api_unavailable");const r=initializePlugin(e);if(!r)return;const o={current:null};installUnhandledRejectionObserver(e,r.writeObs,o),registerUploadTools(e,r,o),registerArouterMediaProviders(e),registerServices(e,r,o),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
|
|
1
|
+
import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{hasRuntimeReplyApi}from"./runtime/reply-router.js";import{PromptRunStore}from"./runtime/prompt-run-store.js";import{SessionWriteGate}from"./acp-gateway/session-write-gate.js";import{OpenClawSessionWriteQuiescence}from"./runtime/openclaw-session-write-quiescence.js";import{SessionWriteGateReleaseCoordinator}from"./runtime/session-write-gate-release-coordinator.js";import{createImInboundHandler}from"./im-inbound-handler.js";import{MessageServiceRecoveryScanner}from"./im-recovery-scanner.js";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{BEEOS_LIST_FILES_TOOL_NAME,BEEOS_LIST_FILES_TOOL_SCHEMA,BEEOS_LIST_FILES_TOOL_DESCRIPTION,executeListFiles}from"./files/list-tool.js";import{BEEOS_FETCH_FILE_TOOL_NAME,BEEOS_FETCH_FILE_TOOL_SCHEMA,BEEOS_FETCH_FILE_TOOL_DESCRIPTION,executeFetchFile}from"./files/fetch-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{BEEOS_FILE_RESOURCE_ROUTING_RULE}from"./files/beeos-file-uri.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";import{registerArouterMediaProviders}from"./media/index.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null;const sharedSessionWriteGate=new SessionWriteGate,sharedPromptRunStore=new PromptRunStore;let sharedSessionWriteReleaseCoordinator,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async r=>{const{createPrivateKey:o,sign:t}=await import("node:crypto"),a=o({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(t(null,Buffer.from(r),a))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,r,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] unhandledRejection:",o),r({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:t,error:{code:"unhandled_rejection",message:t}})}),process.on("uncaughtException",o=>{const t=o instanceof Error?o.message:String(o);e.logger.error?.("[beeos-claw] uncaughtException:",o),r({component:"plugin",domain:"fatal",name:"uncaught_exception",severity:"error",summary:t,error:{code:"uncaught_exception",message:t}})}))}function initializePlugin(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const t=o.config,a=buildThirdPartyChannelSet(r);a.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...a].join(", ")}`);const n=t.log.enabled?createObsEventWriter({dir:t.log.dir,redaction:{gatewayToken:t.gateway.token},logger:e.logger}):()=>{};n({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),n({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:t,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)n({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),t.keyFile)try{agentKeys=loadAgentKeyPair(t.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&t.agentGatewayUrl&&(setUploadDeps({gatewayUrl:t.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:t,writeObs:n,thirdPartyChannels:a,openclawConfig:r}}function registerUploadTools(e,r,o){const{cfg:t,writeObs:a}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,t,agentKeys,n);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),a({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,r.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,r){handleStatusRequest(e,r,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}}),e.registerTool({name:BEEOS_LIST_FILES_TOOL_NAME,description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,parameters:BEEOS_LIST_FILES_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_list_files] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeListFiles(o,t,agentKeys,n);return a({component:"file-list-tool",domain:"tool",name:s.ok?"list_files.success":"list_files.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s}}),e.registerTool({name:BEEOS_FETCH_FILE_TOOL_NAME,description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,parameters:BEEOS_FETCH_FILE_TOOL_SCHEMA,async execute(r,o,n){e.logger.debug?.(`[beeos_fetch_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeFetchFile(o,t,agentKeys,{signal:n,resolvePath:e.resolvePath});return a({component:"file-fetch-tool",domain:"tool",name:s.ok?"fetch_file.success":"fetch_file.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,path:s.path,size:s.size,error:s.error}}),s}})}function registerServices(e,r,o){const{cfg:t,writeObs:a,thirdPartyChannels:n}=r,s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedSessionWriteReleaseCoordinator??=new SessionWriteGateReleaseCoordinator(sharedSessionWriteGate,new OpenClawSessionWriteQuiescence(e.logger,{agentId:t.gateway.agentId||"main"}),e.logger);const l=sharedSessionWriteReleaseCoordinator;sharedAcpServer||(sharedAcpServer=new ACPServer(t,e.logger,a,s,i,n,sharedSessionWriteGate,l));const c=sharedAcpServer;o.current=c;const g=sharedPromptRunStore;e.registerService({id:"beeos-acp",async start(){const r=Date.now(),o=_pluginRegisteredAt?r-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${o}ms, bridge.url=${t.bridge.url||"(empty)"}`),!t.bridge.url&&t.agentGatewayUrl&&s){const r=Date.now(),o=await discoverBridgeFromGW(t.agentGatewayUrl,s);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-r}ms result=${o?.url??"null"}`),o)t.bridge.url=o.url,writeDiscoveryCache({bridge:{url:o.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${o.url}`);else{const r=readDiscoveryCache();r?.bridge?.url?(t.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await c.start(t.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-r}ms)`)},async stop(){await c.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const{enabled:d,url:p,principalId:u,a2aEnabled:f}=t.messageBus;let m;if(t.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);m=createTokenProvider({agentGatewayUrl:t.agentGatewayUrl,identity:e})}const y=d&&(m||t.messageBus.serviceUrl),w=y?c.getMessageClient():null;if(y&&w){w.on("disconnect",r=>{e.logger.warn?.("[beeos-claw] message bus disconnected (code=%s, reason=%s) — SDK will auto-reconnect",r?.code??"?",r?.reason??"?")}),w.on("error",r=>{e.logger.warn?.("[beeos-claw] message bus error: %s",r instanceof Error?r.message:String(r))});let r=u||process.env.BEEOS_PRINCIPAL_ID||"";const o={serviceUrl:t.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:p,tokenProvider:m,logger:e.logger},n=new Map;let s,i=!1;const d=async()=>{if(r||!m)return r;try{const o=await m("");r=o.identityId||"",r&&e.logger.info?.("[beeos-claw] Message bus principal resolved from Agent Gateway token: %s",r)}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve message bus principal from Agent Gateway token: %s",r instanceof Error?r.message:String(r))}return r},f=()=>{if(r)return[{localAgentId:c.getLocalAgentId(),principalId:r}];const e=c.getPlatformAgentMappings().map(({localAgentId:e,platformAgent:r})=>({localAgentId:e,principalId:r.instanceId||""})).filter(e=>e.principalId);if(e.length>0)return e;const o=c.getDefaultPlatformAgent()?.instanceId||"";return o?[{localAgentId:c.getLocalAgentId(),principalId:o}]:[]},y=e=>({getLocalAgentId:()=>e.localAgentId,getDefaultPlatformAgent:()=>({instanceId:e.principalId}),ensureImInboundSession:(e,r,o)=>{c.ensureImInboundSession(e,r,o)}}),_=async(r,s)=>{if(n.has(r.principalId))return;const i=s?w:new MessageClient(o);s&&c.setMessageClient(o,r.principalId);const d=createImInboundHandler({runtime:e.runtime,pluginConfig:e.pluginConfig,client:i,localServer:y(r),promptRunStore:g,sessionWriteGate:sharedSessionWriteGate,sessionWriteReleaseCoordinator:l,logger:e.logger,writeObs:a,principalIdOverride:r.principalId,promptTimeoutMs:t.promptTimeoutMs}),p=new MessageServiceRecoveryScanner({client:i,imInbound:d,logger:e.logger});i.on("connect",()=>{p.trigger("reconnect"),e.logger.info?.("[beeos-claw] MessageClient connected (localAgentId=%s, principalId=%s, via=%s)",r.localAgentId,r.principalId,m?"gateway":"direct")}),i.on("message",r=>{(async()=>{"chat_message"===r.type?await d.onChatMessage(r):"chat_cancel"===r.type&&d.onChatCancel(r)})().catch(r=>{e.logger.warn?.(`[beeos-claw] message handler failed: ${r instanceof Error?r.message:String(r)}`)})}),n.set(r.principalId,{client:i,scanner:p,primary:s});try{await i.connect({identityId:r.principalId}),p.start(r.principalId)}catch(e){throw n.delete(r.principalId),p.stop(),s||await i.disconnect().catch(()=>{}),e}},b=async r=>{if(i)if(s)await s;else{s=(async()=>{const o=f();if(0===o.length)return void e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled");const t=new Set(o.map(e=>e.principalId));let a=!1;for(const[o,s]of n)t.has(o)||(s.primary&&(a=!0),s.scanner.stop(),await s.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected stale identity principalId=%s reason=%s",o,r));if(a&&n.size>0)for(const[o,t]of n)t.scanner.stop(),await t.client.disconnect(),n.delete(o),e.logger.info?.("[beeos-claw] MessageClient disconnected for primary rebalance principalId=%s reason=%s",o,r);let s=[...n.values()].some(e=>e.primary);for(const e of o){const r=!s;await _(e,r),r&&(s=!0)}})();try{await s}finally{s=void 0}}};e.registerService({id:"beeos-message-bus",async start(){if(i=!0,c.onAgentMappingUpdated(()=>{b("agent-mapping-updated").catch(r=>{e.logger.error?.("[beeos-claw] Message bus mapping sync failed:",r)})}),await d(),!r)try{await c.waitForAgentMapping()}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}await b("startup")},async stop(){i=!1;const r=[...n.values()];n.clear();for(const e of r)e.scanner.stop(),await e.client.disconnect();e.logger.info?.("[beeos-claw] MessageClient disconnected")}})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==f&&t.agentGatewayUrl){const r=e=>c.getGatewaySessionKeyForToolCall(e),o=e=>c.getMentionsForToolCall(e),a=(e,r)=>{c.broadcastNotification(e,r)},n=new TaskHandleRegistry;registerA2ATools(e,{agentGatewayUrl:t.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},n,r,o,a,()=>c?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(t.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const r=sharedCanvasState;if(t.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=t.agentGatewayUrl,r=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}const a=a=>{const n=new CanvasRelayClient({canvasRelayUrl:t.canvasRelayUrl,agentKeys:agentKeys,agentId:a.id,displayName:a.displayName,avatarUrl:a.avatarUrl,tokenProvider:o,onReconnect:()=>c.onRelayReconnected(),logger:e.logger});n.connect(),r.setRelayClient(n),r.setAgentId(a.id),c.wireRelayActionHandler(),c.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${a.id}, name=${a.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await c.waitForAgentMapping();const r=c.getDefaultPlatformAgent();if(r?.id)return void a(r);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),c.onceAgentMappingReady(()=>{const r=c.getDefaultPlatformAgent();if(r?.id)try{a(r)}catch(r){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${r}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>a(e),isCanvasActive:e=>!!e&&c.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>c.getSessionIdForToolCall(e),getPlatformAgentId:e=>c.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>c.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>c.waitForAgentMapping()},s=new FormSchemaRegistry,i=buildCronSchemas(n,{capMs:t.cronMaxDurationCapMs});s.register(i.create),s.register(i.update);const l=new ResourceDescriptorRegistry;l.register(CRON_RESOURCE);const g=e=>async(r,o,t)=>(c.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),c.setCanvasState(r,{schemaRegistry:s,descriptorRegistry:l});const d=c.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(s),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:s,builder:d,logger:e.logger}))});const p=c.getResourceRenderer();if(p){generateActionTools(l,e,async(e,r)=>c.gatewayRequest(e,r),p,e.logger)}const u=`\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\n- ${BEEOS_FILE_RESOURCE_ROUTING_RULE}\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.`,f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),m=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(y.size>=m&&!y.has(r.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!r?.sessionKey)return{appendSystemContext:u};return c.isCanvasActiveForGatewayKey(r.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&c.setPendingToolCallSessionKey(r.sessionKey),!f.has(e.toolName))return;const o=r?.sessionKey?y.get(r.sessionKey):void 0;return isThirdPartyChannel(n,{sessionKey:r?.sessionKey,channelId:o})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:r?.sessionKey?c.isCanvasActiveForGatewayKey(r.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const _=new TerminalManager,b=createTerminalWebSocketService({config:{port:t.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:t.terminalWsAllowedOrigins},terminalMgr:_,logger:e.logger,writeObs:a});if(t.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${t.terminalWsPort}`)},stop(){b.stop(),_.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const o=mirrorOpenClawConfig(r.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{if(!hasRuntimeReplyApi(e.runtime))throw e.logger.error?.("[beeos-claw] FATAL: OpenClaw runtime.channel.reply.dispatchReplyFromConfig not exposed. Upgrade OpenClaw or revert beeos-claw."),new Error("openclaw_runtime_reply_api_unavailable");const r=initializePlugin(e);if(!r)return;const o={current:null};installUnhandledRejectionObserver(e,r.writeObs,o),registerUploadTools(e,r,o),registerArouterMediaProviders(e),registerServices(e,r,o),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const n=t.slice(e.text.length);return e.text=t,{kind:"append",text:n}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let n,r="",o="";const s=new Map,i=new Map,a=[],l=[];let d=0,u=0;const m=(t,n,r)=>{const o=u;return e.stream.appendToolUse(t,n,r,"streaming"),u+=1,o},g=()=>(d+=1,`tool_${e.requestId}_${d}`),c=e=>{const t=a.indexOf(e);t>=0&&a.splice(t,1)},p=e=>{const t=e?a.filter(t=>s.get(t)?.name===e):a;return 1===t.length?t[0]:void 0},h=e=>{const t=l.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:n,index:r}]=t;return l.splice(r,1),n},f=(e,t)=>{i.set(e,t)},y=e=>i.get(e)??e,I=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),v=(t,n,r,o)=>{const i=s.get(t);if(i){const s="tool"===i.name&&"tool"!==n?n:i.name,l=!I(i.arguments)&&I(r)?r:i.arguments;return s===i.name&&l===i.arguments||(e.stream.replacePart(i.partIndex,{type:"tool_use",id:i.id,name:s,arguments:l,state:i.state}),i.name=s,i.arguments=l),o&&!a.includes(t)&&"streaming"===i.state&&a.push(t),i}const l={id:t,name:n,arguments:r,partIndex:m(t,n,r),state:"streaming",resultSignatures:new Set,itemEventObserved:!1,toolStartObserved:!1};return s.set(t,l),o&&a.push(t),l},$=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,{type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done"}),t.state="done")},x=(t,n,r)=>{const o=`${r?"1":"0"}:${"string"==typeof n?n:JSON.stringify(n)}`;t.resultSignatures.has(o)||(((t,n,r)=>{e.stream.appendToolResult(t,n,r),u+=1})(t.id,n,r),t.resultSignatures.add(o))},b=(e,t,n)=>{const r=i.get(e);if(r)return v(r,t??"tool",n??{},!1);if(s.has(e))return v(e,t??"tool",n??{},!1);const o=p(t);return o?(f(e,o),v(o,t??"tool",n??{},!1)):v(e,t??"tool",n??{},!1)},
|
|
1
|
+
import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const n=t.slice(e.text.length);return e.text=t,{kind:"append",text:n}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let n,r="",o="";const s=new Map,i=new Map,a=[],l=[];let d=0,u=0;const m=(t,n,r)=>{const o=u;return e.stream.appendToolUse(t,n,r,"streaming"),u+=1,o},g=()=>(d+=1,`tool_${e.requestId}_${d}`),c=e=>{const t=a.indexOf(e);t>=0&&a.splice(t,1)},p=e=>{const t=e?a.filter(t=>s.get(t)?.name===e):a;return 1===t.length?t[0]:void 0},h=e=>{const t=l.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:n,index:r}]=t;return l.splice(r,1),n},f=(e,t)=>{i.set(e,t)},y=e=>i.get(e)??e,I=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),v=(t,n,r,o)=>{const i=s.get(t);if(i){const s="tool"===i.name&&"tool"!==n?n:i.name,l=!I(i.arguments)&&I(r)?r:i.arguments;return s===i.name&&l===i.arguments||(e.stream.replacePart(i.partIndex,{type:"tool_use",id:i.id,name:s,arguments:l,state:i.state}),i.name=s,i.arguments=l),o&&!a.includes(t)&&"streaming"===i.state&&a.push(t),i}const l={id:t,name:n,arguments:r,partIndex:m(t,n,r),state:"streaming",resultSignatures:new Set,itemEventObserved:!1,toolStartObserved:!1};return s.set(t,l),o&&a.push(t),l},$=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,{type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done"}),t.state="done")},x=(t,n,r)=>{const o=`${r?"1":"0"}:${"string"==typeof n?n:JSON.stringify(n)}`;t.resultSignatures.has(o)||(((t,n,r)=>{e.stream.appendToolResult(t,n,r),u+=1})(t.id,n,r),t.resultSignatures.add(o))},b=(e,t,n)=>{const r=i.get(e);if(r)return v(r,t??"tool",n??{},!1);if(s.has(e))return v(e,t??"tool",n??{},!1);const o=p(t);return o?(f(e,o),v(o,t??"tool",n??{},!1)):v(e,t??"tool",n??{},!1)},k=(e,t)=>{const n=i.get(e);if(n)return v(n,t,{},!1);if(s.has(e))return v(e,t,{},!1);const r=(e=>{const t=a.filter(t=>{const n=s.get(t);return!(void 0===n||e&&n.name!==e||n.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(r)return f(e,r),v(r,t,{},!1);const o=h(t);return v(e,t,o?.arguments??{},!1)},S={tool:0,block:0,final:0};let _=!1;const R=(t,n)=>{e.callbackQueue.enqueueSync(()=>{if((t=>e.abortSignal.aborted?(e.logger.debug?.(`[im-sink] ${t} ignored after abort requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):e.stream.isTerminated?(e.logger.debug?.(`[im-sink] ${t} ignored after stream_terminated requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):!_||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{n()}catch(n){const r=n instanceof Error?n.message:String(n);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${r}`)}})};return{dispatcher:{sendToolResult:t=>(R("tool_result",()=>{const n=resolveRuntimeToolCallId(t),r=resolveRuntimeToolName(t),o=n?void 0:r?p(r):p(),i=n||o?void 0:h(r);if(!n&&!o&&!i)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool result ignored requestId=${e.requestId} channel=${e.channelId} tool=${r??"unknown"} pending=${a.length}`);const l=n?b(n,r,resolveRuntimeToolArguments(t)):v(o??g(),r??s.get(o??"")?.name??i.name,I(resolveRuntimeToolArguments(t))?resolveRuntimeToolArguments(t):i?.arguments??{},!1);S.tool+=1,$(l),x(l,(e=>{const t=[e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e));return isNonEmptyString(e.text)&&t.length>0?{text:e.text,media_urls:t}:isNonEmptyString(e.text)?e.text:1===t.length?t[0]:t.length>1?{media_urls:t}:!0===e.isError?"Tool execution failed.":""})(t),!0===t.isError),c(l.id)}),!0),sendBlockReply:n=>(R("block_reply",()=>{S.block+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),sendFinalReply:n=>(R("final_reply",()=>{S.final+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),waitForIdle:()=>e.callbackQueue.waitForIdle(),getQueuedCounts:()=>({...S}),markComplete:t=>{if(1===l.length){const e=l.shift();v(g(),e.name,e.arguments,!1)}for(const n of s.values())try{"success"===t&&$(n),c(n.id)}catch(t){const r=t instanceof Error?t.message:String(t);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${n.id} error=${r}`)}l.length=0,_=!0}},replyOptions:{abortSignal:e.abortSignal,onAgentRunStart:t=>{e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${t} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:n=>{R("partial",()=>{const r=extractBodyUpdate(t,n.text);switch(r.kind){case"append":e.stream.appendBody(r.text);break;case"replace":e.stream.setBody(r.text)}})},onReasoningStream:t=>{R("reasoning",()=>{if(!isNonEmptyString(t.text))return;if(!n&&(n=e.openReasoningStream?.(),!n))return;const s=t.text;""===o&&""!==r&&(n.appendBody("\n"),r+="\n"),s!==o&&(s.startsWith(o)?(n.appendBody(s.slice(o.length)),o=s):o.startsWith(s)||(n.setBody(r+s),o=s))})},onReasoningEnd:()=>{R("reasoning_end",()=>{var t;""!==o&&(t=o,e.stream.appendThinking(t,"done"),u+=1,r+=o,o="")})},onAssistantMessageStart:()=>{e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)},onToolStart:t=>{R("tool_start",()=>{const n="string"==typeof t.phase?t.phase:"start",r=resolveRuntimeToolName(t)??"tool",o=resolveRuntimeToolCallId(t);if("update"===n){if(o){const e=y(o);s.has(e)&&v(e,r,resolveRuntimeToolArguments(t),!0)}return}const i=resolveRuntimeToolArguments(t);if(o){const e=b(o,r,i);return e.toolStartObserved=!0,void(a.includes(e.id)||"streaming"!==e.state||a.push(e.id))}const d=(u=r,a.map(e=>s.get(e)).filter(e=>void 0!==e&&(!u||e.name===u)&&e.itemEventObserved));var u;if(1===d.length){return void(v(d[0].id,r,i,!0).toolStartObserved=!0)}d.length>1?e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${r} candidates=${d.length}`):l.push({name:r,arguments:i})})},onItemEvent:e=>{R("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const n="string"==typeof e.phase?e.phase.toLowerCase():"update",r=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),o=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);if(!r||!o)return;if("update"===n){const e=y(r),t=s.get(e);return void(t&&(v(t.id,o,{},!0).itemEventObserved=!0))}const i=k(r,o);if(i.itemEventObserved=!0,a.includes(i.id)||"streaming"!==i.state||a.push(i.id),"end"!==n)return;const l="string"==typeof e.status?e.status.toLowerCase():"",d="failed"===l||"error"===l||"cancelled"===l,u=isNonEmptyString(e.summary)?e.summary:isNonEmptyString(e.meta)?e.meta:"";$(i),u&&x(i,u,d),c(i.id)})}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const RUN_KEY_SEPARATOR="\0";export const promptRunKey=(e,n)=>`${e}\0${n}`;export class PromptRunStore{byRunKey=new Map;size(){return this.byRunKey.size}register(e,n){if(this.byRunKey.has(e)){try{n.abortController.abort("duplicate_inbound_message_id")}catch{}return!1}return n.phase="running",this.byRunKey.set(e,n),!0}
|
|
1
|
+
const RUN_KEY_SEPARATOR="\0";export const promptRunKey=(e,n)=>`${e}\0${n}`;export class PromptRunStore{byRunKey=new Map;size(){return this.byRunKey.size}register(e,n){if(this.byRunKey.has(e)){try{n.abortController.abort("duplicate_inbound_message_id")}catch{}return!1}return n.phase="running",this.byRunKey.set(e,n),!0}claimTerminal(e){const n=this.byRunKey.get(e);return!(!n||"running"!==n.phase)&&(n.phase="terminal_committing",!0)}abort(e,n){const t=this.byRunKey.get(e);if(!t)return{ok:!1,reason:"not_found"};if("terminal_committing"===t.phase)return{ok:!1,reason:"terminal_committing",requestId:t.requestId,channelId:t.channelId,...t.sessionKey?{sessionKey:t.sessionKey}:{}};if(t.abortController.signal.aborted)return{ok:!1,reason:"already_aborted",requestId:t.requestId,channelId:t.channelId,...t.sessionKey?{sessionKey:t.sessionKey}:{}};try{t.abortController.abort(n)}catch{}return{ok:!0,requestId:t.requestId,channelId:t.channelId,...t.sessionKey?{sessionKey:t.sessionKey}:{}}}abortByInboundMessageId(e,n){if(!e)return{ok:!1,reason:"not_found"};const t=`\0${e}`,r=Array.from(this.byRunKey.keys()).filter(n=>n===e||n.endsWith(t));return 0===r.length?{ok:!1,reason:"not_found"}:r.length>1?{ok:!1,reason:"ambiguous"}:this.abort(r[0],n)}unregister(e){this.byRunKey.delete(e)}snapshot(){return Array.from(this.byRunKey.entries(),([e,n])=>[e,{...n}])}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect,beforeEach}from"vitest";import{PromptRunStore,promptRunKey}from"./prompt-run-store.js";const makeEntry=(e={})=>({requestId:"im_req_test_001",channelId:"ch_test_001",abortController:new AbortController,startedAt:Date.now(),...e});describe("PromptRunStore",()=>{let e;beforeEach(()=>{e=new PromptRunStore}),it("starts empty",()=>{expect(e.size()).toBe(0),expect(e.snapshot()).toEqual([])}),it("register adds an entry and reports size correctly",()=>{const t=makeEntry();expect(e.register("msg_001",t)).toBe(!0),expect(e.size()).toBe(1),expect(e.snapshot()).toEqual([["msg_001",t]])}),it("register rejects a duplicate inboundMessageId and aborts the duplicate's controller",()=>{const t=makeEntry({requestId:"req_1"}),r=makeEntry({requestId:"req_2"});expect(e.register("msg_dup",t)).toBe(!0),expect(e.register("msg_dup",r)).toBe(!1),expect(e.snapshot()).toEqual([["msg_dup",t]]),expect(r.abortController.signal.aborted).toBe(!0),expect(r.abortController.signal.reason).toBe("duplicate_inbound_message_id"),expect(t.abortController.signal.aborted).toBe(!1)}),it("abort triggers the AbortSignal and returns ok=true with metadata",()=>{const t=makeEntry({requestId:"req_abort_ok",channelId:"ch_abort_ok"});e.register("msg_abort_ok",t);const r=e.abort("msg_abort_ok","chat_cancel:user_request");expect(r).toEqual({ok:!0,requestId:"req_abort_ok",channelId:"ch_abort_ok"}),expect(t.abortController.signal.aborted).toBe(!0),expect(t.abortController.signal.reason).toBe("chat_cancel:user_request")}),it("rejects cancellation after the terminal writer wins the CAS",()=>{const t=makeEntry({requestId:"req_terminal"});e.register("msg_terminal",t),expect(e.claimTerminal("msg_terminal")).toBe(!0),expect(e.claimTerminal("msg_terminal")).toBe(!1),expect(e.abort("msg_terminal","chat_cancel:too_late")).toMatchObject({ok:!1,reason:"terminal_committing",requestId:"req_terminal"}),expect(t.abortController.signal.aborted).toBe(!1)}),it("
|
|
1
|
+
import{describe,it,expect,beforeEach}from"vitest";import{PromptRunStore,promptRunKey}from"./prompt-run-store.js";const makeEntry=(e={})=>({requestId:"im_req_test_001",channelId:"ch_test_001",abortController:new AbortController,startedAt:Date.now(),...e});describe("PromptRunStore",()=>{let e;beforeEach(()=>{e=new PromptRunStore}),it("starts empty",()=>{expect(e.size()).toBe(0),expect(e.snapshot()).toEqual([])}),it("register adds an entry and reports size correctly",()=>{const t=makeEntry();expect(e.register("msg_001",t)).toBe(!0),expect(e.size()).toBe(1),expect(e.snapshot()).toEqual([["msg_001",t]])}),it("register rejects a duplicate inboundMessageId and aborts the duplicate's controller",()=>{const t=makeEntry({requestId:"req_1"}),r=makeEntry({requestId:"req_2"});expect(e.register("msg_dup",t)).toBe(!0),expect(e.register("msg_dup",r)).toBe(!1),expect(e.snapshot()).toEqual([["msg_dup",t]]),expect(r.abortController.signal.aborted).toBe(!0),expect(r.abortController.signal.reason).toBe("duplicate_inbound_message_id"),expect(t.abortController.signal.aborted).toBe(!1)}),it("abort triggers the AbortSignal and returns ok=true with metadata",()=>{const t=makeEntry({requestId:"req_abort_ok",channelId:"ch_abort_ok"});e.register("msg_abort_ok",t);const r=e.abort("msg_abort_ok","chat_cancel:user_request");expect(r).toEqual({ok:!0,requestId:"req_abort_ok",channelId:"ch_abort_ok"}),expect(t.abortController.signal.aborted).toBe(!0),expect(t.abortController.signal.reason).toBe("chat_cancel:user_request")}),it("rejects cancellation after the terminal writer wins the CAS",()=>{const t=makeEntry({requestId:"req_terminal"});e.register("msg_terminal",t),expect(e.claimTerminal("msg_terminal")).toBe(!0),expect(e.claimTerminal("msg_terminal")).toBe(!1),expect(e.abort("msg_terminal","chat_cancel:too_late")).toMatchObject({ok:!1,reason:"terminal_committing",requestId:"req_terminal"}),expect(t.abortController.signal.aborted).toBe(!1)}),it("abort on unknown inboundMessageId returns not_found and is a no-op",()=>{const t=e.abort("msg_unknown","whatever");expect(t).toEqual({ok:!1,reason:"not_found"})}),it("abortByInboundMessageId aborts the only matching composite run key",()=>{const t=makeEntry({requestId:"req_fallback",channelId:"ch_fallback"});e.register(promptRunKey("ch_fallback","msg_fallback"),t);const r=e.abortByInboundMessageId("msg_fallback","chat_cancel:legacy");expect(r).toEqual({ok:!0,requestId:"req_fallback",channelId:"ch_fallback"}),expect(t.abortController.signal.aborted).toBe(!0),expect(t.abortController.signal.reason).toBe("chat_cancel:legacy")}),it("abortByInboundMessageId refuses ambiguous matches",()=>{const t=makeEntry({requestId:"req_a",channelId:"ch_a"}),r=makeEntry({requestId:"req_b",channelId:"ch_b"});e.register(promptRunKey("ch_a","msg_same"),t),e.register(promptRunKey("ch_b","msg_same"),r);const o=e.abortByInboundMessageId("msg_same","chat_cancel:legacy");expect(o).toEqual({ok:!1,reason:"ambiguous"}),expect(t.abortController.signal.aborted).toBe(!1),expect(r.abortController.signal.aborted).toBe(!1)}),it("abort on an already-aborted entry returns already_aborted",()=>{const t=makeEntry({requestId:"req_2x"});t.abortController.abort("first_call"),e.register("msg_2x",t);const r=e.abort("msg_2x","second_call");expect(r).toEqual({ok:!1,reason:"already_aborted",requestId:"req_2x",channelId:t.channelId}),expect(t.abortController.signal.reason).toBe("first_call")}),it("unregister removes the entry and is safe to call twice",()=>{const t=makeEntry();e.register("msg_drop",t),expect(e.size()).toBe(1),e.unregister("msg_drop"),expect(e.size()).toBe(0),e.unregister("msg_drop"),expect(e.size()).toBe(0)}),it("unregister of unknown id is a silent no-op",()=>{expect(()=>e.unregister("msg_ghost")).not.toThrow(),expect(e.size()).toBe(0)}),it("supports multiple concurrent entries with independent abort",()=>{const t=makeEntry({requestId:"req_a",channelId:"ch_a"}),r=makeEntry({requestId:"req_b",channelId:"ch_b"}),o=makeEntry({requestId:"req_c",channelId:"ch_c"});e.register("msg_a",t),e.register("msg_b",r),e.register("msg_c",o),expect(e.size()).toBe(3),expect(e.abort("msg_b","cancel_b").ok).toBe(!0),expect(t.abortController.signal.aborted).toBe(!1),expect(r.abortController.signal.aborted).toBe(!0),expect(o.abortController.signal.aborted).toBe(!1),e.unregister("msg_b"),expect(e.size()).toBe(2),expect(e.snapshot().map(([e])=>e).sort()).toEqual(["msg_a","msg_c"])}),it("snapshot returns a defensive array copy (mutation does not affect store)",()=>{e.register("msg_snap",makeEntry());const t=e.snapshot();t[0][1].phase="terminal_committing",t.length=0,expect(e.size()).toBe(1),expect(e.abort("msg_snap","still_running").ok).toBe(!0)})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.55",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{coordinateOpenClawAfterToolCall}from"./openclaw-process-ownership-hook.js";export class OpenClawProcessOwnershipHookRegistrar{registered=new WeakSet;runIdBySessionKey=new Map;maxTrackedSubagentSessions=4096;register(e,s,n,r){this.registered.has(e)||(e.on("before_tool_call",(...e)=>{const n=e[0],r=e[1],o=n?.runId??r?.runId,t=n?.toolName;o&&t&&s.beforeToolCall({runId:o,toolCallId:n?.toolCallId??r?.toolCallId,toolName:t,params:n?.params??{}})}),e.on("after_tool_call",(...e)=>{const r=e[0],o=e[1];if(!r)return;const t=r.runId??o?.runId??"";return coordinateOpenClawAfterToolCall(r,o??{},s).then(e=>e.termination&&!e.termination.ok?s.terminateRunWithRetry(t).then(s=>(s.terminations.some(e=>!e.ok)&&n.warn?.("[beeos-claw] late owned process termination unverified runId=%s sessionId=%s",t,e.termination.process.sessionId),e)):e).catch(e=>{n.warn?.("[beeos-claw] process ownership hook failed runId=%s error=%s",t,e instanceof Error?e.message:String(e))})}),e.on("subagent_spawned",(...e)=>{const o=e[0],t=e[1],i=o?.runId,a=t?.requesterSessionKey,l=a?this.runIdBySessionKey.get(a)??r?.(a):void 0;if(l&&i){if(o?.childSessionKey){if(this.runIdBySessionKey.size>=this.maxTrackedSubagentSessions&&!this.runIdBySessionKey.has(o.childSessionKey)){const e=this.runIdBySessionKey.keys().next().value;e&&this.runIdBySessionKey.delete(e)}this.runIdBySessionKey.set(o.childSessionKey,i)}return s.linkChildRun(l,i).then(async e=>(e.terminations.some(e=>!e.ok)&&await s.terminateRunWithRetry(i),e)).catch(e=>{n.warn?.("[beeos-claw] subagent ownership link failed parentRunId=%s childRunId=%s error=%s",l,i,e instanceof Error?e.message:String(e))})}}),e.on("subagent_ended",(...e)=>{const r=e[0];if(r?.runId)return r.targetSessionKey&&this.runIdBySessionKey.delete(r.targetSessionKey),s.terminateRunWithRetry(r.runId).then(e=>{const s=e.terminations.filter(e=>!e.ok);s.length>0&&n.warn?.("[beeos-claw] subagent terminal process cleanup unverified runId=%s failureCount=%s",r.runId,s.length)}).catch(e=>{n.warn?.("[beeos-claw] subagent terminal process cleanup failed runId=%s error=%s",r.runId,e instanceof Error?e.message:String(e))})}),this.registered.add(e),n.debug?.("[beeos-claw] Process ownership hooks registered: before_tool_call, after_tool_call"))}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{OpenClawProcessOwnershipHookRegistrar}from"./openclaw-process-ownership-hook-registrar.js";import{ProcessSessionTerminator}from"./process-session-terminator.js";import{RunProcessLifecycleCoordinator}from"./run-process-lifecycle-coordinator.js";import{RunProcessOwnershipRegistry}from"./run-process-ownership-registry.js";describe("OpenClawProcessOwnershipHookRegistrar",()=>{it("registers independently for discovery and full runtime registries",()=>{const e=new OpenClawProcessOwnershipHookRegistrar,s=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:async()=>({ok:!1})})),r={on:vi.fn()},n={on:vi.fn()};e.register(r,s,{}),e.register(n,s,{}),e.register(n,s,{}),expect(r.on.mock.calls.map(([e])=>e)).toEqual(["before_tool_call","after_tool_call","subagent_spawned","subagent_ended"]),expect(n.on.mock.calls.map(([e])=>e)).toEqual(["before_tool_call","after_tool_call","subagent_spawned","subagent_ended"])}),it("links spawned child runs so parent termination reaches child processes",async()=>{const e=new Map,s={on:vi.fn((s,r)=>{e.set(s,r)})},r=vi.fn(async e=>({ok:!0,toolName:"process",output:{details:{status:"completed",sessionId:e.args.sessionId}}})),n=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:r}));(new OpenClawProcessOwnershipHookRegistrar).register(s,n,{},e=>"agent:main:parent"===e?"parent-run":void 0),e.get("subagent_spawned")?.({runId:"child-run",childSessionKey:"agent:worker:child"},{requesterSessionKey:"agent:main:parent"}),e.get("subagent_spawned")?.({runId:"grandchild-run",childSessionKey:"agent:worker:grandchild"},{requesterSessionKey:"agent:worker:child"}),await Promise.resolve(),await(e.get("after_tool_call")?.({toolName:"exec",params:{command:"sleep 90"},runId:"grandchild-run",result:{details:{status:"running",sessionId:"child-process"}}},{runId:"grandchild-run",sessionKey:"agent:worker:grandchild"})),await Promise.resolve(),await n.terminateRun("parent-run"),expect(r).toHaveBeenCalledWith({name:"process",args:{action:"kill",sessionId:"child-process"},sessionKey:"agent:worker:grandchild"})}),it("reports a subagent cleanup whose termination could not be verified",async()=>{const e=new Map,s={on:vi.fn((s,r)=>{e.set(s,r)})},r=vi.fn(),n=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:async e=>"kill"===e.args.action?{ok:!0,output:{}}:{ok:!0,output:{details:{status:"running",sessionId:"proc-child"}}}},{verificationAttempts:1}));(new OpenClawProcessOwnershipHookRegistrar).register(s,n,{warn:r}),await n.afterToolCall({runId:"child-run",sessionKey:"agent:worker:child",toolName:"exec",params:{command:"sleep 90"},result:{details:{status:"running",sessionId:"proc-child"}}}),e.get("subagent_ended")?.({runId:"child-run"}),await vi.waitFor(()=>expect(r).toHaveBeenCalledWith("[beeos-claw] subagent terminal process cleanup unverified runId=%s failureCount=%s","child-run",1))})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function coordinateOpenClawAfterToolCall(o,l,a){return a.afterToolCall({runId:o.runId??l.runId??"",toolCallId:o.toolCallId??l.toolCallId,sessionKey:l.sessionKey,toolName:o.toolName??l.toolName??"",params:o.params??{},result:o.result})}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{ProcessSessionTerminator}from"./process-session-terminator.js";import{RunProcessLifecycleCoordinator}from"./run-process-lifecycle-coordinator.js";import{RunProcessOwnershipRegistry}from"./run-process-ownership-registry.js";import{coordinateOpenClawAfterToolCall}from"./openclaw-process-ownership-hook.js";const runningResult={details:{status:"running",sessionId:"process-hook"}},successfulToolEnvelope=e=>({ok:!0,toolName:"process",output:{details:{status:"completed",sessionId:e?.args?.sessionId}}});describe("coordinateOpenClawAfterToolCall",()=>{it("uses the event run id and kills a late process after cancellation",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),o=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:e}));await o.terminateRun("event-run");const s=await coordinateOpenClawAfterToolCall({toolName:"exec",params:{command:"sleep 90"},runId:"event-run",result:runningResult},{runId:"context-run",sessionKey:"agent:main:conversation:one"},o);expect(s.ownership.kind).toBe("terminate"),expect(e).toHaveBeenCalledWith({name:"process",args:{action:"kill",sessionId:"process-hook"},sessionKey:"agent:main:conversation:one"})}),it("falls back to the hook context run id",async()=>{const e=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:async e=>successfulToolEnvelope(e)}));await e.terminateRun("context-run");const o=await coordinateOpenClawAfterToolCall({toolName:"exec",params:{},result:runningResult},{runId:"context-run",sessionKey:"agent:worker:conversation:two"},e);expect(o.ownership.kind).toBe("terminate")})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class ProcessSessionTerminator{tools;verificationAttempts;verificationDelayMs;sleep;killAcknowledgements=new Map;maxAcknowledgements=4096;constructor(e,s={}){this.tools=e,this.verificationAttempts=s.verificationAttempts??5,this.verificationDelayMs=s.verificationDelayMs??100,this.sleep=s.sleep??(e=>new Promise(s=>setTimeout(s,e)))}async terminate(e){const s=processKey(e);let t=this.killAcknowledgements.get(s);if(!t){const o=await this.tools.invoke({name:"process",args:{action:"kill",sessionId:e.sessionId},...e.sessionKey?{sessionKey:e.sessionKey}:{}});if(this.isNotFoundEnvelope(o,e.sessionId))return o;const i=this.requireEnvelope(o);if(!this.isKillAcknowledgement(i.output,e.sessionId))throw new Error(`process ${e.sessionId} kill was not acknowledged`);for(t=i,this.killAcknowledgements.delete(s),this.killAcknowledgements.set(s,t);this.killAcknowledgements.size>this.maxAcknowledgements;){const e=this.killAcknowledgements.keys().next().value;if(!e)break;this.killAcknowledgements.delete(e)}}for(let o=0;o<this.verificationAttempts;o+=1){const i=await this.tools.invoke({name:"process",args:{action:"poll",sessionId:e.sessionId,timeout:0},...e.sessionKey?{sessionKey:e.sessionKey}:{}});if(this.isNotFoundEnvelope(i,e.sessionId))return this.killAcknowledgements.delete(s),t;const n=this.requireEnvelope(i),r=this.outputDetails(n.output);if(r?.sessionId===e.sessionId&&("completed"===r.status||"failed"===r.status))return this.killAcknowledgements.delete(s),t;o+1<this.verificationAttempts&&await this.sleep(this.verificationDelayMs)}throw new Error(`process ${e.sessionId} termination could not be verified`)}isKillAcknowledgement(e,s){const t=this.outputDetails(e);if(t?.sessionId===s)return!0;const o=this.outputText(e),i=escapeRegex(s);return new RegExp(`^(?:Termination requested for session|Killed session) ${i}\\.?$`,"i").test(o.trim())}isNotFoundEnvelope(e,s){if(!e||"object"!=typeof e)return!1;const t=e,o=escapeRegex(s);if(!1===t.ok){const e=JSON.stringify(t.error??"");return new RegExp(`(?:not found|no (?:such )?(?:process )?session|unknown session)[^\\n]*${o}`,"i").test(e)}if(!0!==t.ok)return!1;const i=t.output;return new RegExp(`^No session found for ${o}\\.?$`,"i").test(this.outputText(i).trim())}outputText(e){if(!e||"object"!=typeof e)return"";const s=e.content;return Array.isArray(s)?s.map(e=>e&&"object"==typeof e&&"string"==typeof e.text?String(e.text):"").filter(Boolean).join("\n"):""}requireEnvelope(e){if(!e||"object"!=typeof e||"boolean"!=typeof e.ok)throw new Error("invalid tools.invoke response envelope");const s=e;if(!s.ok)throw new Error(`process invocation refused: ${JSON.stringify(s.error??{})}`);return s}outputDetails(e){if(!e||"object"!=typeof e)return;const s=e.details;if(!s||"object"!=typeof s)return;const t=s;return{..."string"==typeof t.status?{status:t.status}:{},..."string"==typeof t.sessionId?{sessionId:t.sessionId}:{}}}}const processKey=e=>JSON.stringify([e.sessionKey??null,e.sessionId]),escapeRegex=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{ProcessSessionTerminator}from"./process-session-terminator.js";describe("ProcessSessionTerminator",()=>{it("uses tools.invoke to precisely kill the scoped process session",async()=>{const e={ok:!0,toolName:"process",output:{details:{status:"failed",sessionId:"proc-1"}}},s=vi.fn(async s=>"kill"===s.args.action?e:{ok:!0,toolName:"process",output:{details:{status:"completed",sessionId:"proc-1"}}}),o=new ProcessSessionTerminator({invoke:s});await expect(o.terminate({sessionId:"proc-1",sessionKey:"scope-1"})).resolves.toEqual(e),expect(s).toHaveBeenCalledWith({name:"process",args:{action:"kill",sessionId:"proc-1"},sessionKey:"scope-1"}),expect(s).toHaveBeenCalledWith({name:"process",args:{action:"poll",sessionId:"proc-1",timeout:0},sessionKey:"scope-1"})}),it("supports an unscoped adapter and preserves invoke failures",async()=>{const e=new Error("gateway unavailable"),s={invoke:vi.fn(async()=>{throw e})},o=new ProcessSessionTerminator(s);await expect(o.terminate({sessionId:"proc-2"})).rejects.toBe(e),expect(s.invoke).toHaveBeenCalledWith({name:"process",args:{action:"kill",sessionId:"proc-2"}})}),it("accepts the OpenClaw process tool kill and reaped-session envelopes",async()=>{const e=vi.fn(async e=>{const s=e.args;return"kill"===s.action?{ok:!0,output:{content:[{type:"text",text:`Termination requested for session ${s.sessionId}.`}],details:{status:"failed"}}}:{ok:!0,output:{content:[{type:"text",text:`No session found for ${s.sessionId}`}],details:{status:"failed"}}}}),s=new ProcessSessionTerminator({invoke:e});await expect(s.terminate({sessionId:"proc-openclaw",sessionKey:"agent:main:session"})).resolves.toMatchObject({ok:!0}),expect(e).toHaveBeenCalledTimes(2)}),it("turns an OpenClaw policy refusal envelope into a failed termination",async()=>{const e=new ProcessSessionTerminator({invoke:async()=>({ok:!1,error:{code:"policy_denied"}})});await expect(e.terminate({sessionId:"proc-denied"})).rejects.toThrow(/policy_denied/)}),it("fails when verification proves the process is still running",async()=>{const e=new ProcessSessionTerminator({invoke:async e=>({ok:!0,toolName:"process",output:{details:{status:"kill"===e.args.action?"failed":"running",sessionId:"proc-running"}}})},{verificationAttempts:2,verificationDelayMs:0,sleep:async()=>{}});await expect(e.terminate({sessionId:"proc-running"})).rejects.toThrow(/could not be verified/)}),it("treats an exact scoped already-absent session as idempotent success",async()=>{const e=vi.fn(async()=>({ok:!0,toolName:"process",output:{content:[{type:"text",text:"No session found for proc-scoped"}],details:{status:"failed"}}})),s=new ProcessSessionTerminator({invoke:e},{verificationAttempts:1});await expect(s.terminate({sessionId:"proc-scoped",sessionKey:"agent:main:scope"})).resolves.toMatchObject({ok:!0}),expect(e).toHaveBeenCalledTimes(1)}),it("continues verification without re-killing after an acknowledged kill",async()=>{let e=0;const s=vi.fn(async s=>{const o=s.args;return"kill"===o.action?{ok:!0,output:{details:{status:"failed",sessionId:o.sessionId}}}:(e+=1,1===e?{ok:!0,output:{details:{status:"running",sessionId:o.sessionId}}}:{ok:!1,error:{message:`No session found for ${o.sessionId}`}})}),o=new ProcessSessionTerminator({invoke:s},{verificationAttempts:1});await expect(o.terminate({sessionId:"proc-reaped",sessionKey:"scope-1"})).rejects.toThrow(/could not be verified/),await expect(o.terminate({sessionId:"proc-reaped",sessionKey:"scope-1"})).resolves.toMatchObject({ok:!0}),expect(s.mock.calls.filter(([e])=>"kill"===e.args.action)).toHaveLength(1)})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class RunProcessLifecycleCoordinator{registry;terminator;inFlightTerminations=new Map;pendingProcesses=new Map;activeScopes=new Map;activeExecTools=new Map;constructor(e,s){this.registry=e,this.terminator=s}beforeToolCall(e){if("exec"!==e.toolName||!0===e.params.background)return;const s=e.toolCallId?.trim();if(!e.runId.trim()||!s)return;const t=this.activeExecTools.get(e.runId)??new Map;if(t.has(s))return;let r;const n=new Promise(e=>{r=e});t.set(s,{promise:n,resolve:r}),this.activeExecTools.set(e.runId,t)}async afterToolCall(e){const s=this.registry.observeAfterToolCall(e);if("released"===s.kind&&this.forgetPending(s.process),"terminate"!==s.kind)return this.completeActiveTool(e,emptyTerminationResult()),{ownership:s};const t=this.terminate(e.runId,s.process).then(t=>(t.ok||this.rememberPending(e.runId,s.process),{processes:[s.process],terminations:[t]}));this.attachTaskToMatchingScopes(e.runId,t);const r=await t;return this.completeActiveTool(e,r),{ownership:s,termination:r.terminations[0]}}terminateRun(e){const s=this.inFlightTerminations.get(e);if(s)return s;const t=new Set(this.registry.runIdsForTermination(e));t.add(e);const r={rootRunId:e,members:t,childTasks:new Set,version:0};this.activeScopes.set(e,r);for(const s of t){for(const e of this.activeExecTools.get(s)?.values()??[])this.attachChildTask(r,s,e.promise);if(s===e)continue;const t=this.inFlightTerminations.get(s);t&&this.attachChildTask(r,s,t)}const n=Promise.resolve().then(()=>this.performTerminateRun(e,r)).finally(()=>{this.inFlightTerminations.delete(e),this.activeScopes.delete(e)});return this.inFlightTerminations.set(e,n),n}async terminateRunWithRetry(e,s={}){const t=Math.max(1,Math.trunc(s.maxAttempts??3)),r=Math.max(0,Math.trunc(s.retryDelayMs??250)),n=s.sleep??(e=>new Promise(s=>{const t=setTimeout(s,e);t.unref?.()})),i=[];for(let s=0;s<t;s+=1){const o=await this.terminateRun(e);if(i.push(o),o.terminations.every(e=>e.ok))break;s+1<t&&await n(r)}return mergeTerminationResults(i)}async performTerminateRun(e,s){const t=deduplicateOwnedProcesses([...this.registry.runIdsForTermination(e).flatMap(e=>Array.from(this.pendingProcesses.get(e)?.values()??[]).map(s=>({ownerRunId:e,process:s}))),...this.registry.terminateRun(e).ownedProcesses]),r=t.map(({process:e})=>e),n=await Promise.all(t.map(({ownerRunId:e,process:s})=>this.terminate(e,s)));for(const e of n)this.forgetPending(e.process),e.ok||this.rememberPending(e.ownerRunId,e.process);return mergeTerminationResults([{processes:r,terminations:n},...await this.drainScope(s)])}async linkChildRun(e,s){const t=this.registry.linkChildRun(e,s);if(!t.terminalized)return{...t,terminations:[]};for(const e of t.ownedProcesses)this.rememberPending(e.ownerRunId,e.process);const r=this.terminateRun(s);for(const t of this.activeScopes.values())t.members.has(e)&&this.attachChildTask(t,s,r);return{terminalized:!0,...await r}}completeActiveTool(e,s){const t=e.toolCallId?.trim();if(!t)return;const r=this.activeExecTools.get(e.runId),n=r?.get(t);n&&(r.delete(t),0===r.size&&this.activeExecTools.delete(e.runId),n.resolve(s))}attachTaskToMatchingScopes(e,s){for(const t of this.activeScopes.values())t.members.has(e)&&this.attachChildTask(t,e,s)}releaseRun(e){for(const s of this.registry.runIdsForTermination(e))this.pendingProcesses.delete(s);return this.registry.releaseRun(e)}rememberPending(e,s){const t=this.pendingProcesses.get(e)??new Map;t.set(processKey(s),s),this.pendingProcesses.set(e,t)}forgetPending(e){const s=processKey(e);for(const e of Array.from(this.pendingProcesses.keys())){const t=this.pendingProcesses.get(e);t?.delete(s),0===t?.size&&this.pendingProcesses.delete(e)}}attachChildTask(e,s,t){e.members.add(s),e.childTasks.has(t)||(e.childTasks.add(t),e.version+=1)}async drainScope(e){for(;;){const s=e.version,t=Array.from(e.childTasks),r=await Promise.all(t);if(await new Promise(e=>setImmediate(e)),e.version===s)return r}}async terminate(e,s){try{return{ownerRunId:e,process:s,ok:!0,result:await this.terminator.terminate(s)}}catch(t){return{ownerRunId:e,process:s,ok:!1,error:t}}}}function processKey(e){return`${e.sessionKey??""}\0${e.sessionId}`}function deduplicateProcesses(e){return Array.from(new Map(e.map(e=>[processKey(e),e])).values())}function deduplicateOwnedProcesses(e){return Array.from(new Map(e.map(e=>[processKey(e.process),e])).values())}function mergeTerminationResults(e){const s=deduplicateProcesses(e.flatMap(e=>e.processes)),t=new Map;for(const s of e)for(const e of s.terminations){const s=processKey(e.process),r=t.get(s);r?.ok&&!e.ok||t.set(s,e)}return{processes:s,terminations:Array.from(t.values())}}function emptyTerminationResult(){return{processes:[],terminations:[]}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{ProcessSessionTerminator}from"./process-session-terminator.js";import{RunProcessLifecycleCoordinator}from"./run-process-lifecycle-coordinator.js";import{RunProcessOwnershipRegistry}from"./run-process-ownership-registry.js";const runningExec=(e={})=>({runId:"run-1",sessionKey:"scope-1",toolName:"exec",params:{command:"sleep 60"},result:{details:{status:"running",sessionId:"proc-1"}},...e}),createCoordinator=e=>new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:e})),successfulToolEnvelope=e=>({ok:!0,toolName:"process",output:{details:{status:"completed",sessionId:e?.args?.sessionId}}});describe("RunProcessLifecycleCoordinator",()=>{it("kills an observed parent-owned process when the run is canceled",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),n=createCoordinator(e);await n.afterToolCall(runningExec());const t=await n.terminateRun("run-1");expect(t.processes).toEqual([{sessionId:"proc-1",sessionKey:"scope-1"}]),expect(t.terminations[0]).toMatchObject({ok:!0}),expect(e).toHaveBeenCalledTimes(2)}),it("kills a late process when cancel happens before after_tool_call",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),n=createCoordinator(e);await n.terminateRun("run-1");const t=await n.afterToolCall(runningExec());expect(t.ownership.kind).toBe("terminate"),expect(t.termination).toMatchObject({process:{sessionId:"proc-1",sessionKey:"scope-1"},ok:!0}),expect(e).toHaveBeenCalledWith({name:"process",args:{action:"kill",sessionId:"proc-1"},sessionKey:"scope-1"})}),it("does not kill explicit detached background work",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),n=createCoordinator(e);await n.terminateRun("run-1");const t=await n.afterToolCall(runningExec({params:{command:"worker",background:!0}}));expect(t).toEqual({ownership:{kind:"ignored",reason:"detached"}}),expect(e).not.toHaveBeenCalled()}),it("isolates individual kill failures while attempting every process",async()=>{const e=vi.fn(async e=>{if("proc-1"===e.args.sessionId)throw new Error("first failed");return successfulToolEnvelope(e)}),n=createCoordinator(e);await n.afterToolCall(runningExec()),await n.afterToolCall(runningExec({result:{details:{status:"running",sessionId:"proc-2"}}}));const t=await n.terminateRun("run-1");expect(e).toHaveBeenCalledTimes(3),expect(t.terminations.map(({ok:e})=>e)).toEqual([!1,!0]),expect(t.terminations[0]?.error).toEqual(new Error("first failed"))}),it("releaseRun drops ownership without killing or tombstoning",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),n=createCoordinator(e);await n.afterToolCall(runningExec()),expect(n.releaseRun("run-1")).toEqual([{sessionId:"proc-1",sessionKey:"scope-1"}]),expect((await n.terminateRun("run-1")).processes).toEqual([]),expect(e).not.toHaveBeenCalled()}),it("deduplicates concurrent terminal cleanup for the same run",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),n=createCoordinator(e);await n.afterToolCall(runningExec());const t=n.terminateRun("run-1"),o=n.terminateRun("run-1");expect(o).toBe(t),await Promise.all([t,o]),expect(e).toHaveBeenCalledTimes(2)}),it("keeps an unverified target pending so a later cleanup can retry it",async()=>{let e=!1;const n=vi.fn(async n=>{const t=n.args;return"kill"===t.action||e?successfulToolEnvelope(n):{ok:!0,output:{details:{status:"running",sessionId:t.sessionId}}}}),t=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:n},{verificationAttempts:1}));await t.afterToolCall(runningExec()),expect((await t.terminateRun("run-1")).terminations).toEqual([expect.objectContaining({ok:!1})]),e=!0,expect((await t.terminateRun("run-1")).terminations).toEqual([expect.objectContaining({ok:!0})]),expect(n.mock.calls.map(([e])=>e.args.action)).toEqual(["kill","poll","poll"])}),it("clears failed pending cleanup when a later poll proves natural completion",async()=>{const e=vi.fn(async e=>{const n=e.args;return"kill"===n.action?successfulToolEnvelope(e):{ok:!0,output:{details:{status:"running",sessionId:n.sessionId}}}}),n=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:e},{verificationAttempts:1}));await n.afterToolCall(runningExec()),expect((await n.terminateRun("run-1")).terminations).toEqual([expect.objectContaining({ok:!1})]),await n.afterToolCall({runId:"run-1",sessionKey:"scope-1",toolName:"process",params:{action:"poll",sessionId:"proc-1"},result:{details:{status:"completed",sessionId:"proc-1"}}}),expect(await n.terminateRun("run-1")).toEqual({processes:[],terminations:[]}),expect(e).toHaveBeenCalledTimes(2)}),it("accepts natural completion when terminal polling wins the cancel IO race",async()=>{let e,n;const t=new Promise(e=>{n=e}),o=new Promise(n=>{e=n}),s=vi.fn(async e=>{const t=e.args;if("kill"!==t.action)throw new Error("already-absent kill must not require verification polling");return n(),await o,{ok:!0,output:{content:[{type:"text",text:`No session found for ${t.sessionId}`}]}}}),a=createCoordinator(s);await a.afterToolCall(runningExec());const i=a.terminateRun("run-1");await t,await a.afterToolCall({runId:"run-1",sessionKey:"scope-1",toolName:"process",params:{action:"poll",sessionId:"proc-1"},result:{details:{status:"completed",sessionId:"proc-1"}}}),e(),await expect(i).resolves.toMatchObject({terminations:[expect.objectContaining({ok:!0})]}),expect(s).toHaveBeenCalledTimes(1)}),it("retries pending terminal cleanup within a bounded terminal hook",async()=>{let e=0;const n=vi.fn(async n=>{const t=n.args;return"kill"===t.action?successfulToolEnvelope(n):(e+=1,1===e?{ok:!0,output:{details:{status:"running",sessionId:t.sessionId}}}:successfulToolEnvelope(n))}),t=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:n},{verificationAttempts:1}));await t.afterToolCall(runningExec());const o=await t.terminateRunWithRetry("run-1",{retryDelayMs:0,sleep:async()=>{}});expect(o.terminations).toEqual([expect.objectContaining({ok:!0})]),expect(n.mock.calls.map(([e])=>e.args.action)).toEqual(["kill","poll","poll"])}),it("makes parent cleanup wait for a child cleanup already in flight",async()=>{let e;const n=new Promise(n=>{e=n}),t=vi.fn(async e=>("poll"===e.args.action&&await n,successfulToolEnvelope(e))),o=createCoordinator(t);await o.linkChildRun("parent-run","child-run"),await o.afterToolCall(runningExec({runId:"child-run",result:{details:{status:"running",sessionId:"child-process"}}}));const s=o.terminateRun("child-run"),a=o.terminateRun("parent-run");let i=!1;a.finally(()=>{i=!0}),await Promise.resolve(),expect(i).toBe(!1),e(),await expect(a).resolves.toMatchObject({terminations:[expect.objectContaining({ok:!0})]}),await s}),it("terminates processes owned by a child linked after its parent ended",async()=>{const e=vi.fn(async e=>successfulToolEnvelope(e)),n=createCoordinator(e);await n.terminateRun("parent-run"),await n.afterToolCall(runningExec({runId:"late-child",result:{details:{status:"running",sessionId:"late-process"}}}));const t=await n.linkChildRun("parent-run","late-child");expect(t).toMatchObject({terminalized:!0,processes:[{sessionId:"late-process",sessionKey:"scope-1"}],terminations:[expect.objectContaining({ok:!0})]})}),it("keeps the parent barrier open for a child linked during terminalization",async()=>{let e;const n=new Promise(n=>{e=n}),t=vi.fn(async e=>("poll"===e.args.action&&await n,successfulToolEnvelope(e))),o=createCoordinator(t);await o.afterToolCall(runningExec({runId:"late-child",result:{details:{status:"running",sessionId:"late-process"}}}));const s=o.terminateRun("parent-run");await Promise.resolve();const a=o.linkChildRun("parent-run","late-child");let i=!1;s.finally(()=>{i=!0}),await Promise.resolve(),expect(i).toBe(!1),e(),await a,await expect(s).resolves.toMatchObject({terminations:[expect.objectContaining({ok:!0})]})}),it("waits for a running exec discovered after same-run termination starts",async()=>{let e;const n=new Promise(n=>{e=n}),t=vi.fn(async e=>("poll"===e.args.action&&await n,successfulToolEnvelope(e))),o=createCoordinator(t);o.beforeToolCall({runId:"run-late",toolCallId:"tool-late",toolName:"exec",params:{command:"sleep 90"}});const s=o.terminateRun("run-late");await Promise.resolve();const a=o.afterToolCall(runningExec({runId:"run-late",toolCallId:"tool-late",result:{details:{status:"running",sessionId:"late-process"}}}));let i=!1;s.finally(()=>{i=!0}),await Promise.resolve(),expect(i).toBe(!1),e(),await a,await expect(s).resolves.toMatchObject({processes:[{sessionId:"late-process",sessionKey:"scope-1"}],terminations:[expect.objectContaining({ok:!0})]})}),it("keeps a failed descendant target under its original child owner",async()=>{let e=!1;const n=vi.fn(async n=>{const t=n.args;return"kill"===t.action||e?successfulToolEnvelope(n):{ok:!0,output:{details:{status:"running",sessionId:t.sessionId}}}}),t=new RunProcessLifecycleCoordinator(new RunProcessOwnershipRegistry,new ProcessSessionTerminator({invoke:n},{verificationAttempts:1}));await t.linkChildRun("parent-run","child-run"),await t.afterToolCall(runningExec({runId:"child-run",result:{details:{status:"running",sessionId:"child-process"}}})),expect((await t.terminateRun("parent-run")).terminations).toEqual([expect.objectContaining({ownerRunId:"child-run",ok:!1})]),e=!0,expect((await t.terminateRun("child-run")).terminations).toEqual([expect.objectContaining({ownerRunId:"child-run",ok:!0})])})});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const DEFAULT_OWNERSHIP_TTL_MS=216e5,DEFAULT_MAX_RUNS=1024,DEFAULT_MAX_PROCESSES=4096,DEFAULT_MAX_TOMBSTONES=1024,isRecord=s=>"object"==typeof s&&null!==s&&!Array.isArray(s),nonEmptyString=s=>{if("string"!=typeof s)return;return s.trim()||void 0},positiveInteger=(s,e)=>Number.isInteger(s)&&(s??0)>0?s:e,processKey=s=>JSON.stringify([s.sessionKey??null,s.sessionId]);export const classifyOwnedExecProcess=s=>{if("exec"!==s.toolName)return;if(!0===s.params.background)return;if(!isRecord(s.result))return;const e=s.result.details;if(!isRecord(e)||"running"!==e.status)return;const n=nonEmptyString(e.sessionId);if(!n)return;const t=nonEmptyString(s.sessionKey);return t?{sessionId:n,sessionKey:t}:{sessionId:n}};export const classifyTerminalProcess=s=>{if("process"!==s.toolName)return;if("poll"!==s.params.action)return;if(!isRecord(s.result))return;const e=s.result.details;if(!isRecord(e)||"completed"!==e.status&&"failed"!==e.status)return;const n=nonEmptyString(e.sessionId)??nonEmptyString(s.params.sessionId);if(!n)return;const t=nonEmptyString(s.sessionKey);return t?{sessionId:n,sessionKey:t}:{sessionId:n}};export class RunProcessOwnershipRegistry{ownershipTtlMs;maxRuns;maxProcesses;maxTombstones;now;runs=new Map;processOwners=new Map;tombstones=new Map;childRuns=new Map;parentRuns=new Map;constructor(s={}){this.ownershipTtlMs=positiveInteger(s.ownershipTtlMs,216e5),this.maxRuns=positiveInteger(s.maxRuns,1024),this.maxProcesses=positiveInteger(s.maxProcesses,4096),this.maxTombstones=positiveInteger(s.maxTombstones,1024),this.now=s.now??Date.now}observeAfterToolCall(s){const e=this.now();this.prune(e);const n=nonEmptyString(s.runId);if(!n)return{kind:"ignored",reason:"invalid_run_id"};if("process"===s.toolName){const e=classifyTerminalProcess(s);return e?this.releaseProcess(e):{kind:"ignored",reason:"not_terminal"}}if("exec"!==s.toolName)return{kind:"ignored",reason:"unsupported_tool"};if(!0===s.params.background)return{kind:"ignored",reason:"detached"};const t=classifyOwnedExecProcess(s);return t?this.tombstones.has(n)?{kind:"terminate",process:t}:(this.claim(n,t,e),this.enforceOwnershipBounds(),{kind:"owned",process:t}):{kind:"ignored",reason:"not_running"}}releaseProcess(s){const e=processKey(s),n=this.processOwners.get(e);if(!n)return{kind:"released",process:s,wasOwned:!1};this.processOwners.delete(e);const t=this.runs.get(n.runId);return t?.processes.delete(e),0===t?.processes.size&&this.runs.delete(n.runId),{kind:"released",process:s,wasOwned:!0}}terminateRun(s){const e=this.now();this.prune(e);const n=nonEmptyString(s);if(!n)return{processes:[],ownedProcesses:[]};const t=this.runIdsForTermination(n),r=[],o=[];for(const s of t){this.tombstones.delete(s),this.tombstones.set(s,{observedAt:e});const n=this.runs.get(s);if(n)for(const e of n.processes.values())r.push(e),o.push({ownerRunId:s,process:e});this.removeRun(s)}return this.enforceTombstoneBound(),{processes:r,ownedProcesses:o}}runIdsForTermination(s){const e=nonEmptyString(s);return e?this.runAndDescendants(e):[]}linkChildRun(s,e){const n=this.now();this.prune(n);const t=nonEmptyString(s),r=nonEmptyString(e);if(!t||!r||t===r)return{terminalized:!1,processes:[],ownedProcesses:[]};this.parentRuns.set(r,t);const o=this.childRuns.get(t)??new Set;return o.add(r),this.childRuns.set(t,o),this.tombstones.has(t)?{terminalized:!0,...this.terminateRun(r)}:{terminalized:!1,processes:[],ownedProcesses:[]}}releaseRun(s){const e=this.now();this.prune(e);const n=nonEmptyString(s);if(!n)return[];const t=this.runs.get(n),r=t?Array.from(t.processes.values()):[];return this.removeRun(n),this.removeRunRelation(n),this.tombstones.delete(n),r}snapshot(){return this.prune(this.now()),{runCount:this.runs.size,processCount:this.processOwners.size,tombstoneCount:this.tombstones.size}}claim(s,e,n){const t=processKey(e),r=this.processOwners.get(t);if(r&&r.runId!==s){const s=this.runs.get(r.runId);s?.processes.delete(t),0===s?.processes.size&&this.runs.delete(r.runId)}let o=this.runs.get(s);o?(this.runs.delete(s),o.observedAt=n):o={observedAt:n,processes:new Map},o.processes.set(t,e),this.runs.set(s,o),this.processOwners.delete(t),this.processOwners.set(t,{runId:s})}prune(s){for(const[e,n]of this.tombstones)s-n.observedAt>=this.ownershipTtlMs&&(this.tombstones.delete(e),this.removeRunRelation(e));for(const[e,n]of this.runs)s-n.observedAt>=this.ownershipTtlMs&&this.removeRun(e)}enforceOwnershipBounds(){for(;this.runs.size>this.maxRuns;){const s=this.runs.keys().next().value;if(!s)break;this.removeRun(s)}for(;this.processOwners.size>this.maxProcesses;){const s=this.processOwners.keys().next().value;if(!s)break;const e=this.processOwners.get(s);if(this.processOwners.delete(s),!e)continue;const n=this.runs.get(e.runId);n?.processes.delete(s),0===n?.processes.size&&this.runs.delete(e.runId)}}removeRun(s){const e=this.runs.get(s);if(e){for(const n of e.processes.keys()){const e=this.processOwners.get(n);e?.runId===s&&this.processOwners.delete(n)}this.runs.delete(s)}}runAndDescendants(s){const e=[],n=[s],t=new Set;for(;n.length>0;){const s=n.pop();if(!t.has(s)){t.add(s),e.push(s);for(const e of this.childRuns.get(s)??[])n.push(e)}}return e}removeRunRelation(s){const e=this.parentRuns.get(s);if(e){const n=this.childRuns.get(e);n?.delete(s),0===n?.size&&this.childRuns.delete(e),this.parentRuns.delete(s)}const n=this.childRuns.get(s);if(n){for(const s of n)this.parentRuns.delete(s);this.childRuns.delete(s)}}enforceTombstoneBound(){for(;this.tombstones.size>this.maxTombstones;){const s=this.tombstones.keys().next().value;if(!s)break;this.tombstones.delete(s),this.removeRunRelation(s)}}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{describe,expect,it}from"vitest";import{RunProcessOwnershipRegistry,classifyOwnedExecProcess,classifyTerminalProcess}from"./run-process-ownership-registry.js";const runningExec=(e={})=>({runId:"run-1",sessionKey:"agent:main:conversation-1",toolName:"exec",params:{command:"sleep 60"},result:{content:[{type:"text",text:"still running"}],details:{status:"running",sessionId:"proc-1",pid:123}},...e});describe("classifyOwnedExecProcess",()=>{it("owns default auto-yield and explicit yieldMs exec sessions",()=>{expect(classifyOwnedExecProcess(runningExec())).toEqual({sessionId:"proc-1",sessionKey:"agent:main:conversation-1"}),expect(classifyOwnedExecProcess(runningExec({params:{yieldMs:50}}))).toEqual({sessionId:"proc-1",sessionKey:"agent:main:conversation-1"})}),it("treats explicit background exec as detached",()=>{expect(classifyOwnedExecProcess(runningExec({params:{command:"worker",background:!0}}))).toBeUndefined()}),it("requires a structured running result and never parses display text",()=>{expect(classifyOwnedExecProcess(runningExec({result:{content:[{type:"text",text:"Command still running (session fake)"}]}}))).toBeUndefined(),expect(classifyOwnedExecProcess(runningExec({result:{details:{status:"completed"}}}))).toBeUndefined()})}),describe("classifyTerminalProcess",()=>{it("recognizes structured completed and failed process results",()=>{const e={runId:"run-1",sessionKey:"agent:main:conversation-1",toolName:"process",params:{action:"poll",sessionId:"proc-1"},result:{details:{status:"completed",sessionId:"proc-1"}}};expect(classifyTerminalProcess(e)).toEqual({sessionId:"proc-1",sessionKey:"agent:main:conversation-1"}),expect(classifyTerminalProcess({...e,result:{details:{status:"failed",sessionId:"proc-1"}}})).toEqual({sessionId:"proc-1",sessionKey:"agent:main:conversation-1"})}),it("ignores running process results and display text",()=>{expect(classifyTerminalProcess({runId:"run-1",toolName:"process",params:{action:"poll",sessionId:"proc-1"},result:{content:[{type:"text",text:"Process exited with code 0."}],details:{status:"running",sessionId:"proc-1"}}})).toBeUndefined()})}),describe("RunProcessOwnershipRegistry",()=>{it("releases ownership when process polling proves natural completion",()=>{const e=new RunProcessOwnershipRegistry;e.observeAfterToolCall(runningExec()),expect(e.observeAfterToolCall({runId:"run-1",sessionKey:"agent:main:conversation-1",toolName:"process",params:{action:"poll",sessionId:"proc-1"},result:{details:{status:"completed",sessionId:"proc-1"}}})).toEqual({kind:"released",wasOwned:!0,process:{sessionId:"proc-1",sessionKey:"agent:main:conversation-1"}}),expect(e.terminateRun("run-1").processes).toEqual([])}),it("does not release ownership when a non-poll process action fails",()=>{const e=new RunProcessOwnershipRegistry;e.observeAfterToolCall(runningExec()),expect(e.observeAfterToolCall({runId:"run-1",sessionKey:"agent:main:conversation-1",toolName:"process",params:{action:"paste",sessionId:"proc-1",text:""},result:{details:{status:"failed",sessionId:"proc-1"}}})).toEqual({kind:"ignored",reason:"not_terminal"}),expect(e.terminateRun("run-1").processes).toEqual([{sessionId:"proc-1",sessionKey:"agent:main:conversation-1"}])}),it("surfaces late terminal evidence after cancellation removed ownership",()=>{const e=new RunProcessOwnershipRegistry;e.observeAfterToolCall(runningExec()),e.terminateRun("run-1"),expect(e.observeAfterToolCall({runId:"run-1",sessionKey:"agent:main:conversation-1",toolName:"process",params:{action:"poll",sessionId:"proc-1"},result:{details:{status:"completed",sessionId:"proc-1"}}})).toEqual({kind:"released",wasOwned:!1,process:{sessionId:"proc-1",sessionKey:"agent:main:conversation-1"}})}),it("returns all parent-owned sessions when cancellation follows after_tool_call",()=>{const e=new RunProcessOwnershipRegistry;expect(e.observeAfterToolCall(runningExec()).kind).toBe("owned"),e.observeAfterToolCall(runningExec({result:{details:{status:"running",sessionId:"proc-2"}}})),expect(e.terminateRun("run-1").processes).toEqual([{sessionId:"proc-1",sessionKey:"agent:main:conversation-1"},{sessionId:"proc-2",sessionKey:"agent:main:conversation-1"}]),expect(e.snapshot()).toEqual({runCount:0,processCount:0,tombstoneCount:1})}),it("returns terminate when cancellation wins the after_tool_call race",()=>{const e=new RunProcessOwnershipRegistry;expect(e.terminateRun("run-1").processes).toEqual([]),expect(e.observeAfterToolCall(runningExec())).toEqual({kind:"terminate",process:{sessionId:"proc-1",sessionKey:"agent:main:conversation-1"}}),expect(e.snapshot().processCount).toBe(0)}),it("never terminates detached work, including after parent cancellation",()=>{const e=new RunProcessOwnershipRegistry;e.terminateRun("run-1"),expect(e.observeAfterToolCall(runningExec({params:{command:"worker",background:!0}}))).toEqual({kind:"ignored",reason:"detached"})}),it("retains terminal ownership until the run lifecycle expires",()=>{let e=1e3;const s=new RunProcessOwnershipRegistry({ownershipTtlMs:100,now:()=>e});s.terminateRun("run-1"),e+=101,expect(s.observeAfterToolCall(runningExec()).kind).toBe("owned"),expect(s.snapshot().tombstoneCount).toBe(0)}),it("terminalizes descendant run processes with their parent",()=>{const e=new RunProcessOwnershipRegistry;e.linkChildRun("parent-run","child-run"),e.observeAfterToolCall(runningExec({runId:"child-run",sessionKey:"agent:worker:child"})),expect(e.terminateRun("parent-run").processes).toEqual([{sessionId:"proc-1",sessionKey:"agent:worker:child"}]),expect(e.observeAfterToolCall(runningExec({runId:"child-run",sessionKey:"agent:worker:child"})).kind).toBe("terminate")}),it("walks an arbitrary-depth subagent run graph",()=>{const e=new RunProcessOwnershipRegistry;e.linkChildRun("root","child"),e.linkChildRun("child","grandchild"),e.observeAfterToolCall(runningExec({runId:"grandchild",sessionKey:"agent:grandchild:conversation"})),expect(e.terminateRun("root").processes).toEqual([{sessionId:"proc-1",sessionKey:"agent:grandchild:conversation"}])}),it("bounds runs, processes, tombstones, and stale ownership",()=>{let e=0;const s=new RunProcessOwnershipRegistry({maxRuns:2,maxProcesses:2,maxTombstones:2,ownershipTtlMs:100,now:()=>e});for(let e=1;e<=3;e+=1)s.observeAfterToolCall(runningExec({runId:`run-${e}`,result:{details:{status:"running",sessionId:`proc-${e}`}}}));expect(s.snapshot()).toEqual({runCount:2,processCount:2,tombstoneCount:0}),s.terminateRun("cancel-1"),s.terminateRun("cancel-2"),s.terminateRun("cancel-3"),expect(s.snapshot().tombstoneCount).toBe(2),e=101,expect(s.snapshot()).toEqual({runCount:0,processCount:0,tombstoneCount:0})}),it("keys process ownership by session scope as well as session id",()=>{const e=new RunProcessOwnershipRegistry;e.observeAfterToolCall(runningExec({sessionKey:"scope-a"})),e.observeAfterToolCall(runningExec({runId:"run-2",sessionKey:"scope-b"})),expect(e.terminateRun("run-1").processes).toEqual([{sessionId:"proc-1",sessionKey:"scope-a"}]),expect(e.terminateRun("run-2").processes).toEqual([{sessionId:"proc-1",sessionKey:"scope-b"}])})});
|