@nuvin/session 0.2.0-rc.7 → 0.2.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/{chunk-I5U3WU6B.js → chunk-2OTEZX2E.js} +87 -28
  2. package/dist/chunk-7LEIACBZ.js +85 -0
  3. package/dist/{chunk-MRVGQ4DK.js → chunk-MPBST5DM.js} +1 -1
  4. package/dist/client/daemon-client.d.ts.map +1 -1
  5. package/dist/client/data-client.d.ts +9 -9
  6. package/dist/client/data-client.d.ts.map +1 -1
  7. package/dist/client/directory.d.ts +12 -14
  8. package/dist/client/directory.d.ts.map +1 -1
  9. package/dist/client/http-data-client.d.ts +5 -0
  10. package/dist/client/http-data-client.d.ts.map +1 -1
  11. package/dist/client/index.js +86 -22
  12. package/dist/client/session-client.d.ts +2 -2
  13. package/dist/client/session-client.d.ts.map +1 -1
  14. package/dist/controller/agent-channel.d.ts +2 -2
  15. package/dist/controller/agent-channel.d.ts.map +1 -1
  16. package/dist/controller/index.js +108 -49
  17. package/dist/controller/session-controller.d.ts +4 -11
  18. package/dist/controller/session-controller.d.ts.map +1 -1
  19. package/dist/protocol/data-rpc.d.ts +12 -8
  20. package/dist/protocol/data-rpc.d.ts.map +1 -1
  21. package/dist/protocol/history-text.d.ts +4 -0
  22. package/dist/protocol/history-text.d.ts.map +1 -0
  23. package/dist/protocol/index.d.ts +1 -0
  24. package/dist/protocol/index.d.ts.map +1 -1
  25. package/dist/protocol/index.js +12 -4
  26. package/dist/protocol/model-identity.d.ts +7 -0
  27. package/dist/protocol/model-identity.d.ts.map +1 -1
  28. package/dist/protocol/types.d.ts +51 -16
  29. package/dist/protocol/types.d.ts.map +1 -1
  30. package/dist/protocol/version.d.ts +1 -1
  31. package/dist/state/index.d.ts +2 -0
  32. package/dist/state/index.d.ts.map +1 -1
  33. package/dist/state/index.js +9 -1
  34. package/dist/state/messages.d.ts +3 -3
  35. package/dist/state/messages.d.ts.map +1 -1
  36. package/dist/state/questions.d.ts +5 -0
  37. package/dist/state/questions.d.ts.map +1 -0
  38. package/dist/state/session.d.ts.map +1 -1
  39. package/dist/state/tool-key.d.ts +2 -0
  40. package/dist/state/tool-key.d.ts.map +1 -0
  41. package/dist/test-utils/index.js +2 -2
  42. package/package.json +3 -3
  43. package/dist/chunk-7HGJO7ZX.js +0 -9
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createSessionViewState,
3
3
  reduceSessionEvent
4
- } from "../chunk-I5U3WU6B.js";
4
+ } from "../chunk-2OTEZX2E.js";
5
5
  import {
6
6
  PROTOCOL_VERSION
7
- } from "../chunk-MRVGQ4DK.js";
7
+ } from "../chunk-MPBST5DM.js";
8
8
 
9
9
  // src/client/endpoint.ts
10
10
  var TerminalEndpointError = class extends Error {
@@ -17,6 +17,13 @@ var TerminalEndpointError = class extends Error {
17
17
  };
18
18
 
19
19
  // src/client/http-data-client.ts
20
+ var DataRpcClientError = class extends Error {
21
+ constructor(code, message) {
22
+ super(message);
23
+ this.code = code;
24
+ this.name = "DataRpcClientError";
25
+ }
26
+ };
20
27
  function httpBaseFromWsUrl(wsUrl) {
21
28
  const u = new URL(wsUrl);
22
29
  const scheme = u.protocol === "wss:" ? "https:" : "http:";
@@ -46,6 +53,9 @@ async function throwFromResponse(response, fallback, terminal, onUnauthorized) {
46
53
  typeof body?.message === "string" ? body.message : mapped.message
47
54
  );
48
55
  }
56
+ if (body?.code === "invalid-history-cursor") {
57
+ throw new DataRpcClientError(body.code, body.message ?? fallback);
58
+ }
49
59
  throw new Error(
50
60
  typeof body?.message === "string" ? body.message : `${fallback} (HTTP ${response.status}).`
51
61
  );
@@ -173,10 +183,10 @@ function createHttpDataClient(options = {}) {
173
183
  const body = await response.json();
174
184
  return { url: body.wsUrl, credential: { grant: body.grant } };
175
185
  },
176
- async fetchTranscript(daemonId, historyId, opts) {
177
- const suffix = `/transcript/${encodeURIComponent(historyId)}${queryString({
178
- workspace: opts?.workspace,
179
- profile: opts?.profile
186
+ async fetchTranscript(daemonId, identity) {
187
+ const suffix = `/transcript/${encodeURIComponent(identity.sessionId)}${queryString({
188
+ workspace: identity.workspace,
189
+ profile: identity.profile
180
190
  })}`;
181
191
  const response = await daemonFetch(daemonId, suffix);
182
192
  if (response.status === 404) {
@@ -202,6 +212,35 @@ function createHttpDataClient(options = {}) {
202
212
  });
203
213
  return await response.json();
204
214
  },
215
+ async listHistoryWorkspaces(daemonId, opts) {
216
+ const suffix = `/history/workspaces${queryString({
217
+ profile: opts?.profile,
218
+ currentCwd: opts?.currentCwd,
219
+ query: opts?.query
220
+ })}`;
221
+ const response = await daemonFetch(daemonId, suffix, { signal: opts?.signal });
222
+ if (!response.ok)
223
+ await throwFromResponse(response, "Failed to load history workspaces", {
224
+ 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
225
+ });
226
+ return await response.json();
227
+ },
228
+ async listWorkspaceHistory(daemonId, opts) {
229
+ const suffix = `/history/workspace${queryString({
230
+ profile: opts?.profile,
231
+ workspace: opts?.workspace,
232
+ cwd: opts?.cwd,
233
+ query: opts?.query,
234
+ limit: opts?.limit,
235
+ cursor: opts?.cursor
236
+ })}`;
237
+ const response = await daemonFetch(daemonId, suffix, { signal: opts?.signal });
238
+ if (!response.ok)
239
+ await throwFromResponse(response, "Failed to load workspace history", {
240
+ 404: { reason: "daemon-revoked", message: "This daemon is no longer registered." }
241
+ });
242
+ return await response.json();
243
+ },
205
244
  async listWorkspaces(daemonId, opts) {
206
245
  const suffix = `/workspaces${queryString({ profile: opts?.profile })}`;
207
246
  const response = await daemonFetch(daemonId, suffix);
@@ -224,10 +263,10 @@ function createHttpDataClient(options = {}) {
224
263
  if (!response.ok) await throwFromResponse(response, "Failed to create the workspace");
225
264
  return await response.json();
226
265
  },
227
- async deleteSession(daemonId, historyId, opts) {
228
- const suffix = `/sessions/${encodeURIComponent(historyId)}${queryString({
229
- workspace: opts?.workspace,
230
- profile: opts?.profile
266
+ async deleteSession(daemonId, identity) {
267
+ const suffix = `/sessions/${encodeURIComponent(identity.sessionId)}${queryString({
268
+ workspace: identity.workspace,
269
+ profile: identity.profile
231
270
  })}`;
232
271
  const response = await daemonFetch(daemonId, suffix, { method: "DELETE" });
233
272
  if (response.status === 404) {
@@ -540,22 +579,24 @@ function createSessionClient(transport) {
540
579
  })).then(() => {
541
580
  });
542
581
  },
543
- decideApproval(toolCallId, decision, comment, grantDir) {
582
+ decideApproval(toolCallId, decision, comment, grantDir, parentToolCallId) {
544
583
  return sendCommand((id) => ({
545
584
  id,
546
585
  type: "decide-approval",
547
586
  toolCallId,
548
587
  decision,
549
588
  ...comment !== void 0 ? { comment } : {},
550
- ...grantDir !== void 0 ? { grantDir } : {}
589
+ ...grantDir !== void 0 ? { grantDir } : {},
590
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {}
551
591
  })).then(() => {
552
592
  });
553
593
  },
554
- persistBashPermission(toolCallId, request) {
594
+ persistBashPermission(toolCallId, request, parentToolCallId) {
555
595
  return sendCommand((id) => ({
556
596
  id,
557
597
  type: "persist-bash-permission",
558
598
  toolCallId,
599
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {},
559
600
  request
560
601
  })).then(toPersistBashPermissionResult);
561
602
  },
@@ -994,11 +1035,13 @@ function throwCollectedErrors(errors, message) {
994
1035
  }
995
1036
  function bindDaemonDataClient(client, daemonId) {
996
1037
  return {
997
- fetchTranscript: (historyId, opts) => client.fetchTranscript(daemonId, historyId, opts),
1038
+ fetchTranscript: (identity) => client.fetchTranscript(daemonId, identity),
998
1039
  fetchHistory: (opts) => client.fetchHistory(daemonId, opts),
1040
+ listHistoryWorkspaces: (opts) => client.listHistoryWorkspaces(daemonId, opts),
1041
+ listWorkspaceHistory: (opts) => client.listWorkspaceHistory(daemonId, opts),
999
1042
  listWorkspaces: (opts) => client.listWorkspaces(daemonId, opts),
1000
1043
  createWorkspace: (opts) => client.createWorkspace(daemonId, opts),
1001
- deleteSession: (historyId, opts) => client.deleteSession(daemonId, historyId, opts),
1044
+ deleteSession: (identity) => client.deleteSession(daemonId, identity),
1002
1045
  listProfiles: () => client.listProfiles(daemonId),
1003
1046
  listProviders: (opts) => client.listProviders(daemonId, opts),
1004
1047
  addProvider: (input) => client.addProvider(daemonId, input),
@@ -1198,6 +1241,24 @@ function createDaemonClient(target, deps = {}) {
1198
1241
  }
1199
1242
 
1200
1243
  // src/client/directory.ts
1244
+ var DAEMON_REQUEST_ERROR_CODES = /* @__PURE__ */ new Set([
1245
+ "transcript-unavailable",
1246
+ "transcript-corrupt",
1247
+ "history-state-conflict",
1248
+ "ambiguous-session-identity"
1249
+ ]);
1250
+ var DaemonRequestError = class extends Error {
1251
+ code;
1252
+ identity;
1253
+ constructor(message, options) {
1254
+ super(message);
1255
+ this.name = "DaemonRequestError";
1256
+ if (options?.code && DAEMON_REQUEST_ERROR_CODES.has(options.code)) {
1257
+ this.code = options.code;
1258
+ }
1259
+ this.identity = options?.identity;
1260
+ }
1261
+ };
1201
1262
  var toText2 = (data) => {
1202
1263
  if (typeof data === "string") return data;
1203
1264
  if (data instanceof Buffer) return data.toString("utf8");
@@ -1323,7 +1384,12 @@ function createServerDirectory(options) {
1323
1384
  if (!entry || entry.kind === "list") return;
1324
1385
  link.pending.delete(message.id);
1325
1386
  if (!message.ok) {
1326
- entry.reject(new Error(message.error?.message ?? "Command failed."));
1387
+ entry.reject(
1388
+ new DaemonRequestError(message.error?.message ?? "Command failed.", {
1389
+ ...message.error?.code ? { code: message.error.code } : {},
1390
+ ...message.error?.identity ? { identity: message.error.identity } : {}
1391
+ })
1392
+ );
1327
1393
  return;
1328
1394
  }
1329
1395
  if (entry.kind === "kill") {
@@ -1524,7 +1590,7 @@ function createServerDirectory(options) {
1524
1590
  );
1525
1591
  });
1526
1592
  },
1527
- resumeSession(server, historyId, resumeOptions) {
1593
+ resumeSession(server, identity) {
1528
1594
  return new Promise((resolve, reject) => {
1529
1595
  const link = linkFor(server);
1530
1596
  if (link instanceof Error) {
@@ -1538,11 +1604,7 @@ function createServerDirectory(options) {
1538
1604
  JSON.stringify({
1539
1605
  id,
1540
1606
  type: "resume-session",
1541
- historyId,
1542
- cwd: resumeOptions.cwd,
1543
- ...resumeOptions.name !== void 0 ? { name: resumeOptions.name } : {},
1544
- ...resumeOptions.workspace !== void 0 ? { workspace: resumeOptions.workspace } : {},
1545
- ...resumeOptions.profile !== void 0 ? { profile: resumeOptions.profile } : {}
1607
+ identity
1546
1608
  })
1547
1609
  );
1548
1610
  });
@@ -1590,6 +1652,8 @@ function createServerDirectory(options) {
1590
1652
  };
1591
1653
  }
1592
1654
  export {
1655
+ DaemonRequestError,
1656
+ DataRpcClientError,
1593
1657
  SessionCommandError,
1594
1658
  TerminalEndpointError,
1595
1659
  adaptWhatwgSocket,
@@ -18,8 +18,8 @@ export interface SessionClient {
18
18
  /** Raw frame hook for UI side-effects (e.g. close modals on state-reset). */
19
19
  onEvent(listener: (event: ServerEvent) => void): () => void;
20
20
  submit(input: AgentInput, opts: ClientSubmitOptions): Promise<void>;
21
- decideApproval(toolCallId: string, decision: "a" | "n" | "y", comment?: string, grantDir?: string): Promise<void>;
22
- persistBashPermission(toolCallId: string, request: PersistBashPermissionRequest): Promise<PersistBashPermissionResult>;
21
+ decideApproval(toolCallId: string, decision: "a" | "n" | "y", comment?: string, grantDir?: string, parentToolCallId?: string): Promise<void>;
22
+ persistBashPermission(toolCallId: string, request: PersistBashPermissionRequest, parentToolCallId?: string): Promise<PersistBashPermissionResult>;
23
23
  answerQuestion(questionId: string, answers: AskUserAnswers): Promise<void>;
24
24
  abort(): Promise<void>;
25
25
  /** Cancel a queued (not-yet-running) message by its `turn-status.queued` id. */
@@ -1 +1 @@
1
- {"version":3,"file":"session-client.d.ts","sourceRoot":"","sources":["../../src/client/session-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-client.d.ts","sourceRoot":"","sources":["../../src/client/session-client.ts"],"names":[],"mappings":"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"}
@@ -36,7 +36,7 @@ export type ToolCallRequest = {
36
36
  export type AgentEventListener = (payload: AgentEventPayload) => void;
37
37
  export type SessionLoadedListener = (loaded: SessionLoadedPayload) => void;
38
38
  export type ToolCallDecider = (request: ToolCallRequest) => Promise<ToolRuntimeDispatchDecision> | ToolRuntimeDispatchDecision;
39
- export type UserQuestionHandler = (request: AskUserQuestionRequest) => Promise<AskUserAnswers> | AskUserAnswers;
39
+ export type UserQuestionHandler = (request: AskUserQuestionRequest, scope?: DelegationScope) => Promise<AskUserAnswers> | AskUserAnswers;
40
40
  /**
41
41
  * In-process channel between the Agent (constructed in `main()`) and the
42
42
  * React UI (`<App />`). Two distinct shapes:
@@ -54,7 +54,7 @@ export declare class AgentChannel {
54
54
  publishEvent(event: AgentEvent, scope?: DelegationScope): void;
55
55
  publishSessionLoaded(loaded: SessionLoadedPayload): void;
56
56
  requestToolDecision(request: ToolCallRequest): Promise<ToolRuntimeDispatchDecision> | ToolRuntimeDispatchDecision;
57
- requestUserQuestion(request: AskUserQuestionRequest): Promise<AskUserAnswers>;
57
+ requestUserQuestion(request: AskUserQuestionRequest, scope?: DelegationScope): Promise<AskUserAnswers>;
58
58
  onEvent(listener: AgentEventListener): () => void;
59
59
  onSessionLoaded(listener: SessionLoadedListener): () => void;
60
60
  setToolDecider(decider: ToolCallDecider | null): void;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-channel.d.ts","sourceRoot":"","sources":["../../src/controller/agent-channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,sBAAsB,EACtB,OAAO,EACP,mBAAmB,EACnB,2BAA2B,EAC3B,YAAY,EACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,kCAAkC,EAClC,4BAA4B,EAC5B,yBAAyB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,OAAO,EAAE,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,6BAA6B,CAAC;IAC3C,gFAAgF;IAChF,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CACtB,OAAO,EAAE,4BAA4B,KAClC,OAAO,CAAC,kCAAkC,CAAC,CAAC;IACjD,QAAQ,EAAE,YAAY,CAAC;IACvB,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;AACtE,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE3E,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,eAAe,KACrB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;AAExE,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,sBAAsB,KAC5B,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAAC;AAO9C;;;;;;;;;GASG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,eAAe,CAAoC;IAI3D,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI;IAI9D,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;IAIxD,mBAAmB,CACjB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B;IAOrE,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,cAAc,CAAC;IAS7E,OAAO,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAOjD,eAAe,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI;IAO5D,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI;IAIrD,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI;CAG9D"}
1
+ {"version":3,"file":"agent-channel.d.ts","sourceRoot":"","sources":["../../src/controller/agent-channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,sBAAsB,EACtB,OAAO,EACP,mBAAmB,EACnB,2BAA2B,EAC3B,YAAY,EACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,kCAAkC,EAClC,4BAA4B,EAC5B,yBAAyB,EAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,OAAO,EAAE,CAAC;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,6BAA6B,CAAC;IAC3C,gFAAgF;IAChF,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CACtB,OAAO,EAAE,4BAA4B,KAClC,OAAO,CAAC,kCAAkC,CAAC,CAAC;IACjD,QAAQ,EAAE,YAAY,CAAC;IACvB,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;AACtE,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE3E,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,eAAe,KACrB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;AAExE,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,CAAC,EAAE,eAAe,KACpB,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAAC;AAO9C;;;;;;;;;GASG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,eAAe,CAAoC;IAI3D,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI;IAI9D,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;IAIxD,mBAAmB,CACjB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B;IAOrE,mBAAmB,CACjB,OAAO,EAAE,sBAAsB,EAC/B,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAAC,cAAc,CAAC;IAS1B,OAAO,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAOjD,eAAe,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI;IAO5D,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI;IAIrD,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI;CAG9D"}
@@ -6,8 +6,9 @@ import {
6
6
  createMessageStateFromMessages,
7
7
  createSessionViewState,
8
8
  isToolAutoApprovedForMode,
9
- reduceSessionEvent
10
- } from "../chunk-I5U3WU6B.js";
9
+ reduceSessionEvent,
10
+ scopedToolKey
11
+ } from "../chunk-2OTEZX2E.js";
11
12
 
12
13
  // src/controller/agent-channel.ts
13
14
  import { EventEmitter } from "events";
@@ -32,11 +33,11 @@ var AgentChannel = class {
32
33
  }
33
34
  return this.decider(request);
34
35
  }
35
- requestUserQuestion(request) {
36
+ requestUserQuestion(request, scope) {
36
37
  if (!this.questionHandler) {
37
38
  return Promise.reject(new Error("UI is not ready to handle user questions."));
38
39
  }
39
- return Promise.resolve(this.questionHandler(request));
40
+ return Promise.resolve(this.questionHandler(request, scope));
40
41
  }
41
42
  // ---------- UI-side (subscribers) ----------
42
43
  onEvent(listener) {
@@ -86,6 +87,10 @@ function createSessionController(deps) {
86
87
  let seq = 0;
87
88
  let approvalMode = deps.approvalMode ?? "readOnly";
88
89
  let abortController = null;
90
+ let activeTurnPromise = null;
91
+ let closePromise = null;
92
+ let closing = false;
93
+ let detached = false;
89
94
  let activeTurnId;
90
95
  let activePolicyDenial;
91
96
  let hostBusyCount = 0;
@@ -246,7 +251,7 @@ function createSessionController(deps) {
246
251
  const descriptor = {
247
252
  agentId,
248
253
  permissionClass,
249
- summary: findToolSummary(toolCall.id),
254
+ summary: findToolSummary(toolCall.id, parentToolCallId),
250
255
  toolCallId: toolCall.id,
251
256
  toolName: toolCall.name,
252
257
  ...dirAccess !== void 0 ? { dirAccess } : {},
@@ -257,8 +262,13 @@ function createSessionController(deps) {
257
262
  ...request.workflowBashPolicy !== void 0 ? { workflowBashPolicy: request.workflowBashPolicy } : {}
258
263
  };
259
264
  return new Promise((resolve) => {
260
- approvalResolvers.set(toolCall.id, resolve);
261
- privateApprovals.set(toolCall.id, {
265
+ const toolKey = scopedToolKey(parentToolCallId, toolCall.id);
266
+ approvalResolvers.set(toolKey, {
267
+ resolve,
268
+ toolCallId: toolCall.id,
269
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {}
270
+ });
271
+ privateApprovals.set(toolKey, {
262
272
  approvalKey,
263
273
  bashPolicy: hasBashPolicy,
264
274
  ...!dirAccess && toolCall.name === "Bash" && bashPolicy?.persistence.eligible && request.persistBashPermission !== void 0 ? { persistBashPermission: request.persistBashPermission } : {}
@@ -266,7 +276,7 @@ function createSessionController(deps) {
266
276
  emit({ type: "approval-requested", approval: descriptor });
267
277
  });
268
278
  });
269
- deps.channel.setQuestionHandler((request) => {
279
+ deps.channel.setQuestionHandler((request, scope) => {
270
280
  if (deps.headless) {
271
281
  activePolicyDenial = "Questions are unavailable in headless mode.";
272
282
  return Promise.reject(new Error(activePolicyDenial));
@@ -275,31 +285,47 @@ function createSessionController(deps) {
275
285
  const questionId = `question-${questionCounter}`;
276
286
  return new Promise((resolve, reject) => {
277
287
  questionResolvers.set(questionId, { resolve, reject });
278
- emit({ type: "question-asked", question: { questionId, request } });
288
+ const nickname = nicknameFor(scope?.parentToolCallId);
289
+ emit({
290
+ type: "question-asked",
291
+ question: {
292
+ questionId,
293
+ request,
294
+ ...scope?.agentId !== void 0 ? { agentId: scope.agentId } : {},
295
+ ...scope?.parentToolCallId !== void 0 ? { parentToolCallId: scope.parentToolCallId } : {},
296
+ ...nickname !== void 0 ? { nickname } : {}
297
+ }
298
+ });
279
299
  });
280
300
  });
281
- function findToolSummary(toolCallId) {
282
- const messageId = state.messages.toolMessageIds[toolCallId];
301
+ function findToolSummary(toolCallId, parentToolCallId) {
302
+ const messageId = state.messages.toolMessageIds[scopedToolKey(parentToolCallId, toolCallId)];
283
303
  const message = state.messages.messages.find((entry) => entry.id === messageId);
284
304
  return message?.role === "tool" ? message.summary : "";
285
305
  }
286
306
  function nicknameFor(parentToolCallId) {
287
307
  if (!parentToolCallId) return void 0;
288
- const parentMessageId = state.messages.toolMessageIds[parentToolCallId];
308
+ const parentMessageId = state.messages.toolMessageIds[scopedToolKey(void 0, parentToolCallId)];
289
309
  const parentMessage = state.messages.messages.find((entry) => entry.id === parentMessageId);
290
310
  if (parentMessage?.role !== "tool") return void 0;
291
311
  const nickname = parentMessage.input?.nickname;
292
312
  return typeof nickname === "string" ? nickname : void 0;
293
313
  }
294
- function findDescriptor(toolCallId) {
314
+ function findDescriptor(toolCallId, parentToolCallId) {
295
315
  return [state.approval.active, ...state.approval.pending].find(
296
- (entry) => entry?.toolCallId === toolCallId
316
+ (entry) => entry?.toolCallId === toolCallId && entry.parentToolCallId === parentToolCallId
297
317
  );
298
318
  }
299
319
  function rejectAllPending(reason) {
300
- for (const [toolCallId, resolve] of approvalResolvers) {
301
- resolve({ action: "reject", reason });
302
- emit({ type: "approval-settled", toolCallId, status: "rejected", by: "abort" });
320
+ for (const pending of approvalResolvers.values()) {
321
+ pending.resolve({ action: "reject", reason });
322
+ emit({
323
+ type: "approval-settled",
324
+ toolCallId: pending.toolCallId,
325
+ ...pending.parentToolCallId === void 0 ? {} : { parentToolCallId: pending.parentToolCallId },
326
+ status: "rejected",
327
+ by: "abort"
328
+ });
303
329
  }
304
330
  approvalResolvers.clear();
305
331
  privateApprovals.clear();
@@ -323,7 +349,7 @@ function createSessionController(deps) {
323
349
  }
324
350
  }
325
351
  function startNextQueuedSubmission() {
326
- if (abortController || hostBusyCount > 0) return;
352
+ if (closing || abortController || hostBusyCount > 0) return;
327
353
  const next = queue.shift();
328
354
  if (!next) return;
329
355
  if (!next.opts.silent) {
@@ -336,7 +362,7 @@ function createSessionController(deps) {
336
362
  next.opts.turnId
337
363
  );
338
364
  }
339
- void runTurn(next.input, {
365
+ void startTrackedTurn(next.input, {
340
366
  turnId: next.opts.turnId,
341
367
  allowSteering: next.opts.turnId === void 0
342
368
  });
@@ -486,10 +512,28 @@ function createSessionController(deps) {
486
512
  if (ownedTurn) startNextQueuedSubmission();
487
513
  }
488
514
  }
515
+ function startTrackedTurn(input, opts) {
516
+ let tracked;
517
+ tracked = runTurn(input, opts).finally(() => {
518
+ if (activeTurnPromise === tracked) activeTurnPromise = null;
519
+ });
520
+ activeTurnPromise = tracked;
521
+ return tracked;
522
+ }
523
+ function detachController() {
524
+ if (detached) return;
525
+ detached = true;
526
+ unsubscribeEvents();
527
+ unsubscribeLoaded();
528
+ deps.channel.setToolDecider(null);
529
+ deps.channel.setQuestionHandler(null);
530
+ listeners.clear();
531
+ }
489
532
  return {
490
533
  abort() {
491
534
  queue.length = 0;
492
535
  abortController?.abort(new Error("User aborted the current turn."));
536
+ rejectAllPending("User aborted the current turn.");
493
537
  },
494
538
  attachHostHooks(hooks) {
495
539
  if (hooks.compaction) compaction = hooks.compaction;
@@ -510,14 +554,21 @@ function createSessionController(deps) {
510
554
  emit({ type: "info", message });
511
555
  },
512
556
  close() {
557
+ if (closePromise) return closePromise;
558
+ closing = true;
559
+ const shouldEmitTurnStatus = queue.length > 0 || abortController !== null || hostBusyCount > 0;
513
560
  queue.length = 0;
514
561
  abortController?.abort(new Error("Session closed."));
515
562
  rejectAllPending("Session closed.");
516
- unsubscribeEvents();
517
- unsubscribeLoaded();
518
- deps.channel.setToolDecider(null);
519
- deps.channel.setQuestionHandler(null);
520
- listeners.clear();
563
+ if (shouldEmitTurnStatus) emitTurnStatus();
564
+ const active = activeTurnPromise;
565
+ if (!active) {
566
+ detachController();
567
+ closePromise = Promise.resolve();
568
+ return closePromise;
569
+ }
570
+ closePromise = active.then(detachController, detachController);
571
+ return closePromise;
521
572
  },
522
573
  dequeue(queuedId) {
523
574
  const index = queue.findIndex((q) => q.id === queuedId);
@@ -526,17 +577,18 @@ function createSessionController(deps) {
526
577
  emitTurnStatus();
527
578
  return true;
528
579
  },
529
- decideApproval(toolCallId, decision, comment, grantDir) {
530
- if (persistenceInFlight.has(toolCallId)) {
580
+ decideApproval(toolCallId, decision, comment, grantDir, parentToolCallId) {
581
+ const toolKey = scopedToolKey(parentToolCallId, toolCallId);
582
+ if (persistenceInFlight.has(toolKey)) {
531
583
  return false;
532
584
  }
533
- const resolve = approvalResolvers.get(toolCallId);
534
- if (!resolve) return false;
535
- approvalResolvers.delete(toolCallId);
536
- persistenceInFlight.delete(toolCallId);
537
- const descriptor = findDescriptor(toolCallId);
538
- const privateApproval = privateApprovals.get(toolCallId);
539
- privateApprovals.delete(toolCallId);
585
+ const pending = approvalResolvers.get(toolKey);
586
+ if (!pending) return false;
587
+ approvalResolvers.delete(toolKey);
588
+ persistenceInFlight.delete(toolKey);
589
+ const descriptor = findDescriptor(toolCallId, parentToolCallId);
590
+ const privateApproval = privateApprovals.get(toolKey);
591
+ privateApprovals.delete(toolKey);
540
592
  if (decision === "a" && descriptor && !descriptor.dirAccess) {
541
593
  alwaysApproved.add(
542
594
  privateApproval?.approvalKey ?? `${descriptor.agentId}:${descriptor.toolName}`
@@ -545,9 +597,12 @@ function createSessionController(deps) {
545
597
  const rejected = decision === "n";
546
598
  if (rejected) {
547
599
  const baseReason = `User rejected tool execution (${descriptor?.toolName ?? "unknown tool"})`;
548
- resolve({ action: "reject", reason: comment ? `${baseReason}: ${comment}` : baseReason });
600
+ pending.resolve({
601
+ action: "reject",
602
+ reason: comment ? `${baseReason}: ${comment}` : baseReason
603
+ });
549
604
  } else if (descriptor?.dirAccess) {
550
- resolve({
605
+ pending.resolve({
551
606
  action: "run",
552
607
  grantDir: {
553
608
  dir: resolveGrantDir(
@@ -563,30 +618,32 @@ function createSessionController(deps) {
563
618
  } : {}
564
619
  });
565
620
  } else if (descriptor?.toolName === "Bash" && privateApproval?.bashPolicy) {
566
- resolve({
621
+ pending.resolve({
567
622
  action: "run",
568
623
  approvalDecision: decision,
569
624
  approvalSource: "user"
570
625
  });
571
626
  } else {
572
- resolve({ action: "run" });
627
+ pending.resolve({ action: "run" });
573
628
  }
574
629
  emit({
575
630
  type: "approval-settled",
576
631
  toolCallId,
632
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {},
577
633
  status: rejected ? "rejected" : "approved",
578
634
  by: "client"
579
635
  });
580
636
  return true;
581
637
  },
582
- async persistBashPermission(toolCallId, request) {
638
+ async persistBashPermission(toolCallId, request, parentToolCallId) {
639
+ const toolKey = scopedToolKey(parentToolCallId, toolCallId);
583
640
  const descriptor = state.approval.active;
584
- const privateApproval = privateApprovals.get(toolCallId);
641
+ const privateApproval = privateApprovals.get(toolKey);
585
642
  const handler = privateApproval?.persistBashPermission;
586
- if (descriptor?.toolCallId !== toolCallId || descriptor.toolName !== "Bash" || descriptor.dirAccess !== void 0 || descriptor.bashPolicy?.persistence.eligible !== true || handler === void 0 || !approvalResolvers.has(toolCallId)) {
643
+ if (descriptor?.toolCallId !== toolCallId || descriptor.parentToolCallId !== parentToolCallId || descriptor.toolName !== "Bash" || descriptor.dirAccess !== void 0 || descriptor.bashPolicy?.persistence.eligible !== true || handler === void 0 || !approvalResolvers.has(toolKey)) {
587
644
  return void 0;
588
645
  }
589
- if (persistenceInFlight.has(toolCallId)) {
646
+ if (persistenceInFlight.has(toolKey)) {
590
647
  return {
591
648
  status: "validation-error",
592
649
  message: "A persistent Bash permission save is already in progress.",
@@ -600,7 +657,7 @@ function createSessionController(deps) {
600
657
  fieldErrors: { server: "Wait for the current operation to finish." }
601
658
  };
602
659
  }
603
- persistenceInFlight.add(toolCallId);
660
+ persistenceInFlight.add(toolKey);
604
661
  hostBusyCount += 1;
605
662
  emitTurnStatus();
606
663
  try {
@@ -608,13 +665,13 @@ function createSessionController(deps) {
608
665
  if (outcome.status === "validation-error") {
609
666
  return outcome;
610
667
  }
611
- const resolve = approvalResolvers.get(toolCallId);
612
- if (resolve === void 0 || state.approval.active?.toolCallId !== toolCallId || privateApprovals.get(toolCallId) !== privateApproval) {
668
+ const pending = approvalResolvers.get(toolKey);
669
+ if (pending === void 0 || state.approval.active?.toolCallId !== toolCallId || state.approval.active.parentToolCallId !== parentToolCallId || privateApprovals.get(toolKey) !== privateApproval) {
613
670
  return void 0;
614
671
  }
615
- approvalResolvers.delete(toolCallId);
616
- privateApprovals.delete(toolCallId);
617
- resolve({
672
+ approvalResolvers.delete(toolKey);
673
+ privateApprovals.delete(toolKey);
674
+ pending.resolve({
618
675
  action: "run",
619
676
  executionAuthorization: outcome.executionAuthorization,
620
677
  approvalSource: "user"
@@ -622,12 +679,13 @@ function createSessionController(deps) {
622
679
  emit({
623
680
  type: "approval-settled",
624
681
  toolCallId,
682
+ ...parentToolCallId !== void 0 ? { parentToolCallId } : {},
625
683
  status: "approved",
626
684
  by: "client"
627
685
  });
628
686
  return { status: "approved" };
629
687
  } finally {
630
- persistenceInFlight.delete(toolCallId);
688
+ persistenceInFlight.delete(toolKey);
631
689
  hostBusyCount = Math.max(0, hostBusyCount - 1);
632
690
  emitTurnStatus();
633
691
  }
@@ -695,6 +753,7 @@ function createSessionController(deps) {
695
753
  startNextQueuedSubmission();
696
754
  },
697
755
  async submit(input, opts) {
756
+ if (closing) throw new Error("Session is closing.");
698
757
  if (abortController || hostBusyCount > 0) {
699
758
  queue.push({
700
759
  id: `q-${++queueCounter}`,
@@ -715,7 +774,7 @@ function createSessionController(deps) {
715
774
  opts.turnId
716
775
  );
717
776
  }
718
- await runTurn(input, {
777
+ await startTrackedTurn(input, {
719
778
  turnId: opts.turnId,
720
779
  allowSteering: opts.turnId === void 0
721
780
  });
@@ -84,28 +84,21 @@ export interface SessionController {
84
84
  compaction?: CompactionHooks;
85
85
  guardPrompt?: (input: AgentInput) => Promise<GuardResult>;
86
86
  }): void;
87
- /**
88
- * Settle a pending question by id.
89
- *
90
- * Single-active-question is the current protocol invariant: when parallel
91
- * sub-agents ask concurrently, only the latest question is surfaced as
92
- * `question.active` in state. All resolvers survive in the internal map, so
93
- * answering any questionId settles the matching ask correctly.
94
- */
87
+ /** Settle a pending question by id. */
95
88
  answerQuestion(questionId: string, answers: AskUserAnswers): boolean;
96
89
  /** Emit an error line into the replicated transcript. */
97
90
  appendError(message: string): void;
98
91
  /** Emit an info line into the replicated transcript (slash output, system notices). */
99
92
  appendInfo(message: string): void;
100
- close(): void;
93
+ close(): Promise<void>;
101
94
  /**
102
95
  * Cancel a queued (not-yet-running) submission by its `turn-status.queued` id.
103
96
  * Returns true if an item was removed, false if the id was unknown (already
104
97
  * drained or never existed) — a race-safe no-op the host still acks ok.
105
98
  */
106
99
  dequeue(queuedId: string): boolean;
107
- decideApproval(toolCallId: string, decision: "a" | "n" | "y", comment?: string, grantDir?: string): boolean;
108
- persistBashPermission(toolCallId: string, request: PersistBashPermissionRequest): Promise<PersistBashPermissionResult | undefined>;
100
+ decideApproval(toolCallId: string, decision: "a" | "n" | "y", comment?: string, grantDir?: string, parentToolCallId?: string): boolean;
101
+ persistBashPermission(toolCallId: string, request: PersistBashPermissionRequest, parentToolCallId?: string): Promise<PersistBashPermissionResult | undefined>;
109
102
  /** Echo a user-style message without starting a turn (slash commands echo their invocation). */
110
103
  echoUserMessage(text: string, attachmentLabels?: string[]): void;
111
104
  /**