14 KiB
14 KiB
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/<session>/<run>/ (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/<session>/ (xworkmate-app)
App terminal rule:
completed,failed,cancelled, andcanceledsnapshots end task execution immediately.- Artifact presence controls only
lastArtifactSyncStatus; it is not a reason to keeplifecycleStatus=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 = <workspace>/tasks/<safeSessionKey>/<safeRunId>/
→ fs.mkdir(scopeRoot, { recursive: true })
7. Validate: isWithinRoot(workspaceRoot, scopeRoot)
Output:
artifactScope: "tasks/<safeSessionKey>/<safeRunId>/"
artifactDirectory: "<workspace>/tasks/<safeSessionKey>/<safeRunId>/"
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/<session>/<run>/` 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/<uuid>.png
→ download: writeExternalFileWithinOutputRoot()
→ requested download path (bounded by output root security)
→ uploads: /tmp/openclaw/uploads/<file>
Tool: image-generation
→ Returns GeneratedImageAsset { buffer, mimeType } (in-memory)
→ Agent must explicitly write buffer to disk
→ Usually saveMediaBuffer(buffer, mimeType, "browser")
→ ~/.openclaw/media/browser/<uuid>.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/<session>/<run>/)
→ OR may write to ~/.openclaw/agents/<id>/workspace/
═══════════════════════════════════════════════════════════
CRITICAL: None of these paths are inside tasks/<session>/<run>/
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/<session>/<run>/
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/<session>/<run>/artifacts/media/...
- tasks/<session>/<run>/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/<session>/<run>/"
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)
→ <workspace>/tasks/<session>/<run>/
→ Validate isWithinRoot()
2. collectCandidates(scopeRoot)
→ Recursive walk of ONLY tasks/<session>/<run>/
→ 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/<session>/<run>/",
sessionKey: "<sessionKey>",
runId: "<runId>",
totalCandidates: <N>,
files: [
{ relativePath, displayPath, size, contentType, sha256, inline }
]
}
5. Generate signed refs:
→ signArtifactRef(sessionKey, runId, relativePath)
→ HMAC-SHA256(signingSecret, "<key>::<run>::<path>")
→ Valid for 24h
Output:
manifest: { scope, sessionKey, runId, totalCandidates, files[] }
Each file: { relativePath, displayPath, size, contentType, sha256, inline?, ref }
Fragile:
- Export only scans tasks/<session>/<run>/; 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=<signed>&t=<expiry>
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: "<id>",
threadId: "<id>",
turnId: "<id>",
runId: "<id>",
text: "<agent final output>",
artifacts: {
items: [{ path, url, sha256, size, contentType }],
scope: "tasks/<session>/<run>/"
}
}
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=<signed>&t=<expiry>
Handler: HandleOpenClawArtifactDownload() in openclaw_artifact_download.go
Process:
1. Parse signed ref: <sessionKey>::<runId>::<relativePath>::<hmac>
2. Verify HMAC signature against signing secret
3. Check expiry (24h TTL, t=<unixSeconds>)
4. Resolve artifact:
→ Build artifactScope: tasks/<session>/<run>/
→ 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: <workspace>/tasks/<session>/<run>/<relativePath>
→ 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/<session>/<artifact-relative>
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
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/// | <workspace>/tasks/<s>/<r>/ |
| 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/<s>/ |
| OpenClaw media | saveMediaBuffer(subdir) | ~/.openclaw/media/<subdir>/ |
| OpenClaw temp | resolvePreferredOpenClawTmpDir() | /tmp/openclaw/ |