@orion-agents/orion-code 0.3.1 → 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.
Files changed (109) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +39 -28
  3. package/README.zh-CN.md +32 -25
  4. package/dist/commands/model-command-handlers.d.ts.map +1 -1
  5. package/dist/commands/model-command-handlers.js +59 -14
  6. package/dist/commands/model-command-handlers.js.map +1 -1
  7. package/dist/commands/types.d.ts +6 -0
  8. package/dist/commands/types.d.ts.map +1 -1
  9. package/dist/commands/types.js.map +1 -1
  10. package/dist/commands/workflow-command-handlers.d.ts +1 -1
  11. package/dist/commands/workflow-command-handlers.d.ts.map +1 -1
  12. package/dist/commands/workflow-command-handlers.js +42 -3
  13. package/dist/commands/workflow-command-handlers.js.map +1 -1
  14. package/dist/commands/workflow-commands.d.ts.map +1 -1
  15. package/dist/commands/workflow-commands.js +2 -2
  16. package/dist/commands/workflow-commands.js.map +1 -1
  17. package/dist/runtime/agent-runtime-controller.d.ts +16 -2
  18. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  19. package/dist/runtime/agent-runtime-controller.js +115 -6
  20. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  21. package/dist/runtime/agent-runtime-protocol.d.ts +3 -0
  22. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  23. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  24. package/dist/runtime/agent-runtime-runner.d.ts +8 -0
  25. package/dist/runtime/agent-runtime-runner.d.ts.map +1 -1
  26. package/dist/runtime/orion-session-runner.d.ts +4 -0
  27. package/dist/runtime/orion-session-runner.d.ts.map +1 -1
  28. package/dist/runtime/orion-session-runner.js +15 -0
  29. package/dist/runtime/orion-session-runner.js.map +1 -1
  30. package/dist/runtime/plan-review.d.ts +38 -0
  31. package/dist/runtime/plan-review.d.ts.map +1 -0
  32. package/dist/runtime/plan-review.js +173 -0
  33. package/dist/runtime/plan-review.js.map +1 -0
  34. package/dist/runtime/product-bootstrap.d.ts.map +1 -1
  35. package/dist/runtime/product-bootstrap.js +28 -6
  36. package/dist/runtime/product-bootstrap.js.map +1 -1
  37. package/dist/runtime/product-orion-runtime.d.ts.map +1 -1
  38. package/dist/runtime/product-orion-runtime.js +14 -35
  39. package/dist/runtime/product-orion-runtime.js.map +1 -1
  40. package/dist/runtime/protocol/runtime-protocol-v1.d.ts +73 -0
  41. package/dist/runtime/protocol/runtime-protocol-v1.d.ts.map +1 -1
  42. package/dist/runtime/protocol/runtime-protocol-v1.js +37 -0
  43. package/dist/runtime/protocol/runtime-protocol-v1.js.map +1 -1
  44. package/dist/runtime/release-receipts.d.ts +5 -3
  45. package/dist/runtime/release-receipts.d.ts.map +1 -1
  46. package/dist/runtime/release-receipts.js +42 -1
  47. package/dist/runtime/release-receipts.js.map +1 -1
  48. package/dist/runtime/session-composer-control.d.ts +136 -0
  49. package/dist/runtime/session-composer-control.d.ts.map +1 -0
  50. package/dist/runtime/session-composer-control.js +532 -0
  51. package/dist/runtime/session-composer-control.js.map +1 -0
  52. package/dist/runtime/thread-projection.d.ts +17 -0
  53. package/dist/runtime/thread-projection.d.ts.map +1 -1
  54. package/dist/runtime/thread-projection.js +87 -0
  55. package/dist/runtime/thread-projection.js.map +1 -1
  56. package/dist/runtime/ui-events.d.ts +6 -1
  57. package/dist/runtime/ui-events.d.ts.map +1 -1
  58. package/dist/runtime/ui-events.js.map +1 -1
  59. package/dist/services/session-storage.d.ts +36 -0
  60. package/dist/services/session-storage.d.ts.map +1 -1
  61. package/dist/services/session-storage.js +77 -1
  62. package/dist/services/session-storage.js.map +1 -1
  63. package/dist/web/event-hub.js +10 -0
  64. package/dist/web/event-hub.js.map +1 -1
  65. package/dist/web/file-read-service.d.ts +9 -0
  66. package/dist/web/file-read-service.d.ts.map +1 -1
  67. package/dist/web/file-read-service.js +17 -7
  68. package/dist/web/file-read-service.js.map +1 -1
  69. package/dist/web/git-read-model-service.d.ts +8 -0
  70. package/dist/web/git-read-model-service.d.ts.map +1 -1
  71. package/dist/web/git-read-model-service.js +28 -2
  72. package/dist/web/git-read-model-service.js.map +1 -1
  73. package/dist/web/protocol.d.ts +142 -0
  74. package/dist/web/protocol.d.ts.map +1 -1
  75. package/dist/web/protocol.js +207 -0
  76. package/dist/web/protocol.js.map +1 -1
  77. package/dist/web/server.d.ts.map +1 -1
  78. package/dist/web/server.js +45 -8
  79. package/dist/web/server.js.map +1 -1
  80. package/dist/web/terminal-manager.d.ts +1 -0
  81. package/dist/web/terminal-manager.d.ts.map +1 -1
  82. package/dist/web/terminal-manager.js +41 -9
  83. package/dist/web/terminal-manager.js.map +1 -1
  84. package/dist/web/workbench-controller.d.ts +26 -9
  85. package/dist/web/workbench-controller.d.ts.map +1 -1
  86. package/dist/web/workbench-controller.js +545 -29
  87. package/dist/web/workbench-controller.js.map +1 -1
  88. package/dist/web-client/assets/{DiffViewer-DpKoD07C.js → DiffViewer-eaQNsnCh.js} +1 -1
  89. package/dist/web-client/assets/FilesPanel-DYs1MhoM.js +2 -0
  90. package/dist/web-client/assets/{GitPanel-D20StcdG.js → GitPanel-CnU4XVr7.js} +1 -1
  91. package/dist/web-client/assets/{ReviewPanel-3MVWbMQV.js → ReviewPanel-BDxG670T.js} +1 -1
  92. package/dist/web-client/assets/{TerminalPanel-BLQ592Fb.js → TerminalPanel-Q5mSSsoK.js} +15 -13
  93. package/dist/web-client/assets/index-BWV5sIvw.css +1 -0
  94. package/dist/web-client/assets/index-VVUKj6FE.js +17 -0
  95. package/dist/web-client/index.html +2 -2
  96. package/docs/architecture/agent-mode-permission-contract.md +87 -0
  97. package/docs/architecture/v0.3.1-web-api.yaml +53 -5
  98. package/docs/architecture/v0.3.2-web-api.yaml +3029 -0
  99. package/docs/migration/v0.3.1-to-v0.3.2.md +94 -0
  100. package/docs/plan/v0.3.1-web-workbench-professional-shell-plan.md +1 -1
  101. package/docs/plan/v0.3.2-web-workbench-layout-and-composer-plan.md +869 -0
  102. package/docs/readme.md +12 -2
  103. package/docs/test/v0.3.1-web-workbench-e2e-plan.md +1 -1
  104. package/docs/test/v0.3.2-web-workbench-e2e-plan.md +100 -0
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +6 -1
  107. package/dist/web-client/assets/FilesPanel-BkoLmgjR.js +0 -2
  108. package/dist/web-client/assets/index-BkIDl_xk.js +0 -16
  109. package/dist/web-client/assets/index-DXMpnWE8.css +0 -1
@@ -11,11 +11,14 @@ const durable_tool_receipt_reader_1 = require("../runtime/durable-tool-receipt-r
11
11
  const mcp_1 = require("../runtime/mcp");
12
12
  const product_bootstrap_1 = require("../runtime/product-bootstrap");
13
13
  const canonical_1 = require("../runtime/protocol/canonical");
14
+ const plan_review_1 = require("../runtime/plan-review");
14
15
  const thread_session_index_1 = require("../runtime/thread-session-index");
15
16
  const thread_session_view_1 = require("../runtime/thread-session-view");
16
17
  const tool_detail_repository_1 = require("../runtime/tool-detail-repository");
17
18
  const turn_commit_1 = require("../runtime/turn-commit");
19
+ const session_composer_control_1 = require("../runtime/session-composer-control");
18
20
  const global_config_1 = require("../services/global-config");
21
+ const redaction_1 = require("../services/redaction");
19
22
  const settings_coordinator_1 = require("../services/settings-coordinator");
20
23
  const session_storage_1 = require("../services/session-storage");
21
24
  const workspace_registry_1 = require("../services/workspace-registry");
@@ -30,6 +33,8 @@ var errors_2 = require("./errors");
30
33
  Object.defineProperty(exports, "WebWorkbenchError", { enumerable: true, get: function () { return errors_2.WebWorkbenchError; } });
31
34
  const SESSION_VIEW_CACHE_BYTES = 16 * 1024 * 1024;
32
35
  const MAX_MUTATION_RESULTS = 4096;
36
+ const MAX_CONTEXT_REFERENCE_BYTES = 64 * 1024;
37
+ const MAX_CONTEXT_MANIFEST_BYTES = 256 * 1024;
33
38
  /** Owns the sole runtime/controller pair exposed through the local Web host. */
34
39
  class WebWorkbenchController {
35
40
  constructor(options) {
@@ -38,6 +43,8 @@ class WebWorkbenchController {
38
43
  this.sessionViewBytes = 0;
39
44
  this.toolDetails = new tool_detail_repository_1.FileToolDetailRepository();
40
45
  this.contextRevisionValue = (0, crypto_1.randomUUID)();
46
+ this.composerRevisionValue = (0, crypto_1.randomUUID)();
47
+ this.suppressComposerEdges = 0;
41
48
  this.suppressContextEdges = 0;
42
49
  this.latestStatus = 'Ready';
43
50
  this.sessionTransition = false;
@@ -131,18 +138,15 @@ class WebWorkbenchController {
131
138
  throw mapSettingsError(error);
132
139
  }
133
140
  }
134
- listWorkspaces() {
135
- const counts = new Map();
136
- for (const session of (0, session_storage_1.listSessions)()) {
137
- try {
138
- const path = canonicalDirectory(session.projectPath);
139
- counts.set(path, (counts.get(path) ?? 0) + 1);
140
- }
141
- catch {
142
- // Stale projects remain in session history but are not selectable workspaces.
143
- }
144
- }
145
- return Object.freeze(this.workspaceRegistry.list().map(entry => {
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 => {
146
150
  let available = false;
147
151
  try {
148
152
  available = canonicalDirectory(entry.canonicalPath) === entry.canonicalPath;
@@ -161,8 +165,13 @@ class WebWorkbenchController {
161
165
  ...(entry.pinnedOrder !== undefined ? { pinnedOrder: entry.pinnedOrder } : {}),
162
166
  });
163
167
  }));
168
+ if (context)
169
+ this.assertContextGuard(context);
170
+ return result;
164
171
  }
165
- listWorkspaceSessions(workspaceId) {
172
+ listWorkspaceSessions(workspaceId, context) {
173
+ if (context)
174
+ this.assertContextGuard(context);
166
175
  const entry = this.workspaceRegistry.find(workspaceId);
167
176
  if (!entry) {
168
177
  throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
@@ -173,9 +182,14 @@ class WebWorkbenchController {
173
182
  catch {
174
183
  throw new errors_1.WebWorkbenchError(409, 'Workspace is unavailable.', 'workspace_unavailable');
175
184
  }
176
- return Object.freeze((0, session_storage_1.listProjectSessions)(entry.canonicalPath).map(protocol_1.projectSessionSummary));
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;
177
189
  }
178
- async workspaceProjectSummary(workspaceId) {
190
+ async workspaceProjectSummary(workspaceId, context) {
191
+ if (context)
192
+ this.assertContextGuard(context);
179
193
  const entry = this.workspaceRegistry.find(workspaceId);
180
194
  if (!entry) {
181
195
  throw new errors_1.WebWorkbenchError(404, 'Workspace was not found.', 'workspace_not_found');
@@ -188,7 +202,7 @@ class WebWorkbenchController {
188
202
  throw new errors_1.WebWorkbenchError(409, 'Workspace is unavailable.', 'workspace_unavailable');
189
203
  }
190
204
  const status = await new git_read_model_service_1.GitReadModelServiceV1(workspace).status({ pageSize: 2000 });
191
- return Object.freeze({
205
+ const result = Object.freeze({
192
206
  workspaceId,
193
207
  repositoryRevision: status.repositoryRevision,
194
208
  isRepository: status.isRepository,
@@ -198,6 +212,9 @@ class WebWorkbenchController {
198
212
  dirtyCount: status.totalFiles,
199
213
  conflictCount: status.conflicted.length,
200
214
  });
215
+ if (context)
216
+ this.assertContextGuard(context);
217
+ return result;
201
218
  }
202
219
  setWorkspacePinned(workspaceId, pinned, context) {
203
220
  this.assertContextGuard(context);
@@ -385,15 +402,206 @@ class WebWorkbenchController {
385
402
  pendingApprovals: Object.freeze(pendingPermissions),
386
403
  goal,
387
404
  plan,
405
+ composer: this.projectComposerState(session, active),
388
406
  recoveryDiagnostics: indexedPage ? [] : (view?.diagnostics ?? []),
389
407
  });
390
408
  if (context)
391
409
  this.assertContextGuard(context);
392
410
  return snapshot;
393
411
  }
394
- async skills() {
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
+ }),
487
+ });
488
+ }
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);
395
603
  const descriptors = (await this.runtimeValue.inspectSkills?.()) ?? [];
396
- return Object.freeze(descriptors.map(descriptor => Object.freeze({
604
+ const result = Object.freeze(descriptors.map(descriptor => Object.freeze({
397
605
  id: descriptor.id,
398
606
  name: descriptor.name,
399
607
  description: descriptor.description,
@@ -404,8 +612,13 @@ class WebWorkbenchController {
404
612
  requestedCapabilities: Object.freeze([...descriptor.requestedCapabilities]),
405
613
  digest: descriptor.digest,
406
614
  })));
615
+ if (context)
616
+ this.assertContextGuard(context);
617
+ return result;
407
618
  }
408
- mcp() {
619
+ mcp(context) {
620
+ if (context)
621
+ this.assertContextGuard(context);
409
622
  const descriptors = this.runtimeValue.inspectMcp?.() ?? [];
410
623
  let runtimeServers = new Map();
411
624
  try {
@@ -417,7 +630,7 @@ class WebWorkbenchController {
417
630
  catch {
418
631
  // A concurrent session transition may close the previous diagnostics graph.
419
632
  }
420
- return Object.freeze(descriptors.map(descriptor => {
633
+ const result = Object.freeze(descriptors.map(descriptor => {
421
634
  const runtime = runtimeServers.get(descriptor.id);
422
635
  return Object.freeze({
423
636
  id: descriptor.id,
@@ -435,20 +648,33 @@ class WebWorkbenchController {
435
648
  : {}),
436
649
  });
437
650
  }));
651
+ if (context)
652
+ this.assertContextGuard(context);
653
+ return result;
438
654
  }
439
- async listToolDetails() {
440
- return Object.freeze(await this.toolDetails.list(this.workspaceValue));
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;
441
662
  }
442
- async readToolDetail(callId, offsetBytes, limitBytes) {
663
+ async readToolDetail(callId, offsetBytes, limitBytes, context) {
664
+ if (context)
665
+ this.assertContextGuard(context);
443
666
  const entry = (await this.toolDetails.list(this.workspaceValue)).find(detail => detail.callId === callId || detail.artifactId === callId);
444
667
  if (!entry?.artifactId)
445
668
  throw new errors_1.WebWorkbenchError(404, 'Tool detail was not found.');
446
- return this.toolDetails.read({
669
+ const result = await this.toolDetails.read({
447
670
  callId: entry.callId,
448
671
  sequence: entry.sequence,
449
672
  artifactId: entry.artifactId,
450
673
  outputBytes: entry.outputBytes,
451
674
  }, { offsetBytes, limitBytes }, this.workspaceValue);
675
+ if (context)
676
+ this.assertContextGuard(context);
677
+ return result;
452
678
  }
453
679
  listFiles(context, input) {
454
680
  this.assertContextGuard(context);
@@ -648,16 +874,179 @@ class WebWorkbenchController {
648
874
  }
649
875
  dispatch(raw) {
650
876
  const command = (0, protocol_1.parseWebCommand)(raw);
651
- return this.executeMutation(command.requestId, 'command', command, () => {
877
+ return this.executeMutation(command.requestId, 'command', command, async () => {
878
+ this.assertCommandSession(command.expectedSessionId);
879
+ const resolved = await this.resolveCommandContext(command);
652
880
  this.assertCommandSession(command.expectedSessionId);
653
- const runtimeResult = this.controllerValue.handle((0, protocol_1.toAgentRuntimeInput)(command));
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);
654
890
  return Object.freeze({
655
891
  requestId: command.requestId,
656
892
  result: runtimeResult.type,
657
893
  detail: JSON.stringify(runtimeResult),
894
+ ...(resolved ? { contextReceipt: resolved.receipt } : {}),
658
895
  });
659
896
  });
660
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
+ }
661
1050
  async updateSettings(input) {
662
1051
  this.assertSettingsAvailable();
663
1052
  const update = this.runtimeValue.updateSettings;
@@ -716,7 +1105,9 @@ class WebWorkbenchController {
716
1105
  this.mutationResults.set(requestId, { fingerprint, result });
717
1106
  return result;
718
1107
  }
719
- async diagnostics() {
1108
+ async diagnostics(context) {
1109
+ if (context)
1110
+ this.assertContextGuard(context);
720
1111
  const state = this.runtimeValue.store.getSnapshot();
721
1112
  const mcp = (0, mcp_1.loadFirstPartyMcpConfigurationV1)();
722
1113
  const activeSessionId = this.runtimeValue.getSession()?.id ?? null;
@@ -725,7 +1116,7 @@ class WebWorkbenchController {
725
1116
  const harness = activeSessionId
726
1117
  ? ((await this.runtimeValue.getHarnessDiagnostics?.()) ?? null)
727
1118
  : null;
728
- return {
1119
+ const result = {
729
1120
  workspace: this.workspaceValue,
730
1121
  activeSessionId,
731
1122
  configured: this.runtimeValue.isConfigured,
@@ -743,7 +1134,14 @@ class WebWorkbenchController {
743
1134
  mcp: { servers: mcpServerIds(mcp) },
744
1135
  harness,
745
1136
  eventStream: this.eventHub.snapshot(),
1137
+ performance: {
1138
+ files: this.fileService.performanceCounters(),
1139
+ git: this.gitService.performanceCounters(),
1140
+ },
746
1141
  };
1142
+ if (context)
1143
+ this.assertContextGuard(context);
1144
+ return result;
747
1145
  }
748
1146
  async shutdown() {
749
1147
  if (this.closed)
@@ -752,11 +1150,13 @@ class WebWorkbenchController {
752
1150
  await this.controllerValue.stopActiveTurn();
753
1151
  await this.controllerValue.waitForIdle();
754
1152
  this.closeWorkspaceWatchers();
1153
+ this.closeComposerObserver();
755
1154
  await this.terminalManager.shutdown();
756
1155
  await this.runtimeValue.shutdown();
757
1156
  this.eventHub.close();
758
1157
  }
759
1158
  async installRuntime(workspace, publishState = true) {
1159
+ this.closeComposerObserver();
760
1160
  const runtime = await this.createRuntime(workspace);
761
1161
  this.closeWorkspaceWatchers();
762
1162
  this.activeOrionRuntime = undefined;
@@ -775,6 +1175,21 @@ class WebWorkbenchController {
775
1175
  if (this.suppressContextEdges > 0)
776
1176
  return undefined;
777
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
+ }
778
1193
  const activeThread = activeSessionId && this.activeOrionRuntime?.sessionId === activeSessionId
779
1194
  ? this.activeOrionRuntime.runtime.thread.getProjection().threadId
780
1195
  : undefined;
@@ -806,6 +1221,10 @@ class WebWorkbenchController {
806
1221
  suppressAbortNotice: true,
807
1222
  },
808
1223
  });
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());
809
1228
  this.installWorkspaceWatchers();
810
1229
  if (publishState)
811
1230
  this.emitState(false);
@@ -815,13 +1234,27 @@ class WebWorkbenchController {
815
1234
  return;
816
1235
  if (advanceRevision)
817
1236
  this.contextRevisionValue = (0, crypto_1.randomUUID)();
1237
+ if (advanceRevision)
1238
+ this.composerRevisionValue = (0, crypto_1.randomUUID)();
818
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
+ }
819
1252
  this.eventHub.emit({
820
1253
  type: 'workbench_state',
821
1254
  contextRevision: this.contextRevisionValue,
822
1255
  workspaceId: workspace.id,
823
1256
  workspace: this.workspaceValue,
824
- activeSessionId: this.runtimeValue.getSession()?.id ?? null,
1257
+ activeSessionId: sessionId,
825
1258
  });
826
1259
  }
827
1260
  emitWorkspaceResourceInvalidation(reason) {
@@ -848,6 +1281,42 @@ class WebWorkbenchController {
848
1281
  }, 120);
849
1282
  this.resourceInvalidationTimer.unref();
850
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
+ }
851
1320
  installWorkspaceWatchers() {
852
1321
  const watchPaths = [this.workspaceValue, (0, path_1.resolve)(this.workspaceValue, '.git')].filter(path => (0, fs_1.existsSync)(path));
853
1322
  for (const path of watchPaths) {
@@ -1071,6 +1540,53 @@ function mapSettingsError(error) {
1071
1540
  }
1072
1541
  return new errors_1.WebWorkbenchError(503, 'The product Settings coordinator is unavailable.', 'settings_document_unavailable');
1073
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
+ });
1589
+ }
1074
1590
  function pickPlanReceipt(receipt) {
1075
1591
  return Object.freeze({
1076
1592
  body: receipt.plan,