@tea-agent/loop-agent 0.44.0-next.5 → 0.44.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +299 -445
- package/dist/build-stamp.json +3 -3
- package/dist/executors/pi-sdk-executor.js +7 -7
- package/dist/infrastructure/console/operation-store.js +46 -14
- package/dist/shared/git-workspace-status.js +2 -0
- package/dist/shared/wizard-local-telemetry.js +2 -1
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +27 -10
- package/dist/worker/console/chat/db-connections.js +72 -8
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
- package/dist/worker/console/chat/pi-runtime.js +251 -18
- package/dist/worker/console/chat/repo-browser.js +49 -0
- package/dist/worker/console/chat/routes.js +215 -66
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/turn-process.js +69 -23
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BHuhGM1e.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-CKh5E_sr.js → arc-CQbCsSqb.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DizhiBEv.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-3Neea-Z-.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCP_Qrgi.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C_wydftK.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-jRPXZbzx.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BLQf2OYO.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-0qRq64rI.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-ClMg8xmV.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Coop2FlL.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CDdO-jPJ.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D8Gkppjl.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DA9HIySf.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DZNDRX1k.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CZ1zGyjq.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B8u1_FtM.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CPBB6lfo.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CG3Eov3E.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ycI-dOjh.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bx_GyQ7Y.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-m0G6mP4d.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DTDJkqcJ.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BCfFrG-p.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BIkTNPpp.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DTcMWrd2.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqzDaNQ7.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSl_vGFe.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCA5HXM6.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
- package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DvqT18ge.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BpLQPI9C.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CzIz_-Lt.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CMQ8Jpp-.js → linear-BGKgMc2T.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BdAMrTf7.js → mermaid.core-DF6mMPJ9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-hSwF6sOq.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DZdv509B.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CmD4-b0K.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKxNxlS.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-D48iIoiS.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3N0K-2p.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CACWR-Xs.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-g_QgugvZ.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqLoRPGK.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-ES59VgMG.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-fjyLVNWo.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cd5-biKv.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CZTu6mPf.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DR01jjbe.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-iZcuGVGa.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -41
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
- package/docs/templates/backend-test-dag.json +1 -0
- package/package.json +4 -4
- package/dist/worker/console/static/assets/channel-DxgzA1GF.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +0 -1
- package/dist/worker/console/static/assets/index-BAEDjTbD.css +0 -1
- package/dist/worker/console/static/assets/index-wKEJUaP5.js +0 -482
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +0 -8
|
@@ -8,6 +8,47 @@ import { isLostChatSessionError } from "./runtime-snapshot-store.js";
|
|
|
8
8
|
import { upsertCompactionMessage, } from "./compaction-message.js";
|
|
9
9
|
import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
|
|
10
10
|
import { SessionTitleWatcher } from "./session-title-watcher.js";
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function isSessionListItem(value) {
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
return false;
|
|
17
|
+
const optionalString = (field) => field === undefined || typeof field === "string";
|
|
18
|
+
return (typeof value.sessionId === "string" &&
|
|
19
|
+
value.sessionId.trim().length > 0 &&
|
|
20
|
+
(value.state === "active" || value.state === "archived") &&
|
|
21
|
+
typeof value.updatedAt === "string" &&
|
|
22
|
+
optionalString(value.workspaceId) &&
|
|
23
|
+
optionalString(value.title) &&
|
|
24
|
+
optionalString(value.displayTitle) &&
|
|
25
|
+
optionalString(value.activeTurnId) &&
|
|
26
|
+
(value.isBlank === undefined || typeof value.isBlank === "boolean") &&
|
|
27
|
+
(value.sessionMode === undefined ||
|
|
28
|
+
value.sessionMode === "workflow" ||
|
|
29
|
+
value.sessionMode === "pi") &&
|
|
30
|
+
(value.turnPhase === undefined ||
|
|
31
|
+
value.turnPhase === "idle" ||
|
|
32
|
+
value.turnPhase === "running" ||
|
|
33
|
+
value.turnPhase === "stopping"));
|
|
34
|
+
}
|
|
35
|
+
/** A 2xx response is authoritative only after its versioned payload validates. */
|
|
36
|
+
export function parseSessionListBaseline(value) {
|
|
37
|
+
if (!isRecord(value) || value.schemaVersion !== 2) {
|
|
38
|
+
throw new Error("会话列表响应版本无效");
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(value.sessions) || !value.sessions.every(isSessionListItem)) {
|
|
41
|
+
throw new Error("会话列表响应缺少有效的 sessions");
|
|
42
|
+
}
|
|
43
|
+
const workspaceErrors = value.workspaceErrors ?? [];
|
|
44
|
+
if (!Array.isArray(workspaceErrors) || !workspaceErrors.every(isRecord)) {
|
|
45
|
+
throw new Error("会话列表响应包含无效的 workspaceErrors");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
sessions: value.sessions,
|
|
49
|
+
workspaceErrors: workspaceErrors,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
11
52
|
/** Pure list mutations keep optimistic UI deterministic and easy to verify. */
|
|
12
53
|
export function insertOptimisticSession(list, row) {
|
|
13
54
|
return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
|
|
@@ -199,11 +240,19 @@ export function useChatSessions(params) {
|
|
|
199
240
|
return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
|
|
200
241
|
}, [refs]);
|
|
201
242
|
const [sessionList, setSessionList] = useState([]);
|
|
243
|
+
const [sessionListStatus, setSessionListStatus] = useState({
|
|
244
|
+
phase: "pending",
|
|
245
|
+
state: "idle",
|
|
246
|
+
error: null,
|
|
247
|
+
});
|
|
202
248
|
const [selectedSession, setSelectedSession] = useState(null);
|
|
203
249
|
const selectedSessionRef = useRef(null);
|
|
204
250
|
selectedSessionRef.current = selectedSession;
|
|
205
251
|
const sessionListRef = useRef([]);
|
|
206
252
|
const sessionListRequestVersionRef = useRef(0);
|
|
253
|
+
const sessionListRefreshRef = useRef(null);
|
|
254
|
+
const sessionListRefreshQueuedRef = useRef(false);
|
|
255
|
+
const sessionListGlobalErrorRef = useRef(null);
|
|
207
256
|
const pendingDeletedIdsRef = useRef(new Set());
|
|
208
257
|
const pendingCreatedRowsRef = useRef(new Map());
|
|
209
258
|
const sessionMutationVersionRef = useRef(0);
|
|
@@ -257,46 +306,101 @@ export function useChatSessions(params) {
|
|
|
257
306
|
setProcessOpen,
|
|
258
307
|
setAutoScroll,
|
|
259
308
|
]);
|
|
260
|
-
const refreshSessionList = useCallback(
|
|
309
|
+
const refreshSessionList = useCallback(() => {
|
|
261
310
|
if (blocked)
|
|
262
|
-
return; // AC-R5-002: fail-closed, zero
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
270
|
-
credentials: "include",
|
|
271
|
-
});
|
|
272
|
-
const body = (await res.json().catch(() => ({})));
|
|
273
|
-
if (version !== sessionListRequestVersionRef.current)
|
|
274
|
-
return;
|
|
275
|
-
if (!res.ok) {
|
|
276
|
-
setError(body.error?.message ?? `会话列表加载失败(HTTP ${res.status})`);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const sessions = body.sessions ?? [];
|
|
280
|
-
const belongsToVisibleWorkspace = (item) => !item.workspaceId || visibleWorkspaceIds.has(item.workspaceId);
|
|
281
|
-
const durable = sessions.filter((item) => belongsToVisibleWorkspace(item) &&
|
|
282
|
-
!pendingDeletedIdsRef.current.has(item.sessionId));
|
|
283
|
-
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
|
|
284
|
-
!durable.some((session) => session.sessionId === item.sessionId));
|
|
285
|
-
// Preserve summaries for visible workspaces. ChatWorkspaceSidebar applies
|
|
286
|
-
// the recent-item limit independently inside each workspace group.
|
|
287
|
-
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
288
|
-
sessionListRef.current = next;
|
|
289
|
-
setSessionList(next);
|
|
290
|
-
if (body.workspaceErrors?.length) {
|
|
291
|
-
setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
|
|
292
|
-
}
|
|
311
|
+
return Promise.resolve(); // AC-R5-002: fail-closed, zero requests
|
|
312
|
+
if (sessionListRefreshRef.current) {
|
|
313
|
+
// The newest refresh may carry a different visible-workspace projection.
|
|
314
|
+
// Do not briefly commit the older scope before the queued baseline runs.
|
|
315
|
+
sessionListRequestVersionRef.current += 1;
|
|
316
|
+
sessionListRefreshQueuedRef.current = true;
|
|
317
|
+
return sessionListRefreshRef.current;
|
|
293
318
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
const request = (async () => {
|
|
320
|
+
do {
|
|
321
|
+
sessionListRefreshQueuedRef.current = false;
|
|
322
|
+
const version = ++sessionListRequestVersionRef.current;
|
|
323
|
+
setSessionListStatus((current) => ({
|
|
324
|
+
...current,
|
|
325
|
+
state: "loading",
|
|
326
|
+
error: null,
|
|
327
|
+
}));
|
|
328
|
+
try {
|
|
329
|
+
// The sidebar is one unscoped multi-workspace index. Session detail
|
|
330
|
+
// and mutations remain owner-scoped.
|
|
331
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
|
|
332
|
+
credentials: "include",
|
|
333
|
+
});
|
|
334
|
+
let rawBody;
|
|
335
|
+
try {
|
|
336
|
+
rawBody = await res.json();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
throw new Error("会话列表响应不是有效 JSON");
|
|
340
|
+
}
|
|
341
|
+
if (version !== sessionListRequestVersionRef.current) {
|
|
342
|
+
sessionListRefreshQueuedRef.current = true;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
const message = isRecord(rawBody) &&
|
|
347
|
+
isRecord(rawBody.error) &&
|
|
348
|
+
typeof rawBody.error.message === "string"
|
|
349
|
+
? rawBody.error.message
|
|
350
|
+
: `会话列表加载失败(HTTP ${res.status})`;
|
|
351
|
+
throw new Error(message);
|
|
352
|
+
}
|
|
353
|
+
const baseline = parseSessionListBaseline(rawBody);
|
|
354
|
+
if (baseline.workspaceErrors.length > 0) {
|
|
355
|
+
throw new Error(`部分工作区的会话加载失败(${baseline.workspaceErrors.length} 个)`);
|
|
356
|
+
}
|
|
357
|
+
const belongsToVisibleWorkspace = (item) => params.workspacesLoading ||
|
|
358
|
+
!item.workspaceId ||
|
|
359
|
+
visibleWorkspaceIds.has(item.workspaceId);
|
|
360
|
+
const durable = baseline.sessions.filter((item) => belongsToVisibleWorkspace(item) &&
|
|
361
|
+
!pendingDeletedIdsRef.current.has(item.sessionId));
|
|
362
|
+
const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => belongsToVisibleWorkspace(item) &&
|
|
363
|
+
!durable.some((session) => session.sessionId === item.sessionId));
|
|
364
|
+
const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
|
|
365
|
+
sessionListRef.current = next;
|
|
366
|
+
setSessionList(next);
|
|
367
|
+
setSessionListStatus({ phase: "ready", state: "idle", error: null });
|
|
368
|
+
const previousListError = sessionListGlobalErrorRef.current;
|
|
369
|
+
if (previousListError) {
|
|
370
|
+
setError((current) => current === previousListError ? null : current);
|
|
371
|
+
sessionListGlobalErrorRef.current = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (version !== sessionListRequestVersionRef.current) {
|
|
376
|
+
sessionListRefreshQueuedRef.current = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const message = `会话列表加载失败:${error instanceof Error ? error.message : String(error)}`;
|
|
380
|
+
setSessionListStatus((current) => ({
|
|
381
|
+
...current,
|
|
382
|
+
state: "error",
|
|
383
|
+
error: message,
|
|
384
|
+
}));
|
|
385
|
+
sessionListGlobalErrorRef.current = message;
|
|
386
|
+
setError(message);
|
|
387
|
+
}
|
|
388
|
+
} while (sessionListRefreshQueuedRef.current && !blocked);
|
|
389
|
+
})();
|
|
390
|
+
sessionListRefreshRef.current = request;
|
|
391
|
+
void request.finally(() => {
|
|
392
|
+
if (sessionListRefreshRef.current === request) {
|
|
393
|
+
sessionListRefreshRef.current = null;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return request;
|
|
397
|
+
}, [origin, blocked, setError, visibleWorkspaceIds, params.workspacesLoading]);
|
|
398
|
+
const invalidateSessionListBaseline = useCallback(() => {
|
|
399
|
+
sessionListRequestVersionRef.current += 1;
|
|
400
|
+
if (sessionListRefreshRef.current) {
|
|
401
|
+
sessionListRefreshQueuedRef.current = true;
|
|
298
402
|
}
|
|
299
|
-
}, [
|
|
403
|
+
}, []);
|
|
300
404
|
const deleteSession = useCallback(async (sessionId) => {
|
|
301
405
|
if (pendingDeletedIdsRef.current.has(sessionId))
|
|
302
406
|
return false;
|
|
@@ -306,7 +410,7 @@ export function useChatSessions(params) {
|
|
|
306
410
|
if (!removed || removedIndex < 0)
|
|
307
411
|
return false;
|
|
308
412
|
// No pre-mutation snapshot may overwrite this deletion or its rollback.
|
|
309
|
-
|
|
413
|
+
invalidateSessionListBaseline();
|
|
310
414
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
311
415
|
deleteRequestVersionsRef.current.set(sessionId, requestId);
|
|
312
416
|
const activeSession = refs.sessionRef.current?.sessionId === sessionId
|
|
@@ -339,7 +443,7 @@ export function useChatSessions(params) {
|
|
|
339
443
|
const restore = () => {
|
|
340
444
|
if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
|
|
341
445
|
return;
|
|
342
|
-
|
|
446
|
+
invalidateSessionListBaseline();
|
|
343
447
|
pendingDeletedIdsRef.current.delete(sessionId);
|
|
344
448
|
deleteRequestVersionsRef.current.delete(sessionId);
|
|
345
449
|
const restored = restoreSessionAt(sessionListRef.current.filter((item) => item.sessionId !== sessionId), removed, removedIndex);
|
|
@@ -410,6 +514,7 @@ export function useChatSessions(params) {
|
|
|
410
514
|
setError,
|
|
411
515
|
sessionOwnerQuery,
|
|
412
516
|
workspaceId,
|
|
517
|
+
invalidateSessionListBaseline,
|
|
413
518
|
]);
|
|
414
519
|
const forgetLostSession = useCallback((sessionId, ownerWorkspaceId) => {
|
|
415
520
|
const active = refs.sessionRef.current;
|
|
@@ -422,6 +527,7 @@ export function useChatSessions(params) {
|
|
|
422
527
|
forgetActiveChatSession(window.localStorage, scope, sessionId);
|
|
423
528
|
releaseSession(identity);
|
|
424
529
|
pendingDeletedIdsRef.current.add(sessionId);
|
|
530
|
+
invalidateSessionListBaseline();
|
|
425
531
|
const { next } = removeSessionById(sessionListRef.current, sessionId);
|
|
426
532
|
sessionListRef.current = next;
|
|
427
533
|
setSessionList(next);
|
|
@@ -434,7 +540,15 @@ export function useChatSessions(params) {
|
|
|
434
540
|
setSession(null);
|
|
435
541
|
}
|
|
436
542
|
void refreshSessionList();
|
|
437
|
-
}, [
|
|
543
|
+
}, [
|
|
544
|
+
refs,
|
|
545
|
+
releaseSession,
|
|
546
|
+
resetActiveProjection,
|
|
547
|
+
setSession,
|
|
548
|
+
workspaceId,
|
|
549
|
+
refreshSessionList,
|
|
550
|
+
invalidateSessionListBaseline,
|
|
551
|
+
]);
|
|
438
552
|
const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
|
|
439
553
|
const selectionVersion = ++sessionSelectionVersionRef.current;
|
|
440
554
|
const listedOwner = sessionListRef.current.find((item) => item.sessionId === sessionId)?.workspaceId;
|
|
@@ -501,10 +615,12 @@ export function useChatSessions(params) {
|
|
|
501
615
|
applyActivatedSession({
|
|
502
616
|
messages: hydrated.messages,
|
|
503
617
|
toolCalls: hydrated.toolCalls,
|
|
618
|
+
turnStates: body.session.turnStates,
|
|
504
619
|
conversationEvents: body.conversationEvents,
|
|
505
620
|
sessionStats: body.sessionStats,
|
|
506
621
|
usage: body.usage,
|
|
507
622
|
turnTelemetry: body.turnTelemetry,
|
|
623
|
+
userTurnOutline: body.userTurnOutline,
|
|
508
624
|
sddSessionIdentity: body.sddDataAlignment ?? null,
|
|
509
625
|
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
510
626
|
turnPhase: body.session.turnPhase ??
|
|
@@ -541,6 +657,32 @@ export function useChatSessions(params) {
|
|
|
541
657
|
forgetLostSession,
|
|
542
658
|
]);
|
|
543
659
|
activateSessionRef.current = activateSession;
|
|
660
|
+
const forkSessionAt = useCallback(async (throughMessageId) => {
|
|
661
|
+
const current = refs.sessionRef.current;
|
|
662
|
+
if (!current || refs.activeTurnIdRef.current)
|
|
663
|
+
return false;
|
|
664
|
+
try {
|
|
665
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/fork${sessionOwnerQuery(current.workspaceId)}`, {
|
|
666
|
+
method: "POST",
|
|
667
|
+
credentials: "include",
|
|
668
|
+
headers: {
|
|
669
|
+
"content-type": "application/json",
|
|
670
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
671
|
+
},
|
|
672
|
+
body: JSON.stringify({ throughMessageId }),
|
|
673
|
+
});
|
|
674
|
+
const body = (await res.json().catch(() => ({})));
|
|
675
|
+
if (!res.ok || !body.sessionId) {
|
|
676
|
+
throw new Error(body.error?.message ?? "创建分叉会话失败");
|
|
677
|
+
}
|
|
678
|
+
await refreshSessionList();
|
|
679
|
+
return activateSession(body.sessionId, current.workspaceId);
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}, [activateSession, origin, refreshSessionList, refs, sessionOwnerQuery, setError]);
|
|
544
686
|
useEffect(() => {
|
|
545
687
|
void refreshSessionList();
|
|
546
688
|
}, [refreshSessionList]);
|
|
@@ -637,10 +779,12 @@ export function useChatSessions(params) {
|
|
|
637
779
|
applyActivatedSession({
|
|
638
780
|
messages: hydrated.messages,
|
|
639
781
|
toolCalls: hydrated.toolCalls,
|
|
782
|
+
turnStates: body.session.turnStates,
|
|
640
783
|
conversationEvents: body.conversationEvents,
|
|
641
784
|
sessionStats: body.sessionStats,
|
|
642
785
|
usage: body.usage,
|
|
643
786
|
turnTelemetry: body.turnTelemetry,
|
|
787
|
+
userTurnOutline: body.userTurnOutline,
|
|
644
788
|
sddSessionIdentity: body.sddDataAlignment ?? null,
|
|
645
789
|
streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
|
|
646
790
|
turnPhase: body.session.turnPhase ??
|
|
@@ -696,6 +840,7 @@ export function useChatSessions(params) {
|
|
|
696
840
|
optimisticState: "creating",
|
|
697
841
|
};
|
|
698
842
|
pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
|
|
843
|
+
invalidateSessionListBaseline();
|
|
699
844
|
const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
|
|
700
845
|
sessionListRef.current = optimisticList;
|
|
701
846
|
setSessionList(optimisticList);
|
|
@@ -711,6 +856,14 @@ export function useChatSessions(params) {
|
|
|
711
856
|
creatingRef: creatingSessionRef,
|
|
712
857
|
});
|
|
713
858
|
pendingCreatedRowsRef.current.delete(optimisticSessionId);
|
|
859
|
+
const durableRow = sessionListRef.current.find((item) => item.sessionId === created.sessionId);
|
|
860
|
+
if (durableRow) {
|
|
861
|
+
pendingCreatedRowsRef.current.set(created.sessionId, durableRow);
|
|
862
|
+
invalidateSessionListBaseline();
|
|
863
|
+
void refreshSessionList().finally(() => {
|
|
864
|
+
pendingCreatedRowsRef.current.delete(created.sessionId);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
714
867
|
return created;
|
|
715
868
|
}
|
|
716
869
|
catch (error) {
|
|
@@ -720,7 +873,13 @@ export function useChatSessions(params) {
|
|
|
720
873
|
setSessionList(cleaned);
|
|
721
874
|
throw error;
|
|
722
875
|
}
|
|
723
|
-
}, [
|
|
876
|
+
}, [
|
|
877
|
+
origin,
|
|
878
|
+
refreshSessionList,
|
|
879
|
+
workspaceId,
|
|
880
|
+
setError,
|
|
881
|
+
invalidateSessionListBaseline,
|
|
882
|
+
]);
|
|
724
883
|
const createSession = useCallback(async (options) => {
|
|
725
884
|
// Suppress concurrent create from any entry (sidebar / Landing / chip).
|
|
726
885
|
if (creatingSessionRef.current)
|
|
@@ -902,11 +1061,13 @@ export function useChatSessions(params) {
|
|
|
902
1061
|
};
|
|
903
1062
|
return {
|
|
904
1063
|
sessionList,
|
|
1064
|
+
sessionListStatus,
|
|
905
1065
|
selectedSession,
|
|
906
1066
|
sessionSwitching: Boolean(selectedSession &&
|
|
907
1067
|
refs.sessionRef.current?.sessionId !== selectedSession.sessionId),
|
|
908
1068
|
creatingSession,
|
|
909
1069
|
activateSession,
|
|
1070
|
+
forkSessionAt,
|
|
910
1071
|
refreshSessionList,
|
|
911
1072
|
deleteSession,
|
|
912
1073
|
createSession,
|
|
@@ -3,8 +3,50 @@ import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
|
3
3
|
import { inspectChatSseContinuity, readChatStreamReadyCursor, selectChatSseRepairBlocks, selectLaterChatEventId, } from "./chat-stream-continuity.js";
|
|
4
4
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
5
5
|
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
6
|
-
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
|
+
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, createBoundedOwnerSignal, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
7
7
|
import { appendPendingSubmission, removePendingSubmission } from "./pending-user-message.js";
|
|
8
|
+
import { createTurnStateReconcileQueue, turnStateReconcileTrigger, } from "./turn-state-projection.js";
|
|
9
|
+
const STATE_REQUEST_TIMEOUT_MS = 2_000;
|
|
10
|
+
async function fetchReconciledSessionState(origin, sessionId, options) {
|
|
11
|
+
const bounded = createBoundedOwnerSignal({
|
|
12
|
+
ownerSignal: options?.ownerSignal,
|
|
13
|
+
timeoutMs: STATE_REQUEST_TIMEOUT_MS,
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(operatorChatStateUrl(origin, sessionId, options?.turnIds), {
|
|
17
|
+
credentials: "include",
|
|
18
|
+
signal: bounded.signal,
|
|
19
|
+
});
|
|
20
|
+
const body = (await res.json().catch(() => ({})));
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
return null;
|
|
23
|
+
return body.state ?? null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
bounded.dispose();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readChatSseTurnId(block) {
|
|
33
|
+
const data = block
|
|
34
|
+
.split("\n")
|
|
35
|
+
.filter((line) => line.startsWith("data: "))
|
|
36
|
+
.map((line) => line.slice(6))
|
|
37
|
+
.join("");
|
|
38
|
+
if (!data)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(data);
|
|
42
|
+
return typeof parsed.turnId === "string" && parsed.turnId
|
|
43
|
+
? parsed.turnId
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
8
50
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
9
51
|
function ownershipBagFromRefs(refs) {
|
|
10
52
|
return {
|
|
@@ -106,6 +148,17 @@ export function useChatStream(params) {
|
|
|
106
148
|
* smooth (M4.1/M4.3). */
|
|
107
149
|
const openEventsStream = useCallback(async (sessionId, generation, signal) => {
|
|
108
150
|
const gapReplayBuffer = [];
|
|
151
|
+
const turnStateReconciles = createTurnStateReconcileQueue({
|
|
152
|
+
read: (turnIds) => fetchReconciledSessionState(origin, sessionId, {
|
|
153
|
+
turnIds,
|
|
154
|
+
ownerSignal: signal,
|
|
155
|
+
}),
|
|
156
|
+
apply: (summaries) => thread.reconcileTurnStates([...summaries]),
|
|
157
|
+
wait: (delayMs) => sleep(delayMs, signal),
|
|
158
|
+
isCancelled: () => signal.aborted ||
|
|
159
|
+
refs.sessionRef.current?.sessionId !== sessionId ||
|
|
160
|
+
refs.sessionGenerationRef.current !== generation,
|
|
161
|
+
});
|
|
109
162
|
while (!signal.aborted) {
|
|
110
163
|
const state = refs.eventsStreamStateRef.current;
|
|
111
164
|
if (!state ||
|
|
@@ -145,6 +198,16 @@ export function useChatStream(params) {
|
|
|
145
198
|
const block = blocks[blockIndex];
|
|
146
199
|
if (block === undefined)
|
|
147
200
|
continue;
|
|
201
|
+
const eventName = sseEventName(block);
|
|
202
|
+
const reconcileTrigger = turnStateReconcileTrigger(replay, eventName);
|
|
203
|
+
if (reconcileTrigger) {
|
|
204
|
+
const settledTurnId = readChatSseTurnId(block);
|
|
205
|
+
if (settledTurnId) {
|
|
206
|
+
turnStateReconciles.enqueue(settledTurnId, {
|
|
207
|
+
defer: reconcileTrigger === "after-replay",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
148
211
|
if (!replay) {
|
|
149
212
|
const continuity = inspectChatSseContinuity(block, liveContinuityCursor);
|
|
150
213
|
if (continuity.action === "reconnect") {
|
|
@@ -156,7 +219,7 @@ export function useChatStream(params) {
|
|
|
156
219
|
break readLoop;
|
|
157
220
|
}
|
|
158
221
|
}
|
|
159
|
-
if (
|
|
222
|
+
if (eventName === "stream_ready") {
|
|
160
223
|
const current = refs.eventsStreamStateRef.current;
|
|
161
224
|
if (current?.sessionId === sessionId &&
|
|
162
225
|
current.generation === generation)
|
|
@@ -179,7 +242,7 @@ export function useChatStream(params) {
|
|
|
179
242
|
if (!replay)
|
|
180
243
|
liveContinuityCursor = selectLaterChatEventId(liveContinuityCursor, eventId);
|
|
181
244
|
}
|
|
182
|
-
if (
|
|
245
|
+
if (eventName === "stream_ready" &&
|
|
183
246
|
gapReplayBuffer.length > 0) {
|
|
184
247
|
const repairedBlocks = selectChatSseRepairBlocks(gapReplayBuffer.splice(0), liveContinuityCursor);
|
|
185
248
|
for (const repairedBlock of repairedBlocks) {
|
|
@@ -193,6 +256,9 @@ export function useChatStream(params) {
|
|
|
193
256
|
refs.lastEventIdRef.current = selectLaterChatEventId(refs.lastEventIdRef.current, repairedEventId);
|
|
194
257
|
}
|
|
195
258
|
}
|
|
259
|
+
if (eventName === "stream_ready") {
|
|
260
|
+
void turnStateReconciles.start();
|
|
261
|
+
}
|
|
196
262
|
}
|
|
197
263
|
}
|
|
198
264
|
if (replayRepairNeeded) {
|
|
@@ -213,7 +279,7 @@ export function useChatStream(params) {
|
|
|
213
279
|
await sleep(2_000, signal);
|
|
214
280
|
}
|
|
215
281
|
}
|
|
216
|
-
}, [origin, refs, applySseBlock]);
|
|
282
|
+
}, [origin, refs, applySseBlock, thread.reconcileTurnStates]);
|
|
217
283
|
const startEventsStream = useCallback((sessionId, generation) => {
|
|
218
284
|
const existing = refs.eventsStreamStateRef.current;
|
|
219
285
|
if (existing?.sessionId === sessionId &&
|
|
@@ -249,20 +315,7 @@ export function useChatStream(params) {
|
|
|
249
315
|
}
|
|
250
316
|
throw new Error("事件连接未就绪,请检查 Console 连接后重试");
|
|
251
317
|
}, [refs, startEventsStream]);
|
|
252
|
-
const fetchSessionState = useCallback(
|
|
253
|
-
try {
|
|
254
|
-
const res = await fetch(operatorChatStateUrl(origin, sessionId), {
|
|
255
|
-
credentials: "include",
|
|
256
|
-
});
|
|
257
|
-
const body = (await res.json().catch(() => ({})));
|
|
258
|
-
if (!res.ok)
|
|
259
|
-
return null;
|
|
260
|
-
return body.state ?? null;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
}, [origin]);
|
|
318
|
+
const fetchSessionState = useCallback((sessionId) => fetchReconciledSessionState(origin, sessionId), [origin]);
|
|
266
319
|
const postCreateTurn = useCallback(async (input) => {
|
|
267
320
|
try {
|
|
268
321
|
// Resolve against the configured Console origin and fail closed if a
|
|
@@ -1042,20 +1095,20 @@ export function useChatStream(params) {
|
|
|
1042
1095
|
const sessionId = session.sessionId;
|
|
1043
1096
|
const sessionIdentity = { ...session };
|
|
1044
1097
|
const generation = refs.sessionGenerationRef.current;
|
|
1098
|
+
const stateReconcileAbort = new AbortController();
|
|
1045
1099
|
let cancelled = false;
|
|
1046
1100
|
let syncInFlight = null;
|
|
1047
1101
|
const runSync = async () => {
|
|
1048
1102
|
try {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1103
|
+
const state = await fetchReconciledSessionState(origin, sessionId, {
|
|
1104
|
+
ownerSignal: stateReconcileAbort.signal,
|
|
1051
1105
|
});
|
|
1052
|
-
const body = (await res.json().catch(() => ({})));
|
|
1053
1106
|
if (cancelled ||
|
|
1054
|
-
!
|
|
1107
|
+
!state ||
|
|
1055
1108
|
refs.sessionRef.current?.sessionId !== sessionId ||
|
|
1056
1109
|
refs.sessionGenerationRef.current !== generation)
|
|
1057
1110
|
return;
|
|
1058
|
-
|
|
1111
|
+
thread.reconcileTurnStates(state?.turnStates);
|
|
1059
1112
|
const pending = refs.pendingSubmissionRef.current;
|
|
1060
1113
|
// Pending ownership reconcile. Controller owns bind/release; same
|
|
1061
1114
|
// clientRequestId only. Re-enter after acceptedTurnId so terminal
|
|
@@ -1200,6 +1253,7 @@ export function useChatStream(params) {
|
|
|
1200
1253
|
void sync().finally(scheduleStopRetry);
|
|
1201
1254
|
return () => {
|
|
1202
1255
|
cancelled = true;
|
|
1256
|
+
stateReconcileAbort.abort();
|
|
1203
1257
|
window.clearInterval(interval);
|
|
1204
1258
|
if (stopRetryTimer !== undefined)
|
|
1205
1259
|
window.clearTimeout(stopRetryTimer);
|
|
@@ -1216,6 +1270,7 @@ export function useChatStream(params) {
|
|
|
1216
1270
|
markIdleIfCurrent,
|
|
1217
1271
|
releaseSubmitOwnership,
|
|
1218
1272
|
postAbortForTurn,
|
|
1273
|
+
thread.reconcileTurnStates,
|
|
1219
1274
|
]);
|
|
1220
1275
|
// Exactly one owner starts/aborts GET /events. Switching sessions or
|
|
1221
1276
|
// recovering a cursor aborts the prior stream before the next one is made.
|