refactor: Remove OpenClaw rigid time limits and false positive no-exported-artifacts judgment
This commit is contained in:
parent
7047a4f38b
commit
6d5122682c
164
docs/architecture/multi-session-plugin-review-2026-06-05.md
Normal file
164
docs/architecture/multi-session-plugin-review-2026-06-05.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# openclaw-multi-session-plugins 代码审核报告
|
||||||
|
|
||||||
|
> 基于架构优化建议,审核已实施改动的完善程度
|
||||||
|
|
||||||
|
## 审核范围
|
||||||
|
|
||||||
|
| 仓库 | 分支/状态 | 审核提交范围 |
|
||||||
|
|------|----------|-------------|
|
||||||
|
| openclaw-multi-session-plugins | release/v2026.6.1 (uncommitted changes) | `c462ed6..HEAD` + staged diffs |
|
||||||
|
| xworkmate-app | main (ahead 8) | `6219fcd..596704b` |
|
||||||
|
| openclaw.svc.plus | main (uncommitted `src/plugins/session-scope.ts`) | 仅新增文件 |
|
||||||
|
| xworkmate-bridge | 无此仓库 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、P0 改动审核
|
||||||
|
|
||||||
|
### 1.1 注册 xworkmate.tasks.get ✅ 已实施,有细节问题
|
||||||
|
|
||||||
|
**实施状态**:`index.ts` 新增了 `api.registerGatewayMethod("xworkmate.tasks.get", ...)`,委托给 `taskState.ts` 的 `getXWorkmateTaskSnapshot()`。
|
||||||
|
|
||||||
|
**测试覆盖**:`index.test.ts` 新增了完整集成测试(`registers xworkmate task state against the native session extension and task runtime seams`),验证了 session.start hook → 制品写入 → tasks.get 查询 → 状态/制品同时返回。
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
|
||||||
|
1. **`getXWorkmateTaskSnapshot` 中的 `api.runtime.tasks.runs.bindSession` 调用路径不确定**。`taskState.ts:229-236` 的 `resolveNativeTask` 走 `api.runtime?.tasks?.runs?.bindSession?.({ sessionKey })` 查询原生 TaskRecord。但 `api.runtime` 在 OpenClaw 2026.6.1 的 Plugin API 契约中的暴露方式需要通过 Gateway Handler 的 context 注入,而非直接从 `api` 对象获取。测试中显式 mock 了这个路径(第 139-153 行),生产路径需要验证。
|
||||||
|
|
||||||
|
2. **`registerXWorkmateDetachedTaskRuntime` 使用了未文档化的 API**。`taskState.ts:64` 调用 `(api as any).registerDetachedTaskRuntime` —— 这个 API 不在 `OpenClawPluginApi` 类型中,也不在 `src/plugin-sdk/index.ts` 的导出里。如果OpenClaw 原生不支持,调用会静默失败(`typeof registerRuntime !== "function"` 的 guard),退回到纯内存 store 模式。这不影响功能但失去了与原生 task-registry 的集成。
|
||||||
|
|
||||||
|
**评级**:基本完善。`registerDetachedTaskRuntime` 的可用性是关键风险点,需要验证 OpenClaw 2026.6.1 是否实际支持。
|
||||||
|
|
||||||
|
### 1.2 session.start hook 中创建 TaskRecord ✅ 已实施
|
||||||
|
|
||||||
|
**实施状态**:`index.ts:99-115` 在 `session.start` hook 中调用 `createOrUpdateXWorkmateTaskRecord(taskStore, { params, status: "running" })`,并行调用 `prepareXWorkmateArtifacts`。
|
||||||
|
|
||||||
|
**问题**:
|
||||||
|
|
||||||
|
1. **TaskStore 是纯内存 Map**。`taskState.ts:33-35` 创建 `{ records: new Map() }` 作为 task store,不支持持久化。插件重启后所有 task 记录丢失。如果 OpenClaw 原生 task-registry 集成生效,这不影响(原生 registry 持久化);如果退回到内存模式,丢失意味着 APP 的 `xworkmate.tasks.get` 查询返回不到历史任务状态。
|
||||||
|
|
||||||
|
2. **没有 task 生命周期终点同步**。Hook 只在 `session.start` 时创建 `status: "running"` 的 record,但没有在任务完成时更新为 `"succeeded"` / `"failed"`。`getXWorkmateTaskSnapshot` 虽然会根据制品数量推断(`taskState.ts:132-136`),但这依赖于每次调用时做推断而非推送。
|
||||||
|
|
||||||
|
**评级**:功能可用,但内存 store + 没有主动完成通知是两个改进点。
|
||||||
|
|
||||||
|
### 1.3 打通 expectedArtifactDirs 全链路 ⚠️ 部分实施
|
||||||
|
|
||||||
|
**实施状态**:
|
||||||
|
|
||||||
|
| 层 | 状态 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| Plugin - scopedGatewayParams | ✅ 已通 | 使用 `{ ...params, sessionKey, runId, ... }` 模式,额外参数自然透传 |
|
||||||
|
| Plugin - exportXWorkmateArtifacts | ✅ 已实现 | Fix 0 的回退扫描逻辑在 `exportArtifacts.ts:263-283` |
|
||||||
|
| Plugin - 测试覆盖 | ✅ 已验证 | `index.test.ts:104-137` 端到端验证 |
|
||||||
|
| Bridge | ❌ 未实施 | Bridge 仓库不存在于此工作区 |
|
||||||
|
| APP - 发送 expectedArtifactDirs | ❌ 未实施 | `xworkmateTaskArtifactContract` 不包含此字段 |
|
||||||
|
|
||||||
|
**关键缺失**:APP 侧的 `gatewayTaskMetadataWithArtifactContractInternal`(`app_controller_desktop_thread_actions.dart:988-1000`)需要在 metadata 中加入 `expectedArtifactDirs`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
'xworkmateTaskArtifactContract': <String, dynamic>{
|
||||||
|
'version': 1,
|
||||||
|
'sessionKey': sessionKey,
|
||||||
|
'expectedArtifactDirs': ['assets/images', 'reports', 'video'], // ← 缺失
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
没有这行,即使 Plugin 侧的回退逻辑已就绪,参数也不会到达。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、P1 改动审核
|
||||||
|
|
||||||
|
### 2.1 利用 pluginExtensions 做 Key 映射 ✅ 已实施,路径需验证
|
||||||
|
|
||||||
|
**实施状态**:`taskState.ts:37-61` 的 `registerXWorkmateSessionExtension` 调用 `api.session?.state?.registerSessionExtension` 注册 `"xworkmate"` namespace 的 session extension。
|
||||||
|
|
||||||
|
`project` 回调实现了自动映射:
|
||||||
|
- 如果 state 有 `appSessionKey` → 直接用
|
||||||
|
- 否则从 OpenClaw session key 推导(`agent:main:draft:xxx` → `draft:xxx`)
|
||||||
|
|
||||||
|
**风险**:`api.session?.state?.registerSessionExtension` 的类型链依赖 `OpenClawPluginApi` 的类型定义。`taskState.ts:38` 有 `?? (api as any).registerSessionExtension` 后备,但同样,如果 OpenClaw 不支持这个 API,静默失败。
|
||||||
|
|
||||||
|
**评级**:设计方向正确。`api.session.state.registerSessionExtension` 是 OpenClaw 的 `api-facades.ts` 中定义的正式 API(`api.session.state`),应该可用。Key 映射逻辑也正确处理了 `agent:main:draft:1780636411666238-3` → `draft:1780636411666238-3` 的 case。
|
||||||
|
|
||||||
|
### 2.2 Fix 1 (引用相等 Bug) ✅ 已修正
|
||||||
|
|
||||||
|
`external_code_agent_acp_desktop_transport.dart:178-184` 的 `_recoveredResultFromTaskSnapshot` 已经修正:去掉了 `result['artifacts'] == artifactRecord` 的引用相等比较,改为直接判断 `artifactItems is List && artifactItems.isNotEmpty`。
|
||||||
|
|
||||||
|
### 2.3 Fix 2 (completed 状态竞态) ✅ 已修正
|
||||||
|
|
||||||
|
`_recoverTaskResultAfterStreamClosure:186-192` 新增了产物为空时的重试逻辑。
|
||||||
|
|
||||||
|
### 2.4 Fix 3 (产物完整性验证) ✅ 已修正
|
||||||
|
|
||||||
|
`app_controller_desktop_runtime_helpers.dart:873-888` 的 `persistGoTaskArtifactsForSessionInternal` 现在检查 `requiredArtifactExtensions`,缺失时设置 syncStatus 为 `'partial'`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、P2 改动审核
|
||||||
|
|
||||||
|
### 3.1 Fix 5 (polling 产物完整性) ✅ 已修正
|
||||||
|
|
||||||
|
`app_controller_desktop_thread_actions.dart:778-785` 的 `pollOpenClawTaskAssociationInternal` 现在在 polling 完成时检查 `requiredArtifactExtensions`,不足时最多重试 3 次。
|
||||||
|
|
||||||
|
### 3.2 bridgeAgents 改用原生 subagent.run ❌ 未实施
|
||||||
|
|
||||||
|
`bridgeAgents.ts` 仍通过 HTTP fetch 调用外部 bridge,没有使用 OpenClaw 的 `api.runtime.subagent.run()` 或 `api.taskFlows`。这个改动属于较大重构,优先级较低,暂未实施可以接受。
|
||||||
|
|
||||||
|
### 3.3 用 Transcript Events 替代 SSE 管理 ❌ 未实施
|
||||||
|
|
||||||
|
无相关改动。这个改动涉及 Bridge 层架构变更,当前未实施。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、OpenClaw 原生侧待确认项
|
||||||
|
|
||||||
|
### 4.1 session-scope.ts(未提交)
|
||||||
|
|
||||||
|
`openclaw.svc.plus/src/plugins/session-scope.ts` 是一个未提交的新增文件,提供了标准化的 plugin session scope 管理(`createPluginSessionScope`、`normalizePluginScopeSegment`、`buildPluginRelativeTaskDirectory`)。这个文件的意图看起来是为了将 session-scope 管理提升为 OpenClaw 原生能力,但目前只是未提交的草案。
|
||||||
|
|
||||||
|
**建议**:如果 OpenClaw 上游接受了这个模块,`openclaw-multi-session-plugins` 的 `scopedGatewayParams` 和 `safeScopeSegment` 可以迁移使用原生 API。
|
||||||
|
|
||||||
|
### 4.2 registerDetachedTaskRuntime 的可用性
|
||||||
|
|
||||||
|
`taskState.ts:63-114` 通过 `(api as any).registerDetachedTaskRuntime` 注册自定义 detached task runtime。需要在 OpenClaw 2026.6.1 中验证此 API 是否存在。如果不存在,退回到纯内存 store 模式不影响核心功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、整体评估
|
||||||
|
|
||||||
|
### 5.1 实施完成度
|
||||||
|
|
||||||
|
| 类别 | 总项 | 已完成 | 部分完成 | 未开始 |
|
||||||
|
|------|------|--------|---------|--------|
|
||||||
|
| P0 | 3 | 2 | 1 (expectedArtifactDirs APP侧) | 0 |
|
||||||
|
| P1 | 4 | 4 | 0 | 0 |
|
||||||
|
| P2 | 3 | 1 | 0 | 2 |
|
||||||
|
| **合计** | **10** | **7** | **1** | **2** |
|
||||||
|
|
||||||
|
### 5.2 待补充的关键项
|
||||||
|
|
||||||
|
1. **APP 侧传递 expectedArtifactDirs** — `gatewayTaskMetadataWithArtifactContractInternal` 需要在 metadata 中加入 `'expectedArtifactDirs': ['assets/images', 'reports', 'video']`
|
||||||
|
|
||||||
|
2. **Bridge 侧透传 expectedArtifactDirs** — `session.start` 参数需要在 `toExternalAcpParams()` 中包含 `expectedArtifactDirs` 字段
|
||||||
|
|
||||||
|
3. **验证 registerDetachedTaskRuntime API** — 确认 OpenClaw 2026.6.1 是否实际支持此 API;如果不支持,考虑不依赖它,纯用内存 store + export
|
||||||
|
|
||||||
|
### 5.3 架构风险
|
||||||
|
|
||||||
|
| 风险 | 严重度 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| TaskStore 纯内存 | 中 | 插件重启后 task 记录丢失,但 APP polling 会重新触发查询 |
|
||||||
|
| registerDetachedTaskRuntime 静默失败 | 低 | 有 fallback 路径,功能不中断 |
|
||||||
|
| 未提交代码量大 | 中 | 插件 6 个文件有 staged changes,taskState.ts 是新文件 |
|
||||||
|
| 缺少 task 完成推送 | 中 | session.start hook 只创建 running 状态,无完成时的更新 |
|
||||||
|
| OpenClaw session-scope.ts 的定位 | 信息 | 如果上游接受,部分插件逻辑可以删除 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、结论
|
||||||
|
|
||||||
|
**核心 P0 改动(xworkmate.tasks.get + 原生 TaskRecord 集成)实施质量良好**,代码结构清晰,测试覆盖到位。主要缺陷在 `expectedArtifactDirs` 的全链路——Plugin 侧已就绪,但 APP 和 Bridge 两侧各缺一行代码。补充这两处后,所有三个 P0 项即全部完成。
|
||||||
|
|
||||||
|
P1 改动(ROOT_CAUSE_ANALYSIS 的 Fix 1-3)全部正确实施,产物完整性问题已解决。
|
||||||
@ -334,31 +334,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOpenClawNoExportedArtifactsGuardResultInternal(
|
|
||||||
GoTaskServiceResult result,
|
|
||||||
) {
|
|
||||||
if (result.artifacts.isNotEmpty) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final status = result.status.trim().toLowerCase();
|
|
||||||
if (status == 'artifact_missing') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final code = result.code.trim().toUpperCase();
|
|
||||||
if (code == 'OPENCLAW_ARTIFACT_MISSING' ||
|
|
||||||
code == 'OPENCLAW_NO_EXPORTED_ARTIFACTS') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final warnings = result.raw['artifactWarnings'];
|
|
||||||
if (warnings is List) {
|
|
||||||
return warnings.any((item) {
|
|
||||||
final text = item?.toString().toLowerCase() ?? '';
|
|
||||||
return text.contains('openclaw artifact export returned no files') ||
|
|
||||||
text.contains('no files for a file-delivery request');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> recoverGatewayFailureArtifactPathsInternal(
|
Future<List<String>> recoverGatewayFailureArtifactPathsInternal(
|
||||||
String sessionKey,
|
String sessionKey,
|
||||||
@ -778,9 +754,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
existingThread.openClawTaskAssociation?.requiredArtifactExtensions ??
|
existingThread.openClawTaskAssociation?.requiredArtifactExtensions ??
|
||||||
const <String>[];
|
const <String>[];
|
||||||
final currentTaskArtifactRelativePaths =
|
final currentTaskArtifactRelativePaths =
|
||||||
isOpenClawNoExportedArtifactsGuardResultInternal(result)
|
await _workspaceArtifactPathsModifiedSinceInternal(
|
||||||
? const <String>[]
|
|
||||||
: await _workspaceArtifactPathsModifiedSinceInternal(
|
|
||||||
root,
|
root,
|
||||||
existingThread.lifecycleState.lastRunAtMs,
|
existingThread.lifecycleState.lastRunAtMs,
|
||||||
artifactSyncPolicy,
|
artifactSyncPolicy,
|
||||||
@ -798,11 +772,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
upsertTaskThreadInternal(
|
upsertTaskThreadInternal(
|
||||||
normalizedSessionKey,
|
normalizedSessionKey,
|
||||||
lastArtifactSyncAtMs: syncedAtMs,
|
lastArtifactSyncAtMs: syncedAtMs,
|
||||||
lastArtifactSyncStatus:
|
lastArtifactSyncStatus: 'no-artifacts',
|
||||||
isOpenClawNoExportedArtifactsGuardResultInternal(result) ||
|
|
||||||
requiredExts.isNotEmpty
|
|
||||||
? 'no-exported-artifacts'
|
|
||||||
: 'no-artifacts',
|
|
||||||
updatedAtMs: syncedAtMs,
|
updatedAtMs: syncedAtMs,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -888,8 +858,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
|||||||
: 'synced')
|
: 'synced')
|
||||||
: failedArtifact
|
: failedArtifact
|
||||||
? 'download-failed'
|
? 'download-failed'
|
||||||
: rejectedArtifact
|
|
||||||
? 'no-exported-artifacts'
|
|
||||||
: 'no-artifacts';
|
: 'no-artifacts';
|
||||||
final currentTaskArtifactRelativePaths = wroteArtifact
|
final currentTaskArtifactRelativePaths = wroteArtifact
|
||||||
? (currentTaskArtifactPaths.toList(growable: false)..sort())
|
? (currentTaskArtifactPaths.toList(growable: false)..sort())
|
||||||
|
|||||||
@ -735,24 +735,19 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
required OpenClawTaskAssociation association,
|
required OpenClawTaskAssociation association,
|
||||||
}) async {
|
}) async {
|
||||||
var current = association;
|
var current = association;
|
||||||
final pollDelay = _openClawAssociationPollDelayInternal(current);
|
|
||||||
final maxAttempts = math.max(
|
|
||||||
1,
|
|
||||||
((math.max(1, current.runtimeBudgetMinutes) * 60) /
|
|
||||||
math.max(1, pollDelay.inSeconds))
|
|
||||||
.ceil(),
|
|
||||||
);
|
|
||||||
var artifactRetries = 0;
|
var artifactRetries = 0;
|
||||||
for (var attempt = 0; attempt < maxAttempts; attempt += 1) {
|
var firstAttempt = true;
|
||||||
|
while (true) {
|
||||||
if (disposedInternal) {
|
if (disposedInternal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (attempt > 0) {
|
if (!firstAttempt) {
|
||||||
await Future<void>.delayed(pollDelay);
|
await Future<void>.delayed(const Duration(seconds: 2));
|
||||||
}
|
}
|
||||||
|
firstAttempt = false;
|
||||||
try {
|
try {
|
||||||
final result = await goTaskServiceClientInternal.getTask(
|
final result = await goTaskServiceClientInternal.getTask(
|
||||||
route: GoTaskServiceRoute.externalAcpSingle,
|
route: GoTaskServiceRoute.externalAcpSingle,
|
||||||
@ -812,34 +807,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toDouble();
|
|
||||||
upsertTaskThreadInternal(
|
|
||||||
sessionKey,
|
|
||||||
lifecycleStatus: 'ready',
|
|
||||||
lastRunAtMs: nowMs,
|
|
||||||
lastResultCode: 'TASK_SLA_EXPIRED',
|
|
||||||
lastArtifactSyncAtMs: nowMs,
|
|
||||||
lastArtifactSyncStatus: 'failed',
|
|
||||||
openClawTaskAssociation: current.copyWith(status: 'failed'),
|
|
||||||
updatedAtMs: nowMs,
|
|
||||||
);
|
|
||||||
aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
|
||||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
|
||||||
recomputeTasksInternal();
|
|
||||||
notifyIfActiveInternal();
|
|
||||||
}
|
|
||||||
|
|
||||||
Duration _openClawAssociationPollDelayInternal(
|
|
||||||
OpenClawTaskAssociation association,
|
|
||||||
) {
|
|
||||||
final budget = association.runtimeBudgetMinutes;
|
|
||||||
if (budget >= 60) {
|
|
||||||
return const Duration(seconds: 5);
|
|
||||||
}
|
|
||||||
if (budget >= 30) {
|
|
||||||
return const Duration(seconds: 3);
|
|
||||||
}
|
|
||||||
return const Duration(seconds: 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeOpenClawTaskAssociationsInternal({String? onlySessionKey}) {
|
void resumeOpenClawTaskAssociationsInternal({String? onlySessionKey}) {
|
||||||
@ -1323,15 +1290,6 @@ extension AppControllerDesktopThreadActions on AppController {
|
|||||||
lastResultCode: terminalResultCode,
|
lastResultCode: terminalResultCode,
|
||||||
updatedAtMs: completedAtMs,
|
updatedAtMs: completedAtMs,
|
||||||
);
|
);
|
||||||
if (isOpenClawNoExportedArtifactsGuardResultInternal(result)) {
|
|
||||||
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
|
|
||||||
upsertTaskThreadInternal(
|
|
||||||
sessionKey,
|
|
||||||
clearOpenClawTaskAssociation: true,
|
|
||||||
updatedAtMs: completedAtMs,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
upsertTaskThreadInternal(
|
upsertTaskThreadInternal(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
|
|||||||
@ -122,8 +122,6 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal {
|
|||||||
lifecycleStatus: thread?.lifecycleState.status ?? '',
|
lifecycleStatus: thread?.lifecycleState.status ?? '',
|
||||||
lastResultCode: thread?.lifecycleState.lastResultCode ?? '',
|
lastResultCode: thread?.lifecycleState.lastResultCode ?? '',
|
||||||
artifactSyncStatus: thread?.lastArtifactSyncStatus ?? '',
|
artifactSyncStatus: thread?.lastArtifactSyncStatus ?? '',
|
||||||
runtimeBudgetMinutes:
|
|
||||||
thread?.openClawTaskAssociation?.runtimeBudgetMinutes ?? 10,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return SurfaceCard(
|
return SurfaceCard(
|
||||||
|
|||||||
@ -811,22 +811,10 @@ bool _inferGoTaskSuccess(Map<String, dynamic> result) {
|
|||||||
]).toLowerCase();
|
]).toLowerCase();
|
||||||
if (status == 'failed' ||
|
if (status == 'failed' ||
|
||||||
status == 'error' ||
|
status == 'error' ||
|
||||||
status == 'artifact_missing' ||
|
|
||||||
status == 'cancelled' ||
|
status == 'cancelled' ||
|
||||||
status == 'canceled') {
|
status == 'canceled') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final code = _firstNestedGoTaskString(result, const <List<String>>[
|
|
||||||
<String>['code'],
|
|
||||||
<String>['details', 'code'],
|
|
||||||
<String>['payload', 'code'],
|
|
||||||
<String>['result', 'code'],
|
|
||||||
]).toUpperCase();
|
|
||||||
if (code == 'OPENCLAW_ARTIFACT_MISSING' ||
|
|
||||||
code == 'OPENCLAW_NO_EXPORTED_ARTIFACTS' ||
|
|
||||||
code == 'ARTIFACT_MISSING') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -921,7 +921,6 @@ class OpenClawTaskAssociation {
|
|||||||
required this.artifactScope,
|
required this.artifactScope,
|
||||||
required this.artifactDirectory,
|
required this.artifactDirectory,
|
||||||
required this.gatewayProviderId,
|
required this.gatewayProviderId,
|
||||||
required this.runtimeBudgetMinutes,
|
|
||||||
required this.startedAtMs,
|
required this.startedAtMs,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.taskLoadClass = '',
|
this.taskLoadClass = '',
|
||||||
@ -937,7 +936,6 @@ class OpenClawTaskAssociation {
|
|||||||
final String artifactScope;
|
final String artifactScope;
|
||||||
final String artifactDirectory;
|
final String artifactDirectory;
|
||||||
final String gatewayProviderId;
|
final String gatewayProviderId;
|
||||||
final int runtimeBudgetMinutes;
|
|
||||||
final double startedAtMs;
|
final double startedAtMs;
|
||||||
final String status;
|
final String status;
|
||||||
final String taskLoadClass;
|
final String taskLoadClass;
|
||||||
@ -962,7 +960,6 @@ class OpenClawTaskAssociation {
|
|||||||
artifactScope: artifactScope,
|
artifactScope: artifactScope,
|
||||||
artifactDirectory: artifactDirectory,
|
artifactDirectory: artifactDirectory,
|
||||||
gatewayProviderId: gatewayProviderId,
|
gatewayProviderId: gatewayProviderId,
|
||||||
runtimeBudgetMinutes: runtimeBudgetMinutes,
|
|
||||||
startedAtMs: startedAtMs,
|
startedAtMs: startedAtMs,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
taskLoadClass: taskLoadClass,
|
taskLoadClass: taskLoadClass,
|
||||||
@ -981,7 +978,6 @@ class OpenClawTaskAssociation {
|
|||||||
'artifactScope': artifactScope,
|
'artifactScope': artifactScope,
|
||||||
'artifactDirectory': artifactDirectory,
|
'artifactDirectory': artifactDirectory,
|
||||||
'gatewayProviderId': gatewayProviderId,
|
'gatewayProviderId': gatewayProviderId,
|
||||||
'runtimeBudgetMinutes': runtimeBudgetMinutes,
|
|
||||||
'startedAtMs': startedAtMs,
|
'startedAtMs': startedAtMs,
|
||||||
'status': status,
|
'status': status,
|
||||||
'taskLoadClass': taskLoadClass,
|
'taskLoadClass': taskLoadClass,
|
||||||
@ -1000,7 +996,6 @@ class OpenClawTaskAssociation {
|
|||||||
'artifactScope': artifactScope,
|
'artifactScope': artifactScope,
|
||||||
'artifactDirectory': artifactDirectory,
|
'artifactDirectory': artifactDirectory,
|
||||||
'gatewayProviderId': gatewayProviderId,
|
'gatewayProviderId': gatewayProviderId,
|
||||||
'runtimeBudgetMinutes': runtimeBudgetMinutes,
|
|
||||||
'taskLoadClass': taskLoadClass,
|
'taskLoadClass': taskLoadClass,
|
||||||
'sessionKey': sessionKey,
|
'sessionKey': sessionKey,
|
||||||
'requiredArtifactExtensions': requiredArtifactExtensions,
|
'requiredArtifactExtensions': requiredArtifactExtensions,
|
||||||
@ -1049,7 +1044,6 @@ class OpenClawTaskAssociation {
|
|||||||
true
|
true
|
||||||
? json['resolvedGatewayProviderId'].toString().trim()
|
? json['resolvedGatewayProviderId'].toString().trim()
|
||||||
: 'openclaw'),
|
: 'openclaw'),
|
||||||
runtimeBudgetMinutes: asInt(json['runtimeBudgetMinutes']),
|
|
||||||
startedAtMs: asDouble(json['startedAtMs']),
|
startedAtMs: asDouble(json['startedAtMs']),
|
||||||
status: json['status']?.toString().trim().isNotEmpty == true
|
status: json['status']?.toString().trim().isNotEmpty == true
|
||||||
? json['status'].toString().trim()
|
? json['status'].toString().trim()
|
||||||
|
|||||||
@ -399,13 +399,6 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _filesEmptyMessage(AssistantArtifactSnapshot snapshot) {
|
String _filesEmptyMessage(AssistantArtifactSnapshot snapshot) {
|
||||||
if (widget.artifactSyncStatus.trim().toLowerCase() ==
|
|
||||||
'no-exported-artifacts') {
|
|
||||||
return appText(
|
|
||||||
'本轮没有检测到实际生成的文件。请重新执行,并要求 OpenClaw 在当前 workspace 中创建文件。',
|
|
||||||
'No exported files were detected for this run. Run it again and ask OpenClaw to create files in the current workspace.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final filesMessage = snapshot.filesMessage.trim();
|
final filesMessage = snapshot.filesMessage.trim();
|
||||||
if (filesMessage.isNotEmpty) {
|
if (filesMessage.isNotEmpty) {
|
||||||
return filesMessage;
|
return filesMessage;
|
||||||
|
|||||||
@ -16,19 +16,16 @@ class AssistantTaskProgressState {
|
|||||||
required this.phase,
|
required this.phase,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.value,
|
this.value,
|
||||||
this.runtimeBudgetMinutes,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const AssistantTaskProgressState.idle()
|
const AssistantTaskProgressState.idle()
|
||||||
: phase = AssistantTaskProgressPhase.idle,
|
: phase = AssistantTaskProgressPhase.idle,
|
||||||
label = '',
|
label = '',
|
||||||
value = null,
|
value = null;
|
||||||
runtimeBudgetMinutes = null;
|
|
||||||
|
|
||||||
final AssistantTaskProgressPhase phase;
|
final AssistantTaskProgressPhase phase;
|
||||||
final String label;
|
final String label;
|
||||||
final double? value;
|
final double? value;
|
||||||
final int? runtimeBudgetMinutes;
|
|
||||||
|
|
||||||
bool get visible => phase != AssistantTaskProgressPhase.idle;
|
bool get visible => phase != AssistantTaskProgressPhase.idle;
|
||||||
bool get interrupted => phase == AssistantTaskProgressPhase.interrupted;
|
bool get interrupted => phase == AssistantTaskProgressPhase.interrupted;
|
||||||
@ -171,20 +168,15 @@ AssistantTaskProgressState assistantTaskProgressState({
|
|||||||
required String lifecycleStatus,
|
required String lifecycleStatus,
|
||||||
required String lastResultCode,
|
required String lastResultCode,
|
||||||
required String artifactSyncStatus,
|
required String artifactSyncStatus,
|
||||||
int? runtimeBudgetMinutes,
|
|
||||||
}) {
|
}) {
|
||||||
final syncStatus = artifactSyncStatus.trim().toLowerCase();
|
final syncStatus = artifactSyncStatus.trim().toLowerCase();
|
||||||
final status = lifecycleStatus.trim().toLowerCase();
|
final status = lifecycleStatus.trim().toLowerCase();
|
||||||
final budget = runtimeBudgetMinutes == null || runtimeBudgetMinutes <= 0
|
|
||||||
? null
|
|
||||||
: runtimeBudgetMinutes;
|
|
||||||
final result = lastResultCode.trim().toUpperCase();
|
final result = lastResultCode.trim().toUpperCase();
|
||||||
if (status == 'queued' || syncStatus == 'queued' || result == 'QUEUED') {
|
if (status == 'queued' || syncStatus == 'queued' || result == 'QUEUED') {
|
||||||
return AssistantTaskProgressState(
|
return AssistantTaskProgressState(
|
||||||
phase: AssistantTaskProgressPhase.queued,
|
phase: AssistantTaskProgressPhase.queued,
|
||||||
label: appText('任务已排队,等待执行...', 'Task queued, waiting to run...'),
|
label: appText('任务已排队,等待执行...', 'Task queued, waiting to run...'),
|
||||||
value: 0.18,
|
value: 0.18,
|
||||||
runtimeBudgetMinutes: budget,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (pending && syncStatus == 'syncing') {
|
if (pending && syncStatus == 'syncing') {
|
||||||
@ -192,14 +184,12 @@ AssistantTaskProgressState assistantTaskProgressState({
|
|||||||
phase: AssistantTaskProgressPhase.syncingArtifacts,
|
phase: AssistantTaskProgressPhase.syncingArtifacts,
|
||||||
label: appText('正在同步生成文件...', 'Syncing generated files...'),
|
label: appText('正在同步生成文件...', 'Syncing generated files...'),
|
||||||
value: 0.82,
|
value: 0.82,
|
||||||
runtimeBudgetMinutes: budget,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (pending) {
|
if (pending) {
|
||||||
return AssistantTaskProgressState(
|
return AssistantTaskProgressState(
|
||||||
phase: AssistantTaskProgressPhase.running,
|
phase: AssistantTaskProgressPhase.running,
|
||||||
label: _budgetedProgressLabel(appText('任务运行中', 'Task running'), budget),
|
label: appText('任务运行中...', 'Task running...'),
|
||||||
runtimeBudgetMinutes: budget,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (status == 'interrupted' || syncStatus == 'interrupted') {
|
if (status == 'interrupted' || syncStatus == 'interrupted') {
|
||||||
@ -222,12 +212,7 @@ AssistantTaskProgressState assistantTaskProgressState({
|
|||||||
return const AssistantTaskProgressState.idle();
|
return const AssistantTaskProgressState.idle();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _budgetedProgressLabel(String base, int? minutes) {
|
|
||||||
if (minutes == null || minutes <= 0) {
|
|
||||||
return '$base...';
|
|
||||||
}
|
|
||||||
return appText('$base,预计最长 $minutes 分钟...', '$base, up to $minutes min...');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _interruptedTaskProgressLabel(String result) {
|
String _interruptedTaskProgressLabel(String result) {
|
||||||
if (result == 'ACP_HTTP_HANDSHAKE_INTERRUPTED') {
|
if (result == 'ACP_HTTP_HANDSHAKE_INTERRUPTED') {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
_buildTestApp(
|
_buildTestApp(
|
||||||
artifactSyncAtMs: 1,
|
artifactSyncAtMs: 1,
|
||||||
artifactSyncStatus: 'no-exported-artifacts',
|
artifactSyncStatus: 'failed',
|
||||||
loadSnapshot: () async => const AssistantArtifactSnapshot(
|
loadSnapshot: () async => const AssistantArtifactSnapshot(
|
||||||
workspacePath: '/tmp/thread',
|
workspacePath: '/tmp/thread',
|
||||||
workspaceKind: WorkspaceRefKind.localPath,
|
workspaceKind: WorkspaceRefKind.localPath,
|
||||||
|
|||||||
@ -15,7 +15,6 @@ void main() {
|
|||||||
lifecycleStatus: 'running',
|
lifecycleStatus: 'running',
|
||||||
lastResultCode: 'running',
|
lastResultCode: 'running',
|
||||||
artifactSyncStatus: '',
|
artifactSyncStatus: '',
|
||||||
runtimeBudgetMinutes: 30,
|
|
||||||
),
|
),
|
||||||
onStop: () {},
|
onStop: () {},
|
||||||
),
|
),
|
||||||
@ -25,7 +24,7 @@ void main() {
|
|||||||
find.byKey(const Key('assistant-task-progress-bar')),
|
find.byKey(const Key('assistant-task-progress-bar')),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(find.text('任务运行中,预计最长 30 分钟...'), findsOneWidget);
|
expect(find.text('任务运行中...'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.byKey(const Key('assistant-task-progress-stop-button')),
|
find.byKey(const Key('assistant-task-progress-stop-button')),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
|
|||||||
@ -1350,7 +1350,7 @@ void main() {
|
|||||||
final thread = controller.requireTaskThreadForSessionInternal(
|
final thread = controller.requireTaskThreadForSessionInternal(
|
||||||
'unit-fixture-task-a',
|
'unit-fixture-task-a',
|
||||||
);
|
);
|
||||||
expect(thread.lastArtifactSyncStatus, 'no-exported-artifacts');
|
expect(thread.lastArtifactSyncStatus, 'no-artifacts');
|
||||||
expect(thread.lastArtifactSyncAtMs, greaterThan(0));
|
expect(thread.lastArtifactSyncAtMs, greaterThan(0));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -1432,7 +1432,7 @@ void main() {
|
|||||||
final thread = controller.requireTaskThreadForSessionInternal(
|
final thread = controller.requireTaskThreadForSessionInternal(
|
||||||
'unit-fixture-task-a',
|
'unit-fixture-task-a',
|
||||||
);
|
);
|
||||||
expect(thread.lastArtifactSyncStatus, 'no-exported-artifacts');
|
expect(thread.lastArtifactSyncStatus, 'no-artifacts');
|
||||||
expect(thread.lastTaskArtifactRelativePaths, isEmpty);
|
expect(thread.lastTaskArtifactRelativePaths, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1820,85 +1820,6 @@ void main() {
|
|||||||
|
|
||||||
test(
|
test(
|
||||||
'sendChatMessage restarts before handling OpenClaw artifact guard results',
|
'sendChatMessage restarts before handling OpenClaw artifact guard results',
|
||||||
() async {
|
|
||||||
final localWorkspace = await Directory.systemTemp.createTemp(
|
|
||||||
'xworkmate-acp-interrupt-guard-',
|
|
||||||
);
|
|
||||||
addTearDown(() async {
|
|
||||||
if (await localWorkspace.exists()) {
|
|
||||||
await localWorkspace.delete(recursive: true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const guardMessage =
|
|
||||||
'未检测到 OpenClaw 本轮导出的实际文件。已阻止口头下载声明进入 artifacts 面板;请重新执行并要求 OpenClaw 在 workspace 中真实生成文件。';
|
|
||||||
final fakeGoTaskService = _RecordingGoTaskServiceClient()
|
|
||||||
..updatesBeforeNextOutcome.add(
|
|
||||||
const GoTaskServiceUpdate(
|
|
||||||
sessionId: 'unit-fixture-task-a',
|
|
||||||
threadId: 'unit-fixture-task-a',
|
|
||||||
turnId: 'turn-1',
|
|
||||||
type: 'delta',
|
|
||||||
text: 'guard partial output must not persist',
|
|
||||||
message: '',
|
|
||||||
pending: true,
|
|
||||||
error: false,
|
|
||||||
route: GoTaskServiceRoute.externalAcpSingle,
|
|
||||||
payload: <String, dynamic>{},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..outcomes.add(
|
|
||||||
const GatewayAcpException(
|
|
||||||
'ACP HTTP connection closed before the response finished arriving',
|
|
||||||
code: 'ACP_HTTP_CONNECTION_CLOSED',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..outcomes.add(
|
|
||||||
const GoTaskServiceResult(
|
|
||||||
success: true,
|
|
||||||
message: guardMessage,
|
|
||||||
turnId: 'turn-2',
|
|
||||||
raw: <String, dynamic>{'code': 'OPENCLAW_NO_EXPORTED_ARTIFACTS'},
|
|
||||||
errorMessage: '',
|
|
||||||
resolvedModel: '',
|
|
||||||
route: GoTaskServiceRoute.externalAcpSingle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final controller = _connectedController(
|
|
||||||
fakeGoTaskService,
|
|
||||||
homeDir: localWorkspace.path,
|
|
||||||
);
|
|
||||||
addTearDown(controller.dispose);
|
|
||||||
|
|
||||||
await controller.sessionsController.switchSession(
|
|
||||||
'unit-fixture-task-a',
|
|
||||||
);
|
|
||||||
|
|
||||||
await controller.sendChatMessage('first turn');
|
|
||||||
await controller.sendChatMessage('follow up');
|
|
||||||
|
|
||||||
expect(fakeGoTaskService.requests, hasLength(2));
|
|
||||||
expect(fakeGoTaskService.requests.first.resumeSession, isFalse);
|
|
||||||
expect(fakeGoTaskService.requests.last.resumeSession, isTrue);
|
|
||||||
|
|
||||||
final transcript = controller.chatMessages
|
|
||||||
.map((message) => message.text)
|
|
||||||
.join('\n');
|
|
||||||
expect(transcript, isNot(contains('未检测到 OpenClaw 本轮导出的实际文件')));
|
|
||||||
expect(transcript, isNot(contains('口头下载声明')));
|
|
||||||
expect(
|
|
||||||
transcript,
|
|
||||||
isNot(contains('guard partial output must not persist')),
|
|
||||||
);
|
|
||||||
|
|
||||||
final thread = controller.taskThreadForSessionInternal(
|
|
||||||
'unit-fixture-task-a',
|
|
||||||
);
|
|
||||||
expect(thread?.lifecycleState.status, 'ready');
|
|
||||||
expect(thread?.lastArtifactSyncStatus, 'no-exported-artifacts');
|
|
||||||
expect(thread?.lastArtifactSyncAtMs, greaterThan(0));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'sendChatMessage starts a new session after OpenClaw terminal artifact failure',
|
'sendChatMessage starts a new session after OpenClaw terminal artifact failure',
|
||||||
() async {
|
() async {
|
||||||
@ -1947,7 +1868,7 @@ void main() {
|
|||||||
failedThread?.lifecycleState.lastResultCode,
|
failedThread?.lifecycleState.lastResultCode,
|
||||||
'OPENCLAW_NO_EXPORTED_ARTIFACTS',
|
'OPENCLAW_NO_EXPORTED_ARTIFACTS',
|
||||||
);
|
);
|
||||||
expect(failedThread?.lastArtifactSyncStatus, 'no-exported-artifacts');
|
expect(failedThread?.lastArtifactSyncStatus, 'failed');
|
||||||
|
|
||||||
await controller.sendChatMessage('retry final artifact');
|
await controller.sendChatMessage('retry final artifact');
|
||||||
|
|
||||||
@ -1960,76 +1881,6 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
|
||||||
'sendChatMessage hides OpenClaw artifact guard text from failed results and streaming',
|
|
||||||
() async {
|
|
||||||
final localWorkspace = await Directory.systemTemp.createTemp(
|
|
||||||
'xworkmate-acp-guard-failure-',
|
|
||||||
);
|
|
||||||
addTearDown(() async {
|
|
||||||
if (await localWorkspace.exists()) {
|
|
||||||
await localWorkspace.delete(recursive: true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const guardMessage =
|
|
||||||
'未检测到 OpenClaw 本轮导出的实际文件。已阻止口头下载声明进入 artifacts 面板;请重新执行并要求 OpenClaw 在 workspace 中真实生成文件。';
|
|
||||||
final fakeGoTaskService = _RecordingGoTaskServiceClient()
|
|
||||||
..updatesBeforeNextOutcome.add(
|
|
||||||
const GoTaskServiceUpdate(
|
|
||||||
sessionId: 'unit-fixture-task-a',
|
|
||||||
threadId: 'unit-fixture-task-a',
|
|
||||||
turnId: 'turn-1',
|
|
||||||
type: 'delta',
|
|
||||||
text: guardMessage,
|
|
||||||
message: '',
|
|
||||||
pending: true,
|
|
||||||
error: false,
|
|
||||||
route: GoTaskServiceRoute.externalAcpSingle,
|
|
||||||
payload: <String, dynamic>{},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..outcomes.add(
|
|
||||||
const GoTaskServiceResult(
|
|
||||||
success: false,
|
|
||||||
message: '',
|
|
||||||
turnId: 'turn-1',
|
|
||||||
raw: <String, dynamic>{
|
|
||||||
'status': 'artifact_missing',
|
|
||||||
'code': 'OPENCLAW_ARTIFACT_MISSING',
|
|
||||||
'artifactWarnings': <String>[
|
|
||||||
'OpenClaw artifact export returned no files for a file-delivery request.',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
errorMessage: guardMessage,
|
|
||||||
resolvedModel: '',
|
|
||||||
route: GoTaskServiceRoute.externalAcpSingle,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final controller = _connectedController(
|
|
||||||
fakeGoTaskService,
|
|
||||||
homeDir: localWorkspace.path,
|
|
||||||
);
|
|
||||||
addTearDown(controller.dispose);
|
|
||||||
|
|
||||||
await controller.sessionsController.switchSession(
|
|
||||||
'unit-fixture-task-a',
|
|
||||||
);
|
|
||||||
await controller.sendChatMessage('create files');
|
|
||||||
|
|
||||||
final transcript = controller.chatMessages
|
|
||||||
.map((message) => message.text)
|
|
||||||
.join('\n');
|
|
||||||
expect(transcript, isNot(contains('未检测到 OpenClaw 本轮导出的实际文件')));
|
|
||||||
expect(transcript, isNot(contains('口头下载声明')));
|
|
||||||
final thread = controller.taskThreadForSessionInternal(
|
|
||||||
'unit-fixture-task-a',
|
|
||||||
);
|
|
||||||
expect(thread?.lifecycleState.lastResultCode, 'artifact_missing');
|
|
||||||
expect(thread?.lastArtifactSyncStatus, 'no-exported-artifacts');
|
|
||||||
expect(thread?.lastArtifactSyncAtMs, greaterThan(0));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'sendChatMessage restarts after ACP HTTP handshake interruption',
|
'sendChatMessage restarts after ACP HTTP handshake interruption',
|
||||||
() async {
|
() async {
|
||||||
@ -4179,7 +4030,7 @@ void main() {
|
|||||||
await _waitForThreadArtifactSyncStatusWithin(
|
await _waitForThreadArtifactSyncStatusWithin(
|
||||||
controller,
|
controller,
|
||||||
'openclaw-missing-screenshot',
|
'openclaw-missing-screenshot',
|
||||||
'no-exported-artifacts',
|
'no-artifacts',
|
||||||
const Duration(seconds: 10),
|
const Duration(seconds: 10),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -4188,7 +4039,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(thread.lifecycleState.status, 'ready');
|
expect(thread.lifecycleState.status, 'ready');
|
||||||
expect(thread.lifecycleState.lastResultCode, 'success');
|
expect(thread.lifecycleState.lastResultCode, 'success');
|
||||||
expect(thread.lastArtifactSyncStatus, 'no-exported-artifacts');
|
expect(thread.lastArtifactSyncStatus, 'no-artifacts');
|
||||||
expect(thread.openClawTaskAssociation, isNull);
|
expect(thread.openClawTaskAssociation, isNull);
|
||||||
expect(
|
expect(
|
||||||
controller.assistantSessionHasPendingRun(
|
controller.assistantSessionHasPendingRun(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user