@oh-my-pi/pi-utils 18.12.5 → 18.12.6
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/CHANGELOG.md +6 -0
- package/dist/types/acp/connection.d.ts +18 -1
- package/dist/types/acp/transport.d.ts +26 -2
- package/dist/types/fs-open.d.ts +2 -1
- package/package.json +2 -2
- package/src/acp/connection.ts +38 -6
- package/src/acp/transport.ts +101 -4
- package/src/fs-open.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.6] - 2026-10-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `RpcConnection.requestCancellable()` and `Client.requestPermission(params, { signal })`; client connections abort a handler and answer `-32800` when the peer sends `$/cancel_request`, while agent connections keep running the request and return its real result ([#15885](https://github.com/can1357/oh-my-pi/pull/15885) by [@marcintustin](https://github.com/marcintustin))
|
|
10
|
+
|
|
5
11
|
## [18.12.4] - 2026-10-20
|
|
6
12
|
|
|
7
13
|
### Breaking Changes
|
|
@@ -7,7 +7,15 @@ export interface CreateTerminalResponse {
|
|
|
7
7
|
}
|
|
8
8
|
/** Client callbacks handled by a client-side connection. */
|
|
9
9
|
export interface Client {
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Serve a permission elicit. `options.signal` aborts when the agent
|
|
12
|
+
* cancels the request (`$/cancel_request`) — e.g. user steering superseded
|
|
13
|
+
* the dialog — so a held handler can end instead of stranding the
|
|
14
|
+
* connection's clean-EOF drain.
|
|
15
|
+
*/
|
|
16
|
+
requestPermission(params: RequestPermissionRequest, options?: {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}): MaybePromise<RequestPermissionResponse>;
|
|
11
19
|
sessionUpdate(params: SessionNotification): MaybePromise<void>;
|
|
12
20
|
readTextFile?(params: ReadTextFileRequest): MaybePromise<ReadTextFileResponse>;
|
|
13
21
|
writeTextFile?(params: WriteTextFileRequest): MaybePromise<WriteTextFileResponse | void>;
|
|
@@ -45,6 +53,15 @@ export declare class AgentSideConnection {
|
|
|
45
53
|
sessionUpdate(params: SessionNotification): Promise<void>;
|
|
46
54
|
/** Requests permission from the client. */
|
|
47
55
|
requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse>;
|
|
56
|
+
/**
|
|
57
|
+
* Requests permission from the client, returning a handle that cancels the
|
|
58
|
+
* pending wire request (`$/cancel_request`) — used when user steering
|
|
59
|
+
* supersedes an open permission dialog instead of waiting behind it.
|
|
60
|
+
*/
|
|
61
|
+
requestPermissionWithCancel(params: RequestPermissionRequest): {
|
|
62
|
+
promise: Promise<RequestPermissionResponse>;
|
|
63
|
+
cancel(): void;
|
|
64
|
+
};
|
|
48
65
|
/** Reads a client-side text file. */
|
|
49
66
|
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
|
|
50
67
|
/** Writes a client-side text file. */
|
|
@@ -69,17 +69,41 @@ export declare class RequestError extends Error {
|
|
|
69
69
|
/** Converts this error into a JSON-RPC error payload. */
|
|
70
70
|
toErrorResponse(): ErrorResponse;
|
|
71
71
|
}
|
|
72
|
-
type Dispatcher = (method: string, params: unknown, notification: boolean
|
|
72
|
+
type Dispatcher = (method: string, params: unknown, notification: boolean,
|
|
73
|
+
/** Abort signal for an inbound *request*: fires when the peer cancels it via `$/cancel_request`. */
|
|
74
|
+
signal?: AbortSignal) => MaybePromise<unknown>;
|
|
75
|
+
/** Options for {@link RpcConnection}. */
|
|
76
|
+
export interface RpcConnectionOptions {
|
|
77
|
+
/**
|
|
78
|
+
* The dispatcher honors the abort signal it receives for inbound requests.
|
|
79
|
+
* Only then does a peer `$/cancel_request` abort the handler and answer the
|
|
80
|
+
* request `-32800` immediately; otherwise the handler runs to completion and
|
|
81
|
+
* its real result is the response, since answering "cancelled" while the
|
|
82
|
+
* work keeps going would hide its outcome from the peer.
|
|
83
|
+
*/
|
|
84
|
+
honorsInboundCancellation?: boolean;
|
|
85
|
+
}
|
|
73
86
|
/** Correlated bidirectional JSON-RPC connection. */
|
|
74
87
|
export declare class RpcConnection {
|
|
75
88
|
#private;
|
|
76
|
-
constructor(stream: Stream, dispatcher: Dispatcher);
|
|
89
|
+
constructor(stream: Stream, dispatcher: Dispatcher, options?: RpcConnectionOptions);
|
|
77
90
|
/** Signal aborted when the stream closes. */
|
|
78
91
|
get signal(): AbortSignal;
|
|
79
92
|
/** Promise resolved when the stream closes. */
|
|
80
93
|
get closed(): Promise<void>;
|
|
81
94
|
/** Sends a correlated JSON-RPC request. */
|
|
82
95
|
request<Response>(method: string, params?: unknown): Promise<Response>;
|
|
96
|
+
/**
|
|
97
|
+
* Sends a JSON-RPC request whose pending entry can be cancelled locally:
|
|
98
|
+
* `cancel()` rejects the promise with a request-cancelled error, notifies
|
|
99
|
+
* the peer via `$/cancel_request`, and drops the pending entry so a late
|
|
100
|
+
* response is ignored. Used for agent→client requests a mid-turn event can
|
|
101
|
+
* supersede (e.g. a permission elicit cancelled when user steering arrives).
|
|
102
|
+
*/
|
|
103
|
+
requestCancellable<Response>(method: string, params?: unknown): {
|
|
104
|
+
promise: Promise<Response>;
|
|
105
|
+
cancel(): void;
|
|
106
|
+
};
|
|
83
107
|
/** Sends a JSON-RPC notification. */
|
|
84
108
|
notify(method: string, params?: unknown): Promise<void>;
|
|
85
109
|
/** Closes the connection and rejects outstanding requests. */
|
package/dist/types/fs-open.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `fs.openSync` with close-on-exec, matching Node's default.
|
|
3
3
|
*
|
|
4
4
|
* libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
|
|
5
|
-
* dies at `exec`. Bun's `fs.open*`
|
|
5
|
+
* dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
|
|
6
|
+
* `bun@>=1.4` pin), and descriptors it returns survive
|
|
6
7
|
* into any child spawned through a path that does not close strays itself — the
|
|
7
8
|
* bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
|
|
8
9
|
* this for every descriptor kept open past such a spawn (session transcripts,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.12.
|
|
3
|
+
"version": "18.12.6",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.12.
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.12.6"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/acp/connection.ts
CHANGED
|
@@ -47,7 +47,16 @@ export interface CreateTerminalResponse {
|
|
|
47
47
|
}
|
|
48
48
|
/** Client callbacks handled by a client-side connection. */
|
|
49
49
|
export interface Client {
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Serve a permission elicit. `options.signal` aborts when the agent
|
|
52
|
+
* cancels the request (`$/cancel_request`) — e.g. user steering superseded
|
|
53
|
+
* the dialog — so a held handler can end instead of stranding the
|
|
54
|
+
* connection's clean-EOF drain.
|
|
55
|
+
*/
|
|
56
|
+
requestPermission(
|
|
57
|
+
params: RequestPermissionRequest,
|
|
58
|
+
options?: { signal?: AbortSignal },
|
|
59
|
+
): MaybePromise<RequestPermissionResponse>;
|
|
51
60
|
sessionUpdate(params: SessionNotification): MaybePromise<void>;
|
|
52
61
|
readTextFile?(params: ReadTextFileRequest): MaybePromise<ReadTextFileResponse>;
|
|
53
62
|
writeTextFile?(params: WriteTextFileRequest): MaybePromise<WriteTextFileResponse | void>;
|
|
@@ -90,6 +99,17 @@ export class AgentSideConnection {
|
|
|
90
99
|
requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
91
100
|
return this.#connection.request("session/request_permission", params);
|
|
92
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Requests permission from the client, returning a handle that cancels the
|
|
104
|
+
* pending wire request (`$/cancel_request`) — used when user steering
|
|
105
|
+
* supersedes an open permission dialog instead of waiting behind it.
|
|
106
|
+
*/
|
|
107
|
+
requestPermissionWithCancel(params: RequestPermissionRequest): {
|
|
108
|
+
promise: Promise<RequestPermissionResponse>;
|
|
109
|
+
cancel(): void;
|
|
110
|
+
} {
|
|
111
|
+
return this.#connection.requestCancellable<RequestPermissionResponse>("session/request_permission", params);
|
|
112
|
+
}
|
|
93
113
|
/** Reads a client-side text file. */
|
|
94
114
|
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
95
115
|
return this.#connection.request("fs/read_text_file", params);
|
|
@@ -168,10 +188,18 @@ export class ClientSideConnection {
|
|
|
168
188
|
#connection: RpcConnection;
|
|
169
189
|
|
|
170
190
|
constructor(toClient: (connection: ClientSideConnection) => Client, stream: Stream) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
// Client handlers receive the cancel signal (`requestPermission`'s
|
|
192
|
+
// `options.signal`), so a peer `$/cancel_request` may answer `-32800`
|
|
193
|
+
// at once. The agent side keeps the default: its handlers ignore the
|
|
194
|
+
// signal, and `session/cancel` is ACP's cancellation path for prompts.
|
|
195
|
+
this.#connection = new RpcConnection(
|
|
196
|
+
stream,
|
|
197
|
+
async (method, params, notification, signal) => {
|
|
198
|
+
if (!client) throw RequestError.internalError(undefined, "Client is not initialized");
|
|
199
|
+
return dispatchClient(client, method, params, notification, signal);
|
|
200
|
+
},
|
|
201
|
+
{ honorsInboundCancellation: true },
|
|
202
|
+
);
|
|
175
203
|
const client = toClient(this);
|
|
176
204
|
}
|
|
177
205
|
/** Signal aborted when the transport closes. */
|
|
@@ -272,10 +300,11 @@ async function dispatchClient(
|
|
|
272
300
|
method: string,
|
|
273
301
|
params: unknown,
|
|
274
302
|
notification: boolean,
|
|
303
|
+
signal?: AbortSignal,
|
|
275
304
|
): Promise<unknown> {
|
|
276
305
|
switch (method) {
|
|
277
306
|
case "session/request_permission":
|
|
278
|
-
return client.requestPermission(params as RequestPermissionRequest);
|
|
307
|
+
return client.requestPermission(params as RequestPermissionRequest, { signal });
|
|
279
308
|
case "session/update":
|
|
280
309
|
return client.sessionUpdate(params as SessionNotification);
|
|
281
310
|
case "fs/read_text_file":
|
|
@@ -322,6 +351,9 @@ async function dispatchClient(
|
|
|
322
351
|
params as CompleteElicitationNotification,
|
|
323
352
|
);
|
|
324
353
|
default:
|
|
354
|
+
// The transport already aborted the matching inbound handler;
|
|
355
|
+
// like the agent side, the notification itself carries no work.
|
|
356
|
+
if (method === "$/cancel_request") return;
|
|
325
357
|
if (notification && client.extNotification)
|
|
326
358
|
return client.extNotification(method, params as Record<string, unknown>);
|
|
327
359
|
if (!notification && client.extMethod) return client.extMethod(method, params as Record<string, unknown>);
|
package/src/acp/transport.ts
CHANGED
|
@@ -105,9 +105,27 @@ function createStandardError(
|
|
|
105
105
|
return new RequestError(code, additionalMessage ? `${message}: ${additionalMessage}` : message, data);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
type Dispatcher = (
|
|
108
|
+
type Dispatcher = (
|
|
109
|
+
method: string,
|
|
110
|
+
params: unknown,
|
|
111
|
+
notification: boolean,
|
|
112
|
+
/** Abort signal for an inbound *request*: fires when the peer cancels it via `$/cancel_request`. */
|
|
113
|
+
signal?: AbortSignal,
|
|
114
|
+
) => MaybePromise<unknown>;
|
|
109
115
|
type Pending = { resolve(value: unknown): void; reject(reason: unknown): void };
|
|
110
116
|
|
|
117
|
+
/** Options for {@link RpcConnection}. */
|
|
118
|
+
export interface RpcConnectionOptions {
|
|
119
|
+
/**
|
|
120
|
+
* The dispatcher honors the abort signal it receives for inbound requests.
|
|
121
|
+
* Only then does a peer `$/cancel_request` abort the handler and answer the
|
|
122
|
+
* request `-32800` immediately; otherwise the handler runs to completion and
|
|
123
|
+
* its real result is the response, since answering "cancelled" while the
|
|
124
|
+
* work keeps going would hide its outcome from the peer.
|
|
125
|
+
*/
|
|
126
|
+
honorsInboundCancellation?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
/** Bound on clean-EOF inbound drain so `closed` cannot hang if a handler never settles. */
|
|
112
130
|
const INBOUND_DRAIN_TIMEOUT_MS = 30_000;
|
|
113
131
|
|
|
@@ -117,15 +135,18 @@ export class RpcConnection {
|
|
|
117
135
|
#pending = new Map<JsonRpcId, Pending>();
|
|
118
136
|
#inbound = new Set<Promise<void>>();
|
|
119
137
|
#openInboundIds = new Set<JsonRpcId>();
|
|
138
|
+
/** Abort controller per inbound request, fired by a peer `$/cancel_request` (opt-in). */
|
|
139
|
+
#inboundCancellers: Map<JsonRpcId, AbortController> | undefined;
|
|
120
140
|
#writable: WritableStream<AnyMessage>;
|
|
121
141
|
#writeTail: Promise<void> = Promise.resolve();
|
|
122
142
|
#abort = new AbortController();
|
|
123
143
|
#closed = Promise.withResolvers<void>();
|
|
124
144
|
#dispatcher: Dispatcher;
|
|
125
145
|
|
|
126
|
-
constructor(stream: Stream, dispatcher: Dispatcher) {
|
|
146
|
+
constructor(stream: Stream, dispatcher: Dispatcher, options?: RpcConnectionOptions) {
|
|
127
147
|
this.#writable = stream.writable;
|
|
128
148
|
this.#dispatcher = dispatcher;
|
|
149
|
+
this.#inboundCancellers = options?.honorsInboundCancellation ? new Map() : undefined;
|
|
129
150
|
void this.#read(stream.readable);
|
|
130
151
|
}
|
|
131
152
|
|
|
@@ -141,6 +162,48 @@ export class RpcConnection {
|
|
|
141
162
|
/** Sends a correlated JSON-RPC request. */
|
|
142
163
|
request<Response>(method: string, params?: unknown): Promise<Response> {
|
|
143
164
|
if (this.signal.aborted) return Promise.reject(new Error("Connection closed"));
|
|
165
|
+
return this.#beginRequest(method, params).deferred.promise as Promise<Response>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sends a JSON-RPC request whose pending entry can be cancelled locally:
|
|
170
|
+
* `cancel()` rejects the promise with a request-cancelled error, notifies
|
|
171
|
+
* the peer via `$/cancel_request`, and drops the pending entry so a late
|
|
172
|
+
* response is ignored. Used for agent→client requests a mid-turn event can
|
|
173
|
+
* supersede (e.g. a permission elicit cancelled when user steering arrives).
|
|
174
|
+
*/
|
|
175
|
+
requestCancellable<Response>(
|
|
176
|
+
method: string,
|
|
177
|
+
params?: unknown,
|
|
178
|
+
): {
|
|
179
|
+
promise: Promise<Response>;
|
|
180
|
+
cancel(): void;
|
|
181
|
+
} {
|
|
182
|
+
if (this.signal.aborted) {
|
|
183
|
+
return {
|
|
184
|
+
promise: Promise.reject(new Error("Connection closed")),
|
|
185
|
+
cancel: () => {},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const { id, deferred } = this.#beginRequest(method, params);
|
|
189
|
+
return {
|
|
190
|
+
promise: deferred.promise as Promise<Response>,
|
|
191
|
+
cancel: () => {
|
|
192
|
+
if (!this.#pending.delete(id)) return;
|
|
193
|
+
deferred.reject(RequestError.requestCancelled({ requestId: id, method }));
|
|
194
|
+
void this.notify("$/cancel_request", { requestId: id }).catch(() => undefined);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Shared request plumbing: allocate an id, register the pending entry, write the frame. */
|
|
200
|
+
#beginRequest(
|
|
201
|
+
method: string,
|
|
202
|
+
params: unknown,
|
|
203
|
+
): {
|
|
204
|
+
id: number;
|
|
205
|
+
deferred: { promise: Promise<unknown>; resolve: (value?: unknown) => void; reject: (reason?: unknown) => void };
|
|
206
|
+
} {
|
|
144
207
|
const id = this.#nextId++;
|
|
145
208
|
const deferred = Promise.withResolvers<unknown>();
|
|
146
209
|
this.#pending.set(id, deferred);
|
|
@@ -148,7 +211,7 @@ export class RpcConnection {
|
|
|
148
211
|
this.#pending.delete(id);
|
|
149
212
|
deferred.reject(error);
|
|
150
213
|
});
|
|
151
|
-
return deferred
|
|
214
|
+
return { id, deferred };
|
|
152
215
|
}
|
|
153
216
|
|
|
154
217
|
/** Sends a JSON-RPC notification. */
|
|
@@ -235,6 +298,18 @@ export class RpcConnection {
|
|
|
235
298
|
}
|
|
236
299
|
if (!("method" in message)) return;
|
|
237
300
|
if (!("id" in message)) {
|
|
301
|
+
// ACP v1 names the cancelled request `requestId`. A cancelled
|
|
302
|
+
// inbound request must still be answered (spec: result or
|
|
303
|
+
// `-32800`): the handler is aborted and the error response is
|
|
304
|
+
// written here, so a late-settling handler lands in `#respond`'s
|
|
305
|
+
// closed-id no-op instead of answering a request the peer gave
|
|
306
|
+
// up on.
|
|
307
|
+
if (message.method === "$/cancel_request" && this.#inboundCancellers) {
|
|
308
|
+
this.#cancelInbound(
|
|
309
|
+
this.#inboundCancellers,
|
|
310
|
+
(message.params as { requestId?: JsonRpcId } | undefined)?.requestId,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
238
313
|
try {
|
|
239
314
|
await this.#dispatcher(message.method, message.params, true);
|
|
240
315
|
} catch {
|
|
@@ -242,8 +317,11 @@ export class RpcConnection {
|
|
|
242
317
|
}
|
|
243
318
|
return;
|
|
244
319
|
}
|
|
320
|
+
const cancellers = this.#inboundCancellers;
|
|
321
|
+
const canceller = cancellers ? new AbortController() : undefined;
|
|
322
|
+
if (canceller) cancellers?.set(message.id, canceller);
|
|
245
323
|
try {
|
|
246
|
-
const result = await this.#dispatcher(message.method, message.params, false);
|
|
324
|
+
const result = await this.#dispatcher(message.method, message.params, false, canceller?.signal);
|
|
247
325
|
await this.#respond(message.id, { result: result ?? {} });
|
|
248
326
|
} catch (error) {
|
|
249
327
|
const protocolError =
|
|
@@ -251,6 +329,25 @@ export class RpcConnection {
|
|
|
251
329
|
? error
|
|
252
330
|
: RequestError.internalError({ details: error instanceof Error ? error.message : String(error) });
|
|
253
331
|
await this.#respond(message.id, { error: protocolError.toErrorResponse() });
|
|
332
|
+
} finally {
|
|
333
|
+
cancellers?.delete(message.id);
|
|
254
334
|
}
|
|
255
335
|
}
|
|
336
|
+
|
|
337
|
+
#cancelInbound(cancellers: Map<JsonRpcId, AbortController>, id: JsonRpcId | undefined): void {
|
|
338
|
+
if (id === undefined) return;
|
|
339
|
+
const canceller = cancellers.get(id);
|
|
340
|
+
if (canceller) {
|
|
341
|
+
cancellers.delete(id);
|
|
342
|
+
canceller.abort(RequestError.requestCancelled({ requestId: id }));
|
|
343
|
+
}
|
|
344
|
+
// Spec: a cancelled request still gets a response. Answer `-32800`
|
|
345
|
+
// here — synchronously with the notification — so the peer is never
|
|
346
|
+
// left waiting on a handler that may never settle. `#respond` closes
|
|
347
|
+
// the id, so a late handler result (a handler that ignored its abort
|
|
348
|
+
// signal) lands in the closed-id no-op instead of answering twice.
|
|
349
|
+
void this.#respond(id, { error: RequestError.requestCancelled({ requestId: id }).toErrorResponse() }).catch(
|
|
350
|
+
() => undefined,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
256
353
|
}
|
package/src/fs-open.ts
CHANGED
|
@@ -15,7 +15,8 @@ const O_CLOEXEC = process.platform === "linux" ? 0o2000000 : process.platform ==
|
|
|
15
15
|
* `fs.openSync` with close-on-exec, matching Node's default.
|
|
16
16
|
*
|
|
17
17
|
* libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
|
|
18
|
-
* dies at `exec`. Bun's `fs.open*`
|
|
18
|
+
* dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
|
|
19
|
+
* `bun@>=1.4` pin), and descriptors it returns survive
|
|
19
20
|
* into any child spawned through a path that does not close strays itself — the
|
|
20
21
|
* bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
|
|
21
22
|
* this for every descriptor kept open past such a spawn (session transcripts,
|