@cjhyy/code-shell-core 0.9.5 → 0.9.7

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 (89) hide show
  1. package/dist/automation/scheduler.d.ts +3 -0
  2. package/dist/automation/scheduler.js +21 -0
  3. package/dist/context/manager.d.ts +8 -2
  4. package/dist/context/manager.js +20 -4
  5. package/dist/context/notes.d.ts +39 -0
  6. package/dist/context/notes.js +314 -0
  7. package/dist/credentials/access.d.ts +9 -1
  8. package/dist/credentials/access.js +15 -4
  9. package/dist/credentials/store.d.ts +11 -0
  10. package/dist/credentials/store.js +44 -9
  11. package/dist/credentials/types.d.ts +4 -0
  12. package/dist/credentials/types.js +4 -0
  13. package/dist/credentials/use-credential-tool.js +12 -2
  14. package/dist/engine/engine-workspace-authority.js +10 -3
  15. package/dist/engine/engine.d.ts +8 -4
  16. package/dist/engine/engine.js +115 -15
  17. package/dist/engine/prompt-cache-diagnostics.js +10 -1
  18. package/dist/engine/run-goal.js +7 -3
  19. package/dist/engine/run-tooling.d.ts +3 -0
  20. package/dist/engine/run-tooling.js +25 -23
  21. package/dist/engine/run-types.d.ts +20 -0
  22. package/dist/engine/run-workspace.js +5 -21
  23. package/dist/engine/subagent-spawner.d.ts +3 -0
  24. package/dist/engine/subagent-spawner.js +48 -17
  25. package/dist/engine/turn-loop.d.ts +14 -4
  26. package/dist/engine/turn-loop.js +120 -28
  27. package/dist/engine/types.d.ts +19 -0
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +2 -2
  30. package/dist/links/index.d.ts +1 -0
  31. package/dist/links/index.js +1 -0
  32. package/dist/links/link-action-tool.d.ts +2 -1
  33. package/dist/links/link-action-tool.js +80 -44
  34. package/dist/links/status.d.ts +53 -0
  35. package/dist/links/status.js +175 -0
  36. package/dist/llm/prompt-cache.d.ts +35 -3
  37. package/dist/llm/prompt-cache.js +63 -3
  38. package/dist/llm/providers/openai.d.ts +3 -0
  39. package/dist/llm/providers/openai.js +57 -14
  40. package/dist/prompt/section-loader.js +1 -0
  41. package/dist/prompt/sections/browser.md +4 -2
  42. package/dist/prompt/sections/context-notes.md +9 -0
  43. package/dist/protocol/background-result-wakeup.d.ts +8 -1
  44. package/dist/protocol/background-result-wakeup.js +76 -38
  45. package/dist/protocol/chat-session-manager.d.ts +7 -1
  46. package/dist/protocol/chat-session-manager.js +44 -8
  47. package/dist/protocol/chat-session.d.ts +2 -0
  48. package/dist/protocol/chat-session.js +4 -1
  49. package/dist/protocol/server.d.ts +4 -0
  50. package/dist/protocol/server.js +355 -62
  51. package/dist/protocol/session-message-result.d.ts +12 -0
  52. package/dist/protocol/session-message-result.js +42 -0
  53. package/dist/protocol/session-message-workspace.d.ts +21 -0
  54. package/dist/protocol/session-message-workspace.js +57 -0
  55. package/dist/protocol/types.d.ts +4 -0
  56. package/dist/session/session-manager.js +8 -7
  57. package/dist/session/session-message.d.ts +17 -2
  58. package/dist/session/transcript.d.ts +17 -0
  59. package/dist/session/transcript.js +271 -16
  60. package/dist/settings/schema.d.ts +9 -0
  61. package/dist/settings/schema.js +4 -0
  62. package/dist/themes/paths.js +20 -1
  63. package/dist/tool-system/browser-bridge.d.ts +21 -1
  64. package/dist/tool-system/browser-discovery.d.ts +6 -0
  65. package/dist/tool-system/browser-discovery.js +17 -0
  66. package/dist/tool-system/builtin/agent.js +23 -9
  67. package/dist/tool-system/builtin/browser-tools.js +30 -9
  68. package/dist/tool-system/builtin/context-notes.d.ts +12 -0
  69. package/dist/tool-system/builtin/context-notes.js +188 -0
  70. package/dist/tool-system/builtin/index.js +50 -3
  71. package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
  72. package/dist/tool-system/builtin/mcp-tools.js +10 -10
  73. package/dist/tool-system/builtin/send-message-to-session.js +19 -3
  74. package/dist/tool-system/builtin/tool-search.js +15 -3
  75. package/dist/tool-system/context.d.ts +9 -0
  76. package/dist/tool-system/executor.js +5 -3
  77. package/dist/tool-system/mcp-compat.d.ts +3 -0
  78. package/dist/tool-system/mcp-compat.js +51 -0
  79. package/dist/tool-system/mcp-manager.d.ts +27 -26
  80. package/dist/tool-system/mcp-manager.js +273 -111
  81. package/dist/tool-system/mcp-workspace.d.ts +18 -0
  82. package/dist/tool-system/mcp-workspace.js +56 -0
  83. package/dist/tool-system/permission.d.ts +6 -0
  84. package/dist/tool-system/permission.js +45 -9
  85. package/dist/tool-system/plan-mode-allowlist.js +5 -0
  86. package/dist/tool-system/sandbox/seatbelt.js +71 -2
  87. package/dist/tool-system/session-tool-host.js +9 -1
  88. package/dist/types.d.ts +4 -2
  89. package/package.json +1 -1
@@ -17,8 +17,13 @@ import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotific
17
17
  import { diskDefaultsFrom } from "../engine/engine.js";
18
18
  import { ISOLATED_TASK_BEHAVIOR_MODE } from "../engine/run-types.js";
19
19
  import { isProtectedSettingKey, SettingsManager } from "../settings/manager.js";
20
- import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
20
+ import { sessionMessageOutcome, sessionMessageFailure, sessionMessageResultNotification, } from "./session-message-result.js";
21
+ import { resolveSessionMessageWorkspace } from "./session-message-workspace.js";
22
+ import { validateWorkspaceContext, workspacePrimaryRoot } from "../workspace/workspace-context.js";
23
+ import { canonicalKey } from "../workspace/canonical-key.js";
24
+ import { getApprovalRouter, getInteractiveApprovalBackend, InteractiveApprovalBackend, } from "../tool-system/permission.js";
21
25
  import { agentNotificationBus, notificationQueue, notificationEnvelopeToLegacyStreamEvent, } from "../tool-system/builtin/agent-notifications.js";
26
+ import { clearSessionPathApprovals, openSessionPathApprovals } from "../tool-system/path-policy.js";
22
27
  import { backgroundShellManager } from "../runtime/background-shell.js";
23
28
  import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
24
29
  import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
@@ -168,6 +173,7 @@ const COMPACT_STREAM_STRATEGIES = new Set([
168
173
  // Range archival (archive_range query) emits its boundary through this map;
169
174
  // without "range" here it would be silently downgraded to "compacted".
170
175
  "range",
176
+ "notes",
171
177
  ]);
172
178
  function toCompactStreamStrategy(strategy) {
173
179
  return COMPACT_STREAM_STRATEGIES.has(strategy)
@@ -318,6 +324,7 @@ export class AgentServer {
318
324
  panelBridgeEnabled;
319
325
  connectionId;
320
326
  strictApprovalRouting;
327
+ childHostDisposers = new Map();
321
328
  approvalRouter;
322
329
  approvalConnectionUnregister = null;
323
330
  disconnected = false;
@@ -593,6 +600,13 @@ export class AgentServer {
593
600
  approvalRouter: this.approvalRouter,
594
601
  onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
595
602
  notificationMailbox: this.notificationMailbox,
603
+ resolveWorkspace: this.workspaceBridgeEnabled
604
+ ? async (session) => {
605
+ const slice = await this.resolveHostSessionWorkspace(session, sessionId);
606
+ this.rememberSessionSlice(sessionId, slice);
607
+ return slice;
608
+ }
609
+ : undefined,
596
610
  });
597
611
  }
598
612
  async rehydrateSessionForWake(sessionId) {
@@ -622,7 +636,8 @@ export class AgentServer {
622
636
  });
623
637
  return null;
624
638
  }
625
- const session = await this.chatManager.getOrCreate(sessionId, slice);
639
+ const session = await this.chatManager.getOrCreate(sessionId, slice, { allowReopen: false });
640
+ session.engine.restoreSessionModel?.(sessionId);
626
641
  this.wireInteractiveSession(session, sessionId);
627
642
  logger.debug("bg_wakeup.rehydrated_session", {
628
643
  sessionId,
@@ -662,6 +677,151 @@ export class AgentServer {
662
677
  session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
663
678
  session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
664
679
  session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
680
+ session.engine.setChildHostBindings?.((input) => {
681
+ if (input.parentSessionId !== sid)
682
+ throw new Error("child host parent identity mismatch");
683
+ assertSafeSessionId(input.sessionId);
684
+ const route = this.approvalRouter.current(sid);
685
+ if (!route || route.connectionId !== this.connectionId) {
686
+ throw new Error("child host unavailable: parent approval connection no longer owns session");
687
+ }
688
+ const backend = new InteractiveApprovalBackend();
689
+ backend.openSession(input.sessionId);
690
+ let active = false;
691
+ let closed = false;
692
+ let closedFailure = {
693
+ failure: "cancelled",
694
+ reason: "child run ended or was cancelled",
695
+ };
696
+ const scope = {
697
+ sourceSessionId: input.sessionId,
698
+ bindingId: nanoid(),
699
+ route,
700
+ pending: new Set(),
701
+ failure: () => {
702
+ if (closed || input.signal?.aborted)
703
+ return closedFailure;
704
+ if (this.chatManager?.get(sid) !== session)
705
+ return { failure: "session_closed", reason: "parent host session closed" };
706
+ if (this.disconnected || !this.approvalRouter.matches(route))
707
+ return { failure: "owner_lost", reason: "parent host approval ownership changed" };
708
+ return undefined;
709
+ },
710
+ };
711
+ backend.setRequestGuard((request) => {
712
+ const failure = scope.failure();
713
+ if (failure)
714
+ return { approved: false, ...failure };
715
+ if (request.sessionId !== input.sessionId)
716
+ return {
717
+ approved: false,
718
+ failure: "owner_lost",
719
+ reason: "child approval identity mismatch",
720
+ };
721
+ return undefined;
722
+ });
723
+ backend.setPromptFn((request) => {
724
+ const failure = scope.failure();
725
+ if (failure)
726
+ return Promise.resolve({ approved: false, ...failure });
727
+ if (request.sessionId !== input.sessionId) {
728
+ return Promise.resolve({
729
+ approved: false,
730
+ failure: "owner_lost",
731
+ reason: "child approval identity mismatch",
732
+ });
733
+ }
734
+ return this.requestApprovalFromClient(request, route, scope);
735
+ });
736
+ const dispose = () => {
737
+ if (closed)
738
+ return;
739
+ closed = true;
740
+ input.signal?.removeEventListener("abort", dispose);
741
+ this.childHostDisposers.delete(scope.bindingId);
742
+ backend.clearSession(input.sessionId);
743
+ if (active)
744
+ clearSessionPathApprovals(input.sessionId);
745
+ for (const id of scope.pending) {
746
+ const entry = this.takeSessionApproval(session, id, "cancelled");
747
+ this.clearApprovalTimer(id);
748
+ this.pendingApprovalTargets.delete(id);
749
+ entry?.resolve(entry.metadata.kind === "internal"
750
+ ? internalCancellation(closedFailure.failure, closedFailure.reason)
751
+ : {
752
+ approved: false,
753
+ ...closedFailure,
754
+ });
755
+ try {
756
+ this.notify(Methods.ApprovalResolved, { sessionId: sid, requestId: id });
757
+ }
758
+ catch {
759
+ /* transport may already be closed */
760
+ }
761
+ }
762
+ scope.pending.clear();
763
+ // One-way release uses the existing private host channel; it is never
764
+ // a model tool and never inherits a parent tab handoff grant.
765
+ try {
766
+ this.notify(Methods.ApprovalRequest, {
767
+ sessionId: sid,
768
+ requestId: nanoid(12),
769
+ childHost: {
770
+ sourceSessionId: input.sessionId,
771
+ bindingId: scope.bindingId,
772
+ release: true,
773
+ },
774
+ request: {
775
+ toolName: "__browser_action__",
776
+ args: { action: "release" },
777
+ description: "release child browser",
778
+ riskLevel: "low",
779
+ },
780
+ });
781
+ }
782
+ catch {
783
+ /* resource owner also closes targets on host teardown */
784
+ }
785
+ };
786
+ this.childHostDisposers.set(scope.bindingId, {
787
+ route,
788
+ dispose: (reason) => {
789
+ closedFailure = { failure: "owner_lost", reason };
790
+ dispose();
791
+ },
792
+ });
793
+ input.signal?.addEventListener("abort", dispose, { once: true });
794
+ if (input.signal?.aborted)
795
+ dispose();
796
+ return {
797
+ activate: () => {
798
+ if (!closed && !active) {
799
+ this.notify(Methods.ApprovalRequest, {
800
+ sessionId: sid,
801
+ requestId: nanoid(12),
802
+ childHost: {
803
+ sourceSessionId: input.sessionId,
804
+ bindingId: scope.bindingId,
805
+ activate: true,
806
+ },
807
+ request: {
808
+ toolName: "__browser_action__",
809
+ args: { action: "activate" },
810
+ description: "bind child browser",
811
+ riskLevel: "low",
812
+ },
813
+ });
814
+ active = true;
815
+ openSessionPathApprovals(input.sessionId);
816
+ }
817
+ },
818
+ approvalBackend: backend,
819
+ browserBridge: this.makeBrowserBridge(session, sid, scope),
820
+ askUser: (question, opts) => this.requestAskUserForSession(session, sid, question, opts, scope),
821
+ injectCredentialToBrowser: (id, credentialScope) => this.requestCredentialInjectForSession(session, sid, id, credentialScope, scope),
822
+ dispose,
823
+ };
824
+ });
665
825
  session.engine.setSessionMessageRouter((input) => this.routeSessionMessage(input));
666
826
  if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
667
827
  session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
@@ -672,6 +832,7 @@ export class AgentServer {
672
832
  }
673
833
  /** Queue a model-sent message as an ordinary user turn in another Session. */
674
834
  async routeSessionMessage(input) {
835
+ input.signal?.throwIfAborted();
675
836
  const manager = this.chatManager;
676
837
  if (!manager)
677
838
  throw new Error("cross-Session messaging requires a multi-session host");
@@ -680,12 +841,49 @@ export class AgentServer {
680
841
  throw new Error(`target Session is closing or closed: ${targetId}`);
681
842
  }
682
843
  const sourceSlice = this.lastSliceBySession.get(input.sourceSessionId);
683
- const targetSlice = {
684
- cwd: input.target.workspaceRoot,
685
- projectTrusted: sourceSlice?.projectTrusted ?? false,
844
+ const sourceSession = manager.get(input.sourceSessionId);
845
+ const assertStillAuthorized = () => {
846
+ input.signal?.throwIfAborted();
847
+ if (this.disconnected || manager.isUnavailable(targetId)) {
848
+ throw new Error(`target Session is closing or closed: ${targetId}`);
849
+ }
850
+ if (sourceSession &&
851
+ (manager.get(input.sourceSessionId) !== sourceSession ||
852
+ sourceSession.wasCancelledSinceLastTurn())) {
853
+ throw new Error("source Session was cancelled or closed before the message was queued");
854
+ }
686
855
  };
856
+ let targetSlice;
857
+ if (this.workspaceBridgeEnabled) {
858
+ if (!sourceSession)
859
+ throw new Error("source Session is no longer available");
860
+ targetSlice = await this.resolveHostSessionWorkspace(sourceSession, targetId);
861
+ }
862
+ else {
863
+ const sessionManager = sourceSession?.engine.getSessionManager?.() ??
864
+ (this.diskSessionReader ??= new SessionManager(this.sessionDiskRoot));
865
+ const sourceWorkspace = sourceSession?.engine.resolveSessionRunWorkspace?.(input.sourceSessionId);
866
+ targetSlice = resolveSessionMessageWorkspace({
867
+ sessionManager,
868
+ sourceSessionId: input.sourceSessionId,
869
+ targetSessionId: targetId,
870
+ sourceWorkspace: {
871
+ cwd: sourceWorkspace?.cwd ?? sourceSlice?.cwd ?? input.target.workspaceRoot,
872
+ workspaceContext: sourceWorkspace?.workspaceContext ?? sourceSlice?.workspaceContext,
873
+ projectTrusted: sourceSlice?.projectTrusted ?? false,
874
+ },
875
+ });
876
+ }
877
+ assertStillAuthorized();
687
878
  const targetAlreadyExists = manager.sessionExistsOnDisk(targetId, targetSlice);
688
- const targetSession = await manager.getOrCreate(targetId, targetSlice);
879
+ const targetWasResident = manager.get(targetId) !== undefined;
880
+ const targetSession = await manager.getOrCreate(targetId, targetSlice, {
881
+ allowReopen: false,
882
+ signal: input.signal,
883
+ });
884
+ assertStillAuthorized();
885
+ if (targetAlreadyExists && !targetWasResident)
886
+ targetSession.engine.restoreSessionModel?.(targetId);
689
887
  const approvalRegistration = this.approvalRouter.register(targetId, this.connectionId);
690
888
  if (!approvalRegistration.ok) {
691
889
  throw new Error(`target Session ${targetId} is owned by another connection`);
@@ -693,6 +891,43 @@ export class AgentServer {
693
891
  this.rememberSessionSlice(targetId, targetSlice);
694
892
  this.observeSessionAttached(targetId, targetSession.lastActivityAt);
695
893
  this.wireInteractiveSession(targetSession, targetId);
894
+ const messageId = `session-message-${nanoid(12)}`;
895
+ const state = { started: false, acknowledged: false, errorStreamed: false };
896
+ const finish = (outcome, result) => {
897
+ state.outcome = outcome;
898
+ state.result = result;
899
+ if (state.started)
900
+ this.observeRunBoundary(targetId, "end");
901
+ if (outcome.status !== "completed") {
902
+ logger.warn("session_message.turn_failed", {
903
+ sourceSessionId: input.sourceSessionId,
904
+ targetSessionId: targetId,
905
+ messageId,
906
+ error: outcome.error,
907
+ });
908
+ // Setup can return a failure without ever emitting an error event.
909
+ // UI observation must not prevent the durable reply from being queued.
910
+ try {
911
+ if (!state.errorStreamed) {
912
+ this.notify(Methods.StreamEvent, {
913
+ sessionId: targetId,
914
+ event: { type: "error", error: outcome.error ?? "cross-Session message failed" },
915
+ });
916
+ }
917
+ }
918
+ catch (error) {
919
+ logger.warn("session_message.error_observation_failed", {
920
+ messageId,
921
+ error: String(error),
922
+ });
923
+ }
924
+ }
925
+ if (state.acknowledged) {
926
+ this.notificationMailbox.enqueue(sessionMessageResultNotification(input, messageId, outcome));
927
+ }
928
+ if (result && result.turnCount > 0)
929
+ void this.maybeWakeIdleSession(targetId);
930
+ };
696
931
  const userMessageEvent = {
697
932
  type: "session_user_message",
698
933
  text: input.message,
@@ -700,50 +935,43 @@ export class AgentServer {
700
935
  this.observeSessionStream(targetId, userMessageEvent);
701
936
  this.notify(Methods.StreamEvent, { sessionId: targetId, event: userMessageEvent });
702
937
  const run = targetSession.enqueueTurn(input.message, {
703
- cwd: input.target.workspaceRoot,
938
+ cwd: targetSlice.cwd,
939
+ workspaceContext: targetSlice.workspaceContext,
704
940
  // A planned Session needs its renderer-selected initial profile. Once a
705
941
  // Session exists, omitting this field makes Engine use the target's own
706
942
  // persisted binding, so a stale source catalog cannot switch it back.
707
943
  workspaceProfile: targetAlreadyExists ? undefined : input.target.workspaceProfile,
708
944
  sessionMessageTargets: input.catalog,
709
945
  onStream: (event) => {
946
+ if (event.type === "session_started" && !state.started) {
947
+ state.started = true;
948
+ this.observeRunBoundary(targetId, "start");
949
+ }
950
+ if (event.type === "error")
951
+ state.errorStreamed = true;
710
952
  this.observeSessionStream(targetId, event);
711
953
  this.notify(Methods.StreamEvent, { sessionId: targetId, event });
712
954
  },
713
955
  approvalRouter: this.approvalRouter,
714
956
  });
715
- // The turn itself must NOT block the sender — a member's work can take
716
- // minutes and the lead has more to dispatch. But a turn that fails to *start*
717
- // (most often: the target's digital human is not installed, so run-setup
718
- // throws "Workspace profile ... is unavailable") used to be reported only to
719
- // the log while SendMessageToSession still answered "has queued the turn".
720
- // A real lead then waited two hours and re-sent, never learning the reason.
721
- // So: surface an immediate failure to the caller, keep a slow turn detached.
722
- let startupError;
723
- const tracked = run
724
- .then(() => this.maybeWakeIdleSession(targetId))
725
- .catch((error) => {
726
- startupError = error;
727
- logger.warn("session_message.turn_failed", {
728
- sourceSessionId: input.sourceSessionId,
729
- targetSessionId: targetId,
730
- error: error instanceof Error ? error.message : String(error),
731
- });
732
- this.notify(Methods.StreamEvent, {
733
- sessionId: targetId,
734
- event: {
735
- type: "error",
736
- error: error instanceof Error ? error.message : "cross-Session message failed",
737
- },
738
- });
739
- });
740
- // One macrotask is enough for a synchronous-throw path to settle; anything
741
- // still running by then is genuine work and stays detached.
957
+ const tracked = run.then((result) => finish(sessionMessageOutcome(result), result), (error) => finish(sessionMessageFailure(error)));
958
+ // Acknowledge promptly, but never infer "started" from elapsed time. If
959
+ // setup/work is still pending, its eventual outcome takes the mailbox path.
742
960
  await Promise.race([tracked, new Promise((resolve) => setTimeout(resolve, 0))]);
743
- if (startupError) {
744
- throw startupError instanceof Error ? startupError : new Error(String(startupError));
961
+ if (state.outcome) {
962
+ if (state.outcome.status !== "completed")
963
+ throw new Error(state.outcome.error);
964
+ return {
965
+ messageId,
966
+ status: "completed",
967
+ result: { text: state.outcome.text, reason: state.result.reason },
968
+ };
745
969
  }
746
- void tracked;
970
+ state.acknowledged = true;
971
+ void tracked.catch((error) => {
972
+ logger.warn("session_message.result_delivery_failed", { messageId, error: String(error) });
973
+ });
974
+ return { messageId, status: state.started ? "started" : "queued" };
747
975
  }
748
976
  // ─── Request Dispatch ───────────────────────────────────────────
749
977
  async handleRequest(req) {
@@ -1233,6 +1461,7 @@ export class AgentServer {
1233
1461
  (params.goal != null && typeof params.goal === "object")
1234
1462
  ? params.goal
1235
1463
  : undefined,
1464
+ disableGoal: params.disableGoal === true,
1236
1465
  onStream: (event) => {
1237
1466
  this.observeSessionStream(sid, event);
1238
1467
  this.notify(Methods.StreamEvent, { sessionId: sid, event });
@@ -1510,7 +1739,7 @@ export class AgentServer {
1510
1739
  }
1511
1740
  this.abortController.abort();
1512
1741
  for (const [, resolve] of this.pendingApprovals) {
1513
- resolve({ approved: false, reason: "cancelled" });
1742
+ resolve({ approved: false, failure: "cancelled", reason: "cancelled" });
1514
1743
  }
1515
1744
  this.pendingApprovals.clear();
1516
1745
  this.clearAllApprovalTimers();
@@ -2714,7 +2943,10 @@ export class AgentServer {
2714
2943
  /**
2715
2944
  * Ask the client to approve a tool operation (legacy single-engine path).
2716
2945
  */
2717
- requestApprovalFromClient(request, route) {
2946
+ requestApprovalFromClient(request, route, scope) {
2947
+ const failure = scope?.failure();
2948
+ if (failure)
2949
+ return Promise.resolve({ approved: false, ...failure });
2718
2950
  return new Promise((resolve) => {
2719
2951
  const requestId = nanoid(12);
2720
2952
  const effectiveRoute = route ?? {
@@ -2725,7 +2957,11 @@ export class AgentServer {
2725
2957
  const sessionId = effectiveRoute.sessionId;
2726
2958
  const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
2727
2959
  if (this.chatManager && sessionId && !session) {
2728
- resolve({ approved: false, reason: `session closed: ${sessionId}` });
2960
+ resolve({
2961
+ approved: false,
2962
+ failure: "session_closed",
2963
+ reason: `session closed: ${sessionId}`,
2964
+ });
2729
2965
  return;
2730
2966
  }
2731
2967
  if (session) {
@@ -2743,7 +2979,7 @@ export class AgentServer {
2743
2979
  createdAt: Date.now(),
2744
2980
  expiresAt: Date.now() + AgentServer.APPROVAL_TIMEOUT_MS,
2745
2981
  surfaceable: !internal,
2746
- }, (decision) => resolve(decision));
2982
+ }, (decision) => resolve(decision), scope);
2747
2983
  }
2748
2984
  else {
2749
2985
  this.pendingApprovals.set(requestId, resolve);
@@ -2760,7 +2996,8 @@ export class AgentServer {
2760
2996
  }
2761
2997
  this.pendingApprovalTargets.delete(requestId);
2762
2998
  this.approvalTimers.delete(requestId);
2763
- resolve({ approved: false, reason: "approval timed out" });
2999
+ scope?.pending.delete(requestId);
3000
+ resolve({ approved: false, failure: "timed_out", reason: "approval timed out" });
2764
3001
  }
2765
3002
  }, AgentServer.APPROVAL_TIMEOUT_MS);
2766
3003
  this.approvalTimers.set(requestId, timer);
@@ -2788,7 +3025,10 @@ export class AgentServer {
2788
3025
  * renderer routes the question to the right chat tab. This intentionally has
2789
3026
  * no wall-clock timeout; Stop/cancel drains the pending ask.
2790
3027
  */
2791
- requestAskUserForSession(session, sessionId, question, opts) {
3028
+ requestAskUserForSession(session, sessionId, question, opts, scope) {
3029
+ const failure = scope?.failure();
3030
+ if (failure)
3031
+ return Promise.resolve(`(question ${failure.failure}: ${failure.reason})`);
2792
3032
  return new Promise((resolve) => {
2793
3033
  const requestId = nanoid(12);
2794
3034
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
@@ -2813,7 +3053,7 @@ export class AgentServer {
2813
3053
  // chatManager approve handler resolves with the raw decision value.
2814
3054
  resolve(typeof decision === "string" ? decision : "");
2815
3055
  }
2816
- });
3056
+ }, scope);
2817
3057
  const args = { question };
2818
3058
  if (opts?.header !== undefined)
2819
3059
  args.header = opts.header;
@@ -2825,6 +3065,7 @@ export class AgentServer {
2825
3065
  args.optionsOnly = opts.optionsOnly;
2826
3066
  this.notify(Methods.ApprovalRequest, {
2827
3067
  ...routeEnvelope,
3068
+ ...(scope ? { sourceSessionId: scope.sourceSessionId } : {}),
2828
3069
  request: {
2829
3070
  toolName: "__ask_user__",
2830
3071
  args,
@@ -2872,8 +3113,8 @@ export class AgentServer {
2872
3113
  * result string we parse back into the typed result. On timeout / malformed
2873
3114
  * reply we degrade to a safe error result rather than throwing.
2874
3115
  */
2875
- makeBrowserBridge(session, sessionId) {
2876
- const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
3116
+ makeBrowserBridge(session, sessionId, scope) {
3117
+ const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload, scope);
2877
3118
  return {
2878
3119
  requestHumanTakeover: () => call("requestTakeover", {}),
2879
3120
  snapshot: () => call("snapshot", {}),
@@ -2893,7 +3134,10 @@ export class AgentServer {
2893
3134
  switchTab: (tabId) => call("switchTab", { tabId }),
2894
3135
  };
2895
3136
  }
2896
- requestBrowserActionForSession(session, sessionId, action, payload) {
3137
+ requestBrowserActionForSession(session, sessionId, action, payload, scope) {
3138
+ const failure = scope?.failure();
3139
+ if (failure)
3140
+ return Promise.resolve({ ok: false, failure: failure.failure, detail: failure.reason });
2897
3141
  return new Promise((resolve) => {
2898
3142
  const requestId = nanoid(12);
2899
3143
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
@@ -2905,12 +3149,13 @@ export class AgentServer {
2905
3149
  resolve(parsed.ok
2906
3150
  ? parsed.value
2907
3151
  : { ok: false, failure: parsed.failure, detail: parsed.detail });
2908
- });
3152
+ }, scope);
2909
3153
  const timer = setTimeout(() => {
2910
3154
  if (session.pendingApprovals.has(requestId)) {
2911
3155
  this.takeSessionApproval(session, requestId, "expired");
2912
3156
  this.pendingApprovalTargets.delete(requestId);
2913
3157
  this.approvalTimers.delete(requestId);
3158
+ scope?.pending.delete(requestId);
2914
3159
  resolve({
2915
3160
  ok: false,
2916
3161
  failure: "timed_out",
@@ -2921,6 +3166,9 @@ export class AgentServer {
2921
3166
  this.approvalTimers.set(requestId, timer);
2922
3167
  this.notify(Methods.ApprovalRequest, {
2923
3168
  ...routeEnvelope,
3169
+ ...(scope
3170
+ ? { childHost: { sourceSessionId: scope.sourceSessionId, bindingId: scope.bindingId } }
3171
+ : {}),
2924
3172
  request: {
2925
3173
  toolName: "__browser_action__",
2926
3174
  args: { action, ...payload },
@@ -2936,7 +3184,10 @@ export class AgentServer {
2936
3184
  * calls restoreCookiesToBrowser and replies with a JSON result string we
2937
3185
  * parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
2938
3186
  */
2939
- requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
3187
+ requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full", scope) {
3188
+ const failure = scope?.failure();
3189
+ if (failure)
3190
+ return Promise.resolve({ ok: false, error: `${failure.failure}: ${failure.reason}` });
2940
3191
  return new Promise((resolve) => {
2941
3192
  const requestId = nanoid(12);
2942
3193
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
@@ -2946,12 +3197,13 @@ export class AgentServer {
2946
3197
  resolve(parsed.ok
2947
3198
  ? parsed.value
2948
3199
  : { ok: false, error: parsed.detail });
2949
- });
3200
+ }, scope);
2950
3201
  const timer = setTimeout(() => {
2951
3202
  if (session.pendingApprovals.has(requestId)) {
2952
3203
  this.takeSessionApproval(session, requestId, "expired");
2953
3204
  this.pendingApprovalTargets.delete(requestId);
2954
3205
  this.approvalTimers.delete(requestId);
3206
+ scope?.pending.delete(requestId);
2955
3207
  resolve({
2956
3208
  ok: false,
2957
3209
  error: `credential inject ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
@@ -2961,6 +3213,7 @@ export class AgentServer {
2961
3213
  this.approvalTimers.set(requestId, timer);
2962
3214
  this.notify(Methods.ApprovalRequest, {
2963
3215
  ...routeEnvelope,
3216
+ ...(scope ? { sourceSessionId: scope.sourceSessionId } : {}),
2964
3217
  request: {
2965
3218
  toolName: "__credential_action__",
2966
3219
  args: { action: "injectCookie", credentialId, credentialScope },
@@ -3075,13 +3328,37 @@ export class AgentServer {
3075
3328
  });
3076
3329
  });
3077
3330
  }
3078
- requestWorkspaceSwitchForSession(session, sessionId, target) {
3331
+ async requestWorkspaceSwitchForSession(session, sessionId, target) {
3332
+ return (await this.requestWorkspaceActionForSession(session, sessionId, "switch", target));
3333
+ }
3334
+ /** The same host authority resolves both dispatched turns and their reply wakeups. */
3335
+ async resolveHostSessionWorkspace(source, targetId) {
3336
+ const registration = this.approvalRouter.register(source.id, this.connectionId);
3337
+ if (!registration.ok)
3338
+ throw new Error(`source Session ${source.id} is owned by another connection`);
3339
+ const resolved = (await this.requestWorkspaceActionForSession(source, source.id, "resolve_session_run", targetId));
3340
+ if (typeof resolved.cwd !== "string" ||
3341
+ !resolved.cwd ||
3342
+ typeof resolved.projectTrusted !== "boolean") {
3343
+ throw new Error("host returned an invalid Session workspace");
3344
+ }
3345
+ const workspaceContext = resolved.workspaceContext === undefined
3346
+ ? undefined
3347
+ : validateWorkspaceContext(resolved.workspaceContext);
3348
+ if (workspaceContext &&
3349
+ canonicalKey(workspacePrimaryRoot(workspaceContext).path) !== canonicalKey(resolved.cwd)) {
3350
+ throw new Error("host Session workspace does not match its working directory");
3351
+ }
3352
+ return { cwd: resolved.cwd, workspaceContext, projectTrusted: resolved.projectTrusted };
3353
+ }
3354
+ requestWorkspaceActionForSession(session, sessionId, action, target) {
3355
+ const label = action === "switch" ? "workspace switch" : "Session workspace resolution";
3079
3356
  return new Promise((resolve, reject) => {
3080
3357
  const requestId = nanoid(12);
3081
3358
  const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
3082
3359
  this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__workspace_action__"), (decision) => {
3083
3360
  this.clearApprovalTimer(requestId);
3084
- const outcome = parseHostLoopbackDecision(decision, "workspace switch");
3361
+ const outcome = parseHostLoopbackDecision(decision, label);
3085
3362
  if (!outcome.ok) {
3086
3363
  reject(new Error(outcome.detail));
3087
3364
  return;
@@ -3094,11 +3371,11 @@ export class AgentServer {
3094
3371
  // parsed` TypeError happened to be caught and turned into a rejection;
3095
3372
  // this states the requirement instead of relying on that.)
3096
3373
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3097
- reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`));
3374
+ reject(new Error(`${label} ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`));
3098
3375
  return;
3099
3376
  }
3100
3377
  if ("ok" in parsed && parsed.ok === false) {
3101
- reject(new Error(parsed.error ?? "workspace switch failed"));
3378
+ reject(new Error(parsed.error ?? `${label} failed`));
3102
3379
  return;
3103
3380
  }
3104
3381
  resolve(parsed);
@@ -3108,7 +3385,7 @@ export class AgentServer {
3108
3385
  this.takeSessionApproval(session, requestId, "expired");
3109
3386
  this.pendingApprovalTargets.delete(requestId);
3110
3387
  this.approvalTimers.delete(requestId);
3111
- reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`));
3388
+ reject(new Error(`${label} ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`));
3112
3389
  }
3113
3390
  }, AgentServer.APPROVAL_TIMEOUT_MS);
3114
3391
  this.approvalTimers.set(requestId, timer);
@@ -3116,8 +3393,8 @@ export class AgentServer {
3116
3393
  ...routeEnvelope,
3117
3394
  request: {
3118
3395
  toolName: "__workspace_action__",
3119
- args: { action: "switch", target },
3120
- description: `workspace:switch:${target}`,
3396
+ args: { action, target },
3397
+ description: `workspace:${action}:${target}`,
3121
3398
  riskLevel: "low",
3122
3399
  },
3123
3400
  });
@@ -3226,7 +3503,7 @@ export class AgentServer {
3226
3503
  }
3227
3504
  }
3228
3505
  for (const [, resolve] of this.pendingApprovals) {
3229
- resolve({ approved: false, reason: "server closing" });
3506
+ resolve({ approved: false, failure: "session_closed", reason: "server closing" });
3230
3507
  }
3231
3508
  this.pendingApprovals.clear();
3232
3509
  this.clearAllApprovalTimers();
@@ -3259,9 +3536,19 @@ export class AgentServer {
3259
3536
  workerGeneration() {
3260
3537
  return this.chatManager?.getLiveSessionSnapshot().generation ?? 0;
3261
3538
  }
3262
- registerSessionApproval(session, metadata, resolve) {
3539
+ registerSessionApproval(session, metadata, resolve, scope) {
3540
+ if (scope) {
3541
+ metadata = { ...metadata, sourceSessionId: scope.sourceSessionId };
3542
+ scope.pending.add(metadata.requestId);
3543
+ }
3263
3544
  metadata = this.observeApprovalCreated(metadata);
3264
- session.pendingApprovals.set(metadata.requestId, { resolve, metadata });
3545
+ session.pendingApprovals.set(metadata.requestId, {
3546
+ resolve: (decision) => {
3547
+ scope?.pending.delete(metadata.requestId);
3548
+ resolve(decision);
3549
+ },
3550
+ metadata,
3551
+ });
3265
3552
  }
3266
3553
  takeSessionApproval(session, requestId, status) {
3267
3554
  const entry = session.pendingApprovals.get(requestId);
@@ -3341,7 +3628,7 @@ export class AgentServer {
3341
3628
  try {
3342
3629
  entry.resolve(entry.metadata.kind === "internal"
3343
3630
  ? internalCancellation(failure, reason)
3344
- : { approved: false, reason });
3631
+ : { approved: false, failure, reason });
3345
3632
  }
3346
3633
  catch {
3347
3634
  /* a resolver must never break cancel cleanup */
@@ -3352,6 +3639,12 @@ export class AgentServer {
3352
3639
  }
3353
3640
  failClosedApprovalTargets(targets, reason) {
3354
3641
  for (const target of targets) {
3642
+ for (const child of this.childHostDisposers.values()) {
3643
+ if (child.route.connectionId === target.connectionId &&
3644
+ child.route.sessionId === target.sessionId &&
3645
+ child.route.generation === target.generation)
3646
+ child.dispose(reason);
3647
+ }
3355
3648
  const session = this.chatManager?.get(target.sessionId);
3356
3649
  if (session) {
3357
3650
  this.cancelSessionApprovals(session, reason, "owner-lost", "owner_lost");
@@ -3367,7 +3660,7 @@ export class AgentServer {
3367
3660
  this.pendingApprovals.delete(requestId);
3368
3661
  this.pendingApprovalTargets.delete(requestId);
3369
3662
  this.clearApprovalTimer(requestId);
3370
- resolve?.({ approved: false, reason });
3663
+ resolve?.({ approved: false, failure: "owner_lost", reason });
3371
3664
  }
3372
3665
  }
3373
3666
  }