@tea-agent/loop-agent 0.40.0-next.7 → 0.40.0-next.8

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.
Files changed (79) hide show
  1. package/CHANGELOG.md +105 -140
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/worker/console/chat/chat-event-store.js +13 -1
  4. package/dist/worker/console/chat/pi-runtime.js +70 -5
  5. package/dist/worker/console/chat/routes.js +99 -29
  6. package/dist/worker/console/chat/workspace-landing.js +65 -0
  7. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DGt5TRsk.js → abnfDiagram-N423BO3Z-BYBmDfPW.js} +1 -1
  8. package/dist/worker/console/static/assets/{arc-DKBSJsXm.js → arc-BKMmxpGF.js} +1 -1
  9. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BnhooRBs.js → architectureDiagram-T3A2C74G-DJpwxr1d.js} +1 -1
  10. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bm9qkrJP.js → blockDiagram-VBNYF7ZC-BaPj_ZlY.js} +1 -1
  11. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B2cFh4bO.js → c4Diagram-5PPSVZJV-B8iGmHlm.js} +1 -1
  12. package/dist/worker/console/static/assets/channel-CrzAa3FA.js +1 -0
  13. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DhvWRI03.js → chunk-2GRJ4B5K-DaxXQayc.js} +1 -1
  14. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-chzTP_2w.js → chunk-2Q5K7J3B-DqQKGA7q.js} +1 -1
  15. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-SdWIR12m.js → chunk-5RXB4S5H-CgMKpJBB.js} +1 -1
  16. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C2-8Eo_A.js → chunk-5VM5RSS4-BpToUlaW.js} +1 -1
  17. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DvbE1yQW.js → chunk-6Q2QTUOP-BsEWOHxv.js} +1 -1
  18. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CEenyPTz.js → chunk-GF5L2VYU-CKGSCe71.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CcaZrq0k.js → chunk-JWPE2WC7-Bm1U5299.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-FGngOTto.js → chunk-KBJHAD2P-BI0ov870.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-jT_yX3sf.js → chunk-RYQCIY6F-BmnQo0iJ.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CjE9LQOM.js → chunk-XXDRQBXY-BJBqa6Xc.js} +1 -1
  23. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-B7QwCCb4.js +1 -0
  24. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-B7QwCCb4.js +1 -0
  25. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-GQWns1tb.js → cose-bilkent-JH36ORCC-BuN9-7AR.js} +1 -1
  26. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CGRpxWNJ.js → cynefin-VYW2F7L2-Bw22jc2k.js} +1 -1
  27. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cd3uFGE4.js → cynefinDiagram-MW4NZA55-BHnSH8Dr.js} +1 -1
  28. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DmDHuUW0.js → dagre-VZM6K2ZE-BoXXIegj.js} +1 -1
  29. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-kAr--hF3.js → diagram-7IWD3JNH-CU8qyPhT.js} +1 -1
  30. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-woahL2Oh.js → diagram-B4RE2ZJO-lrjhF0S8.js} +1 -1
  31. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-7syNVaNf.js → diagram-LBJQPF4R-DL0QDEpo.js} +1 -1
  32. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BELPXiaY.js → diagram-Q27KOJAE-DxVMJMBH.js} +1 -1
  33. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CrPa4aXi.js → diagram-UB23O5K3-CFg9FzvG.js} +1 -1
  34. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B48u2VVe.js → ebnfDiagram-BXEA7PRR-CQgP9fTG.js} +1 -1
  35. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DhBTfwTF.js → erDiagram-JOGREHBK-A5vQXx9U.js} +1 -1
  36. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CAeZ5nDU.js → flowDiagram-UKHOOZJN-ytXnLyXD.js} +1 -1
  37. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CkIFeFLN.js → ganttDiagram-PKOTCBZU-CYuksfnS.js} +1 -1
  38. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-t0HGYDel.js → gitGraphDiagram-DS77QQ5N-f_jW3zgU.js} +1 -1
  39. package/dist/worker/console/static/assets/index-BB6KxwT_.css +1 -0
  40. package/dist/worker/console/static/assets/index-D_wxbadT.js +407 -0
  41. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-WM0HZhe4.js → infoDiagram-6WML65LV-C3HLE-xV.js} +1 -1
  42. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CV5byHe0.js → ishikawaDiagram-WSZJBQD7-DwpgejS6.js} +1 -1
  43. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-lIyWkgnw.js → journeyDiagram-NVQOT4AX-ll0YWX1y.js} +1 -1
  44. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BIH4YNRY.js → kanban-definition-27J2QSJJ-Hq3rchvU.js} +1 -1
  45. package/dist/worker/console/static/assets/{linear-lQ0ZGrWc.js → linear-BaGjJx8z.js} +1 -1
  46. package/dist/worker/console/static/assets/{mermaid.core-BYdXtke8.js → mermaid.core-CKGQHWX_.js} +5 -5
  47. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-k93vkIa4.js → mindmap-definition-FAOFIHXS-DM1FA6Dr.js} +1 -1
  48. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bm6AqvSy.js → pegDiagram-VL7TDLO6-Xa9fHkE2.js} +1 -1
  49. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnJW6Xpf.js → pieDiagram-7S7Q4E2Y-pCd4PDqJ.js} +1 -1
  50. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CMzm4ObL.js → quadrantDiagram-CIZ2JOQS-KqjpEBLk.js} +1 -1
  51. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-fwFF5dFQ.js → railroadDiagram-AXF67PYL-DgvR7LZD.js} +1 -1
  52. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-tzl9VDUc.js → requirementDiagram-LRYGKXZP-Bys1UCWA.js} +1 -1
  53. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B26V09DD.js → sankeyDiagram-W5VNT64P-Ms9npnZt.js} +1 -1
  54. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-D2npyCeP.js → sequenceDiagram-SI44F4Z6-DEIWrtQZ.js} +1 -1
  55. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CTJtkRU2.js → sizeCapture-X5ZJPWSS-DELJ0AhZ.js} +1 -1
  56. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B8lk4Lzu.js → stateDiagram-OKZ733FA-BpHt9wKh.js} +1 -1
  57. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BEeSswlv.js +1 -0
  58. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C3yP53hH.js → swimlanes-SLNWSIFB-GKRsBluT.js} +2 -2
  59. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B2m_wca8.js +8 -0
  60. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bck7xjhe.js → timeline-definition-Z64GVDOM-Cwsq1DxL.js} +1 -1
  61. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CCntr1w7.js → vennDiagram-T6HMQDX7-CfaqtKx0.js} +1 -1
  62. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsCqCBZz.js → wardleyDiagram-T6FBY63Y-BlEUtK_g.js} +1 -1
  63. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Br2l90OM.js → xychartDiagram-ELKLHX3M-CHSRXPvO.js} +1 -1
  64. package/dist/worker/console/static/index.html +2 -2
  65. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +33 -32
  66. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  67. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  68. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +54 -56
  69. package/dist/worker/console/static-src/operator-chat/useChatStream.js +81 -27
  70. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -25
  71. package/dist/worker/console/static-src/shell/workspace-route.js +7 -5
  72. package/package.json +1 -1
  73. package/dist/worker/console/static/assets/channel-DSgFX07x.js +0 -1
  74. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-unJObRls.js +0 -1
  75. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-unJObRls.js +0 -1
  76. package/dist/worker/console/static/assets/index-B4l5ag79.js +0 -407
  77. package/dist/worker/console/static/assets/index-CwVHWB71.css +0 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BmOcMTJE.js +0 -1
  79. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Beah-dq0.js +0 -8
@@ -1,9 +1,10 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
3
3
  import { hydrateSessionThread } from "../../chat/turn-process.js";
4
- import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
4
+ import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
5
5
  import { CHAT_SESSION_LIST_LIMIT } from "../chat-view-types.js";
6
6
  import { confirmationToken } from "./format.js";
7
+ import { upsertCompactionMessage } from "./compaction-message.js";
7
8
  import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
8
9
  import { SessionTitleWatcher } from "./session-title-watcher.js";
9
10
  export async function createSessionAndPrimeTitleWatcher(input) {
@@ -64,28 +65,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
64
65
  }
65
66
  }
66
67
  function mergeCompactionMessages(messages, compactions) {
67
- const result = [...messages];
68
- for (const item of compactions ?? []) {
69
- const data = item.data && typeof item.data === "object" ? item.data : {};
70
- const eventId = typeof item.eventId === "string" ? item.eventId : undefined;
71
- const id = eventId ? `compaction-${eventId}` : `compaction-${item.at ?? Date.now()}`;
72
- if (result.some((message) => message.id === id))
73
- continue;
74
- const summary = typeof data.summary === "string" ? data.summary : "历史上下文已精简,关键决策与当前任务已保留。";
75
- result.push({
76
- id,
77
- role: "assistant",
78
- kind: "compaction",
79
- text: summary,
80
- createdAt: Date.parse(item.at ?? "") || Date.now(),
81
- compaction: {
82
- ...(typeof data.tokensBefore === "number" ? { tokensBefore: data.tokensBefore } : {}),
83
- ...(typeof data.firstKeptEntryId === "string" ? { firstKeptEntryId: data.firstKeptEntryId } : {}),
84
- ...(eventId ? { eventId } : {}),
85
- },
86
- });
87
- }
88
- return result.sort((a, b) => a.createdAt - b.createdAt);
68
+ return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
89
69
  }
90
70
  /** Session lifecycle: list / activate / create / delete / rename-archive /
91
71
  * compact, plus browser-refresh recovery from the durable session store. */
@@ -206,11 +186,12 @@ export function useChatSessions(params) {
206
186
  }
207
187
  titleWatcherRef.current?.cancel(sessionId);
208
188
  if (refs.sessionRef.current?.sessionId === sessionId) {
189
+ const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
209
190
  sessionSelectionVersionRef.current += 1;
210
191
  refs.sessionRef.current = null;
211
192
  resetActiveProjection();
212
193
  setSession(null);
213
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
194
+ forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
214
195
  }
215
196
  await refreshSessionList();
216
197
  return true;
@@ -227,6 +208,7 @@ export function useChatSessions(params) {
227
208
  refreshSessionList,
228
209
  setError,
229
210
  sessionOwnerQuery,
211
+ workspaceId,
230
212
  ]);
231
213
  const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
232
214
  const selectionVersion = ++sessionSelectionVersionRef.current;
@@ -251,9 +233,15 @@ export function useChatSessions(params) {
251
233
  // re-bound by useChatStream's session effect.
252
234
  resetActiveProjection({ keepStreams: true });
253
235
  refs.sessionRef.current = next;
254
- refs.streamingRef.current = Boolean(body.session.activeTurnId);
255
236
  refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
256
- refs.streamingRef.current = Boolean(body.session.activeTurnId);
237
+ const visuallyStopped = body.session.turnPhase === "stopping";
238
+ refs.streamingRef.current = visuallyStopped
239
+ ? false
240
+ : Boolean(body.session.activeTurnId);
241
+ refs.stoppingTurnIdRef.current = visuallyStopped
242
+ ? (body.session.activeTurnId ?? null)
243
+ : null;
244
+ refs.stopRequestedRef.current = visuallyStopped;
257
245
  refs.seenEventIdsRef.current = new Set();
258
246
  refs.turnAssistantIdsRef.current = new Map();
259
247
  refs.sealedAssistantIdsRef.current = new Set();
@@ -273,7 +261,7 @@ export function useChatSessions(params) {
273
261
  toolCalls: hydrated.toolCalls,
274
262
  sessionStats: body.sessionStats,
275
263
  usage: body.usage,
276
- streaming: Boolean(body.session.activeTurnId),
264
+ streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
277
265
  turnPhase: body.session.turnPhase ??
278
266
  (body.session.activeTurnId ? "running" : "idle"),
279
267
  deferredTurn: body.session.deferredTurn ?? null,
@@ -281,7 +269,7 @@ export function useChatSessions(params) {
281
269
  pendingQuestion: body.session.pendingQuestion ?? null,
282
270
  todos: body.session.todos ?? [],
283
271
  });
284
- window.localStorage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, sessionId);
272
+ rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
285
273
  setActivePreviewPath(null);
286
274
  }
287
275
  catch (error) {
@@ -301,6 +289,7 @@ export function useChatSessions(params) {
301
289
  setActivePreviewPath,
302
290
  setError,
303
291
  sessionOwnerQuery,
292
+ workspaceId,
304
293
  ]);
305
294
  useEffect(() => {
306
295
  void refreshSessionList();
@@ -331,21 +320,30 @@ export function useChatSessions(params) {
331
320
  .map((item) => item.sessionId));
332
321
  }, [sessionList]);
333
322
  useEffect(() => () => titleWatcherRef.current?.dispose(), []);
334
- // Browser refresh recovery: retain only the durable session id locally, then
335
- // re-read messages and operation refs/facts from the server-owned stores.
323
+ // Browser refresh/workspace-switch recovery: selection is client state keyed
324
+ // by workspace; messages and operation refs/facts remain server-owned.
336
325
  useEffect(() => {
337
326
  let cancelled = false;
338
327
  if (blocked)
339
328
  return; // AC-R5-002: unknown workspace — no restore GET
340
- const savedSessionId = window.localStorage.getItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
341
- if (!savedSessionId)
342
- return;
343
329
  const selectionVersion = ++sessionSelectionVersionRef.current;
330
+ // Remove the old workspace projection immediately. Background session
331
+ // streams remain alive; only the visible selection is rebound below.
332
+ resetActiveProjection({ keepStreams: true });
333
+ refs.sessionRef.current = null;
334
+ setSession(null);
335
+ const savedSessionId = readActiveChatSessionId(window.localStorage, workspaceId);
336
+ if (!savedSessionId) {
337
+ // Keep the per-workspace map intact, but clear the legacy global pointer
338
+ // so Operate/Models cannot link a session from another workspace.
339
+ forgetActiveChatSession(window.localStorage, workspaceId);
340
+ return;
341
+ }
344
342
  void (async () => {
345
343
  try {
346
344
  const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}${workspaceQuery}`, { credentials: "include" });
347
345
  if (!res.ok) {
348
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
346
+ forgetActiveChatSession(window.localStorage, workspaceId, savedSessionId);
349
347
  return;
350
348
  }
351
349
  const body = (await res.json());
@@ -356,6 +354,11 @@ export function useChatSessions(params) {
356
354
  return;
357
355
  const restored = {
358
356
  sessionId: body.session.sessionId,
357
+ ...(body.session.workspaceId
358
+ ? { workspaceId: body.session.workspaceId }
359
+ : workspaceId
360
+ ? { workspaceId }
361
+ : {}),
359
362
  ...runtimeSelectionFromRecord(body.session),
360
363
  };
361
364
  refs.sessionGenerationRef.current += 1;
@@ -365,12 +368,21 @@ export function useChatSessions(params) {
365
368
  // in the event ring and must replay once. applySseBlock dedups by
366
369
  // eventId and marks replay so usage/unread/sound do not double-count.
367
370
  refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
371
+ const visuallyStopped = body.session.turnPhase === "stopping";
372
+ refs.streamingRef.current = visuallyStopped
373
+ ? false
374
+ : Boolean(body.session.activeTurnId);
375
+ refs.stoppingTurnIdRef.current = visuallyStopped
376
+ ? (body.session.activeTurnId ?? null)
377
+ : null;
378
+ refs.stopRequestedRef.current = visuallyStopped;
368
379
  refs.seenEventIdsRef.current = new Set();
369
380
  refs.turnAssistantIdsRef.current = new Map();
370
381
  refs.sealedAssistantIdsRef.current = new Set();
371
382
  refs.settledTurnIdsRef.current = new Set();
372
383
  refs.lastEventIdRef.current = null;
373
384
  setSession(restored);
385
+ rememberActiveChatSession(window.localStorage, restored.workspaceId ?? workspaceId, restored.sessionId);
374
386
  setInput(body.session.composerDraft ?? "");
375
387
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
376
388
  hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
@@ -379,7 +391,7 @@ export function useChatSessions(params) {
379
391
  toolCalls: hydrated.toolCalls,
380
392
  sessionStats: body.sessionStats,
381
393
  usage: body.usage,
382
- streaming: Boolean(body.session.activeTurnId),
394
+ streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
383
395
  turnPhase: body.session.turnPhase ??
384
396
  (body.session.activeTurnId ? "running" : "idle"),
385
397
  deferredTurn: body.session.deferredTurn ?? null,
@@ -403,6 +415,8 @@ export function useChatSessions(params) {
403
415
  setInput,
404
416
  applyActivatedSession,
405
417
  blocked,
418
+ resetActiveProjection,
419
+ workspaceId,
406
420
  ]);
407
421
  // The sole production create path delegates to the exported orchestration
408
422
  // helper so its durable commit order is directly traceable and testable.
@@ -444,7 +458,7 @@ export function useChatSessions(params) {
444
458
  // createSession().then(sendPrompt) reads the new session even
445
459
  // though sendPrompt's closure captured the stale (null) value.
446
460
  refs.sessionRef.current = created;
447
- window.localStorage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, created.sessionId);
461
+ rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
448
462
  setSession(created);
449
463
  return created;
450
464
  }
@@ -464,6 +478,7 @@ export function useChatSessions(params) {
464
478
  resetActiveProjection,
465
479
  createSessionAndPrimeTitleWatcherForHook,
466
480
  blocked,
481
+ workspaceId,
467
482
  ]);
468
483
  // Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
469
484
  /** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
@@ -510,26 +525,8 @@ export function useChatSessions(params) {
510
525
  // pi-web parity: reload the compacted session, then leave a distinct
511
526
  // compaction summary card in the timeline plus a transient toast.
512
527
  const summary = body.snapshot?.summary?.trim();
513
- const tokensBefore = body.snapshot?.tokensBefore;
514
528
  const summaryText = summary || "历史上下文已精简,关键决策与当前任务已保留。";
515
529
  await activateSession(current.sessionId);
516
- setMessages((messages) => {
517
- const eventId = typeof body.eventId === "string" ? body.eventId : undefined;
518
- const id = eventId ? `compaction-${eventId}` : `compaction-${Date.now()}`;
519
- if (messages.some((message) => message.id === id))
520
- return messages;
521
- return [...messages, {
522
- id,
523
- role: "assistant",
524
- kind: "compaction",
525
- text: summaryText,
526
- createdAt: Date.now(),
527
- compaction: {
528
- ...(tokensBefore === undefined ? {} : { tokensBefore }),
529
- ...(eventId ? { eventId } : {}),
530
- },
531
- }];
532
- });
533
530
  setNotice({
534
531
  id: Date.now(),
535
532
  message: `已压缩上下文:${summaryText.slice(0, 72)}${summaryText.length > 72 ? "…" : ""}`,
@@ -546,7 +543,6 @@ export function useChatSessions(params) {
546
543
  refs,
547
544
  setError,
548
545
  setNotice,
549
- setMessages,
550
546
  onCompactionChange,
551
547
  activateSession,
552
548
  sessionOwnerQuery,
@@ -573,11 +569,13 @@ export function useChatSessions(params) {
573
569
  }
574
570
  if (patch.state === "archived" &&
575
571
  refs.sessionRef.current?.sessionId === sessionId) {
572
+ const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
573
+ const activeSessionId = refs.sessionRef.current.sessionId;
576
574
  sessionSelectionVersionRef.current += 1;
577
575
  refs.sessionRef.current = null;
578
576
  resetActiveProjection();
579
577
  setSession(null);
580
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
578
+ forgetActiveChatSession(window.localStorage, activeWorkspaceId, activeSessionId);
581
579
  }
582
580
  };
583
581
  return {
@@ -2,7 +2,7 @@ import { useCallback, useEffect } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
3
  import { confirmationToken, requestId, sleep } from "./format.js";
4
4
  import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
- import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, } from "./turn-stream-controller.js";
5
+ import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
6
6
  /** Project ChatRefs into the executable ownership bag used by the controller. */
7
7
  function ownershipBagFromRefs(refs) {
8
8
  return {
@@ -46,6 +46,7 @@ function sseEventName(block) {
46
46
  function sleepBriefly() {
47
47
  return new Promise((resolve) => window.setTimeout(resolve, 25));
48
48
  }
49
+ const STOP_RECONCILE_DELAYS_MS = [100, 250, 500, 1_000, 2_000];
49
50
  function clearEmptyAssistant(setMessages, assistantId) {
50
51
  if (!assistantId)
51
52
  return;
@@ -73,15 +74,26 @@ export function useChatStream(params) {
73
74
  if (refs.sessionRef.current?.sessionId !== sessionId ||
74
75
  refs.sessionGenerationRef.current !== generation)
75
76
  return;
77
+ const pendingClientRequestId = refs.pendingSubmissionRef.current?.clientRequestId;
78
+ if (pendingClientRequestId)
79
+ releaseSubmitOwnership(pendingClientRequestId);
76
80
  refs.streamingRef.current = false;
77
81
  refs.activeTurnIdRef.current = null;
78
82
  refs.assistantIdRef.current = null;
79
83
  refs.stoppingTurnIdRef.current = null;
84
+ refs.stopRequestedRef.current = false;
80
85
  setStreaming(false);
81
86
  setTurnPhase("idle");
82
87
  setStreamingAssistantId(null);
83
88
  clearEmptyAssistant(setMessages, assistantId);
84
- }, [refs, setStreaming, setTurnPhase, setStreamingAssistantId, setMessages]);
89
+ }, [
90
+ refs,
91
+ releaseSubmitOwnership,
92
+ setStreaming,
93
+ setTurnPhase,
94
+ setStreamingAssistantId,
95
+ setMessages,
96
+ ]);
85
97
  /** Follow one session's event stream until it is explicitly aborted. The
86
98
  * server's `stream_ready` marker separates replay from live events; this
87
99
  * means replay cannot render duplicate messages while live text remains
@@ -243,12 +255,12 @@ export function useChatStream(params) {
243
255
  /** Issue real POST /abort for a known turnId (Stop never disposes Session). */
244
256
  const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
245
257
  refs.stoppingTurnIdRef.current = turnId;
246
- // Optimistic visual stop: remove the running spinner immediately, but
247
- // retain ownership as `stopping` until the server proves a terminal state.
248
- // This keeps the composer responsive without falsely advertising idle.
258
+ // DSH-style visual stop: the composer becomes idle immediately. Hidden
259
+ // ownership remains stopping so a new message routes to the durable queue
260
+ // until terminal evidence proves the old Turn is gone.
249
261
  refs.streamingRef.current = false;
250
262
  setStreaming(false);
251
- setTurnPhase("stopping");
263
+ setTurnPhase("idle");
252
264
  setStreamingAssistantId(null);
253
265
  try {
254
266
  const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
@@ -277,13 +289,17 @@ export function useChatStream(params) {
277
289
  const state = await fetchSessionState(sessionId);
278
290
  const stillCurrent = refs.sessionRef.current?.sessionId === sessionId &&
279
291
  refs.sessionGenerationRef.current === generation;
292
+ // Terminal SSE may settle the Turn while /state is in flight. Do not
293
+ // write the pre-request ownership snapshot back after that newer fact.
294
+ if (!stillCurrent || refs.stoppingTurnIdRef.current !== turnId)
295
+ return;
280
296
  applyAbortDecision(bag, decision, noticeSink(), {
281
- sessionStillCurrent: stillCurrent,
297
+ sessionStillCurrent: true,
282
298
  runtimeBusy: isRuntimeBusyUnion(state ?? undefined),
283
299
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
284
300
  });
285
301
  applyOwnershipBagToRefs(refs, bag);
286
- setTurnPhase(turnPhaseFromOwnership(bag));
302
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
287
303
  return;
288
304
  }
289
305
  applyAbortDecision(bag, decision, noticeSink(), {
@@ -291,11 +307,7 @@ export function useChatStream(params) {
291
307
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
292
308
  });
293
309
  applyOwnershipBagToRefs(refs, bag);
294
- setTurnPhase(turnPhaseFromOwnership(bag));
295
- // Sync the post-decision busy state back to React: optimistic stop
296
- // flipped it idle up front; pending/keep-busy outcomes restore it.
297
- if (bag.streaming)
298
- setStreaming(true);
310
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
299
311
  }
300
312
  catch {
301
313
  // Abort network failure: keep busy (UIS-abort-network-fail).
@@ -308,11 +320,11 @@ export function useChatStream(params) {
308
320
  networkError: true,
309
321
  }), noticeSink());
310
322
  applyOwnershipBagToRefs(refs, bag);
311
- // Network failure: the optimistic idle was premature — restore busy
312
- // so the turn is not silently dropped; /state reconcile converges.
313
- refs.streamingRef.current = true;
314
- setStreaming(true);
315
- setTurnPhase("stopping");
323
+ // Transport failure is ambiguous: keep the visual composer available,
324
+ // retain hidden ownership, and let the short /state backoff converge.
325
+ refs.streamingRef.current = false;
326
+ setStreaming(false);
327
+ setTurnPhase("idle");
316
328
  }
317
329
  }, [
318
330
  origin,
@@ -368,7 +380,13 @@ export function useChatStream(params) {
368
380
  },
369
381
  });
370
382
  applyOwnershipBagToRefs(refs, bag);
371
- setTurnPhase(turnPhaseFromOwnership(bag));
383
+ const ownershipPhase = turnPhaseFromOwnership(bag);
384
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
385
+ if (ownershipPhase === "stopping") {
386
+ refs.streamingRef.current = false;
387
+ setStreaming(false);
388
+ setStreamingAssistantId(null);
389
+ }
372
390
  }, [
373
391
  refs,
374
392
  noticeSink,
@@ -806,10 +824,12 @@ export function useChatStream(params) {
806
824
  // Decide + mark intent/coalesce only; POST is issued once below.
807
825
  const stopResult = requestStopOnOwnership(bag, {
808
826
  postAbort: () => { },
809
- notice: noticeSink(),
810
827
  });
811
828
  applyOwnershipBagToRefs(refs, bag);
812
- setTurnPhase("stopping");
829
+ refs.streamingRef.current = false;
830
+ setStreaming(false);
831
+ setStreamingAssistantId(null);
832
+ setTurnPhase("idle");
813
833
  // Immediate abort already scheduled for a known turnId — coalesce via
814
834
  // abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
815
835
  if (stopResult.abortedTurnId) {
@@ -859,7 +879,6 @@ export function useChatStream(params) {
859
879
  markStopRequested(lateBag);
860
880
  const late = requestStopOnOwnership(lateBag, {
861
881
  postAbort: () => { },
862
- notice: noticeSink(),
863
882
  });
864
883
  applyOwnershipBagToRefs(refs, lateBag);
865
884
  if (late.abortedTurnId) {
@@ -869,7 +888,13 @@ export function useChatStream(params) {
869
888
  finally {
870
889
  refs.stopInFlightRef.current = false;
871
890
  }
872
- }, [refs, noticeSink, postAbortForTurn]);
891
+ }, [
892
+ refs,
893
+ postAbortForTurn,
894
+ setStreaming,
895
+ setStreamingAssistantId,
896
+ setTurnPhase,
897
+ ]);
873
898
  // State reconciliation is only a status fence: it never consumes/creates
874
899
  // message events, and a generation check discards late responses (M5.3).
875
900
  // Busy truth = durable activeTurn union runtime busy facts (AC-003/AC-006).
@@ -889,7 +914,8 @@ export function useChatStream(params) {
889
914
  const sessionId = session.sessionId;
890
915
  const generation = refs.sessionGenerationRef.current;
891
916
  let cancelled = false;
892
- const sync = async () => {
917
+ let syncInFlight = null;
918
+ const runSync = async () => {
893
919
  try {
894
920
  const res = await fetch(operatorChatStateUrl(origin, sessionId), {
895
921
  credentials: "include",
@@ -918,10 +944,16 @@ export function useChatStream(params) {
918
944
  },
919
945
  });
920
946
  applyOwnershipBagToRefs(refs, bag);
921
- setTurnPhase(turnPhaseFromOwnership(bag));
947
+ const ownershipPhase = turnPhaseFromOwnership(bag);
948
+ setTurnPhase(visualTurnPhaseFromOwnership(bag));
949
+ if (ownershipPhase === "stopping") {
950
+ refs.streamingRef.current = false;
951
+ setStreaming(false);
952
+ setStreamingAssistantId(null);
953
+ }
922
954
  if (state && "deferredTurn" in state)
923
955
  setDeferredTurn(state.deferredTurn ?? null);
924
- if (result === "bound") {
956
+ if (result === "bound" && ownershipPhase !== "stopping") {
925
957
  // US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
926
958
  setStreaming(true);
927
959
  return;
@@ -999,19 +1031,41 @@ export function useChatStream(params) {
999
1031
  // offline/background case: interval, visibility and online retry.
1000
1032
  }
1001
1033
  };
1034
+ const sync = () => {
1035
+ if (syncInFlight)
1036
+ return syncInFlight;
1037
+ syncInFlight = runSync().finally(() => {
1038
+ syncInFlight = null;
1039
+ });
1040
+ return syncInFlight;
1041
+ };
1002
1042
  const onVisible = () => {
1003
1043
  if (document.visibilityState === "visible")
1004
1044
  void sync();
1005
1045
  };
1006
1046
  const onOnline = () => void sync();
1007
1047
  const interval = window.setInterval(() => void sync(), 15_000);
1048
+ let stopRetryTimer;
1049
+ let stopRetryIndex = 0;
1050
+ const scheduleStopRetry = () => {
1051
+ if (cancelled ||
1052
+ stopRetryIndex >= STOP_RECONCILE_DELAYS_MS.length ||
1053
+ (!refs.stopRequestedRef.current && !refs.stoppingTurnIdRef.current))
1054
+ return;
1055
+ const delay = STOP_RECONCILE_DELAYS_MS[stopRetryIndex++];
1056
+ stopRetryTimer = window.setTimeout(() => {
1057
+ void sync().finally(scheduleStopRetry);
1058
+ }, delay);
1059
+ };
1008
1060
  document.addEventListener("visibilitychange", onVisible);
1009
1061
  window.addEventListener("online", onOnline);
1010
1062
  // Immediate tick so terminal-before-streaming-false races converge promptly.
1011
- void sync();
1063
+ void sync().finally(scheduleStopRetry);
1012
1064
  return () => {
1013
1065
  cancelled = true;
1014
1066
  window.clearInterval(interval);
1067
+ if (stopRetryTimer !== undefined)
1068
+ window.clearTimeout(stopRetryTimer);
1015
1069
  document.removeEventListener("visibilitychange", onVisible);
1016
1070
  window.removeEventListener("online", onOnline);
1017
1071
  };
@@ -3,6 +3,7 @@ import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, }
3
3
  import { mergeOperationCardState, } from "../../chat/operation-card.js";
4
4
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
5
5
  import { taskContextUnchanged } from "./format.js";
6
+ import { upsertCompactionMessage } from "./compaction-message.js";
6
7
  const EMPTY_USAGE = {
7
8
  inputTokens: 0,
8
9
  outputTokens: 0,
@@ -100,27 +101,7 @@ export async function applyOperationLinkMutationProjection(input) {
100
101
  }
101
102
  }
102
103
  function mergeCompactionMessages(messages, compactions) {
103
- const result = [...messages];
104
- for (const item of compactions ?? []) {
105
- const data = item.data && typeof item.data === "object" ? item.data : {};
106
- const eventId = typeof item.eventId === "string" ? item.eventId : undefined;
107
- const id = eventId ? `compaction-${eventId}` : `compaction-${item.at ?? Date.now()}`;
108
- if (result.some((message) => message.id === id))
109
- continue;
110
- result.push({
111
- id,
112
- role: "assistant",
113
- kind: "compaction",
114
- text: typeof data.summary === "string" ? data.summary : "历史上下文已精简,关键决策与当前任务已保留。",
115
- createdAt: Date.parse(item.at ?? "") || Date.now(),
116
- compaction: {
117
- ...(typeof data.tokensBefore === "number" ? { tokensBefore: data.tokensBefore } : {}),
118
- ...(typeof data.firstKeptEntryId === "string" ? { firstKeptEntryId: data.firstKeptEntryId } : {}),
119
- ...(eventId ? { eventId } : {}),
120
- },
121
- });
122
- }
123
- return result.sort((a, b) => a.createdAt - b.createdAt);
104
+ return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
124
105
  }
125
106
  /**
126
107
  * Thread state + SSE block parsing. The event-application logic itself lives
@@ -363,6 +344,7 @@ export function useChatThread(params) {
363
344
  }, [refs]);
364
345
  /** Adopt a freshly activated session's hydrated thread + streaming flags. */
365
346
  const applyActivatedSession = useCallback((next) => {
347
+ const visuallyStopped = next.turnPhase === "stopping";
366
348
  setMessages(next.messages);
367
349
  setToolCalls(next.toolCalls);
368
350
  setOperationCards([]);
@@ -373,8 +355,10 @@ export function useChatThread(params) {
373
355
  setUsage(next.usage ?? EMPTY_USAGE);
374
356
  setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
375
357
  refs.usageResponseKeysRef.current = new Set();
376
- setStreaming(next.streaming);
377
- setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
358
+ setStreaming(visuallyStopped ? false : next.streaming);
359
+ setTurnPhase(visuallyStopped
360
+ ? "idle"
361
+ : (next.turnPhase ?? (next.streaming ? "running" : "idle")));
378
362
  setDeferredTurn(next.deferredTurn ?? null);
379
363
  setDeferredTurns(next.deferredTurns ?? (next.deferredTurn ? [next.deferredTurn] : []));
380
364
  setPendingQuestion(next.pendingQuestion ?? null);
@@ -398,9 +382,13 @@ export function useChatThread(params) {
398
382
  refs.sessionRef.current?.sessionId !== targetSessionId)
399
383
  return false;
400
384
  const activeTurnId = body.session.activeTurnId ?? null;
385
+ const visuallyStopped = body.session.turnPhase === "stopping";
401
386
  refs.activeTurnIdRef.current = activeTurnId;
402
- refs.streamingRef.current = Boolean(activeTurnId);
403
- setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
387
+ refs.streamingRef.current = visuallyStopped ? false : Boolean(activeTurnId);
388
+ setStreaming(visuallyStopped ? false : Boolean(activeTurnId));
389
+ setTurnPhase(visuallyStopped
390
+ ? "idle"
391
+ : (body.session.turnPhase ?? (activeTurnId ? "running" : "idle")));
404
392
  setDeferredTurn(body.session.deferredTurn ?? null);
405
393
  setDeferredTurns(body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []));
406
394
  setPendingQuestion(body.session.pendingQuestion ?? null);
@@ -433,6 +421,7 @@ export function useChatThread(params) {
433
421
  setMessages,
434
422
  setToolCalls,
435
423
  setStreamingAssistantId,
424
+ setStreaming,
436
425
  setTurnPhase,
437
426
  setDeferredTurn,
438
427
  setPendingQuestion,
@@ -145,9 +145,9 @@ export function buildInspectHref(inspectPath = "/", query) {
145
145
  : query?.toString() ?? "";
146
146
  return `/#${path}${qs ? `?${qs}` : ""}`;
147
147
  }
148
- export function buildChatHref(params = {}) {
149
- const qs = paramsQuery(params);
150
- return qs ? `${CHAT_HREF}?${qs}` : CHAT_HREF;
148
+ /** Chat selection is client state, like DSH: keep the browser route stable. */
149
+ export function buildChatHref(_params = {}) {
150
+ return CHAT_HREF;
151
151
  }
152
152
  /** Keep the selected Workspace on any canonical Console destination without
153
153
  * changing its page-specific path or query state. */
@@ -158,6 +158,8 @@ export function withWorkspaceId(href, workspaceId) {
158
158
  ? `/${href}`
159
159
  : rewriteInspectHrefToCanonical(href);
160
160
  const { path, query } = splitHash(canonical.slice(2));
161
+ if (path === "/chat")
162
+ return CHAT_HREF;
161
163
  const id = workspaceId?.trim();
162
164
  if (id)
163
165
  query.set("workspaceId", id);
@@ -201,14 +203,14 @@ function finishOperate(tab, query, replace) {
201
203
  }
202
204
  function finishChat(replace, params) {
203
205
  const effective = params ?? {};
204
- const canonicalHref = buildChatHref(effective);
206
+ const canonicalHref = CHAT_HREF;
205
207
  return {
206
208
  workspace: "chat",
207
209
  operateTab: DEFAULT_OPERATE_TAB,
208
210
  inspectPath: "/",
209
211
  canonicalHash: canonicalHref.slice(2),
210
212
  canonicalHref,
211
- replace,
213
+ replace: replace || paramsQuery(effective).length > 0,
212
214
  ...effective,
213
215
  };
214
216
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.40.0-next.7",
3
+ "version": "0.40.0-next.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -1 +0,0 @@
1
- import{ag as o,ah as n}from"./mermaid.core-BYdXtke8.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-CEenyPTz.js";import{_ as i}from"./mermaid.core-BYdXtke8.js";import"./chunk-5VM5RSS4-C2-8Eo_A.js";import"./chunk-XXDRQBXY-CjE9LQOM.js";import"./chunk-KBJHAD2P-FGngOTto.js";import"./chunk-2GRJ4B5K-DhvWRI03.js";import"./index-B4l5ag79.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-GF5L2VYU-CEenyPTz.js";import{_ as i}from"./mermaid.core-BYdXtke8.js";import"./chunk-5VM5RSS4-C2-8Eo_A.js";import"./chunk-XXDRQBXY-CjE9LQOM.js";import"./chunk-KBJHAD2P-FGngOTto.js";import"./chunk-2GRJ4B5K-DhvWRI03.js";import"./index-B4l5ag79.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};