@orion-agents/orion-code 0.3.0 → 0.3.2
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 +85 -0
- package/README.md +45 -18
- package/README.zh-CN.md +39 -18
- package/dist/commands/model-command-handlers.d.ts.map +1 -1
- package/dist/commands/model-command-handlers.js +59 -14
- package/dist/commands/model-command-handlers.js.map +1 -1
- package/dist/commands/types.d.ts +6 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/workflow-command-handlers.d.ts +1 -1
- package/dist/commands/workflow-command-handlers.d.ts.map +1 -1
- package/dist/commands/workflow-command-handlers.js +42 -3
- package/dist/commands/workflow-command-handlers.js.map +1 -1
- package/dist/commands/workflow-commands.d.ts.map +1 -1
- package/dist/commands/workflow-commands.js +2 -2
- package/dist/commands/workflow-commands.js.map +1 -1
- 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.d.ts +16 -2
- package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
- package/dist/runtime/agent-runtime-controller.js +116 -8
- package/dist/runtime/agent-runtime-controller.js.map +1 -1
- package/dist/runtime/agent-runtime-protocol.d.ts +7 -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/agent-runtime-runner.d.ts +8 -0
- package/dist/runtime/agent-runtime-runner.d.ts.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/orion-session-runner.d.ts +4 -0
- package/dist/runtime/orion-session-runner.d.ts.map +1 -1
- package/dist/runtime/orion-session-runner.js +15 -0
- package/dist/runtime/orion-session-runner.js.map +1 -1
- package/dist/runtime/plan-review.d.ts +38 -0
- package/dist/runtime/plan-review.d.ts.map +1 -0
- package/dist/runtime/plan-review.js +173 -0
- package/dist/runtime/plan-review.js.map +1 -0
- package/dist/runtime/product-bootstrap.d.ts.map +1 -1
- package/dist/runtime/product-bootstrap.js +28 -6
- package/dist/runtime/product-bootstrap.js.map +1 -1
- package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
- package/dist/runtime/product-orion-runtime.js +14 -35
- package/dist/runtime/product-orion-runtime.js.map +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.d.ts +73 -0
- package/dist/runtime/protocol/runtime-protocol-v1.d.ts.map +1 -1
- package/dist/runtime/protocol/runtime-protocol-v1.js +37 -0
- package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
- package/dist/runtime/release-receipts.d.ts +8 -3
- package/dist/runtime/release-receipts.d.ts.map +1 -1
- package/dist/runtime/release-receipts.js +88 -4
- package/dist/runtime/release-receipts.js.map +1 -1
- package/dist/runtime/session-composer-control.d.ts +136 -0
- package/dist/runtime/session-composer-control.d.ts.map +1 -0
- package/dist/runtime/session-composer-control.js +532 -0
- package/dist/runtime/session-composer-control.js.map +1 -0
- 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-projection.d.ts +17 -0
- package/dist/runtime/thread-projection.d.ts.map +1 -1
- package/dist/runtime/thread-projection.js +87 -0
- package/dist/runtime/thread-projection.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/runtime/ui-events.d.ts +6 -1
- package/dist/runtime/ui-events.d.ts.map +1 -1
- package/dist/runtime/ui-events.js.map +1 -1
- 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 +36 -0
- package/dist/services/session-storage.d.ts.map +1 -1
- package/dist/services/session-storage.js +80 -7
- 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 +81 -11
- package/dist/web/event-hub.js.map +1 -1
- package/dist/web/file-read-service.d.ts +64 -0
- package/dist/web/file-read-service.d.ts.map +1 -0
- package/dist/web/file-read-service.js +409 -0
- package/dist/web/file-read-service.js.map +1 -0
- package/dist/web/git-read-model-service.d.ts +85 -0
- package/dist/web/git-read-model-service.d.ts.map +1 -0
- package/dist/web/git-read-model-service.js +657 -0
- package/dist/web/git-read-model-service.js.map +1 -0
- package/dist/web/protocol.d.ts +205 -1
- package/dist/web/protocol.d.ts.map +1 -1
- package/dist/web/protocol.js +229 -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 +312 -29
- package/dist/web/server.js.map +1 -1
- package/dist/web/terminal-manager.d.ts +156 -0
- package/dist/web/terminal-manager.d.ts.map +1 -0
- package/dist/web/terminal-manager.js +760 -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 +71 -18
- package/dist/web/workbench-controller.d.ts.map +1 -1
- package/dist/web/workbench-controller.js +1187 -178
- package/dist/web/workbench-controller.js.map +1 -1
- package/dist/web-client/assets/DiffViewer-eaQNsnCh.js +5 -0
- package/dist/web-client/assets/FilesPanel-DYs1MhoM.js +2 -0
- package/dist/web-client/assets/GitPanel-CnU4XVr7.js +1 -0
- package/dist/web-client/assets/ReviewPanel-BDxG670T.js +1 -0
- package/dist/web-client/assets/TerminalPanel-DLuoa74B.css +1 -0
- package/dist/web-client/assets/TerminalPanel-Q5mSSsoK.js +23 -0
- package/dist/web-client/assets/index-BWV5sIvw.css +1 -0
- package/dist/web-client/assets/index-VVUKj6FE.js +17 -0
- package/dist/web-client/index.html +2 -2
- package/docs/architecture/agent-mode-permission-contract.md +87 -0
- package/docs/architecture/v0.3.0-web-api.yaml +44 -13
- package/docs/architecture/v0.3.1-web-api.yaml +2338 -0
- package/docs/architecture/v0.3.2-web-api.yaml +3029 -0
- package/docs/migration/v0.3.0-to-v0.3.1.md +88 -0
- package/docs/migration/v0.3.1-to-v0.3.2.md +94 -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/plan/v0.3.2-web-workbench-layout-and-composer-plan.md +869 -0
- package/docs/readme.md +20 -0
- package/docs/test/v0.3.1-web-workbench-e2e-plan.md +140 -0
- package/docs/test/v0.3.2-web-workbench-e2e-plan.md +100 -0
- package/npm-shrinkwrap.json +185 -354
- package/package.json +21 -4
- package/dist/web-client/assets/index-BAEO71Gp.css +0 -1
- package/dist/web-client/assets/index-DSkFy7gM.js +0 -13
|
@@ -2,30 +2,39 @@
|
|
|
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 plan_review_1 = require("../runtime/plan-review");
|
|
15
|
+
const thread_session_index_1 = require("../runtime/thread-session-index");
|
|
11
16
|
const thread_session_view_1 = require("../runtime/thread-session-view");
|
|
12
17
|
const tool_detail_repository_1 = require("../runtime/tool-detail-repository");
|
|
13
18
|
const turn_commit_1 = require("../runtime/turn-commit");
|
|
19
|
+
const session_composer_control_1 = require("../runtime/session-composer-control");
|
|
14
20
|
const global_config_1 = require("../services/global-config");
|
|
21
|
+
const redaction_1 = require("../services/redaction");
|
|
15
22
|
const settings_coordinator_1 = require("../services/settings-coordinator");
|
|
16
23
|
const session_storage_1 = require("../services/session-storage");
|
|
24
|
+
const workspace_registry_1 = require("../services/workspace-registry");
|
|
25
|
+
const errors_1 = require("./errors");
|
|
17
26
|
const event_hub_1 = require("./event-hub");
|
|
27
|
+
const file_read_service_1 = require("./file-read-service");
|
|
28
|
+
const git_read_model_service_1 = require("./git-read-model-service");
|
|
18
29
|
const protocol_1 = require("./protocol");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.code = code;
|
|
24
|
-
this.name = 'WebWorkbenchError';
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
exports.WebWorkbenchError = WebWorkbenchError;
|
|
30
|
+
const review_service_1 = require("./review-service");
|
|
31
|
+
const terminal_manager_1 = require("./terminal-manager");
|
|
32
|
+
var errors_2 = require("./errors");
|
|
33
|
+
Object.defineProperty(exports, "WebWorkbenchError", { enumerable: true, get: function () { return errors_2.WebWorkbenchError; } });
|
|
28
34
|
const SESSION_VIEW_CACHE_BYTES = 16 * 1024 * 1024;
|
|
35
|
+
const MAX_MUTATION_RESULTS = 4096;
|
|
36
|
+
const MAX_CONTEXT_REFERENCE_BYTES = 64 * 1024;
|
|
37
|
+
const MAX_CONTEXT_MANIFEST_BYTES = 256 * 1024;
|
|
29
38
|
/** Owns the sole runtime/controller pair exposed through the local Web host. */
|
|
30
39
|
class WebWorkbenchController {
|
|
31
40
|
constructor(options) {
|
|
@@ -33,10 +42,29 @@ class WebWorkbenchController {
|
|
|
33
42
|
this.sessionViews = new Map();
|
|
34
43
|
this.sessionViewBytes = 0;
|
|
35
44
|
this.toolDetails = new tool_detail_repository_1.FileToolDetailRepository();
|
|
45
|
+
this.contextRevisionValue = (0, crypto_1.randomUUID)();
|
|
46
|
+
this.composerRevisionValue = (0, crypto_1.randomUUID)();
|
|
47
|
+
this.suppressComposerEdges = 0;
|
|
48
|
+
this.suppressContextEdges = 0;
|
|
36
49
|
this.latestStatus = 'Ready';
|
|
50
|
+
this.sessionTransition = false;
|
|
37
51
|
this.closed = false;
|
|
52
|
+
this.workspaceWatchers = [];
|
|
38
53
|
this.workspaceValue = canonicalDirectory(options.cwd);
|
|
39
54
|
this.eventHub = options.eventHub ?? new event_hub_1.WebEventHub();
|
|
55
|
+
this.workspaceRegistry = options.workspaceRegistry ?? new workspace_registry_1.WorkspaceRegistryV1();
|
|
56
|
+
this.terminalManager = new terminal_manager_1.TerminalManagerV1({
|
|
57
|
+
resolveWorkspace: workspaceId => this.workspaceRegistry.find(workspaceId)?.canonicalPath,
|
|
58
|
+
getActiveContext: () => ({
|
|
59
|
+
workspaceId: this.activeWorkspaceEntry().id,
|
|
60
|
+
contextRevision: this.contextRevisionValue,
|
|
61
|
+
}),
|
|
62
|
+
onWorkspaceMutationHint: workspaceId => {
|
|
63
|
+
if (workspaceId === this.activeWorkspaceEntry().id) {
|
|
64
|
+
this.scheduleWorkspaceResourceInvalidation('terminal-command');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
40
68
|
this.createRuntime =
|
|
41
69
|
options.createRuntime ??
|
|
42
70
|
(cwd => (0, product_bootstrap_1.createProductUiRuntime)({
|
|
@@ -44,6 +72,8 @@ class WebWorkbenchController {
|
|
|
44
72
|
shutdownReason: 'Orion Web Workbench shutdown',
|
|
45
73
|
onActiveSessionRuntime: (runtime, sessionId, activation) => this.observeThreadRuntime(runtime, sessionId, activation),
|
|
46
74
|
onSettingsInvalidated: event => {
|
|
75
|
+
if (this.suppressContextEdges > 0)
|
|
76
|
+
return;
|
|
47
77
|
this.eventHub.emit({
|
|
48
78
|
type: 'settings_invalidated',
|
|
49
79
|
revision: event.revision,
|
|
@@ -55,6 +85,7 @@ class WebWorkbenchController {
|
|
|
55
85
|
}
|
|
56
86
|
static async create(options) {
|
|
57
87
|
const workbench = new WebWorkbenchController(options);
|
|
88
|
+
workbench.initializeWorkspaceRegistry();
|
|
58
89
|
await workbench.installRuntime(workbench.workspaceValue);
|
|
59
90
|
return workbench;
|
|
60
91
|
}
|
|
@@ -67,11 +98,16 @@ class WebWorkbenchController {
|
|
|
67
98
|
get controller() {
|
|
68
99
|
return this.controllerValue;
|
|
69
100
|
}
|
|
101
|
+
get contextRevision() {
|
|
102
|
+
return this.contextRevisionValue;
|
|
103
|
+
}
|
|
70
104
|
bootstrap(nonce) {
|
|
71
105
|
return Object.freeze({
|
|
72
106
|
apiVersion: protocol_1.WEB_API_VERSION,
|
|
73
107
|
productVersion: this.runtimeValue.version,
|
|
74
108
|
nonce,
|
|
109
|
+
contextRevision: this.contextRevisionValue,
|
|
110
|
+
workspaceId: this.activeWorkspaceEntry().id,
|
|
75
111
|
workspace: this.workspaceValue,
|
|
76
112
|
configured: this.runtimeValue.isConfigured,
|
|
77
113
|
activeSessionId: this.runtimeValue.getSession()?.id ?? null,
|
|
@@ -82,6 +118,10 @@ class WebWorkbenchController {
|
|
|
82
118
|
skills: true,
|
|
83
119
|
mcp: true,
|
|
84
120
|
diagnostics: true,
|
|
121
|
+
review: true,
|
|
122
|
+
files: true,
|
|
123
|
+
git: true,
|
|
124
|
+
terminal: this.terminalManager.available,
|
|
85
125
|
},
|
|
86
126
|
});
|
|
87
127
|
}
|
|
@@ -89,7 +129,7 @@ class WebWorkbenchController {
|
|
|
89
129
|
this.assertSettingsAvailable();
|
|
90
130
|
const describe = this.runtimeValue.describeSettings;
|
|
91
131
|
if (!describe) {
|
|
92
|
-
throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
132
|
+
throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
93
133
|
}
|
|
94
134
|
try {
|
|
95
135
|
return describe();
|
|
@@ -98,86 +138,218 @@ class WebWorkbenchController {
|
|
|
98
138
|
throw mapSettingsError(error);
|
|
99
139
|
}
|
|
100
140
|
}
|
|
101
|
-
listWorkspaces() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
listWorkspaces(context) {
|
|
142
|
+
if (context)
|
|
143
|
+
this.assertContextGuard(context);
|
|
144
|
+
// Session metadata is canonicalized before it enters the catalog. The
|
|
145
|
+
// summary read model deliberately avoids sorting Sessions or re-running
|
|
146
|
+
// realpath/stat for every historical entry. Availability is checked once
|
|
147
|
+
// per registered Workspace below.
|
|
148
|
+
const counts = (0, session_storage_1.countSessionsByProject)();
|
|
149
|
+
const result = Object.freeze(this.workspaceRegistry.list().map(entry => {
|
|
150
|
+
let available = false;
|
|
105
151
|
try {
|
|
106
|
-
|
|
107
|
-
paths.add(path);
|
|
108
|
-
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
152
|
+
available = canonicalDirectory(entry.canonicalPath) === entry.canonicalPath;
|
|
109
153
|
}
|
|
110
154
|
catch {
|
|
111
|
-
|
|
155
|
+
available = false;
|
|
112
156
|
}
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
id: entry.id,
|
|
159
|
+
path: entry.canonicalPath,
|
|
160
|
+
label: entry.label || (0, path_1.basename)(entry.canonicalPath) || entry.canonicalPath,
|
|
161
|
+
active: entry.canonicalPath === this.workspaceValue,
|
|
162
|
+
available,
|
|
163
|
+
sessionCount: counts.get(entry.canonicalPath) ?? 0,
|
|
164
|
+
lastActivatedAt: entry.lastActivatedAt,
|
|
165
|
+
...(entry.pinnedOrder !== undefined ? { pinnedOrder: entry.pinnedOrder } : {}),
|
|
166
|
+
});
|
|
167
|
+
}));
|
|
168
|
+
if (context)
|
|
169
|
+
this.assertContextGuard(context);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
listWorkspaceSessions(workspaceId, context) {
|
|
173
|
+
if (context)
|
|
174
|
+
this.assertContextGuard(context);
|
|
175
|
+
const entry = this.workspaceRegistry.find(workspaceId);
|
|
176
|
+
if (!entry) {
|
|
177
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
113
178
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
179
|
+
try {
|
|
180
|
+
canonicalDirectory(entry.canonicalPath);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new errors_1.WebWorkbenchError(409, 'Workspace is unavailable.', 'workspace_unavailable');
|
|
184
|
+
}
|
|
185
|
+
const result = Object.freeze((0, session_storage_1.listProjectSessions)(entry.canonicalPath).map(protocol_1.projectSessionSummary));
|
|
186
|
+
if (context)
|
|
187
|
+
this.assertContextGuard(context);
|
|
188
|
+
return result;
|
|
120
189
|
}
|
|
121
|
-
|
|
122
|
-
|
|
190
|
+
async workspaceProjectSummary(workspaceId, context) {
|
|
191
|
+
if (context)
|
|
192
|
+
this.assertContextGuard(context);
|
|
193
|
+
const entry = this.workspaceRegistry.find(workspaceId);
|
|
194
|
+
if (!entry) {
|
|
195
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
196
|
+
}
|
|
197
|
+
let workspace;
|
|
198
|
+
try {
|
|
199
|
+
workspace = canonicalDirectory(entry.canonicalPath);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new errors_1.WebWorkbenchError(409, 'Workspace is unavailable.', 'workspace_unavailable');
|
|
203
|
+
}
|
|
204
|
+
const status = await new git_read_model_service_1.GitReadModelServiceV1(workspace).status({ pageSize: 2000 });
|
|
205
|
+
const result = Object.freeze({
|
|
206
|
+
workspaceId,
|
|
207
|
+
repositoryRevision: status.repositoryRevision,
|
|
208
|
+
isRepository: status.isRepository,
|
|
209
|
+
branch: status.branch,
|
|
210
|
+
detached: status.detached,
|
|
211
|
+
head: status.head,
|
|
212
|
+
dirtyCount: status.totalFiles,
|
|
213
|
+
conflictCount: status.conflicted.length,
|
|
214
|
+
});
|
|
215
|
+
if (context)
|
|
216
|
+
this.assertContextGuard(context);
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
setWorkspacePinned(workspaceId, pinned, context) {
|
|
220
|
+
this.assertContextGuard(context);
|
|
221
|
+
try {
|
|
222
|
+
this.workspaceRegistry.setPinned(workspaceId, pinned);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw mapWorkspaceRegistryError(error);
|
|
226
|
+
}
|
|
227
|
+
const updated = this.listWorkspaces().find(workspace => workspace.id === workspaceId);
|
|
228
|
+
if (!updated) {
|
|
229
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
230
|
+
}
|
|
231
|
+
return updated;
|
|
232
|
+
}
|
|
233
|
+
removeWorkspace(workspaceId, context) {
|
|
234
|
+
this.assertContextGuard(context);
|
|
235
|
+
const entry = this.workspaceRegistry.find(workspaceId);
|
|
236
|
+
if (!entry) {
|
|
237
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
238
|
+
}
|
|
239
|
+
if (entry.canonicalPath === this.workspaceValue) {
|
|
240
|
+
throw new errors_1.WebWorkbenchError(409, 'The active Workspace cannot be removed.', 'workspace_active');
|
|
241
|
+
}
|
|
242
|
+
this.terminalManager.closeWorkspace(workspaceId);
|
|
243
|
+
if (!this.workspaceRegistry.remove(workspaceId)) {
|
|
244
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
listSessions(context) {
|
|
248
|
+
if (context)
|
|
249
|
+
this.assertContextGuard(context);
|
|
250
|
+
const result = Object.freeze((0, session_storage_1.listProjectSessions)(this.workspaceValue).map(protocol_1.projectSessionSummary));
|
|
251
|
+
if (context)
|
|
252
|
+
this.assertContextGuard(context);
|
|
253
|
+
return result;
|
|
123
254
|
}
|
|
124
|
-
sessionSnapshot(sessionId, cursor, pageSize = 50, tail = false) {
|
|
255
|
+
sessionSnapshot(sessionId, cursor, pageSize = 50, tail = false, context) {
|
|
256
|
+
if (context)
|
|
257
|
+
this.assertContextGuard(context);
|
|
125
258
|
const session = requireSession(sessionId);
|
|
126
259
|
if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
|
|
127
|
-
throw new WebWorkbenchError(409, 'Session belongs to another workspace.');
|
|
260
|
+
throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
|
|
128
261
|
}
|
|
129
262
|
const active = this.runtimeValue.getSession()?.id === session.id;
|
|
130
263
|
const activeProjection = this.activeOrionRuntime?.sessionId === session.id
|
|
131
264
|
? this.activeOrionRuntime.runtime.thread.getProjection()
|
|
132
265
|
: undefined;
|
|
266
|
+
let indexedPage;
|
|
267
|
+
if (tail) {
|
|
268
|
+
try {
|
|
269
|
+
indexedPage = (0, thread_session_view_1.loadThreadSessionSnapshotPageV1)(this.workspaceValue, session.id, cursor, pageSize);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (error instanceof thread_session_index_1.ThreadSessionIndexError) {
|
|
273
|
+
throw new errors_1.WebWorkbenchError(error.code === 'ORION_THREAD_SESSION_CURSOR_STALE' ? 409 : 400, error.message, error.code === 'ORION_THREAD_SESSION_CURSOR_STALE'
|
|
274
|
+
? 'transcript_cursor_stale'
|
|
275
|
+
: 'page_cursor_invalid');
|
|
276
|
+
}
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (indexedPage &&
|
|
281
|
+
activeProjection &&
|
|
282
|
+
(indexedPage.cursor !== activeProjection.cursor ||
|
|
283
|
+
indexedPage.projectionDigest !== activeProjection.digest)) {
|
|
284
|
+
if (cursor) {
|
|
285
|
+
throw new errors_1.WebWorkbenchError(409, 'Active Thread changed after this transcript cursor was issued.', 'transcript_cursor_stale');
|
|
286
|
+
}
|
|
287
|
+
// Another process may have advanced the durable Thread before the active
|
|
288
|
+
// Runtime has adopted that head. Never combine the newer page with the
|
|
289
|
+
// older in-memory cursor/digest in one snapshot.
|
|
290
|
+
indexedPage = undefined;
|
|
291
|
+
}
|
|
133
292
|
let view;
|
|
134
|
-
|
|
135
|
-
|
|
293
|
+
if (!indexedPage && !activeProjection) {
|
|
294
|
+
try {
|
|
295
|
+
view = this.loadSessionView(session, activeProjection);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (!activeProjection)
|
|
299
|
+
throw error;
|
|
300
|
+
view = undefined;
|
|
301
|
+
}
|
|
136
302
|
}
|
|
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
|
-
|
|
303
|
+
const transcriptSource = indexedPage
|
|
304
|
+
? indexedPage.transcript.items
|
|
305
|
+
: view
|
|
306
|
+
? view.transcriptMessages
|
|
307
|
+
: activeProjection
|
|
308
|
+
? Object.values(activeProjection.items)
|
|
309
|
+
.filter(item => item.kind === 'message' &&
|
|
310
|
+
item.status !== 'started' &&
|
|
311
|
+
['system', 'user', 'assistant', 'tool'].includes(item.role ?? ''))
|
|
312
|
+
.sort((left, right) => left.startedSeq - right.startedSeq)
|
|
313
|
+
.map(item => ({
|
|
314
|
+
role: item.role,
|
|
315
|
+
content: item.content ?? item.summary ?? item.error ?? '',
|
|
316
|
+
timestamp: item.terminalSeq ?? item.startedSeq,
|
|
317
|
+
}))
|
|
318
|
+
: (0, session_storage_1.readSessionMessages)(session.id).map(message => ({
|
|
319
|
+
role: message.role,
|
|
320
|
+
content: message.content,
|
|
321
|
+
timestamp: message.timestamp,
|
|
322
|
+
...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
|
|
323
|
+
...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
|
|
324
|
+
...(message.modelVisibleContent
|
|
325
|
+
? { modelVisibleContent: message.modelVisibleContent }
|
|
326
|
+
: {}),
|
|
327
|
+
...(message.appliedSkills ? { appliedSkills: message.appliedSkills } : {}),
|
|
328
|
+
}));
|
|
329
|
+
const tailPage = tail && !indexedPage ? pageTranscriptTail(transcriptSource, cursor, pageSize) : undefined;
|
|
330
|
+
const transcriptPage = indexedPage
|
|
331
|
+
? {
|
|
332
|
+
items: indexedPage.transcript.items,
|
|
333
|
+
nextCursor: indexedPage.transcript.nextCursor,
|
|
334
|
+
}
|
|
335
|
+
: (tailPage ?? pageItems(transcriptSource, cursor, pageSize));
|
|
336
|
+
const transcriptOffset = indexedPage
|
|
337
|
+
? indexedPage.transcript.offset
|
|
338
|
+
: (tailPage?.offset ?? (cursor ? decodePageCursor(cursor) : 0));
|
|
169
339
|
const transcript = Object.freeze({
|
|
170
340
|
items: Object.freeze(transcriptPage.items.map((message, index) => ({
|
|
171
341
|
...message,
|
|
172
342
|
id: `${session.id}:message:${transcriptOffset + index + 1}`,
|
|
173
343
|
}))),
|
|
174
|
-
nextCursor: transcriptPage.nextCursor,
|
|
344
|
+
nextCursor: indexedPage?.transcript.nextCursor ?? transcriptPage.nextCursor,
|
|
175
345
|
});
|
|
176
|
-
const commitProjections =
|
|
177
|
-
? uniqueCommitProjections([
|
|
178
|
-
:
|
|
179
|
-
?
|
|
180
|
-
:
|
|
346
|
+
const commitProjections = indexedPage
|
|
347
|
+
? uniqueCommitProjections([indexedPage.latestPlanTurnCommit, indexedPage.latestTurnCommit])
|
|
348
|
+
: view
|
|
349
|
+
? uniqueCommitProjections([view.latestPlanTurnCommit, view.latestTurnCommit])
|
|
350
|
+
: activeProjection
|
|
351
|
+
? latestAuthorityCommits(activeProjection)
|
|
352
|
+
: [];
|
|
181
353
|
const commits = commitProjections.map(commit => (0, turn_commit_1.parseTurnCommitV1)(commit.receipt));
|
|
182
354
|
const latestCommit = commits.at(-1);
|
|
183
355
|
const planCommit = [...commits].reverse().find(commit => commit.planReceipt);
|
|
@@ -194,7 +366,7 @@ class WebWorkbenchController {
|
|
|
194
366
|
allowedScopes: ['once', 'project', 'global'],
|
|
195
367
|
}))
|
|
196
368
|
: [];
|
|
197
|
-
const projectionStatus = activeProjection?.status ?? (view ? 'idle' : 'legacy');
|
|
369
|
+
const projectionStatus = activeProjection?.status ?? (indexedPage || view ? 'idle' : 'legacy');
|
|
198
370
|
const goal = latestCommit?.goalState && latestCommit.goalStateDigest
|
|
199
371
|
? {
|
|
200
372
|
authority: 'turn_commit',
|
|
@@ -202,14 +374,16 @@ class WebWorkbenchController {
|
|
|
202
374
|
state: JSON.parse(latestCommit.goalState),
|
|
203
375
|
}
|
|
204
376
|
: null;
|
|
205
|
-
|
|
377
|
+
const snapshot = Object.freeze({
|
|
206
378
|
apiVersion: protocol_1.WEB_API_VERSION,
|
|
207
379
|
session: (0, protocol_1.projectSessionSummary)(session),
|
|
208
|
-
threadId: activeProjection?.threadId ?? view?.threadId ?? null,
|
|
209
|
-
threadCursor: activeProjection?.cursor ?? view?.cursor ?? 0,
|
|
380
|
+
threadId: activeProjection?.threadId ?? indexedPage?.threadId ?? view?.threadId ?? null,
|
|
381
|
+
threadCursor: activeProjection?.cursor ?? indexedPage?.cursor ?? view?.cursor ?? 0,
|
|
210
382
|
eventCursor: this.eventHub.snapshot().latest,
|
|
211
|
-
...(activeProjection?.digest || view?.projectionDigest
|
|
212
|
-
? {
|
|
383
|
+
...(activeProjection?.digest || indexedPage?.projectionDigest || view?.projectionDigest
|
|
384
|
+
? {
|
|
385
|
+
projectionDigest: activeProjection?.digest ?? indexedPage?.projectionDigest ?? view?.projectionDigest,
|
|
386
|
+
}
|
|
213
387
|
: {}),
|
|
214
388
|
threadStatus: projectionStatus,
|
|
215
389
|
...(activeProjection?.activeTurnId ? { activeTurnId: activeProjection.activeTurnId } : {}),
|
|
@@ -228,12 +402,206 @@ class WebWorkbenchController {
|
|
|
228
402
|
pendingApprovals: Object.freeze(pendingPermissions),
|
|
229
403
|
goal,
|
|
230
404
|
plan,
|
|
231
|
-
|
|
405
|
+
composer: this.projectComposerState(session, active),
|
|
406
|
+
recoveryDiagnostics: indexedPage ? [] : (view?.diagnostics ?? []),
|
|
407
|
+
});
|
|
408
|
+
if (context)
|
|
409
|
+
this.assertContextGuard(context);
|
|
410
|
+
return snapshot;
|
|
411
|
+
}
|
|
412
|
+
composerState(sessionId, context) {
|
|
413
|
+
if (context)
|
|
414
|
+
this.assertContextGuard(context);
|
|
415
|
+
this.assertCommandSession(sessionId);
|
|
416
|
+
const session = requireSession(sessionId);
|
|
417
|
+
const state = this.projectComposerState(session, true);
|
|
418
|
+
if (context)
|
|
419
|
+
this.assertContextGuard(context);
|
|
420
|
+
return state;
|
|
421
|
+
}
|
|
422
|
+
projectComposerState(session, active) {
|
|
423
|
+
const state = this.projectComposerStateValue(session, active);
|
|
424
|
+
if (!active)
|
|
425
|
+
return state;
|
|
426
|
+
// Durable Thread and Store authority can advance before the debounced SSE
|
|
427
|
+
// observer runs. Keep every projected control state and its CAS token in
|
|
428
|
+
// the same synchronous boundary so the UI never receives new controls
|
|
429
|
+
// paired with an older revision.
|
|
430
|
+
const authorityDigest = composerAuthorityDigest(state);
|
|
431
|
+
if (this.composerAuthorityDigest === undefined) {
|
|
432
|
+
this.composerAuthorityDigest = authorityDigest;
|
|
433
|
+
return state;
|
|
434
|
+
}
|
|
435
|
+
if (authorityDigest === this.composerAuthorityDigest)
|
|
436
|
+
return state;
|
|
437
|
+
this.bumpComposerRevision();
|
|
438
|
+
this.composerAuthorityDigest = authorityDigest;
|
|
439
|
+
return Object.freeze({ ...state, controlRevision: this.composerRevisionValue });
|
|
440
|
+
}
|
|
441
|
+
projectComposerStateValue(session, active) {
|
|
442
|
+
const controls = this.runtimeValue.sessionComposerControls;
|
|
443
|
+
if (!controls) {
|
|
444
|
+
throw new errors_1.WebWorkbenchError(503, 'Session Composer controls are unavailable.', 'composer_controls_unavailable');
|
|
445
|
+
}
|
|
446
|
+
const runtime = controls.describeSession(session);
|
|
447
|
+
const queue = active
|
|
448
|
+
? this.controllerValue.getFollowupQueueSnapshot()
|
|
449
|
+
: { items: [], limit: 16 };
|
|
450
|
+
const projection = active && this.activeOrionRuntime?.sessionId === session.id
|
|
451
|
+
? this.activeOrionRuntime.runtime.thread.getProjection()
|
|
452
|
+
: undefined;
|
|
453
|
+
const review = projection?.planReview;
|
|
454
|
+
const workspace = this.activeWorkspaceEntry();
|
|
455
|
+
return Object.freeze({
|
|
456
|
+
apiVersion: protocol_1.WEB_API_VERSION,
|
|
457
|
+
workspaceId: workspace.id,
|
|
458
|
+
sessionId: session.id,
|
|
459
|
+
contextRevision: this.contextRevisionValue,
|
|
460
|
+
controlRevision: this.composerRevisionValue,
|
|
461
|
+
processing: active &&
|
|
462
|
+
(this.runtimeValue.store.getSnapshot().isProcessing || Boolean(projection?.activeTurnId)),
|
|
463
|
+
mode: active
|
|
464
|
+
? Object.freeze({ ...this.controllerValue.getAgentModeSnapshot() })
|
|
465
|
+
: Object.freeze({ baseMode: 'interactive', pendingBaseMode: null }),
|
|
466
|
+
model: runtime.model,
|
|
467
|
+
permission: runtime.permission,
|
|
468
|
+
contextUsage: runtime.contextUsage,
|
|
469
|
+
compactAvailable: active && this.controllerValue.canCompactContext(),
|
|
470
|
+
pending: runtime.pending,
|
|
471
|
+
lastError: runtime.lastError,
|
|
472
|
+
planReview: review
|
|
473
|
+
? Object.freeze({
|
|
474
|
+
planDigest: review.planDigest,
|
|
475
|
+
revision: review.revision,
|
|
476
|
+
status: review.status,
|
|
477
|
+
createdAt: review.createdAt,
|
|
478
|
+
createdModel: review.createdModel,
|
|
479
|
+
returnMode: review.returnMode,
|
|
480
|
+
...(review.resolvedAt === undefined ? {} : { resolvedAt: review.resolvedAt }),
|
|
481
|
+
})
|
|
482
|
+
: null,
|
|
483
|
+
queue: Object.freeze({
|
|
484
|
+
items: Object.freeze(queue.items.map(item => Object.freeze({ ...item }))),
|
|
485
|
+
limit: queue.limit,
|
|
486
|
+
}),
|
|
232
487
|
});
|
|
233
488
|
}
|
|
234
|
-
|
|
489
|
+
modelCatalog(sessionId, context) {
|
|
490
|
+
if (context)
|
|
491
|
+
this.assertContextGuard(context);
|
|
492
|
+
this.assertCommandSession(sessionId);
|
|
493
|
+
const controls = this.runtimeValue.sessionComposerControls;
|
|
494
|
+
if (!controls) {
|
|
495
|
+
throw new errors_1.WebWorkbenchError(503, 'Session model catalog is unavailable.', 'composer_controls_unavailable');
|
|
496
|
+
}
|
|
497
|
+
const catalog = controls.catalog();
|
|
498
|
+
if (context)
|
|
499
|
+
this.assertContextGuard(context);
|
|
500
|
+
return catalog;
|
|
501
|
+
}
|
|
502
|
+
async applyComposerAction(input) {
|
|
503
|
+
this.assertContextGuard(input);
|
|
504
|
+
this.assertCommandSession(input.expectedSessionId);
|
|
505
|
+
const admittedState = this.composerState(input.expectedSessionId);
|
|
506
|
+
if (input.expectedControlRevision !== admittedState.controlRevision) {
|
|
507
|
+
throw new errors_1.WebWorkbenchError(409, 'Composer controls changed before the action was admitted.', 'composer_control_conflict');
|
|
508
|
+
}
|
|
509
|
+
const controls = this.runtimeValue.sessionComposerControls;
|
|
510
|
+
if (!controls) {
|
|
511
|
+
throw new errors_1.WebWorkbenchError(503, 'Session Composer controls are unavailable.', 'composer_controls_unavailable');
|
|
512
|
+
}
|
|
513
|
+
let outcome = 'applied';
|
|
514
|
+
let detail;
|
|
515
|
+
let modelReceipt;
|
|
516
|
+
let permissionReceipt;
|
|
517
|
+
let planReviewReceipt;
|
|
518
|
+
this.suppressComposerEdges += 1;
|
|
519
|
+
try {
|
|
520
|
+
switch (input.type) {
|
|
521
|
+
case 'set_agent_mode': {
|
|
522
|
+
const result = this.controllerValue.setAgentMode(input.mode);
|
|
523
|
+
outcome =
|
|
524
|
+
result.type === 'agent_mode_changed' && result.appliesFrom === 'next-logical-request'
|
|
525
|
+
? 'deferred'
|
|
526
|
+
: 'applied';
|
|
527
|
+
detail = JSON.stringify(result);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
case 'set_permission_override':
|
|
531
|
+
permissionReceipt = await controls.setPermissionOverride(input.value);
|
|
532
|
+
outcome = permissionReceipt.appliesFrom === 'immediate' ? 'applied' : 'deferred';
|
|
533
|
+
break;
|
|
534
|
+
case 'clear_permission_override':
|
|
535
|
+
permissionReceipt = await controls.setPermissionOverride(null);
|
|
536
|
+
outcome = permissionReceipt.appliesFrom === 'immediate' ? 'applied' : 'deferred';
|
|
537
|
+
break;
|
|
538
|
+
case 'select_model':
|
|
539
|
+
modelReceipt = await controls.selectModel({
|
|
540
|
+
modelId: input.modelId,
|
|
541
|
+
...(input.effort ? { effort: input.effort } : {}),
|
|
542
|
+
});
|
|
543
|
+
outcome = modelReceipt.appliesFrom === 'next-logical-request' ? 'deferred' : 'applied';
|
|
544
|
+
break;
|
|
545
|
+
case 'compact_context': {
|
|
546
|
+
const result = await this.controllerValue.compactContext();
|
|
547
|
+
if (result.status === 'rejected') {
|
|
548
|
+
throw new errors_1.WebWorkbenchError(409, 'Context compact was rejected.', 'runtime_busy');
|
|
549
|
+
}
|
|
550
|
+
detail = JSON.stringify(result);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
case 'edit_queue_item':
|
|
554
|
+
detail = JSON.stringify(this.controllerValue.editFollowupQueueItem(input.itemId, input.expectedItemRevision, input.text));
|
|
555
|
+
break;
|
|
556
|
+
case 'move_queue_item':
|
|
557
|
+
detail = JSON.stringify(this.controllerValue.moveFollowupQueueItem(input.itemId, input.expectedItemRevision, input.targetIndex));
|
|
558
|
+
break;
|
|
559
|
+
case 'remove_queue_item':
|
|
560
|
+
detail = JSON.stringify(this.controllerValue.removeFollowupQueueItem(input.itemId, input.expectedItemRevision));
|
|
561
|
+
break;
|
|
562
|
+
case 'review_plan': {
|
|
563
|
+
planReviewReceipt = await this.controllerValue.reviewPlan({
|
|
564
|
+
planDigest: input.planDigest,
|
|
565
|
+
action: input.action,
|
|
566
|
+
...(input.feedback ? { feedback: input.feedback } : {}),
|
|
567
|
+
});
|
|
568
|
+
if (input.action === 'approve')
|
|
569
|
+
this.controllerValue.setAgentMode('interactive');
|
|
570
|
+
else if (input.action === 'continue')
|
|
571
|
+
this.controllerValue.setAgentMode('plan');
|
|
572
|
+
detail = JSON.stringify(planReviewReceipt.admission);
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
throw mapComposerControlError(error);
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
this.suppressComposerEdges -= 1;
|
|
582
|
+
}
|
|
583
|
+
this.bumpComposerRevision();
|
|
584
|
+
const state = this.projectComposerStateValue(requireSession(input.expectedSessionId), true);
|
|
585
|
+
this.composerAuthorityDigest = composerAuthorityDigest(state);
|
|
586
|
+
this.eventHub.emit({ type: 'composer_state_changed', state }, true, {
|
|
587
|
+
sessionId: input.expectedSessionId,
|
|
588
|
+
});
|
|
589
|
+
return Object.freeze({
|
|
590
|
+
requestId: input.requestId,
|
|
591
|
+
outcome,
|
|
592
|
+
controlRevision: this.composerRevisionValue,
|
|
593
|
+
state,
|
|
594
|
+
...(modelReceipt ? { modelReceipt } : {}),
|
|
595
|
+
...(permissionReceipt ? { permissionReceipt } : {}),
|
|
596
|
+
...(planReviewReceipt ? { planReviewReceipt } : {}),
|
|
597
|
+
...(detail ? { detail } : {}),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
async skills(context) {
|
|
601
|
+
if (context)
|
|
602
|
+
this.assertContextGuard(context);
|
|
235
603
|
const descriptors = (await this.runtimeValue.inspectSkills?.()) ?? [];
|
|
236
|
-
|
|
604
|
+
const result = Object.freeze(descriptors.map(descriptor => Object.freeze({
|
|
237
605
|
id: descriptor.id,
|
|
238
606
|
name: descriptor.name,
|
|
239
607
|
description: descriptor.description,
|
|
@@ -244,8 +612,13 @@ class WebWorkbenchController {
|
|
|
244
612
|
requestedCapabilities: Object.freeze([...descriptor.requestedCapabilities]),
|
|
245
613
|
digest: descriptor.digest,
|
|
246
614
|
})));
|
|
615
|
+
if (context)
|
|
616
|
+
this.assertContextGuard(context);
|
|
617
|
+
return result;
|
|
247
618
|
}
|
|
248
|
-
mcp() {
|
|
619
|
+
mcp(context) {
|
|
620
|
+
if (context)
|
|
621
|
+
this.assertContextGuard(context);
|
|
249
622
|
const descriptors = this.runtimeValue.inspectMcp?.() ?? [];
|
|
250
623
|
let runtimeServers = new Map();
|
|
251
624
|
try {
|
|
@@ -257,7 +630,7 @@ class WebWorkbenchController {
|
|
|
257
630
|
catch {
|
|
258
631
|
// A concurrent session transition may close the previous diagnostics graph.
|
|
259
632
|
}
|
|
260
|
-
|
|
633
|
+
const result = Object.freeze(descriptors.map(descriptor => {
|
|
261
634
|
const runtime = runtimeServers.get(descriptor.id);
|
|
262
635
|
return Object.freeze({
|
|
263
636
|
id: descriptor.id,
|
|
@@ -270,103 +643,228 @@ class WebWorkbenchController {
|
|
|
270
643
|
generation: runtime?.generation ?? 0,
|
|
271
644
|
toolCount: runtime?.toolCount ?? 0,
|
|
272
645
|
activeCallCount: runtime?.activeCallCount ?? 0,
|
|
273
|
-
...(runtime?.failure
|
|
646
|
+
...(runtime?.failure
|
|
647
|
+
? { failure: 'Runtime transport failure. See local Host diagnostics for details.' }
|
|
648
|
+
: {}),
|
|
274
649
|
});
|
|
275
650
|
}));
|
|
651
|
+
if (context)
|
|
652
|
+
this.assertContextGuard(context);
|
|
653
|
+
return result;
|
|
276
654
|
}
|
|
277
|
-
async listToolDetails() {
|
|
278
|
-
|
|
655
|
+
async listToolDetails(context) {
|
|
656
|
+
if (context)
|
|
657
|
+
this.assertContextGuard(context);
|
|
658
|
+
const result = Object.freeze(await this.toolDetails.list(this.workspaceValue));
|
|
659
|
+
if (context)
|
|
660
|
+
this.assertContextGuard(context);
|
|
661
|
+
return result;
|
|
279
662
|
}
|
|
280
|
-
async readToolDetail(callId, offsetBytes, limitBytes) {
|
|
663
|
+
async readToolDetail(callId, offsetBytes, limitBytes, context) {
|
|
664
|
+
if (context)
|
|
665
|
+
this.assertContextGuard(context);
|
|
281
666
|
const entry = (await this.toolDetails.list(this.workspaceValue)).find(detail => detail.callId === callId || detail.artifactId === callId);
|
|
282
667
|
if (!entry?.artifactId)
|
|
283
|
-
throw new WebWorkbenchError(404, 'Tool detail was not found.');
|
|
284
|
-
|
|
668
|
+
throw new errors_1.WebWorkbenchError(404, 'Tool detail was not found.');
|
|
669
|
+
const result = await this.toolDetails.read({
|
|
285
670
|
callId: entry.callId,
|
|
286
671
|
sequence: entry.sequence,
|
|
287
672
|
artifactId: entry.artifactId,
|
|
288
673
|
outputBytes: entry.outputBytes,
|
|
289
674
|
}, { offsetBytes, limitBytes }, this.workspaceValue);
|
|
675
|
+
if (context)
|
|
676
|
+
this.assertContextGuard(context);
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
listFiles(context, input) {
|
|
680
|
+
this.assertContextGuard(context);
|
|
681
|
+
const result = this.fileService.list(input);
|
|
682
|
+
this.assertContextGuard(context);
|
|
683
|
+
return result;
|
|
290
684
|
}
|
|
291
|
-
|
|
685
|
+
readFileContent(context, input) {
|
|
686
|
+
this.assertContextGuard(context);
|
|
687
|
+
const result = this.fileService.readContent(input);
|
|
688
|
+
this.assertContextGuard(context);
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
async gitStatus(context, input) {
|
|
692
|
+
this.assertContextGuard(context);
|
|
693
|
+
const result = await this.gitService.status(input);
|
|
694
|
+
this.assertContextGuard(context);
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
async gitLog(context, input) {
|
|
698
|
+
this.assertContextGuard(context);
|
|
699
|
+
const result = await this.gitService.log(input);
|
|
700
|
+
this.assertContextGuard(context);
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
async gitDiff(context, input) {
|
|
704
|
+
this.assertContextGuard(context);
|
|
705
|
+
const result = await this.gitService.diff(input);
|
|
706
|
+
this.assertContextGuard(context);
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
async review(context) {
|
|
710
|
+
this.assertContextGuard(context);
|
|
711
|
+
try {
|
|
712
|
+
const result = await this.reviewService.snapshot();
|
|
713
|
+
this.assertContextGuard(context);
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
if (error instanceof durable_tool_receipt_reader_1.DurableToolReceiptReaderError) {
|
|
718
|
+
throw new errors_1.WebWorkbenchError(500, 'Durable Review receipt facts failed integrity validation.', 'review_receipt_invalid');
|
|
719
|
+
}
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
listTerminals(context) {
|
|
724
|
+
this.assertContextGuard(context);
|
|
725
|
+
const result = this.terminalManager.list(this.activeWorkspaceEntry().id);
|
|
726
|
+
this.assertContextGuard(context);
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
async createSession(name, context) {
|
|
292
730
|
this.assertReadyForTransition('create a session');
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
731
|
+
if (context)
|
|
732
|
+
this.assertContextGuard(context);
|
|
733
|
+
this.sessionTransition = true;
|
|
734
|
+
try {
|
|
735
|
+
// A new Session inherits the durable default; an override on the currently
|
|
736
|
+
// active Session must never leak into the next Session.
|
|
737
|
+
const session = (0, session_storage_1.createSession)(this.workspaceValue, this.settings().sections.defaults.model.effectiveValue);
|
|
738
|
+
(0, global_config_1.incrementSessionCount)();
|
|
739
|
+
const named = name?.trim() ? ((0, session_storage_1.renameSession)(session.id, name.trim()) ?? session) : session;
|
|
740
|
+
this.runtimeValue.setSession(named);
|
|
741
|
+
await this.runtimeValue.rebindSessionRuntime?.();
|
|
742
|
+
this.eventHub.emitRuntime({ type: 'transcript_clear' });
|
|
743
|
+
this.emitState();
|
|
744
|
+
return (0, protocol_1.projectSessionSummary)(named);
|
|
745
|
+
}
|
|
746
|
+
finally {
|
|
747
|
+
this.sessionTransition = false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async activateSession(sessionId, context) {
|
|
305
751
|
this.assertReadyForTransition('switch sessions');
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
752
|
+
if (context)
|
|
753
|
+
this.assertContextGuard(context);
|
|
754
|
+
this.sessionTransition = true;
|
|
755
|
+
this.suppressContextEdges += 1;
|
|
756
|
+
let activated = false;
|
|
757
|
+
try {
|
|
758
|
+
const session = requireSession(sessionId);
|
|
759
|
+
if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
|
|
760
|
+
throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
|
|
761
|
+
}
|
|
762
|
+
const result = this.controllerValue.handle({
|
|
763
|
+
type: 'select_session',
|
|
764
|
+
sessionId: session.id,
|
|
765
|
+
source: 'programmatic',
|
|
766
|
+
});
|
|
767
|
+
await this.controllerValue.waitForIdle();
|
|
768
|
+
if (this.runtimeValue.getSession()?.id !== session.id) {
|
|
769
|
+
throw new errors_1.WebWorkbenchError(409, 'Session activation was rejected by the runtime.');
|
|
770
|
+
}
|
|
771
|
+
activated = true;
|
|
772
|
+
return { requestId: `activate:${session.id}`, result: result.type };
|
|
309
773
|
}
|
|
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.');
|
|
774
|
+
finally {
|
|
775
|
+
this.suppressContextEdges -= 1;
|
|
776
|
+
this.sessionTransition = false;
|
|
777
|
+
if (activated)
|
|
778
|
+
this.emitState();
|
|
318
779
|
}
|
|
319
|
-
return { requestId: `activate:${session.id}`, result: result.type };
|
|
320
780
|
}
|
|
321
|
-
renameSession(sessionId, name) {
|
|
781
|
+
renameSession(sessionId, name, context) {
|
|
782
|
+
if (context)
|
|
783
|
+
this.assertContextGuard(context);
|
|
322
784
|
const session = requireSession(sessionId);
|
|
323
785
|
if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
|
|
324
|
-
throw new WebWorkbenchError(409, 'Session belongs to another workspace.');
|
|
786
|
+
throw new errors_1.WebWorkbenchError(409, 'Session belongs to another workspace.');
|
|
325
787
|
}
|
|
326
788
|
if (name.length > 120)
|
|
327
|
-
throw new WebWorkbenchError(400, 'Session name is too long.');
|
|
789
|
+
throw new errors_1.WebWorkbenchError(400, 'Session name is too long.');
|
|
328
790
|
const updated = (0, session_storage_1.renameSession)(session.id, name);
|
|
329
791
|
if (!updated)
|
|
330
|
-
throw new WebWorkbenchError(404, 'Session no longer exists.');
|
|
792
|
+
throw new errors_1.WebWorkbenchError(404, 'Session no longer exists.');
|
|
331
793
|
return (0, protocol_1.projectSessionSummary)(updated);
|
|
332
794
|
}
|
|
333
|
-
async switchWorkspace(path) {
|
|
334
|
-
|
|
335
|
-
throw new WebWorkbenchError(409, 'A workspace transition is running.');
|
|
795
|
+
async switchWorkspace(path, context) {
|
|
796
|
+
this.assertContextGuard(context);
|
|
336
797
|
const next = canonicalDirectory(path);
|
|
337
798
|
if (next === this.workspaceValue)
|
|
338
799
|
return;
|
|
339
|
-
|
|
800
|
+
let entry;
|
|
801
|
+
try {
|
|
802
|
+
entry = this.workspaceRegistry.register(next);
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
throw mapWorkspaceRegistryError(error);
|
|
806
|
+
}
|
|
807
|
+
await this.activateContext({
|
|
808
|
+
expectedContextRevision: context.expectedContextRevision,
|
|
809
|
+
workspaceId: entry.id,
|
|
810
|
+
sessionId: null,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
async activateContext(input) {
|
|
814
|
+
if (input.expectedContextRevision !== this.contextRevisionValue) {
|
|
815
|
+
throw new errors_1.WebWorkbenchError(409, 'The active Context changed before activation was admitted.', 'context_revision_conflict');
|
|
816
|
+
}
|
|
817
|
+
const entry = this.workspaceRegistry.find(input.workspaceId);
|
|
818
|
+
if (!entry)
|
|
819
|
+
throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
|
|
820
|
+
const targetWorkspace = canonicalDirectory(entry.canonicalPath);
|
|
821
|
+
const targetSession = input.sessionId ? requireSession(input.sessionId) : null;
|
|
822
|
+
if (targetSession && canonicalDirectory(targetSession.projectPath) !== targetWorkspace) {
|
|
823
|
+
throw new errors_1.WebWorkbenchError(409, 'Session does not belong to the requested Workspace.', 'context_session_mismatch');
|
|
824
|
+
}
|
|
825
|
+
const currentSessionId = this.runtimeValue.getSession()?.id ?? null;
|
|
826
|
+
if (targetWorkspace === this.workspaceValue && targetSession?.id === currentSessionId)
|
|
827
|
+
return;
|
|
828
|
+
if (targetWorkspace === this.workspaceValue && !targetSession && currentSessionId === null)
|
|
829
|
+
return;
|
|
830
|
+
this.assertReadyForTransition('activate a Context');
|
|
831
|
+
const previousWorkspace = this.workspaceValue;
|
|
832
|
+
const previousSessionId = currentSessionId;
|
|
340
833
|
const previousRuntime = this.runtimeValue;
|
|
341
834
|
const previousController = this.controllerValue;
|
|
342
|
-
const previousWorkspace = this.workspaceValue;
|
|
343
835
|
const transition = (async () => {
|
|
344
|
-
|
|
345
|
-
await previousController.waitForIdle();
|
|
346
|
-
await previousRuntime.shutdown();
|
|
836
|
+
this.suppressContextEdges += 1;
|
|
347
837
|
try {
|
|
348
|
-
await
|
|
838
|
+
await previousController.stopActiveTurn();
|
|
839
|
+
await previousController.waitForIdle();
|
|
840
|
+
if (targetWorkspace !== previousWorkspace) {
|
|
841
|
+
await previousRuntime.shutdown();
|
|
842
|
+
await this.installRuntime(targetWorkspace, false);
|
|
843
|
+
}
|
|
844
|
+
await this.restoreContextSession(targetSession?.id ?? null);
|
|
845
|
+
this.workspaceRegistry.register(targetWorkspace, { activated: true });
|
|
349
846
|
}
|
|
350
|
-
catch (
|
|
847
|
+
catch (activationError) {
|
|
351
848
|
try {
|
|
352
|
-
|
|
849
|
+
if (targetWorkspace !== previousWorkspace) {
|
|
850
|
+
if (this.runtimeValue !== previousRuntime)
|
|
851
|
+
await this.runtimeValue.shutdown();
|
|
852
|
+
await this.installRuntime(previousWorkspace, false);
|
|
853
|
+
}
|
|
854
|
+
await this.restoreContextSession(previousSessionId);
|
|
353
855
|
}
|
|
354
856
|
catch {
|
|
355
|
-
throw new WebWorkbenchError(503, '
|
|
857
|
+
throw new errors_1.WebWorkbenchError(503, 'Context activation failed and the previous Context could not be restored.', 'context_recovery_required');
|
|
356
858
|
}
|
|
357
|
-
throw
|
|
859
|
+
throw activationError;
|
|
358
860
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
revision: settings.revision,
|
|
367
|
-
reason: 'workspace-change',
|
|
368
|
-
state: settings.state === 'ready' || settings.state === 'read-only' ? 'ready' : 'invalid',
|
|
369
|
-
}, false);
|
|
861
|
+
finally {
|
|
862
|
+
this.suppressContextEdges -= 1;
|
|
863
|
+
}
|
|
864
|
+
this.emitState();
|
|
865
|
+
this.emitWorkspaceResourceInvalidation('context-change');
|
|
866
|
+
if (targetWorkspace !== previousWorkspace)
|
|
867
|
+
this.emitSettingsWorkspaceChange();
|
|
370
868
|
})().finally(() => {
|
|
371
869
|
if (this.transition === transition)
|
|
372
870
|
this.transition = undefined;
|
|
@@ -376,20 +874,184 @@ class WebWorkbenchController {
|
|
|
376
874
|
}
|
|
377
875
|
dispatch(raw) {
|
|
378
876
|
const command = (0, protocol_1.parseWebCommand)(raw);
|
|
379
|
-
return this.executeMutation(command.requestId, 'command', command, () => {
|
|
380
|
-
|
|
877
|
+
return this.executeMutation(command.requestId, 'command', command, async () => {
|
|
878
|
+
this.assertCommandSession(command.expectedSessionId);
|
|
879
|
+
const resolved = await this.resolveCommandContext(command);
|
|
880
|
+
this.assertCommandSession(command.expectedSessionId);
|
|
881
|
+
const runtimeInput = resolved && command.type === 'queue_followup'
|
|
882
|
+
? {
|
|
883
|
+
type: 'queue_followup',
|
|
884
|
+
text: command.text,
|
|
885
|
+
resolvedText: resolved.text,
|
|
886
|
+
source: 'programmatic',
|
|
887
|
+
}
|
|
888
|
+
: (0, protocol_1.toAgentRuntimeInput)({ ...command, ...(resolved ? { text: resolved.text } : {}) });
|
|
889
|
+
const runtimeResult = this.controllerValue.handle(runtimeInput);
|
|
381
890
|
return Object.freeze({
|
|
382
891
|
requestId: command.requestId,
|
|
383
892
|
result: runtimeResult.type,
|
|
384
893
|
detail: JSON.stringify(runtimeResult),
|
|
894
|
+
...(resolved ? { contextReceipt: resolved.receipt } : {}),
|
|
385
895
|
});
|
|
386
896
|
});
|
|
387
897
|
}
|
|
898
|
+
async resolveCommandContext(command) {
|
|
899
|
+
const references = command.contextReferences ?? [];
|
|
900
|
+
if (references.length === 0)
|
|
901
|
+
return undefined;
|
|
902
|
+
if (command.type !== 'submit' && command.type !== 'queue_followup') {
|
|
903
|
+
throw new errors_1.WebWorkbenchError(400, 'Context references are valid only for submitted or queued messages.', 'context_reference_invalid');
|
|
904
|
+
}
|
|
905
|
+
const resolved = [];
|
|
906
|
+
let totalBytes = 0;
|
|
907
|
+
for (const reference of references) {
|
|
908
|
+
let entry;
|
|
909
|
+
try {
|
|
910
|
+
entry = await this.resolveContextReference(reference);
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
throw mapContextReferenceError(error);
|
|
914
|
+
}
|
|
915
|
+
const bytes = Buffer.byteLength(JSON.stringify(entry), 'utf8');
|
|
916
|
+
if (bytes > MAX_CONTEXT_REFERENCE_BYTES) {
|
|
917
|
+
throw new errors_1.WebWorkbenchError(413, 'A Context reference exceeds the per-reference byte budget.', 'context_reference_too_large');
|
|
918
|
+
}
|
|
919
|
+
totalBytes += bytes;
|
|
920
|
+
if (totalBytes > MAX_CONTEXT_MANIFEST_BYTES) {
|
|
921
|
+
throw new errors_1.WebWorkbenchError(413, 'Context references exceed the request byte budget.', 'context_reference_too_large');
|
|
922
|
+
}
|
|
923
|
+
resolved.push(Object.freeze(entry));
|
|
924
|
+
}
|
|
925
|
+
const manifestContent = Object.freeze({
|
|
926
|
+
version: 1,
|
|
927
|
+
references: Object.freeze(resolved),
|
|
928
|
+
});
|
|
929
|
+
const manifestDigest = (0, canonical_1.digestRuntimeValue)(manifestContent);
|
|
930
|
+
const manifest = Object.freeze({ ...manifestContent, manifestDigest });
|
|
931
|
+
return Object.freeze({
|
|
932
|
+
text: [
|
|
933
|
+
command.text.trim(),
|
|
934
|
+
'',
|
|
935
|
+
'[Orion Context Manifest V1]',
|
|
936
|
+
JSON.stringify(manifest),
|
|
937
|
+
].join('\n'),
|
|
938
|
+
receipt: Object.freeze({
|
|
939
|
+
manifestDigest,
|
|
940
|
+
referenceCount: resolved.length,
|
|
941
|
+
totalBytes,
|
|
942
|
+
}),
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
async resolveContextReference(reference) {
|
|
946
|
+
switch (reference.kind) {
|
|
947
|
+
case 'file': {
|
|
948
|
+
const page = this.fileService.readContent({
|
|
949
|
+
fileId: reference.id,
|
|
950
|
+
limitBytes: MAX_CONTEXT_REFERENCE_BYTES,
|
|
951
|
+
});
|
|
952
|
+
if (page.revision !== reference.revision) {
|
|
953
|
+
throw new errors_1.WebWorkbenchError(409, 'Referenced file changed.', 'context_reference_stale');
|
|
954
|
+
}
|
|
955
|
+
if (page.binary || page.content === undefined) {
|
|
956
|
+
throw new errors_1.WebWorkbenchError(403, 'Binary files cannot be added to model Context.', 'context_reference_forbidden');
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
kind: 'file',
|
|
960
|
+
id: reference.id,
|
|
961
|
+
label: this.sanitizeContextText(page.name),
|
|
962
|
+
revision: page.revision,
|
|
963
|
+
content: this.sanitizeContextText(page.content),
|
|
964
|
+
truncated: page.nextCursor !== null,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
case 'folder': {
|
|
968
|
+
const page = this.fileService.list({ parentId: reference.id, pageSize: 100 });
|
|
969
|
+
if (page.revision !== reference.revision) {
|
|
970
|
+
throw new errors_1.WebWorkbenchError(409, 'Referenced folder changed.', 'context_reference_stale');
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
kind: 'folder',
|
|
974
|
+
id: reference.id,
|
|
975
|
+
label: this.sanitizeContextText(reference.label),
|
|
976
|
+
revision: page.revision,
|
|
977
|
+
entries: page.items
|
|
978
|
+
.filter(item => !item.sensitive)
|
|
979
|
+
.map(item => ({
|
|
980
|
+
name: this.sanitizeContextText(item.name),
|
|
981
|
+
kind: item.kind,
|
|
982
|
+
sizeBytes: item.sizeBytes ?? null,
|
|
983
|
+
})),
|
|
984
|
+
truncated: page.nextCursor !== null,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
case 'review': {
|
|
988
|
+
const snapshot = await this.reviewService.snapshot();
|
|
989
|
+
if (snapshot.repositoryRevision !== reference.gitRevision) {
|
|
990
|
+
throw new errors_1.WebWorkbenchError(409, 'Referenced Review changed.', 'context_reference_stale');
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
kind: 'review',
|
|
994
|
+
id: reference.id,
|
|
995
|
+
revision: snapshot.repositoryRevision,
|
|
996
|
+
clean: snapshot.clean,
|
|
997
|
+
files: snapshot.changedFiles.slice(0, 100).map(file => ({
|
|
998
|
+
id: file.fileId,
|
|
999
|
+
path: this.sanitizeContextText(file.path),
|
|
1000
|
+
indexStatus: file.indexStatus,
|
|
1001
|
+
worktreeStatus: file.worktreeStatus,
|
|
1002
|
+
})),
|
|
1003
|
+
truncated: snapshot.truncated || snapshot.changedFiles.length > 100,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
case 'session': {
|
|
1007
|
+
const session = requireSession(reference.id);
|
|
1008
|
+
if (canonicalDirectory(session.projectPath) !== this.workspaceValue) {
|
|
1009
|
+
throw new errors_1.WebWorkbenchError(403, 'Referenced Session belongs to another Workspace.', 'context_reference_forbidden');
|
|
1010
|
+
}
|
|
1011
|
+
const summary = (0, protocol_1.projectSessionSummary)(session);
|
|
1012
|
+
if (summary.contextDigest !== reference.digest) {
|
|
1013
|
+
throw new errors_1.WebWorkbenchError(409, 'Referenced Session changed.', 'context_reference_stale');
|
|
1014
|
+
}
|
|
1015
|
+
const page = (0, thread_session_view_1.loadThreadSessionSnapshotPageV1)(this.workspaceValue, session.id, undefined, 8);
|
|
1016
|
+
return {
|
|
1017
|
+
kind: 'session',
|
|
1018
|
+
id: session.id,
|
|
1019
|
+
label: this.sanitizeContextText(session.name?.trim() || `Session ${session.id.slice(0, 8)}`),
|
|
1020
|
+
digest: summary.contextDigest,
|
|
1021
|
+
messages: (page?.transcript.items ?? []).map(message => ({
|
|
1022
|
+
role: message.role,
|
|
1023
|
+
content: this.sanitizeContextText(message.content),
|
|
1024
|
+
})),
|
|
1025
|
+
truncated: Boolean(page?.transcript.nextCursor),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
case 'skill': {
|
|
1029
|
+
const descriptor = ((await this.runtimeValue.inspectSkills?.()) ?? []).find(skill => skill.id === reference.id);
|
|
1030
|
+
if (!descriptor || descriptor.digest !== reference.digest) {
|
|
1031
|
+
throw new errors_1.WebWorkbenchError(409, 'Referenced Skill changed.', 'context_reference_stale');
|
|
1032
|
+
}
|
|
1033
|
+
if (!descriptor.userInvocable) {
|
|
1034
|
+
throw new errors_1.WebWorkbenchError(403, 'Referenced Skill is not user-invocable.', 'context_reference_forbidden');
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
kind: 'skill',
|
|
1038
|
+
id: descriptor.id,
|
|
1039
|
+
label: this.sanitizeContextText(descriptor.name),
|
|
1040
|
+
digest: descriptor.digest,
|
|
1041
|
+
invocation: `$${descriptor.id}`,
|
|
1042
|
+
description: this.sanitizeContextText(descriptor.description),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
sanitizeContextText(value) {
|
|
1048
|
+
return (0, redaction_1.redactTraceText)(value).split(this.workspaceValue).join('<workspace>');
|
|
1049
|
+
}
|
|
388
1050
|
async updateSettings(input) {
|
|
389
1051
|
this.assertSettingsAvailable();
|
|
390
1052
|
const update = this.runtimeValue.updateSettings;
|
|
391
1053
|
if (!update) {
|
|
392
|
-
throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
1054
|
+
throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
393
1055
|
}
|
|
394
1056
|
try {
|
|
395
1057
|
const result = await update({
|
|
@@ -411,7 +1073,7 @@ class WebWorkbenchController {
|
|
|
411
1073
|
this.assertSettingsAvailable();
|
|
412
1074
|
const coordinator = this.runtimeValue.settingsCoordinator;
|
|
413
1075
|
if (!coordinator) {
|
|
414
|
-
throw new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
1076
|
+
throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
415
1077
|
}
|
|
416
1078
|
try {
|
|
417
1079
|
await coordinator.openDocument();
|
|
@@ -423,27 +1085,29 @@ class WebWorkbenchController {
|
|
|
423
1085
|
}
|
|
424
1086
|
executeMutation(requestId, operation, payload, action) {
|
|
425
1087
|
if (!requestId.trim() || requestId.length > 128) {
|
|
426
|
-
throw new WebWorkbenchError(400, 'requestId must be a non-empty string up to 128 characters.');
|
|
1088
|
+
throw new errors_1.WebWorkbenchError(400, 'requestId must be a non-empty string up to 128 characters.');
|
|
427
1089
|
}
|
|
428
1090
|
const fingerprint = (0, canonical_1.digestRuntimeValue)({ operation, payload });
|
|
429
1091
|
const cached = this.mutationResults.get(requestId);
|
|
430
1092
|
if (cached) {
|
|
431
1093
|
if (cached.fingerprint !== fingerprint) {
|
|
432
|
-
throw new WebWorkbenchError(409, 'requestId was already used for another mutation.', 'request_id_conflict');
|
|
1094
|
+
throw new errors_1.WebWorkbenchError(409, 'requestId was already used for another mutation.', 'request_id_conflict');
|
|
433
1095
|
}
|
|
434
1096
|
return cached.result;
|
|
435
1097
|
}
|
|
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);
|
|
1098
|
+
if (this.mutationResults.size >= MAX_MUTATION_RESULTS) {
|
|
1099
|
+
throw new errors_1.WebWorkbenchError(503, 'The mutation idempotency ledger is full; restart the local Web Host before retrying.', 'mutation_capacity_exhausted');
|
|
443
1100
|
}
|
|
1101
|
+
const result = Promise.resolve().then(() => {
|
|
1102
|
+
this.assertMutationAdmission();
|
|
1103
|
+
return action();
|
|
1104
|
+
});
|
|
1105
|
+
this.mutationResults.set(requestId, { fingerprint, result });
|
|
444
1106
|
return result;
|
|
445
1107
|
}
|
|
446
|
-
async diagnostics() {
|
|
1108
|
+
async diagnostics(context) {
|
|
1109
|
+
if (context)
|
|
1110
|
+
this.assertContextGuard(context);
|
|
447
1111
|
const state = this.runtimeValue.store.getSnapshot();
|
|
448
1112
|
const mcp = (0, mcp_1.loadFirstPartyMcpConfigurationV1)();
|
|
449
1113
|
const activeSessionId = this.runtimeValue.getSession()?.id ?? null;
|
|
@@ -452,7 +1116,7 @@ class WebWorkbenchController {
|
|
|
452
1116
|
const harness = activeSessionId
|
|
453
1117
|
? ((await this.runtimeValue.getHarnessDiagnostics?.()) ?? null)
|
|
454
1118
|
: null;
|
|
455
|
-
|
|
1119
|
+
const result = {
|
|
456
1120
|
workspace: this.workspaceValue,
|
|
457
1121
|
activeSessionId,
|
|
458
1122
|
configured: this.runtimeValue.isConfigured,
|
|
@@ -470,7 +1134,14 @@ class WebWorkbenchController {
|
|
|
470
1134
|
mcp: { servers: mcpServerIds(mcp) },
|
|
471
1135
|
harness,
|
|
472
1136
|
eventStream: this.eventHub.snapshot(),
|
|
1137
|
+
performance: {
|
|
1138
|
+
files: this.fileService.performanceCounters(),
|
|
1139
|
+
git: this.gitService.performanceCounters(),
|
|
1140
|
+
},
|
|
473
1141
|
};
|
|
1142
|
+
if (context)
|
|
1143
|
+
this.assertContextGuard(context);
|
|
1144
|
+
return result;
|
|
474
1145
|
}
|
|
475
1146
|
async shutdown() {
|
|
476
1147
|
if (this.closed)
|
|
@@ -478,22 +1149,47 @@ class WebWorkbenchController {
|
|
|
478
1149
|
this.closed = true;
|
|
479
1150
|
await this.controllerValue.stopActiveTurn();
|
|
480
1151
|
await this.controllerValue.waitForIdle();
|
|
1152
|
+
this.closeWorkspaceWatchers();
|
|
1153
|
+
this.closeComposerObserver();
|
|
1154
|
+
await this.terminalManager.shutdown();
|
|
481
1155
|
await this.runtimeValue.shutdown();
|
|
482
1156
|
this.eventHub.close();
|
|
483
1157
|
}
|
|
484
|
-
async installRuntime(workspace) {
|
|
1158
|
+
async installRuntime(workspace, publishState = true) {
|
|
1159
|
+
this.closeComposerObserver();
|
|
485
1160
|
const runtime = await this.createRuntime(workspace);
|
|
1161
|
+
this.closeWorkspaceWatchers();
|
|
486
1162
|
this.activeOrionRuntime = undefined;
|
|
487
1163
|
this.sessionViews.clear();
|
|
488
1164
|
this.sessionViewBytes = 0;
|
|
489
1165
|
this.latestStatus = 'Ready';
|
|
490
1166
|
this.workspaceValue = workspace;
|
|
491
1167
|
this.runtimeValue = runtime;
|
|
1168
|
+
this.fileService = new file_read_service_1.FileReadServiceV1(workspace);
|
|
1169
|
+
this.gitService = new git_read_model_service_1.GitReadModelServiceV1(workspace);
|
|
1170
|
+
this.reviewService = new review_service_1.ReviewServiceV1(this.gitService, () => (0, durable_tool_receipt_reader_1.listProjectDurableToolReceiptRefsV1)(this.workspaceValue));
|
|
492
1171
|
const eventSink = {
|
|
493
1172
|
emit: (event) => {
|
|
494
1173
|
if (event.type === 'status_changed')
|
|
495
1174
|
this.latestStatus = event.message;
|
|
1175
|
+
if (this.suppressContextEdges > 0)
|
|
1176
|
+
return undefined;
|
|
496
1177
|
const activeSessionId = this.runtimeValue.getSession()?.id;
|
|
1178
|
+
if (event.type === 'followup_queue_changed') {
|
|
1179
|
+
// The queue is part of the Composer CAS authority. Publishing the
|
|
1180
|
+
// Runtime queue event on its own lets the browser observe new items
|
|
1181
|
+
// with an older controlRevision and guarantees a 409 on its next
|
|
1182
|
+
// edit/move/remove action. Project one complete Composer state in
|
|
1183
|
+
// this same synchronous boundary instead. Composer actions suppress
|
|
1184
|
+
// their intermediate queue edge and publish their final state below.
|
|
1185
|
+
if (activeSessionId && this.suppressComposerEdges === 0) {
|
|
1186
|
+
const state = this.composerState(activeSessionId);
|
|
1187
|
+
this.eventHub.emit({ type: 'composer_state_changed', state }, true, {
|
|
1188
|
+
sessionId: activeSessionId,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return undefined;
|
|
1192
|
+
}
|
|
497
1193
|
const activeThread = activeSessionId && this.activeOrionRuntime?.sessionId === activeSessionId
|
|
498
1194
|
? this.activeOrionRuntime.runtime.thread.getProjection().threadId
|
|
499
1195
|
: undefined;
|
|
@@ -503,6 +1199,9 @@ class WebWorkbenchController {
|
|
|
503
1199
|
});
|
|
504
1200
|
if (event.type === 'session_restored')
|
|
505
1201
|
this.emitState();
|
|
1202
|
+
if (event.type === 'tool_finished') {
|
|
1203
|
+
this.scheduleWorkspaceResourceInvalidation('tool-finished');
|
|
1204
|
+
}
|
|
506
1205
|
return event.type === 'transcript_append' ? `web-entry-${envelope.cursor}` : undefined;
|
|
507
1206
|
},
|
|
508
1207
|
};
|
|
@@ -522,14 +1221,157 @@ class WebWorkbenchController {
|
|
|
522
1221
|
suppressAbortNotice: true,
|
|
523
1222
|
},
|
|
524
1223
|
});
|
|
525
|
-
this.
|
|
1224
|
+
this.composerRevisionValue = (0, crypto_1.randomUUID)();
|
|
1225
|
+
this.composerAuthorityDigest = undefined;
|
|
1226
|
+
this.composerStoreUnsubscribe = runtime.store.subscribe(() => this.scheduleComposerStateChanged());
|
|
1227
|
+
this.composerControlsUnsubscribe = runtime.sessionComposerControls?.subscribe(() => this.scheduleComposerStateChanged());
|
|
1228
|
+
this.installWorkspaceWatchers();
|
|
1229
|
+
if (publishState)
|
|
1230
|
+
this.emitState(false);
|
|
526
1231
|
}
|
|
527
|
-
emitState() {
|
|
1232
|
+
emitState(advanceRevision = true) {
|
|
1233
|
+
if (this.suppressContextEdges > 0)
|
|
1234
|
+
return;
|
|
1235
|
+
if (advanceRevision)
|
|
1236
|
+
this.contextRevisionValue = (0, crypto_1.randomUUID)();
|
|
1237
|
+
if (advanceRevision)
|
|
1238
|
+
this.composerRevisionValue = (0, crypto_1.randomUUID)();
|
|
1239
|
+
const workspace = this.activeWorkspaceEntry();
|
|
1240
|
+
const sessionId = this.runtimeValue.getSession()?.id ?? null;
|
|
1241
|
+
if (sessionId) {
|
|
1242
|
+
try {
|
|
1243
|
+
this.composerAuthorityDigest = composerAuthorityDigest(this.projectComposerStateValue(requireSession(sessionId), true));
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
this.composerAuthorityDigest = undefined;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
this.composerAuthorityDigest = undefined;
|
|
1251
|
+
}
|
|
528
1252
|
this.eventHub.emit({
|
|
529
1253
|
type: 'workbench_state',
|
|
1254
|
+
contextRevision: this.contextRevisionValue,
|
|
1255
|
+
workspaceId: workspace.id,
|
|
530
1256
|
workspace: this.workspaceValue,
|
|
531
|
-
activeSessionId:
|
|
1257
|
+
activeSessionId: sessionId,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
emitWorkspaceResourceInvalidation(reason) {
|
|
1261
|
+
if (this.closed || this.suppressContextEdges > 0)
|
|
1262
|
+
return;
|
|
1263
|
+
this.eventHub.emit({
|
|
1264
|
+
type: 'workspace_resource_invalidated',
|
|
1265
|
+
workspaceId: this.activeWorkspaceEntry().id,
|
|
1266
|
+
resources: ['files', 'git', 'review'],
|
|
1267
|
+
reason,
|
|
1268
|
+
}, false);
|
|
1269
|
+
}
|
|
1270
|
+
scheduleWorkspaceResourceInvalidation(reason) {
|
|
1271
|
+
if (this.closed)
|
|
1272
|
+
return;
|
|
1273
|
+
if (this.resourceInvalidationTimer)
|
|
1274
|
+
clearTimeout(this.resourceInvalidationTimer);
|
|
1275
|
+
const workspace = this.workspaceValue;
|
|
1276
|
+
this.resourceInvalidationTimer = setTimeout(() => {
|
|
1277
|
+
this.resourceInvalidationTimer = undefined;
|
|
1278
|
+
if (!this.closed && this.workspaceValue === workspace) {
|
|
1279
|
+
this.emitWorkspaceResourceInvalidation(reason);
|
|
1280
|
+
}
|
|
1281
|
+
}, 120);
|
|
1282
|
+
this.resourceInvalidationTimer.unref();
|
|
1283
|
+
}
|
|
1284
|
+
bumpComposerRevision() {
|
|
1285
|
+
this.composerRevisionValue = (0, crypto_1.randomUUID)();
|
|
1286
|
+
}
|
|
1287
|
+
scheduleComposerStateChanged() {
|
|
1288
|
+
if (this.closed ||
|
|
1289
|
+
this.suppressComposerEdges > 0 ||
|
|
1290
|
+
this.suppressContextEdges > 0 ||
|
|
1291
|
+
!this.runtimeValue.getSession()) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (this.composerChangeTimer)
|
|
1295
|
+
clearTimeout(this.composerChangeTimer);
|
|
1296
|
+
this.composerChangeTimer = setTimeout(() => {
|
|
1297
|
+
this.composerChangeTimer = undefined;
|
|
1298
|
+
const sessionId = this.runtimeValue.getSession()?.id;
|
|
1299
|
+
if (!sessionId || this.closed || this.suppressComposerEdges > 0)
|
|
1300
|
+
return;
|
|
1301
|
+
try {
|
|
1302
|
+
const state = this.composerState(sessionId);
|
|
1303
|
+
this.eventHub.emit({ type: 'composer_state_changed', state }, true, { sessionId });
|
|
1304
|
+
}
|
|
1305
|
+
catch {
|
|
1306
|
+
// A concurrent Context transition publishes a fresh baseline instead.
|
|
1307
|
+
}
|
|
1308
|
+
}, 100);
|
|
1309
|
+
this.composerChangeTimer.unref();
|
|
1310
|
+
}
|
|
1311
|
+
closeComposerObserver() {
|
|
1312
|
+
this.composerStoreUnsubscribe?.();
|
|
1313
|
+
this.composerStoreUnsubscribe = undefined;
|
|
1314
|
+
this.composerControlsUnsubscribe?.();
|
|
1315
|
+
this.composerControlsUnsubscribe = undefined;
|
|
1316
|
+
if (this.composerChangeTimer)
|
|
1317
|
+
clearTimeout(this.composerChangeTimer);
|
|
1318
|
+
this.composerChangeTimer = undefined;
|
|
1319
|
+
}
|
|
1320
|
+
installWorkspaceWatchers() {
|
|
1321
|
+
const watchPaths = [this.workspaceValue, (0, path_1.resolve)(this.workspaceValue, '.git')].filter(path => (0, fs_1.existsSync)(path));
|
|
1322
|
+
for (const path of watchPaths) {
|
|
1323
|
+
try {
|
|
1324
|
+
const watcher = (0, fs_1.watch)(path, { persistent: false }, () => this.scheduleWorkspaceResourceInvalidation('filesystem-change'));
|
|
1325
|
+
watcher.on('error', () => {
|
|
1326
|
+
watcher.close();
|
|
1327
|
+
this.workspaceWatchers = this.workspaceWatchers.filter(item => item !== watcher);
|
|
1328
|
+
});
|
|
1329
|
+
this.workspaceWatchers.push(watcher);
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
// Watchers are only refresh hints. Revisioned reads and explicit refresh remain authoritative.
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
closeWorkspaceWatchers() {
|
|
1337
|
+
if (this.resourceInvalidationTimer)
|
|
1338
|
+
clearTimeout(this.resourceInvalidationTimer);
|
|
1339
|
+
this.resourceInvalidationTimer = undefined;
|
|
1340
|
+
for (const watcher of this.workspaceWatchers)
|
|
1341
|
+
watcher.close();
|
|
1342
|
+
this.workspaceWatchers = [];
|
|
1343
|
+
}
|
|
1344
|
+
async restoreContextSession(sessionId) {
|
|
1345
|
+
if (!sessionId) {
|
|
1346
|
+
this.runtimeValue.setSession(null);
|
|
1347
|
+
await this.runtimeValue.rebindSessionRuntime?.();
|
|
1348
|
+
if (this.suppressContextEdges === 0) {
|
|
1349
|
+
this.eventHub.emitRuntime({ type: 'transcript_clear' });
|
|
1350
|
+
}
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const result = this.controllerValue.handle({
|
|
1354
|
+
type: 'select_session',
|
|
1355
|
+
sessionId,
|
|
1356
|
+
source: 'programmatic',
|
|
532
1357
|
});
|
|
1358
|
+
await this.controllerValue.waitForIdle();
|
|
1359
|
+
if (this.runtimeValue.getSession()?.id !== sessionId) {
|
|
1360
|
+
throw new errors_1.WebWorkbenchError(409, `Session activation was rejected by the runtime (${result.type}).`, 'context_session_rejected');
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
emitSettingsWorkspaceChange() {
|
|
1364
|
+
const describe = this.runtimeValue.describeSettings;
|
|
1365
|
+
if (!describe) {
|
|
1366
|
+
throw new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
1367
|
+
}
|
|
1368
|
+
const settings = describe();
|
|
1369
|
+
this.eventHub.emit({
|
|
1370
|
+
type: 'settings_invalidated',
|
|
1371
|
+
revision: settings.revision,
|
|
1372
|
+
reason: 'workspace-change',
|
|
1373
|
+
state: settings.state === 'ready' || settings.state === 'read-only' ? 'ready' : 'invalid',
|
|
1374
|
+
}, false);
|
|
533
1375
|
}
|
|
534
1376
|
observeThreadRuntime(runtime, sessionId, activation) {
|
|
535
1377
|
let replaying = true;
|
|
@@ -612,31 +1454,138 @@ class WebWorkbenchController {
|
|
|
612
1454
|
}
|
|
613
1455
|
assertReadyForTransition(operation) {
|
|
614
1456
|
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');
|
|
1457
|
+
throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.');
|
|
1458
|
+
if (this.transition || this.sessionTransition) {
|
|
1459
|
+
throw new errors_1.WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
|
|
618
1460
|
}
|
|
619
1461
|
if (this.controllerValue.hasActiveTurn()) {
|
|
620
|
-
throw new WebWorkbenchError(409, `Cannot ${operation} while a turn is active.`, 'runtime_busy');
|
|
1462
|
+
throw new errors_1.WebWorkbenchError(409, `Cannot ${operation} while a turn is active.`, 'runtime_busy');
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
assertMutationAdmission() {
|
|
1466
|
+
if (this.closed)
|
|
1467
|
+
throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.');
|
|
1468
|
+
if (this.transition || this.sessionTransition) {
|
|
1469
|
+
throw new errors_1.WebWorkbenchError(409, 'A Context transition is running.', 'runtime_busy');
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
assertContextGuard(context) {
|
|
1473
|
+
this.assertMutationAdmission();
|
|
1474
|
+
if (context.expectedContextRevision !== this.contextRevisionValue ||
|
|
1475
|
+
context.workspaceId !== this.activeWorkspaceEntry().id) {
|
|
1476
|
+
throw new errors_1.WebWorkbenchError(409, 'The active Context changed before the operation was admitted.', 'context_revision_conflict');
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
assertCommandSession(expectedSessionId) {
|
|
1480
|
+
if (this.closed)
|
|
1481
|
+
throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.');
|
|
1482
|
+
if (this.transition || this.sessionTransition) {
|
|
1483
|
+
throw new errors_1.WebWorkbenchError(409, 'A Session transition is running.', 'runtime_busy');
|
|
1484
|
+
}
|
|
1485
|
+
if (this.runtimeValue.getSession()?.id !== expectedSessionId) {
|
|
1486
|
+
throw new errors_1.WebWorkbenchError(409, 'The active Session changed before the command was admitted.', 'active_session_changed');
|
|
621
1487
|
}
|
|
622
1488
|
}
|
|
623
1489
|
assertSettingsAvailable() {
|
|
624
1490
|
if (this.closed) {
|
|
625
|
-
throw new WebWorkbenchError(503, 'Web Workbench is closed.', 'settings_document_unavailable');
|
|
1491
|
+
throw new errors_1.WebWorkbenchError(503, 'Web Workbench is closed.', 'settings_document_unavailable');
|
|
626
1492
|
}
|
|
627
1493
|
if (this.transition) {
|
|
628
|
-
throw new WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
|
|
1494
|
+
throw new errors_1.WebWorkbenchError(409, 'A workspace transition is running.', 'runtime_busy');
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
initializeWorkspaceRegistry() {
|
|
1498
|
+
const paths = [this.workspaceValue];
|
|
1499
|
+
for (const session of (0, session_storage_1.listSessions)()) {
|
|
1500
|
+
try {
|
|
1501
|
+
paths.push(canonicalDirectory(session.projectPath));
|
|
1502
|
+
}
|
|
1503
|
+
catch {
|
|
1504
|
+
// Unavailable historical projects remain in the Session catalog but
|
|
1505
|
+
// are not imported as selectable Workspace registry entries.
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
try {
|
|
1509
|
+
this.workspaceRegistry.registerKnown(paths, this.workspaceValue);
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
throw mapWorkspaceRegistryError(error);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
activeWorkspaceEntry() {
|
|
1516
|
+
const entry = this.workspaceRegistry
|
|
1517
|
+
.list()
|
|
1518
|
+
.find(candidate => candidate.canonicalPath === this.workspaceValue);
|
|
1519
|
+
if (!entry) {
|
|
1520
|
+
throw new errors_1.WebWorkbenchError(503, 'Active Workspace is missing from the registry.');
|
|
629
1521
|
}
|
|
1522
|
+
return entry;
|
|
630
1523
|
}
|
|
631
1524
|
}
|
|
632
1525
|
exports.WebWorkbenchController = WebWorkbenchController;
|
|
1526
|
+
function mapWorkspaceRegistryError(error) {
|
|
1527
|
+
if (error instanceof errors_1.WebWorkbenchError)
|
|
1528
|
+
return error;
|
|
1529
|
+
if (error instanceof workspace_registry_1.WorkspaceRegistryError) {
|
|
1530
|
+
const status = error.code === 'workspace_registry_invalid' ? 503 : 409;
|
|
1531
|
+
return new errors_1.WebWorkbenchError(status, error.message, error.code);
|
|
1532
|
+
}
|
|
1533
|
+
return new errors_1.WebWorkbenchError(503, 'Workspace registry is unavailable.');
|
|
1534
|
+
}
|
|
633
1535
|
function mapSettingsError(error) {
|
|
634
|
-
if (error instanceof WebWorkbenchError)
|
|
1536
|
+
if (error instanceof errors_1.WebWorkbenchError)
|
|
635
1537
|
return error;
|
|
636
1538
|
if (error instanceof settings_coordinator_1.SettingsCoordinatorError) {
|
|
637
|
-
return new WebWorkbenchError(error.status, error.message, error.code);
|
|
1539
|
+
return new errors_1.WebWorkbenchError(error.status, error.message, error.code);
|
|
638
1540
|
}
|
|
639
|
-
return new WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
1541
|
+
return new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
|
|
1542
|
+
}
|
|
1543
|
+
function mapComposerControlError(error) {
|
|
1544
|
+
if (error instanceof errors_1.WebWorkbenchError)
|
|
1545
|
+
return error;
|
|
1546
|
+
if (error instanceof agent_runtime_controller_1.FollowupQueueConflictError) {
|
|
1547
|
+
return new errors_1.WebWorkbenchError(409, error.message, error.code);
|
|
1548
|
+
}
|
|
1549
|
+
if (error instanceof session_composer_control_1.SessionComposerControlError) {
|
|
1550
|
+
const status = error.code === 'composer_recovery_required'
|
|
1551
|
+
? 503
|
|
1552
|
+
: ['runtime_busy', 'composer_control_conflict'].includes(error.code)
|
|
1553
|
+
? 409
|
|
1554
|
+
: 422;
|
|
1555
|
+
return new errors_1.WebWorkbenchError(status, error.message, error.code);
|
|
1556
|
+
}
|
|
1557
|
+
if (error instanceof plan_review_1.PlanReviewControlError) {
|
|
1558
|
+
const status = error.code === 'plan_review_invalid' ? 422 : 409;
|
|
1559
|
+
return new errors_1.WebWorkbenchError(status, error.message, error.code);
|
|
1560
|
+
}
|
|
1561
|
+
return new errors_1.WebWorkbenchError(503, 'The Session Composer control action failed.', 'composer_control_failed');
|
|
1562
|
+
}
|
|
1563
|
+
function mapContextReferenceError(error) {
|
|
1564
|
+
if (error instanceof errors_1.WebWorkbenchError) {
|
|
1565
|
+
if (error.code.startsWith('context_reference_'))
|
|
1566
|
+
return error;
|
|
1567
|
+
if (error.status === 403 ||
|
|
1568
|
+
['sensitive_file_blocked', 'file_binary', 'file_not_regular'].includes(error.code)) {
|
|
1569
|
+
return new errors_1.WebWorkbenchError(403, 'The Context reference is not available to the model.', 'context_reference_forbidden');
|
|
1570
|
+
}
|
|
1571
|
+
if (error.status === 404 || error.status === 409) {
|
|
1572
|
+
return new errors_1.WebWorkbenchError(409, 'The Context reference changed or is no longer available.', 'context_reference_stale');
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return new errors_1.WebWorkbenchError(422, 'The Context reference could not be resolved.', 'context_reference_invalid');
|
|
1576
|
+
}
|
|
1577
|
+
function composerAuthorityDigest(state) {
|
|
1578
|
+
return (0, canonical_1.digestRuntimeValue)({
|
|
1579
|
+
workspaceId: state.workspaceId,
|
|
1580
|
+
sessionId: state.sessionId,
|
|
1581
|
+
mode: state.mode,
|
|
1582
|
+
model: state.model,
|
|
1583
|
+
permission: state.permission,
|
|
1584
|
+
pending: state.pending,
|
|
1585
|
+
lastError: state.lastError,
|
|
1586
|
+
planReview: state.planReview,
|
|
1587
|
+
queue: state.queue,
|
|
1588
|
+
});
|
|
640
1589
|
}
|
|
641
1590
|
function pickPlanReceipt(receipt) {
|
|
642
1591
|
return Object.freeze({
|
|
@@ -687,11 +1636,11 @@ function estimateSessionViewBytes(view) {
|
|
|
687
1636
|
}
|
|
688
1637
|
function pageItems(items, cursor, pageSize = 50) {
|
|
689
1638
|
if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
|
|
690
|
-
throw new WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
|
|
1639
|
+
throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
|
|
691
1640
|
}
|
|
692
1641
|
const offset = cursor ? decodePageCursor(cursor) : 0;
|
|
693
1642
|
if (offset > items.length)
|
|
694
|
-
throw new WebWorkbenchError(400, 'Page cursor is out of range.');
|
|
1643
|
+
throw new errors_1.WebWorkbenchError(400, 'Page cursor is out of range.');
|
|
695
1644
|
const page = items.slice(offset, offset + pageSize);
|
|
696
1645
|
const nextOffset = offset + page.length;
|
|
697
1646
|
return Object.freeze({
|
|
@@ -699,13 +1648,45 @@ function pageItems(items, cursor, pageSize = 50) {
|
|
|
699
1648
|
nextCursor: nextOffset < items.length ? encodePageCursor(nextOffset) : null,
|
|
700
1649
|
});
|
|
701
1650
|
}
|
|
1651
|
+
function pageCollectionItems(scope, items, cursor, pageSize, keyOf) {
|
|
1652
|
+
if (!scope || scope.length > 80 || !/^[a-z][a-z0-9_-]*$/.test(scope)) {
|
|
1653
|
+
throw new errors_1.WebWorkbenchError(500, 'Collection scope is invalid.');
|
|
1654
|
+
}
|
|
1655
|
+
if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
|
|
1656
|
+
throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
|
|
1657
|
+
}
|
|
1658
|
+
const keys = items.map(keyOf);
|
|
1659
|
+
if (keys.some(key => !key || key.length > 4096) || new Set(keys).size !== keys.length) {
|
|
1660
|
+
throw new errors_1.WebWorkbenchError(500, `Collection ${scope} has invalid or duplicate keys.`);
|
|
1661
|
+
}
|
|
1662
|
+
const revision = (0, canonical_1.digestRuntimeValue)(items);
|
|
1663
|
+
let offset = 0;
|
|
1664
|
+
if (cursor) {
|
|
1665
|
+
const decoded = decodeCollectionCursor(cursor, scope);
|
|
1666
|
+
if (decoded.revision !== revision) {
|
|
1667
|
+
throw new errors_1.WebWorkbenchError(409, 'Collection changed after this page cursor was issued.', 'collection_cursor_stale');
|
|
1668
|
+
}
|
|
1669
|
+
offset = keys.indexOf(decoded.after) + 1;
|
|
1670
|
+
if (offset === 0) {
|
|
1671
|
+
throw new errors_1.WebWorkbenchError(409, 'Collection page boundary no longer exists.', 'collection_cursor_stale');
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const page = items.slice(offset, offset + pageSize);
|
|
1675
|
+
const nextOffset = offset + page.length;
|
|
1676
|
+
return Object.freeze({
|
|
1677
|
+
items: Object.freeze(page),
|
|
1678
|
+
nextCursor: nextOffset < items.length && page.length > 0
|
|
1679
|
+
? encodeCollectionCursor(scope, revision, keys[nextOffset - 1])
|
|
1680
|
+
: null,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
702
1683
|
function pageTranscriptTail(items, cursor, pageSize = 50) {
|
|
703
1684
|
if (!Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100) {
|
|
704
|
-
throw new WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
|
|
1685
|
+
throw new errors_1.WebWorkbenchError(400, 'pageSize must be an integer from 1 through 100.');
|
|
705
1686
|
}
|
|
706
1687
|
const end = cursor ? decodePageCursor(cursor) : items.length;
|
|
707
1688
|
if (end > items.length)
|
|
708
|
-
throw new WebWorkbenchError(400, 'Page cursor is out of range.');
|
|
1689
|
+
throw new errors_1.WebWorkbenchError(400, 'Page cursor is out of range.');
|
|
709
1690
|
const offset = Math.max(0, end - pageSize);
|
|
710
1691
|
return Object.freeze({
|
|
711
1692
|
items: Object.freeze(items.slice(offset, end)),
|
|
@@ -716,9 +1697,37 @@ function pageTranscriptTail(items, cursor, pageSize = 50) {
|
|
|
716
1697
|
function encodePageCursor(offset) {
|
|
717
1698
|
return Buffer.from(JSON.stringify({ version: 1, offset })).toString('base64url');
|
|
718
1699
|
}
|
|
1700
|
+
function encodeCollectionCursor(scope, revision, after) {
|
|
1701
|
+
return Buffer.from(JSON.stringify({ version: 2, kind: 'collection', scope, revision, after })).toString('base64url');
|
|
1702
|
+
}
|
|
1703
|
+
function decodeCollectionCursor(cursor, expectedScope) {
|
|
1704
|
+
if (!cursor || cursor.length > 8192) {
|
|
1705
|
+
throw new errors_1.WebWorkbenchError(400, 'Collection page cursor is invalid.');
|
|
1706
|
+
}
|
|
1707
|
+
try {
|
|
1708
|
+
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
1709
|
+
if (!parsed ||
|
|
1710
|
+
typeof parsed !== 'object' ||
|
|
1711
|
+
Array.isArray(parsed) ||
|
|
1712
|
+
parsed.version !== 2 ||
|
|
1713
|
+
parsed.kind !== 'collection' ||
|
|
1714
|
+
parsed.scope !== expectedScope ||
|
|
1715
|
+
typeof parsed.revision !== 'string' ||
|
|
1716
|
+
typeof parsed.after !== 'string') {
|
|
1717
|
+
throw new Error('invalid');
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
revision: parsed.revision,
|
|
1721
|
+
after: parsed.after,
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
catch {
|
|
1725
|
+
throw new errors_1.WebWorkbenchError(400, 'Collection page cursor is invalid.');
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
719
1728
|
function decodePageCursor(cursor) {
|
|
720
1729
|
if (!cursor || cursor.length > 512)
|
|
721
|
-
throw new WebWorkbenchError(400, 'Page cursor is invalid.');
|
|
1730
|
+
throw new errors_1.WebWorkbenchError(400, 'Page cursor is invalid.');
|
|
722
1731
|
try {
|
|
723
1732
|
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
724
1733
|
if (!parsed ||
|
|
@@ -731,7 +1740,7 @@ function decodePageCursor(cursor) {
|
|
|
731
1740
|
return parsed.offset;
|
|
732
1741
|
}
|
|
733
1742
|
catch {
|
|
734
|
-
throw new WebWorkbenchError(400, 'Page cursor is invalid.');
|
|
1743
|
+
throw new errors_1.WebWorkbenchError(400, 'Page cursor is invalid.');
|
|
735
1744
|
}
|
|
736
1745
|
}
|
|
737
1746
|
function canonicalDirectory(path) {
|
|
@@ -741,26 +1750,26 @@ function canonicalDirectory(path) {
|
|
|
741
1750
|
canonical = (0, fs_1.realpathSync)(candidate);
|
|
742
1751
|
}
|
|
743
1752
|
catch {
|
|
744
|
-
throw new WebWorkbenchError(400, 'Workspace path does not exist.');
|
|
1753
|
+
throw new errors_1.WebWorkbenchError(400, 'Workspace path does not exist.');
|
|
745
1754
|
}
|
|
746
1755
|
try {
|
|
747
1756
|
if (!(0, fs_1.statSync)(canonical).isDirectory()) {
|
|
748
|
-
throw new WebWorkbenchError(400, 'Workspace path must be a directory.');
|
|
1757
|
+
throw new errors_1.WebWorkbenchError(400, 'Workspace path must be a directory.');
|
|
749
1758
|
}
|
|
750
1759
|
}
|
|
751
1760
|
catch (error) {
|
|
752
|
-
if (error instanceof WebWorkbenchError)
|
|
1761
|
+
if (error instanceof errors_1.WebWorkbenchError)
|
|
753
1762
|
throw error;
|
|
754
|
-
throw new WebWorkbenchError(400, 'Workspace path is not readable.');
|
|
1763
|
+
throw new errors_1.WebWorkbenchError(400, 'Workspace path is not readable.');
|
|
755
1764
|
}
|
|
756
1765
|
return canonical;
|
|
757
1766
|
}
|
|
758
1767
|
function requireSession(sessionId) {
|
|
759
1768
|
if (!sessionId.trim())
|
|
760
|
-
throw new WebWorkbenchError(400, 'Session id is required.');
|
|
1769
|
+
throw new errors_1.WebWorkbenchError(400, 'Session id is required.');
|
|
761
1770
|
const session = (0, session_storage_1.loadSessionMeta)(sessionId);
|
|
762
1771
|
if (!session)
|
|
763
|
-
throw new WebWorkbenchError(404, 'Session was not found.');
|
|
1772
|
+
throw new errors_1.WebWorkbenchError(404, 'Session was not found.');
|
|
764
1773
|
return session;
|
|
765
1774
|
}
|
|
766
1775
|
function mcpServerIds(config) {
|