@band-ai/sdk 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.cjs +247 -69
- package/dist/adapters.d.cts +24 -3
- package/dist/adapters.d.ts +24 -3
- package/dist/adapters.js +243 -68
- package/dist/{chunk-FYVLUGW7.js → chunk-V5TSWS7P.js} +4 -1
- package/dist/converters.cjs +4 -1
- package/dist/converters.js +1 -1
- package/package.json +1 -1
package/dist/adapters.cjs
CHANGED
|
@@ -1062,6 +1062,9 @@ var ACPClientHistoryConverter = class {
|
|
|
1062
1062
|
convert(raw) {
|
|
1063
1063
|
const roomToSession = {};
|
|
1064
1064
|
for (const entry of raw) {
|
|
1065
|
+
if (entry.message_type !== "task") {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1065
1068
|
const metadataRaw = entry.metadata;
|
|
1066
1069
|
if (!metadataRaw || typeof metadataRaw !== "object" || Array.isArray(metadataRaw)) {
|
|
1067
1070
|
continue;
|
|
@@ -1069,7 +1072,7 @@ var ACPClientHistoryConverter = class {
|
|
|
1069
1072
|
const metadata = metadataRaw;
|
|
1070
1073
|
const sessionId = metadata.acp_client_session_id;
|
|
1071
1074
|
const roomId = metadata.acp_client_room_id;
|
|
1072
|
-
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId) {
|
|
1075
|
+
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId && roomId === entry.room_id) {
|
|
1073
1076
|
roomToSession[roomId] = sessionId;
|
|
1074
1077
|
}
|
|
1075
1078
|
}
|
|
@@ -1888,7 +1891,13 @@ function normalizeMcpServers(mcpServers) {
|
|
|
1888
1891
|
// src/adapters/acp/client.ts
|
|
1889
1892
|
var BandACPClient = class {
|
|
1890
1893
|
sessionChunks = /* @__PURE__ */ new Map();
|
|
1891
|
-
|
|
1894
|
+
permissionHandler;
|
|
1895
|
+
// The handler is connection-scoped and required at construction, so it is
|
|
1896
|
+
// already in place before the agent process is spawned: there is no window
|
|
1897
|
+
// in which a `session/request_permission` has nowhere to go.
|
|
1898
|
+
constructor(permissionHandler) {
|
|
1899
|
+
this.permissionHandler = permissionHandler;
|
|
1900
|
+
}
|
|
1892
1901
|
async sessionUpdate(params) {
|
|
1893
1902
|
const chunk = toCollectedChunk(params.update);
|
|
1894
1903
|
if (!chunk) {
|
|
@@ -1899,26 +1908,12 @@ var BandACPClient = class {
|
|
|
1899
1908
|
this.sessionChunks.set(params.sessionId, existing);
|
|
1900
1909
|
}
|
|
1901
1910
|
async requestPermission(params) {
|
|
1902
|
-
|
|
1903
|
-
if (handler) {
|
|
1904
|
-
return handler(params);
|
|
1905
|
-
}
|
|
1906
|
-
return {
|
|
1907
|
-
outcome: {
|
|
1908
|
-
outcome: "cancelled"
|
|
1909
|
-
}
|
|
1910
|
-
};
|
|
1911
|
-
}
|
|
1912
|
-
setPermissionHandler(sessionId, handler) {
|
|
1913
|
-
if (!handler) {
|
|
1914
|
-
this.permissionHandlers.delete(sessionId);
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
this.permissionHandlers.set(sessionId, handler);
|
|
1911
|
+
return this.permissionHandler(params);
|
|
1918
1912
|
}
|
|
1919
|
-
|
|
1913
|
+
// Named for the one thing it clears: collected chunks are per-turn, and a
|
|
1914
|
+
// per-turn caller must not be able to reach anything with a longer life.
|
|
1915
|
+
resetChunks(sessionId) {
|
|
1920
1916
|
this.sessionChunks.delete(sessionId);
|
|
1921
|
-
this.permissionHandlers.delete(sessionId);
|
|
1922
1917
|
}
|
|
1923
1918
|
getCollectedText(sessionId) {
|
|
1924
1919
|
return this.getCollectedChunks(sessionId).filter((chunk) => chunk.chunkType === "text").map((chunk) => chunk.content).join("");
|
|
@@ -2162,10 +2157,18 @@ var ACPClientAdapter = class extends SimpleAdapter {
|
|
|
2162
2157
|
clientCapabilities;
|
|
2163
2158
|
connectionFactory;
|
|
2164
2159
|
roomToSession = /* @__PURE__ */ new Map();
|
|
2160
|
+
sessionToRoom = /* @__PURE__ */ new Map();
|
|
2165
2161
|
roomTools = /* @__PURE__ */ new Map();
|
|
2166
2162
|
activeSessions = /* @__PURE__ */ new Set();
|
|
2167
2163
|
bootstrappedSessions = /* @__PURE__ */ new Set();
|
|
2168
2164
|
pendingPermissions = /* @__PURE__ */ new Map();
|
|
2165
|
+
sessionsInFlight = /* @__PURE__ */ new Map();
|
|
2166
|
+
roomTurnLocks = /* @__PURE__ */ new Map();
|
|
2167
|
+
// Bumped each time a room starts a *new* establishment (never on a
|
|
2168
|
+
// coalesced reuse) and whenever a room is torn down. An establishment
|
|
2169
|
+
// captures its own value at the start; if the room has moved on by the
|
|
2170
|
+
// time it would link/activate a session, it was superseded and must not.
|
|
2171
|
+
roomGeneration = /* @__PURE__ */ new Map();
|
|
2169
2172
|
resolvePermission;
|
|
2170
2173
|
resolveSessionMode;
|
|
2171
2174
|
permissionTimeoutMs;
|
|
@@ -2219,17 +2222,16 @@ var ACPClientAdapter = class extends SimpleAdapter {
|
|
|
2219
2222
|
this.rehydrate(history);
|
|
2220
2223
|
}
|
|
2221
2224
|
this.roomTools.set(context.roomId, tools);
|
|
2225
|
+
await this.withRoomTurnLock(context.roomId, () => this.runTurn(message, tools, participantsMessage, contactsMessage, context));
|
|
2226
|
+
}
|
|
2227
|
+
async runTurn(message, tools, participantsMessage, contactsMessage, context) {
|
|
2222
2228
|
const connection = await this.ensureConnection();
|
|
2223
2229
|
const client = this.client;
|
|
2224
2230
|
if (!client) {
|
|
2225
2231
|
throw new Error("ACP client was not initialized");
|
|
2226
2232
|
}
|
|
2227
2233
|
const sessionId = await this.getOrCreateSession(context.roomId, connection);
|
|
2228
|
-
client.
|
|
2229
|
-
client.setPermissionHandler(
|
|
2230
|
-
sessionId,
|
|
2231
|
-
(params) => this.handlePermissionRequest(tools, context.roomId, params)
|
|
2232
|
-
);
|
|
2234
|
+
client.resetChunks(sessionId);
|
|
2233
2235
|
const content = replaceUuidMentions(message.content, mentionSubjectsFromMetadata(message.metadata));
|
|
2234
2236
|
const messageWithContext = [...systemUpdateParts(participantsMessage, contactsMessage), content].join("\n\n");
|
|
2235
2237
|
const promptText = this.bootstrappedSessions.has(sessionId) ? messageWithContext : `${this.buildSystemContext(context.roomId, message)}
|
|
@@ -2262,15 +2264,28 @@ ${messageWithContext}`;
|
|
|
2262
2264
|
acp_client_room_id: context.roomId
|
|
2263
2265
|
});
|
|
2264
2266
|
}
|
|
2267
|
+
// A per-room async mutex: `fn` for a given `roomId` never overlaps another
|
|
2268
|
+
// call for that same room, while different rooms stay fully concurrent.
|
|
2269
|
+
// The tracked tail (`this.roomTurnLocks`) always settles — via the
|
|
2270
|
+
// trailing `.catch` — so one turn's failure can't wedge every later turn
|
|
2271
|
+
// for the room; the real result/rejection is still `run`, returned to this
|
|
2272
|
+
// call's own caller.
|
|
2273
|
+
async withRoomTurnLock(roomId, fn) {
|
|
2274
|
+
const previous = this.roomTurnLocks.get(roomId) ?? Promise.resolve();
|
|
2275
|
+
const run = previous.then(fn, fn);
|
|
2276
|
+
this.roomTurnLocks.set(roomId, run.catch(() => void 0));
|
|
2277
|
+
return run;
|
|
2278
|
+
}
|
|
2265
2279
|
async onCleanup(roomId) {
|
|
2266
|
-
const sessionId = this.
|
|
2267
|
-
this.roomToSession.delete(roomId);
|
|
2280
|
+
const sessionId = this.unlinkRoom(roomId);
|
|
2268
2281
|
this.roomTools.delete(roomId);
|
|
2282
|
+
this.sessionsInFlight.delete(roomId);
|
|
2283
|
+
this.roomTurnLocks.delete(roomId);
|
|
2284
|
+
this.nextRoomGeneration(roomId);
|
|
2269
2285
|
if (sessionId) {
|
|
2270
2286
|
this.activeSessions.delete(sessionId);
|
|
2271
2287
|
this.bootstrappedSessions.delete(sessionId);
|
|
2272
|
-
this.
|
|
2273
|
-
this.cancelPendingPermissions(sessionId);
|
|
2288
|
+
this.cancelPendingPermissions(sessionId, "room-closed");
|
|
2274
2289
|
}
|
|
2275
2290
|
}
|
|
2276
2291
|
async onRuntimeStop() {
|
|
@@ -2282,8 +2297,14 @@ ${messageWithContext}`;
|
|
|
2282
2297
|
this.activeSessions.clear();
|
|
2283
2298
|
this.bootstrappedSessions.clear();
|
|
2284
2299
|
this.roomToSession.clear();
|
|
2300
|
+
this.sessionToRoom.clear();
|
|
2285
2301
|
this.roomTools.clear();
|
|
2286
|
-
this.
|
|
2302
|
+
this.sessionsInFlight.clear();
|
|
2303
|
+
this.roomTurnLocks.clear();
|
|
2304
|
+
for (const roomId of this.roomGeneration.keys()) {
|
|
2305
|
+
this.nextRoomGeneration(roomId);
|
|
2306
|
+
}
|
|
2307
|
+
this.cancelAllPendingPermissions("adapter-stopped");
|
|
2287
2308
|
this.client = null;
|
|
2288
2309
|
this.connection = null;
|
|
2289
2310
|
if (this.backend) {
|
|
@@ -2300,9 +2321,65 @@ ${messageWithContext}`;
|
|
|
2300
2321
|
rehydrate(history) {
|
|
2301
2322
|
for (const [roomId, sessionId] of Object.entries(history.roomToSession)) {
|
|
2302
2323
|
if (!this.roomToSession.has(roomId)) {
|
|
2303
|
-
this.
|
|
2304
|
-
}
|
|
2324
|
+
this.linkSession(roomId, sessionId);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// The only writer of both session maps, so they cannot drift: replacing a
|
|
2329
|
+
// room's session drops the old session's route, and a session id already
|
|
2330
|
+
// routed to another room is refused rather than silently re-pointed — two
|
|
2331
|
+
// rooms sharing one session id would make its permission requests
|
|
2332
|
+
// unattributable. Returns whether the link was made — a caller that goes
|
|
2333
|
+
// on to activate/configure/prompt a session regardless of a `false` here
|
|
2334
|
+
// would use a session this room was refused, not just fail to route its
|
|
2335
|
+
// permissions.
|
|
2336
|
+
linkSession(roomId, sessionId) {
|
|
2337
|
+
const routedRoomId = this.sessionToRoom.get(sessionId);
|
|
2338
|
+
if (routedRoomId !== void 0 && routedRoomId !== roomId) {
|
|
2339
|
+
this.safeWarn("refusing to route one ACP session to a second room", {
|
|
2340
|
+
sessionId,
|
|
2341
|
+
roomId,
|
|
2342
|
+
routedRoomId
|
|
2343
|
+
});
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
const replacedSessionId = this.roomToSession.get(roomId);
|
|
2347
|
+
if (replacedSessionId !== void 0 && replacedSessionId !== sessionId) {
|
|
2348
|
+
this.sessionToRoom.delete(replacedSessionId);
|
|
2305
2349
|
}
|
|
2350
|
+
this.roomToSession.set(roomId, sessionId);
|
|
2351
|
+
this.sessionToRoom.set(sessionId, roomId);
|
|
2352
|
+
return true;
|
|
2353
|
+
}
|
|
2354
|
+
nextRoomGeneration(roomId) {
|
|
2355
|
+
const next = (this.roomGeneration.get(roomId) ?? 0) + 1;
|
|
2356
|
+
this.roomGeneration.set(roomId, next);
|
|
2357
|
+
return next;
|
|
2358
|
+
}
|
|
2359
|
+
isCurrentGeneration(roomId, generation) {
|
|
2360
|
+
return this.roomGeneration.get(roomId) === generation;
|
|
2361
|
+
}
|
|
2362
|
+
// The installed ACP SDK's `sendRequest` never rejects a pending call when
|
|
2363
|
+
// its connection closes (no server response ever arrives to reject it
|
|
2364
|
+
// with) — so a session-establishment RPC in flight when the subprocess
|
|
2365
|
+
// dies would otherwise hang forever, wedging the room's `sessionsInFlight`
|
|
2366
|
+
// entry along with it. Racing every such RPC against the connection's own
|
|
2367
|
+
// `closed` promise gives it a real, prompt failure instead.
|
|
2368
|
+
raceAgainstConnectionClose(connection, operation) {
|
|
2369
|
+
let reject = () => void 0;
|
|
2370
|
+
const closedRejection = new Promise((_resolve, rejectFn) => {
|
|
2371
|
+
reject = rejectFn;
|
|
2372
|
+
});
|
|
2373
|
+
void connection.closed.then(() => reject(new Error("ACP connection closed while a session operation was still in flight")));
|
|
2374
|
+
return Promise.race([operation, closedRejection]);
|
|
2375
|
+
}
|
|
2376
|
+
unlinkRoom(roomId) {
|
|
2377
|
+
const sessionId = this.roomToSession.get(roomId);
|
|
2378
|
+
this.roomToSession.delete(roomId);
|
|
2379
|
+
if (sessionId !== void 0) {
|
|
2380
|
+
this.sessionToRoom.delete(sessionId);
|
|
2381
|
+
}
|
|
2382
|
+
return sessionId;
|
|
2306
2383
|
}
|
|
2307
2384
|
async ensureConnection() {
|
|
2308
2385
|
if (this.connection && !this.connection.signal.aborted) {
|
|
@@ -2323,7 +2400,7 @@ ${messageWithContext}`;
|
|
|
2323
2400
|
}
|
|
2324
2401
|
async spawnConnection() {
|
|
2325
2402
|
const acp = await acpModule.get();
|
|
2326
|
-
const client = new BandACPClient();
|
|
2403
|
+
const client = new BandACPClient((params) => this.routePermissionRequest(params));
|
|
2327
2404
|
const handle = await this.connectionFactory(client, {
|
|
2328
2405
|
command: this.command,
|
|
2329
2406
|
cwd: this.cwd,
|
|
@@ -2349,6 +2426,7 @@ ${messageWithContext}`;
|
|
|
2349
2426
|
this.connectionHandle = null;
|
|
2350
2427
|
this.connectionState = null;
|
|
2351
2428
|
this.activeSessions.clear();
|
|
2429
|
+
this.cancelAllPendingPermissions("connection-lost");
|
|
2352
2430
|
}
|
|
2353
2431
|
});
|
|
2354
2432
|
return connection;
|
|
@@ -2358,25 +2436,56 @@ ${messageWithContext}`;
|
|
|
2358
2436
|
if (existingSessionId && this.activeSessions.has(existingSessionId)) {
|
|
2359
2437
|
return existingSessionId;
|
|
2360
2438
|
}
|
|
2439
|
+
const inFlight = this.sessionsInFlight.get(roomId);
|
|
2440
|
+
if (inFlight) {
|
|
2441
|
+
return inFlight;
|
|
2442
|
+
}
|
|
2443
|
+
const generation = this.nextRoomGeneration(roomId);
|
|
2444
|
+
const establishing = this.establishSession(roomId, existingSessionId, connection, generation);
|
|
2445
|
+
establishing.finally(() => {
|
|
2446
|
+
if (this.sessionsInFlight.get(roomId) === establishing) {
|
|
2447
|
+
this.sessionsInFlight.delete(roomId);
|
|
2448
|
+
}
|
|
2449
|
+
}).catch(() => void 0);
|
|
2450
|
+
this.sessionsInFlight.set(roomId, establishing);
|
|
2451
|
+
return establishing;
|
|
2452
|
+
}
|
|
2453
|
+
async establishSession(roomId, existingSessionId, connection, generation) {
|
|
2361
2454
|
const mcpServers = await this.buildSessionMcpServers();
|
|
2362
2455
|
if (existingSessionId) {
|
|
2363
2456
|
const restored = await this.tryRestoreSession(connection, existingSessionId, mcpServers);
|
|
2364
2457
|
if (restored.ok) {
|
|
2458
|
+
this.linkOrAbandon(roomId, existingSessionId, generation);
|
|
2365
2459
|
this.activeSessions.add(existingSessionId);
|
|
2366
2460
|
this.bootstrappedSessions.add(existingSessionId);
|
|
2367
2461
|
await this.configureSessionMode(roomId, existingSessionId, restored.modes, connection);
|
|
2368
2462
|
return existingSessionId;
|
|
2369
2463
|
}
|
|
2370
2464
|
}
|
|
2371
|
-
const created = await connection.newSession({
|
|
2465
|
+
const created = await this.raceAgainstConnectionClose(connection, connection.newSession({
|
|
2372
2466
|
cwd: this.cwd,
|
|
2373
2467
|
mcpServers
|
|
2374
|
-
});
|
|
2375
|
-
this.
|
|
2468
|
+
}));
|
|
2469
|
+
this.linkOrAbandon(roomId, created.sessionId, generation);
|
|
2376
2470
|
this.activeSessions.add(created.sessionId);
|
|
2377
2471
|
await this.configureSessionMode(roomId, created.sessionId, created.modes, connection);
|
|
2378
2472
|
return created.sessionId;
|
|
2379
2473
|
}
|
|
2474
|
+
// The single gate an establishment must pass before it's allowed to claim
|
|
2475
|
+
// the room: it must still be the room's current generation (not
|
|
2476
|
+
// superseded by a teardown or a fresher establishment while this one was
|
|
2477
|
+
// awaiting an RPC), and its session id must not already belong to another
|
|
2478
|
+
// room. Either failure throws — this establishment cannot silently
|
|
2479
|
+
// continue to activate, configure, and prompt a session it has no right
|
|
2480
|
+
// to use for this room.
|
|
2481
|
+
linkOrAbandon(roomId, sessionId, generation) {
|
|
2482
|
+
if (!this.isCurrentGeneration(roomId, generation)) {
|
|
2483
|
+
throw new Error(`ACP session establishment for room "${roomId}" was superseded before it could be linked`);
|
|
2484
|
+
}
|
|
2485
|
+
if (!this.linkSession(roomId, sessionId)) {
|
|
2486
|
+
throw new Error(`ACP session "${sessionId}" could not be linked to room "${roomId}": already routed elsewhere`);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2380
2489
|
// Best-effort: never throws, so a mode switch going wrong can't take a
|
|
2381
2490
|
// session establishment down with it.
|
|
2382
2491
|
async configureSessionMode(roomId, sessionId, modes, connection) {
|
|
@@ -2455,7 +2564,7 @@ ${messageWithContext}`;
|
|
|
2455
2564
|
return { ok: false };
|
|
2456
2565
|
}
|
|
2457
2566
|
try {
|
|
2458
|
-
const restored = await restore();
|
|
2567
|
+
const restored = await this.raceAgainstConnectionClose(connection, restore());
|
|
2459
2568
|
return { ok: true, modes: restored?.modes };
|
|
2460
2569
|
} catch {
|
|
2461
2570
|
return { ok: false };
|
|
@@ -2568,6 +2677,36 @@ ${messageWithContext}`;
|
|
|
2568
2677
|
"All Band MCP tool calls must include room_id."
|
|
2569
2678
|
].join("\n");
|
|
2570
2679
|
}
|
|
2680
|
+
// The connection's single permission entry point, and total by
|
|
2681
|
+
// construction: every path resolves, nothing throws, and a request that
|
|
2682
|
+
// can't be attributed to a live room is cancelled *and* warned rather than
|
|
2683
|
+
// silently declined. `activeSessions` is the gate that keeps a dead
|
|
2684
|
+
// session from raising a live prompt — `roomToSession` deliberately
|
|
2685
|
+
// outlives a dropped connection so the session can be restored later.
|
|
2686
|
+
async routePermissionRequest(params) {
|
|
2687
|
+
const isActive = this.activeSessions.has(params.sessionId);
|
|
2688
|
+
const roomId = isActive ? this.sessionToRoom.get(params.sessionId) : void 0;
|
|
2689
|
+
const tools = roomId === void 0 ? void 0 : this.roomTools.get(roomId);
|
|
2690
|
+
if (roomId === void 0 || !tools) {
|
|
2691
|
+
this.safeWarn("cancelling a permission request that maps to no live room", {
|
|
2692
|
+
sessionId: params.sessionId,
|
|
2693
|
+
toolName: params.toolCall?.title,
|
|
2694
|
+
sessionActive: isActive,
|
|
2695
|
+
roomId
|
|
2696
|
+
});
|
|
2697
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2698
|
+
}
|
|
2699
|
+
try {
|
|
2700
|
+
return await this.handlePermissionRequest(tools, roomId, params);
|
|
2701
|
+
} catch (error) {
|
|
2702
|
+
this.safeWarn("permission handling failed; cancelling the request", {
|
|
2703
|
+
sessionId: params.sessionId,
|
|
2704
|
+
roomId,
|
|
2705
|
+
error: String(error)
|
|
2706
|
+
});
|
|
2707
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2571
2710
|
async handlePermissionRequest(tools, roomId, params) {
|
|
2572
2711
|
const toolName = params.toolCall.title ?? "unknown";
|
|
2573
2712
|
const autoSelection = this.resolvePermission ? void 0 : choosePermissionOption(params.options);
|
|
@@ -2575,28 +2714,52 @@ ${messageWithContext}`;
|
|
|
2575
2714
|
if (controller) {
|
|
2576
2715
|
this.trackPending(params.sessionId, controller);
|
|
2577
2716
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
//
|
|
2581
|
-
//
|
|
2582
|
-
// can take up to `permissionTimeoutMs`.
|
|
2717
|
+
let requestEventFailed = false;
|
|
2718
|
+
const [, resolvedChosenId] = await Promise.all([
|
|
2719
|
+
// Started immediately rather than serialized in front of a manual
|
|
2720
|
+
// wait that can take up to `permissionTimeoutMs`.
|
|
2583
2721
|
tools.sendEvent(`Permission requested: ${toolName}`, "tool_call", {
|
|
2584
2722
|
permission_request: true,
|
|
2585
2723
|
tool_name: toolName,
|
|
2586
2724
|
tool_call_id: params.toolCall.toolCallId,
|
|
2587
2725
|
acp_session_id: params.sessionId,
|
|
2588
2726
|
auto_allowed: autoSelection !== void 0 && autoSelection !== null
|
|
2727
|
+
}).catch((error) => {
|
|
2728
|
+
requestEventFailed = true;
|
|
2729
|
+
this.safeWarn("failed to post the permission-requested event; cancelling the request", {
|
|
2730
|
+
roomId,
|
|
2731
|
+
sessionId: params.sessionId,
|
|
2732
|
+
error: String(error)
|
|
2733
|
+
});
|
|
2734
|
+
if (controller) {
|
|
2735
|
+
this.abandon(controller, "no-answer");
|
|
2736
|
+
}
|
|
2589
2737
|
}),
|
|
2590
|
-
controller ? this.resolveManually(
|
|
2738
|
+
controller ? this.resolveManually(roomId, params, controller) : Promise.resolve(autoSelection?.optionId)
|
|
2591
2739
|
]);
|
|
2592
|
-
|
|
2740
|
+
const chosenId = requestEventFailed ? void 0 : resolvedChosenId;
|
|
2741
|
+
const response = this.toResponse(chosenId, params.options, { roomId, sessionId: params.sessionId });
|
|
2742
|
+
if (controller && !controller.signal.aborted) {
|
|
2743
|
+
this.abandon(controller, response.outcome.outcome === "selected" ? "settled" : "no-answer");
|
|
2744
|
+
}
|
|
2745
|
+
return response;
|
|
2593
2746
|
}
|
|
2594
2747
|
// `undefined`, or an id absent from this request's own `options` (a buggy
|
|
2595
2748
|
// or stale caller), both map to `cancelled` — never silently treated as a
|
|
2596
2749
|
// deny. A real match, reject-kind options included, maps to `selected`.
|
|
2597
|
-
toResponse(chosenId, options) {
|
|
2598
|
-
|
|
2599
|
-
|
|
2750
|
+
toResponse(chosenId, options, context) {
|
|
2751
|
+
if (chosenId === void 0) {
|
|
2752
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2753
|
+
}
|
|
2754
|
+
if (!options.some((option) => option.optionId === chosenId)) {
|
|
2755
|
+
this.safeWarn("resolvePermission chose an option this request does not offer", {
|
|
2756
|
+
...context,
|
|
2757
|
+
chosenId,
|
|
2758
|
+
optionIds: options.map((option) => option.optionId)
|
|
2759
|
+
});
|
|
2760
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2761
|
+
}
|
|
2762
|
+
return { outcome: { outcome: "selected", optionId: chosenId } };
|
|
2600
2763
|
}
|
|
2601
2764
|
// A caller-supplied `Logger` isn't guaranteed to be synchronous or
|
|
2602
2765
|
// non-throwing. Every best-effort warning in this file routes through here
|
|
@@ -2610,41 +2773,56 @@ ${messageWithContext}`;
|
|
|
2610
2773
|
} catch {
|
|
2611
2774
|
}
|
|
2612
2775
|
}
|
|
2613
|
-
// Races the caller-supplied resolver against
|
|
2614
|
-
//
|
|
2615
|
-
// `cancelPendingPermissions
|
|
2616
|
-
// `
|
|
2617
|
-
//
|
|
2618
|
-
//
|
|
2619
|
-
//
|
|
2620
|
-
async resolveManually(
|
|
2621
|
-
|
|
2622
|
-
const cancelled = new Promise((resolve) => {
|
|
2776
|
+
// Races the caller-supplied resolver against `controller`'s abort signal,
|
|
2777
|
+
// which is the request's single termination channel: the timeout below
|
|
2778
|
+
// fires it with `"timeout"`, and `cancelPendingPermissions` /
|
|
2779
|
+
// `cancelAllPendingPermissions` fire it with the reason their caller
|
|
2780
|
+
// supplies. `controller` is the same object tracked in `pendingPermissions`,
|
|
2781
|
+
// so there is exactly one cancellation channel here, not a second
|
|
2782
|
+
// hand-rolled one alongside it.
|
|
2783
|
+
async resolveManually(roomId, params, controller) {
|
|
2784
|
+
const abandoned = new Promise((resolve) => {
|
|
2623
2785
|
controller.signal.addEventListener("abort", () => resolve(void 0));
|
|
2624
2786
|
});
|
|
2787
|
+
const timer = setTimeout(() => this.abandon(controller, "timeout"), this.permissionTimeoutMs);
|
|
2625
2788
|
try {
|
|
2626
|
-
const timeout = new Promise((resolve) => {
|
|
2627
|
-
timer = setTimeout(() => resolve(void 0), this.permissionTimeoutMs);
|
|
2628
|
-
});
|
|
2629
2789
|
return await Promise.race([
|
|
2630
2790
|
// `resolvePermission` is caller-supplied; nothing guarantees it's
|
|
2631
2791
|
// `async` or otherwise well-behaved. `Promise.resolve().then(...)`
|
|
2632
2792
|
// normalizes a synchronous throw the same way it normalizes a
|
|
2633
2793
|
// rejected promise, so both land in the `.catch` below rather than
|
|
2634
2794
|
// escaping this race uncaught.
|
|
2635
|
-
Promise.resolve().then(() => this.resolvePermission(params, controller.signal)).catch((error) => {
|
|
2795
|
+
Promise.resolve().then(() => this.resolvePermission({ ...params, roomId }, controller.signal)).then((chosenId) => this.discardLateAnswer(chosenId, controller, roomId, params.sessionId)).catch((error) => {
|
|
2636
2796
|
this.safeWarn("resolvePermission threw; treating as no answer", { error: String(error) });
|
|
2637
2797
|
return void 0;
|
|
2638
2798
|
}),
|
|
2639
|
-
|
|
2640
|
-
cancelled
|
|
2799
|
+
abandoned
|
|
2641
2800
|
]);
|
|
2642
2801
|
} finally {
|
|
2643
2802
|
clearTimeout(timer);
|
|
2644
|
-
this.untrackPending(sessionId, controller);
|
|
2645
|
-
controller.abort();
|
|
2803
|
+
this.untrackPending(params.sessionId, controller);
|
|
2646
2804
|
}
|
|
2647
2805
|
}
|
|
2806
|
+
// An answer that lands after the request was given up on can no longer be
|
|
2807
|
+
// honoured — the response has already gone back to the agent. It is dropped
|
|
2808
|
+
// either way; warning is what makes "my click did nothing" explicable.
|
|
2809
|
+
discardLateAnswer(chosenId, controller, roomId, sessionId) {
|
|
2810
|
+
if (!controller.signal.aborted || chosenId === void 0) {
|
|
2811
|
+
return chosenId;
|
|
2812
|
+
}
|
|
2813
|
+
this.safeWarn("resolvePermission answered after the request was abandoned; discarding", {
|
|
2814
|
+
roomId,
|
|
2815
|
+
sessionId,
|
|
2816
|
+
chosenId,
|
|
2817
|
+
reason: String(controller.signal.reason)
|
|
2818
|
+
});
|
|
2819
|
+
return void 0;
|
|
2820
|
+
}
|
|
2821
|
+
// The only place a permission's controller is ever aborted, so every
|
|
2822
|
+
// `signal.reason` a consumer can observe comes from the documented union.
|
|
2823
|
+
abandon(controller, reason) {
|
|
2824
|
+
controller.abort(reason);
|
|
2825
|
+
}
|
|
2648
2826
|
trackPending(sessionId, controller) {
|
|
2649
2827
|
const pending = this.pendingPermissions.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
2650
2828
|
pending.add(controller);
|
|
@@ -2657,14 +2835,14 @@ ${messageWithContext}`;
|
|
|
2657
2835
|
this.pendingPermissions.delete(sessionId);
|
|
2658
2836
|
}
|
|
2659
2837
|
}
|
|
2660
|
-
cancelPendingPermissions(sessionId) {
|
|
2838
|
+
cancelPendingPermissions(sessionId, reason) {
|
|
2661
2839
|
for (const controller of this.pendingPermissions.get(sessionId) ?? []) {
|
|
2662
|
-
|
|
2840
|
+
this.abandon(controller, reason);
|
|
2663
2841
|
}
|
|
2664
2842
|
}
|
|
2665
|
-
cancelAllPendingPermissions() {
|
|
2843
|
+
cancelAllPendingPermissions(reason) {
|
|
2666
2844
|
for (const sessionId of this.pendingPermissions.keys()) {
|
|
2667
|
-
this.cancelPendingPermissions(sessionId);
|
|
2845
|
+
this.cancelPendingPermissions(sessionId, reason);
|
|
2668
2846
|
}
|
|
2669
2847
|
}
|
|
2670
2848
|
async flushChunks(input) {
|
package/dist/adapters.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { A as A2AAdapter, a as A2AAdapterOptions, K as A2AClientFactory, M as A2AClientLike, b as A2AGatewayAdapter, c as AnthropicAdapter, d as AnthropicAdapterOptions, N as AnthropicClientFactory, Q as AnthropicToolCallingModel, R as AnthropicToolCallingModelOptions, C as CODEX_REASONING_EFFORTS, e as CODEX_REASONING_SUMMARIES, f as CODEX_WEB_SEARCH_MODES, g as ClaudePermissionMode, h as ClaudeSDKAdapter, i as ClaudeSDKAdapterOptions, S as ClaudeSDKQuery, U as ClaudeSDKQueryParams, j as CodexAdapter, k as CodexAdapterConfig, W as CodexAppServerStdioClient, l as CodexApprovalPolicy, X as CodexClientLike, Y as CodexJsonRpcError, m as CodexReasoningEffort, n as CodexReasoningSummary, o as CodexSandboxMode, p as CodexWebSearchMode, Z as DynamicToolCallParams, _ as DynamicToolCallResponse, $ as DynamicToolSpec, G as GeminiAdapter, q as GeminiAdapterOptions, a0 as GeminiClientFactory, a1 as GeminiToolCallingModel, a2 as GeminiToolCallingModelOptions, r as GenericAdapter, s as GenericAdapterHandler, t as GoogleADKAdapter, u as GoogleADKAdapterOptions, a3 as HttpOpencodeClient, a4 as HttpOpencodeClientOptions, a5 as HttpStatusError, L as LangGraphAdapter, v as LangGraphAdapterOptions, w as LangGraphGraph, x as LettaAdapter, y as LettaAdapterOptions, a6 as LettaAgentCreateParams, a7 as LettaClientFactory, a8 as LettaClientLike, a9 as LettaHistoryConverter, aa as LettaMessage, ab as LettaMessageCreateParams, ac as LettaMessages, ad as LettaRequestOptions, ae as LettaResponse, af as LettaResponseMessage, O as OpenAIAdapter, z as OpenAIAdapterOptions, ag as OpenAIClientFactory, ah as OpenAIToolCallingModel, ai as OpenAIToolCallingModelOptions, B as OpencodeAdapter, D as OpencodeAdapterConfig, E as OpencodeApprovalMode, F as OpencodeApprovalReply, aj as OpencodeClientLike, H as OpencodeQuestionMode, P as ParlantAdapter, I as ParlantAdapterOptions, ak as ParlantClientFactory, al as ParlantClientLike, am as ToolCall, an as ToolCallingAdapter, ao as ToolCallingAdapterOptions, T as ToolCallingModel, ap as ToolCallingModelRequest, aq as ToolCallingResponse, ar as ToolResult, as as TurnStartParams, V as VercelAISDKAdapter, J as VercelAISDKAdapterOptions, at as VercelAISDKToolCallingModel, au as VercelAISDKToolCallingModelOptions, av as runSingleToolRound } from './ClaudeSDKAdapter-Cx6zhSaG.cjs';
|
|
2
2
|
import * as _agentclientprotocol_sdk from '@agentclientprotocol/sdk';
|
|
3
|
-
import { Client, ClientSideConnection, McpServer, ClientCapabilities,
|
|
3
|
+
import { Client, ClientSideConnection, RequestPermissionRequest, McpServer, ClientCapabilities, SessionMode, AgentSideConnection, SessionModeState, Agent, InitializeResponse, Implementation, Stream, InitializeRequest, LoadSessionRequest, LoadSessionResponse, ListSessionsRequest, ListSessionsResponse, ForkSessionRequest, ForkSessionResponse, ResumeSessionRequest, ResumeSessionResponse, SetSessionModeRequest, SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, AuthenticateRequest, AuthenticateResponse, PromptRequest, PromptResponse } from '@agentclientprotocol/sdk';
|
|
4
4
|
import { A as ACPClientSessionState, a as ACPServerSessionState } from './acp-server-Dlj7D563.cjs';
|
|
5
5
|
export { G as GatewayHistoryConverter } from './acp-server-Dlj7D563.cjs';
|
|
6
6
|
import { S as SimpleAdapter } from './simpleAdapter-BpT4XbZC.cjs';
|
|
@@ -40,6 +40,11 @@ type ACPClientConnectionFactory = (client: Client, options: {
|
|
|
40
40
|
cwd?: string;
|
|
41
41
|
env?: Record<string, string>;
|
|
42
42
|
}) => Promise<ACPClientConnectionHandle>;
|
|
43
|
+
type ACPPermissionRequest = RequestPermissionRequest & {
|
|
44
|
+
roomId: string;
|
|
45
|
+
};
|
|
46
|
+
type ACPPermissionAbandonReason = "timeout" | "room-closed" | "adapter-stopped" | "connection-lost";
|
|
47
|
+
type ACPPermissionEndReason = ACPPermissionAbandonReason | "settled" | "no-answer";
|
|
43
48
|
|
|
44
49
|
interface ACPModeRequest {
|
|
45
50
|
roomId: string;
|
|
@@ -58,7 +63,7 @@ interface ACPClientAdapterOptions {
|
|
|
58
63
|
additionalMcpTools?: McpToolRegistration[];
|
|
59
64
|
clientCapabilities?: ClientCapabilities;
|
|
60
65
|
connectionFactory?: ACPClientConnectionFactory;
|
|
61
|
-
resolvePermission?: (request:
|
|
66
|
+
resolvePermission?: (request: ACPPermissionRequest, signal: AbortSignal) => Promise<string | undefined>;
|
|
62
67
|
permissionTimeoutMs?: number;
|
|
63
68
|
resolveSessionMode?: (request: ACPModeRequest, signal: AbortSignal) => Promise<string | undefined>;
|
|
64
69
|
logger?: Logger;
|
|
@@ -75,10 +80,14 @@ declare class ACPClientAdapter extends SimpleAdapter<ACPClientSessionState, Adap
|
|
|
75
80
|
private readonly clientCapabilities?;
|
|
76
81
|
private readonly connectionFactory;
|
|
77
82
|
private readonly roomToSession;
|
|
83
|
+
private readonly sessionToRoom;
|
|
78
84
|
private readonly roomTools;
|
|
79
85
|
private readonly activeSessions;
|
|
80
86
|
private readonly bootstrappedSessions;
|
|
81
87
|
private readonly pendingPermissions;
|
|
88
|
+
private readonly sessionsInFlight;
|
|
89
|
+
private readonly roomTurnLocks;
|
|
90
|
+
private readonly roomGeneration;
|
|
82
91
|
private readonly resolvePermission?;
|
|
83
92
|
private readonly resolveSessionMode?;
|
|
84
93
|
private readonly permissionTimeoutMs;
|
|
@@ -98,13 +107,22 @@ declare class ACPClientAdapter extends SimpleAdapter<ACPClientSessionState, Adap
|
|
|
98
107
|
isSessionBootstrap: boolean;
|
|
99
108
|
roomId: string;
|
|
100
109
|
}): Promise<void>;
|
|
110
|
+
private runTurn;
|
|
111
|
+
private withRoomTurnLock;
|
|
101
112
|
onCleanup(roomId: string): Promise<void>;
|
|
102
113
|
onRuntimeStop(): Promise<void>;
|
|
103
114
|
stop(): Promise<void>;
|
|
104
115
|
private rehydrate;
|
|
116
|
+
private linkSession;
|
|
117
|
+
private nextRoomGeneration;
|
|
118
|
+
private isCurrentGeneration;
|
|
119
|
+
private raceAgainstConnectionClose;
|
|
120
|
+
private unlinkRoom;
|
|
105
121
|
private ensureConnection;
|
|
106
122
|
private spawnConnection;
|
|
107
123
|
private getOrCreateSession;
|
|
124
|
+
private establishSession;
|
|
125
|
+
private linkOrAbandon;
|
|
108
126
|
private configureSessionMode;
|
|
109
127
|
private resolveSessionModeManually;
|
|
110
128
|
private tryRestoreSession;
|
|
@@ -112,10 +130,13 @@ declare class ACPClientAdapter extends SimpleAdapter<ACPClientSessionState, Adap
|
|
|
112
130
|
private getOrCreateBackend;
|
|
113
131
|
private createBackend;
|
|
114
132
|
private buildSystemContext;
|
|
133
|
+
private routePermissionRequest;
|
|
115
134
|
private handlePermissionRequest;
|
|
116
135
|
private toResponse;
|
|
117
136
|
private safeWarn;
|
|
118
137
|
private resolveManually;
|
|
138
|
+
private discardLateAnswer;
|
|
139
|
+
private abandon;
|
|
119
140
|
private trackPending;
|
|
120
141
|
private untrackPending;
|
|
121
142
|
private cancelPendingPermissions;
|
|
@@ -240,4 +261,4 @@ declare class ACPServer implements Agent {
|
|
|
240
261
|
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
|
|
241
262
|
}
|
|
242
263
|
|
|
243
|
-
export { ACPClientAdapter, type ACPClientAdapterOptions, ACPServer, type ACPServerOptions, BandACPServerAdapter, type BandACPServerAdapterOptions, GatewayServer, GatewayServerLike, GatewayServerOptions, createGatewayServer };
|
|
264
|
+
export { ACPClientAdapter, type ACPClientAdapterOptions, type ACPPermissionAbandonReason, type ACPPermissionEndReason, type ACPPermissionRequest, ACPServer, type ACPServerOptions, BandACPServerAdapter, type BandACPServerAdapterOptions, GatewayServer, GatewayServerLike, GatewayServerOptions, createGatewayServer };
|
package/dist/adapters.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { A as A2AAdapter, a as A2AAdapterOptions, K as A2AClientFactory, M as A2AClientLike, b as A2AGatewayAdapter, c as AnthropicAdapter, d as AnthropicAdapterOptions, N as AnthropicClientFactory, Q as AnthropicToolCallingModel, R as AnthropicToolCallingModelOptions, C as CODEX_REASONING_EFFORTS, e as CODEX_REASONING_SUMMARIES, f as CODEX_WEB_SEARCH_MODES, g as ClaudePermissionMode, h as ClaudeSDKAdapter, i as ClaudeSDKAdapterOptions, S as ClaudeSDKQuery, U as ClaudeSDKQueryParams, j as CodexAdapter, k as CodexAdapterConfig, W as CodexAppServerStdioClient, l as CodexApprovalPolicy, X as CodexClientLike, Y as CodexJsonRpcError, m as CodexReasoningEffort, n as CodexReasoningSummary, o as CodexSandboxMode, p as CodexWebSearchMode, Z as DynamicToolCallParams, _ as DynamicToolCallResponse, $ as DynamicToolSpec, G as GeminiAdapter, q as GeminiAdapterOptions, a0 as GeminiClientFactory, a1 as GeminiToolCallingModel, a2 as GeminiToolCallingModelOptions, r as GenericAdapter, s as GenericAdapterHandler, t as GoogleADKAdapter, u as GoogleADKAdapterOptions, a3 as HttpOpencodeClient, a4 as HttpOpencodeClientOptions, a5 as HttpStatusError, L as LangGraphAdapter, v as LangGraphAdapterOptions, w as LangGraphGraph, x as LettaAdapter, y as LettaAdapterOptions, a6 as LettaAgentCreateParams, a7 as LettaClientFactory, a8 as LettaClientLike, a9 as LettaHistoryConverter, aa as LettaMessage, ab as LettaMessageCreateParams, ac as LettaMessages, ad as LettaRequestOptions, ae as LettaResponse, af as LettaResponseMessage, O as OpenAIAdapter, z as OpenAIAdapterOptions, ag as OpenAIClientFactory, ah as OpenAIToolCallingModel, ai as OpenAIToolCallingModelOptions, B as OpencodeAdapter, D as OpencodeAdapterConfig, E as OpencodeApprovalMode, F as OpencodeApprovalReply, aj as OpencodeClientLike, H as OpencodeQuestionMode, P as ParlantAdapter, I as ParlantAdapterOptions, ak as ParlantClientFactory, al as ParlantClientLike, am as ToolCall, an as ToolCallingAdapter, ao as ToolCallingAdapterOptions, T as ToolCallingModel, ap as ToolCallingModelRequest, aq as ToolCallingResponse, ar as ToolResult, as as TurnStartParams, V as VercelAISDKAdapter, J as VercelAISDKAdapterOptions, at as VercelAISDKToolCallingModel, au as VercelAISDKToolCallingModelOptions, av as runSingleToolRound } from './ClaudeSDKAdapter-CXud2DBE.js';
|
|
2
2
|
import * as _agentclientprotocol_sdk from '@agentclientprotocol/sdk';
|
|
3
|
-
import { Client, ClientSideConnection, McpServer, ClientCapabilities,
|
|
3
|
+
import { Client, ClientSideConnection, RequestPermissionRequest, McpServer, ClientCapabilities, SessionMode, AgentSideConnection, SessionModeState, Agent, InitializeResponse, Implementation, Stream, InitializeRequest, LoadSessionRequest, LoadSessionResponse, ListSessionsRequest, ListSessionsResponse, ForkSessionRequest, ForkSessionResponse, ResumeSessionRequest, ResumeSessionResponse, SetSessionModeRequest, SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, AuthenticateRequest, AuthenticateResponse, PromptRequest, PromptResponse } from '@agentclientprotocol/sdk';
|
|
4
4
|
import { A as ACPClientSessionState, a as ACPServerSessionState } from './acp-server-CiUqN3G3.js';
|
|
5
5
|
export { G as GatewayHistoryConverter } from './acp-server-CiUqN3G3.js';
|
|
6
6
|
import { S as SimpleAdapter } from './simpleAdapter--wznuoOw.js';
|
|
@@ -40,6 +40,11 @@ type ACPClientConnectionFactory = (client: Client, options: {
|
|
|
40
40
|
cwd?: string;
|
|
41
41
|
env?: Record<string, string>;
|
|
42
42
|
}) => Promise<ACPClientConnectionHandle>;
|
|
43
|
+
type ACPPermissionRequest = RequestPermissionRequest & {
|
|
44
|
+
roomId: string;
|
|
45
|
+
};
|
|
46
|
+
type ACPPermissionAbandonReason = "timeout" | "room-closed" | "adapter-stopped" | "connection-lost";
|
|
47
|
+
type ACPPermissionEndReason = ACPPermissionAbandonReason | "settled" | "no-answer";
|
|
43
48
|
|
|
44
49
|
interface ACPModeRequest {
|
|
45
50
|
roomId: string;
|
|
@@ -58,7 +63,7 @@ interface ACPClientAdapterOptions {
|
|
|
58
63
|
additionalMcpTools?: McpToolRegistration[];
|
|
59
64
|
clientCapabilities?: ClientCapabilities;
|
|
60
65
|
connectionFactory?: ACPClientConnectionFactory;
|
|
61
|
-
resolvePermission?: (request:
|
|
66
|
+
resolvePermission?: (request: ACPPermissionRequest, signal: AbortSignal) => Promise<string | undefined>;
|
|
62
67
|
permissionTimeoutMs?: number;
|
|
63
68
|
resolveSessionMode?: (request: ACPModeRequest, signal: AbortSignal) => Promise<string | undefined>;
|
|
64
69
|
logger?: Logger;
|
|
@@ -75,10 +80,14 @@ declare class ACPClientAdapter extends SimpleAdapter<ACPClientSessionState, Adap
|
|
|
75
80
|
private readonly clientCapabilities?;
|
|
76
81
|
private readonly connectionFactory;
|
|
77
82
|
private readonly roomToSession;
|
|
83
|
+
private readonly sessionToRoom;
|
|
78
84
|
private readonly roomTools;
|
|
79
85
|
private readonly activeSessions;
|
|
80
86
|
private readonly bootstrappedSessions;
|
|
81
87
|
private readonly pendingPermissions;
|
|
88
|
+
private readonly sessionsInFlight;
|
|
89
|
+
private readonly roomTurnLocks;
|
|
90
|
+
private readonly roomGeneration;
|
|
82
91
|
private readonly resolvePermission?;
|
|
83
92
|
private readonly resolveSessionMode?;
|
|
84
93
|
private readonly permissionTimeoutMs;
|
|
@@ -98,13 +107,22 @@ declare class ACPClientAdapter extends SimpleAdapter<ACPClientSessionState, Adap
|
|
|
98
107
|
isSessionBootstrap: boolean;
|
|
99
108
|
roomId: string;
|
|
100
109
|
}): Promise<void>;
|
|
110
|
+
private runTurn;
|
|
111
|
+
private withRoomTurnLock;
|
|
101
112
|
onCleanup(roomId: string): Promise<void>;
|
|
102
113
|
onRuntimeStop(): Promise<void>;
|
|
103
114
|
stop(): Promise<void>;
|
|
104
115
|
private rehydrate;
|
|
116
|
+
private linkSession;
|
|
117
|
+
private nextRoomGeneration;
|
|
118
|
+
private isCurrentGeneration;
|
|
119
|
+
private raceAgainstConnectionClose;
|
|
120
|
+
private unlinkRoom;
|
|
105
121
|
private ensureConnection;
|
|
106
122
|
private spawnConnection;
|
|
107
123
|
private getOrCreateSession;
|
|
124
|
+
private establishSession;
|
|
125
|
+
private linkOrAbandon;
|
|
108
126
|
private configureSessionMode;
|
|
109
127
|
private resolveSessionModeManually;
|
|
110
128
|
private tryRestoreSession;
|
|
@@ -112,10 +130,13 @@ declare class ACPClientAdapter extends SimpleAdapter<ACPClientSessionState, Adap
|
|
|
112
130
|
private getOrCreateBackend;
|
|
113
131
|
private createBackend;
|
|
114
132
|
private buildSystemContext;
|
|
133
|
+
private routePermissionRequest;
|
|
115
134
|
private handlePermissionRequest;
|
|
116
135
|
private toResponse;
|
|
117
136
|
private safeWarn;
|
|
118
137
|
private resolveManually;
|
|
138
|
+
private discardLateAnswer;
|
|
139
|
+
private abandon;
|
|
119
140
|
private trackPending;
|
|
120
141
|
private untrackPending;
|
|
121
142
|
private cancelPendingPermissions;
|
|
@@ -240,4 +261,4 @@ declare class ACPServer implements Agent {
|
|
|
240
261
|
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
|
|
241
262
|
}
|
|
242
263
|
|
|
243
|
-
export { ACPClientAdapter, type ACPClientAdapterOptions, ACPServer, type ACPServerOptions, BandACPServerAdapter, type BandACPServerAdapterOptions, GatewayServer, GatewayServerLike, GatewayServerOptions, createGatewayServer };
|
|
264
|
+
export { ACPClientAdapter, type ACPClientAdapterOptions, type ACPPermissionAbandonReason, type ACPPermissionEndReason, type ACPPermissionRequest, ACPServer, type ACPServerOptions, BandACPServerAdapter, type BandACPServerAdapterOptions, GatewayServer, GatewayServerLike, GatewayServerOptions, createGatewayServer };
|
package/dist/adapters.js
CHANGED
|
@@ -35,7 +35,7 @@ import "./chunk-XWHWJP4I.js";
|
|
|
35
35
|
import {
|
|
36
36
|
ACPClientHistoryConverter,
|
|
37
37
|
ACPServerHistoryConverter
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-V5TSWS7P.js";
|
|
39
39
|
import {
|
|
40
40
|
A2AAdapter,
|
|
41
41
|
A2AGatewayAdapter,
|
|
@@ -158,7 +158,13 @@ function normalizeMcpServers(mcpServers) {
|
|
|
158
158
|
// src/adapters/acp/client.ts
|
|
159
159
|
var BandACPClient = class {
|
|
160
160
|
sessionChunks = /* @__PURE__ */ new Map();
|
|
161
|
-
|
|
161
|
+
permissionHandler;
|
|
162
|
+
// The handler is connection-scoped and required at construction, so it is
|
|
163
|
+
// already in place before the agent process is spawned: there is no window
|
|
164
|
+
// in which a `session/request_permission` has nowhere to go.
|
|
165
|
+
constructor(permissionHandler) {
|
|
166
|
+
this.permissionHandler = permissionHandler;
|
|
167
|
+
}
|
|
162
168
|
async sessionUpdate(params) {
|
|
163
169
|
const chunk = toCollectedChunk(params.update);
|
|
164
170
|
if (!chunk) {
|
|
@@ -169,26 +175,12 @@ var BandACPClient = class {
|
|
|
169
175
|
this.sessionChunks.set(params.sessionId, existing);
|
|
170
176
|
}
|
|
171
177
|
async requestPermission(params) {
|
|
172
|
-
|
|
173
|
-
if (handler) {
|
|
174
|
-
return handler(params);
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
outcome: {
|
|
178
|
-
outcome: "cancelled"
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
setPermissionHandler(sessionId, handler) {
|
|
183
|
-
if (!handler) {
|
|
184
|
-
this.permissionHandlers.delete(sessionId);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
this.permissionHandlers.set(sessionId, handler);
|
|
178
|
+
return this.permissionHandler(params);
|
|
188
179
|
}
|
|
189
|
-
|
|
180
|
+
// Named for the one thing it clears: collected chunks are per-turn, and a
|
|
181
|
+
// per-turn caller must not be able to reach anything with a longer life.
|
|
182
|
+
resetChunks(sessionId) {
|
|
190
183
|
this.sessionChunks.delete(sessionId);
|
|
191
|
-
this.permissionHandlers.delete(sessionId);
|
|
192
184
|
}
|
|
193
185
|
getCollectedText(sessionId) {
|
|
194
186
|
return this.getCollectedChunks(sessionId).filter((chunk) => chunk.chunkType === "text").map((chunk) => chunk.content).join("");
|
|
@@ -382,10 +374,18 @@ var ACPClientAdapter = class extends SimpleAdapter {
|
|
|
382
374
|
clientCapabilities;
|
|
383
375
|
connectionFactory;
|
|
384
376
|
roomToSession = /* @__PURE__ */ new Map();
|
|
377
|
+
sessionToRoom = /* @__PURE__ */ new Map();
|
|
385
378
|
roomTools = /* @__PURE__ */ new Map();
|
|
386
379
|
activeSessions = /* @__PURE__ */ new Set();
|
|
387
380
|
bootstrappedSessions = /* @__PURE__ */ new Set();
|
|
388
381
|
pendingPermissions = /* @__PURE__ */ new Map();
|
|
382
|
+
sessionsInFlight = /* @__PURE__ */ new Map();
|
|
383
|
+
roomTurnLocks = /* @__PURE__ */ new Map();
|
|
384
|
+
// Bumped each time a room starts a *new* establishment (never on a
|
|
385
|
+
// coalesced reuse) and whenever a room is torn down. An establishment
|
|
386
|
+
// captures its own value at the start; if the room has moved on by the
|
|
387
|
+
// time it would link/activate a session, it was superseded and must not.
|
|
388
|
+
roomGeneration = /* @__PURE__ */ new Map();
|
|
389
389
|
resolvePermission;
|
|
390
390
|
resolveSessionMode;
|
|
391
391
|
permissionTimeoutMs;
|
|
@@ -439,17 +439,16 @@ var ACPClientAdapter = class extends SimpleAdapter {
|
|
|
439
439
|
this.rehydrate(history);
|
|
440
440
|
}
|
|
441
441
|
this.roomTools.set(context.roomId, tools);
|
|
442
|
+
await this.withRoomTurnLock(context.roomId, () => this.runTurn(message, tools, participantsMessage, contactsMessage, context));
|
|
443
|
+
}
|
|
444
|
+
async runTurn(message, tools, participantsMessage, contactsMessage, context) {
|
|
442
445
|
const connection = await this.ensureConnection();
|
|
443
446
|
const client = this.client;
|
|
444
447
|
if (!client) {
|
|
445
448
|
throw new Error("ACP client was not initialized");
|
|
446
449
|
}
|
|
447
450
|
const sessionId = await this.getOrCreateSession(context.roomId, connection);
|
|
448
|
-
client.
|
|
449
|
-
client.setPermissionHandler(
|
|
450
|
-
sessionId,
|
|
451
|
-
(params) => this.handlePermissionRequest(tools, context.roomId, params)
|
|
452
|
-
);
|
|
451
|
+
client.resetChunks(sessionId);
|
|
453
452
|
const content = replaceUuidMentions(message.content, mentionSubjectsFromMetadata(message.metadata));
|
|
454
453
|
const messageWithContext = [...systemUpdateParts(participantsMessage, contactsMessage), content].join("\n\n");
|
|
455
454
|
const promptText = this.bootstrappedSessions.has(sessionId) ? messageWithContext : `${this.buildSystemContext(context.roomId, message)}
|
|
@@ -482,15 +481,28 @@ ${messageWithContext}`;
|
|
|
482
481
|
acp_client_room_id: context.roomId
|
|
483
482
|
});
|
|
484
483
|
}
|
|
484
|
+
// A per-room async mutex: `fn` for a given `roomId` never overlaps another
|
|
485
|
+
// call for that same room, while different rooms stay fully concurrent.
|
|
486
|
+
// The tracked tail (`this.roomTurnLocks`) always settles — via the
|
|
487
|
+
// trailing `.catch` — so one turn's failure can't wedge every later turn
|
|
488
|
+
// for the room; the real result/rejection is still `run`, returned to this
|
|
489
|
+
// call's own caller.
|
|
490
|
+
async withRoomTurnLock(roomId, fn) {
|
|
491
|
+
const previous = this.roomTurnLocks.get(roomId) ?? Promise.resolve();
|
|
492
|
+
const run = previous.then(fn, fn);
|
|
493
|
+
this.roomTurnLocks.set(roomId, run.catch(() => void 0));
|
|
494
|
+
return run;
|
|
495
|
+
}
|
|
485
496
|
async onCleanup(roomId) {
|
|
486
|
-
const sessionId = this.
|
|
487
|
-
this.roomToSession.delete(roomId);
|
|
497
|
+
const sessionId = this.unlinkRoom(roomId);
|
|
488
498
|
this.roomTools.delete(roomId);
|
|
499
|
+
this.sessionsInFlight.delete(roomId);
|
|
500
|
+
this.roomTurnLocks.delete(roomId);
|
|
501
|
+
this.nextRoomGeneration(roomId);
|
|
489
502
|
if (sessionId) {
|
|
490
503
|
this.activeSessions.delete(sessionId);
|
|
491
504
|
this.bootstrappedSessions.delete(sessionId);
|
|
492
|
-
this.
|
|
493
|
-
this.cancelPendingPermissions(sessionId);
|
|
505
|
+
this.cancelPendingPermissions(sessionId, "room-closed");
|
|
494
506
|
}
|
|
495
507
|
}
|
|
496
508
|
async onRuntimeStop() {
|
|
@@ -502,8 +514,14 @@ ${messageWithContext}`;
|
|
|
502
514
|
this.activeSessions.clear();
|
|
503
515
|
this.bootstrappedSessions.clear();
|
|
504
516
|
this.roomToSession.clear();
|
|
517
|
+
this.sessionToRoom.clear();
|
|
505
518
|
this.roomTools.clear();
|
|
506
|
-
this.
|
|
519
|
+
this.sessionsInFlight.clear();
|
|
520
|
+
this.roomTurnLocks.clear();
|
|
521
|
+
for (const roomId of this.roomGeneration.keys()) {
|
|
522
|
+
this.nextRoomGeneration(roomId);
|
|
523
|
+
}
|
|
524
|
+
this.cancelAllPendingPermissions("adapter-stopped");
|
|
507
525
|
this.client = null;
|
|
508
526
|
this.connection = null;
|
|
509
527
|
if (this.backend) {
|
|
@@ -520,10 +538,66 @@ ${messageWithContext}`;
|
|
|
520
538
|
rehydrate(history) {
|
|
521
539
|
for (const [roomId, sessionId] of Object.entries(history.roomToSession)) {
|
|
522
540
|
if (!this.roomToSession.has(roomId)) {
|
|
523
|
-
this.
|
|
541
|
+
this.linkSession(roomId, sessionId);
|
|
524
542
|
}
|
|
525
543
|
}
|
|
526
544
|
}
|
|
545
|
+
// The only writer of both session maps, so they cannot drift: replacing a
|
|
546
|
+
// room's session drops the old session's route, and a session id already
|
|
547
|
+
// routed to another room is refused rather than silently re-pointed — two
|
|
548
|
+
// rooms sharing one session id would make its permission requests
|
|
549
|
+
// unattributable. Returns whether the link was made — a caller that goes
|
|
550
|
+
// on to activate/configure/prompt a session regardless of a `false` here
|
|
551
|
+
// would use a session this room was refused, not just fail to route its
|
|
552
|
+
// permissions.
|
|
553
|
+
linkSession(roomId, sessionId) {
|
|
554
|
+
const routedRoomId = this.sessionToRoom.get(sessionId);
|
|
555
|
+
if (routedRoomId !== void 0 && routedRoomId !== roomId) {
|
|
556
|
+
this.safeWarn("refusing to route one ACP session to a second room", {
|
|
557
|
+
sessionId,
|
|
558
|
+
roomId,
|
|
559
|
+
routedRoomId
|
|
560
|
+
});
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
const replacedSessionId = this.roomToSession.get(roomId);
|
|
564
|
+
if (replacedSessionId !== void 0 && replacedSessionId !== sessionId) {
|
|
565
|
+
this.sessionToRoom.delete(replacedSessionId);
|
|
566
|
+
}
|
|
567
|
+
this.roomToSession.set(roomId, sessionId);
|
|
568
|
+
this.sessionToRoom.set(sessionId, roomId);
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
nextRoomGeneration(roomId) {
|
|
572
|
+
const next = (this.roomGeneration.get(roomId) ?? 0) + 1;
|
|
573
|
+
this.roomGeneration.set(roomId, next);
|
|
574
|
+
return next;
|
|
575
|
+
}
|
|
576
|
+
isCurrentGeneration(roomId, generation) {
|
|
577
|
+
return this.roomGeneration.get(roomId) === generation;
|
|
578
|
+
}
|
|
579
|
+
// The installed ACP SDK's `sendRequest` never rejects a pending call when
|
|
580
|
+
// its connection closes (no server response ever arrives to reject it
|
|
581
|
+
// with) — so a session-establishment RPC in flight when the subprocess
|
|
582
|
+
// dies would otherwise hang forever, wedging the room's `sessionsInFlight`
|
|
583
|
+
// entry along with it. Racing every such RPC against the connection's own
|
|
584
|
+
// `closed` promise gives it a real, prompt failure instead.
|
|
585
|
+
raceAgainstConnectionClose(connection, operation) {
|
|
586
|
+
let reject = () => void 0;
|
|
587
|
+
const closedRejection = new Promise((_resolve, rejectFn) => {
|
|
588
|
+
reject = rejectFn;
|
|
589
|
+
});
|
|
590
|
+
void connection.closed.then(() => reject(new Error("ACP connection closed while a session operation was still in flight")));
|
|
591
|
+
return Promise.race([operation, closedRejection]);
|
|
592
|
+
}
|
|
593
|
+
unlinkRoom(roomId) {
|
|
594
|
+
const sessionId = this.roomToSession.get(roomId);
|
|
595
|
+
this.roomToSession.delete(roomId);
|
|
596
|
+
if (sessionId !== void 0) {
|
|
597
|
+
this.sessionToRoom.delete(sessionId);
|
|
598
|
+
}
|
|
599
|
+
return sessionId;
|
|
600
|
+
}
|
|
527
601
|
async ensureConnection() {
|
|
528
602
|
if (this.connection && !this.connection.signal.aborted) {
|
|
529
603
|
return this.connection;
|
|
@@ -543,7 +617,7 @@ ${messageWithContext}`;
|
|
|
543
617
|
}
|
|
544
618
|
async spawnConnection() {
|
|
545
619
|
const acp = await acpModule.get();
|
|
546
|
-
const client = new BandACPClient();
|
|
620
|
+
const client = new BandACPClient((params) => this.routePermissionRequest(params));
|
|
547
621
|
const handle = await this.connectionFactory(client, {
|
|
548
622
|
command: this.command,
|
|
549
623
|
cwd: this.cwd,
|
|
@@ -569,6 +643,7 @@ ${messageWithContext}`;
|
|
|
569
643
|
this.connectionHandle = null;
|
|
570
644
|
this.connectionState = null;
|
|
571
645
|
this.activeSessions.clear();
|
|
646
|
+
this.cancelAllPendingPermissions("connection-lost");
|
|
572
647
|
}
|
|
573
648
|
});
|
|
574
649
|
return connection;
|
|
@@ -578,25 +653,56 @@ ${messageWithContext}`;
|
|
|
578
653
|
if (existingSessionId && this.activeSessions.has(existingSessionId)) {
|
|
579
654
|
return existingSessionId;
|
|
580
655
|
}
|
|
656
|
+
const inFlight = this.sessionsInFlight.get(roomId);
|
|
657
|
+
if (inFlight) {
|
|
658
|
+
return inFlight;
|
|
659
|
+
}
|
|
660
|
+
const generation = this.nextRoomGeneration(roomId);
|
|
661
|
+
const establishing = this.establishSession(roomId, existingSessionId, connection, generation);
|
|
662
|
+
establishing.finally(() => {
|
|
663
|
+
if (this.sessionsInFlight.get(roomId) === establishing) {
|
|
664
|
+
this.sessionsInFlight.delete(roomId);
|
|
665
|
+
}
|
|
666
|
+
}).catch(() => void 0);
|
|
667
|
+
this.sessionsInFlight.set(roomId, establishing);
|
|
668
|
+
return establishing;
|
|
669
|
+
}
|
|
670
|
+
async establishSession(roomId, existingSessionId, connection, generation) {
|
|
581
671
|
const mcpServers = await this.buildSessionMcpServers();
|
|
582
672
|
if (existingSessionId) {
|
|
583
673
|
const restored = await this.tryRestoreSession(connection, existingSessionId, mcpServers);
|
|
584
674
|
if (restored.ok) {
|
|
675
|
+
this.linkOrAbandon(roomId, existingSessionId, generation);
|
|
585
676
|
this.activeSessions.add(existingSessionId);
|
|
586
677
|
this.bootstrappedSessions.add(existingSessionId);
|
|
587
678
|
await this.configureSessionMode(roomId, existingSessionId, restored.modes, connection);
|
|
588
679
|
return existingSessionId;
|
|
589
680
|
}
|
|
590
681
|
}
|
|
591
|
-
const created = await connection.newSession({
|
|
682
|
+
const created = await this.raceAgainstConnectionClose(connection, connection.newSession({
|
|
592
683
|
cwd: this.cwd,
|
|
593
684
|
mcpServers
|
|
594
|
-
});
|
|
595
|
-
this.
|
|
685
|
+
}));
|
|
686
|
+
this.linkOrAbandon(roomId, created.sessionId, generation);
|
|
596
687
|
this.activeSessions.add(created.sessionId);
|
|
597
688
|
await this.configureSessionMode(roomId, created.sessionId, created.modes, connection);
|
|
598
689
|
return created.sessionId;
|
|
599
690
|
}
|
|
691
|
+
// The single gate an establishment must pass before it's allowed to claim
|
|
692
|
+
// the room: it must still be the room's current generation (not
|
|
693
|
+
// superseded by a teardown or a fresher establishment while this one was
|
|
694
|
+
// awaiting an RPC), and its session id must not already belong to another
|
|
695
|
+
// room. Either failure throws — this establishment cannot silently
|
|
696
|
+
// continue to activate, configure, and prompt a session it has no right
|
|
697
|
+
// to use for this room.
|
|
698
|
+
linkOrAbandon(roomId, sessionId, generation) {
|
|
699
|
+
if (!this.isCurrentGeneration(roomId, generation)) {
|
|
700
|
+
throw new Error(`ACP session establishment for room "${roomId}" was superseded before it could be linked`);
|
|
701
|
+
}
|
|
702
|
+
if (!this.linkSession(roomId, sessionId)) {
|
|
703
|
+
throw new Error(`ACP session "${sessionId}" could not be linked to room "${roomId}": already routed elsewhere`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
600
706
|
// Best-effort: never throws, so a mode switch going wrong can't take a
|
|
601
707
|
// session establishment down with it.
|
|
602
708
|
async configureSessionMode(roomId, sessionId, modes, connection) {
|
|
@@ -675,7 +781,7 @@ ${messageWithContext}`;
|
|
|
675
781
|
return { ok: false };
|
|
676
782
|
}
|
|
677
783
|
try {
|
|
678
|
-
const restored = await restore();
|
|
784
|
+
const restored = await this.raceAgainstConnectionClose(connection, restore());
|
|
679
785
|
return { ok: true, modes: restored?.modes };
|
|
680
786
|
} catch {
|
|
681
787
|
return { ok: false };
|
|
@@ -788,6 +894,36 @@ ${messageWithContext}`;
|
|
|
788
894
|
"All Band MCP tool calls must include room_id."
|
|
789
895
|
].join("\n");
|
|
790
896
|
}
|
|
897
|
+
// The connection's single permission entry point, and total by
|
|
898
|
+
// construction: every path resolves, nothing throws, and a request that
|
|
899
|
+
// can't be attributed to a live room is cancelled *and* warned rather than
|
|
900
|
+
// silently declined. `activeSessions` is the gate that keeps a dead
|
|
901
|
+
// session from raising a live prompt — `roomToSession` deliberately
|
|
902
|
+
// outlives a dropped connection so the session can be restored later.
|
|
903
|
+
async routePermissionRequest(params) {
|
|
904
|
+
const isActive = this.activeSessions.has(params.sessionId);
|
|
905
|
+
const roomId = isActive ? this.sessionToRoom.get(params.sessionId) : void 0;
|
|
906
|
+
const tools = roomId === void 0 ? void 0 : this.roomTools.get(roomId);
|
|
907
|
+
if (roomId === void 0 || !tools) {
|
|
908
|
+
this.safeWarn("cancelling a permission request that maps to no live room", {
|
|
909
|
+
sessionId: params.sessionId,
|
|
910
|
+
toolName: params.toolCall?.title,
|
|
911
|
+
sessionActive: isActive,
|
|
912
|
+
roomId
|
|
913
|
+
});
|
|
914
|
+
return { outcome: { outcome: "cancelled" } };
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
return await this.handlePermissionRequest(tools, roomId, params);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
this.safeWarn("permission handling failed; cancelling the request", {
|
|
920
|
+
sessionId: params.sessionId,
|
|
921
|
+
roomId,
|
|
922
|
+
error: String(error)
|
|
923
|
+
});
|
|
924
|
+
return { outcome: { outcome: "cancelled" } };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
791
927
|
async handlePermissionRequest(tools, roomId, params) {
|
|
792
928
|
const toolName = params.toolCall.title ?? "unknown";
|
|
793
929
|
const autoSelection = this.resolvePermission ? void 0 : choosePermissionOption(params.options);
|
|
@@ -795,28 +931,52 @@ ${messageWithContext}`;
|
|
|
795
931
|
if (controller) {
|
|
796
932
|
this.trackPending(params.sessionId, controller);
|
|
797
933
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
// can take up to `permissionTimeoutMs`.
|
|
934
|
+
let requestEventFailed = false;
|
|
935
|
+
const [, resolvedChosenId] = await Promise.all([
|
|
936
|
+
// Started immediately rather than serialized in front of a manual
|
|
937
|
+
// wait that can take up to `permissionTimeoutMs`.
|
|
803
938
|
tools.sendEvent(`Permission requested: ${toolName}`, "tool_call", {
|
|
804
939
|
permission_request: true,
|
|
805
940
|
tool_name: toolName,
|
|
806
941
|
tool_call_id: params.toolCall.toolCallId,
|
|
807
942
|
acp_session_id: params.sessionId,
|
|
808
943
|
auto_allowed: autoSelection !== void 0 && autoSelection !== null
|
|
944
|
+
}).catch((error) => {
|
|
945
|
+
requestEventFailed = true;
|
|
946
|
+
this.safeWarn("failed to post the permission-requested event; cancelling the request", {
|
|
947
|
+
roomId,
|
|
948
|
+
sessionId: params.sessionId,
|
|
949
|
+
error: String(error)
|
|
950
|
+
});
|
|
951
|
+
if (controller) {
|
|
952
|
+
this.abandon(controller, "no-answer");
|
|
953
|
+
}
|
|
809
954
|
}),
|
|
810
|
-
controller ? this.resolveManually(
|
|
955
|
+
controller ? this.resolveManually(roomId, params, controller) : Promise.resolve(autoSelection?.optionId)
|
|
811
956
|
]);
|
|
812
|
-
|
|
957
|
+
const chosenId = requestEventFailed ? void 0 : resolvedChosenId;
|
|
958
|
+
const response = this.toResponse(chosenId, params.options, { roomId, sessionId: params.sessionId });
|
|
959
|
+
if (controller && !controller.signal.aborted) {
|
|
960
|
+
this.abandon(controller, response.outcome.outcome === "selected" ? "settled" : "no-answer");
|
|
961
|
+
}
|
|
962
|
+
return response;
|
|
813
963
|
}
|
|
814
964
|
// `undefined`, or an id absent from this request's own `options` (a buggy
|
|
815
965
|
// or stale caller), both map to `cancelled` — never silently treated as a
|
|
816
966
|
// deny. A real match, reject-kind options included, maps to `selected`.
|
|
817
|
-
toResponse(chosenId, options) {
|
|
818
|
-
|
|
819
|
-
|
|
967
|
+
toResponse(chosenId, options, context) {
|
|
968
|
+
if (chosenId === void 0) {
|
|
969
|
+
return { outcome: { outcome: "cancelled" } };
|
|
970
|
+
}
|
|
971
|
+
if (!options.some((option) => option.optionId === chosenId)) {
|
|
972
|
+
this.safeWarn("resolvePermission chose an option this request does not offer", {
|
|
973
|
+
...context,
|
|
974
|
+
chosenId,
|
|
975
|
+
optionIds: options.map((option) => option.optionId)
|
|
976
|
+
});
|
|
977
|
+
return { outcome: { outcome: "cancelled" } };
|
|
978
|
+
}
|
|
979
|
+
return { outcome: { outcome: "selected", optionId: chosenId } };
|
|
820
980
|
}
|
|
821
981
|
// A caller-supplied `Logger` isn't guaranteed to be synchronous or
|
|
822
982
|
// non-throwing. Every best-effort warning in this file routes through here
|
|
@@ -830,40 +990,55 @@ ${messageWithContext}`;
|
|
|
830
990
|
} catch {
|
|
831
991
|
}
|
|
832
992
|
}
|
|
833
|
-
// Races the caller-supplied resolver against
|
|
834
|
-
//
|
|
835
|
-
// `cancelPendingPermissions
|
|
836
|
-
// `
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
async resolveManually(
|
|
841
|
-
|
|
842
|
-
const cancelled = new Promise((resolve) => {
|
|
993
|
+
// Races the caller-supplied resolver against `controller`'s abort signal,
|
|
994
|
+
// which is the request's single termination channel: the timeout below
|
|
995
|
+
// fires it with `"timeout"`, and `cancelPendingPermissions` /
|
|
996
|
+
// `cancelAllPendingPermissions` fire it with the reason their caller
|
|
997
|
+
// supplies. `controller` is the same object tracked in `pendingPermissions`,
|
|
998
|
+
// so there is exactly one cancellation channel here, not a second
|
|
999
|
+
// hand-rolled one alongside it.
|
|
1000
|
+
async resolveManually(roomId, params, controller) {
|
|
1001
|
+
const abandoned = new Promise((resolve) => {
|
|
843
1002
|
controller.signal.addEventListener("abort", () => resolve(void 0));
|
|
844
1003
|
});
|
|
1004
|
+
const timer = setTimeout(() => this.abandon(controller, "timeout"), this.permissionTimeoutMs);
|
|
845
1005
|
try {
|
|
846
|
-
const timeout = new Promise((resolve) => {
|
|
847
|
-
timer = setTimeout(() => resolve(void 0), this.permissionTimeoutMs);
|
|
848
|
-
});
|
|
849
1006
|
return await Promise.race([
|
|
850
1007
|
// `resolvePermission` is caller-supplied; nothing guarantees it's
|
|
851
1008
|
// `async` or otherwise well-behaved. `Promise.resolve().then(...)`
|
|
852
1009
|
// normalizes a synchronous throw the same way it normalizes a
|
|
853
1010
|
// rejected promise, so both land in the `.catch` below rather than
|
|
854
1011
|
// escaping this race uncaught.
|
|
855
|
-
Promise.resolve().then(() => this.resolvePermission(params, controller.signal)).catch((error) => {
|
|
1012
|
+
Promise.resolve().then(() => this.resolvePermission({ ...params, roomId }, controller.signal)).then((chosenId) => this.discardLateAnswer(chosenId, controller, roomId, params.sessionId)).catch((error) => {
|
|
856
1013
|
this.safeWarn("resolvePermission threw; treating as no answer", { error: String(error) });
|
|
857
1014
|
return void 0;
|
|
858
1015
|
}),
|
|
859
|
-
|
|
860
|
-
cancelled
|
|
1016
|
+
abandoned
|
|
861
1017
|
]);
|
|
862
1018
|
} finally {
|
|
863
1019
|
clearTimeout(timer);
|
|
864
|
-
this.untrackPending(sessionId, controller);
|
|
865
|
-
|
|
1020
|
+
this.untrackPending(params.sessionId, controller);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// An answer that lands after the request was given up on can no longer be
|
|
1024
|
+
// honoured — the response has already gone back to the agent. It is dropped
|
|
1025
|
+
// either way; warning is what makes "my click did nothing" explicable.
|
|
1026
|
+
discardLateAnswer(chosenId, controller, roomId, sessionId) {
|
|
1027
|
+
if (!controller.signal.aborted || chosenId === void 0) {
|
|
1028
|
+
return chosenId;
|
|
866
1029
|
}
|
|
1030
|
+
this.safeWarn("resolvePermission answered after the request was abandoned; discarding", {
|
|
1031
|
+
roomId,
|
|
1032
|
+
sessionId,
|
|
1033
|
+
chosenId,
|
|
1034
|
+
reason: String(controller.signal.reason)
|
|
1035
|
+
});
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
// The only place a permission's controller is ever aborted, so every
|
|
1039
|
+
// `signal.reason` a consumer can observe comes from the documented union.
|
|
1040
|
+
abandon(controller, reason) {
|
|
1041
|
+
controller.abort(reason);
|
|
867
1042
|
}
|
|
868
1043
|
trackPending(sessionId, controller) {
|
|
869
1044
|
const pending = this.pendingPermissions.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
@@ -877,14 +1052,14 @@ ${messageWithContext}`;
|
|
|
877
1052
|
this.pendingPermissions.delete(sessionId);
|
|
878
1053
|
}
|
|
879
1054
|
}
|
|
880
|
-
cancelPendingPermissions(sessionId) {
|
|
1055
|
+
cancelPendingPermissions(sessionId, reason) {
|
|
881
1056
|
for (const controller of this.pendingPermissions.get(sessionId) ?? []) {
|
|
882
|
-
|
|
1057
|
+
this.abandon(controller, reason);
|
|
883
1058
|
}
|
|
884
1059
|
}
|
|
885
|
-
cancelAllPendingPermissions() {
|
|
1060
|
+
cancelAllPendingPermissions(reason) {
|
|
886
1061
|
for (const sessionId of this.pendingPermissions.keys()) {
|
|
887
|
-
this.cancelPendingPermissions(sessionId);
|
|
1062
|
+
this.cancelPendingPermissions(sessionId, reason);
|
|
888
1063
|
}
|
|
889
1064
|
}
|
|
890
1065
|
async flushChunks(input) {
|
|
@@ -3,6 +3,9 @@ var ACPClientHistoryConverter = class {
|
|
|
3
3
|
convert(raw) {
|
|
4
4
|
const roomToSession = {};
|
|
5
5
|
for (const entry of raw) {
|
|
6
|
+
if (entry.message_type !== "task") {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
6
9
|
const metadataRaw = entry.metadata;
|
|
7
10
|
if (!metadataRaw || typeof metadataRaw !== "object" || Array.isArray(metadataRaw)) {
|
|
8
11
|
continue;
|
|
@@ -10,7 +13,7 @@ var ACPClientHistoryConverter = class {
|
|
|
10
13
|
const metadata = metadataRaw;
|
|
11
14
|
const sessionId = metadata.acp_client_session_id;
|
|
12
15
|
const roomId = metadata.acp_client_room_id;
|
|
13
|
-
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId) {
|
|
16
|
+
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId && roomId === entry.room_id) {
|
|
14
17
|
roomToSession[roomId] = sessionId;
|
|
15
18
|
}
|
|
16
19
|
}
|
package/dist/converters.cjs
CHANGED
|
@@ -46,6 +46,9 @@ var ACPClientHistoryConverter = class {
|
|
|
46
46
|
convert(raw) {
|
|
47
47
|
const roomToSession = {};
|
|
48
48
|
for (const entry of raw) {
|
|
49
|
+
if (entry.message_type !== "task") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
49
52
|
const metadataRaw = entry.metadata;
|
|
50
53
|
if (!metadataRaw || typeof metadataRaw !== "object" || Array.isArray(metadataRaw)) {
|
|
51
54
|
continue;
|
|
@@ -53,7 +56,7 @@ var ACPClientHistoryConverter = class {
|
|
|
53
56
|
const metadata = metadataRaw;
|
|
54
57
|
const sessionId = metadata.acp_client_session_id;
|
|
55
58
|
const roomId = metadata.acp_client_room_id;
|
|
56
|
-
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId) {
|
|
59
|
+
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId && roomId === entry.room_id) {
|
|
57
60
|
roomToSession[roomId] = sessionId;
|
|
58
61
|
}
|
|
59
62
|
}
|
package/dist/converters.js
CHANGED