dsh-ntr 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +18 -18
- package/lib/bridge.js +120 -291
- package/lib/client/acp-ui.js +60 -32
- package/lib/client/index.js +17 -0
- package/lib/client/panel.js +37 -42
- package/lib/client/permission-dock.js +72 -0
- package/lib/client/session-controls.js +4 -4
- package/lib/client/store.js +58 -12
- package/lib/client.js +296 -95
- package/lib/client.js.map +1 -1
- package/lib/impl.js +30 -19
- package/lib/index.js +6 -51
- package/lib/types/bridge.d.ts +60 -154
- package/lib/types/client/panel.d.ts +12 -19
- package/lib/types/client/permission-dock.d.ts +31 -0
- package/lib/types/client/store.d.ts +42 -12
- package/lib/types/impl.d.ts +31 -17
- package/lib/types/protocol.d.ts +27 -12
- package/package.json +1 -1
- package/src/bridge.ts +147 -357
- package/src/client/acp-ui.tsx +60 -32
- package/src/client/index.tsx +18 -0
- package/src/client/panel.tsx +54 -58
- package/src/client/permission-dock.tsx +133 -0
- package/src/client/session-controls.tsx +4 -4
- package/src/client/store.ts +81 -25
- package/src/impl.ts +49 -29
- package/src/index.ts +6 -65
- package/src/protocol.ts +32 -12
package/src/bridge.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
parseCatalogOptions,
|
|
46
46
|
usableConfigOptions,
|
|
47
47
|
permissionToolKind,
|
|
48
|
+
permissionRequestFrame,
|
|
48
49
|
pickAutoOption,
|
|
49
50
|
quoteCommandToken,
|
|
50
51
|
randomUUID,
|
|
@@ -68,7 +69,7 @@ import type {
|
|
|
68
69
|
RosterPresetRow,
|
|
69
70
|
} from './impl.ts'
|
|
70
71
|
import type {
|
|
71
|
-
AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpMcpServerDef, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike,
|
|
72
|
+
AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike,
|
|
72
73
|
LlmModelInfo, LlmModelReasoningInfo, LlmProviderInfo, LlmReasoningEffortInfo, LlmResolvedModelInfo,
|
|
73
74
|
ApprovalDeadlineKey, ApprovalDeadlinePatch, PermissionOutcome, PermissionOptionView, PermissionTimeoutMode, PreparedAdapterCall, SessionView, StreamChunk,
|
|
74
75
|
} from './protocol.ts'
|
|
@@ -116,9 +117,9 @@ interface LiveSession {
|
|
|
116
117
|
released: boolean
|
|
117
118
|
/**
|
|
118
119
|
* Owning DSH chat session id, when the session was opened through the LLM
|
|
119
|
-
* adapter (`AcpLlmAdapter`).
|
|
120
|
-
*
|
|
121
|
-
*
|
|
120
|
+
* adapter (`AcpLlmAdapter`). It is what the conversation's permission card
|
|
121
|
+
* filters on (`permission/request.dshSessionId`); sessions started over the
|
|
122
|
+
* `/acp` RPC have no chat session, so their asks stay on the settings panel.
|
|
122
123
|
*/
|
|
123
124
|
dshSessionId?: string
|
|
124
125
|
/**
|
|
@@ -176,21 +177,24 @@ export interface BridgeTuning {
|
|
|
176
177
|
/**
|
|
177
178
|
* How long a permission ask may sit unanswered before the deadline fires.
|
|
178
179
|
*
|
|
179
|
-
* ONE window for
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* anyone there?"
|
|
183
|
-
* card
|
|
180
|
+
* ONE window for every ask. Every ask is broadcast as `permission/request`
|
|
181
|
+
* and answered from the conversation's dock card; an `/acp`-RPC ask has no
|
|
182
|
+
* conversation, so the settings page mirrors the same ask as its answer
|
|
183
|
+
* entry. One question, one window: "is anyone there?" gets the same answer
|
|
184
|
+
* wherever the card is rendered, and two numbers made the settings card look
|
|
185
|
+
* like two unrelated knobs.
|
|
184
186
|
*
|
|
185
|
-
* `0` means
|
|
186
|
-
*
|
|
187
|
+
* `0` means 不等待, not "wait forever": `auto` settles the ask on arrival,
|
|
188
|
+
* `abort` floors the window to the shortest real wait (refusing every ask on
|
|
189
|
+
* arrival is a brick wall, not a timeout), and `wait` ignores the window
|
|
190
|
+
* entirely.
|
|
187
191
|
*
|
|
188
|
-
* The deadline is what keeps a turn from hanging on a question nobody
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
192
|
+
* The deadline is what keeps a turn from hanging on a question nobody
|
|
193
|
+
* answers: the card stays visible, but an unattended session would otherwise
|
|
194
|
+
* wait indefinitely. Past the deadline the ask is settled IN PLACE — never
|
|
195
|
+
* relocated: `abort` refuses it, `auto` answers it with the option the
|
|
196
|
+
* agent's own kinds imply (broadest grant first), and `wait` never expires
|
|
197
|
+
* it at all.
|
|
194
198
|
*
|
|
195
199
|
* Default 300000 ms. OmniTerm's equivalent is its reaper's permission-timeout
|
|
196
200
|
* threshold (30 min default, 30 s steps, hot-updatable).
|
|
@@ -246,52 +250,6 @@ export interface BridgeTuning {
|
|
|
246
250
|
probeIdleTimeoutMs?: number
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
/** One native approval question, as the host approval service defines it. */
|
|
250
|
-
export interface ApprovalRequestContext {
|
|
251
|
-
/** DSH chat session that owns the open turn the question is asked in. */
|
|
252
|
-
dshSessionId?: string
|
|
253
|
-
agentId: string
|
|
254
|
-
/** Human-facing identity of the operation awaiting a decision. */
|
|
255
|
-
toolName: string
|
|
256
|
-
reason?: string
|
|
257
|
-
/** The ACP tool call this ask belongs to; the host card attaches its detail to it. */
|
|
258
|
-
callId?: string
|
|
259
|
-
signal?: AbortSignal
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Outcome vocabulary of the host approval chain (`'unavailable'` = no answerer). */
|
|
263
|
-
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Why one permission ask rides the settings-panel ledger instead of the native
|
|
267
|
-
* approval chain, recorded when the ask is broadcast.
|
|
268
|
-
*
|
|
269
|
-
* `no-requester` and `no-chat-session` are the DESIGNED ledger paths: a host
|
|
270
|
-
* without the approval service, and an `/acp`-RPC session that has no chat
|
|
271
|
-
* turn to route into. `unavailable` is a DEGRADATION — the service is wired
|
|
272
|
-
* and this ask still failed, so the conversation the user is looking at will
|
|
273
|
-
* never show the question. The reason decides the chat line's wording (see
|
|
274
|
-
* `BridgeManager.streamPrompt`); the deadline and the mode decide what
|
|
275
|
-
* happens if nobody answers ({@link AgentBridge.handlePermissionRequest}).
|
|
276
|
-
*/
|
|
277
|
-
export type LedgerFallbackReason = 'no-requester' | 'no-chat-session' | 'unavailable'
|
|
278
|
-
|
|
279
|
-
/** One recorded ledger fallback: why the ask is here, and the deadline armed for it. */
|
|
280
|
-
export interface LedgerFallback {
|
|
281
|
-
reason: LedgerFallbackReason
|
|
282
|
-
/** Effective deadline in ms; `0` = no timer (wait for the human). */
|
|
283
|
-
timeoutMs: number
|
|
284
|
-
/** What the deadline does when it fires. */
|
|
285
|
-
mode: PermissionTimeoutMode
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Asks the host approval service (`ctx.approval`) for one decision. Never
|
|
290
|
-
* throws: any failure is reported as `'unavailable'` so the caller can fall
|
|
291
|
-
* back to the settings-panel ledger.
|
|
292
|
-
*/
|
|
293
|
-
export type ApprovalRequester = (request: ApprovalRequestContext) => Promise<ApprovalOutcome>
|
|
294
|
-
|
|
295
253
|
/**
|
|
296
254
|
* Resolves the workspace root of one chat session (`dshSessionId` = DSH session
|
|
297
255
|
* id) — the sidebar's selected workspace, which the host's sandbox policy keeps
|
|
@@ -392,15 +350,6 @@ export class AgentBridge {
|
|
|
392
350
|
/** Inflight `session/load` resumes, shared so concurrent prompts don't double-resume. */
|
|
393
351
|
private readonly resuming = new Map<string, Promise<LiveSession>>()
|
|
394
352
|
readonly permissions = new PermissionLedger()
|
|
395
|
-
/**
|
|
396
|
-
* Why each outstanding ledger ask is on the ledger, keyed by request id and
|
|
397
|
-
* recorded before the `permission/request` broadcast (the hub is
|
|
398
|
-
* synchronous, so a subscriber can read it the moment the frame goes out).
|
|
399
|
-
* Entries live exactly as long as the ask: the ledger's resolve callback
|
|
400
|
-
* deletes them, and every terminal path (answer, timeout, session close,
|
|
401
|
-
* process death) resolves through it.
|
|
402
|
-
*/
|
|
403
|
-
private readonly ledgerFallbacks = new Map<string, LedgerFallback>
|
|
404
353
|
private readonly startupTimeoutMs: number
|
|
405
354
|
/**
|
|
406
355
|
* The ONE approval deadline, and the mode it settles with. Neither is
|
|
@@ -465,13 +414,8 @@ export class AgentBridge {
|
|
|
465
414
|
private readonly onCommands?: (agentId: string, commands: NonNullable<SessionView['availableCommands']>) => void,
|
|
466
415
|
tuning?: BridgeTuning,
|
|
467
416
|
/**
|
|
468
|
-
* Resolved per
|
|
469
|
-
* was built
|
|
470
|
-
*/
|
|
471
|
-
private readonly approvalRequester?: () => ApprovalRequester | undefined,
|
|
472
|
-
/**
|
|
473
|
-
* Resolved per turn, like {@link approvalRequester}: the host entry may
|
|
474
|
-
* wire the access-mode mapping after this bridge was built.
|
|
417
|
+
* Resolved per turn: the host entry may wire the workspace reader after
|
|
418
|
+
* this bridge was built.
|
|
475
419
|
*/
|
|
476
420
|
private readonly sessionWorkspace?: () => SessionWorkspaceResolver | undefined,
|
|
477
421
|
) {
|
|
@@ -1263,12 +1207,15 @@ export class AgentBridge {
|
|
|
1263
1207
|
// Closing a session is activity of its own: restart the idle window here
|
|
1264
1208
|
// rather than at the prompt that opened it.
|
|
1265
1209
|
this.touch()
|
|
1210
|
+
// Answer every outstanding ask BEFORE `session/close` is awaited: an agent
|
|
1211
|
+
// blocked on an unanswered `session/request_permission` would otherwise be
|
|
1212
|
+
// able to hold the close open indefinitely.
|
|
1213
|
+
this.settlePermissions(session)
|
|
1266
1214
|
const conn = this.conn
|
|
1267
1215
|
if (conn !== undefined && this.state === 'ready') {
|
|
1268
1216
|
// `session/close` is an optional capability; the local teardown stands regardless.
|
|
1269
1217
|
await conn.agent.request(methods.agent.session.close, { sessionId: session.acpSessionId }).catch(() => { /* unsupported */ })
|
|
1270
1218
|
}
|
|
1271
|
-
this.settlePermissions(session)
|
|
1272
1219
|
this.sessions.delete(session.sessionId)
|
|
1273
1220
|
this.acpIndex.delete(session.acpSessionId)
|
|
1274
1221
|
this.hub.broadcast({ type: 'session/closed', sessionId: session.sessionId, reason })
|
|
@@ -1333,29 +1280,26 @@ export class AgentBridge {
|
|
|
1333
1280
|
}
|
|
1334
1281
|
|
|
1335
1282
|
/**
|
|
1336
|
-
* Answer one ACP permission ask.
|
|
1337
|
-
*
|
|
1338
|
-
* ledger
|
|
1283
|
+
* Answer one ACP permission ask.
|
|
1284
|
+
*
|
|
1285
|
+
* EVERY ask is answered by a human through the plugin's own ledger. The
|
|
1286
|
+
* host's approval chain is deliberately not involved: it could accept an ask
|
|
1287
|
+
* and render nothing, leaving the turn hanging behind something invisible
|
|
1288
|
+
* with no deadline (issue #2). The ledger broadcasts `permission/request`,
|
|
1289
|
+
* the browser renders the answerable card, and `permission/respond` settles
|
|
1290
|
+
* it.
|
|
1291
|
+
*
|
|
1292
|
+
* The host answers on its own only when a deadline expires: `abort` refuses,
|
|
1293
|
+
* `auto` picks the option the agent's own kinds imply, `wait` never expires.
|
|
1294
|
+
* The ACP request's own `signal` (the agent withdrawing the ask, or the
|
|
1295
|
+
* connection dying) settles the same entry cancelled, so a withdrawn ask can
|
|
1296
|
+
* never leave a stale card behind.
|
|
1339
1297
|
*/
|
|
1340
1298
|
private async handlePermissionRequest(acpSessionId: string, toolCall: unknown, options: PermissionOption[], signal?: AbortSignal): Promise<RequestPermissionResponse> {
|
|
1341
1299
|
const session = this.acpIndex.get(acpSessionId)
|
|
1342
1300
|
if (session === undefined || session.closed) return { outcome: { outcome: 'cancelled' } }
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
const native = await this.askNativeApproval(session, toolCall, options, signal)
|
|
1346
|
-
if ('response' in native) {
|
|
1347
|
-
this.touch()
|
|
1348
|
-
return native.response
|
|
1349
|
-
}
|
|
1350
|
-
// Deadline: how long this ask may sit unanswered. ONE window for every
|
|
1351
|
-
// route — an ask that never reached the conversation included. They have
|
|
1352
|
-
// no other answer entry, so exempting them would be the very invisible
|
|
1353
|
-
// hang this mechanism exists to bound.
|
|
1354
|
-
//
|
|
1355
|
-
// What happens when it fires is the MODE's business: refused, answered
|
|
1356
|
-
// with the option the agent's own kinds imply, or (in `wait`) never.
|
|
1357
|
-
// The mode is read once, here, for the same reason the window is: an ask
|
|
1358
|
-
// that started under one mode keeps it.
|
|
1301
|
+
// The deadline and the mode are read ONCE, here: an ask that started under
|
|
1302
|
+
// one pair keeps it, even if the settings page rewrites them mid-flight.
|
|
1359
1303
|
const mode = this.permissionTimeoutMode
|
|
1360
1304
|
const timeoutMs = this.permissionTimeoutMs
|
|
1361
1305
|
// 不等待(0)只属自动推进:请求到达即按那一档收场,不建 ledger 条目、不发卡
|
|
@@ -1364,11 +1308,28 @@ export class AgentBridge {
|
|
|
1364
1308
|
if (mode === 'auto' && timeoutMs === 0) {
|
|
1365
1309
|
return this.settleExpiredAsk(session, mode, toolCall, options, 0)
|
|
1366
1310
|
}
|
|
1311
|
+
const optionViews = options.map((o): PermissionOptionView => ({ optionId: o.optionId, name: o.name, kind: o.kind }))
|
|
1312
|
+
const reason = permissionAskReason(toolCall)
|
|
1313
|
+
// Computed once, at ask creation: the frame, the chat line and the timer
|
|
1314
|
+
// all describe the same instant, and a slow reader cannot stretch it.
|
|
1315
|
+
const deadlineAt = timeoutMs > 0 && mode !== 'wait' ? Date.now() + timeoutMs : undefined
|
|
1316
|
+
const view = {
|
|
1317
|
+
...(session.dshSessionId === undefined ? {} : { dshSessionId: session.dshSessionId }),
|
|
1318
|
+
// Kept whole for the reconnect replay: a client that connects after the
|
|
1319
|
+
// ask was broadcast must be re-shown the very card it has to answer.
|
|
1320
|
+
toolCall,
|
|
1321
|
+
toolName: permissionToolName(toolCall),
|
|
1322
|
+
...(reason === '' ? {} : { reason }),
|
|
1323
|
+
options: optionViews,
|
|
1324
|
+
timeoutMode: mode,
|
|
1325
|
+
...(deadlineAt === undefined ? {} : { deadlineAt }),
|
|
1326
|
+
}
|
|
1367
1327
|
return new Promise<RequestPermissionResponse>(resolve => {
|
|
1368
1328
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1329
|
+
let detachSignal: (() => void) | undefined
|
|
1369
1330
|
const requestId = this.permissions.create(session.sessionId, (outcome: PermissionOutcome) => {
|
|
1370
1331
|
clearTimeout(timer)
|
|
1371
|
-
|
|
1332
|
+
detachSignal?.()
|
|
1372
1333
|
this.touch()
|
|
1373
1334
|
this.hub.broadcast({
|
|
1374
1335
|
type: 'permission/resolved',
|
|
@@ -1379,202 +1340,57 @@ export class AgentBridge {
|
|
|
1379
1340
|
resolve(outcome.outcome === 'selected'
|
|
1380
1341
|
? { outcome: { outcome: 'selected', optionId: outcome.optionId } }
|
|
1381
1342
|
: { outcome: { outcome: 'cancelled' } })
|
|
1382
|
-
},
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
})
|
|
1388
|
-
// Recorded before the broadcast: the hub is synchronous, and the chat
|
|
1389
|
-
// line `streamPrompt` folds from this frame words itself by the path
|
|
1390
|
-
// this ask actually took.
|
|
1391
|
-
this.ledgerFallbacks.set(requestId, { reason: native.reason, timeoutMs, mode })
|
|
1392
|
-
if (timeoutMs > 0 && mode !== 'wait') {
|
|
1343
|
+
}, view)
|
|
1344
|
+
// The live frame and the replay frame come from ONE builder, so a
|
|
1345
|
+
// reconnecting client receives exactly what a live one did.
|
|
1346
|
+
this.hub.broadcast(permissionRequestFrame({ requestId, sessionId: session.sessionId, ...view }))
|
|
1347
|
+
if (deadlineAt !== undefined) {
|
|
1393
1348
|
timer = setTimeout(() => {
|
|
1394
1349
|
if (!this.permissions.has(requestId)) return
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
type: 'agent/notice',
|
|
1401
|
-
agentId: this.def.id,
|
|
1402
|
-
level: 'info',
|
|
1403
|
-
// Name what was missed and what was done about it, the way a human
|
|
1404
|
-
// needs it: which operation sat unanswered, and what the plugin
|
|
1405
|
-
// answered instead (OmniTerm's reaper writes the same notice).
|
|
1406
|
-
text: picked === undefined
|
|
1407
|
-
? `acp: permission request for "${tool}" timed out after ${String(Math.round(timeoutMs / 1000))}s; refused`
|
|
1408
|
-
: `acp: permission request for "${tool}" timed out after ${String(Math.round(timeoutMs / 1000))}s; answered "${picked.name}"`,
|
|
1409
|
-
})
|
|
1410
|
-
this.permissions.respond(requestId, picked === undefined
|
|
1411
|
-
? { outcome: 'cancelled' }
|
|
1412
|
-
: { outcome: 'selected', optionId: picked.optionId })
|
|
1413
|
-
}, timeoutMs)
|
|
1350
|
+
// Settle IN PLACE: exactly one notice, one `permission/expired` (the
|
|
1351
|
+
// conversation's trace of the expiry) and the ledger answer.
|
|
1352
|
+
const outcome = this.announceExpiredAsk(session, mode, toolCall, options, timeoutMs)
|
|
1353
|
+
this.permissions.respond(requestId, outcome)
|
|
1354
|
+
}, Math.max(0, deadlineAt - Date.now()))
|
|
1414
1355
|
timer.unref?.()
|
|
1415
1356
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
* answered, timed out, or settled.
|
|
1431
|
-
*/
|
|
1432
|
-
ledgerFallbackOf(requestId: string): LedgerFallback | undefined {
|
|
1433
|
-
return this.ledgerFallbacks.get(requestId)
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
/**
|
|
1437
|
-
* Ask the host approval service for this permission (`ctx.approval` → the
|
|
1438
|
-
* browser's composer takeover). Only sessions bound to a DSH chat session
|
|
1439
|
-
* qualify: the service appends its audit pair to that session inside an
|
|
1440
|
-
* open turn, so a session started over `/acp` (no chat turn) cannot be
|
|
1441
|
-
* routed and must stay on the ledger path.
|
|
1442
|
-
*
|
|
1443
|
-
* The ask is raced against {@link BridgeTuning.permissionTimeoutMs}. A
|
|
1444
|
-
* routed ask has exactly one answer entry — the composer takeover — and no
|
|
1445
|
-
* broadcast at all, so a host that takes the question and never renders it
|
|
1446
|
-
* would leave the turn hanging behind something invisible. Past the deadline
|
|
1447
|
-
* the ask is settled RIGHT THERE, by
|
|
1448
|
-
* {@link BridgeTuning.permissionTimeoutMode}: refused (`abort`), answered
|
|
1449
|
-
* with the option the agent's own kinds imply (`auto`), or never expiring
|
|
1450
|
-
* at all (`wait`).
|
|
1451
|
-
*
|
|
1452
|
-
* It is NOT handed somewhere else. An earlier revision withdrew the ask and
|
|
1453
|
-
* re-asked it on the settings ledger, which made the question travel to a
|
|
1454
|
-
* page the user was not looking at — and then cancelled it there a few
|
|
1455
|
-
* minutes later anyway. A deadline now ends the ask where it was asked.
|
|
1456
|
-
*
|
|
1457
|
-
* @returns the agent-facing response, or the reason the ask must fall back
|
|
1458
|
-
* to the ledger (see {@link LedgerFallbackReason}).
|
|
1459
|
-
*/
|
|
1460
|
-
private async askNativeApproval(
|
|
1461
|
-
session: LiveSession,
|
|
1462
|
-
toolCall: unknown,
|
|
1463
|
-
options: PermissionOption[],
|
|
1464
|
-
signal?: AbortSignal,
|
|
1465
|
-
): Promise<{ response: RequestPermissionResponse } | { reason: LedgerFallbackReason }> {
|
|
1466
|
-
const requester = this.approvalRequester?.()
|
|
1467
|
-
if (requester === undefined) return { reason: 'no-requester' }
|
|
1468
|
-
if (session.dshSessionId === undefined) return { reason: 'no-chat-session' }
|
|
1469
|
-
// The mode is read ONCE, here: it decides both whether a deadline exists
|
|
1470
|
-
// and what happens when it fires. An ask that started under one mode keeps
|
|
1471
|
-
// it — the same rule the window follows (an ask already holding a timer
|
|
1472
|
-
// keeps the one it started with).
|
|
1473
|
-
const mode = this.permissionTimeoutMode
|
|
1474
|
-
// 不等待(0)只属自动推进:没有问人的必要。直接按那一档收场,composer 接管
|
|
1475
|
-
// 根本不必出现——它只会闪一下就没了,问了也来不及答。超时中止没有这一档。
|
|
1476
|
-
if (mode === 'auto' && this.permissionTimeoutMs === 0) {
|
|
1477
|
-
return { response: this.settleExpiredAsk(session, mode, toolCall, options, 0) }
|
|
1478
|
-
}
|
|
1479
|
-
// One signal carries both withdrawals: the agent's own cancel (whatever
|
|
1480
|
-
// aborted the ACP request) and the deadline. The host's waterfall races its
|
|
1481
|
-
// answerers against this signal, so aborting is a clean withdrawal rather
|
|
1482
|
-
// than a leak.
|
|
1483
|
-
const withdraw = new AbortController()
|
|
1484
|
-
const askSignal = signal === undefined ? withdraw.signal : AbortSignal.any([signal, withdraw.signal])
|
|
1485
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1486
|
-
let timedOut = false
|
|
1487
|
-
// `wait` never expires, so it never arms a timer at all. A window of `0`
|
|
1488
|
-
// never reaches here: 不等待 was already settled above.
|
|
1489
|
-
const windowMs = this.permissionTimeoutMs
|
|
1490
|
-
const deadline = mode !== 'wait' && windowMs > 0
|
|
1491
|
-
? new Promise<{ timedOut: true }>(resolve => {
|
|
1492
|
-
timer = setTimeout(() => {
|
|
1493
|
-
timedOut = true
|
|
1494
|
-
withdraw.abort()
|
|
1495
|
-
resolve({ timedOut: true })
|
|
1496
|
-
}, windowMs)
|
|
1497
|
-
timer.unref?.()
|
|
1498
|
-
})
|
|
1499
|
-
: undefined
|
|
1500
|
-
let outcome: ApprovalOutcome
|
|
1501
|
-
try {
|
|
1502
|
-
const row = (typeof toolCall === 'object' && toolCall !== null ? toolCall : {}) as Record<string, unknown>
|
|
1503
|
-
const asked = requester({
|
|
1504
|
-
dshSessionId: session.dshSessionId,
|
|
1505
|
-
agentId: this.def.id,
|
|
1506
|
-
toolName: permissionToolName(toolCall),
|
|
1507
|
-
reason: permissionAskReason(toolCall),
|
|
1508
|
-
// The ACP call id is the id the adapter streamed on the host tool card
|
|
1509
|
-
// (`TurnStreamController` fills `tool-call` blocks from
|
|
1510
|
-
// `snapshot.callId`), so the host's approval detail slot can attach
|
|
1511
|
-
// the operation's own card to this ask.
|
|
1512
|
-
...(typeof row.toolCallId === 'string' && row.toolCallId !== '' ? { callId: row.toolCallId } : {}),
|
|
1513
|
-
signal: askSignal,
|
|
1514
|
-
})
|
|
1515
|
-
// A withdrawn ask settles on the host's own schedule; dropping it here
|
|
1516
|
-
// must not surface as an unhandled rejection later.
|
|
1517
|
-
asked.catch(() => { /* withdrawn: the deadline settles this ask now */ })
|
|
1518
|
-
const settled = deadline === undefined
|
|
1519
|
-
? await asked.then(o => ({ outcome: o }))
|
|
1520
|
-
: await Promise.race([asked.then(o => ({ outcome: o })), deadline])
|
|
1521
|
-
// The flag, not the race winner, decides: the host may settle `cancelled`
|
|
1522
|
-
// from the very abort the deadline caused, and that is still a timeout.
|
|
1523
|
-
if (timedOut || 'timedOut' in settled) {
|
|
1524
|
-
return { response: this.settleExpiredAsk(session, mode, toolCall, options, windowMs) }
|
|
1357
|
+
// The agent's own withdrawal settles the entry cancelled; the ledger's
|
|
1358
|
+
// resolve callback broadcasts `permission/resolved`, and drops this
|
|
1359
|
+
// listener, so nothing outlives the ask.
|
|
1360
|
+
if (signal !== undefined) {
|
|
1361
|
+
const onAbort = (): void => {
|
|
1362
|
+
if (!this.permissions.has(requestId)) return
|
|
1363
|
+
this.permissions.respond(requestId, { outcome: 'cancelled' })
|
|
1364
|
+
}
|
|
1365
|
+
if (signal.aborted) {
|
|
1366
|
+
onAbort()
|
|
1367
|
+
return
|
|
1368
|
+
}
|
|
1369
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
1370
|
+
detachSignal = () => { signal.removeEventListener('abort', onAbort) }
|
|
1525
1371
|
}
|
|
1526
|
-
outcome = settled.outcome
|
|
1527
|
-
} catch {
|
|
1528
|
-
// Synchronous throw or rejection, either way: the ask falls back to the
|
|
1529
|
-
// ledger. An approval failure must never break the agent's turn.
|
|
1530
|
-
outcome = 'unavailable'
|
|
1531
|
-
} finally {
|
|
1532
|
-
clearTimeout(timer)
|
|
1533
|
-
}
|
|
1534
|
-
if (outcome === 'unavailable') return { reason: 'unavailable' }
|
|
1535
|
-
const response = nativeOutcomeResponse(outcome, options)
|
|
1536
|
-
// Keep the event stream symmetric with the ledger path: a routed decision
|
|
1537
|
-
// is still a resolution, and the browser uses it to drop any card it may
|
|
1538
|
-
// hold for this session. No `permission/request` is broadcast — the
|
|
1539
|
-
// settings panel must never show a card for a composer-routed ask.
|
|
1540
|
-
this.hub.broadcast({
|
|
1541
|
-
type: 'permission/resolved',
|
|
1542
|
-
requestId: randomUUID(),
|
|
1543
|
-
sessionId: session.sessionId,
|
|
1544
|
-
outcome: response.outcome.outcome === 'selected'
|
|
1545
|
-
? { outcome: 'selected', optionId: response.outcome.optionId }
|
|
1546
|
-
: { outcome: 'cancelled' },
|
|
1547
1372
|
})
|
|
1548
|
-
return { response }
|
|
1549
1373
|
}
|
|
1550
1374
|
|
|
1551
1375
|
/**
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
* `
|
|
1555
|
-
*
|
|
1556
|
-
* kinds imply ({@link pickAutoOption}) so the agent CONTINUES; it says which
|
|
1557
|
-
* option it picked, because an unattended approval nobody looked at has to
|
|
1558
|
-
* be visible afterwards. A mode that cannot pick an option (no options, or
|
|
1559
|
-
* none with a usable id) degrades to `abort`: refusing is safer than
|
|
1560
|
-
* guessing, and hanging is worse than either.
|
|
1561
|
-
*
|
|
1562
|
-
* The `permission/resolved` frame keeps the event stream symmetric with the
|
|
1563
|
-
* ledger path, so the browser drops any card it may hold for this session.
|
|
1376
|
+
* Announce and compute one ask's expiry. Shared by the 不等待 arrival path
|
|
1377
|
+
* and the ledger deadline, so an expiry can never broadcast two notices or
|
|
1378
|
+
* skip the `permission/expired` line: once the card is gone that frame is
|
|
1379
|
+
* the conversation's only trace of what happened to the question.
|
|
1564
1380
|
*/
|
|
1565
|
-
private
|
|
1381
|
+
private announceExpiredAsk(
|
|
1566
1382
|
session: LiveSession,
|
|
1567
1383
|
mode: PermissionTimeoutMode,
|
|
1568
1384
|
toolCall: unknown,
|
|
1569
1385
|
options: PermissionOption[],
|
|
1570
1386
|
timeoutMs: number,
|
|
1571
|
-
):
|
|
1387
|
+
): PermissionOutcome {
|
|
1572
1388
|
const picked = mode === 'auto'
|
|
1573
1389
|
? pickAutoOption(options.map(option => ({ optionId: option.optionId, name: option.name, kind: option.kind })))
|
|
1574
1390
|
: undefined
|
|
1575
|
-
const
|
|
1576
|
-
? { outcome:
|
|
1577
|
-
: { outcome:
|
|
1391
|
+
const outcome: PermissionOutcome = picked === undefined
|
|
1392
|
+
? { outcome: 'cancelled' }
|
|
1393
|
+
: { outcome: 'selected', optionId: picked.optionId }
|
|
1578
1394
|
const tool = permissionToolName(toolCall)
|
|
1579
1395
|
// 不等待(0)不是「等了 0 秒」,是没等:两种说法的区别就在这里。
|
|
1580
1396
|
const waited = timeoutMs > 0
|
|
@@ -1591,19 +1407,36 @@ export class AgentBridge {
|
|
|
1591
1407
|
? `acp: permission request for "${tool}" ${waited}; refused`
|
|
1592
1408
|
: `acp: permission request for "${tool}" ${waited}; answered "${picked.name}"`,
|
|
1593
1409
|
})
|
|
1594
|
-
// The conversation's only trace of the expiry: the composer card is going
|
|
1595
|
-
// away and nothing else says why.
|
|
1596
1410
|
this.hub.broadcast({
|
|
1597
1411
|
type: 'permission/expired',
|
|
1598
1412
|
sessionId: session.sessionId,
|
|
1599
1413
|
toolCall,
|
|
1600
1414
|
timeoutMs,
|
|
1601
|
-
outcome
|
|
1602
|
-
? { outcome: 'selected', optionId: response.outcome.optionId }
|
|
1603
|
-
: { outcome: 'cancelled' },
|
|
1415
|
+
outcome,
|
|
1604
1416
|
...(picked === undefined ? {} : { optionName: picked.name }),
|
|
1605
1417
|
})
|
|
1606
|
-
return
|
|
1418
|
+
return outcome
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Settle the 不等待 (`auto` + zero window) ask on arrival: no ledger entry
|
|
1423
|
+
* and no card — nothing asks a human, so there is nothing to show. `auto`
|
|
1424
|
+
* answers with the option the agent's own kinds imply ({@link pickAutoOption})
|
|
1425
|
+
* and the agent CONTINUES. A mode that cannot pick an option (no options, or
|
|
1426
|
+
* none with a usable id) degrades to refusing: safer than guessing, and
|
|
1427
|
+
* hanging is worse than either.
|
|
1428
|
+
*/
|
|
1429
|
+
private settleExpiredAsk(
|
|
1430
|
+
session: LiveSession,
|
|
1431
|
+
mode: PermissionTimeoutMode,
|
|
1432
|
+
toolCall: unknown,
|
|
1433
|
+
options: PermissionOption[],
|
|
1434
|
+
timeoutMs: number,
|
|
1435
|
+
): RequestPermissionResponse {
|
|
1436
|
+
const outcome = this.announceExpiredAsk(session, mode, toolCall, options, timeoutMs)
|
|
1437
|
+
return outcome.outcome === 'selected'
|
|
1438
|
+
? { outcome: { outcome: 'selected', optionId: outcome.optionId } }
|
|
1439
|
+
: { outcome: { outcome: 'cancelled' } }
|
|
1607
1440
|
}
|
|
1608
1441
|
|
|
1609
1442
|
private settlePermissions(session: LiveSession): void {
|
|
@@ -1643,7 +1476,7 @@ export class AgentBridge {
|
|
|
1643
1476
|
* cannot reach into the agent's own process, so a second gate here would only
|
|
1644
1477
|
* be a fiction in front of a tool that can write without asking us at all.
|
|
1645
1478
|
* What the agent chooses to ask about goes to the human unchanged
|
|
1646
|
-
* (`
|
|
1479
|
+
* (`handlePermissionRequest`). See docs/acp-convergence.md §2.3.
|
|
1647
1480
|
*/
|
|
1648
1481
|
private async handleFsWrite(params: { sessionId: string; path: string; content: string }): Promise<{ }> {
|
|
1649
1482
|
const session = this.acpIndex.get(params.sessionId)
|
|
@@ -1760,12 +1593,11 @@ const APPROVAL_REASON_INPUT_KEYS = [
|
|
|
1760
1593
|
] as const
|
|
1761
1594
|
|
|
1762
1595
|
/**
|
|
1763
|
-
* One line saying WHAT the
|
|
1596
|
+
* One line saying WHAT the permission ask is about.
|
|
1764
1597
|
*
|
|
1765
|
-
* WHY: the
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1768
|
-
* (for execute/edit tools) a short scalar in `rawInput`. Only ONE declared
|
|
1598
|
+
* WHY: without it the human only sees the tool's NAME ("Bash") and cannot
|
|
1599
|
+
* judge the request; the ACP tool call already declares the missing half — its
|
|
1600
|
+
* title and (for execute/edit tools) a short scalar in `rawInput`. Only ONE declared
|
|
1769
1601
|
* string is used, never a serialized object or a whole file, and the line is
|
|
1770
1602
|
* capped so a pathological input cannot fill the card or the audit log.
|
|
1771
1603
|
*/
|
|
@@ -1798,22 +1630,6 @@ export function permissionAskReason(toolCall: unknown): string {
|
|
|
1798
1630
|
return line.length > APPROVAL_REASON_MAX ? `${line.slice(0, APPROVAL_REASON_MAX - 1)}…` : line
|
|
1799
1631
|
}
|
|
1800
1632
|
|
|
1801
|
-
/**
|
|
1802
|
-
* Translate a native approval outcome into the ACP option the agent must see.
|
|
1803
|
-
* `'allowed-once'` is the chain's only grant; a rejection prefers the once-variant.
|
|
1804
|
-
*/
|
|
1805
|
-
function nativeOutcomeResponse(outcome: ApprovalOutcome, options: PermissionOption[]): RequestPermissionResponse {
|
|
1806
|
-
if (outcome === 'allowed-once') {
|
|
1807
|
-
const allow = options.find(o => o.kind === 'allow_once') ?? options.find(o => o.kind === 'allow_always')
|
|
1808
|
-
return allow === undefined ? { outcome: { outcome: 'cancelled' } } : { outcome: { outcome: 'selected', optionId: allow.optionId } }
|
|
1809
|
-
}
|
|
1810
|
-
if (outcome === 'rejected') {
|
|
1811
|
-
const reject = options.find(o => o.kind === 'reject_once') ?? options.find(o => o.kind === 'reject_always')
|
|
1812
|
-
return reject === undefined ? { outcome: { outcome: 'cancelled' } } : { outcome: { outcome: 'selected', optionId: reject.optionId } }
|
|
1813
|
-
}
|
|
1814
|
-
return { outcome: { outcome: 'cancelled' } }
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
1633
|
function snapshot(session: LiveSession): SessionView {
|
|
1818
1634
|
return {
|
|
1819
1635
|
sessionId: session.sessionId,
|
|
@@ -2628,7 +2444,6 @@ class TurnStreamController {
|
|
|
2628
2444
|
constructor(
|
|
2629
2445
|
private readonly manager: BridgeManager,
|
|
2630
2446
|
private readonly hub: EventHub,
|
|
2631
|
-
private readonly bridge: AgentBridge,
|
|
2632
2447
|
readonly session: LiveSession,
|
|
2633
2448
|
readonly turnId: string,
|
|
2634
2449
|
/** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
|
|
@@ -2840,7 +2655,7 @@ class TurnStreamController {
|
|
|
2840
2655
|
// segment; the event that CUTS the segment belongs to the next one.
|
|
2841
2656
|
if (this.foldInto(seg, update)) this.cursorSeq = typed.seq
|
|
2842
2657
|
} else if (row.type === 'permission/request') {
|
|
2843
|
-
const typed = event as {
|
|
2658
|
+
const typed = event as Extract<AcpEvent, { type: 'permission/request' }>
|
|
2844
2659
|
this.pushProse(this.permissionLine(typed))
|
|
2845
2660
|
} else if (row.type === 'permission/expired') {
|
|
2846
2661
|
const typed = event as { toolCall: unknown; timeoutMs: number; outcome: PermissionOutcome; optionName?: string }
|
|
@@ -2999,29 +2814,28 @@ class TurnStreamController {
|
|
|
2999
2814
|
})
|
|
3000
2815
|
}
|
|
3001
2816
|
|
|
3002
|
-
private permissionLine(event: {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
// The line must
|
|
3007
|
-
//
|
|
3008
|
-
//
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
//
|
|
2817
|
+
private permissionLine(event: Extract<AcpEvent, { type: 'permission/request' }>): string {
|
|
2818
|
+
// `toolName` is host-derived (`permissionToolName`) so agents that omit
|
|
2819
|
+
// `title` and declare it in `_meta` still name the operation here.
|
|
2820
|
+
const title = event.toolName
|
|
2821
|
+
// The line must point at the place the answer entry actually is: a
|
|
2822
|
+
// chat-bound ask renders its card in the conversation; an `/acp`-RPC ask
|
|
2823
|
+
// has no conversation, so its card lives on the settings page.
|
|
2824
|
+
const lead = event.dshSessionId !== undefined
|
|
2825
|
+
? 'please answer the permission card above the composer'
|
|
2826
|
+
: 'please approve in Settings'
|
|
3013
2827
|
// The last clause says what happens if nobody answers, and it follows the
|
|
3014
2828
|
// MODE: refused, answered automatically, or (in `wait`) nothing at all.
|
|
3015
|
-
|
|
3016
|
-
const why = fallback?.reason === 'unavailable' ? 'the approval service is unavailable' : undefined
|
|
3017
|
-
const lead = why === undefined ? 'please approve in Settings' : `${why}; approve in Settings`
|
|
3018
|
-
if (fallback === undefined || fallback.timeoutMs <= 0 || fallback.mode === 'wait') {
|
|
2829
|
+
if (event.timeoutMode === 'wait' || event.deadlineAt === undefined) {
|
|
3019
2830
|
return `\n> 🔐 [Permission Required: ${title}] - ${lead}\n`
|
|
3020
2831
|
}
|
|
3021
|
-
const
|
|
2832
|
+
const remainingMs = Math.max(0, event.deadlineAt - Date.now())
|
|
2833
|
+
// Floor (+1 ms for the Date.now tick), min 1: rounding up would promise time already spent.
|
|
2834
|
+
const remainingS = Math.max(1, Math.floor((remainingMs + 1) / 1000))
|
|
2835
|
+
const otherwise = event.timeoutMode === 'auto'
|
|
3022
2836
|
? 'this request is answered automatically'
|
|
3023
2837
|
: 'this request is cancelled'
|
|
3024
|
-
return `\n> 🔐 [Permission Required: ${title}] - ${lead} within ${String(
|
|
2838
|
+
return `\n> 🔐 [Permission Required: ${title}] - ${lead} within ${String(remainingS)}s or ${otherwise}\n`
|
|
3025
2839
|
}
|
|
3026
2840
|
|
|
3027
2841
|
/**
|
|
@@ -3033,9 +2847,7 @@ class TurnStreamController {
|
|
|
3033
2847
|
* answered with the option the agent's own kinds implied.
|
|
3034
2848
|
*/
|
|
3035
2849
|
private expiredLine(event: { toolCall: unknown; timeoutMs: number; outcome: PermissionOutcome; optionName?: string }): string {
|
|
3036
|
-
const title =
|
|
3037
|
-
? String((event.toolCall as { title: string }).title)
|
|
3038
|
-
: 'permission'
|
|
2850
|
+
const title = permissionToolName(event.toolCall)
|
|
3039
2851
|
const waited = event.timeoutMs > 0
|
|
3040
2852
|
? `no answer within ${String(Math.round(event.timeoutMs / 1000))}s`
|
|
3041
2853
|
: 'not waited for'
|
|
@@ -3212,30 +3024,14 @@ export class BridgeManager {
|
|
|
3212
3024
|
onCatalog?: (agentId: string, configOptions: unknown, shapeOnly?: boolean) => void,
|
|
3213
3025
|
onCommands?: (agentId: string, commands: NonNullable<SessionView['availableCommands']>) => void,
|
|
3214
3026
|
): AgentBridge {
|
|
3215
|
-
return new AgentBridge(def, this.hub, this.transcriptMax, onConnectionLost, onCatalog, onCommands, this.tuning, () => this.
|
|
3027
|
+
return new AgentBridge(def, this.hub, this.transcriptMax, onConnectionLost, onCatalog, onCommands, this.tuning, () => this.sessionWorkspaceResolver)
|
|
3216
3028
|
}
|
|
3217
3029
|
|
|
3218
3030
|
/**
|
|
3219
|
-
*
|
|
3220
|
-
*
|
|
3221
|
-
*
|
|
3222
|
-
*
|
|
3223
|
-
*/
|
|
3224
|
-
setApprovalRequester(requester?: ApprovalRequester): void {
|
|
3225
|
-
this.approvalRequester = requester
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
|
-
/** True when permission asks go to native approval instead of the settings panel. */
|
|
3229
|
-
approvalRouting(): boolean {
|
|
3230
|
-
return this.approvalRequester !== undefined
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
/**
|
|
3234
|
-
* Every outstanding ledger ask, across all agents, with the view the browser
|
|
3235
|
-
* was shown it — the source of the reconnect replay (see
|
|
3236
|
-
* `permissionReplayFrames`). Asks on the native route are deliberately
|
|
3237
|
-
* absent: they broadcast nothing and their answer entry is the composer
|
|
3238
|
-
* takeover the client already holds.
|
|
3031
|
+
* Every outstanding ask, across all agents, with the view the browser was
|
|
3032
|
+
* shown it — the source of the reconnect replay (see
|
|
3033
|
+
* `permissionReplayFrames`). Every ACP ask is a ledger entry, so this is the
|
|
3034
|
+
* whole live set.
|
|
3239
3035
|
*/
|
|
3240
3036
|
pendingPermissionViews(): PendingPermissionView[] {
|
|
3241
3037
|
const views: PendingPermissionView[] = []
|
|
@@ -5193,11 +4989,6 @@ export class BridgeManager {
|
|
|
5193
4989
|
if (session.sessionId === sessionId) this.dshSessionMap.delete(key)
|
|
5194
4990
|
}
|
|
5195
4991
|
}
|
|
5196
|
-
/**
|
|
5197
|
-
* Latest native-approval requester. Held as a field (read through a thunk by
|
|
5198
|
-
* each bridge) so a requester wired after bridges were built still applies.
|
|
5199
|
-
*/
|
|
5200
|
-
private approvalRequester?: ApprovalRequester
|
|
5201
4992
|
private sessionWorkspaceResolver?: SessionWorkspaceResolver
|
|
5202
4993
|
private acpModeResolver?: AcpModeResolver
|
|
5203
4994
|
/** Cached `chatSessionAcpMode` answers, keyed by DSH chat session id. */
|
|
@@ -5330,7 +5121,7 @@ export class BridgeManager {
|
|
|
5330
5121
|
if (existing !== undefined && existing.released) {
|
|
5331
5122
|
const resumed = await this.bridge(agentId).resumeSession(existing)
|
|
5332
5123
|
// Rebind explicitly: a resume keeps the record's identity, but the chat
|
|
5333
|
-
// session id is what
|
|
5124
|
+
// session id is what the conversation's permission card addresses.
|
|
5334
5125
|
resumed.dshSessionId = dshSessionId
|
|
5335
5126
|
// A resume without an explicit pick must keep serving the agent the
|
|
5336
5127
|
// conversation was ON (its own model string when one was recorded). The
|
|
@@ -5369,7 +5160,7 @@ export class BridgeManager {
|
|
|
5369
5160
|
workspaceRoot,
|
|
5370
5161
|
)
|
|
5371
5162
|
// Absent for `/acp`-RPC sessions and for adapter calls without a chat
|
|
5372
|
-
// session;
|
|
5163
|
+
// session; their permission cards are addressed by the ACP session only.
|
|
5373
5164
|
session.dshSessionId = dshSessionId
|
|
5374
5165
|
this.dshSessionMap.set(key, session)
|
|
5375
5166
|
// Turn the displayed defaults into this conversation's pick, so the first
|
|
@@ -5651,7 +5442,6 @@ export class BridgeManager {
|
|
|
5651
5442
|
const controller = new TurnStreamController(
|
|
5652
5443
|
this,
|
|
5653
5444
|
this.hub,
|
|
5654
|
-
bridge,
|
|
5655
5445
|
session,
|
|
5656
5446
|
turnId,
|
|
5657
5447
|
endTurn,
|