@orion-agents/orion-code 0.3.0 → 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 (120) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +24 -8
  3. package/README.zh-CN.md +23 -9
  4. package/dist/core/tool-artifacts.d.ts +2 -0
  5. package/dist/core/tool-artifacts.d.ts.map +1 -1
  6. package/dist/core/tool-artifacts.js +27 -1
  7. package/dist/core/tool-artifacts.js.map +1 -1
  8. package/dist/runtime/agent-runtime-controller.js +1 -2
  9. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  10. package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
  11. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  12. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  13. package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
  14. package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
  15. package/dist/runtime/durable-tool-receipt-reader.js +130 -0
  16. package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
  17. package/dist/runtime/legacy-thread-materializer.d.ts +14 -1
  18. package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
  19. package/dist/runtime/legacy-thread-materializer.js +44 -5
  20. package/dist/runtime/legacy-thread-materializer.js.map +1 -1
  21. package/dist/runtime/release-receipts.d.ts +6 -3
  22. package/dist/runtime/release-receipts.d.ts.map +1 -1
  23. package/dist/runtime/release-receipts.js +47 -4
  24. package/dist/runtime/release-receipts.js.map +1 -1
  25. package/dist/runtime/thread-event-store.d.ts +62 -9
  26. package/dist/runtime/thread-event-store.d.ts.map +1 -1
  27. package/dist/runtime/thread-event-store.js +363 -31
  28. package/dist/runtime/thread-event-store.js.map +1 -1
  29. package/dist/runtime/thread-session-index.d.ts +84 -0
  30. package/dist/runtime/thread-session-index.d.ts.map +1 -0
  31. package/dist/runtime/thread-session-index.js +503 -0
  32. package/dist/runtime/thread-session-index.js.map +1 -0
  33. package/dist/runtime/thread-session-view.d.ts +31 -13
  34. package/dist/runtime/thread-session-view.d.ts.map +1 -1
  35. package/dist/runtime/thread-session-view.js +116 -61
  36. package/dist/runtime/thread-session-view.js.map +1 -1
  37. package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
  38. package/dist/runtime/thread-ui-adapter.js +18 -58
  39. package/dist/runtime/thread-ui-adapter.js.map +1 -1
  40. package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
  41. package/dist/runtime/tool-detail-repository.js +23 -19
  42. package/dist/runtime/tool-detail-repository.js.map +1 -1
  43. package/dist/runtime/tool-receipt-validator.d.ts +21 -0
  44. package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
  45. package/dist/runtime/tool-receipt-validator.js +157 -0
  46. package/dist/runtime/tool-receipt-validator.js.map +1 -0
  47. package/dist/services/redaction.d.ts +4 -0
  48. package/dist/services/redaction.d.ts.map +1 -1
  49. package/dist/services/redaction.js +63 -0
  50. package/dist/services/redaction.js.map +1 -1
  51. package/dist/services/session-index.d.ts +16 -0
  52. package/dist/services/session-index.d.ts.map +1 -1
  53. package/dist/services/session-index.js +45 -36
  54. package/dist/services/session-index.js.map +1 -1
  55. package/dist/services/session-storage.d.ts.map +1 -1
  56. package/dist/services/session-storage.js +3 -6
  57. package/dist/services/session-storage.js.map +1 -1
  58. package/dist/services/workspace-registry.d.ts +42 -0
  59. package/dist/services/workspace-registry.d.ts.map +1 -0
  60. package/dist/services/workspace-registry.js +222 -0
  61. package/dist/services/workspace-registry.js.map +1 -0
  62. package/dist/web/errors.d.ts +6 -0
  63. package/dist/web/errors.d.ts.map +1 -0
  64. package/dist/web/errors.js +13 -0
  65. package/dist/web/errors.js.map +1 -0
  66. package/dist/web/event-hub.d.ts +4 -0
  67. package/dist/web/event-hub.d.ts.map +1 -1
  68. package/dist/web/event-hub.js +71 -11
  69. package/dist/web/event-hub.js.map +1 -1
  70. package/dist/web/file-read-service.d.ts +55 -0
  71. package/dist/web/file-read-service.d.ts.map +1 -0
  72. package/dist/web/file-read-service.js +399 -0
  73. package/dist/web/file-read-service.js.map +1 -0
  74. package/dist/web/git-read-model-service.d.ts +77 -0
  75. package/dist/web/git-read-model-service.d.ts.map +1 -0
  76. package/dist/web/git-read-model-service.js +631 -0
  77. package/dist/web/git-read-model-service.js.map +1 -0
  78. package/dist/web/protocol.d.ts +63 -1
  79. package/dist/web/protocol.d.ts.map +1 -1
  80. package/dist/web/protocol.js +22 -1
  81. package/dist/web/protocol.js.map +1 -1
  82. package/dist/web/review-service.d.ts +38 -0
  83. package/dist/web/review-service.d.ts.map +1 -0
  84. package/dist/web/review-service.js +74 -0
  85. package/dist/web/review-service.js.map +1 -0
  86. package/dist/web/server.d.ts.map +1 -1
  87. package/dist/web/server.js +273 -27
  88. package/dist/web/server.js.map +1 -1
  89. package/dist/web/terminal-manager.d.ts +155 -0
  90. package/dist/web/terminal-manager.d.ts.map +1 -0
  91. package/dist/web/terminal-manager.js +728 -0
  92. package/dist/web/terminal-manager.js.map +1 -0
  93. package/dist/web/terminal-server.d.ts +8 -0
  94. package/dist/web/terminal-server.d.ts.map +1 -0
  95. package/dist/web/terminal-server.js +275 -0
  96. package/dist/web/terminal-server.js.map +1 -0
  97. package/dist/web/workbench-controller.d.ts +48 -12
  98. package/dist/web/workbench-controller.d.ts.map +1 -1
  99. package/dist/web/workbench-controller.js +651 -158
  100. package/dist/web/workbench-controller.js.map +1 -1
  101. package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
  102. package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
  103. package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
  104. package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
  105. package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
  106. package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
  107. package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
  108. package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
  109. package/dist/web-client/index.html +2 -2
  110. package/docs/architecture/v0.3.0-web-api.yaml +44 -13
  111. package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
  112. package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
  113. package/docs/plan/v0.3.0-web-workbench-plan.md +58 -7
  114. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
  115. package/docs/readme.md +10 -0
  116. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
  117. package/npm-shrinkwrap.json +185 -354
  118. package/package.json +16 -4
  119. package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
  120. package/dist/web-client/assets/index-DSkFy7gM.js +0 -13
@@ -2,30 +2,34 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WebWorkbenchController = exports.WebWorkbenchError = void 0;
4
4
  exports.pageItems = pageItems;
5
+ exports.pageCollectionItems = pageCollectionItems;
6
+ const crypto_1 = require("crypto");
5
7
  const fs_1 = require("fs");
6
8
  const path_1 = require("path");
7
9
  const agent_runtime_controller_1 = require("../runtime/agent-runtime-controller");
10
+ const durable_tool_receipt_reader_1 = require("../runtime/durable-tool-receipt-reader");
8
11
  const mcp_1 = require("../runtime/mcp");
9
12
  const product_bootstrap_1 = require("../runtime/product-bootstrap");
10
13
  const canonical_1 = require("../runtime/protocol/canonical");
14
+ const thread_session_index_1 = require("../runtime/thread-session-index");
11
15
  const thread_session_view_1 = require("../runtime/thread-session-view");
12
16
  const tool_detail_repository_1 = require("../runtime/tool-detail-repository");
13
17
  const turn_commit_1 = require("../runtime/turn-commit");
14
18
  const global_config_1 = require("../services/global-config");
15
19
  const settings_coordinator_1 = require("../services/settings-coordinator");
16
20
  const session_storage_1 = require("../services/session-storage");
21
+ const workspace_registry_1 = require("../services/workspace-registry");
22
+ const errors_1 = require("./errors");
17
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");
18
26
  const protocol_1 = require("./protocol");
19
- class WebWorkbenchError extends Error {
20
- constructor(status, message, code = status === 409 ? 'request_conflict' : 'web_workbench_error') {
21
- super(message);
22
- this.status = status;
23
- this.code = code;
24
- this.name = 'WebWorkbenchError';
25
- }
26
- }
27
- exports.WebWorkbenchError = WebWorkbenchError;
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; } });
28
31
  const SESSION_VIEW_CACHE_BYTES = 16 * 1024 * 1024;
32
+ const MAX_MUTATION_RESULTS = 4096;
29
33
  /** Owns the sole runtime/controller pair exposed through the local Web host. */
30
34
  class WebWorkbenchController {
31
35
  constructor(options) {
@@ -33,10 +37,27 @@ class WebWorkbenchController {
33
37
  this.sessionViews = new Map();
34
38
  this.sessionViewBytes = 0;
35
39
  this.toolDetails = new tool_detail_repository_1.FileToolDetailRepository();
40
+ this.contextRevisionValue = (0, crypto_1.randomUUID)();
41
+ this.suppressContextEdges = 0;
36
42
  this.latestStatus = 'Ready';
43
+ this.sessionTransition = false;
37
44
  this.closed = false;
45
+ this.workspaceWatchers = [];
38
46
  this.workspaceValue = canonicalDirectory(options.cwd);
39
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
+ });
40
61
  this.createRuntime =
41
62
  options.createRuntime ??
42
63
  (cwd => (0, product_bootstrap_1.createProductUiRuntime)({
@@ -44,6 +65,8 @@ class WebWorkbenchController {
44
65
  shutdownReason: 'Orion Web Workbench shutdown',
45
66
  onActiveSessionRuntime: (runtime, sessionId, activation) => this.observeThreadRuntime(runtime, sessionId, activation),
46
67
  onSettingsInvalidated: event => {
68
+ if (this.suppressContextEdges > 0)
69
+ return;
47
70
  this.eventHub.emit({
48
71
  type: 'settings_invalidated',
49
72
  revision: event.revision,
@@ -55,6 +78,7 @@ class WebWorkbenchController {
55
78
  }
56
79
  static async create(options) {
57
80
  const workbench = new WebWorkbenchController(options);
81
+ workbench.initializeWorkspaceRegistry();
58
82
  await workbench.installRuntime(workbench.workspaceValue);
59
83
  return workbench;
60
84
  }
@@ -67,11 +91,16 @@ class WebWorkbenchController {
67
91
  get controller() {
68
92
  return this.controllerValue;
69
93
  }
94
+ get contextRevision() {
95
+ return this.contextRevisionValue;
96
+ }
70
97
  bootstrap(nonce) {
71
98
  return Object.freeze({
72
99
  apiVersion: protocol_1.WEB_API_VERSION,
73
100
  productVersion: this.runtimeValue.version,
74
101
  nonce,
102
+ contextRevision: this.contextRevisionValue,
103
+ workspaceId: this.activeWorkspaceEntry().id,
75
104
  workspace: this.workspaceValue,
76
105
  configured: this.runtimeValue.isConfigured,
77
106
  activeSessionId: this.runtimeValue.getSession()?.id ?? null,
@@ -82,6 +111,10 @@ class WebWorkbenchController {
82
111
  skills: true,
83
112
  mcp: true,
84
113
  diagnostics: true,
114
+ review: true,
115
+ files: true,
116
+ git: true,
117
+ terminal: this.terminalManager.available,
85
118
  },
86
119
  });
87
120
  }
@@ -89,7 +122,7 @@ class WebWorkbenchController {
89
122
  this.assertSettingsAvailable();
90
123
  const describe = this.runtimeValue.describeSettings;
91
124
  if (!describe) {
92
- throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
125
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
93
126
  }
94
127
  try {
95
128
  return describe();
@@ -99,85 +132,207 @@ class WebWorkbenchController {
99
132
  }
100
133
  }
101
134
  listWorkspaces() {
102
- const paths = new Set([this.workspaceValue]);
103
135
  const counts = new Map();
104
136
  for (const session of (0, session_storage_1.listSessions)()) {
105
137
  try {
106
138
  const path = canonicalDirectory(session.projectPath);
107
- paths.add(path);
108
139
  counts.set(path, (counts.get(path) ?? 0) + 1);
109
140
  }
110
141
  catch {
111
142
  // Stale projects remain in session history but are not selectable workspaces.
112
143
  }
113
144
  }
114
- return Object.freeze([...paths].sort().map(path => Object.freeze({
115
- path,
116
- label: (0, path_1.basename)(path) || path,
117
- active: path === this.workspaceValue,
118
- sessionCount: counts.get(path) ?? 0,
119
- })));
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
+ });
120
201
  }
121
- listSessions() {
122
- return Object.freeze((0, session_storage_1.listProjectSessions)(this.workspaceValue).map(protocol_1.projectSessionSummary));
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
+ }
123
229
  }
124
- sessionSnapshot(sessionId, cursor, pageSize = 50, tail = false) {
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);
125
241
  const session = requireSession(sessionId);
126
242
  if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
127
- throw new WebWorkbenchError(409, 'Session belongs to another workspace.');
243
+ throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
128
244
  }
129
245
  const active = this.runtimeValue.getSession()?.id === session.id;
130
246
  const activeProjection = this.activeOrionRuntime?.sessionId === session.id
131
247
  ? this.activeOrionRuntime.runtime.thread.getProjection()
132
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
+ }
133
275
  let view;
134
- try {
135
- view = this.loadSessionView(session, activeProjection);
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
+ }
136
285
  }
137
- catch (error) {
138
- if (!activeProjection)
139
- throw error;
140
- view = undefined;
141
- }
142
- const transcriptSource = view
143
- ? view.transcriptMessages
144
- : activeProjection
145
- ? Object.values(activeProjection.items)
146
- .filter(item => item.kind === 'message' &&
147
- item.status !== 'started' &&
148
- ['system', 'user', 'assistant', 'tool'].includes(item.role ?? ''))
149
- .sort((left, right) => left.startedSeq - right.startedSeq)
150
- .map(item => ({
151
- role: item.role,
152
- content: item.content ?? item.summary ?? item.error ?? '',
153
- timestamp: item.terminalSeq ?? item.startedSeq,
154
- }))
155
- : (0, session_storage_1.readSessionMessages)(session.id).map(message => ({
156
- role: message.role,
157
- content: message.content,
158
- timestamp: message.timestamp,
159
- ...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
160
- ...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
161
- ...(message.modelVisibleContent
162
- ? { modelVisibleContent: message.modelVisibleContent }
163
- : {}),
164
- ...(message.appliedSkills ? { appliedSkills: message.appliedSkills } : {}),
165
- }));
166
- const tailPage = tail ? pageTranscriptTail(transcriptSource, cursor, pageSize) : undefined;
167
- const transcriptPage = tailPage ?? pageItems(transcriptSource, cursor, pageSize);
168
- const transcriptOffset = tailPage?.offset ?? (cursor ? decodePageCursor(cursor) : 0);
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));
169
322
  const transcript = Object.freeze({
170
323
  items: Object.freeze(transcriptPage.items.map((message, index) => ({
171
324
  ...message,
172
325
  id: `${session.id}:message:${transcriptOffset + index + 1}`,
173
326
  }))),
174
- nextCursor: transcriptPage.nextCursor,
327
+ nextCursor: indexedPage?.transcript.nextCursor ?? transcriptPage.nextCursor,
175
328
  });
176
- const commitProjections = view
177
- ? uniqueCommitProjections([view.latestPlanTurnCommit, view.latestTurnCommit])
178
- : activeProjection
179
- ? latestAuthorityCommits(activeProjection)
180
- : [];
329
+ const commitProjections = indexedPage
330
+ ? uniqueCommitProjections([indexedPage.latestPlanTurnCommit, indexedPage.latestTurnCommit])
331
+ : view
332
+ ? uniqueCommitProjections([view.latestPlanTurnCommit, view.latestTurnCommit])
333
+ : activeProjection
334
+ ? latestAuthorityCommits(activeProjection)
335
+ : [];
181
336
  const commits = commitProjections.map(commit => (0, turn_commit_1.parseTurnCommitV1)(commit.receipt));
182
337
  const latestCommit = commits.at(-1);
183
338
  const planCommit = [...commits].reverse().find(commit => commit.planReceipt);
@@ -194,7 +349,7 @@ class WebWorkbenchController {
194
349
  allowedScopes: ['once', 'project', 'global'],
195
350
  }))
196
351
  : [];
197
- const projectionStatus = activeProjection?.status ?? (view ? 'idle' : 'legacy');
352
+ const projectionStatus = activeProjection?.status ?? (indexedPage || view ? 'idle' : 'legacy');
198
353
  const goal = latestCommit?.goalState && latestCommit.goalStateDigest
199
354
  ? {
200
355
  authority: 'turn_commit',
@@ -202,14 +357,16 @@ class WebWorkbenchController {
202
357
  state: JSON.parse(latestCommit.goalState),
203
358
  }
204
359
  : null;
205
- return Object.freeze({
360
+ const snapshot = Object.freeze({
206
361
  apiVersion: protocol_1.WEB_API_VERSION,
207
362
  session: (0, protocol_1.projectSessionSummary)(session),
208
- threadId: activeProjection?.threadId ?? view?.threadId ?? null,
209
- threadCursor: activeProjection?.cursor ?? view?.cursor ?? 0,
363
+ threadId: activeProjection?.threadId ?? indexedPage?.threadId ?? view?.threadId ?? null,
364
+ threadCursor: activeProjection?.cursor ?? indexedPage?.cursor ?? view?.cursor ?? 0,
210
365
  eventCursor: this.eventHub.snapshot().latest,
211
- ...(activeProjection?.digest || view?.projectionDigest
212
- ? { projectionDigest: activeProjection?.digest ?? view?.projectionDigest }
366
+ ...(activeProjection?.digest || indexedPage?.projectionDigest || view?.projectionDigest
367
+ ? {
368
+ projectionDigest: activeProjection?.digest ?? indexedPage?.projectionDigest ?? view?.projectionDigest,
369
+ }
213
370
  : {}),
214
371
  threadStatus: projectionStatus,
215
372
  ...(activeProjection?.activeTurnId ? { activeTurnId: activeProjection.activeTurnId } : {}),
@@ -228,8 +385,11 @@ class WebWorkbenchController {
228
385
  pendingApprovals: Object.freeze(pendingPermissions),
229
386
  goal,
230
387
  plan,
231
- recoveryDiagnostics: view?.diagnostics ?? [],
388
+ recoveryDiagnostics: indexedPage ? [] : (view?.diagnostics ?? []),
232
389
  });
390
+ if (context)
391
+ this.assertContextGuard(context);
392
+ return snapshot;
233
393
  }
234
394
  async skills() {
235
395
  const descriptors = (await this.runtimeValue.inspectSkills?.()) ?? [];
@@ -270,7 +430,9 @@ class WebWorkbenchController {
270
430
  generation: runtime?.generation ?? 0,
271
431
  toolCount: runtime?.toolCount ?? 0,
272
432
  activeCallCount: runtime?.activeCallCount ?? 0,
273
- ...(runtime?.failure ? { failure: runtime.failure } : {}),
433
+ ...(runtime?.failure
434
+ ? { failure: 'Runtime transport failure. See local Host diagnostics for details.' }
435
+ : {}),
274
436
  });
275
437
  }));
276
438
  }
@@ -280,7 +442,7 @@ class WebWorkbenchController {
280
442
  async readToolDetail(callId, offsetBytes, limitBytes) {
281
443
  const entry = (await this.toolDetails.list(this.workspaceValue)).find(detail => detail.callId === callId || detail.artifactId === callId);
282
444
  if (!entry?.artifactId)
283
- throw new WebWorkbenchError(404, 'Tool detail was not found.');
445
+ throw new errors_1.WebWorkbenchError(404, 'Tool detail was not found.');
284
446
  return this.toolDetails.read({
285
447
  callId: entry.callId,
286
448
  sequence: entry.sequence,
@@ -288,85 +450,195 @@ class WebWorkbenchController {
288
450
  outputBytes: entry.outputBytes,
289
451
  }, { offsetBytes, limitBytes }, this.workspaceValue);
290
452
  }
291
- async createSession(name) {
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) {
292
504
  this.assertReadyForTransition('create a session');
293
- // A new Session inherits the durable default; an override on the currently
294
- // active Session must never leak into the next Session.
295
- const session = (0, session_storage_1.createSession)(this.workspaceValue, this.settings().sections.defaults.model.effectiveValue);
296
- (0, global_config_1.incrementSessionCount)();
297
- const named = name?.trim() ? ((0, session_storage_1.renameSession)(session.id, name.trim()) ?? session) : session;
298
- this.runtimeValue.setSession(named);
299
- await this.runtimeValue.rebindSessionRuntime?.();
300
- this.eventHub.emitRuntime({ type: 'transcript_clear' });
301
- this.emitState();
302
- return (0, protocol_1.projectSessionSummary)(named);
303
- }
304
- async activateSession(sessionId) {
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) {
305
525
  this.assertReadyForTransition('switch sessions');
306
- const session = requireSession(sessionId);
307
- if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
308
- throw new WebWorkbenchError(409, 'Session belongs to another workspace.');
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 };
309
547
  }
310
- const result = this.controllerValue.handle({
311
- type: 'select_session',
312
- sessionId: session.id,
313
- source: 'programmatic',
314
- });
315
- await this.controllerValue.waitForIdle();
316
- if (this.runtimeValue.getSession()?.id !== session.id) {
317
- throw new WebWorkbenchError(409, 'Session activation was rejected by the runtime.');
548
+ finally {
549
+ this.suppressContextEdges -= 1;
550
+ this.sessionTransition = false;
551
+ if (activated)
552
+ this.emitState();
318
553
  }
319
- return { requestId: `activate:${session.id}`, result: result.type };
320
554
  }
321
- renameSession(sessionId, name) {
555
+ renameSession(sessionId, name, context) {
556
+ if (context)
557
+ this.assertContextGuard(context);
322
558
  const session = requireSession(sessionId);
323
559
  if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
324
- throw new WebWorkbenchError(409, 'Session belongs to another workspace.');
560
+ throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
325
561
  }
326
562
  if (name.length > 120)
327
- throw new WebWorkbenchError(400, 'Session name is too long.');
563
+ throw new errors_1.WebWorkbenchError(400, 'Session name is too long.');
328
564
  const updated = (0, session_storage_1.renameSession)(session.id, name);
329
565
  if (!updated)
330
- throw new WebWorkbenchError(404, 'Session no longer exists.');
566
+ throw new errors_1.WebWorkbenchError(404, 'Session no longer exists.');
331
567
  return (0, protocol_1.projectSessionSummary)(updated);
332
568
  }
333
- async switchWorkspace(path) {
334
- if (this.transition)
335
- throw new WebWorkbenchError(409, 'A workspace transition is running.');
569
+ async switchWorkspace(path, context) {
570
+ this.assertContextGuard(context);
336
571
  const next = canonicalDirectory(path);
337
572
  if (next === this.workspaceValue)
338
573
  return;
339
- this.assertReadyForTransition('switch workspaces');
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;
340
607
  const previousRuntime = this.runtimeValue;
341
608
  const previousController = this.controllerValue;
342
- const previousWorkspace = this.workspaceValue;
343
609
  const transition = (async () => {
344
- await previousController.stopActiveTurn();
345
- await previousController.waitForIdle();
346
- await previousRuntime.shutdown();
610
+ this.suppressContextEdges += 1;
347
611
  try {
348
- await this.installRuntime(next);
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 });
349
620
  }
350
- catch (error) {
621
+ catch (activationError) {
351
622
  try {
352
- await this.installRuntime(previousWorkspace);
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);
353
629
  }
354
630
  catch {
355
- throw new WebWorkbenchError(503, 'Workspace activation failed and the previous workspace could not be restored.');
631
+ throw new errors_1.WebWorkbenchError(503, 'Context activation failed and the previous Context could not be restored.', 'context_recovery_required');
356
632
  }
357
- throw error;
633
+ throw activationError;
358
634
  }
359
- const describe = this.runtimeValue.describeSettings;
360
- if (!describe) {
361
- throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
635
+ finally {
636
+ this.suppressContextEdges -= 1;
362
637
  }
363
- const settings = describe();
364
- this.eventHub.emit({
365
- type: 'settings_invalidated',
366
- revision: settings.revision,
367
- reason: 'workspace-change',
368
- state: settings.state === 'ready' || settings.state === 'read-only' ? 'ready' : 'invalid',
369
- }, false);
638
+ this.emitState();
639
+ this.emitWorkspaceResourceInvalidation('context-change');
640
+ if (targetWorkspace !== previousWorkspace)
641
+ this.emitSettingsWorkspaceChange();
370
642
  })().finally(() => {
371
643
  if (this.transition === transition)
372
644
  this.transition = undefined;
@@ -377,6 +649,7 @@ class WebWorkbenchController {
377
649
  dispatch(raw) {
378
650
  const command = (0, protocol_1.parseWebCommand)(raw);
379
651
  return this.executeMutation(command.requestId, 'command', command, () => {
652
+ this.assertCommandSession(command.expectedSessionId);
380
653
  const runtimeResult = this.controllerValue.handle((0, protocol_1.toAgentRuntimeInput)(command));
381
654
  return Object.freeze({
382
655
  requestId: command.requestId,
@@ -389,7 +662,7 @@ class WebWorkbenchController {
389
662
  this.assertSettingsAvailable();
390
663
  const update = this.runtimeValue.updateSettings;
391
664
  if (!update) {
392
- throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
665
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
393
666
  }
394
667
  try {
395
668
  const result = await update({
@@ -411,7 +684,7 @@ class WebWorkbenchController {
411
684
  this.assertSettingsAvailable();
412
685
  const coordinator = this.runtimeValue.settingsCoordinator;
413
686
  if (!coordinator) {
414
- throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
687
+ throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
415
688
  }
416
689
  try {
417
690
  await coordinator.openDocument();
@@ -423,24 +696,24 @@ class WebWorkbenchController {
423
696
  }
424
697
  executeMutation(requestId, operation, payload, action) {
425
698
  if (!requestId.trim() || requestId.length > 128) {
426
- throw new WebWorkbenchError(400, 'requestId must be a non-empty string up to 128 characters.');
699
+ throw new errors_1.WebWorkbenchError(400, 'requestId must be a non-empty string up to 128 characters.');
427
700
  }
428
701
  const fingerprint = (0, canonical_1.digestRuntimeValue)({ operation, payload });
429
702
  const cached = this.mutationResults.get(requestId);
430
703
  if (cached) {
431
704
  if (cached.fingerprint !== fingerprint) {
432
- throw new WebWorkbenchError(409, 'requestId was already used for another mutation.', 'request_id_conflict');
705
+ throw new errors_1.WebWorkbenchError(409, 'requestId was already used for another mutation.', 'request_id_conflict');
433
706
  }
434
707
  return cached.result;
435
708
  }
436
- const result = Promise.resolve().then(action);
437
- this.mutationResults.set(requestId, { fingerprint, result });
438
- while (this.mutationResults.size > 512) {
439
- const oldest = this.mutationResults.keys().next().value;
440
- if (!oldest)
441
- break;
442
- this.mutationResults.delete(oldest);
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');
443
711
  }
712
+ const result = Promise.resolve().then(() => {
713
+ this.assertMutationAdmission();
714
+ return action();
715
+ });
716
+ this.mutationResults.set(requestId, { fingerprint, result });
444
717
  return result;
445
718
  }
446
719
  async diagnostics() {
@@ -478,21 +751,29 @@ class WebWorkbenchController {
478
751
  this.closed = true;
479
752
  await this.controllerValue.stopActiveTurn();
480
753
  await this.controllerValue.waitForIdle();
754
+ this.closeWorkspaceWatchers();
755
+ await this.terminalManager.shutdown();
481
756
  await this.runtimeValue.shutdown();
482
757
  this.eventHub.close();
483
758
  }
484
- async installRuntime(workspace) {
759
+ async installRuntime(workspace, publishState = true) {
485
760
  const runtime = await this.createRuntime(workspace);
761
+ this.closeWorkspaceWatchers();
486
762
  this.activeOrionRuntime = undefined;
487
763
  this.sessionViews.clear();
488
764
  this.sessionViewBytes = 0;
489
765
  this.latestStatus = 'Ready';
490
766
  this.workspaceValue = workspace;
491
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));
492
771
  const eventSink = {
493
772
  emit: (event) => {
494
773
  if (event.type === 'status_changed')
495
774
  this.latestStatus = event.message;
775
+ if (this.suppressContextEdges > 0)
776
+ return undefined;
496
777
  const activeSessionId = this.runtimeValue.getSession()?.id;
497
778
  const activeThread = activeSessionId && this.activeOrionRuntime?.sessionId === activeSessionId
498
779
  ? this.activeOrionRuntime.runtime.thread.getProjection().threadId
@@ -503,6 +784,9 @@ class WebWorkbenchController {
503
784
  });
504
785
  if (event.type === 'session_restored')
505
786
  this.emitState();
787
+ if (event.type === 'tool_finished') {
788
+ this.scheduleWorkspaceResourceInvalidation('tool-finished');
789
+ }
506
790
  return event.type === 'transcript_append' ? `web-entry-${envelope.cursor}` : undefined;
507
791
  },
508
792
  };
@@ -522,15 +806,104 @@ class WebWorkbenchController {
522
806
  suppressAbortNotice: true,
523
807
  },
524
808
  });
525
- this.emitState();
809
+ this.installWorkspaceWatchers();
810
+ if (publishState)
811
+ this.emitState(false);
526
812
  }
527
- emitState() {
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();
528
819
  this.eventHub.emit({
529
820
  type: 'workbench_state',
821
+ contextRevision: this.contextRevisionValue,
822
+ workspaceId: workspace.id,
530
823
  workspace: this.workspaceValue,
531
824
  activeSessionId: this.runtimeValue.getSession()?.id ?? null,
532
825
  });
533
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
+ }
534
907
  observeThreadRuntime(runtime, sessionId, activation) {
535
908
  let replaying = true;
536
909
  const pending = [];
@@ -612,31 +985,91 @@ class WebWorkbenchController {
612
985
  }
613
986
  assertReadyForTransition(operation) {
614
987
  if (this.closed)
615
- throw new WebWorkbenchError(503, 'Web Workbench is closed.');
616
- if (this.transition) {
617
- throw new WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
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');
618
991
  }
619
992
  if (this.controllerValue.hasActiveTurn()) {
620
- throw new WebWorkbenchError(409, `Cannot ${operation} while a turn is active.`, 'runtime_busy');
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');
621
1018
  }
622
1019
  }
623
1020
  assertSettingsAvailable() {
624
1021
  if (this.closed) {
625
- throw new WebWorkbenchError(503, 'Web Workbench is closed.', 'settings_document_unavailable');
1022
+ throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.', 'settings_document_unavailable');
626
1023
  }
627
1024
  if (this.transition) {
628
- throw new WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
1025
+ throw new errors_1.WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
629
1026
  }
630
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
+ }
631
1055
  }
632
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
+ }
633
1066
  function mapSettingsError(error) {
634
- if (error instanceof WebWorkbenchError)
1067
+ if (error instanceof errors_1.WebWorkbenchError)
635
1068
  return error;
636
1069
  if (error instanceof settings_coordinator_1.SettingsCoordinatorError) {
637
- return new WebWorkbenchError(error.status, error.message, error.code);
1070
+ return new errors_1.WebWorkbenchError(error.status, error.message, error.code);
638
1071
  }
639
- return new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
1072
+ return new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
640
1073
  }
641
1074
  function pickPlanReceipt(receipt) {
642
1075
  return Object.freeze({
@@ -687,11 +1120,11 @@ function estimateSessionViewBytes(view) {
687
1120
  }
688
1121
  function pageItems(items, cursor, pageSize = 50) {
689
1122
  if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
690
- throw new WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
1123
+ throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
691
1124
  }
692
1125
  const offset = cursor ? decodePageCursor(cursor) : 0;
693
1126
  if (offset > items.length)
694
- throw new WebWorkbenchError(400, 'Page cursor is out of range.');
1127
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is out of range.');
695
1128
  const page = items.slice(offset, offset + pageSize);
696
1129
  const nextOffset = offset + page.length;
697
1130
  return Object.freeze({
@@ -699,13 +1132,45 @@ function pageItems(items, cursor, pageSize = 50) {
699
1132
  nextCursor: nextOffset < items.length ? encodePageCursor(nextOffset) : null,
700
1133
  });
701
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
+ }
702
1167
  function pageTranscriptTail(items, cursor, pageSize = 50) {
703
1168
  if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
704
- throw new WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
1169
+ throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
705
1170
  }
706
1171
  const end = cursor ? decodePageCursor(cursor) : items.length;
707
1172
  if (end > items.length)
708
- throw new WebWorkbenchError(400, 'Page cursor is out of range.');
1173
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is out of range.');
709
1174
  const offset = Math.max(0, end - pageSize);
710
1175
  return Object.freeze({
711
1176
  items: Object.freeze(items.slice(offset, end)),
@@ -716,9 +1181,37 @@ function pageTranscriptTail(items, cursor, pageSize = 50) {
716
1181
  function encodePageCursor(offset) {
717
1182
  return Buffer.from(JSON.stringify({ version: 1, offset })).toString('base64url');
718
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
+ }
719
1212
  function decodePageCursor(cursor) {
720
1213
  if (!cursor || cursor.length > 512)
721
- throw new WebWorkbenchError(400, 'Page cursor is invalid.');
1214
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is invalid.');
722
1215
  try {
723
1216
  const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
724
1217
  if (!parsed ||
@@ -731,7 +1224,7 @@ function decodePageCursor(cursor) {
731
1224
  return parsed.offset;
732
1225
  }
733
1226
  catch {
734
- throw new WebWorkbenchError(400, 'Page cursor is invalid.');
1227
+ throw new errors_1.WebWorkbenchError(400, 'Page cursor is invalid.');
735
1228
  }
736
1229
  }
737
1230
  function canonicalDirectory(path) {
@@ -741,26 +1234,26 @@ function canonicalDirectory(path) {
741
1234
  canonical = (0, fs_1.realpathSync)(candidate);
742
1235
  }
743
1236
  catch {
744
- throw new WebWorkbenchError(400, 'Workspace path does not exist.');
1237
+ throw new errors_1.WebWorkbenchError(400, 'Workspace path does not exist.');
745
1238
  }
746
1239
  try {
747
1240
  if (!(0, fs_1.statSync)(canonical).isDirectory()) {
748
- throw new WebWorkbenchError(400, 'Workspace path must be a directory.');
1241
+ throw new errors_1.WebWorkbenchError(400, 'Workspace path must be a directory.');
749
1242
  }
750
1243
  }
751
1244
  catch (error) {
752
- if (error instanceof WebWorkbenchError)
1245
+ if (error instanceof errors_1.WebWorkbenchError)
753
1246
  throw error;
754
- throw new WebWorkbenchError(400, 'Workspace path is not readable.');
1247
+ throw new errors_1.WebWorkbenchError(400, 'Workspace path is not readable.');
755
1248
  }
756
1249
  return canonical;
757
1250
  }
758
1251
  function requireSession(sessionId) {
759
1252
  if (!sessionId.trim())
760
- throw new WebWorkbenchError(400, 'Session id is required.');
1253
+ throw new errors_1.WebWorkbenchError(400, 'Session id is required.');
761
1254
  const session = (0, session_storage_1.loadSessionMeta)(sessionId);
762
1255
  if (!session)
763
- throw new WebWorkbenchError(404, 'Session was not found.');
1256
+ throw new errors_1.WebWorkbenchError(404, 'Session was not found.');
764
1257
  return session;
765
1258
  }
766
1259
  function mcpServerIds(config) {