@zhanguym/chatroom 1.2.0 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/app/application.d.ts +5 -0
  2. package/dist/app/application.js +55 -4
  3. package/dist/app/lifecycle.js +12 -0
  4. package/dist/app/workspace-access.d.ts +17 -4
  5. package/dist/core/operations/types.d.ts +1 -1
  6. package/dist/core/operations/types.js +1 -1
  7. package/dist/infrastructure/database/agent-mcp-settings-repository.d.ts +12 -0
  8. package/dist/infrastructure/database/agent-mcp-settings-repository.js +55 -0
  9. package/dist/infrastructure/database/app-database.js +3 -3
  10. package/dist/infrastructure/database/migrate.d.ts +2 -0
  11. package/dist/infrastructure/database/migrate.js +83 -0
  12. package/dist/infrastructure/database/schema.d.ts +3 -1
  13. package/dist/infrastructure/database/schema.js +35 -6
  14. package/dist/infrastructure/database/workspace-repository.d.ts +28 -0
  15. package/dist/infrastructure/database/workspace-repository.js +126 -0
  16. package/dist/mcp/custom/manager.d.ts +3 -1
  17. package/dist/mcp/custom/manager.js +37 -2
  18. package/dist/mcp/server/plugin-mcp-registrar.d.ts +4 -1
  19. package/dist/mcp/server/plugin-mcp-registrar.js +7 -2
  20. package/dist/mcp/server/tool-control.d.ts +6 -1
  21. package/dist/mcp/server/tool-control.js +11 -2
  22. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  23. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  24. package/dist/plugins/agent/custom-mcp-store.d.ts +17 -0
  25. package/dist/plugins/agent/custom-mcp-store.js +38 -0
  26. package/dist/plugins/agent/mcp-gateway.d.ts +16 -0
  27. package/dist/plugins/agent/mcp-gateway.js +110 -0
  28. package/dist/plugins/agent/mcp.js +3 -2
  29. package/dist/plugins/agent/plugin.js +1 -1
  30. package/dist/plugins/agent/provider.d.ts +1 -0
  31. package/dist/plugins/agent/providers/codex/launch.d.ts +17 -0
  32. package/dist/plugins/agent/providers/codex/launch.js +24 -0
  33. package/dist/plugins/agent/providers/codex/provider.d.ts +3 -1
  34. package/dist/plugins/agent/providers/codex/provider.js +8 -2
  35. package/dist/plugins/agent/providers/codex/rpc.d.ts +7 -1
  36. package/dist/plugins/agent/providers/codex/rpc.js +28 -3
  37. package/dist/plugins/agent/providers/index.d.ts +2 -1
  38. package/dist/plugins/agent/providers/index.js +5 -2
  39. package/dist/plugins/agent/repository.js +10 -7
  40. package/dist/plugins/agent/runtime-files.d.ts +4 -4
  41. package/dist/plugins/agent/runtime-files.js +18 -14
  42. package/dist/plugins/agent/runtime.d.ts +2 -0
  43. package/dist/plugins/agent/runtime.js +52 -13
  44. package/dist/plugins/agent/types.d.ts +3 -2
  45. package/dist/plugins/cloud/v2/tunnel-client.js +1 -7
  46. package/dist/plugins/plugin-manager.d.ts +6 -1
  47. package/dist/plugins/plugin-manager.js +9 -2
  48. package/dist/plugins/types.d.ts +4 -0
  49. package/dist/plugins/web/http/agent-api-router.js +15 -1
  50. package/dist/plugins/web/http/git-api-router.js +13 -13
  51. package/dist/plugins/web/http/rtc-api-router.js +3 -0
  52. package/dist/plugins/web/http/runtime-api-router.js +22 -0
  53. package/dist/plugins/web/http/workspace-api-router.js +63 -12
  54. package/dist/plugins/web/plugin.js +1 -1
  55. package/dist/plugins/web/runtime.d.ts +3 -1
  56. package/dist/plugins/web/runtime.js +5 -1
  57. package/dist/plugins/workspace/mcp.js +20 -15
  58. package/dist/plugins/workspace/plugin.js +1 -1
  59. package/dist/plugins/workspace/workspace-root-policy.d.ts +0 -1
  60. package/dist/plugins/workspace/workspace-root-policy.js +25 -19
  61. package/dist/plugins/workspace/workspace-service.d.ts +20 -8
  62. package/dist/plugins/workspace/workspace-service.js +270 -64
  63. package/dist/web/assets/{AgentView-CMxIn3KB.css → AgentView-BqAfmgc4.css} +1 -1
  64. package/dist/web/assets/AgentView-tr6COjRg.js +4 -0
  65. package/dist/web/assets/{CloudView-CXuTK7Of.js → CloudView-D-fYLw_O.js} +1 -1
  66. package/dist/web/assets/{CodeViewer.vue_vue_type_script_setup_true_lang-DK6xGH2G.js → CodeViewer.vue_vue_type_script_setup_true_lang-BtA5aWrS.js} +1 -1
  67. package/dist/web/assets/ComputerView-B25C_C_h.js +1 -0
  68. package/dist/web/assets/ComputerView-vSSsexBY.css +1 -0
  69. package/dist/web/assets/{MarkdownContent-D4KYOn-f.js → MarkdownContent-BuDRrZ6c.js} +1 -1
  70. package/dist/web/assets/McpToolsView-D0G_HACK.css +1 -0
  71. package/dist/web/assets/McpToolsView-s0-Qvg2g.js +2 -0
  72. package/dist/web/assets/OperationsView-B_0SN40Y.js +4 -0
  73. package/dist/web/assets/{ProcessesView-NUAplTg4.js → ProcessesView-B2i-O0AL.js} +3 -3
  74. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-DwnqcvSQ.js +1 -0
  75. package/dist/web/assets/{SystemLogsView-DGW_4nS0.js → SystemLogsView-BR4FZoAh.js} +1 -1
  76. package/dist/web/assets/{TerminalView-DCqHCfb5.js → TerminalView-2Z1blITD.js} +1 -1
  77. package/dist/web/assets/VSelect-CHI_FOMl.js +1 -0
  78. package/dist/web/assets/VSheet-CKEhsY-7.js +1 -0
  79. package/dist/web/assets/VSpacer-FbdDPUGx.js +1 -0
  80. package/dist/web/assets/{VSwitch-o-WPjZs0.js → VSwitch-o9DuRTKF.js} +1 -1
  81. package/dist/web/assets/{VTabs-D2y6M66D.js → VTabs-BSfSKBtT.js} +1 -1
  82. package/dist/web/assets/{VTextarea-DFSNyIwH.js → VTextarea-D_kPhUVU.js} +1 -1
  83. package/dist/web/assets/WorkspaceDownloadPreview-BoALqL1M.js +1 -0
  84. package/dist/web/assets/WorkspaceDownloadPreview-dFslZB0W.css +1 -0
  85. package/dist/web/assets/{WorkspaceMediaPreview-tkpcPK82.js → WorkspaceMediaPreview-qjt04eze.js} +3 -3
  86. package/dist/web/assets/{WorkspacePdfPreview-PqIFAx2J.js → WorkspacePdfPreview-BrUJpmNs.js} +1 -1
  87. package/dist/web/assets/WorkspacesView-CHXmDTlX.js +2 -0
  88. package/dist/web/assets/WorkspacesView-Dt24eGQ4.css +1 -0
  89. package/dist/web/assets/index-9DLDvF8M.js +19 -0
  90. package/dist/web/assets/{useMasterDetailFocus-DgNjKho5.js → useMasterDetailFocus-Chh0cM6g.js} +1 -1
  91. package/dist/web/assets/useOperationLabels-DF67nIF0.js +1 -0
  92. package/dist/web/index.html +1 -1
  93. package/dist/web/transport-sw.js +43 -15
  94. package/package.json +1 -1
  95. package/dist/web/assets/AgentView-XEf1AUcH.js +0 -4
  96. package/dist/web/assets/ComputerView-Bz2Wsz2X.css +0 -1
  97. package/dist/web/assets/ComputerView-Cxn7zhrw.js +0 -1
  98. package/dist/web/assets/McpToolsView-DdYOoY9_.css +0 -1
  99. package/dist/web/assets/McpToolsView-XLtFPY_M.js +0 -2
  100. package/dist/web/assets/OperationsView-DRx2Qq8W.js +0 -4
  101. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-BYoHRX9h.js +0 -1
  102. package/dist/web/assets/VSelect-Dla-KtWu.js +0 -1
  103. package/dist/web/assets/VSheet-B9d-Gdzq.js +0 -1
  104. package/dist/web/assets/VSpacer--LXbKJE2.js +0 -1
  105. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +0 -1
  106. package/dist/web/assets/WorkspaceDownloadPreview-DaFQwUIa.js +0 -1
  107. package/dist/web/assets/WorkspacesView-ClA6xcjn.css +0 -1
  108. package/dist/web/assets/WorkspacesView-DkGvIFCb.js +0 -2
  109. package/dist/web/assets/index-CacDJSDn.js +0 -19
  110. package/dist/web/assets/useOperationLabels-BNiE-bRq.js +0 -1
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  export class AgentRepository {
2
3
  database;
3
4
  constructor(database) {
@@ -27,23 +28,23 @@ export class AgentRepository {
27
28
  }
28
29
  insertSession(session) {
29
30
  this.database.raw
30
- .prepare("INSERT INTO agent_sessions(id, workspace_root, provider, status, created_at, updated_at) VALUES(?,?,?,?,?,?)")
31
- .run(session.id, session.workspaceRoot, session.provider, session.status, session.createdAt, session.updatedAt);
31
+ .prepare("INSERT INTO agent_sessions(id, workspace_id, provider, status, created_at, updated_at) VALUES(?,?,?,?,?,?)")
32
+ .run(session.id, session.workspaceId, session.provider, session.status, session.createdAt, session.updatedAt);
32
33
  }
33
34
  updateSession(session) {
34
35
  this.database.raw
35
- .prepare("UPDATE agent_sessions SET workspace_root=?, provider=?, status=?, created_at=?, updated_at=? WHERE id=?")
36
- .run(session.workspaceRoot, session.provider, session.status, session.createdAt, session.updatedAt, session.id);
36
+ .prepare("UPDATE agent_sessions SET workspace_id=?, provider=?, status=?, created_at=?, updated_at=? WHERE id=?")
37
+ .run(session.workspaceId, session.provider, session.status, session.createdAt, session.updatedAt, session.id);
37
38
  }
38
39
  getSession(id) {
39
40
  const row = this.database.raw
40
- .prepare("SELECT s.*, p.model AS model, p.reasoning_effort, p.reasoning_summary, p.service_tier, p.approval_policy_json, p.approvals_reviewer, p.permission_mode, p.token_usage_json, CASE WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting' AND json_extract(ai.payload_json,'$.interaction.kind')='approval') THEN 'approval' WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') THEN 'input' ELSE NULL END AS attention_kind, (SELECT COUNT(*) FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') AS attention_count FROM agent_sessions s LEFT JOIN agent_session_providers p ON p.session_id=s.id AND p.provider=s.provider WHERE s.id=?")
41
+ .prepare("SELECT s.*, w.name_override AS workspace_name, w.path AS workspace_path, p.model AS model, p.reasoning_effort, p.reasoning_summary, p.service_tier, p.approval_policy_json, p.approvals_reviewer, p.permission_mode, p.token_usage_json, CASE WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting' AND json_extract(ai.payload_json,'$.interaction.kind')='approval') THEN 'approval' WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') THEN 'input' ELSE NULL END AS attention_kind, (SELECT COUNT(*) FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') AS attention_count FROM agent_sessions s LEFT JOIN workspaces w ON w.id=s.workspace_id LEFT JOIN agent_session_providers p ON p.session_id=s.id AND p.provider=s.provider WHERE s.id=?")
41
42
  .get(id);
42
43
  return row ? sessionFromRow(row) : null;
43
44
  }
44
45
  listSessions() {
45
46
  return this.database.raw
46
- .prepare("SELECT s.*, p.model AS model, p.reasoning_effort, p.reasoning_summary, p.service_tier, p.approval_policy_json, p.approvals_reviewer, p.permission_mode, p.token_usage_json, CASE WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting' AND json_extract(ai.payload_json,'$.interaction.kind')='approval') THEN 'approval' WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') THEN 'input' ELSE NULL END AS attention_kind, (SELECT COUNT(*) FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') AS attention_count FROM agent_sessions s LEFT JOIN agent_session_providers p ON p.session_id=s.id AND p.provider=s.provider ORDER BY s.updated_at DESC")
47
+ .prepare("SELECT s.*, w.name_override AS workspace_name, w.path AS workspace_path, p.model AS model, p.reasoning_effort, p.reasoning_summary, p.service_tier, p.approval_policy_json, p.approvals_reviewer, p.permission_mode, p.token_usage_json, CASE WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting' AND json_extract(ai.payload_json,'$.interaction.kind')='approval') THEN 'approval' WHEN EXISTS (SELECT 1 FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') THEN 'input' ELSE NULL END AS attention_kind, (SELECT COUNT(*) FROM agent_items ai JOIN agent_turns at ON at.id=ai.turn_id WHERE at.session_id=s.id AND ai.type='interaction' AND ai.status='waiting') AS attention_count FROM agent_sessions s LEFT JOIN workspaces w ON w.id=s.workspace_id LEFT JOIN agent_session_providers p ON p.session_id=s.id AND p.provider=s.provider ORDER BY s.updated_at DESC")
47
48
  .all().map(sessionFromRow);
48
49
  }
49
50
  deleteSession(id) {
@@ -131,7 +132,9 @@ function selectByIds(database, prefix, sessionId, ids) {
131
132
  function sessionFromRow(row) {
132
133
  return {
133
134
  id: row.id,
134
- workspaceRoot: row.workspace_root,
135
+ workspaceId: row.workspace_id,
136
+ workspaceName: row.workspace_name ??
137
+ (row.workspace_path ? path.basename(row.workspace_path) : ""),
135
138
  tokenUsage: parseJson(row.token_usage_json),
136
139
  provider: row.provider,
137
140
  model: row.model,
@@ -2,7 +2,7 @@ import type { ProviderItem } from "./provider.js";
2
2
  import type { AgentStore } from "./store.js";
3
3
  import type { AgentHistory, AgentSession, AgentTurnInput } from "./types.js";
4
4
  export declare function sessionAttachmentPrefix(sessionId: string): string;
5
- export declare function cleanupSessionFiles(store: AgentStore, history: AgentHistory): Promise<void>;
6
- export declare function cleanupOrphanAttachments(store: AgentStore, maxAgeMs: number): Promise<void>;
7
- export declare function prepareProviderItem(session: AgentSession, item: ProviderItem): Promise<ProviderItem>;
8
- export declare function validateAttachments(session: AgentSession, attachments: AgentTurnInput["attachments"]): Promise<AgentTurnInput["attachments"]>;
5
+ export declare function cleanupSessionFiles(store: AgentStore, history: AgentHistory, workspaceRoot: string | null): Promise<void>;
6
+ export declare function cleanupOrphanAttachments(store: AgentStore, resolveWorkspace: (workspaceId: string) => Promise<string>, maxAgeMs: number): Promise<void>;
7
+ export declare function prepareProviderItem(workspaceRoot: string, item: ProviderItem): Promise<ProviderItem>;
8
+ export declare function validateAttachments(session: AgentSession, workspaceRoot: string, attachments: AgentTurnInput["attachments"]): Promise<AgentTurnInput["attachments"]>;
@@ -7,9 +7,10 @@ import { runtimeErrorMessage } from "./runtime-state.js";
7
7
  export function sessionAttachmentPrefix(sessionId) {
8
8
  return ".chatroom/agent-attachments/" + sessionId + "/";
9
9
  }
10
- export async function cleanupSessionFiles(store, history) {
11
- const workspaceRoot = history.session.workspaceRoot;
12
- const retained = referencedManagedFiles(store, workspaceRoot, history.session.id);
10
+ export async function cleanupSessionFiles(store, history, workspaceRoot) {
11
+ if (!workspaceRoot)
12
+ return;
13
+ const retained = referencedManagedFiles(store, history.session.workspaceId, history.session.id);
13
14
  const attachmentPrefix = sessionAttachmentPrefix(history.session.id);
14
15
  const attachmentDirectory = path.join(workspaceRoot, attachmentPrefix);
15
16
  try {
@@ -29,14 +30,17 @@ export async function cleanupSessionFiles(store, history) {
29
30
  await removeFileWithin(workspaceRoot, filePath).catch(() => undefined);
30
31
  }
31
32
  }
32
- export async function cleanupOrphanAttachments(store, maxAgeMs) {
33
+ export async function cleanupOrphanAttachments(store, resolveWorkspace, maxAgeMs) {
33
34
  const now = Date.now();
34
- const workspaceRoots = new Set(store.listSessions().map((session) => session.workspaceRoot));
35
- for (const workspaceRoot of workspaceRoots) {
36
- await removeUnreferencedAttachmentFiles(workspaceRoot, referencedManagedFiles(store, workspaceRoot), now, maxAgeMs);
35
+ const workspaceIds = new Set(store.listSessions().map((session) => session.workspaceId));
36
+ for (const workspaceId of workspaceIds) {
37
+ const workspaceRoot = await resolveWorkspace(workspaceId).catch(() => null);
38
+ if (!workspaceRoot)
39
+ continue;
40
+ await removeUnreferencedAttachmentFiles(workspaceRoot, referencedManagedFiles(store, workspaceId), now, maxAgeMs);
37
41
  }
38
42
  }
39
- export async function prepareProviderItem(session, item) {
43
+ export async function prepareProviderItem(workspaceRoot, item) {
40
44
  if (item.type !== "artifact")
41
45
  return item;
42
46
  try {
@@ -45,7 +49,7 @@ export async function prepareProviderItem(session, item) {
45
49
  ...item,
46
50
  artifact: {
47
51
  ...item.artifact,
48
- path: (await resolveReadableFileWithin(session.workspaceRoot, item.artifact.path)).relative,
52
+ path: (await resolveReadableFileWithin(workspaceRoot, item.artifact.path)).relative,
49
53
  },
50
54
  };
51
55
  }
@@ -61,7 +65,7 @@ export async function prepareProviderItem(session, item) {
61
65
  const content = await readFile(item.artifact.path);
62
66
  if (content.byteLength > 32 * 1024 * 1024)
63
67
  throw new ChatRoomError("INVALID_INPUT", "Agent artifact is too large");
64
- await writeBytesAtomicWithin(session.workspaceRoot, target, content);
68
+ await writeBytesAtomicWithin(workspaceRoot, target, content);
65
69
  return {
66
70
  ...item,
67
71
  artifact: {
@@ -78,9 +82,9 @@ export async function prepareProviderItem(session, item) {
78
82
  };
79
83
  }
80
84
  }
81
- export async function validateAttachments(session, attachments) {
85
+ export async function validateAttachments(session, workspaceRoot, attachments) {
82
86
  return Promise.all(attachments.map(async (attachment) => {
83
- const resolved = await resolveReadableFileWithin(session.workspaceRoot, attachment.path);
87
+ const resolved = await resolveReadableFileWithin(workspaceRoot, attachment.path);
84
88
  if (resolved.relative.startsWith(".chatroom/agent-attachments/") &&
85
89
  !resolved.relative.startsWith(sessionAttachmentPrefix(session.id)))
86
90
  throw new ChatRoomError("FORBIDDEN", "Agent attachment belongs to another session");
@@ -90,12 +94,12 @@ export async function validateAttachments(session, attachments) {
90
94
  };
91
95
  }));
92
96
  }
93
- function referencedManagedFiles(store, workspaceRoot, excludedSessionId) {
97
+ function referencedManagedFiles(store, workspaceId, excludedSessionId) {
94
98
  const paths = new Set();
95
99
  for (const session of store.listSessions()) {
96
100
  if (session.id === excludedSessionId)
97
101
  continue;
98
- if (session.workspaceRoot !== workspaceRoot)
102
+ if (session.workspaceId !== workspaceId)
99
103
  continue;
100
104
  const history = store.history(session.id);
101
105
  if (!history)
@@ -14,6 +14,7 @@ export declare class AgentRuntime {
14
14
  private closed;
15
15
  private readonly providers;
16
16
  constructor(store: AgentStore, workspaces: WorkspaceAccess, events: RuntimeEventBus, providers: AgentProvider[]);
17
+ refreshMcpServers(): Promise<void>;
17
18
  providerStatuses(): Promise<AgentProviderStatus[]>;
18
19
  providerDetails(providerId: AgentProviderId): Promise<AgentProviderDetails>;
19
20
  models(providerId: AgentProviderId): Promise<AgentModel[]>;
@@ -33,6 +34,7 @@ export declare class AgentRuntime {
33
34
  interrupt(sessionId: string): Promise<void>;
34
35
  respondInteraction(sessionId: string, itemId: string, response: AgentInteractionResponse): Promise<AgentItem>;
35
36
  deleteSession(sessionId: string): Promise<void>;
37
+ deleteSessions(sessionIds: readonly string[]): Promise<void>;
36
38
  private deleteSessionUnlocked;
37
39
  switchProvider(sessionId: string, targetProvider: AgentProviderId, patch?: Partial<AgentSessionConfig>): Promise<AgentSession>;
38
40
  private switchProviderUnlocked;
@@ -30,6 +30,12 @@ export class AgentRuntime {
30
30
  }
31
31
  this.store.reconcileInterrupted(new Date().toISOString());
32
32
  }
33
+ async refreshMcpServers() {
34
+ const results = await Promise.allSettled([...this.providers.values()].map((provider) => provider.refreshMcpServers?.()));
35
+ const failures = results.filter((result) => result.status === "rejected");
36
+ if (failures.length)
37
+ throw new AggregateError(failures.map((failure) => failure.reason), "Failed to refresh MCP servers for one or more Agent providers");
38
+ }
33
39
  async providerStatuses() {
34
40
  return Promise.all([...this.providers.values()].map(async (provider) => ({
35
41
  id: provider.id,
@@ -67,8 +73,8 @@ export class AgentRuntime {
67
73
  return update;
68
74
  }
69
75
  async createSession(input) {
70
- const workspace = await this.workspaces.info(input.workspaceRoot);
71
- const workspaceRoot = workspace.root;
76
+ const workspace = await this.workspaces.info(input.workspaceId);
77
+ const workspaceRoot = workspace.path;
72
78
  const provider = this.requireProvider(input.provider);
73
79
  const providerStatus = await provider.probe();
74
80
  if (!providerStatus.installed)
@@ -87,7 +93,8 @@ export class AgentRuntime {
87
93
  const now = new Date().toISOString();
88
94
  const session = {
89
95
  id: "agent_" + randomUUID(),
90
- workspaceRoot,
96
+ workspaceId: workspace.id,
97
+ workspaceName: workspace.name,
91
98
  provider: input.provider,
92
99
  ...config,
93
100
  tokenUsage: null,
@@ -122,7 +129,8 @@ export class AgentRuntime {
122
129
  const text = input.text.trim();
123
130
  if (!text && input.attachments.length === 0)
124
131
  throw new ChatRoomError("INVALID_INPUT", "Agent message is empty");
125
- const attachments = await validateAttachments(session, input.attachments);
132
+ const workspaceRoot = await this.workspaces.resolve(session.workspaceId);
133
+ const attachments = await validateAttachments(session, workspaceRoot, input.attachments);
126
134
  await this.validateInputModalities(session, text, attachments);
127
135
  const providerSession = await this.providerSession(session);
128
136
  const active = this.beginTurn(session, providerSession, source, session.serviceTier, { text, attachments });
@@ -160,7 +168,8 @@ export class AgentRuntime {
160
168
  const text = input.text.trim();
161
169
  if (!text && input.attachments.length === 0)
162
170
  throw new ChatRoomError("INVALID_INPUT", "Agent message is empty");
163
- const attachments = await validateAttachments(session, input.attachments);
171
+ const workspaceRoot = await this.workspaces.resolve(session.workspaceId);
172
+ const attachments = await validateAttachments(session, workspaceRoot, input.attachments);
164
173
  await this.validateInputModalities(session, text, attachments);
165
174
  if (this.activeTurns.get(sessionId) !== active)
166
175
  throw new ChatRoomError("CONFLICT", "Agent turn ended before steering");
@@ -412,6 +421,34 @@ export class AgentRuntime {
412
421
  async deleteSession(sessionId) {
413
422
  return this.withSessionMutation(sessionId, () => this.deleteSessionUnlocked(sessionId));
414
423
  }
424
+ async deleteSessions(sessionIds) {
425
+ const ids = [...new Set(sessionIds)];
426
+ if (ids.length === 0)
427
+ throw new ChatRoomError("INVALID_INPUT", "Select at least one Agent session");
428
+ if (ids.length > 100)
429
+ throw new ChatRoomError("INVALID_INPUT", "Cannot delete more than 100 Agent sessions at once");
430
+ // Validate every target before changing anything, so active/unknown sessions
431
+ // cannot cause an avoidable partial batch deletion.
432
+ for (const id of ids) {
433
+ this.requireSession(id);
434
+ if (this.sessionMutations.has(id))
435
+ throw new ChatRoomError("CONFLICT", "An Agent session is already being modified");
436
+ if (this.activeTurns.has(id))
437
+ throw new ChatRoomError("CONFLICT", "Cannot delete a running Agent session");
438
+ }
439
+ // Acquire all locks synchronously before the first await, preventing a turn
440
+ // or another mutation from starting for any member of this batch.
441
+ for (const id of ids)
442
+ this.sessionMutations.add(id);
443
+ try {
444
+ for (const id of ids)
445
+ await this.deleteSessionUnlocked(id);
446
+ }
447
+ finally {
448
+ for (const id of ids)
449
+ this.sessionMutations.delete(id);
450
+ }
451
+ }
415
452
  async deleteSessionUnlocked(sessionId) {
416
453
  if (this.activeTurns.has(sessionId))
417
454
  throw new ChatRoomError("CONFLICT", "Cannot delete a running Agent session");
@@ -421,7 +458,9 @@ export class AgentRuntime {
421
458
  await providerSession.close().catch(() => undefined);
422
459
  this.providerSessions.delete(sessionId);
423
460
  }
424
- await cleanupSessionFiles(this.store, history);
461
+ await cleanupSessionFiles(this.store, history, await this.workspaces
462
+ .resolve(history.session.workspaceId)
463
+ .catch(() => null));
425
464
  this.store.deleteSession(sessionId);
426
465
  this.invalidateSession(sessionId, { deleted: true });
427
466
  }
@@ -434,8 +473,8 @@ export class AgentRuntime {
434
473
  throw new ChatRoomError("CONFLICT", "Wait for the current turn to finish before switching agents");
435
474
  if (session.provider === targetProvider)
436
475
  return this.configureSessionUnlocked(sessionId, patch);
437
- const workspace = await this.workspaces.info(session.workspaceRoot);
438
- const workspaceRoot = workspace.root;
476
+ const workspace = await this.workspaces.info(session.workspaceId);
477
+ const workspaceRoot = workspace.path;
439
478
  const provider = this.requireProvider(targetProvider);
440
479
  const providerStatus = await provider.probe();
441
480
  if (!providerStatus.installed)
@@ -569,7 +608,7 @@ export class AgentRuntime {
569
608
  }
570
609
  if (event.type === "item.updated") {
571
610
  const existing = active.items.get(event.providerItemId);
572
- const payload = await prepareProviderItem(session, event.item);
611
+ const payload = await prepareProviderItem(await this.workspaces.resolve(session.workspaceId), event.item);
573
612
  const updated = materializeProviderItem(active.turn.id, event.providerItemId, existing?.sequence ?? active.nextSequence++, existing?.status ?? "running", payload, existing);
574
613
  active.items.set(event.providerItemId, updated);
575
614
  const bounded = this.boundCommandOutput(active, event.providerItemId, updated);
@@ -624,7 +663,7 @@ export class AgentRuntime {
624
663
  }
625
664
  if (event.type === "item.completed") {
626
665
  const existing = active.items.get(event.providerItemId);
627
- const providerItem = await prepareProviderItem(session, event.item);
666
+ const providerItem = await prepareProviderItem(await this.workspaces.resolve(session.workspaceId), event.item);
628
667
  const completed = materializeProviderItem(active.turn.id, event.providerItemId, existing?.sequence ?? active.nextSequence++, event.failed ? "failed" : "completed", providerItem, existing);
629
668
  active.items.set(event.providerItemId, completed);
630
669
  const bounded = this.boundCommandOutput(active, event.providerItemId, completed);
@@ -713,8 +752,8 @@ export class AgentRuntime {
713
752
  });
714
753
  }
715
754
  async providerSession(session) {
716
- const workspace = await this.workspaces.info(session.workspaceRoot);
717
- const workspaceRoot = workspace.root;
755
+ const workspace = await this.workspaces.info(session.workspaceId);
756
+ const workspaceRoot = workspace.path;
718
757
  const existing = this.providerSessions.get(session.id);
719
758
  if (existing)
720
759
  return existing;
@@ -767,7 +806,7 @@ export class AgentRuntime {
767
806
  return session;
768
807
  }
769
808
  async cleanupOrphanAttachments(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
770
- await cleanupOrphanAttachmentFiles(this.store, maxAgeMs);
809
+ await cleanupOrphanAttachmentFiles(this.store, (workspaceId) => this.workspaces.resolve(workspaceId), maxAgeMs);
771
810
  }
772
811
  async withSessionMutation(sessionId, action) {
773
812
  if (this.sessionMutations.has(sessionId))
@@ -155,7 +155,8 @@ export interface AgentSessionAttention {
155
155
  }
156
156
  export interface AgentSession extends AgentSessionConfig {
157
157
  id: string;
158
- workspaceRoot: string;
158
+ workspaceId: string;
159
+ workspaceName: string;
159
160
  provider: AgentProviderId;
160
161
  status: AgentSessionStatus;
161
162
  attention: AgentSessionAttention | null;
@@ -314,7 +315,7 @@ export interface AgentTurnInput {
314
315
  attachments: AgentAttachment[];
315
316
  }
316
317
  export interface CreateAgentSessionInput extends Partial<AgentSessionConfig> {
317
- workspaceRoot: string;
318
+ workspaceId: string;
318
319
  provider: AgentProviderId;
319
320
  model: string | null;
320
321
  }
@@ -199,18 +199,13 @@ export class V2TunnelClient {
199
199
  const sender = new CreditSender(frame.id, (buffer) => this.send(buffer), () => response.pause(), () => response.resume(), () => {
200
200
  state.responseEnded = true;
201
201
  this.finishIfIdle(frame.id);
202
- });
202
+ }, () => (this.socket?.bufferedAmount ?? 0) < 1024 * 1024);
203
203
  state.sender = sender;
204
204
  this.send(metaFrame(2 /* Op.META */, frame.id, {
205
205
  status: response.statusCode ?? 502,
206
206
  headers: responseHeaders(response.headers),
207
207
  }));
208
208
  response.on("data", (chunk) => {
209
- state.responseBytes += chunk.length;
210
- if (state.responseBytes > 128 * 1024 * 1024) {
211
- this.abort(frame.id);
212
- return;
213
- }
214
209
  try {
215
210
  sender.enqueue(chunk);
216
211
  }
@@ -232,7 +227,6 @@ export class V2TunnelClient {
232
227
  pendingCredit: 0,
233
228
  drainArmed: false,
234
229
  requestBytes: 0,
235
- responseBytes: 0,
236
230
  requestEnded: false,
237
231
  responseEnded: false,
238
232
  };
@@ -6,6 +6,11 @@ export declare class PluginManager {
6
6
  private readonly active;
7
7
  constructor(context: PluginContext, plugins: InternalPlugin[]);
8
8
  start(): Promise<void>;
9
- registerMcp(server: McpServer): void;
9
+ defineMcp(toolControl: import("#mcp/server/tool-control").McpToolControl, options?: {
10
+ agentMode?: boolean;
11
+ }): void;
12
+ registerMcp(server: McpServer, toolControl?: import("#mcp/server/tool-control").McpToolControl, options?: {
13
+ agentMode?: boolean;
14
+ }): void;
10
15
  stop(): Promise<void>;
11
16
  }
@@ -36,11 +36,18 @@ export class PluginManager {
36
36
  throw error;
37
37
  }
38
38
  }
39
- registerMcp(server) {
39
+ defineMcp(toolControl, options = {}) {
40
40
  for (const plugin of this.active) {
41
41
  if (!plugin.registerMcp)
42
42
  continue;
43
- plugin.registerMcp(new PluginMcpRegistrar(server, this.context.operations, plugin.id, this.context.mcpTools));
43
+ plugin.registerMcp(new PluginMcpRegistrar(null, this.context.operations, plugin.id, toolControl, options));
44
+ }
45
+ }
46
+ registerMcp(server, toolControl = this.context.mcpTools, options = {}) {
47
+ for (const plugin of this.active) {
48
+ if (!plugin.registerMcp)
49
+ continue;
50
+ plugin.registerMcp(new PluginMcpRegistrar(server, this.context.operations, plugin.id, toolControl, options));
44
51
  }
45
52
  }
46
53
  async stop() {
@@ -7,6 +7,7 @@ import type { CustomMcpManager } from "#mcp/custom/manager";
7
7
  import type { RuntimeEventBus } from "#app/event-bus";
8
8
  import type { ExternalAccessRegistry } from "#app/external-access-registry";
9
9
  import type { LogWriter } from "#core/logging/types";
10
+ import type { AgentMcpConnection } from "#plugins/agent/mcp-gateway";
10
11
  import type { ServiceRegistry } from "#app/service-registry";
11
12
  export interface PluginContext {
12
13
  config: ChatRoomConfig;
@@ -14,6 +15,9 @@ export interface PluginContext {
14
15
  operations: OperationLog;
15
16
  mcpTools: McpToolControl;
16
17
  customMcp: CustomMcpManager;
18
+ agentMcp: AgentMcpConnection;
19
+ agentMcpTools: import("#mcp/server/tool-control").McpToolControl;
20
+ agentCustomMcp: CustomMcpManager;
17
21
  events: RuntimeEventBus;
18
22
  externalAccess: ExternalAccessRegistry;
19
23
  services: ServiceRegistry;
@@ -19,7 +19,7 @@ export function createAgentApiRouter(agents) {
19
19
  router.post("/agents/sessions", asyncRoute(async (req, res) => {
20
20
  const body = bodyRecord(req.body);
21
21
  res.json(await agents.createSession({
22
- workspaceRoot: requireString(body.workspaceRoot, "workspaceRoot"),
22
+ workspaceId: requireString(body.workspaceId, "workspaceId"),
23
23
  provider: requireProvider(requireString(body.provider, "provider")),
24
24
  model: null,
25
25
  reasoningEffort: null,
@@ -76,6 +76,20 @@ export function createAgentApiRouter(agents) {
76
76
  const body = bodyRecord(req.body);
77
77
  res.json(await agents.switchProvider(requireString(req.params.sessionId, "sessionId"), requireProvider(requireString(body.provider, "provider")), parseConfig(body)));
78
78
  }));
79
+ const bulkDeleteSessions = asyncRoute(async (req, res) => {
80
+ const body = bodyRecord(req.body);
81
+ if (!Array.isArray(body.sessionIds) || body.sessionIds.length === 0)
82
+ throw new ChatRoomError("INVALID_INPUT", "sessionIds must be a non-empty array");
83
+ if (body.sessionIds.length > 100)
84
+ throw new ChatRoomError("INVALID_INPUT", "Cannot delete more than 100 Agent sessions at once");
85
+ const sessionIds = body.sessionIds.map((id) => requireString(id, "sessionIds[]"));
86
+ await agents.deleteSessions(sessionIds);
87
+ res.status(204).end();
88
+ });
89
+ // POST is the primary batch action for maximum compatibility with clients
90
+ // and intermediaries that mishandle bodies on DELETE requests.
91
+ router.post("/agents/sessions/bulk-delete", bulkDeleteSessions);
92
+ router.delete("/agents/sessions", bulkDeleteSessions);
79
93
  router.delete("/agents/sessions/:sessionId", asyncRoute(async (req, res) => {
80
94
  await agents.deleteSession(requireString(req.params.sessionId, "sessionId"));
81
95
  res.status(204).end();
@@ -3,77 +3,77 @@ import { asyncRoute, bodyRecord, requireString, requireStringArray, } from "#pre
3
3
  export function createGitApiRouter(application) {
4
4
  const router = Router();
5
5
  router.get("/git/status", asyncRoute(async (req, res) => {
6
- const root = await resolveRoot(application, req.query.root);
6
+ const root = await resolveRoot(application, req.query.workspaceId);
7
7
  res.json(await application.git.status(root));
8
8
  }));
9
9
  router.get("/git/diff", asyncRoute(async (req, res) => {
10
- const root = await resolveRoot(application, req.query.root);
10
+ const root = await resolveRoot(application, req.query.workspaceId);
11
11
  const filePath = typeof req.query.path === "string" ? req.query.path : undefined;
12
12
  res.json(await application.git.diff(root, filePath));
13
13
  }));
14
14
  router.get("/git/branches", asyncRoute(async (req, res) => {
15
- const root = await resolveRoot(application, req.query.root);
15
+ const root = await resolveRoot(application, req.query.workspaceId);
16
16
  res.json(await application.git.branches(root));
17
17
  }));
18
18
  router.get("/git/log", asyncRoute(async (req, res) => {
19
- const root = await resolveRoot(application, req.query.root);
19
+ const root = await resolveRoot(application, req.query.workspaceId);
20
20
  const requested = Number(req.query.limit ?? 30);
21
21
  const limit = Number.isFinite(requested) ? requested : 30;
22
22
  res.json(await application.git.log(root, limit));
23
23
  }));
24
24
  router.post("/git/stage", asyncRoute(async (req, res) => {
25
25
  const body = bodyRecord(req.body);
26
- const root = await resolveRoot(application, body.root);
26
+ const root = await resolveRoot(application, body.workspaceId);
27
27
  const paths = requireStringArray(body.paths, "paths");
28
28
  res.json(await mutate(application, "git.stage", root, { paths }, () => application.git.stage(root, paths)));
29
29
  }));
30
30
  router.post("/git/unstage", asyncRoute(async (req, res) => {
31
31
  const body = bodyRecord(req.body);
32
- const root = await resolveRoot(application, body.root);
32
+ const root = await resolveRoot(application, body.workspaceId);
33
33
  const paths = requireStringArray(body.paths, "paths");
34
34
  res.json(await mutate(application, "git.unstage", root, { paths }, () => application.git.unstage(root, paths)));
35
35
  }));
36
36
  router.post("/git/restore", asyncRoute(async (req, res) => {
37
37
  const body = bodyRecord(req.body);
38
- const root = await resolveRoot(application, body.root);
38
+ const root = await resolveRoot(application, body.workspaceId);
39
39
  const filePath = requireString(body.path, "path");
40
40
  res.json(await mutate(application, "git.restore", root, { path: filePath }, () => application.git.restore(root, filePath)));
41
41
  }));
42
42
  router.post("/git/commit", asyncRoute(async (req, res) => {
43
43
  const body = bodyRecord(req.body);
44
- const root = await resolveRoot(application, body.root);
44
+ const root = await resolveRoot(application, body.workspaceId);
45
45
  const message = requireString(body.message, "message");
46
46
  res.json(await mutate(application, "git.commit", root, { message }, () => application.git.commit(root, message)));
47
47
  }));
48
48
  router.post("/git/branches", asyncRoute(async (req, res) => {
49
49
  const body = bodyRecord(req.body);
50
- const root = await resolveRoot(application, body.root);
50
+ const root = await resolveRoot(application, body.workspaceId);
51
51
  const name = requireString(body.name, "name");
52
52
  res.json(await mutate(application, "git.branch.create", root, { name }, () => application.git.createBranch(root, name)));
53
53
  }));
54
54
  router.post("/git/switch", asyncRoute(async (req, res) => {
55
55
  const body = bodyRecord(req.body);
56
- const root = await resolveRoot(application, body.root);
56
+ const root = await resolveRoot(application, body.workspaceId);
57
57
  const name = requireString(body.name, "name");
58
58
  res.json(await mutate(application, "git.branch.switch", root, { name }, () => application.git.switchBranch(root, name)));
59
59
  }));
60
60
  router.delete("/git/branches", asyncRoute(async (req, res) => {
61
61
  const body = bodyRecord(req.body);
62
- const root = await resolveRoot(application, body.root);
62
+ const root = await resolveRoot(application, body.workspaceId);
63
63
  const name = requireString(body.name, "name");
64
64
  res.json(await mutate(application, "git.branch.delete", root, { name }, () => application.git.deleteBranch(root, name)));
65
65
  }));
66
66
  for (const action of ["fetch", "pull", "push"]) {
67
67
  router.post(`/git/${action}`, asyncRoute(async (req, res) => {
68
68
  const body = bodyRecord(req.body);
69
- const root = await resolveRoot(application, body.root);
69
+ const root = await resolveRoot(application, body.workspaceId);
70
70
  res.json(await mutate(application, `git.${action}`, root, {}, () => application.git[action](root)));
71
71
  }));
72
72
  }
73
73
  return router;
74
74
  }
75
75
  async function resolveRoot(application, value) {
76
- return application.workspaces.resolve(requireString(value, "root"));
76
+ return application.workspaces.resolve(requireString(value, "workspaceId"));
77
77
  }
78
78
  function mutate(application, action, root, input, operation) {
79
79
  return application.operations.run({
@@ -28,6 +28,7 @@ const HEADER_NAMES = new Set([
28
28
  "if-none-match",
29
29
  "if-modified-since",
30
30
  "x-chatroom-mime-type",
31
+ "x-chatroom-upload-size",
31
32
  ]);
32
33
  const RESPONSE_HEADERS = new Set([
33
34
  "content-type",
@@ -36,6 +37,8 @@ const RESPONSE_HEADERS = new Set([
36
37
  "content-range",
37
38
  "accept-ranges",
38
39
  "cache-control",
40
+ "content-security-policy",
41
+ "x-content-type-options",
39
42
  "etag",
40
43
  "last-modified",
41
44
  ]);
@@ -13,6 +13,28 @@ export function createRuntimeApiRouter(application, eventBus, runtimeStatus) {
13
13
  throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
14
14
  res.json(application.mcpTools.setEnabled(requireString(req.params.toolName, "toolName"), body.enabled));
15
15
  });
16
+ router.get("/agent-mcp/tools", (_req, res) => {
17
+ res.json(application.agentMcpTools.list());
18
+ });
19
+ router.patch("/agent-mcp/tools/:toolName", async (req, res) => {
20
+ const body = bodyRecord(req.body);
21
+ if (typeof body.enabled !== "boolean")
22
+ throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
23
+ const updated = application.agentMcpTools.setEnabled(requireString(req.params.toolName, "toolName"), body.enabled);
24
+ await application.agents.refreshMcpServers();
25
+ res.json(updated);
26
+ });
27
+ router.get("/agent-mcp/servers", (_req, res) => {
28
+ res.json(application.agentCustomMcp.list());
29
+ });
30
+ router.patch("/agent-mcp/servers/:id", async (req, res) => {
31
+ const body = bodyRecord(req.body);
32
+ if (typeof body.enabled !== "boolean")
33
+ throw new ChatRoomError("INVALID_INPUT", "enabled must be a boolean");
34
+ const updated = await application.agentCustomMcp.update(requireString(req.params.id, "id"), { enabled: body.enabled });
35
+ await application.agents.refreshMcpServers();
36
+ res.json(updated);
37
+ });
16
38
  router.get("/events", (_req, res) => {
17
39
  res.status(200);
18
40
  res.setHeader("Content-Type", "text/event-stream");