fix: restore cli workspace thread id visibility
This commit is contained in:
@@ -25,11 +25,11 @@
|
||||
- Code Agent 交互必须默认暴露 `traceId`、`resultUrl`、终态和 assistant 回复文本摘要;不能要求用户先拉全量 trace 再手工查找回复。
|
||||
- CLI 本地登录态必须支持 `--profile NAME` 隔离,同一 base URL 下不同 profile 写入 `.state/hwlab-cli/profiles/<base-url-hash>/<profile>.json`。切换到其他账号再切回原账号时,`client workbench restore/status` 必须从服务端账号 workspace 恢复之前的 `workspaceId`、`conversationId`、`sessionId`、`threadId`、`activeTraceId` 和 revision,而不是只依赖本地文件。
|
||||
- `client workbench restore/status/watch/reset` 是账号 workspace 的非视觉入口:`restore/status` 对应 `GET /v1/workbench/workspace`,`watch` 对应 `/events?afterRevision=`,`reset --confirm` 对应服务端 reset。输出必须显示 workspace revision、selected conversation/session、active trace 和本地 state file,且不得保存 password、session token 原文以外的 Secret 值。
|
||||
- `client agent send` 是 Cloud Web Code Agent composer 的非视觉等价入口。它必须支持 `--from-trace`、`--conversation-id`、`--session-id`、`--thread-id` 和 `--retry-of`,并在输出中返回 redacted continuation 摘要,证明本次 CLI 请求是否覆盖 Web 的继续会话路径。浏览器 issue 中已经给出 traceId 时,复现命令优先使用 `--from-trace <traceId>`,让 CLI 先走 `/v1/agent/chat/inspect` 读取 Web 上下文,再提交同源 `/v1/agent/chat`。
|
||||
- `client agent send` 是 Cloud Web Code Agent composer 的非视觉等价入口。它必须支持 `--from-trace`、`--conversation-id`、`--session-id`、`--thread-id` 和 `--retry-of`,并在输出中返回 redacted continuation 摘要,证明本次 CLI 请求是否覆盖 Web 的继续会话路径。浏览器 issue 中已经给出 traceId 时,复现命令优先使用 `--from-trace <traceId>`,让 CLI 先走 `/v1/agent/chat/inspect` 读取标准 continuation IDs,再提交同源 `/v1/agent/chat`。
|
||||
- `client agent send --from-trace` 只能用 inspect 恢复 `conversationId/sessionId/threadId/retryOf` 和提交本轮原始消息;不得把 inspect 的 messages/facts 作为 `conversationContext`、`messages` 或 prompt 前缀提交。CLI 的 continuation 摘要只用于可见性,不是模型上下文。
|
||||
- `client agent composer status|submit` 是 Cloud Web composer 的状态机等价入口。`status` 必须先恢复账号 workspace,再用与 Web 相同的 composer policy 输出 `locked`、`disabled`、`submitMode`、`route`、`targetTraceId`、conversation/session/thread 和 workspace revision;运行中 turn 必须显示 `locked=false`、`disabled=false`、`submitMode=steer`。`submit` 必须按该 policy 自动选择 `/v1/agent/chat` 或 `/v1/agent/chat/steer`,不得要求用户手动判断 URL 或绕过 Cloud Web 同源 path。
|
||||
- Code Agent continuation 的 thread 字段只有 `threadId` 一个标准名称。CLI 读取 inspect、`--from-trace` 回放、手动 `--thread-id` 提交和输出摘要都必须以该字段为唯一 thread identity;服务端响应也应保持同一字段口径。
|
||||
- `client agent send` 默认先恢复账号 workspace,再向 `/v1/agent/chat` 发送 `workspaceId` 和 `expectedWorkspaceRevision`;服务端接受后 CLI 保存新的 workspace revision,终态轮询后再 PATCH workspace 清理终态 `activeTraceId`。只有显式 `--no-workspace` 才跳过这一默认恢复路径。
|
||||
- `client agent send` 默认先恢复账号 workspace,从 selected conversation/session 和 workspace JSON 中读取标准 `conversationId/sessionId/threadId`,再向 `/v1/agent/chat` 发送这些 IDs、`workspaceId` 和 `expectedWorkspaceRevision`;服务端接受后 CLI 保存新的 workspace revision,终态轮询后再 PATCH workspace 清理终态 `activeTraceId`。默认 workspace 恢复只恢复 ID 和 revision,不恢复 messages/facts,不生成 `conversationContext`,也不得把历史文本拼入 prompt。只有显式 `--no-workspace` 才跳过这一默认恢复路径。
|
||||
- `client agent steer <traceId>` 是运行中引导入口,必须调用 Cloud Web 同源 `POST /v1/agent/chat/steer`,把 steer 文本装配成 AgentRun `type=steer` command 作用到目标 trace 的 active turn。CLI 不手动穿内部 URL;验收使用当前 runtime namespace/lane 自动解析的 `19666` Web 入口,并通过原 trace 的 result/trace 观察 steer 是否被 runner 接收和应用。
|
||||
- `client agent trace <traceId> --render web` 必须调用 Cloud Web trace row 的同一纯转换路径,输出 `render="web"`、renderer 标识、source event count、rendered row count、默认压制的 noise event count 和 row 摘要。浏览器 trace 展示错乱时,必须先用该 CLI 入口确认 Web 渲染转换是否已经乱序、重复、缺 final response、吞掉关键 row 或只显示泛化 tool call,再继续修浏览器 DOM/CSS。
|
||||
- AgentRun v0.1 短连接 runner 已要求支持同 run/runner 多轮 command。CLI 仍应把 Web 提交的 `conversationId/sessionId/threadId` 原样送到 Cloud Web API,用于验证 adapter 是否在 runner lease 有效时复用同一个 AgentRun `runId` / `jobName` 并创建新 `commandId`;每轮都新建 runner 或重新 bundle 不是通过状态,trace 中的原因说明只能用于定位。
|
||||
|
||||
@@ -320,7 +320,7 @@ test("hwlab-cli client agent send submits async and returns trace without waitin
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null });
|
||||
if (String(url).endsWith("/v1/workbench/workspace?projectId=prj_device_pod_workbench")) {
|
||||
return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_test", revision: 3, selectedConversationId: "cnv_test", selectedAgentSessionId: "ses_test", workspace: { threadId: "thread-test" } } }), { status: 200 });
|
||||
return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_test", revision: 3, selectedConversationId: "cnv_test", selectedAgentSessionId: "ses_test", selectedConversation: { conversationId: "cnv_test", sessionId: "ses_test", threadId: "thread-test" }, workspace: {} } }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/chat")) {
|
||||
return new Response(JSON.stringify({ accepted: true, status: "running", traceId: "trc_test", resultUrl: "/v1/agent/chat/result/trc_test" }), { status: 202 });
|
||||
@@ -339,8 +339,11 @@ test("hwlab-cli client agent send submits async and returns trace without waitin
|
||||
assert.equal(calls[1].body.expectedWorkspaceRevision, 3);
|
||||
assert.equal(calls[1].body.sessionId, "ses_test");
|
||||
assert.equal(calls[1].body.threadId, "thread-test");
|
||||
assert.equal(Object.hasOwn(calls[1].body, "conversationContext"), false);
|
||||
assert.equal(Object.hasOwn(calls[1].body, "messages"), false);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(result.payload.traceId, "trc_test");
|
||||
assert.equal(result.payload.continuation.threadId, "thread-test");
|
||||
assert.equal(result.payload.waited, false);
|
||||
assert.equal(result.payload.waitPolicy.defaultWait, false);
|
||||
assert.equal(result.payload.waitPolicy.webEquivalent, true);
|
||||
@@ -372,13 +375,13 @@ test("hwlab-cli client agent send waits only when --wait is explicit", async ()
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url: String(url), init, body: init?.body ? JSON.parse(String(init.body)) : null });
|
||||
if (String(url).endsWith("/v1/workbench/workspace?projectId=prj_device_pod_workbench")) {
|
||||
return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_wait", revision: 3, selectedConversationId: "cnv_wait", selectedAgentSessionId: "ses_wait", workspace: { threadId: "thread-wait" } } }), { status: 200 });
|
||||
return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_wait", revision: 3, selectedConversationId: "cnv_wait", selectedAgentSessionId: "ses_wait", selectedConversation: { conversationId: "cnv_wait", sessionId: "ses_wait", threadId: "thread-wait" }, workspace: {} } }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/chat")) {
|
||||
return new Response(JSON.stringify({ accepted: true, status: "running", traceId: "trc_wait", resultUrl: "/v1/agent/chat/result/trc_wait" }), { status: 202 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/workbench/workspace/wsp_wait")) {
|
||||
return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_wait", revision: 5, selectedConversationId: "cnv_wait", selectedAgentSessionId: "ses_wait", workspace: { threadId: "thread-wait", sessionStatus: "completed" } } }), { status: 200 });
|
||||
return new Response(JSON.stringify({ ok: true, workspace: { workspaceId: "wsp_wait", revision: 5, selectedConversationId: "cnv_wait", selectedAgentSessionId: "ses_wait", selectedConversation: { conversationId: "cnv_wait", sessionId: "ses_wait", threadId: "thread-wait" }, workspace: { sessionStatus: "completed" } } }), { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ status: "completed", traceId: "trc_wait", conversationId: "cnv_wait", reply: { role: "assistant", content: "hi" } }), { status: 200 });
|
||||
},
|
||||
@@ -386,11 +389,14 @@ test("hwlab-cli client agent send waits only when --wait is explicit", async ()
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(calls[1].body.threadId, "thread-wait");
|
||||
assert.equal(calls[2].url, "http://web.test/v1/agent/chat/result/trc_wait");
|
||||
assert.equal(calls[3].url, "http://web.test/v1/workbench/workspace/wsp_wait");
|
||||
assert.equal(Object.hasOwn(calls[3].body, "activeTraceId"), true);
|
||||
assert.equal(calls[3].body.activeTraceId, null);
|
||||
assert.equal(calls[3].body.threadId, "thread-wait");
|
||||
assert.equal(result.payload.traceId, "trc_wait");
|
||||
assert.equal(result.payload.continuation.threadId, "thread-wait");
|
||||
assert.equal(result.payload.result.body.status, "completed");
|
||||
assert.equal(result.payload.result.body.assistantText, "hi");
|
||||
assert.equal(result.payload.result.body.reply.content, "hi");
|
||||
|
||||
@@ -1761,6 +1761,7 @@ async function saveAcceptedAgentWorkspaceState(context: any, acceptedBody: any,
|
||||
revision: numberOption(acceptedBody?.workspaceRevision) ?? existing.revision,
|
||||
selectedConversationId: text(fallback.conversationId) || text(existing.selectedConversationId),
|
||||
selectedAgentSessionId: text(fallback.sessionId) || text(existing.selectedAgentSessionId),
|
||||
threadId: text(acceptedBody?.threadId ?? acceptedBody?.session?.threadId ?? acceptedBody?.providerTrace?.threadId) || text(fallback.threadId) || text(existing.threadId),
|
||||
activeTraceId: text(acceptedBody?.traceId) || text(fallback.traceId),
|
||||
providerProfile: text(fallback.providerProfile) || text(existing.providerProfile),
|
||||
updatedByClient: "hwlab-cli",
|
||||
@@ -1820,7 +1821,7 @@ function normalizeWorkbenchWorkspace(workspace: any) {
|
||||
activeTraceId: text(workspace.activeTraceId ?? workspaceJson.activeTraceId),
|
||||
providerProfile: text(workspace.providerProfile ?? workspaceJson.providerProfile),
|
||||
sessionStatus: text(workspaceJson.sessionStatus),
|
||||
threadId: text(workspaceJson.threadId),
|
||||
threadId: workbenchThreadId(workspace, workspaceJson),
|
||||
updatedAt: text(workspace.updatedAt),
|
||||
updatedByClient: text(workspace.updatedByClient),
|
||||
messages: Array.isArray(workspaceJson.messages) ? workspaceJson.messages : undefined,
|
||||
@@ -1830,6 +1831,19 @@ function normalizeWorkbenchWorkspace(workspace: any) {
|
||||
});
|
||||
}
|
||||
|
||||
function workbenchThreadId(workspace: any, workspaceJson: any) {
|
||||
const selectedConversation = workspace?.selectedConversation && typeof workspace.selectedConversation === "object" ? workspace.selectedConversation : {};
|
||||
const selectedAgentSession = workspace?.selectedAgentSession && typeof workspace.selectedAgentSession === "object" ? workspace.selectedAgentSession : {};
|
||||
const selectedConversationJson = workspaceJson?.selectedConversation && typeof workspaceJson.selectedConversation === "object" ? workspaceJson.selectedConversation : {};
|
||||
const selectedAgentSessionJson = workspaceJson?.selectedAgentSession && typeof workspaceJson.selectedAgentSession === "object" ? workspaceJson.selectedAgentSession : {};
|
||||
return text(workspaceJson?.threadId)
|
||||
|| text(workspace?.threadId)
|
||||
|| text(selectedConversation.threadId)
|
||||
|| text(selectedAgentSession.threadId)
|
||||
|| text(selectedConversationJson.threadId)
|
||||
|| text(selectedAgentSessionJson.threadId);
|
||||
}
|
||||
|
||||
function workspaceSummaryFromSession(session: any) {
|
||||
return normalizeWorkbenchWorkspace(session?.workspace);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user