@oxbit/cli 0.6.0 → 0.7.0

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.
@@ -5,7 +5,7 @@ import * as fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { spawn, execFileSync } from "node:child_process";
7
7
  import { parseArgs } from "node:util";
8
- import http from "node:http";
8
+ import http, { createServer } from "node:http";
9
9
  import os, { userInfo } from "node:os";
10
10
  import { createHash, randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
11
11
  import { WebSocketServer, WebSocket } from "ws";
@@ -43,6 +43,13 @@ function workspaceDataDir(root, home = os.homedir()) {
43
43
  return path.join(home, ".oxbit", "workspaces", id);
44
44
  }
45
45
  const MAX_MESSAGE_BYTES = 2 * 1024 * 1024;
46
+ const MAX_FILE_BYTES = 20 * 1024 * 1024;
47
+ const MAX_FILE_WRITE_MESSAGE_BYTES = MAX_FILE_BYTES * 6 + MAX_MESSAGE_BYTES;
48
+ const MAX_COLLABORATION_UPDATE_BYTES = Math.ceil(MAX_FILE_BYTES / 3) * 4 + MAX_MESSAGE_BYTES;
49
+ const requestMessageLimit = (method) => method === "fs.write" || method === "collab.save" ? MAX_FILE_WRITE_MESSAGE_BYTES : method === "collab.update" ? MAX_COLLABORATION_UPDATE_BYTES + MAX_MESSAGE_BYTES : MAX_MESSAGE_BYTES;
50
+ function operationEpoch(id) {
51
+ return /^op:([0-9a-f-]{36}):[0-9a-f-]{36}$/.exec(id)?.[1];
52
+ }
46
53
  const MAX_BUFFER_BYTES = 1024 * 1024;
47
54
  const READ_CHUNK_BYTES = 512 * 1024;
48
55
  const RUNTIME_SERVICE_TYPE = "_oxbit._tcp";
@@ -57,8 +64,9 @@ class RpcError extends Error {
57
64
  data;
58
65
  }
59
66
  function parseClientMessage(raw) {
60
- if (new TextEncoder().encode(raw).byteLength > MAX_MESSAGE_BYTES)
61
- throw new RpcError("TOO_LARGE", "Message exceeds 2 MiB");
67
+ const bytes = new TextEncoder().encode(raw).byteLength;
68
+ if (bytes > MAX_FILE_WRITE_MESSAGE_BYTES)
69
+ throw new RpcError("TOO_LARGE", "Message exceeds the file save limit");
62
70
  let m;
63
71
  try {
64
72
  m = JSON.parse(raw);
@@ -67,6 +75,8 @@ function parseClientMessage(raw) {
67
75
  }
68
76
  if (!m || m.v !== 1 || !["request", "cancel", "ack"].includes(m.type))
69
77
  throw new RpcError("INVALID_MESSAGE", "Invalid protocol message");
78
+ if (bytes > (m.type === "request" ? requestMessageLimit(m.method) : MAX_MESSAGE_BYTES))
79
+ throw new RpcError("TOO_LARGE", "Message exceeds 2 MiB");
70
80
  if (m.type === "ack") {
71
81
  if (typeof m.stream !== "string" || !m.stream || m.stream.length > 256 || !Number.isSafeInteger(m.seq) || m.seq < 0)
72
82
  throw new RpcError("INVALID_MESSAGE", "Invalid acknowledgement");
@@ -224,6 +234,10 @@ class WorkspaceFiles {
224
234
  protected(candidate) {
225
235
  return [...this.protectedRoots].some((root) => candidate === root || candidate.startsWith(root + path.sep));
226
236
  }
237
+ protectDescendants(candidate) {
238
+ if ([...this.protectedRoots].some((root) => root.startsWith(candidate + path.sep)))
239
+ throw new RpcError("PATH_DENIED", "Directory contains protected files");
240
+ }
227
241
  inside(candidate) {
228
242
  const rel = path.relative(this.root, candidate);
229
243
  return rel === "" || !rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel);
@@ -311,7 +325,7 @@ class WorkspaceFiles {
311
325
  const stat = await fs.stat(full);
312
326
  if (!stat.isFile())
313
327
  throw new RpcError("NOT_FILE", "Path is not a regular file");
314
- if (stat.size > 20 * 1024 * 1024)
328
+ if (stat.size > MAX_FILE_BYTES)
315
329
  throw new RpcError(
316
330
  "FILE_TOO_LARGE",
317
331
  "Files above 20 MiB must be opened outside this editor"
@@ -329,7 +343,7 @@ class WorkspaceFiles {
329
343
  const stat = await fs.stat(full);
330
344
  if (!stat.isFile())
331
345
  throw new RpcError("NOT_FILE", "Path is not a regular file");
332
- if (stat.size > 20 * 1024 * 1024)
346
+ if (stat.size > MAX_FILE_BYTES)
333
347
  throw new RpcError(
334
348
  "FILE_TOO_LARGE",
335
349
  "Files above 20 MiB must be opened outside this editor"
@@ -393,7 +407,7 @@ class WorkspaceFiles {
393
407
  actualRevision: actual
394
408
  });
395
409
  const bytes = encode(text2, options2.encoding, options2.eol);
396
- if (bytes.length > 20 * 1024 * 1024)
410
+ if (bytes.length > MAX_FILE_BYTES)
397
411
  throw new RpcError(
398
412
  "FILE_TOO_LARGE",
399
413
  "Files above 20 MiB are unsupported"
@@ -427,6 +441,8 @@ class WorkspaceFiles {
427
441
  const target = await this.resolve(to, true);
428
442
  if (from === this.root || target === this.root)
429
443
  throw new RpcError("PATH_DENIED", "Cannot rename workspace root");
444
+ this.protectDescendants(from);
445
+ this.protectDescendants(target);
430
446
  try {
431
447
  await fs.lstat(target);
432
448
  throw new RpcError("EXISTS", "Destination already exists");
@@ -439,9 +455,103 @@ class WorkspaceFiles {
439
455
  const full = await this.resolve(relative);
440
456
  if (full === this.root)
441
457
  throw new RpcError("PATH_DENIED", "Cannot delete workspace root");
458
+ this.protectDescendants(full);
442
459
  await fs.rm(path.resolve(this.root, relative), { recursive: true });
443
460
  }
444
461
  }
462
+ const OPERATIONS_PER_EPOCH = 128;
463
+ const MAX_RETAINED_OPERATIONS = OPERATIONS_PER_EPOCH * 2;
464
+ const MAX_RUNNING_OPERATIONS = 64;
465
+ const MAX_OPERATION_RESULT_BYTES = 1024 * 1024;
466
+ class OperationHistory {
467
+ records = /* @__PURE__ */ new Map();
468
+ state;
469
+ constructor(state2, records2 = []) {
470
+ this.state = state2 ? { ...state2 } : { epoch: randomUUID(), admitted: 0 };
471
+ for (const record of records2) {
472
+ const operation = { ...record };
473
+ if (operation.status === "running") {
474
+ operation.status = "interrupted";
475
+ operation.error = { code: "INTERRUPTED", message: "Runtime stopped before operation completion was recorded; inspect state before retrying with a new request ID" };
476
+ }
477
+ this.records.set(this.key(operation.owner, operation.id), operation);
478
+ }
479
+ if (!state2 && this.records.size > OPERATIONS_PER_EPOCH || this.records.size > MAX_RETAINED_OPERATIONS + MAX_RUNNING_OPERATIONS) {
480
+ this.state = { epoch: randomUUID(), admitted: 0 };
481
+ while (this.records.size > OPERATIONS_PER_EPOCH) this.records.delete(this.records.keys().next().value);
482
+ } else if (!state2) this.state.admitted = this.records.size;
483
+ this.compactResults();
484
+ }
485
+ get epoch() {
486
+ return this.state.epoch;
487
+ }
488
+ checkpoint() {
489
+ return { ...this.state };
490
+ }
491
+ values() {
492
+ return this.records.values();
493
+ }
494
+ key(owner, id) {
495
+ return `${owner}:${id}`;
496
+ }
497
+ get(owner, id) {
498
+ return this.records.get(this.key(owner, id));
499
+ }
500
+ accepts(id) {
501
+ const epoch = operationEpoch(id);
502
+ return epoch === void 0 ? !id.startsWith("op:") : epoch === this.state.epoch || epoch === this.state.previousEpoch;
503
+ }
504
+ status(owner, id) {
505
+ return this.get(owner, id) ?? (this.accepts(id) ? { id, status: "unknown" } : { id, status: "expired", error: { code: "OPERATION_EXPIRED", message: "Operation history expired; inspect current state before submitting a new operation" } });
506
+ }
507
+ begin(owner, id, method) {
508
+ if (this.get(owner, id)) throw new RpcError("DUPLICATE_ID", "Operation already exists");
509
+ if (!this.accepts(id)) throw new RpcError("OPERATION_EXPIRED", "Operation ID expired; inspect current state before submitting a new operation");
510
+ if ([...this.records.values()].filter((operation2) => operation2.status === "running").length >= MAX_RUNNING_OPERATIONS)
511
+ throw new RpcError("BUSY", "Maximum 64 concurrent operations");
512
+ const operation = { id, owner, method, status: "running", startedAt: Date.now() };
513
+ this.records.set(this.key(owner, id), operation);
514
+ if (++this.state.admitted >= OPERATIONS_PER_EPOCH) {
515
+ this.state = { epoch: randomUUID(), previousEpoch: this.state.epoch, admitted: 0 };
516
+ let retained = 0;
517
+ for (const [key, record] of [...this.records].reverse())
518
+ if (record.status !== "running" && (!this.accepts(record.id) || ++retained > OPERATIONS_PER_EPOCH)) this.records.delete(key);
519
+ }
520
+ return operation;
521
+ }
522
+ complete(operation, result) {
523
+ operation.status = "completed";
524
+ operation.result = result;
525
+ this.compactResults();
526
+ }
527
+ fail(operation, error) {
528
+ operation.status = "failed";
529
+ operation.error = error;
530
+ delete operation.result;
531
+ this.compactResults();
532
+ }
533
+ compactResults() {
534
+ let remaining = MAX_OPERATION_RESULT_BYTES;
535
+ for (const operation of [...this.records.values()].reverse()) {
536
+ if (operation.error) {
537
+ operation.error.code = operation.error.code.slice(0, 128);
538
+ if (operation.error.message.length > 4096) {
539
+ operation.error.message = operation.error.message.slice(0, 4096);
540
+ operation.resultExpired = true;
541
+ }
542
+ }
543
+ const result = operation.result;
544
+ const largeText = (operation.method === "fs.write" || operation.method === "collab.save") && typeof result?.text === "string" && result.text.length > remaining;
545
+ const body = largeText ? void 0 : JSON.stringify({ result: operation.result, data: operation.error?.data });
546
+ const bytes = body === void 0 ? remaining + 1 : body === "{}" ? 0 : Buffer.byteLength(body);
547
+ if (bytes > remaining) {
548
+ delete operation.result;
549
+ if (operation.error) delete operation.error.data;
550
+ operation.resultExpired = true;
551
+ } else remaining -= bytes;
552
+ }
553
+ }
554
+ }
445
555
  function killProcess(child) {
446
556
  if (child.exitCode !== null) return;
447
557
  try {
@@ -544,6 +654,7 @@ async function runCommand(command, args, options2) {
544
654
  });
545
655
  });
546
656
  }
657
+ const minimumColumns = 4;
547
658
  class Processes {
548
659
  constructor(root, emit, loadPty = loadNodePty) {
549
660
  this.root = root;
@@ -575,7 +686,7 @@ class Processes {
575
686
  const id = randomUUID(), { shell, args } = resolveTerminalShell();
576
687
  const terminal2 = pty.spawn(shell, args, {
577
688
  name: "xterm-256color",
578
- cols: this.dimension(cols, 1e3),
689
+ cols: this.dimension(cols, 1e3, minimumColumns),
579
690
  rows: this.dimension(rows, 500),
580
691
  cwd: this.root,
581
692
  env: { ...process.env, SHELL: shell, TERM: "xterm-256color" }
@@ -622,10 +733,10 @@ class Processes {
622
733
  });
623
734
  return { id };
624
735
  }
625
- dimension(value, max) {
736
+ dimension(value, max, min = 1) {
626
737
  if (!Number.isSafeInteger(value) || value < 1 || value > max)
627
738
  throw new RpcError("INVALID_PARAMS", "Invalid terminal dimensions");
628
- return value;
739
+ return Math.max(value, min);
629
740
  }
630
741
  flow(session) {
631
742
  const slow = session.subscribers.size === 0 || [...session.subscribers.values()].some(
@@ -647,7 +758,7 @@ class Processes {
647
758
  }
648
759
  resize(id, owner, cols, rows) {
649
760
  this.terminal(id, owner).pty.resize(
650
- this.dimension(cols, 1e3),
761
+ this.dimension(cols, 1e3, minimumColumns),
651
762
  this.dimension(rows, 500)
652
763
  );
653
764
  }
@@ -775,7 +886,7 @@ function validateJson(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
775
886
  }
776
887
  seen.delete(value);
777
888
  }
778
- function object$2(value) {
889
+ function object$3(value) {
779
890
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a JSON object");
780
891
  }
781
892
  function strings$1(value) {
@@ -783,17 +894,17 @@ function strings$1(value) {
783
894
  }
784
895
  function validateFileAssociations(value) {
785
896
  validateJson(value);
786
- object$2(value);
897
+ object$3(value);
787
898
  for (const [pattern, id] of Object.entries(value))
788
899
  if (!pattern || pattern.length > 1024 || typeof id !== "string" || !/^[\w+-]+$/.test(id)) throw new Error("File associations map glob patterns to language IDs");
789
900
  }
790
901
  function validateLanguageServers(value) {
791
902
  validateJson(value);
792
- object$2(value);
903
+ object$3(value);
793
904
  const keys = ["enabled", "selectors", "rootMarkers", "executable", "args", "env", "initializationOptions", "settings", "priority"];
794
905
  for (const [id, config] of Object.entries(value)) {
795
906
  if (!/^[a-zA-Z0-9._-]+$/.test(id)) throw new Error("Invalid language server ID");
796
- object$2(config);
907
+ object$3(config);
797
908
  for (const key of Object.keys(config)) if (!keys.includes(key)) throw new Error(`Unknown language server option: ${key}`);
798
909
  if (config.enabled !== void 0 && typeof config.enabled !== "boolean") throw new Error("enabled must be boolean");
799
910
  if (config.priority !== void 0 && (typeof config.priority !== "number" || !Number.isFinite(config.priority))) throw new Error("priority must be finite");
@@ -806,11 +917,11 @@ function validateLanguageServers(value) {
806
917
  if (config.selectors !== void 0) {
807
918
  if (!Array.isArray(config.selectors)) throw new Error("selectors must be an array");
808
919
  for (const selector of config.selectors) {
809
- object$2(selector);
920
+ object$3(selector);
810
921
  if (Object.keys(selector).some((key) => !["language", "pattern", "scheme"].includes(key)) || !Object.keys(selector).length || Object.values(selector).some((item) => typeof item !== "string") || selector.scheme !== void 0 && selector.scheme !== "file") throw new Error("Invalid document selector");
811
922
  }
812
923
  }
813
- for (const key of ["env", "settings", "initializationOptions"]) if (config[key] !== void 0) object$2(config[key]);
924
+ for (const key of ["env", "settings", "initializationOptions"]) if (config[key] !== void 0) object$3(config[key]);
814
925
  if (config.env && Object.entries(config.env).some(([key, item]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof item !== "string" || item.includes("\0"))) throw new Error("Invalid environment override");
815
926
  const credentials = (item) => Boolean(item && typeof item === "object" && Object.entries(item).some(([key, child]) => /(?:^|[._-])licen[cs]e(?:[._-]?key)?$/i.test(key) || credentials(child)));
816
927
  if (credentials(config.initializationOptions) || credentials(config.settings) || credentials(config.env)) throw new Error("Store license credentials in the runtime-local language server credential file");
@@ -821,7 +932,7 @@ const $id$1 = "https://oxbit.dev/schemas/settings.v1.schema.json";
821
932
  const title$2 = "Oxbit Settings";
822
933
  const description$2 = "User, private workspace and repository settings. Literal dotted IDs; objects merge recursively, arrays and scalar values replace. Unrecognized extension settings are preserved. Null is a value, not a reset, and must be valid for its setting.";
823
934
  const type$2 = "object";
824
- const properties$2 = /* @__PURE__ */ JSON.parse(`{"$schema":{"type":"string","description":"JSON Schema URI. Ignored when merging effective preferences."},"agentACP.provider":{"title":"Default agent","type":"string","default":"codex","enum":["codex","cursor","amp"]},"agentACP.codex.command":{"title":"Codex ACP executable","type":"string","default":"npx"},"agentACP.codex.args":{"title":"Codex ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/codex-acp@1.10.0\\"]"},"agentACP.cursor.command":{"title":"Cursor ACP executable","type":"string","default":"agent"},"agentACP.cursor.args":{"title":"Cursor ACP arguments (JSON array)","type":"string","default":"[\\"acp\\"]"},"agentACP.amp.command":{"title":"Amp Agent ACP executable","type":"string","default":"npx"},"agentACP.amp.args":{"title":"Amp Agent ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"amp-acp@0.9.0\\"]"},"workbench.keymap":{"title":"Keymap","description":"Keyboard layout applied on top of the Oxbit defaults. User keybindings always win.","type":"string","default":"default","enum":["default","vscode","jetbrains","macos","sublime","atom","visual-studio","emacs"]},"runtime.keepAlive":{"title":"Keep Runtime Alive","description":"How long a runtime Oxbit started keeps running after the last client disconnects or its connection heartbeats stop. Terminals and tasks continue while it runs.","type":"string","default":"75s","enum":["75s","15m","1h","8h","untilStopped"]},"runtime.autoReconnect":{"title":"Reconnect Automatically","description":"Reconnect to the runtime after the connection drops, including when the iOS app returns to the foreground.","type":"boolean","default":true},"workbench.tooltipDelay":{"title":"Tooltip Delay","description":"Delay in milliseconds before showing a tooltip on hover. Keyboard focus shows tooltips immediately.","type":"number","default":400,"minimum":0,"maximum":5000},"workbench.colorTheme":{"title":"Color Theme","description":"Specifies the color theme used in the workbench.","type":"string","default":"Graphite (dark)"},"workbench.density":{"title":"Layout Density","description":"Controls row heights, padding and label sizes across the workbench.","type":"string","default":"compact","enum":["compact","comfortable"]},"workbench.sidebarLocation":{"title":"Sidebar Location","description":"Where the primary sidebar is docked.","type":"string","default":"left","enum":["left","right"]},"editor.fontFamily":{"title":"Font Family","description":"Controls the font family of the editor.","type":"string","default":"'JetBrains Mono', ui-monospace, monospace"},"editor.fontSize":{"title":"Font Size","description":"Font size in pixels.","type":"number","default":13,"minimum":8,"maximum":32},"editor.lineHeight":{"title":"Line Height","description":"Line height in pixels. 0 uses 1.55 × font size.","type":"number","default":20,"minimum":0,"maximum":60},"editor.fontLigatures":{"title":"Font Ligatures","description":"Enables font ligatures when the font supports them.","type":"boolean","default":false},"editor.tabSize":{"title":"Tab Size","description":"The number of spaces a tab is equal to.","type":"number","default":2,"minimum":1,"maximum":8},"editor.insertSpaces":{"title":"Insert Spaces","description":"Insert spaces when pressing Tab.","type":"boolean","default":true},"editor.detectIndentation":{"title":"Detect Indentation","description":"Detect Tab Size and Insert Spaces from file contents on open.","type":"boolean","default":true},"editor.renderIndentGuides":{"title":"Indentation Guides","description":"Render vertical guides for each indent level.","type":"boolean","default":true},"editor.wordWrap":{"title":"Word Wrap","description":"Controls how lines should wrap.","type":"string","default":"off","enum":["off","on","bounded"]},"editor.renderWhitespace":{"title":"Render Whitespace","description":"Controls how the editor renders whitespace characters.","type":"string","default":"selection","enum":["none","boundary","selection","all"]},"editor.cursorBlinking":{"title":"Cursor Blinking","description":"Cursor animation style.","type":"string","default":"blink","enum":["blink","smooth","phase","solid"]},"editor.minimap":{"title":"Minimap","description":"Show a minimap of the document.","type":"boolean","default":false},"files.autoSave":{"title":"Auto Save","description":"Controls auto save of editors that have unsaved changes.","type":"string","default":"off","enum":["off","afterDelay","onFocusChange","onWindowChange"]},"files.autoSaveDelay":{"title":"Auto Save Delay","description":"Delay in milliseconds after which an editor is saved automatically. Applies when Auto Save is afterDelay.","type":"number","default":1000,"minimum":100,"maximum":60000},"files.trimTrailingWhitespace":{"title":"Trim Trailing Whitespace","description":"Remove trailing whitespace when saving.","type":"boolean","default":true},"editor.formatOnSave":{"title":"Format On Save","description":"Format a file on save. A formatter must be available.","type":"boolean","default":false},"editor.defaultFormatter":{"title":"Default Formatter","description":"Formatter used when several are available.","type":"string","default":"oxbit.prettier","anyOf":[{"enum":["oxbit.prettier","oxbit.builtin-ts"]},{"type":"string"}]},"terminal.fontSize":{"title":"Terminal Font Size","description":"Font size in pixels for the terminal.","type":"number","default":12,"minimum":8,"maximum":32},"terminal.scrollback":{"title":"Scrollback","description":"Maximum number of lines kept in the terminal buffer.","type":"number","default":5000,"minimum":100,"maximum":100000},"terminal.confirmOnKill":{"title":"Confirm On Kill","description":"Ask before killing a terminal with a running process.","type":"boolean","default":true},"scm.autoFetch":{"title":"Auto Fetch","description":"Periodically fetch from the default remote.","type":"boolean","default":true},"scm.diffLayout":{"title":"Diff Layout","description":"Default layout for diff editors.","type":"string","default":"side-by-side","enum":["side-by-side","inline"]},"files.associations":{"title":"File Associations","description":"Map filename or workspace-relative glob patterns to language IDs. Explicit associations take precedence over built-in detection.","type":"object","default":{},"propertyNames":{"minLength":1,"maxLength":1024},"additionalProperties":{"type":"string","pattern":"^[\\\\w+-]+$"}},"languageServers":{"title":"Language Servers","description":"Configure enabled state, selectors, rootMarkers, executable, args, env, initializationOptions, settings, and priority by server ID. Servers install on first use in a trusted runtime. License keys belong in the runtime-local credential file.","type":"object","default":{},"propertyNames":{"pattern":"^[a-zA-Z0-9._-]+$"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Enable this language server."},"selectors":{"type":"array","items":{"type":"object","properties":{"language":{"type":"string"},"pattern":{"type":"string"},"scheme":{"const":"file"}},"additionalProperties":false,"minProperties":1}},"rootMarkers":{"type":"array","items":{"type":"string","pattern":"^(?!/)(?!.*\\\\\\\\)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))[^\\\\u0000]+$","minLength":1}},"executable":{"type":"string","pattern":"^[^\\\\u0000]*$","minLength":1},"args":{"type":"array","items":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"env":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"additionalProperties":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"initializationOptions":{"type":"object","additionalProperties":true,"description":"Server-specific initialization options. Store license credentials in the runtime credential file."},"settings":{"type":"object","additionalProperties":true,"description":"Server-specific configuration; nested objects merge across files."},"priority":{"type":"number"}},"additionalProperties":false}},"ui.fontFamily":{"title":"UI Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"ui.fontSize":{"title":"UI Font Size","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":12,"minimum":8,"maximum":72},"ui.fontWeight":{"title":"UI Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"ui.fontStyle":{"title":"UI Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"ui.lineHeight":{"title":"UI Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1.4,"minimum":1,"maximum":3},"ui.letterSpacing":{"title":"UI Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"ui.fontLigatures":{"title":"UI Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"terminal.fontFamily":{"title":"Terminal Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"terminal.fontWeight":{"title":"Terminal Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"terminal.fontStyle":{"title":"Terminal Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"terminal.lineHeight":{"title":"Terminal Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1,"minimum":1,"maximum":3},"terminal.letterSpacing":{"title":"Terminal Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"terminal.fontLigatures":{"title":"Terminal Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"workbench.iconTheme":{"title":"File Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"workbench.productIconTheme":{"title":"Product Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"editor.semanticHighlighting":{"title":"Semantic Highlighting","description":"Color symbols using language server information.","type":"boolean","default":true},"editor.inlayHints.types":{"title":"Type Inlay Hints","description":"Show supported type hints in the visible editor.","type":"boolean","default":true},"editor.inlayHints.parameters":{"title":"Parameter Inlay Hints","description":"Show supported parameter hints in the visible editor.","type":"boolean","default":true},"editor.largeFileIntelligence":{"title":"Large File Intelligence","description":"Allow full semantic highlighting and inlay hints for files larger than 1 MiB.","type":"boolean","default":false},"workbench.locale":{"title":"Display Language","type":"string","default":"en","enum":["en","de","es","ja","zh"]},"workbench.reducedMotion":{"title":"Reduced Motion","type":"boolean","default":false},"desktop.projects.openBehavior":{"title":"Open projects","type":"string","default":"currentWindow","enum":["currentWindow","newWindow"]},"desktop.tools.gitPath":{"title":"git executable path","type":"string","default":""},"desktop.tools.ghPath":{"title":"gh executable path","type":"string","default":""},"project.intelligence":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Index project dependencies, imports and related files."},"exclude":{"type":"array","items":{"type":"string","maxLength":1024},"default":[]},"maxFiles":{"type":"integer","minimum":1,"maximum":100000,"default":20000},"maxFileBytes":{"type":"integer","minimum":1,"maximum":5242880,"default":1048576}},"additionalProperties":false,"title":"Project Intelligence","description":"Partial overrides of project.json intelligence. Objects merge across files."},"project.schemas":{"type":"object","properties":{"catalog":{"type":"boolean","default":true,"description":"Discover file schemas through SchemaStore. Bundled Oxbit settings support remains available."},"download":{"type":"boolean","default":true,"description":"Allow remote schema downloads; cached and bundled schemas work offline."},"associations":{"type":"array","maxItems":1000,"default":[],"items":{"type":"object","properties":{"url":{"type":"string","minLength":1,"description":"Schema URL or workspace file URI."},"fileMatch":{"type":"array","items":{"type":"string","maxLength":1024}},"schema":{"type":["object","boolean"],"description":"An inline JSON Schema."}},"additionalProperties":false,"anyOf":[{"required":["url"]},{"required":["schema"]}]}}},"additionalProperties":false,"title":"JSON Schemas","description":"Partial overrides of project.json schemas. Association arrays replace lower arrays."}}`);
935
+ const properties$2 = /* @__PURE__ */ JSON.parse(`{"$schema":{"type":"string","description":"JSON Schema URI. Ignored when merging effective preferences."},"agentACP.provider":{"title":"Default agent","type":"string","default":"codex","enum":["codex","cursor","amp"]},"agentACP.codex.command":{"title":"Codex ACP executable","type":"string","default":"npx"},"agentACP.codex.args":{"title":"Codex ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/codex-acp@2.2.2\\"]"},"agentACP.cursor.command":{"title":"Cursor ACP executable","type":"string","default":"agent"},"agentACP.cursor.args":{"title":"Cursor ACP arguments (JSON array)","type":"string","default":"[\\"acp\\"]"},"agentACP.amp.command":{"title":"Amp Agent ACP executable","type":"string","default":"npx"},"agentACP.amp.args":{"title":"Amp Agent ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"amp-acp@0.10.0\\"]"},"workbench.keymap":{"title":"Keymap","description":"Keyboard layout applied on top of the Oxbit defaults. User keybindings always win.","type":"string","default":"default","enum":["default","vscode","jetbrains","macos","sublime","atom","visual-studio","emacs"]},"runtime.keepAlive":{"title":"Keep Runtime Alive","description":"How long a runtime Oxbit started keeps running after the last client disconnects or its connection heartbeats stop. Terminals and tasks continue while it runs.","type":"string","default":"75s","enum":["75s","15m","1h","8h","untilStopped"]},"runtime.autoReconnect":{"title":"Reconnect Automatically","description":"Reconnect to the runtime after the connection drops, including when the iOS app returns to the foreground.","type":"boolean","default":true},"workbench.tooltipDelay":{"title":"Tooltip Delay","description":"Delay in milliseconds before showing a tooltip on hover. Keyboard focus shows tooltips immediately.","type":"number","default":400,"minimum":0,"maximum":5000},"workbench.colorTheme":{"title":"Color Theme","description":"Specifies the color theme used in the workbench.","type":"string","default":"Graphite (dark)"},"workbench.density":{"title":"Layout Density","description":"Controls row heights, padding and label sizes across the workbench.","type":"string","default":"compact","enum":["compact","comfortable"]},"workbench.sidebarLocation":{"title":"Sidebar Location","description":"Where the primary sidebar is docked.","type":"string","default":"left","enum":["left","right"]},"editor.fontFamily":{"title":"Font Family","description":"Controls the font family of the editor.","type":"string","default":"'JetBrains Mono', ui-monospace, monospace"},"editor.fontSize":{"title":"Font Size","description":"Font size in pixels.","type":"number","default":13,"minimum":8,"maximum":32},"editor.lineHeight":{"title":"Line Height","description":"Line height in pixels. 0 uses 1.55 × font size.","type":"number","default":20,"minimum":0,"maximum":60},"editor.fontLigatures":{"title":"Font Ligatures","description":"Enables font ligatures when the font supports them.","type":"boolean","default":false},"editor.tabSize":{"title":"Tab Size","description":"The number of spaces a tab is equal to.","type":"number","default":2,"minimum":1,"maximum":8},"editor.insertSpaces":{"title":"Insert Spaces","description":"Insert spaces when pressing Tab.","type":"boolean","default":true},"editor.detectIndentation":{"title":"Detect Indentation","description":"Detect Tab Size and Insert Spaces from file contents on open.","type":"boolean","default":true},"editor.renderIndentGuides":{"title":"Indentation Guides","description":"Render vertical guides for each indent level.","type":"boolean","default":true},"editor.wordWrap":{"title":"Word Wrap","description":"Controls how lines should wrap.","type":"string","default":"off","enum":["off","on","bounded"]},"editor.renderWhitespace":{"title":"Render Whitespace","description":"Controls how the editor renders whitespace characters.","type":"string","default":"selection","enum":["none","boundary","selection","all"]},"editor.cursorBlinking":{"title":"Cursor Blinking","description":"Cursor animation style.","type":"string","default":"blink","enum":["blink","smooth","phase","solid"]},"editor.minimap":{"title":"Minimap","description":"Show a minimap of the document.","type":"boolean","default":false},"files.autoSave":{"title":"Auto Save","description":"Controls auto save of editors that have unsaved changes.","type":"string","default":"off","enum":["off","afterDelay","onFocusChange","onWindowChange"]},"files.autoSaveDelay":{"title":"Auto Save Delay","description":"Delay in milliseconds after which an editor is saved automatically. Applies when Auto Save is afterDelay.","type":"number","default":1000,"minimum":100,"maximum":60000},"files.trimTrailingWhitespace":{"title":"Trim Trailing Whitespace","description":"Remove trailing whitespace when saving.","type":"boolean","default":true},"editor.formatOnSave":{"title":"Format On Save","description":"Format a file on save. A formatter must be available.","type":"boolean","default":false},"editor.defaultFormatter":{"title":"Default Formatter","description":"Formatter used when several are available.","type":"string","default":"oxbit.prettier","anyOf":[{"enum":["oxbit.prettier","oxbit.builtin-ts"]},{"type":"string"}]},"terminal.fontSize":{"title":"Terminal Font Size","description":"Font size in pixels for the terminal.","type":"number","default":12,"minimum":8,"maximum":32},"terminal.scrollback":{"title":"Scrollback","description":"Maximum number of lines kept in the terminal buffer.","type":"number","default":5000,"minimum":100,"maximum":100000},"terminal.confirmOnKill":{"title":"Confirm On Kill","description":"Ask before killing a terminal with a running process.","type":"boolean","default":true},"scm.autoFetch":{"title":"Auto Fetch","description":"Periodically fetch from the default remote.","type":"boolean","default":true},"scm.diffLayout":{"title":"Diff Layout","description":"Default layout for diff editors.","type":"string","default":"side-by-side","enum":["side-by-side","inline"]},"files.associations":{"title":"File Associations","description":"Map filename or workspace-relative glob patterns to language IDs. Explicit associations take precedence over built-in detection.","type":"object","default":{},"propertyNames":{"minLength":1,"maxLength":1024},"additionalProperties":{"type":"string","pattern":"^[\\\\w+-]+$"}},"languageServers":{"title":"Language Servers","description":"Configure enabled state, selectors, rootMarkers, executable, args, env, initializationOptions, settings, and priority by server ID. Servers install on first use in a trusted runtime. License keys belong in the runtime-local credential file.","type":"object","default":{},"propertyNames":{"pattern":"^[a-zA-Z0-9._-]+$"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Enable this language server."},"selectors":{"type":"array","items":{"type":"object","properties":{"language":{"type":"string"},"pattern":{"type":"string"},"scheme":{"const":"file"}},"additionalProperties":false,"minProperties":1}},"rootMarkers":{"type":"array","items":{"type":"string","pattern":"^(?!/)(?!.*\\\\\\\\)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))[^\\\\u0000]+$","minLength":1}},"executable":{"type":"string","pattern":"^[^\\\\u0000]*$","minLength":1},"args":{"type":"array","items":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"env":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"additionalProperties":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"initializationOptions":{"type":"object","additionalProperties":true,"description":"Server-specific initialization options. Store license credentials in the runtime credential file."},"settings":{"type":"object","additionalProperties":true,"description":"Server-specific configuration; nested objects merge across files."},"priority":{"type":"number"}},"additionalProperties":false}},"ui.fontFamily":{"title":"UI Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"ui.fontSize":{"title":"UI Font Size","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":12,"minimum":8,"maximum":72},"ui.fontWeight":{"title":"UI Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"ui.fontStyle":{"title":"UI Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"ui.lineHeight":{"title":"UI Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1.4,"minimum":1,"maximum":3},"ui.letterSpacing":{"title":"UI Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"ui.fontLigatures":{"title":"UI Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"terminal.fontFamily":{"title":"Terminal Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"terminal.fontWeight":{"title":"Terminal Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"terminal.fontStyle":{"title":"Terminal Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"terminal.lineHeight":{"title":"Terminal Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1,"minimum":1,"maximum":3},"terminal.letterSpacing":{"title":"Terminal Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"terminal.fontLigatures":{"title":"Terminal Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"workbench.iconTheme":{"title":"File Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"workbench.productIconTheme":{"title":"Product Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"editor.semanticHighlighting":{"title":"Semantic Highlighting","description":"Color symbols using language server information.","type":"boolean","default":true},"editor.inlayHints.types":{"title":"Type Inlay Hints","description":"Show supported type hints in the visible editor.","type":"boolean","default":true},"editor.inlayHints.parameters":{"title":"Parameter Inlay Hints","description":"Show supported parameter hints in the visible editor.","type":"boolean","default":true},"editor.largeFileIntelligence":{"title":"Large File Intelligence","description":"Allow full semantic highlighting and inlay hints for files larger than 1 MiB.","type":"boolean","default":false},"workbench.locale":{"title":"Display Language","type":"string","default":"en","enum":["en","de","es","ja","zh"]},"workbench.reducedMotion":{"title":"Reduced Motion","type":"boolean","default":false},"desktop.projects.openBehavior":{"title":"Open projects","type":"string","default":"currentWindow","enum":["currentWindow","newWindow"]},"desktop.tools.gitPath":{"title":"git executable path","type":"string","default":""},"desktop.tools.ghPath":{"title":"gh executable path","type":"string","default":""},"project.intelligence":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Index project dependencies, imports and related files."},"exclude":{"type":"array","items":{"type":"string","maxLength":1024},"default":[]},"maxFiles":{"type":"integer","minimum":1,"maximum":100000,"default":20000},"maxFileBytes":{"type":"integer","minimum":1,"maximum":5242880,"default":1048576}},"additionalProperties":false,"title":"Project Intelligence","description":"Partial overrides of project.json intelligence. Objects merge across files."},"project.schemas":{"type":"object","properties":{"catalog":{"type":"boolean","default":true,"description":"Discover file schemas through SchemaStore. Bundled Oxbit settings support remains available."},"download":{"type":"boolean","default":true,"description":"Allow remote schema downloads; cached and bundled schemas work offline."},"associations":{"type":"array","maxItems":1000,"default":[],"items":{"type":"object","properties":{"url":{"type":"string","minLength":1,"description":"Schema URL or workspace file URI."},"fileMatch":{"type":"array","items":{"type":"string","maxLength":1024}},"schema":{"type":["object","boolean"],"description":"An inline JSON Schema."}},"additionalProperties":false,"anyOf":[{"required":["url"]},{"required":["schema"]}]}}},"additionalProperties":false,"title":"JSON Schemas","description":"Partial overrides of project.json schemas. Association arrays replace lower arrays."}}`);
825
936
  const propertyNames = { "anyOf": [{ "not": { "pattern": "^\\[.*\\]$" } }, { "pattern": "^\\[[^\\[\\]]+\\]$" }] };
826
937
  const patternProperties = { "^\\[[^\\[\\]]+\\]$": { "$ref": "#/definitions/languageOverrides" } };
827
938
  const additionalProperties$1 = true;
@@ -999,7 +1110,7 @@ const ACP_PROVIDERS = [
999
1110
  id: "codex",
1000
1111
  name: "Codex ACP",
1001
1112
  command: "npx",
1002
- args: ["-y", "@agentclientprotocol/codex-acp@1.10.0"],
1113
+ args: ["-y", "@agentclientprotocol/codex-acp@2.2.2"],
1003
1114
  setup: "Uses the Codex ACP adapter. Sign in through an advertised authentication method, or use your existing Codex credentials.",
1004
1115
  url: "https://github.com/agentclientprotocol/codex-acp"
1005
1116
  },
@@ -1015,7 +1126,7 @@ const ACP_PROVIDERS = [
1015
1126
  id: "amp",
1016
1127
  name: "Amp Agent ACP",
1017
1128
  command: "npx",
1018
- args: ["-y", "amp-acp@0.9.0"],
1129
+ args: ["-y", "amp-acp@0.10.0"],
1019
1130
  setup: "Uses the community Amp ACP adapter. Install Amp CLI and run amp login first. Set AMP_CLI_PATH in the runtime environment if needed.",
1020
1131
  url: "https://github.com/tao12345666333/amp-acp"
1021
1132
  }
@@ -1059,7 +1170,7 @@ const SDK_VERSION = "1.0.0";
1059
1170
  function invalid(message) {
1060
1171
  throw new RpcError("INVALID_TASK_CONFIG", message);
1061
1172
  }
1062
- function object$1(value, label) {
1173
+ function object$2(value, label) {
1063
1174
  if (!value || typeof value !== "object" || Array.isArray(value))
1064
1175
  invalid(`${label} must be an object`);
1065
1176
  return value;
@@ -1070,7 +1181,7 @@ function taskName(value) {
1070
1181
  return value;
1071
1182
  }
1072
1183
  function stringMap(value, label) {
1073
- const record = object$1(value, label);
1184
+ const record = object$2(value, label);
1074
1185
  for (const [key, item] of Object.entries(record))
1075
1186
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ["__proto__", "constructor", "prototype"].includes(key) || typeof item !== "string" || item.length > 32768 || item.includes("\0"))
1076
1187
  invalid(`${label}: invalid environment entry ${key}`);
@@ -1086,13 +1197,13 @@ function port(value) {
1086
1197
  number(value, 1, 65535, "port");
1087
1198
  return value;
1088
1199
  }
1089
- const range2 = object$1(value, "port range");
1200
+ const range2 = object$2(value, "port range");
1090
1201
  number(range2.min, 1, 65535, "port minimum");
1091
1202
  number(range2.max, range2.min, 65535, "port maximum");
1092
1203
  return { min: range2.min, max: range2.max };
1093
1204
  }
1094
1205
  function hooks(value) {
1095
- const data = object$1(value ?? {}, "worktree");
1206
+ const data = object$2(value ?? {}, "worktree");
1096
1207
  for (const key of Object.keys(data))
1097
1208
  if (!taskPhases.includes(key))
1098
1209
  invalid(`Unknown worktree phase: ${key}`);
@@ -1109,7 +1220,7 @@ function hooks(value) {
1109
1220
  );
1110
1221
  }
1111
1222
  function definition(value) {
1112
- const data = object$1(value, "task");
1223
+ const data = object$2(value, "task");
1113
1224
  const known = /* @__PURE__ */ new Set([
1114
1225
  "command",
1115
1226
  "type",
@@ -1151,7 +1262,7 @@ function definition(value) {
1151
1262
  if (data.env !== void 0) stringMap(data.env, "task.env");
1152
1263
  if (data.port !== void 0) port(data.port);
1153
1264
  if (data.ports !== void 0) {
1154
- const entries = Object.entries(object$1(data.ports, "ports"));
1265
+ const entries = Object.entries(object$2(data.ports, "ports"));
1155
1266
  if (entries.length > 16) invalid("Maximum 16 named ports per task");
1156
1267
  for (const [key, value2] of entries) {
1157
1268
  if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key) || key.toLowerCase() === "port")
@@ -1162,7 +1273,7 @@ function definition(value) {
1162
1273
  invalid("Port names must be unique ignoring case");
1163
1274
  }
1164
1275
  if (data.ready !== void 0) {
1165
- const ready = object$1(data.ready, "ready");
1276
+ const ready = object$2(data.ready, "ready");
1166
1277
  for (const key of Object.keys(ready))
1167
1278
  if (!["pattern", "url", "timeoutMs", "intervalMs"].includes(key))
1168
1279
  invalid(`Unknown readiness field: ${key}`);
@@ -1178,7 +1289,7 @@ function definition(value) {
1178
1289
  number(ready.intervalMs, 100, 6e4, "ready.intervalMs");
1179
1290
  }
1180
1291
  if (data.restart !== void 0) {
1181
- const restart = object$1(data.restart, "restart");
1292
+ const restart = object$2(data.restart, "restart");
1182
1293
  for (const key of Object.keys(restart))
1183
1294
  if (!["policy", "maxAttempts", "delayMs"].includes(key))
1184
1295
  invalid(`Unknown restart field: ${key}`);
@@ -1190,7 +1301,7 @@ function definition(value) {
1190
1301
  number(restart.delayMs, 100, 6e4, "restart.delayMs");
1191
1302
  }
1192
1303
  if (data.stop !== void 0) {
1193
- const stop2 = object$1(data.stop, "stop");
1304
+ const stop2 = object$2(data.stop, "stop");
1194
1305
  for (const key of Object.keys(stop2))
1195
1306
  if (!["signal", "timeoutMs"].includes(key))
1196
1307
  invalid(`Unknown stop field: ${key}`);
@@ -1207,7 +1318,7 @@ function json(text2) {
1207
1318
  const errors = [];
1208
1319
  const result = parse$1(text2, errors, { allowTrailingComma: true });
1209
1320
  if (errors.length) invalid(`Invalid JSON near offset ${errors[0].offset}`);
1210
- return object$1(result, "configuration");
1321
+ return object$2(result, "configuration");
1211
1322
  }
1212
1323
  function editJson(text2, location, value) {
1213
1324
  const tree = value === void 0 ? parseTree(text2) : void 0;
@@ -1409,7 +1520,7 @@ function readSource(kind, text2) {
1409
1520
  );
1410
1521
  } else {
1411
1522
  const data = json(text2);
1412
- const meta = object$1(data.oxbit ?? {}, "oxbit metadata");
1523
+ const meta = object$2(data.oxbit ?? {}, "oxbit metadata");
1413
1524
  result.env = stringMap(
1414
1525
  (kind === "oxbit" ? data.env : meta.env) ?? {},
1415
1526
  "env"
@@ -1429,7 +1540,7 @@ function readSource(kind, text2) {
1429
1540
  ].includes(key))
1430
1541
  invalid(`Unknown Oxbit configuration field: ${key}`);
1431
1542
  for (const [name, task] of Object.entries(
1432
- object$1(data.tasks ?? {}, "tasks")
1543
+ object$2(data.tasks ?? {}, "tasks")
1433
1544
  ))
1434
1545
  add(name, definition(task));
1435
1546
  } else if (kind === "paseo") {
@@ -1441,9 +1552,9 @@ function readSource(kind, text2) {
1441
1552
  ...result.worktree
1442
1553
  };
1443
1554
  for (const [name, value] of Object.entries(
1444
- object$1(data.scripts ?? {}, "scripts")
1555
+ object$2(data.scripts ?? {}, "scripts")
1445
1556
  )) {
1446
- const script = object$1(value, name);
1557
+ const script = object$2(value, name);
1447
1558
  add(
1448
1559
  name,
1449
1560
  definition({
@@ -1495,10 +1606,10 @@ function readSource(kind, text2) {
1495
1606
  invalid("Only VS Code tasks version 2.0.0 is supported");
1496
1607
  if (!Array.isArray(data.tasks)) invalid("VS Code tasks must be an array");
1497
1608
  for (const raw of data.tasks) {
1498
- const entry = object$1(raw, "VS Code task"), platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1609
+ const entry = object$2(raw, "VS Code task"), platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1499
1610
  const item = { ...entry, ...entry[platform] };
1500
1611
  const args = (item.args ?? []).map(
1501
- (arg) => typeof arg === "string" ? arg : object$1(arg, "argument").value
1612
+ (arg) => typeof arg === "string" ? arg : object$2(arg, "argument").value
1502
1613
  );
1503
1614
  const opts = { ...data.options, ...item.options };
1504
1615
  let command = item.command ?? data.command ?? "", execution = item.type === "process" ? "process" : "shell";
@@ -1537,7 +1648,7 @@ function readSource(kind, text2) {
1537
1648
  }
1538
1649
  } else {
1539
1650
  for (const [name, script] of Object.entries(
1540
- object$1(data.scripts ?? {}, "scripts")
1651
+ object$2(data.scripts ?? {}, "scripts")
1541
1652
  )) {
1542
1653
  if (typeof script !== "string" && !(kind === "composer" && Array.isArray(script)))
1543
1654
  continue;
@@ -6526,8 +6637,8 @@ class Collaboration {
6526
6637
  )
6527
6638
  );
6528
6639
  }
6529
- bytes(encoded) {
6530
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length > 2 * 1024 * 1024)
6640
+ bytes(encoded, limit2 = MAX_MESSAGE_BYTES) {
6641
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length > limit2)
6531
6642
  throw new RpcError("INVALID_PARAMS", "Invalid collaboration update");
6532
6643
  return Buffer.from(encoded, "base64");
6533
6644
  }
@@ -6563,14 +6674,14 @@ class Collaboration {
6563
6674
  }
6564
6675
  async update(relative, connection, encoded) {
6565
6676
  const room = await this.member(relative, connection);
6566
- const bytes = this.bytes(encoded);
6677
+ const bytes = this.bytes(encoded, MAX_COLLABORATION_UPDATE_BYTES);
6567
6678
  try {
6568
- if (room.doc.getText("content").length + bytes.length > 20 * 1024 * 1024) {
6679
+ if (room.doc.getText("content").length + bytes.length > MAX_FILE_BYTES) {
6569
6680
  const candidate = new Y.Doc();
6570
6681
  try {
6571
6682
  Y.applyUpdate(candidate, Y.encodeStateAsUpdate(room.doc));
6572
6683
  Y.applyUpdate(candidate, bytes);
6573
- if (candidate.getText("content").length > 20 * 1024 * 1024) throw new RpcError("FILE_TOO_LARGE", "Shared documents are limited to 20 MiB");
6684
+ if (candidate.getText("content").length > MAX_FILE_BYTES) throw new RpcError("FILE_TOO_LARGE", "Shared documents are limited to 20 MiB");
6574
6685
  } finally {
6575
6686
  candidate.destroy();
6576
6687
  }
@@ -6739,7 +6850,7 @@ class Collaboration {
6739
6850
  }
6740
6851
  }
6741
6852
  const terminal = (state2) => !["pending", "running", "unknown"].includes(state2);
6742
- const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
6853
+ const object$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
6743
6854
  const str = (value, max = 4096) => typeof value === "string" ? value.slice(0, max) : void 0;
6744
6855
  const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 512 && !value.includes("\0");
6745
6856
  const preview = (value, max = 8192) => value === void 0 ? void 0 : (typeof value === "string" ? value : JSON.stringify(value)).slice(0, max);
@@ -6769,11 +6880,11 @@ ${item.newText ?? ""}` : JSON.stringify(item))
6769
6880
  ).join("\n");
6770
6881
  }
6771
6882
  function normalizeDelegation(provider, update) {
6772
- const raw = object(update.rawInput), meta = object(update._meta?.codex);
6883
+ const raw = object$1(update.rawInput), meta = object$1(update._meta?.codex);
6773
6884
  const toolId = update.toolCallId;
6774
6885
  if (!identity(toolId)) return [];
6775
6886
  if (provider === "codex") {
6776
- const collab = object(meta.collaboration), sub = object(meta.subagent);
6887
+ const collab = object$1(meta.collaboration), sub = object$1(meta.subagent);
6777
6888
  if (identity(sub.threadId))
6778
6889
  return [
6779
6890
  {
@@ -6787,7 +6898,7 @@ function normalizeDelegation(provider, update) {
6787
6898
  ];
6788
6899
  if (!identity(collab.tool)) return [];
6789
6900
  const receivers = Array.isArray(collab.receiverThreadIds) ? collab.receiverThreadIds.filter(identity) : [];
6790
- const states = object(raw.agentsStates);
6901
+ const states = object$1(raw.agentsStates);
6791
6902
  const ids = [
6792
6903
  .../* @__PURE__ */ new Set([...receivers, ...Object.keys(states).filter(identity)])
6793
6904
  ];
@@ -6815,8 +6926,8 @@ function normalizeDelegation(provider, update) {
6815
6926
  }));
6816
6927
  }
6817
6928
  if (provider === "cursor" && update.taskMeta) {
6818
- const task = object(update.taskMeta), output = object(update.rawOutput);
6819
- const outcome = object(task.outcome ?? output.outcome);
6929
+ const task = object$1(update.taskMeta), output = object$1(update.rawOutput);
6930
+ const outcome = object$1(task.outcome ?? output.outcome);
6820
6931
  const outcomeName = typeof (task.outcome ?? output.outcome) === "string" ? task.outcome ?? output.outcome : outcome.outcome;
6821
6932
  return [
6822
6933
  {
@@ -7236,7 +7347,273 @@ class SubagentRegistry {
7236
7347
  }
7237
7348
  }
7238
7349
  }
7350
+ let cached;
7351
+ function runtimeVersion() {
7352
+ if (cached) return cached;
7353
+ return cached = "0.7.0";
7354
+ }
7355
+ const MAX_REQUEST_BYTES = 1024 * 1024;
7356
+ const MAX_CONCURRENT_REQUESTS = 16;
7357
+ const PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
7358
+ const SERVER_NAME = "oxbit_workspace";
7359
+ const NATIVE_AGENT_TOOLS = [
7360
+ {
7361
+ name: "oxbit_get_workspace",
7362
+ description: "Get the workspace, active editor, and current selection when an editor is connected.",
7363
+ inputSchema: { type: "object", properties: {} }
7364
+ },
7365
+ {
7366
+ name: "oxbit_list_files",
7367
+ description: "List files in a workspace directory. Paths are workspace relative or absolute within the workspace.",
7368
+ inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path; defaults to the workspace root." } } }
7369
+ },
7370
+ {
7371
+ name: "oxbit_read_file",
7372
+ description: "Read a bounded range of a workspace file, including live unsaved editor content. Paths are workspace relative or absolute within the workspace.",
7373
+ inputSchema: { type: "object", properties: { path: { type: "string" }, line: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1 } }, required: ["path"] }
7374
+ },
7375
+ {
7376
+ name: "oxbit_get_diagnostics",
7377
+ description: "Get live editor diagnostics for a workspace file or the workspace. Requires a connected editor; headless runtimes have no diagnostics.",
7378
+ inputSchema: { type: "object", properties: { path: { type: "string", description: "Optional workspace file path." } } }
7379
+ },
7380
+ {
7381
+ name: "oxbit_open_file",
7382
+ description: "Open a workspace file in the editor at an optional line.",
7383
+ inputSchema: { type: "object", properties: { path: { type: "string" }, line: { type: "integer", minimum: 1 } }, required: ["path"] }
7384
+ },
7385
+ {
7386
+ name: "oxbit_propose_edit",
7387
+ description: "Propose replacement content for a workspace file. The tool waits for engineer review and returns the decision.",
7388
+ inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] }
7389
+ },
7390
+ {
7391
+ name: "oxbit_update_plan",
7392
+ description: "Publish the current task plan with at most 50 entries.",
7393
+ inputSchema: { type: "object", properties: { entries: { type: "array", maxItems: 50, items: { type: "object", properties: { content: { type: "string" }, status: { type: "string", enum: ["pending", "in_progress", "completed"] }, priority: { type: "string" } }, required: ["content", "status"] } } }, required: ["entries"] }
7394
+ }
7395
+ ];
7396
+ const STDIO_PROXY = String.raw`
7397
+ const readline = require('node:readline');
7398
+ const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
7399
+ const pending = new Set();
7400
+ let closing = false;
7401
+ const maxBytes = 1024 * 1024;
7402
+ const send = (message) => {
7403
+ if (!closing) process.stdout.write(JSON.stringify(message) + '\n');
7404
+ };
7405
+ const fail = (id, message) => send({ jsonrpc: '2.0', id: id ?? null, error: { code: -32603, message } });
7406
+ input.on('line', (line) => {
7407
+ if (Buffer.byteLength(line) > maxBytes) return fail(null, 'MCP input line is too large');
7408
+ let message;
7409
+ try { message = JSON.parse(line); }
7410
+ catch { return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Invalid JSON' } }); }
7411
+ const hasId = message !== null && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'id');
7412
+ if (pending.size >= 16) {
7413
+ if (hasId) fail(message.id, 'MCP proxy is busy');
7414
+ return;
7415
+ }
7416
+ const controller = new AbortController();
7417
+ pending.add(controller);
7418
+ void (async () => {
7419
+ try {
7420
+ const response = await fetch(process.env.OXBIT_MCP_URL, {
7421
+ method: 'POST',
7422
+ headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', authorization: 'Bearer ' + process.env.OXBIT_MCP_TOKEN },
7423
+ body: line,
7424
+ signal: controller.signal,
7425
+ });
7426
+ if (!hasId) { await response.body?.cancel(); return; }
7427
+ const length = Number(response.headers.get('content-length'));
7428
+ if (length > maxBytes) throw new Error('MCP response is too large');
7429
+ const reader = response.body?.getReader();
7430
+ if (!reader) throw new Error(response.ok ? 'MCP proxy returned an empty response' : 'MCP proxy HTTP ' + response.status);
7431
+ const chunks = [];
7432
+ let size = 0;
7433
+ while (true) {
7434
+ const { done, value } = await reader.read();
7435
+ if (done) break;
7436
+ size += value.byteLength;
7437
+ if (size > maxBytes) { await reader.cancel(); throw new Error('MCP response is too large'); }
7438
+ chunks.push(value);
7439
+ }
7440
+ let result;
7441
+ try { result = JSON.parse(Buffer.concat(chunks, size).toString('utf8')); }
7442
+ catch { throw new Error(response.ok ? 'MCP proxy returned invalid JSON' : 'MCP proxy HTTP ' + response.status); }
7443
+ if (result?.jsonrpc !== '2.0' || result.id !== message.id || (!result.result && !result.error)) throw new Error(response.ok ? 'MCP proxy returned an invalid response' : 'MCP proxy HTTP ' + response.status);
7444
+ if (!response.ok && !result.error) throw new Error('MCP proxy HTTP ' + response.status);
7445
+ send(result);
7446
+ } catch (error) {
7447
+ if (hasId && !closing) fail(message.id, error instanceof Error ? error.message : String(error));
7448
+ } finally {
7449
+ pending.delete(controller);
7450
+ }
7451
+ })();
7452
+ });
7453
+ input.on('close', () => {
7454
+ closing = true;
7455
+ for (const controller of pending) controller.abort();
7456
+ });`;
7457
+ function object(value) {
7458
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7459
+ }
7460
+ function validateArguments(name, args) {
7461
+ if (!NATIVE_AGENT_TOOLS.some((tool) => tool.name === name)) return "Unknown tool";
7462
+ const keys = Object.keys(args);
7463
+ const allowed = {
7464
+ oxbit_get_workspace: [],
7465
+ oxbit_list_files: ["path"],
7466
+ oxbit_read_file: ["path", "line", "limit"],
7467
+ oxbit_get_diagnostics: ["path"],
7468
+ oxbit_open_file: ["path", "line"],
7469
+ oxbit_propose_edit: ["path", "content"],
7470
+ oxbit_update_plan: ["entries"]
7471
+ };
7472
+ if (keys.some((key) => !allowed[name].includes(key))) return "Unknown tool argument";
7473
+ if (["oxbit_read_file", "oxbit_open_file", "oxbit_propose_edit"].includes(name) && (typeof args.path !== "string" || !args.path)) return "path must be a nonempty string";
7474
+ if (["oxbit_list_files", "oxbit_get_diagnostics"].includes(name) && args.path !== void 0 && typeof args.path !== "string") return "path must be a string";
7475
+ if (args.line !== void 0 && (!Number.isSafeInteger(args.line) || args.line < 1)) return "line must be a positive integer";
7476
+ if (args.limit !== void 0 && (!Number.isSafeInteger(args.limit) || args.limit < 1)) return "limit must be a positive integer";
7477
+ if (name === "oxbit_propose_edit" && typeof args.content !== "string") return "content must be a string";
7478
+ if (name === "oxbit_update_plan" && (!Array.isArray(args.entries) || args.entries.length > 50 || args.entries.some((entry) => !object(entry) || typeof entry.content !== "string" || !["pending", "in_progress", "completed"].includes(String(entry.status)) || entry.priority !== void 0 && typeof entry.priority !== "string"))) return "entries must contain plan content and status";
7479
+ return void 0;
7480
+ }
7481
+ function reply(response, status2, body) {
7482
+ response.statusCode = status2;
7483
+ if (body === void 0) {
7484
+ response.end();
7485
+ return;
7486
+ }
7487
+ response.setHeader("content-type", "application/json; charset=utf-8");
7488
+ response.end(JSON.stringify(body));
7489
+ }
7490
+ function rpcError(id, code, message) {
7491
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
7492
+ }
7493
+ class AgentMCP {
7494
+ constructor(handler) {
7495
+ this.handler = handler;
7496
+ }
7497
+ handler;
7498
+ listener;
7499
+ starting;
7500
+ url;
7501
+ tokens = /* @__PURE__ */ new Map();
7502
+ activeRequests = 0;
7503
+ async server(connectionId, httpSupported) {
7504
+ if (!this.listener && !this.starting) this.starting = this.startListener();
7505
+ if (this.starting) await this.starting;
7506
+ let token = this.tokens.get(connectionId);
7507
+ if (!token) {
7508
+ token = randomBytes(32).toString("hex");
7509
+ this.tokens.set(connectionId, token);
7510
+ }
7511
+ if (httpSupported) return { name: SERVER_NAME, type: "http", url: this.url, headers: [{ name: "Authorization", value: `Bearer ${token}` }] };
7512
+ return { name: SERVER_NAME, command: process.execPath, args: ["-e", STDIO_PROXY], env: [
7513
+ { name: "OXBIT_MCP_URL", value: this.url },
7514
+ { name: "OXBIT_MCP_TOKEN", value: token },
7515
+ ...process.versions.electron ? [{ name: "ELECTRON_RUN_AS_NODE", value: "1" }] : []
7516
+ ] };
7517
+ }
7518
+ async startListener() {
7519
+ const listener = createServer((request, response) => {
7520
+ void this.handle(request, response);
7521
+ });
7522
+ try {
7523
+ await new Promise((resolve, reject) => {
7524
+ listener.once("error", reject);
7525
+ listener.listen(0, "127.0.0.1", () => {
7526
+ listener.off("error", reject);
7527
+ resolve();
7528
+ });
7529
+ });
7530
+ const address = listener.address();
7531
+ if (!address || typeof address === "string") throw new Error("MCP listener has no TCP address");
7532
+ this.listener = listener;
7533
+ this.url = "http://127.0.0.1:" + address.port + "/mcp";
7534
+ } catch (error) {
7535
+ listener.close();
7536
+ throw error;
7537
+ } finally {
7538
+ this.starting = void 0;
7539
+ }
7540
+ }
7541
+ revoke(connectionId) {
7542
+ this.tokens.delete(connectionId);
7543
+ }
7544
+ async close() {
7545
+ this.tokens.clear();
7546
+ if (this.starting) await this.starting.catch(() => {
7547
+ });
7548
+ const listener = this.listener;
7549
+ this.listener = void 0;
7550
+ this.url = void 0;
7551
+ if (listener) await new Promise((resolve) => listener.close(() => resolve()));
7552
+ }
7553
+ async handle(request, response) {
7554
+ const address = this.listener?.address();
7555
+ if (!address || typeof address === "string") return reply(response, 503);
7556
+ const origin = `http://127.0.0.1:${address.port}`;
7557
+ if (request.headers.host !== `127.0.0.1:${address.port}` || request.headers.origin && request.headers.origin !== origin) return reply(response, 403);
7558
+ if (request.url !== "/mcp") return reply(response, 404);
7559
+ if (request.method !== "POST") return reply(response, 405);
7560
+ const authorization = request.headers.authorization;
7561
+ const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : void 0;
7562
+ const connectionId = token && [...this.tokens].find(([, candidate]) => candidate === token)?.[0];
7563
+ if (!connectionId) return reply(response, 401);
7564
+ if (this.activeRequests >= MAX_CONCURRENT_REQUESTS) return reply(response, 429);
7565
+ const contentLength = Number(request.headers["content-length"]);
7566
+ if (contentLength > MAX_REQUEST_BYTES) return reply(response, 413);
7567
+ this.activeRequests++;
7568
+ try {
7569
+ let length = 0;
7570
+ const chunks = [];
7571
+ for await (const chunk of request) {
7572
+ length += chunk.length;
7573
+ if (length > MAX_REQUEST_BYTES) return reply(response, 413);
7574
+ chunks.push(chunk);
7575
+ }
7576
+ let message;
7577
+ try {
7578
+ message = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7579
+ } catch {
7580
+ return reply(response, 400, rpcError(null, -32700, "Invalid JSON"));
7581
+ }
7582
+ if (!object(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string" || message.id !== void 0 && typeof message.id !== "string" && typeof message.id !== "number") return reply(response, 400, rpcError(object(message) ? message.id : null, -32600, "Invalid request"));
7583
+ const version = request.headers["mcp-protocol-version"];
7584
+ if (version && !PROTOCOL_VERSIONS.includes(version)) return reply(response, 400, rpcError(message.id, -32600, "Unsupported MCP protocol version"));
7585
+ if (message.id === void 0) return reply(response, 202);
7586
+ const id = message.id;
7587
+ if (message.method === "initialize") {
7588
+ const requested = object(message.params) && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0;
7589
+ return reply(response, 200, { jsonrpc: "2.0", id, result: { protocolVersion: requested && PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0], capabilities: { tools: { listChanged: false } }, serverInfo: { name: "oxbit-workspace", version: runtimeVersion() } } });
7590
+ }
7591
+ if (message.method === "ping") return reply(response, 200, { jsonrpc: "2.0", id, result: {} });
7592
+ if (message.method === "tools/list") return reply(response, 200, { jsonrpc: "2.0", id, result: { tools: NATIVE_AGENT_TOOLS } });
7593
+ if (message.method !== "tools/call") return reply(response, 200, rpcError(id, -32601, "Method not found"));
7594
+ const params = message.params;
7595
+ if (!object(params) || typeof params.name !== "string" || !object(params.arguments ?? {})) return reply(response, 200, rpcError(id, -32602, "Invalid tool call"));
7596
+ const args = params.arguments ?? {};
7597
+ const invalid2 = validateArguments(params.name, args);
7598
+ if (invalid2) return reply(response, 200, { jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: invalid2 }] } });
7599
+ try {
7600
+ const value = await this.handler(connectionId, params.name, args);
7601
+ if (this.tokens.get(connectionId) !== token) return reply(response, 401);
7602
+ const structuredContent = object(value) ? value : { value: value ?? null };
7603
+ return reply(response, 200, { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(value ?? null) }], structuredContent } });
7604
+ } catch (error) {
7605
+ if (this.tokens.get(connectionId) !== token) return reply(response, 401);
7606
+ return reply(response, 200, { jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }] } });
7607
+ }
7608
+ } catch (error) {
7609
+ if (!response.writableEnded) reply(response, 400, rpcError(null, -32600, error instanceof Error ? error.message : String(error)));
7610
+ } finally {
7611
+ this.activeRequests--;
7612
+ }
7613
+ }
7614
+ }
7239
7615
  const MAX_BYTES = 1024 * 1024;
7616
+ const EDITOR_UNSUPPORTED = "The connected editor does not support this tool; update Oxbit";
7240
7617
  function strings(value) {
7241
7618
  if (!Array.isArray(value) || value.length > 128 || value.some(
7242
7619
  (v) => typeof v !== "string" || v.length > 16384 || v.includes("\0")
@@ -7248,13 +7625,17 @@ function strings(value) {
7248
7625
  return value;
7249
7626
  }
7250
7627
  class AgentACP {
7251
- constructor(files, emit) {
7628
+ constructor(files, emit, stateChanged = () => {
7629
+ }) {
7252
7630
  this.files = files;
7253
7631
  this.emit = emit;
7632
+ this.stateChanged = stateChanged;
7254
7633
  }
7255
7634
  files;
7256
7635
  emit;
7636
+ stateChanged;
7257
7637
  agents = /* @__PURE__ */ new Map();
7638
+ mcp = new AgentMCP((id, name, args) => this.tool(id, name, args));
7258
7639
  get(owner, id) {
7259
7640
  const agent = this.agents.get(id);
7260
7641
  if (!agent || agent.owner !== owner || agent.stopped)
@@ -7289,15 +7670,89 @@ class AgentACP {
7289
7670
  });
7290
7671
  }
7291
7672
  publish(agent, event, params) {
7292
- this.emit(agent.owner, event, { id: agent.connection.id, ...params });
7673
+ const payload = { id: agent.connection.id, ...params, seq: ++agent.eventSequence };
7674
+ if (["acp.update", "acp.subagent", "acp.terminal", "acp.turnStarted", "acp.turnEnded"].includes(event)) {
7675
+ const entry = JSON.parse(JSON.stringify({ name: event, params: payload }));
7676
+ agent.events.push(entry);
7677
+ agent.eventBytes += Buffer.byteLength(JSON.stringify(entry));
7678
+ while (agent.events.length > 2e3 || agent.eventBytes > 2 * MAX_BYTES) {
7679
+ agent.eventBytes -= Buffer.byteLength(JSON.stringify(agent.events.shift()));
7680
+ agent.truncated = true;
7681
+ }
7682
+ }
7683
+ if (agent.client) this.emit(agent.client, event, payload);
7684
+ this.stateChanged();
7685
+ }
7686
+ list(owner) {
7687
+ return { sessions: [...this.agents.values()].filter((a) => a.owner === owner && !a.stopped).map((a) => ({
7688
+ connection: a.connection,
7689
+ busy: a.busy,
7690
+ title: a.title,
7691
+ pendingRequests: a.approvals.size,
7692
+ queued: a.queue.length
7693
+ })) };
7694
+ }
7695
+ control(owner, id, client) {
7696
+ if (this.get(owner, id).client !== client)
7697
+ throw new RpcError("BUSY", "Attach this agent session before controlling it");
7698
+ }
7699
+ attach(owner, id, client = owner, editorTools = false) {
7700
+ const agent = this.get(owner, id);
7701
+ if (agent.client && agent.client !== client)
7702
+ this.emit(agent.client, "acp.attachedElsewhere", { id, reason: "This session was attached in another Oxbit window" });
7703
+ this.detach(client, id);
7704
+ agent.client = client;
7705
+ agent.editorTools = editorTools;
7706
+ if (!editorTools) this.rejectEditorRequests(agent, EDITOR_UNSUPPORTED);
7707
+ return {
7708
+ connection: agent.connection,
7709
+ busy: agent.busy,
7710
+ events: [...agent.events],
7711
+ requests: [...agent.approvals].map(([requestId, request]) => this.requestEvent(agent, requestId, request)),
7712
+ queue: [...agent.queue],
7713
+ queuePaused: agent.queuePaused,
7714
+ sequence: agent.eventSequence,
7715
+ truncated: agent.truncated
7716
+ };
7293
7717
  }
7294
- async start(owner, launch, signal) {
7718
+ detach(client, exceptId) {
7719
+ for (const agent of this.agents.values()) {
7720
+ if (agent.client !== client || agent.connection.id === exceptId) continue;
7721
+ agent.client = void 0;
7722
+ for (const [id, request] of agent.approvals) {
7723
+ if (request.method === "fs/read_text_file") {
7724
+ agent.approvals.delete(id);
7725
+ void this.readFile(request.params).then(request.resolve, request.reject);
7726
+ }
7727
+ }
7728
+ this.rejectEditorRequests(agent, "The editor disconnected; attach an Oxbit window to use this tool");
7729
+ }
7730
+ }
7731
+ rejectEditorRequests(agent, reason) {
7732
+ for (const [id, request] of agent.approvals) {
7733
+ if (!request.method.startsWith("oxbit/")) continue;
7734
+ agent.approvals.delete(id);
7735
+ request.reject(new Error(reason));
7736
+ }
7737
+ }
7738
+ working(agent) {
7739
+ return agent.busy || agent.queueRunning || !!agent.approvals.size || !!agent.subagents?.activeCount || !!agent.queue.length;
7740
+ }
7741
+ get hasWork() {
7742
+ return [...this.agents.values()].some((a) => this.working(a));
7743
+ }
7744
+ async start(owner, launch, signal, client = owner) {
7295
7745
  const preset2 = ACP_PROVIDERS.find((p) => p.id === launch.provider);
7296
7746
  if (!preset2)
7297
7747
  throw new RpcError("INVALID_PARAMS", "Choose Codex, Cursor, or Amp");
7298
7748
  if (signal?.aborted) throw new Error("Agent connection cancelled");
7299
- if ([...this.agents.values()].filter((a) => a.owner === owner).length >= 3)
7300
- throw new RpcError("BUSY", "Disconnect an agent before starting another");
7749
+ const owned = [...this.agents.values()].filter((a) => a.owner === owner);
7750
+ if (owned.length >= 3) {
7751
+ const idle = owned.find((a) => !a.client && !this.working(a));
7752
+ if (!idle)
7753
+ throw new RpcError("BUSY", "3 agents are running for this device; stop one in Runtime sessions");
7754
+ this.stopAgent(idle, "Stopped to start another agent");
7755
+ }
7301
7756
  const command = requireString(
7302
7757
  { command: launch.command ?? preset2.command },
7303
7758
  "command"
@@ -7315,6 +7770,8 @@ class AgentACP {
7315
7770
  trackChild(child);
7316
7771
  const agent = {
7317
7772
  owner,
7773
+ client,
7774
+ editorTools: launch.clientCapabilities?.editorTools === true,
7318
7775
  child,
7319
7776
  connection: {
7320
7777
  id: randomUUID(),
@@ -7329,7 +7786,15 @@ class AgentACP {
7329
7786
  busy: false,
7330
7787
  stopped: false,
7331
7788
  sequence: 0,
7332
- turn: 0
7789
+ turn: 0,
7790
+ events: [],
7791
+ eventBytes: 0,
7792
+ eventSequence: 0,
7793
+ truncated: false,
7794
+ title: "New conversation",
7795
+ queue: [],
7796
+ queuePaused: false,
7797
+ queueRunning: false
7333
7798
  };
7334
7799
  this.agents.set(agent.connection.id, agent);
7335
7800
  const abort = () => this.stopAgent(agent, "Agent connection cancelled");
@@ -7389,7 +7854,7 @@ class AgentACP {
7389
7854
  terminal: true,
7390
7855
  ...preset2.id === "codex" ? { subagents: {} } : {}
7391
7856
  },
7392
- clientInfo: { name: "oxbit", title: "Oxbit", version: "0.1.0" }
7857
+ clientInfo: { name: "oxbit", title: "Oxbit", version: runtimeVersion() }
7393
7858
  });
7394
7859
  if (initialized.protocolVersion !== 1)
7395
7860
  throw new Error("Agent does not support ACP version 1");
@@ -7459,12 +7924,23 @@ class AgentACP {
7459
7924
  const previous = { ...agent.connection };
7460
7925
  const previousRegistry = agent.subagents;
7461
7926
  const previousTerminals = agent.terminals;
7927
+ const previousEvents = agent.events;
7928
+ const previousBytes = agent.eventBytes;
7929
+ const previousTitle = agent.title;
7930
+ const previousTruncated = agent.truncated;
7462
7931
  const target = method === "session/new" ? void 0 : requireString(params, "sessionId");
7463
7932
  if (agent.approvals.size)
7464
7933
  throw new Error(
7465
7934
  "Resolve pending requests before changing conversations"
7466
7935
  );
7936
+ if (agent.queue.length)
7937
+ throw new Error("Remove queued messages before changing conversations");
7467
7938
  agent.busy = true;
7939
+ agent.events = [];
7940
+ agent.eventBytes = 0;
7941
+ agent.truncated = false;
7942
+ agent.title = "New conversation";
7943
+ agent.queuePaused = false;
7468
7944
  agent.connection.sessionId = target;
7469
7945
  agent.subagents = target ? this.registry(agent, target, true) : void 0;
7470
7946
  agent.terminals = /* @__PURE__ */ new Map();
@@ -7472,7 +7948,7 @@ class AgentACP {
7472
7948
  try {
7473
7949
  const result = await this.request(agent, method, {
7474
7950
  cwd: this.files.root,
7475
- mcpServers: [],
7951
+ mcpServers: [await this.mcp.server(agent.connection.id, !!agent.connection.capabilities?.mcpCapabilities?.http)],
7476
7952
  ...target ? { sessionId: target } : {}
7477
7953
  });
7478
7954
  const sessionId2 = target ?? result?.sessionId;
@@ -7497,6 +7973,10 @@ class AgentACP {
7497
7973
  killProcess(terminal2.child);
7498
7974
  agent.terminals = previousTerminals;
7499
7975
  agent.subagents = previousRegistry;
7976
+ agent.events = previousEvents;
7977
+ agent.eventBytes = previousBytes;
7978
+ agent.title = previousTitle;
7979
+ agent.truncated = previousTruncated;
7500
7980
  throw error;
7501
7981
  } finally {
7502
7982
  agent.newSessionUpdates = void 0;
@@ -7567,35 +8047,119 @@ ${content}`
7567
8047
  throw new Error("The message and attachments are too large");
7568
8048
  if (agent.busy || agent.stopped || agent.connection.sessionId !== sessionId)
7569
8049
  throw new Error("Agent connection is busy or ended");
8050
+ const messageId = params.messageId === void 0 ? randomUUID() : requireString(params, "messageId", 256);
7570
8051
  agent.busy = true;
7571
8052
  agent.turn++;
8053
+ if (agent.title === "New conversation") agent.title = text2.slice(0, 100);
8054
+ this.publish(agent, "acp.turnStarted", { messageId, text: text2, context: params.context ?? [] });
7572
8055
  const abort = () => this.stopAgent(agent, "Agent request interrupted");
7573
8056
  signal?.addEventListener("abort", abort, { once: true });
8057
+ let stopReason;
8058
+ let failure2;
7574
8059
  try {
7575
- return await this.request(
8060
+ const result = await this.request(
7576
8061
  agent,
7577
8062
  method,
7578
8063
  { sessionId, prompt },
7579
8064
  30 * 60 * 1e3
7580
8065
  );
8066
+ stopReason = result?.stopReason;
8067
+ if (stopReason !== "end_turn" && stopReason !== "cancelled") agent.queuePaused = true;
8068
+ return result;
8069
+ } catch (error) {
8070
+ failure2 = error instanceof Error ? error.message : String(error);
8071
+ agent.queuePaused = true;
8072
+ throw error;
7581
8073
  } finally {
7582
8074
  this.expireRequests(agent, sessionId);
7583
8075
  agent.busy = false;
7584
8076
  signal?.removeEventListener("abort", abort);
8077
+ if (!agent.stopped) {
8078
+ this.publish(agent, "acp.turnEnded", { stopReason, error: failure2 });
8079
+ this.publishQueue(agent);
8080
+ queueMicrotask(() => this.runQueue(agent));
8081
+ }
7585
8082
  }
7586
8083
  }
7587
- cancel(owner, id) {
8084
+ publishQueue(agent) {
8085
+ this.publish(agent, "acp.queue", { queue: agent.queue, paused: agent.queuePaused });
8086
+ return { queue: [...agent.queue], paused: agent.queuePaused };
8087
+ }
8088
+ async enqueue(owner, id, params, interrupt = false) {
8089
+ const agent = this.get(owner, id);
8090
+ if (!agent.connection.sessionId) throw new Error("Start a conversation first");
8091
+ const text2 = requireString(params, "text", MAX_BYTES).trim();
8092
+ if (!text2) throw new Error("Enter a message");
8093
+ const messageId = params.messageId === void 0 ? randomUUID() : requireString(params, "messageId", 256);
8094
+ if (params.context !== void 0) {
8095
+ if (!Array.isArray(params.context) || params.context.length > 8)
8096
+ throw new Error("Attach up to eight context items");
8097
+ for (const item of params.context) {
8098
+ requireString(item, "text", 2e5);
8099
+ await this.files.resolve(requireString(item, "path"));
8100
+ }
8101
+ }
8102
+ if (agent.stopped) throw new Error("Agent connection has ended");
8103
+ if (agent.queue.some((p) => p.id === messageId)) return this.publishQueue(agent);
8104
+ const prompt = { id: messageId, text: text2, ...params.context ? { context: structuredClone(params.context) } : {} };
8105
+ if (agent.queue.length >= 16 || Buffer.byteLength(JSON.stringify([...agent.queue, prompt])) > MAX_BYTES)
8106
+ throw new Error("Queued messages exceed the limit; remove a message first");
8107
+ if (interrupt) {
8108
+ agent.queue.unshift(prompt);
8109
+ agent.queuePaused = false;
8110
+ if (agent.busy) this.cancel(owner, id, true);
8111
+ } else agent.queue.push(prompt);
8112
+ const result = this.publishQueue(agent);
8113
+ queueMicrotask(() => this.runQueue(agent));
8114
+ return result;
8115
+ }
8116
+ dequeue(owner, id, promptId) {
8117
+ const agent = this.get(owner, id);
8118
+ const index = agent.queue.findIndex((p) => p.id === promptId);
8119
+ if (index < 0) throw new Error("The queued message has already started or was removed");
8120
+ const [prompt] = agent.queue.splice(index, 1);
8121
+ this.publishQueue(agent);
8122
+ return { prompt };
8123
+ }
8124
+ resumeQueue(owner, id) {
7588
8125
  const agent = this.get(owner, id);
8126
+ agent.queuePaused = false;
8127
+ this.publishQueue(agent);
8128
+ queueMicrotask(() => this.runQueue(agent));
8129
+ return {};
8130
+ }
8131
+ runQueue(agent) {
8132
+ if (agent.busy || agent.queueRunning || agent.stopped || agent.queuePaused || !agent.queue.length) return;
8133
+ agent.queueRunning = true;
8134
+ const prompt = agent.queue.shift();
8135
+ this.publishQueue(agent);
8136
+ void this.call(agent.owner, agent.connection.id, "session/prompt", {
8137
+ text: prompt.text,
8138
+ context: prompt.context,
8139
+ messageId: prompt.id
8140
+ }).catch(() => {
8141
+ if (agent.stopped) return;
8142
+ agent.queuePaused = true;
8143
+ agent.queue.unshift(prompt);
8144
+ this.publishQueue(agent);
8145
+ }).finally(() => {
8146
+ agent.queueRunning = false;
8147
+ this.runQueue(agent);
8148
+ });
8149
+ }
8150
+ cancel(owner, id, continueQueue = false) {
8151
+ const agent = this.get(owner, id);
8152
+ agent.queuePaused = !continueQueue;
8153
+ this.publishQueue(agent);
7589
8154
  this.send(agent, {
7590
8155
  method: "session/cancel",
7591
8156
  params: { sessionId: agent.connection.sessionId }
7592
8157
  });
7593
8158
  for (const [requestId, request] of agent.approvals) {
7594
8159
  if (request.params.sessionId !== agent.connection.sessionId) continue;
7595
- this.send(agent, {
7596
- id: request.rpcId,
7597
- ...request.method === "session/request_permission" ? { result: { outcome: { outcome: "cancelled" } } } : { error: { code: -32800, message: "Cancelled" } }
7598
- });
8160
+ if (request.method === "session/request_permission")
8161
+ request.resolve({ outcome: { outcome: "cancelled" } });
8162
+ else request.reject(new Error("Cancelled"));
7599
8163
  agent.approvals.delete(requestId);
7600
8164
  agent.subagents?.waiting(
7601
8165
  request.params.sessionId,
@@ -7633,6 +8197,8 @@ ${content}`
7633
8197
  routeUpdate(agent, sessionId, update) {
7634
8198
  if (!update || typeof update !== "object" || !agent.subagents?.observe(sessionId, update))
7635
8199
  return;
8200
+ if (sessionId === agent.connection.sessionId && update.sessionUpdate === "session_info_update" && typeof update.title === "string")
8201
+ agent.title = update.title.slice(0, 200);
7636
8202
  this.publish(agent, "acp.update", {
7637
8203
  rootSessionId: agent.subagents.root,
7638
8204
  sessionId,
@@ -7649,11 +8215,9 @@ ${content}`
7649
8215
  const requestIds = [];
7650
8216
  for (const [id, request] of agent.approvals) {
7651
8217
  if (request.params.sessionId !== sessionId) continue;
7652
- if (!agent.stopped)
7653
- this.send(agent, {
7654
- id: request.rpcId,
7655
- ...request.method === "session/request_permission" ? { result: { outcome: { outcome: "cancelled" } } } : { error: { code: -32800, message: "Session turn ended" } }
7656
- });
8218
+ if (request.method === "session/request_permission")
8219
+ request.resolve({ outcome: { outcome: "cancelled" } });
8220
+ else request.reject(new Error("Session turn ended"));
7657
8221
  agent.approvals.delete(id);
7658
8222
  requestIds.push(id);
7659
8223
  agent.subagents?.waiting(
@@ -7676,6 +8240,136 @@ ${content}`
7676
8240
  await this.files.resolve(relative, missing2);
7677
8241
  return relative;
7678
8242
  }
8243
+ requestEvent(agent, requestId, request) {
8244
+ const sessionId = request.params.sessionId;
8245
+ return {
8246
+ id: agent.connection.id,
8247
+ requestId,
8248
+ sessionId,
8249
+ rootSessionId: agent.subagents.root,
8250
+ subagentId: agent.subagents.idForSession(sessionId) ?? agent.subagents.idForTool(sessionId, request.params.toolCallId ?? request.params.toolCall?.toolCallId),
8251
+ method: request.method,
8252
+ params: request.params
8253
+ };
8254
+ }
8255
+ async readFile(params) {
8256
+ const file = await this.files.read(params.path);
8257
+ const start = (params.line ?? 1) - 1;
8258
+ const content = params.line !== void 0 || params.limit !== void 0 ? file.text.split("\n").slice(start, params.limit ? start + params.limit : void 0).join("\n") : file.text;
8259
+ if (Buffer.byteLength(content) > MAX_BYTES)
8260
+ throw new Error("File content exceeds 1 MiB; request a smaller line range");
8261
+ return { content };
8262
+ }
8263
+ async clientRequest(agent, method, params) {
8264
+ const registry = agent.subagents;
8265
+ const sessionId = params.sessionId;
8266
+ if (!registry?.acceptsRequest(sessionId)) throw new Error("Unknown ACP session");
8267
+ if (agent.approvals.size >= 32) throw new Error("Too many pending agent requests");
8268
+ if (method.startsWith("fs/")) {
8269
+ params.path = await this.relative(params.path, method === "fs/write_text_file");
8270
+ for (const field of ["line", "limit"])
8271
+ if (params[field] !== void 0 && (!Number.isSafeInteger(params[field]) || params[field] < 1))
8272
+ throw new Error(`Invalid ${field}`);
8273
+ if (method === "fs/write_text_file") requireString(params, "content", MAX_BYTES);
8274
+ }
8275
+ if (agent.stopped || registry !== agent.subagents || !registry.acceptsRequest(sessionId))
8276
+ throw new Error("ACP session ended");
8277
+ if (!agent.client && method === "fs/read_text_file") return this.readFile(params);
8278
+ return new Promise((resolve, reject) => {
8279
+ const requestId = randomUUID();
8280
+ const request = { method, params, resolve, reject };
8281
+ agent.approvals.set(requestId, request);
8282
+ if (method !== "fs/read_text_file" && !method.startsWith("oxbit/"))
8283
+ registry.waiting(sessionId, requestId, true, params.toolCallId ?? params.toolCall?.toolCallId);
8284
+ this.publish(agent, "acp.request", this.requestEvent(agent, requestId, request));
8285
+ });
8286
+ }
8287
+ async tool(id, name, args) {
8288
+ const agent = this.agents.get(id);
8289
+ const sessionId = agent?.connection.sessionId;
8290
+ if (!agent || agent.stopped || !sessionId) throw new Error("Start an agent conversation first");
8291
+ const registry = agent.subagents;
8292
+ const toolCallId = `oxbit-${randomUUID()}`;
8293
+ const write = name === "oxbit_propose_edit";
8294
+ let relative;
8295
+ if (args.path !== void 0) {
8296
+ const input = requireString(args, "path");
8297
+ relative = path.isAbsolute(input) ? await this.relative(input, write) : input;
8298
+ await this.files.resolve(relative, write);
8299
+ }
8300
+ for (const key of ["line", "limit"])
8301
+ if (args[key] !== void 0 && (!Number.isSafeInteger(args[key]) || args[key] < 1))
8302
+ throw new Error(`Invalid ${key}`);
8303
+ const publish = (update) => {
8304
+ if (!agent.stopped && registry === agent.subagents)
8305
+ this.routeUpdate(agent, sessionId, update);
8306
+ };
8307
+ const editor = () => {
8308
+ if (agent.client && !agent.editorTools) throw new Error(EDITOR_UNSUPPORTED);
8309
+ return !!agent.client;
8310
+ };
8311
+ publish({
8312
+ sessionUpdate: "tool_call",
8313
+ toolCallId,
8314
+ title: name,
8315
+ kind: write ? "edit" : "read",
8316
+ status: "in_progress",
8317
+ rawInput: args,
8318
+ ...relative ? { locations: [{ path: path.join(this.files.root, relative), line: args.line }] } : {}
8319
+ });
8320
+ try {
8321
+ let result;
8322
+ const params = { ...args, path: relative, sessionId, toolCallId };
8323
+ switch (name) {
8324
+ case "oxbit_get_workspace":
8325
+ result = {
8326
+ root: this.files.root,
8327
+ clientConnected: !!agent.client,
8328
+ ...editor() ? await this.clientRequest(agent, "oxbit/workspace", params) : { activeFile: null, openFiles: [], selection: null, diagnostics: [] }
8329
+ };
8330
+ break;
8331
+ case "oxbit_list_files": {
8332
+ const entries = await this.files.list(relative ?? "");
8333
+ result = { entries: entries.slice(0, 1e3), truncated: entries.length > 1e3 };
8334
+ break;
8335
+ }
8336
+ case "oxbit_read_file":
8337
+ case "oxbit_propose_edit":
8338
+ if (relative === void 0) throw new Error("A workspace path is required");
8339
+ result = await this.clientRequest(agent, write ? "fs/write_text_file" : "fs/read_text_file", {
8340
+ ...params,
8341
+ path: path.join(this.files.root, relative)
8342
+ });
8343
+ break;
8344
+ case "oxbit_get_diagnostics":
8345
+ result = editor() ? await this.clientRequest(agent, "oxbit/diagnostics", params) : { diagnostics: [], available: false, reason: "Attach an Oxbit editor for language diagnostics" };
8346
+ break;
8347
+ case "oxbit_open_file":
8348
+ if (relative === void 0) throw new Error("A workspace path is required");
8349
+ if (!editor()) throw new Error("Attach an Oxbit editor to open a file");
8350
+ result = await this.clientRequest(agent, "oxbit/open_file", params);
8351
+ break;
8352
+ case "oxbit_update_plan": {
8353
+ if (!Array.isArray(args.entries) || args.entries.length > 50) throw new Error("Provide up to 50 plan entries");
8354
+ const entries = args.entries.map((entry) => {
8355
+ const content = requireString(entry, "content", 2e3);
8356
+ if (!["pending", "in_progress", "completed"].includes(entry.status)) throw new Error("Invalid plan status");
8357
+ return { content, status: entry.status, ...typeof entry.priority === "string" ? { priority: entry.priority.slice(0, 40) } : {} };
8358
+ });
8359
+ publish({ sessionUpdate: "plan", entries });
8360
+ result = { entries };
8361
+ break;
8362
+ }
8363
+ default:
8364
+ throw new Error("Unknown Oxbit tool");
8365
+ }
8366
+ publish({ sessionUpdate: "tool_call_update", toolCallId, status: "completed", rawOutput: JSON.stringify(result ?? {}).slice(0, 8e3) });
8367
+ return result ?? {};
8368
+ } catch (error) {
8369
+ publish({ sessionUpdate: "tool_call_update", toolCallId, status: "failed", rawOutput: String(error) });
8370
+ throw error;
8371
+ }
8372
+ }
7679
8373
  async message(agent, message) {
7680
8374
  if (agent.stopped) return;
7681
8375
  if (!message.method) {
@@ -7720,8 +8414,8 @@ ${content}`
7720
8414
  throw new Error("Unknown ACP session");
7721
8415
  params.sessionId = sessionId;
7722
8416
  if (message.method.startsWith("terminal/")) {
7723
- const result = await this.terminal(agent, message.method, params);
7724
- this.send(agent, { id: message.id, result });
8417
+ const result2 = await this.terminal(agent, message.method, params);
8418
+ this.send(agent, { id: message.id, result: result2 });
7725
8419
  return;
7726
8420
  }
7727
8421
  if (![
@@ -7737,46 +8431,8 @@ ${content}`
7737
8431
  });
7738
8432
  return;
7739
8433
  }
7740
- if (agent.approvals.size >= 32)
7741
- throw new Error("Too many pending agent requests");
7742
- if (message.method.startsWith("fs/")) {
7743
- params.path = await this.relative(
7744
- params.path,
7745
- message.method === "fs/write_text_file"
7746
- );
7747
- for (const field of ["line", "limit"])
7748
- if (params[field] !== void 0 && (!Number.isSafeInteger(params[field]) || params[field] < 1))
7749
- throw new Error(`Invalid ${field}`);
7750
- if (message.method === "fs/write_text_file")
7751
- requireString(params, "content", MAX_BYTES);
7752
- }
7753
- if (agent.stopped) return;
7754
- if (registry !== agent.subagents || !registry.acceptsRequest(sessionId))
7755
- throw new Error("ACP session ended");
7756
- const requestId = randomUUID();
7757
- agent.approvals.set(requestId, {
7758
- rpcId: message.id,
7759
- method: message.method,
7760
- params
7761
- });
7762
- if (message.method !== "fs/read_text_file")
7763
- registry.waiting(
7764
- sessionId,
7765
- requestId,
7766
- true,
7767
- params.toolCallId ?? params.toolCall?.toolCallId
7768
- );
7769
- this.publish(agent, "acp.request", {
7770
- sessionId,
7771
- rootSessionId: registry.root,
7772
- subagentId: registry.idForSession(sessionId) ?? registry.idForTool(
7773
- sessionId,
7774
- params.toolCallId ?? params.toolCall?.toolCallId
7775
- ),
7776
- requestId,
7777
- method: message.method,
7778
- params
7779
- });
8434
+ const result = await this.clientRequest(agent, message.method, params);
8435
+ if (!agent.stopped) this.send(agent, { id: message.id, result });
7780
8436
  } catch (error) {
7781
8437
  if (!agent.stopped)
7782
8438
  this.send(agent, {
@@ -7795,10 +8451,8 @@ ${content}`
7795
8451
  )))
7796
8452
  throw new Error("Invalid permission response");
7797
8453
  }
7798
- this.send(agent, {
7799
- id: request.rpcId,
7800
- ...error ? { error: { code: -32603, message: error.slice(0, 4096) } } : { result }
7801
- });
8454
+ if (error) request.reject(new Error(error.slice(0, 4096)));
8455
+ else request.resolve(result);
7802
8456
  agent.approvals.delete(requestId);
7803
8457
  agent.subagents?.waiting(
7804
8458
  request.params.sessionId,
@@ -7933,26 +8587,29 @@ ${content}`
7933
8587
  pending.reject(new Error(reason));
7934
8588
  }
7935
8589
  agent.pending.clear();
8590
+ for (const approval of agent.approvals.values()) approval.reject(new Error(reason));
7936
8591
  agent.approvals.clear();
7937
8592
  for (const terminal2 of agent.terminals.values())
7938
8593
  killProcess(terminal2.child);
7939
8594
  agent.terminals.clear();
7940
8595
  killProcess(agent.child);
7941
8596
  this.agents.delete(agent.connection.id);
8597
+ this.mcp.revoke(agent.connection.id);
7942
8598
  this.publish(agent, "acp.exit", { reason });
7943
8599
  }
7944
8600
  stop(owner, id) {
7945
8601
  this.stopAgent(this.get(owner, id), "Agent disconnected");
7946
8602
  return {};
7947
8603
  }
7948
- disconnect(owner) {
8604
+ disconnect(owner, client) {
7949
8605
  for (const agent of this.agents.values())
7950
- if (agent.owner === owner)
8606
+ if (agent.owner === owner && (client === void 0 || agent.client === client))
7951
8607
  this.stopAgent(agent, "Agent client disconnected");
7952
8608
  }
7953
8609
  dispose() {
7954
8610
  for (const agent of this.agents.values())
7955
8611
  this.stopAgent(agent, "Agent tooling stopped");
8612
+ return this.mcp.close();
7956
8613
  }
7957
8614
  }
7958
8615
  var re = { exports: {} };
@@ -10149,12 +10806,12 @@ function createKernel({
10149
10806
  get(id, language) {
10150
10807
  return configuration.inspect(id, language).value;
10151
10808
  },
10152
- inspect(id, language) {
10809
+ inspect(id, language, scope = "workspace") {
10153
10810
  const scopes = language ? [.../* @__PURE__ */ new Set([language, canonicalLanguageId(language), ...languages.find((item) => item.id === canonicalLanguageId(language))?.settingsAliases ?? []])] : [];
10154
10811
  const ordered = [
10155
- ...scopes.map((id2) => configurationData.workspaceLanguages[id2]),
10812
+ ...scope === "workspace" ? scopes.map((id2) => configurationData.workspaceLanguages[id2]) : [],
10156
10813
  ...scopes.map((id2) => configurationData.userLanguages[id2]),
10157
- configurationData.workspace,
10814
+ ...scope === "workspace" ? [configurationData.workspace] : [],
10158
10815
  configurationData.user
10159
10816
  ];
10160
10817
  let value = structuredClone(settings.get(id)?.default);
@@ -10941,11 +11598,6 @@ class RuntimeExtensions {
10941
11598
  this.watchers.clear();
10942
11599
  }
10943
11600
  }
10944
- let cached;
10945
- function runtimeVersion() {
10946
- if (cached) return cached;
10947
- return cached = "0.6.0";
10948
- }
10949
11601
  const wildcardHost = (host) => host === "0.0.0.0" || host === "::";
10950
11602
  const localHost = (host) => wildcardHost(host) ? "127.0.0.1" : host;
10951
11603
  const localName = (hostname = os.hostname()) => hostname.split(".")[0];
@@ -10978,7 +11630,7 @@ function lanUrl(port2, interfaces = os.networkInterfaces()) {
10978
11630
  const address = lanAddresses$1(interfaces).v4[0];
10979
11631
  return address ? `http://${address}:${port2}` : void 0;
10980
11632
  }
10981
- const MAX_SOCKET_BACKLOG_BYTES = 16 * MAX_BUFFER_BYTES;
11633
+ const MAX_SOCKET_BACKLOG_BYTES = MAX_FILE_WRITE_MESSAGE_BYTES;
10982
11634
  const allCapabilities = [
10983
11635
  "filesystem.read",
10984
11636
  "filesystem.write",
@@ -11023,7 +11675,8 @@ async function createRuntime(options2) {
11023
11675
  const dataDir = path.resolve(options2.dataDir ?? workspaceDataDir(root));
11024
11676
  await fs.mkdir(dataDir, { recursive: true, mode: 448 });
11025
11677
  const stateFile = path.join(dataDir, "runtime.json");
11026
- const files = new WorkspaceFiles(root, dataDir), sessions = /* @__PURE__ */ new Map(), operations = /* @__PURE__ */ new Map(), inflight = /* @__PURE__ */ new Map(), connections = /* @__PURE__ */ new Map();
11678
+ const files = new WorkspaceFiles(root, dataDir), sessions = /* @__PURE__ */ new Map(), inflight = /* @__PURE__ */ new Map(), connections = /* @__PURE__ */ new Map();
11679
+ let operations = new OperationHistory();
11027
11680
  let trusted = false, closed = false, runtimeId = randomUUID();
11028
11681
  try {
11029
11682
  const state2 = JSON.parse(await fs.readFile(stateFile, "utf8"));
@@ -11031,16 +11684,7 @@ async function createRuntime(options2) {
11031
11684
  if (typeof state2.id === "string" && /^[A-Za-z0-9-]{8,64}$/.test(state2.id)) runtimeId = state2.id;
11032
11685
  for (const session of state2.sessions ?? [])
11033
11686
  sessions.set(session.hash, session);
11034
- for (const operation of state2.operations ?? []) {
11035
- if (operation.status === "running") {
11036
- operation.status = "interrupted";
11037
- operation.error = {
11038
- code: "INTERRUPTED",
11039
- message: "Runtime stopped before operation completion was recorded; inspect state before retrying with a new request ID"
11040
- };
11041
- }
11042
- operations.set(`${operation.owner}:${operation.id}`, operation);
11043
- }
11687
+ operations = new OperationHistory(state2.operationHistory, state2.operations ?? []);
11044
11688
  } catch (error) {
11045
11689
  if (error.code !== "ENOENT")
11046
11690
  throw new Error(`Cannot recover runtime state: ${String(error)}`);
@@ -11063,6 +11707,7 @@ async function createRuntime(options2) {
11063
11707
  id: runtimeId,
11064
11708
  trusted,
11065
11709
  sessions: [...sessions.values()],
11710
+ operationHistory: operations.checkpoint(),
11066
11711
  operations: [...operations.values()]
11067
11712
  });
11068
11713
  persistence = persistence.catch(() => {
@@ -11129,7 +11774,7 @@ async function createRuntime(options2) {
11129
11774
  const agents = new AgentACP(files, (connectionId, name, params) => {
11130
11775
  const connection = connections.get(connectionId);
11131
11776
  if (connection?.session && !connection.session.revoked && trusted) event(connection, name, params);
11132
- });
11777
+ }, () => updateIdle());
11133
11778
  const processEvent = ({ event: name, params, stream, seq }, ownerId) => {
11134
11779
  const cap = name.startsWith("terminal.") ? "terminal" : "tasks";
11135
11780
  for (const c of connections.values()) {
@@ -11219,7 +11864,8 @@ async function createRuntime(options2) {
11219
11864
  capabilities: session.capabilities,
11220
11865
  owner: session.owner,
11221
11866
  trusted,
11222
- sessionId: session.id
11867
+ sessionId: session.id,
11868
+ operationEpoch: operations.epoch
11223
11869
  });
11224
11870
  const createSession = async (isOwner, capabilities) => {
11225
11871
  const token = randomBytes(32).toString("base64url"), session = {
@@ -11238,6 +11884,7 @@ async function createRuntime(options2) {
11238
11884
  const session = [...sessions.values()].find((s) => s.id === id);
11239
11885
  if (!session) throw new RpcError("NOT_FOUND", "Grant does not exist");
11240
11886
  session.revoked = true;
11887
+ agents.disconnect(session.id);
11241
11888
  processes.revoke(session.id);
11242
11889
  tasks.revoke(session.id);
11243
11890
  for (const c of connections.values())
@@ -11441,7 +12088,7 @@ async function createRuntime(options2) {
11441
12088
  });
11442
12089
  const websocket = new WebSocketServer({
11443
12090
  noServer: true,
11444
- maxPayload: MAX_MESSAGE_BYTES,
12091
+ maxPayload: MAX_FILE_WRITE_MESSAGE_BYTES,
11445
12092
  perMessageDeflate: false
11446
12093
  });
11447
12094
  server.on("upgrade", (request, socket, head) => {
@@ -11501,7 +12148,11 @@ async function createRuntime(options2) {
11501
12148
  if (workspace !== "default")
11502
12149
  throw new RpcError("FORBIDDEN", "Workspace is not granted");
11503
12150
  const required = permission(method), session = authorized(connection, required.cap, required.trust);
11504
- if (method.startsWith("acp.")) owner(connection);
12151
+ if (method.startsWith("acp.")) {
12152
+ owner(connection);
12153
+ if (!["acp.start", "acp.list", "acp.attach", "acp.disconnect"].includes(method))
12154
+ agents.control(session.id, requireString(params, "id"), connection.id);
12155
+ }
11505
12156
  if (method.startsWith("project.")) owner(connection);
11506
12157
  if (method.startsWith("settings.")) owner(connection);
11507
12158
  switch (method) {
@@ -11524,17 +12175,28 @@ async function createRuntime(options2) {
11524
12175
  case "project.relations":
11525
12176
  return project.relations(requireString(params, "path"));
11526
12177
  case "acp.start":
11527
- return agents.start(connection.id, params, signal);
12178
+ return agents.start(session.id, params, signal, connection.id);
12179
+ case "acp.list":
12180
+ return agents.list(session.id);
12181
+ case "acp.attach":
12182
+ return agents.attach(session.id, requireString(params, "id"), connection.id, params.clientCapabilities?.editorTools === true);
12183
+ case "acp.enqueue":
12184
+ case "acp.interrupt":
12185
+ return agents.enqueue(session.id, requireString(params, "id"), params, method === "acp.interrupt");
12186
+ case "acp.dequeue":
12187
+ return agents.dequeue(session.id, requireString(params, "id"), requireString(params, "promptId"));
12188
+ case "acp.resumeQueue":
12189
+ return agents.resumeQueue(session.id, requireString(params, "id"));
11528
12190
  case "acp.call":
11529
- return agents.call(connection.id, requireString(params, "id"), requireString(params, "method"), params.params ?? {}, signal);
12191
+ return agents.call(session.id, requireString(params, "id"), requireString(params, "method"), params.params ?? {});
11530
12192
  case "acp.respond":
11531
- return agents.respond(connection.id, requireString(params, "id"), requireString(params, "requestId"), params.result, params.error === void 0 ? void 0 : requireString(params, "error"));
12193
+ return agents.respond(session.id, requireString(params, "id"), requireString(params, "requestId"), params.result, params.error === void 0 ? void 0 : requireString(params, "error"));
11532
12194
  case "acp.cancel":
11533
- return agents.cancel(connection.id, requireString(params, "id"));
12195
+ return agents.cancel(session.id, requireString(params, "id"));
11534
12196
  case "acp.stop":
11535
- return agents.stop(connection.id, requireString(params, "id"));
12197
+ return agents.stop(session.id, requireString(params, "id"));
11536
12198
  case "acp.disconnect":
11537
- agents.disconnect(connection.id);
12199
+ agents.disconnect(session.id, connection.id);
11538
12200
  return {};
11539
12201
  case "workspace.info":
11540
12202
  return sessionInfo(session);
@@ -11544,7 +12206,7 @@ async function createRuntime(options2) {
11544
12206
  throw new RpcError("INVALID_PARAMS", "trusted must be boolean");
11545
12207
  trusted = params.trusted;
11546
12208
  if (!trusted) {
11547
- agents.dispose();
12209
+ await agents.dispose();
11548
12210
  await extensions.suspend();
11549
12211
  for (const c of connections.values()) for (const pending of c.pending.values()) pending.abort();
11550
12212
  for (const s of sessions.values()) {
@@ -11572,10 +12234,7 @@ async function createRuntime(options2) {
11572
12234
  owner(connection);
11573
12235
  return revoke(requireString(params, "id"));
11574
12236
  case "operation.status": {
11575
- const value = operations.get(
11576
- `${session.id}:${requireString(params, "id")}`
11577
- );
11578
- return value ?? { id: params.id, status: "unknown" };
12237
+ return operations.status(session.id, requireString(params, "id"));
11579
12238
  }
11580
12239
  case "fs.list":
11581
12240
  return files.list(typeof params.path === "string" ? params.path : "");
@@ -11593,7 +12252,7 @@ async function createRuntime(options2) {
11593
12252
  case "fs.write": {
11594
12253
  const snapshot = await files.write(
11595
12254
  requireString(params, "path"),
11596
- requireString(params, "text", 20 * 1024 * 1024),
12255
+ requireString(params, "text", MAX_FILE_BYTES),
11597
12256
  {
11598
12257
  expectedRevision: requireRevision(params),
11599
12258
  encoding: params.encoding,
@@ -11856,7 +12515,7 @@ async function createRuntime(options2) {
11856
12515
  return collaboration.update(
11857
12516
  requireString(params, "path"),
11858
12517
  connection.id,
11859
- requireString(params, "update", MAX_MESSAGE_BYTES)
12518
+ requireString(params, "update", MAX_COLLABORATION_UPDATE_BYTES)
11860
12519
  );
11861
12520
  case "collab.awareness":
11862
12521
  return collaboration.awareness(
@@ -11966,7 +12625,7 @@ async function createRuntime(options2) {
11966
12625
  };
11967
12626
  let idleSince, idleTimer;
11968
12627
  const updateIdle = () => {
11969
- if ([...connections.values()].some((c) => c.session && !c.session.revoked)) {
12628
+ if (agents.hasWork || [...connections.values()].some((c) => c.session && !c.session.revoked)) {
11970
12629
  idleSince = void 0;
11971
12630
  clearTimeout(idleTimer);
11972
12631
  return;
@@ -12056,7 +12715,7 @@ async function createRuntime(options2) {
12056
12715
  let result;
12057
12716
  if (durableMethods.has(method)) {
12058
12717
  const required = permission(method), session = authorized(connection, required.cap, required.trust);
12059
- const key = `${session.id}:${id}`, existing = operations.get(key);
12718
+ const key = `${session.id}:${id}`, existing = operations.get(session.id, id);
12060
12719
  if (existing) {
12061
12720
  if (existing.method !== method)
12062
12721
  throw new RpcError(
@@ -12064,6 +12723,8 @@ async function createRuntime(options2) {
12064
12723
  "Request ID was used for another operation"
12065
12724
  );
12066
12725
  if (inflight.has(key)) result = await inflight.get(key);
12726
+ else if (existing.resultExpired)
12727
+ throw new RpcError("OPERATION_RESULT_EXPIRED", "Operation result expired; inspect current state before submitting a new operation", { status: existing.status });
12067
12728
  else if (existing.status === "completed")
12068
12729
  result = existing.result;
12069
12730
  else
@@ -12073,14 +12734,11 @@ async function createRuntime(options2) {
12073
12734
  existing.error?.data
12074
12735
  );
12075
12736
  } else {
12076
- const operation = {
12077
- id,
12078
- owner: session.id,
12079
- method,
12080
- status: "running",
12081
- startedAt: Date.now()
12082
- };
12083
- operations.set(key, operation);
12737
+ const previousEpoch = operations.epoch;
12738
+ const operation = operations.begin(session.id, id, method);
12739
+ if (operations.epoch !== previousEpoch) {
12740
+ for (const c of connections.values()) if (c.session && !c.session.revoked) event(c, "operation.epoch", { epoch: operations.epoch });
12741
+ }
12084
12742
  const run2 = (async () => {
12085
12743
  await persist();
12086
12744
  try {
@@ -12091,13 +12749,11 @@ async function createRuntime(options2) {
12091
12749
  controller.signal,
12092
12750
  id
12093
12751
  );
12094
- operation.status = "completed";
12095
- operation.result = value;
12752
+ operations.complete(operation, value);
12096
12753
  await persist();
12097
12754
  return value;
12098
12755
  } catch (error) {
12099
- operation.status = "failed";
12100
- operation.error = errorShape(error);
12756
+ operations.fail(operation, errorShape(error));
12101
12757
  await persist();
12102
12758
  throw error;
12103
12759
  }
@@ -12142,7 +12798,7 @@ async function createRuntime(options2) {
12142
12798
  });
12143
12799
  ws.on("close", () => {
12144
12800
  clearTimeout(authDeadline);
12145
- agents.disconnect(connection.id);
12801
+ agents.detach(connection.id);
12146
12802
  lsp.detach(connection.id);
12147
12803
  connections.delete(connection.id);
12148
12804
  updateIdle();
@@ -12154,7 +12810,7 @@ async function createRuntime(options2) {
12154
12810
  processes.disconnect(connection.id);
12155
12811
  void collaboration.leave(connection.id);
12156
12812
  for (const [id, controller] of connection.pending) {
12157
- const operation = connection.session && operations.get(`${connection.session.id}:${id}`);
12813
+ const operation = connection.session && operations.get(connection.session.id, id);
12158
12814
  if (!operation) controller.abort();
12159
12815
  }
12160
12816
  });
@@ -12284,7 +12940,7 @@ async function createRuntime(options2) {
12284
12940
  for (const controller of c.pending.values()) controller.abort();
12285
12941
  c.ws.terminate();
12286
12942
  }
12287
- agents.dispose();
12943
+ await agents.dispose();
12288
12944
  extensions.dispose();
12289
12945
  processes.close();
12290
12946
  await tasks.close();
@@ -12534,17 +13190,17 @@ function advertise(ad, log = stderrLog) {
12534
13190
  try {
12535
13191
  const query = parseMessage(packet);
12536
13192
  if (query.flags & 32768) return;
12537
- const reply = answer(query.questions, current());
12538
- if (!reply) return;
12539
- if (from.port === PORT) return send(encodeMessage(reply));
13193
+ const reply2 = answer(query.questions, current());
13194
+ if (!reply2) return;
13195
+ if (from.port === PORT) return send(encodeMessage(reply2));
12540
13196
  const legacy = (r) => ({ ...r, ttl: Math.min(r.ttl, 10), flush: false });
12541
13197
  send(
12542
13198
  encodeMessage({
12543
13199
  id: query.id,
12544
13200
  flags: FLAGS_RESPONSE,
12545
13201
  questions: query.questions,
12546
- answers: reply.answers.map(legacy),
12547
- additionals: reply.additionals.map(legacy)
13202
+ answers: reply2.answers.map(legacy),
13203
+ additionals: reply2.additionals.map(legacy)
12548
13204
  }),
12549
13205
  from.port,
12550
13206
  from.address