diff --git a/README.md b/README.md
index 3c077849..4b29fac4 100644
--- a/README.md
+++ b/README.md
@@ -76,5 +76,6 @@ All download buttons currently point to the latest GitHub release page.
- [Changelog](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/releases/xworkmate-changelog.md)
- [Feature Matrix](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/plans/xworkmate-ui-feature-matrix.md)
- [Roadmap](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/plans/xworkmate-ui-feature-roadmap.md)
+- [Cross-Repo Task State Workflow](docs/architecture/cross-repo-task-state-workflow.md)
- [Gateway Dev Runbook](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/runbooks/gateway-dev-runbook.md)
- [Security Rules](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/security/secure-development-rules.md)
diff --git a/docs/architecture/cross-repo-task-state-workflow.md b/docs/architecture/cross-repo-task-state-workflow.md
new file mode 100644
index 00000000..0caace65
--- /dev/null
+++ b/docs/architecture/cross-repo-task-state-workflow.md
@@ -0,0 +1,196 @@
+# Cross-Repo Task State Workflow
+
+This document records the task-state workflow across:
+
+- `xworkmate-app`
+- `xworkmate-bridge`
+- `openclaw-multi-session-plugins`
+
+The core ownership split 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.
+
+## Overall Flow
+
+```mermaid
+flowchart LR
+ U["User input / follow-up"] --> APP["xworkmate-app
TaskThread + UI state"]
+ APP -->|session.start / session.message| BR["xworkmate-bridge
/acp/rpc or /gateway/openclaw"]
+ 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
task-scoped artifacts"]
+ PLUG -->|artifactRef / files / downloadUrl| BR
+ BR -->|normalized result / SSE update| APP
+ APP -->|write files| LOCAL["$HOME/.xworkmate/threads/"]
+ APP -->|persist index| STORE["~/Library/Application Support/xworkmate/tasks/threads.json"]
+```
+
+## App TaskThread State Machine
+
+`TaskThread.lifecycleState.status` is intentionally small. Most terminal outcomes return to `ready`; the specific terminal result is stored in `lastResultCode`.
+
+```mermaid
+stateDiagram-v2
+ [*] --> Ready: create task / restore persisted task
+
+ Ready --> Queued: OpenClaw gateway active slot is full
+ Queued --> Running: drainOpenClawGatewayQueue
+ Queued --> Ready: abortRun\nlastResultCode=aborted
+ Queued --> Ready: queue full\nlastResultCode=OPENCLAW_GATEWAY_QUEUE_FULL
+
+ Ready --> Running: sendChatMessage\nmarkGatewayChatRun
+ Running --> Ready: GoTaskServiceResult.success=true\nlastResultCode=success
+ Running --> Ready: result.status / result.code\nlastResultCode=
+ Running --> Ready: no displayable output\nlastResultCode=failed
+ Running --> Ready: ACP interrupted / timeout\nlastResultCode=ACP_*
+ Running --> Ready: abortRun\nlastResultCode=aborted
+
+ 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_*
+ end note
+```
+
+## Task Workspace Context Injection
+
+Every app-owned task has a local workspace under `$HOME/.xworkmate/threads/`. 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/"
+ 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
lifecycleStatus=running"]
+ B -->|no| D{"queue < 20 ?"}
+ D -->|yes| E["Enqueue
lifecycleStatus=queued
lastArtifactSyncStatus=queued"]
+ D -->|no| F["Queue full
lastResultCode=OPENCLAW_GATEWAY_QUEUE_FULL"]
+
+ E --> G["drain queue"]
+ G --> C
+ C --> H["Bridge /gateway/openclaw"]
+ H --> I["OpenClaw execution"]
+ I --> J{"Result"}
+ J -->|success + output/files| K["APP ready
lastResultCode=success
sync artifacts"]
+ J -->|artifact guard| L["APP ready
lastResultCode=artifact_missing"]
+ J -->|failure/interruption| M["APP ready
lastResultCode=failed/error/ACP_*"]
+```
+
+## Bridge Session And Routing Workflow
+
+The bridge exposes one public session contract while keeping provider-specific behavior behind compatibility layers. `/gateway/openclaw` is a narrow OpenClaw task-submit lane, not a general ACP base endpoint.
+
+```mermaid
+flowchart TD
+ REQ["APP request"] --> EP{"HTTP / WS entry"}
+ EP -->|/acp or /acp/rpc| RPC["General JSON-RPC"]
+ EP -->|/gateway/openclaw| GW["OpenClaw task-submit endpoint"]
+
+ 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"]
+
+ GW --> GUARD{"method is session.start / session.message ?"}
+ GUARD -->|no| REJECT["Reject: not a global ACP base"]
+ GUARD -->|yes| FORCE["Force routing=gateway/openclaw
Reject multiAgent"]
+ FORCE --> START
+
+ START --> ORCH["session_orchestrator"]
+ MSG --> ORCH
+ ORCH --> PROVIDER{"provider compat"}
+ PROVIDER -->|single-agent| SA["codex / opencode / gemini / hermes"]
+ PROVIDER -->|gateway| OCG["OpenClaw runtime"]
+ PROVIDER -->|multi-agent via /acp/rpc| MA["multi-agent orchestration"]
+
+ SA --> NORM["normalized result"]
+ OCG --> NORM
+ MA --> 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
sessionKey + runId + workspaceDir"] --> PREP["prepareXWorkmateArtifacts"]
+ PREP --> SCOPE["artifactScope = tasks//"]
+ 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//
+ Do not reuse other sessions or previous run files.
+ end note
+```
+
+## Status Field Mapping
+
+```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
artifactScope=tasks/session/run"]
+```
+
+## Boundary Rules
+
+- The app does not store OpenClaw URLs. It only consumes bridge capabilities where `gatewayProviders` includes `openclaw`.
+- `/gateway/openclaw` is only for OpenClaw `session.start` and `session.message`; it is not a global ACP 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//` 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.
diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart
index b3dd476a..6f37f2f6 100644
--- a/lib/app/app_controller_desktop_thread_actions.dart
+++ b/lib/app/app_controller_desktop_thread_actions.dart
@@ -67,6 +67,8 @@ extension AppControllerDesktopThreadActions on AppController {
bool assistantSessionHasPendingRun(String sessionKey) {
final normalized = normalizedAssistantSessionKeyInternal(sessionKey);
return aiGatewayPendingSessionKeysInternal.contains(normalized) ||
+ (openClawGatewayQueuedTurnsBySessionInternal[normalized]?.isNotEmpty ??
+ false) ||
(multiAgentRunPendingInternal &&
matchesSessionKey(
normalized,
@@ -417,11 +419,21 @@ extension AppControllerDesktopThreadActions on AppController {
required String agentId,
required Map metadata,
required bool resumeSessionHint,
+ bool appendUserTurn = true,
}) async {
final resumeSession =
resumeSessionHint ||
- shouldResumeGatewaySessionForNextSendInternal(sessionKey);
- appendGatewayUserTurnInternal(sessionKey, message);
+ (appendUserTurn &&
+ shouldResumeGatewaySessionForNextSendInternal(sessionKey));
+ final taskPrompt = taskWorkspaceContextPromptInternal(
+ sessionKey: sessionKey,
+ userPrompt: message,
+ workingDirectory: workingDirectory,
+ remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
+ );
+ if (appendUserTurn) {
+ appendGatewayUserTurnInternal(sessionKey, message);
+ }
markGatewayChatRunInternal(sessionKey);
try {
final result = await goTaskServiceClientInternal.executeTask(
@@ -430,7 +442,7 @@ extension AppControllerDesktopThreadActions on AppController {
threadId: sessionKey,
target: target,
provider: provider,
- prompt: message,
+ prompt: taskPrompt,
workingDirectory: workingDirectory,
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
model: model,
@@ -482,6 +494,53 @@ extension AppControllerDesktopThreadActions on AppController {
}
}
+ String taskWorkspaceContextPromptInternal({
+ required String sessionKey,
+ required String userPrompt,
+ required String workingDirectory,
+ required String remoteWorkingDirectoryHint,
+ }) {
+ final requestText = userPrompt.trim().isEmpty
+ ? 'See attached.'
+ : userPrompt.trim();
+ final buffer = StringBuffer()
+ ..writeln('TaskThread workspace context:')
+ ..writeln('- sessionKey: $sessionKey')
+ ..writeln('- localWorkspace: ${workingDirectory.trim()}');
+ final remoteHint = remoteWorkingDirectoryHint.trim();
+ if (remoteHint.isNotEmpty) {
+ buffer.writeln('- remoteWorkspaceHint: $remoteHint');
+ }
+ buffer.writeln(
+ '- currentTaskWorkspace: ${remoteHint.isNotEmpty ? remoteHint : workingDirectory.trim()}',
+ );
+ buffer
+ ..writeln()
+ ..writeln('Workspace isolation rules:')
+ ..writeln(
+ '1. Treat currentTaskWorkspace as the only writable workspace for this TaskThread execution.',
+ )
+ ..writeln(
+ '2. Create, modify, and export task files inside currentTaskWorkspace or its task artifact scope.',
+ )
+ ..writeln(
+ '3. Do not use arbitrary global directories, OpenClaw media cache, Downloads, Desktop, or /tmp as final deliverable locations.',
+ )
+ ..writeln(
+ '4. If a tool creates output outside currentTaskWorkspace, copy or export the final deliverables into currentTaskWorkspace before claiming completion.',
+ )
+ ..writeln(
+ '5. When reporting files, prefer paths inside currentTaskWorkspace or paths relative to currentTaskWorkspace.',
+ )
+ ..writeln(
+ '6. The app syncs final artifacts from currentTaskWorkspace back into localWorkspace.',
+ )
+ ..writeln()
+ ..writeln('User request:')
+ ..write(requestText);
+ return buffer.toString();
+ }
+
bool usesOpenClawGatewayQueueInternal(
AssistantExecutionTarget target,
SingleAgentProvider provider,
@@ -493,6 +552,7 @@ extension AppControllerDesktopThreadActions on AppController {
Future enqueueOpenClawGatewayTurnInternal(
OpenClawGatewayQueuedTurnInternal turn,
) async {
+ appendGatewayUserTurnInternal(turn.sessionKey, turn.message);
if (openClawGatewayActiveTasksInternal >=
openClawGatewayMaxActiveTasksInternal &&
openClawGatewayQueuedTurnsInternal.length >=
@@ -632,6 +692,7 @@ extension AppControllerDesktopThreadActions on AppController {
agentId: turn.agentId,
metadata: turn.metadata,
resumeSessionHint: turn.resumeSessionHint,
+ appendUserTurn: false,
),
);
} catch (error) {
diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart
index 8999e21b..95241e21 100644
--- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart
+++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart
@@ -160,12 +160,18 @@ Future runMultiAgentCollaborationThreadSessionInternal(
);
controller.recomputeTasksInternal();
try {
+ final taskPrompt = controller.taskWorkspaceContextPromptInternal(
+ sessionKey: sessionKey,
+ userPrompt: composedPrompt,
+ workingDirectory: workingDirectory,
+ remoteWorkingDirectoryHint: remoteWorkingDirectoryHint?.trim() ?? '',
+ );
final result = await controller.goTaskServiceClientInternal.executeTask(
GoTaskServiceRequest(
sessionId: sessionKey,
threadId: sessionKey,
target: controller.assistantExecutionTargetForSession(sessionKey),
- prompt: composedPrompt,
+ prompt: taskPrompt,
workingDirectory: workingDirectory,
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint?.trim() ?? '',
model: controller.assistantModelForSession(sessionKey),
diff --git a/lib/features/assistant/assistant_page_task_models.dart b/lib/features/assistant/assistant_page_task_models.dart
index 896528a5..ee87dbf4 100644
--- a/lib/features/assistant/assistant_page_task_models.dart
+++ b/lib/features/assistant/assistant_page_task_models.dart
@@ -344,6 +344,9 @@ String sessionStatusInternal(
if (session.abortedLastRun == true) {
return 'failed';
}
+ if (normalizedLifecycle == 'queued') {
+ return 'queued';
+ }
if (sessionPending) {
return 'running';
}
diff --git a/lib/widgets/assistant_task_progress_bar.dart b/lib/widgets/assistant_task_progress_bar.dart
index d2fa09c6..8b814e5e 100644
--- a/lib/widgets/assistant_task_progress_bar.dart
+++ b/lib/widgets/assistant_task_progress_bar.dart
@@ -2,7 +2,13 @@ import 'package:flutter/material.dart';
import '../i18n/app_language.dart';
-enum AssistantTaskProgressPhase { idle, running, syncingArtifacts, interrupted }
+enum AssistantTaskProgressPhase {
+ idle,
+ queued,
+ running,
+ syncingArtifacts,
+ interrupted,
+}
class AssistantTaskProgressState {
const AssistantTaskProgressState({
@@ -26,6 +32,7 @@ class AssistantTaskProgressState {
bool get visible => phase != AssistantTaskProgressPhase.idle;
bool get interrupted => phase == AssistantTaskProgressPhase.interrupted;
bool get running =>
+ phase == AssistantTaskProgressPhase.queued ||
phase == AssistantTaskProgressPhase.running ||
phase == AssistantTaskProgressPhase.syncingArtifacts;
}
@@ -146,6 +153,15 @@ AssistantTaskProgressState assistantTaskProgressState({
final budget = runtimeBudgetMinutes == null || runtimeBudgetMinutes <= 0
? null
: runtimeBudgetMinutes;
+ final result = lastResultCode.trim().toUpperCase();
+ if (status == 'queued' || syncStatus == 'queued' || result == 'QUEUED') {
+ return AssistantTaskProgressState(
+ phase: AssistantTaskProgressPhase.queued,
+ label: appText('任务已排队,等待执行...', 'Task queued, waiting to run...'),
+ value: 0.18,
+ runtimeBudgetMinutes: budget,
+ );
+ }
if (pending && syncStatus == 'syncing') {
return AssistantTaskProgressState(
phase: AssistantTaskProgressPhase.syncingArtifacts,
@@ -161,7 +177,6 @@ AssistantTaskProgressState assistantTaskProgressState({
runtimeBudgetMinutes: budget,
);
}
- final result = lastResultCode.trim().toUpperCase();
if (status == 'interrupted' || syncStatus == 'interrupted') {
return AssistantTaskProgressState(
phase: AssistantTaskProgressPhase.interrupted,
diff --git a/pubspec.yaml b/pubspec.yaml
index 2961a1c5..3973dc4c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,9 +2,9 @@ name: xworkmate
description: "XWorkmate desktop-first AI workspace shell."
publish_to: 'none'
-version: 1.0.0-beta.2+5
-build-date: 2026-05-18
-build-id: e7e1135
+version: 1.0.0-beta.2+6
+build-date: 2026-05-19
+build-id: 0f5440d
environment:
sdk: ^3.11.0
diff --git a/test/features/assistant/assistant_task_progress_bar_test.dart b/test/features/assistant/assistant_task_progress_bar_test.dart
index 10b1afd2..c482d9f5 100644
--- a/test/features/assistant/assistant_task_progress_bar_test.dart
+++ b/test/features/assistant/assistant_task_progress_bar_test.dart
@@ -57,6 +57,32 @@ void main() {
expect(indicator.value, 0.82);
});
+ testWidgets('shows queued progress while a task waits to run', (
+ tester,
+ ) async {
+ await tester.pumpWidget(
+ _buildTestApp(
+ assistantTaskProgressState(
+ pending: true,
+ lifecycleStatus: 'queued',
+ lastResultCode: 'queued',
+ artifactSyncStatus: 'queued',
+ ),
+ onStop: () {},
+ ),
+ );
+
+ expect(find.text('任务已排队,等待执行...'), findsOneWidget);
+ expect(
+ find.byKey(const Key('assistant-task-progress-stop-button')),
+ findsOneWidget,
+ );
+ final indicator = tester.widget(
+ find.byKey(const Key('assistant-task-progress-indicator')),
+ );
+ expect(indicator.value, 0.18);
+ });
+
testWidgets('shows interrupted state after ACP connection closes', (
tester,
) async {
diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart
index d0a19873..1058c504 100644
--- a/test/runtime/assistant_execution_target_test.dart
+++ b/test/runtime/assistant_execution_target_test.dart
@@ -898,7 +898,21 @@ void main() {
await controller.sendChatMessage('first turn');
expect(fakeGoTaskService.requests, hasLength(1));
- expect(fakeGoTaskService.requests.single.resumeSession, isFalse);
+ final request = fakeGoTaskService.requests.single;
+ expect(request.resumeSession, isFalse);
+ expect(request.prompt, contains('TaskThread workspace context:'));
+ expect(request.prompt, contains('- sessionKey: unit-fixture-task-a'));
+ expect(request.prompt, contains(request.workingDirectory));
+ expect(request.prompt, contains(request.remoteWorkingDirectoryHint));
+ expect(request.prompt, contains('User request:\nfirst turn'));
+ expect(
+ controller.chatMessages.map((message) => message.text),
+ contains('first turn'),
+ );
+ expect(
+ controller.chatMessages.map((message) => message.text).join('\n'),
+ isNot(contains('TaskThread workspace context:')),
+ );
});
test(
@@ -1632,11 +1646,15 @@ void main() {
final taskBRequest = fakeGoTaskService.requests[1];
expect(taskARequest.sessionId, sessionA);
expect(taskBRequest.sessionId, sessionB);
- expect(taskARequest.prompt, taskBRequest.prompt);
+ expect(taskARequest.prompt, isNot(taskBRequest.prompt));
+ expect(taskARequest.prompt, contains(prompt));
+ expect(taskBRequest.prompt, contains(prompt));
expect(taskARequest.resumeSession, isFalse);
expect(taskBRequest.resumeSession, isFalse);
expect(taskARequest.workingDirectory, endsWith('/$sessionA'));
expect(taskBRequest.workingDirectory, endsWith('/$sessionB'));
+ expect(taskARequest.prompt, contains(taskARequest.workingDirectory));
+ expect(taskBRequest.prompt, contains(taskBRequest.workingDirectory));
expect(
taskARequest.workingDirectory,
isNot(taskBRequest.workingDirectory),
@@ -2192,6 +2210,26 @@ void main() {
.status,
'queued',
);
+ expect(
+ controller.assistantSessionHasPendingRun('queue-task-b'),
+ isTrue,
+ );
+ expect(
+ controller.assistantSessionHasPendingRun('queue-task-c'),
+ isTrue,
+ );
+ expect(
+ controller.localSessionMessagesInternal['queue-task-b']!.map(
+ (message) => message.text,
+ ),
+ contains('same prompt'),
+ );
+ expect(
+ controller.localSessionMessagesInternal['queue-task-c']!.map(
+ (message) => message.text,
+ ),
+ contains('different prompt'),
+ );
fakeGoTaskService.complete(
'queue-task-a',
@@ -2214,9 +2252,12 @@ void main() {
final taskBRequest = fakeGoTaskService.requests[1];
expect(taskBRequest.sessionId, 'queue-task-b');
- expect(taskBRequest.prompt, 'same prompt');
+ expect(taskBRequest.prompt, contains('TaskThread workspace context:'));
+ expect(taskBRequest.prompt, contains('- sessionKey: queue-task-b'));
+ expect(taskBRequest.prompt, contains('User request:\nsame prompt'));
expect(taskBRequest.resumeSession, isFalse);
expect(taskBRequest.workingDirectory, endsWith('/queue-task-b'));
+ expect(taskBRequest.prompt, contains(taskBRequest.workingDirectory));
expect(
taskBRequest.remoteWorkingDirectoryHint,
endsWith('/threads/queue-task-b'),
@@ -2239,12 +2280,25 @@ void main() {
'queue-task-b',
'ready',
);
+ expect(
+ controller.localSessionMessagesInternal['queue-task-b']!
+ .where(
+ (message) =>
+ message.role == 'user' && message.text == 'same prompt',
+ )
+ .length,
+ 1,
+ );
await fakeGoTaskService.waitForRequestCount(3);
final taskCRequest = fakeGoTaskService.requests[2];
expect(taskCRequest.sessionId, 'queue-task-c');
- expect(taskCRequest.prompt, 'different prompt');
+ expect(
+ taskCRequest.prompt,
+ contains('User request:\ndifferent prompt'),
+ );
expect(taskCRequest.workingDirectory, endsWith('/queue-task-c'));
+ expect(taskCRequest.prompt, contains(taskCRequest.workingDirectory));
fakeGoTaskService.complete(
'queue-task-c',
const GoTaskServiceResult(