277 lines
14 KiB
Markdown
277 lines
14 KiB
Markdown
# Chain Map: Task Execution (App → Bridge → OpenClaw → Plugin)
|
|
|
|
Repo chain: xworkmate-app → xworkmate-bridge → openclaw.svc.plus → openclaw-multi-session-plugins
|
|
|
|
## Entry Points (xworkmate-app)
|
|
|
|
```
|
|
1. AssistantPage.sendChatMessage()
|
|
lib/features/assistant/assistant_page_state_actions.dart
|
|
|
|
2. AppController.resendChatMessage() (retry)
|
|
lib/app/app_controller_desktop_thread_actions.dart
|
|
|
|
3. drainOpenClawGatewayQueue() (queue drain)
|
|
lib/app/app_controller_openclaw_task_queue.dart
|
|
```
|
|
|
|
## Call Flow
|
|
|
|
```
|
|
xworkmate-app
|
|
AssistantPage.sendChatMessage()
|
|
└─ AppController.sendChatMessage()
|
|
├─ Resolve/create TaskThread by sessionKey/threadId
|
|
├─ Prepare local workspace: ~/.xworkmate/threads/<session>/
|
|
├─ Build task context prompt (TaskThread.sessionKey, workspace, contract)
|
|
├─ Attach metadata.xworkmateTaskArtifactContract
|
|
│ └─ schemaVersion, appThreadKey, expectedArtifactDirs
|
|
└─ Select execution path:
|
|
├─ Agent providers (codex/opencode/gemini/hermes)
|
|
│ └─ DesktopGoTaskService.startSession()
|
|
└─ OpenClaw gateway
|
|
├─ Check admission: isOpenClawLaneIdle()
|
|
│ ├─ Idle → send immediately
|
|
│ └─ Busy → queueOpenClawGatewayWork()
|
|
└─ DesktopGoTaskService.startSession()
|
|
|
|
DesktopGoTaskService
|
|
└─ ExternalCodeAgentAcpDesktopTransport.executeTask()
|
|
├─ acp.capabilities → verify provider availability
|
|
├─ xworkmate.routing.resolve → pre-resolve route
|
|
├─ session.start (WebSocket /acp)
|
|
│ ├─ params: sessionId, threadId, prompt, workingDirectory
|
|
│ ├─ routing: executionTarget=gateway, preferredGatewayProviderId=openclaw
|
|
│ └─ metadata: xworkmateTaskArtifactContract
|
|
│ └─ appThreadKey only; no prefilled openclawSessionKey
|
|
└─ Listen for SSE session.update events
|
|
├─ status=running → poll xworkmate.tasks.get
|
|
├─ terminal snapshot → applyGatewayChatResult()
|
|
└─ artifacts → sync to local workspace
|
|
|
|
───────────────────────────────────────────────────────────
|
|
Protocol: ACP JSON-RPC 2.0 over WebSocket
|
|
Path: wss://xworkmate-bridge.svc.plus/acp
|
|
Auth: Bearer <BRIDGE_AUTH_TOKEN>
|
|
───────────────────────────────────────────────────────────
|
|
|
|
xworkmate-bridge
|
|
internal/acp/http_handler.go: WebSocket handler
|
|
└─ handleRequest() (rpc_handler.go)
|
|
├─ Validate routing params
|
|
├─ forceOpenClawGatewayRequest() — ensure gateway routing
|
|
└─ orchestrator.Process()
|
|
├─ Routing engine: Resolve(params, prompt, memory)
|
|
│ ├─ Heuristic: looksLocal() / looksOnline()
|
|
│ ├─ Memory preferences
|
|
│ └─ LLM classifier path
|
|
│
|
|
├─ openClawGatewayAdmissionGate.acquire()
|
|
│ ├─ maxActive: 5, maxQueued: 20
|
|
│ ├─ queueTimeout: 10 min
|
|
│ └─ Returns: admission slot or OPENCLAW_GATEWAY_BUSY
|
|
│
|
|
└─ startOpenClawGatewayTask()
|
|
├─ ensureProductionGatewayConnected()
|
|
├─ openClawArtifactPrepare()
|
|
│ └─ gateway.request('xworkmate.session.prepare')
|
|
│ ├─ schemaVersion: 1
|
|
│ ├─ appThreadKey: App TaskThread key
|
|
│ ├─ openclawSessionKey: OpenClaw SessionEntry key
|
|
│ ├─ expectedArtifactDirs: typed artifact contract
|
|
│ └─ scope: tasks/<openclawSessionKey>/<runId>/
|
|
├─ Rewrite OpenClaw turn workspace references
|
|
│ ├─ `/owners/...` and other app owner-scoped hints are not
|
|
│ │ plugin workspace roots
|
|
│ ├─ workingDirectory / remoteWorkingDirectoryHint for chat.send
|
|
│ │ become prepared.artifactDirectory
|
|
│ └─ prompt `currentTaskWorkspace` points at artifactDirectory
|
|
│
|
|
├─ gateway.request('chat.send')
|
|
│ └─ payload: sessionKey, message, attachments, idempotencyKey
|
|
│ sessionKey is the OpenClaw native field and equals openclawSessionKey
|
|
│ (no expectedArtifactDirs root field)
|
|
│
|
|
├─ Keep OpenClawTaskRecord only as live transport context
|
|
│ ├─ SessionID, ThreadID, TurnID, RunID
|
|
│ ├─ SessionKey (from gateway response)
|
|
│ ├─ TaskLoadClass (short_task/long_task/complex_chain_task)
|
|
│ ├─ RuntimeBudgetMinutes (10/30/60)
|
|
│ └─ PreparedArtifact scope ref
|
|
│
|
|
└─ Native task status lookup
|
|
└─ xworkmate.tasks.get forwards typed
|
|
{appThreadKey, openclawSessionKey, runId/taskId}
|
|
to OpenClaw native task-registry via the plugin.
|
|
Bridge no longer rebuilds task state from artifactScope/runId.
|
|
|
|
───────────────────────────────────────────────────────────
|
|
Protocol: Custom JSON-RPC v4 over WebSocket
|
|
Auth: Ed25519 device identity + shared token
|
|
───────────────────────────────────────────────────────────
|
|
|
|
openclaw.svc.plus
|
|
Gateway: receives 'chat.send'
|
|
└─ Agent runner processes turn
|
|
├─ Model inference (Claude/GPT/Gemini)
|
|
├─ Tool execution loop
|
|
│ ├─ browser → screenshot → saveMediaBuffer(browser)
|
|
│ ├─ image-generation → GeneratedImageAsset (in-memory)
|
|
│ ├─ file-write → write to working directory or arbitrary path
|
|
│ └─ video-render → output to media dir or /tmp
|
|
│
|
|
└─ Tool output destinations:
|
|
├─ ~/.openclaw/media/browser/<uuid>.png ← NOT in tasks/<session>/<run>/
|
|
├─ ~/.openclaw/media/inbound/<uuid>.<ext> ← NOT in tasks/<session>/<run>/
|
|
├─ /tmp/openclaw/downloads/ ← NOT in tasks/<session>/<run>/
|
|
└─ tasks/<session>/<run>/output.md ← MAY be written here
|
|
|
|
openclaw-multi-session-plugins
|
|
Receives gateway RPC: xworkmate.session.prepare
|
|
recordXWorkmateSessionMapping()
|
|
├─ Validate schemaVersion=1 typed metadata
|
|
├─ Require appThreadKey and openclawSessionKey
|
|
├─ Write SessionEntry.pluginExtensions
|
|
│ └─ ["openclaw-multi-session-plugins"]["xworkmate.sessionMapping"]
|
|
└─ Fail closed on appThreadKey/openclawSessionKey conflicts
|
|
|
|
Durable run terminal state
|
|
├─ session.prepare records xworkmate.taskRuns[runId] = running
|
|
├─ agent_end records completed/failed + sanitized error in SessionEntry.pluginExtensions
|
|
└─ xworkmate.tasks.get uses this record when chat.send has no native detached-task record
|
|
|
|
prepareXWorkmateArtifacts()
|
|
├─ resolveWorkspaceDir() → workspace root
|
|
├─ safeScopeSegment(openclawSessionKey) → sanitize
|
|
├─ safeScopeSegment(runId) → sanitize
|
|
└─ mkdir <workspace>/tasks/<safeSessionKey>/<safeRunId>/
|
|
|
|
Receives gateway RPC: xworkmate.artifacts.export
|
|
exportXWorkmateArtifacts()
|
|
├─ resolveScopeRoot(workspaceRoot, artifactScope)
|
|
├─ collectCandidates(scopeRoot)
|
|
│ ├─ Walk tasks/<session>/<run>/ recursively ← ONLY THIS DIRECTORY
|
|
│ ├─ Skip symlinks
|
|
│ ├─ Skip .git, .openclaw, node_modules, etc.
|
|
│ └─ Apply artifact-ignore.md rules
|
|
│
|
|
├─ Read each candidate file, compute SHA-256
|
|
├─ signArtifactRef(sessionKey, runId, relativePath)
|
|
└─ Return manifest + base64 file contents
|
|
|
|
`expectedArtifactDirs` source:
|
|
session.start.metadata.xworkmateTaskArtifactContract.expectedArtifactDirs
|
|
→ bridge artifact contract
|
|
→ xworkmate.session.prepare mapping
|
|
→ xworkmate.artifacts.collect-and-snapshot / export only
|
|
|
|
Forbidden compatibility paths:
|
|
session.start.metadata.xworkmateTaskArtifactContract.sessionKey
|
|
session.start.expectedArtifactDirs
|
|
session.start.metadata.expectedArtifactDirs
|
|
chat.send.expectedArtifactDirs
|
|
xworkmate.tasks.get.sessionKey
|
|
|
|
Receives gateway RPC: xworkmate.artifacts.collect-and-snapshot
|
|
collectAndSnapshotXWorkmateArtifacts()
|
|
├─ Scan ~/.openclaw/media/ and /tmp/openclaw/ for files changed since task start
|
|
├─ Copy regular files into tasks/<session>/<run>/artifacts/
|
|
├─ Preserve source grouping such as artifacts/media/... and artifacts/tmp-openclaw/...
|
|
└─ Reject symlinks and paths outside the fixed source roots
|
|
|
|
───────────────────────────────────────────────────────────
|
|
FIXED GAP: Bridge calls collect-and-snapshot after agent.wait
|
|
terminal completion and before xworkmate.artifacts.export.
|
|
Tool outputs in ~/.openclaw/media/* or /tmp/openclaw/* are
|
|
now copied into tasks/<session>/<run>/artifacts/ before export.
|
|
───────────────────────────────────────────────────────────
|
|
|
|
Back to xworkmate-bridge:
|
|
terminal transport handling
|
|
├─ Call xworkmate.artifacts.collect-and-snapshot via gateway
|
|
├─ Call xworkmate.artifacts.export via gateway
|
|
├─ Collect artifact manifest
|
|
├─ Build App transport payload with:
|
|
│ ├─ status: completed/failed/cancelled
|
|
│ ├─ artifacts: { items: [...], scope: "..." }
|
|
│ └─ text: final output
|
|
│
|
|
├─ decorateOpenClawArtifactDownloadURLs()
|
|
│ └─ Replace artifact refs with signed download URLs
|
|
│ Format: /artifacts/openclaw/download?ref=<signed>&t=<expiry>
|
|
│
|
|
└─ Send SSE session.update to app
|
|
|
|
xworkmate.tasks.get:
|
|
Bridge forwards typed lookup to the plugin/native task-registry.
|
|
The request uses the persisted association:
|
|
{appThreadKey, openclawSessionKey, runId/taskId}
|
|
Terminal state comes from native task records first, then from the plugin's
|
|
durable agent_end run record. Missing both returns structured errors such as
|
|
no_native_task_record instead of inferring success from artifacts.
|
|
|
|
expectedArtifactDirs only drive workspace-root discovery. They never keep a
|
|
terminal run in syncing-artifacts. Only explicit export flags or
|
|
requiredArtifactExtensions are blocking delivery contracts.
|
|
|
|
Back to xworkmate-app:
|
|
ExternalCodeAgentAcpDesktopTransport
|
|
├─ Receive terminal snapshot via SSE or xworkmate.tasks.get
|
|
├─ applyGatewayChatResult()
|
|
│ ├─ Terminal snapshot → lifecycleStatus=ready
|
|
│ ├─ success=true && artifacts present → syncArtifactsFromBridge()
|
|
│ ├─ success=false → lastResultCode=failed
|
|
│ └─ no-exported-artifacts → lastArtifactSyncStatus=no-exported-artifacts
|
|
│
|
|
└─ syncArtifactsFromBridge()
|
|
└─ Download each artifact via /artifacts/openclaw/download
|
|
→ Save to ~/.xworkmate/threads/<session>/
|
|
|
|
Rule: the app must not keep an OpenClaw task in `running` after a bridge
|
|
terminal snapshot. Missing or incomplete artifacts are represented only through
|
|
`lastArtifactSyncStatus` (`no-exported-artifacts`, `partial`, `download-failed`,
|
|
etc.), not by extending the task execution lifecycle.
|
|
```
|
|
|
|
## Key Files by Repo
|
|
|
|
### xworkmate-app
|
|
- `lib/runtime/external_code_agent_acp_desktop_transport.dart` — 核心 ACP transport
|
|
- `lib/runtime/go_task_service_client.dart` — GoTaskService 数据模型
|
|
- `lib/runtime/gateway_acp_client.dart` — ACP HTTP RPC client
|
|
- `lib/app/app_controller_openclaw_task_queue.dart` — OpenClaw 并发队列
|
|
- `lib/app/app_controller_desktop_thread_sessions.dart` — session 恢复逻辑
|
|
- `lib/app/app_controller_desktop_thread_actions.dart` — 消息发送
|
|
- `lib/runtime/agent_registry.dart` — agent registry
|
|
|
|
### xworkmate-bridge
|
|
- `internal/acp/http_handler.go` — HTTP server + WebSocket handler
|
|
- `internal/acp/rpc_handler.go` — JSON-RPC dispatcher
|
|
- `internal/acp/orchestrator.go` — 会话编排 (2000+ lines)
|
|
- `internal/acp/openclaw_gateway_admission.go` — 并发控制
|
|
- `internal/acp/openclaw_async_tasks.go` — 异步任务管理
|
|
- `internal/acp/openclaw_artifact_download.go` — artifact 下载代理
|
|
- `internal/gatewayruntime/runtime.go` — gateway WebSocket client
|
|
- `internal/router/router.go` — 路由引擎
|
|
|
|
### openclaw-multi-session-plugins
|
|
- `src/exportArtifacts.ts` — artifact prepare/export/read (963 lines)
|
|
- `src/taskState.ts` — task snapshot adapter + SessionEntry.pluginExtensions mapping
|
|
- `index.ts` — plugin entry + gateway method registration
|
|
|
|
### openclaw.svc.plus (reference)
|
|
- `src/config/paths.ts` — ~/.openclaw/ state directory
|
|
- `src/infra/tmp-openclaw-dir.ts` — /tmp/openclaw/
|
|
- `dist/store-ezT1dexf.js` — saveMediaBuffer() → media/<subdir>/
|
|
|
|
## Fragile Points
|
|
|
|
1. **F1: Tool output path mismatch** — Tools save to media/, plugin exports from tasks/ → gap
|
|
2. **F2: Session key mismatch** — Bridge maps App `appThreadKey` to an explicit `openclawSessionKey` before prepare/chat/export and the plugin persists that mapping in SessionEntry.pluginExtensions
|
|
3. **F3: Prepare timing** — If prepare fails after send, no scope directory exists
|
|
4. **F4: Admission gate rejection** — Queue full → OPENCLAW_GATEWAY_BUSY → app must handle
|
|
5. **F5: Bridge restart** — In-memory sessions lost → app must detect and recover
|
|
6. **F6: Artifact ref key rotation** — Secret change invalidates all signed refs
|
|
7. **F7: SSE stream interruption** — Recovery polling must align with bridge task deadlines and must apply terminal snapshots immediately
|
|
8. **F8: chat.send is not a detached task** — agent_end state must remain queryable by runId even when the native task registry has no record
|