Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { useTranslation } from 'react-i18next';
import {
deriveAgentTaskStatus,
isSubagentResultError,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
type AgentTaskTerminalStatus,
} from '@cindy/maker-shared/agent-task';

Expand Down Expand Up @@ -235,6 +236,7 @@ export function AgentTaskCard({
resultIsLaunchReceipt:
subagentSpawnReceiptName(toolCall?.toolName, toolCall?.toolInput, result) !== undefined
|| subagentSpawnResultIndicatesRunning(toolCall?.toolName, result),
resultIsError: isSubagentResultError(result),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 跨端终态不一致

当历史 Claude 子任务只有以 <tool_use_error> 开头的工具结果、没有持久化终态或实时更新时,这里只让 Desktop 聊天卡片将其判为 failed;Mobile 共享卡片模型和 Desktop 后台任务列表仍按“有结果即完成”推导为 completed,导致同一启动失败在重连或重载后的不同入口显示相反终态。

Context Used: 使用和PR描述相同的语言进行评论 (source)

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx
Line: 239

Comment:
**跨端终态不一致**

当历史 Claude 子任务只有以 `<tool_use_error>` 开头的工具结果、没有持久化终态或实时更新时,这里只让 Desktop 聊天卡片将其判为 `failed`;Mobile 共享卡片模型和 Desktop 后台任务列表仍按“有结果即完成”推导为 `completed`,导致同一启动失败在重连或重载后的不同入口显示相反终态。

**Context Used:** 使用和PR描述相同的语言进行评论 ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

**Knowledge Base Used:**
- [Agent runtime](https://app.greptile.com/xindong/-/custom-context/knowledge-base/makecindy/cindy/-/docs/agent-runtime.md)
- [Desktop renderer experience](https://app.greptile.com/xindong/-/custom-context/knowledge-base/makecindy/cindy/-/docs/desktop-renderer-experience.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

});
const StatusIcon = statusIcon(status);
const statusIconClassName = cn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import {
deriveAgentTaskStatus,
isAgentTaskToolName,
isSubagentResultError,
subagentSpawnReceiptName,
subagentSpawnResultIndicatesRunning,
} from '@cindy/maker-shared/agent-task';
Expand Down Expand Up @@ -341,6 +342,7 @@ export function listSessionTasks(input: {
resultIsLaunchReceipt:
subagentSpawnReceiptName(toolName, toolInput, resultText) !== undefined
|| subagentSpawnResultIndicatesRunning(toolName, resultText),
resultIsError: isSubagentResultError(resultText),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 在无实时 update 时恢复后台任务失败状态

当 Desktop 重载或远程控制端重连后,live-only 的 taskUpdates 为空,这个新增判定不会执行,因为它仍位于 update ? ... : ... 的真分支;即使配对结果是 <tool_use_error>,第 347 行仍会按 settled 将任务列为 completed,而且列表也没有读取消息上持久化的 agentTaskStatus。此前线程指出过后台列表回放错误;当前提交的新证据是错误判定只接入了存在实时 update 的路径,因此应在无 update 分支同样采用协议错误结果或持久化终态。

Useful? React with 👍 / 👎.

})
: (settled ? 'completed' : isSessionStreaming ? 'running' : 'stopped');
const provider: SessionTaskItem['provider'] =
Expand Down
62 changes: 62 additions & 0 deletions packages/maker-shared/src/__tests__/agentTask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
deriveAgentTaskStatus,
findAgentTaskUpdate,
isAgentTaskToolName,
isSubagentResultError,
isSubagentSpawnToolName,
mergeAgentTaskUpdate,
PI_SUBAGENT_TOOL_NAME,
Expand Down Expand Up @@ -113,6 +114,31 @@ describe('deriveAgentTaskStatus', () => {
persistedStatus: 'cancelled' as never,
})).toBe('completed');
});

it('returns failed when resultIsError is true and result is non-empty', () => {
expect(deriveAgentTaskStatus(undefined, '<tool_use_error>Auth failed</tool_use_error>', {
resultIsError: true,
})).toBe('failed');
});

it('returns failed even when updateStatus is running when resultIsError is true', () => {
expect(deriveAgentTaskStatus('running', '<tool_use_error>timeout</tool_use_error>', {
resultIsError: true,
})).toBe('failed');
});

it('persisted status wins over resultIsError', () => {
expect(deriveAgentTaskStatus(undefined, '<tool_use_error>fail</tool_use_error>', {
resultIsError: true,
persistedStatus: 'completed',
})).toBe('completed');
});

it('resultIsError is ignored when result is empty', () => {
expect(deriveAgentTaskStatus(undefined, '', {
resultIsError: true,
})).toBe('running');
});
});

describe('mergeAgentTaskUpdate', () => {
Expand Down Expand Up @@ -525,3 +551,39 @@ describe('buildAgentTaskCardModel', () => {
expect(model.summary).toBe('thread-2: done');
});
});

describe('isSubagentResultError', () => {
it('returns false for empty/undefined/null', () => {
expect(isSubagentResultError(undefined)).toBe(false);
expect(isSubagentResultError('')).toBe(false);
expect(isSubagentResultError(' ')).toBe(false);
});

it('returns true for Claude protocol <tool_use_error>', () => {
expect(isSubagentResultError('<tool_use_error>Authentication failed</tool_use_error>')).toBe(true);
});

it('returns false for generic <error> prefix (too generic for work product)', () => {
// A subagent returning <error>校验报告</error> as valid output must not be
// misclassified as failure. Only <tool_use_error> is a reliable protocol marker.
expect(isSubagentResultError('<error>Transport failure</error>')).toBe(false);
expect(isSubagentResultError('<error>校验报告</error>')).toBe(false);
});

it('returns false for JSON with error-looking fields (authority boundary)', () => {
// Subagent result content is arbitrary user work product.
// Fields like "errors", "status", "stderr" are data, not execution signals.
expect(isSubagentResultError('{"errors":["not found"]}')).toBe(false);
expect(isSubagentResultError('{"status":"failed"}')).toBe(false);
expect(isSubagentResultError('{"stderr":"something went wrong"}')).toBe(false);
expect(isSubagentResultError('{"error":"custom message"}')).toBe(false);
expect(isSubagentResultError('{"success":false}')).toBe(false);
expect(isSubagentResultError('{"ok":false}')).toBe(false);
});

it('returns false for natural language error phrases', () => {
expect(isSubagentResultError('failed to launch')).toBe(false);
expect(isSubagentResultError('unable to start the service')).toBe(false);
expect(isSubagentResultError('Error: something happened')).toBe(false);
});
});
35 changes: 35 additions & 0 deletions packages/maker-shared/src/agentTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,50 @@ export function deriveAgentTaskStatus(
options?: {
resultIsLaunchReceipt?: boolean;
persistedStatus?: AgentTaskTerminalStatus;
resultIsError?: boolean;
},
): AgentTaskStatus {
const persistedStatus = normalizeAgentTaskTerminalStatus(options?.persistedStatus);
if (persistedStatus) return persistedStatus;
const hasResult = typeof result === 'string' && result.trim().length > 0;
if (options?.resultIsError && hasResult) return 'failed';
if (updateStatus === 'running' && hasResult && !options?.resultIsLaunchReceipt) return 'completed';
return updateStatus ?? (hasResult ? 'completed' : 'running');
}

/**
* 判断子任务工具结果是否以协议级错误收尾(历史回放恢复 failed 的依据)。
*
* 仅识别 Claude SDK 协议标记 `<tool_use_error>` — 这是 SDK 在 tool call 失败时
* 发出的结构化错误格式。不解析任意 JSON 字段或自然语言错误短语,因为子任务结果
* 内容是用户工作产物,其中 "errors"/"status"/"stderr" 等字段是数据而非执行信号。
*
* 调用方约束:此函数仅应在已确认为子任务上下文的调用点使用
* (AgentTaskCard / listSessionTasks)。普通工具结果包含 `<tool_use_error>` 时
* 不应传入此函数,否则会将非子任务结果误判为失败。
*
* Authority: Claude protocol `<tool_use_error>` — SDK 在 tool call 失败时发出。
*/
export function isSubagentResultError(result: string | undefined): boolean {
Comment thread
Battleplus marked this conversation as resolved.
const text = typeof result === 'string' ? result.trim() : '';
if (text.length === 0) return false;
// Only trust protocol-level error markers. Subagent result content is arbitrary
// user work product -- fields like "errors", "status", "stderr" in JSON output
// are data, not execution failure signals. Parsing arbitrary body for
// error-looking fields creates false positives that mark successful tasks as
// failed after Desktop reload / Mobile reconnect.
//
// Authority sources:
// 1. Claude protocol <tool_use_error> -- emitted by the SDK when a tool call fails
// 2. Persisted structured terminal status (agentTaskStatus) -- written by
// messagePersistBroadcaster on terminal observations
//
// Note: <error> prefix removed -- too generic. A subagent returning
// `<error>校验报告</error>` as work output would be misclassified as failure.
// Only <tool_use_error> is a reliable protocol-owned error marker.
return text.startsWith('<tool_use_error>');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 将协议错误判定接入 Mobile 共用卡片模型

当 Mobile 重连后只有配对的 <tool_use_error> 结果、但没有 live update 或持久化终态(例如旧记录或终态写回前退出)时,这个新增 helper 仍不会生效:Mobile 的 MessageRenderer.AgentTaskCard 调用 buildAgentTaskCardModel,而该模型在当前提交第 479–484 行调用 deriveAgentTaskStatus 时没有传入 resultIsError,所以结果会恢复成 completed。此前线程称共享历史推导已经接入;当前提交重新新增 helper 却只接了两个 Desktop 消费者,这是该问题再次出现的新证据。应让共享模型按 Claude TaskAgent 上下文使用该判定,才能覆盖声明的 Mobile 重连场景。 docs/dev-rules/remote-and-mobile-adaptation.mdL7-L15

Useful? React with 👍 / 👎.

}

/**
* Tool names that spawn a sub-agent task: Claude `Task`/`Agent`, Codex collab agents,
* PI `subagent`(Cindy 自有扩展注册的工具名,与 pi 社区惯例一致)。
Expand Down