fix: finalize openclaw task polling results
This commit is contained in:
parent
c4191fa5c9
commit
77be6981cc
@ -1,188 +1,265 @@
|
||||
# Cross-Repo Task State Workflow
|
||||
# Cross-Repo TaskThread Workflow
|
||||
|
||||
This document records the task-state workflow across:
|
||||
This document is the current TaskThread workflow record across:
|
||||
|
||||
- `xworkmate-app`
|
||||
- `xworkmate-bridge`
|
||||
- `openclaw-multi-session-plugins`
|
||||
|
||||
The core ownership split is:
|
||||
It replaces older local-classification and app-side artifact fallback descriptions. The current rule is:
|
||||
|
||||
- `xworkmate-app` owns task UI state, `TaskThread` persistence, local thread workspaces, and the OpenClaw submit queue.
|
||||
- `xworkmate-bridge` owns public session/routing contracts, provider compatibility, normalized results, and OpenClaw task-submit routing.
|
||||
- `openclaw-multi-session-plugins` owns OpenClaw task-scoped artifact preparation, export, and artifact-scope isolation.
|
||||
> The app owns TaskThread state and local sync. The bridge/OpenClaw terminal task snapshot is the truth source for final artifacts.
|
||||
|
||||
## Overall Flow
|
||||
## Ownership
|
||||
|
||||
### xworkmate-app
|
||||
|
||||
The app owns local state and UI only:
|
||||
|
||||
- Resolve or create the current `TaskThread` by `sessionKey` / `threadId`.
|
||||
- Ensure the local workspace at `$HOME/.xworkmate/threads/<session>`.
|
||||
- Build the external `TaskThread workspace context` prompt prefix.
|
||||
- Send `metadata.xworkmateTaskArtifactContract`.
|
||||
- Queue OpenClaw gateway work locally when the constrained OpenClaw lane is busy.
|
||||
- Apply normalized bridge results to `TaskThread.lifecycleState`.
|
||||
- Sync bridge-provided artifacts into the local TaskThread workspace.
|
||||
|
||||
The app must not infer required final file types from user text. It must not treat partial artifacts as final deliverables.
|
||||
|
||||
### xworkmate-bridge
|
||||
|
||||
The bridge owns the public protocol boundary:
|
||||
|
||||
- Expose the unified `/acp/rpc` entrypoint.
|
||||
- Resolve routing for agent providers and gateway/OpenClaw.
|
||||
- Normalize provider/OpenClaw results into a stable result shape.
|
||||
- Stream `session.update` events.
|
||||
- Serve `xworkmate.tasks.get` snapshots for asynchronous recovery and terminal result lookup.
|
||||
- Serve `xworkmate.tasks.cancel` for OpenClaw task cancellation.
|
||||
|
||||
The bridge should return standard contract fields such as `artifacts`, `artifacts.items`, `files`, or `attachments`. App-side support for ad hoc final-artifact field names is not a compatibility layer.
|
||||
|
||||
### openclaw-multi-session-plugins
|
||||
|
||||
OpenClaw plugins own execution-time artifact scope:
|
||||
|
||||
- Create a run.
|
||||
- Allocate `runId`.
|
||||
- Prepare `artifactScope = tasks/<session>/<run>`.
|
||||
- Execute the user task.
|
||||
- Export real final deliverables into the current task artifact scope.
|
||||
- Validate that artifact scope matches the current session and run.
|
||||
- Return artifact refs/files to the bridge.
|
||||
|
||||
Text-only file claims, placeholder files, global workspace files, previous-run files, and partial/intermediate artifacts are not final deliverables.
|
||||
|
||||
## Main Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["User input / follow-up"] --> APP["xworkmate-app<br/>TaskThread + UI state"]
|
||||
APP -->|session.start / session.message| BR["xworkmate-bridge<br/>/acp/rpc"]
|
||||
BR -->|xworkmate.routing.resolve| ROUTE{"Execution target"}
|
||||
ROUTE -->|single-agent| AG["codex / opencode / gemini / hermes"]
|
||||
ROUTE -->|gateway=openclaw| OC["OpenClaw Gateway Runtime"]
|
||||
OC --> PLUG["openclaw-multi-session-plugins<br/>task-scoped artifacts"]
|
||||
PLUG -->|artifactRef / files / downloadUrl| BR
|
||||
BR -->|normalized result / SSE update| APP
|
||||
APP -->|write files| LOCAL["$HOME/.xworkmate/threads/<session>"]
|
||||
APP -->|persist index| STORE["~/Library/Application Support/xworkmate/tasks/threads.json"]
|
||||
flowchart TD
|
||||
U["User input / follow-up"] --> UI["Assistant UI"]
|
||||
UI --> APP["AppController / TaskThread"]
|
||||
|
||||
APP --> T0{"Current TaskThread exists?"}
|
||||
T0 -->|No| T1["Create draft TaskThread<br/>sessionKey / threadId"]
|
||||
T0 -->|Yes| T2["Load current TaskThread"]
|
||||
T1 --> W["Prepare local workspace<br/>$HOME/.xworkmate/threads/<session>"]
|
||||
T2 --> W
|
||||
|
||||
W --> C["Build external task context<br/>TaskThread workspace context"]
|
||||
C --> M["Attach metadata<br/>xworkmateTaskArtifactContract"]
|
||||
|
||||
M --> R{"Execution target"}
|
||||
R -->|Agent: codex / opencode / gemini / hermes| B1["bridge /acp/rpc<br/>session.start or session.message"]
|
||||
R -->|Gateway: OpenClaw| Q{"OpenClaw local lane idle?"}
|
||||
|
||||
Q -->|Idle| B2["bridge /acp/rpc<br/>session.start or session.message"]
|
||||
Q -->|Busy| Q1["Local OpenClaw queue<br/>lifecycleStatus=queued"]
|
||||
Q1 --> Q2["drainOpenClawGatewayQueue"]
|
||||
Q2 --> B2
|
||||
|
||||
B1 --> BR["xworkmate-bridge<br/>routing + protocol normalization"]
|
||||
B2 --> BR
|
||||
|
||||
BR --> P{"Provider / runtime"}
|
||||
P -->|Agent provider| A["Codex / Opencode / Gemini / Hermes"]
|
||||
P -->|OpenClaw| O["OpenClaw runtime"]
|
||||
O --> OS["OpenClaw task artifact scope<br/>tasks/<session>/<run>"]
|
||||
|
||||
A --> N["Bridge normalized result"]
|
||||
OS --> N
|
||||
|
||||
N --> S{"Return path"}
|
||||
S -->|Final response| APP2["APP applyGatewayChatResult"]
|
||||
S -->|SSE update| SSE["APP receives session.update"]
|
||||
S -->|SSE closed / no final envelope| POLL["Transport calls xworkmate.tasks.get<br/>until terminal snapshot"]
|
||||
|
||||
SSE --> H{"Running task handle?"}
|
||||
H -->|Yes| POLL
|
||||
H -->|No| APP2
|
||||
POLL --> APP2
|
||||
|
||||
APP2 --> OK{"success=true and artifacts present?"}
|
||||
OK -->|Yes| SYNC["Sync artifacts to local TaskThread workspace"]
|
||||
OK -->|No, failure| FAIL["Record lastResultCode<br/>lastArtifactSyncStatus=failed"]
|
||||
OK -->|No exported artifacts guard| NOART["lastArtifactSyncStatus=no-exported-artifacts"]
|
||||
|
||||
SYNC --> READY["TaskThread ready"]
|
||||
FAIL --> READY
|
||||
NOART --> READY
|
||||
```
|
||||
|
||||
## App TaskThread State Machine
|
||||
## TaskThread State Machine
|
||||
|
||||
`TaskThread.lifecycleState.status` is intentionally small. Most terminal outcomes return to `ready`; the specific terminal result is stored in `lastResultCode`.
|
||||
`TaskThread.lifecycleState.status` is intentionally small. Most terminal outcomes return to `ready`; result detail is stored in `lastResultCode`.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Ready: create task / restore persisted task
|
||||
[*] --> Ready: create / restore TaskThread
|
||||
|
||||
Ready --> Queued: OpenClaw gateway active slot is full
|
||||
Ready --> Running: sendChatMessage
|
||||
Ready --> Queued: OpenClaw lane busy
|
||||
Queued --> Running: drainOpenClawGatewayQueue
|
||||
Queued --> Ready: abortRun\nlastResultCode=aborted
|
||||
Queued --> Ready: queue full\nlastResultCode=OPENCLAW_GATEWAY_QUEUE_FULL
|
||||
Queued --> Ready: abort / queue full
|
||||
|
||||
Ready --> Running: sendChatMessage\nmarkGatewayChatRun
|
||||
Running --> Ready: GoTaskServiceResult.success=true\nlastResultCode=success
|
||||
Running --> Ready: result.status / result.code\nlastResultCode=<status|code>
|
||||
Running --> Ready: no displayable output\nlastResultCode=failed
|
||||
Running --> Ready: ACP interrupted / timeout\nlastResultCode=ACP_*
|
||||
Running --> Ready: abortRun\nlastResultCode=aborted
|
||||
Running --> Running: SSE delta / session.update
|
||||
Running --> Running: OpenClaw running handle
|
||||
Running --> Running: xworkmate.tasks.get polling
|
||||
|
||||
Running --> Ready: success
|
||||
Running --> Ready: failed / artifact_missing
|
||||
Running --> Ready: ACP_HTTP_* / unrecovered SSE interruption
|
||||
Running --> Ready: xworkmate.tasks.get failure after transport recovery
|
||||
Running --> Ready: abort
|
||||
|
||||
Ready --> Archived: user archives task
|
||||
Archived --> Ready: user restores task
|
||||
|
||||
note right of Ready
|
||||
lifecycleStatus terminal state is usually ready.
|
||||
Read lastResultCode for result detail:
|
||||
success / failed / error / aborted /
|
||||
artifact_missing / ACP_HTTP_*
|
||||
lifecycleStatus=ready does not mean success.
|
||||
Read lastResultCode for terminal detail:
|
||||
success / failed / aborted /
|
||||
artifact_missing / ACP_HTTP_* /
|
||||
OPENCLAW_REQUIRED_ARTIFACT_MISSING.
|
||||
end note
|
||||
|
||||
note right of Running
|
||||
Running does not mean final files exist.
|
||||
An OpenClaw running handle is only an async task handle.
|
||||
The transport must continue polling the bridge task snapshot.
|
||||
end note
|
||||
```
|
||||
|
||||
## Task Workspace Context Injection
|
||||
|
||||
Every app-owned task has a local workspace under `$HOME/.xworkmate/threads/<session>`. For remote execution, the bridge/runtime may also resolve a remote task workspace hint. The app passes the task workspace in two ways:
|
||||
|
||||
- Structured request fields: `workingDirectory` and, when available, `remoteWorkingDirectoryHint`.
|
||||
- External conversation context: a `TaskThread workspace context` prefix is added to the prompt sent to Bridge/OpenClaw.
|
||||
|
||||
The local chat transcript still stores the user's original text. Only the external task prompt is enriched, so the UI does not show internal workspace rules as user content.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as "XWorkmate UI"
|
||||
participant APP as "TaskThread runtime"
|
||||
participant BR as "Bridge session"
|
||||
participant OC as "OpenClaw / provider"
|
||||
participant WS as "Task workspace"
|
||||
|
||||
UI->>APP: "user message"
|
||||
APP->>WS: "ensure $HOME/.xworkmate/threads/<session>"
|
||||
APP->>APP: "build TaskThread workspace context"
|
||||
APP->>BR: "taskPrompt + workingDirectory + remoteWorkingDirectoryHint"
|
||||
BR->>OC: "session.start / session.message"
|
||||
OC->>WS: "final files must be exported into task scope"
|
||||
BR-->>APP: "result + artifact refs"
|
||||
APP->>WS: "sync inline/downloaded artifacts"
|
||||
```
|
||||
|
||||
Prompt-level workspace rules are deliberately strict. `remoteWorkingDirectoryHint` is the writable task workspace for remote OpenClaw/provider execution when present; otherwise `workingDirectory` is used.
|
||||
|
||||
- Treat the current task workspace as the only writable workspace for the task execution.
|
||||
- Create, modify, and export task files inside that workspace or its task artifact scope.
|
||||
- Do not use global OpenClaw media/cache paths, `/tmp`, Downloads, Desktop, or other arbitrary directories as final deliverable locations.
|
||||
- If a tool creates output outside the task workspace, copy/export the final deliverables into the task workspace before claiming completion.
|
||||
- Prefer local task-workspace paths, or paths relative to that workspace, when reporting files back to the user.
|
||||
|
||||
## OpenClaw Gateway Queue
|
||||
|
||||
The app serializes OpenClaw gateway execution locally because OpenClaw task execution is treated as a constrained gateway lane.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["APP selects Gateway + OpenClaw"] --> B{"active < 1 ?"}
|
||||
B -->|yes| C["Run immediately<br/>lifecycleStatus=running"]
|
||||
B -->|no| D{"queue < 20 ?"}
|
||||
D -->|yes| E["Enqueue<br/>lifecycleStatus=queued<br/>lastArtifactSyncStatus=queued"]
|
||||
D -->|no| F["Queue full<br/>lastResultCode=OPENCLAW_GATEWAY_QUEUE_FULL"]
|
||||
|
||||
E --> G["drain queue"]
|
||||
G --> C
|
||||
C --> H["Bridge /acp/rpc<br/>routing=gateway/openclaw"]
|
||||
H --> I["OpenClaw execution"]
|
||||
I --> J{"Result"}
|
||||
J -->|success + output/files| K["APP ready<br/>lastResultCode=success<br/>sync artifacts"]
|
||||
J -->|artifact guard| L["APP ready<br/>lastResultCode=artifact_missing"]
|
||||
J -->|failure/interruption| M["APP ready<br/>lastResultCode=failed/error/ACP_*"]
|
||||
```
|
||||
|
||||
## Bridge Session And Routing Workflow
|
||||
|
||||
The bridge exposes one public session contract while keeping provider-specific behavior behind bridge-owned routing. OpenClaw task submit uses `/acp/rpc` with explicit gateway routing metadata, not a separate app-facing path.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
REQ["APP request"] --> EP{"HTTP / WS entry"}
|
||||
EP -->|/acp or /acp/rpc| RPC["General JSON-RPC"]
|
||||
|
||||
RPC --> METHOD{"method"}
|
||||
METHOD -->|acp.capabilities| CAP["Return agent providers + gatewayProviders=openclaw"]
|
||||
METHOD -->|xworkmate.routing.resolve| ROUTE["Resolve single-agent / gateway"]
|
||||
METHOD -->|session.start| START["Create / start session turn"]
|
||||
METHOD -->|session.message| MSG["Continue existing session"]
|
||||
METHOD -->|session.cancel / session.close| CTRL["Cancel / close session"]
|
||||
|
||||
START --> ORCH["session_orchestrator"]
|
||||
MSG --> ORCH
|
||||
ORCH --> PROVIDER{"provider compat"}
|
||||
PROVIDER -->|single-agent| SA["codex / opencode / gemini / hermes"]
|
||||
PROVIDER -->|gateway| OCG["OpenClaw runtime"]
|
||||
|
||||
SA --> NORM["normalized result"]
|
||||
OCG --> NORM
|
||||
NORM --> OUT["success / status / turnId / output / artifacts / resolved*"]
|
||||
```
|
||||
|
||||
## OpenClaw Plugin Artifact Scope
|
||||
|
||||
OpenClaw artifacts are scoped by task session and run. This prevents one task or turn from borrowing files from another.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CTX["OpenClaw plugin context<br/>sessionKey + runId + workspaceDir"] --> PREP["prepareXWorkmateArtifacts"]
|
||||
PREP --> SCOPE["artifactScope = tasks/<sessionKey>/<runId>"]
|
||||
SCOPE --> DIR["artifactDirectory"]
|
||||
DIR --> RUN["OpenClaw writes files"]
|
||||
RUN --> EXPORT["exportXWorkmateArtifacts"]
|
||||
EXPORT --> VALIDATE{"scope matches sessionKey/runId ?"}
|
||||
VALIDATE -->|no| ERR["Reject cross-task / cross-run artifact"]
|
||||
VALIDATE -->|yes| MANIFEST["manifest + artifactRef + files"]
|
||||
MANIFEST --> BR["Bridge result"]
|
||||
BR --> APP["APP downloads or inlines into local thread workspace"]
|
||||
|
||||
note right of SCOPE
|
||||
Concurrent task isolation is based on:
|
||||
tasks/<session>/<run>
|
||||
Do not reuse other sessions or previous run files.
|
||||
end note
|
||||
```
|
||||
|
||||
## Status Field Mapping
|
||||
## State Field Relationship
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
APP1["APP TaskThread.lifecycleState.status"] --> A1["queued / running / ready / archived"]
|
||||
APP2["APP TaskThread.lifecycleState.lastResultCode"] --> A2["queued / running / success / failed / error / aborted / artifact_missing / ACP_HTTP_*"]
|
||||
APP3["APP lastArtifactSyncStatus"] --> A3["queued / running / synced / no-artifacts / failed"]
|
||||
BR1["Bridge result.status"] --> B1["available / success / failed / provider status"]
|
||||
BR2["Bridge result.success"] --> B2["true / false"]
|
||||
PL1["Plugin artifact export"] --> P1["scopeKind=task<br/>artifactScope=tasks/session/run"]
|
||||
LS["TaskThread.lifecycleStatus"] --> LS2["ready / queued / running / archived"]
|
||||
RC["TaskThread.lastResultCode"] --> RC2["success / failed / aborted / artifact_missing / ACP_HTTP_* / OPENCLAW_*"]
|
||||
AS["TaskThread.lastArtifactSyncStatus"] --> AS2["queued / running / syncing / synced / partial / failed / no-artifacts / no-exported-artifacts"]
|
||||
AP["TaskThread.lastTaskArtifactRelativePaths"] --> AP2["Only files synced from the current run"]
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `lifecycleStatus=ready` only means no task is currently running.
|
||||
- Success or failure is read from `lastResultCode`.
|
||||
- The artifact panel reads `lastArtifactSyncStatus` and `lastTaskArtifactRelativePaths`.
|
||||
- `lastTaskArtifactRelativePaths` must only contain artifacts from the current run.
|
||||
|
||||
## App Request Contract
|
||||
|
||||
App requests to bridge use the unified ACP entrypoint. OpenClaw is selected with routing metadata, not with an app-facing OpenClaw URL.
|
||||
|
||||
The app sends:
|
||||
|
||||
- `sessionId`
|
||||
- `threadId`
|
||||
- `prompt`
|
||||
- `workingDirectory`
|
||||
- `remoteWorkingDirectoryHint`
|
||||
- `routing`
|
||||
- `metadata.xworkmateTaskArtifactContract`
|
||||
|
||||
For Gateway/OpenClaw, the app injects `currentTaskWorkspace` into both prompt context and metadata. The remote runtime must export final files into the current task artifact scope before returning success.
|
||||
|
||||
The prompt context contains:
|
||||
|
||||
- `sessionKey`
|
||||
- local workspace
|
||||
- remote workspace hint when available
|
||||
- current task workspace
|
||||
- workspace isolation rules
|
||||
- final artifact contract rules
|
||||
|
||||
The local transcript stores the user's original text. Internal workspace context is only sent to the external task runtime.
|
||||
|
||||
## Bridge / OpenClaw Async Contract
|
||||
|
||||
The transport handles these bridge methods:
|
||||
|
||||
- `session.start`: start a new turn.
|
||||
- `session.message`: continue an existing TaskThread.
|
||||
- `xworkmate.tasks.get`: recover or query terminal task snapshot.
|
||||
- `xworkmate.tasks.cancel`: cancel a running OpenClaw task.
|
||||
|
||||
Important recovery rule:
|
||||
|
||||
> A running OpenClaw task handle is never a final result.
|
||||
|
||||
When `session.update` contains `status=running` with `runId` and `artifactScope`, the app transport must use those fields only as query parameters for `xworkmate.tasks.get`. It must continue polling until the bridge returns a terminal snapshot:
|
||||
|
||||
- `completed` with artifacts -> success path.
|
||||
- `failed`, `artifact_missing`, `cancelled`, or `canceled` -> failure path.
|
||||
- no terminal snapshot after recovery attempts -> unrecovered interruption path.
|
||||
|
||||
If a persisted OpenClaw running association is polled later and `xworkmate.tasks.get` fails after transport-level recovery, the app must record the concrete diagnostic code, clear the pending run, and return the TaskThread to `ready`. It must not silently swallow the failure and leave the task without an execution result.
|
||||
|
||||
## Artifact Rules
|
||||
|
||||
The app syncs artifacts only when the bridge/OpenClaw terminal result establishes them as current-run final artifacts.
|
||||
|
||||
Valid success:
|
||||
|
||||
- `success=true`
|
||||
- terminal bridge/OpenClaw snapshot
|
||||
- standard artifact contract fields are present
|
||||
- artifact scope belongs to the current session and run
|
||||
|
||||
Failure / no-sync cases:
|
||||
|
||||
- `success=false` with partial artifacts
|
||||
- `OPENCLAW_REQUIRED_ARTIFACT_MISSING`
|
||||
- `artifact_missing`
|
||||
- `no-exported-artifacts`
|
||||
- text-only file/path claims
|
||||
- files from previous runs
|
||||
- files from global OpenClaw workspace/cache
|
||||
- placeholder artifacts generated by a guard
|
||||
|
||||
`openclaw returned partial artifacts without required final deliverables` means the remote artifact contract was not satisfied. It is not an app-side UI classification failure.
|
||||
|
||||
## OpenClaw Artifact Scope
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CTX["OpenClaw context<br/>sessionKey + runId + workspaceDir"] --> PREP["prepareXWorkmateArtifacts"]
|
||||
PREP --> SCOPE["artifactScope = tasks/<session>/<run>"]
|
||||
SCOPE --> DIR["artifactDirectory"]
|
||||
DIR --> RUN["OpenClaw writes files"]
|
||||
RUN --> EXPORT["exportXWorkmateArtifacts"]
|
||||
EXPORT --> VALIDATE{"scope matches session/run?"}
|
||||
VALIDATE -->|No| ERR["Reject cross-session / cross-run artifact"]
|
||||
VALIDATE -->|Yes| MANIFEST["manifest + artifact refs/files"]
|
||||
MANIFEST --> BR["Bridge terminal snapshot"]
|
||||
BR --> APP["App syncs current-run artifacts"]
|
||||
```
|
||||
|
||||
## Boundary Rules
|
||||
|
||||
- The app does not store OpenClaw URLs. It only consumes bridge capabilities where `gatewayProviders` includes `openclaw`.
|
||||
- OpenClaw `session.start` and `session.message` use `/acp/rpc` with explicit OpenClaw gateway routing metadata; `/gateway/openclaw` is not an app-facing endpoint.
|
||||
- Follow-up conversation uses the same `sessionKey` / `threadId`. Bridge `session.message` must continue the provider session state or return a structured continuation error.
|
||||
- Artifact ownership is enforced by `openclaw-multi-session-plugins` with `tasks/<session>/<run>` scope. The app syncs only the current run's artifacts into the local thread workspace.
|
||||
- Upgrade/install flows must preserve real local history. Cleanup must only remove explicitly known test-pollution session keys.
|
||||
- The app does not store or construct OpenClaw runtime URLs.
|
||||
- OpenClaw `session.start` and `session.message` use `/acp/rpc` with explicit routing metadata.
|
||||
- `/gateway/openclaw` and provider-specific paths are not app-facing paths.
|
||||
- The bridge/OpenClaw terminal task snapshot is the final artifact truth source.
|
||||
- App local workspace contents are not used to decide whether a remote run produced final deliverables.
|
||||
- Compatibility/fallback fields for final artifacts are not allowed unless an explicit bridge contract requires them.
|
||||
|
||||
@ -364,7 +364,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
connectionState = assistantConnectionStateForSession(
|
||||
normalizedSessionKey,
|
||||
);
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('Gateway capability refresh fallback: $error');
|
||||
// Fallback to existing connection state if refresh fails.
|
||||
}
|
||||
}
|
||||
@ -411,7 +412,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
if (providerCatalogForExecutionTarget(currentTarget).isEmpty) {
|
||||
try {
|
||||
await refreshSingleAgentCapabilitiesInternal(forceRefresh: true);
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('Gateway provider catalog refresh fallback: $error');
|
||||
// Keep the local guard focused on the post-refresh catalog state.
|
||||
}
|
||||
if (providerCatalogForExecutionTarget(currentTarget).isEmpty) {
|
||||
@ -793,8 +795,19 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
return;
|
||||
} catch (_) {
|
||||
continue;
|
||||
} catch (error) {
|
||||
if (aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
||||
await applyGatewayChatFailureInternal(
|
||||
sessionKey: sessionKey,
|
||||
target: target,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toDouble();
|
||||
@ -1573,7 +1586,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
sessionKey,
|
||||
)?.openClawTaskAssociation,
|
||||
);
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('OpenClaw cancellation fallback: $error');
|
||||
// Best effort cancellation only. Local state must still leave pending.
|
||||
}
|
||||
removeQueuedOpenClawGatewayTurnsForSessionInternal(sessionKey);
|
||||
@ -1587,7 +1601,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
Future<void> prepareForExit() async {
|
||||
try {
|
||||
await abortRun();
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('Prepare for exit abort fallback: $error');
|
||||
// Best effort only. Native termination still proceeds.
|
||||
}
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
|
||||
@ -163,7 +163,8 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
}
|
||||
try {
|
||||
Directory(normalizedPath).createSync(recursive: true);
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('Ensure local thread workspace fallback: $error');
|
||||
// Best effort only. The caller can still decide whether to fail fast.
|
||||
}
|
||||
return Directory(normalizedPath).existsSync();
|
||||
|
||||
@ -43,7 +43,7 @@ Future<MediaStream?> desktopRemoteVideoStreamForTrack(
|
||||
}
|
||||
|
||||
final stream = await createFallbackStream('xworkmate-remote-desktop');
|
||||
await stream.addTrack(event.track);
|
||||
await stream.addTrack(event.track, addToNative: false);
|
||||
return stream;
|
||||
}
|
||||
|
||||
|
||||
@ -86,6 +86,14 @@ void main() {
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('unit-fixture-task-a');
|
||||
controller.initializeAssistantThreadContext(
|
||||
'unit-fixture-task-a',
|
||||
executionTarget: AssistantExecutionTarget.agent,
|
||||
messageViewMode: controller.assistantMessageViewModeForSession(
|
||||
'unit-fixture-task-a',
|
||||
),
|
||||
);
|
||||
controller.notifyListeners();
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(child: _buildLowerPane(controller: controller)),
|
||||
@ -107,7 +115,7 @@ void main() {
|
||||
find.byKey(const Key('assistant-provider-menu-item-gemini')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byIcon(Icons.check_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.check_rounded), findsOneWidget);
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-provider-menu-item-codex')),
|
||||
);
|
||||
@ -372,81 +380,71 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'allows switching from Gateway back to Agent when bridge reports both',
|
||||
(tester) async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
],
|
||||
initialGatewayProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.openclaw,
|
||||
],
|
||||
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.agent,
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
testWidgets('shows Agent and Gateway modes when bridge reports both', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
],
|
||||
initialGatewayProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.openclaw,
|
||||
],
|
||||
initialAvailableExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.agent,
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
await controller.sessionsController.switchSession('unit-fixture-task-a');
|
||||
controller.initializeAssistantThreadContext(
|
||||
'unit-fixture-task-a',
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
messageViewMode: controller.assistantMessageViewModeForSession(
|
||||
'unit-fixture-task-a',
|
||||
);
|
||||
controller.initializeAssistantThreadContext(
|
||||
'unit-fixture-task-a',
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
messageViewMode: controller.assistantMessageViewModeForSession(
|
||||
'unit-fixture-task-a',
|
||||
),
|
||||
);
|
||||
controller.notifyListeners();
|
||||
),
|
||||
);
|
||||
controller.notifyListeners();
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(child: _buildLowerPane(controller: controller)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(child: _buildLowerPane(controller: controller)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.currentAssistantExecutionTarget.isGateway, isTrue);
|
||||
expect(controller.currentAssistantExecutionTarget.isGateway, isTrue);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-execution-target-button')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-execution-target-button')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-agent')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-gateway')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-agent')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-gateway')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-agent')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.currentAssistantExecutionTarget.isAgent, isTrue);
|
||||
expect(
|
||||
controller
|
||||
.providerCatalogForExecutionTarget(AssistantExecutionTarget.agent)
|
||||
.map((provider) => provider.providerId),
|
||||
const <String>['codex', 'opencode'],
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.providerCatalogForExecutionTarget(
|
||||
AssistantExecutionTarget.gateway,
|
||||
)
|
||||
.map((provider) => provider.providerId),
|
||||
const <String>[kCanonicalGatewayProviderId],
|
||||
);
|
||||
},
|
||||
);
|
||||
expect(controller.currentAssistantExecutionTarget.isGateway, isTrue);
|
||||
expect(
|
||||
controller
|
||||
.providerCatalogForExecutionTarget(AssistantExecutionTarget.agent)
|
||||
.map((provider) => provider.providerId),
|
||||
const <String>['codex', 'opencode'],
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.providerCatalogForExecutionTarget(AssistantExecutionTarget.gateway)
|
||||
.map((provider) => provider.providerId),
|
||||
const <String>[kCanonicalGatewayProviderId],
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('uses submit button instead of connect action', (tester) async {
|
||||
final controller = AppController(
|
||||
|
||||
@ -994,7 +994,7 @@ void main() {
|
||||
);
|
||||
for (
|
||||
var attempt = 0;
|
||||
attempt < 20 &&
|
||||
attempt < 300 &&
|
||||
controller
|
||||
.requireTaskThreadForSessionInternal('unit-fixture-task-a')
|
||||
.lastArtifactSyncStatus !=
|
||||
|
||||
@ -24,6 +24,7 @@ const List<String> _openClawE2ECanonicalPrompts = <String>[
|
||||
'围绕\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 右侧是当下 \n测试制作视频',
|
||||
'围绕\n\n从单机权限 → 网络边界 → Web安全 → 云身份 → Zero Trust → AI Agent 身份 → AI模型与知识保护 演进 \n\n拆章节 -> 每章调用 Codex -> 每章 GPT images2 生成图 -> 汇总排版 -> 制作视频',
|
||||
];
|
||||
const Duration _openClawE2ESubmitTimeout = Duration(seconds: 10);
|
||||
|
||||
void main() {
|
||||
group('AssistantExecutionTarget', () {
|
||||
@ -1366,7 +1367,9 @@ void main() {
|
||||
expect(request.prompt, contains('XWorkmate task artifact contract:'));
|
||||
expect(
|
||||
request.prompt,
|
||||
contains('export the final deliverables through the current XWorkmate task artifact scope'),
|
||||
contains(
|
||||
'export the final deliverables through the current XWorkmate task artifact scope',
|
||||
),
|
||||
);
|
||||
expect(request.prompt, contains('最后 输出 PDF文件'));
|
||||
},
|
||||
@ -1628,7 +1631,10 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localWorkspace.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
@ -1859,7 +1865,10 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localWorkspace.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
@ -2005,21 +2014,18 @@ void main() {
|
||||
() async {
|
||||
final fakeGoTaskService = _RecordingGoTaskServiceClient()
|
||||
..outcomes.add(
|
||||
goTaskServiceResultFromAcpResponse(
|
||||
const <String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'nested-openclaw-artifact-error',
|
||||
'result': <String, dynamic>{
|
||||
'status': 'failed',
|
||||
'error': <String, dynamic>{
|
||||
'code': 'OPENCLAW_REQUIRED_ARTIFACT_MISSING',
|
||||
'message':
|
||||
'openclaw returned partial artifacts without required final deliverables',
|
||||
},
|
||||
goTaskServiceResultFromAcpResponse(const <String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'nested-openclaw-artifact-error',
|
||||
'result': <String, dynamic>{
|
||||
'status': 'failed',
|
||||
'error': <String, dynamic>{
|
||||
'code': 'OPENCLAW_REQUIRED_ARTIFACT_MISSING',
|
||||
'message':
|
||||
'openclaw returned partial artifacts without required final deliverables',
|
||||
},
|
||||
},
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
}, route: GoTaskServiceRoute.externalAcpSingle),
|
||||
)
|
||||
..outcomes.add(
|
||||
const GoTaskServiceResult(
|
||||
@ -2107,7 +2113,10 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localWorkspace.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
@ -2172,7 +2181,10 @@ void main() {
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localWorkspace.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localWorkspace.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession(
|
||||
@ -2504,7 +2516,10 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const sessionA = 'background-task-a';
|
||||
@ -2626,7 +2641,10 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const prompt = '用户要求我生成一个关于现代AI基础设施的技术营销内容';
|
||||
@ -2794,7 +2812,10 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const prompt = '用户要求我生成一个关于现代AI基础设施的技术营销内容';
|
||||
@ -2888,7 +2909,10 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.switchSession('artifact-only-task');
|
||||
@ -2952,7 +2976,10 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.switchSession('terminal-failure-task');
|
||||
@ -3029,7 +3056,10 @@ void main() {
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.switchSession('empty-output-task');
|
||||
@ -3341,7 +3371,7 @@ void main() {
|
||||
await expectLater(
|
||||
controller
|
||||
.sendChatMessage(prompts[index])
|
||||
.timeout(const Duration(seconds: 2)),
|
||||
.timeout(_openClawE2ESubmitTimeout),
|
||||
completes,
|
||||
);
|
||||
}
|
||||
@ -3375,7 +3405,10 @@ void main() {
|
||||
'xworkmate-openclaw-five-e2e-',
|
||||
);
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedGatewayController(fakeGoTaskService, homeDir: localHome.path);
|
||||
final controller = _connectedGatewayController(
|
||||
fakeGoTaskService,
|
||||
homeDir: localHome.path,
|
||||
);
|
||||
addTearDown(() async {
|
||||
fakeGoTaskService.completeAll();
|
||||
controller.dispose();
|
||||
@ -3394,7 +3427,7 @@ void main() {
|
||||
await expectLater(
|
||||
controller
|
||||
.sendChatMessage(_openClawE2ECanonicalPrompts[index])
|
||||
.timeout(const Duration(seconds: 2)),
|
||||
.timeout(_openClawE2ESubmitTimeout),
|
||||
completes,
|
||||
);
|
||||
}
|
||||
@ -4079,6 +4112,72 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('OpenClaw task snapshot failure records a terminal result', () async {
|
||||
final fakeGoTaskService = _RecordingGoTaskServiceClient()
|
||||
..outcomes.add(
|
||||
const GoTaskServiceResult(
|
||||
success: true,
|
||||
message: '',
|
||||
turnId: 'turn-openclaw-poll-failed',
|
||||
raw: <String, dynamic>{
|
||||
'success': true,
|
||||
'status': 'running',
|
||||
'sessionId': 'openclaw-poll-failed-task',
|
||||
'threadId': 'openclaw-poll-failed-task',
|
||||
'turnId': 'turn-openclaw-poll-failed',
|
||||
'runId': 'run-openclaw-poll-failed',
|
||||
'artifactScope':
|
||||
'tasks/openclaw-poll-failed-task/run-openclaw-poll-failed',
|
||||
'artifactDirectory':
|
||||
'/tmp/tasks/openclaw-poll-failed-task/run-openclaw-poll-failed',
|
||||
'gatewayProviderId': 'openclaw',
|
||||
'runtimeBudgetMinutes': 1,
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
)
|
||||
..taskOutcomes.add(
|
||||
const GatewayAcpException(
|
||||
'ACP HTTP connection closed before the OpenClaw task snapshot returned',
|
||||
code: 'ACP_HTTP_CONNECTION_CLOSED',
|
||||
),
|
||||
);
|
||||
final controller = _connectedGatewayController(fakeGoTaskService);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _selectGatewaySession(controller, 'openclaw-poll-failed-task');
|
||||
|
||||
await expectLater(
|
||||
controller
|
||||
.sendChatMessage('输出 PDF')
|
||||
.timeout(const Duration(seconds: 2)),
|
||||
completes,
|
||||
);
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
final failedThread = controller.requireTaskThreadForSessionInternal(
|
||||
'openclaw-poll-failed-task',
|
||||
);
|
||||
expect(failedThread.lifecycleState.status, 'ready');
|
||||
expect(
|
||||
failedThread.lifecycleState.lastResultCode,
|
||||
'ACP_HTTP_CONNECTION_CLOSED',
|
||||
);
|
||||
expect(failedThread.lastArtifactSyncStatus, 'failed');
|
||||
expect(failedThread.openClawTaskAssociation, isNull);
|
||||
expect(
|
||||
controller.assistantSessionHasPendingRun('openclaw-poll-failed-task'),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
controller.chatMessages.map((message) => message.text).join('\n'),
|
||||
contains('ACP_HTTP_CONNECTION_CLOSED'),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'sendChatMessage resumes existing interrupted and error states',
|
||||
() async {
|
||||
@ -4494,7 +4593,8 @@ Future<void> _resilientDelete(Directory dir) async {
|
||||
try {
|
||||
await dir.delete(recursive: true);
|
||||
return;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
debugPrint('Temporary directory delete retry: $error');
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
@ -4514,7 +4614,9 @@ AppController _sandboxController({
|
||||
GoTaskServiceClient? goTaskServiceClient,
|
||||
String? homeDir,
|
||||
}) {
|
||||
final actualHome = homeDir ?? Directory.systemTemp.createTempSync('xworkmate-sandbox-home-').path;
|
||||
final actualHome =
|
||||
homeDir ??
|
||||
Directory.systemTemp.createTempSync('xworkmate-sandbox-home-').path;
|
||||
if (homeDir == null) {
|
||||
addTearDown(() async {
|
||||
await _resilientDelete(Directory(actualHome));
|
||||
@ -4537,7 +4639,10 @@ AppController _sandboxController({
|
||||
);
|
||||
}
|
||||
|
||||
AppController _connectedController(GoTaskServiceClient client, {String? homeDir}) {
|
||||
AppController _connectedController(
|
||||
GoTaskServiceClient client, {
|
||||
String? homeDir,
|
||||
}) {
|
||||
return _sandboxController(
|
||||
goTaskServiceClient: client,
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
@ -4554,7 +4659,10 @@ AppController _connectedController(GoTaskServiceClient client, {String? homeDir}
|
||||
);
|
||||
}
|
||||
|
||||
AppController _connectedGatewayController(GoTaskServiceClient client, {String? homeDir}) {
|
||||
AppController _connectedGatewayController(
|
||||
GoTaskServiceClient client, {
|
||||
String? homeDir,
|
||||
}) {
|
||||
return _sandboxController(
|
||||
goTaskServiceClient: client,
|
||||
uiFeatureManifest: _defaultDesktopManifest(),
|
||||
@ -4694,6 +4802,7 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
final List<GoTaskServiceUpdate> updatesBeforeNextOutcome =
|
||||
<GoTaskServiceUpdate>[];
|
||||
final List<Object> outcomes = <Object>[];
|
||||
final List<Object> taskOutcomes = <Object>[];
|
||||
Future<void> Function(GoTaskServiceRequest request)? onExecuteTask;
|
||||
|
||||
@override
|
||||
@ -4748,6 +4857,13 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
required OpenClawTaskAssociation association,
|
||||
required GoTaskServiceRoute route,
|
||||
}) async {
|
||||
if (taskOutcomes.isNotEmpty) {
|
||||
final outcome = taskOutcomes.removeAt(0);
|
||||
if (outcome is GoTaskServiceResult) {
|
||||
return outcome;
|
||||
}
|
||||
throw outcome;
|
||||
}
|
||||
return GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'ok',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user