# Chain Map: Artifact Lifecycle (Prepare → Export → Read → Download) Repo chain: openclaw-multi-session-plugins ↔ xworkmate-bridge ↔ xworkmate-app ## Lifecycle States ``` [prepare] → [execute] → [collect-and-snapshot] → [export] → [snapshot] → [download] → [sync] prepare: map session + mkdir tasks/// (multi-session-plugins) execute: tools write files (openclaw.svc.plus) collect: copy media/tmp outputs into task scope (multi-session-plugins) export: scan + manifest + sign (multi-session-plugins) snapshot: assemble terminal result (xworkmate-bridge) download: signed URL proxy (xworkmate-bridge) sync: save to ~/.xworkmate/threads// (xworkmate-app) ``` App terminal rule: - `completed`, `failed`, `cancelled`, and `canceled` snapshots end task execution immediately. - Artifact presence controls only `lastArtifactSyncStatus`; it is not a reason to keep `lifecycleStatus=running`. ## State 1: Prepare ``` Caller: xworkmate-bridge → gateway.request('xworkmate.session.prepare') Handler: openclaw-multi-session-plugins → recordXWorkmateSessionMapping() + prepareXWorkmateArtifacts() Inputs: schemaVersion: 1 appThreadKey: string // "draft:1780658097668838-1" openclawSessionKey: string // "agent:main:draft:1780658097668838-1" runId: string // "20260605-001" expectedArtifactDirs?: string[] workspaceDir?: string Process: 1. Validate typed mapping metadata. 2. Patch SessionEntry.pluginExtensions: ["openclaw-multi-session-plugins"]["xworkmate.sessionMapping"] → schemaVersion, appThreadKey, openclawSessionKey, expectedArtifactDirs 3. resolveWorkspaceDir({ openclawSessionKey, params, pluginConfig, config }) → Falls back through: explicit → pluginConfig → agent config → profile env → ~/.openclaw/workspace → Bridge must pass only a real OpenClaw workspace root here. App/owner scoped hints such as `/owners/...` are UI/sync references, not plugin workspace roots, and must fall back to the managed OpenClaw workspace. 4. safeScopeSegment(openclawSessionKey) → replace [/\\:*?"<>|] with "-", truncate to 96 chars 5. safeScopeSegment(runId) → same rules 6. scopeRoot = /tasks/// → fs.mkdir(scopeRoot, { recursive: true }) 7. Validate: isWithinRoot(workspaceRoot, scopeRoot) Output: artifactScope: "tasks///" artifactDirectory: "/tasks///" mapping: { appThreadKey, openclawSessionKey, expectedArtifactDirs } Fragile: - workspace resolution chain has 5 ordered sources - `remoteWorkingDirectoryHint` may be an app owner-scoped reference; using it as `workspaceDir` causes plugin `realpath(workspaceDir)` failures before `tasks///` can be created - session key format must match across bridge and plugin - no cleanup of old scope directories ``` ## State 2: Execute (GAP) ``` During agent turn execution, OpenClaw tools write output files. These go to various locations depending on the tool: Tool: browser (screenshot / download) → screenshot: saveMediaBuffer(buffer, "image/png", "browser") → ~/.openclaw/media/browser/.png → download: writeExternalFileWithinOutputRoot() → requested download path (bounded by output root security) → uploads: /tmp/openclaw/uploads/ Tool: image-generation → Returns GeneratedImageAsset { buffer, mimeType } (in-memory) → Agent must explicitly write buffer to disk → Usually saveMediaBuffer(buffer, mimeType, "browser") → ~/.openclaw/media/browser/.png Tool: video-generation / rendering → Output to media/ outbound or /tmp/ rendering workspace Tool: file-write (agent writes files explicitly) → May write to workingDirectory (pointed at tasks///) → OR may write to ~/.openclaw/agents//workspace/ ═══════════════════════════════════════════════════════════ CRITICAL: None of these paths are inside tasks/// unless the agent explicitly directs output there. The artifact export below will NOT find these files. ═══════════════════════════════════════════════════════════ ``` ## State 3: Collect And Snapshot ``` Caller: xworkmate-bridge → gateway.request('xworkmate.artifacts.collect-and-snapshot') Handler: openclaw-multi-session-plugins → collectAndSnapshotXWorkmateArtifacts() Inputs: openclawSessionKey: mapped OpenClaw session key runId: OpenClaw run id artifactScope: tasks/// sinceUnixMs: task start timestamp Process: 1. Validate artifactScope matches openclawSessionKey/runId. 2. Scan fixed OpenClaw output roots: - ~/.openclaw/media/ - /tmp/openclaw/ 3. Copy changed regular files into: - tasks///artifacts/media/... - tasks///artifacts/tmp-openclaw/... 4. Skip symlinks and any path that escapes the fixed source roots. Output: copiedFiles: relative paths under the current task scope warnings: skipped paths or unavailable source roots ``` ## State 4: Export ``` Caller: xworkmate-bridge → gateway.request('xworkmate.artifacts.export') Handler: openclaw-multi-session-plugins → exportXWorkmateArtifacts() Inputs: artifactScope: "tasks///" openclawSessionKey?: string workspaceDir?: string artifactRef?: string // alternative: read single artifact maxFiles?: number // default: 200 maxInlineBytes?: number // default: 512KB, files larger are omitted expectedArtifactDirs?: string[] // from typed mapping/session.prepare only Process: 1. resolveScopeRoot(workspaceRoot, artifactScope) → /tasks/// → Validate isWithinRoot() 2. collectCandidates(scopeRoot) → Recursive walk of ONLY tasks/// → Skip: .git, .openclaw, .xworkmate, .pi, .dart_tool, .next, .turbo, node_modules → Skip: symlinks (security measure) → Apply: artifact-ignore.md rules 2b. If the task scope has no candidates and `expectedArtifactDirs` is present: → Scan only those explicit workspace-root subdirectories → Keep exported entries bound to the current task artifactScope → Do not scan the workspace root broadly and do not borrow older task scopes Protocol boundary: `expectedArtifactDirs` is bridge artifact-contract data, not agent execution data. Bridge must not put it in `chat.send` params. Bridge must not probe old root-level, metadata-root, prompt-text, or `sessionKey` compatibility keys. 3. For each file under maxFiles limit: → Read content (up to maxInlineBytes) → Compute SHA-256 hash → Determine content-type from extension 4. Build artifact manifest: { scope: "tasks///", sessionKey: "", runId: "", totalCandidates: , files: [ { relativePath, displayPath, size, contentType, sha256, inline } ] } 5. Generate signed refs: → signArtifactRef(sessionKey, runId, relativePath) → HMAC-SHA256(signingSecret, "::::") → Valid for 24h Output: manifest: { scope, sessionKey, runId, totalCandidates, files[] } Each file: { relativePath, displayPath, size, contentType, sha256, inline?, ref } Fragile: - Export only scans tasks///; collect-and-snapshot must run first for global tool outputs - symlinks rejected even if pointing within workspace - maxFiles=200, maxInlineBytes=512KB — large files silently omitted - signing secret rotation invalidates all existing refs ``` ## State 5: Native Task Lookup And Artifact Snapshot ``` Caller: App polling/recovery via xworkmate.tasks.get Bridge forwards typed appThreadKey/openclawSessionKey/runId to Plugin Process: 1. Plugin resolves task state from OpenClaw native task-registry → api.runtime.tasks.runs.bindSession({sessionKey: openclawSessionKey}) → resolve(runId) or findLatest() 2. Call gateway.request('xworkmate.artifacts.collect-and-snapshot') → Copy OpenClaw media/tmp outputs into the task scope 3. Call gateway.request('xworkmate.artifacts.export') → Get manifest from plugin 4. openClawArtifactExport() → Transform manifest files into stable result shape → decorateOpenClawArtifactDownloadURLs() → Replace each file.ref with signed download URL: /artifacts/openclaw/download?ref=&t= 5. Return task-registry-backed snapshot: → Terminal success/failure is not decided by Bridge state → Missing native task record returns no_native_task_record { success: true, status: "completed", sessionId: "", threadId: "", turnId: "", runId: "", text: "", artifacts: { items: [{ path, url, sha256, size, contentType }], scope: "tasks///" } } 5. Do not store terminal task truth for xworkmate.tasks.get queries. The query path forwards to OpenClaw native task-registry through the plugin. 6. Send SSE session.update to app Removed compatibility paths: - Bridge no longer falls back when xworkmate.session.prepare is unsupported. - Bridge no longer reassociates OpenClaw tasks from artifactScope/runId. - Bridge no longer treats artifact export as terminal task-state evidence. Fragile: - If export returns empty manifest, snapshot has no artifacts - Artifact download URLs expire after 24h - Snapshot stored only in memory (lost on bridge restart) - App execution state must still transition to ready for any terminal snapshot. Empty or incomplete artifact manifests update only artifact sync status; they must not keep the task lifecycle running. ``` ## State 6: Download (Bridge Proxy) ``` Endpoint: GET /artifacts/openclaw/download?ref=&t= Handler: HandleOpenClawArtifactDownload() in openclaw_artifact_download.go Process: 1. Parse signed ref: :::::: 2. Verify HMAC signature against signing secret 3. Check expiry (24h TTL, t=) 4. Resolve artifact: → Build artifactScope: tasks/// → Call gateway.request('xworkmate.artifacts.read', { artifactScope, relativePath }) → Up to 3 retries on failure 5. Plugin's readXWorkmateArtifact(): → resolveScopeRoot(workspaceRoot, artifactScope) → Verify isWithinRoot() → Resolve file path: /tasks/// → Check file exists and not a symlink (reject symlinks to outside) → Read file content (up to 64MB max) → Return { content, contentType, size, sha256 } 6. Bridge streams response: → Content-Type from artifact metadata → Content-Length header → Supports Range header for partial content → Validate SHA-256 of received content Fragile: - Ref expiry: links stale after 24h - Secret rotation: all existing refs invalid - 64MB size limit - Symlink rejection: if agent created a symlink, download fails - 3 retry attempts only — persistent gateway failure = permanent 502 ``` ## State 7: Sync (App) ``` Location: xworkmate-app lib/runtime/desktop_thread_artifact_service.dart lib/app/app_controller_desktop_thread_actions.dart Process: 1. Receive terminal snapshot from bridge (SSE or task.get) 2. Check success=true AND artifacts.items[] present 3. For each artifact item: → Download via bridge's artifact download URL → Write to local workspace: ~/.xworkmate/threads// 4. Update TaskThread state: → lastArtifactSyncStatus = synced → lastTaskArtifactRelativePaths = [downloaded paths] → lastResultCode = success Failure paths: - success=false → lastResultCode=failed - success=true, no artifacts → lastArtifactSyncStatus=no-exported-artifacts - download failed → lastArtifactSyncStatus=failed - artifact missing from source → OPENCLAW_REQUIRED_ARTIFACT_MISSING Fragile: - Local workspace may have stale artifacts from previous runs - Concurrent writes to same thread directory (multiple turns) - Large artifact downloads block UI thread ``` ## Full State Machine ```mermaid stateDiagram-v2 [*] --> Prepare: bridge calls prepare Prepare --> Execute: scope dir created Execute --> Execute: agent writes files Execute --> Export: task complete Export --> Snapshot: manifest returned Snapshot --> Download: app requests Download --> Sync: file saved locally Sync --> [*]: artifact lifecycle complete Prepare --> PrepareFailed: gateway error / timeout Export --> NoArtifacts: scope dir empty or no matching files Download --> DownloadFailed: ref expired / file missing / secret rotated ``` ## Path Resolution Reference | Layer | What | Default Path | |-------|------|-------------| | Plugin workspace | resolveWorkspaceDir() | `~/.openclaw/workspace` | | Plugin scope | tasks/// | `/tasks///` | | Plugin export | exportXWorkmateArtifacts() | Scans only scope dir | | Bridge snapshot | xworkmate.tasks.get proxy | Native task-registry + plugin artifact manifest | | Bridge download | /artifacts/openclaw/download | Proxied from plugin read | | App sync | syncArtifactsFromBridge() | `~/.xworkmate/threads//` | | OpenClaw media | saveMediaBuffer(subdir) | `~/.openclaw/media//` | | OpenClaw temp | resolvePreferredOpenClawTmpDir() | `/tmp/openclaw/` |