@orion-agents/orion-code 0.2.2 → 0.3.1

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 (217) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +89 -14
  3. package/README.zh-CN.md +77 -14
  4. package/bin/orion +15 -4
  5. package/dist/cli.js +51 -172
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/model-command-handlers.d.ts +2 -2
  8. package/dist/commands/model-command-handlers.d.ts.map +1 -1
  9. package/dist/commands/model-command-handlers.js +113 -32
  10. package/dist/commands/model-command-handlers.js.map +1 -1
  11. package/dist/commands/session-command-handlers.d.ts.map +1 -1
  12. package/dist/commands/session-command-handlers.js +106 -15
  13. package/dist/commands/session-command-handlers.js.map +1 -1
  14. package/dist/commands/types.d.ts +13 -3
  15. package/dist/commands/types.d.ts.map +1 -1
  16. package/dist/commands/types.js +1 -1
  17. package/dist/commands/types.js.map +1 -1
  18. package/dist/core/tool-artifacts.d.ts +2 -0
  19. package/dist/core/tool-artifacts.d.ts.map +1 -1
  20. package/dist/core/tool-artifacts.js +27 -1
  21. package/dist/core/tool-artifacts.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/runtime/agent-loop.d.ts.map +1 -1
  25. package/dist/runtime/agent-loop.js +3 -3
  26. package/dist/runtime/agent-loop.js.map +1 -1
  27. package/dist/runtime/agent-runtime-controller.d.ts +32 -1
  28. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  29. package/dist/runtime/agent-runtime-controller.js +241 -27
  30. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  31. package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
  32. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  33. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  34. package/dist/runtime/agent-runtime-runner.d.ts +2 -1
  35. package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
  36. package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
  37. package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
  38. package/dist/runtime/durable-tool-receipt-reader.js +130 -0
  39. package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
  40. package/dist/runtime/legacy-thread-materializer.d.ts +31 -0
  41. package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
  42. package/dist/runtime/legacy-thread-materializer.js +71 -17
  43. package/dist/runtime/legacy-thread-materializer.js.map +1 -1
  44. package/dist/runtime/orion-runtime-v1.d.ts +2 -0
  45. package/dist/runtime/orion-runtime-v1.d.ts.map +1 -1
  46. package/dist/runtime/orion-runtime-v1.js +7 -1
  47. package/dist/runtime/orion-runtime-v1.js.map +1 -1
  48. package/dist/runtime/orion-session-runner.d.ts +7 -2
  49. package/dist/runtime/orion-session-runner.d.ts.map +1 -1
  50. package/dist/runtime/orion-session-runner.js +16 -6
  51. package/dist/runtime/orion-session-runner.js.map +1 -1
  52. package/dist/runtime/product-bootstrap.d.ts +22 -0
  53. package/dist/runtime/product-bootstrap.d.ts.map +1 -0
  54. package/dist/runtime/product-bootstrap.js +374 -0
  55. package/dist/runtime/product-bootstrap.js.map +1 -0
  56. package/dist/runtime/product-orion-runtime.d.ts +2 -1
  57. package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
  58. package/dist/runtime/product-orion-runtime.js +83 -13
  59. package/dist/runtime/product-orion-runtime.js.map +1 -1
  60. package/dist/runtime/protocol/runtime-protocol-v1.d.ts +1 -1
  61. package/dist/runtime/protocol/runtime-protocol-v1.js +1 -1
  62. package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
  63. package/dist/runtime/release-receipts.d.ts +66 -3
  64. package/dist/runtime/release-receipts.d.ts.map +1 -1
  65. package/dist/runtime/release-receipts.js +340 -7
  66. package/dist/runtime/release-receipts.js.map +1 -1
  67. package/dist/runtime/step-snapshot.d.ts.map +1 -1
  68. package/dist/runtime/step-snapshot.js +22 -2
  69. package/dist/runtime/step-snapshot.js.map +1 -1
  70. package/dist/runtime/subagents/runtime-integration.d.ts +1 -1
  71. package/dist/runtime/subagents/runtime-integration.d.ts.map +1 -1
  72. package/dist/runtime/subagents/runtime-integration.js +5 -2
  73. package/dist/runtime/subagents/runtime-integration.js.map +1 -1
  74. package/dist/runtime/thread-event-store.d.ts +80 -0
  75. package/dist/runtime/thread-event-store.d.ts.map +1 -1
  76. package/dist/runtime/thread-event-store.js +456 -22
  77. package/dist/runtime/thread-event-store.js.map +1 -1
  78. package/dist/runtime/thread-projection.d.ts +10 -0
  79. package/dist/runtime/thread-projection.d.ts.map +1 -1
  80. package/dist/runtime/thread-projection.js +40 -0
  81. package/dist/runtime/thread-projection.js.map +1 -1
  82. package/dist/runtime/thread-runtime.d.ts +3 -1
  83. package/dist/runtime/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/thread-runtime.js +6 -0
  85. package/dist/runtime/thread-runtime.js.map +1 -1
  86. package/dist/runtime/thread-session-index.d.ts +84 -0
  87. package/dist/runtime/thread-session-index.d.ts.map +1 -0
  88. package/dist/runtime/thread-session-index.js +503 -0
  89. package/dist/runtime/thread-session-index.js.map +1 -0
  90. package/dist/runtime/thread-session-view.d.ts +70 -9
  91. package/dist/runtime/thread-session-view.d.ts.map +1 -1
  92. package/dist/runtime/thread-session-view.js +200 -82
  93. package/dist/runtime/thread-session-view.js.map +1 -1
  94. package/dist/runtime/thread-ui-adapter.d.ts +2 -0
  95. package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
  96. package/dist/runtime/thread-ui-adapter.js +86 -4
  97. package/dist/runtime/thread-ui-adapter.js.map +1 -1
  98. package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
  99. package/dist/runtime/tool-detail-repository.js +23 -19
  100. package/dist/runtime/tool-detail-repository.js.map +1 -1
  101. package/dist/runtime/tool-receipt-validator.d.ts +21 -0
  102. package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
  103. package/dist/runtime/tool-receipt-validator.js +157 -0
  104. package/dist/runtime/tool-receipt-validator.js.map +1 -0
  105. package/dist/runtime/ui-events.d.ts +35 -0
  106. package/dist/runtime/ui-events.d.ts.map +1 -1
  107. package/dist/runtime/ui-events.js +1 -0
  108. package/dist/runtime/ui-events.js.map +1 -1
  109. package/dist/runtime/ui-view-model.d.ts +2 -0
  110. package/dist/runtime/ui-view-model.d.ts.map +1 -1
  111. package/dist/runtime/ui-view-model.js +7 -3
  112. package/dist/runtime/ui-view-model.js.map +1 -1
  113. package/dist/services/global-config.d.ts +25 -0
  114. package/dist/services/global-config.d.ts.map +1 -1
  115. package/dist/services/global-config.js +373 -5
  116. package/dist/services/global-config.js.map +1 -1
  117. package/dist/services/redaction.d.ts +4 -0
  118. package/dist/services/redaction.d.ts.map +1 -1
  119. package/dist/services/redaction.js +63 -0
  120. package/dist/services/redaction.js.map +1 -1
  121. package/dist/services/session-index.d.ts +16 -0
  122. package/dist/services/session-index.d.ts.map +1 -1
  123. package/dist/services/session-index.js +45 -36
  124. package/dist/services/session-index.js.map +1 -1
  125. package/dist/services/session-storage.d.ts +59 -0
  126. package/dist/services/session-storage.d.ts.map +1 -1
  127. package/dist/services/session-storage.js +306 -107
  128. package/dist/services/session-storage.js.map +1 -1
  129. package/dist/services/settings-coordinator.d.ts +174 -0
  130. package/dist/services/settings-coordinator.d.ts.map +1 -0
  131. package/dist/services/settings-coordinator.js +605 -0
  132. package/dist/services/settings-coordinator.js.map +1 -0
  133. package/dist/services/settings-document-repository.d.ts +138 -0
  134. package/dist/services/settings-document-repository.d.ts.map +1 -0
  135. package/dist/services/settings-document-repository.js +551 -0
  136. package/dist/services/settings-document-repository.js.map +1 -0
  137. package/dist/services/workspace-registry.d.ts +42 -0
  138. package/dist/services/workspace-registry.d.ts.map +1 -0
  139. package/dist/services/workspace-registry.js +222 -0
  140. package/dist/services/workspace-registry.js.map +1 -0
  141. package/dist/terminal-ui/launch.d.ts.map +1 -1
  142. package/dist/terminal-ui/launch.js +5 -0
  143. package/dist/terminal-ui/launch.js.map +1 -1
  144. package/dist/tui-ui/state.d.ts.map +1 -1
  145. package/dist/tui-ui/state.js +5 -0
  146. package/dist/tui-ui/state.js.map +1 -1
  147. package/dist/web/errors.d.ts +6 -0
  148. package/dist/web/errors.d.ts.map +1 -0
  149. package/dist/web/errors.js +13 -0
  150. package/dist/web/errors.js.map +1 -0
  151. package/dist/web/event-hub.d.ts +43 -0
  152. package/dist/web/event-hub.d.ts.map +1 -0
  153. package/dist/web/event-hub.js +323 -0
  154. package/dist/web/event-hub.js.map +1 -0
  155. package/dist/web/file-read-service.d.ts +55 -0
  156. package/dist/web/file-read-service.d.ts.map +1 -0
  157. package/dist/web/file-read-service.js +399 -0
  158. package/dist/web/file-read-service.js.map +1 -0
  159. package/dist/web/git-read-model-service.d.ts +77 -0
  160. package/dist/web/git-read-model-service.d.ts.map +1 -0
  161. package/dist/web/git-read-model-service.js +631 -0
  162. package/dist/web/git-read-model-service.js.map +1 -0
  163. package/dist/web/index.d.ts +6 -0
  164. package/dist/web/index.d.ts.map +1 -0
  165. package/dist/web/index.js +28 -0
  166. package/dist/web/index.js.map +1 -0
  167. package/dist/web/launch.d.ts +9 -0
  168. package/dist/web/launch.d.ts.map +1 -0
  169. package/dist/web/launch.js +64 -0
  170. package/dist/web/launch.js.map +1 -0
  171. package/dist/web/protocol.d.ts +408 -0
  172. package/dist/web/protocol.d.ts.map +1 -0
  173. package/dist/web/protocol.js +369 -0
  174. package/dist/web/protocol.js.map +1 -0
  175. package/dist/web/review-service.d.ts +38 -0
  176. package/dist/web/review-service.d.ts.map +1 -0
  177. package/dist/web/review-service.js +74 -0
  178. package/dist/web/review-service.js.map +1 -0
  179. package/dist/web/server.d.ts +20 -0
  180. package/dist/web/server.d.ts.map +1 -0
  181. package/dist/web/server.js +744 -0
  182. package/dist/web/server.js.map +1 -0
  183. package/dist/web/terminal-manager.d.ts +155 -0
  184. package/dist/web/terminal-manager.d.ts.map +1 -0
  185. package/dist/web/terminal-manager.js +728 -0
  186. package/dist/web/terminal-manager.js.map +1 -0
  187. package/dist/web/terminal-server.d.ts +8 -0
  188. package/dist/web/terminal-server.d.ts.map +1 -0
  189. package/dist/web/terminal-server.js +275 -0
  190. package/dist/web/terminal-server.js.map +1 -0
  191. package/dist/web/workbench-controller.d.ts +99 -0
  192. package/dist/web/workbench-controller.d.ts.map +1 -0
  193. package/dist/web/workbench-controller.js +1267 -0
  194. package/dist/web/workbench-controller.js.map +1 -0
  195. package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
  196. package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
  197. package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
  198. package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
  199. package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
  200. package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
  201. package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
  202. package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
  203. package/dist/web-client/index.html +16 -0
  204. package/docs/architecture/v0.3.0-web-api.yaml +1340 -0
  205. package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
  206. package/docs/migration/v0.2.2-to-v0.3.0-settings.md +114 -0
  207. package/docs/migration/v0.2.2-to-v0.3.0.md +55 -0
  208. package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
  209. package/docs/orion.example.json +9 -2
  210. package/docs/plan/v0.3.0-node-runtime-compatibility-plan.md +57 -0
  211. package/docs/plan/v0.3.0-settings-integration-plan.md +740 -0
  212. package/docs/plan/v0.3.0-web-workbench-plan.md +376 -0
  213. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
  214. package/docs/readme.md +22 -1
  215. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
  216. package/npm-shrinkwrap.json +1229 -45
  217. package/package.json +47 -11
@@ -0,0 +1,1267 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebWorkbenchController = exports.WebWorkbenchError = void 0;
4
+ exports.pageItems = pageItems;
5
+ exports.pageCollectionItems = pageCollectionItems;
6
+ const crypto_1 = require("crypto");
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const agent_runtime_controller_1 = require("../runtime/agent-runtime-controller");
10
+ const durable_tool_receipt_reader_1 = require("../runtime/durable-tool-receipt-reader");
11
+ const mcp_1 = require("../runtime/mcp");
12
+ const product_bootstrap_1 = require("../runtime/product-bootstrap");
13
+ const canonical_1 = require("../runtime/protocol/canonical");
14
+ const thread_session_index_1 = require("../runtime/thread-session-index");
15
+ const thread_session_view_1 = require("../runtime/thread-session-view");
16
+ const tool_detail_repository_1 = require("../runtime/tool-detail-repository");
17
+ const turn_commit_1 = require("../runtime/turn-commit");
18
+ const global_config_1 = require("../services/global-config");
19
+ const settings_coordinator_1 = require("../services/settings-coordinator");
20
+ const session_storage_1 = require("../services/session-storage");
21
+ const workspace_registry_1 = require("../services/workspace-registry");
22
+ const errors_1 = require("./errors");
23
+ const event_hub_1 = require("./event-hub");
24
+ const file_read_service_1 = require("./file-read-service");
25
+ const git_read_model_service_1 = require("./git-read-model-service");
26
+ const protocol_1 = require("./protocol");
27
+ const review_service_1 = require("./review-service");
28
+ const terminal_manager_1 = require("./terminal-manager");
29
+ var errors_2 = require("./errors");
30
+ Object.defineProperty(exports, "WebWorkbenchError", { enumerable: true, get: function () { return errors_2.WebWorkbenchError; } });
31
+ const SESSION_VIEW_CACHE_BYTES = 16 * 1024 * 1024;
32
+ const MAX_MUTATION_RESULTS = 4096;
33
+ /** Owns the sole runtime/controller pair exposed through the local Web host. */
34
+ class WebWorkbenchController {
35
+ constructor(options) {
36
+ this.mutationResults = new Map();
37
+ this.sessionViews = new Map();
38
+ this.sessionViewBytes = 0;
39
+ this.toolDetails = new tool_detail_repository_1.FileToolDetailRepository();
40
+ this.contextRevisionValue = (0, crypto_1.randomUUID)();
41
+ this.suppressContextEdges = 0;
42
+ this.latestStatus = 'Ready';
43
+ this.sessionTransition = false;
44
+ this.closed = false;
45
+ this.workspaceWatchers = [];
46
+ this.workspaceValue = canonicalDirectory(options.cwd);
47
+ this.eventHub = options.eventHub ?? new event_hub_1.WebEventHub();
48
+ this.workspaceRegistry = options.workspaceRegistry ?? new workspace_registry_1.WorkspaceRegistryV1();
49
+ this.terminalManager = new terminal_manager_1.TerminalManagerV1({
50
+ resolveWorkspace: workspaceId => this.workspaceRegistry.find(workspaceId)?.canonicalPath,
51
+ getActiveContext: () => ({
52
+ workspaceId: this.activeWorkspaceEntry().id,
53
+ contextRevision: this.contextRevisionValue,
54
+ }),
55
+ onWorkspaceMutationHint: workspaceId => {
56
+ if (workspaceId === this.activeWorkspaceEntry().id) {
57
+ this.scheduleWorkspaceResourceInvalidation('terminal-command');
58
+ }
59
+ },
60
+ });
61
+ this.createRuntime =
62
+ options.createRuntime ??
63
+ (cwd => (0, product_bootstrap_1.createProductUiRuntime)({
64
+ cwd,
65
+ shutdownReason: 'Orion Web Workbench shutdown',
66
+ onActiveSessionRuntime: (runtime, sessionId, activation) => this.observeThreadRuntime(runtime, sessionId, activation),
67
+ onSettingsInvalidated: event => {
68
+ if (this.suppressContextEdges > 0)
69
+ return;
70
+ this.eventHub.emit({
71
+ type: 'settings_invalidated',
72
+ revision: event.revision,
73
+ reason: event.reason,
74
+ state: event.state,
75
+ }, false);
76
+ },
77
+ }));
78
+ }
79
+ static async create(options) {
80
+ const workbench = new WebWorkbenchController(options);
81
+ workbench.initializeWorkspaceRegistry();
82
+ await workbench.installRuntime(workbench.workspaceValue);
83
+ return workbench;
84
+ }
85
+ get workspace() {
86
+ return this.workspaceValue;
87
+ }
88
+ get runtime() {
89
+ return this.runtimeValue;
90
+ }
91
+ get controller() {
92
+ return this.controllerValue;
93
+ }
94
+ get contextRevision() {
95
+ return this.contextRevisionValue;
96
+ }
97
+ bootstrap(nonce) {
98
+ return Object.freeze({
99
+ apiVersion: protocol_1.WEB_API_VERSION,
100
+ productVersion: this.runtimeValue.version,
101
+ nonce,
102
+ contextRevision: this.contextRevisionValue,
103
+ workspaceId: this.activeWorkspaceEntry().id,
104
+ workspace: this.workspaceValue,
105
+ configured: this.runtimeValue.isConfigured,
106
+ activeSessionId: this.runtimeValue.getSession()?.id ?? null,
107
+ settings: this.settings(),
108
+ capabilities: {
109
+ goal: true,
110
+ plan: true,
111
+ skills: true,
112
+ mcp: true,
113
+ diagnostics: true,
114
+ review: true,
115
+ files: true,
116
+ git: true,
117
+ terminal: this.terminalManager.available,
118
+ },
119
+ });
120
+ }
121
+ settings() {
122
+ this.assertSettingsAvailable();
123
+ const describe = this.runtimeValue.describeSettings;
124
+ if (!describe) {
125
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
126
+ }
127
+ try {
128
+ return describe();
129
+ }
130
+ catch (error) {
131
+ throw mapSettingsError(error);
132
+ }
133
+ }
134
+ listWorkspaces() {
135
+ const counts = new Map();
136
+ for (const session of (0, session_storage_1.listSessions)()) {
137
+ try {
138
+ const path = canonicalDirectory(session.projectPath);
139
+ counts.set(path, (counts.get(path) ?? 0) + 1);
140
+ }
141
+ catch {
142
+ // Stale projects remain in session history but are not selectable workspaces.
143
+ }
144
+ }
145
+ return Object.freeze(this.workspaceRegistry.list().map(entry => {
146
+ let available = false;
147
+ try {
148
+ available = canonicalDirectory(entry.canonicalPath) === entry.canonicalPath;
149
+ }
150
+ catch {
151
+ available = false;
152
+ }
153
+ return Object.freeze({
154
+ id: entry.id,
155
+ path: entry.canonicalPath,
156
+ label: entry.label || (0, path_1.basename)(entry.canonicalPath) || entry.canonicalPath,
157
+ active: entry.canonicalPath === this.workspaceValue,
158
+ available,
159
+ sessionCount: counts.get(entry.canonicalPath) ?? 0,
160
+ lastActivatedAt: entry.lastActivatedAt,
161
+ ...(entry.pinnedOrder !== undefined ? { pinnedOrder: entry.pinnedOrder } : {}),
162
+ });
163
+ }));
164
+ }
165
+ listWorkspaceSessions(workspaceId) {
166
+ const entry = this.workspaceRegistry.find(workspaceId);
167
+ if (!entry) {
168
+ throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
169
+ }
170
+ try {
171
+ canonicalDirectory(entry.canonicalPath);
172
+ }
173
+ catch {
174
+ throw new errors_1.WebWorkbenchError(409, 'Workspace is unavailable.', 'workspace_unavailable');
175
+ }
176
+ return Object.freeze((0, session_storage_1.listProjectSessions)(entry.canonicalPath).map(protocol_1.projectSessionSummary));
177
+ }
178
+ async workspaceProjectSummary(workspaceId) {
179
+ const entry = this.workspaceRegistry.find(workspaceId);
180
+ if (!entry) {
181
+ throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
182
+ }
183
+ let workspace;
184
+ try {
185
+ workspace = canonicalDirectory(entry.canonicalPath);
186
+ }
187
+ catch {
188
+ throw new errors_1.WebWorkbenchError(409, 'Workspace is unavailable.', 'workspace_unavailable');
189
+ }
190
+ const status = await new git_read_model_service_1.GitReadModelServiceV1(workspace).status({ pageSize: 2000 });
191
+ return Object.freeze({
192
+ workspaceId,
193
+ repositoryRevision: status.repositoryRevision,
194
+ isRepository: status.isRepository,
195
+ branch: status.branch,
196
+ detached: status.detached,
197
+ head: status.head,
198
+ dirtyCount: status.totalFiles,
199
+ conflictCount: status.conflicted.length,
200
+ });
201
+ }
202
+ setWorkspacePinned(workspaceId, pinned, context) {
203
+ this.assertContextGuard(context);
204
+ try {
205
+ this.workspaceRegistry.setPinned(workspaceId, pinned);
206
+ }
207
+ catch (error) {
208
+ throw mapWorkspaceRegistryError(error);
209
+ }
210
+ const updated = this.listWorkspaces().find(workspace => workspace.id === workspaceId);
211
+ if (!updated) {
212
+ throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
213
+ }
214
+ return updated;
215
+ }
216
+ removeWorkspace(workspaceId, context) {
217
+ this.assertContextGuard(context);
218
+ const entry = this.workspaceRegistry.find(workspaceId);
219
+ if (!entry) {
220
+ throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
221
+ }
222
+ if (entry.canonicalPath === this.workspaceValue) {
223
+ throw new errors_1.WebWorkbenchError(409, 'The active Workspace cannot be removed.', 'workspace_active');
224
+ }
225
+ this.terminalManager.closeWorkspace(workspaceId);
226
+ if (!this.workspaceRegistry.remove(workspaceId)) {
227
+ throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
228
+ }
229
+ }
230
+ listSessions(context) {
231
+ if (context)
232
+ this.assertContextGuard(context);
233
+ const result = Object.freeze((0, session_storage_1.listProjectSessions)(this.workspaceValue).map(protocol_1.projectSessionSummary));
234
+ if (context)
235
+ this.assertContextGuard(context);
236
+ return result;
237
+ }
238
+ sessionSnapshot(sessionId, cursor, pageSize = 50, tail = false, context) {
239
+ if (context)
240
+ this.assertContextGuard(context);
241
+ const session = requireSession(sessionId);
242
+ if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
243
+ throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
244
+ }
245
+ const active = this.runtimeValue.getSession()?.id === session.id;
246
+ const activeProjection = this.activeOrionRuntime?.sessionId === session.id
247
+ ? this.activeOrionRuntime.runtime.thread.getProjection()
248
+ : undefined;
249
+ let indexedPage;
250
+ if (tail) {
251
+ try {
252
+ indexedPage = (0, thread_session_view_1.loadThreadSessionSnapshotPageV1)(this.workspaceValue, session.id, cursor, pageSize);
253
+ }
254
+ catch (error) {
255
+ if (error instanceof thread_session_index_1.ThreadSessionIndexError) {
256
+ throw new errors_1.WebWorkbenchError(error.code === 'ORION_THREAD_SESSION_CURSOR_STALE' ? 409 : 400, error.message, error.code === 'ORION_THREAD_SESSION_CURSOR_STALE'
257
+ ? 'transcript_cursor_stale'
258
+ : 'page_cursor_invalid');
259
+ }
260
+ throw error;
261
+ }
262
+ }
263
+ if (indexedPage &&
264
+ activeProjection &&
265
+ (indexedPage.cursor !== activeProjection.cursor ||
266
+ indexedPage.projectionDigest !== activeProjection.digest)) {
267
+ if (cursor) {
268
+ throw new errors_1.WebWorkbenchError(409, 'Active Thread changed after this transcript cursor was issued.', 'transcript_cursor_stale');
269
+ }
270
+ // Another process may have advanced the durable Thread before the active
271
+ // Runtime has adopted that head. Never combine the newer page with the
272
+ // older in-memory cursor/digest in one snapshot.
273
+ indexedPage = undefined;
274
+ }
275
+ let view;
276
+ if (!indexedPage && !activeProjection) {
277
+ try {
278
+ view = this.loadSessionView(session, activeProjection);
279
+ }
280
+ catch (error) {
281
+ if (!activeProjection)
282
+ throw error;
283
+ view = undefined;
284
+ }
285
+ }
286
+ const transcriptSource = indexedPage
287
+ ? indexedPage.transcript.items
288
+ : view
289
+ ? view.transcriptMessages
290
+ : activeProjection
291
+ ? Object.values(activeProjection.items)
292
+ .filter(item => item.kind === 'message' &&
293
+ item.status !== 'started' &&
294
+ ['system', 'user', 'assistant', 'tool'].includes(item.role ?? ''))
295
+ .sort((left, right) => left.startedSeq - right.startedSeq)
296
+ .map(item => ({
297
+ role: item.role,
298
+ content: item.content ?? item.summary ?? item.error ?? '',
299
+ timestamp: item.terminalSeq ?? item.startedSeq,
300
+ }))
301
+ : (0, session_storage_1.readSessionMessages)(session.id).map(message => ({
302
+ role: message.role,
303
+ content: message.content,
304
+ timestamp: message.timestamp,
305
+ ...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
306
+ ...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
307
+ ...(message.modelVisibleContent
308
+ ? { modelVisibleContent: message.modelVisibleContent }
309
+ : {}),
310
+ ...(message.appliedSkills ? { appliedSkills: message.appliedSkills } : {}),
311
+ }));
312
+ const tailPage = tail && !indexedPage ? pageTranscriptTail(transcriptSource, cursor, pageSize) : undefined;
313
+ const transcriptPage = indexedPage
314
+ ? {
315
+ items: indexedPage.transcript.items,
316
+ nextCursor: indexedPage.transcript.nextCursor,
317
+ }
318
+ : (tailPage ?? pageItems(transcriptSource, cursor, pageSize));
319
+ const transcriptOffset = indexedPage
320
+ ? indexedPage.transcript.offset
321
+ : (tailPage?.offset ?? (cursor ? decodePageCursor(cursor) : 0));
322
+ const transcript = Object.freeze({
323
+ items: Object.freeze(transcriptPage.items.map((message, index) => ({
324
+ ...message,
325
+ id: `${session.id}:message:${transcriptOffset + index + 1}`,
326
+ }))),
327
+ nextCursor: indexedPage?.transcript.nextCursor ?? transcriptPage.nextCursor,
328
+ });
329
+ const commitProjections = indexedPage
330
+ ? uniqueCommitProjections([indexedPage.latestPlanTurnCommit, indexedPage.latestTurnCommit])
331
+ : view
332
+ ? uniqueCommitProjections([view.latestPlanTurnCommit, view.latestTurnCommit])
333
+ : activeProjection
334
+ ? latestAuthorityCommits(activeProjection)
335
+ : [];
336
+ const commits = commitProjections.map(commit => (0, turn_commit_1.parseTurnCommitV1)(commit.receipt));
337
+ const latestCommit = commits.at(-1);
338
+ const planCommit = [...commits].reverse().find(commit => commit.planReceipt);
339
+ const plan = planCommit?.planReceipt
340
+ ? pickPlanReceipt((0, turn_commit_1.parsePlanReceiptV1)(planCommit.planReceipt))
341
+ : null;
342
+ const state = this.runtimeValue.store.getSnapshot();
343
+ const pendingPermissions = active
344
+ ? this.controllerValue.getPendingPermissions().map(request => ({
345
+ id: request.id,
346
+ toolName: request.name,
347
+ ...(request.reason ? { reason: request.reason } : {}),
348
+ sanitizedArguments: request.args,
349
+ allowedScopes: ['once', 'project', 'global'],
350
+ }))
351
+ : [];
352
+ const projectionStatus = activeProjection?.status ?? (indexedPage || view ? 'idle' : 'legacy');
353
+ const goal = latestCommit?.goalState && latestCommit.goalStateDigest
354
+ ? {
355
+ authority: 'turn_commit',
356
+ digest: latestCommit.goalStateDigest,
357
+ state: JSON.parse(latestCommit.goalState),
358
+ }
359
+ : null;
360
+ const snapshot = Object.freeze({
361
+ apiVersion: protocol_1.WEB_API_VERSION,
362
+ session: (0, protocol_1.projectSessionSummary)(session),
363
+ threadId: activeProjection?.threadId ?? indexedPage?.threadId ?? view?.threadId ?? null,
364
+ threadCursor: activeProjection?.cursor ?? indexedPage?.cursor ?? view?.cursor ?? 0,
365
+ eventCursor: this.eventHub.snapshot().latest,
366
+ ...(activeProjection?.digest || indexedPage?.projectionDigest || view?.projectionDigest
367
+ ? {
368
+ projectionDigest: activeProjection?.digest ?? indexedPage?.projectionDigest ?? view?.projectionDigest,
369
+ }
370
+ : {}),
371
+ threadStatus: projectionStatus,
372
+ ...(activeProjection?.activeTurnId ? { activeTurnId: activeProjection.activeTurnId } : {}),
373
+ transcript,
374
+ runtime: {
375
+ active,
376
+ processing: active ? state.isProcessing : false,
377
+ agentMode: state.agentMode,
378
+ permissionMode: state.permissionMode,
379
+ status: this.latestStatus,
380
+ followups: active ? Object.freeze([...this.controllerValue.getFollowupQueue()]) : [],
381
+ followupLimit: 16,
382
+ contextUsage: active ? state.contextUsage : null,
383
+ tokenUsage: active ? state.tokenUsage : null,
384
+ },
385
+ pendingApprovals: Object.freeze(pendingPermissions),
386
+ goal,
387
+ plan,
388
+ recoveryDiagnostics: indexedPage ? [] : (view?.diagnostics ?? []),
389
+ });
390
+ if (context)
391
+ this.assertContextGuard(context);
392
+ return snapshot;
393
+ }
394
+ async skills() {
395
+ const descriptors = (await this.runtimeValue.inspectSkills?.()) ?? [];
396
+ return Object.freeze(descriptors.map(descriptor => Object.freeze({
397
+ id: descriptor.id,
398
+ name: descriptor.name,
399
+ description: descriptor.description,
400
+ providerId: descriptor.providerId,
401
+ sourceScope: descriptor.sourceScope,
402
+ modelInvocable: descriptor.modelInvocable,
403
+ userInvocable: descriptor.userInvocable,
404
+ requestedCapabilities: Object.freeze([...descriptor.requestedCapabilities]),
405
+ digest: descriptor.digest,
406
+ })));
407
+ }
408
+ mcp() {
409
+ const descriptors = this.runtimeValue.inspectMcp?.() ?? [];
410
+ let runtimeServers = new Map();
411
+ try {
412
+ runtimeServers = new Map((this.activeOrionRuntime?.runtime.diagnostics().mcp.servers ?? []).map(server => [
413
+ server.serverId,
414
+ server,
415
+ ]));
416
+ }
417
+ catch {
418
+ // A concurrent session transition may close the previous diagnostics graph.
419
+ }
420
+ return Object.freeze(descriptors.map(descriptor => {
421
+ const runtime = runtimeServers.get(descriptor.id);
422
+ return Object.freeze({
423
+ id: descriptor.id,
424
+ name: descriptor.name,
425
+ ...(descriptor.description ? { description: descriptor.description } : {}),
426
+ transport: descriptor.transport,
427
+ tags: Object.freeze([...(descriptor.tags ?? [])]),
428
+ disabled: descriptor.disabled ?? false,
429
+ state: runtime?.state ?? 'dormant',
430
+ generation: runtime?.generation ?? 0,
431
+ toolCount: runtime?.toolCount ?? 0,
432
+ activeCallCount: runtime?.activeCallCount ?? 0,
433
+ ...(runtime?.failure
434
+ ? { failure: 'Runtime transport failure. See local Host diagnostics for details.' }
435
+ : {}),
436
+ });
437
+ }));
438
+ }
439
+ async listToolDetails() {
440
+ return Object.freeze(await this.toolDetails.list(this.workspaceValue));
441
+ }
442
+ async readToolDetail(callId, offsetBytes, limitBytes) {
443
+ const entry = (await this.toolDetails.list(this.workspaceValue)).find(detail => detail.callId === callId || detail.artifactId === callId);
444
+ if (!entry?.artifactId)
445
+ throw new errors_1.WebWorkbenchError(404, 'Tool detail was not found.');
446
+ return this.toolDetails.read({
447
+ callId: entry.callId,
448
+ sequence: entry.sequence,
449
+ artifactId: entry.artifactId,
450
+ outputBytes: entry.outputBytes,
451
+ }, { offsetBytes, limitBytes }, this.workspaceValue);
452
+ }
453
+ listFiles(context, input) {
454
+ this.assertContextGuard(context);
455
+ const result = this.fileService.list(input);
456
+ this.assertContextGuard(context);
457
+ return result;
458
+ }
459
+ readFileContent(context, input) {
460
+ this.assertContextGuard(context);
461
+ const result = this.fileService.readContent(input);
462
+ this.assertContextGuard(context);
463
+ return result;
464
+ }
465
+ async gitStatus(context, input) {
466
+ this.assertContextGuard(context);
467
+ const result = await this.gitService.status(input);
468
+ this.assertContextGuard(context);
469
+ return result;
470
+ }
471
+ async gitLog(context, input) {
472
+ this.assertContextGuard(context);
473
+ const result = await this.gitService.log(input);
474
+ this.assertContextGuard(context);
475
+ return result;
476
+ }
477
+ async gitDiff(context, input) {
478
+ this.assertContextGuard(context);
479
+ const result = await this.gitService.diff(input);
480
+ this.assertContextGuard(context);
481
+ return result;
482
+ }
483
+ async review(context) {
484
+ this.assertContextGuard(context);
485
+ try {
486
+ const result = await this.reviewService.snapshot();
487
+ this.assertContextGuard(context);
488
+ return result;
489
+ }
490
+ catch (error) {
491
+ if (error instanceof durable_tool_receipt_reader_1.DurableToolReceiptReaderError) {
492
+ throw new errors_1.WebWorkbenchError(500, 'Durable Review receipt facts failed integrity validation.', 'review_receipt_invalid');
493
+ }
494
+ throw error;
495
+ }
496
+ }
497
+ listTerminals(context) {
498
+ this.assertContextGuard(context);
499
+ const result = this.terminalManager.list(this.activeWorkspaceEntry().id);
500
+ this.assertContextGuard(context);
501
+ return result;
502
+ }
503
+ async createSession(name, context) {
504
+ this.assertReadyForTransition('create a session');
505
+ if (context)
506
+ this.assertContextGuard(context);
507
+ this.sessionTransition = true;
508
+ try {
509
+ // A new Session inherits the durable default; an override on the currently
510
+ // active Session must never leak into the next Session.
511
+ const session = (0, session_storage_1.createSession)(this.workspaceValue, this.settings().sections.defaults.model.effectiveValue);
512
+ (0, global_config_1.incrementSessionCount)();
513
+ const named = name?.trim() ? ((0, session_storage_1.renameSession)(session.id, name.trim()) ?? session) : session;
514
+ this.runtimeValue.setSession(named);
515
+ await this.runtimeValue.rebindSessionRuntime?.();
516
+ this.eventHub.emitRuntime({ type: 'transcript_clear' });
517
+ this.emitState();
518
+ return (0, protocol_1.projectSessionSummary)(named);
519
+ }
520
+ finally {
521
+ this.sessionTransition = false;
522
+ }
523
+ }
524
+ async activateSession(sessionId, context) {
525
+ this.assertReadyForTransition('switch sessions');
526
+ if (context)
527
+ this.assertContextGuard(context);
528
+ this.sessionTransition = true;
529
+ this.suppressContextEdges += 1;
530
+ let activated = false;
531
+ try {
532
+ const session = requireSession(sessionId);
533
+ if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
534
+ throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
535
+ }
536
+ const result = this.controllerValue.handle({
537
+ type: 'select_session',
538
+ sessionId: session.id,
539
+ source: 'programmatic',
540
+ });
541
+ await this.controllerValue.waitForIdle();
542
+ if (this.runtimeValue.getSession()?.id !== session.id) {
543
+ throw new errors_1.WebWorkbenchError(409, 'Session activation was rejected by the runtime.');
544
+ }
545
+ activated = true;
546
+ return { requestId: `activate:${session.id}`, result: result.type };
547
+ }
548
+ finally {
549
+ this.suppressContextEdges -= 1;
550
+ this.sessionTransition = false;
551
+ if (activated)
552
+ this.emitState();
553
+ }
554
+ }
555
+ renameSession(sessionId, name, context) {
556
+ if (context)
557
+ this.assertContextGuard(context);
558
+ const session = requireSession(sessionId);
559
+ if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
560
+ throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
561
+ }
562
+ if (name.length > 120)
563
+ throw new errors_1.WebWorkbenchError(400, 'Session name is too long.');
564
+ const updated = (0, session_storage_1.renameSession)(session.id, name);
565
+ if (!updated)
566
+ throw new errors_1.WebWorkbenchError(404, 'Session no longer exists.');
567
+ return (0, protocol_1.projectSessionSummary)(updated);
568
+ }
569
+ async switchWorkspace(path, context) {
570
+ this.assertContextGuard(context);
571
+ const next = canonicalDirectory(path);
572
+ if (next === this.workspaceValue)
573
+ return;
574
+ let entry;
575
+ try {
576
+ entry = this.workspaceRegistry.register(next);
577
+ }
578
+ catch (error) {
579
+ throw mapWorkspaceRegistryError(error);
580
+ }
581
+ await this.activateContext({
582
+ expectedContextRevision: context.expectedContextRevision,
583
+ workspaceId: entry.id,
584
+ sessionId: null,
585
+ });
586
+ }
587
+ async activateContext(input) {
588
+ if (input.expectedContextRevision !== this.contextRevisionValue) {
589
+ throw new errors_1.WebWorkbenchError(409, 'The active Context changed before activation was admitted.', 'context_revision_conflict');
590
+ }
591
+ const entry = this.workspaceRegistry.find(input.workspaceId);
592
+ if (!entry)
593
+ throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
594
+ const targetWorkspace = canonicalDirectory(entry.canonicalPath);
595
+ const targetSession = input.sessionId ? requireSession(input.sessionId) : null;
596
+ if (targetSession && canonicalDirectory(targetSession.projectPath) !== targetWorkspace) {
597
+ throw new errors_1.WebWorkbenchError(409, 'Session does not belong to the requested Workspace.', 'context_session_mismatch');
598
+ }
599
+ const currentSessionId = this.runtimeValue.getSession()?.id ?? null;
600
+ if (targetWorkspace === this.workspaceValue && targetSession?.id === currentSessionId)
601
+ return;
602
+ if (targetWorkspace === this.workspaceValue && !targetSession && currentSessionId === null)
603
+ return;
604
+ this.assertReadyForTransition('activate a Context');
605
+ const previousWorkspace = this.workspaceValue;
606
+ const previousSessionId = currentSessionId;
607
+ const previousRuntime = this.runtimeValue;
608
+ const previousController = this.controllerValue;
609
+ const transition = (async () => {
610
+ this.suppressContextEdges += 1;
611
+ try {
612
+ await previousController.stopActiveTurn();
613
+ await previousController.waitForIdle();
614
+ if (targetWorkspace !== previousWorkspace) {
615
+ await previousRuntime.shutdown();
616
+ await this.installRuntime(targetWorkspace, false);
617
+ }
618
+ await this.restoreContextSession(targetSession?.id ?? null);
619
+ this.workspaceRegistry.register(targetWorkspace, { activated: true });
620
+ }
621
+ catch (activationError) {
622
+ try {
623
+ if (targetWorkspace !== previousWorkspace) {
624
+ if (this.runtimeValue !== previousRuntime)
625
+ await this.runtimeValue.shutdown();
626
+ await this.installRuntime(previousWorkspace, false);
627
+ }
628
+ await this.restoreContextSession(previousSessionId);
629
+ }
630
+ catch {
631
+ throw new errors_1.WebWorkbenchError(503, 'Context activation failed and the previous Context could not be restored.', 'context_recovery_required');
632
+ }
633
+ throw activationError;
634
+ }
635
+ finally {
636
+ this.suppressContextEdges -= 1;
637
+ }
638
+ this.emitState();
639
+ this.emitWorkspaceResourceInvalidation('context-change');
640
+ if (targetWorkspace !== previousWorkspace)
641
+ this.emitSettingsWorkspaceChange();
642
+ })().finally(() => {
643
+ if (this.transition === transition)
644
+ this.transition = undefined;
645
+ });
646
+ this.transition = transition;
647
+ return transition;
648
+ }
649
+ dispatch(raw) {
650
+ const command = (0, protocol_1.parseWebCommand)(raw);
651
+ return this.executeMutation(command.requestId, 'command', command, () => {
652
+ this.assertCommandSession(command.expectedSessionId);
653
+ const runtimeResult = this.controllerValue.handle((0, protocol_1.toAgentRuntimeInput)(command));
654
+ return Object.freeze({
655
+ requestId: command.requestId,
656
+ result: runtimeResult.type,
657
+ detail: JSON.stringify(runtimeResult),
658
+ });
659
+ });
660
+ }
661
+ async updateSettings(input) {
662
+ this.assertSettingsAvailable();
663
+ const update = this.runtimeValue.updateSettings;
664
+ if (!update) {
665
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
666
+ }
667
+ try {
668
+ const result = await update({
669
+ requestId: input.requestId,
670
+ expectedRevision: input.expectedRevision,
671
+ operations: input.operations,
672
+ });
673
+ return Object.freeze({
674
+ revision: result.revision,
675
+ appliedKeys: result.appliedKeys,
676
+ settings: result.document,
677
+ });
678
+ }
679
+ catch (error) {
680
+ throw mapSettingsError(error);
681
+ }
682
+ }
683
+ async openSettingsDocument() {
684
+ this.assertSettingsAvailable();
685
+ const coordinator = this.runtimeValue.settingsCoordinator;
686
+ if (!coordinator) {
687
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
688
+ }
689
+ try {
690
+ await coordinator.openDocument();
691
+ return true;
692
+ }
693
+ catch (error) {
694
+ throw mapSettingsError(error);
695
+ }
696
+ }
697
+ executeMutation(requestId, operation, payload, action) {
698
+ if (!requestId.trim() || requestId.length > 128) {
699
+ throw new errors_1.WebWorkbenchError(400, 'requestId must be a non-empty string up to 128 characters.');
700
+ }
701
+ const fingerprint = (0, canonical_1.digestRuntimeValue)({ operation, payload });
702
+ const cached = this.mutationResults.get(requestId);
703
+ if (cached) {
704
+ if (cached.fingerprint !== fingerprint) {
705
+ throw new errors_1.WebWorkbenchError(409, 'requestId was already used for another mutation.', 'request_id_conflict');
706
+ }
707
+ return cached.result;
708
+ }
709
+ if (this.mutationResults.size >= MAX_MUTATION_RESULTS) {
710
+ throw new errors_1.WebWorkbenchError(503, 'The mutation idempotency ledger is full; restart the local Web Host before retrying.', 'mutation_capacity_exhausted');
711
+ }
712
+ const result = Promise.resolve().then(() => {
713
+ this.assertMutationAdmission();
714
+ return action();
715
+ });
716
+ this.mutationResults.set(requestId, { fingerprint, result });
717
+ return result;
718
+ }
719
+ async diagnostics() {
720
+ const state = this.runtimeValue.store.getSnapshot();
721
+ const mcp = (0, mcp_1.loadFirstPartyMcpConfigurationV1)();
722
+ const activeSessionId = this.runtimeValue.getSession()?.id ?? null;
723
+ // Harness diagnostics may need the lazy Session runtime. A read-only Web
724
+ // baseline must not manufacture a Session merely by opening the page.
725
+ const harness = activeSessionId
726
+ ? ((await this.runtimeValue.getHarnessDiagnostics?.()) ?? null)
727
+ : null;
728
+ return {
729
+ workspace: this.workspaceValue,
730
+ activeSessionId,
731
+ configured: this.runtimeValue.isConfigured,
732
+ processing: state.isProcessing,
733
+ agentMode: state.agentMode,
734
+ permissionMode: state.permissionMode,
735
+ contextUsage: state.contextUsage,
736
+ tokenUsage: state.tokenUsage,
737
+ plan: state.currentPlan,
738
+ todos: state.todos,
739
+ skills: {
740
+ configuredPaths: this.runtimeValue.config.skills?.paths ?? [],
741
+ loadedFromPrompt: state.skillsContent.length > 0,
742
+ },
743
+ mcp: { servers: mcpServerIds(mcp) },
744
+ harness,
745
+ eventStream: this.eventHub.snapshot(),
746
+ };
747
+ }
748
+ async shutdown() {
749
+ if (this.closed)
750
+ return;
751
+ this.closed = true;
752
+ await this.controllerValue.stopActiveTurn();
753
+ await this.controllerValue.waitForIdle();
754
+ this.closeWorkspaceWatchers();
755
+ await this.terminalManager.shutdown();
756
+ await this.runtimeValue.shutdown();
757
+ this.eventHub.close();
758
+ }
759
+ async installRuntime(workspace, publishState = true) {
760
+ const runtime = await this.createRuntime(workspace);
761
+ this.closeWorkspaceWatchers();
762
+ this.activeOrionRuntime = undefined;
763
+ this.sessionViews.clear();
764
+ this.sessionViewBytes = 0;
765
+ this.latestStatus = 'Ready';
766
+ this.workspaceValue = workspace;
767
+ this.runtimeValue = runtime;
768
+ this.fileService = new file_read_service_1.FileReadServiceV1(workspace);
769
+ this.gitService = new git_read_model_service_1.GitReadModelServiceV1(workspace);
770
+ this.reviewService = new review_service_1.ReviewServiceV1(this.gitService, () => (0, durable_tool_receipt_reader_1.listProjectDurableToolReceiptRefsV1)(this.workspaceValue));
771
+ const eventSink = {
772
+ emit: (event) => {
773
+ if (event.type === 'status_changed')
774
+ this.latestStatus = event.message;
775
+ if (this.suppressContextEdges > 0)
776
+ return undefined;
777
+ const activeSessionId = this.runtimeValue.getSession()?.id;
778
+ const activeThread = activeSessionId && this.activeOrionRuntime?.sessionId === activeSessionId
779
+ ? this.activeOrionRuntime.runtime.thread.getProjection().threadId
780
+ : undefined;
781
+ const envelope = this.eventHub.emitRuntime(event, {
782
+ ...(activeSessionId ? { sessionId: activeSessionId } : {}),
783
+ ...(activeThread ? { threadId: activeThread } : {}),
784
+ });
785
+ if (event.type === 'session_restored')
786
+ this.emitState();
787
+ if (event.type === 'tool_finished') {
788
+ this.scheduleWorkspaceResourceInvalidation('tool-finished');
789
+ }
790
+ return event.type === 'transcript_append' ? `web-entry-${envelope.cursor}` : undefined;
791
+ },
792
+ };
793
+ this.controllerValue = new agent_runtime_controller_1.AgentRuntimeController({
794
+ runtime,
795
+ eventSink,
796
+ uiRenderer: 'web',
797
+ // Thread events are authoritative for Web transcript projection. Echoing here
798
+ // would duplicate every submitted user item when the Thread observer catches up.
799
+ echoSubmittedInput: false,
800
+ useRuntimeToolPermissions: true,
801
+ uiCapabilities: {
802
+ structuredPickers: true,
803
+ inlineProgress: true,
804
+ suppressLegacyTokenMeta: true,
805
+ extraAssistantSpacing: true,
806
+ suppressAbortNotice: true,
807
+ },
808
+ });
809
+ this.installWorkspaceWatchers();
810
+ if (publishState)
811
+ this.emitState(false);
812
+ }
813
+ emitState(advanceRevision = true) {
814
+ if (this.suppressContextEdges > 0)
815
+ return;
816
+ if (advanceRevision)
817
+ this.contextRevisionValue = (0, crypto_1.randomUUID)();
818
+ const workspace = this.activeWorkspaceEntry();
819
+ this.eventHub.emit({
820
+ type: 'workbench_state',
821
+ contextRevision: this.contextRevisionValue,
822
+ workspaceId: workspace.id,
823
+ workspace: this.workspaceValue,
824
+ activeSessionId: this.runtimeValue.getSession()?.id ?? null,
825
+ });
826
+ }
827
+ emitWorkspaceResourceInvalidation(reason) {
828
+ if (this.closed || this.suppressContextEdges > 0)
829
+ return;
830
+ this.eventHub.emit({
831
+ type: 'workspace_resource_invalidated',
832
+ workspaceId: this.activeWorkspaceEntry().id,
833
+ resources: ['files', 'git', 'review'],
834
+ reason,
835
+ }, false);
836
+ }
837
+ scheduleWorkspaceResourceInvalidation(reason) {
838
+ if (this.closed)
839
+ return;
840
+ if (this.resourceInvalidationTimer)
841
+ clearTimeout(this.resourceInvalidationTimer);
842
+ const workspace = this.workspaceValue;
843
+ this.resourceInvalidationTimer = setTimeout(() => {
844
+ this.resourceInvalidationTimer = undefined;
845
+ if (!this.closed && this.workspaceValue === workspace) {
846
+ this.emitWorkspaceResourceInvalidation(reason);
847
+ }
848
+ }, 120);
849
+ this.resourceInvalidationTimer.unref();
850
+ }
851
+ installWorkspaceWatchers() {
852
+ const watchPaths = [this.workspaceValue, (0, path_1.resolve)(this.workspaceValue, '.git')].filter(path => (0, fs_1.existsSync)(path));
853
+ for (const path of watchPaths) {
854
+ try {
855
+ const watcher = (0, fs_1.watch)(path, { persistent: false }, () => this.scheduleWorkspaceResourceInvalidation('filesystem-change'));
856
+ watcher.on('error', () => {
857
+ watcher.close();
858
+ this.workspaceWatchers = this.workspaceWatchers.filter(item => item !== watcher);
859
+ });
860
+ this.workspaceWatchers.push(watcher);
861
+ }
862
+ catch {
863
+ // Watchers are only refresh hints. Revisioned reads and explicit refresh remain authoritative.
864
+ }
865
+ }
866
+ }
867
+ closeWorkspaceWatchers() {
868
+ if (this.resourceInvalidationTimer)
869
+ clearTimeout(this.resourceInvalidationTimer);
870
+ this.resourceInvalidationTimer = undefined;
871
+ for (const watcher of this.workspaceWatchers)
872
+ watcher.close();
873
+ this.workspaceWatchers = [];
874
+ }
875
+ async restoreContextSession(sessionId) {
876
+ if (!sessionId) {
877
+ this.runtimeValue.setSession(null);
878
+ await this.runtimeValue.rebindSessionRuntime?.();
879
+ if (this.suppressContextEdges === 0) {
880
+ this.eventHub.emitRuntime({ type: 'transcript_clear' });
881
+ }
882
+ return;
883
+ }
884
+ const result = this.controllerValue.handle({
885
+ type: 'select_session',
886
+ sessionId,
887
+ source: 'programmatic',
888
+ });
889
+ await this.controllerValue.waitForIdle();
890
+ if (this.runtimeValue.getSession()?.id !== sessionId) {
891
+ throw new errors_1.WebWorkbenchError(409, `Session activation was rejected by the runtime (${result.type}).`, 'context_session_rejected');
892
+ }
893
+ }
894
+ emitSettingsWorkspaceChange() {
895
+ const describe = this.runtimeValue.describeSettings;
896
+ if (!describe) {
897
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
898
+ }
899
+ const settings = describe();
900
+ this.eventHub.emit({
901
+ type: 'settings_invalidated',
902
+ revision: settings.revision,
903
+ reason: 'workspace-change',
904
+ state: settings.state === 'ready' || settings.state === 'read-only' ? 'ready' : 'invalid',
905
+ }, false);
906
+ }
907
+ observeThreadRuntime(runtime, sessionId, activation) {
908
+ let replaying = true;
909
+ const pending = [];
910
+ const unsubscribe = runtime.thread.observeCommittedEvents(events => {
911
+ if (replaying)
912
+ pending.push(...events);
913
+ else
914
+ for (const event of events)
915
+ this.eventHub.emitThread(event, sessionId);
916
+ });
917
+ try {
918
+ const edge = runtime.thread.getProjection().cursor;
919
+ // Historical transcript state comes from the cursor-bound snapshot.
920
+ // Publishing the entire Thread through the live SSE hub on every switch
921
+ // made activation O(history) and evicted the reconnect window.
922
+ replaying = false;
923
+ for (const event of pending
924
+ .filter(event => event.seq > edge)
925
+ .sort((left, right) => left.seq - right.seq)) {
926
+ this.eventHub.emitThread(event, sessionId);
927
+ }
928
+ if (activation?.sessionId === sessionId && activation.view) {
929
+ this.rememberSessionView(sessionId, activation.view);
930
+ }
931
+ this.activeOrionRuntime = { runtime, sessionId };
932
+ }
933
+ catch (error) {
934
+ unsubscribe();
935
+ throw error;
936
+ }
937
+ return () => {
938
+ unsubscribe();
939
+ if (this.activeOrionRuntime?.runtime === runtime)
940
+ this.activeOrionRuntime = undefined;
941
+ };
942
+ }
943
+ loadSessionView(session, activeProjection) {
944
+ const expectedFingerprint = activeProjection
945
+ ? threadViewFingerprint(activeProjection)
946
+ : session.threadReadModel
947
+ ? [
948
+ session.threadReadModel.threadId,
949
+ session.threadReadModel.cursor,
950
+ session.threadReadModel.projectionDigest,
951
+ ].join(':')
952
+ : undefined;
953
+ const cached = expectedFingerprint ? this.sessionViews.get(session.id) : undefined;
954
+ if (cached && expectedFingerprint && cached.fingerprint === expectedFingerprint) {
955
+ this.sessionViews.delete(session.id);
956
+ this.sessionViews.set(session.id, cached);
957
+ return cached.view;
958
+ }
959
+ const view = (0, thread_session_view_1.loadThreadSessionViewV1)(this.workspaceValue, session.id);
960
+ if (!view)
961
+ return undefined;
962
+ return this.rememberSessionView(session.id, view);
963
+ }
964
+ rememberSessionView(sessionId, view) {
965
+ const previous = this.sessionViews.get(sessionId);
966
+ if (previous)
967
+ this.sessionViewBytes -= previous.bytes;
968
+ const entry = {
969
+ fingerprint: [view.threadId, view.cursor, view.projectionDigest].join(':'),
970
+ view,
971
+ bytes: estimateSessionViewBytes(view),
972
+ };
973
+ this.sessionViews.delete(sessionId);
974
+ this.sessionViews.set(sessionId, entry);
975
+ this.sessionViewBytes += entry.bytes;
976
+ while (this.sessionViewBytes > SESSION_VIEW_CACHE_BYTES && this.sessionViews.size > 1) {
977
+ const oldest = this.sessionViews.keys().next().value;
978
+ if (!oldest)
979
+ break;
980
+ const evicted = this.sessionViews.get(oldest);
981
+ this.sessionViews.delete(oldest);
982
+ this.sessionViewBytes -= evicted?.bytes ?? 0;
983
+ }
984
+ return view;
985
+ }
986
+ assertReadyForTransition(operation) {
987
+ if (this.closed)
988
+ throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.');
989
+ if (this.transition || this.sessionTransition) {
990
+ throw new errors_1.WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
991
+ }
992
+ if (this.controllerValue.hasActiveTurn()) {
993
+ throw new errors_1.WebWorkbenchError(409, `Cannot ${operation} while a turn is active.`, 'runtime_busy');
994
+ }
995
+ }
996
+ assertMutationAdmission() {
997
+ if (this.closed)
998
+ throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.');
999
+ if (this.transition || this.sessionTransition) {
1000
+ throw new errors_1.WebWorkbenchError(409, 'A Context transition is running.', 'runtime_busy');
1001
+ }
1002
+ }
1003
+ assertContextGuard(context) {
1004
+ this.assertMutationAdmission();
1005
+ if (context.expectedContextRevision !== this.contextRevisionValue ||
1006
+ context.workspaceId !== this.activeWorkspaceEntry().id) {
1007
+ throw new errors_1.WebWorkbenchError(409, 'The active Context changed before the operation was admitted.', 'context_revision_conflict');
1008
+ }
1009
+ }
1010
+ assertCommandSession(expectedSessionId) {
1011
+ if (this.closed)
1012
+ throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.');
1013
+ if (this.transition || this.sessionTransition) {
1014
+ throw new errors_1.WebWorkbenchError(409, 'A Session transition is running.', 'runtime_busy');
1015
+ }
1016
+ if (this.runtimeValue.getSession()?.id !== expectedSessionId) {
1017
+ throw new errors_1.WebWorkbenchError(409, 'The active Session changed before the command was admitted.', 'active_session_changed');
1018
+ }
1019
+ }
1020
+ assertSettingsAvailable() {
1021
+ if (this.closed) {
1022
+ throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.', 'settings_document_unavailable');
1023
+ }
1024
+ if (this.transition) {
1025
+ throw new errors_1.WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
1026
+ }
1027
+ }
1028
+ initializeWorkspaceRegistry() {
1029
+ const paths = [this.workspaceValue];
1030
+ for (const session of (0, session_storage_1.listSessions)()) {
1031
+ try {
1032
+ paths.push(canonicalDirectory(session.projectPath));
1033
+ }
1034
+ catch {
1035
+ // Unavailable historical projects remain in the Session catalog but
1036
+ // are not imported as selectable Workspace registry entries.
1037
+ }
1038
+ }
1039
+ try {
1040
+ this.workspaceRegistry.registerKnown(paths, this.workspaceValue);
1041
+ }
1042
+ catch (error) {
1043
+ throw mapWorkspaceRegistryError(error);
1044
+ }
1045
+ }
1046
+ activeWorkspaceEntry() {
1047
+ const entry = this.workspaceRegistry
1048
+ .list()
1049
+ .find(candidate => candidate.canonicalPath === this.workspaceValue);
1050
+ if (!entry) {
1051
+ throw new errors_1.WebWorkbenchError(503, 'Active Workspace is missing from the registry.');
1052
+ }
1053
+ return entry;
1054
+ }
1055
+ }
1056
+ exports.WebWorkbenchController = WebWorkbenchController;
1057
+ function mapWorkspaceRegistryError(error) {
1058
+ if (error instanceof errors_1.WebWorkbenchError)
1059
+ return error;
1060
+ if (error instanceof workspace_registry_1.WorkspaceRegistryError) {
1061
+ const status = error.code === 'workspace_registry_invalid' ? 503 : 409;
1062
+ return new errors_1.WebWorkbenchError(status, error.message, error.code);
1063
+ }
1064
+ return new errors_1.WebWorkbenchError(503, 'Workspace registry is unavailable.');
1065
+ }
1066
+ function mapSettingsError(error) {
1067
+ if (error instanceof errors_1.WebWorkbenchError)
1068
+ return error;
1069
+ if (error instanceof settings_coordinator_1.SettingsCoordinatorError) {
1070
+ return new errors_1.WebWorkbenchError(error.status, error.message, error.code);
1071
+ }
1072
+ return new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
1073
+ }
1074
+ function pickPlanReceipt(receipt) {
1075
+ return Object.freeze({
1076
+ body: receipt.plan,
1077
+ returnMode: receipt.returnMode,
1078
+ digest: receipt.digest,
1079
+ });
1080
+ }
1081
+ function threadViewFingerprint(projection) {
1082
+ return [projection.threadId, projection.cursor, projection.digest].join(':');
1083
+ }
1084
+ function uniqueCommitProjections(commits) {
1085
+ return [...new Map(commits.filter(Boolean).map(commit => [commit.seq, commit])).values()].sort((left, right) => left.seq - right.seq);
1086
+ }
1087
+ function latestAuthorityCommits(projection) {
1088
+ const commits = Object.values(projection.turns)
1089
+ .flatMap(turn => (turn.commit ? [turn.commit] : []))
1090
+ .sort((left, right) => left.seq - right.seq);
1091
+ const latest = commits.at(-1);
1092
+ const latestPlan = [...commits].reverse().find(commit => {
1093
+ try {
1094
+ return Boolean((0, turn_commit_1.parseTurnCommitV1)(commit.receipt).planReceipt);
1095
+ }
1096
+ catch {
1097
+ return false;
1098
+ }
1099
+ });
1100
+ return uniqueCommitProjections([latestPlan, latest]);
1101
+ }
1102
+ function estimateSessionViewBytes(view) {
1103
+ let bytes = 1024;
1104
+ for (const message of view.transcriptMessages) {
1105
+ bytes += Buffer.byteLength(message.content, 'utf8') + 256;
1106
+ if (message.modelVisibleContent) {
1107
+ bytes += Buffer.byteLength(message.modelVisibleContent, 'utf8');
1108
+ }
1109
+ }
1110
+ for (const message of view.modelHistory) {
1111
+ bytes += Buffer.byteLength(JSON.stringify(message), 'utf8') + 128;
1112
+ }
1113
+ for (const commit of uniqueCommitProjections([
1114
+ view.latestPlanTurnCommit,
1115
+ view.latestTurnCommit,
1116
+ ])) {
1117
+ bytes += Buffer.byteLength(commit.receipt, 'utf8');
1118
+ }
1119
+ return bytes;
1120
+ }
1121
+ function pageItems(items, cursor, pageSize = 50) {
1122
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
1123
+ throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
1124
+ }
1125
+ const offset = cursor ? decodePageCursor(cursor) : 0;
1126
+ if (offset > items.length)
1127
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is out of range.');
1128
+ const page = items.slice(offset, offset + pageSize);
1129
+ const nextOffset = offset + page.length;
1130
+ return Object.freeze({
1131
+ items: Object.freeze(page),
1132
+ nextCursor: nextOffset < items.length ? encodePageCursor(nextOffset) : null,
1133
+ });
1134
+ }
1135
+ function pageCollectionItems(scope, items, cursor, pageSize, keyOf) {
1136
+ if (!scope || scope.length > 80 || !/^[a-z][a-z0-9_-]*$/.test(scope)) {
1137
+ throw new errors_1.WebWorkbenchError(500, 'Collection scope is invalid.');
1138
+ }
1139
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
1140
+ throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
1141
+ }
1142
+ const keys = items.map(keyOf);
1143
+ if (keys.some(key => !key || key.length > 4096) || new Set(keys).size !== keys.length) {
1144
+ throw new errors_1.WebWorkbenchError(500, `Collection ${scope} has invalid or duplicate keys.`);
1145
+ }
1146
+ const revision = (0, canonical_1.digestRuntimeValue)(items);
1147
+ let offset = 0;
1148
+ if (cursor) {
1149
+ const decoded = decodeCollectionCursor(cursor, scope);
1150
+ if (decoded.revision !== revision) {
1151
+ throw new errors_1.WebWorkbenchError(409, 'Collection changed after this page cursor was issued.', 'collection_cursor_stale');
1152
+ }
1153
+ offset = keys.indexOf(decoded.after) + 1;
1154
+ if (offset === 0) {
1155
+ throw new errors_1.WebWorkbenchError(409, 'Collection page boundary no longer exists.', 'collection_cursor_stale');
1156
+ }
1157
+ }
1158
+ const page = items.slice(offset, offset + pageSize);
1159
+ const nextOffset = offset + page.length;
1160
+ return Object.freeze({
1161
+ items: Object.freeze(page),
1162
+ nextCursor: nextOffset < items.length && page.length > 0
1163
+ ? encodeCollectionCursor(scope, revision, keys[nextOffset - 1])
1164
+ : null,
1165
+ });
1166
+ }
1167
+ function pageTranscriptTail(items, cursor, pageSize = 50) {
1168
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
1169
+ throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
1170
+ }
1171
+ const end = cursor ? decodePageCursor(cursor) : items.length;
1172
+ if (end > items.length)
1173
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is out of range.');
1174
+ const offset = Math.max(0, end - pageSize);
1175
+ return Object.freeze({
1176
+ items: Object.freeze(items.slice(offset, end)),
1177
+ nextCursor: offset > 0 ? encodePageCursor(offset) : null,
1178
+ offset,
1179
+ });
1180
+ }
1181
+ function encodePageCursor(offset) {
1182
+ return Buffer.from(JSON.stringify({ version: 1, offset })).toString('base64url');
1183
+ }
1184
+ function encodeCollectionCursor(scope, revision, after) {
1185
+ return Buffer.from(JSON.stringify({ version: 2, kind: 'collection', scope, revision, after })).toString('base64url');
1186
+ }
1187
+ function decodeCollectionCursor(cursor, expectedScope) {
1188
+ if (!cursor || cursor.length > 8192) {
1189
+ throw new errors_1.WebWorkbenchError(400, 'Collection page cursor is invalid.');
1190
+ }
1191
+ try {
1192
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
1193
+ if (!parsed ||
1194
+ typeof parsed !== 'object' ||
1195
+ Array.isArray(parsed) ||
1196
+ parsed.version !== 2 ||
1197
+ parsed.kind !== 'collection' ||
1198
+ parsed.scope !== expectedScope ||
1199
+ typeof parsed.revision !== 'string' ||
1200
+ typeof parsed.after !== 'string') {
1201
+ throw new Error('invalid');
1202
+ }
1203
+ return {
1204
+ revision: parsed.revision,
1205
+ after: parsed.after,
1206
+ };
1207
+ }
1208
+ catch {
1209
+ throw new errors_1.WebWorkbenchError(400, 'Collection page cursor is invalid.');
1210
+ }
1211
+ }
1212
+ function decodePageCursor(cursor) {
1213
+ if (!cursor || cursor.length > 512)
1214
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is invalid.');
1215
+ try {
1216
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
1217
+ if (!parsed ||
1218
+ typeof parsed !== 'object' ||
1219
+ parsed.version !== 1 ||
1220
+ !Number.isSafeInteger(parsed.offset) ||
1221
+ (parsed.offset ?? -1) < 0) {
1222
+ throw new Error('invalid cursor');
1223
+ }
1224
+ return parsed.offset;
1225
+ }
1226
+ catch {
1227
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is invalid.');
1228
+ }
1229
+ }
1230
+ function canonicalDirectory(path) {
1231
+ const candidate = (0, path_1.resolve)(path);
1232
+ let canonical;
1233
+ try {
1234
+ canonical = (0, fs_1.realpathSync)(candidate);
1235
+ }
1236
+ catch {
1237
+ throw new errors_1.WebWorkbenchError(400, 'Workspace path does not exist.');
1238
+ }
1239
+ try {
1240
+ if (!(0, fs_1.statSync)(canonical).isDirectory()) {
1241
+ throw new errors_1.WebWorkbenchError(400, 'Workspace path must be a directory.');
1242
+ }
1243
+ }
1244
+ catch (error) {
1245
+ if (error instanceof errors_1.WebWorkbenchError)
1246
+ throw error;
1247
+ throw new errors_1.WebWorkbenchError(400, 'Workspace path is not readable.');
1248
+ }
1249
+ return canonical;
1250
+ }
1251
+ function requireSession(sessionId) {
1252
+ if (!sessionId.trim())
1253
+ throw new errors_1.WebWorkbenchError(400, 'Session id is required.');
1254
+ const session = (0, session_storage_1.loadSessionMeta)(sessionId);
1255
+ if (!session)
1256
+ throw new errors_1.WebWorkbenchError(404, 'Session was not found.');
1257
+ return session;
1258
+ }
1259
+ function mcpServerIds(config) {
1260
+ const ids = new Set();
1261
+ for (const envelope of [config.mcpServers, config.servers, config.orion?.mcp?.servers]) {
1262
+ for (const id of Object.keys(envelope ?? {}))
1263
+ ids.add(id);
1264
+ }
1265
+ return [...ids].sort();
1266
+ }
1267
+ //# sourceMappingURL=workbench-controller.js.map