beeos-claw 0.5.39 → 0.5.41-staging.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{stripLeadingSenderMetadataBlocks,stripTransportMetadata}from"../message-sanitization/transport-metadata-sanitizer.js";const USER_ENVELOPE_MARKER_RE=/\b(?:User Message From|Message From)\s+([A-Za-z0-9_-]+)(?:\s+Group Chat Room)?:\s*/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/;function normalizeLineEndings(t){return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")}function stripCanvasPrelude(t){let e=t;return e=e.replace(CANVAS_STATE_RE,"").trimStart(),e=e.replace(SYSTEM_NOTE_RE,"").trimStart(),e}function isTransportOnlyPrefix(t){return!t.trim()||0===stripTransportMetadata(t).trim().length}export function extractBeeosUserTextFromHistoryContent(t){const e=normalizeLineEndings(t),r=stripCanvasPrelude(stripLeadingSenderMetadataBlocks(e).trimStart()),n=USER_ENVELOPE_MARKER_RE.exec(r);if(n&&isTransportOnlyPrefix(r.slice(0,n.index))){const t=r.slice(n.index+n[0].length).trim();return{text:t,sanitized:t!==e,markerSource:n[1],beforeLength:e.length,afterLength:t.length}}const a=stripTransportMetadata(r).trim();return{text:a,sanitized:a!==e,beforeLength:e.length,afterLength:a.length}}
@@ -0,0 +1 @@
1
+ import{describe,expect,it}from"vitest";import{extractBeeosUserTextFromHistoryContent}from"./beeos-user-envelope.js";describe("extractBeeosUserTextFromHistoryContent",()=>{it("extracts production UTC same-line BeeOS user envelopes",()=>{expect(extractBeeosUserTextFromHistoryContent("[Tue 2026-07-07 12:07 UTC] User Message From BeeOS:\n今天天气怎么样?").text).toBe("今天天气怎么样?"),expect(extractBeeosUserTextFromHistoryContent("[Tue 2026-07-07 12:08 UTC] User Message From BeeOS:\n深圳").text).toBe("深圳")}),it("extracts after leading sender metadata blocks",()=>{const e=["Sender (untrusted metadata):","```json",'{"label":"BeeOS Platform Bridge (cli)"}',"```","[Tue 2026-07-07 12:07 UTC] User Message From BeeOS:","深圳"].join("\n"),t=extractBeeosUserTextFromHistoryContent(e);expect(t.text).toBe("深圳"),expect(t.sanitized).toBe(!0),expect(t.markerSource).toBe("BeeOS")}),it("keeps text after the real envelope marker even if the body mentions a marker",()=>{expect(extractBeeosUserTextFromHistoryContent("[Tue 2026-07-07 12:07 GMT+8] User Message From BeeOS:\n请解释 User Message From BeeOS: 这段文本").text).toBe("请解释 User Message From BeeOS: 这段文本")}),it("supports Kimi-style group chat markers",()=>{expect(extractBeeosUserTextFromHistoryContent("[Tue 2026-07-07 12:07 GMT+8] Message From Kimi Group Chat Room:\nhello").text).toBe("hello")}),it("does not cut ordinary user text that only mentions the marker",()=>{expect(extractBeeosUserTextFromHistoryContent("请把字符串 User Message From BeeOS:\n深圳 作为样例解释").text).toBe("请把字符串 User Message From BeeOS:\n深圳 作为样例解释")}),it("strips canvas and system preludes before envelope extraction",()=>{expect(extractBeeosUserTextFromHistoryContent("[Canvas State] open\n[System Note] note\nUser Message From BeeOS:\n打开文件").text).toBe("打开文件")})});
@@ -1 +1 @@
1
- import{randomUUID,createHash}from"node:crypto";import{mkdir,writeFile,readFile,rm,lstat,readlink,stat,readdir,rename,cp}from"node:fs/promises";import path from"node:path";import{join,resolve,relative,sep}from"node:path";import{homedir}from"node:os";import AdmZip from"adm-zip";import{sanitizePromptPayload,stripTransportMetadata,isHeartbeatPromptText,isCronPromptText,extractCronJobId,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{createCronOverridesStore}from"../cron-overrides.js";import{CRON_DURATION_FLOOR_MS}from"../config.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas/tools.js";import{parseMentionsParam,buildMentionInstruction}from"../delegation/mention-resolver.js";import{EXTERNAL_PROTOCOL_TAGS}from"./external-protocol-tags.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,loadHistoryPage,groupIntoTurns,splitUserReplayBlocks}from"./history-replay.js";import{LocalSessionHistory}from"./local-session-history.js";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{readEnrichmentCache,readUploadToolResult}from"../upload/result-cache.js";import{buildToolResultSessionContentBlocks}from"./tool-result-blocks.js";import{prefersChatFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.js";import{resolveTurnId,recordTurnIdForRole,recordToolCallTurnForUpdate}from"./gateway-events.js";import{emitToolResultProjectionForTurn}from"./tool-result-emitter.js";import{extractPersistedToolResultEnvelope,findPersistedToolResultEnvelopeByMessageId}from"./tool-result-transcript.js";import{projectPersistedToolResultForReplay}from"./tool-result-projection.js";import{McpService}from"./mcp-service.js";import{DEFAULT_PROMPT_TIMEOUT_MS}from"./types.js";import{ACP_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/acp";import{parseAndValidateTemplatePackage,TemplatePackageError,MAX_ZIP_BYTES}from"./agent-template-package.js";const AGENT_VALID_ID_RE=/^[a-z0-9][a-z0-9_-]{0,63}$/i,AGENT_INVALID_CHARS_RE=/[^a-z0-9_-]+/g,AGENT_LEADING_DASH_RE=/^-+/,AGENT_TRAILING_DASH_RE=/-+$/,DEFAULT_AGENT_ID="main";export function normalizeAgentId(e){const t=(e??"").trim();return t?AGENT_VALID_ID_RE.test(t)?t.toLowerCase():t.toLowerCase().replace(AGENT_INVALID_CHARS_RE,"-").replace(AGENT_LEADING_DASH_RE,"").replace(AGENT_TRAILING_DASH_RE,"").slice(0,64)||"main":"main"}function narrowContentBlock(e){switch(e.type){case"text":return"string"==typeof e.text?{type:"text",text:e.text}:void 0;case"resource_link":return"string"==typeof e.uri?{type:"resource_link",uri:e.uri,...void 0!==e.name?{name:e.name}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}}:void 0;case"file":return{type:"file",...void 0!==e.text?{text:e.text}:{},...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};case"image":return{type:"image",...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};default:return}}const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i,HISTORY_PAGE_DEFAULT_LIMIT=100,HISTORY_PAGE_MAX_LIMIT=1e3;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-",...EXTERNAL_PROTOCOL_TAGS];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function isPlaceholderTitle(e){return!e||"New chat"===e||(!!/^[0-9a-f]{6,12}\s*\(20\d{2}/i.test(e)||(!!/^Sender\s*\(untrusted/i.test(e)||!!/^Conversation\s+info\s*\(untrusted/i.test(e)))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function mergePersistedToolResultEnvelopes(e,t){if(e||t)return{toolName:e?.toolName||t?.toolName||"tool",...e?.toolCallId||t?.toolCallId?{toolCallId:e?.toolCallId??t?.toolCallId}:{},...e?.rawText??t?.rawText?{rawText:e?.rawText??t?.rawText}:{},...void 0!==e?.rawResult||void 0!==t?.rawResult?{rawResult:e?.rawResult??t?.rawResult}:{},...e?.timestamp||t?.timestamp?{timestamp:e?.timestamp??t?.timestamp}:{}}}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const r=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return r&&!r.includes("/")&&SLASH_COMMAND_NAME_RE.test(r)?r.toLowerCase():void 0}export const BEEOS_CHANNEL_TAG_RE=/<!-- beeos:channel=(.+?) -->/;export function encodeBeeosChannelTag(e,t){const s=`\x3c!-- beeos:channel=${t} --\x3e`,r=stripBeeosChannelTag(e);return r?`${r}\n${s}`:s}export function decodeBeeosChannelId(e){if(!e)return;const t=BEEOS_CHANNEL_TAG_RE.exec(e);return t?.[1]}export function stripBeeosChannelTag(e){return e?e.replace(/\n?<!-- beeos:channel=.+? -->/g,"").trim():""}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}function createSessionObject(e){const t=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:t,platformAgentId:e.resolvePlatformAgentId?.(t),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}class HistoryUpdateRecorder{transport;updates=[];constructor(e){this.transport=e}sendSessionUpdate(e,t,s){const r=this.transport.materializeSessionUpdate(t,s);return null!==r&&this.updates.push(r),!0}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;beeosFileResolver;agentFetch;agentGatewayUrl;triggerAgentSync;cronChannelCache=new Map;static CRON_CHANNEL_CACHE_MAX=256;localSessionHistory;mcp;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];_mappingUpdateCallbacks=[];cronIsolatedSessionKeys=new Set;cronOverrides;setAgentMapping(e){this.agentMapping=e;for(const t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];for(const e of this._mappingReadyCallbacks)e();this._mappingReadyCallbacks=[];for(const t of this._mappingUpdateCallbacks)t(e)}onceAgentMappingReady(e){this.agentMapping&&this.agentMapping.size>0?e():this._mappingReadyCallbacks.push(e)}onAgentMappingUpdated(e){this._mappingUpdateCallbacks.push(e),this.agentMapping&&this.agentMapping.size>0&&e(this.agentMapping)}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(t=>{this._syncResolvers.push(t);const s=setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e);"object"==typeof s&&"unref"in s&&s.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;onRunEnded=null;handleSlashCommand=null;onPromptStart=null;instanceId;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway,this.instanceId=e.instanceId||"unknown",this.thirdPartyChannels=e.thirdPartyChannels??new Set,this.beeosFileResolver=e.beeosFileResolver,this.agentFetch=e.agentFetch,this.agentGatewayUrl=e.agentGatewayUrl,this.triggerAgentSync=e.triggerAgentSync,this.localSessionHistory=new LocalSessionHistory({logger:this.logger}),this.cronOverrides=createCronOverridesStore(this.logger),this.mcp=new McpService({transport:this.transport,logger:this.logger,state:this.state}),this.transport.setSessionUpdateObserver((e,t,s)=>{const r=s?.turnId;if(!r)return;const n=this.state.sessions.get(e);n&&(recordTurnIdForRole(n,t,r),recordToolCallTurnForUpdate(n,t,r))})}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e,t={}){const{ensureVerbose:s=!0}=t;this.state.sessions.has(e.sessionId);this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e),s&&this.ensureVerboseFull(e),this.subscribeSessionMessages(e)}async ensureVerboseFull(e){if(e.verbosePatched)return;const t=this.getGateway();if(t?.isConnected){e.verbosePatched=!0;try{await t.sessionsPatch(e.gatewaySessionKey,{verboseLevel:"full"})}catch(t){e.verbosePatched=!1,this.logger.debug?.(`[bridge-core] ensureVerboseFull failed: ${t instanceof Error?t.message:t}`)}}}ensureSessionForKey(e){const t=this.state.findSessionByKey(e);if(t)return t;const s=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(s),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${s.sessionId} key=${e}`),s}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.sessionsList();if(!t||"object"!=typeof t)return;const s=t.sessions;if(!Array.isArray(s))return;let r=0;for(const e of s){const t=e,s=t.sessionKey??t.key??t.id??t.name;if(!s||"string"!=typeof s)continue;if(this.state.findSessionByKey(s))continue;const n=t.label||t.derivedTitle||t.title||void 0,i=n&&!isPlaceholderTitle(n)?n:void 0,o=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:i});this.registerSession(o,{ensureVerbose:!1}),r++}r>0&&this.logger.info?.(`[bridge-core] preloadSessions: loaded ${r} session(s) from gateway`)}catch(e){this.logger.debug?.("[bridge-core] preloadSessions failed:",e)}else this.logger.debug?.("[bridge-core] preloadSessions skipped: gateway not connected")}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});const t=e;try{switch(t.method){case"initialize":this.handleInitialize(t);break;case"ping":this.transport.sendResult(t.id,{pong:!0});break;case"session/new":await this.handleSessionNew(t);break;case"session/load":await this.handleSessionLoad(t);break;case"session/list":await this.handleSessionList(t);break;case"session/rename":this.handleSessionRename(t);break;case"session/delete":await this.handleSessionDelete(t);break;case"session/clear":await this.handleSessionClear(t);break;case"session/prompt":await this.handleSessionPrompt(t);break;case"session/set_mode":await this.handleSessionSetMode(t);break;case"session/set_model":await this.handleSessionSetModel(t);break;case"agent/create":await this.handleAgentCreate(t);break;case"agent/update":await this.handleAgentUpdate(t);break;case"agent/delete":await this.handleAgentDelete(t);break;case"agent/applyTemplate":await this.handleApplyTemplate(t);break;case"skills/list":await this.handleSkillsList(t);break;case"skills/install":await this.handleSkillsInstall(t);break;case"skills/uninstall":await this.handleSkillsUninstall(t);break;case"skills/update":await this.handleSkillsUpdate(t);break;case"models/list":await this.handleModelsList(t);break;case"session/cancel":this.handleSessionCancel(t.id,t.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(t);break;default:"_beeos/session/history"===e.method?await this.handleSessionHistoryPage(e):"mcp/list"===e.method?await this.mcp.handleList(e):"mcp/set"===e.method?await this.mcp.handleSet(e):"mcp/unset"===e.method?await this.mcp.handleUnset(e):e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSkillInstalled=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,r=this.state.resolveSessionFromParams(t);if(!r){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}r.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,multipleSessions:!0,sessionCapabilities:{"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled},promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,s="string"==typeof e.params?.cwd?e.params.cwd:".",r=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s});this.registerSession(r,{ensureVerbose:!1}),this.transport.sendResult(e.id,{sessionId:r.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t,agentId:r.agentId,platformAgentId:r.platformAgentId??null}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async subscribeSessionMessages(e){if(e.messageSubscriptionActive)return;const t=this.getGateway();if(t?.isConnected)try{await t.sessionsMessagesSubscribe(e.gatewaySessionKey),e.messageSubscriptionActive=!0,this.logger.debug?.(`[bridge-core] session.message subscribed sessionKey=${e.gatewaySessionKey}`)}catch(t){this.logger.debug?.(`[bridge-core] session.message subscribe failed key=${e.gatewaySessionKey}: ${t instanceof Error?t.message:t}`)}}async resubscribeAllSessionMessages(){for(const e of this.state.sessions.values())e.messageSubscriptionActive=!1,await this.subscribeSessionMessages(e)}handleSessionMessage(e){const t=e?.sessionKey,s=e?.messageId;if(!t||!s)return;const r=this.state.sessionsByGatewayKey.get(t);if(!r)return;const n=e.message?.role;if("toolResult"===n||"tool"===n)return void this.handlePersistedToolResultMessage(r,e).catch(e=>{this.logger.warn?.(`[bridge-core] persisted toolResult handling failed sessionKey=${t} messageId=${s}: ${e instanceof Error?e.message:String(e)}`)});const i="user"===n?"user":"system"===n?"system":"assistant",o=r.lastTurnIdByRole?.[i];if(!o)return void this.logger.debug?.(`[bridge-core] session.message skipped: no lastTurnId role=${i} sessionKey=${t} messageId=${s}`);const a=e.message?.timestamp,l="number"==typeof a&&a>0?new Date(a).toISOString():void 0,d={turnId:o};"number"==typeof e.messageSeq&&(d.seq=e.messageSeq),this.transport.sendSessionUpdate(r.sessionId,{sessionUpdate:"message_id_bind",role:i,messageId:s,...l?{timestamp:l}:{}},d),r.lastTurnIdByRole&&(r.lastTurnIdByRole[i]=void 0),"assistant"===i&&reconcileMissingArtifacts({localHistory:this.localSessionHistory,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:()=>this.getCanvasState?.()??null,isSessionAlive:e=>this.state.sessions.has(e)},{session:r,turnId:o,messageId:s,runId:void 0})}async handlePersistedToolResultMessage(e,t){const s=t.messageId;if(!s)return;const r=extractPersistedToolResultEnvelope(t.message),n=this.extractPersistedToolResultFromHistory(e,s),i=mergePersistedToolResultEnvelopes(n,r);if(!i?.toolCallId)return void this.writeObs({component:"bridge-core",domain:"tool_result",name:"tool_result.skipped_no_tool_call_id",severity:"warn",payload:{sessionId:e.sessionId,messageId:s}});n&&this.writeObs({component:"bridge-core",domain:"tool_result",name:"tool_result.hydrated_from_history",severity:"debug",payload:{sessionId:e.sessionId,messageId:s,toolCallId:i.toolCallId,toolName:i.toolName}});const o=e.toolCallTurns?.get(i.toolCallId),a=o?.turnId??resolveTurnId(e);if(!a)return void this.writeObs({component:"bridge-core",domain:"tool_result",name:"tool_result.skipped_no_turn",severity:"warn",payload:{sessionId:e.sessionId,messageId:t.messageId,toolCallId:i.toolCallId,toolName:i.toolName}});const l=projectPersistedToolResultForReplay({toolCallId:i.toolCallId,toolName:i.toolName||o?.toolName||"tool",...i.rawText?{rawText:i.rawText}:{},...i.rawResult?{rawResult:i.rawResult}:{},...i.timestamp?{timestamp:i.timestamp}:{}});await emitToolResultProjectionForTurn({session:e,projection:l,toolCallId:i.toolCallId,turnId:a,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:()=>this.getCanvasState?.()??null,sendUpdate:s=>(a&&void 0===s._turnId&&(s._turnId=a),this.transport.sendSessionUpdate(e.sessionId,s,{turnId:a,..."number"==typeof t.messageSeq?{seq:t.messageSeq}:{}}))})}extractPersistedToolResultFromHistory(e,t){if(!e.agentId)return;const s=this.localSessionHistory.readEntries({agentId:e.agentId,sessionKey:e.gatewaySessionKey,sessionId:e.sessionId});return findPersistedToolResultEnvelopeByMessageId(s,t)}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const r=s.sessions;if(!Array.isArray(r))return;for(const t of r){const s=t,r=s.sessionKey??s.key??s.id??s.name;if(r&&"string"==typeof r&&deriveSessionId(r,this.instanceId)===e){const e=s.label||s.derivedTitle||s.title||void 0,t=e&&!isPlaceholderTitle(e)?e:void 0,n=createSessionObject({gatewayKey:r,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:t});return this.registerSession(n),n}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let t=this.state.sessions.get(e);if(!t&&e.startsWith("agent:")&&(t=this.state.findSessionByKey(e)),t||e.startsWith("agent:")||(t=await this.recoverSessionFromGateway(e)),t)return{session:t,created:!1};const s=e.startsWith("agent:"),r=this.config.gateway.agentId||this.getDefaultAgentId();return t=createSessionObject({gatewayKey:s?e:`agent:${r}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(t),{session:t,created:!0}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:s}=await this.resolveOrCreateSession(t),r=s.sessionId;this.state.lastActiveSessionId=r,s.chatReplayDedupKeys.clear(),s.sessionLoadInProgress=!0;try{const e=await this.emitHistoryTurns(s);this.emitCurrentStreamSnapshot(s,e)}finally{s.sessionLoadInProgress=!1}this.transport.sendResult(e.id,{sessionId:r}),this.onSessionLoaded?.(r,s.gatewaySessionKey,s.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})}async emitHistoryTurns(e){const t=new Set,s=e.sessionId;try{const r=this.getGateway(),n=e.assistantMq&&!e.assistantMq.closed,i=n?null:r,o=await loadHistory(i,e.gatewaySessionKey,100,{localHistory:this.localSessionHistory,agentId:e.agentId,sessionId:e.sessionId,logger:this.logger,emitObs:e=>{this.writeObs({component:"history-replay",domain:"lifecycle",name:e.name,severity:e.severity,payload:e.payload})}}),a=groupIntoTurns(o),l=a.filter(e=>"user"===e.role).length,d=a.filter(e=>"assistant"===e.role).length,c=a.filter(e=>e.toolCalls?.length).length,g=a.reduce((e,t)=>e+(t.toolCalls?.length??0),0);if(this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${s} messages=${o.length} turns=${a.length} userTurns=${l} asstTurns=${d} turnsWithToolCalls=${c} totalToolCalls=${g}`),l>0&&e.pendingUserTurnUpdates&&e.pendingUserTurnUpdates.length>0&&(e.pendingUserTurnUpdates=[]),a.length>0&&isPlaceholderTitle(e.title)&&"manual"!==e.titleSource){const t=a.find(e=>{if("user"!==e.role)return!1;if("internal_system"===e.provenance?.kind)return!1;if(isCronPromptText(e.content))return!1;const t=stripTransportMetadata(e.content).trim();return t.length>0&&!/^Sender\s*\(untrusted/i.test(t)});if(t?.content){const r=stripTransportMetadata(t.content).trim().slice(0,64).replace(/\n/g," ").trim();if(r){e.title=r,e.titleSource="truncated";const t=this.getGateway();t?.isConnected&&t.sessionsPatch(e.gatewaySessionKey,{label:r}).catch(()=>{}),this.transport.sendSessionUpdate(s,{sessionUpdate:"title_update",title:r}),this.tryGenerateTitleFromHistory(e,s,a)}}}const p=t=>`history:${e.gatewaySessionKey}:${t}`;let m=-1;if(n)for(let e=a.length-1;e>=0;e--)if("assistant"===a[e].role){m=e;break}const{replayedToolCallIds:h,streamCompleteCount:u}=await this.replayHistoryTurnsToSink(e,a,this.transport,{mqActive:Boolean(n),lastAsstIdx:m,turnIdFor:(e,t)=>p(t)});for(const e of h)t.add(e);this.logger.debug?.(`[bridge-core] emitHistoryTurns DONE sid=${s} historyStreamCompleteEmitted=${u} replayedToolCallIds=${t.size}`)}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}return t}async replayHistoryTurnsToSink(e,t,s,r){const n=e.sessionId,i=new Set;let o=0;for(let a=0;a<t.length;a++){const l=t[a],d=r.turnIdFor(l,a);if(r.mqActive&&a===r.lastAsstIdx)this.logger.debug?.(`[bridge-core] SKIP history_turn[${a}] assistant — mq active, deferring to emitCurrentStreamSnapshot`);else if("user"!==l.role||"internal_system"!==l.provenance?.kind)if("user"===l.role&&/^\s*system\s*\(untrusted[^)]*\)/i.test(l.content))this.logger.debug?.(`[bridge-core] SKIP history_turn[${a}] system-injected user message`);else if("user"===l.role&&isHeartbeatPromptText(l.content))this.logger.info?.(`[bridge-core] SKIP history_turn[${a}] heartbeat cron prompt sessionId=${e.sessionId} contentLen=${l.content.length} preview="${previewFilteredText(l.content)}"`);else if("user"===l.role&&isCronPromptText(l.content))this.logger.info?.(`[bridge-core] SKIP history_turn[${a}] cron-triggered user message sessionId=${e.sessionId} contentLen=${l.content.length} preview="${previewFilteredText(l.content)}"`);else{if("assistant"===l.role){const t=classifyHeartbeatFilter(l.content);if(t.filtered){this.logger.info?.(`[bridge-core] SKIP history_turn[${a}] heartbeat assistant reply reason=${t.reason} sessionId=${e.sessionId} contentLen=${l.content.length} preview="${previewFilteredText(l.content)}"`);continue}}if("system"!==l.role)if(this.logger.debug?.(`[bridge-core] SENDING history_turn[${a}] role=${l.role} contentLen=${l.content.length} content=${JSON.stringify(l.content.slice(0,80))} tcCount=${l.toolCalls?.length??0}`),"user"===l.role)this.replayUserTurnAsChunks(s,n,l.content,l.metadata,l.timestamp,d,l._seq);else{if(await this.replayAssistantTurnAsStream(s,n,l,d),l.toolCalls)for(const e of l.toolCalls)e.id&&i.add(e.id);s.sendSessionUpdate(n,{sessionUpdate:"stream_complete",stopReason:"end_turn"},{turnId:d}),o+=1}else this.logger.debug?.(`[bridge-core] SENDING system_message[${a}] contentLen=${l.content.length}`),s.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:l.content},metadata:l.metadata,...l.timestamp?{timestamp:l.timestamp}:{}},{turnId:d,...void 0!==l._seq?{seq:l._seq}:{}})}else this.logger.debug?.(`[bridge-core] SKIP history_turn[${a}] internal_system user message`)}return{replayedToolCallIds:i,streamCompleteCount:o}}async handleSessionHistoryPage(e){const t=e.params??{},s="string"==typeof t.sessionId?t.sessionId:"";if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const r="string"==typeof t.cursor&&t.cursor.length>0?t.cursor:void 0,n="number"==typeof t.limit&&Number.isFinite(t.limit)&&t.limit>0?Math.min(1e3,Math.floor(t.limit)):100,{session:i}=await this.resolveOrCreateSession(s),o=this.getGateway(),a=await loadHistoryPage(o,i.gatewaySessionKey,{...r?{cursor:r}:{},limit:n,logger:this.logger}),l=groupIntoTurns(a.messages),d=new HistoryUpdateRecorder(this.transport);await this.replayHistoryTurnsToSink(i,l,d,{mqActive:!1,lastAsstIdx:-1,turnIdFor:(e,t)=>void 0!==e._seq?`history:${i.gatewaySessionKey}:seq:${e._seq}`:`history:${i.gatewaySessionKey}:seq:none:${r??"head"}:${t}`}),this.logger.debug?.(`[bridge-core] _beeos/session/history sid=${i.sessionId} cursor=${r??"<head>"} limit=${n} turns=${l.length} updates=${d.updates.length} hasMore=${a.hasMore} nextCursor=${a.nextCursor??"<none>"}`),this.transport.sendResult(e.id,{updates:d.updates,...a.nextCursor?{nextCursor:a.nextCursor}:{},hasMore:a.hasMore})}emitCurrentStreamSnapshot(e,t){const s=e.pendingUserTurnUpdates;if(s&&s.length>0)for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t);const r=e.assistantMq;if(r&&!r.closed){const s=r.peek(0);let n=0;if(t&&t.size>0)for(let e=0;e<s.length;e++){const r=s[e],i=r.sessionUpdate,o=r.toolCallId;if("tool_call"!==i&&"tool_call_update"!==i||!o||!t.has(o)){if("agent_message_chunk"===i){if(""===(r.content?.text??"").trim()&&e<s.length-1){const r=s[e+1];if(("tool_call"===r.sessionUpdate||"tool_call_update"===r.sessionUpdate)&&r.toolCallId&&t.has(r.toolCallId)){n=e+1;continue}}break}break}n=e+1}const i=resolveTurnId(e),o=i?{turnId:i}:void 0;for(let t=n;t<s.length;t++)this.transport.sendSessionUpdate(e.sessionId,s[t],o)}}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||this.cronIsolatedSessionKeys.has(e.gatewaySessionKey)||t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,r=s.sessionKey??s.key??s.id??s.name;if(!r||"string"!=typeof r)continue;if(isBackgroundSessionKey(r))continue;if(this.cronIsolatedSessionKeys.has(r))continue;const n=deriveSessionId(r,this.instanceId),i=t.find(e=>e.sessionId===r||e.sessionId===n||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===r),o=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0,a=o&&!isPlaceholderTitle(o)?o:void 0,l=s.model,d=s.modelProvider;if(i){if(a&&isPlaceholderTitle(i.title)){i.title=a;const e=this.state.sessions.get(i.sessionId);e&&isPlaceholderTitle(e.title)&&(e.title=a)}const e=s.updatedAt;e&&(i.updatedAt=e),l&&(i.model=l),d&&(i.modelProvider=d)}else{const e=createSessionObject({gatewayKey:r,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s.cwd,title:a});this.registerSession(e,{ensureVerbose:!1}),t.push({sessionId:e.sessionId,title:a||"",cwd:s.cwd,updatedAt:s.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(r),model:l,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const r=s.slice(0,64),n=this.state.sessions.get(t);if(!n)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);n.title=r,n.titleSource="manual";const i=n.gatewaySessionKey,o=this.getGateway();o?.isConnected&&o.sessionsPatch(i,{label:r}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:t,title:r})}static TITLE_TEMPLATES=[{match:["今日日报","日报"],zh:"今日工作日报",en:"Daily Work Log"},{match:["周报","周报总结","本周周报"],zh:"本周工作周报",en:"Weekly Work Report"},{match:["月报","月度汇报","月度总结"],zh:"本月工作月报",en:"Monthly Work Report"},{match:["工作日志","日志"],zh:"工作日志整理",en:"Work Log Review"},{match:["晨会","今日晨会"],zh:"今日晨会纪要",en:"Daily Standup Notes"},{match:["复盘","今日复盘"],zh:"今日工作复盘",en:"Daily Retrospective"},{match:["新会话","new chat","new conversation"],zh:"新会话",en:"New Conversation"}];static TITLE_SYSTEM_PROMPT='You are a conversation title generator. Your task is to generate a concise, human-readable title summarizing the topic of the user\'s question together with the assistant\'s answer.\n\n### Rules:\n- Length: 3-6 words in English, or 6-20 characters in Chinese (up to 24 if needed to keep all key domain terms intact).\n- Language: Match the user\'s input language exactly. Mixed zh/en input → use the dominant language.\n- Extract the core topic (noun-centric) of the Q&A exchange, not the action verb.\n- Use the assistant\'s answer to disambiguate or enrich the topic when the question alone is vague (e.g. "帮我看下" + an answer about x402 → "x402 支付集成").\n- No punctuation, no quotation marks, no emojis, no markdown.\n- No prefix like "Title:" or "Here is". Output the title only.\n- Avoid generic words: "help", "question", "chat", "problem", "task".\n- If both the question and answer are too vague to summarize, return "新会话" (for zh) or "New Conversation" (for en).\n\n### Input Format:\nThe user message will contain both the question and the assistant\'s answer, delimited like this:\n[User Question]\n...question text...\n\n[Assistant Answer]\n...answer text (may be truncated with "..." in the middle)...\n\n### Output Format:\nRespond ONLY with valid JSON, no other text:\n{"title": "<your title here>"}\n\n### Examples:\nInput:\n[User Question]\n帮我写一个关于BeeOS设备接入的PRD\n\n[Assistant Answer]\n好的,这是 BeeOS 设备接入 PRD 的草稿:1. 目标 ...\nOutput: {"title": "BeeOS 设备接入 PRD"}\n\nInput:\n[User Question]\n帮我看下\n\n[Assistant Answer]\n你说的 x402 支付协议是一个基于 HTTP 402 的链上支付标准 ...\nOutput: {"title": "x402 支付协议解析"}\n\nInput:\n[User Question]\nWhat\'s the best way to integrate x402 payment protocol?\n\n[Assistant Answer]\nYou have three options: 1. Use the official SDK ...\nOutput: {"title": "x402 Payment Integration"}\n\nInput:\n[User Question]\nhi\n\n[Assistant Answer]\nHello! How can I help you today?\nOutput: {"title": "New Conversation"}';matchTitleTemplate(e){const t=e.trim().toLowerCase();for(const s of AcpGatewayBridgeCore.TITLE_TEMPLATES)if(s.match.some(e=>e.toLowerCase()===t))return/[\u4e00-\u9fff]/.test(e)?s.zh:s.en}isGuardMessage(e){const t=e.trim();return[...t].length<3||[...t].every(e=>/[\p{P}\p{S}\s]/u.test(e))}truncateQuestionForTitle(e){return e.length>2e3?e.slice(0,1500):e}truncateAnswerForTitle(e){return e.length<=1e3?e:`${e.slice(0,500)}\n...\n${e.slice(-500)}`}generateTitleFromQA(e,t,s,r){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen SKIP guard message sid=${t}`);const n=this.matchTitleTemplate(s);if(n){e.title=n,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:n}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:n})}const i=(process.env.AROUTER_BASE_URL||"").replace(/\/$/,""),o=process.env.AROUTER_API_KEY||"";if(!i||!o)return void this.logger.info?.(`[bridge-core] title-gen SKIP sid=${t}: no AROUTER env vars`);const a=`[User Question]\n${this.truncateQuestionForTitle(s)}\n\n[Assistant Answer]\n${this.truncateAnswerForTitle(r)}`;this.logger.info?.(`[bridge-core] title-gen CALLING LLM sid=${t} q="${s.slice(0,60)}" aLen=${r.length}`),fetch(`${i}/v1/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({model:"openai/gpt-4o-mini",max_tokens:32,temperature:.3,messages:[{role:"system",content:AcpGatewayBridgeCore.TITLE_SYSTEM_PROMPT},{role:"user",content:a}]})}).then(async s=>{if(!s.ok){const e=await s.text().catch(()=>"");return void this.logger.info?.(`[bridge-core] title-gen FAILED sid=${t} status=${s.status} body=${e.slice(0,300)}`)}const r=await s.json(),n=r.choices?.[0]?.message?.content?.trim();if(!n)return;const i=n.replace(/^```json\s*/,"").replace(/```$/,"").trim();let o;try{o=JSON.parse(i).title?.trim()??""}catch{return void this.logger.debug?.(`[bridge-core] title-gen JSON parse failed: ${i}`)}if(!o)return;if("manual"===e.titleSource)return void this.logger.debug?.("[bridge-core] title-gen discarded: user already renamed");this.logger.info?.(`[bridge-core] title-gen OK sid=${t} title="${o}"`),e.title=o,e.titleSource="auto";const a=this.getGateway();a?.isConnected&&a.sessionsPatch(e.gatewaySessionKey,{label:o}).catch(()=>{}),this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:o})}).catch(e=>{this.logger.info?.(`[bridge-core] title-gen ERROR sid=${t}: ${e instanceof Error?e.message:e}`)})}async tryGenerateTitleFromHistory(e,t,s){if("manual"===e.titleSource||"auto"===e.titleSource)return;const r=this.getGateway();if(!r?.isConnected)return;let n=s;if(!n)try{const t=await loadHistory(r,e.gatewaySessionKey);n=groupIntoTurns(t)}catch(e){return void this.logger.debug?.(`[bridge-core] title-gen loadHistory failed sid=${t}: ${e instanceof Error?e.message:e}`)}const i=n.find(e=>{if("user"!==e.role)return!1;if(!e.content?.trim())return!1;if("internal_system"===e.provenance?.kind)return!1;if(isCronPromptText(e.content))return!1;const t=stripTransportMetadata(e.content).trim();return!!t&&!/^Sender\s*\(untrusted/i.test(t)});if(!i)return;const o=n.indexOf(i),a=n.slice(o+1).find(e=>"assistant"===e.role&&!!e.content?.trim());if(!a)return;const l=stripTransportMetadata(i.content).trim(),d=a.content.trim();l&&d&&this.generateTitleFromQA(e,t,l,d)}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const r=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${r} gwConnected=${!!this.getGateway()?.isConnected}`);let n=!1;const i=this.getGateway();if(i?.isConnected&&r)try{await i.sessionsDelete(r),n=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${r}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${r}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await i.sessionsReset(r),n=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${r}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${r}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else r&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${r}`);void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.assistantMq=void 0,s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const o=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),o.add(e));for(const[e,t]of this.state.toolCallIdMap)o.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(r),this.transport.resetSessionUpdateCount(t),n&&this.onSessionDeleted?.(r),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${r} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const r=this.getGateway(),n=r?.isConnected;if(s.promptDone||void 0===s.pendingPromptId&&void 0===s.currentRunId||(this.logger.info?.(`[bridge-core] session/clear: cancelling active run sid=${t} runId=${s.currentRunId}`),n&&(s.currentRunId||s.gatewayRequestId)&&r.agentCancel(s.gatewaySessionKey,{runId:s.currentRunId,requestId:s.gatewayRequestId}),this.endRun(s,{kind:"complete",stopReason:"cancelled"})),n&&s.gatewaySessionKey)try{await r.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,r=t?this.state.sessions.get(t):void 0,n=this.getGateway();if(r&&n?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await n.sessionsPatch(r.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,r=t?this.state.sessions.get(t):void 0,n=this.getGateway();let i=s??null,o=null;if(r&&n?.isConnected&&s)try{const e=await n.sessionsPatch(r.gatewaySessionKey,{model:s}),t=e?.resolved;t?.model&&(i=t.model),t?.modelProvider&&(o=t.modelProvider)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:i,modelProvider:o})}async createAgentShell(e,t,s){const r=this.getGateway();if(!r?.isConnected)throw new Error("gateway not connected");const n=`agent-${randomUUID().replace(/-/g,"").slice(0,12)}`,i=n,o=e,a=join(this.getOpenclawHome(),`workspace-${n}`),l=await r.request("agents.create",{name:i,workspace:a,...s?{model:s}:{},...t?.emoji?{emoji:t.emoji}:{},...t?.avatar?{avatar:t.avatar}:{}}),d=l?.agentId??n,c=l?.workspace??a;if(o)try{await r.request("agents.update",{agentId:d,name:o})}catch(e){this.logger.warn?.(`[bridge-core] createAgentShell: failed to set display name for "${d}": ${e instanceof Error?e.message:e}`)}return{agentId:d,workspace:c}}async handleAgentCreate(e){const t=e.params,s=(t?.name??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing agent name"});const r=this.getGateway();if(r?.isConnected)try{const{agentId:n,workspace:i}=await this.createAgentShell(s,t?.identity,t?.model),o=t?.files??{};for(const[e,t]of Object.entries(o))if("IDENTITY.md"!==e){if("string"==typeof t)try{await r.request("agents.files.set",{agentId:n,name:e,content:t})}catch(t){this.logger.warn?.(`[bridge-core] agent/create: failed to write "${e}" for "${n}": ${t instanceof Error?t.message:t}`)}}else this.logger.warn?.("[bridge-core] agent/create: ignoring IDENTITY.md in files (identity is set via structured fields)");this.logger.info?.(`[bridge-core] Created agent "${n}" (name="${s}") workspace=${i}`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/create: agent sync after create failed: ${e instanceof Error?e.message:e}`)}const a=this.agentMapping?.get(n)?.id??"";this.transport.sendResult(e.id,{agentId:n,platformAgentId:a,workspace:i})}catch(t){this.logger.error?.(`[bridge-core] Failed to create agent "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Create agent failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleApplyTemplate(e){const t=e.params,s=(t?.name??"").trim(),r=(t?.packageUrl??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing agent name"});if(!r)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"packageUrl"});const n=this.getGateway();if(n?.isConnected)try{const n=await fetch(r);if(!n.ok)throw new TemplatePackageError(`failed to download package: HTTP ${n.status}`);const i=Buffer.from(await n.arrayBuffer());if(i.length>MAX_ZIP_BYTES)throw new TemplatePackageError(`package too large: ${i.length} bytes (max ${MAX_ZIP_BYTES})`);const o=t?.sha256?.trim().toLowerCase();if(o){if(createHash("sha256").update(i).digest("hex")!==o)throw new TemplatePackageError("package sha256 mismatch")}const a=parseAndValidateTemplatePackage(i),{agentId:l,workspace:d}=await this.createAgentShell(s,t?.identity,t?.model);let c=0;for(const[e,t]of a.workspaceFiles){const s=resolve(d,e),r=relative(d,s);if(""===r||r.startsWith("..")||path.isAbsolute(r))throw new TemplatePackageError(`refusing to write outside workspace: ${e}`);await mkdir(path.dirname(s),{recursive:!0}),await writeFile(s,t),c++}const g=a.manifest?.slug??"(no manifest)",p=a.manifest?.version??"";this.logger.info?.(`[bridge-core] agent/applyTemplate: applied "${g}${p?`@${p}`:""}" to agent "${l}" (${c} files) workspace=${d}`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/applyTemplate: agent sync after apply failed: ${e instanceof Error?e.message:e}`)}const m=this.agentMapping?.get(l)?.id??"";this.transport.sendResult(e.id,{agentId:l,platformAgentId:m,workspace:d})}catch(t){if(t instanceof TemplatePackageError)return this.logger.warn?.(`[bridge-core] agent/applyTemplate: invalid package: ${t.message}`),void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:`Invalid template package: ${t.message}`});this.logger.error?.(`[bridge-core] Failed to apply template for "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Apply template failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleAgentUpdate(e){const t=e.params,s=(t?.agentId??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"agentId"});const r=t?.name?.trim(),n=t?.model?.trim(),i=t?.identity?.emoji?.trim(),o=t?.identity?.avatar?.trim();if(!(r||n||i||o))return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"No updatable fields provided (name/model/identity)"});const a=this.getGateway();if(a?.isConnected)try{const t=await a.request("agents.update",{agentId:s,...r?{name:r}:{},...n?{model:n}:{},...i?{emoji:i}:{},...o?{avatar:o}:{}}),l=t?.agentId??s;this.logger.info?.(`[bridge-core] Updated agent "${l}"${r?` name="${r}"`:""}`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/update: agent sync after update failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{agentId:l})}catch(t){this.logger.error?.(`[bridge-core] Failed to update agent "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update agent failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleAgentDelete(e){const t=e.params,s=(t?.agentId??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"agentId"});if("main"===normalizeAgentId(s))return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:'"main" cannot be deleted; delete the instance instead'});const r=this.getGateway();if(!r?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const n=t?.deleteFiles??!0;try{const t=await r.request("agents.delete",{agentId:s,deleteFiles:n}),i=t?.agentId??s;this.logger.info?.(`[bridge-core] Deleted agent "${i}" (deleteFiles=${n})`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/delete: agent sync after delete failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{agentId:i,..."number"==typeof t?.removedBindings?{removedBindings:t.removedBindings}:{}})}catch(t){this.logger.error?.(`[bridge-core] Failed to delete agent "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Delete agent failed: ${t instanceof Error?t.message:String(t)}`})}}async handleModelsList(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("models.list",{});this.transport.sendResult(e.id,s??{models:[]})}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${t instanceof Error?t.message:t}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const t=e.params?.agentId,s=await this.fetchSimplifiedSkills(t);this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(e){const t=this.getGateway();if(!t?.isConnected)return[];try{const s=await t.skillsStatus(e);if(!s?.skills)return[];const r=s.skills.filter(e=>!1!==e.eligible||!0===e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0,disabled:e.disabled||void 0,skillKey:e.skillKey||void 0,iconUrl:void 0})),n=await this.getSkillsDir(e);for(const e of r){const t=e.skillKey??e.name;try{const s=await readFile(join(n,t,".beeos-meta.json"),"utf8"),r=JSON.parse(s);r?.iconUrl&&(e.iconUrl=r.iconUrl)}catch{}}return r}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(e){const t=await this.fetchSimplifiedSkills(e);this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:t}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:t.length}})}getOpenclawHome(){return process.env.OPENCLAW_HOME||join(homedir(),".openclaw")}async getSkillsDir(e){const t=this.getOpenclawHome();if(!e||"main"===e)return join(t,"skills");const s=await this.resolveAgentWorkspaceDir(e);return join(s??join(t,`workspace-${e}`),"skills")}async resolveAgentWorkspaceDir(e){const t=this.getGateway();if(!t?.isConnected)return null;try{const s=await t.request("agents.list",{}),r=normalizeAgentId(e),n=s?.agents?.find(e=>normalizeAgentId(e.id)===r);return n?.workspace??null}catch(t){return this.logger.warn?.(`[bridge-core] resolveAgentWorkspaceDir("${e}") failed: ${t instanceof Error?t.message:t}`),null}}async handleSkillsInstall(e){const t=e.params,s=t?.zipUrl,r=t?.files,n=t?.agentId;let i;try{i=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill slug"})}if(!r?.length&&!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skill files or zipUrl"});const o=resolveSkillInstallDir(await this.getSkillsDir(n),i);try{if(await mkdir(o,{recursive:!0}),s){const e=await fetchWithRetry(s,{maxRetries:4,logger:this.logger});await extractZipToDir(e,o)}else if(r?.length){const e=resolve(o);for(const t of r){const s=join(o,t.path);if(!resolve(s).startsWith(e+sep))throw new Error(`File path escapes skill directory: ${t.path}`);await mkdir(join(s,".."),{recursive:!0});const r=await fetch(t.downloadUrl,{signal:AbortSignal.timeout(12e4)});if(!r.ok)throw new Error(`Failed to download ${t.path}: ${r.status}`);const n=Buffer.from(await r.arrayBuffer());await writeFile(s,n)}}if(t?.iconUrl)try{await writeFile(join(o,".beeos-meta.json"),JSON.stringify({iconUrl:t.iconUrl}),"utf8")}catch{}this.logger.info?.(`[bridge-core] Installed skill "${i}"${n?` for agent "${n}"`:""} via ${s?"zip":"files"}`),this.transport.sendResult(e.id,{success:!0}),this.onSkillInstalled?.(i,o),await this.pushSkillsList(n)}catch(t){this.logger.error?.(`[bridge-core] Failed to install skill "${i}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Install failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUninstall(e){const t=e.params,s=t?.agentId;let r;try{r=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill name"})}const n=resolveSkillInstallDir(await this.getSkillsDir(s),r),i=resolveSkillInstallDir(join(homedir(),".agents","skills"),r);try{let t=!1;try{const e=await lstat(n);if(e.isSymbolicLink()){const e=await readlink(n);await rm(n,{force:!0}),await rm(e,{recursive:!0,force:!0}),t=!0}else e.isDirectory()&&(await rm(n,{recursive:!0,force:!0}),t=!0)}catch{}try{const e=await lstat(i);(e.isDirectory()||e.isSymbolicLink())&&(await rm(i,{recursive:!0,force:!0}),t=!0)}catch{}t||this.logger.warn?.(`[bridge-core] Skill "${r}" not found in any known location`),this.logger.info?.(`[bridge-core] Uninstalled skill "${r}"${s?` for agent "${s}"`:""}`),this.transport.sendResult(e.id,{success:!0}),await this.pushSkillsList(s)}catch(t){this.logger.error?.(`[bridge-core] Failed to uninstall skill "${r}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Uninstall failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUpdate(e){const t=e.params,s=t?.skillKey,r=t?.enabled,n=t?.agentId;if(!s||"boolean"!=typeof r)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skillKey or enabled"});const i=this.getGateway();if(i?.isConnected)try{const t=await i.skillsUpdate(s,r,n);this.logger.info?.(`[bridge-core] Updated skill "${s}" enabled=${r}${n?` for agent "${n}"`:""}`),this.transport.sendResult(e.id,{ok:t?.ok??!0,skillKey:s,enabled:r}),await this.pushSkillsList(n)}catch(t){this.logger.error?.(`[bridge-core] Failed to update skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.InternalError,{message:"Gateway not connected"})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:r,created:n}=await this.resolveOrCreateSession(t);n&&this.onSessionLoaded?.(r.sessionId,r.gatewaySessionKey,r.platformAgentId);const i=r.sessionId;this.state.lastActiveSessionId=i,this.onPromptStart?.();const o=this.getGateway();if(!o?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);await this.ensureVerboseFull(r);let a=extractPromptBlocks(sanitizePromptPayload(s));const l=a.filter(e=>"text"===e.type&&"text"in e&&"string"==typeof e.text).map(e=>e.text).join("\n");if(r.fileArtifactDisplayMode=prefersChatFileArtifacts(l)?"chat":"auto",!r.title||isPlaceholderTitle(r.title)){const e=a.find(e=>"text"===e.type&&e.text);if(e&&"text"in e&&e.text){const t=stripTransportMetadata(e.text).trim();if(this.logger.info?.(`[bridge-core] title-gen sid=${i} existing="${r.title}" cleanText="${t.slice(0,80)}" isPlaceholder=${isPlaceholderTitle(t)}`),t&&!isPlaceholderTitle(t)){const e=t.slice(0,64).replace(/\n/g," ").trim();r.title=e,r.titleSource="truncated",o?.isConnected&&e&&o.sessionsPatch(r.gatewaySessionKey,{label:e}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendSessionUpdate(i,{sessionUpdate:"title_update",title:e})}}}const d=buildResolutionPlan(a);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let c;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(a);if(e.refs.length>0)try{c=await this.beeosFileResolver.resolveMetadataForPrompt(e)}catch(e){this.logger.warn?.(`[bridge-core] BeeOS file resolution failed: ${e instanceof Error?e.message:e}`)}}const g=toGatewayPrompt(a,{beeosFileResolutionsByUri:c});if("error"in g)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:g.error});const{message:p,attachments:m}=g,h=resolveStandaloneSlashCommandName(a,p)??resolveSlashCmd(a,p),u="string"==typeof h;if(h&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(h)}catch(e){return t.logger.warn?.(`[bridge-core] /${h} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(r,a,resolveTurnId(r)),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${h}...`},timestamp:(new Date).toISOString()},{turnId:resolveTurnId(r)}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const f="chat.send";let y=u?p:withUserMessagePrefix(p);const w=this.config.canvasEnabled&&!u&&!0===r.canvasActive&&!r.channelType;if(w){const e=r.promptsSinceCanvasInjection??0;!r.canvasPromptInjected||e>=10?(y=`${CANVAS_TERM_MAPPING}\n\n${y}`,r.canvasPromptInjected=!0,r.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):r.promptsSinceCanvasInjection=e+1}if(w){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(r.gatewaySessionKey);y=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${y}`}const t=r.referencedSurfaceId;t&&(y=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${y}`,r.referencedSurfaceId=void 0)}if(!u){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){r.activeMentions=t;y=`${buildMentionInstruction(t)}\n${y}`,this.logger.debug?.(`[bridge-core] delegation mention instruction injected for ${t.length} agent(s)`)}else r.activeMentions=void 0}u&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${i} command=/${h}`),r.promptDone||void 0===r.pendingPromptId&&void 0===r.currentRunId||(this.logger.warn?.(`[bridge-core] handleSessionPrompt: force-ending stale run sid=${i} oldRunId=${r.currentRunId} oldPromptId=${r.pendingPromptId}`),(r.currentRunId||r.gatewayRequestId)&&o.agentCancel(r.gatewaySessionKey,{runId:r.currentRunId,requestId:r.gatewayRequestId}),this.endRun(r,{kind:"complete",stopReason:"cancelled"})),this.state.resetForNewRun(r,i,this.transport),r.pendingPromptId=e.id,r.gatewayMethod=f;const I=resolveTurnId(r);this.emitPromptAsUserSessionUpdates(r,a,I),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""},timestamp:(new Date).toISOString()},I?{turnId:I}:void 0);const S=extractCronJobId(p);S&&(r.cronJobId=S),this.schedulePromptTimeout(r);const v=m.length>0?m.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0,b=`req_${randomUUID().replace(/-/g,"")}`,T={sessionKey:r.gatewaySessionKey,message:y,deliver:!1,idempotencyKey:randomUUID()};v?.length&&(T.attachments=v);if(!o.sendFrame({type:"req",id:b,method:f,params:T}))return this.state.clearPromptTimeout(r),r.pendingPromptId=void 0,r.assistantMq?.close(),r.assistantMq=void 0,r.pendingUserTurnUpdates=void 0,void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});r.gatewayRequestId=b,this.state.promptsByGatewayRequestId.set(b,r)}replayUserTurnAsChunks(e,t,s,r,n,i,o){const a=splitUserReplayBlocks(s),l=i||void 0!==o?{...i?{turnId:i}:{},...void 0!==o?{seq:o}:{}}:void 0;for(const s of a){let i;if("text"===s.type){if(!s.text)continue;i={type:"text",text:s.text}}else i="resource_link"===s.type?{type:"resource_link",uri:s.uri??"",...s.fileName?{name:s.fileName}:{},...s.mimeType?{mimeType:s.mimeType}:{}}:{type:s.type,...s.mimeType?{mimeType:s.mimeType}:{},...s.fileName?{fileName:s.fileName}:{}};e.sendSessionUpdate(t,{sessionUpdate:"user_message_chunk",content:i,...r?{metadata:r}:{},...n?{timestamp:n}:{}},l)}}async replayAssistantTurnAsStream(e,t,s,r){const n=e=>{if(r||void 0!==e)return{...r?{turnId:r}:{},...void 0!==e?{seq:e}:{}}},i=new Set,o=(s,r,o)=>{if(!s?.length)return;const a=n(o);for(const n of s)n.uri&&!i.has(n.uri)&&(i.add(n.uri),e.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:n.uri,name:n.name??"file",...n.mimeType?{mimeType:n.mimeType}:{}},...r?{timestamp:r}:{}},a))},a=n(s._seq);if(!s.sourceMessages?.length){const r={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls,...s.timestamp?{timestamp:s.timestamp}:{}};return s.metadata&&(r.metadata=s.metadata),e.sendSessionUpdate(t,r,a),void o(s.files,s.timestamp,s._seq)}for(const r of s.sourceMessages){const i=r.updateType??"agent_message_chunk",a=n(r._seq);switch(i){case"agent_message_chunk":e.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:r.content},...r.timestamp?{timestamp:r.timestamp}:{},...r.metadata?{metadata:r.metadata}:{}},a);break;case"agent_thought_chunk":e.sendSessionUpdate(t,{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:r.content},...r.timestamp?{timestamp:r.timestamp}:{}},a);break;case"tool_call":{const n=r.toolCallId??"",i=s.toolCalls?.find(e=>e.id===n),o=r.toolName??"tool",l=r.content??"";let d={};if(l)try{d=JSON.parse(l)}catch{}e.sendSessionUpdate(t,buildToolCallPayload({toolCallId:n,toolName:o,args:d,kind:i?.kind,status:i?.status??"in_progress",timestamp:r.timestamp}),a);break}case"tool_call_update":{const n=r.toolCallId??"",i=s.toolCalls?.find(e=>e.id===n),l=r.toolName??i?.name??"tool",d=r.content??i?.output??"",c=await this.resolveReplayResourceLinks(n,l,r.files);e.sendSessionUpdate(t,buildToolCallUpdatePayload({toolCallId:n,toolName:l,outputText:d,kind:i?.kind,status:i?.status??"completed",resourceLinks:c,timestamp:r.timestamp}),a),o(c,r.timestamp,r._seq);break}}}o(s.files,s.timestamp,s._seq)}async resolveReplayResourceLinks(e,t,s){let r=s?.map(e=>({uri:e.uri,name:e.name??"file",...e.mimeType?{mimeType:e.mimeType}:{}}));if(r?.length)return r;if(r=await readEnrichmentCache(e).catch(()=>{}),r?.length)return r;if("beeos_upload_file"!==t)return;const n=await readUploadToolResult(e).catch(()=>{});if(!n)return;const i=buildToolResultSessionContentBlocks(t,n).filter(e=>"resource_link"===e.content?.type&&e.content.uri).map(e=>({uri:e.content.uri,name:e.content.name??"file",...e.content.mimeType?{mimeType:e.content.mimeType}:{}}));return i.length>0?i:void 0}emitPromptAsUserSessionUpdates(e,t,s){const r=(new Date).toISOString(),n=s?{turnId:s}:void 0;for(const i of t){const t=this.toUserSessionUpdateContent(i);if(!t)continue;const o={sessionUpdate:"user_message_chunk",content:t,timestamp:r};s&&void 0===o._turnId&&(o._turnId=s),this.transport.sendSessionUpdate(e.sessionId,o,n),e.pendingUserTurnUpdates?.push(o)}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,r=t.mimeType??t.mime_type??e.mimeType??e.mime_type,n=t.text??e.text,i=t.data??e.data,o=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!n&&!i)return;return{type:"resource",resource:{...s?{uri:s}:{},...r?{mimeType:r}:{},...n?{text:n}:{},...i?{data:i}:{},...o?{fileName:o}:{}}}}}handleSessionCancel(e,t){const s=t&&"object"==typeof t&&"sessionId"in t?t.sessionId:void 0;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const r=this.state.sessions.get(s);if(!r)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const n=this.getGateway(),i=n?.isConnected,o=!(!r.currentRunId&&!r.gatewayRequestId);if(i&&o){n.agentCancel(r.gatewaySessionKey,{runId:r.currentRunId,requestId:r.gatewayRequestId})}this.endRun(r,{kind:"complete",stopReason:"cancelled"})||this.transport.sendSessionUpdate(r.sessionId,{sessionUpdate:"stream_complete",stopReason:"cancelled"},{turnId:resolveTurnId(r)}),void 0!==e&&this.transport.sendResult(e,{})}endRun(e,t){if(e.promptDone)return this.logger.debug?.(`[bridge-core] endRun skipped (promptDone=true) sessionId=${e.sessionId} outcome=${t.kind}`),!1;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return this.logger.debug?.(`[bridge-core] endRun skipped (no pendingPromptId and no currentRunId) sessionId=${e.sessionId} outcome=${t.kind}`),!1;const s=resolveTurnId(e),r=s?{turnId:s}:void 0;e.promptDone=!0;const n=e.currentRunId;if("complete"===t.kind){if(n&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(n,s,t.stopReason)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason},r),this.tryGenerateTitleFromHistory(e,e.sessionId)}else n&&this.onRunFailed&&this.onRunFailed(n,t.message),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t.code,t.message),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:"error",error:t.message},r);return this.cleanupPrompt(e),this.onRunEnded?.(e,t),!0}schedulePromptTimeout(e){let t;if(this.state.clearPromptTimeout(e),e.cronJobId){const s=this.cronOverrides.get(e.cronJobId)??this.config.cronMaxDurationDefaultMs;t=Math.max(CRON_DURATION_FLOOR_MS,Math.min(this.config.cronMaxDurationCapMs,s))}else t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0===e.pendingPromptId&&void 0===e.currentRunId||((e.currentRunId||e.gatewayRequestId)&&this.getGateway()?.agentCancel(e.gatewaySessionKey,{runId:e.currentRunId,requestId:e.gatewayRequestId}),this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t),this.state.runIdToCronJobId.delete(t),this.state.markRunCompleted(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.gatewayRequestId&&this.state.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close(),e.assistantMq=void 0,e.pendingUserTurnUpdates=void 0}completePrompt(e,t){this.endRun(e,{kind:"complete",stopReason:t})}failPrompt(e,t,s){this.endRun(e,{kind:"error",code:t,message:s})}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}handleGatewayResponse(e){const t=e.id;if(!t)return;const s=this.state.promptsByGatewayRequestId.get(t);if(!s||s.promptDone)return;if(!e.ok){const r=e.error,n=r?.message??"gateway returned error";return this.logger.warn?.(`[bridge-core] handleGatewayResponse: gateway rejected prompt gatewayRequestId=${t} error=${n}`),void this.failPrompt(s,Errors.SendFailed.rpcCode,n)}const r=e.payload,n=r?.runId;n?(s.currentRunId=n,s.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(n,s),this.logger.debug?.(`[bridge-core] handleGatewayResponse: bound runId=${n} gatewayRequestId=${t} sessionId=${s.sessionId}`)):this.logger.debug?.(`[bridge-core] handleGatewayResponse: no runId in payload gatewayRequestId=${t} sessionId=${s.sessionId}`)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s){const s="string"==typeof t.stopReason?t.stopReason:void 0;this.completePrompt(e,s||"end_turn")}else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/","."),r={...e.params??{}},n="cron.update"===s;!n||r.patch&&"object"==typeof r.patch||(r.patch={});const i=n?r.patch:r,o=i._beeos_meta;let a;if(delete i._beeos_meta,"cron.add"!==s&&"cron.update"!==s||!0!==i._beeos_internal)void 0!==i._beeos_internal&&delete i._beeos_internal;else if(delete i._beeos_internal,i.delivery={mode:"none"},"cron.add"===s){const e=await this.allocateCronChannel(i.name??"cron");e&&(i.description=encodeBeeosChannelTag(i.description,e))}else{const e=r.id,t="description"in i,s=i.description;let n,o=decodeBeeosChannelId(s);!o&&e&&(o=this.cronChannelCache.get(e)),!o&&e&&(n=await this.lookupCronJobDescription(e),o=decodeBeeosChannelId(n));let l=!1;o||(o=await this.allocateCronChannel(i.name??"cron"),l=!0),o&&(t?i.description=encodeBeeosChannelTag(s,o):l&&(i.description=encodeBeeosChannelTag(n,o)),a=o)}try{const n=await t.request(s,r),l="cron.list"===s||"cron.status"===s?this.enrichCronResponseWithOverrides(n):n;if(this.transport.sendResult(e.id,l??{}),("cron.add"===s||"cron.update"===s)&&n&&"object"==typeof n){const e=n.id;if(e&&void 0!==o?.maxDurationMs){const t=Math.max(CRON_DURATION_FLOOR_MS,Math.min(this.config.cronMaxDurationCapMs,o.maxDurationMs));this.cronOverrides.set(e,t)}}if("cron.remove"===s){const t=e.params?.id;t&&(this.cronOverrides.delete(t),this.cronChannelCache.delete(t))}if(("cron.add"===s||"cron.update"===s)&&n&&"object"==typeof n){const e=n.id,t=decodeBeeosChannelId(i.description)??a;e&&t&&this.setCronChannelCache(e,t)}"cron.add"!==s&&"cron.update"!==s&&"cron.remove"!==s||this.refreshCronIsolatedSessionKeys()}catch(t){const r=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${r}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:r})}}enrichCronResponseWithOverrides(e){if(!e||"object"!=typeof e)return e;const t=e,s=e=>{if(!e||"object"!=typeof e)return e;const t=e,s=t.id;if("string"!=typeof s)return e;const r=this.cronOverrides.get(s);return"number"!=typeof r?e:{...t,_beeos_meta:{maxDurationMs:r}}};if(Array.isArray(t.jobs))return{...t,jobs:t.jobs.map(s)};if(t.jobs&&"object"==typeof t.jobs){const e=t.jobs,r={};for(const[t,n]of Object.entries(e))r[t]=s(n);return{...t,jobs:r}}return e}async allocateCronChannel(e){if(this.agentFetch&&this.agentGatewayUrl)try{const t=`${this.agentGatewayUrl}/api/v1/channels`,s=await this.agentFetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({participants:[this.instanceId,"service:cron"],metadata:{source:"cron",job_name:e}})});if(!s.ok)return void this.logger.warn?.(`[bridge-core] channel alloc failed: ${s.status}`);return(await s.json()).id}catch(e){return void this.logger.warn?.(`[bridge-core] channel alloc error: ${e instanceof Error?e.message:e}`)}else this.logger.debug?.("[bridge-core] agentFetch/agentGatewayUrl unavailable, skip channel alloc")}async lookupCronJobDescription(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("cron.list",{}),r=s?.jobs;if(!Array.isArray(r))return;for(const t of r){if(!t||"object"!=typeof t)continue;const s=t;if(s.id===e)return"string"==typeof s.description?s.description:void 0}}catch(e){this.logger.debug?.(`[bridge-core] cron.list description lookup failed: ${e instanceof Error?e.message:e}`)}}setCronChannelCache(e,t){if(this.cronChannelCache.size>=AcpGatewayBridgeCore.CRON_CHANNEL_CACHE_MAX){const e=this.cronChannelCache.keys().next().value;void 0!==e&&this.cronChannelCache.delete(e)}this.cronChannelCache.set(e,t)}async refreshCronIsolatedSessionKeys(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.request("cron.list",{}),s=t?.jobs;if(!Array.isArray(s))return;const r=new Set,n=[];for(const e of s){if(!e||"object"!=typeof e)continue;const t=e;if("string"==typeof t.id&&n.push(t.id),"isolated"!==t.sessionTarget)continue;const s=t.sessionKey;"string"==typeof s&&s.length>0&&r.add(s)}this.cronIsolatedSessionKeys=r,this.cronOverrides.reconcile(n)}catch(e){this.logger.debug?.(`[bridge-core] refreshCronIsolatedSessionKeys failed: ${e instanceof Error?e.message:e}`)}}}async function fetchWithRetry(e,t={}){const s=t.maxRetries??3;let r;for(let n=0;n<=s;n++){const i=await fetch(e,{signal:AbortSignal.timeout(12e4),redirect:"follow"});if(i.ok)return Buffer.from(await i.arrayBuffer());if(429===i.status||i.status>=500){const o=Number(i.headers.get("retry-after"))||0,a=1e3*Math.max(o,2**n);if(r=new Error(`HTTP ${i.status} from ${e}`),n<s){t.logger?.info?.(`[bridge-core] Download ${i.status}, retry ${n+1}/${s} in ${(a/1e3).toFixed(0)}s`),await new Promise(e=>setTimeout(e,a));continue}}throw new Error(`Failed to download zip: ${i.status}`)}throw r??new Error("Download failed after retries")}const SKILL_ROOT_MARKERS=["SKILL.md","skill.md","SKILL.MD"];async function resolveSkillRoot(e){for(const t of SKILL_ROOT_MARKERS)try{return await stat(join(e,t)),e}catch{}const t=(await readdir(e,{withFileTypes:!0})).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&"__MACOSX"!==e.name);if(1===t.length){const s=join(e,t[0].name);for(const e of SKILL_ROOT_MARKERS)try{return await stat(join(s,e)),s}catch{}return s}return e}async function extractZipToDir(e,t){const s=new AdmZip(e),r=join(t,".beeos-extract-tmp-"+Date.now());try{await mkdir(r,{recursive:!0}),s.extractAllTo(r,!0);const e=await resolveSkillRoot(r),n=await readdir(e,{withFileTypes:!0});let i=0;for(const s of n){if(s.name.startsWith(".beeos-extract-tmp")||"__MACOSX"===s.name)continue;const r=join(e,s.name),n=join(t,s.name);await rename(r,n).catch(async()=>{await cp(r,n,{recursive:!0})}),i++}if(0===i)throw new Error("Zip archive contained no extractable files")}finally{await rm(r,{recursive:!0,force:!0}).catch(()=>{})}}const VALID_SLUG_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,NON_ASCII_PATTERN=/[^\x00-\x7F]/;function validateSkillSlug(e){const t=e.trim();if(!t||t.includes("/")||t.includes("\\")||t.includes(".."))throw new Error(`Invalid skill slug: ${e}`);if(NON_ASCII_PATTERN.test(t)||!VALID_SLUG_PATTERN.test(t))throw new Error(`Invalid skill slug: ${e}`);return t}function safeDirName(e){const t=e.trim();return t?t.replaceAll("/","__").replaceAll("\\","__"):t}function resolveSkillInstallDir(e,t){const s=safeDirName(t),r=join(e,s),n=resolve(e),i=resolve(r),o=relative(n,i);if(!o||".."===o||o.startsWith(`..${sep}`)||path.isAbsolute(o))throw new Error("Invalid skill target path");return r}
1
+ import{randomUUID,createHash}from"node:crypto";import{mkdir,writeFile,readFile,rm,lstat,readlink,stat,readdir,rename,cp}from"node:fs/promises";import path from"node:path";import{join,resolve,relative,sep}from"node:path";import{homedir}from"node:os";import AdmZip from"adm-zip";import{sanitizePromptPayload}from"../message-sanitization/prompt-payload-sanitizer.js";import{previewSanitizedText,stripTransportMetadata}from"../message-sanitization/transport-metadata-sanitizer.js";import{isHeartbeatPromptText,isCronPromptText,extractCronJobId,classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{createCronOverridesStore}from"../cron-overrides.js";import{CRON_DURATION_FLOOR_MS}from"../config.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas/tools.js";import{parseMentionsParam,buildMentionInstruction}from"../delegation/mention-resolver.js";import{EXTERNAL_PROTOCOL_TAGS}from"./external-protocol-tags.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,loadHistoryPage,groupIntoTurns,splitUserReplayBlocks}from"./history-replay.js";import{LocalSessionHistory}from"./local-session-history.js";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{readEnrichmentCache,readUploadToolResult}from"../upload/result-cache.js";import{buildToolResultSessionContentBlocks}from"./tool-result-blocks.js";import{prefersChatFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.js";import{resolveTurnId,recordTurnIdForRole,recordToolCallTurnForUpdate}from"./gateway-events.js";import{emitToolResultProjectionForTurn}from"./tool-result-emitter.js";import{extractPersistedToolResultEnvelope,findPersistedToolResultEnvelopeByMessageId}from"./tool-result-transcript.js";import{projectPersistedToolResultForReplay}from"./tool-result-projection.js";import{McpService}from"./mcp-service.js";import{DEFAULT_PROMPT_TIMEOUT_MS}from"./types.js";import{ACP_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/acp";import{parseAndValidateTemplatePackage,TemplatePackageError,MAX_ZIP_BYTES}from"./agent-template-package.js";const AGENT_VALID_ID_RE=/^[a-z0-9][a-z0-9_-]{0,63}$/i,AGENT_INVALID_CHARS_RE=/[^a-z0-9_-]+/g,AGENT_LEADING_DASH_RE=/^-+/,AGENT_TRAILING_DASH_RE=/-+$/,DEFAULT_AGENT_ID="main";export function normalizeAgentId(e){const t=(e??"").trim();return t?AGENT_VALID_ID_RE.test(t)?t.toLowerCase():t.toLowerCase().replace(AGENT_INVALID_CHARS_RE,"-").replace(AGENT_LEADING_DASH_RE,"").replace(AGENT_TRAILING_DASH_RE,"").slice(0,64)||"main":"main"}function narrowContentBlock(e){switch(e.type){case"text":return"string"==typeof e.text?{type:"text",text:e.text}:void 0;case"resource_link":return"string"==typeof e.uri?{type:"resource_link",uri:e.uri,...void 0!==e.name?{name:e.name}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}}:void 0;case"file":return{type:"file",...void 0!==e.text?{text:e.text}:{},...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};case"image":return{type:"image",...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};default:return}}const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i,HISTORY_PAGE_DEFAULT_LIMIT=100,HISTORY_PAGE_MAX_LIMIT=1e3;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-",...EXTERNAL_PROTOCOL_TAGS];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function isPlaceholderTitle(e){return!e||"New chat"===e||(!!/^[0-9a-f]{6,12}\s*\(20\d{2}/i.test(e)||(!!/^Sender\s*\(untrusted/i.test(e)||!!/^Conversation\s+info\s*\(untrusted/i.test(e)))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function mergePersistedToolResultEnvelopes(e,t){if(e||t)return{toolName:e?.toolName||t?.toolName||"tool",...e?.toolCallId||t?.toolCallId?{toolCallId:e?.toolCallId??t?.toolCallId}:{},...e?.rawText??t?.rawText?{rawText:e?.rawText??t?.rawText}:{},...void 0!==e?.rawResult||void 0!==t?.rawResult?{rawResult:e?.rawResult??t?.rawResult}:{},...e?.timestamp||t?.timestamp?{timestamp:e?.timestamp??t?.timestamp}:{}}}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const r=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return r&&!r.includes("/")&&SLASH_COMMAND_NAME_RE.test(r)?r.toLowerCase():void 0}export const BEEOS_CHANNEL_TAG_RE=/<!-- beeos:channel=(.+?) -->/;export function encodeBeeosChannelTag(e,t){const s=`\x3c!-- beeos:channel=${t} --\x3e`,r=stripBeeosChannelTag(e);return r?`${r}\n${s}`:s}export function decodeBeeosChannelId(e){if(!e)return;const t=BEEOS_CHANNEL_TAG_RE.exec(e);return t?.[1]}export function stripBeeosChannelTag(e){return e?e.replace(/\n?<!-- beeos:channel=.+? -->/g,"").trim():""}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}function createSessionObject(e){const t=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:t,platformAgentId:e.resolvePlatformAgentId?.(t),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}class HistoryUpdateRecorder{transport;updates=[];constructor(e){this.transport=e}sendSessionUpdate(e,t,s){const r=this.transport.materializeSessionUpdate(t,s);return null!==r&&this.updates.push(r),!0}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;beeosFileResolver;agentFetch;agentGatewayUrl;triggerAgentSync;cronChannelCache=new Map;static CRON_CHANNEL_CACHE_MAX=256;localSessionHistory;mcp;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];_mappingUpdateCallbacks=[];cronIsolatedSessionKeys=new Set;cronOverrides;setAgentMapping(e){this.agentMapping=e;for(const t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];for(const e of this._mappingReadyCallbacks)e();this._mappingReadyCallbacks=[];for(const t of this._mappingUpdateCallbacks)t(e)}onceAgentMappingReady(e){this.agentMapping&&this.agentMapping.size>0?e():this._mappingReadyCallbacks.push(e)}onAgentMappingUpdated(e){this._mappingUpdateCallbacks.push(e),this.agentMapping&&this.agentMapping.size>0&&e(this.agentMapping)}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(t=>{this._syncResolvers.push(t);const s=setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e);"object"==typeof s&&"unref"in s&&s.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;onRunEnded=null;handleSlashCommand=null;onPromptStart=null;instanceId;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway,this.instanceId=e.instanceId||"unknown",this.thirdPartyChannels=e.thirdPartyChannels??new Set,this.beeosFileResolver=e.beeosFileResolver,this.agentFetch=e.agentFetch,this.agentGatewayUrl=e.agentGatewayUrl,this.triggerAgentSync=e.triggerAgentSync,this.localSessionHistory=new LocalSessionHistory({logger:this.logger}),this.cronOverrides=createCronOverridesStore(this.logger),this.mcp=new McpService({transport:this.transport,logger:this.logger,state:this.state}),this.transport.setSessionUpdateObserver((e,t,s)=>{const r=s?.turnId;if(!r)return;const n=this.state.sessions.get(e);n&&(recordTurnIdForRole(n,t,r),recordToolCallTurnForUpdate(n,t,r))})}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e,t={}){const{ensureVerbose:s=!0}=t;this.state.sessions.has(e.sessionId);this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e),s&&this.ensureVerboseFull(e),this.subscribeSessionMessages(e)}async ensureVerboseFull(e){if(e.verbosePatched)return;const t=this.getGateway();if(t?.isConnected){e.verbosePatched=!0;try{await t.sessionsPatch(e.gatewaySessionKey,{verboseLevel:"full"})}catch(t){e.verbosePatched=!1,this.logger.debug?.(`[bridge-core] ensureVerboseFull failed: ${t instanceof Error?t.message:t}`)}}}ensureSessionForKey(e){const t=this.state.findSessionByKey(e);if(t)return t;const s=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(s),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${s.sessionId} key=${e}`),s}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.sessionsList();if(!t||"object"!=typeof t)return;const s=t.sessions;if(!Array.isArray(s))return;let r=0;for(const e of s){const t=e,s=t.sessionKey??t.key??t.id??t.name;if(!s||"string"!=typeof s)continue;if(this.state.findSessionByKey(s))continue;const n=t.label||t.derivedTitle||t.title||void 0,i=n&&!isPlaceholderTitle(n)?n:void 0,o=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:i});this.registerSession(o,{ensureVerbose:!1}),r++}r>0&&this.logger.info?.(`[bridge-core] preloadSessions: loaded ${r} session(s) from gateway`)}catch(e){this.logger.debug?.("[bridge-core] preloadSessions failed:",e)}else this.logger.debug?.("[bridge-core] preloadSessions skipped: gateway not connected")}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});const t=e;try{switch(t.method){case"initialize":this.handleInitialize(t);break;case"ping":this.transport.sendResult(t.id,{pong:!0});break;case"session/new":await this.handleSessionNew(t);break;case"session/load":await this.handleSessionLoad(t);break;case"session/list":await this.handleSessionList(t);break;case"session/rename":this.handleSessionRename(t);break;case"session/delete":await this.handleSessionDelete(t);break;case"session/clear":await this.handleSessionClear(t);break;case"session/prompt":await this.handleSessionPrompt(t);break;case"session/set_mode":await this.handleSessionSetMode(t);break;case"session/set_model":await this.handleSessionSetModel(t);break;case"agent/create":await this.handleAgentCreate(t);break;case"agent/update":await this.handleAgentUpdate(t);break;case"agent/delete":await this.handleAgentDelete(t);break;case"agent/applyTemplate":await this.handleApplyTemplate(t);break;case"skills/list":await this.handleSkillsList(t);break;case"skills/install":await this.handleSkillsInstall(t);break;case"skills/uninstall":await this.handleSkillsUninstall(t);break;case"skills/update":await this.handleSkillsUpdate(t);break;case"models/list":await this.handleModelsList(t);break;case"session/cancel":this.handleSessionCancel(t.id,t.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(t);break;default:"_beeos/session/history"===e.method?await this.handleSessionHistoryPage(e):"mcp/list"===e.method?await this.mcp.handleList(e):"mcp/set"===e.method?await this.mcp.handleSet(e):"mcp/unset"===e.method?await this.mcp.handleUnset(e):e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSkillInstalled=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,r=this.state.resolveSessionFromParams(t);if(!r){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}r.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,multipleSessions:!0,sessionCapabilities:{"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled},promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,s="string"==typeof e.params?.cwd?e.params.cwd:".",r=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s});this.registerSession(r,{ensureVerbose:!1}),this.transport.sendResult(e.id,{sessionId:r.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t,agentId:r.agentId,platformAgentId:r.platformAgentId??null}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async subscribeSessionMessages(e){if(e.messageSubscriptionActive)return;const t=this.getGateway();if(t?.isConnected)try{await t.sessionsMessagesSubscribe(e.gatewaySessionKey),e.messageSubscriptionActive=!0,this.logger.debug?.(`[bridge-core] session.message subscribed sessionKey=${e.gatewaySessionKey}`)}catch(t){this.logger.debug?.(`[bridge-core] session.message subscribe failed key=${e.gatewaySessionKey}: ${t instanceof Error?t.message:t}`)}}async resubscribeAllSessionMessages(){for(const e of this.state.sessions.values())e.messageSubscriptionActive=!1,await this.subscribeSessionMessages(e)}handleSessionMessage(e){const t=e?.sessionKey,s=e?.messageId;if(!t||!s)return;const r=this.state.sessionsByGatewayKey.get(t);if(!r)return;const n=e.message?.role;if("toolResult"===n||"tool"===n)return void this.handlePersistedToolResultMessage(r,e).catch(e=>{this.logger.warn?.(`[bridge-core] persisted toolResult handling failed sessionKey=${t} messageId=${s}: ${e instanceof Error?e.message:String(e)}`)});const i="user"===n?"user":"system"===n?"system":"assistant",o=r.lastTurnIdByRole?.[i];if(!o)return void this.logger.debug?.(`[bridge-core] session.message skipped: no lastTurnId role=${i} sessionKey=${t} messageId=${s}`);const a=e.message?.timestamp,l="number"==typeof a&&a>0?new Date(a).toISOString():void 0,d={turnId:o};"number"==typeof e.messageSeq&&(d.seq=e.messageSeq),this.transport.sendSessionUpdate(r.sessionId,{sessionUpdate:"message_id_bind",role:i,messageId:s,...l?{timestamp:l}:{}},d),r.lastTurnIdByRole&&(r.lastTurnIdByRole[i]=void 0),"assistant"===i&&reconcileMissingArtifacts({localHistory:this.localSessionHistory,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:()=>this.getCanvasState?.()??null,isSessionAlive:e=>this.state.sessions.has(e)},{session:r,turnId:o,messageId:s,runId:void 0})}async handlePersistedToolResultMessage(e,t){const s=t.messageId;if(!s)return;const r=extractPersistedToolResultEnvelope(t.message),n=this.extractPersistedToolResultFromHistory(e,s),i=mergePersistedToolResultEnvelopes(n,r);if(!i?.toolCallId)return void this.writeObs({component:"bridge-core",domain:"tool_result",name:"tool_result.skipped_no_tool_call_id",severity:"warn",payload:{sessionId:e.sessionId,messageId:s}});n&&this.writeObs({component:"bridge-core",domain:"tool_result",name:"tool_result.hydrated_from_history",severity:"debug",payload:{sessionId:e.sessionId,messageId:s,toolCallId:i.toolCallId,toolName:i.toolName}});const o=e.toolCallTurns?.get(i.toolCallId),a=o?.turnId??resolveTurnId(e);if(!a)return void this.writeObs({component:"bridge-core",domain:"tool_result",name:"tool_result.skipped_no_turn",severity:"warn",payload:{sessionId:e.sessionId,messageId:t.messageId,toolCallId:i.toolCallId,toolName:i.toolName}});const l=projectPersistedToolResultForReplay({toolCallId:i.toolCallId,toolName:i.toolName||o?.toolName||"tool",...i.rawText?{rawText:i.rawText}:{},...i.rawResult?{rawResult:i.rawResult}:{},...i.timestamp?{timestamp:i.timestamp}:{}});await emitToolResultProjectionForTurn({session:e,projection:l,toolCallId:i.toolCallId,turnId:a,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:()=>this.getCanvasState?.()??null,sendUpdate:s=>(a&&void 0===s._turnId&&(s._turnId=a),this.transport.sendSessionUpdate(e.sessionId,s,{turnId:a,..."number"==typeof t.messageSeq?{seq:t.messageSeq}:{}}))})}extractPersistedToolResultFromHistory(e,t){if(!e.agentId)return;const s=this.localSessionHistory.readEntries({agentId:e.agentId,sessionKey:e.gatewaySessionKey,sessionId:e.sessionId});return findPersistedToolResultEnvelopeByMessageId(s,t)}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const r=s.sessions;if(!Array.isArray(r))return;for(const t of r){const s=t,r=s.sessionKey??s.key??s.id??s.name;if(r&&"string"==typeof r&&deriveSessionId(r,this.instanceId)===e){const e=s.label||s.derivedTitle||s.title||void 0,t=e&&!isPlaceholderTitle(e)?e:void 0,n=createSessionObject({gatewayKey:r,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:t});return this.registerSession(n),n}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let t=this.state.sessions.get(e);if(!t&&e.startsWith("agent:")&&(t=this.state.findSessionByKey(e)),t||e.startsWith("agent:")||(t=await this.recoverSessionFromGateway(e)),t)return{session:t,created:!1};const s=e.startsWith("agent:"),r=this.config.gateway.agentId||this.getDefaultAgentId();return t=createSessionObject({gatewayKey:s?e:`agent:${r}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(t),{session:t,created:!0}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:s}=await this.resolveOrCreateSession(t),r=s.sessionId;this.state.lastActiveSessionId=r,s.chatReplayDedupKeys.clear(),s.sessionLoadInProgress=!0;try{const e=await this.emitHistoryTurns(s);this.emitCurrentStreamSnapshot(s,e)}finally{s.sessionLoadInProgress=!1}this.transport.sendResult(e.id,{sessionId:r}),this.onSessionLoaded?.(r,s.gatewaySessionKey,s.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})}async emitHistoryTurns(e){const t=new Set,s=e.sessionId;try{const r=this.getGateway(),n=e.assistantMq&&!e.assistantMq.closed,i=n?null:r,o=await loadHistory(i,e.gatewaySessionKey,100,{localHistory:this.localSessionHistory,agentId:e.agentId,sessionId:e.sessionId,logger:this.logger,emitObs:e=>{this.writeObs({component:"history-replay",domain:"lifecycle",name:e.name,severity:e.severity,payload:e.payload})}}),a=groupIntoTurns(o),l=a.filter(e=>"user"===e.role).length,d=a.filter(e=>"assistant"===e.role).length,c=a.filter(e=>e.toolCalls?.length).length,g=a.reduce((e,t)=>e+(t.toolCalls?.length??0),0);if(this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${s} messages=${o.length} turns=${a.length} userTurns=${l} asstTurns=${d} turnsWithToolCalls=${c} totalToolCalls=${g}`),l>0&&e.pendingUserTurnUpdates&&e.pendingUserTurnUpdates.length>0&&(e.pendingUserTurnUpdates=[]),a.length>0&&isPlaceholderTitle(e.title)&&"manual"!==e.titleSource){const t=a.find(e=>{if("user"!==e.role)return!1;if("internal_system"===e.provenance?.kind)return!1;if(isCronPromptText(e.content))return!1;const t=stripTransportMetadata(e.content).trim();return t.length>0&&!/^Sender\s*\(untrusted/i.test(t)});if(t?.content){const r=stripTransportMetadata(t.content).trim().slice(0,64).replace(/\n/g," ").trim();if(r){e.title=r,e.titleSource="truncated";const t=this.getGateway();t?.isConnected&&t.sessionsPatch(e.gatewaySessionKey,{label:r}).catch(()=>{}),this.transport.sendSessionUpdate(s,{sessionUpdate:"title_update",title:r}),this.tryGenerateTitleFromHistory(e,s,a)}}}const p=t=>`history:${e.gatewaySessionKey}:${t}`;let m=-1;if(n)for(let e=a.length-1;e>=0;e--)if("assistant"===a[e].role){m=e;break}const{replayedToolCallIds:h,streamCompleteCount:u}=await this.replayHistoryTurnsToSink(e,a,this.transport,{mqActive:Boolean(n),lastAsstIdx:m,turnIdFor:(e,t)=>p(t)});for(const e of h)t.add(e);this.logger.debug?.(`[bridge-core] emitHistoryTurns DONE sid=${s} historyStreamCompleteEmitted=${u} replayedToolCallIds=${t.size}`)}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}return t}async replayHistoryTurnsToSink(e,t,s,r){const n=e.sessionId,i=new Set;let o=0;for(let a=0;a<t.length;a++){const l=t[a],d=r.turnIdFor(l,a);if(r.mqActive&&a===r.lastAsstIdx)this.logger.debug?.(`[bridge-core] SKIP history_turn[${a}] assistant — mq active, deferring to emitCurrentStreamSnapshot`);else if("user"!==l.role||"internal_system"!==l.provenance?.kind)if("user"===l.role&&/^\s*system\s*\(untrusted[^)]*\)/i.test(l.content))this.logger.debug?.(`[bridge-core] SKIP history_turn[${a}] system-injected user message`);else if("user"===l.role&&isHeartbeatPromptText(l.content))this.logger.info?.(`[bridge-core] SKIP history_turn[${a}] heartbeat cron prompt sessionId=${e.sessionId} contentLen=${l.content.length} preview="${previewSanitizedText(l.content)}"`);else if("user"===l.role&&isCronPromptText(l.content))this.logger.info?.(`[bridge-core] SKIP history_turn[${a}] cron-triggered user message sessionId=${e.sessionId} contentLen=${l.content.length} preview="${previewSanitizedText(l.content)}"`);else{if("assistant"===l.role){const t=classifyHeartbeatFilter(l.content);if(t.filtered){this.logger.info?.(`[bridge-core] SKIP history_turn[${a}] heartbeat assistant reply reason=${t.reason} sessionId=${e.sessionId} contentLen=${l.content.length} preview="${previewSanitizedText(l.content)}"`);continue}}if("system"!==l.role)if(this.logger.debug?.(`[bridge-core] SENDING history_turn[${a}] role=${l.role} contentLen=${l.content.length} content=${JSON.stringify(l.content.slice(0,80))} tcCount=${l.toolCalls?.length??0}`),"user"===l.role)this.replayUserTurnAsChunks(s,n,l.content,l.metadata,l.timestamp,d,l._seq);else{if(await this.replayAssistantTurnAsStream(s,n,l,d),l.toolCalls)for(const e of l.toolCalls)e.id&&i.add(e.id);s.sendSessionUpdate(n,{sessionUpdate:"stream_complete",stopReason:"end_turn"},{turnId:d}),o+=1}else this.logger.debug?.(`[bridge-core] SENDING system_message[${a}] contentLen=${l.content.length}`),s.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:l.content},metadata:l.metadata,...l.timestamp?{timestamp:l.timestamp}:{}},{turnId:d,...void 0!==l._seq?{seq:l._seq}:{}})}else this.logger.debug?.(`[bridge-core] SKIP history_turn[${a}] internal_system user message`)}return{replayedToolCallIds:i,streamCompleteCount:o}}async handleSessionHistoryPage(e){const t=e.params??{},s="string"==typeof t.sessionId?t.sessionId:"";if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const r="string"==typeof t.cursor&&t.cursor.length>0?t.cursor:void 0,n="number"==typeof t.limit&&Number.isFinite(t.limit)&&t.limit>0?Math.min(1e3,Math.floor(t.limit)):100,{session:i}=await this.resolveOrCreateSession(s),o=this.getGateway(),a=await loadHistoryPage(o,i.gatewaySessionKey,{...r?{cursor:r}:{},limit:n,logger:this.logger}),l=groupIntoTurns(a.messages),d=new HistoryUpdateRecorder(this.transport);await this.replayHistoryTurnsToSink(i,l,d,{mqActive:!1,lastAsstIdx:-1,turnIdFor:(e,t)=>void 0!==e._seq?`history:${i.gatewaySessionKey}:seq:${e._seq}`:`history:${i.gatewaySessionKey}:seq:none:${r??"head"}:${t}`}),this.logger.debug?.(`[bridge-core] _beeos/session/history sid=${i.sessionId} cursor=${r??"<head>"} limit=${n} turns=${l.length} updates=${d.updates.length} hasMore=${a.hasMore} nextCursor=${a.nextCursor??"<none>"}`),this.transport.sendResult(e.id,{updates:d.updates,...a.nextCursor?{nextCursor:a.nextCursor}:{},hasMore:a.hasMore})}emitCurrentStreamSnapshot(e,t){const s=e.pendingUserTurnUpdates;if(s&&s.length>0)for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t);const r=e.assistantMq;if(r&&!r.closed){const s=r.peek(0);let n=0;if(t&&t.size>0)for(let e=0;e<s.length;e++){const r=s[e],i=r.sessionUpdate,o=r.toolCallId;if("tool_call"!==i&&"tool_call_update"!==i||!o||!t.has(o)){if("agent_message_chunk"===i){if(""===(r.content?.text??"").trim()&&e<s.length-1){const r=s[e+1];if(("tool_call"===r.sessionUpdate||"tool_call_update"===r.sessionUpdate)&&r.toolCallId&&t.has(r.toolCallId)){n=e+1;continue}}break}break}n=e+1}const i=resolveTurnId(e),o=i?{turnId:i}:void 0;for(let t=n;t<s.length;t++)this.transport.sendSessionUpdate(e.sessionId,s[t],o)}}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||this.cronIsolatedSessionKeys.has(e.gatewaySessionKey)||t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,r=s.sessionKey??s.key??s.id??s.name;if(!r||"string"!=typeof r)continue;if(isBackgroundSessionKey(r))continue;if(this.cronIsolatedSessionKeys.has(r))continue;const n=deriveSessionId(r,this.instanceId),i=t.find(e=>e.sessionId===r||e.sessionId===n||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===r),o=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0,a=o&&!isPlaceholderTitle(o)?o:void 0,l=s.model,d=s.modelProvider;if(i){if(a&&isPlaceholderTitle(i.title)){i.title=a;const e=this.state.sessions.get(i.sessionId);e&&isPlaceholderTitle(e.title)&&(e.title=a)}const e=s.updatedAt;e&&(i.updatedAt=e),l&&(i.model=l),d&&(i.modelProvider=d)}else{const e=createSessionObject({gatewayKey:r,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s.cwd,title:a});this.registerSession(e,{ensureVerbose:!1}),t.push({sessionId:e.sessionId,title:a||"",cwd:s.cwd,updatedAt:s.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(r),model:l,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const r=s.slice(0,64),n=this.state.sessions.get(t);if(!n)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);n.title=r,n.titleSource="manual";const i=n.gatewaySessionKey,o=this.getGateway();o?.isConnected&&o.sessionsPatch(i,{label:r}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:t,title:r})}static TITLE_TEMPLATES=[{match:["今日日报","日报"],zh:"今日工作日报",en:"Daily Work Log"},{match:["周报","周报总结","本周周报"],zh:"本周工作周报",en:"Weekly Work Report"},{match:["月报","月度汇报","月度总结"],zh:"本月工作月报",en:"Monthly Work Report"},{match:["工作日志","日志"],zh:"工作日志整理",en:"Work Log Review"},{match:["晨会","今日晨会"],zh:"今日晨会纪要",en:"Daily Standup Notes"},{match:["复盘","今日复盘"],zh:"今日工作复盘",en:"Daily Retrospective"},{match:["新会话","new chat","new conversation"],zh:"新会话",en:"New Conversation"}];static TITLE_SYSTEM_PROMPT='You are a conversation title generator. Your task is to generate a concise, human-readable title summarizing the topic of the user\'s question together with the assistant\'s answer.\n\n### Rules:\n- Length: 3-6 words in English, or 6-20 characters in Chinese (up to 24 if needed to keep all key domain terms intact).\n- Language: Match the user\'s input language exactly. Mixed zh/en input → use the dominant language.\n- Extract the core topic (noun-centric) of the Q&A exchange, not the action verb.\n- Use the assistant\'s answer to disambiguate or enrich the topic when the question alone is vague (e.g. "帮我看下" + an answer about x402 → "x402 支付集成").\n- No punctuation, no quotation marks, no emojis, no markdown.\n- No prefix like "Title:" or "Here is". Output the title only.\n- Avoid generic words: "help", "question", "chat", "problem", "task".\n- If both the question and answer are too vague to summarize, return "新会话" (for zh) or "New Conversation" (for en).\n\n### Input Format:\nThe user message will contain both the question and the assistant\'s answer, delimited like this:\n[User Question]\n...question text...\n\n[Assistant Answer]\n...answer text (may be truncated with "..." in the middle)...\n\n### Output Format:\nRespond ONLY with valid JSON, no other text:\n{"title": "<your title here>"}\n\n### Examples:\nInput:\n[User Question]\n帮我写一个关于BeeOS设备接入的PRD\n\n[Assistant Answer]\n好的,这是 BeeOS 设备接入 PRD 的草稿:1. 目标 ...\nOutput: {"title": "BeeOS 设备接入 PRD"}\n\nInput:\n[User Question]\n帮我看下\n\n[Assistant Answer]\n你说的 x402 支付协议是一个基于 HTTP 402 的链上支付标准 ...\nOutput: {"title": "x402 支付协议解析"}\n\nInput:\n[User Question]\nWhat\'s the best way to integrate x402 payment protocol?\n\n[Assistant Answer]\nYou have three options: 1. Use the official SDK ...\nOutput: {"title": "x402 Payment Integration"}\n\nInput:\n[User Question]\nhi\n\n[Assistant Answer]\nHello! How can I help you today?\nOutput: {"title": "New Conversation"}';matchTitleTemplate(e){const t=e.trim().toLowerCase();for(const s of AcpGatewayBridgeCore.TITLE_TEMPLATES)if(s.match.some(e=>e.toLowerCase()===t))return/[\u4e00-\u9fff]/.test(e)?s.zh:s.en}isGuardMessage(e){const t=e.trim();return[...t].length<3||[...t].every(e=>/[\p{P}\p{S}\s]/u.test(e))}truncateQuestionForTitle(e){return e.length>2e3?e.slice(0,1500):e}truncateAnswerForTitle(e){return e.length<=1e3?e:`${e.slice(0,500)}\n...\n${e.slice(-500)}`}generateTitleFromQA(e,t,s,r){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen SKIP guard message sid=${t}`);const n=this.matchTitleTemplate(s);if(n){e.title=n,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:n}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:n})}const i=(process.env.AROUTER_BASE_URL||"").replace(/\/$/,""),o=process.env.AROUTER_API_KEY||"";if(!i||!o)return void this.logger.info?.(`[bridge-core] title-gen SKIP sid=${t}: no AROUTER env vars`);const a=`[User Question]\n${this.truncateQuestionForTitle(s)}\n\n[Assistant Answer]\n${this.truncateAnswerForTitle(r)}`;this.logger.info?.(`[bridge-core] title-gen CALLING LLM sid=${t} q="${s.slice(0,60)}" aLen=${r.length}`),fetch(`${i}/v1/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({model:"openai/gpt-4o-mini",max_tokens:32,temperature:.3,messages:[{role:"system",content:AcpGatewayBridgeCore.TITLE_SYSTEM_PROMPT},{role:"user",content:a}]})}).then(async s=>{if(!s.ok){const e=await s.text().catch(()=>"");return void this.logger.info?.(`[bridge-core] title-gen FAILED sid=${t} status=${s.status} body=${e.slice(0,300)}`)}const r=await s.json(),n=r.choices?.[0]?.message?.content?.trim();if(!n)return;const i=n.replace(/^```json\s*/,"").replace(/```$/,"").trim();let o;try{o=JSON.parse(i).title?.trim()??""}catch{return void this.logger.debug?.(`[bridge-core] title-gen JSON parse failed: ${i}`)}if(!o)return;if("manual"===e.titleSource)return void this.logger.debug?.("[bridge-core] title-gen discarded: user already renamed");this.logger.info?.(`[bridge-core] title-gen OK sid=${t} title="${o}"`),e.title=o,e.titleSource="auto";const a=this.getGateway();a?.isConnected&&a.sessionsPatch(e.gatewaySessionKey,{label:o}).catch(()=>{}),this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:o})}).catch(e=>{this.logger.info?.(`[bridge-core] title-gen ERROR sid=${t}: ${e instanceof Error?e.message:e}`)})}async tryGenerateTitleFromHistory(e,t,s){if("manual"===e.titleSource||"auto"===e.titleSource)return;const r=this.getGateway();if(!r?.isConnected)return;let n=s;if(!n)try{const t=await loadHistory(r,e.gatewaySessionKey);n=groupIntoTurns(t)}catch(e){return void this.logger.debug?.(`[bridge-core] title-gen loadHistory failed sid=${t}: ${e instanceof Error?e.message:e}`)}const i=n.find(e=>{if("user"!==e.role)return!1;if(!e.content?.trim())return!1;if("internal_system"===e.provenance?.kind)return!1;if(isCronPromptText(e.content))return!1;const t=stripTransportMetadata(e.content).trim();return!!t&&!/^Sender\s*\(untrusted/i.test(t)});if(!i)return;const o=n.indexOf(i),a=n.slice(o+1).find(e=>"assistant"===e.role&&!!e.content?.trim());if(!a)return;const l=stripTransportMetadata(i.content).trim(),d=a.content.trim();l&&d&&this.generateTitleFromQA(e,t,l,d)}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const r=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${r} gwConnected=${!!this.getGateway()?.isConnected}`);let n=!1;const i=this.getGateway();if(i?.isConnected&&r)try{await i.sessionsDelete(r),n=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${r}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${r}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await i.sessionsReset(r),n=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${r}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${r}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else r&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${r}`);void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.assistantMq=void 0,s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const o=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),o.add(e));for(const[e,t]of this.state.toolCallIdMap)o.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(r),this.transport.resetSessionUpdateCount(t),n&&this.onSessionDeleted?.(r),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${r} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const r=this.getGateway(),n=r?.isConnected;if(s.promptDone||void 0===s.pendingPromptId&&void 0===s.currentRunId||(this.logger.info?.(`[bridge-core] session/clear: cancelling active run sid=${t} runId=${s.currentRunId}`),n&&(s.currentRunId||s.gatewayRequestId)&&r.agentCancel(s.gatewaySessionKey,{runId:s.currentRunId,requestId:s.gatewayRequestId}),this.endRun(s,{kind:"complete",stopReason:"cancelled"})),n&&s.gatewaySessionKey)try{await r.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,r=t?this.state.sessions.get(t):void 0,n=this.getGateway();if(r&&n?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await n.sessionsPatch(r.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,r=t?this.state.sessions.get(t):void 0,n=this.getGateway();let i=s??null,o=null;if(r&&n?.isConnected&&s)try{const e=await n.sessionsPatch(r.gatewaySessionKey,{model:s}),t=e?.resolved;t?.model&&(i=t.model),t?.modelProvider&&(o=t.modelProvider)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:i,modelProvider:o})}async createAgentShell(e,t,s){const r=this.getGateway();if(!r?.isConnected)throw new Error("gateway not connected");const n=`agent-${randomUUID().replace(/-/g,"").slice(0,12)}`,i=n,o=e,a=join(this.getOpenclawHome(),`workspace-${n}`),l=await r.request("agents.create",{name:i,workspace:a,...s?{model:s}:{},...t?.emoji?{emoji:t.emoji}:{},...t?.avatar?{avatar:t.avatar}:{}}),d=l?.agentId??n,c=l?.workspace??a;if(o)try{await r.request("agents.update",{agentId:d,name:o})}catch(e){this.logger.warn?.(`[bridge-core] createAgentShell: failed to set display name for "${d}": ${e instanceof Error?e.message:e}`)}return{agentId:d,workspace:c}}async handleAgentCreate(e){const t=e.params,s=(t?.name??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing agent name"});const r=this.getGateway();if(r?.isConnected)try{const{agentId:n,workspace:i}=await this.createAgentShell(s,t?.identity,t?.model),o=t?.files??{};for(const[e,t]of Object.entries(o))if("IDENTITY.md"!==e){if("string"==typeof t)try{await r.request("agents.files.set",{agentId:n,name:e,content:t})}catch(t){this.logger.warn?.(`[bridge-core] agent/create: failed to write "${e}" for "${n}": ${t instanceof Error?t.message:t}`)}}else this.logger.warn?.("[bridge-core] agent/create: ignoring IDENTITY.md in files (identity is set via structured fields)");this.logger.info?.(`[bridge-core] Created agent "${n}" (name="${s}") workspace=${i}`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/create: agent sync after create failed: ${e instanceof Error?e.message:e}`)}const a=this.agentMapping?.get(n)?.id??"";this.transport.sendResult(e.id,{agentId:n,platformAgentId:a,workspace:i})}catch(t){this.logger.error?.(`[bridge-core] Failed to create agent "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Create agent failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleApplyTemplate(e){const t=e.params,s=(t?.name??"").trim(),r=(t?.packageUrl??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing agent name"});if(!r)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"packageUrl"});const n=this.getGateway();if(n?.isConnected)try{const n=await fetch(r);if(!n.ok)throw new TemplatePackageError(`failed to download package: HTTP ${n.status}`);const i=Buffer.from(await n.arrayBuffer());if(i.length>MAX_ZIP_BYTES)throw new TemplatePackageError(`package too large: ${i.length} bytes (max ${MAX_ZIP_BYTES})`);const o=t?.sha256?.trim().toLowerCase();if(o){if(createHash("sha256").update(i).digest("hex")!==o)throw new TemplatePackageError("package sha256 mismatch")}const a=parseAndValidateTemplatePackage(i),{agentId:l,workspace:d}=await this.createAgentShell(s,t?.identity,t?.model);let c=0;for(const[e,t]of a.workspaceFiles){const s=resolve(d,e),r=relative(d,s);if(""===r||r.startsWith("..")||path.isAbsolute(r))throw new TemplatePackageError(`refusing to write outside workspace: ${e}`);await mkdir(path.dirname(s),{recursive:!0}),await writeFile(s,t),c++}const g=a.manifest?.slug??"(no manifest)",p=a.manifest?.version??"";this.logger.info?.(`[bridge-core] agent/applyTemplate: applied "${g}${p?`@${p}`:""}" to agent "${l}" (${c} files) workspace=${d}`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/applyTemplate: agent sync after apply failed: ${e instanceof Error?e.message:e}`)}const m=this.agentMapping?.get(l)?.id??"";this.transport.sendResult(e.id,{agentId:l,platformAgentId:m,workspace:d})}catch(t){if(t instanceof TemplatePackageError)return this.logger.warn?.(`[bridge-core] agent/applyTemplate: invalid package: ${t.message}`),void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:`Invalid template package: ${t.message}`});this.logger.error?.(`[bridge-core] Failed to apply template for "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Apply template failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleAgentUpdate(e){const t=e.params,s=(t?.agentId??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"agentId"});const r=t?.name?.trim(),n=t?.model?.trim(),i=t?.identity?.emoji?.trim(),o=t?.identity?.avatar?.trim();if(!(r||n||i||o))return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"No updatable fields provided (name/model/identity)"});const a=this.getGateway();if(a?.isConnected)try{const t=await a.request("agents.update",{agentId:s,...r?{name:r}:{},...n?{model:n}:{},...i?{emoji:i}:{},...o?{avatar:o}:{}}),l=t?.agentId??s;this.logger.info?.(`[bridge-core] Updated agent "${l}"${r?` name="${r}"`:""}`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/update: agent sync after update failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{agentId:l})}catch(t){this.logger.error?.(`[bridge-core] Failed to update agent "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update agent failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleAgentDelete(e){const t=e.params,s=(t?.agentId??"").trim();if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"agentId"});if("main"===normalizeAgentId(s))return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:'"main" cannot be deleted; delete the instance instead'});const r=this.getGateway();if(!r?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const n=t?.deleteFiles??!0;try{const t=await r.request("agents.delete",{agentId:s,deleteFiles:n}),i=t?.agentId??s;this.logger.info?.(`[bridge-core] Deleted agent "${i}" (deleteFiles=${n})`);try{await(this.triggerAgentSync?.())}catch(e){this.logger.warn?.(`[bridge-core] agent/delete: agent sync after delete failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{agentId:i,..."number"==typeof t?.removedBindings?{removedBindings:t.removedBindings}:{}})}catch(t){this.logger.error?.(`[bridge-core] Failed to delete agent "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Delete agent failed: ${t instanceof Error?t.message:String(t)}`})}}async handleModelsList(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("models.list",{});this.transport.sendResult(e.id,s??{models:[]})}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${t instanceof Error?t.message:t}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const t=e.params?.agentId,s=await this.fetchSimplifiedSkills(t);this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(e){const t=this.getGateway();if(!t?.isConnected)return[];try{const s=await t.skillsStatus(e);if(!s?.skills)return[];const r=s.skills.filter(e=>!1!==e.eligible||!0===e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0,disabled:e.disabled||void 0,skillKey:e.skillKey||void 0,iconUrl:void 0})),n=await this.getSkillsDir(e);for(const e of r){const t=e.skillKey??e.name;try{const s=await readFile(join(n,t,".beeos-meta.json"),"utf8"),r=JSON.parse(s);r?.iconUrl&&(e.iconUrl=r.iconUrl)}catch{}}return r}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(e){const t=await this.fetchSimplifiedSkills(e);this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:t}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:t.length}})}getOpenclawHome(){return process.env.OPENCLAW_HOME||join(homedir(),".openclaw")}async getSkillsDir(e){const t=this.getOpenclawHome();if(!e||"main"===e)return join(t,"skills");const s=await this.resolveAgentWorkspaceDir(e);return join(s??join(t,`workspace-${e}`),"skills")}async resolveAgentWorkspaceDir(e){const t=this.getGateway();if(!t?.isConnected)return null;try{const s=await t.request("agents.list",{}),r=normalizeAgentId(e),n=s?.agents?.find(e=>normalizeAgentId(e.id)===r);return n?.workspace??null}catch(t){return this.logger.warn?.(`[bridge-core] resolveAgentWorkspaceDir("${e}") failed: ${t instanceof Error?t.message:t}`),null}}async handleSkillsInstall(e){const t=e.params,s=t?.zipUrl,r=t?.files,n=t?.agentId;let i;try{i=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill slug"})}if(!r?.length&&!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skill files or zipUrl"});const o=resolveSkillInstallDir(await this.getSkillsDir(n),i);try{if(await mkdir(o,{recursive:!0}),s){const e=await fetchWithRetry(s,{maxRetries:4,logger:this.logger});await extractZipToDir(e,o)}else if(r?.length){const e=resolve(o);for(const t of r){const s=join(o,t.path);if(!resolve(s).startsWith(e+sep))throw new Error(`File path escapes skill directory: ${t.path}`);await mkdir(join(s,".."),{recursive:!0});const r=await fetch(t.downloadUrl,{signal:AbortSignal.timeout(12e4)});if(!r.ok)throw new Error(`Failed to download ${t.path}: ${r.status}`);const n=Buffer.from(await r.arrayBuffer());await writeFile(s,n)}}if(t?.iconUrl)try{await writeFile(join(o,".beeos-meta.json"),JSON.stringify({iconUrl:t.iconUrl}),"utf8")}catch{}this.logger.info?.(`[bridge-core] Installed skill "${i}"${n?` for agent "${n}"`:""} via ${s?"zip":"files"}`),this.transport.sendResult(e.id,{success:!0}),this.onSkillInstalled?.(i,o),await this.pushSkillsList(n)}catch(t){this.logger.error?.(`[bridge-core] Failed to install skill "${i}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Install failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUninstall(e){const t=e.params,s=t?.agentId;let r;try{r=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill name"})}const n=resolveSkillInstallDir(await this.getSkillsDir(s),r),i=resolveSkillInstallDir(join(homedir(),".agents","skills"),r);try{let t=!1;try{const e=await lstat(n);if(e.isSymbolicLink()){const e=await readlink(n);await rm(n,{force:!0}),await rm(e,{recursive:!0,force:!0}),t=!0}else e.isDirectory()&&(await rm(n,{recursive:!0,force:!0}),t=!0)}catch{}try{const e=await lstat(i);(e.isDirectory()||e.isSymbolicLink())&&(await rm(i,{recursive:!0,force:!0}),t=!0)}catch{}t||this.logger.warn?.(`[bridge-core] Skill "${r}" not found in any known location`),this.logger.info?.(`[bridge-core] Uninstalled skill "${r}"${s?` for agent "${s}"`:""}`),this.transport.sendResult(e.id,{success:!0}),await this.pushSkillsList(s)}catch(t){this.logger.error?.(`[bridge-core] Failed to uninstall skill "${r}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Uninstall failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUpdate(e){const t=e.params,s=t?.skillKey,r=t?.enabled,n=t?.agentId;if(!s||"boolean"!=typeof r)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skillKey or enabled"});const i=this.getGateway();if(i?.isConnected)try{const t=await i.skillsUpdate(s,r,n);this.logger.info?.(`[bridge-core] Updated skill "${s}" enabled=${r}${n?` for agent "${n}"`:""}`),this.transport.sendResult(e.id,{ok:t?.ok??!0,skillKey:s,enabled:r}),await this.pushSkillsList(n)}catch(t){this.logger.error?.(`[bridge-core] Failed to update skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.InternalError,{message:"Gateway not connected"})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:r,created:n}=await this.resolveOrCreateSession(t);n&&this.onSessionLoaded?.(r.sessionId,r.gatewaySessionKey,r.platformAgentId);const i=r.sessionId;this.state.lastActiveSessionId=i,this.onPromptStart?.();const o=this.getGateway();if(!o?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);await this.ensureVerboseFull(r);let a=extractPromptBlocks(sanitizePromptPayload(s));const l=a.filter(e=>"text"===e.type&&"text"in e&&"string"==typeof e.text).map(e=>e.text).join("\n");if(r.fileArtifactDisplayMode=prefersChatFileArtifacts(l)?"chat":"auto",!r.title||isPlaceholderTitle(r.title)){const e=a.find(e=>"text"===e.type&&e.text);if(e&&"text"in e&&e.text){const t=stripTransportMetadata(e.text).trim();if(this.logger.info?.(`[bridge-core] title-gen sid=${i} existing="${r.title}" cleanText="${t.slice(0,80)}" isPlaceholder=${isPlaceholderTitle(t)}`),t&&!isPlaceholderTitle(t)){const e=t.slice(0,64).replace(/\n/g," ").trim();r.title=e,r.titleSource="truncated",o?.isConnected&&e&&o.sessionsPatch(r.gatewaySessionKey,{label:e}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendSessionUpdate(i,{sessionUpdate:"title_update",title:e})}}}const d=buildResolutionPlan(a);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let c;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(a);if(e.refs.length>0)try{c=await this.beeosFileResolver.resolveMetadataForPrompt(e)}catch(e){this.logger.warn?.(`[bridge-core] BeeOS file resolution failed: ${e instanceof Error?e.message:e}`)}}const g=toGatewayPrompt(a,{beeosFileResolutionsByUri:c});if("error"in g)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:g.error});const{message:p,attachments:m}=g,h=resolveStandaloneSlashCommandName(a,p)??resolveSlashCmd(a,p),u="string"==typeof h;if(h&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(h)}catch(e){return t.logger.warn?.(`[bridge-core] /${h} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(r,a,resolveTurnId(r)),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${h}...`},timestamp:(new Date).toISOString()},{turnId:resolveTurnId(r)}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const f="chat.send";let y=u?p:withUserMessagePrefix(p);const w=this.config.canvasEnabled&&!u&&!0===r.canvasActive&&!r.channelType;if(w){const e=r.promptsSinceCanvasInjection??0;!r.canvasPromptInjected||e>=10?(y=`${CANVAS_TERM_MAPPING}\n\n${y}`,r.canvasPromptInjected=!0,r.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):r.promptsSinceCanvasInjection=e+1}if(w){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(r.gatewaySessionKey);y=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${y}`}const t=r.referencedSurfaceId;t&&(y=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${y}`,r.referencedSurfaceId=void 0)}if(!u){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){r.activeMentions=t;y=`${buildMentionInstruction(t)}\n${y}`,this.logger.debug?.(`[bridge-core] delegation mention instruction injected for ${t.length} agent(s)`)}else r.activeMentions=void 0}u&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${i} command=/${h}`),r.promptDone||void 0===r.pendingPromptId&&void 0===r.currentRunId||(this.logger.warn?.(`[bridge-core] handleSessionPrompt: force-ending stale run sid=${i} oldRunId=${r.currentRunId} oldPromptId=${r.pendingPromptId}`),(r.currentRunId||r.gatewayRequestId)&&o.agentCancel(r.gatewaySessionKey,{runId:r.currentRunId,requestId:r.gatewayRequestId}),this.endRun(r,{kind:"complete",stopReason:"cancelled"})),this.state.resetForNewRun(r,i,this.transport),r.pendingPromptId=e.id,r.gatewayMethod=f;const I=resolveTurnId(r);this.emitPromptAsUserSessionUpdates(r,a,I),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""},timestamp:(new Date).toISOString()},I?{turnId:I}:void 0);const S=extractCronJobId(p);S&&(r.cronJobId=S),this.schedulePromptTimeout(r);const v=m.length>0?m.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0,b=`req_${randomUUID().replace(/-/g,"")}`,T={sessionKey:r.gatewaySessionKey,message:y,deliver:!1,idempotencyKey:randomUUID()};v?.length&&(T.attachments=v);if(!o.sendFrame({type:"req",id:b,method:f,params:T}))return this.state.clearPromptTimeout(r),r.pendingPromptId=void 0,r.assistantMq?.close(),r.assistantMq=void 0,r.pendingUserTurnUpdates=void 0,void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});r.gatewayRequestId=b,this.state.promptsByGatewayRequestId.set(b,r)}replayUserTurnAsChunks(e,t,s,r,n,i,o){const a=splitUserReplayBlocks(s),l=i||void 0!==o?{...i?{turnId:i}:{},...void 0!==o?{seq:o}:{}}:void 0;for(const s of a){let i;if("text"===s.type){if(!s.text)continue;i={type:"text",text:s.text}}else i="resource_link"===s.type?{type:"resource_link",uri:s.uri??"",...s.fileName?{name:s.fileName}:{},...s.mimeType?{mimeType:s.mimeType}:{}}:{type:s.type,...s.mimeType?{mimeType:s.mimeType}:{},...s.fileName?{fileName:s.fileName}:{}};e.sendSessionUpdate(t,{sessionUpdate:"user_message_chunk",content:i,...r?{metadata:r}:{},...n?{timestamp:n}:{}},l)}}async replayAssistantTurnAsStream(e,t,s,r){const n=e=>{if(r||void 0!==e)return{...r?{turnId:r}:{},...void 0!==e?{seq:e}:{}}},i=new Set,o=(s,r,o)=>{if(!s?.length)return;const a=n(o);for(const n of s)n.uri&&!i.has(n.uri)&&(i.add(n.uri),e.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:n.uri,name:n.name??"file",...n.mimeType?{mimeType:n.mimeType}:{}},...r?{timestamp:r}:{}},a))},a=n(s._seq);if(!s.sourceMessages?.length){const r={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls,...s.timestamp?{timestamp:s.timestamp}:{}};return s.metadata&&(r.metadata=s.metadata),e.sendSessionUpdate(t,r,a),void o(s.files,s.timestamp,s._seq)}for(const r of s.sourceMessages){const i=r.updateType??"agent_message_chunk",a=n(r._seq);switch(i){case"agent_message_chunk":e.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:r.content},...r.timestamp?{timestamp:r.timestamp}:{},...r.metadata?{metadata:r.metadata}:{}},a);break;case"agent_thought_chunk":e.sendSessionUpdate(t,{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:r.content},...r.timestamp?{timestamp:r.timestamp}:{}},a);break;case"tool_call":{const n=r.toolCallId??"",i=s.toolCalls?.find(e=>e.id===n),o=r.toolName??"tool",l=r.content??"";let d={};if(l)try{d=JSON.parse(l)}catch{}e.sendSessionUpdate(t,buildToolCallPayload({toolCallId:n,toolName:o,args:d,kind:i?.kind,status:i?.status??"in_progress",timestamp:r.timestamp}),a);break}case"tool_call_update":{const n=r.toolCallId??"",i=s.toolCalls?.find(e=>e.id===n),l=r.toolName??i?.name??"tool",d=r.content??i?.output??"",c=await this.resolveReplayResourceLinks(n,l,r.files);e.sendSessionUpdate(t,buildToolCallUpdatePayload({toolCallId:n,toolName:l,outputText:d,kind:i?.kind,status:i?.status??"completed",resourceLinks:c,timestamp:r.timestamp}),a),o(c,r.timestamp,r._seq);break}}}o(s.files,s.timestamp,s._seq)}async resolveReplayResourceLinks(e,t,s){let r=s?.map(e=>({uri:e.uri,name:e.name??"file",...e.mimeType?{mimeType:e.mimeType}:{}}));if(r?.length)return r;if(r=await readEnrichmentCache(e).catch(()=>{}),r?.length)return r;if("beeos_upload_file"!==t)return;const n=await readUploadToolResult(e).catch(()=>{});if(!n)return;const i=buildToolResultSessionContentBlocks(t,n).filter(e=>"resource_link"===e.content?.type&&e.content.uri).map(e=>({uri:e.content.uri,name:e.content.name??"file",...e.content.mimeType?{mimeType:e.content.mimeType}:{}}));return i.length>0?i:void 0}emitPromptAsUserSessionUpdates(e,t,s){const r=(new Date).toISOString(),n=s?{turnId:s}:void 0;for(const i of t){const t=this.toUserSessionUpdateContent(i);if(!t)continue;const o={sessionUpdate:"user_message_chunk",content:t,timestamp:r};s&&void 0===o._turnId&&(o._turnId=s),this.transport.sendSessionUpdate(e.sessionId,o,n),e.pendingUserTurnUpdates?.push(o)}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,r=t.mimeType??t.mime_type??e.mimeType??e.mime_type,n=t.text??e.text,i=t.data??e.data,o=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!n&&!i)return;return{type:"resource",resource:{...s?{uri:s}:{},...r?{mimeType:r}:{},...n?{text:n}:{},...i?{data:i}:{},...o?{fileName:o}:{}}}}}handleSessionCancel(e,t){const s=t&&"object"==typeof t&&"sessionId"in t?t.sessionId:void 0;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const r=this.state.sessions.get(s);if(!r)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const n=this.getGateway(),i=n?.isConnected,o=!(!r.currentRunId&&!r.gatewayRequestId);if(i&&o){n.agentCancel(r.gatewaySessionKey,{runId:r.currentRunId,requestId:r.gatewayRequestId})}this.endRun(r,{kind:"complete",stopReason:"cancelled"})||this.transport.sendSessionUpdate(r.sessionId,{sessionUpdate:"stream_complete",stopReason:"cancelled"},{turnId:resolveTurnId(r)}),void 0!==e&&this.transport.sendResult(e,{})}endRun(e,t){if(e.promptDone)return this.logger.debug?.(`[bridge-core] endRun skipped (promptDone=true) sessionId=${e.sessionId} outcome=${t.kind}`),!1;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return this.logger.debug?.(`[bridge-core] endRun skipped (no pendingPromptId and no currentRunId) sessionId=${e.sessionId} outcome=${t.kind}`),!1;const s=resolveTurnId(e),r=s?{turnId:s}:void 0;e.promptDone=!0;const n=e.currentRunId;if("complete"===t.kind){if(n&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(n,s,t.stopReason)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason},r),this.tryGenerateTitleFromHistory(e,e.sessionId)}else n&&this.onRunFailed&&this.onRunFailed(n,t.message),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t.code,t.message),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:"error",error:t.message},r);return this.cleanupPrompt(e),this.onRunEnded?.(e,t),!0}schedulePromptTimeout(e){let t;if(this.state.clearPromptTimeout(e),e.cronJobId){const s=this.cronOverrides.get(e.cronJobId)??this.config.cronMaxDurationDefaultMs;t=Math.max(CRON_DURATION_FLOOR_MS,Math.min(this.config.cronMaxDurationCapMs,s))}else t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0===e.pendingPromptId&&void 0===e.currentRunId||((e.currentRunId||e.gatewayRequestId)&&this.getGateway()?.agentCancel(e.gatewaySessionKey,{runId:e.currentRunId,requestId:e.gatewayRequestId}),this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t),this.state.runIdToCronJobId.delete(t),this.state.markRunCompleted(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.gatewayRequestId&&this.state.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close(),e.assistantMq=void 0,e.pendingUserTurnUpdates=void 0}completePrompt(e,t){this.endRun(e,{kind:"complete",stopReason:t})}failPrompt(e,t,s){this.endRun(e,{kind:"error",code:t,message:s})}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}handleGatewayResponse(e){const t=e.id;if(!t)return;const s=this.state.promptsByGatewayRequestId.get(t);if(!s||s.promptDone)return;if(!e.ok){const r=e.error,n=r?.message??"gateway returned error";return this.logger.warn?.(`[bridge-core] handleGatewayResponse: gateway rejected prompt gatewayRequestId=${t} error=${n}`),void this.failPrompt(s,Errors.SendFailed.rpcCode,n)}const r=e.payload,n=r?.runId;n?(s.currentRunId=n,s.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(n,s),this.logger.debug?.(`[bridge-core] handleGatewayResponse: bound runId=${n} gatewayRequestId=${t} sessionId=${s.sessionId}`)):this.logger.debug?.(`[bridge-core] handleGatewayResponse: no runId in payload gatewayRequestId=${t} sessionId=${s.sessionId}`)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s){const s="string"==typeof t.stopReason?t.stopReason:void 0;this.completePrompt(e,s||"end_turn")}else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/","."),r={...e.params??{}},n="cron.update"===s;!n||r.patch&&"object"==typeof r.patch||(r.patch={});const i=n?r.patch:r,o=i._beeos_meta;let a;if(delete i._beeos_meta,"cron.add"!==s&&"cron.update"!==s||!0!==i._beeos_internal)void 0!==i._beeos_internal&&delete i._beeos_internal;else if(delete i._beeos_internal,i.delivery={mode:"none"},"cron.add"===s){const e=await this.allocateCronChannel(i.name??"cron");e&&(i.description=encodeBeeosChannelTag(i.description,e))}else{const e=r.id,t="description"in i,s=i.description;let n,o=decodeBeeosChannelId(s);!o&&e&&(o=this.cronChannelCache.get(e)),!o&&e&&(n=await this.lookupCronJobDescription(e),o=decodeBeeosChannelId(n));let l=!1;o||(o=await this.allocateCronChannel(i.name??"cron"),l=!0),o&&(t?i.description=encodeBeeosChannelTag(s,o):l&&(i.description=encodeBeeosChannelTag(n,o)),a=o)}try{const n=await t.request(s,r),l="cron.list"===s||"cron.status"===s?this.enrichCronResponseWithOverrides(n):n;if(this.transport.sendResult(e.id,l??{}),("cron.add"===s||"cron.update"===s)&&n&&"object"==typeof n){const e=n.id;if(e&&void 0!==o?.maxDurationMs){const t=Math.max(CRON_DURATION_FLOOR_MS,Math.min(this.config.cronMaxDurationCapMs,o.maxDurationMs));this.cronOverrides.set(e,t)}}if("cron.remove"===s){const t=e.params?.id;t&&(this.cronOverrides.delete(t),this.cronChannelCache.delete(t))}if(("cron.add"===s||"cron.update"===s)&&n&&"object"==typeof n){const e=n.id,t=decodeBeeosChannelId(i.description)??a;e&&t&&this.setCronChannelCache(e,t)}"cron.add"!==s&&"cron.update"!==s&&"cron.remove"!==s||this.refreshCronIsolatedSessionKeys()}catch(t){const r=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${r}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:r})}}enrichCronResponseWithOverrides(e){if(!e||"object"!=typeof e)return e;const t=e,s=e=>{if(!e||"object"!=typeof e)return e;const t=e,s=t.id;if("string"!=typeof s)return e;const r=this.cronOverrides.get(s);return"number"!=typeof r?e:{...t,_beeos_meta:{maxDurationMs:r}}};if(Array.isArray(t.jobs))return{...t,jobs:t.jobs.map(s)};if(t.jobs&&"object"==typeof t.jobs){const e=t.jobs,r={};for(const[t,n]of Object.entries(e))r[t]=s(n);return{...t,jobs:r}}return e}async allocateCronChannel(e){if(this.agentFetch&&this.agentGatewayUrl)try{const t=`${this.agentGatewayUrl}/api/v1/channels`,s=await this.agentFetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({participants:[this.instanceId,"service:cron"],metadata:{source:"cron",job_name:e}})});if(!s.ok)return void this.logger.warn?.(`[bridge-core] channel alloc failed: ${s.status}`);return(await s.json()).id}catch(e){return void this.logger.warn?.(`[bridge-core] channel alloc error: ${e instanceof Error?e.message:e}`)}else this.logger.debug?.("[bridge-core] agentFetch/agentGatewayUrl unavailable, skip channel alloc")}async lookupCronJobDescription(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("cron.list",{}),r=s?.jobs;if(!Array.isArray(r))return;for(const t of r){if(!t||"object"!=typeof t)continue;const s=t;if(s.id===e)return"string"==typeof s.description?s.description:void 0}}catch(e){this.logger.debug?.(`[bridge-core] cron.list description lookup failed: ${e instanceof Error?e.message:e}`)}}setCronChannelCache(e,t){if(this.cronChannelCache.size>=AcpGatewayBridgeCore.CRON_CHANNEL_CACHE_MAX){const e=this.cronChannelCache.keys().next().value;void 0!==e&&this.cronChannelCache.delete(e)}this.cronChannelCache.set(e,t)}async refreshCronIsolatedSessionKeys(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.request("cron.list",{}),s=t?.jobs;if(!Array.isArray(s))return;const r=new Set,n=[];for(const e of s){if(!e||"object"!=typeof e)continue;const t=e;if("string"==typeof t.id&&n.push(t.id),"isolated"!==t.sessionTarget)continue;const s=t.sessionKey;"string"==typeof s&&s.length>0&&r.add(s)}this.cronIsolatedSessionKeys=r,this.cronOverrides.reconcile(n)}catch(e){this.logger.debug?.(`[bridge-core] refreshCronIsolatedSessionKeys failed: ${e instanceof Error?e.message:e}`)}}}async function fetchWithRetry(e,t={}){const s=t.maxRetries??3;let r;for(let n=0;n<=s;n++){const i=await fetch(e,{signal:AbortSignal.timeout(12e4),redirect:"follow"});if(i.ok)return Buffer.from(await i.arrayBuffer());if(429===i.status||i.status>=500){const o=Number(i.headers.get("retry-after"))||0,a=1e3*Math.max(o,2**n);if(r=new Error(`HTTP ${i.status} from ${e}`),n<s){t.logger?.info?.(`[bridge-core] Download ${i.status}, retry ${n+1}/${s} in ${(a/1e3).toFixed(0)}s`),await new Promise(e=>setTimeout(e,a));continue}}throw new Error(`Failed to download zip: ${i.status}`)}throw r??new Error("Download failed after retries")}const SKILL_ROOT_MARKERS=["SKILL.md","skill.md","SKILL.MD"];async function resolveSkillRoot(e){for(const t of SKILL_ROOT_MARKERS)try{return await stat(join(e,t)),e}catch{}const t=(await readdir(e,{withFileTypes:!0})).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&"__MACOSX"!==e.name);if(1===t.length){const s=join(e,t[0].name);for(const e of SKILL_ROOT_MARKERS)try{return await stat(join(s,e)),s}catch{}return s}return e}async function extractZipToDir(e,t){const s=new AdmZip(e),r=join(t,".beeos-extract-tmp-"+Date.now());try{await mkdir(r,{recursive:!0}),s.extractAllTo(r,!0);const e=await resolveSkillRoot(r),n=await readdir(e,{withFileTypes:!0});let i=0;for(const s of n){if(s.name.startsWith(".beeos-extract-tmp")||"__MACOSX"===s.name)continue;const r=join(e,s.name),n=join(t,s.name);await rename(r,n).catch(async()=>{await cp(r,n,{recursive:!0})}),i++}if(0===i)throw new Error("Zip archive contained no extractable files")}finally{await rm(r,{recursive:!0,force:!0}).catch(()=>{})}}const VALID_SLUG_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,NON_ASCII_PATTERN=/[^\x00-\x7F]/;function validateSkillSlug(e){const t=e.trim();if(!t||t.includes("/")||t.includes("\\")||t.includes(".."))throw new Error(`Invalid skill slug: ${e}`);if(NON_ASCII_PATTERN.test(t)||!VALID_SLUG_PATTERN.test(t))throw new Error(`Invalid skill slug: ${e}`);return t}function safeDirName(e){const t=e.trim();return t?t.replaceAll("/","__").replaceAll("\\","__"):t}function resolveSkillInstallDir(e,t){const s=safeDirName(t),r=join(e,s),n=resolve(e),i=resolve(r),o=relative(n,i);if(!o||".."===o||o.startsWith(`..${sep}`)||path.isAbsolute(o))throw new Error("Invalid skill target path");return r}
@@ -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{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}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"))}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 o=roleFromSessionUpdate(e);o&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[o]=s)}export function recordToolCallTurnForUpdate(t,e,s){s&&"tool_call"===e.sessionUpdate&&e.toolCallId&&(t.toolCallTurns??=new Map,t.toolCallTurns.set(e.toolCallId,{turnId:s,..."string"==typeof e.name&&e.name?{toolName:e.name}:{}}))}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),recordToolCallTurnForUpdate(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 o=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(o){const e=this.state.toolCallIdMap.get(o);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(o,o),t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o)}const n=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n}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 o of s.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,o)&&e.assistantMq?.push(o);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",t,JSON.stringify(o));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:o,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,n,"result"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=n.result,a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,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),this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:o,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(n.text);if(!s)continue;const o=classifyHeartbeatFilter(s);if(o.filtered){this.logHeartbeatSkip({source:"chat-text",reason:o.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),o=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(e,t.data??{});if(n){const a=normalizeTransportText(n);if(a){const n=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(n);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:n,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"}`),o({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 n=asString(t.type);if("thinking"===n){e.hasAgentThinkingStream=!0;const o=normalizeText(t.thinking)??normalizeText(t.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);o(buildToolCallPayload({toolCallId:n,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const n=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&&this.emitNormalizedToolResult(e,{toolCallId:n,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:o}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const o=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const n=t.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",r=resolveToolArgs(n);o(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=n?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(n?.name)??asString(n?.toolName)??"tool",r={...n,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);o(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("end"===o){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===o||"cancel"===o)this.completePrompt(e,"cancelled");else if("error"===o){const o=s?.error,n=o&&"object"==typeof o?o:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof o?o:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n?JSON.stringify(n):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 o=e.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof n?.name?n.name:void 0)??("string"==typeof o?.name?o.name:void 0)??"tool",l="string"==typeof o?.text?o.text:void 0,d=a??r,c=o?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:d,toolName:i,rawText:l,rawResult:c,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)});await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:resolveTurnId(t),config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}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.emittedToolResultKeys=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 o=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(o&&(e.sessionId=o),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),o=s?.jobs;if(Array.isArray(o))for(const s of o)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 n=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(n,{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",{}),o=t?.jobs;if(Array.isArray(o))for(const t of o){if(!t||"object"!=typeof t)continue;const o=t;if(o.id===e){s=o;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const o="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(o)){const t=decodeBeeosChannelId(o);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const n=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(n&&"string"==typeof n.kind?n.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(o,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 o="string"==typeof t.jobId?t.jobId:void 0;if(!o)return;const n=t.summary;let a=this.cronChannelCache?.get(o);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===o){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:n,cron_job_id:o,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${o}`)}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=${o} err=${e}`),void this.cronChannelCache?.delete(o);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,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}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"))}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 o=roleFromSessionUpdate(e);o&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[o]=s)}export function recordToolCallTurnForUpdate(t,e,s){s&&"tool_call"===e.sessionUpdate&&e.toolCallId&&(t.toolCallTurns??=new Map,t.toolCallTurns.set(e.toolCallId,{turnId:s,..."string"==typeof e.name&&e.name?{toolName:e.name}:{}}))}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),recordToolCallTurnForUpdate(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 o=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(o){const e=this.state.toolCallIdMap.get(o);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(o,o),t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o)}const n=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n}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 o of s.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,o)&&e.assistantMq?.push(o);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",t,JSON.stringify(o));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:o,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,n,"result"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=n.result,a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,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),this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:o,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(n.text);if(!s)continue;const o=classifyHeartbeatFilter(s);if(o.filtered){this.logHeartbeatSkip({source:"chat-text",reason:o.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),o=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(e,t.data??{});if(n){const a=normalizeTransportText(n);if(a){const n=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(n);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:n,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"}`),o({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 n=asString(t.type);if("thinking"===n){e.hasAgentThinkingStream=!0;const o=normalizeText(t.thinking)??normalizeText(t.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);o(buildToolCallPayload({toolCallId:n,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const n=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&&this.emitNormalizedToolResult(e,{toolCallId:n,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:o}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const o=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const n=t.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",r=resolveToolArgs(n);o(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=n?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(n?.name)??asString(n?.toolName)??"tool",r={...n,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);o(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("end"===o){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===o||"cancel"===o)this.completePrompt(e,"cancelled");else if("error"===o){const o=s?.error,n=o&&"object"==typeof o?o:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof o?o:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n?JSON.stringify(n):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 o=e.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof n?.name?n.name:void 0)??("string"==typeof o?.name?o.name:void 0)??"tool",l="string"==typeof o?.text?o.text:void 0,d=a??r,c=o?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:d,toolName:i,rawText:l,rawResult:c,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)});await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:resolveTurnId(t),config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}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.emittedToolResultKeys=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 o=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(o&&(e.sessionId=o),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),o=s?.jobs;if(Array.isArray(o))for(const s of o)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 n=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(n,{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",{}),o=t?.jobs;if(Array.isArray(o))for(const t of o){if(!t||"object"!=typeof t)continue;const o=t;if(o.id===e){s=o;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const o="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(o)){const t=decodeBeeosChannelId(o);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const n=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(n&&"string"==typeof n.kind?n.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(o,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 o="string"==typeof t.jobId?t.jobId:void 0;if(!o)return;const n=t.summary;let a=this.cronChannelCache?.get(o);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===o){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:n,cron_job_id:o,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${o}`)}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=${o} err=${e}`),void this.cronChannelCache?.delete(o);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewSanitizedText(t.cumulative),s=void 0!==t.delta?previewSanitizedText(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{stripTransportMetadata}from"../message-filter.js";import{formatProviderError}from"./error-message-formatter.js";import{extractPersistedToolResultEnvelope}from"./persisted-tool-result-envelope.js";import{hydrateFromLocalArtifacts}from"./history-artifact-hydrator.js";import{projectPersistedToolResultForReplay,toReplayFiles}from"./tool-result-projection.js";const DEFAULT_CHAT_HISTORY_RPC_TIMEOUT_MS=3e3;function toISO(e){if(null!=e){if("string"==typeof e){if(!e)return;const t=new Date(e),o=t.getTime();return Number.isFinite(o)?t.toISOString():void 0}if(!(e<=0))try{return new Date(e).toISOString()}catch{return}}}function inferToolKindFromName(e){if(!e)return;const t=e.toLowerCase();return t.includes("read")?"read":t.includes("write")||t.includes("edit")?"edit":t.includes("delete")||t.includes("remove")?"delete":t.includes("move")||t.includes("rename")?"move":t.includes("search")||t.includes("find")?"search":t.includes("exec")||t.includes("run")||t.includes("bash")?"execute":t.includes("fetch")||t.includes("http")?"fetch":"other"}const BEEOS_USER_MSG_MARKER_RE=/(?:^|\n)\s*User Message From \w+:\s*\n/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/,CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;function stripBeeosUserEnvelope(e){const t=BEEOS_USER_MSG_MARKER_RE.exec(e);if(t){return e.slice(t.index+t[0].length).trim()}let o=e;return o=o.replace(CANVAS_STATE_RE,"").trimStart(),o=o.replace(SYSTEM_NOTE_RE,"").trimStart(),stripTransportMetadata(o).trim()}export async function loadHistory(e,t,o=100,s){const n=Date.now(),a=s?.rpcTimeoutMs??3e3,r=s?.logger,i=s?.emitObs,l=e=>{r?.info&&r.info(e)};let c,u=null;if(e?.isConnected)try{u=await Promise.race([e.chatHistory(t,o),new Promise(e=>{const t=setTimeout(()=>e(null),a);"object"==typeof t&&"unref"in t&&t.unref()})])}catch(e){c=e}const p=u?parseGatewayHistory(u):[];if(p.length>0){const e=hydrateFromLocalArtifacts({messages:p,gatewaySessionKey:t,options:s,logger:r,parseGatewayHistory:parseGatewayHistory}),o=e.messages,a=Date.now()-n,c=o.filter(e=>"user"===e.role).length,u=o.filter(e=>"assistant"===e.role).length,m=e.addedFileCount>0?"chat.history+local-jsonl-artifacts":"chat.history";return l(`[loadHistory] source=${m} sid=${t} messages=${o.length} (user=${c} asst=${u}) artifacts=${e.addedFileCount} elapsedMs=${a}`),i?.({name:"history.source",severity:"info",payload:{sessionKey:t,source:m,messages:o.length,userCount:c,asstCount:u,addedFileArtifacts:e.addedFileCount,elapsedMs:a}}),o}const{localHistory:m,agentId:d,sessionId:f}=s??{},g=e?.isConnected?null===u?c?"rpc-error":"rpc-timeout":"rpc-empty":"gateway-disconnected";if(!m||!d){const e=Date.now()-n;return l(`[loadHistory] source=none sid=${t} reason=${g} elapsedMs=${e}`),c&&(r?.warn??console.debug)?.(`[loadHistory] chat.history error: ${c instanceof Error?c.message:String(c)}`),i?.({name:"history.none",severity:"warn",payload:{sessionKey:t,reason:g,fallbackAvailable:!1,elapsedMs:e}}),[]}let y=[];try{y=m.readEntries({agentId:d,sessionKey:t,sessionId:f})}catch(e){(r?.warn??console.debug)?.(`[loadHistory] local jsonl read failed: ${e instanceof Error?e.message:String(e)}`)}if(0===y.length){const e=Date.now()-n;return l(`[loadHistory] source=none sid=${t} reason=${g},local-empty elapsedMs=${e}`),i?.({name:"history.none",severity:"warn",payload:{sessionKey:t,reason:`${g},local-empty`,fallbackAvailable:!0,elapsedMs:e}}),[]}const h=parseGatewayHistory({messages:y}),_=Date.now()-n;return l(`[loadHistory] source=local-jsonl sid=${t} reason=${g} messages=${h.length} localLines=${y.length} elapsedMs=${_}`),i?.({name:"history.local_fallback",severity:"info",payload:{sessionKey:t,reason:g,localLines:y.length,messages:h.length,elapsedMs:_}}),h}export async function loadHistoryPage(e,t,o){if(!e)return{messages:[],hasMore:!1};const s=await e.chatHistoryPage(t,{...null!=o.limit?{limit:o.limit}:{},...o.cursor?{cursor:o.cursor}:{}});if(!s)return(o.logger?.debug??(()=>{}))(`[loadHistoryPage] no page sid=${t} cursor=${o.cursor??"<head>"}`),{messages:[],hasMore:!1};const n=parseGatewayHistory({messages:s.messages});return(o.logger?.debug??(()=>{}))(`[loadHistoryPage] sid=${t} cursor=${o.cursor??"<head>"} raw=${s.messages.length} parsed=${n.length} hasMore=${s.hasMore} nextCursor=${s.nextCursor??"<none>"}`),{messages:n,...s.nextCursor?{nextCursor:s.nextCursor}:{},hasMore:s.hasMore}}export function groupIntoTurns(e){const t=[];let o=null;const s=()=>{o&&(t.push(o),o=null)};for(const n of e){if("assistant"===n.role&&(n.reasoning||n.toolCalls?.length)){s(),o={role:"assistant",content:n.content,reasoning:n.reasoning,files:n.files,sourceMessages:[n],...n.timestamp?{timestamp:n.timestamp}:{},...n.messageId?{messageId:n.messageId}:{},...void 0!==n._seq?{_seq:n._seq}:{}},n.toolCalls?.length&&(o.toolCalls=n.toolCalls.map(e=>({id:e.id,callId:e.id,name:e.name,arguments:e.arguments??"",status:e.status??"in_progress",output:e.output,isError:"error"===e.status||"failed"===e.status,kind:inferToolKindFromName(e.name)})));continue}if("assistant"===n.role&&"string"==typeof n.metadata?.status&&"error"===n.metadata.status){s();const e={role:"assistant",content:n.content,files:n.files,sourceMessages:[n],...n.timestamp?{timestamp:n.timestamp}:{},...n.messageId?{messageId:n.messageId}:{},...void 0!==n._seq?{_seq:n._seq}:{}};n.metadata&&(e.metadata=n.metadata),t.push(e);continue}if("system"===n.role){s();const e={role:"system",content:n.content};n.metadata&&(e.metadata=n.metadata),n.timestamp&&(e.timestamp=n.timestamp),n.messageId&&(e.messageId=n.messageId),void 0!==n._seq&&(e._seq=n._seq),t.push(e);continue}if("user"===n.role){s();const e={role:"user",content:n.content};n.provenance&&(e.provenance=n.provenance),n.metadata&&(e.metadata=n.metadata),n.timestamp&&(e.timestamp=n.timestamp),n.messageId&&(e.messageId=n.messageId),void 0!==n._seq&&(e._seq=n._seq),t.push(e);continue}o||(o={role:"assistant",content:"",sourceMessages:[]}),o.sourceMessages||(o.sourceMessages=[]),o.sourceMessages.push(n),!o.timestamp&&n.timestamp&&(o.timestamp=n.timestamp),!o.messageId&&n.messageId&&(o.messageId=n.messageId),void 0===o._seq&&void 0!==n._seq&&(o._seq=n._seq);switch(n.updateType??"agent_message_chunk"){case"agent_thought_chunk":o.reasoning=(o.reasoning??"")+n.content;break;case"tool_call":o.toolCalls||(o.toolCalls=[]),o.toolCalls.push({id:n.toolCallId??`tc_${o.toolCalls.length}`,callId:n.toolCallId??`tc_${o.toolCalls.length}`,name:n.toolName??"tool",arguments:n.content||"",status:"in_progress",kind:inferToolKindFromName(n.toolName)});break;case"tool_call_update":{n.files?.length&&(o.files=[...o.files??[],...n.files]),o.toolCalls||(o.toolCalls=[]);const e=n.toolCallId?o.toolCalls.find(e=>e.id===n.toolCallId):void 0;e?(e.output=n.content,e.status="completed"):o.toolCalls.push({id:n.toolCallId??`tc_${o.toolCalls.length}`,callId:n.toolCallId??`tc_${o.toolCalls.length}`,name:n.toolName??"tool",arguments:"",status:"completed",output:n.content,kind:inferToolKindFromName(n.toolName)});break}default:o.content+=n.content,n.files?.length&&(o.files=[...o.files??[],...n.files])}}return s(),t}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function readOpenClawMeta(e){const t=e.__openclaw;if(!t||"object"!=typeof t)return{};const o={},s=t.id,n=t.seq;return"string"==typeof s&&s.length>0&&(o.id=s),"number"==typeof n&&Number.isFinite(n)&&(o.seq=n),o}function rawSeqOf(e){if(!e||"object"!=typeof e)return;const t=e.__openclaw;if(!t||"object"!=typeof t)return;const o=t.seq;return"number"==typeof o&&Number.isFinite(o)?o:void 0}function sortBySeqWhenComplete(e){if(e.length<2)return e;for(const t of e)if(void 0===rawSeqOf(t))return e;const t=e.slice();return t.sort((e,t)=>rawSeqOf(e)-rawSeqOf(t)),t}function collapseErrorRetryChains(e){const t=new Set,o=new Set;for(const s of e){if(!s||"object"!=typeof s)continue;const e=s;if("assistant"!==e.role)continue;if("error"!==e.stopReason)continue;const n=e.__openclaw&&"object"==typeof e.__openclaw?e.__openclaw:void 0,a="string"==typeof n?.id?n.id:"string"==typeof e.id?e.id:void 0,r="string"==typeof e.parentId?e.parentId:void 0;a&&t.add(a),r&&o.add(r)}const s=[];let n=null;for(const a of e){if(!a||"object"!=typeof a){s.push(a),n=null;continue}const e=a;if(!("assistant"===e.role&&"error"===e.stopReason)){s.push(a),n=null;continue}const r=e.__openclaw&&"object"==typeof e.__openclaw?e.__openclaw:void 0,i="string"==typeof r?.id?r.id:"string"==typeof e.id?e.id:void 0;i&&o.has(i)&&t.has(i)||(null===n?(n=s.length,s.push(a)):s[n]=a)}return s}function extractBlockText(e){if("string"==typeof e)return e;if(!Array.isArray(e))return"";let t="";for(const o of e)"string"!=typeof o?o&&"object"==typeof o&&"string"==typeof o.text&&(t+=o.text):t+=o;return t}function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}export function parseGatewayHistory(e){if(!e||"object"!=typeof e)return[];const t=e.messages;if(!Array.isArray(t))return[];const o=collapseErrorRetryChains(sortBySeqWhenComplete(t)),s=[];let n=null;for(const e of o){if(!e||"object"!=typeof e)continue;const t=e,o="string"==typeof t.role?t.role:"assistant",a=readOpenClawMeta(t),r=a.id,i=a.seq,l=t.message?.timestamp,c=toISO("number"==typeof t.timestamp||"string"==typeof t.timestamp?t.timestamp:"number"==typeof l||"string"==typeof l?l:void 0),u=e=>(r&&(e.messageId=r),void 0!==i&&(e._seq=i),c&&!e.timestamp&&(e.timestamp=c),e);if("toolResult"===o||"tool"===o){const e=extractPersistedToolResultEnvelope(t),o=e?.toolCallId??"",a=e?.toolName??"tool",r=projectPersistedToolResultForReplay({toolCallId:o||void 0,toolName:a,rawText:e?.rawText,rawResult:e?.rawResult,timestamp:c}),i=toReplayFiles(r.resourceLinks),l=r.outputText,p=!0===t.is_error||!0===t.isError;if(n?.toolCalls?.length){const e=(o?n.toolCalls.find(e=>e.id===o):void 0)??n.toolCalls.find(e=>void 0===e.output);e&&(e.output=l,e.status=p?"failed":"completed"),i?.length&&(n.files=[...n.files??[],...i])}else s.push(u({role:"assistant",content:l,updateType:"tool_call_update",toolCallId:o||void 0,toolName:a,files:i}));continue}if("user"===o&&Array.isArray(t.content)){let e=!1,o=!1;for(const s of t.content){if(!s||"object"!=typeof s)continue;const t=s,n="string"==typeof t.type?t.type:"";TOOL_RESULT_BLOCK_TYPES.has(n)?e=!0:"text"===n&&"string"==typeof t.text&&t.text.trim()&&(o=!0)}if(e&&!o){for(const e of t.content){if(!e||"object"!=typeof e)continue;const t=e,o="string"==typeof t.type?t.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(o))continue;const a=String(t.toolUseId??t.tool_use_id??t.toolCallId??t.tool_call_id??""),r=String(t.name??t.toolName??t.tool_name??"tool"),i="string"==typeof t.output?t.output:extractBlockText(t.content),l=projectPersistedToolResultForReplay({toolCallId:a||void 0,toolName:r,rawText:i,rawResult:t,timestamp:c}),p=toReplayFiles(l.resourceLinks),m=l.outputText||i,d=!0===t.is_error||!0===t.isError;if(n?.toolCalls?.length){const e=(a?n.toolCalls.find(e=>e.id===a):void 0)??n.toolCalls.find(e=>void 0===e.output);e&&(e.output=m,e.status=d?"failed":"completed"),p?.length&&(n.files=[...n.files??[],...p])}else s.push(u({role:"assistant",content:m,updateType:"tool_call_update",toolCallId:a||void 0,toolName:r,files:p}))}continue}}const p="user"===o?"user":"assistant";let m,d,f,g,y,h="";if("string"==typeof t.content)h=t.content;else if(Array.isArray(t.content)){if("assistant"===p&&t.content.some(e=>{if(!e||"object"!=typeof e)return!1;const t="string"==typeof e.type?e.type:"";return TOOL_CALL_BLOCK_TYPES.has(t)||TOOL_RESULT_BLOCK_TYPES.has(t)})&&"assistant"===p){const e=new Map;for(const o of t.content){if("string"==typeof o){o&&s.push(u({role:"assistant",content:o,updateType:"agent_message_chunk"}));continue}if(!o||"object"!=typeof o)continue;const t=o,n="string"==typeof t.type?t.type:"";if("text"===n&&"string"==typeof t.text&&t.text)s.push(u({role:"assistant",content:t.text,updateType:"agent_message_chunk"}));else if("thinking"===n&&"string"==typeof t.thinking)s.push(u({role:"assistant",content:t.thinking,updateType:"agent_thought_chunk"}));else if("resource_link"===n&&"string"==typeof t.uri&&t.uri)f||(f=[]),f.push({uri:t.uri,..."string"==typeof t.name&&t.name?{name:t.name}:{},..."string"==typeof t.mimeType&&t.mimeType?{mimeType:t.mimeType}:{}});else if(TOOL_CALL_BLOCK_TYPES.has(n)){const o=t.arguments??t.input,n="string"==typeof o?o:null!=o?JSON.stringify(o):"",a=String(t.id??t.toolCallId??t.tool_call_id??""),r=u({role:"assistant",content:n,updateType:"tool_call",toolCallId:a,toolName:String(t.name??"tool")});s.push(r),a&&e.set(a,r)}else if(TOOL_RESULT_BLOCK_TYPES.has(n)){const o=String(t.toolUseId??t.tool_use_id??t.toolCallId??t.tool_call_id??""),n=e.get(o)?.toolName??String(t.name??t.toolName??t.tool_name??"tool"),a="string"==typeof t.output?t.output:extractBlockText(t.content),r=projectPersistedToolResultForReplay({toolCallId:o||void 0,toolName:n,rawText:a,rawResult:t,timestamp:c}),i=toReplayFiles(r.resourceLinks),l=r.outputText||a,p=!0===t.is_error||!0===t.isError;if(s.push(u({role:"assistant",content:l,updateType:"tool_call_update",toolCallId:o||void 0,toolName:n,files:i})),o&&e.has(o)){const t=e.get(o);i?.length&&(t.files=[...t.files??[],...i]),t.toolCalls||(t.toolCalls=[{id:o,name:t.toolName??"tool",arguments:t.content,status:p?"failed":"completed",output:l}])}}}f?.length&&s.push(u({role:"assistant",content:"",files:f})),"string"==typeof t.reasoning&&t.reasoning?s.push(u({role:"assistant",content:t.reasoning,updateType:"agent_thought_chunk"})):"string"==typeof t.thinking&&t.thinking&&s.push(u({role:"assistant",content:t.thinking,updateType:"agent_thought_chunk"})),s.length>0&&"assistant"===s[s.length-1].role&&(n=s[s.length-1]);continue}for(const e of t.content){if("string"==typeof e){h+=e;continue}if(!e||"object"!=typeof e)continue;const t=e,o="string"==typeof t.type?t.type:"";"text"===o&&"string"==typeof t.text?h+=t.text:"thinking"===o&&"string"==typeof t.thinking?d=(d??"")+t.thinking:"resource_link"===o&&"string"==typeof t.uri&&t.uri&&(f||(f=[]),f.push({uri:t.uri,..."string"==typeof t.name&&t.name?{name:t.name}:{},..."string"==typeof t.mimeType&&t.mimeType?{mimeType:t.mimeType}:{}}))}}if(d||("string"==typeof t.reasoning&&t.reasoning?d=t.reasoning:"string"==typeof t.thinking&&t.thinking&&(d=t.thinking)),!m?.length){const e=t.tool_calls??t.toolCalls;if(Array.isArray(e)){m=[];for(const t of e){if(!t||"object"!=typeof t)continue;const e=t,o=e.function;m.push({id:String(e.id??e.toolCallId??""),name:String(o?.name??e.name??e.toolName??"tool"),arguments:"string"==typeof e.arguments?e.arguments:"string"==typeof o?.arguments?o.arguments:"",status:String(e.status??"in_progress"),output:"string"==typeof e.output?e.output:void 0})}0===m.length&&(m=void 0)}}if("user"===p&&h){h=stripBeeosUserEnvelope(h);const e=CANVAS_INTERACTION_RE.exec(h);if(e){const t=e[1];g={kind:"canvas_action"};const o={type:"canvas_action",actionName:t,surfaceId:e[2]},s=ACTION_CONTEXT_RE.exec(h);if(s)try{o.actionPayload=JSON.parse(s[1])}catch{}y=o,h=`Clicked "${t}"`}}if(!h&&!d&&!m?.length&&!f?.length&&"assistant"===p&&"string"==typeof t.errorMessage&&t.errorMessage){const e=t.errorMessage,o="string"==typeof t.stopReason?t.stopReason:void 0,s=formatProviderError({raw:e,stopReason:o});h=s.content,y={...y??{},status:"error",errorSource:"provider",errorCategory:s.category,...void 0!==s.httpStatus?{httpStatus:s.httpStatus}:{},...o?{stopReason:o}:{},rawError:e}}if(!(h||d||m?.length||f?.length))continue;if(!g&&"user"===p&&t.provenance&&"object"==typeof t.provenance){const e=t.provenance;"string"==typeof e.kind&&(g={kind:e.kind},"string"==typeof e.sourceTool&&(g.sourceTool=e.sourceTool))}const _=u({role:p,content:h,reasoning:d,toolCalls:m,provenance:g,metadata:y,files:f});s.push(_),"assistant"===p&&(n=_)}s.filter(e=>e.toolCalls?.length),s.reduce((e,t)=>e+(t.toolCalls?.length??0),0),s.reduce((e,t)=>e+(t.toolCalls?.filter(e=>void 0!==e.output).length??0),0);return s}const BEEOS_REF_RE=/<BEEOS_REF\s+((?:[^"]|"[^"]*")*?)\s*\/>/g;function parseBeeosRefAttrs(e){const t=new Map,o=/(\w+)="([^"]*)"/g;for(const s of e.matchAll(o))t.set(s[1],s[2]);return t}function guessMimeFromName(e){if(!e)return;const t=e.split(".").pop()?.toLowerCase();return t?{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",pdf:"application/pdf",mp3:"audio/mpeg",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",txt:"text/plain",json:"application/json",csv:"text/csv"}[t]:void 0}export function splitBeeosRefReplayText(e){if(!e)return[];if(!e.includes("<BEEOS_REF "))return[{type:"text",text:e}];const t=[];let o=0;for(const s of e.matchAll(BEEOS_REF_RE)){const n=e.slice(o,s.index).trim();n&&t.push({type:"text",text:n});const a=parseBeeosRefAttrs(s[1]),r=a.get("id"),i=a.get("name"),l=a.get("status");r&&"download_failed"!==l?t.push({type:"resource_link",uri:`beeos-file://${r}`,fileName:i,mimeType:guessMimeFromName(i)??"application/octet-stream"}):r&&t.push({type:"resource_link",uri:`beeos-file://${r}`,fileName:i??"file",mimeType:guessMimeFromName(i)??"application/octet-stream"}),o=(s.index??0)+s[0].length}const s=e.slice(o).trim();return s&&t.push({type:"text",text:s}),t}const ATTACHMENT_MARKER_RE=/\[(image|file)\s+([^\]]*)\]/g;export function splitUserAttachmentMarkers(e){if(!e)return[];if(!e.includes("[image ")&&!e.includes("[file "))return[{type:"text",text:e}];const t=[];let o=0;for(const s of e.matchAll(ATTACHMENT_MARKER_RE)){const n=e.slice(o,s.index).trim();n&&t.push({type:"text",text:n});const a=s[1],r=s[2],i=new Map;for(const e of r.split(/\s+/)){const t=e.indexOf("=");t>0&&i.set(e.slice(0,t),e.slice(t+1))}t.push({type:a,mimeType:i.get("mime"),fileName:i.get("name")||("image"===a?"Image":"File")}),o=(s.index??0)+s[0].length}const s=e.slice(o).trim();return s&&t.push({type:"text",text:s}),t}export function splitUserReplayBlocks(e){return e?e.includes("<BEEOS_REF ")?splitBeeosRefReplayText(e):e.includes("[image ")||e.includes("[file ")?splitUserAttachmentMarkers(e):[{type:"text",text:e}]:[]}
1
+ import{formatProviderError}from"./error-message-formatter.js";import{extractBeeosUserTextFromHistoryContent}from"./beeos-user-envelope.js";import{extractPersistedToolResultEnvelope}from"./persisted-tool-result-envelope.js";import{hydrateFromLocalArtifacts}from"./history-artifact-hydrator.js";import{projectPersistedToolResultForReplay,toReplayFiles}from"./tool-result-projection.js";const DEFAULT_CHAT_HISTORY_RPC_TIMEOUT_MS=3e3;function toISO(e){if(null!=e){if("string"==typeof e){if(!e)return;const t=new Date(e),o=t.getTime();return Number.isFinite(o)?t.toISOString():void 0}if(!(e<=0))try{return new Date(e).toISOString()}catch{return}}}function inferToolKindFromName(e){if(!e)return;const t=e.toLowerCase();return t.includes("read")?"read":t.includes("write")||t.includes("edit")?"edit":t.includes("delete")||t.includes("remove")?"delete":t.includes("move")||t.includes("rename")?"move":t.includes("search")||t.includes("find")?"search":t.includes("exec")||t.includes("run")||t.includes("bash")?"execute":t.includes("fetch")||t.includes("http")?"fetch":"other"}const CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;export async function loadHistory(e,t,o=100,s){const n=Date.now(),a=s?.rpcTimeoutMs??3e3,r=s?.logger,i=s?.emitObs,l=e=>{r?.info&&r.info(e)};let c,u=null;if(e?.isConnected)try{u=await Promise.race([e.chatHistory(t,o),new Promise(e=>{const t=setTimeout(()=>e(null),a);"object"==typeof t&&"unref"in t&&t.unref()})])}catch(e){c=e}const p=u?parseGatewayHistory(u):[];if(p.length>0){const e=hydrateFromLocalArtifacts({messages:p,gatewaySessionKey:t,options:s,logger:r,parseGatewayHistory:parseGatewayHistory}),o=e.messages,a=Date.now()-n,c=o.filter(e=>"user"===e.role).length,u=o.filter(e=>"assistant"===e.role).length,m=e.addedFileCount>0?"chat.history+local-jsonl-artifacts":"chat.history";return l(`[loadHistory] source=${m} sid=${t} messages=${o.length} (user=${c} asst=${u}) artifacts=${e.addedFileCount} elapsedMs=${a}`),i?.({name:"history.source",severity:"info",payload:{sessionKey:t,source:m,messages:o.length,userCount:c,asstCount:u,addedFileArtifacts:e.addedFileCount,elapsedMs:a}}),o}const{localHistory:m,agentId:d,sessionId:f}=s??{},g=e?.isConnected?null===u?c?"rpc-error":"rpc-timeout":"rpc-empty":"gateway-disconnected";if(!m||!d){const e=Date.now()-n;return l(`[loadHistory] source=none sid=${t} reason=${g} elapsedMs=${e}`),c&&(r?.warn??console.debug)?.(`[loadHistory] chat.history error: ${c instanceof Error?c.message:String(c)}`),i?.({name:"history.none",severity:"warn",payload:{sessionKey:t,reason:g,fallbackAvailable:!1,elapsedMs:e}}),[]}let y=[];try{y=m.readEntries({agentId:d,sessionKey:t,sessionId:f})}catch(e){(r?.warn??console.debug)?.(`[loadHistory] local jsonl read failed: ${e instanceof Error?e.message:String(e)}`)}if(0===y.length){const e=Date.now()-n;return l(`[loadHistory] source=none sid=${t} reason=${g},local-empty elapsedMs=${e}`),i?.({name:"history.none",severity:"warn",payload:{sessionKey:t,reason:`${g},local-empty`,fallbackAvailable:!0,elapsedMs:e}}),[]}const h=parseGatewayHistory({messages:y}),_=Date.now()-n;return l(`[loadHistory] source=local-jsonl sid=${t} reason=${g} messages=${h.length} localLines=${y.length} elapsedMs=${_}`),i?.({name:"history.local_fallback",severity:"info",payload:{sessionKey:t,reason:g,localLines:y.length,messages:h.length,elapsedMs:_}}),h}export async function loadHistoryPage(e,t,o){if(!e)return{messages:[],hasMore:!1};const s=await e.chatHistoryPage(t,{...null!=o.limit?{limit:o.limit}:{},...o.cursor?{cursor:o.cursor}:{}});if(!s)return(o.logger?.debug??(()=>{}))(`[loadHistoryPage] no page sid=${t} cursor=${o.cursor??"<head>"}`),{messages:[],hasMore:!1};const n=parseGatewayHistory({messages:s.messages});return(o.logger?.debug??(()=>{}))(`[loadHistoryPage] sid=${t} cursor=${o.cursor??"<head>"} raw=${s.messages.length} parsed=${n.length} hasMore=${s.hasMore} nextCursor=${s.nextCursor??"<none>"}`),{messages:n,...s.nextCursor?{nextCursor:s.nextCursor}:{},hasMore:s.hasMore}}export function groupIntoTurns(e){const t=[];let o=null;const s=()=>{o&&(t.push(o),o=null)};for(const n of e){if("assistant"===n.role&&(n.reasoning||n.toolCalls?.length)){s(),o={role:"assistant",content:n.content,reasoning:n.reasoning,files:n.files,sourceMessages:[n],...n.timestamp?{timestamp:n.timestamp}:{},...n.messageId?{messageId:n.messageId}:{},...void 0!==n._seq?{_seq:n._seq}:{}},n.toolCalls?.length&&(o.toolCalls=n.toolCalls.map(e=>({id:e.id,callId:e.id,name:e.name,arguments:e.arguments??"",status:e.status??"in_progress",output:e.output,isError:"error"===e.status||"failed"===e.status,kind:inferToolKindFromName(e.name)})));continue}if("assistant"===n.role&&"string"==typeof n.metadata?.status&&"error"===n.metadata.status){s();const e={role:"assistant",content:n.content,files:n.files,sourceMessages:[n],...n.timestamp?{timestamp:n.timestamp}:{},...n.messageId?{messageId:n.messageId}:{},...void 0!==n._seq?{_seq:n._seq}:{}};n.metadata&&(e.metadata=n.metadata),t.push(e);continue}if("system"===n.role){s();const e={role:"system",content:n.content};n.metadata&&(e.metadata=n.metadata),n.timestamp&&(e.timestamp=n.timestamp),n.messageId&&(e.messageId=n.messageId),void 0!==n._seq&&(e._seq=n._seq),t.push(e);continue}if("user"===n.role){s();const e={role:"user",content:n.content};n.provenance&&(e.provenance=n.provenance),n.metadata&&(e.metadata=n.metadata),n.timestamp&&(e.timestamp=n.timestamp),n.messageId&&(e.messageId=n.messageId),void 0!==n._seq&&(e._seq=n._seq),t.push(e);continue}o||(o={role:"assistant",content:"",sourceMessages:[]}),o.sourceMessages||(o.sourceMessages=[]),o.sourceMessages.push(n),!o.timestamp&&n.timestamp&&(o.timestamp=n.timestamp),!o.messageId&&n.messageId&&(o.messageId=n.messageId),void 0===o._seq&&void 0!==n._seq&&(o._seq=n._seq);switch(n.updateType??"agent_message_chunk"){case"agent_thought_chunk":o.reasoning=(o.reasoning??"")+n.content;break;case"tool_call":o.toolCalls||(o.toolCalls=[]),o.toolCalls.push({id:n.toolCallId??`tc_${o.toolCalls.length}`,callId:n.toolCallId??`tc_${o.toolCalls.length}`,name:n.toolName??"tool",arguments:n.content||"",status:"in_progress",kind:inferToolKindFromName(n.toolName)});break;case"tool_call_update":{n.files?.length&&(o.files=[...o.files??[],...n.files]),o.toolCalls||(o.toolCalls=[]);const e=n.toolCallId?o.toolCalls.find(e=>e.id===n.toolCallId):void 0;e?(e.output=n.content,e.status="completed"):o.toolCalls.push({id:n.toolCallId??`tc_${o.toolCalls.length}`,callId:n.toolCallId??`tc_${o.toolCalls.length}`,name:n.toolName??"tool",arguments:"",status:"completed",output:n.content,kind:inferToolKindFromName(n.toolName)});break}default:o.content+=n.content,n.files?.length&&(o.files=[...o.files??[],...n.files])}}return s(),t}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function readOpenClawMeta(e){const t=e.__openclaw;if(!t||"object"!=typeof t)return{};const o={},s=t.id,n=t.seq;return"string"==typeof s&&s.length>0&&(o.id=s),"number"==typeof n&&Number.isFinite(n)&&(o.seq=n),o}function rawSeqOf(e){if(!e||"object"!=typeof e)return;const t=e.__openclaw;if(!t||"object"!=typeof t)return;const o=t.seq;return"number"==typeof o&&Number.isFinite(o)?o:void 0}function sortBySeqWhenComplete(e){if(e.length<2)return e;for(const t of e)if(void 0===rawSeqOf(t))return e;const t=e.slice();return t.sort((e,t)=>rawSeqOf(e)-rawSeqOf(t)),t}function collapseErrorRetryChains(e){const t=new Set,o=new Set;for(const s of e){if(!s||"object"!=typeof s)continue;const e=s;if("assistant"!==e.role)continue;if("error"!==e.stopReason)continue;const n=e.__openclaw&&"object"==typeof e.__openclaw?e.__openclaw:void 0,a="string"==typeof n?.id?n.id:"string"==typeof e.id?e.id:void 0,r="string"==typeof e.parentId?e.parentId:void 0;a&&t.add(a),r&&o.add(r)}const s=[];let n=null;for(const a of e){if(!a||"object"!=typeof a){s.push(a),n=null;continue}const e=a;if(!("assistant"===e.role&&"error"===e.stopReason)){s.push(a),n=null;continue}const r=e.__openclaw&&"object"==typeof e.__openclaw?e.__openclaw:void 0,i="string"==typeof r?.id?r.id:"string"==typeof e.id?e.id:void 0;i&&o.has(i)&&t.has(i)||(null===n?(n=s.length,s.push(a)):s[n]=a)}return s}function extractBlockText(e){if("string"==typeof e)return e;if(!Array.isArray(e))return"";let t="";for(const o of e)"string"!=typeof o?o&&"object"==typeof o&&"string"==typeof o.text&&(t+=o.text):t+=o;return t}function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}export function parseGatewayHistory(e){if(!e||"object"!=typeof e)return[];const t=e.messages;if(!Array.isArray(t))return[];const o=collapseErrorRetryChains(sortBySeqWhenComplete(t)),s=[];let n=null;for(const e of o){if(!e||"object"!=typeof e)continue;const t=e,o="string"==typeof t.role?t.role:"assistant",a=readOpenClawMeta(t),r=a.id,i=a.seq,l=t.message?.timestamp,c=toISO("number"==typeof t.timestamp||"string"==typeof t.timestamp?t.timestamp:"number"==typeof l||"string"==typeof l?l:void 0),u=e=>(r&&(e.messageId=r),void 0!==i&&(e._seq=i),c&&!e.timestamp&&(e.timestamp=c),e);if("toolResult"===o||"tool"===o){const e=extractPersistedToolResultEnvelope(t),o=e?.toolCallId??"",a=e?.toolName??"tool",r=projectPersistedToolResultForReplay({toolCallId:o||void 0,toolName:a,rawText:e?.rawText,rawResult:e?.rawResult,timestamp:c}),i=toReplayFiles(r.resourceLinks),l=r.outputText,p=!0===t.is_error||!0===t.isError;if(n?.toolCalls?.length){const e=(o?n.toolCalls.find(e=>e.id===o):void 0)??n.toolCalls.find(e=>void 0===e.output);e&&(e.output=l,e.status=p?"failed":"completed"),i?.length&&(n.files=[...n.files??[],...i])}else s.push(u({role:"assistant",content:l,updateType:"tool_call_update",toolCallId:o||void 0,toolName:a,files:i}));continue}if("user"===o&&Array.isArray(t.content)){let e=!1,o=!1;for(const s of t.content){if(!s||"object"!=typeof s)continue;const t=s,n="string"==typeof t.type?t.type:"";TOOL_RESULT_BLOCK_TYPES.has(n)?e=!0:"text"===n&&"string"==typeof t.text&&t.text.trim()&&(o=!0)}if(e&&!o){for(const e of t.content){if(!e||"object"!=typeof e)continue;const t=e,o="string"==typeof t.type?t.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(o))continue;const a=String(t.toolUseId??t.tool_use_id??t.toolCallId??t.tool_call_id??""),r=String(t.name??t.toolName??t.tool_name??"tool"),i="string"==typeof t.output?t.output:extractBlockText(t.content),l=projectPersistedToolResultForReplay({toolCallId:a||void 0,toolName:r,rawText:i,rawResult:t,timestamp:c}),p=toReplayFiles(l.resourceLinks),m=l.outputText||i,d=!0===t.is_error||!0===t.isError;if(n?.toolCalls?.length){const e=(a?n.toolCalls.find(e=>e.id===a):void 0)??n.toolCalls.find(e=>void 0===e.output);e&&(e.output=m,e.status=d?"failed":"completed"),p?.length&&(n.files=[...n.files??[],...p])}else s.push(u({role:"assistant",content:m,updateType:"tool_call_update",toolCallId:a||void 0,toolName:r,files:p}))}continue}}const p="user"===o?"user":"assistant";let m,d,f,g,y,h="";if("string"==typeof t.content)h=t.content;else if(Array.isArray(t.content)){if("assistant"===p&&t.content.some(e=>{if(!e||"object"!=typeof e)return!1;const t="string"==typeof e.type?e.type:"";return TOOL_CALL_BLOCK_TYPES.has(t)||TOOL_RESULT_BLOCK_TYPES.has(t)})&&"assistant"===p){const e=new Map;for(const o of t.content){if("string"==typeof o){o&&s.push(u({role:"assistant",content:o,updateType:"agent_message_chunk"}));continue}if(!o||"object"!=typeof o)continue;const t=o,n="string"==typeof t.type?t.type:"";if("text"===n&&"string"==typeof t.text&&t.text)s.push(u({role:"assistant",content:t.text,updateType:"agent_message_chunk"}));else if("thinking"===n&&"string"==typeof t.thinking)s.push(u({role:"assistant",content:t.thinking,updateType:"agent_thought_chunk"}));else if("resource_link"===n&&"string"==typeof t.uri&&t.uri)f||(f=[]),f.push({uri:t.uri,..."string"==typeof t.name&&t.name?{name:t.name}:{},..."string"==typeof t.mimeType&&t.mimeType?{mimeType:t.mimeType}:{}});else if(TOOL_CALL_BLOCK_TYPES.has(n)){const o=t.arguments??t.input,n="string"==typeof o?o:null!=o?JSON.stringify(o):"",a=String(t.id??t.toolCallId??t.tool_call_id??""),r=u({role:"assistant",content:n,updateType:"tool_call",toolCallId:a,toolName:String(t.name??"tool")});s.push(r),a&&e.set(a,r)}else if(TOOL_RESULT_BLOCK_TYPES.has(n)){const o=String(t.toolUseId??t.tool_use_id??t.toolCallId??t.tool_call_id??""),n=e.get(o)?.toolName??String(t.name??t.toolName??t.tool_name??"tool"),a="string"==typeof t.output?t.output:extractBlockText(t.content),r=projectPersistedToolResultForReplay({toolCallId:o||void 0,toolName:n,rawText:a,rawResult:t,timestamp:c}),i=toReplayFiles(r.resourceLinks),l=r.outputText||a,p=!0===t.is_error||!0===t.isError;if(s.push(u({role:"assistant",content:l,updateType:"tool_call_update",toolCallId:o||void 0,toolName:n,files:i})),o&&e.has(o)){const t=e.get(o);i?.length&&(t.files=[...t.files??[],...i]),t.toolCalls||(t.toolCalls=[{id:o,name:t.toolName??"tool",arguments:t.content,status:p?"failed":"completed",output:l}])}}}f?.length&&s.push(u({role:"assistant",content:"",files:f})),"string"==typeof t.reasoning&&t.reasoning?s.push(u({role:"assistant",content:t.reasoning,updateType:"agent_thought_chunk"})):"string"==typeof t.thinking&&t.thinking&&s.push(u({role:"assistant",content:t.thinking,updateType:"agent_thought_chunk"})),s.length>0&&"assistant"===s[s.length-1].role&&(n=s[s.length-1]);continue}for(const e of t.content){if("string"==typeof e){h+=e;continue}if(!e||"object"!=typeof e)continue;const t=e,o="string"==typeof t.type?t.type:"";"text"===o&&"string"==typeof t.text?h+=t.text:"thinking"===o&&"string"==typeof t.thinking?d=(d??"")+t.thinking:"resource_link"===o&&"string"==typeof t.uri&&t.uri&&(f||(f=[]),f.push({uri:t.uri,..."string"==typeof t.name&&t.name?{name:t.name}:{},..."string"==typeof t.mimeType&&t.mimeType?{mimeType:t.mimeType}:{}}))}}if(d||("string"==typeof t.reasoning&&t.reasoning?d=t.reasoning:"string"==typeof t.thinking&&t.thinking&&(d=t.thinking)),!m?.length){const e=t.tool_calls??t.toolCalls;if(Array.isArray(e)){m=[];for(const t of e){if(!t||"object"!=typeof t)continue;const e=t,o=e.function;m.push({id:String(e.id??e.toolCallId??""),name:String(o?.name??e.name??e.toolName??"tool"),arguments:"string"==typeof e.arguments?e.arguments:"string"==typeof o?.arguments?o.arguments:"",status:String(e.status??"in_progress"),output:"string"==typeof e.output?e.output:void 0})}0===m.length&&(m=void 0)}}if("user"===p&&h){const e=extractBeeosUserTextFromHistoryContent(h);h=e.text,e.sanitized;const t=CANVAS_INTERACTION_RE.exec(h);if(t){const e=t[1];g={kind:"canvas_action"};const o={type:"canvas_action",actionName:e,surfaceId:t[2]},s=ACTION_CONTEXT_RE.exec(h);if(s)try{o.actionPayload=JSON.parse(s[1])}catch{}y=o,h=`Clicked "${e}"`}}if(!h&&!d&&!m?.length&&!f?.length&&"assistant"===p&&"string"==typeof t.errorMessage&&t.errorMessage){const e=t.errorMessage,o="string"==typeof t.stopReason?t.stopReason:void 0,s=formatProviderError({raw:e,stopReason:o});h=s.content,y={...y??{},status:"error",errorSource:"provider",errorCategory:s.category,...void 0!==s.httpStatus?{httpStatus:s.httpStatus}:{},...o?{stopReason:o}:{},rawError:e}}if(!(h||d||m?.length||f?.length))continue;if(!g&&"user"===p&&t.provenance&&"object"==typeof t.provenance){const e=t.provenance;"string"==typeof e.kind&&(g={kind:e.kind},"string"==typeof e.sourceTool&&(g.sourceTool=e.sourceTool))}const _=u({role:p,content:h,reasoning:d,toolCalls:m,provenance:g,metadata:y,files:f});s.push(_),"assistant"===p&&(n=_)}s.filter(e=>e.toolCalls?.length),s.reduce((e,t)=>e+(t.toolCalls?.length??0),0),s.reduce((e,t)=>e+(t.toolCalls?.filter(e=>void 0!==e.output).length??0),0);return s}const BEEOS_REF_RE=/<BEEOS_REF\s+((?:[^"]|"[^"]*")*?)\s*\/>/g;function parseBeeosRefAttrs(e){const t=new Map,o=/(\w+)="([^"]*)"/g;for(const s of e.matchAll(o))t.set(s[1],s[2]);return t}function guessMimeFromName(e){if(!e)return;const t=e.split(".").pop()?.toLowerCase();return t?{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",pdf:"application/pdf",mp3:"audio/mpeg",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",txt:"text/plain",json:"application/json",csv:"text/csv"}[t]:void 0}export function splitBeeosRefReplayText(e){if(!e)return[];if(!e.includes("<BEEOS_REF "))return[{type:"text",text:e}];const t=[];let o=0;for(const s of e.matchAll(BEEOS_REF_RE)){const n=e.slice(o,s.index).trim();n&&t.push({type:"text",text:n});const a=parseBeeosRefAttrs(s[1]),r=a.get("id"),i=a.get("name"),l=a.get("status");r&&"download_failed"!==l?t.push({type:"resource_link",uri:`beeos-file://${r}`,fileName:i,mimeType:guessMimeFromName(i)??"application/octet-stream"}):r&&t.push({type:"resource_link",uri:`beeos-file://${r}`,fileName:i??"file",mimeType:guessMimeFromName(i)??"application/octet-stream"}),o=(s.index??0)+s[0].length}const s=e.slice(o).trim();return s&&t.push({type:"text",text:s}),t}const ATTACHMENT_MARKER_RE=/\[(image|file)\s+([^\]]*)\]/g;export function splitUserAttachmentMarkers(e){if(!e)return[];if(!e.includes("[image ")&&!e.includes("[file "))return[{type:"text",text:e}];const t=[];let o=0;for(const s of e.matchAll(ATTACHMENT_MARKER_RE)){const n=e.slice(o,s.index).trim();n&&t.push({type:"text",text:n});const a=s[1],r=s[2],i=new Map;for(const e of r.split(/\s+/)){const t=e.indexOf("=");t>0&&i.set(e.slice(0,t),e.slice(t+1))}t.push({type:a,mimeType:i.get("mime"),fileName:i.get("name")||("image"===a?"Image":"File")}),o=(s.index??0)+s[0].length}const s=e.slice(o).trim();return s&&t.push({type:"text",text:s}),t}export function splitUserReplayBlocks(e){return e?e.includes("<BEEOS_REF ")?splitBeeosRefReplayText(e):e.includes("[image ")||e.includes("[file ")?splitUserAttachmentMarkers(e):[{type:"text",text:e}]:[]}
@@ -1 +1 @@
1
- import{describe,expect,it,vi,beforeEach,afterEach}from"vitest";import{mkdtempSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import os from"node:os";import path from"node:path";import{groupIntoTurns,loadHistory,parseGatewayHistory}from"./history-replay.js";import{LocalSessionHistory}from"./local-session-history.js";function makeGateway(e={}){const{connected:t=!0,delayMs:s=0,response:a={messages:[]},reject:o=!1}=e;return{get isConnected(){return t},chatHistory:vi.fn(async()=>{if(s>0&&await new Promise(e=>setTimeout(e,s)),o)throw new Error("rpc-failed");return a})}}describe("loadHistory",()=>{const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn()};let t;function s(e,s,a){const o=path.join(t,"agents",e,"sessions");mkdirSync(o,{recursive:!0});const n=path.join(o,`${s}.jsonl`),r=a.map(e=>JSON.stringify({id:e.id,type:"message",timestamp:(new Date).toISOString(),message:{role:e.role,content:[{type:"text",text:e.text}],timestamp:Date.now()}}));writeFileSync(n,r.join("\n")+"\n")}beforeEach(()=>{t=mkdtempSync(path.join(os.tmpdir(),"beeos-claw-lh-")),e.info.mockClear(),e.warn.mockClear(),e.debug.mockClear()}),afterEach(()=>{rmSync(t,{recursive:!0,force:!0})}),it("returns gateway messages when chat.history has content",async()=>{const t=makeGateway({response:{messages:[{id:"m1",role:"user",content:[{type:"text",text:"hi"}]},{id:"m2",role:"assistant",content:[{type:"text",text:"hello"}]}]}}),s=vi.fn(),a=await loadHistory(t,"agent:main:s1",100,{logger:e,emitObs:s});expect(a.length).toBeGreaterThan(0),expect(t.chatHistory).toHaveBeenCalledOnce(),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("source=chat.history")),expect(s).toHaveBeenCalledOnce(),expect(s).toHaveBeenCalledWith(expect.objectContaining({name:"history.source",severity:"info",payload:expect.objectContaining({source:"chat.history",messages:a.length})}))}),it("hydrates chat.history with local persisted toolResult file artifacts",async()=>{const s={status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"device-screenshots/screenshot.jpeg",mimeType:"image/jpeg"}]};!function(e,s,a){const o=path.join(t,"agents",e,"sessions");mkdirSync(o,{recursive:!0});const n=path.join(o,`${s}.jsonl`);writeFileSync(n,a.map(e=>JSON.stringify(e)).join("\n")+"\n")}("main","s1",[{id:"u1",type:"message",timestamp:"2026-07-02T18:02:44.475Z",message:{role:"user",content:[{type:"text",text:"@device /screenshot"}],timestamp:1783015364475}},{id:"a1",type:"message",timestamp:"2026-07-02T18:02:56.660Z",message:{role:"assistant",content:[{type:"toolCall",id:"tc-get-task",name:"beeos_get_task",arguments:{taskId:"task-1",agentId:"device"}}],timestamp:1783015376660}},{id:"tr1",type:"message",timestamp:"2026-07-02T18:02:57.046Z",message:{role:"toolResult",toolCallId:"tc-get-task",toolName:"beeos_get_task",content:[{type:"text",text:JSON.stringify({content:JSON.stringify(s)})}],details:{content:JSON.stringify(s)},isError:!1,timestamp:1783015377046}},{id:"a2",type:"message",timestamp:"2026-07-02T18:03:01.926Z",message:{role:"assistant",content:[{type:"text",text:"Done"}],timestamp:1783015381926}}]);const a=makeGateway({response:{messages:[{role:"user",content:[{type:"text",text:"@device /screenshot"}],timestamp:1783015364475,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[{type:"toolCall",id:"tc-get-task",name:"beeos_get_task",arguments:{taskId:"task-1",agentId:"device"}}],timestamp:1783015376660,__openclaw:{id:"a1",seq:2}},{role:"assistant",content:[{type:"text",text:"Done"}],timestamp:1783015381926,__openclaw:{id:"a2",seq:4}}]}}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=vi.fn(),r=(await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1",emitObs:n})).find(e=>"tool_call_update"===e.updateType&&"tc-get-task"===e.toolCallId);expect(r).toMatchObject({role:"assistant",updateType:"tool_call_update",toolCallId:"tc-get-task",toolName:"beeos_get_task",files:[{uri:"beeos-file://file-1",name:"device-screenshots/screenshot.jpeg",mimeType:"image/jpeg"}]}),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("source=chat.history+local-jsonl-artifacts")),expect(n).toHaveBeenCalledWith(expect.objectContaining({name:"history.source",severity:"info",payload:expect.objectContaining({source:"chat.history+local-jsonl-artifacts",addedFileArtifacts:1})}))}),it("falls back to local jsonl when gateway is disconnected",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"},{id:"m2",role:"assistant",text:"hello"}]);const a=makeGateway({connected:!1}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=vi.fn(),r=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1",emitObs:n});expect(r.length).toBeGreaterThan(0),expect(a.chatHistory).not.toHaveBeenCalled(),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("source=local-jsonl")),expect(n).toHaveBeenCalledWith(expect.objectContaining({name:"history.local_fallback",severity:"info",payload:expect.objectContaining({reason:"gateway-disconnected"})}))}),it("falls back to local jsonl when chat.history times out",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"}]);const a=makeGateway({delayMs:500}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1",rpcTimeoutMs:50});expect(n.length).toBeGreaterThan(0),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("reason=rpc-timeout"))}),it("falls back to local jsonl when chat.history returns empty",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"}]);const a=makeGateway({response:{messages:[]}}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1"});expect(n.length).toBeGreaterThan(0),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("reason=rpc-empty"))}),it("returns [] when both gateway and local are empty",async()=>{const s=makeGateway({response:{messages:[]}}),a=new LocalSessionHistory({logger:e,openclawHome:t}),o=vi.fn(),n=await loadHistory(s,"agent:main:s1",100,{logger:e,localHistory:a,agentId:"main",sessionId:"missing",emitObs:o});expect(n).toEqual([]),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("local-empty")),expect(o).toHaveBeenCalledWith(expect.objectContaining({name:"history.none",severity:"warn",payload:expect.objectContaining({fallbackAvailable:!0})}))}),it("falls back to local on rpc error",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"}]);const a=makeGateway({reject:!0}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1"});expect(n.length).toBeGreaterThan(0),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("reason=rpc-error"))})}),describe("parseGatewayHistory provider error replay",()=>{it("converts assistant stopReason=error + errorMessage (HTML) into a summarised bubble",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"你好"}],timestamp:17767039e5,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html><html><head><title>Bad gateway</title></head><body><h1>Bad gateway</h1><p>Cloudflare</p></body></html>",timestamp:1776703955154,__openclaw:{id:"a1",seq:2}}]});expect(e).toHaveLength(2);const t=e[1];expect(t.role).toBe("assistant"),expect(t.content).toContain("HTTP 502"),expect(t.content).not.toContain("<!DOCTYPE"),expect(t.content).not.toContain("<html"),expect(t.metadata).toMatchObject({status:"error",errorSource:"provider",errorCategory:"gateway",httpStatus:502,stopReason:"error"}),expect(t.metadata?.rawError).toContain("<!DOCTYPE"),expect(t.messageId).toBe("a1")}),it("passes through short plain-text error messages verbatim",()=>{const e=parseGatewayHistory({messages:[{role:"assistant",content:[],stopReason:"error",errorMessage:"rate_limit_exceeded: please slow down",timestamp:1776703955154,__openclaw:{id:"a2",seq:1}}]});expect(e).toHaveLength(1),expect(e[0].content).toBe("rate_limit_exceeded: please slow down"),expect(e[0].metadata).toMatchObject({status:"error",errorSource:"provider",errorCategory:"rate_limit"})}),it("truncates oversized plain-text error bodies",()=>{const e="x".repeat(2e3),t=parseGatewayHistory({messages:[{role:"assistant",content:[],stopReason:"error",errorMessage:e,__openclaw:{id:"a3",seq:1}}]});expect(t).toHaveLength(1),expect(t[0].content.length).toBeLessThan(e.length),expect(t[0].content.endsWith("…")).toBe(!0)}),it("does NOT reformat assistant messages whose normal content happens to contain HTML",()=>{const e="Sure, here is the snippet: <html><body><h1>Hello</h1></body></html>. The <h1> tag is a top-level heading.",t=parseGatewayHistory({messages:[{role:"assistant",content:[{type:"text",text:e}],errorMessage:"502 <html>should be ignored</html>",stopReason:"error",__openclaw:{id:"a5",seq:1}}]});expect(t).toHaveLength(1),expect(t[0].content).toBe(e),expect(t[0].content).toContain("<html>"),expect(t[0].metadata?.status).toBeUndefined(),expect(t[0].metadata?.errorCategory).toBeUndefined()}),it("collapses provider retry chains to a single terminal error entry (parentId linked)",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"hi"}],__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"u1",__openclaw:{id:"a1",seq:2}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"a1",__openclaw:{id:"a2",seq:3}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"a2",__openclaw:{id:"a3",seq:4}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"a3",__openclaw:{id:"a4",seq:5}}]});expect(e).toHaveLength(2),expect(e[0].role).toBe("user"),expect(e[1].role).toBe("assistant"),expect(e[1].messageId).toBe("a4"),expect(e[1].content).toContain("HTTP 502");const t=e[1].content.split("HTTP 502").length-1;expect(t).toBe(1)}),it("collapse fallback: missing __openclaw metadata still keeps only the last error in a contiguous run",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"hi"}]},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 1"},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 2 final"}]});expect(e).toHaveLength(2),expect(e[1].content).toBe("502 attempt 2 final")}),it("collapses sibling-style retries where every retry's parent is the same user (not a prior error)",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"hi"}],__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 1",parentId:"u1",__openclaw:{id:"a1",seq:2}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 2",parentId:"u1",__openclaw:{id:"a2",seq:3}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 3",parentId:"u1",__openclaw:{id:"a3",seq:4}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 4 final",parentId:"u1",__openclaw:{id:"a4",seq:5}}]}).filter(e=>"error"===e.metadata?.status);expect(e).toHaveLength(1),expect(e[0].messageId).toBe("a4"),expect(e[0].metadata?.rawError).toBe("502 attempt 4 final")}),it("does NOT collapse error chains that are separated by a successful reply",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"q1"}],__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 first turn",parentId:"u1",__openclaw:{id:"a1",seq:2}},{role:"user",content:[{type:"text",text:"q2"}],parentId:"a1",__openclaw:{id:"u2",seq:3}},{role:"assistant",content:[],stopReason:"error",errorMessage:"503 second turn",parentId:"u2",__openclaw:{id:"a2",seq:4}}]});expect(e).toHaveLength(4),expect(e[1].role).toBe("assistant"),expect(e[1].metadata?.status).toBe("error"),expect(e[3].role).toBe("assistant"),expect(e[3].metadata?.status).toBe("error")}),it("still drops genuinely empty assistant messages (no errorMessage, no content)",()=>{const e=parseGatewayHistory({messages:[{role:"assistant",content:[],__openclaw:{id:"a4",seq:1}}]});expect(e).toHaveLength(0)})}),describe("parseGatewayHistory deterministic ordering (Bug-W)",()=>{const e=[{role:"user",content:[{type:"text",text:"hi"}],timestamp:1776704e6,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[{type:"text",text:"hello"}],timestamp:1776704001e3,__openclaw:{id:"a1",seq:2}},{role:"user",content:[{type:"text",text:"what time is it"}],timestamp:1776704002e3,__openclaw:{id:"u2",seq:3}},{role:"assistant",content:[{type:"text",text:"3pm"}],timestamp:1776704003e3,__openclaw:{id:"a2",seq:4}}];it("produces the same sequence regardless of input order (shuffled inputs)",()=>{const t=parseGatewayHistory({messages:e}),s=parseGatewayHistory({messages:[...e].reverse()}),a=parseGatewayHistory({messages:[e[2],e[0],e[3],e[1]]});expect(t).toHaveLength(4),expect(s).toEqual(t),expect(a).toEqual(t);for(let e=1;e<t.length;e++)expect(t[e]._seq).toBeGreaterThan(t[e-1]._seq)}),it("preserves original order when any entry is missing __openclaw.seq",()=>{const t=parseGatewayHistory({messages:[e[0],e[1],{role:"assistant",content:[{type:"text",text:"oops"}]},e[2],e[3]]}),s=t.findIndex(e=>"assistant"===e.role&&e.content.includes("oops")),a=t.findIndex(e=>"user"===e.role&&e.content.includes("what time"));expect(s).toBeGreaterThan(-1),expect(a).toBeGreaterThan(s)}),it("sorts within a single turn that was split into multiple JSONL rows",()=>{const e=parseGatewayHistory({messages:[{role:"toolResult",content:[{type:"tool_result",tool_use_id:"tc_1",content:"42"}],timestamp:1776704011e3,__openclaw:{id:"tr1",seq:13}},{role:"user",content:[{type:"text",text:"go"}],timestamp:1776704009e3,__openclaw:{id:"u9",seq:10}},{role:"assistant",content:[{type:"tool_use",id:"tc_1",name:"calc",input:{a:6,b:7}}],timestamp:177670401e4,__openclaw:{id:"a9",seq:12}},{role:"assistant",content:[{type:"text",text:"let me compute"}],timestamp:177670401e4,__openclaw:{id:"a9",seq:11}}]}).map(e=>e._seq);for(let t=1;t<e.length;t++)expect(e[t]).toBeGreaterThanOrEqual(e[t-1])}),it("replays persisted role=toolResult file artifacts as turn files",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"screenshot"}],timestamp:1776704009e3,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[{type:"tool_use",id:"tc_1",name:"beeos_get_task",input:{taskId:"task-1"}}],timestamp:177670401e4,__openclaw:{id:"a1",seq:2}},{role:"toolResult",content:[{type:"tool_result",tool_use_id:"tc_1",name:"beeos_get_task",content:JSON.stringify({status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})}],timestamp:1776704011e3,__openclaw:{id:"tr1",seq:3}},{role:"assistant",content:[{type:"text",text:"Done"}],timestamp:1776704012e3,__openclaw:{id:"a2",seq:4}}]}),t=e.find(e=>"tool_call_update"===e.updateType),s=e.findIndex(e=>"tool_call_update"===e.updateType),a=e.findIndex(e=>"assistant"===e.role&&"Done"===e.content);expect(s).toBeGreaterThanOrEqual(0),expect(a).toBeGreaterThan(s),expect(t).toMatchObject({content:"",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});const o=groupIntoTurns(e).find(e=>"assistant"===e.role&&e.toolCalls?.length);expect(o?.files).toEqual([{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]),expect(o?.toolCalls?.[0]?.output).toBe("")}),it("does not replay text-only beeos-file URI tool results as files",()=>{const e=parseGatewayHistory({messages:[{role:"assistant",content:[{type:"tool_use",id:"tc_1",name:"beeos_get_task",input:{}}],timestamp:177670401e4,__openclaw:{id:"a1",seq:1}},{role:"toolResult",content:[{type:"tool_result",tool_use_id:"tc_1",name:"beeos_get_task",content:"beeos-file://file-1"}],timestamp:1776704011e3,__openclaw:{id:"tr1",seq:2}}]}),t=e.find(e=>"tool_call_update"===e.updateType);expect(t).toMatchObject({content:"beeos-file://file-1"}),expect(t?.files).toBeUndefined();const s=groupIntoTurns(e).find(e=>"assistant"===e.role);expect(s?.files).toBeUndefined()}),it("does not reorder when all seqs are already ascending (no-op path)",()=>{const t=parseGatewayHistory({messages:e}).map(e=>e._seq);expect(t).toEqual([1,2,3,4])})});
1
+ import{describe,expect,it,vi,beforeEach,afterEach}from"vitest";import{mkdtempSync,mkdirSync,rmSync,writeFileSync}from"node:fs";import os from"node:os";import path from"node:path";import{groupIntoTurns,loadHistory,parseGatewayHistory}from"./history-replay.js";import{LocalSessionHistory}from"./local-session-history.js";function makeGateway(e={}){const{connected:t=!0,delayMs:s=0,response:a={messages:[]},reject:o=!1}=e;return{get isConnected(){return t},chatHistory:vi.fn(async()=>{if(s>0&&await new Promise(e=>setTimeout(e,s)),o)throw new Error("rpc-failed");return a})}}describe("loadHistory",()=>{const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn()};let t;function s(e,s,a){const o=path.join(t,"agents",e,"sessions");mkdirSync(o,{recursive:!0});const n=path.join(o,`${s}.jsonl`),r=a.map(e=>JSON.stringify({id:e.id,type:"message",timestamp:(new Date).toISOString(),message:{role:e.role,content:[{type:"text",text:e.text}],timestamp:Date.now()}}));writeFileSync(n,r.join("\n")+"\n")}beforeEach(()=>{t=mkdtempSync(path.join(os.tmpdir(),"beeos-claw-lh-")),e.info.mockClear(),e.warn.mockClear(),e.debug.mockClear()}),afterEach(()=>{rmSync(t,{recursive:!0,force:!0})}),it("returns gateway messages when chat.history has content",async()=>{const t=makeGateway({response:{messages:[{id:"m1",role:"user",content:[{type:"text",text:"hi"}]},{id:"m2",role:"assistant",content:[{type:"text",text:"hello"}]}]}}),s=vi.fn(),a=await loadHistory(t,"agent:main:s1",100,{logger:e,emitObs:s});expect(a.length).toBeGreaterThan(0),expect(t.chatHistory).toHaveBeenCalledOnce(),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("source=chat.history")),expect(s).toHaveBeenCalledOnce(),expect(s).toHaveBeenCalledWith(expect.objectContaining({name:"history.source",severity:"info",payload:expect.objectContaining({source:"chat.history",messages:a.length})}))}),it("hydrates chat.history with local persisted toolResult file artifacts",async()=>{const s={status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"device-screenshots/screenshot.jpeg",mimeType:"image/jpeg"}]};!function(e,s,a){const o=path.join(t,"agents",e,"sessions");mkdirSync(o,{recursive:!0});const n=path.join(o,`${s}.jsonl`);writeFileSync(n,a.map(e=>JSON.stringify(e)).join("\n")+"\n")}("main","s1",[{id:"u1",type:"message",timestamp:"2026-07-02T18:02:44.475Z",message:{role:"user",content:[{type:"text",text:"@device /screenshot"}],timestamp:1783015364475}},{id:"a1",type:"message",timestamp:"2026-07-02T18:02:56.660Z",message:{role:"assistant",content:[{type:"toolCall",id:"tc-get-task",name:"beeos_get_task",arguments:{taskId:"task-1",agentId:"device"}}],timestamp:1783015376660}},{id:"tr1",type:"message",timestamp:"2026-07-02T18:02:57.046Z",message:{role:"toolResult",toolCallId:"tc-get-task",toolName:"beeos_get_task",content:[{type:"text",text:JSON.stringify({content:JSON.stringify(s)})}],details:{content:JSON.stringify(s)},isError:!1,timestamp:1783015377046}},{id:"a2",type:"message",timestamp:"2026-07-02T18:03:01.926Z",message:{role:"assistant",content:[{type:"text",text:"Done"}],timestamp:1783015381926}}]);const a=makeGateway({response:{messages:[{role:"user",content:[{type:"text",text:"@device /screenshot"}],timestamp:1783015364475,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[{type:"toolCall",id:"tc-get-task",name:"beeos_get_task",arguments:{taskId:"task-1",agentId:"device"}}],timestamp:1783015376660,__openclaw:{id:"a1",seq:2}},{role:"assistant",content:[{type:"text",text:"Done"}],timestamp:1783015381926,__openclaw:{id:"a2",seq:4}}]}}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=vi.fn(),r=(await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1",emitObs:n})).find(e=>"tool_call_update"===e.updateType&&"tc-get-task"===e.toolCallId);expect(r).toMatchObject({role:"assistant",updateType:"tool_call_update",toolCallId:"tc-get-task",toolName:"beeos_get_task",files:[{uri:"beeos-file://file-1",name:"device-screenshots/screenshot.jpeg",mimeType:"image/jpeg"}]}),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("source=chat.history+local-jsonl-artifacts")),expect(n).toHaveBeenCalledWith(expect.objectContaining({name:"history.source",severity:"info",payload:expect.objectContaining({source:"chat.history+local-jsonl-artifacts",addedFileArtifacts:1})}))}),it("falls back to local jsonl when gateway is disconnected",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"},{id:"m2",role:"assistant",text:"hello"}]);const a=makeGateway({connected:!1}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=vi.fn(),r=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1",emitObs:n});expect(r.length).toBeGreaterThan(0),expect(a.chatHistory).not.toHaveBeenCalled(),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("source=local-jsonl")),expect(n).toHaveBeenCalledWith(expect.objectContaining({name:"history.local_fallback",severity:"info",payload:expect.objectContaining({reason:"gateway-disconnected"})}))}),it("falls back to local jsonl when chat.history times out",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"}]);const a=makeGateway({delayMs:500}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1",rpcTimeoutMs:50});expect(n.length).toBeGreaterThan(0),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("reason=rpc-timeout"))}),it("falls back to local jsonl when chat.history returns empty",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"}]);const a=makeGateway({response:{messages:[]}}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1"});expect(n.length).toBeGreaterThan(0),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("reason=rpc-empty"))}),it("returns [] when both gateway and local are empty",async()=>{const s=makeGateway({response:{messages:[]}}),a=new LocalSessionHistory({logger:e,openclawHome:t}),o=vi.fn(),n=await loadHistory(s,"agent:main:s1",100,{logger:e,localHistory:a,agentId:"main",sessionId:"missing",emitObs:o});expect(n).toEqual([]),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("local-empty")),expect(o).toHaveBeenCalledWith(expect.objectContaining({name:"history.none",severity:"warn",payload:expect.objectContaining({fallbackAvailable:!0})}))}),it("falls back to local on rpc error",async()=>{s("main","s1",[{id:"m1",role:"user",text:"hi"}]);const a=makeGateway({reject:!0}),o=new LocalSessionHistory({logger:e,openclawHome:t}),n=await loadHistory(a,"agent:main:s1",100,{logger:e,localHistory:o,agentId:"main",sessionId:"s1"});expect(n.length).toBeGreaterThan(0),expect(e.info).toHaveBeenCalledWith(expect.stringContaining("reason=rpc-error"))})}),describe("parseGatewayHistory user envelope sanitization",()=>{it("replays production UTC same-line BeeOS user content without transport envelope",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:["Sender (untrusted metadata):","```json",'{"label":"BeeOS Platform Bridge (cli)"}',"```","[Tue 2026-07-07 12:07 UTC] User Message From BeeOS:","今天天气怎么样?"].join("\n")}],timestamp:1783426065672,__openclaw:{id:"8883d542",seq:14}}]});expect(e).toHaveLength(1),expect(e[0]).toMatchObject({role:"user",content:"今天天气怎么样?",messageId:"8883d542",_seq:14}),expect(e[0].content).not.toContain("Sender (untrusted metadata)"),expect(e[0].content).not.toContain("User Message From BeeOS"),expect(e[0].content).not.toContain("UTC]")}),it("does not sanitize assistant text that discusses BeeOS markers",()=>{const e="请解释 User Message From BeeOS: 这个字符串",t=parseGatewayHistory({messages:[{role:"assistant",content:[{type:"text",text:e}],__openclaw:{id:"a1",seq:1}}]});expect(t).toHaveLength(1),expect(t[0].content).toBe(e)})}),describe("parseGatewayHistory provider error replay",()=>{it("converts assistant stopReason=error + errorMessage (HTML) into a summarised bubble",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"你好"}],timestamp:17767039e5,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html><html><head><title>Bad gateway</title></head><body><h1>Bad gateway</h1><p>Cloudflare</p></body></html>",timestamp:1776703955154,__openclaw:{id:"a1",seq:2}}]});expect(e).toHaveLength(2);const t=e[1];expect(t.role).toBe("assistant"),expect(t.content).toContain("HTTP 502"),expect(t.content).not.toContain("<!DOCTYPE"),expect(t.content).not.toContain("<html"),expect(t.metadata).toMatchObject({status:"error",errorSource:"provider",errorCategory:"gateway",httpStatus:502,stopReason:"error"}),expect(t.metadata?.rawError).toContain("<!DOCTYPE"),expect(t.messageId).toBe("a1")}),it("passes through short plain-text error messages verbatim",()=>{const e=parseGatewayHistory({messages:[{role:"assistant",content:[],stopReason:"error",errorMessage:"rate_limit_exceeded: please slow down",timestamp:1776703955154,__openclaw:{id:"a2",seq:1}}]});expect(e).toHaveLength(1),expect(e[0].content).toBe("rate_limit_exceeded: please slow down"),expect(e[0].metadata).toMatchObject({status:"error",errorSource:"provider",errorCategory:"rate_limit"})}),it("truncates oversized plain-text error bodies",()=>{const e="x".repeat(2e3),t=parseGatewayHistory({messages:[{role:"assistant",content:[],stopReason:"error",errorMessage:e,__openclaw:{id:"a3",seq:1}}]});expect(t).toHaveLength(1),expect(t[0].content.length).toBeLessThan(e.length),expect(t[0].content.endsWith("…")).toBe(!0)}),it("does NOT reformat assistant messages whose normal content happens to contain HTML",()=>{const e="Sure, here is the snippet: <html><body><h1>Hello</h1></body></html>. The <h1> tag is a top-level heading.",t=parseGatewayHistory({messages:[{role:"assistant",content:[{type:"text",text:e}],errorMessage:"502 <html>should be ignored</html>",stopReason:"error",__openclaw:{id:"a5",seq:1}}]});expect(t).toHaveLength(1),expect(t[0].content).toBe(e),expect(t[0].content).toContain("<html>"),expect(t[0].metadata?.status).toBeUndefined(),expect(t[0].metadata?.errorCategory).toBeUndefined()}),it("collapses provider retry chains to a single terminal error entry (parentId linked)",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"hi"}],__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"u1",__openclaw:{id:"a1",seq:2}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"a1",__openclaw:{id:"a2",seq:3}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"a2",__openclaw:{id:"a3",seq:4}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 <!DOCTYPE html>bad gateway</html>",parentId:"a3",__openclaw:{id:"a4",seq:5}}]});expect(e).toHaveLength(2),expect(e[0].role).toBe("user"),expect(e[1].role).toBe("assistant"),expect(e[1].messageId).toBe("a4"),expect(e[1].content).toContain("HTTP 502");const t=e[1].content.split("HTTP 502").length-1;expect(t).toBe(1)}),it("collapse fallback: missing __openclaw metadata still keeps only the last error in a contiguous run",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"hi"}]},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 1"},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 2 final"}]});expect(e).toHaveLength(2),expect(e[1].content).toBe("502 attempt 2 final")}),it("collapses sibling-style retries where every retry's parent is the same user (not a prior error)",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"hi"}],__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 1",parentId:"u1",__openclaw:{id:"a1",seq:2}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 2",parentId:"u1",__openclaw:{id:"a2",seq:3}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 3",parentId:"u1",__openclaw:{id:"a3",seq:4}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 attempt 4 final",parentId:"u1",__openclaw:{id:"a4",seq:5}}]}).filter(e=>"error"===e.metadata?.status);expect(e).toHaveLength(1),expect(e[0].messageId).toBe("a4"),expect(e[0].metadata?.rawError).toBe("502 attempt 4 final")}),it("does NOT collapse error chains that are separated by a successful reply",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"q1"}],__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[],stopReason:"error",errorMessage:"502 first turn",parentId:"u1",__openclaw:{id:"a1",seq:2}},{role:"user",content:[{type:"text",text:"q2"}],parentId:"a1",__openclaw:{id:"u2",seq:3}},{role:"assistant",content:[],stopReason:"error",errorMessage:"503 second turn",parentId:"u2",__openclaw:{id:"a2",seq:4}}]});expect(e).toHaveLength(4),expect(e[1].role).toBe("assistant"),expect(e[1].metadata?.status).toBe("error"),expect(e[3].role).toBe("assistant"),expect(e[3].metadata?.status).toBe("error")}),it("still drops genuinely empty assistant messages (no errorMessage, no content)",()=>{const e=parseGatewayHistory({messages:[{role:"assistant",content:[],__openclaw:{id:"a4",seq:1}}]});expect(e).toHaveLength(0)})}),describe("parseGatewayHistory deterministic ordering (Bug-W)",()=>{const e=[{role:"user",content:[{type:"text",text:"hi"}],timestamp:1776704e6,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[{type:"text",text:"hello"}],timestamp:1776704001e3,__openclaw:{id:"a1",seq:2}},{role:"user",content:[{type:"text",text:"what time is it"}],timestamp:1776704002e3,__openclaw:{id:"u2",seq:3}},{role:"assistant",content:[{type:"text",text:"3pm"}],timestamp:1776704003e3,__openclaw:{id:"a2",seq:4}}];it("produces the same sequence regardless of input order (shuffled inputs)",()=>{const t=parseGatewayHistory({messages:e}),s=parseGatewayHistory({messages:[...e].reverse()}),a=parseGatewayHistory({messages:[e[2],e[0],e[3],e[1]]});expect(t).toHaveLength(4),expect(s).toEqual(t),expect(a).toEqual(t);for(let e=1;e<t.length;e++)expect(t[e]._seq).toBeGreaterThan(t[e-1]._seq)}),it("preserves original order when any entry is missing __openclaw.seq",()=>{const t=parseGatewayHistory({messages:[e[0],e[1],{role:"assistant",content:[{type:"text",text:"oops"}]},e[2],e[3]]}),s=t.findIndex(e=>"assistant"===e.role&&e.content.includes("oops")),a=t.findIndex(e=>"user"===e.role&&e.content.includes("what time"));expect(s).toBeGreaterThan(-1),expect(a).toBeGreaterThan(s)}),it("sorts within a single turn that was split into multiple JSONL rows",()=>{const e=parseGatewayHistory({messages:[{role:"toolResult",content:[{type:"tool_result",tool_use_id:"tc_1",content:"42"}],timestamp:1776704011e3,__openclaw:{id:"tr1",seq:13}},{role:"user",content:[{type:"text",text:"go"}],timestamp:1776704009e3,__openclaw:{id:"u9",seq:10}},{role:"assistant",content:[{type:"tool_use",id:"tc_1",name:"calc",input:{a:6,b:7}}],timestamp:177670401e4,__openclaw:{id:"a9",seq:12}},{role:"assistant",content:[{type:"text",text:"let me compute"}],timestamp:177670401e4,__openclaw:{id:"a9",seq:11}}]}).map(e=>e._seq);for(let t=1;t<e.length;t++)expect(e[t]).toBeGreaterThanOrEqual(e[t-1])}),it("replays persisted role=toolResult file artifacts as turn files",()=>{const e=parseGatewayHistory({messages:[{role:"user",content:[{type:"text",text:"screenshot"}],timestamp:1776704009e3,__openclaw:{id:"u1",seq:1}},{role:"assistant",content:[{type:"tool_use",id:"tc_1",name:"beeos_get_task",input:{taskId:"task-1"}}],timestamp:177670401e4,__openclaw:{id:"a1",seq:2}},{role:"toolResult",content:[{type:"tool_result",tool_use_id:"tc_1",name:"beeos_get_task",content:JSON.stringify({status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})}],timestamp:1776704011e3,__openclaw:{id:"tr1",seq:3}},{role:"assistant",content:[{type:"text",text:"Done"}],timestamp:1776704012e3,__openclaw:{id:"a2",seq:4}}]}),t=e.find(e=>"tool_call_update"===e.updateType),s=e.findIndex(e=>"tool_call_update"===e.updateType),a=e.findIndex(e=>"assistant"===e.role&&"Done"===e.content);expect(s).toBeGreaterThanOrEqual(0),expect(a).toBeGreaterThan(s),expect(t).toMatchObject({content:"",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});const o=groupIntoTurns(e).find(e=>"assistant"===e.role&&e.toolCalls?.length);expect(o?.files).toEqual([{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]),expect(o?.toolCalls?.[0]?.output).toBe("")}),it("does not replay text-only beeos-file URI tool results as files",()=>{const e=parseGatewayHistory({messages:[{role:"assistant",content:[{type:"tool_use",id:"tc_1",name:"beeos_get_task",input:{}}],timestamp:177670401e4,__openclaw:{id:"a1",seq:1}},{role:"toolResult",content:[{type:"tool_result",tool_use_id:"tc_1",name:"beeos_get_task",content:"beeos-file://file-1"}],timestamp:1776704011e3,__openclaw:{id:"tr1",seq:2}}]}),t=e.find(e=>"tool_call_update"===e.updateType);expect(t).toMatchObject({content:"beeos-file://file-1"}),expect(t?.files).toBeUndefined();const s=groupIntoTurns(e).find(e=>"assistant"===e.role);expect(s?.files).toBeUndefined()}),it("does not reorder when all seqs are already ascending (no-op path)",()=>{const t=parseGatewayHistory({messages:e}).map(e=>e._seq);expect(t).toEqual([1,2,3,4])})});
@@ -1 +1 @@
1
- import{findLocalHistoryEntryByMessageId}from"./local-session-history.js";import{recordToolCallTurnForUpdate}from"./gateway-events.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{emitNormalizedToolResultForTurn,emitToolResultProjectionForTurn}from"./tool-result-emitter.js";import{collectToolCallsFromTurnWindow,collectToolResultsFromTurnWindow,findCurrentUserTurnStart,findLocalHistoryEntryIndexByMessageId,resolveToolArgs,resolveToolCallId}from"./tool-result-transcript.js";import{stripTransportMetadata}from"../message-filter.js";export function reconcileMissingArtifacts(e,t){const{session:o,turnId:s,messageId:n,runId:r}=t;if(!o.agentId)return;const i=(o.toolCallTurns?.size??0)>0;o.hasAgentAssistantStream&&o.hasAgentThinkingStream&&o.hasAgentToolStartStream&&o.hasAgentToolResultStream&&!i||setImmediate(()=>{(async()=>{if(e.isSessionAlive(o.sessionId))try{const t=e.localHistory.readEntries({agentId:o.agentId,sessionKey:o.gatewaySessionKey,sessionId:o.sessionId}),i=findLocalHistoryEntryByMessageId(t,n);if(!i)return;const a=findLocalHistoryEntryIndexByMessageId(t,n),l=Array.isArray(i.content)?i.content:[];if(0===l.length)return;const d="number"==typeof i.timestamp?new Date(i.timestamp).toISOString():"string"==typeof i.timestamp?i.timestamp:void 0;let c=0;const m=t=>{void 0===t._turnId&&(t._turnId=s),e.transport.sendSessionUpdate(o.sessionId,t,{turnId:s})&&(c+=1)};await reconcilePersistedToolResultsInTurn({deps:e,session:o,turnId:s,entries:t,currentIndex:a,timestamp:d,emit:m});for(const t of l){if(!isRecord(t))continue;const n=asString(t.type);if(n){if("text"===n&&!o.hasAgentAssistantStream){const e=asString(t.text);if(!e)continue;const o=stripTransportMetadata(e);if(!o)continue;m({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o},...d?{timestamp:d}:{}});continue}if("thinking"===n&&!o.hasAgentThinkingStream){const e=asString(t.thinking)??asString(t.text);if(!e)continue;m({sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...d?{timestamp:d}:{}});continue}if("toolCall"===n&&!o.hasAgentToolStartStream){const e=resolveToolCallId(t),n=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t),i=buildToolCallPayload({toolCallId:e,toolName:n,args:r,timestamp:d});recordToolCallTurnForUpdate(o,i,s),m(i);continue}if("toolResult"===n&&!o.hasAgentToolResultStream){const n=resolveToolCallId(t),r=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",i=t.result,a=asString(t.text)??asString(t.result)??(isRecord(i)||Array.isArray(i)?JSON.stringify(i,null,2):void 0);if(!a)continue;await emitNormalizedToolResultForTurn({session:o,envelope:{toolCallId:n,toolName:r,rawText:a,rawResult:i,...d?{timestamp:d}:{}},turnId:s,config:e.config,logger:e.logger,writeObs:e.writeObs,getCanvasState:e.getCanvasState,sendUpdate:e=>(m(e),!0)});continue}}}c>0&&(e.logger.info?.(`[missing-artifacts] reconciled ${c} block(s) sessionId=${o.sessionId} runId=${r??"n/a"} messageId=${n}`),e.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.emitted",severity:"info",payload:{sessionId:o.sessionId,runId:r,messageId:n,emitted:c}}))}catch(t){e.logger.warn?.(`[missing-artifacts] reconcile failed sessionId=${o.sessionId} messageId=${n}: ${t instanceof Error?t.message:String(t)}`),e.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.failed",severity:"warn",payload:{sessionId:o.sessionId,messageId:n,error:t instanceof Error?t.message:String(t)}})}else e.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.skipped_stale",severity:"debug",payload:{sessionId:o.sessionId,messageId:n,runId:r}})})()})}async function reconcilePersistedToolResultsInTurn(e){const{deps:t,session:o,turnId:s,entries:n,currentIndex:r,timestamp:i,emit:a}=e;if(r<0)return 0;const l=findCurrentUserTurnStart(n,r);recordToolCallsFromTurnWindow(o,s,n.slice(l,r+1));let d=0;const c=collectToolResultsFromTurnWindow({entries:n,currentIndex:r,turnId:s,toolCallTurns:o.toolCallTurns,timestamp:i});for(const e of c){if(!e.toolCallId)continue;const n=d;await emitToolResultProjectionForTurn({session:o,projection:e,toolCallId:e.toolCallId,turnId:s,config:t.config,logger:t.logger,writeObs:t.writeObs,getCanvasState:t.getCanvasState,sendUpdate:e=>(a(e),d+=1,!0)}),d>n&&t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.persisted_tool_result.emitted",severity:"info",payload:{sessionId:o.sessionId,toolCallId:e.toolCallId,toolName:e.toolName,projectionKey:e.projectionKey,emitted:d-n}})}return d}function recordToolCallsFromTurnWindow(e,t,o){for(const s of collectToolCallsFromTurnWindow(o))e.toolCallTurns??=new Map,e.toolCallTurns.has(s.toolCallId)||e.toolCallTurns.set(s.toolCallId,{turnId:t,toolName:s.toolName})}function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function asString(e){if("string"==typeof e&&e.length>0)return e}
1
+ import{findLocalHistoryEntryByMessageId}from"./local-session-history.js";import{recordToolCallTurnForUpdate}from"./gateway-events.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{emitNormalizedToolResultForTurn,emitToolResultProjectionForTurn}from"./tool-result-emitter.js";import{collectToolCallsFromTurnWindow,collectToolResultsFromTurnWindow,findCurrentUserTurnStart,findLocalHistoryEntryIndexByMessageId,resolveToolArgs,resolveToolCallId}from"./tool-result-transcript.js";import{stripTransportMetadata}from"../message-sanitization/transport-metadata-sanitizer.js";export function reconcileMissingArtifacts(t,e){const{session:o,turnId:s,messageId:n,runId:r}=e;if(!o.agentId)return;const i=(o.toolCallTurns?.size??0)>0;o.hasAgentAssistantStream&&o.hasAgentThinkingStream&&o.hasAgentToolStartStream&&o.hasAgentToolResultStream&&!i||setImmediate(()=>{(async()=>{if(t.isSessionAlive(o.sessionId))try{const e=t.localHistory.readEntries({agentId:o.agentId,sessionKey:o.gatewaySessionKey,sessionId:o.sessionId}),i=findLocalHistoryEntryByMessageId(e,n);if(!i)return;const a=findLocalHistoryEntryIndexByMessageId(e,n),l=Array.isArray(i.content)?i.content:[];if(0===l.length)return;const d="number"==typeof i.timestamp?new Date(i.timestamp).toISOString():"string"==typeof i.timestamp?i.timestamp:void 0;let c=0;const m=e=>{void 0===e._turnId&&(e._turnId=s),t.transport.sendSessionUpdate(o.sessionId,e,{turnId:s})&&(c+=1)};await reconcilePersistedToolResultsInTurn({deps:t,session:o,turnId:s,entries:e,currentIndex:a,timestamp:d,emit:m});for(const e of l){if(!isRecord(e))continue;const n=asString(e.type);if(n){if("text"===n&&!o.hasAgentAssistantStream){const t=asString(e.text);if(!t)continue;const o=stripTransportMetadata(t);if(!o)continue;m({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o},...d?{timestamp:d}:{}});continue}if("thinking"===n&&!o.hasAgentThinkingStream){const t=asString(e.thinking)??asString(e.text);if(!t)continue;m({sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...d?{timestamp:d}:{}});continue}if("toolCall"===n&&!o.hasAgentToolStartStream){const t=resolveToolCallId(e),n=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",r=resolveToolArgs(e),i=buildToolCallPayload({toolCallId:t,toolName:n,args:r,timestamp:d});recordToolCallTurnForUpdate(o,i,s),m(i);continue}if("toolResult"===n&&!o.hasAgentToolResultStream){const n=resolveToolCallId(e),r=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",i=e.result,a=asString(e.text)??asString(e.result)??(isRecord(i)||Array.isArray(i)?JSON.stringify(i,null,2):void 0);if(!a)continue;await emitNormalizedToolResultForTurn({session:o,envelope:{toolCallId:n,toolName:r,rawText:a,rawResult:i,...d?{timestamp:d}:{}},turnId:s,config:t.config,logger:t.logger,writeObs:t.writeObs,getCanvasState:t.getCanvasState,sendUpdate:t=>(m(t),!0)});continue}}}c>0&&(t.logger.info?.(`[missing-artifacts] reconciled ${c} block(s) sessionId=${o.sessionId} runId=${r??"n/a"} messageId=${n}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.emitted",severity:"info",payload:{sessionId:o.sessionId,runId:r,messageId:n,emitted:c}}))}catch(e){t.logger.warn?.(`[missing-artifacts] reconcile failed sessionId=${o.sessionId} messageId=${n}: ${e instanceof Error?e.message:String(e)}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.failed",severity:"warn",payload:{sessionId:o.sessionId,messageId:n,error:e instanceof Error?e.message:String(e)}})}else t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.skipped_stale",severity:"debug",payload:{sessionId:o.sessionId,messageId:n,runId:r}})})()})}async function reconcilePersistedToolResultsInTurn(t){const{deps:e,session:o,turnId:s,entries:n,currentIndex:r,timestamp:i,emit:a}=t;if(r<0)return 0;const l=findCurrentUserTurnStart(n,r);recordToolCallsFromTurnWindow(o,s,n.slice(l,r+1));let d=0;const c=collectToolResultsFromTurnWindow({entries:n,currentIndex:r,turnId:s,toolCallTurns:o.toolCallTurns,timestamp:i});for(const t of c){if(!t.toolCallId)continue;const n=d;await emitToolResultProjectionForTurn({session:o,projection:t,toolCallId:t.toolCallId,turnId:s,config:e.config,logger:e.logger,writeObs:e.writeObs,getCanvasState:e.getCanvasState,sendUpdate:t=>(a(t),d+=1,!0)}),d>n&&e.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.persisted_tool_result.emitted",severity:"info",payload:{sessionId:o.sessionId,toolCallId:t.toolCallId,toolName:t.toolName,projectionKey:t.projectionKey,emitted:d-n}})}return d}function recordToolCallsFromTurnWindow(t,e,o){for(const s of collectToolCallsFromTurnWindow(o))t.toolCallTurns??=new Map,t.toolCallTurns.has(s.toolCallId)||t.toolCallTurns.set(s.toolCallId,{turnId:e,toolName:s.toolName})}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function asString(t){if("string"==typeof t&&t.length>0)return t}
@@ -1 +1 @@
1
- export function isAgentSyncWaitForGateway(t){return!(!t||t instanceof Map||"wait_gateway"!==t.status)}export async function syncAgentsToPlatform(t){const{gateway:e,agentGatewayUrl:n,capabilities:a,agentFetch:s,logger:i}=t;if(!s)return i?.debug?.("[agent-sync] no agentFetch — skipping sync (no key pair)"),null;if(!n)return i?.debug?.("[agent-sync] no agentGatewayUrl — skipping sync"),null;try{const t=await e.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return i?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const o=[];for(const t of r){let n=[];try{const a=await e.request("skills.status",{agentId:t.id});n=(a?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{}}))}catch{i?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const s={};if(a)for(const[t,e]of Object.entries(a))e&&(s[t]=!0);const r=t.identity?.name||t.name||t.id;o.push({name:t.id,displayName:r,description:t.name||t.identity?.name||t.id,version:"0.1.0",capabilities:s,skills:n,avatarUrl:t.identity?.avatarUrl||"",homepage:""})}const c=new URL("/api/v1/agents/sync",n),d=await s(c,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agents:o})});if(d.ok){const t=await d.json();i?.info?.(`[agent-sync] synced ${o.length} agent(s) — created=${t.created??0} updated=${t.updated??0} deactivated=${t.deactivated??0}`);const e=new Map,n=Array.isArray(t.data)?t.data:[];for(const t of n)t&&"string"==typeof t.name&&"string"==typeof t.id&&e.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return e.size>0?e:null}{const t=await d.text().catch(()=>"");return i?.warn?.(`[agent-sync] sync failed: HTTP ${d.status} — ${t.slice(0,200)}`),null}}catch(t){const e=t instanceof Error?t.message:String(t);return i?.warn?.(`[agent-sync] sync error: ${e}`),"Not connected to Gateway"===e?{status:"wait_gateway",reason:e}:null}}
1
+ export function isAgentSyncWaitForGateway(t){return!(!t||t instanceof Map||"wait_gateway"!==t.status)}export async function syncAgentsToPlatform(t){const{gateway:e,agentGatewayUrl:n,capabilities:a,agentFetch:s,logger:i}=t;if(!s)return i?.debug?.("[agent-sync] no agentFetch — skipping sync (no key pair)"),null;if(!n)return i?.debug?.("[agent-sync] no agentGatewayUrl — skipping sync"),null;try{const t=await e.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return i?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const o=[];for(const t of r){let n=[];try{const a=await e.request("skills.status",{agentId:t.id});n=(a?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{},...t.skillKey?{skillKey:t.skillKey}:{}}))}catch{i?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const s={};if(a)for(const[t,e]of Object.entries(a))e&&(s[t]=!0);const r=t.identity?.name||t.name||t.id;o.push({name:t.id,displayName:r,description:t.name||t.identity?.name||t.id,version:"0.1.0",capabilities:s,skills:n,avatarUrl:t.identity?.avatarUrl||"",homepage:""})}const c=new URL("/api/v1/agents/sync",n),l=await s(c,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agents:o})});if(l.ok){const t=await l.json();i?.info?.(`[agent-sync] synced ${o.length} agent(s) — created=${t.created??0} updated=${t.updated??0} deactivated=${t.deactivated??0}`);const e=new Map,n=Array.isArray(t.data)?t.data:[];for(const t of n)t&&"string"==typeof t.name&&"string"==typeof t.id&&e.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return e.size>0?e:null}{const t=await l.text().catch(()=>"");return i?.warn?.(`[agent-sync] sync failed: HTTP ${l.status} — ${t.slice(0,200)}`),null}}catch(t){const e=t instanceof Error?t.message:String(t);return i?.warn?.(`[agent-sync] sync error: ${e}`),"Not connected to Gateway"===e?{status:"wait_gateway",reason:e}:null}}
@@ -0,0 +1 @@
1
+ import{HEARTBEAT_TOKEN,isSilentReplyPrefixText,stripHeartbeatToken}from"../auto-reply-tokens.js";import{stripTransportMetadata}from"./transport-metadata-sanitizer.js";const HEARTBEAT_OK_LINE=HEARTBEAT_TOKEN,HEARTBEAT_OK_LINE_RE=/^[\s\p{P}\p{S}]*HEARTBEAT_OK[\s\p{P}\p{S}]*$/u;export function isHeartbeatOnlyText(t){const e=t.trim();if(!e)return!1;if(HEARTBEAT_OK_LINE_RE.test(e))return!0;if(e.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(e,{mode:"message"}).shouldSkip)return!0;if(0===stripTransportMetadata(e).trim().length)return!0}return!1}export function isHeartbeatPrefixText(t){if(!t)return!1;const e=t.trim();return!!e&&(!(e.length>=HEARTBEAT_OK_LINE.length)&&isSilentReplyPrefixText(e,HEARTBEAT_OK_LINE))}export function isHeartbeatFullOrPrefix(t){return"none"!==classifyHeartbeatFilter(t).reason}export function classifyHeartbeatFilter(t){if(!t)return{reason:"none",filtered:!1};const e=t.trim();if(!e)return{reason:"none",filtered:!1};if(HEARTBEAT_OK_LINE_RE.test(e))return{reason:"full",filtered:!0};if(e.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(e,{mode:"message"}).shouldSkip)return{reason:"full-stripped",filtered:!0};if(0===stripTransportMetadata(e).trim().length)return{reason:"full-metadata-residue",filtered:!0}}return isHeartbeatPrefixText(e)?{reason:"prefix",filtered:!0}:{reason:"none",filtered:!1}}const HEARTBEAT_PROMPT_PREFIX="read heartbeat.md if it exists";export function isHeartbeatPromptText(t){return t.slice(0,80).toLowerCase().startsWith(HEARTBEAT_PROMPT_PREFIX)}const CRON_PROMPT_PREFIX_RE=/^\[cron:([0-9a-f-]{8,})\s/i;export function isCronPromptText(t){return CRON_PROMPT_PREFIX_RE.test(t.trimStart())}export function extractCronJobId(t){return CRON_PROMPT_PREFIX_RE.exec(t.trimStart())?.[1]}
@@ -0,0 +1 @@
1
+ import{describe,it,expect}from"vitest";import{isCronPromptText,isHeartbeatPromptText,isHeartbeatOnlyText,isHeartbeatPrefixText,isHeartbeatFullOrPrefix,classifyHeartbeatFilter}from"./message-classifier.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)}})});
@@ -0,0 +1 @@
1
+ import{stripTransportMetadata}from"./transport-metadata-sanitizer.js";function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function sanitizeNode(t){if("string"==typeof t){const e=stripTransportMetadata(t);return{value:e,changed:e!==t}}if(Array.isArray(t)){let e=!1;const r=t.map(t=>{const r=sanitizeNode(t);return r.changed&&(e=!0),r.value});return{value:e?r:t,changed:e}}if(!isRecord(t))return{value:t,changed:!1};let e=!1,r=t;if("string"==typeof t.type&&"text"===t.type&&"string"==typeof t.text){const a=stripTransportMetadata(t.text);a!==t.text&&(r={...r,text:a},e=!0)}if(Array.isArray(t.content)){const a=sanitizeNode(t.content);a.changed&&(e||(r={...r}),r.content=a.value,e=!0)}return{value:e?r:t,changed:e}}export function sanitizePromptPayload(t){return sanitizeNode(t).value}
@@ -0,0 +1 @@
1
+ const WEEKDAY=String.raw`(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)`,DATE=String.raw`\d{4}-\d{2}-\d{2}`,CLOCK=String.raw`\d{1,2}:\d{2}(?::\d{2})?`,TIMEZONE=String.raw`(?:UTC|GMT[+-]\d{1,2}(?::?\d{2})?)`,TIMESTAMP_PREFIX_RE=new RegExp(String.raw`^\s*\[${WEEKDAY}\s+${DATE}\s+${CLOCK}\s+${TIMEZONE}\]\s*`,"i"),NESTED_TIME_PREFIX_RE=new RegExp(String.raw`^\s*\[Time:\s*\[${DATE}\s+${WEEKDAY}\s+${CLOCK}\s+${TIMEZONE}\]\]\s*`,"i"),WORKDIR_PREFIX_RE=/^\s*\[working directory:[^\]]*]\s*/i,MESSAGE_ID_PREFIX_RE=/^\s*\[message_id:[^\]]*]\s*/i,SENDER_SHORT_ID_PREFIX_RE=/^\s*\[sender_short_id:[^\]]*]\s*/i,REPLY_TO_CURRENT_PREFIX_RE=/^\s*\[reply_to_current:[^\]]*]\s*/i,USER_MESSAGE_MARKER_PREFIX_RE=/^\s*(?:User Message From|Message From)\s+[A-Za-z0-9_-]+(?:\s+Group Chat Room)?:\s*/i,HEARTBEAT_OK_LINE="HEARTBEAT_OK",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,NESTED_TIME_PREFIX_RE,WORKDIR_PREFIX_RE,MESSAGE_ID_PREFIX_RE,SENDER_SHORT_ID_PREFIX_RE,REPLY_TO_CURRENT_PREFIX_RE,USER_MESSAGE_MARKER_PREFIX_RE],CONVERSATION_INFO_MARKER="Conversation info (untrusted metadata):",SENDER_UNTRUSTED_MARKER="Sender (untrusted metadata):",SENDER_UNTRUSTED_BLOCK_RE=/Sender\s*\(untrusted[^)]*\)\s*:\s*```[a-zA-Z0-9_-]*\s*[\s\S]*?```/i;function normalizeLineEndings(e){return e.replace(/\r\n/g,"\n").replace(/\r/g,"\n")}function stripMetadataPrefixesFromLine(e){let n=e,t=!1,r=!0;for(;r;){r=!1;for(const e of METADATA_PREFIX_PATTERNS)e.test(n)&&(n=n.replace(e,""),t=!0,r=!0)}return{line:n,changed:t}}function stripConversationInfoBlock(e){const n=e.indexOf(CONVERSATION_INFO_MARKER);if(-1===n)return e;const t=/\nCurrent time:[^\n]*/i.exec(e.slice(n));if(t){const r=n+t.index+t[0].length;return e.slice(0,n)+e.slice(r).replace(/^\n+/,"")}const r=e.indexOf("\n\n",n),E=-1===r?e.length:r;return e.slice(0,n)+e.slice(E).replace(/^\n+/,"")}export function stripLeadingSenderMetadataBlocks(e){let n=normalizeLineEndings(e),t=!0;for(;t;){t=!1;const e=/^\s*/.exec(n)?.[0]??"",r=n.slice(e.length),E=/^Sender\s*\(untrusted[^)]*\)\s*:/i.exec(r);if(!E)break;const s=r.slice(E[0].length),i=/^\s*```[a-zA-Z0-9_-]*\s*[\s\S]*?```\s*/.exec(s);if(i){n=s.slice(i[0].length).replace(/^\n+/,""),t=!0;continue}const R=s.indexOf("\n\n");if(-1!==R){n=s.slice(R).replace(/^\n+/,""),t=!0;continue}const _=s.indexOf("\n");n=-1===_?"":s.slice(_+1),t=!0}return n}function stripSenderMetadataBlocks(e){if(!e.includes(SENDER_UNTRUSTED_MARKER))return e;const n=e.replace(SENDER_UNTRUSTED_BLOCK_RE,"").replace(/^\n+/,"");if(n!==e)return n;const t=e.indexOf(SENDER_UNTRUSTED_MARKER),r=e.indexOf("\n\n",t),E=-1===r?e.length:r;return e.slice(0,t)+e.slice(E).replace(/^\n+/,"")}export function stripTransportMetadata(e){const n=normalizeLineEndings(e),t=n.includes(CONVERSATION_INFO_MARKER),r=n.includes(SENDER_UNTRUSTED_MARKER);if(!(n.includes("[")||n.includes("HEARTBEAT_OK")||/MEDIA\s*:/i.test(n)||/User Message From|Message From/i.test(n)||t||r))return n;if(r){const e=stripSenderMetadataBlocks(n).trim();if(e!==n.trim())return e?stripTransportMetadata(e):""}if(t){const e=stripConversationInfoBlock(n).trim();if(e!==n.trim())return e?stripTransportMetadata(e):""}const E=n.split("\n"),s=[];let i=!1;for(const e of E){const n=stripMetadataPrefixesFromLine(e);HEARTBEAT_OK_LINE_RE.test(n.line.trim())?i=!0:UNSUPPORTED_BEEOS_MEDIA_DIRECTIVE_RE.test(n.line)?i=!0:n.changed&&(i=!0,!n.line.trim())||s.push(n.line)}return i?s.join("\n").replace(/^\n+/,"").replace(/\n+$/,""):n}export function normalizeTransportText(e){if(!e)return;const n=stripTransportMetadata(e);return n.length>0?n:void 0}export function previewSanitizedText(e,n=120){if(!e)return"";const t=e.replace(/\\/g,"\\\\").replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/\s+/g," ").trim();if(t.length<=n)return t;const r="...";if(n<=3)return t.slice(0,n);const E=Math.floor((n-3)/2),s=n-3-E;return`${t.slice(0,E)}${r}${t.slice(-s)}`}
@@ -0,0 +1 @@
1
+ import{describe,expect,it}from"vitest";import{normalizeTransportText,previewSanitizedText,stripLeadingSenderMetadataBlocks,stripTransportMetadata}from"./transport-metadata-sanitizer.js";describe("stripTransportMetadata",()=>{it("strips UTC timestamp prefixes from production-shaped lines",()=>{expect(stripTransportMetadata("[Tue 2026-07-07 12:07 UTC] User Message From BeeOS:\n深圳")).toBe("深圳")}),it("strips UTC timestamp prefixes with seconds",()=>{expect(stripTransportMetadata("[Tue 2026-07-07 12:07:45 UTC] hello")).toBe("hello")}),it("strips GMT offsets in legacy and ISO-ish forms",()=>{expect(stripTransportMetadata("[Tue 2026-07-07 12:07 GMT+8] hello")).toBe("hello"),expect(stripTransportMetadata("[Tue 2026-07-07 12:07 GMT+08:00] hello")).toBe("hello"),expect(stripTransportMetadata("[Tue 2026-07-07 12:07 GMT+0800] hello")).toBe("hello")}),it("strips Kimi-style nested Time prefixes",()=>{expect(stripTransportMetadata("[Time: [2026-07-07 Tue 20:07:00 GMT+8]] hello")).toBe("hello")}),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.")}),it("strips leading Sender metadata fenced blocks",()=>{const e=["Sender (untrusted metadata):","```json",'{"label":"BeeOS Platform Bridge (cli)"}',"```","[Tue 2026-07-07 12:07 UTC] User Message From BeeOS:","深圳"].join("\n");expect(stripLeadingSenderMetadataBlocks(e)).toBe("[Tue 2026-07-07 12:07 UTC] User Message From BeeOS:\n深圳"),expect(stripTransportMetadata(e)).toBe("深圳")})}),describe("previewSanitizedText",()=>{it("returns empty string on empty input",()=>{expect(previewSanitizedText("")).toBe("")}),it("escapes newlines / tabs and collapses runs of whitespace",()=>{expect(previewSanitizedText("HEARTBEAT_OK\n[meta]")).toBe("HEARTBEAT_OK\\n[meta]"),expect(previewSanitizedText("a\t\tb")).toBe("a\\t\\tb"),expect(previewSanitizedText("a b c")).toBe("a b c")}),it("returns the full text when within maxLen",()=>{expect(previewSanitizedText("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=previewSanitizedText(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.39",
3
+ "version": "0.5.41-staging.0",
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{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 +0,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)})});