@tea-agent/loop-agent 0.40.0-next.9 → 0.40.0

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 (143) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +21 -21
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +9 -5
  7. package/dist/cli/program.js +38 -3
  8. package/dist/commands/client-recovery.js +8 -36
  9. package/dist/commands/init-model-catalog.js +22 -0
  10. package/dist/commands/task-advance.js +23 -1
  11. package/dist/commands/task-source-prepare.js +5 -0
  12. package/dist/commands/task-status.js +1 -0
  13. package/dist/executors/pi-sdk-executor.js +33 -5
  14. package/dist/executors/shell-executor.js +94 -20
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/shared/operator/capabilities.js +9 -1
  17. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  18. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  19. package/dist/shared/pi-context-pressure/env.js +58 -0
  20. package/dist/shared/pi-context-pressure/extension.js +100 -0
  21. package/dist/shared/pi-context-pressure/index.js +7 -0
  22. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  23. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  24. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  25. package/dist/worker/console/chat/assistant-content.js +7 -0
  26. package/dist/worker/console/chat/chat-event-store.js +82 -11
  27. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  28. package/dist/worker/console/chat/goals.js +300 -0
  29. package/dist/worker/console/chat/pi-runtime.js +285 -8
  30. package/dist/worker/console/chat/provider-error.js +2 -1
  31. package/dist/worker/console/chat/repo-browser.js +94 -12
  32. package/dist/worker/console/chat/routes.js +495 -30
  33. package/dist/worker/console/chat/session-catalog.js +48 -0
  34. package/dist/worker/console/chat/session-stats.js +96 -0
  35. package/dist/worker/console/chat/session-store.js +27 -18
  36. package/dist/worker/console/chat/shortcuts.js +7 -0
  37. package/dist/worker/console/chat/sift-bridge.js +1 -0
  38. package/dist/worker/console/chat/turn-process.js +42 -5
  39. package/dist/worker/console/dag-execution-receipt.js +20 -2
  40. package/dist/worker/console/operator-actions.js +20 -5
  41. package/dist/worker/console/operator-selection.js +6 -2
  42. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  43. package/dist/worker/console/pi-plugins.js +179 -0
  44. package/dist/worker/console/routes.js +246 -6
  45. package/dist/worker/console/server.js +173 -42
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-M25fNMTE.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
  78. package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
  79. package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-CG8D9fuL.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-BuB6P2rX.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-nniBEGv2.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  105. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  106. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  107. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  108. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  109. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  110. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  111. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  112. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  113. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  114. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  115. package/dist/worker/console/workspace-context.js +28 -3
  116. package/dist/worker/console/workspace-registry.js +333 -60
  117. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  118. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  119. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  120. package/dist/worker/observe/static/operator-chrome.css +23 -4
  121. package/dist/worker/observe/static/operator-chrome.js +7 -2
  122. package/dist/worker/observe/static/styles.css +87 -25
  123. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  124. package/dist/worker/observe/static/views/dags.js +7 -5
  125. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  127. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  128. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  129. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  130. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  131. package/dist/workflows/dag/init-hybrid.js +5 -4
  132. package/docs/architecture/runtime-boundaries.md +2 -1
  133. package/docs/templates/backend-test-dag.json +6 -5
  134. package/docs/templates/init-managed-agents.md +1 -1
  135. package/package.json +3 -2
  136. package/skills/loop-agent/references/command-reference.md +15 -1
  137. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  138. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  139. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  140. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  141. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  142. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  143. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
@@ -4,7 +4,7 @@ import { describeMutationGateFailure, isMutationGateFailureCode, } from "../oper
4
4
  /**
5
5
  * Multi-workspace registry state (2026-08-23, AC-001/AC-003).
6
6
  *
7
- * Consumes the process-level GET/POST/DELETE /api/operator/v1/workspaces
7
+ * Consumes the process-level GET/POST/PATCH/DELETE /api/operator/v1/workspaces
8
8
  * surface. POST/DELETE pass the Console mutation-gate confirmation token.
9
9
  * The selected workspaceId is remembered in localStorage and validated
10
10
  * against the visible list. A stale local preference falls back to the first
@@ -64,6 +64,11 @@ export function createWorkspacesController(params = {}) {
64
64
  const storage = params.storage;
65
65
  const token = params.confirmationTokenFn ?? confirmationToken;
66
66
  const listeners = new Set();
67
+ let appliedRevision = 0;
68
+ let refreshQueued = false;
69
+ let refreshPromise = null;
70
+ let eventStream;
71
+ let disposed = false;
67
72
  let state = {
68
73
  workspaces: [],
69
74
  loading: true,
@@ -79,44 +84,90 @@ export function createWorkspacesController(params = {}) {
79
84
  state = { ...state, ...patch };
80
85
  emit();
81
86
  };
82
- const refresh = async () => {
87
+ const refresh = () => {
88
+ refreshQueued = true;
89
+ if (refreshPromise)
90
+ return refreshPromise;
91
+ refreshPromise = (async () => {
92
+ while (refreshQueued && !disposed) {
93
+ refreshQueued = false;
94
+ try {
95
+ const res = await fetchImpl(`${origin}/api/operator/v1/workspaces`, {
96
+ credentials: "include",
97
+ });
98
+ if (!res.ok)
99
+ continue;
100
+ const body = (await res.json());
101
+ const revision = Number.isSafeInteger(body.revision)
102
+ ? Math.max(0, body.revision ?? 0)
103
+ : undefined;
104
+ // Legacy/injected responses without revision remain usable. A
105
+ // revisioned response can never roll the projection backwards.
106
+ if (revision !== undefined && revision < appliedRevision)
107
+ continue;
108
+ if (revision !== undefined)
109
+ appliedRevision = revision;
110
+ const list = body.workspaces ?? [];
111
+ const pick = pickDefaultWorkspaceId(list, state.workspaceId);
112
+ if (pick.status === "unknown-workspace") {
113
+ setState({
114
+ workspaces: list,
115
+ unknownWorkspaceId: pick.workspaceId,
116
+ });
117
+ continue;
118
+ }
119
+ const nextId = pick.workspaceId;
120
+ const previousId = state.workspaceId;
121
+ setState({
122
+ workspaces: list,
123
+ unknownWorkspaceId: null,
124
+ ...(nextId !== previousId ? { workspaceId: nextId } : {}),
125
+ });
126
+ if (nextId !== previousId)
127
+ rememberWorkspaceId(nextId, storage);
128
+ }
129
+ catch {
130
+ /* Registry unreachable: keep last known list. */
131
+ }
132
+ finally {
133
+ if (state.loading)
134
+ setState({ loading: false });
135
+ }
136
+ }
137
+ })().finally(() => {
138
+ refreshPromise = null;
139
+ // An event can arrive between the loop condition and finally.
140
+ if (refreshQueued && !disposed)
141
+ void refresh();
142
+ });
143
+ return refreshPromise;
144
+ };
145
+ const consumeRegistryEvent = (event) => {
83
146
  try {
84
- const res = await fetchImpl(`${origin}/api/operator/v1/workspaces`, {
85
- credentials: "include",
86
- });
87
- if (!res.ok)
88
- return;
89
- const body = (await res.json());
90
- const list = body.workspaces ?? [];
91
- // A stale local preference falls back to the first server-visible
92
- // Workspace. Explicit route ids are validated separately and remain
93
- // fail-closed.
94
- const pick = pickDefaultWorkspaceId(list, state.workspaceId);
95
- if (pick.status === "unknown-workspace") {
96
- setState({
97
- workspaces: list,
98
- unknownWorkspaceId: pick.workspaceId,
99
- });
100
- return;
147
+ const revision = Number(JSON.parse(event.data).revision);
148
+ if (Number.isSafeInteger(revision) && revision > appliedRevision) {
149
+ void refresh();
101
150
  }
102
- const nextId = pick.workspaceId;
103
- const previousId = state.workspaceId;
104
- setState({
105
- workspaces: list,
106
- unknownWorkspaceId: null,
107
- ...(nextId !== previousId ? { workspaceId: nextId } : {}),
108
- });
109
- if (nextId !== previousId)
110
- rememberWorkspaceId(nextId, storage);
111
151
  }
112
152
  catch {
113
- /* Registry unreachable: keep last known list. */
114
- }
115
- finally {
116
- if (state.loading)
117
- setState({ loading: false });
153
+ /* Malformed frames are ignored; reconnect baseline remains authoritative. */
118
154
  }
119
155
  };
156
+ const startEventStream = () => {
157
+ if (eventStream || disposed)
158
+ return;
159
+ const factory = params.eventSourceFactory ??
160
+ (typeof EventSource !== "undefined"
161
+ ? (url) => new EventSource(url, {
162
+ withCredentials: true,
163
+ })
164
+ : undefined);
165
+ if (!factory)
166
+ return;
167
+ eventStream = factory(`${origin}/api/operator/v1/workspaces/events`);
168
+ eventStream.addEventListener("workspace_registry_ready", consumeRegistryEvent);
169
+ eventStream.addEventListener("workspace_registry_changed", consumeRegistryEvent);
170
+ };
120
171
  const addWorkspace = async (directoryPath, name) => {
121
172
  setState({ error: null });
122
173
  try {
@@ -214,12 +265,66 @@ export function createWorkspacesController(params = {}) {
214
265
  return { ok: false, error: display };
215
266
  }
216
267
  };
268
+ const reorderWorkspace = async (workspaceId, beforeWorkspaceId) => {
269
+ const previous = state.workspaces;
270
+ const source = previous.find((workspace) => workspace.workspaceId === workspaceId);
271
+ const anchor = beforeWorkspaceId
272
+ ? previous.find((workspace) => workspace.workspaceId === beforeWorkspaceId)
273
+ : undefined;
274
+ if (!source || (beforeWorkspaceId && !anchor)) {
275
+ return { ok: false, error: "工作区顺序已变化,请稍后重试。" };
276
+ }
277
+ const optimistic = previous.filter((workspace) => workspace.workspaceId !== workspaceId);
278
+ const insertAt = beforeWorkspaceId
279
+ ? optimistic.findIndex((workspace) => workspace.workspaceId === beforeWorkspaceId)
280
+ : optimistic.length;
281
+ optimistic.splice(insertAt, 0, source);
282
+ if (optimistic.every((workspace, index) => workspace.workspaceId === previous[index]?.workspaceId)) {
283
+ return { ok: true };
284
+ }
285
+ setState({ workspaces: optimistic, error: null });
286
+ try {
287
+ const res = await fetchImpl(`${origin}/api/operator/v1/workspaces/${encodeURIComponent(workspaceId)}`, {
288
+ method: "PATCH",
289
+ credentials: "include",
290
+ headers: {
291
+ "Content-Type": "application/json",
292
+ "x-loop-console-confirmation": token(),
293
+ },
294
+ body: JSON.stringify({ beforeWorkspaceId }),
295
+ });
296
+ const body = (await res.json().catch(() => ({})));
297
+ if (!res.ok) {
298
+ const message = isMutationGateFailureCode(body.error?.code)
299
+ ? describeMutationGateFailure(body.error?.code ?? "")
300
+ : (body.error?.message ?? `调整 Workspace 顺序失败(HTTP ${res.status})`);
301
+ setState({ workspaces: previous, error: message });
302
+ void refresh();
303
+ return { ok: false, error: message };
304
+ }
305
+ return { ok: true };
306
+ }
307
+ catch (error) {
308
+ const message = `调整 Workspace 顺序失败:${error instanceof Error ? error.message : String(error)}`;
309
+ setState({ workspaces: previous, error: message });
310
+ void refresh();
311
+ return { ok: false, error: message };
312
+ }
313
+ };
217
314
  const selectWorkspace = (next) => {
218
315
  if (next === state.workspaceId && !state.unknownWorkspaceId)
219
316
  return;
220
317
  setState({ workspaceId: next, unknownWorkspaceId: null });
221
318
  rememberWorkspaceId(next, storage);
222
319
  };
320
+ const dispose = () => {
321
+ if (disposed)
322
+ return;
323
+ disposed = true;
324
+ eventStream?.close();
325
+ eventStream = undefined;
326
+ listeners.clear();
327
+ };
223
328
  void refresh();
224
329
  return {
225
330
  get workspaces() {
@@ -241,10 +346,19 @@ export function createWorkspacesController(params = {}) {
241
346
  pickWorkspaceDirectory,
242
347
  addWorkspace,
243
348
  removeWorkspace,
349
+ reorderWorkspace,
244
350
  selectWorkspace,
351
+ dispose,
245
352
  subscribe(listener) {
246
353
  listeners.add(listener);
247
- return () => listeners.delete(listener);
354
+ startEventStream();
355
+ return () => {
356
+ listeners.delete(listener);
357
+ if (listeners.size === 0) {
358
+ eventStream?.close();
359
+ eventStream = undefined;
360
+ }
361
+ };
248
362
  },
249
363
  };
250
364
  }
@@ -294,6 +408,8 @@ export function useWorkspaces(params) {
294
408
  pickWorkspaceDirectory: controller.pickWorkspaceDirectory,
295
409
  addWorkspace: controller.addWorkspace,
296
410
  removeWorkspace: controller.removeWorkspace,
411
+ reorderWorkspace: controller.reorderWorkspace,
297
412
  selectWorkspace: controller.selectWorkspace,
413
+ dispose: controller.dispose,
298
414
  }), [state, controller]);
299
415
  }
@@ -12,6 +12,7 @@ export const OPERATE_TAB_IDS = [
12
12
  "night",
13
13
  "models",
14
14
  "settings",
15
+ "pi-plugins",
15
16
  ];
16
17
  export const LAST_OPERATE_TAB_STORAGE_KEY = "loop-console.operator-shell.last-operate-tab.v1";
17
18
  export const LAST_INSPECT_HASH_STORAGE_KEY = "loop-console.operator-shell.last-inspect-hash.v1";
@@ -44,7 +45,8 @@ function isOperateTab(value) {
44
45
  value === "recovery" ||
45
46
  value === "night" ||
46
47
  value === "models" ||
47
- value === "settings");
48
+ value === "settings" ||
49
+ value === "pi-plugins");
48
50
  }
49
51
  function readParam(query, key) {
50
52
  const value = query.get(key)?.trim();
@@ -33,6 +33,9 @@ import { createChatTurnExecutionRegistry } from "./chat/turn-execution-registry.
33
33
  import { ConsolePiRuntime } from "./chat/pi-runtime.js";
34
34
  import { UserQuestionRegistry } from "./chat/user-questions.js";
35
35
  import { TodoListRegistry } from "./chat/todos.js";
36
+ import { GoalRegistry } from "./chat/goals.js";
37
+ import { GoalRoundDriver } from "./chat/goal-round-driver.js";
38
+ import { driveGoalRound, recoverDeferredTurns } from "./chat/routes.js";
36
39
  import { createOperationEventStore, } from "./operation-sse.js";
37
40
  import { OperationStore } from "./operation-store.js";
38
41
  import { probePiReadiness, } from "./pi-readiness.js";
@@ -107,7 +110,16 @@ export async function createWorkspaceContext(record, deps) {
107
110
  fetchImpl: deps.fetchImpl,
108
111
  getConsoleOrigin: deps.getConsoleOrigin,
109
112
  };
110
- const chatStore = new ChatSessionStore(appData);
113
+ const chatStore = new ChatSessionStore(appData, {
114
+ onRecordChange: (sessionId, session) => {
115
+ if (session) {
116
+ deps.chatSessionCatalog?.upsert(session, record.workspaceId);
117
+ }
118
+ else {
119
+ deps.chatSessionCatalog?.remove(sessionId);
120
+ }
121
+ },
122
+ });
111
123
  const chatEventEpoch = randomBytes(12).toString("hex");
112
124
  const chatEvents = createChatEventStore({
113
125
  appData,
@@ -116,6 +128,7 @@ export async function createWorkspaceContext(record, deps) {
116
128
  const chatTurnExecutions = createChatTurnExecutionRegistry();
117
129
  const questionRegistry = new UserQuestionRegistry(path.join(appData.chats, "user-questions"));
118
130
  const todoRegistry = new TodoListRegistry();
131
+ const goalRegistry = new GoalRegistry(chatEvents);
119
132
  questionRegistry.subscribe((change) => {
120
133
  const question = change.question;
121
134
  chatEvents.append(question.sessionId, question.turnId ?? `${question.sessionId}:question`, {
@@ -144,21 +157,27 @@ export async function createWorkspaceContext(record, deps) {
144
157
  actionContext,
145
158
  questionRegistry,
146
159
  todoRegistry,
160
+ goalRegistry,
147
161
  });
148
- const chatRouteDeps = {
162
+ let chatRouteDeps;
163
+ const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
164
+ chatRouteDeps = {
149
165
  runtime: chatRuntime,
150
166
  store: chatStore,
151
167
  events: chatEvents,
152
168
  turnExecutions: chatTurnExecutions,
153
169
  questionRegistry,
154
170
  todoRegistry,
171
+ goalRegistry,
172
+ goalRoundDriver,
155
173
  operationLinker: chatOperationLinker,
156
174
  appData,
157
175
  bootToken: deps.bootToken,
158
176
  getConsoleOrigin: deps.getConsoleOrigin,
159
177
  allowNonLoopbackAccess: deps.allowNonLoopbackAccess,
178
+ attachSession: (sessionId) => registry.attachSession(record.workspaceId, sessionId),
179
+ detachSession: (sessionId) => registry.detachSession(record.workspaceId, sessionId),
160
180
  };
161
- const { recoverDeferredTurns } = await import("./chat/routes.js");
162
181
  const nightAuxTicker = new NightAuxTicker({ repoRoot, client });
163
182
  const observeRouteContext = createObserveRouteContext(repoRoot, {
164
183
  staticDir: deps.inspectStaticDir,
@@ -191,6 +210,12 @@ export async function createWorkspaceContext(record, deps) {
191
210
  if (disposed)
192
211
  return;
193
212
  disposed = true;
213
+ try {
214
+ goalRoundDriver.dispose();
215
+ }
216
+ catch {
217
+ // best-effort
218
+ }
194
219
  try {
195
220
  chatOperationLinker.dispose();
196
221
  }