beeos-claw 0.5.27 → 0.5.29

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.
@@ -0,0 +1 @@
1
+ import{describe,expect,it,vi}from"vitest";import{AcpGatewayEvents}from"./gateway-events.js";import{AcpGatewaySessionState}from"./session-state.js";function makeHarness(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),s=vi.fn().mockReturnValue(!0),n={sendSessionUpdate:s,sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},a=new AcpGatewayEvents({state:t,transport:n,config:{gateway:{agentId:"main"},canvasEnabled:!0},logger:e,writeObs:vi.fn(),completePrompt:vi.fn(),failPrompt:vi.fn(),schedulePromptTimeout:vi.fn(),maybeCompletePromptFromChatState:vi.fn()}),o={sessionId:"s1",gatewaySessionKey:"main/s1",agentId:"main",currentRunId:"run-1",pendingPromptId:1,promptDone:!1,createdAt:Date.now(),chatReplayDedupKeys:new Set};return t.sessions.set(o.sessionId,o),t.promptsByRunId.set("run-1",o),{events:a,sendSessionUpdate:s}}function sentUpdates(e){return e.mock.calls.map(e=>e[1])}describe("gateway-events file artifact presentation",()=>{it("presents structured files from chat.toolResult content wrappers as chat resource links",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,state:"delta",message:{role:"assistant",timestamp:Date.now(),content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",result:{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})}}]}};e.events.handleChatEvent(t),await Promise.resolve();const s=sentUpdates(e.sendSessionUpdate);expect(s.some(e=>"tool_call_update"===e.sessionUpdate)).toBe(!0),expect(s).toContainEqual(expect.objectContaining({sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}))})});
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{presentFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if(!isRecord(t)||!Array.isArray(t.files))return;const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;schedulePromptTimeout;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.schedulePromptTimeout=t.schedulePromptTimeout,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.getMessageClient=t.getMessageClient,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"timeout"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),!!t.sessionLoadInProgress||this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const l=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:n,resourceLinks:extractResourceLinksFromRawResult(n),timestamp:r});this.sendTurnUpdate(e,l)&&e.assistantMq?.push(l);continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const d={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,d),e.assistantMq?.push(d)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&n(buildToolCallUpdatePayload({toolCallId:o,toolName:a,outputText:i,rawOutput:r,resourceLinks:extractResourceLinksFromRawResult(r),timestamp:s}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===n||"cancel"===n)this.completePrompt(e,"cancelled");else if("error"===n){const n=s?.error,o=n&&"object"==typeof n?n:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?n:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,d=a??r,c=n?.result;let u;try{u=await resolveToolResultBlocks({toolCallId:d,toolName:i,rawText:l,rawResult:c})}catch{u=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===u.length&&l&&(u=[{type:"content",content:{type:"text",text:l}}]);const h=[],g=[];for(const t of u)t.content&&("resource_link"===t.content.type&&t.content.uri?g.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&h.push(t.content.text));const p=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:h.join("\n"),rawOutput:c,resourceLinks:g,timestamp:s});this.sendTurnUpdate(t,p)&&t.assistantMq?.push(p),g.length>0&&await presentFileArtifacts(t,r,g,{config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}})}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const s=this.state.runIdToCronJobId.get(e);s&&(t.cronJobId=s),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),this.schedulePromptTimeout(t)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e={...t},s="string"==typeof t.sessionKey?t.sessionKey:void 0;s&&null==e.sessionKey&&(e.sessionKey=s);const n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(n&&(e.sessionId=n),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),n=s?.jobs;if(Array.isArray(n))for(const s of n)if(isRecord(s)&&s.id===t.jobId&&"string"==typeof s.name&&s.name){e.name=s.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:e})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const d=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:d}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getMessageClient?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const n="string"==typeof t.jobId?t.jobId:void 0;if(!n)return;const o=t.summary;let a=this.cronChannelCache?.get(n);if(!a&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===n){a=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(!a)return;const r=randomUUID();try{await e.messages.send({conversationId:a,id:r,type:"agent_reply",content:{text:o,cron_job_id:n,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${n} err=${e}`),void this.cronChannelCache?.delete(n);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
1
+ import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{presentFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if("string"==typeof t){const e=parseJsonObject(t);return e?extractResourceLinksFromRawResult(e):void 0}if(!isRecord(t))return;if(!Array.isArray(t.files)){for(const e of["content","output","result","details"]){const s=t[e];if(s===t)continue;const n=extractResourceLinksFromRawResult(s);if(n?.length)return n}return}const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}function parseJsonObject(t){const e=t.trim();if(e.startsWith("{"))try{const t=JSON.parse(e);return isRecord(t)?t:void 0}catch{return}}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;schedulePromptTimeout;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.schedulePromptTimeout=t.schedulePromptTimeout,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.getMessageClient=t.getMessageClient,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"timeout"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),!!t.sessionLoadInProgress||this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const l=extractResourceLinksFromRawResult(n),c=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:n,resourceLinks:l,timestamp:r});this.sendTurnUpdate(e,c)&&e.assistantMq?.push(c),this.presentToolResultFiles(e,t,l).catch(t=>{this.logger.warn?.(`[gateway-events] File artifact presentation failed: ${t instanceof Error?t.message:t}`)});continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,c),e.assistantMq?.push(c)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);if(i){const t=extractResourceLinksFromRawResult(r);n(buildToolCallUpdatePayload({toolCallId:o,toolName:a,outputText:i,rawOutput:r,resourceLinks:t,timestamp:s})),this.presentToolResultFiles(e,o,t).catch(t=>{this.logger.warn?.(`[gateway-events] File artifact presentation failed: ${t instanceof Error?t.message:t}`)})}continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===n||"cancel"===n)this.completePrompt(e,"cancelled");else if("error"===n){const n=s?.error,o=n&&"object"==typeof n?n:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?n:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,c=a??r,d=n?.result;let u;try{u=await resolveToolResultBlocks({toolCallId:c,toolName:i,rawText:l,rawResult:d})}catch{u=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===u.length&&l&&(u=[{type:"content",content:{type:"text",text:l}}]);const h=[],g=[];for(const t of u)t.content&&("resource_link"===t.content.type&&t.content.uri?g.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&h.push(t.content.text));const p=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:h.join("\n"),rawOutput:d,resourceLinks:g,timestamp:s});this.sendTurnUpdate(t,p)&&t.assistantMq?.push(p),await this.presentToolResultFiles(t,r,g)}async presentToolResultFiles(t,e,s){s?.length&&await presentFileArtifacts(t,e,s,{config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}})}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const s=this.state.runIdToCronJobId.get(e);s&&(t.cronJobId=s),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),this.schedulePromptTimeout(t)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e={...t},s="string"==typeof t.sessionKey?t.sessionKey:void 0;s&&null==e.sessionKey&&(e.sessionKey=s);const n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(n&&(e.sessionId=n),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),n=s?.jobs;if(Array.isArray(n))for(const s of n)if(isRecord(s)&&s.id===t.jobId&&"string"==typeof s.name&&s.name){e.name=s.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:e})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const c=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:c}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getMessageClient?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const n="string"==typeof t.jobId?t.jobId:void 0;if(!n)return;const o=t.summary;let a=this.cronChannelCache?.get(n);if(!a&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===n){a=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(!a)return;const r=randomUUID();try{await e.messages.send({conversationId:a,id:r,type:"agent_reply",content:{text:o,cron_job_id:n,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${n} err=${e}`),void this.cronChannelCache?.delete(n);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
@@ -1 +1 @@
1
- import{readUploadToolResult,writeEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{tryExtractLocalFiles}from"../upload/local-file-extractors.js";import{uploadLocalFile,getUploadDeps}from"../upload/upload-file.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){let e=[];for(const o of strategies)if(o.canHandle(t))try{const n=await o.resolve(t);if(n&&n.length>0){e=n;break}}catch{}return enrichWithLocalFileUploads(t,e)}async function enrichWithLocalFileUploads(t,e){const o=getUploadDeps();if(!o)return e;const n=tryExtractLocalFiles(t);if(0===n.length)return e;const r=[...e],l=[];for(const e of n)try{o.logger?.debug?.(`[enrich] Uploading local file ${e.filePath} for toolCallId=${t.toolCallId}`);const n=await uploadLocalFile(e.filePath,o.gatewayUrl,o.keys,{fileNameOverride:e.suggestedName});o.logger?.info?.(`[enrich] Uploaded: fileId=${n.fileId} toolCallId=${t.toolCallId}`);const s={uri:n.uri,name:n.name,...n.mimeType?{mimeType:n.mimeType}:{}};l.push(s),r.push({type:"content",content:{type:"resource_link",...s}}),r.push({type:"content",content:{type:"text",text:`[Auto-uploaded: ${n.name}. Do NOT call beeos_upload_file for this file.]`}})}catch(t){o.logger?.warn?.(`[enrich] Upload failed for ${e.filePath}: ${t instanceof Error?t.message:String(t)}, skipping`)}return l.length>0&&writeEnrichmentCache(t.toolCallId,l).catch(e=>{o.logger?.warn?.(`[enrich] Cache write failed for toolCallId=${t.toolCallId}: ${e instanceof Error?e.message:String(e)}`)}),r}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const o=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,o??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const o=JSON.parse(e);if(isRecord(o)){const e=buildToolResultSessionContentBlocks(t.toolName,o);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const o=t[e];if(isUploadPayload(o))return o;if(isRecord(o))for(const t of["output","result","details"])if(isUploadPayload(o[t]))return o[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const o=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&o.push({type:"content",content:{type:"text",text:e.trim()}}),o;const n=buildFileResourceBlocks(e),r=e.content??e.result;if(Array.isArray(r)){for(const t of r){const e=toContentBlock(t);e&&o.push(e)}if(o.length>0)return appendFileBlocks(o,n)}const l=e.result;if(l&&"object"==typeof l&&!Array.isArray(l)){const t=l.content;if(Array.isArray(t)){for(const e of t){const t=toContentBlock(e);t&&o.push(t)}if(o.length>0)return appendFileBlocks(o,n)}}const s=asStr(e.output)??asStr(e.text)??asStr(e.result)??asStr(e.message)??asStr(e.error);return s&&o.push({type:"content",content:{type:"text",text:s}}),appendFileBlocks(o,n)}function buildFileResourceBlocks(t){if(!Array.isArray(t.files))return[];const e=[];for(const o of t.files){if(!isRecord(o))continue;const t=asStr(o.fileId)??asStr(o.file_id),n=t?`beeos-file://${t}`:asStr(o.uri)??asStr(o.downloadUrl)??asStr(o.url);if(!n)continue;const r=asStr(o.name)??asStr(o.fileName)??asStr(o.file_name),l=asStr(o.mimeType)??asStr(o.mime_type);e.push({type:"content",content:{type:"resource_link",uri:n,...r?{name:r}:{},...l?{mimeType:l}:{}}})}return e}function appendFileBlocks(t,e){if(0===e.length)return t;const o=new Set(e.map(t=>t.content.uri).filter(Boolean));return[...t.filter(t=>!("text"===t.content.type&&t.content.text&&o.has(t.content.text))),...e]}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),o=asStr(t.uri);return e||o?{type:"content",content:{type:"image",...e?{data:e}:{},...o?{uri:o}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
1
+ import{readUploadToolResult,writeEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{tryExtractLocalFiles}from"../upload/local-file-extractors.js";import{uploadLocalFile,getUploadDeps}from"../upload/upload-file.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){let e=[];for(const o of strategies)if(o.canHandle(t))try{const n=await o.resolve(t);if(n&&n.length>0){e=n;break}}catch{}return enrichWithLocalFileUploads(t,e)}async function enrichWithLocalFileUploads(t,e){const o=getUploadDeps();if(!o)return e;const n=tryExtractLocalFiles(t);if(0===n.length)return e;const r=[...e],l=[];for(const e of n)try{o.logger?.debug?.(`[enrich] Uploading local file ${e.filePath} for toolCallId=${t.toolCallId}`);const n=await uploadLocalFile(e.filePath,o.gatewayUrl,o.keys,{fileNameOverride:e.suggestedName});o.logger?.info?.(`[enrich] Uploaded: fileId=${n.fileId} toolCallId=${t.toolCallId}`);const s={uri:n.uri,name:n.name,...n.mimeType?{mimeType:n.mimeType}:{}};l.push(s),r.push({type:"content",content:{type:"resource_link",...s}}),r.push({type:"content",content:{type:"text",text:`[Auto-uploaded: ${n.name}. Do NOT call beeos_upload_file for this file.]`}})}catch(t){o.logger?.warn?.(`[enrich] Upload failed for ${e.filePath}: ${t instanceof Error?t.message:String(t)}, skipping`)}return l.length>0&&writeEnrichmentCache(t.toolCallId,l).catch(e=>{o.logger?.warn?.(`[enrich] Cache write failed for toolCallId=${t.toolCallId}: ${e instanceof Error?e.message:String(e)}`)}),r}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const o=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,o??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const o=JSON.parse(e);if(isRecord(o)){const e=buildToolResultSessionContentBlocks(t.toolName,o);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const o=t[e];if(isUploadPayload(o))return o;if(isRecord(o))for(const t of["output","result","details"])if(isUploadPayload(o[t]))return o[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const o=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&o.push({type:"content",content:{type:"text",text:e.trim()}}),o;const n=buildFileResourceBlocks(e),r=asStr(e.content);if(r){const e=buildBlocksFromJsonText(t,r);if(e.length>0)return appendFileBlocks(e,n)}const l=e.content??e.result;if(Array.isArray(l)){for(const t of l){const e=toContentBlock(t);e&&o.push(e)}if(o.length>0)return appendFileBlocks(o,n)}const s=e.result;if(s&&"object"==typeof s&&!Array.isArray(s)){const t=s.content;if(Array.isArray(t)){for(const e of t){const t=toContentBlock(e);t&&o.push(t)}if(o.length>0)return appendFileBlocks(o,n)}}const i=asStr(e.output)??asStr(e.text)??asStr(e.result)??r??asStr(e.message)??asStr(e.error);return i&&o.push({type:"content",content:{type:"text",text:i}}),appendFileBlocks(o,n)}function buildBlocksFromJsonText(t,e){const o=e.trim();if(!o||!o.startsWith("{")&&!o.startsWith("["))return[];try{return buildToolResultSessionContentBlocks(t,JSON.parse(o))}catch{return[]}}function buildFileResourceBlocks(t){if(!Array.isArray(t.files))return[];const e=[];for(const o of t.files){if(!isRecord(o))continue;const t=asStr(o.fileId)??asStr(o.file_id),n=t?`beeos-file://${t}`:asStr(o.uri)??asStr(o.downloadUrl)??asStr(o.url);if(!n)continue;const r=asStr(o.name)??asStr(o.fileName)??asStr(o.file_name),l=asStr(o.mimeType)??asStr(o.mime_type);e.push({type:"content",content:{type:"resource_link",uri:n,...r?{name:r}:{},...l?{mimeType:l}:{}}})}return e}function appendFileBlocks(t,e){if(0===e.length)return t;const o=new Set(e.map(t=>t.content.uri).filter(Boolean));return[...t.filter(t=>!("text"===t.content.type&&t.content.text&&o.has(t.content.text))),...e]}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),o=asStr(t.uri);return e||o?{type:"content",content:{type:"image",...e?{data:e}:{},...o?{uri:o}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
@@ -1 +1 @@
1
- import{describe,expect,it}from"vitest";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";describe("buildToolResultSessionContentBlocks",()=>{it("converts structured files to resource_link blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("does not output duplicate raw URI text when result equals the file URI",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toHaveLength(1),expect(e[0]?.content.type).toBe("resource_link")}),it("keeps ordinary result text alongside structured files",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"Screenshot captured.",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("appends files even when the payload also has typed content blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:[{type:"text",text:"Screenshot captured."}],files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})});
1
+ import{describe,expect,it}from"vitest";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";describe("buildToolResultSessionContentBlocks",()=>{it("converts structured files to resource_link blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("does not output duplicate raw URI text when result equals the file URI",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toHaveLength(1),expect(e[0]?.content.type).toBe("resource_link")}),it("keeps ordinary result text alongside structured files",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"Screenshot captured.",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("appends files even when the payload also has typed content blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:[{type:"text",text:"Screenshot captured."}],files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("extracts structured files from a JSON string content wrapper",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})});
@@ -1 +1 @@
1
- import{HEARTBEAT_TOKEN,isSilentReplyPrefixText,stripHeartbeatToken}from"./auto-reply-tokens.js";const TIMESTAMP_PREFIX_RE=/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}(?::\d{2})?\s+GMT[+-]\d{1,2}(?::?\d{2})?]\s*/i,WORKDIR_PREFIX_RE=/^\s*\[working directory:[^\]]*]\s*/i,MESSAGE_ID_PREFIX_RE=/^\s*\[message_id:[^\]]*]\s*/i,USER_MSG_PREFIX_RE=/^\s*User Message From \w+:\s*/i,HEARTBEAT_OK_LINE=HEARTBEAT_TOKEN,HEARTBEAT_OK_LINE_RE=/^[\s\p{P}\p{S}]*HEARTBEAT_OK[\s\p{P}\p{S}]*$/u,METADATA_PREFIX_PATTERNS=[TIMESTAMP_PREFIX_RE,WORKDIR_PREFIX_RE,MESSAGE_ID_PREFIX_RE,USER_MSG_PREFIX_RE],UNTRUSTED_METADATA_BLOCK_RE=/^Conversation info \(untrusted metadata\):[\s\S]*?Current time:\s*.*?$/i,UNTRUSTED_BLOCK_MARKER="Conversation info (untrusted metadata):",SENDER_UNTRUSTED_MARKER="Sender (untrusted metadata):",SENDER_UNTRUSTED_BLOCK_RE=/Sender\s*\(untrusted[^)]*\)\s*:\s*```[a-z]*\s*[\s\S]*?```/i;function stripMetadataPrefixesFromLine(e){let t=e,r=!1,n=!0;for(;n;){n=!1;for(const e of METADATA_PREFIX_PATTERNS)e.test(t)&&(t=t.replace(e,""),r=!0,n=!0)}return{line:t,changed:r}}export function stripTransportMetadata(e){const t=e.includes(UNTRUSTED_BLOCK_MARKER),r=e.includes(SENDER_UNTRUSTED_MARKER);if(!(e.includes("[")||e.includes(HEARTBEAT_OK_LINE)||e.includes("User Message From")||t||r))return e;if(r){const t=e.replace(SENDER_UNTRUSTED_BLOCK_RE,"").replace(/^\n+/,"").trim();if(t!==e)return t?stripTransportMetadata(t):"";const r=e.indexOf(SENDER_UNTRUSTED_MARKER),n=e.indexOf("\n\n",r),i=-1===n?e.length:n,s=e.slice(i).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}if(t){const t=e.indexOf(UNTRUSTED_BLOCK_MARKER),r=/\nCurrent time:[^\n]*/i.exec(e.slice(t));if(r){const n=t+r.index+r[0].length,i=e.slice(n).replace(/^\n+/,"").trim();return i?stripTransportMetadata(i):""}const n=e.indexOf("\n\n",t),i=-1===n?e.length:n,s=e.slice(i).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}const n=e.split(/\r?\n/),i=[];let s=!1;for(const e of n){const t=stripMetadataPrefixesFromLine(e);HEARTBEAT_OK_LINE_RE.test(t.line.trim())?s=!0:t.changed&&(s=!0,!t.line.trim())||i.push(t.line)}return s?i.join("\n").replace(/^\n+/,"").replace(/\n+$/,""):e}export function normalizeTransportText(e){if(!e)return;const t=stripTransportMetadata(e);return t.length>0?t:void 0}export function isHeartbeatOnlyText(e){const t=e.trim();if(!t)return!1;if(HEARTBEAT_OK_LINE_RE.test(t))return!0;if(t.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(t,{mode:"message"}).shouldSkip)return!0;if(0===stripTransportMetadata(t).trim().length)return!0}return!1}export function isHeartbeatPrefixText(e){if(!e)return!1;const t=e.trim();return!!t&&(!(t.length>=HEARTBEAT_OK_LINE.length)&&isSilentReplyPrefixText(t,HEARTBEAT_OK_LINE))}export function isHeartbeatFullOrPrefix(e){return"none"!==classifyHeartbeatFilter(e).reason}export function previewFilteredText(e,t=120){if(!e)return"";const r=e.replace(/\\/g,"\\\\").replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/\s+/g," ").trim();if(r.length<=t)return r;const n=Math.floor((t-1)/2),i=t-1-n;return`${r.slice(0,n)}…${r.slice(-i)}`}export function classifyHeartbeatFilter(e){if(!e)return{reason:"none",filtered:!1};const t=e.trim();if(!t)return{reason:"none",filtered:!1};if(HEARTBEAT_OK_LINE_RE.test(t))return{reason:"full",filtered:!0};if(t.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(t,{mode:"message"}).shouldSkip)return{reason:"full-stripped",filtered:!0};if(0===stripTransportMetadata(t).trim().length)return{reason:"full-metadata-residue",filtered:!0}}return isHeartbeatPrefixText(t)?{reason:"prefix",filtered:!0}:{reason:"none",filtered:!1}}const HEARTBEAT_PROMPT_PREFIX="read heartbeat.md if it exists";export function isHeartbeatPromptText(e){return e.slice(0,80).toLowerCase().startsWith(HEARTBEAT_PROMPT_PREFIX)}const CRON_PROMPT_PREFIX_RE=/^\[cron:([0-9a-f-]{8,})\s/i;export function isCronPromptText(e){return CRON_PROMPT_PREFIX_RE.test(e.trimStart())}export function extractCronJobId(e){return CRON_PROMPT_PREFIX_RE.exec(e.trimStart())?.[1]}function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function sanitizeNode(e){if("string"==typeof e){const t=stripTransportMetadata(e);return{value:t,changed:t!==e}}if(Array.isArray(e)){let t=!1;const r=e.map(e=>{const r=sanitizeNode(e);return r.changed&&(t=!0),r.value});return{value:t?r:e,changed:t}}if(!isRecord(e))return{value:e,changed:!1};let t=!1,r=e;if("string"==typeof e.type&&"text"===e.type&&"string"==typeof e.text){const n=stripTransportMetadata(e.text);n!==e.text&&(r={...r,text:n},t=!0)}if(Array.isArray(e.content)){const n=sanitizeNode(e.content);n.changed&&(t||(r={...r}),r.content=n.value,t=!0)}return{value:t?r:e,changed:t}}export function sanitizePromptPayload(e){return sanitizeNode(e).value}
1
+ import{HEARTBEAT_TOKEN,isSilentReplyPrefixText,stripHeartbeatToken}from"./auto-reply-tokens.js";const TIMESTAMP_PREFIX_RE=/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}(?::\d{2})?\s+GMT[+-]\d{1,2}(?::?\d{2})?]\s*/i,WORKDIR_PREFIX_RE=/^\s*\[working directory:[^\]]*]\s*/i,MESSAGE_ID_PREFIX_RE=/^\s*\[message_id:[^\]]*]\s*/i,USER_MSG_PREFIX_RE=/^\s*User Message From \w+:\s*/i,HEARTBEAT_OK_LINE=HEARTBEAT_TOKEN,HEARTBEAT_OK_LINE_RE=/^[\s\p{P}\p{S}]*HEARTBEAT_OK[\s\p{P}\p{S}]*$/u,UNSUPPORTED_BEEOS_MEDIA_DIRECTIVE_RE=/^\s*MEDIA\s*:\s*beeos-file:\/\/[^\s)]+[^\S\r\n]*$/i,METADATA_PREFIX_PATTERNS=[TIMESTAMP_PREFIX_RE,WORKDIR_PREFIX_RE,MESSAGE_ID_PREFIX_RE,USER_MSG_PREFIX_RE],UNTRUSTED_METADATA_BLOCK_RE=/^Conversation info \(untrusted metadata\):[\s\S]*?Current time:\s*.*?$/i,UNTRUSTED_BLOCK_MARKER="Conversation info (untrusted metadata):",SENDER_UNTRUSTED_MARKER="Sender (untrusted metadata):",SENDER_UNTRUSTED_BLOCK_RE=/Sender\s*\(untrusted[^)]*\)\s*:\s*```[a-z]*\s*[\s\S]*?```/i;function stripMetadataPrefixesFromLine(e){let t=e,r=!1,n=!0;for(;n;){n=!1;for(const e of METADATA_PREFIX_PATTERNS)e.test(t)&&(t=t.replace(e,""),r=!0,n=!0)}return{line:t,changed:r}}export function stripTransportMetadata(e){const t=e.includes(UNTRUSTED_BLOCK_MARKER),r=e.includes(SENDER_UNTRUSTED_MARKER);if(!(e.includes("[")||e.includes(HEARTBEAT_OK_LINE)||/MEDIA\s*:/i.test(e)||e.includes("User Message From")||t||r))return e;if(r){const t=e.replace(SENDER_UNTRUSTED_BLOCK_RE,"").replace(/^\n+/,"").trim();if(t!==e)return t?stripTransportMetadata(t):"";const r=e.indexOf(SENDER_UNTRUSTED_MARKER),n=e.indexOf("\n\n",r),i=-1===n?e.length:n,s=e.slice(i).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}if(t){const t=e.indexOf(UNTRUSTED_BLOCK_MARKER),r=/\nCurrent time:[^\n]*/i.exec(e.slice(t));if(r){const n=t+r.index+r[0].length,i=e.slice(n).replace(/^\n+/,"").trim();return i?stripTransportMetadata(i):""}const n=e.indexOf("\n\n",t),i=-1===n?e.length:n,s=e.slice(i).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}const n=e.split(/\r?\n/),i=[];let s=!1;for(const e of n){const t=stripMetadataPrefixesFromLine(e);HEARTBEAT_OK_LINE_RE.test(t.line.trim())?s=!0:UNSUPPORTED_BEEOS_MEDIA_DIRECTIVE_RE.test(t.line)?s=!0:t.changed&&(s=!0,!t.line.trim())||i.push(t.line)}return s?i.join("\n").replace(/^\n+/,"").replace(/\n+$/,""):e}export function normalizeTransportText(e){if(!e)return;const t=stripTransportMetadata(e);return t.length>0?t:void 0}export function isHeartbeatOnlyText(e){const t=e.trim();if(!t)return!1;if(HEARTBEAT_OK_LINE_RE.test(t))return!0;if(t.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(t,{mode:"message"}).shouldSkip)return!0;if(0===stripTransportMetadata(t).trim().length)return!0}return!1}export function isHeartbeatPrefixText(e){if(!e)return!1;const t=e.trim();return!!t&&(!(t.length>=HEARTBEAT_OK_LINE.length)&&isSilentReplyPrefixText(t,HEARTBEAT_OK_LINE))}export function isHeartbeatFullOrPrefix(e){return"none"!==classifyHeartbeatFilter(e).reason}export function previewFilteredText(e,t=120){if(!e)return"";const r=e.replace(/\\/g,"\\\\").replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/\s+/g," ").trim();if(r.length<=t)return r;const n=Math.floor((t-1)/2),i=t-1-n;return`${r.slice(0,n)}…${r.slice(-i)}`}export function classifyHeartbeatFilter(e){if(!e)return{reason:"none",filtered:!1};const t=e.trim();if(!t)return{reason:"none",filtered:!1};if(HEARTBEAT_OK_LINE_RE.test(t))return{reason:"full",filtered:!0};if(t.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(t,{mode:"message"}).shouldSkip)return{reason:"full-stripped",filtered:!0};if(0===stripTransportMetadata(t).trim().length)return{reason:"full-metadata-residue",filtered:!0}}return isHeartbeatPrefixText(t)?{reason:"prefix",filtered:!0}:{reason:"none",filtered:!1}}const HEARTBEAT_PROMPT_PREFIX="read heartbeat.md if it exists";export function isHeartbeatPromptText(e){return e.slice(0,80).toLowerCase().startsWith(HEARTBEAT_PROMPT_PREFIX)}const CRON_PROMPT_PREFIX_RE=/^\[cron:([0-9a-f-]{8,})\s/i;export function isCronPromptText(e){return CRON_PROMPT_PREFIX_RE.test(e.trimStart())}export function extractCronJobId(e){return CRON_PROMPT_PREFIX_RE.exec(e.trimStart())?.[1]}function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function sanitizeNode(e){if("string"==typeof e){const t=stripTransportMetadata(e);return{value:t,changed:t!==e}}if(Array.isArray(e)){let t=!1;const r=e.map(e=>{const r=sanitizeNode(e);return r.changed&&(t=!0),r.value});return{value:t?r:e,changed:t}}if(!isRecord(e))return{value:e,changed:!1};let t=!1,r=e;if("string"==typeof e.type&&"text"===e.type&&"string"==typeof e.text){const n=stripTransportMetadata(e.text);n!==e.text&&(r={...r,text:n},t=!0)}if(Array.isArray(e.content)){const n=sanitizeNode(e.content);n.changed&&(t||(r={...r}),r.content=n.value,t=!0)}return{value:t?r:e,changed:t}}export function sanitizePromptPayload(e){return sanitizeNode(e).value}
@@ -1 +1 @@
1
- import{describe,it,expect}from"vitest";import{isCronPromptText,isHeartbeatPromptText,isHeartbeatOnlyText,isHeartbeatPrefixText,isHeartbeatFullOrPrefix,classifyHeartbeatFilter,previewFilteredText}from"./message-filter.js";describe("isCronPromptText",()=>{it("matches canonical upstream cron prefix",()=>{expect(isCronPromptText("[cron:3f50c57c-e9ee-420c-acb4-cc80c1dbb15d daily-summary] please summarize")).toBe(!0)}),it("matches case-insensitively and tolerates leading whitespace",()=>{expect(isCronPromptText("\n [CRON:abcdef12-3456-7890-abcd-ef0123456789 job] run")).toBe(!0)}),it("rejects text without the cron prefix",()=>{expect(isCronPromptText("hello, please run my task")).toBe(!1),expect(isCronPromptText("[cron] missing id")).toBe(!1),expect(isCronPromptText("")).toBe(!1)}),it("does not collide with heartbeat prompts",()=>{const e="Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.";expect(isHeartbeatPromptText(e)).toBe(!0),expect(isCronPromptText(e)).toBe(!1)})}),describe("isHeartbeatOnlyText",()=>{it("detects bare token",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK")).toBe(!0)}),it("detects token with surrounding whitespace",()=>{expect(isHeartbeatOnlyText(" HEARTBEAT_OK ")).toBe(!0),expect(isHeartbeatOnlyText("\n\nHEARTBEAT_OK\n\n")).toBe(!0)}),it("detects token with trailing punctuation (OpenClaw rule: up to 4 non-word chars)",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK.")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK!")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK!!!")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK ✅")).toBe(!0)}),it("detects token wrapped in markdown markup",()=>{expect(isHeartbeatOnlyText("**HEARTBEAT_OK**")).toBe(!0),expect(isHeartbeatOnlyText("`HEARTBEAT_OK`")).toBe(!0),expect(isHeartbeatOnlyText("_HEARTBEAT_OK_")).toBe(!0)}),it("detects token sitting next to stripped transport metadata",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK\n[working directory: /home/node]\n[message_id: abc-123]")).toBe(!0)}),it("rejects text that merely mentions the token inline",()=>{expect(isHeartbeatOnlyText("Use the sentinel HEARTBEAT_OK in your ack handler.")).toBe(!1),expect(isHeartbeatOnlyText("The response should be HEARTBEAT_OK always.")).toBe(!1)}),it("rejects empty and pure-whitespace input",()=>{expect(isHeartbeatOnlyText("")).toBe(!1),expect(isHeartbeatOnlyText(" \n\t ")).toBe(!1)})}),describe("isHeartbeatPrefixText",()=>{it("flags the real-world prefix that leaked to staging",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_")).toBe(!0)}),it("flags every meaningful uppercase prefix of HEARTBEAT_OK that includes '_'",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_O")).toBe(!0)}),it("does NOT flag prefixes without '_' (upstream trade-off, avoids eating real words)",()=>{expect(isHeartbeatPrefixText("HEART")).toBe(!1),expect(isHeartbeatPrefixText("HEARTBEAT")).toBe(!1)}),it("rejects length < 2",()=>{expect(isHeartbeatPrefixText("H")).toBe(!1),expect(isHeartbeatPrefixText("")).toBe(!1)}),it("rejects mixed case (avoids eating conversational text)",()=>{expect(isHeartbeatPrefixText("heartbeat_")).toBe(!1),expect(isHeartbeatPrefixText("Heartbeat_")).toBe(!1)}),it("rejects text with non-[A-Z_] characters",()=>{expect(isHeartbeatPrefixText("HEARTBEAT!")).toBe(!1),expect(isHeartbeatPrefixText("HEART-BEAT")).toBe(!1),expect(isHeartbeatPrefixText("HEARTBEAT_OK TRAILING")).toBe(!1)}),it("does NOT flag the full token (that's isHeartbeatOnlyText's job)",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_OK")).toBe(!1)}),it("tolerates leading whitespace",()=>{expect(isHeartbeatPrefixText(" HEARTBEAT_")).toBe(!0)})}),describe("isHeartbeatFullOrPrefix — exactly matches OpenClaw semantics",()=>{it("catches the full token",()=>{expect(isHeartbeatFullOrPrefix("HEARTBEAT_OK")).toBe(!0),expect(isHeartbeatFullOrPrefix("HEARTBEAT_OK.")).toBe(!0),expect(isHeartbeatFullOrPrefix("**HEARTBEAT_OK**")).toBe(!0)}),it("catches prefixes that contain '_'",()=>{expect(isHeartbeatFullOrPrefix("HEARTBEAT_")).toBe(!0),expect(isHeartbeatFullOrPrefix("HEARTBEAT_O")).toBe(!0)}),it("intentionally does NOT catch prefixes without '_' (upstream trade-off)",()=>{expect(isHeartbeatFullOrPrefix("HEART")).toBe(!1),expect(isHeartbeatFullOrPrefix("HEARTBEAT")).toBe(!1),expect(isHeartbeatFullOrPrefix("HE")).toBe(!1)}),it("lets real user content through",()=>{expect(isHeartbeatFullOrPrefix("Let's design a HEARTBEAT_OK-like ack for our API.")).toBe(!1),expect(isHeartbeatFullOrPrefix("Hello, how are you?")).toBe(!1),expect(isHeartbeatFullOrPrefix("Heart of darkness is a novel")).toBe(!1)})}),describe("classifyHeartbeatFilter — structured drop reasons for logs",()=>{it("reports `full` on canonical tokens",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK")).toEqual({reason:"full",filtered:!0}),expect(classifyHeartbeatFilter("HEARTBEAT_OK.").reason).toBe("full"),expect(classifyHeartbeatFilter("**HEARTBEAT_OK**").reason).toBe("full"),expect(classifyHeartbeatFilter("`HEARTBEAT_OK`").reason).toBe("full")}),it("reports `full-stripped` as a defensive fallback when regex misses but stripHeartbeatToken catches",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK ✅").reason).toBe("full")}),it("reports `full-metadata-residue` when the token is surrounded by transport metadata",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK\n[working directory: /home/node]\n[message_id: abc-123]").reason).toBe("full-metadata-residue")}),it("reports `prefix` on uppercase prefixes that contain '_' (upstream rule)",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_").reason).toBe("prefix"),expect(classifyHeartbeatFilter("HEARTBEAT_O").reason).toBe("prefix")}),it("reports `none` for uppercase prefixes WITHOUT '_' (upstream trade-off — avoids eating real words)",()=>{expect(classifyHeartbeatFilter("HEART").reason).toBe("none"),expect(classifyHeartbeatFilter("HEARTBEAT").reason).toBe("none"),expect(classifyHeartbeatFilter("HE").reason).toBe("none")}),it("reports `none` for real content",()=>{expect(classifyHeartbeatFilter("Hello").reason).toBe("none"),expect(classifyHeartbeatFilter("Use HEARTBEAT_OK to reply.").reason).toBe("none"),expect(classifyHeartbeatFilter("").reason).toBe("none"),expect(classifyHeartbeatFilter(" ").reason).toBe("none")}),it("keeps `filtered` in sync with `reason` (never drifts)",()=>{for(const e of["HEARTBEAT_OK","**HEARTBEAT_OK**","HEARTBEAT_","HE","HEART","hello",""]){const t=classifyHeartbeatFilter(e);expect(t.filtered).toBe("none"!==t.reason)}})}),describe("previewFilteredText",()=>{it("returns empty string on empty input",()=>{expect(previewFilteredText("")).toBe("")}),it("escapes newlines / tabs and collapses runs of whitespace",()=>{expect(previewFilteredText("HEARTBEAT_OK\n[meta]")).toBe("HEARTBEAT_OK\\n[meta]"),expect(previewFilteredText("a\t\tb")).toBe("a\\t\\tb"),expect(previewFilteredText("a b c")).toBe("a b c")}),it("returns the full text when within maxLen",()=>{expect(previewFilteredText("HEARTBEAT_OK",20)).toBe("HEARTBEAT_OK")}),it("keeps head + tail with … when longer than maxLen",()=>{const e="A".repeat(50)+"HEARTBEAT_OK"+"B".repeat(50),t=previewFilteredText(e,40);expect(t.length).toBeLessThanOrEqual(40),expect(t).toContain("…"),expect(t.startsWith("A")).toBe(!0),expect(t.endsWith("B")).toBe(!0)})});
1
+ import{describe,it,expect}from"vitest";import{isCronPromptText,isHeartbeatPromptText,isHeartbeatOnlyText,isHeartbeatPrefixText,isHeartbeatFullOrPrefix,classifyHeartbeatFilter,normalizeTransportText,previewFilteredText}from"./message-filter.js";describe("isCronPromptText",()=>{it("matches canonical upstream cron prefix",()=>{expect(isCronPromptText("[cron:3f50c57c-e9ee-420c-acb4-cc80c1dbb15d daily-summary] please summarize")).toBe(!0)}),it("matches case-insensitively and tolerates leading whitespace",()=>{expect(isCronPromptText("\n [CRON:abcdef12-3456-7890-abcd-ef0123456789 job] run")).toBe(!0)}),it("rejects text without the cron prefix",()=>{expect(isCronPromptText("hello, please run my task")).toBe(!1),expect(isCronPromptText("[cron] missing id")).toBe(!1),expect(isCronPromptText("")).toBe(!1)}),it("does not collide with heartbeat prompts",()=>{const e="Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.";expect(isHeartbeatPromptText(e)).toBe(!0),expect(isCronPromptText(e)).toBe(!1)})}),describe("isHeartbeatOnlyText",()=>{it("detects bare token",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK")).toBe(!0)}),it("detects token with surrounding whitespace",()=>{expect(isHeartbeatOnlyText(" HEARTBEAT_OK ")).toBe(!0),expect(isHeartbeatOnlyText("\n\nHEARTBEAT_OK\n\n")).toBe(!0)}),it("detects token with trailing punctuation (OpenClaw rule: up to 4 non-word chars)",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK.")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK!")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK!!!")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK ✅")).toBe(!0)}),it("detects token wrapped in markdown markup",()=>{expect(isHeartbeatOnlyText("**HEARTBEAT_OK**")).toBe(!0),expect(isHeartbeatOnlyText("`HEARTBEAT_OK`")).toBe(!0),expect(isHeartbeatOnlyText("_HEARTBEAT_OK_")).toBe(!0)}),it("detects token sitting next to stripped transport metadata",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK\n[working directory: /home/node]\n[message_id: abc-123]")).toBe(!0)}),it("rejects text that merely mentions the token inline",()=>{expect(isHeartbeatOnlyText("Use the sentinel HEARTBEAT_OK in your ack handler.")).toBe(!1),expect(isHeartbeatOnlyText("The response should be HEARTBEAT_OK always.")).toBe(!1)}),it("rejects empty and pure-whitespace input",()=>{expect(isHeartbeatOnlyText("")).toBe(!1),expect(isHeartbeatOnlyText(" \n\t ")).toBe(!1)})}),describe("normalizeTransportText",()=>{it("strips unsupported standalone BeeOS MEDIA directives",()=>{expect(normalizeTransportText("MEDIA:beeos-file://abc")).toBeUndefined(),expect(normalizeTransportText("media: beeos-file://abc")).toBeUndefined()}),it("removes MEDIA directive lines while preserving normal assistant text",()=>{expect(normalizeTransportText("Here's the screenshot from your device:\n\nMEDIA:beeos-file://abc")).toBe("Here's the screenshot from your device:")}),it("does not strip inline discussion of MEDIA directives",()=>{expect(normalizeTransportText("The invalid form is MEDIA:beeos-file://abc.")).toBe("The invalid form is MEDIA:beeos-file://abc.")})}),describe("isHeartbeatPrefixText",()=>{it("flags the real-world prefix that leaked to staging",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_")).toBe(!0)}),it("flags every meaningful uppercase prefix of HEARTBEAT_OK that includes '_'",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_O")).toBe(!0)}),it("does NOT flag prefixes without '_' (upstream trade-off, avoids eating real words)",()=>{expect(isHeartbeatPrefixText("HEART")).toBe(!1),expect(isHeartbeatPrefixText("HEARTBEAT")).toBe(!1)}),it("rejects length < 2",()=>{expect(isHeartbeatPrefixText("H")).toBe(!1),expect(isHeartbeatPrefixText("")).toBe(!1)}),it("rejects mixed case (avoids eating conversational text)",()=>{expect(isHeartbeatPrefixText("heartbeat_")).toBe(!1),expect(isHeartbeatPrefixText("Heartbeat_")).toBe(!1)}),it("rejects text with non-[A-Z_] characters",()=>{expect(isHeartbeatPrefixText("HEARTBEAT!")).toBe(!1),expect(isHeartbeatPrefixText("HEART-BEAT")).toBe(!1),expect(isHeartbeatPrefixText("HEARTBEAT_OK TRAILING")).toBe(!1)}),it("does NOT flag the full token (that's isHeartbeatOnlyText's job)",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_OK")).toBe(!1)}),it("tolerates leading whitespace",()=>{expect(isHeartbeatPrefixText(" HEARTBEAT_")).toBe(!0)})}),describe("isHeartbeatFullOrPrefix — exactly matches OpenClaw semantics",()=>{it("catches the full token",()=>{expect(isHeartbeatFullOrPrefix("HEARTBEAT_OK")).toBe(!0),expect(isHeartbeatFullOrPrefix("HEARTBEAT_OK.")).toBe(!0),expect(isHeartbeatFullOrPrefix("**HEARTBEAT_OK**")).toBe(!0)}),it("catches prefixes that contain '_'",()=>{expect(isHeartbeatFullOrPrefix("HEARTBEAT_")).toBe(!0),expect(isHeartbeatFullOrPrefix("HEARTBEAT_O")).toBe(!0)}),it("intentionally does NOT catch prefixes without '_' (upstream trade-off)",()=>{expect(isHeartbeatFullOrPrefix("HEART")).toBe(!1),expect(isHeartbeatFullOrPrefix("HEARTBEAT")).toBe(!1),expect(isHeartbeatFullOrPrefix("HE")).toBe(!1)}),it("lets real user content through",()=>{expect(isHeartbeatFullOrPrefix("Let's design a HEARTBEAT_OK-like ack for our API.")).toBe(!1),expect(isHeartbeatFullOrPrefix("Hello, how are you?")).toBe(!1),expect(isHeartbeatFullOrPrefix("Heart of darkness is a novel")).toBe(!1)})}),describe("classifyHeartbeatFilter — structured drop reasons for logs",()=>{it("reports `full` on canonical tokens",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK")).toEqual({reason:"full",filtered:!0}),expect(classifyHeartbeatFilter("HEARTBEAT_OK.").reason).toBe("full"),expect(classifyHeartbeatFilter("**HEARTBEAT_OK**").reason).toBe("full"),expect(classifyHeartbeatFilter("`HEARTBEAT_OK`").reason).toBe("full")}),it("reports `full-stripped` as a defensive fallback when regex misses but stripHeartbeatToken catches",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK ✅").reason).toBe("full")}),it("reports `full-metadata-residue` when the token is surrounded by transport metadata",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK\n[working directory: /home/node]\n[message_id: abc-123]").reason).toBe("full-metadata-residue")}),it("reports `prefix` on uppercase prefixes that contain '_' (upstream rule)",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_").reason).toBe("prefix"),expect(classifyHeartbeatFilter("HEARTBEAT_O").reason).toBe("prefix")}),it("reports `none` for uppercase prefixes WITHOUT '_' (upstream trade-off — avoids eating real words)",()=>{expect(classifyHeartbeatFilter("HEART").reason).toBe("none"),expect(classifyHeartbeatFilter("HEARTBEAT").reason).toBe("none"),expect(classifyHeartbeatFilter("HE").reason).toBe("none")}),it("reports `none` for real content",()=>{expect(classifyHeartbeatFilter("Hello").reason).toBe("none"),expect(classifyHeartbeatFilter("Use HEARTBEAT_OK to reply.").reason).toBe("none"),expect(classifyHeartbeatFilter("").reason).toBe("none"),expect(classifyHeartbeatFilter(" ").reason).toBe("none")}),it("keeps `filtered` in sync with `reason` (never drifts)",()=>{for(const e of["HEARTBEAT_OK","**HEARTBEAT_OK**","HEARTBEAT_","HE","HEART","hello",""]){const t=classifyHeartbeatFilter(e);expect(t.filtered).toBe("none"!==t.reason)}})}),describe("previewFilteredText",()=>{it("returns empty string on empty input",()=>{expect(previewFilteredText("")).toBe("")}),it("escapes newlines / tabs and collapses runs of whitespace",()=>{expect(previewFilteredText("HEARTBEAT_OK\n[meta]")).toBe("HEARTBEAT_OK\\n[meta]"),expect(previewFilteredText("a\t\tb")).toBe("a\\t\\tb"),expect(previewFilteredText("a b c")).toBe("a b c")}),it("returns the full text when within maxLen",()=>{expect(previewFilteredText("HEARTBEAT_OK",20)).toBe("HEARTBEAT_OK")}),it("keeps head + tail with … when longer than maxLen",()=>{const e="A".repeat(50)+"HEARTBEAT_OK"+"B".repeat(50),t=previewFilteredText(e,40);expect(t.length).toBeLessThanOrEqual(40),expect(t).toContain("…"),expect(t.startsWith("A")).toBe(!0),expect(t.endsWith("B")).toBe(!0)})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.5.27",
3
+ "version": "0.5.29",
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>",