xworkmate-bridge/internal/acp/orchestrator.go
Haitao Pan c7b2f8ee3a fix(acp): treat numeric gateway codes as unknown-method so session.prepare falls back
A gateway that doesn't implement xworkmate.session.prepare returns an
"unknown method: xworkmate.session.prepare" error. isOpenClawUnknownMethodError
gated on a string code allowlist {"", INVALID_REQUEST, METHOD_NOT_FOUND}, but real
gateways send a numeric JSON-RPC code (e.g. -32002 / -32601) which shared.StringArg
stringifies to "-32002". The matcher then returned false, so the graceful fallback
(openClawFallbackSessionPreparePayload) never fired and every turn hard-failed with
"-32002: unknown method: xworkmate.session.prepare".

Match on the unambiguous message ("unknown method" + the method name) instead of the
stringified numeric code. Add a regression test covering numeric codes and guarding
against swallowing unrelated errors / other method names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:04:08 +08:00

2304 lines
74 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package acp
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"xworkmate-bridge/internal/gatewayruntime"
"xworkmate-bridge/internal/memory"
"xworkmate-bridge/internal/router"
"xworkmate-bridge/internal/shared"
)
type SessionOrchestrator struct {
server *Server
}
const (
openClawInlineAttachmentMaxFileBytes = 10 * 1024 * 1024
openClawInlineAttachmentMaxTotalBytes = 25 * 1024 * 1024
)
const (
openClawAgentWaitDefaultTimeout = 6 * time.Minute
openClawAgentWaitMaxTimeout = time.Hour
openClawAgentWaitHTTPMargin = time.Minute
openClawNoDisplayableText = "OpenClaw completed without displayable output."
openClawRequiredArtifactMissingText = "OpenClaw completed without required final artifacts."
openClawArtifactExportAttemptedField = "_openClawArtifactExportAttempted"
)
func NewSessionOrchestrator(server *Server) *SessionOrchestrator {
return &SessionOrchestrator{server: server}
}
func (o *SessionOrchestrator) Process(ctx context.Context, method string, params map[string]any, notify func(map[string]any)) (map[string]any, *shared.RPCError) {
res, err := o.server.routingEngine.Resolve(ctx, params)
if err != nil {
if err.Error() == "ROUTING_REQUIRED" {
return nil, &shared.RPCError{Code: -32602, Message: err.Error()}
}
return nil, &shared.RPCError{Code: -32602, Message: "ROUTING_FAILED: " + err.Error()}
}
if res.Status == "unavailable" {
return o.formatUnavailable(res), nil
}
sessionID := shared.StringArg(params, "sessionId", "")
threadID := shared.StringArg(params, "threadId", sessionID)
turnID := fmt.Sprintf("turn-%d", time.Now().UnixNano())
sess := o.server.getOrCreateSession(sessionID, threadID)
sess.mu.Lock()
sess.target = res.TargetID
sess.provider = res.ProviderID
sess.mode = res.TargetID
sess.control.ControlPlaneSessionID = sessionID
sess.control.ThreadID = threadID
sess.control.RequestedWorkingDir = strings.TrimSpace(shared.StringArg(params, "workingDirectory", ""))
sess.control.RemoteWorkingDirHint = strings.TrimSpace(shared.StringArg(params, "remoteWorkingDirectoryHint", ""))
sess.control.UpdatedAt = time.Now()
sess.task = QueuedTask{
SessionID: sessionID,
ThreadID: threadID,
TurnID: turnID,
Provider: res.ProviderID,
Target: res.TargetID,
State: TaskStateRunning,
Kind: taskKindFromParams(params, res),
UpdatedAt: time.Now(),
}
prompt := strings.TrimSpace(shared.StringArg(params, "taskPrompt", ""))
if prompt != "" {
sess.history = append(sess.history, "USER: "+prompt)
}
sess.mu.Unlock()
o.server.emitSessionUpdate(notify, turnID, map[string]any{
"type": "status",
"event": "started",
"message": "session started",
"pending": true,
"error": false,
})
if res.TargetID == "gateway" {
result, rpcErr := o.runGateway(ctx, method, params, res, turnID, notify)
if rpcErr != nil {
failed := o.normalizeGatewayFailureResult(sess, rpcErr, res, turnID)
if (isOpenClawProvider(res.GatewayProviderID) || isOpenClawProvider(res.ProviderID)) &&
!openClawGatewayBusyError(rpcErr) {
o.server.emitSessionUpdate(notify, turnID, openClawGatewayCompletedResultUpdate(sessionID, threadID, turnID, failed))
}
return nil, rpcErr
}
return o.normalizeResult(sess, result, res, turnID, params), nil
}
compat, ok := o.server.providers[res.ProviderID]
if !ok {
return nil, &shared.RPCError{Code: -32001, Message: "PROVIDER_NOT_FOUND: " + res.ProviderID}
}
sess.mu.Lock()
sess.compat = compat
sess.mu.Unlock()
sink := func(update map[string]any) {
o.server.emitSessionUpdate(notify, turnID, update)
}
var result map[string]any
switch method {
case "session.start":
result, err = compat.StartSession(ctx, sessionID, threadID, params, sink)
case "session.message":
result, err = compat.SendMessage(ctx, sessionID, threadID, params, sink)
default:
err = fmt.Errorf("unsupported session method: %s", method)
}
if err != nil {
sess.mu.Lock()
sess.task.State = TaskStateFailed
sess.task.UpdatedAt = time.Now()
sess.mu.Unlock()
if continuationErr, ok := asSessionContinuationUnavailableError(err); ok {
return nil, sessionContinuationUnavailableRPCError(continuationErr)
}
return nil, &shared.RPCError{Code: -32002, Message: "EXECUTION_FAILED: " + err.Error()}
}
return o.normalizeResult(sess, result, res, turnID, params), nil
}
func openClawGatewayBusyError(rpcErr *shared.RPCError) bool {
if rpcErr == nil {
return false
}
data := shared.AsMap(rpcErr.Data)
return strings.TrimSpace(shared.StringArg(data, "code", "")) == "OPENCLAW_GATEWAY_BUSY"
}
func (o *SessionOrchestrator) normalizeGatewayFailureResult(
sess *session,
rpcErr *shared.RPCError,
routing RoutingResult,
turnID string,
) map[string]any {
message := "gateway execution failed"
code := "GATEWAY_EXECUTION_FAILED"
if rpcErr != nil {
if strings.TrimSpace(rpcErr.Message) != "" {
message = strings.TrimSpace(rpcErr.Message)
}
data := shared.AsMap(rpcErr.Data)
if dataCode := strings.TrimSpace(shared.StringArg(data, "code", "")); dataCode != "" {
code = dataCode
}
}
result := map[string]any{
"success": false,
"status": "failed",
"code": code,
"error": message,
"message": message,
"summary": message,
"output": message,
"turnId": turnID,
"mode": router.ExecutionTargetGatewayChat,
"resolvedExecutionTarget": routing.TargetID,
"resolvedProviderId": routing.ProviderID,
"resolvedGatewayProviderId": routing.GatewayProviderID,
"resolvedModel": routing.Model,
"resolvedSkills": append([]string(nil), routing.Skills...),
}
artifactRecord := buildArtifactRecord(sess, result, message)
if artifactRecord.RemoteWorkingDirectory != "" {
result["remoteWorkingDirectory"] = artifactRecord.RemoteWorkingDirectory
}
if artifactRecord.RemoteWorkspaceRefKind != "" {
result["remoteWorkspaceRefKind"] = artifactRecord.RemoteWorkspaceRefKind
}
if artifactRecord.ResultSummary != "" && strings.TrimSpace(shared.StringArg(result, "resultSummary", "")) == "" {
result["resultSummary"] = artifactRecord.ResultSummary
}
sess.mu.Lock()
sess.task.State = TaskStateFailed
sess.task.UpdatedAt = time.Now()
sess.lastResult = cloneMap(result)
sess.artifacts = artifactRecord
sess.mu.Unlock()
return result
}
func (o *SessionOrchestrator) runGateway(
ctx context.Context,
method string,
params map[string]any,
routing RoutingResult,
turnID string,
notify func(map[string]any),
) (map[string]any, *shared.RPCError) {
if o.server.gateway == nil {
return nil, &shared.RPCError{Code: -32001, Message: "GATEWAY_NOT_INITIALIZED"}
}
gatewayProvider := resolvedGatewayProviderID(params, routing)
if gatewayProvider == "" {
return nil, &shared.RPCError{Code: -32602, Message: "GATEWAY_PROVIDER_REQUIRED"}
}
params = withResolvedGatewayProvider(params, gatewayProvider)
if isOpenClawMode(gatewayProvider) && isSessionTaskMethod(method) {
sessionID := strings.TrimSpace(shared.StringArg(params, "sessionId", ""))
threadID := strings.TrimSpace(shared.StringArg(params, "threadId", sessionID))
release, rpcErr := o.server.openClawGate.acquire(
ctx,
func(position int, queued int) {
o.server.emitSessionUpdate(notify, turnID, map[string]any{
"sessionId": sessionID,
"threadId": threadID,
"type": "status",
"event": "queued",
"message": "OpenClaw gateway task queued",
"pending": true,
"error": false,
"queue": map[string]any{
"position": position,
"queued": queued,
},
})
},
func(active int) {
o.server.emitSessionUpdate(notify, turnID, map[string]any{
"sessionId": sessionID,
"threadId": threadID,
"type": "status",
"event": "running",
"message": "OpenClaw gateway task admitted",
"pending": true,
"error": false,
"queue": map[string]any{
"active": active,
},
})
},
)
if rpcErr != nil {
return nil, rpcErr
}
if rpcErr := ensureProductionGatewayConnected(o.server, gatewayProvider, notify); rpcErr != nil {
release()
return nil, rpcErr
}
result, rpcErr := o.startOpenClawGatewayTask(ctx, params, routing, gatewayProvider, turnID, release, notify)
if rpcErr != nil {
release()
return nil, rpcErr
}
return result, nil
}
if rpcErr := ensureProductionGatewayConnected(o.server, gatewayProvider, notify); rpcErr != nil {
return nil, rpcErr
}
result := o.server.gateway.RequestByMode(
gatewayProvider,
method,
params,
2*time.Minute,
notify,
)
if !result.OK {
errMessage := strings.TrimSpace(shared.StringArg(result.Error, "message", "gateway execution failed"))
return nil, &shared.RPCError{Code: -32002, Message: errMessage}
}
payload := shared.AsMap(result.Payload)
if len(payload) == 0 {
payload = map[string]any{
"success": true,
"turnId": turnID,
"mode": router.ExecutionTargetGatewayChat,
}
}
if _, ok := payload["turnId"]; !ok {
payload["turnId"] = turnID
}
if _, ok := payload["mode"]; !ok {
payload["mode"] = router.ExecutionTargetGatewayChat
}
return payload, nil
}
func (o *SessionOrchestrator) startOpenClawGatewayTask(
_ context.Context,
params map[string]any,
routing RoutingResult,
gatewayProvider string,
turnID string,
releaseAdmission func(),
notify func(map[string]any),
) (map[string]any, *shared.RPCError) {
sessionID := strings.TrimSpace(shared.StringArg(params, "sessionId", ""))
threadID := strings.TrimSpace(shared.StringArg(params, "threadId", sessionID))
if sessionID == "" {
sessionID = threadID
}
notifyWithCollection := func(message map[string]any) {
if notify == nil {
return
}
if update := openClawGatewaySessionUpdate(message, sessionID, threadID, turnID); update != nil {
notify(update)
}
}
sessionKey := o.openClawSessionKey(params, turnID)
params = withOpenClawWritableWorkspace(params, openClawAppThreadKey(params))
artifactContract := openClawArtifactContractForParams(params, nil)
preparedArtifact, prepareErr := o.openClawArtifactPrepare(
gatewayProvider,
params,
sessionKey,
turnID,
artifactContract,
notifyWithCollection,
)
if prepareErr != nil {
return nil, prepareErr
}
logOpenClawArtifactSync(gatewayProvider, sessionKey, turnID, "prepare", true, false, false)
params = withOpenClawPreparedArtifactWorkspace(params, preparedArtifact)
chatParams, rpcErr := openClawChatSendParamsWithSessionKey(params, turnID, sessionKey)
if rpcErr != nil {
return nil, rpcErr
}
applyOpenClawPreparedArtifactToChatParams(chatParams, preparedArtifact, sessionKey, turnID, artifactContract)
chatSendTimeout := openClawAgentWaitTimeout(params, chatParams)
sendStarted := time.Now()
sendResult := o.openClawGatewayRequestWithRetry(
gatewayProvider,
"chat.send",
chatParams,
chatSendTimeout,
notifyWithCollection,
)
logOpenClawGatewayTiming(
gatewayProvider,
"chat.send",
sessionKey,
turnID,
time.Since(sendStarted),
sendResult.OK,
)
if !sendResult.OK {
return nil, gatewayRPCError(sendResult.Error, "openclaw chat.send failed")
}
sendPayload := shared.AsMap(sendResult.Payload)
if rpcErr := validateOpenClawAcceptedSessionKey(sendPayload, sessionKey); rpcErr != nil {
return nil, rpcErr
}
runID := strings.TrimSpace(shared.StringArg(sendPayload, "runId", turnID))
if runID != turnID {
preparedArtifact, prepareErr = o.openClawArtifactPrepare(
gatewayProvider,
params,
sessionKey,
runID,
artifactContract,
notifyWithCollection,
)
if prepareErr != nil {
return nil, prepareErr
}
logOpenClawArtifactSync(gatewayProvider, sessionKey, runID, "prepare", true, false, false)
applyOpenClawPreparedArtifactToChatParams(chatParams, preparedArtifact, sessionKey, runID, artifactContract)
}
taskLoadClass, runtimeBudgetMinutes := openClawTaskRuntimePolicy(params, chatParams, artifactContract)
startedAt := time.Now()
record := &OpenClawTaskRecord{
SessionID: sessionID,
ThreadID: threadID,
TurnID: turnID,
RunID: runID,
SessionKey: sessionKey,
GatewayProviderID: gatewayProvider,
TaskLoadClass: taskLoadClass,
RuntimeBudgetMinutes: runtimeBudgetMinutes,
StartedAt: startedAt,
DeadlineAt: startedAt.Add(time.Duration(runtimeBudgetMinutes) * time.Minute),
ProgressStage: "running",
ProgressMessage: "OpenClaw task accepted",
PreparedArtifact: preparedArtifact,
RequiresArtifactExport: artifactContract.RequiresArtifactExport,
ExpectedArtifactDirs: append([]string(nil), artifactContract.ExpectedArtifactDirs...),
RequiredArtifactExts: append([]string(nil), artifactContract.RequiredArtifactExts...),
ResolvedModel: routing.Model,
ResolvedSkills: append([]string(nil), routing.Skills...),
}
sess := o.server.getOrCreateSession(sessionID, threadID)
sess.mu.Lock()
sess.task.RunID = runID
sess.task.SessionKey = sessionKey
sess.task.GatewayProviderID = gatewayProvider
sess.task.TaskLoadClass = taskLoadClass
sess.task.ArtifactScope = strings.TrimSpace(preparedArtifact.ArtifactScope)
sess.task.ArtifactDirectory = strings.TrimSpace(preparedArtifact.ArtifactDirectory)
sess.task.RuntimeBudgetMinutes = runtimeBudgetMinutes
sess.task.StartedAt = startedAt
sess.task.DeadlineAt = record.DeadlineAt
sess.task.ProgressStage = "running"
sess.task.ProgressMessage = "OpenClaw task accepted"
sess.openClaw = record
running := openClawRunningTaskResult(record)
sess.lastResult = cloneMap(running)
sess.mu.Unlock()
if releaseAdmission != nil {
releaseAdmission()
}
if notify != nil {
notify(shared.NotificationEnvelope("session.update", map[string]any{
"sessionId": sessionID,
"threadId": threadID,
"turnId": turnID,
"runId": runID,
"type": "status",
"event": "running",
"message": "OpenClaw task accepted",
"pending": true,
"error": false,
"status": string(TaskStateRunning),
"runtimeBudgetMinutes": runtimeBudgetMinutes,
"progress": running["progress"],
}))
}
return running, nil
}
func openClawGatewayCompletedResultUpdate(sessionID string, threadID string, turnID string, result map[string]any) map[string]any {
success := true
if value, ok := result["success"].(bool); ok {
success = value
}
update := map[string]any{
"sessionId": sessionID,
"threadId": threadID,
"turnId": turnID,
"type": "status",
"event": "completed",
"pending": false,
"error": !success,
"result": result,
}
if output := firstNonEmptyString(result, "output", "message", "summary", "text"); output != "" {
update["message"] = output
update["text"] = output
}
for _, key := range []string{
"success",
"status",
"code",
"artifacts",
"files",
"attachments",
"artifactWarnings",
"remoteWorkingDirectory",
"remoteWorkspaceRefKind",
"resolvedGatewayProviderId",
"mode",
"runId",
} {
if value, ok := result[key]; ok {
update[key] = value
}
}
return update
}
func logOpenClawGatewayTiming(
gatewayProvider string,
method string,
sessionKey string,
runID string,
duration time.Duration,
ok bool,
) {
log.Printf(
"level=info component=openclaw_gateway event=request_timing provider=%q method=%q sessionId=%q runId=%q durationMs=%d ok=%t",
gatewayProvider,
method,
sessionKey,
runID,
duration.Milliseconds(),
ok,
)
}
func logOpenClawArtifactSync(
gatewayProvider string,
sessionKey string,
runID string,
stage string,
prepared bool,
exported bool,
empty bool,
) {
log.Printf(
"level=info component=openclaw_gateway event=artifact_sync provider=%q sessionId=%q runId=%q stage=%q prepared=%t exported=%t empty=%t",
gatewayProvider,
sessionKey,
runID,
stage,
prepared,
exported,
empty,
)
}
func isSessionTaskMethod(method string) bool {
switch strings.TrimSpace(method) {
case "session.start", "session.message":
return true
default:
return false
}
}
type openClawPreparedArtifactScope struct {
RemoteWorkingDirectory string
RemoteWorkspaceRefKind string
ArtifactScope string
ArtifactDirectory string
RelativeArtifactDirectory string
ScopeKind string
}
func openClawPreparedArtifactScopeFromPayload(payload map[string]any) *openClawPreparedArtifactScope {
if payload == nil {
return nil
}
prepared := &openClawPreparedArtifactScope{
RemoteWorkingDirectory: strings.TrimSpace(shared.StringArg(payload, "remoteWorkingDirectory", "")),
RemoteWorkspaceRefKind: strings.TrimSpace(shared.StringArg(payload, "remoteWorkspaceRefKind", "")),
ArtifactScope: strings.TrimSpace(shared.StringArg(payload, "artifactScope", "")),
ArtifactDirectory: strings.TrimSpace(shared.StringArg(payload, "artifactDirectory", "")),
RelativeArtifactDirectory: strings.TrimSpace(shared.StringArg(payload, "relativeArtifactDirectory", "")),
ScopeKind: strings.TrimSpace(shared.StringArg(payload, "scopeKind", "")),
}
if prepared.ArtifactScope == "" || prepared.ArtifactDirectory == "" {
return nil
}
if prepared.ScopeKind == "" {
prepared.ScopeKind = "task"
}
return prepared
}
func (o *SessionOrchestrator) openClawArtifactPrepare(
gatewayProvider string,
params map[string]any,
sessionKey string,
runID string,
artifactContract openClawArtifactContract,
notify func(map[string]any),
) (*openClawPreparedArtifactScope, *shared.RPCError) {
sessionKey = strings.TrimSpace(sessionKey)
runID = strings.TrimSpace(runID)
if sessionKey == "" || runID == "" {
log.Printf(
"level=warn component=openclaw_gateway event=artifact_prepare_missing_context provider=%q hasOpenClawSessionKey=%t hasRunId=%t appThreadKey=%q",
gatewayProvider,
sessionKey != "",
runID != "",
openClawAppThreadKey(params),
)
return nil, &shared.RPCError{Code: -32602, Message: "openclaw artifact prepare requires openclawSessionKey and runId"}
}
prepareParams := openClawSessionPrepareParams(params, sessionKey, runID, artifactContract)
log.Printf(
"level=info component=openclaw_gateway event=artifact_prepare_context provider=%q hasOpenClawSessionKey=%t hasRunId=%t hasWorkspaceDir=%t expectedArtifactDirs=%d",
gatewayProvider,
strings.TrimSpace(shared.StringArg(prepareParams, "openclawSessionKey", "")) != "",
strings.TrimSpace(shared.StringArg(prepareParams, "runId", "")) != "",
strings.TrimSpace(shared.StringArg(prepareParams, "workspaceDir", "")) != "",
len(shared.ListArg(prepareParams, "expectedArtifactDirs")),
)
prepareResult := o.openClawGatewayRequestWithRetry(
gatewayProvider,
"xworkmate.session.prepare",
prepareParams,
30*time.Second,
notify,
)
if !prepareResult.OK {
if isOpenClawUnknownMethodError(prepareResult.Error, "xworkmate.session.prepare") {
return openClawPreparedArtifactScopeFromPayload(openClawFallbackSessionPreparePayload(prepareParams)), nil
}
return nil, gatewayRPCError(prepareResult.Error, "openclaw artifact prepare failed")
}
prepared := openClawPreparedArtifactScopeFromPayload(shared.AsMap(prepareResult.Payload))
if prepared == nil {
return nil, &shared.RPCError{Code: -32002, Message: "openclaw artifact prepare returned no scoped artifact directory"}
}
return prepared, nil
}
func isOpenClawUnknownMethodError(errorPayload map[string]any, method string) bool {
message := strings.ToLower(strings.TrimSpace(shared.StringArg(errorPayload, "message", "")))
if message == "" {
return false
}
// 消息形如「unknown method: <method>」已明确指向「网关不认识该方法」,足以判定,
// 据此走 graceful fallback如 openClawFallbackSessionPreparePayload
//
// 注意:不能再用严格的 code 白名单来 gate。真实网关常以数字 JSON-RPC code
// (-32601 method not found / -32600 invalid request / -32002 等) 回传,
// 经 shared.StringArg(fmt.Sprint) 会被字符串化为 "-32601"/"-32002"
// 旧实现只接受 {"", INVALID_REQUEST, METHOD_NOT_FOUND},导致 fallback 失效、
// session.prepare 直接以 -32002 硬失败整轮任务。
return strings.Contains(message, "unknown method") &&
strings.Contains(message, strings.ToLower(strings.TrimSpace(method)))
}
func openClawFallbackSessionPreparePayload(params map[string]any) map[string]any {
sessionKey := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", ""))
if sessionKey == "" {
sessionKey = strings.TrimSpace(shared.StringArg(params, "sessionKey", ""))
}
if sessionKey == "" {
sessionKey = "main"
}
runID := strings.TrimSpace(shared.StringArg(params, "runId", ""))
if runID == "" {
runID = strings.TrimSpace(shared.StringArg(params, "taskId", ""))
}
if runID == "" {
runID = strings.TrimSpace(shared.StringArg(params, "requestId", ""))
}
if runID == "" {
runID = "default"
}
relativeArtifactDirectory := filepath.Join("tasks", sessionKey, runID)
workspaceDir := openClawArtifactWorkspaceDir(params)
artifactDirectory := filepath.Join(workspaceDir, relativeArtifactDirectory)
return map[string]any{
"ok": true,
"fallback": true,
"compatibilityMode": "local-session-prepare",
"runId": runID,
"sessionKey": sessionKey,
"openclawSessionKey": sessionKey,
"remoteWorkingDirectory": workspaceDir,
"remoteWorkspaceRefKind": "path",
"artifactScope": relativeArtifactDirectory,
"artifactDirectory": artifactDirectory,
"relativeArtifactDirectory": relativeArtifactDirectory,
"scopeKind": "task",
}
}
func openClawSessionPrepareParams(params map[string]any, openClawSessionKey string, runID string, artifactContract openClawArtifactContract) map[string]any {
appThreadKey := openClawAppThreadKey(params)
result := map[string]any{
"schemaVersion": 1,
"appThreadKey": appThreadKey,
"openclawSessionKey": strings.TrimSpace(openClawSessionKey),
"runId": strings.TrimSpace(runID),
"requestId": strings.TrimSpace(runID),
"externalTaskId": strings.TrimSpace(runID),
}
if len(artifactContract.ExpectedArtifactDirs) > 0 {
result["expectedArtifactDirs"] = append([]string(nil), artifactContract.ExpectedArtifactDirs...)
}
if artifactContract.RequiresArtifactExport {
result["requiresArtifactExport"] = true
}
if len(artifactContract.RequiredArtifactExts) > 0 {
result["requiredArtifactExtensions"] = append([]string(nil), artifactContract.RequiredArtifactExts...)
}
if workspaceDir := openClawArtifactWorkspaceDir(params); workspaceDir != "" {
result["workspaceDir"] = workspaceDir
}
return result
}
func openClawArtifactWorkspaceDir(params map[string]any) string {
if value := strings.TrimSpace(shared.StringArg(params, "workspaceDir", "")); value != "" {
return value
}
for _, key := range []string{"remoteWorkingDirectoryHint", "remoteWorkingDirectory", "workingDirectory"} {
if value := strings.TrimSpace(shared.StringArg(params, key, "")); isOpenClawWorkspacePath(value) {
return value
}
}
if configured := strings.TrimSpace(os.Getenv("OPENCLAW_WORKSPACE_DIR")); configured != "" {
return configured
}
return "~/.openclaw/workspace"
}
func isOpenClawWorkspacePath(path string) bool {
path = strings.TrimSpace(path)
if path == "" {
return false
}
normalized := filepath.ToSlash(filepath.Clean(path))
return strings.HasPrefix(normalized, "/home/ubuntu/.openclaw/workspace") ||
strings.HasPrefix(normalized, "/Users/") && strings.Contains(normalized, "/.openclaw/workspace") ||
strings.HasPrefix(normalized, "~/.openclaw/workspace") ||
strings.HasPrefix(normalized, "$HOME/.openclaw/workspace")
}
func openClawAppThreadKey(params map[string]any) string {
if value := strings.TrimSpace(shared.StringArg(params, "appThreadKey", "")); value != "" {
return value
}
metadata := shared.AsMap(params["metadata"])
for _, key := range []string{"appThreadKey"} {
if value := strings.TrimSpace(shared.StringArg(metadata, key, "")); value != "" {
return value
}
}
contract := shared.AsMap(metadata["xworkmateTaskArtifactContract"])
if value := strings.TrimSpace(shared.StringArg(contract, "appThreadKey", "")); value != "" {
return value
}
for _, key := range []string{"threadId", "sessionId"} {
if value := strings.TrimSpace(shared.StringArg(params, key, "")); value != "" {
return value
}
}
return "main"
}
func applyOpenClawPreparedArtifactToResult(result map[string]any, prepared *openClawPreparedArtifactScope) {
if result == nil || prepared == nil {
return
}
if strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", "")) == "" && prepared.RemoteWorkingDirectory != "" {
result["remoteWorkingDirectory"] = prepared.RemoteWorkingDirectory
}
if strings.TrimSpace(shared.StringArg(result, "remoteWorkspaceRefKind", "")) == "" && prepared.RemoteWorkspaceRefKind != "" {
result["remoteWorkspaceRefKind"] = prepared.RemoteWorkspaceRefKind
}
if strings.TrimSpace(shared.StringArg(result, "artifactScope", "")) == "" {
result["artifactScope"] = prepared.ArtifactScope
}
if strings.TrimSpace(shared.StringArg(result, "artifactDirectory", "")) == "" {
result["artifactDirectory"] = prepared.ArtifactDirectory
}
if strings.TrimSpace(shared.StringArg(result, "relativeArtifactDirectory", "")) == "" && prepared.RelativeArtifactDirectory != "" {
result["relativeArtifactDirectory"] = prepared.RelativeArtifactDirectory
}
if strings.TrimSpace(shared.StringArg(result, "scopeKind", "")) == "" {
result["scopeKind"] = prepared.ScopeKind
}
}
func applyOpenClawPreparedArtifactToChatParams(
chatParams map[string]any,
prepared *openClawPreparedArtifactScope,
sessionKey string,
runID string,
contract openClawArtifactContract,
) {
if chatParams == nil || prepared == nil || strings.TrimSpace(prepared.ArtifactDirectory) == "" {
return
}
receipt := openClawArtifactSystemProvenanceReceipt(prepared, sessionKey, runID, contract)
if receipt == "" {
return
}
existing := strings.TrimSpace(shared.StringArg(chatParams, "systemProvenanceReceipt", ""))
if existing != "" {
chatParams["systemProvenanceReceipt"] = existing + "\n\n" + receipt
return
}
chatParams["systemProvenanceReceipt"] = receipt
}
func openClawArtifactSystemProvenanceReceipt(
prepared *openClawPreparedArtifactScope,
sessionKey string,
runID string,
contract openClawArtifactContract,
) string {
if prepared == nil {
return ""
}
artifactDirectory := strings.TrimSpace(prepared.ArtifactDirectory)
artifactScope := strings.TrimSpace(prepared.ArtifactScope)
if artifactDirectory == "" || artifactScope == "" {
return ""
}
lines := []string{
"XWorkmate task artifact context:",
"- Treat artifactDirectory as the working directory for all files generated in this turn.",
"- Write final artifacts directly under artifactDirectory using relative paths such as assets/images/... or prompts/....",
"- Do not create a nested task_artifacts/<session> directory inside artifactDirectory.",
"- Environment contract for shell commands:",
" export XWORKMATE_TASK_ARTIFACT_DIR=" + shellSingleQuote(artifactDirectory),
" export XWORKMATE_ARTIFACT_DIRECTORY=" + shellSingleQuote(artifactDirectory),
" export XWORKMATE_ARTIFACT_SCOPE=" + shellSingleQuote(artifactScope),
" export XWORKMATE_SESSION_KEY=" + shellSingleQuote(strings.TrimSpace(sessionKey)),
" export XWORKMATE_RUN_ID=" + shellSingleQuote(strings.TrimSpace(runID)),
" cd " + shellSingleQuote(artifactDirectory),
"artifactDirectory: " + artifactDirectory,
"artifactScope: " + artifactScope,
}
if relative := strings.TrimSpace(prepared.RelativeArtifactDirectory); relative != "" {
lines = append(lines, "relativeArtifactDirectory: "+relative)
}
if remote := strings.TrimSpace(prepared.RemoteWorkingDirectory); remote != "" {
lines = append(lines, "remoteWorkingDirectory: "+remote)
}
if contract.ComplexLongChain {
lines = append(lines,
"",
"Complex XWorkmate artifact contract:",
"- Persist the stage plan, intermediate outputs, generated assets, final manifest, and final deliverables inside artifactDirectory.",
"- Use clear relative paths that match the task structure instead of global cache or temporary directories.",
"- Do not report completion until requested final deliverables are present in artifactDirectory.",
)
}
return strings.Join(lines, "\n")
}
func shellSingleQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
type openClawArtifactContract struct {
TaskLoadClass string
ComplexLongChain bool
RequiresArtifactExport bool
ExpectedArtifactDirs []string
RequiredArtifactExts []string
ExpectedFileCounts map[string]int
SourceMessage string
}
func openClawArtifactContractForParams(params map[string]any, chatParams map[string]any) openClawArtifactContract {
metadata := shared.AsMap(params["metadata"])
taskLoadClass := strings.TrimSpace(shared.StringArg(metadata, "taskLoadClass", ""))
message := strings.TrimSpace(shared.StringArg(chatParams, "message", ""))
if message == "" {
message = openClawCurrentTurnMessage(params)
}
lowerMessage := strings.ToLower(message)
contract := shared.AsMap(metadata["xworkmateTaskArtifactContract"])
expectedDirs := normalizeOpenClawDirList(shared.ListArg(contract, "expectedArtifactDirs"))
requiresExport := parseBool(contract["requiresExportBeforeFinalResponse"]) || len(expectedDirs) > 0
complex := taskLoadClass == "complex_long_chain_task" || isOpenClawLongArtifactTask(lowerMessage)
requiredExts := normalizeOpenClawArtifactExtList(shared.ListArg(metadata, "requiredArtifactExtensions"))
if len(requiredExts) == 0 {
requiredExts = normalizeOpenClawArtifactExtList(shared.ListArg(metadata, "expectedArtifactExtensions"))
}
if len(requiredExts) == 0 {
requiredExts = inferOpenClawRequiredArtifactExts(lowerMessage)
}
expectedFileCounts := normalizeOpenClawArtifactExtCountMap(shared.AsMap(contract["expectedFileCountByExtension"]))
if len(expectedFileCounts) == 0 {
expectedFileCounts = normalizeOpenClawArtifactExtCountMap(shared.AsMap(metadata["expectedFileCountByExtension"]))
}
if len(expectedFileCounts) == 0 {
expectedFileCounts = normalizeOpenClawArtifactExtCountMap(shared.AsMap(shared.AsMap(metadata["xworkmateArtifactConstraints"])["expectedFileCountByExtension"]))
}
return openClawArtifactContract{
TaskLoadClass: taskLoadClass,
ComplexLongChain: complex,
RequiresArtifactExport: requiresExport,
ExpectedArtifactDirs: expectedDirs,
RequiredArtifactExts: requiredExts,
ExpectedFileCounts: expectedFileCounts,
SourceMessage: message,
}
}
func normalizeOpenClawArtifactExtCountMap(values map[string]any) map[string]int {
if len(values) == 0 {
return nil
}
result := map[string]int{}
for key, raw := range values {
ext := strings.ToLower(strings.TrimSpace(key))
ext = strings.TrimPrefix(ext, ".")
if ext == "" || strings.Contains(ext, "/") || strings.Contains(ext, "\\") {
continue
}
count := openClawPositiveInt(raw)
if count <= 0 {
continue
}
result[ext] = count
}
if len(result) == 0 {
return nil
}
return result
}
func openClawPositiveInt(value any) int {
switch v := value.(type) {
case int:
return v
case int64:
return int(v)
case float64:
return int(v)
case float32:
return int(v)
case string:
var parsed int
if _, err := fmt.Sscanf(strings.TrimSpace(v), "%d", &parsed); err == nil {
return parsed
}
}
return 0
}
func normalizeOpenClawDirList(values []any) []string {
if len(values) == 0 {
return nil
}
result := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
dir := strings.TrimSpace(fmt.Sprint(value))
if dir == "" || seen[dir] {
continue
}
seen[dir] = true
result = append(result, dir)
}
return result
}
func normalizeOpenClawArtifactExtList(values []any) []string {
if len(values) == 0 {
return nil
}
result := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
ext := strings.ToLower(strings.TrimSpace(fmt.Sprint(value)))
ext = strings.TrimPrefix(ext, ".")
if ext == "" || strings.Contains(ext, "/") || strings.Contains(ext, "\\") || seen[ext] {
continue
}
seen[ext] = true
result = append(result, ext)
}
return result
}
func inferOpenClawRequiredArtifactExts(lowerMessage string) []string {
switch {
case openClawMessageContainsAny(lowerMessage, []string{"pdf", "输出 pdf", "生成 pdf"}):
return []string{"pdf"}
case openClawMessageContainsAny(lowerMessage, []string{"视频", "video", "mp4", "渲染"}):
return []string{"mp4"}
case openClawMessageContainsAny(lowerMessage, []string{"图片", "图像", "png", "jpg", "jpeg", "webp", "生成图"}):
return []string{"png", "jpg", "jpeg", "webp"}
case openClawMessageContainsAny(lowerMessage, []string{"markdown", "md文件", ".md", "文案", "资讯"}):
return []string{"md"}
default:
return nil
}
}
func openClawChatSendParams(
params map[string]any,
turnID string,
) (map[string]any, *shared.RPCError) {
return openClawChatSendParamsWithSessionKey(params, turnID, openClawAgentMainSessionKey(openClawAppThreadKey(params)))
}
func openClawChatSendParamsWithSessionKey(
params map[string]any,
turnID string,
sessionKey string,
) (map[string]any, *shared.RPCError) {
message := openClawCurrentTurnMessage(params)
if message == "" {
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_TASK_PROMPT_REQUIRED"}
}
chatParams := map[string]any{
"sessionKey": sessionKey,
"message": message,
"idempotencyKey": turnID,
}
attachments := openClawNonEmptyPathAttachments(params)
inlineAttachments, rpcErr := materializeOpenClawInlineAttachments(params, turnID)
if rpcErr != nil {
return nil, rpcErr
}
attachments = append(attachments, inlineAttachments...)
if len(attachments) > 0 {
chatParams["message"] = shared.AugmentPromptWithAttachments(
message,
map[string]any{"attachments": attachments},
)
}
if thinking := strings.TrimSpace(shared.StringArg(params, "thinking", "")); thinking != "" {
chatParams["thinking"] = thinking
}
return chatParams, nil
}
func withOpenClawWritableWorkspace(params map[string]any, appThreadKey string) map[string]any {
workingDirectory := strings.TrimSpace(shared.StringArg(params, "workingDirectory", ""))
remoteHint := strings.TrimSpace(shared.StringArg(params, "remoteWorkingDirectoryHint", ""))
ownerScoped := firstOwnerScopedWorkspace(workingDirectory, remoteHint)
if ownerScoped == "" {
return params
}
writable := openClawWritableWorkspaceForOwnerPath(ownerScoped, appThreadKey)
if writable == "" || writable == ownerScoped {
return params
}
next := make(map[string]any, len(params)+1)
for key, value := range params {
next[key] = value
}
if workingDirectory == ownerScoped {
next["workingDirectory"] = writable
}
if remoteHint == ownerScoped {
next["remoteWorkingDirectoryHint"] = writable
}
for _, key := range []string{"taskPrompt", "prompt", "message"} {
if value, ok := next[key].(string); ok && strings.Contains(value, ownerScoped) {
next[key] = strings.ReplaceAll(value, ownerScoped, writable)
}
}
return next
}
func withOpenClawPreparedArtifactWorkspace(params map[string]any, prepared *openClawPreparedArtifactScope) map[string]any {
if prepared == nil {
return params
}
artifactDirectory := strings.TrimSpace(prepared.ArtifactDirectory)
if artifactDirectory == "" {
return params
}
replacements := openClawWorkspacePromptReplacementValues(params)
next := make(map[string]any, len(params)+2)
for key, value := range params {
next[key] = value
}
next["workingDirectory"] = artifactDirectory
next["remoteWorkingDirectoryHint"] = artifactDirectory
for _, key := range []string{"taskPrompt", "prompt", "message"} {
value, ok := next[key].(string)
if !ok || strings.TrimSpace(value) == "" {
continue
}
next[key] = rewriteOpenClawWorkspaceReferences(value, artifactDirectory, replacements)
}
return next
}
func openClawWorkspacePromptReplacementValues(params map[string]any) []string {
values := []string{
shared.StringArg(params, "workingDirectory", ""),
shared.StringArg(params, "remoteWorkingDirectoryHint", ""),
shared.StringArg(params, "remoteWorkingDirectory", ""),
}
metadata := shared.AsMap(params["metadata"])
contract := shared.AsMap(metadata["xworkmateTaskArtifactContract"])
values = append(values,
shared.StringArg(contract, "currentTaskWorkspace", ""),
shared.StringArg(contract, "remoteWorkspaceHint", ""),
)
result := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" || seen[trimmed] {
continue
}
seen[trimmed] = true
result = append(result, trimmed)
}
return result
}
func rewriteOpenClawWorkspaceReferences(message string, artifactDirectory string, replacements []string) string {
result := message
for _, value := range replacements {
if value != artifactDirectory {
result = strings.ReplaceAll(result, value, artifactDirectory)
}
}
return result
}
func firstOwnerScopedWorkspace(values ...string) string {
for _, value := range values {
trimmed := strings.TrimSpace(value)
if strings.HasPrefix(filepath.Clean(trimmed), "/owners/") {
return trimmed
}
}
return ""
}
func openClawWritableWorkspaceForOwnerPath(ownerPath string, sessionKey string) string {
root := strings.TrimSpace(os.Getenv("OPENCLAW_WRITABLE_WORKSPACE_ROOT"))
if root == "" {
return ""
}
root = strings.TrimRight(filepath.Clean(root), string(os.PathSeparator))
if root == "" || root == "." || root == string(os.PathSeparator) {
return ""
}
leaf := safeOpenClawAttachmentPathSegment(sessionKey, "task")
if leaf == "task" {
leaf = safeOpenClawAttachmentPathSegment(filepath.Base(filepath.Clean(ownerPath)), "task")
}
return filepath.Join(root, leaf)
}
func openClawNonEmptyPathAttachments(params map[string]any) []any {
rawAttachments := shared.ListArg(params, "attachments")
if len(rawAttachments) == 0 {
return nil
}
inlineAttachmentNames := map[string]bool{}
for _, raw := range shared.ListArg(params, "inlineAttachments") {
name := strings.TrimSpace(shared.StringArg(shared.AsMap(raw), "name", ""))
if name != "" {
inlineAttachmentNames[name] = true
}
}
attachments := make([]any, 0, len(rawAttachments))
for _, raw := range rawAttachments {
attachment := shared.AsMap(raw)
if len(attachment) == 0 {
continue
}
name := strings.TrimSpace(shared.StringArg(attachment, "name", "attachment"))
if inlineAttachmentNames[name] {
continue
}
if strings.TrimSpace(shared.StringArg(attachment, "path", "")) == "" {
continue
}
attachments = append(attachments, map[string]any{
"name": name,
"description": strings.TrimSpace(shared.StringArg(attachment, "description", "")),
"path": strings.TrimSpace(shared.StringArg(attachment, "path", "")),
})
}
return attachments
}
func materializeOpenClawInlineAttachments(params map[string]any, turnID string) ([]any, *shared.RPCError) {
rawAttachments := shared.ListArg(params, "inlineAttachments")
if len(rawAttachments) == 0 {
return nil, nil
}
workingDirectory := strings.TrimSpace(shared.StringArg(params, "workingDirectory", ""))
if workingDirectory == "" {
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_ATTACHMENT_WORKING_DIRECTORY_REQUIRED"}
}
workingDirectory = openClawAttachmentWorkingDirectory(params, workingDirectory)
attachmentDirectory := filepath.Join(
workingDirectory,
".xworkmate",
"attachments",
safeOpenClawAttachmentPathSegment(turnID, "turn"),
)
if err := os.MkdirAll(attachmentDirectory, 0o755); err != nil {
return nil, &shared.RPCError{Code: -32002, Message: "OPENCLAW_ATTACHMENT_DIRECTORY_FAILED: " + err.Error()}
}
attachments := make([]any, 0, len(rawAttachments))
totalBytes := 0
for index, raw := range rawAttachments {
attachment := shared.AsMap(raw)
if len(attachment) == 0 {
continue
}
name := safeOpenClawAttachmentFileName(
shared.StringArg(attachment, "name", shared.StringArg(attachment, "fileName", "attachment")),
)
mimeType := strings.TrimSpace(shared.StringArg(attachment, "mimeType", shared.StringArg(attachment, "description", "")))
content := strings.TrimSpace(shared.StringArg(attachment, "content", ""))
if content == "" {
continue
}
bytes, err := decodeOpenClawInlineAttachmentContent(content)
if err != nil {
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_ATTACHMENT_INVALID_BASE64: " + name}
}
if len(bytes) > openClawInlineAttachmentMaxFileBytes {
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_ATTACHMENT_FILE_TOO_LARGE: " + name}
}
if totalBytes+len(bytes) > openClawInlineAttachmentMaxTotalBytes {
return nil, &shared.RPCError{Code: -32602, Message: "OPENCLAW_ATTACHMENT_TOTAL_TOO_LARGE"}
}
totalBytes += len(bytes)
path := filepath.Join(attachmentDirectory, fmt.Sprintf("%02d-%s", index+1, name))
if err := os.WriteFile(path, bytes, 0o600); err != nil {
return nil, &shared.RPCError{Code: -32002, Message: "OPENCLAW_ATTACHMENT_WRITE_FAILED: " + err.Error()}
}
attachments = append(attachments, map[string]any{
"name": name,
"description": mimeType,
"path": path,
"sizeBytes": len(bytes),
"source": "inlineAttachment",
})
}
return attachments, nil
}
func openClawAttachmentWorkingDirectory(params map[string]any, workingDirectory string) string {
candidate := strings.TrimSpace(workingDirectory)
remoteHint := strings.TrimSpace(shared.StringArg(params, "remoteWorkingDirectoryHint", ""))
if remoteHint == "" || remoteHint == candidate {
return candidate
}
if isDesktopLocalWorkspacePath(candidate) {
return remoteHint
}
return candidate
}
func isDesktopLocalWorkspacePath(path string) bool {
cleaned := filepath.Clean(strings.TrimSpace(path))
return strings.HasPrefix(cleaned, "/Users/") ||
strings.HasPrefix(cleaned, "/Volumes/")
}
func decodeOpenClawInlineAttachmentContent(content string) ([]byte, error) {
normalized := strings.TrimSpace(content)
if comma := strings.LastIndex(normalized, ","); comma >= 0 {
normalized = strings.TrimSpace(normalized[comma+1:])
}
return base64.StdEncoding.DecodeString(normalized)
}
func safeOpenClawAttachmentPathSegment(value string, fallback string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
trimmed = fallback
}
var builder strings.Builder
for _, r := range trimmed {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
builder.WriteRune(r)
} else {
builder.WriteByte('-')
}
}
result := strings.Trim(builder.String(), "-")
if result == "" {
return fallback
}
return result
}
func safeOpenClawAttachmentFileName(value string) string {
name := strings.TrimSpace(filepath.Base(value))
if name == "." || name == string(os.PathSeparator) || name == "" {
name = "attachment"
}
name = strings.ReplaceAll(name, string(os.PathSeparator), "-")
name = strings.ReplaceAll(name, "/", "-")
name = strings.ReplaceAll(name, "\\", "-")
return name
}
func openClawAgentWaitTimeout(params map[string]any, chatParams map[string]any) time.Duration {
message := strings.TrimSpace(shared.StringArg(chatParams, "message", ""))
if message == "" {
message = openClawCurrentTurnMessage(params)
}
timeout := openClawAgentWaitDefaultTimeout
lowerMessage := strings.ToLower(message)
if strings.TrimSpace(shared.StringArg(shared.AsMap(params["metadata"]), "taskLoadClass", "")) == "complex_long_chain_task" ||
isOpenClawLongArtifactTask(lowerMessage) {
return openClawAgentWaitMaxTimeout
}
for _, keyword := range []string{
"video",
"mp4",
"hyperframes",
"remotion",
"ffmpeg",
"render",
"视频",
"渲染",
"口播",
"字幕",
} {
if strings.Contains(lowerMessage, keyword) {
timeout += 8 * time.Minute
break
}
}
if strings.Contains(lowerMessage, "it-infra-evolution-video") ||
strings.Contains(lowerMessage, "ai-tech-news-video") ||
strings.Contains(lowerMessage, "product-intro-video") {
timeout += 4 * time.Minute
}
if len([]rune(message)) > 1200 {
timeout += 2 * time.Minute
}
if attachments := shared.ListArg(params, "attachments"); len(attachments) > 0 {
timeout += time.Duration(min(len(attachments), 6)) * time.Minute
}
if timeout > openClawAgentWaitMaxTimeout {
return openClawAgentWaitMaxTimeout
}
return timeout
}
func isOpenClawLongArtifactTask(message string) bool {
hasDocumentOutput := openClawMessageContainsAny(message, []string{
"pdf",
"ppt",
"pptx",
"powerpoint",
"docx",
"document",
"文档",
"文件",
"输出",
"导出",
})
hasImageWork := openClawMessageContainsAny(message, []string{
"gpt images",
"gpt-images",
"images2",
"image",
"images",
"illustration",
"插图",
"配图",
"图片",
"生成图",
})
hasMultiStageWork := openClawMessageContainsAny(message, []string{
"chapter",
"section",
"codex",
"artifact export",
"章节",
"每章",
"拆分",
"多章节",
"汇总",
"排版",
})
return hasDocumentOutput && hasImageWork && hasMultiStageWork
}
func openClawMessageContainsAny(message string, keywords []string) bool {
for _, keyword := range keywords {
if strings.Contains(message, keyword) {
return true
}
}
return false
}
func openClawCurrentTurnMessage(params map[string]any) string {
if params == nil {
return ""
}
for _, key := range []string{"taskPrompt", "prompt", "message"} {
if text := strings.TrimSpace(strings.Join(openClawTextFragments(params[key]), "\n")); text != "" {
return text
}
}
if text := strings.TrimSpace(strings.Join(openClawLatestUserMessageText(params["messages"]), "\n")); text != "" {
return text
}
for _, key := range []string{"input", "content"} {
if text := strings.TrimSpace(strings.Join(openClawTextFragments(params[key]), "\n")); text != "" {
return text
}
}
return ""
}
func openClawLatestUserMessageText(raw any) []string {
messages, ok := raw.([]any)
if !ok || len(messages) == 0 {
return nil
}
var fallback []string
for index := len(messages) - 1; index >= 0; index-- {
message := shared.AsMap(messages[index])
if len(message) == 0 {
continue
}
text := compactOpenClawTexts(openClawMessageText(message))
if len(text) == 0 {
continue
}
if fallback == nil {
fallback = text
}
role := strings.ToLower(strings.TrimSpace(shared.StringArg(message, "role", "")))
switch role {
case "user", "human", "client":
return text
}
}
return fallback
}
func openClawMessageText(message map[string]any) []string {
if len(message) == 0 {
return nil
}
texts := make([]string, 0, 4)
for _, key := range []string{"content", "parts", "text", "message"} {
texts = append(texts, openClawTextFragments(message[key])...)
}
return texts
}
func openClawTextFragments(raw any) []string {
switch value := raw.(type) {
case nil:
return nil
case string:
if text := strings.TrimSpace(value); text != "" {
return []string{text}
}
case []any:
texts := make([]string, 0, len(value))
for _, item := range value {
texts = append(texts, openClawTextFragments(item)...)
}
return compactOpenClawTexts(texts)
case map[string]any:
texts := make([]string, 0, len(value))
for _, key := range []string{"text", "content", "message", "value"} {
texts = append(texts, openClawTextFragments(value[key])...)
}
return compactOpenClawTexts(texts)
}
return nil
}
func compactOpenClawTexts(texts []string) []string {
if len(texts) == 0 {
return nil
}
result := make([]string, 0, len(texts))
for _, text := range texts {
if trimmed := strings.TrimSpace(text); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
func (o *SessionOrchestrator) openClawSessionKey(params map[string]any, turnID string) string {
if explicit := strings.TrimSpace(shared.StringArg(params, "openclawSessionKey", "")); explicit != "" {
return explicit
}
return openClawAgentMainSessionKey(openClawAppThreadKey(params))
}
func openClawAgentMainSessionKey(appThreadKey string) string {
appThreadKey = strings.TrimSpace(appThreadKey)
if appThreadKey == "" {
appThreadKey = "main"
}
return "agent:main:" + appThreadKey
}
func validateOpenClawAcceptedSessionKey(payload map[string]any, expectedSessionKey string) *shared.RPCError {
actual := strings.TrimSpace(shared.StringArg(payload, "sessionKey", ""))
expected := strings.TrimSpace(expectedSessionKey)
if actual == "" || expected == "" || actual == expected {
return nil
}
return &shared.RPCError{
Code: -32002,
Message: fmt.Sprintf(
"OPENCLAW_SESSION_MISMATCH: expected %s but OpenClaw accepted %s",
expected,
actual,
),
Data: map[string]any{
"code": "OPENCLAW_SESSION_MISMATCH",
"expectedSessionKey": expected,
"acceptedSessionKey": actual,
"expectedOpenClawKey": expected,
"actualOpenClawKey": actual,
},
}
}
func (o *SessionOrchestrator) openClawArtifactExport(
gatewayProvider string,
chatParams map[string]any,
artifactContract openClawArtifactContract,
runID string,
sinceUnixMs int64,
preparedArtifact *openClawPreparedArtifactScope,
notify func(map[string]any),
) map[string]any {
sessionKey := strings.TrimSpace(shared.StringArg(chatParams, "sessionKey", ""))
if sessionKey == "" || strings.TrimSpace(runID) == "" {
return nil
}
exportParams := map[string]any{
"openclawSessionKey": sessionKey,
"runId": strings.TrimSpace(runID),
"sinceUnixMs": sinceUnixMs,
"maxFiles": 64,
"maxInlineBytes": 0,
"includeContent": false,
}
if preparedArtifact != nil && strings.TrimSpace(preparedArtifact.ArtifactScope) != "" {
exportParams["artifactScope"] = strings.TrimSpace(preparedArtifact.ArtifactScope)
}
if len(artifactContract.ExpectedArtifactDirs) > 0 {
exportParams["expectedArtifactDirs"] = append([]string(nil), artifactContract.ExpectedArtifactDirs...)
}
if len(artifactContract.RequiredArtifactExts) > 0 {
exportParams["requiredArtifactExtensions"] = append([]string(nil), artifactContract.RequiredArtifactExts...)
}
if len(artifactContract.ExpectedFileCounts) > 0 {
counts := map[string]int{}
for ext, count := range artifactContract.ExpectedFileCounts {
counts[ext] = count
}
exportParams["expectedFileCountByExtension"] = counts
}
payload := o.openClawArtifactExportRequest(gatewayProvider, exportParams, notify)
return payload
}
func (o *SessionOrchestrator) openClawArtifactExportRequest(
gatewayProvider string,
exportParams map[string]any,
notify func(map[string]any),
) map[string]any {
exportResult := o.openClawGatewayRequestWithRetry(
gatewayProvider,
"xworkmate.artifacts.export",
exportParams,
30*time.Second,
notify,
)
if exportResult.OK {
return shared.AsMap(exportResult.Payload)
}
message := strings.TrimSpace(shared.StringArg(exportResult.Error, "message", ""))
if message == "" {
message = "openclaw artifact export unavailable"
}
return map[string]any{
"artifactWarnings": []any{message},
}
}
func mergeOpenClawArtifactPayload(result map[string]any, source map[string]any) {
if result == nil || len(source) == 0 {
return
}
if strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", "")) == "" {
if remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(source, "remoteWorkingDirectory", "")); remoteWorkingDirectory != "" {
result["remoteWorkingDirectory"] = remoteWorkingDirectory
}
}
if strings.TrimSpace(shared.StringArg(result, "remoteWorkspaceRefKind", "")) == "" {
if remoteWorkspaceRefKind := strings.TrimSpace(shared.StringArg(source, "remoteWorkspaceRefKind", "")); remoteWorkspaceRefKind != "" {
result["remoteWorkspaceRefKind"] = remoteWorkspaceRefKind
}
}
if strings.TrimSpace(shared.StringArg(result, "artifactScope", "")) == "" {
if artifactScope := strings.TrimSpace(shared.StringArg(source, "artifactScope", "")); artifactScope != "" {
result["artifactScope"] = artifactScope
}
}
if strings.TrimSpace(shared.StringArg(result, "scopeKind", "")) == "" {
if scopeKind := strings.TrimSpace(shared.StringArg(source, "scopeKind", "")); scopeKind != "" {
result["scopeKind"] = scopeKind
}
}
for _, key := range []string{"artifacts", "files", "attachments", "artifactWarnings", "warnings"} {
merged := appendArtifactList(result[key], source[key])
if len(merged) > 0 {
if key == "warnings" {
result["artifactWarnings"] = appendArtifactList(result["artifactWarnings"], source[key])
continue
}
result[key] = merged
}
}
if value, ok := source["constraintSatisfied"]; ok {
result["constraintSatisfied"] = parseBool(value)
}
if _, ok := source["missingRequiredExtensions"]; ok {
result["missingRequiredExtensions"] = appendStringList(result["missingRequiredExtensions"], source["missingRequiredExtensions"])
}
if value, ok := source["missingRequiredFileCounts"]; ok {
result["missingRequiredFileCounts"] = value
}
}
func appendStringList(existing any, incoming any) []any {
seen := map[string]bool{}
merged := make([]any, 0)
add := func(value any) {
item := strings.TrimSpace(fmt.Sprint(value))
if item == "" || seen[item] {
return
}
seen[item] = true
merged = append(merged, item)
}
for _, values := range []any{existing, incoming} {
switch typed := values.(type) {
case []any:
for _, item := range typed {
add(item)
}
case []string:
for _, item := range typed {
add(item)
}
}
}
return merged
}
func appendArtifactList(existing any, incoming any) []any {
merged := make([]any, 0)
switch typed := existing.(type) {
case []any:
merged = append(merged, typed...)
case []map[string]any:
for _, item := range typed {
merged = append(merged, item)
}
}
switch typed := incoming.(type) {
case []any:
merged = append(merged, typed...)
case []map[string]any:
for _, item := range typed {
merged = append(merged, item)
}
}
return merged
}
func applyOpenClawConstraintDeliveryStatus(result map[string]any) {
if result == nil || !parseBool(result["success"]) {
return
}
if value, ok := result["constraintSatisfied"]; !ok || parseBool(value) {
return
}
switch strings.ToLower(strings.TrimSpace(shared.StringArg(result, "status", ""))) {
case string(TaskStateRunning), string(TaskStateFailed), string(TaskStateCancelled):
return
default:
result["status"] = "partially_delivered"
result["artifactSyncStatus"] = "partial"
}
}
func gatewayRPCError(errorPayload map[string]any, fallback string) *shared.RPCError {
if isOpenClawRetryableGatewayError(errorPayload) {
return &shared.RPCError{
Code: -32002,
Message: "OPENCLAW_GATEWAY_SOCKET_CLOSED: OpenClaw gateway connection closed during task execution",
Data: map[string]any{
"code": "OPENCLAW_GATEWAY_SOCKET_CLOSED",
"originalCode": strings.TrimSpace(shared.StringArg(errorPayload, "code", "")),
"originalError": strings.TrimSpace(shared.StringArg(errorPayload, "message", "")),
},
}
}
message := strings.TrimSpace(shared.StringArg(errorPayload, "message", fallback))
if message == "" {
message = fallback
}
data := map[string]any{}
if code := strings.TrimSpace(shared.StringArg(errorPayload, "code", "")); code != "" {
data["code"] = code
}
if len(data) == 0 {
return &shared.RPCError{Code: -32002, Message: message}
}
return &shared.RPCError{Code: -32002, Message: message, Data: data}
}
func sessionContinuationUnavailableRPCError(err sessionContinuationUnavailableError) *shared.RPCError {
return &shared.RPCError{
Code: -32002,
Message: "SESSION_CONTINUATION_UNAVAILABLE: provider session state is unavailable",
Data: map[string]any{
"code": "SESSION_CONTINUATION_UNAVAILABLE",
"sessionId": err.sessionID,
"threadId": err.threadID,
"providerId": err.providerID,
"reason": err.reason,
},
}
}
func (o *SessionOrchestrator) openClawGatewayRequestWithRetry(
gatewayProvider string,
method string,
params map[string]any,
timeout time.Duration,
notify func(map[string]any),
) gatewayruntime.RequestResult {
result := o.server.gateway.RequestByMode(
gatewayProvider,
method,
params,
timeout,
notify,
)
if result.OK || !isOpenClawRetryableGatewayError(result.Error) {
return result
}
if rpcErr := ensureProductionGatewayConnected(o.server, gatewayProvider, notify); rpcErr != nil {
return result
}
return o.server.gateway.RequestByMode(
gatewayProvider,
method,
params,
timeout,
notify,
)
}
func isOpenClawRetryableGatewayError(errorPayload map[string]any) bool {
code := strings.TrimSpace(strings.ToUpper(shared.StringArg(errorPayload, "code", "")))
if code == "SOCKET_CLOSED" || code == "SOCKET_FAILURE" || code == "OFFLINE" || code == "INVALID_HANDSHAKE" {
return true
}
message := strings.TrimSpace(strings.ToLower(shared.StringArg(errorPayload, "message", "")))
return strings.Contains(message, "socket closed") ||
strings.Contains(message, "invalid handshake") ||
strings.Contains(message, "first request must be connect")
}
func firstNonEmptyString(values map[string]any, keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(shared.StringArg(values, key, "")); value != "" {
return value
}
}
return ""
}
func openClawGatewaySessionUpdate(notification map[string]any, sessionID string, threadID string, turnID string) map[string]any {
params := shared.AsMap(notification["params"])
event := shared.AsMap(params["event"])
if strings.TrimSpace(shared.StringArg(event, "event", "")) != "chat.run" {
return nil
}
payload := shared.AsMap(event["payload"])
text := firstNonEmptyString(payload, "assistantText", "text", "message", "output", "summary")
if text == "" {
return nil
}
update := map[string]any{
"sessionId": sessionID,
"threadId": threadID,
"turnId": turnID,
"type": "delta",
"event": "delta",
"delta": text,
"text": text,
"pending": true,
"error": false,
}
if isTerminalGatewayPayload(payload) {
update["type"] = "status"
update["event"] = "completed"
update["message"] = text
update["pending"] = false
if strings.EqualFold(strings.TrimSpace(shared.StringArg(payload, "state", "")), "error") {
update["error"] = true
}
}
return shared.NotificationEnvelope("session.update", update)
}
func isTerminalGatewayPayload(payload map[string]any) bool {
if payload == nil {
return false
}
if value, ok := payload["terminal"].(bool); ok && value {
return true
}
switch strings.TrimSpace(strings.ToLower(shared.StringArg(payload, "state", ""))) {
case "complete", "completed", "done", "final", "ok", "success", "failed", "error", "timeout", "timed_out", "cancelled", "canceled":
return true
default:
return false
}
}
func resolvedGatewayProviderID(params map[string]any, routing RoutingResult) string {
for _, value := range []string{
routing.GatewayProviderID,
shared.StringArg(params, "gatewayProvider", ""),
shared.StringArg(params, "gatewayProviderId", ""),
} {
if provider := strings.TrimSpace(value); provider != "" {
return provider
}
}
routingParams := shared.AsMap(params["routing"])
for _, key := range []string{
"gatewayProvider",
"gatewayProviderId",
"preferredGatewayProviderId",
} {
if provider := strings.TrimSpace(shared.StringArg(routingParams, key, "")); provider != "" {
return provider
}
}
return ""
}
func withResolvedGatewayProvider(params map[string]any, gatewayProvider string) map[string]any {
next := make(map[string]any, len(params)+2)
for key, value := range params {
next[key] = value
}
next["gatewayProvider"] = gatewayProvider
next["gatewayProviderId"] = gatewayProvider
return next
}
func (o *SessionOrchestrator) formatUnavailable(res RoutingResult) map[string]any {
return map[string]any{
"success": false,
"status": "unavailable",
"unavailable": true,
"unavailableCode": res.UnavailableCode,
"unavailableMessage": res.UnavailableMsg,
"resolvedExecutionTarget": res.TargetID,
"resolvedProviderId": res.ProviderID,
"resolvedGatewayProviderId": res.GatewayProviderID,
"resolvedModel": res.Model,
"resolvedSkills": append([]string(nil), res.Skills...),
}
}
func (o *SessionOrchestrator) normalizeResult(sess *session, result map[string]any, routing RoutingResult, turnID string, params map[string]any) map[string]any {
if result == nil {
result = map[string]any{}
}
if routing.TargetID == "gateway" && strings.TrimSpace(shared.StringArg(result, "status", "")) == string(TaskStateRunning) {
result["turnId"] = turnID
result["success"] = true
result["resolvedExecutionTarget"] = routing.TargetID
result["resolvedProviderId"] = routing.ProviderID
result["resolvedGatewayProviderId"] = routing.GatewayProviderID
result["resolvedModel"] = routing.Model
result["resolvedSkills"] = append([]string(nil), routing.Skills...)
sess.mu.Lock()
sess.task.State = TaskStateRunning
sess.task.UpdatedAt = time.Now()
sess.lastResult = cloneMap(result)
sess.mu.Unlock()
return result
}
if openClawArtifactResponse(result, routing, params) {
o.completeOpenClawScopedArtifactExport(result, params, openClawGatewayProviderForArtifacts(result, routing, params), turnID)
}
delete(result, openClawArtifactExportAttemptedField)
successValue, hasSuccess := result["success"]
successSource := "explicit"
success := parseBool(successValue)
if !hasSuccess {
successSource = "absent"
success = true
}
output := strings.TrimSpace(shared.StringArg(result, "output", ""))
if output == "" {
output = strings.TrimSpace(shared.StringArg(result, "summary", ""))
}
if output == "" && success {
output = strings.TrimSpace(shared.StringArg(result, "message", ""))
}
if routing.TargetID == "gateway" && successSource == "absent" {
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", ""))
if output == "" && len(extractArtifactPayloads(result, remoteWorkingDirectory)) == 0 {
result["success"] = false
result["status"] = string(TaskStateFailed)
result["code"] = "OPENCLAW_TERMINAL_WITHOUT_EVIDENCE"
result["error"] = "OPENCLAW_TERMINAL_WITHOUT_EVIDENCE"
result["message"] = "OPENCLAW_TERMINAL_WITHOUT_EVIDENCE"
} else {
result["success"] = true
result["successSource"] = "inferred"
}
}
sess.mu.Lock()
if output != "" {
sess.history = append(sess.history, "ASSISTANT: "+output)
}
sess.task.State = TaskStateCompleted
sess.task.UpdatedAt = time.Now()
sess.mu.Unlock()
result["turnId"] = turnID
if strings.TrimSpace(shared.StringArg(result, "status", "")) == "" {
result["status"] = "completed"
}
if !hasSuccess {
if _, ok := result["success"]; !ok {
result["success"] = true
}
}
if !parseBool(result["success"]) && strings.TrimSpace(shared.StringArg(result, "status", "")) == string(TaskStateCompleted) {
result["status"] = string(TaskStateFailed)
}
result["resolvedExecutionTarget"] = routing.TargetID
result["resolvedProviderId"] = routing.ProviderID
result["resolvedGatewayProviderId"] = routing.GatewayProviderID
result["resolvedModel"] = routing.Model
result["resolvedSkills"] = append([]string(nil), routing.Skills...)
if output != "" {
result["output"] = output
if _, ok := result["summary"]; !ok {
result["summary"] = output
}
}
if output == "" && routing.TargetID != "gateway" && !parseBool(result["success"]) {
result["status"] = "failed"
} else if output == "" && routing.TargetID != "gateway" {
result["success"] = false
result["status"] = "failed"
result["error"] = "provider returned no displayable output"
result["message"] = "provider returned no displayable output"
}
if !parseBool(result["success"]) {
sess.mu.Lock()
sess.task.State = TaskStateFailed
sess.task.UpdatedAt = time.Now()
sess.mu.Unlock()
}
applyOpenClawConstraintDeliveryStatus(result)
artifactRecord := buildArtifactRecord(sess, result, output)
if artifactRecord.RemoteWorkingDirectory != "" {
result["remoteWorkingDirectory"] = artifactRecord.RemoteWorkingDirectory
}
if artifactRecord.RemoteWorkspaceRefKind != "" {
result["remoteWorkspaceRefKind"] = artifactRecord.RemoteWorkspaceRefKind
}
if artifactRecord.ResultSummary != "" && strings.TrimSpace(shared.StringArg(result, "resultSummary", "")) == "" {
result["resultSummary"] = artifactRecord.ResultSummary
}
if len(artifactRecord.Artifacts) > 0 {
result["artifacts"] = artifactRecord.Artifacts
}
if openClawArtifactResponse(result, routing, params) {
stripOpenClawArtifactInlineContent(result)
}
workingDirectory := shared.StringArg(params, "workingDirectory", "")
routingParams := shared.AsMap(params["routing"])
routingMode := strings.TrimSpace(shared.StringArg(routingParams, "routingMode", ""))
if workingDirectory != "" && routingMode == "auto" {
_ = o.server.memoryService.RecordSuccess(workingDirectory, memory.SuccessEntry{
ResolvedExecutionTarget: routing.TargetID,
ResolvedProviderID: routing.ProviderID,
ResolvedModel: routing.Model,
ResolvedSkills: routing.Skills,
Summary: output,
})
}
sess.mu.Lock()
sess.lastResult = cloneMap(result)
sess.mu.Unlock()
return result
}
func (o *SessionOrchestrator) completeOpenClawScopedArtifactExport(
result map[string]any,
params map[string]any,
gatewayProvider string,
turnID string,
) {
if result == nil || o.server == nil || o.server.gateway == nil {
return
}
if !parseBool(result["success"]) {
return
}
remoteWorkingDirectory := strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", ""))
if len(extractArtifactPayloads(result, remoteWorkingDirectory)) > 0 {
return
}
if parseBool(result[openClawArtifactExportAttemptedField]) {
return
}
preparedArtifact := openClawPreparedArtifactScopeFromPayload(result)
if preparedArtifact == nil {
return
}
sessionKey := o.openClawSessionKey(params, turnID)
runID := strings.TrimSpace(shared.StringArg(result, "runId", turnID))
chatParams := map[string]any{"sessionKey": sessionKey}
artifactContract := openClawArtifactContractForParams(params, chatParams)
mergeOpenClawArtifactPayload(result, o.openClawArtifactExport(
gatewayProvider,
chatParams,
artifactContract,
runID,
0,
preparedArtifact,
nil,
))
o.server.decorateOpenClawArtifactDownloadURLs(result, sessionKey, runID)
stripOpenClawArtifactInlineContent(result)
}
func openClawGatewayProviderForArtifacts(result map[string]any, routing RoutingResult, params map[string]any) string {
for _, provider := range []string{
routing.GatewayProviderID,
shared.StringArg(result, "resolvedGatewayProviderId", ""),
shared.StringArg(result, "gatewayProviderId", ""),
shared.StringArg(result, "gatewayProvider", ""),
shared.StringArg(params, "gatewayProviderId", ""),
shared.StringArg(params, "gatewayProvider", ""),
} {
if isOpenClawMode(provider) {
return strings.TrimSpace(provider)
}
}
routingParams := shared.AsMap(params["routing"])
for _, key := range []string{"preferredGatewayProviderId", "gatewayProviderId", "gatewayProvider"} {
if provider := strings.TrimSpace(shared.StringArg(routingParams, key, "")); isOpenClawMode(provider) {
return provider
}
}
return "openclaw"
}
func openClawArtifactResponse(result map[string]any, routing RoutingResult, params map[string]any) bool {
for _, provider := range []string{
routing.GatewayProviderID,
shared.StringArg(result, "resolvedGatewayProviderId", ""),
shared.StringArg(result, "gatewayProviderId", ""),
shared.StringArg(result, "gatewayProvider", ""),
shared.StringArg(params, "gatewayProviderId", ""),
shared.StringArg(params, "gatewayProvider", ""),
} {
if isOpenClawMode(provider) {
return true
}
}
routingParams := shared.AsMap(params["routing"])
for _, key := range []string{"preferredGatewayProviderId", "gatewayProviderId", "gatewayProvider"} {
if isOpenClawMode(shared.StringArg(routingParams, key, "")) {
return true
}
}
if strings.HasPrefix(strings.TrimSpace(shared.StringArg(result, "artifactScope", "")), "tasks/") {
return true
}
for _, key := range []string{"artifacts", "files", "attachments"} {
if openClawArtifactListHasScopedArtifact(result[key]) {
return true
}
}
return false
}
func openClawArtifactListHasScopedArtifact(raw any) bool {
switch values := raw.(type) {
case []map[string]any:
for _, artifact := range values {
if openClawArtifactHasBridgeDownloadRef(artifact) {
return true
}
}
case []any:
for _, item := range values {
artifact := shared.AsMap(item)
if openClawArtifactHasBridgeDownloadRef(artifact) {
return true
}
}
}
return false
}
func openClawArtifactHasBridgeDownloadRef(artifact map[string]any) bool {
if strings.HasPrefix(strings.TrimSpace(shared.StringArg(artifact, "artifactScope", "")), "tasks/") {
return true
}
downloadURL := strings.TrimSpace(shared.StringArg(artifact, "downloadUrl", ""))
if downloadURL == "" {
downloadURL = strings.TrimSpace(shared.StringArg(artifact, "downloadURL", ""))
}
return strings.Contains(downloadURL, "/artifacts/openclaw/download")
}
func taskKindFromParams(params map[string]any, routing RoutingResult) TaskKind {
if routing.TargetID == "gateway" {
return TaskKindGateway
}
return TaskKindSingleAgent
}
func buildArtifactRecord(sess *session, result map[string]any, output string) ArtifactRecord {
record := ArtifactRecord{
SessionID: sess.sessionID,
ThreadID: sess.threadID,
ResultSummary: strings.TrimSpace(output),
UpdatedAt: time.Now(),
}
if record.ResultSummary == "" {
record.ResultSummary = strings.TrimSpace(shared.StringArg(result, "resultSummary", ""))
}
if record.ResultSummary == "" {
record.ResultSummary = strings.TrimSpace(shared.StringArg(result, "summary", ""))
}
record.RemoteWorkingDirectory = strings.TrimSpace(shared.StringArg(result, "remoteWorkingDirectory", ""))
if record.RemoteWorkingDirectory == "" {
record.RemoteWorkingDirectory = strings.TrimSpace(sess.control.RemoteWorkingDirHint)
}
record.RemoteWorkspaceRefKind = strings.TrimSpace(shared.StringArg(result, "remoteWorkspaceRefKind", ""))
if record.RemoteWorkspaceRefKind == "" && record.RemoteWorkingDirectory != "" {
record.RemoteWorkspaceRefKind = "remotePath"
}
record.Artifacts = extractArtifactPayloads(result, record.RemoteWorkingDirectory)
sess.mu.Lock()
sess.artifacts = record
sess.control.UpdatedAt = record.UpdatedAt
sess.mu.Unlock()
return record
}
func extractArtifactPayloads(result map[string]any, remoteWorkingDirectory string) []map[string]any {
artifacts := make([]map[string]any, 0)
for _, key := range []string{"artifacts", "files", "attachments"} {
rawArtifacts := result[key]
items, ok := rawArtifacts.([]any)
if !ok {
if typed, ok := rawArtifacts.([]map[string]any); ok {
for _, item := range typed {
if artifact := normalizeArtifactPayload(item, remoteWorkingDirectory); len(artifact) > 0 {
artifacts = append(artifacts, artifact)
}
}
}
continue
}
for _, item := range items {
if mapped := shared.AsMap(item); len(mapped) > 0 {
if artifact := normalizeArtifactPayload(mapped, remoteWorkingDirectory); len(artifact) > 0 {
artifacts = append(artifacts, artifact)
}
}
}
}
return artifacts
}
func normalizeArtifactPayload(item map[string]any, remoteWorkingDirectory string) map[string]any {
artifact := make(map[string]any, len(item)+4)
for key, value := range item {
artifact[key] = value
}
relativePath := strings.TrimSpace(shared.StringArg(artifact, "relativePath", ""))
if relativePath == "" {
relativePath = strings.TrimSpace(shared.StringArg(artifact, "path", ""))
}
if relativePath == "" {
relativePath = strings.TrimSpace(shared.StringArg(artifact, "name", ""))
}
downloadURL := artifactDownloadURL(artifact)
if relativePath == "" && downloadURL != "" {
relativePath = artifactRelativePathFromDownloadURL(downloadURL)
}
relativePath = safeArtifactRelativePath(remoteWorkingDirectory, relativePath)
if relativePath == "" {
return nil
}
artifact["relativePath"] = relativePath
if downloadURL != "" {
artifact["downloadUrl"] = downloadURL
delete(artifact, "downloadURL")
delete(artifact, "download_url")
}
if strings.TrimSpace(shared.StringArg(artifact, "label", "")) == "" {
artifact["label"] = filepath.Base(relativePath)
}
if strings.TrimSpace(shared.StringArg(artifact, "contentType", "")) == "" {
artifact["contentType"] = artifactContentType(relativePath)
}
return artifact
}
func artifactDownloadURL(artifact map[string]any) string {
for _, key := range []string{"downloadUrl", "downloadURL", "download_url"} {
if value := strings.TrimSpace(shared.StringArg(artifact, key, "")); value != "" {
return value
}
}
return ""
}
func artifactRelativePathFromDownloadURL(raw string) string {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
return ""
}
base := strings.TrimSpace(filepath.Base(parsed.Path))
if base == "" || base == "." || base == "/" {
sum := sha256.Sum256([]byte(raw))
base = fmt.Sprintf("artifact-%x.bin", sum[:6])
}
return base
}
func safeArtifactRelativePath(root string, rawPath string) string {
path := strings.TrimSpace(rawPath)
if path == "" || strings.Contains(path, "\x00") {
return ""
}
path = filepath.ToSlash(path)
if strings.TrimSpace(root) != "" && filepath.IsAbs(path) {
rel, err := filepath.Rel(root, path)
if err != nil {
return ""
}
path = filepath.ToSlash(rel)
}
path = filepath.Clean(filepath.FromSlash(path))
if path == "." || filepath.IsAbs(path) || strings.HasPrefix(path, ".."+string(os.PathSeparator)) || path == ".." {
return ""
}
return filepath.ToSlash(path)
}
func artifactContentType(relativePath string) string {
switch strings.ToLower(filepath.Ext(relativePath)) {
case ".pdf":
return "application/pdf"
case ".pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
case ".docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
case ".txt", ".md":
return "text/plain"
default:
return "application/octet-stream"
}
}
func (s *Server) getOrCreateSession(sessionID, threadID string) *session {
s.mu.Lock()
defer s.mu.Unlock()
sess, ok := s.sessions[sessionID]
if !ok {
sess = &session{
sessionID: sessionID,
threadID: threadID,
}
s.sessions[sessionID] = sess
}
return sess
}
func (s *Server) emitSessionUpdate(notify func(map[string]any), turnID string, update map[string]any) {
if notify == nil || update == nil {
return
}
update["turnId"] = turnID
notify(shared.NotificationEnvelope("session.update", update))
}