mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-01 15:30:52 +08:00
Fix target session thread submission
This commit is contained in:
@@ -79,3 +79,15 @@
|
||||
- Gate B failure path 已补只读诊断,失败时保存 `thread/read`、`log/list`、`log/persistedTail`、`diagnostics/server/read`、renderer invoke trace、provider request 与 MCP ledger,供下一次 Windows runner 定位 turn 终态/错误;不改变通过条件。
|
||||
- runner `31345998150` 使用完整 SHA `c228ffde93f14000d7ee6daa99113c96164a0a7a`;Checkout、插件路径契约、sherpa runtime、Electron Windows 包、N-1 Squirrel 下载与安装 smoke 均通过,Plugin Gate B 仍在 `submit-renderer-form` 等待 90 秒后失败。失败诊断确认 enabled turn 已 `completed/idle`、provider request 为 `0`、MCP ledger 仅有 runtime `initialize`,且失败截图显示 disabled boundary 文本;尚未取得结构化 turn/item 错误字段。
|
||||
- 为下一轮 Windows runner 增加安全的 `thread/read` turn/item 状态摘要与 localhost provider connection diagnostics;本地 Plugin Gate B 已通过。下一步用该证据确认是 runtime turn 终态错误、Plugin snapshot/Skill 解析,还是 renderer session 投影漂移,再实施窄产品修复。
|
||||
|
||||
## Windows target session 投影修复
|
||||
|
||||
状态:`fix-validated-locally / windows-runner-pending`
|
||||
|
||||
- runner `31347609969` 使用完整 SHA `22a96e5e1df9b771120ba6ea26ab7f562d5eafcd`;Windows path contract、sherpa runtime、Electron Windows package、N-1 Squirrel 安装 smoke 均通过,Plugin Gate B 仍在等待 `mcp_elicitation` 90 秒后失败。
|
||||
- 失败证据确认 enabled canonical turn 已 `completed`、thread 已 `idle`,items 只有 user message 与空 agent message;enabled provider 只有 `/v1/models`,没有 `/v1/chat/completions`,而 disabled boundary provider 收到后续请求。失败截图标题已进入 enabled 会话,但正文仍显示 disabled boundary 结果,证明 renderer 的目标 session 与提交 thread 发生漂移。
|
||||
- 根因:显式 `targetSessionId` 发送链完成 `ensureSession(target)` 后,canonical thread lookup 仍读取全局 `threadReadRef`。Windows 慢时序下该 ref 可能仍绑定旧会话,导致 enabled 请求提交到 disabled boundary thread/provider。
|
||||
- 修复:`useAgentSession` 按 session 记录 canonical `thread_id`;显式目标提交只允许按目标 session 查询,映射缺失时刷新该目标的 read model,禁止回退旧会话 thread;同步收紧 prepared send / stream 类型契约并补时序回归测试。
|
||||
- 窄写集:`src/components/agent/chat/hooks/useAgentSession.ts`、`agentStreamSubmitExecution.ts`、`agentStreamPreparedSendEnv.ts`、`useAgentStream.ts`、`agentStreamSubmitExecution.test.ts` 与本计划。上述部分文件同时含并发工作树改动,提交前必须重新确认 release candidate 范围。
|
||||
- 本地验证:相关 Vitest `10/10` 通过;`npm run typecheck` 通过;`npm run smoke:plugin-package-electron-gate-b -- --timeout-ms 180000 --keep-temp` 通过,enabled provider request `2`、MCP elicitation accepted、provider final text observed、production mock fallback `0`。
|
||||
- 下一步:提交并推送完整依赖闭包后,以新完整 SHA 重触发 `build-windows-test.yml`,持续跟踪到 Plugin Gate B 与 artifact 结论;不移动已发布的 `v1.125.0` tag。
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface AgentStreamPreparedSendEnv {
|
||||
sessionIdRef: MutableRefObject<string | null>;
|
||||
runPreparedSubmit: <T>(task: () => Promise<T>) => Promise<T>;
|
||||
getWorkspaceIdForSubmit: () => string | undefined;
|
||||
getThreadIdForSubmit: () => string | undefined;
|
||||
getThreadIdForSubmit: (targetSessionId?: string) => string | undefined;
|
||||
getSyncedSessionModelPreference: (
|
||||
sessionId: string,
|
||||
) => SessionModelPreference | null;
|
||||
|
||||
@@ -400,6 +400,18 @@ describe("agentStreamSubmitExecution", () => {
|
||||
const unlisten = vi.fn();
|
||||
const submitOp = vi.fn(async () => {});
|
||||
const ensureSession = vi.fn(async () => "session-materialized");
|
||||
const threadIdBySessionId = new Map<string, string>();
|
||||
const getThreadIdForSubmit = vi.fn((targetSessionId?: string) =>
|
||||
targetSessionId
|
||||
? threadIdBySessionId.get(targetSessionId)
|
||||
: "thread-previous",
|
||||
);
|
||||
const refreshSessionReadModel = vi.fn(async (targetSessionId?: string) => {
|
||||
if (targetSessionId) {
|
||||
threadIdBySessionId.set(targetSessionId, "thread-materialized");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const registerListener = vi.fn();
|
||||
const activateStream = vi.fn();
|
||||
const runtime = {
|
||||
@@ -420,12 +432,12 @@ describe("agentStreamSubmitExecution", () => {
|
||||
runtime,
|
||||
ensureSession,
|
||||
attemptSilentTurnRecovery: async () => false,
|
||||
refreshSessionReadModel: async () => true,
|
||||
refreshSessionReadModel,
|
||||
sessionIdRef: {
|
||||
current: "session-previous",
|
||||
} as MutableRefObject<string | null>,
|
||||
getWorkspaceIdForSubmit: () => "workspace-1",
|
||||
getThreadIdForSubmit: () => "thread-materialized",
|
||||
getThreadIdForSubmit,
|
||||
getSyncedSessionExecutionStrategy: () => "react",
|
||||
getSyncedSessionRecentPreferences: () => null,
|
||||
effectiveAccessMode: "read-only",
|
||||
@@ -482,6 +494,10 @@ describe("agentStreamSubmitExecution", () => {
|
||||
skipSessionRestore: true,
|
||||
skipSessionStartHooks: true,
|
||||
});
|
||||
expect(getThreadIdForSubmit).toHaveBeenCalledWith("session-materialized");
|
||||
expect(refreshSessionReadModel).toHaveBeenCalledWith(
|
||||
"session-materialized",
|
||||
);
|
||||
expect(runtime.listenToTurnEvents).toHaveBeenCalledWith(
|
||||
"event-materialized",
|
||||
expect.any(Function),
|
||||
|
||||
@@ -63,7 +63,7 @@ interface ExecuteAgentStreamSubmitOptions {
|
||||
refreshSessionReadModel: (targetSessionId?: string) => Promise<boolean>;
|
||||
sessionIdRef: MutableRefObject<string | null>;
|
||||
getWorkspaceIdForSubmit: () => string | undefined;
|
||||
getThreadIdForSubmit: () => string | undefined;
|
||||
getThreadIdForSubmit: (targetSessionId?: string) => string | undefined;
|
||||
getSyncedSessionExecutionStrategy: (
|
||||
sessionId: string,
|
||||
) => AgentExecutionStrategy | null;
|
||||
@@ -292,10 +292,10 @@ export async function executeAgentStreamSubmit(
|
||||
if (!resolvedActiveSessionId) {
|
||||
throw new Error("缺少会话 ID,无法启动流式任务");
|
||||
}
|
||||
let resolvedThreadId = getThreadIdForSubmit()?.trim();
|
||||
let resolvedThreadId = getThreadIdForSubmit(targetSessionId)?.trim();
|
||||
if (!resolvedThreadId) {
|
||||
await refreshSessionReadModel(resolvedActiveSessionId);
|
||||
resolvedThreadId = getThreadIdForSubmit()?.trim();
|
||||
resolvedThreadId = getThreadIdForSubmit(resolvedActiveSessionId)?.trim();
|
||||
}
|
||||
if (!resolvedThreadId) {
|
||||
throw new Error("缺少 canonical threadId,无法启动流式任务");
|
||||
|
||||
@@ -519,6 +519,9 @@ export function useAgentSession(options: UseAgentSessionOptions) {
|
||||
const threadTurnsRef = useRef<AgentThreadTurn[]>(threadTurns);
|
||||
const threadItemsRef = useRef<AgentThreadItem[]>(threadItems);
|
||||
const threadReadRef = useRef<AgentRuntimeThreadReadModel | null>(threadRead);
|
||||
const canonicalThreadIdBySessionIdRef = useRef<Map<string, string>>(
|
||||
new Map(),
|
||||
);
|
||||
const sessionHistoryWindowRef = useRef<AgentSessionHistoryWindow | null>(
|
||||
sessionHistoryWindow,
|
||||
);
|
||||
@@ -733,6 +736,14 @@ export function useAgentSession(options: UseAgentSessionOptions) {
|
||||
threadTurnsRef.current = stableSnapshot.threadTurns;
|
||||
threadItemsRef.current = stableSnapshot.threadItems;
|
||||
threadReadRef.current = resolvedThreadRead;
|
||||
const resolvedSessionId = stableSnapshot.sessionId?.trim();
|
||||
const canonicalThreadId = resolvedThreadRead?.thread_id?.trim();
|
||||
if (resolvedSessionId && canonicalThreadId) {
|
||||
canonicalThreadIdBySessionIdRef.current.set(
|
||||
resolvedSessionId,
|
||||
canonicalThreadId,
|
||||
);
|
||||
}
|
||||
executionRuntimeRef.current = stableSnapshot.executionRuntime;
|
||||
sessionWorkingDirRef.current = stableSnapshot.workingDir;
|
||||
setSessionId(stableSnapshot.sessionId);
|
||||
@@ -757,13 +768,33 @@ export function useAgentSession(options: UseAgentSessionOptions) {
|
||||
mergeAgentSessionReadModelThreadItems(currentItems, snapshot),
|
||||
);
|
||||
threadReadRef.current = snapshot.threadRead;
|
||||
const currentSessionId = sessionIdRef.current?.trim();
|
||||
const canonicalThreadId = snapshot.threadRead.thread_id?.trim();
|
||||
if (currentSessionId && canonicalThreadId) {
|
||||
canonicalThreadIdBySessionIdRef.current.set(
|
||||
currentSessionId,
|
||||
canonicalThreadId,
|
||||
);
|
||||
}
|
||||
setThreadRead(snapshot.threadRead);
|
||||
},
|
||||
[setThreadItemsState],
|
||||
[setThreadItemsState, sessionIdRef],
|
||||
);
|
||||
const getThreadIdForSubmit = useCallback(
|
||||
() => threadReadRef.current?.thread_id,
|
||||
[],
|
||||
(targetSessionId?: string) => {
|
||||
const resolvedTargetSessionId = targetSessionId?.trim();
|
||||
if (resolvedTargetSessionId) {
|
||||
return canonicalThreadIdBySessionIdRef.current.get(
|
||||
resolvedTargetSessionId,
|
||||
);
|
||||
}
|
||||
return (
|
||||
canonicalThreadIdBySessionIdRef.current.get(
|
||||
sessionIdRef.current?.trim() ?? "",
|
||||
) || threadReadRef.current?.thread_id
|
||||
);
|
||||
},
|
||||
[sessionIdRef],
|
||||
);
|
||||
|
||||
const hasActiveStreamingTimelineNow = useCallback(
|
||||
@@ -1505,6 +1536,14 @@ export function useAgentSession(options: UseAgentSessionOptions) {
|
||||
persistSessionRestoreCandidate(resolvedSessionId);
|
||||
|
||||
if (detail) {
|
||||
const canonicalThreadId =
|
||||
detail.thread_read?.thread_id?.trim() || detail.thread_id?.trim();
|
||||
if (canonicalThreadId) {
|
||||
canonicalThreadIdBySessionIdRef.current.set(
|
||||
resolvedSessionId,
|
||||
canonicalThreadId,
|
||||
);
|
||||
}
|
||||
setTopics((prev) =>
|
||||
upsertTopicFromSessionDetail(
|
||||
prev,
|
||||
|
||||
@@ -145,7 +145,7 @@ interface UseAgentStreamOptions {
|
||||
currentStreamingEventNameRef: MutableRefObject<string | null>;
|
||||
warnedKeysRef: MutableRefObject<Set<string>>;
|
||||
getWorkspaceIdForSubmit: () => string | undefined;
|
||||
getThreadIdForSubmit: () => string | undefined;
|
||||
getThreadIdForSubmit: (targetSessionId?: string) => string | undefined;
|
||||
setWorkspacePathMissing: Dispatch<
|
||||
SetStateAction<WorkspacePathMissingState | null>
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user