@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10

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 (208) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +106 -109
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/executors/dag-pi-executor.js +69 -7
  9. package/dist/executors/pi-executor.js +4 -2
  10. package/dist/executors/pi-sdk-executor.js +46 -1
  11. package/dist/executors/pi-writer-tool-policy.js +57 -1
  12. package/dist/executors/shell-executor.js +43 -3
  13. package/dist/executors/workspace-write-snapshot.js +68 -0
  14. package/dist/infrastructure/harness/atomic-write.js +23 -0
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +10 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/source-prepare/completeness.js +188 -0
  27. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  28. package/dist/task/source-prepare/index.js +3 -0
  29. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  30. package/dist/task/source-prepare/ledger-review.js +214 -0
  31. package/dist/task/source-prepare/ledger.js +545 -0
  32. package/dist/task/source-prepare/prepare.js +262 -1
  33. package/dist/task/source-prepare/semantic-intake.js +19 -3
  34. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  35. package/dist/task/source-prepare/types.js +22 -0
  36. package/dist/worker/cli.js +12 -26
  37. package/dist/worker/console/chat/chat-event-store.js +75 -5
  38. package/dist/worker/console/chat/context-panel.js +7 -0
  39. package/dist/worker/console/chat/deferred-turn.js +12 -0
  40. package/dist/worker/console/chat/pi-runtime.js +331 -18
  41. package/dist/worker/console/chat/provider-error.js +98 -0
  42. package/dist/worker/console/chat/routes.js +389 -56
  43. package/dist/worker/console/chat/session-stats.js +179 -0
  44. package/dist/worker/console/chat/session-store.js +93 -63
  45. package/dist/worker/console/chat/todos.js +115 -0
  46. package/dist/worker/console/chat/tool-preview.js +42 -0
  47. package/dist/worker/console/chat/turn-process.js +20 -55
  48. package/dist/worker/console/chat/user-questions.js +266 -0
  49. package/dist/worker/console/chat/workspace-landing.js +79 -0
  50. package/dist/worker/console/console-handoff.js +82 -0
  51. package/dist/worker/console/console-update-and-init.js +115 -0
  52. package/dist/worker/console/console-update-runtime.js +84 -0
  53. package/dist/worker/console/draft-store.js +49 -0
  54. package/dist/worker/console/native-directory-picker.js +13 -0
  55. package/dist/worker/console/operation-store.js +1 -1
  56. package/dist/worker/console/operator-actions.js +129 -4
  57. package/dist/worker/console/operator-surface-health.js +1 -0
  58. package/dist/worker/console/prd-intake-bridge.js +13 -0
  59. package/dist/worker/console/routes.js +312 -1
  60. package/dist/worker/console/security.js +4 -4
  61. package/dist/worker/console/server.js +583 -172
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  94. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  95. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/console-types.js +68 -4
  121. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  122. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  123. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  124. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  125. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  126. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  127. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  128. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  129. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  130. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  131. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  132. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  133. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  134. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  135. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  136. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  137. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  138. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  139. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  140. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  141. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  142. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  143. package/dist/worker/console/workspace-context.js +234 -0
  144. package/dist/worker/console/workspace-registry.js +214 -0
  145. package/dist/worker/observability/init-runtime-activity.js +26 -0
  146. package/dist/worker/observe/node-input.js +88 -2
  147. package/dist/worker/observe/routes.js +188 -0
  148. package/dist/worker/observe/server.js +4 -0
  149. package/dist/worker/observe/static/api.js +29 -9
  150. package/dist/worker/observe/static/app.js +18 -70
  151. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  152. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  153. package/dist/worker/observe/static/format.js +37 -0
  154. package/dist/worker/observe/static/index.html +6 -1
  155. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  156. package/dist/worker/observe/static/operator-chrome.css +145 -20
  157. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  158. package/dist/worker/observe/static/operator-chrome.js +330 -100
  159. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  160. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  161. package/dist/worker/observe/static/router.d.ts +46 -0
  162. package/dist/worker/observe/static/router.js +25 -1
  163. package/dist/worker/observe/static/state.d.ts +50 -0
  164. package/dist/worker/observe/static/state.js +174 -2
  165. package/dist/worker/observe/static/styles.css +357 -35
  166. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  167. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  168. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  169. package/dist/worker/observe/static/views/dag.js +6 -4
  170. package/dist/worker/observe/static/views/dashboard.js +6 -4
  171. package/dist/worker/observe/static/views/night.js +2 -2
  172. package/dist/worker/observe/static/views/pool.js +1 -0
  173. package/dist/worker/observe/static/views/run.js +3 -1
  174. package/dist/worker/observe/static/views/task.js +56 -1
  175. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  177. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  178. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  179. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  180. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  181. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  182. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  183. package/dist/workflows/dag/frontend-review-context.js +92 -10
  184. package/dist/workflows/dag/init-hybrid.js +238 -51
  185. package/dist/workflows/dag/interrupt-request.js +3 -3
  186. package/dist/workflows/dag/lifecycle.js +3 -3
  187. package/dist/workflows/dag/node-execution.js +45 -0
  188. package/dist/workflows/dag/types.js +62 -12
  189. package/docs/README.md +2 -0
  190. package/docs/architecture/evolution.md +45 -0
  191. package/docs/init-surface.manifest.json +1 -0
  192. package/docs/operations/local-development-environment.md +21 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +18 -0
  195. package/docs/templates/backend-test-dag.json +265 -237
  196. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  197. package/harness.json +3 -3
  198. package/package.json +3 -1
  199. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  200. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  201. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  203. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  204. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  205. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  206. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  207. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  208. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { parseAssistantContent } from "../../chat/assistant-content.js";
3
2
  import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
4
3
  import { hydrateSessionThread } from "../../chat/turn-process.js";
5
- import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
4
+ import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
6
5
  import { CHAT_SESSION_LIST_LIMIT } from "../chat-view-types.js";
7
6
  import { confirmationToken } from "./format.js";
7
+ import { upsertCompactionMessage } from "./compaction-message.js";
8
8
  import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
9
9
  import { SessionTitleWatcher } from "./session-title-watcher.js";
10
10
  export async function createSessionAndPrimeTitleWatcher(input) {
@@ -28,6 +28,9 @@ export async function createSessionAndPrimeTitleWatcher(input) {
28
28
  });
29
29
  if (!res.ok) {
30
30
  const body = (await res.json().catch(() => ({})));
31
+ if (isMutationGateFailureCode(body.error?.code)) {
32
+ throw new Error(describeMutationGateFailure(body.error.code));
33
+ }
31
34
  throw new Error(body.error?.message ?? `HTTP ${res.status}`);
32
35
  }
33
36
  const body = (await res.json());
@@ -36,6 +39,11 @@ export async function createSessionAndPrimeTitleWatcher(input) {
36
39
  const durable = body.session;
37
40
  const created = {
38
41
  sessionId: durable.sessionId,
42
+ ...(durable.workspaceId || input.options?.workspaceId
43
+ ? {
44
+ workspaceId: durable.workspaceId ?? input.options?.workspaceId,
45
+ }
46
+ : {}),
39
47
  ...runtimeSelectionFromRecord(durable),
40
48
  };
41
49
  const nextList = [
@@ -56,6 +64,9 @@ export async function createSessionAndPrimeTitleWatcher(input) {
56
64
  input.setCreatingSession?.(false);
57
65
  }
58
66
  }
67
+ function mergeCompactionMessages(messages, compactions) {
68
+ return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
69
+ }
59
70
  /** Session lifecycle: list / activate / create / delete / rename-archive /
60
71
  * compact, plus browser-refresh recovery from the durable session store. */
61
72
  export function useChatSessions(params) {
@@ -63,6 +74,21 @@ export function useChatSessions(params) {
63
74
  const { applyActivatedSession, resetForNewSession, setMessages } = thread;
64
75
  const { setInput, setPendingImages } = composer;
65
76
  const { setPreviews, setActivePreviewPath } = browser;
77
+ const { workspaceId, blocked } = params;
78
+ const workspaceQuery = workspaceId
79
+ ? `?workspaceId=${encodeURIComponent(workspaceId)}`
80
+ : "";
81
+ /** AC-002 (2026-08-26): session requests follow the session's DURABLE
82
+ * owning workspace, not the global active workspace. The sidebar list
83
+ * items carry workspaceId; once a session is activated its owner is kept
84
+ * on the session record itself. Switching the Operate/Inspect active
85
+ * workspace therefore never retargets an open session's requests. */
86
+ const sessionOwnerQuery = useCallback((ownerWorkspaceId) => {
87
+ const owner = ownerWorkspaceId?.trim() ||
88
+ refs.sessionRef.current?.workspaceId?.trim() ||
89
+ "";
90
+ return owner ? `?workspaceId=${encodeURIComponent(owner)}` : "";
91
+ }, [refs]);
66
92
  const [sessionList, setSessionList] = useState([]);
67
93
  const sessionListRef = useRef([]);
68
94
  sessionListRef.current = sessionList;
@@ -76,11 +102,15 @@ export function useChatSessions(params) {
76
102
  // state. This prevents slow responses from an older selection overwriting a
77
103
  // newer user choice.
78
104
  const sessionSelectionVersionRef = useRef(0);
79
- const resetActiveProjection = useCallback(() => {
80
- // M4.5/M5.2: selection is a hard ownership boundary. Abort every request
81
- // owned by the old projection, then invalidate its async callbacks.
105
+ const resetActiveProjection = useCallback((options) => {
106
+ // M4.5/M5.2: selection is a hard ownership boundary for THIS projection.
107
+ // Multi-workspace (AC-002): keepStreams=true (workspace switch) aborts
108
+ // only projection-local requests and leaves background SSE streams of
109
+ // other sessions/workspaces running.
110
+ const keepStreams = options?.keepStreams === true;
82
111
  refs.sessionGenerationRef.current += 1;
83
- refs.abortRef.current?.abort();
112
+ if (!keepStreams)
113
+ refs.abortRef.current?.abort();
84
114
  refs.reconnectAbortRef.current?.abort();
85
115
  for (const controller of refs.sessionAbortControllersRef.current.values())
86
116
  controller.abort();
@@ -114,43 +144,36 @@ export function useChatSessions(params) {
114
144
  setAutoScroll,
115
145
  ]);
116
146
  const refreshSessionList = useCallback(async () => {
147
+ if (blocked)
148
+ return; // AC-R5-002: fail-closed, zero session requests
117
149
  try {
118
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions`, {
150
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions${workspaceQuery}`, {
119
151
  credentials: "include",
120
152
  });
121
153
  if (!res.ok)
122
154
  return;
123
- const sessions = (await res.json()).sessions ??
124
- [];
155
+ const sessions = (await res.json())
156
+ .sessions ?? [];
157
+ // Multi-workspace (AC-002): list only this workspace's sessions when a
158
+ // scope is active; unscoped (legacy) keeps the full list.
159
+ const scoped = workspaceId
160
+ ? sessions.filter((item) => !item.workspaceId || item.workspaceId === workspaceId)
161
+ : sessions;
125
162
  // API returns newest-first; keep only the recent window in the sidebar.
126
- const keep = sessions.slice(0, CHAT_SESSION_LIST_LIMIT);
127
- const excess = sessions.slice(CHAT_SESSION_LIST_LIMIT);
163
+ const keep = scoped.slice(0, CHAT_SESSION_LIST_LIMIT);
128
164
  setSessionList(keep);
129
- if (excess.length === 0)
130
- return;
131
- await Promise.all(excess.map((item) => fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(item.sessionId)}`, {
132
- method: "DELETE",
133
- credentials: "include",
134
- headers: {
135
- "x-loop-console-confirmation": confirmationToken(),
136
- },
137
- }).catch(() => undefined)));
138
- const activeId = refs.sessionRef.current?.sessionId;
139
- if (activeId && excess.some((item) => item.sessionId === activeId)) {
140
- sessionSelectionVersionRef.current += 1;
141
- refs.sessionRef.current = null;
142
- resetActiveProjection();
143
- setSession(null);
144
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
145
- }
146
165
  }
147
166
  catch {
148
167
  /* Chat remains usable without the sidebar index. */
149
168
  }
150
- }, [origin, refs, resetActiveProjection, setSession]);
169
+ }, [origin, workspaceQuery, workspaceId, blocked]);
151
170
  const deleteSession = useCallback(async (sessionId) => {
152
171
  try {
153
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`, {
172
+ // AC-002: delete follows the session's durable owner, not the
173
+ // global active workspace.
174
+ const ownerQuery = sessionOwnerQuery(sessionListRef.current.find((item) => item.sessionId === sessionId)
175
+ ?.workspaceId ?? null);
176
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${ownerQuery}`, {
154
177
  method: "DELETE",
155
178
  credentials: "include",
156
179
  headers: {
@@ -163,11 +186,12 @@ export function useChatSessions(params) {
163
186
  }
164
187
  titleWatcherRef.current?.cancel(sessionId);
165
188
  if (refs.sessionRef.current?.sessionId === sessionId) {
189
+ const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
166
190
  sessionSelectionVersionRef.current += 1;
167
191
  refs.sessionRef.current = null;
168
192
  resetActiveProjection();
169
193
  setSession(null);
170
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
194
+ forgetActiveChatSession(window.localStorage, activeWorkspaceId, sessionId);
171
195
  }
172
196
  await refreshSessionList();
173
197
  return true;
@@ -183,11 +207,17 @@ export function useChatSessions(params) {
183
207
  resetActiveProjection,
184
208
  refreshSessionList,
185
209
  setError,
210
+ sessionOwnerQuery,
211
+ workspaceId,
186
212
  ]);
187
- const activateSession = useCallback(async (sessionId) => {
213
+ const activateSession = useCallback(async (sessionId, ownerWorkspaceId) => {
188
214
  const selectionVersion = ++sessionSelectionVersionRef.current;
215
+ // AC-002: activation targets the session's durable owning workspace —
216
+ // never the global active workspace (no cross-workspace leak when
217
+ // Operate/Inspect switch their scope while a session stays open).
218
+ const ownerQuery = sessionOwnerQuery(ownerWorkspaceId);
189
219
  try {
190
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`, { credentials: "include" });
220
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${ownerQuery}`, { credentials: "include" });
191
221
  const body = (await res.json());
192
222
  if (!res.ok || !body.session)
193
223
  throw new Error(body.error?.message ?? "会话不可用");
@@ -195,12 +225,23 @@ export function useChatSessions(params) {
195
225
  return;
196
226
  const next = {
197
227
  sessionId: body.session.sessionId,
228
+ ...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
198
229
  ...runtimeSelectionFromRecord(body.session),
199
230
  };
200
- resetActiveProjection();
231
+ // AC-002: session switch (same or cross workspace) never aborts other
232
+ // sessions' background request controllers; the single /events owner is
233
+ // re-bound by useChatStream's session effect.
234
+ resetActiveProjection({ keepStreams: true });
201
235
  refs.sessionRef.current = next;
202
- refs.streamingRef.current = Boolean(body.session.activeTurnId);
203
236
  refs.activeTurnIdRef.current = body.session.activeTurnId ?? null;
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;
204
245
  refs.seenEventIdsRef.current = new Set();
205
246
  refs.turnAssistantIdsRef.current = new Map();
206
247
  refs.sealedAssistantIdsRef.current = new Set();
@@ -214,15 +255,21 @@ export function useChatSessions(params) {
214
255
  setPreviews([]);
215
256
  setInput(body.session.composerDraft ?? "");
216
257
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
258
+ hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
217
259
  applyActivatedSession({
218
260
  messages: hydrated.messages,
219
261
  toolCalls: hydrated.toolCalls,
220
- streaming: Boolean(body.session.activeTurnId),
262
+ sessionStats: body.sessionStats,
263
+ usage: body.usage,
264
+ streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
221
265
  turnPhase: body.session.turnPhase ??
222
266
  (body.session.activeTurnId ? "running" : "idle"),
223
267
  deferredTurn: body.session.deferredTurn ?? null,
268
+ deferredTurns: body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []),
269
+ pendingQuestion: body.session.pendingQuestion ?? null,
270
+ todos: body.session.todos ?? [],
224
271
  });
225
- window.localStorage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, sessionId);
272
+ rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
226
273
  setActivePreviewPath(null);
227
274
  }
228
275
  catch (error) {
@@ -241,6 +288,8 @@ export function useChatSessions(params) {
241
288
  resetActiveProjection,
242
289
  setActivePreviewPath,
243
290
  setError,
291
+ sessionOwnerQuery,
292
+ workspaceId,
244
293
  ]);
245
294
  useEffect(() => {
246
295
  void refreshSessionList();
@@ -271,28 +320,45 @@ export function useChatSessions(params) {
271
320
  .map((item) => item.sessionId));
272
321
  }, [sessionList]);
273
322
  useEffect(() => () => titleWatcherRef.current?.dispose(), []);
274
- // Browser refresh recovery: retain only the durable session id locally, then
275
- // 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.
276
325
  useEffect(() => {
277
326
  let cancelled = false;
278
- const savedSessionId = window.localStorage.getItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
279
- if (!savedSessionId)
280
- return;
327
+ if (blocked)
328
+ return; // AC-R5-002: unknown workspace — no restore GET
281
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
+ }
282
342
  void (async () => {
283
343
  try {
284
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}`, { credentials: "include" });
344
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(savedSessionId)}${workspaceQuery}`, { credentials: "include" });
285
345
  if (!res.ok) {
286
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
346
+ forgetActiveChatSession(window.localStorage, workspaceId, savedSessionId);
287
347
  return;
288
348
  }
289
349
  const body = (await res.json());
290
350
  if (cancelled ||
351
+ blocked ||
291
352
  selectionVersion !== sessionSelectionVersionRef.current ||
292
353
  !body.session)
293
354
  return;
294
355
  const restored = {
295
356
  sessionId: body.session.sessionId,
357
+ ...(body.session.workspaceId
358
+ ? { workspaceId: body.session.workspaceId }
359
+ : workspaceId
360
+ ? { workspaceId }
361
+ : {}),
296
362
  ...runtimeSelectionFromRecord(body.session),
297
363
  };
298
364
  refs.sessionGenerationRef.current += 1;
@@ -302,25 +368,37 @@ export function useChatSessions(params) {
302
368
  // in the event ring and must replay once. applySseBlock dedups by
303
369
  // eventId and marks replay so usage/unread/sound do not double-count.
304
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;
305
379
  refs.seenEventIdsRef.current = new Set();
306
380
  refs.turnAssistantIdsRef.current = new Map();
307
381
  refs.sealedAssistantIdsRef.current = new Set();
308
382
  refs.settledTurnIdsRef.current = new Set();
309
383
  refs.lastEventIdRef.current = null;
310
384
  setSession(restored);
385
+ rememberActiveChatSession(window.localStorage, restored.workspaceId ?? workspaceId, restored.sessionId);
311
386
  setInput(body.session.composerDraft ?? "");
312
- setMessages((body.session.messages ?? [])
313
- .filter((message) => message.role === "user" || message.role === "assistant")
314
- .map((message) => {
315
- const content = parseAssistantContent(message.content);
316
- return {
317
- id: message.id,
318
- role: message.role,
319
- text: message.text,
320
- createdAt: Date.parse(message.at) || Date.now(),
321
- ...(content && content.length > 0 ? { content } : {}),
322
- };
323
- }));
387
+ const hydrated = hydrateSessionThread(body.session.messages ?? []);
388
+ hydrated.messages = mergeCompactionMessages(hydrated.messages, body.compactions);
389
+ applyActivatedSession({
390
+ messages: hydrated.messages,
391
+ toolCalls: hydrated.toolCalls,
392
+ sessionStats: body.sessionStats,
393
+ usage: body.usage,
394
+ streaming: visuallyStopped ? false : Boolean(body.session.activeTurnId),
395
+ turnPhase: body.session.turnPhase ??
396
+ (body.session.activeTurnId ? "running" : "idle"),
397
+ deferredTurn: body.session.deferredTurn ?? null,
398
+ deferredTurns: body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []),
399
+ pendingQuestion: body.session.pendingQuestion ?? null,
400
+ todos: body.session.todos ?? [],
401
+ });
324
402
  }
325
403
  catch {
326
404
  // Recovery is best-effort; a new session remains available.
@@ -329,35 +407,58 @@ export function useChatSessions(params) {
329
407
  return () => {
330
408
  cancelled = true;
331
409
  };
332
- }, [origin, refs, setSession, setInput, setMessages]);
410
+ }, [
411
+ origin,
412
+ workspaceQuery,
413
+ refs,
414
+ setSession,
415
+ setInput,
416
+ applyActivatedSession,
417
+ blocked,
418
+ resetActiveProjection,
419
+ workspaceId,
420
+ ]);
333
421
  // The sole production create path delegates to the exported orchestration
334
422
  // helper so its durable commit order is directly traceable and testable.
335
- const createSessionAndPrimeTitleWatcherForHook = useCallback((options) => createSessionAndPrimeTitleWatcher({
336
- origin,
337
- sessionListRef,
338
- setSessionList,
339
- titleWatcher: titleWatcherRef.current,
340
- refreshSessionList,
341
- options,
342
- setCreatingSession,
343
- creatingRef: creatingSessionRef,
344
- }), [origin, refreshSessionList]);
423
+ // AC-003 (2026-08-26): session creation REQUIRES a known workspaceId —
424
+ // either explicit (sidebar create-in-workspace) or the current valid
425
+ // selection. No scope → fail closed client-side (the server would reject
426
+ // with WORKSPACE_REQUIRED anyway); never a default-workspace fallback.
427
+ const createSessionAndPrimeTitleWatcherForHook = useCallback((options) => {
428
+ const scope = options?.workspaceId?.trim() ?? workspaceId?.trim();
429
+ if (!scope) {
430
+ setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
431
+ return Promise.reject(new Error("WORKSPACE_REQUIRED"));
432
+ }
433
+ return createSessionAndPrimeTitleWatcher({
434
+ origin,
435
+ sessionListRef,
436
+ setSessionList,
437
+ titleWatcher: titleWatcherRef.current,
438
+ refreshSessionList,
439
+ options: { ...options, workspaceId: scope },
440
+ setCreatingSession,
441
+ creatingRef: creatingSessionRef,
442
+ });
443
+ }, [origin, refreshSessionList, workspaceId, setError]);
345
444
  const createSession = useCallback(async (options) => {
346
445
  // Suppress concurrent create from any entry (sidebar / Landing / chip).
347
446
  if (creatingSessionRef.current)
348
447
  return null;
448
+ if (blocked)
449
+ return null; // AC-R5-002: unknown workspace — no POST
349
450
  const selectionVersion = ++sessionSelectionVersionRef.current;
350
451
  setError(null);
351
452
  try {
352
453
  const created = await createSessionAndPrimeTitleWatcherForHook(options);
353
454
  if (selectionVersion !== sessionSelectionVersionRef.current)
354
455
  return null;
355
- resetActiveProjection();
456
+ resetActiveProjection({ keepStreams: true });
356
457
  // Set ref synchronously BEFORE setState so a chained
357
458
  // createSession().then(sendPrompt) reads the new session even
358
459
  // though sendPrompt's closure captured the stale (null) value.
359
460
  refs.sessionRef.current = created;
360
- window.localStorage.setItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, created.sessionId);
461
+ rememberActiveChatSession(window.localStorage, created.workspaceId ?? options?.workspaceId ?? workspaceId, created.sessionId);
361
462
  setSession(created);
362
463
  return created;
363
464
  }
@@ -376,6 +477,8 @@ export function useChatSessions(params) {
376
477
  setError,
377
478
  resetActiveProjection,
378
479
  createSessionAndPrimeTitleWatcherForHook,
480
+ blocked,
481
+ workspaceId,
379
482
  ]);
380
483
  // Phase-1: fork / in-session branch / mainline switch UI deferred; API routes remain.
381
484
  /** Single compact helper for toolbar + `/compact <instructions>` (AC-1). */
@@ -392,7 +495,8 @@ export function useChatSessions(params) {
392
495
  if (trimmed) {
393
496
  headers["content-type"] = "application/json";
394
497
  }
395
- const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/compact`, {
498
+ // AC-002: compaction follows the session's durable owner.
499
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(current.sessionId)}/compact${sessionOwnerQuery()}`, {
396
500
  method: "POST",
397
501
  credentials: "include",
398
502
  headers,
@@ -421,22 +525,8 @@ export function useChatSessions(params) {
421
525
  // pi-web parity: reload the compacted session, then leave a distinct
422
526
  // compaction summary card in the timeline plus a transient toast.
423
527
  const summary = body.snapshot?.summary?.trim();
424
- const tokensBefore = body.snapshot?.tokensBefore;
425
528
  const summaryText = summary || "历史上下文已精简,关键决策与当前任务已保留。";
426
529
  await activateSession(current.sessionId);
427
- setMessages((messages) => [
428
- ...messages,
429
- {
430
- id: `compaction-${Date.now()}`,
431
- role: "assistant",
432
- kind: "compaction",
433
- text: summaryText,
434
- createdAt: Date.now(),
435
- ...(tokensBefore
436
- ? { compaction: { tokensBefore } }
437
- : {}),
438
- },
439
- ]);
440
530
  setNotice({
441
531
  id: Date.now(),
442
532
  message: `已压缩上下文:${summaryText.slice(0, 72)}${summaryText.length > 72 ? "…" : ""}`,
@@ -453,12 +543,14 @@ export function useChatSessions(params) {
453
543
  refs,
454
544
  setError,
455
545
  setNotice,
456
- setMessages,
457
546
  onCompactionChange,
458
547
  activateSession,
548
+ sessionOwnerQuery,
459
549
  ]);
460
550
  const updateSession = async (sessionId, patch) => {
461
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`, {
551
+ // AC-002: rename/archive follows the session's durable owner.
552
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}${sessionOwnerQuery(sessionListRef.current.find((item) => item.sessionId === sessionId)
553
+ ?.workspaceId ?? null)}`, {
462
554
  method: "PATCH",
463
555
  credentials: "include",
464
556
  headers: {
@@ -477,11 +569,13 @@ export function useChatSessions(params) {
477
569
  }
478
570
  if (patch.state === "archived" &&
479
571
  refs.sessionRef.current?.sessionId === sessionId) {
572
+ const activeWorkspaceId = refs.sessionRef.current.workspaceId ?? workspaceId;
573
+ const activeSessionId = refs.sessionRef.current.sessionId;
480
574
  sessionSelectionVersionRef.current += 1;
481
575
  refs.sessionRef.current = null;
482
576
  resetActiveProjection();
483
577
  setSession(null);
484
- window.localStorage.removeItem(ACTIVE_CHAT_SESSION_STORAGE_KEY);
578
+ forgetActiveChatSession(window.localStorage, activeWorkspaceId, activeSessionId);
485
579
  }
486
580
  };
487
581
  return {