@tea-agent/loop-agent 0.42.0-next.4 → 0.42.0-next.5

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 (80) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/worker/console/chat/pi-runtime.js +26 -0
  4. package/dist/worker/console/chat/routes.js +60 -5
  5. package/dist/worker/console/chat/sdd-data-alignment.js +219 -0
  6. package/dist/worker/console/chat/session-catalog.js +2 -0
  7. package/dist/worker/console/chat/session-store.js +26 -0
  8. package/dist/worker/console/chat/shortcuts.js +18 -2
  9. package/dist/worker/console/server.js +1 -0
  10. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CeznJ4Iw.js → abnfDiagram-N423BO3Z-BJpsGgAv.js} +1 -1
  11. package/dist/worker/console/static/assets/{arc-B4u9u7SX.js → arc-DjMFPsEd.js} +1 -1
  12. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-oghpT81p.js → architectureDiagram-T3A2C74G-DqdZlr1O.js} +1 -1
  13. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BPJ5Kvcj.js → blockDiagram-VBNYF7ZC-BmZnfFEO.js} +1 -1
  14. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CoIu69pd.js → c4Diagram-5PPSVZJV-3cKberpL.js} +1 -1
  15. package/dist/worker/console/static/assets/channel-DWYwciiv.js +1 -0
  16. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DtgiAsSc.js → chunk-2GRJ4B5K-q-0yjQ1h.js} +1 -1
  17. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-YSu_EVSD.js → chunk-2Q5K7J3B-DnONBaDW.js} +1 -1
  18. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DaRO-NB6.js → chunk-5RXB4S5H-v3a6PCOS.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Dh0ZKiGA.js → chunk-5VM5RSS4-D1kLrlU_.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DOrvbR-H.js → chunk-6Q2QTUOP-BPelzihi.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3b22LN2.js → chunk-GF5L2VYU-DauNE69H.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CXlqO8l6.js → chunk-JWPE2WC7-RIg5_bFJ.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BblTBrDu.js → chunk-KBJHAD2P-BuILuoxs.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DswqPALe.js → chunk-RYQCIY6F-VHORV5oW.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CfTwVOlc.js → chunk-XXDRQBXY-BJYr4xHC.js} +1 -1
  26. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +1 -0
  27. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +1 -0
  28. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-bLD8jVFo.js → cose-bilkent-JH36ORCC-BS6nuqQn.js} +1 -1
  29. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BvoG1fh5.js → cynefin-VYW2F7L2-BBhF0NAT.js} +1 -1
  30. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DEPAdJu3.js → cynefinDiagram-MW4NZA55-D1135Ifg.js} +1 -1
  31. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DmrNvBLy.js → dagre-VZM6K2ZE-vAOYPUWJ.js} +1 -1
  32. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdwzIwvs.js → diagram-7IWD3JNH-DuaSPkfa.js} +1 -1
  33. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CJ3hj-md.js → diagram-B4RE2ZJO-mMvXvGU6.js} +1 -1
  34. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C6YJT2a4.js → diagram-LBJQPF4R-CDIEJtlL.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C_LPfjhX.js → diagram-Q27KOJAE-Bi_6JvCW.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-UB23O5K3-xvsSb5Ie.js → diagram-UB23O5K3-DAqj57yN.js} +1 -1
  37. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Rxp2tKcD.js → ebnfDiagram-BXEA7PRR-DfWaxNEo.js} +1 -1
  38. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DRyLYmZh.js → erDiagram-JOGREHBK-Bt7QqkNG.js} +1 -1
  39. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-TLSU_D2B.js → flowDiagram-UKHOOZJN-CRRhQwT2.js} +1 -1
  40. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGqeTvHC.js → ganttDiagram-PKOTCBZU-DAV1O5B6.js} +1 -1
  41. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BZxyH2wh.js → gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js} +1 -1
  42. package/dist/worker/console/static/assets/index-CVdddQsA.js +451 -0
  43. package/dist/worker/console/static/assets/index-DDQc5a50.css +1 -0
  44. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BsYtzx01.js → infoDiagram-6WML65LV-C5W5eILC.js} +1 -1
  45. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js → ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js} +1 -1
  46. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DYbB3ibo.js → journeyDiagram-NVQOT4AX-CKDRcaKf.js} +1 -1
  47. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BHJHewZO.js → kanban-definition-27J2QSJJ-Cc7_TuSb.js} +1 -1
  48. package/dist/worker/console/static/assets/{linear-CmPIPmCG.js → linear-BcQYYmeT.js} +1 -1
  49. package/dist/worker/console/static/assets/{mermaid.core-F--C1uV0.js → mermaid.core-CwLZPtQo.js} +5 -5
  50. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-YTXfL1st.js → mindmap-definition-FAOFIHXS-D29M025z.js} +1 -1
  51. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DdDMYXP9.js → pegDiagram-VL7TDLO6-CV-ZAWYe.js} +1 -1
  52. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C3nH4KVd.js → pieDiagram-7S7Q4E2Y-C6iPS_o5.js} +1 -1
  53. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js → quadrantDiagram-CIZ2JOQS-BFCD1sTg.js} +1 -1
  54. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-KNzbl-7w.js → railroadDiagram-AXF67PYL-CztNj4O9.js} +1 -1
  55. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CSwI6dG0.js → requirementDiagram-LRYGKXZP-BWIELSNd.js} +1 -1
  56. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bl1AyuXy.js → sankeyDiagram-W5VNT64P-CPtxrjJQ.js} +1 -1
  57. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-2y4EmXQa.js → sequenceDiagram-SI44F4Z6-BEWVSFsu.js} +1 -1
  58. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-E8v8YrMg.js → sizeCapture-X5ZJPWSS-DyhzoX_J.js} +1 -1
  59. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eGGlR856.js → stateDiagram-OKZ733FA-xLox0nu7.js} +1 -1
  60. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +1 -0
  61. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-B7g7xc07.js → swimlanes-SLNWSIFB-CLC_ow9A.js} +2 -2
  62. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +8 -0
  63. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DR3VCHBG.js → timeline-definition-Z64GVDOM-DmJZ4uyY.js} +1 -1
  64. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LUvapZIy.js → vennDiagram-T6HMQDX7-DD7GGi9Z.js} +1 -1
  65. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C7GDcaXq.js → wardleyDiagram-T6FBY63Y-Bi8PQoGt.js} +1 -1
  66. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CbNAqoGK.js → xychartDiagram-ELKLHX3M-uKdAY8Dp.js} +1 -1
  67. package/dist/worker/console/static/index.html +2 -2
  68. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +225 -21
  69. package/dist/worker/console/static-src/operator-chat/useComposer.js +5 -1
  70. package/dist/worker/console/workspace-context.js +4 -0
  71. package/dist/workflows/dag/init-hybrid.js +22 -15
  72. package/docs/templates/backend-test-dag.json +3 -3
  73. package/package.json +1 -1
  74. package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +0 -1
  75. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +0 -1
  76. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +0 -1
  77. package/dist/worker/console/static/assets/index-C8V8K9b4.js +0 -451
  78. package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +0 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +0 -1
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +0 -8
@@ -6,6 +6,94 @@ import { confirmationToken } from "./format.js";
6
6
  import { upsertCompactionMessage } from "./compaction-message.js";
7
7
  import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
8
8
  import { SessionTitleWatcher } from "./session-title-watcher.js";
9
+ /** Pure list mutations keep optimistic UI deterministic and easy to verify. */
10
+ export function insertOptimisticSession(list, row) {
11
+ return [row, ...list.filter((item) => item.sessionId !== row.sessionId)];
12
+ }
13
+ export function replaceOptimisticSession(list, temporaryId, durable) {
14
+ return [
15
+ durable,
16
+ ...list.filter((item) => item.sessionId !== durable.sessionId &&
17
+ (!temporaryId || item.sessionId !== temporaryId)),
18
+ ];
19
+ }
20
+ export function removeSessionById(list, sessionId) {
21
+ const index = list.findIndex((item) => item.sessionId === sessionId);
22
+ if (index < 0)
23
+ return { next: list, index: -1 };
24
+ return {
25
+ next: list.filter((item) => item.sessionId !== sessionId),
26
+ removed: list[index],
27
+ index,
28
+ };
29
+ }
30
+ export function restoreSessionAt(list, row, index) {
31
+ if (list.some((item) => item.sessionId === row.sessionId))
32
+ return list;
33
+ const next = [...list];
34
+ next.splice(Math.max(0, Math.min(index, next.length)), 0, row);
35
+ return next;
36
+ }
37
+ function sameQueueSummary(a, b) {
38
+ const left = a.deferredTurns ?? (a.deferredTurn ? [a.deferredTurn] : []);
39
+ const right = b.deferredTurns ?? (b.deferredTurn ? [b.deferredTurn] : []);
40
+ if (left.length !== right.length)
41
+ return false;
42
+ return left.every((item, index) => {
43
+ const other = right[index];
44
+ return Boolean(other &&
45
+ item.clientRequestId === other.clientRequestId &&
46
+ item.revision === other.revision &&
47
+ item.state === other.state &&
48
+ item.updatedAt === other.updatedAt &&
49
+ item.textPreview === other.textPreview &&
50
+ item.imageCount === other.imageCount &&
51
+ item.acceptedTurnId === other.acceptedTurnId &&
52
+ item.lastError?.code === other.lastError?.code &&
53
+ item.lastError?.message === other.lastError?.message);
54
+ });
55
+ }
56
+ function sameSessionSummary(a, b) {
57
+ return (a.sessionId === b.sessionId &&
58
+ a.workspaceId === b.workspaceId &&
59
+ a.state === b.state &&
60
+ a.title === b.title &&
61
+ a.displayTitle === b.displayTitle &&
62
+ a.updatedAt === b.updatedAt &&
63
+ a.activeTurnId === b.activeTurnId &&
64
+ a.turnPhase === b.turnPhase &&
65
+ a.model?.provider === b.model?.provider &&
66
+ a.model?.modelId === b.model?.modelId &&
67
+ a.thinkingLevel === b.thinkingLevel &&
68
+ a.isBlank === b.isBlank &&
69
+ a.optimisticState === b.optimisticState &&
70
+ sameQueueSummary(a, b));
71
+ }
72
+ /** Reuse unchanged row and list references across catalog refreshes. */
73
+ export function stabilizeSessionList(previous, next) {
74
+ const previousById = new Map(previous.map((item) => [item.sessionId, item]));
75
+ let changed = previous.length !== next.length;
76
+ const stabilized = next.map((item, index) => {
77
+ const existing = previousById.get(item.sessionId);
78
+ const value = existing && sameSessionSummary(existing, item) ? existing : item;
79
+ if (value !== previous[index])
80
+ changed = true;
81
+ return value;
82
+ });
83
+ return changed ? stabilized : previous;
84
+ }
85
+ /** Select a server-confirmed blank session owned by the requested workspace. */
86
+ export function findReusableBlankSession(list, workspaceId) {
87
+ const scope = workspaceId.trim();
88
+ if (!scope)
89
+ return undefined;
90
+ return list.find((item) => item.workspaceId === scope &&
91
+ item.state === "active" &&
92
+ item.isBlank === true &&
93
+ item.turnPhase === "idle" &&
94
+ !item.activeTurnId &&
95
+ !item.optimisticState);
96
+ }
9
97
  export async function createSessionAndPrimeTitleWatcher(input) {
10
98
  if (input.creatingRef?.current) {
11
99
  throw new Error("CREATE_SESSION_IN_FLIGHT");
@@ -23,7 +111,10 @@ export async function createSessionAndPrimeTitleWatcher(input) {
23
111
  "x-loop-console-confirmation": confirmationToken(),
24
112
  },
25
113
  credentials: "include",
26
- body: JSON.stringify(input.options ?? {}),
114
+ body: JSON.stringify((() => {
115
+ const { optimisticSessionId: _optimisticSessionId, ...body } = input.options ?? {};
116
+ return body;
117
+ })()),
27
118
  });
28
119
  if (!res.ok) {
29
120
  const body = (await res.json().catch(() => ({})));
@@ -35,7 +126,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
35
126
  const body = (await res.json());
36
127
  if (!body.session)
37
128
  throw new Error("创建会话响应缺少 durable session");
38
- const durable = body.session;
129
+ const durable = { ...body.session, isBlank: true };
39
130
  const created = {
40
131
  sessionId: durable.sessionId,
41
132
  ...(durable.workspaceId || input.options?.workspaceId
@@ -45,16 +136,12 @@ export async function createSessionAndPrimeTitleWatcher(input) {
45
136
  : {}),
46
137
  ...runtimeSelectionFromRecord(durable),
47
138
  };
48
- const nextList = [
49
- durable,
50
- ...input.sessionListRef.current.filter((item) => item.sessionId !== durable.sessionId),
51
- ];
139
+ const nextList = replaceOptimisticSession(input.sessionListRef.current, input.options?.optimisticSessionId, durable);
52
140
  // Commit ref → state → eligibility → deferred refresh. The ref assignment
53
141
  // makes the durable record observable before React's state commit.
54
142
  input.sessionListRef.current = nextList;
55
143
  input.setSessionList(nextList);
56
144
  input.titleWatcher?.markEligible(durable.sessionId);
57
- void input.refreshSessionList();
58
145
  return created;
59
146
  }
60
147
  finally {
@@ -90,6 +177,11 @@ export function useChatSessions(params) {
90
177
  }, [refs]);
91
178
  const [sessionList, setSessionList] = useState([]);
92
179
  const sessionListRef = useRef([]);
180
+ const pendingDeletedIdsRef = useRef(new Set());
181
+ const pendingCreatedRowsRef = useRef(new Map());
182
+ const sessionMutationVersionRef = useRef(0);
183
+ const deleteRequestVersionsRef = useRef(new Map());
184
+ const activateSessionRef = useRef(null);
93
185
  sessionListRef.current = sessionList;
94
186
  const titleWatcherRef = useRef(null);
95
187
  // Shared busy gate for sidebar + Landing create entries (one render feedback).
@@ -159,9 +251,13 @@ export function useChatSessions(params) {
159
251
  return;
160
252
  }
161
253
  const sessions = body.sessions ?? [];
254
+ const durable = sessions.filter((item) => !pendingDeletedIdsRef.current.has(item.sessionId));
255
+ const optimistic = [...pendingCreatedRowsRef.current.values()].filter((item) => !durable.some((session) => session.sessionId === item.sessionId));
162
256
  // Preserve every workspace's summaries. ChatWorkspaceSidebar applies the
163
257
  // visible recent-item limit independently inside each workspace group.
164
- setSessionList(sessions);
258
+ const next = stabilizeSessionList(sessionListRef.current, [...optimistic, ...durable]);
259
+ sessionListRef.current = next;
260
+ setSessionList(next);
165
261
  if (body.workspaceErrors?.length) {
166
262
  setError(`部分工作区的会话加载失败(${body.workspaceErrors.length} 个)`);
167
263
  }
@@ -171,6 +267,60 @@ export function useChatSessions(params) {
171
267
  }
172
268
  }, [origin, blocked, setError]);
173
269
  const deleteSession = useCallback(async (sessionId) => {
270
+ if (pendingDeletedIdsRef.current.has(sessionId))
271
+ return false;
272
+ const requestId = ++sessionMutationVersionRef.current;
273
+ const currentList = sessionListRef.current;
274
+ const { next: removedList, removed, index: removedIndex } = removeSessionById(currentList, sessionId);
275
+ if (!removed || removedIndex < 0)
276
+ return false;
277
+ pendingDeletedIdsRef.current.add(sessionId);
278
+ deleteRequestVersionsRef.current.set(sessionId, requestId);
279
+ const activeSession = refs.sessionRef.current?.sessionId === sessionId
280
+ ? refs.sessionRef.current
281
+ : null;
282
+ const activeWorkspaceId = activeSession?.workspaceId ?? workspaceId;
283
+ const deletingRow = {
284
+ ...removed,
285
+ optimisticState: "deleting",
286
+ };
287
+ const deletingList = restoreSessionAt(removedList, deletingRow, removedIndex);
288
+ sessionListRef.current = deletingList;
289
+ setSessionList(deletingList);
290
+ window.setTimeout(() => {
291
+ if (!pendingDeletedIdsRef.current.has(sessionId) ||
292
+ deleteRequestVersionsRef.current.get(sessionId) !== requestId)
293
+ return;
294
+ const next = sessionListRef.current.filter((item) => item.sessionId !== sessionId);
295
+ sessionListRef.current = next;
296
+ setSessionList(next);
297
+ }, 150);
298
+ const restore = () => {
299
+ if (deleteRequestVersionsRef.current.get(sessionId) !== requestId)
300
+ return;
301
+ pendingDeletedIdsRef.current.delete(sessionId);
302
+ deleteRequestVersionsRef.current.delete(sessionId);
303
+ const restored = restoreSessionAt(sessionListRef.current, removed, removedIndex);
304
+ if (restored === sessionListRef.current)
305
+ return;
306
+ sessionListRef.current = restored;
307
+ setSessionList(restored);
308
+ };
309
+ let deletionSelectionVersion = null;
310
+ if (activeSession) {
311
+ deletionSelectionVersion = ++sessionSelectionVersionRef.current;
312
+ refs.sessionRef.current = null;
313
+ resetActiveProjection();
314
+ setSession(null);
315
+ forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
316
+ }
317
+ const restoreActiveSession = async () => {
318
+ if (activeSession &&
319
+ deletionSelectionVersion === sessionSelectionVersionRef.current &&
320
+ !refs.sessionRef.current) {
321
+ await activateSessionRef.current?.(activeSession.sessionId, activeSession.workspaceId);
322
+ }
323
+ };
174
324
  try {
175
325
  // AC-002: delete follows the session's durable owner, not the
176
326
  // global active workspace.
@@ -184,22 +334,21 @@ export function useChatSessions(params) {
184
334
  },
185
335
  });
186
336
  if (!res.ok) {
337
+ restore();
338
+ await restoreActiveSession();
187
339
  setError("删除会话失败");
188
340
  return false;
189
341
  }
190
342
  titleWatcherRef.current?.cancel(sessionId);
191
- if (refs.sessionRef.current?.sessionId === sessionId) {
192
- const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
193
- sessionSelectionVersionRef.current += 1;
194
- refs.sessionRef.current = null;
195
- resetActiveProjection();
196
- setSession(null);
197
- forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
198
- }
199
- await refreshSessionList();
343
+ void refreshSessionList().finally(() => {
344
+ pendingDeletedIdsRef.current.delete(sessionId);
345
+ deleteRequestVersionsRef.current.delete(sessionId);
346
+ });
200
347
  return true;
201
348
  }
202
349
  catch (error) {
350
+ restore();
351
+ await restoreActiveSession();
203
352
  setError(error instanceof Error ? error.message : String(error));
204
353
  return false;
205
354
  }
@@ -225,7 +374,7 @@ export function useChatSessions(params) {
225
374
  if (!res.ok || !body.session)
226
375
  throw new Error(body.error?.message ?? "会话不可用");
227
376
  if (selectionVersion !== sessionSelectionVersionRef.current)
228
- return;
377
+ return false;
229
378
  const next = {
230
379
  sessionId: body.session.sessionId,
231
380
  ...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
@@ -276,11 +425,13 @@ export function useChatSessions(params) {
276
425
  });
277
426
  rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
278
427
  setActivePreviewPath(null);
428
+ return true;
279
429
  }
280
430
  catch (error) {
281
431
  if (selectionVersion !== sessionSelectionVersionRef.current)
282
- return;
432
+ return false;
283
433
  setError(error instanceof Error ? error.message : String(error));
434
+ return false;
284
435
  }
285
436
  }, [
286
437
  origin,
@@ -296,6 +447,7 @@ export function useChatSessions(params) {
296
447
  sessionOwnerQuery,
297
448
  workspaceId,
298
449
  ]);
450
+ activateSessionRef.current = activateSession;
299
451
  useEffect(() => {
300
452
  void refreshSessionList();
301
453
  }, [refreshSessionList]);
@@ -437,15 +589,38 @@ export function useChatSessions(params) {
437
589
  setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
438
590
  return Promise.reject(new Error("WORKSPACE_REQUIRED"));
439
591
  }
592
+ const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
593
+ const now = new Date().toISOString();
594
+ const optimisticRow = {
595
+ sessionId: optimisticSessionId,
596
+ workspaceId: scope,
597
+ state: "active",
598
+ updatedAt: now,
599
+ turnPhase: "idle",
600
+ optimisticState: "creating",
601
+ };
602
+ pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
603
+ const optimisticList = insertOptimisticSession(sessionListRef.current, optimisticRow);
604
+ sessionListRef.current = optimisticList;
605
+ setSessionList(optimisticList);
440
606
  return createSessionAndPrimeTitleWatcher({
441
607
  origin,
442
608
  sessionListRef,
443
609
  setSessionList,
444
610
  titleWatcher: titleWatcherRef.current,
445
611
  refreshSessionList,
446
- options: { ...options, workspaceId: scope },
612
+ options: { ...options, workspaceId: scope, optimisticSessionId },
447
613
  setCreatingSession,
448
614
  creatingRef: creatingSessionRef,
615
+ }).then((created) => {
616
+ pendingCreatedRowsRef.current.delete(optimisticSessionId);
617
+ return created;
618
+ }).catch((error) => {
619
+ pendingCreatedRowsRef.current.delete(optimisticSessionId);
620
+ const cleaned = sessionListRef.current.filter((item) => item.sessionId !== optimisticSessionId);
621
+ sessionListRef.current = cleaned;
622
+ setSessionList(cleaned);
623
+ throw error;
449
624
  });
450
625
  }, [origin, refreshSessionList, workspaceId, setError]);
451
626
  const createSession = useCallback(async (options) => {
@@ -456,11 +631,36 @@ export function useChatSessions(params) {
456
631
  return null; // AC-R5-002: unknown workspace — no POST
457
632
  const selectionVersion = ++sessionSelectionVersionRef.current;
458
633
  setError(null);
634
+ const previousSession = refs.sessionRef.current;
635
+ const scope = options?.workspaceId?.trim() ?? workspaceId?.trim();
636
+ if (!scope) {
637
+ setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
638
+ return null;
639
+ }
640
+ // A caller-provided snapshot is an explicit new-session boundary; reusing
641
+ // an existing shell would silently drop that context suffix.
642
+ const reusable = options?.snapshot
643
+ ? undefined
644
+ : findReusableBlankSession(sessionListRef.current, scope);
645
+ if (reusable) {
646
+ if (previousSession?.sessionId === reusable.sessionId)
647
+ return previousSession;
648
+ const activated = await activateSession(reusable.sessionId, reusable.workspaceId);
649
+ if (!activated)
650
+ return null;
651
+ return {
652
+ sessionId: reusable.sessionId,
653
+ ...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
654
+ ...runtimeSelectionFromRecord(reusable),
655
+ };
656
+ }
459
657
  try {
658
+ resetActiveProjection({ keepStreams: true });
659
+ refs.sessionRef.current = null;
660
+ setSession(null);
460
661
  const created = await createSessionAndPrimeTitleWatcherForHook(options);
461
662
  if (selectionVersion !== sessionSelectionVersionRef.current)
462
663
  return null;
463
- resetActiveProjection({ keepStreams: true });
464
664
  // Set ref synchronously BEFORE setState so a chained
465
665
  // createSession().then(sendPrompt) reads the new session even
466
666
  // though sendPrompt's closure captured the stale (null) value.
@@ -472,6 +672,9 @@ export function useChatSessions(params) {
472
672
  catch (e) {
473
673
  if (selectionVersion !== sessionSelectionVersionRef.current)
474
674
  return null;
675
+ if (previousSession) {
676
+ await activateSession(previousSession.sessionId, previousSession.workspaceId);
677
+ }
475
678
  const message = e instanceof Error ? e.message : String(e);
476
679
  if (message !== "CREATE_SESSION_IN_FLIGHT") {
477
680
  setError(message);
@@ -484,6 +687,7 @@ export function useChatSessions(params) {
484
687
  setError,
485
688
  resetActiveProjection,
486
689
  createSessionAndPrimeTitleWatcherForHook,
690
+ activateSession,
487
691
  blocked,
488
692
  workspaceId,
489
693
  ]);
@@ -28,6 +28,7 @@ export function useComposer(params) {
28
28
  }, []);
29
29
  const [shortcutIndex, setShortcutIndex] = useState(0);
30
30
  const [dynamicCommands, setDynamicCommands] = useState([]);
31
+ const [commandCapabilities, setCommandCapabilities] = useState({ sddBingo: false });
31
32
  const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
32
33
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
33
34
  const [commandsEpoch, setCommandsEpoch] = useState(0);
@@ -74,6 +75,7 @@ export function useComposer(params) {
74
75
  commandsSessionRef.current = sessionId;
75
76
  commandsGenerationRef.current += 1;
76
77
  setDynamicCommands([]);
78
+ setCommandCapabilities({ sddBingo: false });
77
79
  setDynamicCommandsError(null);
78
80
  setDynamicCommandsLoading(false);
79
81
  }, [sessionId]);
@@ -122,6 +124,7 @@ export function useComposer(params) {
122
124
  if (!isCurrent())
123
125
  return;
124
126
  setDynamicCommands(items);
127
+ setCommandCapabilities({ sddBingo: body?.capabilities?.sddBingo === true });
125
128
  // Compact server fail-soft / partial-source warning; local Operator/Web still merge.
126
129
  const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
127
130
  setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
@@ -131,6 +134,7 @@ export function useComposer(params) {
131
134
  return;
132
135
  setDynamicCommandsError("动态命令加载失败");
133
136
  setDynamicCommands([]);
137
+ setCommandCapabilities({ sddBingo: false });
134
138
  }
135
139
  finally {
136
140
  if (isCurrent()) {
@@ -227,7 +231,7 @@ export function useComposer(params) {
227
231
  }, 400);
228
232
  return () => window.clearTimeout(timer);
229
233
  }, [origin, sessionId, input]);
230
- const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands), [dynamicCommands]);
234
+ const mergedCommands = useMemo(() => mergeSlashCommands(dynamicCommands, commandCapabilities), [dynamicCommands, commandCapabilities]);
231
235
  const filteredShortcuts = useMemo(() => input.startsWith("/")
232
236
  ? filterSlashCommands(mergedCommands, input)
233
237
  : [], [input, mergedCommands]);
@@ -32,6 +32,7 @@ import { ChatSessionStore } from "./chat/session-store.js";
32
32
  import { createChatEventStore, createChatOperationLinker, } from "./chat/chat-event-store.js";
33
33
  import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.js";
34
34
  import { ConsolePiRuntime } from "./chat/pi-runtime.js";
35
+ import { AimaxAuthReader, SddDataAlignmentService } from "./chat/sdd-data-alignment.js";
35
36
  import { UserQuestionRegistry } from "./chat/user-questions.js";
36
37
  import { TodoListRegistry } from "./chat/todos.js";
37
38
  import { GoalRegistry } from "./chat/goals.js";
@@ -253,6 +254,7 @@ export async function createWorkspaceContext(record, deps) {
253
254
  }
254
255
  },
255
256
  });
257
+ const sddDataAlignment = new SddDataAlignmentService(chatStore, new AimaxAuthReader(), deps.fetchImpl);
256
258
  const chatEventEpoch = randomBytes(12).toString("hex");
257
259
  const chatEvents = createChatEventStore({
258
260
  appData,
@@ -291,6 +293,7 @@ export async function createWorkspaceContext(record, deps) {
291
293
  questionRegistry,
292
294
  todoRegistry,
293
295
  goalRegistry,
296
+ sddDataAlignment,
294
297
  });
295
298
  let chatRouteDeps;
296
299
  const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
@@ -302,6 +305,7 @@ export async function createWorkspaceContext(record, deps) {
302
305
  questionRegistry,
303
306
  todoRegistry,
304
307
  goalRegistry,
308
+ sddDataAlignment,
305
309
  goalRoundDriver,
306
310
  operationLinker: chatOperationLinker,
307
311
  appData,
@@ -4256,21 +4256,26 @@ const headerIndex=lines.findIndex(line=>{const row=cells(line);return expectedHe
4256
4256
  if(headerIndex<0){process.stderr.write('invalid-module-index-header: require exact business ownership and path columns\\n');process.exit(2);}
4257
4257
  const dataRows=lines.slice(headerIndex+2).map(cells).filter(row=>row.length>=8&&row[0]&&row[0]!=='Module Stem');
4258
4258
  const allowedSplit=new Set(['explicit-user-layout','primary-business-resource','independent-business-resource','output-budget']);
4259
- const strictExpectedStems=new Set((strictLayout&&strictLayout.modules||[]).map(item=>item.stem));
4260
- const operationOwners=new Map();const declaredModules=[];let planRepairApplied=false;
4259
+ const operationOwners=new Map();const canonicalSeen=new Set();const declaredModules=[];let planRepairApplied=false;
4261
4260
  for(const row of dataRows){
4262
- const stem=norm(row[0]),resource=String(row[1]||'').trim(),operations=String(row[2]||'').split(';').map(value=>value.trim()).filter(Boolean),split=String(row[5]||'').trim();
4263
- if(strictLayout&&!strictExpectedStems.has(stem)){planRepairApplied=true;continue;}
4261
+ const rawStem=String(row[0]||'').trim(),resource=String(row[1]||'').trim(),operations=String(row[2]||'').split(';').map(value=>value.trim()).filter(Boolean),split=String(row[5]||'').trim();
4262
+ const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];
4263
+ if(mdMatches.length!==1){process.stderr.write('module-markdown-path-mismatch: '+(rawStem||'unknown')+'; require exactly one canonical Markdown Path\\n');process.exit(2);}
4264
+ let stem=norm(mdMatches[0][1]),markdownPath=mdMatches[0][0];
4265
+ if(strictLayout&&!strictLayout.modules.some(item=>item.markdownPath===markdownPath)){planRepairApplied=true;continue;}
4264
4266
  if(!resource){process.stderr.write('missing-business-resource: '+stem+'\\n');process.exit(2);}
4265
4267
  if(/^(?:response|resp|regression|positive|negative|boundary|error|combo|filter)(?:[_ -]|$)/i.test(resource)){process.stderr.write('test-purpose-business-resource: '+stem+' -> '+resource+'\\n');process.exit(2);}
4266
4268
  if(!allowedSplit.has(split)){process.stderr.write('invalid-module-split-reason: '+stem+' -> '+split+'\\n');process.exit(2);}
4267
4269
  if(operations.length===0){process.stderr.write('missing-owned-operation: '+stem+'\\n');process.exit(2);}
4268
- const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];let markdownPath=mdMatches[0]&&mdMatches[0][0];
4269
4270
  let pytestPath=String(row[7]||'').replace(/^\\.\\//,'').replace(/\\\\/g,'/');
4270
- if(mdMatches.length!==1||norm(mdMatches[0][1])!==stem){process.stderr.write('module-markdown-path-mismatch: '+stem+'\\n');process.exit(2);}
4271
4271
  if(!/^[A-Za-z0-9_./-]+\\.py$/.test(pytestPath)||pytestPath.includes('..')){process.stderr.write('invalid-module-pytest-path: '+stem+' -> '+pytestPath+'\\n');process.exit(2);}
4272
- const required=strictLayout&&strictLayout.modules.find(item=>item.stem===stem);
4272
+ const requiredCandidates=strictLayout?strictLayout.modules.filter(item=>item.markdownPath===markdownPath):[];
4273
+ if(strictLayout&&requiredCandidates.length!==1){process.stderr.write('MODULE_LAYOUT_CONFLICT: no unique required module for Markdown Path '+markdownPath+'\\n');process.exit(2);}
4274
+ const required=requiredCandidates[0];
4275
+ if(!required&&norm(rawStem)!==stem)planRepairApplied=true;
4273
4276
  if(required){
4277
+ stem=required.stem;
4278
+ if(norm(rawStem)!==stem)planRepairApplied=true;
4274
4279
  if(!required.markdownPath.startsWith(mdDir+'/')||!required.pytestPath.startsWith(scriptDir+'/')||required.markdownPath.includes('..')||required.pytestPath.includes('..')){process.stderr.write('MODULE_LAYOUT_CONFLICT: strict paths outside frozen roots for '+stem+'\\n');process.exit(2);}
4275
4280
  if(markdownPath!==required.markdownPath||pytestPath!==required.pytestPath||resource!==required.businessResource||JSON.stringify(operations)!==JSON.stringify(required.ownedOperations)||split!==required.splitReason){planRepairApplied=true;}
4276
4281
  markdownPath=required.markdownPath;pytestPath=required.pytestPath;
@@ -4280,17 +4285,19 @@ for(const row of dataRows){
4280
4285
  }
4281
4286
  }
4282
4287
  const item={stem,markdownPath,pytestPath,businessResource:required?required.businessResource:resource,ownedOperations:required?required.ownedOperations:operations,ownedRuleKeys:String(row[3]||'').split(';').map(value=>value.trim()).filter(Boolean),caseIds:String(row[4]||'').split(';').map(value=>value.trim()).filter(Boolean),splitReason:required?required.splitReason:split,...(required&&required.budgetProof?{budgetProof:required.budgetProof}:{})};
4288
+ if(canonicalSeen.has(item.stem)){process.stderr.write('duplicate-canonical-module-stem: '+item.stem+'; module identity must be unique\\n');process.exit(2);}
4289
+ canonicalSeen.add(item.stem);
4283
4290
  declaredModules.push(item);
4284
4291
  for(const operation of item.ownedOperations){const owners=operationOwners.get(operation)||[];owners.push({stem,split:item.splitReason});operationOwners.set(operation,owners);}
4285
4292
  }
4286
4293
  if(strictLayout){
4287
4294
  const missing=strictLayout.modules.filter(item=>!declaredModules.some(actual=>actual.stem===item.stem));
4288
4295
  if(missing.length){process.stderr.write('MODULE_LAYOUT_CONFLICT: plan missing required modules '+missing.map(item=>item.stem).join(',')+'; deterministic Plan-only repair cannot invent Rule/Case ownership\\n');process.exit(2);}
4289
- if(planRepairApplied){
4290
- const table=['| '+expectedHeader.join(' | ')+' |','|'+expectedHeader.map(()=>'---').join('|')+'|',...declaredModules.map(item=>'| '+[item.stem,item.businessResource,item.ownedOperations.join('; '),item.ownedRuleKeys.join('; '),item.caseIds.join('; '),item.splitReason,'['+item.stem+'](./'+item.stem+'.md) '+item.markdownPath,item.pytestPath].join(' | ')+' |')].join('\\n');
4291
- readme=[...allLines.slice(0,headings[0]+1),table,...allLines.slice(end)].join('\\n');
4292
- fs.writeFileSync(planPath,readme,'utf8');
4293
- }
4296
+ }
4297
+ if(planRepairApplied){
4298
+ const table=['| '+expectedHeader.join(' | ')+' |','|'+expectedHeader.map(()=>'---').join('|')+'|',...declaredModules.map(item=>'| '+[item.stem,item.businessResource,item.ownedOperations.join('; '),item.ownedRuleKeys.join('; '),item.caseIds.join('; '),item.splitReason,'['+item.stem+'](./'+item.stem+'.md) '+item.markdownPath,item.pytestPath].join(' | ')+' |')].join('\\n');
4299
+ readme=[...allLines.slice(0,headings[0]+1),table,...allLines.slice(end)].join('\\n');
4300
+ fs.writeFileSync(planPath,readme,'utf8');
4294
4301
  }
4295
4302
  for(const [operation,owners] of operationOwners){if(owners.length>1&&!owners.every(owner=>owner.split==='explicit-user-layout'||owner.split==='output-budget')){process.stderr.write('overlapping-operation-modules: '+operation+' -> '+owners.map(owner=>owner.stem).join(',')+'; merge by business resource or use an authoritative explicit-user-layout\\n');process.exit(2);}}
4296
4303
  if(!strictLayout){
@@ -4302,8 +4309,8 @@ if(!strictLayout){
4302
4309
  }
4303
4310
  const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
4304
4311
  if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
4305
- const seen=new Set();const modules=[];
4306
- for(const item of declaredModules){if(valid(item.stem)&&!seen.has(item.stem)){seen.add(item.stem);modules.push(item);}}
4312
+ const modules=[];
4313
+ for(const item of declaredModules){if(valid(item.stem))modules.push(item);}
4307
4314
  if(modules.length===0){process.stderr.write('empty-module-index: require at least one stable business module\\n');process.exit(2);}
4308
4315
  if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
4309
4316
  const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
@@ -4899,7 +4906,7 @@ async function buildBackendTestHybridDag(sources) {
4899
4906
  "Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
4900
4907
  "Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
4901
4908
  "For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
4902
- "Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden.",
4909
+ "Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden. The Module Stem cell must contain only the plain filename stem; never put Markdown link syntax or a path in that cell.",
4903
4910
  ...(taskConfig.backendTest?.moduleLayout
4904
4911
  ? [
4905
4912
  "A strict task-contract `backendTest.moduleLayout` is bound and is authoritative over model-derived layout. Reproduce every stem, businessResource, ownedOperations, splitReason, markdownPath and pytestPath exactly; do not add, omit, rename or reorder physical modules. The downstream preflight compares exact path sets and may perform at most one deterministic Plan-only pruning/path normalization; it cannot invent missing Rule/Case ownership.",