beeos-claw 0.6.88 → 0.6.91

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.
Files changed (48) hide show
  1. package/README.md +66 -6
  2. package/dist/acp-gateway/gateway-events.js +1 -1
  3. package/dist/acp-gateway/history-artifact-hydrator.js +1 -1
  4. package/dist/acp-gateway/history-replay.js +1 -1
  5. package/dist/acp-gateway/local-session-history.js +1 -1
  6. package/dist/canvas/html-delivery.js +1 -1
  7. package/dist/canvas/runtime-lease-token.js +1 -0
  8. package/dist/canvas/tools.js +1 -1
  9. package/dist/canvas-orchestrator.js +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/message-sanitization/transport-metadata-sanitizer.js +1 -1
  12. package/dist/runtime/agent-runtime-failure-classifier.js +1 -1
  13. package/dist/runtime/central-native-composition.js +1 -0
  14. package/dist/runtime/central-native-context-engine.js +1 -0
  15. package/dist/runtime/central-native-credentials.js +1 -0
  16. package/dist/runtime/central-native-delta.js +1 -0
  17. package/dist/runtime/central-native-host.js +1 -0
  18. package/dist/runtime/central-native-interactions.js +1 -0
  19. package/dist/runtime/central-native-memory.js +1 -0
  20. package/dist/runtime/central-native-pending-journal.js +1 -0
  21. package/dist/runtime/central-native-production.js +1 -0
  22. package/dist/runtime/central-native-response.js +1 -0
  23. package/dist/runtime/central-native-runtime.js +1 -0
  24. package/dist/runtime/central-native-titles.js +1 -0
  25. package/dist/runtime/central-session-key.js +1 -0
  26. package/dist/runtime/conversation-title.js +1 -1
  27. package/dist/runtime/identity-lane-lease.js +1 -0
  28. package/dist/runtime/im-reply-sink.js +1 -1
  29. package/dist/runtime/ms-runtime-message-channel-adapter.js +1 -1
  30. package/dist/runtime/ms-runtime-message-channel-consumer-factory.js +1 -1
  31. package/dist/runtime/native-cloud-prompt-bridge.js +1 -1
  32. package/dist/runtime/openclaw-session-write-quiescence.js +1 -1
  33. package/dist/runtime/reply-router.js +1 -1
  34. package/dist/runtime/runtime-command-journal.js +1 -1
  35. package/dist/runtime/runtime-production-authority.js +1 -1
  36. package/dist/runtime/runtime-service-contract-compatibility.js +1 -1
  37. package/dist/runtime/runtime-service-production-candidate.js +1 -1
  38. package/dist/runtime/sandbox-native-activation.js +1 -0
  39. package/dist/runtime/sandbox-native-credentials.js +1 -0
  40. package/dist/runtime/sandbox-native-execution-resolver.js +1 -0
  41. package/dist/runtime/session-mutation-coordinator.js +1 -1
  42. package/dist/terminal/runtime-terminal-outbound-registration.js +1 -1
  43. package/dist/terminal/session-manager.js +1 -1
  44. package/dist/terminal/standalone-runtime-terminal.js +1 -0
  45. package/dist/upload/native-output-tool-hook.js +1 -1
  46. package/openclaw.plugin.json +1 -1
  47. package/package.json +4 -4
  48. package/runtime-service-wire-identity.v1.json +1 -1
package/README.md CHANGED
@@ -321,6 +321,21 @@ the SDK's responsibility. See the
321
321
 
322
322
  ### File Transfer
323
323
 
324
+ Standalone `MEDIA:beeos-file://…` transport directives are removed from reply
325
+ text before Gateway output reaches ACP or Message. The live parser keeps line
326
+ and Markdown fence context per run, holding only possible directive text while
327
+ ordinary prose streams immediately. Terminal events flush unfinished prose;
328
+ complete directives without a trailing newline are removed. Inline explanations,
329
+ fenced and indented code examples remain visible. Full-message history cleanup
330
+ uses the same directive rules. Native Cloud replies bypass Gateway events, so
331
+ `im-reply-sink.ts` applies the same parser to cumulative previews, committed body
332
+ and ordered text parts, and final reply fallback. Clients replay the cleaned
333
+ Message body/parts after refresh; existing stored replies are not rewritten.
334
+ Structured file references bypass this text filter and remain available for
335
+ preview and download. Boundary and completion coverage lives in
336
+ `transport-metadata-sanitizer.test.ts`, `im-reply-sink.test.ts`,
337
+ `gateway-events-file-artifact.test.ts`, and `gateway-events-run-output-routing.test.ts`.
338
+
324
339
  Provides the `beeos_upload_file` tool for agents to upload files from the sandbox to BeeOS platform storage. Also supports file staging via the `POST /beeos/files/stage` HTTP endpoint for downloading files from the platform into the agent sandbox.
325
340
 
326
341
  In Cloud runtime, `beeos_fetch_file` reads confirmed output files from the current
@@ -338,11 +353,17 @@ rejects a default or relative destination before download. Temporary bytes
338
353
  are removed. A failed check returns an error and never tries the legacy account
339
354
  file route. Other installations keep their existing account-file download flow.
340
355
 
356
+ Ordinary text with Canvas enabled must finish once without a finalize revision.
357
+ The native bridge regression checks the actual reply dispatcher, final guard,
358
+ and durable terminal together. Failed file delivery remains an error, identified
359
+ internally as `html_delivery_missing` instead of a model `provider_error`; raw
360
+ exception details are not exposed in chat and cancellation retains priority.
361
+
341
362
  Fetch tests cover success, missing Host authority, mismatched run/file bindings,
342
363
  changed object origins, corrupt bytes, HTTP rejection, and an ended attempt.
343
364
  They also cover default, relative, and absolute destinations, missing workspace,
344
365
  and preserving factory names and per-agent context on discovery/restart.
345
- When the user does not request another file format, real report acceptance must
366
+ When the user requests an HTML report, real report acceptance must
346
367
  generate one HTML file, fetch the same file ID successfully when the model
347
368
  verifies it, and retain one summary and one HTML delivery in live events and
348
369
  stored history. An explicitly requested file format takes precedence in the
@@ -370,11 +391,19 @@ downloaded a new Markdown file without HTML, then delivered explicitly requested
370
391
  HTML while preserving access to the earlier Markdown. Other instances and
371
392
  default images were unchanged. See [selected staging evidence](docs/static-html-native-tool-result.md#selected-staging-acceptance).
372
393
 
373
- This correction does not add a plain-text-only delivery mode. Without a
374
- successful file upload or interactive result, the existing guard still requests
375
- default HTML and rejects a completed prose-only reply without its file. A user
376
- request for plain text with no HTML remains a separate platform gap; it is not
377
- covered by the requested-file-format fix.
394
+ Canvas availability no longer requires every completed turn to deliver a file.
395
+ Ordinary text replies, including explicit text-only requests, may complete without
396
+ calling presentation tools. When a turn attempts HTML publication or file upload,
397
+ the guard still requires successful publication and its matching durable reply
398
+ projection; failed tools or prose claiming delivery cannot satisfy that check.
399
+ The model prompts require actual files for explicitly requested artifacts and
400
+ preserve the requested format. The current public prompt contract has no structured
401
+ delivery requirement, so the guard does not infer a mandatory file from natural
402
+ language: a model that ignores a file request without even attempting a tool is
403
+ not detected by this guard. Adding such a contract is outside this correction.
404
+ Regression tests cover plain text with Canvas enabled, failed HTML attempts,
405
+ HTML reply projection, and Markdown/CSV/PDF uploads. These local checks do not
406
+ claim deployed real-model acceptance for this correction.
378
407
 
379
408
  Artifact generation guidance uses one executable per `exec` call so native
380
409
  approval can bind the command and its file operands. Discovery uses `ls` / `read`
@@ -429,8 +458,39 @@ Logs are written to rolling files in `log.dir` with automatic rotation and sensi
429
458
  - Node.js >= 22
430
459
  - OpenClaw runtime
431
460
 
461
+ ## Cloud staging release
462
+
463
+ This split Cloud repository uses `.github/workflows/publish-cloud-staging.yml`
464
+ with exact `source_sha` and plain-semver `version` inputs. Update `package.json`,
465
+ its root versions in `package-lock.json`, and `openclaw.plugin.json` together.
466
+ The version must advance npm `cloud-staging`; this workflow publishes only that
467
+ dist-tag and does not build or deploy an instance image.
468
+
469
+ When npm publication is unavailable, dispatch `ci.yml` from `main` with an exact
470
+ merged `source_sha`. CI runs the existing quality gates against that source and
471
+ retains the same checked candidate, its dependency lock and a receipt as
472
+ `beeos-claw-ci-package-<run ID>-<attempt>`. The receipt binds package version,
473
+ source SHA, workflow SHA, run/attempt, SHA-256 and SHA-512. It is not an npm
474
+ publication receipt; consumers must independently verify that the main dispatch
475
+ completed successfully. Old successful CI runs without an artifact cannot be
476
+ reconstructed by claiming that their output was retained.
477
+
478
+ After publication, the Cloud backend's `build-runtime-overlay-staging.yml` builds
479
+ `target=openclaw` with the same `source_sha` and `claw_version`; leave
480
+ `claw_source_tarball=false`. Its `runtime-overlay-record.json` binds the package
481
+ source/integrity and image digest. The backend's `apply-beeos-cloud-staging.yml`
482
+ then supports separate `native-create-image-pin` and `instance-runtime-upgrade`
483
+ phases. The release owner supplies explicit instance IDs for selected acceptance
484
+ and verifies the installed package `version`/`gitHead` and actual Pod `imageID`.
485
+ The temporary source-tarball recovery is restricted to 0.6.89 at its original
486
+ fixed commit and cannot publish or build newer MEDIA fixes. See the backend
487
+ `.github/workflows/README.md` for its current recovery limits.
488
+
432
489
  ## Release Channels
433
490
 
491
+ The following general staging/production channel workflow is separate from the
492
+ Cloud split staging path above.
493
+
434
494
  `beeos-claw` publishes to two npm dist-tags so that staging validation never affects production agent pods:
435
495
 
436
496
  | Channel | Git branch | Version shape | npm dist-tag | Docker image tag |
@@ -1 +1 @@
1
- import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{classifyAgentRuntimeFailure}from"../runtime/agent-runtime-failure-classifier.js";import{stripTransportMetadata,normalizeTransportText,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{toMessageServiceCompletedStopReason}from"../runtime/message-service-stop-reason.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";import{observeLifecyclePromise}from"../runtime/lifecycle-promise-observer.js";import{runOutputTerminalFromFailure}from"../runtime/run-output-router.js";import{fromAcpRunOutputUpdate}from"./acp-run-output-mapper.js";import{openRunOutputRoute}from"./run-output-route.js";export function lifecycleEndFailureMessage(t){if(!t)return;const e=!0===t.isError||!0===t.is_error,s=t.error,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof t.message||"string"==typeof t.reason;if(e||o){if(!e){const e="string"==typeof t.stopReason?t.stopReason:"";if("error"!==e&&"timeout"!==e&&"aborted"!==e)return}return("string"==typeof t.message?t.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof t.reason?t.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error"}}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 n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=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;runOutputRouter;adoptBackgroundMutation;abortGatewayRun;cloud;getCronConversationDelivery;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.runOutputRouter=t.runOutputRouter,this.adoptBackgroundMutation=t.adoptBackgroundMutation,this.abortGatewayRun=t.abortGatewayRun,this.cloud=t.cloud,this.getCronConversationDelivery=t.getCronConversationDelivery,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}observeLifecycle(t,e,s){observeLifecyclePromise(t,this.logger,{component:"gateway-events",action:e,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}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.observeLifecycle(this.terminalRunOutput(t,this.failedRunOutput(classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${e}ms`,source:"live_gateway",runtimeKind:"openclaw"}))),"stream_stale_timeout",t))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);if(!s)return!1;s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),recordToolCallTurnForUpdate(t,e,s);const n=fromAcpRunOutputUpdate(e);return!!n&&(this.runOutputRouter.project(s,n),!0)}trackRunOutputWork(t,e){const s=resolveTurnId(t);s&&this.runOutputRouter.track(s,e)}beginRunOutputUpload(t,e,s){if(s!==BEEOS_UPLOAD_TOOL_NAME)return;const n=resolveTurnId(t);n&&this.runOutputRouter.beginUpload(n,e)}terminalRunOutput(t,e){const s=resolveTurnId(t);return s?this.runOutputRouter.terminal(s,e):Promise.reject(new Error(`run output terminal has no run id: ${t.sessionId}`))}failedRunOutput(t){return runOutputTerminalFromFailure(t,Errors.AgentError.rpcCode)}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.beginRunOutputUpload(e,t,s),this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const u={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,u),e.assistantMq?.push(u)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(!e.promptDone)if("final"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t.stopReason)}),"chat_final",e);else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"chat_cancelled",e);else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(n)),"interrupted"===n.kind?"chat_interrupt":"chat_failure",e)}}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;if(!this.attachGatewayRun(e,t.runId))return}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0,this.scheduleStructuredAssistantMedia(e,t.data);const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);this.beginRunOutputUpload(e,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).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 n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.beginRunOutputUpload(e,t,a),n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.trackRunOutputWork(e,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)}));else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,source:"live_gateway",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(a)),"interrupted"===a.kind?"agent_interrupt":o,e)};if("end"===n){const t=lifecycleEndFailureMessage(s);if(t)o(t,"agent_failure");else{const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t)}),"agent_final",e)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"agent_cancelled",e);else if("error"===n){const t=s?.error,e=t&&"object"==typeof t?t:void 0;o(("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?t:void 0)??("string"==typeof e?.message?e.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(e?JSON.stringify(e):void 0)??"gateway lifecycle error","agent_failure")}}}scheduleStructuredAssistantMedia(t,e){const s=resolveTurnId(t);if(!s||!this.runOutputRouter.routesTo(s,"message_service"))return;const n=isRecord(e)?e:void 0,o=[...new Set([..."string"==typeof n?.mediaUrl?[n.mediaUrl]:[],...Array.isArray(n?.mediaUrls)?n.mediaUrls.filter(t=>"string"==typeof t):[]].map(t=>t.trim()).filter(Boolean))];if(0===o.length)return;const a=resolveToolArtifactLinks({toolCallId:`final:${s}`,toolName:"final_reply_media",payloadMediaUrls:o}).then(e=>{for(const s of e){const e={sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}},timestamp:(new Date).toISOString()};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}).catch(t=>{this.logger.warn?.(`[gateway-events] structured assistant media enrichment failed runId=${s}: ${t instanceof Error?t.message:String(t)}`)});this.trackRunOutputWork(t,a)}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,u=a??r,d=n?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:u,toolName:i,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}),n=resolveTurnId(t);try{await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:n,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}finally{n&&e.toolName===BEEOS_UPLOAD_TOOL_NAME&&this.runOutputRouter.completeUpload(n,e.toolCallId)}}attachGatewayRun(t,e){this.logger.debug?.(`[gateway-events] Attaching Gateway run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`);const s=this.adoptBackgroundMutation(t,e);if(!s.ok)return this.observeLifecycle(this.abortGatewayRun(t.gatewaySessionKey,e),"background_run_conflict_abort",t),!1;t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const n=this.state.runIdToCronJobId.get(e);n&&(t.cronJobId=n),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;const o=["acp"];return"owner"===s.ownership&&t.channelId?.trim()&&!t.cronJobId&&o.push("message_service"),openRunOutputRoute(this.runOutputRouter,t,e,o,{awaitPersistenceReconciliation:o.includes("message_service")}),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),!0}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),"started"===e.action&&this.cloud&&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.cloud)return;const e="string"==typeof t.jobId&&t.jobId.trim()?t.jobId.trim():void 0,s="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!e||void 0===s)return void this.logger.warn?.("[gateway-events] cron start missing stable execution identity; skipping notification");const n="string"==typeof t.runId&&t.runId.trim()?t.runId.trim():`cron:${e}:${s}`,o={...t,jobId:e,runAtMs:s,runId:n},a="string"==typeof t.sessionKey?t.sessionKey:void 0;a&&null==o.sessionKey&&(o.sessionKey=a);const r=a?this.state.findSessionByKey(a)?.sessionId:void 0;if(r&&(o.sessionId=r),("string"!=typeof o.name||!o.name)&&e&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),s=t?.jobs;if(Array.isArray(s))for(const t of s)if(isRecord(t)&&t.id===e&&"string"==typeof t.name&&t.name){o.name=t.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}try{await this.cloud.events.publish({type:"cron",data:o})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const u=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:u}}),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.getCronConversationDelivery?.();if(!e)return;const s="string"==typeof t.jobId?t.jobId:void 0;if(!s)return;const n=t.summary,o="string"==typeof t.runId&&t.runId?t.runId:void 0,a="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!o&&void 0===a)return void this.logger.warn?.(`[gateway-events] cron result missing stable execution identity; skipping conversation delivery job=${s}`);let r=this.cronChannelCache?.get(s);if(!r&&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===s){r=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(r)try{const i=await e.deliver({conversationId:r,jobId:s,...o?{runId:o}:{},...void 0!==a?{runAtMs:a}:{},..."string"==typeof t.sessionKey&&t.sessionKey?{sessionKey:t.sessionKey}:{},body:n,..."string"==typeof t.status&&t.status?{status:t.status}:{}});this.logger.info?.(`[gateway-events] delivered cron result to channel=${r} job=${s} message=${i.messageId} disposition=${i.disposition}`)}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=${r} job=${s} err=${e}`),void this.cronChannelCache?.delete(s);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
+ import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{classifyAgentRuntimeFailure}from"../runtime/agent-runtime-failure-classifier.js";import{BeeosMediaDirectiveStream,stripTransportMetadata,normalizeTransportText,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{toMessageServiceCompletedStopReason}from"../runtime/message-service-stop-reason.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";import{observeLifecyclePromise}from"../runtime/lifecycle-promise-observer.js";import{runOutputTerminalFromFailure}from"../runtime/run-output-router.js";import{fromAcpRunOutputUpdate}from"./acp-run-output-mapper.js";import{openRunOutputRoute}from"./run-output-route.js";export function lifecycleEndFailureMessage(t){if(!t)return;const e=!0===t.isError||!0===t.is_error,s=t.error,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof t.message||"string"==typeof t.reason;if(e||o){if(!e){const e="string"==typeof t.stopReason?t.stopReason:"";if("error"!==e&&"timeout"!==e&&"aborted"!==e)return}return("string"==typeof t.message?t.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof t.reason?t.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error"}}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 n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=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;runOutputRouter;adoptBackgroundMutation;abortGatewayRun;cloud;getCronConversationDelivery;gatewayRequest;getCanvasState;cronChannelCache;assistantMediaStreams=new WeakMap;assistantMediaStream(t){const e=resolveTurnId(t);let s=this.assistantMediaStreams.get(t);return s&&s.runId===e||(s={runId:e,stream:new BeeosMediaDirectiveStream},this.assistantMediaStreams.set(t,s)),s.stream}flushAssistantMedia(t){const e=this.assistantMediaStreams.get(t);if(!e||e.runId!==resolveTurnId(t))return;const s=normalizeTransportText(e.stream.finish(),!1);if(!s||classifyHeartbeatFilter(t.agentAssistantTextSoFar??s).filtered)return;const n={sessionUpdate:"agent_message_chunk",content:{type:"text",text:s}};this.sendTurnUpdate(t,n)&&t.assistantMq?.push(n)}constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.runOutputRouter=t.runOutputRouter,this.adoptBackgroundMutation=t.adoptBackgroundMutation,this.abortGatewayRun=t.abortGatewayRun,this.cloud=t.cloud,this.getCronConversationDelivery=t.getCronConversationDelivery,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}observeLifecycle(t,e,s){observeLifecyclePromise(t,this.logger,{component:"gateway-events",action:e,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}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.observeLifecycle(this.terminalRunOutput(t,this.failedRunOutput(classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${e}ms`,source:"live_gateway",runtimeKind:"openclaw"}))),"stream_stale_timeout",t))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);if(!s)return!1;s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),recordToolCallTurnForUpdate(t,e,s);const n=fromAcpRunOutputUpdate(e);return!!n&&(this.runOutputRouter.project(s,n),!0)}trackRunOutputWork(t,e){const s=resolveTurnId(t);s&&this.runOutputRouter.track(s,e)}beginRunOutputUpload(t,e,s){if(s!==BEEOS_UPLOAD_TOOL_NAME)return;const n=resolveTurnId(t);n&&this.runOutputRouter.beginUpload(n,e)}terminalRunOutput(t,e){const s=resolveTurnId(t);return s?(this.flushAssistantMedia(t),this.runOutputRouter.terminal(s,e)):Promise.reject(new Error(`run output terminal has no run id: ${t.sessionId}`))}failedRunOutput(t){return runOutputTerminalFromFailure(t,Errors.AgentError.rpcCode)}sendToolCallSpacer(t,e){this.flushAssistantMedia(t);const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.beginRunOutputUpload(e,t,s),this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const u={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,u),e.assistantMq?.push(u)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(!e.promptDone)if("final"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t.stopReason)}),"chat_final",e);else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"chat_cancelled",e);else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(n)),"interrupted"===n.kind?"chat_interrupt":"chat_failure",e)}}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;if(!this.attachGatewayRun(e,t.runId))return}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0,this.scheduleStructuredAssistantMedia(e,t.data);const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(this.assistantMediaStream(e).write(o),!1);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);this.beginRunOutputUpload(e,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).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 n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.beginRunOutputUpload(e,t,a),n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.trackRunOutputWork(e,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)}));else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,source:"live_gateway",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(a)),"interrupted"===a.kind?"agent_interrupt":o,e)};if("end"===n){const t=lifecycleEndFailureMessage(s);if(t)o(t,"agent_failure");else{const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t)}),"agent_final",e)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"agent_cancelled",e);else if("error"===n){const t=s?.error,e=t&&"object"==typeof t?t:void 0;o(("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?t:void 0)??("string"==typeof e?.message?e.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(e?JSON.stringify(e):void 0)??"gateway lifecycle error","agent_failure")}}}scheduleStructuredAssistantMedia(t,e){const s=resolveTurnId(t);if(!s||!this.runOutputRouter.routesTo(s,"message_service"))return;const n=isRecord(e)?e:void 0,o=[...new Set([..."string"==typeof n?.mediaUrl?[n.mediaUrl]:[],...Array.isArray(n?.mediaUrls)?n.mediaUrls.filter(t=>"string"==typeof t):[]].map(t=>t.trim()).filter(Boolean))];if(0===o.length)return;const a=resolveToolArtifactLinks({toolCallId:`final:${s}`,toolName:"final_reply_media",payloadMediaUrls:o}).then(e=>{for(const s of e){const e={sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}},timestamp:(new Date).toISOString()};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}).catch(t=>{this.logger.warn?.(`[gateway-events] structured assistant media enrichment failed runId=${s}: ${t instanceof Error?t.message:String(t)}`)});this.trackRunOutputWork(t,a)}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,u=a??r,d=n?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:u,toolName:i,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}),n=resolveTurnId(t);try{await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:n,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}finally{n&&e.toolName===BEEOS_UPLOAD_TOOL_NAME&&this.runOutputRouter.completeUpload(n,e.toolCallId)}}attachGatewayRun(t,e){this.logger.debug?.(`[gateway-events] Attaching Gateway run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`);const s=this.adoptBackgroundMutation(t,e);if(!s.ok)return this.observeLifecycle(this.abortGatewayRun(t.gatewaySessionKey,e),"background_run_conflict_abort",t),!1;t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const n=this.state.runIdToCronJobId.get(e);n&&(t.cronJobId=n),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;const o=["acp"];return"owner"===s.ownership&&t.channelId?.trim()&&!t.cronJobId&&o.push("message_service"),openRunOutputRoute(this.runOutputRouter,t,e,o,{awaitPersistenceReconciliation:o.includes("message_service")}),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),!0}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),"started"===e.action&&this.cloud&&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.cloud)return;const e="string"==typeof t.jobId&&t.jobId.trim()?t.jobId.trim():void 0,s="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!e||void 0===s)return void this.logger.warn?.("[gateway-events] cron start missing stable execution identity; skipping notification");const n="string"==typeof t.runId&&t.runId.trim()?t.runId.trim():`cron:${e}:${s}`,o={...t,jobId:e,runAtMs:s,runId:n},a="string"==typeof t.sessionKey?t.sessionKey:void 0;a&&null==o.sessionKey&&(o.sessionKey=a);const r=a?this.state.findSessionByKey(a)?.sessionId:void 0;if(r&&(o.sessionId=r),("string"!=typeof o.name||!o.name)&&e&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),s=t?.jobs;if(Array.isArray(s))for(const t of s)if(isRecord(t)&&t.id===e&&"string"==typeof t.name&&t.name){o.name=t.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}try{await this.cloud.events.publish({type:"cron",data:o})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const u=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:u}}),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.getCronConversationDelivery?.();if(!e)return;const s="string"==typeof t.jobId?t.jobId:void 0;if(!s)return;const n=t.summary,o="string"==typeof t.runId&&t.runId?t.runId:void 0,a="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!o&&void 0===a)return void this.logger.warn?.(`[gateway-events] cron result missing stable execution identity; skipping conversation delivery job=${s}`);let r=this.cronChannelCache?.get(s);if(!r&&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===s){r=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(r)try{const i=await e.deliver({conversationId:r,jobId:s,...o?{runId:o}:{},...void 0!==a?{runAtMs:a}:{},..."string"==typeof t.sessionKey&&t.sessionKey?{sessionKey:t.sessionKey}:{},body:n,..."string"==typeof t.status&&t.status?{status:t.status}:{}});this.logger.info?.(`[gateway-events] delivered cron result to channel=${r} job=${s} message=${i.messageId} disposition=${i.disposition}`)}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=${r} job=${s} err=${e}`),void this.cronChannelCache?.delete(s);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
- export function hydrateFromLocalArtifacts(e){const{messages:l,gatewaySessionKey:t,options:s,logger:a,parseGatewayHistory:o}=e,{localHistory:n,agentId:i,sessionId:r}=s??{};if(!n||!i)return{messages:l,addedFileCount:0};let d=[];try{d=n.readEntries({agentId:i,sessionKey:t,sessionId:r})}catch(e){return(a?.warn??console.debug)?.(`[loadHistory] local artifact hydration read failed: ${e instanceof Error?e.message:String(e)}`),{messages:l,addedFileCount:0}}if(0===d.length)return{messages:l,addedFileCount:0};return mergeLocalFileArtifactMessages(l,o({messages:d}))}export function mergeLocalFileArtifactMessages(e,l){const t=l.filter(e=>e.files?.length);if(0===t.length)return{messages:e,addedFileCount:0};const s=e.map(cloneReplayMessage);let a=0;for(const e of t){const l=replayFileToolCallId(e),t=replayFileToolName(e),o=(e.files??[]).filter(e=>!hasReplayableFileMessage(s,l,e));if(0===o.length)continue;const n=findReplayableFileMessageIndex(s,l);if(n>=0){const e=s[n];e.files=[...e.files??[],...o],a+=o.length;continue}const i={role:"assistant",content:e.content,updateType:l?"tool_call_update":"agent_message_chunk",...l?{toolCallId:l}:{},...t?{toolName:t}:{},files:o,...e.timestamp?{timestamp:e.timestamp}:{},...e.messageId?{messageId:e.messageId}:{},...void 0!==e._seq?{_seq:e._seq}:{}};s.splice(findLocalArtifactInsertIndex(s,e),0,i),a+=o.length}return a>0?{messages:s,addedFileCount:a}:{messages:e,addedFileCount:0}}function cloneReplayMessage(e){return{...e,...e.files?{files:e.files.map(e=>({...e}))}:{},...e.toolCalls?{toolCalls:e.toolCalls.map(e=>({...e}))}:{}}}function hasReplayableFileMessage(e,l,t){return e.some(e=>!!e.files?.some(e=>e.uri===t.uri)&&("agent_message_chunk"===e.updateType||"tool_call_update"===e.updateType&&(!l||e.toolCallId===l)))}function findReplayableFileMessageIndex(e,l){return e.findIndex(e=>"agent_message_chunk"===e.updateType&&!l||Boolean("tool_call_update"===e.updateType&&l&&e.toolCallId===l))}function findLocalArtifactInsertIndex(e,l){if(void 0!==l._seq){const t=e.findIndex(e=>void 0!==e._seq&&e._seq>l._seq);if(t>=0)return t}const t=replayFileToolCallId(l);if(t)for(let l=e.length-1;l>=0;l--){const s=e[l];if(s.toolCallId===t)return l+1;if(s.toolCalls?.some(e=>e.id===t))return l+1}return e.length}function replayFileToolCallId(e){return e.toolCallId?e.toolCallId:1===e.toolCalls?.length?e.toolCalls[0]?.id:void 0}function replayFileToolName(e){return e.toolName?e.toolName:1===e.toolCalls?.length?e.toolCalls[0]?.name:void 0}
1
+ import{centralNativeChatEnabled}from"../runtime/central-native-runtime.js";export function hydrateFromLocalArtifacts(e){const{messages:l,gatewaySessionKey:t,options:a,logger:s,parseGatewayHistory:o}=e,{localHistory:n,agentId:i,sessionId:r}=a??{};if(!n||!i)return{messages:l,addedFileCount:0};let d=[];try{d=n.readEntries({agentId:i,sessionKey:t,sessionId:r})}catch(e){if(centralNativeChatEnabled())throw e;return(s?.warn??console.debug)?.(`[loadHistory] local artifact hydration read failed: ${e instanceof Error?e.message:String(e)}`),{messages:l,addedFileCount:0}}if(0===d.length)return{messages:l,addedFileCount:0};return mergeLocalFileArtifactMessages(l,o({messages:d}))}export function mergeLocalFileArtifactMessages(e,l){const t=l.filter(e=>e.files?.length);if(0===t.length)return{messages:e,addedFileCount:0};const a=e.map(cloneReplayMessage);let s=0;for(const e of t){const l=replayFileToolCallId(e),t=replayFileToolName(e),o=(e.files??[]).filter(e=>!hasReplayableFileMessage(a,l,e));if(0===o.length)continue;const n=findReplayableFileMessageIndex(a,l);if(n>=0){const e=a[n];e.files=[...e.files??[],...o],s+=o.length;continue}const i={role:"assistant",content:e.content,updateType:l?"tool_call_update":"agent_message_chunk",...l?{toolCallId:l}:{},...t?{toolName:t}:{},files:o,...e.timestamp?{timestamp:e.timestamp}:{},...e.messageId?{messageId:e.messageId}:{},...void 0!==e._seq?{_seq:e._seq}:{}};a.splice(findLocalArtifactInsertIndex(a,e),0,i),s+=o.length}return s>0?{messages:a,addedFileCount:s}:{messages:e,addedFileCount:0}}function cloneReplayMessage(e){return{...e,...e.files?{files:e.files.map(e=>({...e}))}:{},...e.toolCalls?{toolCalls:e.toolCalls.map(e=>({...e}))}:{}}}function hasReplayableFileMessage(e,l,t){return e.some(e=>!!e.files?.some(e=>e.uri===t.uri)&&("agent_message_chunk"===e.updateType||"tool_call_update"===e.updateType&&(!l||e.toolCallId===l)))}function findReplayableFileMessageIndex(e,l){return e.findIndex(e=>"agent_message_chunk"===e.updateType&&!l||Boolean("tool_call_update"===e.updateType&&l&&e.toolCallId===l))}function findLocalArtifactInsertIndex(e,l){if(void 0!==l._seq){const t=e.findIndex(e=>void 0!==e._seq&&e._seq>l._seq);if(t>=0)return t}const t=replayFileToolCallId(l);if(t)for(let l=e.length-1;l>=0;l--){const a=e[l];if(a.toolCallId===t)return l+1;if(a.toolCalls?.some(e=>e.id===t))return l+1}return e.length}function replayFileToolCallId(e){return e.toolCallId?e.toolCallId:1===e.toolCalls?.length?e.toolCalls[0]?.id:void 0}function replayFileToolName(e){return e.toolName?e.toolName:1===e.toolCalls?.length?e.toolCalls[0]?.name:void 0}
@@ -1 +1 @@
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
+ 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{centralNativeChatEnabled}from"../runtime/central-native-runtime.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){if(centralNativeChatEnabled()){if(!s?.localHistory||!s.agentId)throw new Error("central native history requires the prepared native session reader");return parseGatewayHistory({messages:s.localHistory.readEntries({agentId:s.agentId,sessionKey:t,sessionId:s.sessionId})}).slice(-o)}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,d=e.addedFileCount>0?"chat.history+local-jsonl-artifacts":"chat.history";return l(`[loadHistory] source=${d} sid=${t} messages=${o.length} (user=${c} asst=${u}) artifacts=${e.addedFileCount} elapsedMs=${a}`),i?.({name:"history.source",severity:"info",payload:{sessionKey:t,source:d,messages:o.length,userCount:c,asstCount:u,addedFileArtifacts:e.addedFileCount,elapsedMs:a}}),o}const{localHistory:d,agentId:m,sessionId:f}=s??{},g=e?.isConnected?null===u?c?"rpc-error":"rpc-timeout":"rpc-empty":"gateway-disconnected";if(!d||!m){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=d.readEntries({agentId:m,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),d=l.outputText||i,m=!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=d,e.status=m?"failed":"completed"),p?.length&&(n.files=[...n.files??[],...p])}else s.push(u({role:"assistant",content:d,updateType:"tool_call_update",toolCallId:a||void 0,toolName:r,files:p}))}continue}}const p="user"===o?"user":"assistant";let d,m,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?m=(m??"")+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(m||("string"==typeof t.reasoning&&t.reasoning?m=t.reasoning:"string"==typeof t.thinking&&t.thinking&&(m=t.thinking)),!d?.length){const e=t.tool_calls??t.toolCalls;if(Array.isArray(e)){d=[];for(const t of e){if(!t||"object"!=typeof t)continue;const e=t,o=e.function;d.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===d.length&&(d=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&&!m&&!d?.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||m||d?.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:m,toolCalls:d,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{closeSync,existsSync,openSync,readFileSync,readSync,statSync}from"node:fs";import path from"node:path";import{resolveOpenClawStateDir}from"../runtime/openclaw-state-dir.js";export const LOCAL_HISTORY_LIMIT=200;export const LOCAL_HISTORY_TAIL_WINDOW_LINES=6400;export const LOCAL_HISTORY_TAIL_CHUNK_BYTES=65536;export const LOCAL_HISTORY_TAIL_MAX_BYTES=1048576;export function findLocalHistoryEntryByMessageId(t,e){for(let s=t.length-1;s>=0;s--){const i=t[s];if(!i)continue;const o=i.__openclaw;if(isRecord(o)&&o.id===e)return i;if(i.id===e)return i}}export class LocalSessionHistory{logger;stateDirOverride;agentsDirOverride;constructor(t){this.logger=t.logger,this.stateDirOverride=t.stateDir,this.agentsDirOverride=t.agentsDir}readEntries(t){const{agentId:e,sessionKey:s,sessionId:i}=t;if(!e||!s)return[];const o=this.resolveSessionsDir(e),r=this.resolveSessionFile(o,s,i);if(!r)return[];const n=this.readTailLines(r);if(0===n.length)return[];const c=[];for(const t of n){let e;try{e=JSON.parse(t)}catch{continue}if(!e||"object"!=typeof e)continue;const s=e;if("message"!==s.type)continue;const i=s.message;if(!i||"object"!=typeof i)continue;const o=i,r="string"==typeof o.role?o.role:void 0;if(!r)continue;const n="string"==typeof s.id?s.id:void 0,a=n?{id:n}:void 0,l=o.timestamp,f=s.timestamp,p="number"==typeof l||"string"==typeof l?l:"string"==typeof f?f:void 0,y={...o,role:r,...void 0!==p?{timestamp:p}:{},...a?{__openclaw:a}:{}};if(c.push(y),c.length>=200)break}return c.reverse()}resolveStateDir(){return this.stateDirOverride??resolveOpenClawStateDir()}resolveSessionsDir(t){return this.agentsDirOverride?path.join(this.agentsDirOverride,t,"sessions"):path.join(this.resolveStateDir(),"agents",t,"sessions")}resolveSessionFile(t,e,s){const i=path.join(t,"sessions.json");let o;if(existsSync(i))try{const t=readFileSync(i,"utf-8"),e=JSON.parse(t);e&&"object"==typeof e&&(o=e)}catch(t){this.logger.warn?.(`[local-session-history] failed to read sessions.json at ${i}: ${String(t)}`)}const r=s?[e,s]:[e];let n,c=s;for(const t of r){const e=o?.[t];if(!e||"object"!=typeof e)continue;const s=e,i="string"==typeof s.sessionFile?s.sessionFile.trim():"",r="string"==typeof s.sessionId?s.sessionId.trim():"";if(i&&(n=i),r&&!c&&(c=r),n)break}if(n){const e=path.isAbsolute(n)?n:path.join(t,n);return existsSync(e)?e:void 0}if(!c)return;const a=path.join(t,`${c}.jsonl`);return existsSync(a)?a:void 0}readTailLines(t){let e,s;try{e=statSync(t).size}catch(e){return this.logger.warn?.(`[local-session-history] failed to stat ${t}: ${String(e)}`),[]}if(e<=0)return[];try{s=openSync(t,"r");let i=e,o=1048576,r="";const n=[];for(;i>0&&o>0&&n.length<6400;){const t=Math.min(65536,i,o),e=i-t,c=Buffer.allocUnsafe(t),a=readSync(s,c,0,t,e);if(a<=0)break;i=e,o-=a;const l=(c.toString("utf-8",0,a)+r).split("\n");r=l.shift()??"";for(let t=l.length-1;t>=0;t--){const e=l[t]?.trim();if(e&&(n.push(e),n.length>=6400))break}}const c=r.trim();return c&&n.length<6400&&n.push(c),n}catch(e){return this.logger.warn?.(`[local-session-history] failed to tail ${t}: ${String(e)}`),[]}finally{if(void 0!==s)try{closeSync(s)}catch{}}}}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}
1
+ import{closeSync,existsSync,openSync,readFileSync,readSync,statSync}from"node:fs";import path from"node:path";import{resolveOpenClawStateDir}from"../runtime/openclaw-state-dir.js";import{centralNativeChatEnabled,centralNativeRuntime}from"../runtime/central-native-runtime.js";export const LOCAL_HISTORY_LIMIT=200;export const LOCAL_HISTORY_TAIL_WINDOW_LINES=6400;export const LOCAL_HISTORY_TAIL_CHUNK_BYTES=65536;export const LOCAL_HISTORY_TAIL_MAX_BYTES=1048576;export function findLocalHistoryEntryByMessageId(t,e){for(let s=t.length-1;s>=0;s--){const i=t[s];if(!i)continue;const r=i.__openclaw;if(isRecord(r)&&r.id===e)return i;if(i.id===e)return i}}export class LocalSessionHistory{logger;stateDirOverride;agentsDirOverride;constructor(t){this.logger=t.logger,this.stateDirOverride=t.stateDir,this.agentsDirOverride=t.agentsDir}readEntries(t){const{agentId:e,sessionKey:s,sessionId:i}=t;if(!e||!s)return[];if(centralNativeChatEnabled())return centralNativeRuntime().readEvents(s).flatMap(t=>{if(!isRecord(t)||"message"!==t.type||!isRecord(t.message))return[];const e=t.message;return"string"!=typeof e.role?[]:[{...e,..."string"==typeof t.id?{__openclaw:{id:t.id}}:{},...void 0===e.timestamp&&"string"==typeof t.timestamp?{timestamp:t.timestamp}:{}}]});const r=this.resolveSessionsDir(e),n=this.resolveSessionFile(r,s,i);if(!n)return[];const o=this.readTailLines(n);if(0===o.length)return[];const a=[];for(const t of o){let e;try{e=JSON.parse(t)}catch{continue}if(!e||"object"!=typeof e)continue;const s=e;if("message"!==s.type)continue;const i=s.message;if(!i||"object"!=typeof i)continue;const r=i,n="string"==typeof r.role?r.role:void 0;if(!n)continue;const o="string"==typeof s.id?s.id:void 0,c=o?{id:o}:void 0,l=r.timestamp,f=s.timestamp,p="number"==typeof l||"string"==typeof l?l:"string"==typeof f?f:void 0,d={...r,role:n,...void 0!==p?{timestamp:p}:{},...c?{__openclaw:c}:{}};if(a.push(d),a.length>=200)break}return a.reverse()}resolveStateDir(){return this.stateDirOverride??resolveOpenClawStateDir()}resolveSessionsDir(t){return this.agentsDirOverride?path.join(this.agentsDirOverride,t,"sessions"):path.join(this.resolveStateDir(),"agents",t,"sessions")}resolveSessionFile(t,e,s){const i=path.join(t,"sessions.json");let r;if(existsSync(i))try{const t=readFileSync(i,"utf-8"),e=JSON.parse(t);e&&"object"==typeof e&&(r=e)}catch(t){this.logger.warn?.(`[local-session-history] failed to read sessions.json at ${i}: ${String(t)}`)}const n=s?[e,s]:[e];let o,a=s;for(const t of n){const e=r?.[t];if(!e||"object"!=typeof e)continue;const s=e,i="string"==typeof s.sessionFile?s.sessionFile.trim():"",n="string"==typeof s.sessionId?s.sessionId.trim():"";if(i&&(o=i),n&&!a&&(a=n),o)break}if(o){const e=path.isAbsolute(o)?o:path.join(t,o);return existsSync(e)?e:void 0}if(!a)return;const c=path.join(t,`${a}.jsonl`);return existsSync(c)?c:void 0}readTailLines(t){let e,s;try{e=statSync(t).size}catch(e){return this.logger.warn?.(`[local-session-history] failed to stat ${t}: ${String(e)}`),[]}if(e<=0)return[];try{s=openSync(t,"r");let i=e,r=1048576,n="";const o=[];for(;i>0&&r>0&&o.length<6400;){const t=Math.min(65536,i,r),e=i-t,a=Buffer.allocUnsafe(t),c=readSync(s,a,0,t,e);if(c<=0)break;i=e,r-=c;const l=(a.toString("utf-8",0,c)+n).split("\n");n=l.shift()??"";for(let t=l.length-1;t>=0;t--){const e=l[t]?.trim();if(e&&(o.push(e),o.length>=6400))break}}const a=n.trim();return a&&o.length<6400&&o.push(a),o}catch(e){return this.logger.warn?.(`[local-session-history] failed to tail ${t}: ${String(e)}`),[]}finally{if(void 0!==s)try{closeSync(s)}catch{}}}}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}
@@ -1 +1 @@
1
- import{confirmedUploadResourceLinks}from"../upload/tool-artifact-enrichment.js";import{isPlainRecord}from"../utils/text.js";import{isInteractiveUiResult,staticHtmlProjectionOf}from"./static-html-result.js";export class HtmlDeliveryGuard{published;projectedFileId;uploaded=new Map;uploadedReply=!1;revised=!1;interactive=!1;observe(e,t,i,s,l){if(s)return;const r=staticHtmlProjectionOf(e,t,i);if(r&&!this.published&&(this.published=r.result),l){const s=confirmedUploadResourceLinks({toolCallId:l,toolName:e,params:isPlainRecord(t)?t:void 0,rawResult:i});s.length&&this.uploaded.set(l,new Set(s.map(e=>e.uri)))}isInteractiveUiResult(e,t,i)&&(this.interactive=!0)}get publishedFileId(){return this.published?.artifact.fileId}recordReply(e){e.artifact.fileId===this.publishedFileId&&(this.projectedFileId=e.artifact.fileId)}recordUploadReply(e,t){const i=this.uploaded.get(e);i&&t.some(e=>i.has(e.uri))&&(this.uploadedReply=!0)}beforeFinalize(e){if(!(this.published||this.uploaded.size>0||this.interactive||this.revised||e.stopHookActive))return this.revised=!0,{action:"revise",reason:"html_delivery_missing",retry:{instruction:"Continue the previous request. The requested deliverable has not been published. Follow the user's explicitly requested file format: create the actual file, check its contents, call beeos_upload_file with its local path, and wait for a successful upload. Do not substitute HTML or add an HTML copy for a Markdown (.md), CSV, PDF, or other requested-format file. Only when no other file format was requested, call render_ui with id, title and self-contained html containing the completed answer, and wait for a successful static_html result; do not upload that HTML again. Claim delivery only after the matching tool succeeds, then give one brief confirmation with no duplicate answer in chat.",idempotencyKey:"beeos-html-delivery",maxAttempts:1}}}assertComplete(){if((!this.interactive&&!this.uploadedReply||this.published)&&(!this.published||this.projectedFileId!==this.published.artifact.fileId))throw new Error("html_delivery_missing: required HTML or confirmed uploaded file was not published and delivered to this reply")}}
1
+ import{confirmedUploadResourceLinks}from"../upload/tool-artifact-enrichment.js";import{isPlainRecord}from"../utils/text.js";import{isInteractiveUiResult,isStaticHtmlInvocation,staticHtmlProjectionOf}from"./static-html-result.js";export class HtmlDeliveryError extends Error{constructor(){super("html_delivery_missing: required HTML or confirmed uploaded file was not published and delivered to this reply"),this.name="HtmlDeliveryError"}}export class HtmlDeliveryGuard{required;attempted=!1;constructor(e=!0){this.required=e}observeAttempt(e,t){("beeos_upload_file"===e||"openclaw:beeos-claw:beeos_upload_file"===e||"tool_call"===e&&isPlainRecord(t)&&("beeos_upload_file"===t.id||"openclaw:beeos-claw:beeos_upload_file"===t.id)||isStaticHtmlInvocation(e,t))&&(this.attempted=!0)}get needsDelivery(){return this.required||this.attempted}published;projectedFileId;uploaded=new Map;uploadedReply=!1;revised=!1;interactive=!1;observe(e,t,i,s,l){if(this.observeAttempt(e,t),s)return;const r=staticHtmlProjectionOf(e,t,i);if(r&&!this.published&&(this.published=r.result),l){const s=confirmedUploadResourceLinks({toolCallId:l,toolName:e,params:isPlainRecord(t)?t:void 0,rawResult:i});s.length&&this.uploaded.set(l,new Set(s.map(e=>e.uri)))}isInteractiveUiResult(e,t,i)&&(this.interactive=!0)}get publishedFileId(){return this.published?.artifact.fileId}recordReply(e){e.artifact.fileId===this.publishedFileId&&(this.projectedFileId=e.artifact.fileId)}recordUploadReply(e,t){const i=this.uploaded.get(e);i&&t.some(e=>i.has(e.uri))&&(this.uploadedReply=!0)}beforeFinalize(e){if(!(!this.needsDelivery||this.published||this.uploaded.size>0||this.interactive||this.revised||e.stopHookActive))return this.revised=!0,{action:"revise",reason:"html_delivery_missing",retry:{instruction:"Continue the previous request. The requested deliverable has not been published. Follow the user's explicitly requested file format: create the actual file, check its contents, call beeos_upload_file with its local path, and wait for a successful upload. Do not substitute HTML or add an HTML copy for a Markdown (.md), CSV, PDF, or other requested-format file. Only when no other file format was requested, call render_ui with id, title and self-contained html containing the completed answer, and wait for a successful static_html result; do not upload that HTML again. Claim delivery only after the matching tool succeeds, then give one brief confirmation with no duplicate answer in chat.",idempotencyKey:"beeos-html-delivery",maxAttempts:1}}}assertComplete(){if((this.needsDelivery||this.published||0!==this.uploaded.size)&&(!this.interactive&&!this.uploadedReply||this.published)&&(!this.published||this.projectedFileId!==this.published.artifact.fileId))throw new HtmlDeliveryError}}
@@ -0,0 +1 @@
1
+ export const RUNTIME_LEASE_HEADER="X-BeeOS-Runtime-Lease";export function createSandboxCanvasTokenProvider(e,n,r){return()=>{const t=n?r()?.runtimeLeaseCredential?.trim():"";return e.canvas.token(t?{headers:{[RUNTIME_LEASE_HEADER]:t}}:void 0,n)}}
@@ -1 +1 @@
1
- import{extractBeeosConversationIdFromSessionKey}from"../runtime/openclaw-session-key-resolution.js";const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}const A2UI_INTERACTIVE_TYPES=new Set(["Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider"]);export const NON_INTERACTIVE_A2UI_ERROR="Display-only A2UI is not supported. Call render_ui again with id, title, and self-contained static HTML in html. A2UI root/components are reserved for interactive buttons, inputs, or component actions.";export function hasInteractiveComponent(e){return e.some(e=>{const t=e?.component;return!(!t||"object"!=typeof t)&&Object.entries(t).some(([e,t])=>A2UI_INTERACTIVE_TYPES.has(e)||Boolean(t&&"object"==typeof t&&t.action))})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const CANVAS_COMPONENT_ID_GUIDANCE="Component IDs must differ from non-reference scalar values (usageHint, literalString, and action.name). Use type-qualified IDs such as text_heading, text_body, and column_root; do not use scalar values such as body, h1, or center as IDs. A2UI v0.8 resolves strings matching component IDs as references. Keep root, child, explicitList, and template.componentId references matching their target IDs. Reuse the same component IDs when updating a surface.";export const RENDER_UI_TOOL_DESCRIPTION="Publish a static Canvas result as self-contained HTML using id, title, and html. The result is stored as a BeeOS file and shown in the conversation. If the user explicitly requests another file format, create and upload that file with beeos_upload_file instead; HTML does not replace it. Legacy A2UI calls with id, root, and components remain available for interactive experiences.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"Surface id. Reuse to update."},title:{type:"string",description:"Human-readable title for a static HTML Canvas result."},html:{type:"string",description:"Complete self-contained static HTML. Maximum UTF-8 size: 5 MiB."},root:{type:"string",description:"Root component id."},components:{type:"array",description:"Flat ComponentInstance list. Each item has id plus one component type.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:CANVAS_COMPONENT_ID_GUIDANCE},component:{type:"object",description:"One type key: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart."}}}},data:{type:"object",description:"Optional binding model, path keys like /name."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e)),MAX_STATIC_HTML_BYTES=5242880,FILE_ID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function renderStaticHtml(e,t,n){const a=e=>({success:!1,presentation:"static_html",error:e}),o="string"==typeof n.id?n.id.trim():"",i="string"==typeof n.title?n.title.trim():"",r=n.html;if(!o||!i||"string"!=typeof r||!r.trim())return a("id, title, and non-empty html are required");if("root"in n||"components"in n)return a("html cannot be combined with root or components");if(i.length>160)return a("Canvas title is too long (max 160 characters)");if(Buffer.byteLength(r,"utf8")>5242880)return a("Canvas HTML exceeds the 5 MiB preview limit");if(!e.publishStaticHtml)return a("Canvas file upload is unavailable");try{const n=await e.publishStaticHtml({id:o,title:i,html:r},t);return FILE_ID.test(n.fileId)&&n.uri===`beeos-file://${n.fileId}`&&"text/html"===n.mimeType?{success:!0,presentation:"static_html",surfaceId:o,artifact:{kind:"canvas",fileId:n.fileId,uri:n.uri,mimeType:"text/html",title:i}}:a("Canvas HTML upload returned an invalid file reference")}catch(e){return a(`Canvas HTML upload failed: ${e instanceof Error?e.message:String(e)}`)}}async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=(r?extractBeeosConversationIdFromSessionKey(r):void 0)??(r&&r!==t?t:void 0),u=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(u,s),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,getGatewaySessionKey:o}=e;let i=0;return async(o,r)=>{const s=r;if(Object.prototype.hasOwnProperty.call(s,"html"))return renderStaticHtml(e,o,s);const l=s.id,c=s.root,d="panel";let u=s.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!(l&&c&&Array.isArray(u)&&u.length))return{success:!1,error:"id, root, and components (array) are required"};const p=normalizeComponents(u);if(!hasInteractiveComponent(p))return{success:!1,error:NON_INTERACTIVE_A2UI_ERROR};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return n.warn?.(`[canvas] Rate limited surfaceId=${l}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const m=s.data;let h=e.getActiveSessionId?.(o);for(let t=0;!h&&t<3;t++)await delay(400),h=e.getActiveSessionId?.(o);if(!h&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}h=e.getActiveSessionId?.(o)}if(!h)return n.warn?.(`[canvas] No active session for toolCallId=${o} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(o),y=await ensureRelayConnected(e,h,o,f),v=t.has(l,y??void 0),g=Date.now(),T=t.getRelayClient();y&&T&&g-i>6e4&&T.createSnapshot(y,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),v?t.update(l,p,y,c,m):t.create(y??`_fallback_${Date.now()}`,l,d,p,{origin:{kind:"agent"},rootComponentId:c,dataModel:m});const b=p.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:v?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:b}}),n.debug?.(`[canvas] ${v?"Updated":"Created"} surface=${l} components=${p.length}`);const C={success:!0,surfaceId:l,presentation:d,isUpdate:v,activeSurfaces:y?t.getAllActive(y).map(e=>e.surfaceId):[l]};return m&&(C.dataModel=m),C}}export function createRenderUiToolExecutor(e){const t=createRenderUiHandler(e);return async(e,n)=>{const a=await t(e,n),o=[{type:"text",text:JSON.stringify(a)}];if(!0===a.success&&"static_html"===a.presentation){const e=a.artifact;o.push({type:"resource_link",uri:e.uri,name:`${e.title}.html`,mimeType:e.mimeType})}return{...a,content:o,details:a,isError:!1===a.success}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_PRESENTATION_GATE="## BeeOS Canvas presentation gate\n\nCanvas presentation is available in the background for this BeeOS conversation. The Canvas resource tab does not need to be open and no Web subscriber is required before a Canvas tool is called. For static HTML success, say the HTML file is ready and can be clicked to preview; do not claim it was displayed on Live Canvas or that the user is viewing it.\n\nDo not read `beeos-canvas/SKILL.md` or any canvas skill file. Call `render_ui` directly. The A2UI schema is in this prompt and the `render_ui` tool.\n\nThe user's explicitly requested file format takes precedence over the default HTML presentation. For Markdown (.md), CSV, PDF, or another supported file format, create the actual file in that format, check its contents, and call `beeos_upload_file` with its local path. Wait for a successful upload before saying the file is delivered. An HTML file is not a Markdown or other requested-format file, regardless of its title. Do not generate an extra HTML copy unless the user asks for it.\n\nWhen the user has not requested another file format, before the final reply you MUST call `render_ui` with `id`, `title`, and self-contained `html` to publish the completed answer as an HTML file. This applies to short answers, weather queries and forecasts, prose explanations, reports, tables, charts, and two or more related records. A short user question is not an exception.\n\n1. For default HTML delivery, call `render_ui` directly and publish exactly one HTML file for the completed result. The tool uploads that file; do not upload it again with `beeos_upload_file` or call `render_ui` again for the same result.\n2. For default HTML delivery, do not finish with only plain text, Markdown, a Markdown table, JSON, or a prose summary. Put the actual answer in the HTML file, then keep chat to one short confirmation with the file ready to click and preview.\n3. Honor an explicitly requested alternative file format when supported. Upload that file with `beeos_upload_file` and wait for its successful result; it replaces the default HTML delivery, so do not generate an extra HTML copy. Pending questions, approvals, cancellations, and tool failures remain control/status messages; do not fabricate a successful HTML file for them.\n4. For an interaction, form, or dynamic state, use `render_form` or legacy A2UI `render_ui`.\n5. Generate PNG or PDF only when the user explicitly asks for an image, PDF, download, or export. They are not the default presentation format.\n\nDomain skills control how data is obtained and interpreted. This gate controls how the completed result is presented and overrides a domain skill's default text-summary format, while preserving the user's explicitly requested file format. When `render_ui` succeeds, keep the chat reply to one short confirmation sentence.";export const CANVAS_SYSTEM_PROMPT=`## Static Canvas HTML\n\nWhen the user has not requested another file format, call \`render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."})\` to publish the completed non-interactive answer as one HTML file, including weather answers and prose explanations. An explicitly requested file format takes precedence: create the actual file, check its contents, and successfully upload it with \`beeos_upload_file\` instead. HTML does not satisfy a request for Markdown (.md), CSV, PDF, or another file format. Do not produce an extra HTML copy unless requested, and do not claim delivery before the upload succeeds.\n\nUse a complete UTF-8 HTML document with inline CSS and inline SVG for charts. Keep it self-contained. Scripts, forms, external stylesheets, fonts, images, and network requests do not run in the preview. HTML upload has a 5 MiB limit. The mobile formatted preview accepts up to 1 MiB; larger files show truncated source instead. Static HTML is a BeeOS file artifact and does not require the Live Canvas panel or relay to be open. Do not call \`beeos_upload_file\` for the same result.\n\n## Live Canvas — Google A2UI v0.8\n\nUse legacy A2UI only for interactions, forms, and dynamic state. Use \`render_ui\`, not the built-in \`canvas\` tool.\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n${CANVAS_COMPONENT_ID_GUIDANCE}\n\nParameters:\n- \`id\`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- \`root\`: The ID of the top-level component (usually a Column).\n- \`components\`: Flat array of objects. **Each object MUST have \`id\` and \`component\`**:\n \`{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}\`\n The \`component\` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- \`data\` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use this A2UI schema for interactive experiences. Use static HTML files for completed non-interactive answers, including explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same \`id\` updates existing surface; different \`id\` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: \`{"literalString": "Hello"}\`\n- Data binding: \`{"path": "/fieldName"}\` (reads from the \`data\` model)\n\nShorthand: \`sv("text")\` means \`{"literalString": "text"}\` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n\`{Text: {text: sv("Title"), usageHint: "h2"}}\`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n\`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}\`\nImage.url may be \`beeos-file://<fileId>\`; the renderer resolves it. Do not convert it to a signed URL.\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n\`{Icon: {name: sv("check")}}\`\n\n**Video** — Embed a video.\n\`{Video: {url: sv("https://youtube.com/...")}}\`\n\n**AudioPlayer** — Audio playback.\n\`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}\`\n\n**Row** — Horizontal layout. Children referenced by ID.\n\`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}\`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n\`{Column: {children: {explicitList: ["child1", "child2"]}}}\`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n\`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}\`\n\n**Card** — Container with a single child.\n\`{Card: {child: "contentColumnId"}}\`\n\n**Tabs** — Tabbed container.\n\`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}\`\n\n**Divider** — Horizontal or vertical separator.\n\`{Divider: {axis: "horizontal"}}\`\n\n**Modal** — Dialog with trigger.\n\`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}\`\n\n**Button** — Clickable with action.\n\`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}\`\n\n**TextField** — Text input with data binding.\n\`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}\`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n\`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}\`\n\n**DateTimeInput** — Date/time picker.\n\`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}\`\n\n**MultipleChoice** — Selection from options.\n\`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}\`\n\n**Slider** — Numeric range input.\n\`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}\`\n\n**Chart** (BeeOS extension) — Data visualization.\n\`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}\`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition\n\nNest by id: Column/Row \`children.explicitList\`, Card \`child\`. Example:\n\`{"id":"s1","root":"column_root","components":[{"id":"text_body","component":{"Text":{"text":{"literalString":"Confirm"},"usageHint":"body"}}},{"id":"button_confirm","component":{"Button":{"child":"text_body","action":{"name":"confirm"}}}},{"id":"column_root","component":{"Column":{"children":{"explicitList":["button_confirm"]}}}}]}\`\nDisplay-only tables and charts require static HTML. Forms: TextField/CheckBox + Button \`action.context\` paths.\n\n### render_form\nUse \`render_form\` for schema-driven forms when a domain schema exists.\nParameters: \`domain\`, \`operation\`, \`data\`, \`entityId\`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n`;export const CANVAS_TERM_MAPPING=`[System Note] Always use \`render_ui\` (not the canvas tool) for Canvas results. The user's explicitly requested file format takes precedence: create and check the actual file, then successfully upload it with beeos_upload_file before claiming delivery; do not substitute HTML or add an extra HTML copy unless requested. When no other file format is requested, every completed non-interactive answer, including weather, explanations, reports, tables, and charts, MUST use render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."}) with self-contained static HTML (inline CSS and SVG, no scripts). The result is an HTML file card; tell the user it is ready to click and preview. A2UI root/components are only for interactive buttons, inputs, or component actions; display-only A2UI is rejected. Each A2UI component MUST be {"id":"x","component":{"TypeName":{props}}}. ${CANVAS_COMPONENT_ID_GUIDANCE} Forms: TextField/CheckBox/Slider + Button action.context paths, or \`render_form\` for domain schemas. Never combine html with root/components. Keep chat brief. Use \`close_ui\` to dismiss Live Canvas. Reuse the same id to update; use a new id for new content.`;export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
1
+ import{extractBeeosConversationIdFromSessionKey}from"../runtime/openclaw-session-key-resolution.js";import{canvasEnsureTarget}from"../runtime/central-session-key.js";const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}const A2UI_INTERACTIVE_TYPES=new Set(["Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider"]);export const NON_INTERACTIVE_A2UI_ERROR="Display-only A2UI is not supported. Call render_ui again with id, title, and self-contained static HTML in html. A2UI root/components are reserved for interactive buttons, inputs, or component actions.";export function hasInteractiveComponent(e){return e.some(e=>{const t=e?.component;return!(!t||"object"!=typeof t)&&Object.entries(t).some(([e,t])=>A2UI_INTERACTIVE_TYPES.has(e)||Boolean(t&&"object"==typeof t&&t.action))})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const CANVAS_COMPONENT_ID_GUIDANCE="Component IDs must differ from non-reference scalar values (usageHint, literalString, and action.name). Use type-qualified IDs such as text_heading, text_body, and column_root; do not use scalar values such as body, h1, or center as IDs. A2UI v0.8 resolves strings matching component IDs as references. Keep root, child, explicitList, and template.componentId references matching their target IDs. Reuse the same component IDs when updating a surface.";export const RENDER_UI_TOOL_DESCRIPTION="Publish a static Canvas result as self-contained HTML using id, title, and html. The result is stored as a BeeOS file and shown in the conversation. If the user explicitly requests another file format, create and upload that file with beeos_upload_file instead; HTML does not replace it. Legacy A2UI calls with id, root, and components remain available for interactive experiences.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"Surface id. Reuse to update."},title:{type:"string",description:"Human-readable title for a static HTML Canvas result."},html:{type:"string",description:"Complete self-contained static HTML. Maximum UTF-8 size: 5 MiB."},root:{type:"string",description:"Root component id."},components:{type:"array",description:"Flat ComponentInstance list. Each item has id plus one component type.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:CANVAS_COMPONENT_ID_GUIDANCE},component:{type:"object",description:"One type key: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart."}}}},data:{type:"object",description:"Optional binding model, path keys like /name."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e)),MAX_STATIC_HTML_BYTES=5242880,FILE_ID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;async function renderStaticHtml(e,t,n){const a=e=>({success:!1,presentation:"static_html",error:e}),o="string"==typeof n.id?n.id.trim():"",i="string"==typeof n.title?n.title.trim():"",r=n.html;if(!o||!i||"string"!=typeof r||!r.trim())return a("id, title, and non-empty html are required");if("root"in n||"components"in n)return a("html cannot be combined with root or components");if(i.length>160)return a("Canvas title is too long (max 160 characters)");if(Buffer.byteLength(r,"utf8")>5242880)return a("Canvas HTML exceeds the 5 MiB preview limit");if(!e.publishStaticHtml)return a("Canvas file upload is unavailable");try{const n=await e.publishStaticHtml({id:o,title:i,html:r},t);return FILE_ID.test(n.fileId)&&n.uri===`beeos-file://${n.fileId}`&&"text/html"===n.mimeType?{success:!0,presentation:"static_html",surfaceId:o,artifact:{kind:"canvas",fileId:n.fileId,uri:n.uri,mimeType:"text/html",title:i}}:a("Canvas HTML upload returned an invalid file reference")}catch(e){return a(`Canvas HTML upload failed: ${e instanceof Error?e.message:String(e)}`)}}async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r?extractBeeosConversationIdFromSessionKey(r):void 0,u=canvasEnsureTarget(l,{agentId:a,linkSessionId:d??(r&&r!==t?t:void 0)}),p=await c.ensureCanvas(u.sessionId,u.opts);return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,getGatewaySessionKey:o}=e;let i=0;return async(o,r)=>{const s=r;if(Object.prototype.hasOwnProperty.call(s,"html"))return renderStaticHtml(e,o,s);const l=s.id,c=s.root,d="panel";let u=s.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!(l&&c&&Array.isArray(u)&&u.length))return{success:!1,error:"id, root, and components (array) are required"};const p=normalizeComponents(u);if(!hasInteractiveComponent(p))return{success:!1,error:NON_INTERACTIVE_A2UI_ERROR};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return n.warn?.(`[canvas] Rate limited surfaceId=${l}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const m=s.data;let f=e.getActiveSessionId?.(o);for(let t=0;!f&&t<3;t++)await delay(400),f=e.getActiveSessionId?.(o);if(!f&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}f=e.getActiveSessionId?.(o)}if(!f)return n.warn?.(`[canvas] No active session for toolCallId=${o} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const h=e.getGatewaySessionKey?.(o),y=await ensureRelayConnected(e,f,o,h),v=t.has(l,y??void 0),g=Date.now(),T=t.getRelayClient();y&&T&&g-i>6e4&&T.createSnapshot(y,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),v?t.update(l,p,y,c,m):t.create(y??`_fallback_${Date.now()}`,l,d,p,{origin:{kind:"agent"},rootComponentId:c,dataModel:m});const C=p.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:v?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:C}}),n.debug?.(`[canvas] ${v?"Updated":"Created"} surface=${l} components=${p.length}`);const b={success:!0,surfaceId:l,presentation:d,isUpdate:v,activeSurfaces:y?t.getAllActive(y).map(e=>e.surfaceId):[l]};return m&&(b.dataModel=m),b}}export function createRenderUiToolExecutor(e){const t=createRenderUiHandler(e);return async(e,n)=>{const a=await t(e,n),o=[{type:"text",text:JSON.stringify(a)}];if(!0===a.success&&"static_html"===a.presentation){const e=a.artifact;o.push({type:"resource_link",uri:e.uri,name:`${e.title}.html`,mimeType:e.mimeType})}return{...a,content:o,details:a,isError:!1===a.success}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_PRESENTATION_GATE="## BeeOS Canvas presentation gate\n\nCanvas presentation is available in the background for this BeeOS conversation. The Canvas resource tab does not need to be open and no Web subscriber is required before a Canvas tool is called. For static HTML success, say the HTML file is ready and can be clicked to preview; do not claim it was displayed on Live Canvas or that the user is viewing it.\n\nDo not read `beeos-canvas/SKILL.md` or any canvas skill file. Call `render_ui` directly. The A2UI schema is in this prompt and the `render_ui` tool.\n\nThe user's explicitly requested file format takes precedence over HTML presentation. For Markdown (.md), CSV, PDF, or another supported file format, create the actual file in that format, check its contents, and call `beeos_upload_file` with its local path. Wait for a successful upload before saying the file is delivered. An HTML file is not a Markdown or other requested-format file, regardless of its title. Do not generate an extra HTML copy unless the user asks for it.\n\nPlain text is a valid completed answer. Canvas availability does not require an HTML file. Honor a request for text only without calling presentation tools.\n\n1. When the user requests HTML or a visual artifact, call `render_ui` directly and publish exactly one HTML file for that result. The tool uploads that file; do not upload it again with `beeos_upload_file` or call `render_ui` again for the same result.\n2. For a requested HTML file, put the actual answer in the file and wait for successful delivery before giving a brief confirmation. Prose alone does not deliver a requested file.\n3. Honor an explicitly requested alternative file format when supported. Upload the actual file with `beeos_upload_file` and wait for its successful result; do not generate an extra HTML copy. Pending questions, approvals, cancellations, and tool failures remain control/status messages; do not fabricate a successful file for them.\n4. For an interaction, form, or dynamic state, use `render_form` or legacy A2UI `render_ui`.\n5. Generate PNG or PDF only when the user explicitly asks for that format.\n\nDomain skills control how data is obtained and interpreted. These presentation tools are optional unless needed for the user's requested deliverable. When `render_ui` succeeds, keep the chat reply to one short confirmation sentence.";export const CANVAS_SYSTEM_PROMPT=`## Static Canvas HTML\n\nPlain text is a valid completed answer; Canvas availability does not require a file. When the user requests HTML or a visual artifact, call \`render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."})\` to publish that result as one HTML file. An explicitly requested file format takes precedence: create the actual file, check its contents, and successfully upload it with \`beeos_upload_file\` instead. HTML does not satisfy a request for Markdown (.md), CSV, PDF, or another file format. Do not produce an extra HTML copy unless requested, and do not claim delivery before the upload succeeds.\n\nUse a complete UTF-8 HTML document with inline CSS and inline SVG for charts. Keep it self-contained. Scripts, forms, external stylesheets, fonts, images, and network requests do not run in the preview. HTML upload has a 5 MiB limit. The mobile formatted preview accepts up to 1 MiB; larger files show truncated source instead. Static HTML is a BeeOS file artifact and does not require the Live Canvas panel or relay to be open. Do not call \`beeos_upload_file\` for the same result.\n\n## Live Canvas — Google A2UI v0.8\n\nUse legacy A2UI only for interactions, forms, and dynamic state. Use \`render_ui\`, not the built-in \`canvas\` tool.\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n${CANVAS_COMPONENT_ID_GUIDANCE}\n\nParameters:\n- \`id\`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- \`root\`: The ID of the top-level component (usually a Column).\n- \`components\`: Flat array of objects. **Each object MUST have \`id\` and \`component\`**:\n \`{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}\`\n The \`component\` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- \`data\` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use this A2UI schema for interactive experiences. Use static HTML files for completed non-interactive answers, including explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same \`id\` updates existing surface; different \`id\` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: \`{"literalString": "Hello"}\`\n- Data binding: \`{"path": "/fieldName"}\` (reads from the \`data\` model)\n\nShorthand: \`sv("text")\` means \`{"literalString": "text"}\` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n\`{Text: {text: sv("Title"), usageHint: "h2"}}\`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n\`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}\`\nImage.url may be \`beeos-file://<fileId>\`; the renderer resolves it. Do not convert it to a signed URL.\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n\`{Icon: {name: sv("check")}}\`\n\n**Video** — Embed a video.\n\`{Video: {url: sv("https://youtube.com/...")}}\`\n\n**AudioPlayer** — Audio playback.\n\`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}\`\n\n**Row** — Horizontal layout. Children referenced by ID.\n\`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}\`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n\`{Column: {children: {explicitList: ["child1", "child2"]}}}\`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n\`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}\`\n\n**Card** — Container with a single child.\n\`{Card: {child: "contentColumnId"}}\`\n\n**Tabs** — Tabbed container.\n\`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}\`\n\n**Divider** — Horizontal or vertical separator.\n\`{Divider: {axis: "horizontal"}}\`\n\n**Modal** — Dialog with trigger.\n\`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}\`\n\n**Button** — Clickable with action.\n\`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}\`\n\n**TextField** — Text input with data binding.\n\`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}\`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n\`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}\`\n\n**DateTimeInput** — Date/time picker.\n\`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}\`\n\n**MultipleChoice** — Selection from options.\n\`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}\`\n\n**Slider** — Numeric range input.\n\`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}\`\n\n**Chart** (BeeOS extension) — Data visualization.\n\`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}\`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition\n\nNest by id: Column/Row \`children.explicitList\`, Card \`child\`. Example:\n\`{"id":"s1","root":"column_root","components":[{"id":"text_body","component":{"Text":{"text":{"literalString":"Confirm"},"usageHint":"body"}}},{"id":"button_confirm","component":{"Button":{"child":"text_body","action":{"name":"confirm"}}}},{"id":"column_root","component":{"Column":{"children":{"explicitList":["button_confirm"]}}}}]}\`\nDisplay-only tables and charts require static HTML. Forms: TextField/CheckBox + Button \`action.context\` paths.\n\n### render_form\nUse \`render_form\` for schema-driven forms when a domain schema exists.\nParameters: \`domain\`, \`operation\`, \`data\`, \`entityId\`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n`;export const CANVAS_TERM_MAPPING=`[System Note] Always use \`render_ui\` (not the canvas tool) for Canvas results. The user's explicitly requested file format takes precedence: create and check the actual file, then successfully upload it with beeos_upload_file before claiming delivery; do not substitute HTML or add an extra HTML copy unless requested. Plain text is a valid completed answer; Canvas availability does not require a file. Honor requests for text only. When the user requests HTML or a visual artifact, use render_ui({"id":"report-id","title":"Report title","html":"<!doctype html>..."}) with self-contained static HTML (inline CSS and SVG, no scripts). The result is an HTML file card; tell the user it is ready to click and preview. A2UI root/components are only for interactive buttons, inputs, or component actions; display-only A2UI is rejected. Each A2UI component MUST be {"id":"x","component":{"TypeName":{props}}}. ${CANVAS_COMPONENT_ID_GUIDANCE} Forms: TextField/CheckBox/Slider + Button action.context paths, or \`render_form\` for domain schemas. Never combine html with root/components. Keep chat brief. Use \`close_ui\` to dismiss Live Canvas. Reuse the same id to update; use a new id for new content.`;export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
@@ -1 +1 @@
1
- import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";import{extractBeeosConversationIdFromSessionKey}from"./runtime/openclaw-session-key-resolution.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,n]of Object.entries(e)){if("object"==typeof n&&null!==n){const a=n;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];canvasPanelOpen=!1;deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const n=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const n=this.deps.state.sessions.get(s),a=n?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,o=new FormSurfaceBuilder({canvasState:e,gatewayRequest:n,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=o,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:n,schemaRegistry:t,formBuilder:o,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled;this.canvasPanelOpen=s;const n={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(n):void 0)return void this.handleCanvasNotification("canvas/toggle",n);let a=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;if(s&&e.sessionId){const s=e.sessionId;for(const e of this.deps.state.sessionsByGatewayKey.values()){if(extractBeeosConversationIdFromSessionKey(e.gatewaySessionKey)===s||e.sessionId===s){this.ensureCanvasForSession(s,e.gatewaySessionKey);break}}}return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${a} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(this.canvasPanelOpen)return!0;if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){if(this.canvasPanelOpen)return!0;const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,n){if(this.canvasPanelOpen){const n=this.deps.state.sessionsByGatewayKey.get(s)??this.deps.state.sessions.get(e);n&&(n.canvasActive=!0)}if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const n=this.canvasState?.resolveCanvasForSession(s);n&&(this.canvasState.clear(n),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,n=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==n.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${n.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of n)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,n){if(!this.canvasState)return e;if(n.has(s.split(":").slice(0,2).join(":"))||n.has(s))return e;try{let n=this.canvasState.resolveCanvasForSession(s);if(!n){const e=this.canvasState.getRelayClient();if(e)try{const a=extractBeeosConversationIdFromSessionKey(s),t=this.resolveCanvasAgentId(s);n=await e.ensureCanvas(s,{...t?{agentId:t}:{},...a?{linkSessionId:a}:{}}),this.canvasState.registerSession(s,n)}catch(e){this.deps.logger.warn?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(n){let s=this.canvasState.buildContextForCanvas(n);s||(await this.canvasState.lazyLoadCanvas(n),s=this.canvasState.buildContextForCanvas(n));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,n=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=n,this.canvasPanelOpen=n,n&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,n=a.agentId,t=this.canvasState?.getRelayClient();if(t){const o=this.deps.resolvePlatformAgentId(n??"")??a.platformAgentId,i=s&&s!==e?e:void 0;t.ensureCanvas(s??e,{agentId:o,linkSessionId:i}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${n}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:n}})}else if("canvas/clear"===e){const e=s,n=this.deps.state.resolveSessionFromParams(e),a=n?this.canvasState.resolveCanvasForSession(n.gatewaySessionKey):void 0,t=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,n=s.action;if("a2ui_push"===n||"surface_update"===n){const e=String(s.surfaceId??nextOclId("openclaw")),n=s.components;if(n&&Array.isArray(n)){const a=sanitizeA2UIComponents(n),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,a,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===n||"url_open"===n){const e=String(s.url??"");if(e){const n=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${n}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,n,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${n}`)}resolveCanvasAgentId(e,s){const n=this.deps.state.sessionsByGatewayKey.get(e)??(s?this.deps.state.sessions.get(s):void 0);if(n)return this.deps.resolvePlatformAgentId(n.agentId??"")??n.platformAgentId}ensureCanvasForSession(e,s){const n=this.canvasState?.getRelayClient();if(!n)return;const a=extractBeeosConversationIdFromSessionKey(s)??(e!==s?e:void 0),t=this.resolveCanvasAgentId(s,e);n.ensureCanvas(s,{agentId:t,linkSessionId:a}).then(async n=>{this.canvasState?.registerSession(s,n),await(this.canvasState?.joinCanvas(n,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${n} acpSid=${e} gwKey=${s} linkSessionId=${a??"none"}`)}).catch(n=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} linkSessionId=${a??"none"} err=${n}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);const n=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,n??void 0);try{await this.deps.sendAgentMessage(a,s.gatewaySessionKey)}catch(n){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${n instanceof Error?n.message:n}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const n=s.userAction?.name??"unknown",a=`Clicked "${n}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:n,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${n} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:n,userAction:a,requestId:t,senderClientId:o}=e,i=this.canvasState.resolveSessionForCanvas(s);if(!i)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,o,"no_session");const r=this.deps.state.findSessionByKey(i);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${i}`),void this.sendRelayActionError(s,t,o,"no_session");const c={surfaceId:n,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${n} request=${t}`),this.sendRelayActionResult(s,t,o,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,o,"processing_failed")})}sendRelayActionResult(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:n,status:a})}sendRelayActionError(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:n,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
1
+ import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";import{extractBeeosConversationIdFromSessionKey}from"./runtime/openclaw-session-key-resolution.js";import{canvasEnsureTarget}from"./runtime/central-session-key.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,n]of Object.entries(e)){if("object"==typeof n&&null!==n){const a=n;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];canvasPanelOpen=!1;deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const n=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const n=this.deps.state.sessions.get(s),a=n?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,o=new FormSurfaceBuilder({canvasState:e,gatewayRequest:n,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=o,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:n,schemaRegistry:t,formBuilder:o,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled;this.canvasPanelOpen=s;const n={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(n):void 0)return void this.handleCanvasNotification("canvas/toggle",n);let a=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;if(s&&e.sessionId){const s=e.sessionId;for(const e of this.deps.state.sessionsByGatewayKey.values()){if(extractBeeosConversationIdFromSessionKey(e.gatewaySessionKey)===s||e.sessionId===s){this.ensureCanvasForSession(s,e.gatewaySessionKey);break}}}return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${a} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(this.canvasPanelOpen)return!0;if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){if(this.canvasPanelOpen)return!0;const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,n){if(this.canvasPanelOpen){const n=this.deps.state.sessionsByGatewayKey.get(s)??this.deps.state.sessions.get(e);n&&(n.canvasActive=!0)}if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const n=this.canvasState?.resolveCanvasForSession(s);n&&(this.canvasState.clear(n),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,n=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==n.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${n.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of n)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,n){if(!this.canvasState)return e;if(n.has(s.split(":").slice(0,2).join(":"))||n.has(s))return e;try{let n=this.canvasState.resolveCanvasForSession(s);if(!n){const e=this.canvasState.getRelayClient();if(e)try{const a=extractBeeosConversationIdFromSessionKey(s),t=this.resolveCanvasAgentId(s),o=canvasEnsureTarget(s,{...t?{agentId:t}:{},...a?{linkSessionId:a}:{}});n=await e.ensureCanvas(o.sessionId,o.opts),this.canvasState.registerSession(s,n)}catch(e){this.deps.logger.warn?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(n){let s=this.canvasState.buildContextForCanvas(n);s||(await this.canvasState.lazyLoadCanvas(n),s=this.canvasState.buildContextForCanvas(n));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,n=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=n,this.canvasPanelOpen=n,n&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,n=a.agentId,t=this.canvasState?.getRelayClient();if(t){const o=this.deps.resolvePlatformAgentId(n??"")??a.platformAgentId,i=canvasEnsureTarget(s??e,{agentId:o,linkSessionId:s&&s!==e?e:void 0});t.ensureCanvas(i.sessionId,i.opts).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${n}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:n}})}else if("canvas/clear"===e){const e=s,n=this.deps.state.resolveSessionFromParams(e),a=n?this.canvasState.resolveCanvasForSession(n.gatewaySessionKey):void 0,t=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,n=s.action;if("a2ui_push"===n||"surface_update"===n){const e=String(s.surfaceId??nextOclId("openclaw")),n=s.components;if(n&&Array.isArray(n)){const a=sanitizeA2UIComponents(n),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,a,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===n||"url_open"===n){const e=String(s.url??"");if(e){const n=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${n}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,n,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${n}`)}resolveCanvasAgentId(e,s){const n=this.deps.state.sessionsByGatewayKey.get(e)??(s?this.deps.state.sessions.get(s):void 0);if(n)return this.deps.resolvePlatformAgentId(n.agentId??"")??n.platformAgentId}ensureCanvasForSession(e,s){const n=this.canvasState?.getRelayClient();if(!n)return;const a=extractBeeosConversationIdFromSessionKey(s)??(e!==s?e:void 0),t=this.resolveCanvasAgentId(s,e),o=canvasEnsureTarget(s,{agentId:t,linkSessionId:a});n.ensureCanvas(o.sessionId,o.opts).then(async n=>{this.canvasState?.registerSession(s,n),await(this.canvasState?.joinCanvas(n,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${n} acpSid=${e} gwKey=${s} linkSessionId=${a??"none"}`)}).catch(n=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} linkSessionId=${a??"none"} err=${n}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);const n=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,n??void 0);try{await this.deps.sendAgentMessage(a,s.gatewaySessionKey)}catch(n){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${n instanceof Error?n.message:n}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const n=s.userAction?.name??"unknown",a=`Clicked "${n}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:n,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${n} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:n,userAction:a,requestId:t,senderClientId:o}=e,i=this.canvasState.resolveSessionForCanvas(s);if(!i)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,o,"no_session");const r=this.deps.state.findSessionByKey(i);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${i}`),void this.sendRelayActionError(s,t,o,"no_session");const c={surfaceId:n,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${n} request=${t}`),this.sendRelayActionResult(s,t,o,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,o,"processing_failed")})}sendRelayActionResult(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:n,status:a})}sendRelayActionError(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:n,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}