@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.
- package/CHANGELOG.md +42 -0
- package/README.md +24 -8
- package/README.zh-CN.md +23 -9
- package/dist/core/tool-artifacts.d.ts +2 -0
- package/dist/core/tool-artifacts.d.ts.map +1 -1
- package/dist/core/tool-artifacts.js +27 -1
- package/dist/core/tool-artifacts.js.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +1 -2
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +4 -0
- package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-protocol.js.map +1 -1
- package/dist/runtime/durable-tool-receipt-reader.d.ts +32 -0
- package/dist/runtime/durable-tool-receipt-reader.d.ts.map +1 -0
- package/dist/runtime/durable-tool-receipt-reader.js +130 -0
- package/dist/runtime/durable-tool-receipt-reader.js.map +1 -0
- package/dist/runtime/legacy-thread-materializer.d.ts +14 -1
- package/dist/runtime/legacy-thread-materializer.d.ts.map +1 -1
- package/dist/runtime/legacy-thread-materializer.js +44 -5
- package/dist/runtime/legacy-thread-materializer.js.map +1 -1
- package/dist/runtime/release-receipts.d.ts +6 -3
- package/dist/runtime/release-receipts.d.ts.map +1 -1
- package/dist/runtime/release-receipts.js +47 -4
- package/dist/runtime/release-receipts.js.map +1 -1
- package/dist/runtime/thread-event-store.d.ts +62 -9
- package/dist/runtime/thread-event-store.d.ts.map +1 -1
- package/dist/runtime/thread-event-store.js +363 -31
- package/dist/runtime/thread-event-store.js.map +1 -1
- package/dist/runtime/thread-session-index.d.ts +84 -0
- package/dist/runtime/thread-session-index.d.ts.map +1 -0
- package/dist/runtime/thread-session-index.js +503 -0
- package/dist/runtime/thread-session-index.js.map +1 -0
- package/dist/runtime/thread-session-view.d.ts +31 -13
- package/dist/runtime/thread-session-view.d.ts.map +1 -1
- package/dist/runtime/thread-session-view.js +116 -61
- package/dist/runtime/thread-session-view.js.map +1 -1
- package/dist/runtime/thread-ui-adapter.d.ts.map +1 -1
- package/dist/runtime/thread-ui-adapter.js +18 -58
- package/dist/runtime/thread-ui-adapter.js.map +1 -1
- package/dist/runtime/tool-detail-repository.d.ts.map +1 -1
- package/dist/runtime/tool-detail-repository.js +23 -19
- package/dist/runtime/tool-detail-repository.js.map +1 -1
- package/dist/runtime/tool-receipt-validator.d.ts +21 -0
- package/dist/runtime/tool-receipt-validator.d.ts.map +1 -0
- package/dist/runtime/tool-receipt-validator.js +157 -0
- package/dist/runtime/tool-receipt-validator.js.map +1 -0
- package/dist/services/redaction.d.ts +4 -0
- package/dist/services/redaction.d.ts.map +1 -1
- package/dist/services/redaction.js +63 -0
- package/dist/services/redaction.js.map +1 -1
- package/dist/services/session-index.d.ts +16 -0
- package/dist/services/session-index.d.ts.map +1 -1
- package/dist/services/session-index.js +45 -36
- package/dist/services/session-index.js.map +1 -1
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +3 -6
- package/dist/services/session-storage.js.map +1 -1
- package/dist/services/workspace-registry.d.ts +42 -0
- package/dist/services/workspace-registry.d.ts.map +1 -0
- package/dist/services/workspace-registry.js +222 -0
- package/dist/services/workspace-registry.js.map +1 -0
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.d.ts.map +1 -0
- package/dist/web/errors.js +13 -0
- package/dist/web/errors.js.map +1 -0
- package/dist/web/event-hub.d.ts +4 -0
- package/dist/web/event-hub.d.ts.map +1 -1
- package/dist/web/event-hub.js +71 -11
- package/dist/web/event-hub.js.map +1 -1
- package/dist/web/file-read-service.d.ts +55 -0
- package/dist/web/file-read-service.d.ts.map +1 -0
- package/dist/web/file-read-service.js +399 -0
- package/dist/web/file-read-service.js.map +1 -0
- package/dist/web/git-read-model-service.d.ts +77 -0
- package/dist/web/git-read-model-service.d.ts.map +1 -0
- package/dist/web/git-read-model-service.js +631 -0
- package/dist/web/git-read-model-service.js.map +1 -0
- package/dist/web/protocol.d.ts +63 -1
- package/dist/web/protocol.d.ts.map +1 -1
- package/dist/web/protocol.js +22 -1
- package/dist/web/protocol.js.map +1 -1
- package/dist/web/review-service.d.ts +38 -0
- package/dist/web/review-service.d.ts.map +1 -0
- package/dist/web/review-service.js +74 -0
- package/dist/web/review-service.js.map +1 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +273 -27
- package/dist/web/server.js.map +1 -1
- package/dist/web/terminal-manager.d.ts +155 -0
- package/dist/web/terminal-manager.d.ts.map +1 -0
- package/dist/web/terminal-manager.js +728 -0
- package/dist/web/terminal-manager.js.map +1 -0
- package/dist/web/terminal-server.d.ts +8 -0
- package/dist/web/terminal-server.d.ts.map +1 -0
- package/dist/web/terminal-server.js +275 -0
- package/dist/web/terminal-server.js.map +1 -0
- package/dist/web/workbench-controller.d.ts +48 -12
- package/dist/web/workbench-controller.d.ts.map +1 -1
- package/dist/web/workbench-controller.js +651 -158
- package/dist/web/workbench-controller.js.map +1 -1
- package/dist/web-client/assets/DiffViewer-DpKoD07C.js +5 -0
- package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +2 -0
- package/dist/web-client/assets/GitPanel-D20StcdG.js +1 -0
- package/dist/web-client/assets/ReviewPanel-3MVWbMQV.js +1 -0
- package/dist/web-client/assets/TerminalPanel-BLQ592Fb.js +21 -0
- package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
- package/dist/web-client/assets/index-BkIDl_xk.js +16 -0
- package/dist/web-client/assets/index-DXMpnWE8.css +1 -0
- package/dist/web-client/index.html +2 -2
- package/docs/architecture/v0.3.0-web-api.yaml +44 -13
- package/docs/architecture/v0.3.1-web-api.yaml +2290 -0
- package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
- package/docs/plan/v0.3.0-web-workbench-plan.md +58 -7
- package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +848 -0
- package/docs/readme.md +10 -0
- package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
- package/npm-shrinkwrap.json +185 -354
- package/package.json +16 -4
- package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 =
|
|
177
|
-
? uniqueCommitProjections([
|
|
178
|
-
:
|
|
179
|
-
?
|
|
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
|
-
|
|
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
|
-
? {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
await previousController.waitForIdle();
|
|
346
|
-
await previousRuntime.shutdown();
|
|
610
|
+
this.suppressContextEdges += 1;
|
|
347
611
|
try {
|
|
348
|
-
await
|
|
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 (
|
|
621
|
+
catch (activationError) {
|
|
351
622
|
try {
|
|
352
|
-
|
|
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, '
|
|
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
|
|
633
|
+
throw activationError;
|
|
358
634
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
635
|
+
finally {
|
|
636
|
+
this.suppressContextEdges -= 1;
|
|
362
637
|
}
|
|
363
|
-
|
|
364
|
-
this.
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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.
|
|
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) {
|