@ouro.bot/cli 0.1.0-alpha.803 → 0.1.0-alpha.805

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.McpManager = void 0;
4
4
  exports.getSharedMcpManager = getSharedMcpManager;
5
+ exports.releaseRuntimeMcpServers = releaseRuntimeMcpServers;
5
6
  exports.shutdownSharedMcpManager = shutdownSharedMcpManager;
6
7
  exports.resetSharedMcpManager = resetSharedMcpManager;
7
8
  const mcp_client_1 = require("./mcp-client");
@@ -57,10 +58,20 @@ function buildMergedServerConfig(runtimeServers) {
57
58
  }
58
59
  return { mergedServers, pluginOrigins };
59
60
  }
61
+ function sameMcpServerConfig(left, right) {
62
+ const leftEnv = Object.entries(left.env ?? {}).sort();
63
+ const rightEnv = Object.entries(right.env ?? {}).sort();
64
+ return left.command === right.command
65
+ && JSON.stringify(left.args ?? []) === JSON.stringify(right.args ?? [])
66
+ && JSON.stringify(leftEnv) === JSON.stringify(rightEnv)
67
+ && (left.cwd ?? "") === (right.cwd ?? "");
68
+ }
60
69
  class McpManager {
61
70
  servers = new Map();
71
+ desiredGenerations = new Map();
72
+ nextGeneration = 0;
62
73
  shuttingDown = false;
63
- async start(servers, pluginOrigins = {}) {
74
+ async start(servers, pluginOrigins = {}, ownerAgent) {
64
75
  (0, runtime_1.emitNervesEvent)({
65
76
  event: "mcp.manager_start",
66
77
  component: "repertoire",
@@ -72,7 +83,7 @@ class McpManager {
72
83
  });
73
84
  const entries = Object.entries(servers);
74
85
  for (const [name, config] of entries) {
75
- await this.connectServer(name, config, pluginOrigins[name]);
86
+ await this.connectServer(name, config, pluginOrigins[name], ownerAgent);
76
87
  }
77
88
  }
78
89
  listAllTools() {
@@ -150,18 +161,34 @@ class McpManager {
150
161
  async reconcile(runtimeServers) {
151
162
  try {
152
163
  const { mergedServers, pluginOrigins } = buildMergedServerConfig(runtimeServers);
164
+ const ownerAgent = (0, identity_1.getAgentName)();
153
165
  const currentNames = new Set(this.servers.keys());
154
166
  const desiredNames = new Set(Object.keys(mergedServers));
155
167
  // Connect new servers
156
168
  for (const [name, cfg] of Object.entries(mergedServers)) {
157
- if (!currentNames.has(name)) {
169
+ const current = this.servers.get(name);
170
+ const pluginId = pluginOrigins[name];
171
+ if (!current) {
158
172
  (0, runtime_1.emitNervesEvent)({
159
173
  event: "mcp.server_added",
160
174
  component: "repertoire",
161
175
  message: `connecting new MCP server: ${name}`,
162
176
  meta: { server: name, command: cfg.command },
163
177
  });
164
- await this.connectServer(name, cfg, pluginOrigins[name]);
178
+ await this.connectServer(name, cfg, pluginId, ownerAgent);
179
+ }
180
+ else if (!sameMcpServerConfig(current.config, cfg)
181
+ || current.pluginId !== pluginId
182
+ || current.ownerAgent !== ownerAgent) {
183
+ (0, runtime_1.emitNervesEvent)({
184
+ event: "mcp.server_changed",
185
+ component: "repertoire",
186
+ message: `reconnecting changed MCP server: ${name}`,
187
+ meta: { server: name, command: cfg.command },
188
+ });
189
+ current.client.shutdown();
190
+ this.servers.delete(name);
191
+ await this.connectServer(name, cfg, pluginId, ownerAgent);
165
192
  }
166
193
  }
167
194
  // Disconnect removed servers
@@ -178,8 +205,10 @@ class McpManager {
178
205
  if (entry)
179
206
  entry.client.shutdown();
180
207
  this.servers.delete(name);
208
+ this.desiredGenerations.delete(name);
181
209
  }
182
210
  }
211
+ return true;
183
212
  }
184
213
  catch (error) {
185
214
  (0, runtime_1.emitNervesEvent)({
@@ -189,6 +218,8 @@ class McpManager {
189
218
  message: "failed to reconcile MCP servers",
190
219
  meta: { reason: error instanceof Error ? error.message : String(error) },
191
220
  });
221
+ this.shutdown();
222
+ return false;
192
223
  }
193
224
  }
194
225
  shutdown() {
@@ -205,12 +236,13 @@ class McpManager {
205
236
  entry.client.shutdown();
206
237
  }
207
238
  this.servers.clear();
239
+ this.desiredGenerations.clear();
208
240
  }
209
241
  /**
210
242
  * Resolve `vault:DOMAIN/FIELD` references in server env config.
211
243
  * Returns resolved env or throws with a descriptive error.
212
244
  */
213
- async resolveVaultEnv(_serverName, env) {
245
+ async resolveVaultEnv(_serverName, env, ownerAgent) {
214
246
  const resolved = { ...env };
215
247
  // Short-circuit: only spin up a credential store if at least one env value
216
248
  // actually requests vault resolution. Plugin MCP servers commonly ship with
@@ -219,7 +251,7 @@ class McpManager {
219
251
  const hasVaultRef = Object.values(resolved).some((v) => /^vault:/.test(v));
220
252
  if (!hasVaultRef)
221
253
  return resolved;
222
- const store = (0, credential_access_1.getCredentialStore)();
254
+ const store = (0, credential_access_1.getCredentialStore)(ownerAgent);
223
255
  for (const [key, value] of Object.entries(resolved)) {
224
256
  const match = value.match(/^vault:([^/]+)\/(.+)$/);
225
257
  if (!match)
@@ -244,12 +276,14 @@ class McpManager {
244
276
  }
245
277
  return resolved;
246
278
  }
247
- async connectServer(name, config, pluginId) {
279
+ async connectServer(name, config, pluginId, ownerAgent) {
280
+ const generation = ++this.nextGeneration;
281
+ this.desiredGenerations.set(name, generation);
248
282
  // Resolve vault: references in env before spawning
249
283
  let resolvedConfig = config;
250
284
  if (config.env) {
251
285
  try {
252
- const resolvedEnv = await this.resolveVaultEnv(name, config.env);
286
+ const resolvedEnv = await this.resolveVaultEnv(name, config.env, ownerAgent);
253
287
  resolvedConfig = { ...config, env: resolvedEnv };
254
288
  }
255
289
  catch (err) {
@@ -262,9 +296,14 @@ class McpManager {
262
296
  message: `skipping MCP server "${name}": ${reason}`,
263
297
  meta: { server: name, reason },
264
298
  });
299
+ if (this.desiredGenerations.get(name) === generation) {
300
+ this.desiredGenerations.delete(name);
301
+ }
265
302
  return; // Skip this server, continue to next
266
303
  }
267
304
  }
305
+ if (this.shuttingDown || this.desiredGenerations.get(name) !== generation)
306
+ return;
268
307
  const client = new mcp_client_1.McpClient(resolvedConfig);
269
308
  const entry = {
270
309
  name,
@@ -273,16 +312,26 @@ class McpManager {
273
312
  cachedTools: [],
274
313
  consecutiveFailures: 0,
275
314
  pluginId,
315
+ ownerAgent,
316
+ generation,
276
317
  };
277
318
  this.servers.set(name, entry);
278
319
  client.onClose(() => {
279
320
  if (this.shuttingDown)
280
321
  return;
281
- this.handleServerCrash(name);
322
+ if (this.servers.get(name)?.client !== client)
323
+ return;
324
+ this.handleServerCrash(name, client);
282
325
  });
283
326
  try {
284
327
  await client.connect();
285
328
  const tools = await client.listTools();
329
+ if (this.shuttingDown
330
+ || this.desiredGenerations.get(name) !== generation
331
+ || this.servers.get(name)?.client !== client) {
332
+ client.shutdown();
333
+ return;
334
+ }
286
335
  entry.cachedTools = tools;
287
336
  entry.consecutiveFailures = 0;
288
337
  }
@@ -302,10 +351,10 @@ class McpManager {
302
351
  });
303
352
  }
304
353
  }
305
- handleServerCrash(name) {
354
+ handleServerCrash(name, crashedClient) {
306
355
  const entry = this.servers.get(name);
307
- /* v8 ignore next -- defensive: entry removed between close event and handler @preserve */
308
- if (!entry)
356
+ /* v8 ignore next -- defensive: entry removed or replaced between close event and handler @preserve */
357
+ if (!entry || entry.client !== crashedClient)
309
358
  return;
310
359
  entry.consecutiveFailures++;
311
360
  if (entry.consecutiveFailures > MAX_RESTART_RETRIES) {
@@ -329,21 +378,26 @@ class McpManager {
329
378
  setTimeout(() => {
330
379
  if (this.shuttingDown)
331
380
  return;
332
- this.restartServer(name).catch(() => {
381
+ if (this.servers.get(name)?.client !== crashedClient)
382
+ return;
383
+ this.restartServer(name, crashedClient).catch(() => {
333
384
  // Error handling is inside restartServer
334
385
  });
335
386
  }, RESTART_DELAY_MS);
336
387
  /* v8 ignore stop */
337
388
  }
338
389
  /* v8 ignore start -- called from timer callback: covered by mcp-manager.test.ts via fake timers but v8 can't trace @preserve */
339
- async restartServer(name) {
390
+ async restartServer(name, expectedClient) {
340
391
  const entry = this.servers.get(name);
341
392
  if (!entry)
342
393
  return;
394
+ if (expectedClient && entry.client !== expectedClient)
395
+ return;
343
396
  // Remove old entry and reconnect
344
397
  this.servers.delete(name);
398
+ this.desiredGenerations.delete(name);
345
399
  entry.client.shutdown();
346
- await this.connectServer(name, entry.config);
400
+ await this.connectServer(name, entry.config, entry.pluginId, entry.ownerAgent);
347
401
  // Preserve failure count
348
402
  const newEntry = this.servers.get(name);
349
403
  if (newEntry) {
@@ -383,7 +437,11 @@ async function getSharedMcpManager(options) {
383
437
  // AND this turn's runtime overrides. Passing runtimeServers per-call is what
384
438
  // scopes the runtime MCP to the active agent's turn.
385
439
  if (_sharedManager) {
386
- await _sharedManager.reconcile(runtimeServers);
440
+ const reconciled = await _sharedManager.reconcile(runtimeServers);
441
+ if (!reconciled) {
442
+ _sharedManager = null;
443
+ return null;
444
+ }
387
445
  return _sharedManager;
388
446
  }
389
447
  /* v8 ignore next -- race guard: deduplicates concurrent initialization calls @preserve */
@@ -396,7 +454,7 @@ async function getSharedMcpManager(options) {
396
454
  if (Object.keys(mergedServers).length === 0)
397
455
  return null;
398
456
  const manager = new McpManager();
399
- await manager.start(mergedServers, pluginOrigins);
457
+ await manager.start(mergedServers, pluginOrigins, (0, identity_1.getAgentName)());
400
458
  _sharedManager = manager;
401
459
  return manager;
402
460
  }
@@ -417,6 +475,13 @@ async function getSharedMcpManager(options) {
417
475
  })();
418
476
  return _sharedManagerPromise;
419
477
  }
478
+ async function releaseRuntimeMcpServers() {
479
+ if (!_sharedManager)
480
+ return;
481
+ if (!await _sharedManager.reconcile()) {
482
+ _sharedManager = null;
483
+ }
484
+ }
420
485
  /**
421
486
  * Shut down the shared MCP manager and clear the singleton.
422
487
  * Called during daemon/agent shutdown.
@@ -0,0 +1,386 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createAcpServer = createAcpServer;
37
+ const node_crypto_1 = require("node:crypto");
38
+ const readline = __importStar(require("node:readline"));
39
+ const frontend_socket_client_1 = require("../heart/frontend-socket-client");
40
+ const runtime_1 = require("../nerves/runtime");
41
+ const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
42
+ class AcpProtocolError extends Error {
43
+ code;
44
+ constructor(code, message) {
45
+ super(message);
46
+ this.code = code;
47
+ this.name = "AcpProtocolError";
48
+ }
49
+ }
50
+ function createAcpServer(options) {
51
+ const sessions = new Set();
52
+ const activePrompts = new Map();
53
+ const pendingOutbound = new Map();
54
+ const createClient = options.createFrontendClient ?? (async () => new frontend_socket_client_1.SocketFrontendClient(options.frontendSocketPath));
55
+ let clientPromise = null;
56
+ let frontendClient = null;
57
+ let removeEventListener = null;
58
+ let removeCloseListener = null;
59
+ let lines = null;
60
+ let running = false;
61
+ function write(message) {
62
+ options.stdout.write(`${JSON.stringify(message)}\n`);
63
+ }
64
+ function result(id, value) {
65
+ write({ jsonrpc: "2.0", id, result: value });
66
+ }
67
+ function error(id, code, message) {
68
+ write({ jsonrpc: "2.0", id, error: { code, message } });
69
+ }
70
+ async function client() {
71
+ if (!clientPromise) {
72
+ clientPromise = createClient().then((value) => {
73
+ frontendClient = value;
74
+ removeEventListener = value.onEvent(handleFrontendEvent);
75
+ removeCloseListener = value.onClose((error) => {
76
+ const failure = error ?? new Error("frontend provider disconnected");
77
+ for (const prompt of activePrompts.values())
78
+ prompt.reject(failure);
79
+ pendingOutbound.clear();
80
+ });
81
+ return value;
82
+ });
83
+ }
84
+ return clientPromise;
85
+ }
86
+ async function subscribe(sessionId) {
87
+ await (await client()).request("session.subscribe", { sessionKey: sessionId });
88
+ sessions.add(sessionId);
89
+ }
90
+ function sessionUpdate(sessionId, update) {
91
+ write({ jsonrpc: "2.0", method: "session/update", params: { sessionId, update } });
92
+ }
93
+ function permissionRequest(sessionId, event) {
94
+ const requestId = typeof event.requestId === "string" ? event.requestId : "";
95
+ if (!requestId || [...pendingOutbound.values()].some((pending) => pending.requestId === requestId))
96
+ return;
97
+ const rpcId = `ouro:${(0, node_crypto_1.randomUUID)()}`;
98
+ pendingOutbound.set(rpcId, { sessionId, requestId });
99
+ write({
100
+ jsonrpc: "2.0",
101
+ id: rpcId,
102
+ method: "session/request_permission",
103
+ params: {
104
+ sessionId,
105
+ toolCall: {
106
+ toolCallId: String(event.toolCallId ?? requestId),
107
+ title: String(event.title ?? "Permission requested"),
108
+ },
109
+ options: Array.isArray(event.options) ? event.options : [],
110
+ },
111
+ });
112
+ }
113
+ function replayEvent(sessionId, event) {
114
+ const data = event.data ?? {};
115
+ if (event.type === "user_message" && typeof data.text === "string") {
116
+ sessionUpdate(sessionId, {
117
+ sessionUpdate: "user_message_chunk",
118
+ messageId: `${event.turnId}:user`,
119
+ content: { type: "text", text: data.text },
120
+ });
121
+ }
122
+ else if (event.type === "assistant_delivery" && typeof data.text === "string") {
123
+ sessionUpdate(sessionId, {
124
+ sessionUpdate: "agent_message_chunk",
125
+ messageId: `${event.turnId}:agent`,
126
+ content: { type: "text", text: data.text },
127
+ });
128
+ }
129
+ else if (event.type === "tool_started") {
130
+ sessionUpdate(sessionId, {
131
+ sessionUpdate: "tool_call",
132
+ toolCallId: `${event.turnId}:${String(data.name ?? "tool")}`,
133
+ title: String(data.name ?? "tool"),
134
+ status: "pending",
135
+ rawInput: data.args ?? {},
136
+ });
137
+ }
138
+ else if (event.type === "tool_completed") {
139
+ sessionUpdate(sessionId, {
140
+ sessionUpdate: "tool_call_update",
141
+ toolCallId: `${event.turnId}:${String(data.name ?? "tool")}`,
142
+ status: data.success === false ? "failed" : "completed",
143
+ content: [{ type: "content", content: { type: "text", text: String(data.summary ?? "") } }],
144
+ });
145
+ }
146
+ else if (event.type === "structured_output") {
147
+ const output = data.output;
148
+ sessionUpdate(sessionId, {
149
+ sessionUpdate: "plan",
150
+ entries: (output?.items ?? []).flatMap((item) => typeof item.text === "string" && item.text.trim()
151
+ ? [{ content: item.text.trim(), status: "pending" }]
152
+ : []),
153
+ });
154
+ }
155
+ }
156
+ function handleFrontendEvent(event) {
157
+ const sessionId = typeof event.sessionKey === "string" ? event.sessionKey : "";
158
+ if (!sessions.has(sessionId))
159
+ return;
160
+ const turnId = typeof event.turnId === "string" ? event.turnId : "";
161
+ const active = activePrompts.get(sessionId);
162
+ if (event.event === "text.delta" && typeof event.text === "string") {
163
+ sessionUpdate(sessionId, {
164
+ sessionUpdate: "agent_message_chunk",
165
+ messageId: `${turnId}:agent`,
166
+ content: { type: "text", text: event.text },
167
+ });
168
+ }
169
+ else if (event.event === "reasoning.delta" && typeof event.text === "string") {
170
+ sessionUpdate(sessionId, {
171
+ sessionUpdate: "agent_thought_chunk",
172
+ messageId: `${turnId}:thought`,
173
+ content: { type: "text", text: event.text },
174
+ });
175
+ }
176
+ else if (event.event === "tool.started") {
177
+ replayEvent(sessionId, { type: "tool_started", turnId, data: event });
178
+ }
179
+ else if (event.event === "tool.completed") {
180
+ replayEvent(sessionId, { type: "tool_completed", turnId, data: event });
181
+ }
182
+ else if (event.event === "structured.output") {
183
+ replayEvent(sessionId, { type: "structured_output", turnId, data: event });
184
+ }
185
+ else if (event.event === "permission.requested") {
186
+ const requestId = typeof event.requestId === "string" ? event.requestId : "";
187
+ if (!requestId || !active || active.turnId !== turnId)
188
+ return;
189
+ permissionRequest(sessionId, event);
190
+ }
191
+ else if (active && active.turnId === turnId && event.event === "turn.completed") {
192
+ active.resolve("end_turn");
193
+ }
194
+ else if (active && active.turnId === turnId && event.event === "turn.cancelled") {
195
+ active.resolve("cancelled");
196
+ }
197
+ else if (active && active.turnId === turnId && event.event === "turn.failed") {
198
+ active.reject(new Error(String(event.error ?? "frontend turn failed")));
199
+ }
200
+ }
201
+ function params(value) {
202
+ if (!value || typeof value !== "object" || Array.isArray(value))
203
+ throw new AcpProtocolError(-32602, "params must be an object");
204
+ return value;
205
+ }
206
+ function requiredString(value, field) {
207
+ if (typeof value !== "string" || !value.trim())
208
+ throw new AcpProtocolError(-32602, `${field} must be a non-empty string`);
209
+ return value.trim();
210
+ }
211
+ function promptText(value) {
212
+ if (!Array.isArray(value))
213
+ throw new AcpProtocolError(-32602, "prompt must be an array");
214
+ const text = value
215
+ .flatMap((part) => part?.type === "text" && typeof part.text === "string" ? [part.text] : [])
216
+ .join("");
217
+ if (!text.trim())
218
+ throw new AcpProtocolError(-32602, "prompt must contain text");
219
+ return text;
220
+ }
221
+ async function handleResponse(response) {
222
+ if (response.jsonrpc !== "2.0")
223
+ throw new AcpProtocolError(-32600, "jsonrpc must be 2.0");
224
+ if (typeof response.id !== "string")
225
+ throw new AcpProtocolError(-32602, "response id must be a string");
226
+ const pending = pendingOutbound.get(response.id);
227
+ if (!pending)
228
+ throw new AcpProtocolError(-32602, `unknown response id: ${response.id}`);
229
+ pendingOutbound.delete(response.id);
230
+ const outcome = response.result?.outcome;
231
+ const optionId = outcome?.outcome === "selected" && typeof outcome.optionId === "string"
232
+ ? outcome.optionId
233
+ : "cancelled";
234
+ await (await client()).request("permission.resolve", { requestId: pending.requestId, optionId });
235
+ }
236
+ async function handle(request) {
237
+ if (request.jsonrpc !== "2.0")
238
+ throw new AcpProtocolError(-32600, "jsonrpc must be 2.0");
239
+ const method = requiredString(request.method, "method");
240
+ const requestParams = params(request.params ?? {});
241
+ const id = request.id;
242
+ if (method === "initialize") {
243
+ if (id === undefined)
244
+ return;
245
+ if (requestParams.protocolVersion !== 1)
246
+ throw new AcpProtocolError(-32602, "protocolVersion must be 1");
247
+ result(id, { protocolVersion: 1 });
248
+ return;
249
+ }
250
+ if (method === "session/new") {
251
+ if (id === undefined)
252
+ return;
253
+ const sessionId = requestParams.sessionId === undefined
254
+ ? (0, node_crypto_1.randomUUID)()
255
+ : requiredString(requestParams.sessionId, "sessionId");
256
+ if (!SESSION_ID.test(sessionId))
257
+ throw new AcpProtocolError(-32602, "sessionId must be a safe identifier");
258
+ if (sessions.has(sessionId))
259
+ throw new AcpProtocolError(-32002, `session already exists: ${sessionId}`);
260
+ await subscribe(sessionId);
261
+ result(id, { sessionId, friendId: options.friendId });
262
+ return;
263
+ }
264
+ if (method === "session/load") {
265
+ if (id === undefined)
266
+ return;
267
+ if (options.disableTools) {
268
+ throw new AcpProtocolError(-32601, "session/load is unavailable in observe-only mode");
269
+ }
270
+ const sessionId = requiredString(requestParams.sessionId, "sessionId");
271
+ const frontend = await client();
272
+ const replay = await frontend.request("session.load", {
273
+ agent: options.agent,
274
+ friendId: options.friendId,
275
+ sessionKey: sessionId,
276
+ afterSequence: 0,
277
+ });
278
+ if (!Array.isArray(replay?.events) || replay.events.length === 0) {
279
+ throw new AcpProtocolError(-32001, "session not found");
280
+ }
281
+ for (const event of replay.events)
282
+ replayEvent(sessionId, event);
283
+ await subscribe(sessionId);
284
+ for (const pending of replay.pendingPermissions ?? [])
285
+ permissionRequest(sessionId, pending);
286
+ result(id, {});
287
+ return;
288
+ }
289
+ if (method === "session/prompt") {
290
+ if (id === undefined)
291
+ return;
292
+ const sessionId = requiredString(requestParams.sessionId, "sessionId");
293
+ if (!sessions.has(sessionId))
294
+ throw new AcpProtocolError(-32001, `unknown session: ${sessionId}`);
295
+ if (activePrompts.has(sessionId))
296
+ throw new AcpProtocolError(-32002, "session already has an active prompt");
297
+ const message = promptText(requestParams.prompt);
298
+ const turnId = (0, node_crypto_1.randomUUID)();
299
+ const completion = Promise.withResolvers();
300
+ activePrompts.set(sessionId, { rpcId: id, turnId, resolve: completion.resolve, reject: completion.reject });
301
+ try {
302
+ await (await client()).request(options.disableTools ? "turn.start.observe-only" : "turn.start", {
303
+ turnId,
304
+ agent: options.agent,
305
+ friendId: options.friendId,
306
+ channel: "mcp",
307
+ sessionKey: sessionId,
308
+ message,
309
+ ...(options.runtimeMcpServers ? { runtimeMcpServers: options.runtimeMcpServers } : {}),
310
+ });
311
+ result(id, { stopReason: await completion.promise });
312
+ }
313
+ finally {
314
+ activePrompts.delete(sessionId);
315
+ }
316
+ return;
317
+ }
318
+ if (method === "session/cancel") {
319
+ const sessionId = requiredString(requestParams.sessionId, "sessionId");
320
+ const active = activePrompts.get(sessionId);
321
+ if (active)
322
+ await (await client()).request("turn.cancel", { turnId: active.turnId });
323
+ return;
324
+ }
325
+ throw new AcpProtocolError(-32601, `method not found: ${method}`);
326
+ }
327
+ async function dispatch(line) {
328
+ let request;
329
+ try {
330
+ request = JSON.parse(line);
331
+ }
332
+ catch {
333
+ error(null, -32700, "parse error");
334
+ return;
335
+ }
336
+ try {
337
+ if (request.method === undefined && (request.result !== undefined || request.error !== undefined)) {
338
+ await handleResponse(request);
339
+ }
340
+ else {
341
+ await handle(request);
342
+ }
343
+ }
344
+ catch (caught) {
345
+ const id = request.id ?? null;
346
+ const code = caught instanceof AcpProtocolError ? caught.code : -32000;
347
+ error(id, code, caught instanceof Error ? caught.message : String(caught));
348
+ }
349
+ }
350
+ return {
351
+ start() {
352
+ if (running)
353
+ return;
354
+ running = true;
355
+ (0, runtime_1.emitNervesEvent)({
356
+ component: "senses",
357
+ event: "senses.acp_server_started",
358
+ message: "ACP server started",
359
+ });
360
+ lines = readline.createInterface({ input: options.stdin, crlfDelay: Infinity });
361
+ lines.on("line", (line) => {
362
+ if (line.trim())
363
+ void dispatch(line);
364
+ });
365
+ },
366
+ stop() {
367
+ if (!running)
368
+ return;
369
+ running = false;
370
+ lines?.close();
371
+ lines = null;
372
+ removeEventListener?.();
373
+ removeEventListener = null;
374
+ removeCloseListener?.();
375
+ removeCloseListener = null;
376
+ frontendClient?.close();
377
+ frontendClient = null;
378
+ clientPromise = null;
379
+ for (const prompt of activePrompts.values())
380
+ prompt.reject(new Error("ACP server stopped"));
381
+ activePrompts.clear();
382
+ pendingOutbound.clear();
383
+ sessions.clear();
384
+ },
385
+ };
386
+ }
@@ -73,6 +73,7 @@ const orientation_frame_1 = require("../heart/orientation-frame");
73
73
  const flight_recorder_1 = require("../arc/flight-recorder");
74
74
  const context_loss_sentinel_1 = require("../heart/context-loss-sentinel");
75
75
  const cares_1 = require("../arc/cares");
76
+ const turn_execution_lease_1 = require("../heart/turn-execution-lease");
76
77
  const VOICE_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
77
78
  const TELEGRAM_GENERIC_OUTREACH_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
78
79
  function pendingExpirationReason(channel, message, now, agentName) {
@@ -497,6 +498,9 @@ function prepareInboundRunLedger(input) {
497
498
  }
498
499
  }
499
500
  async function handleInboundTurn(input) {
501
+ return (0, turn_execution_lease_1.withTurnExecutionLease)(() => handleInboundTurnExclusive(input));
502
+ }
503
+ async function handleInboundTurnExclusive(input) {
500
504
  // Continuity resumption is structured caller intent. Never infer it from the
501
505
  // current message or from similarity to prior obligations.
502
506
  const resumePriorWork = input.runAgentOptions?.resumePriorWork === true;
@@ -769,6 +773,7 @@ async function handleInboundTurn(input) {
769
773
  currentSession,
770
774
  channel: input.channel,
771
775
  friendStore: input.friendStore,
776
+ includeAmbientBackgroundDiscovery: !liveLatencyMode,
772
777
  });
773
778
  // Propagate sync failure from pre-turn pull
774
779
  ctx.syncFailure = syncFailure;