@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.
- package/dist/automation/scheduler.d.ts +3 -0
- package/dist/automation/scheduler.js +21 -0
- package/dist/context/manager.d.ts +8 -2
- package/dist/context/manager.js +20 -4
- package/dist/context/notes.d.ts +39 -0
- package/dist/context/notes.js +314 -0
- package/dist/credentials/access.d.ts +9 -1
- package/dist/credentials/access.js +15 -4
- package/dist/credentials/store.d.ts +11 -0
- package/dist/credentials/store.js +44 -9
- package/dist/credentials/types.d.ts +4 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/use-credential-tool.js +12 -2
- package/dist/engine/engine-workspace-authority.js +10 -3
- package/dist/engine/engine.d.ts +8 -4
- package/dist/engine/engine.js +115 -15
- package/dist/engine/prompt-cache-diagnostics.js +10 -1
- package/dist/engine/run-goal.js +7 -3
- package/dist/engine/run-tooling.d.ts +3 -0
- package/dist/engine/run-tooling.js +25 -23
- package/dist/engine/run-types.d.ts +20 -0
- package/dist/engine/run-workspace.js +5 -21
- package/dist/engine/subagent-spawner.d.ts +3 -0
- package/dist/engine/subagent-spawner.js +48 -17
- package/dist/engine/turn-loop.d.ts +14 -4
- package/dist/engine/turn-loop.js +120 -28
- package/dist/engine/types.d.ts +19 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/links/index.d.ts +1 -0
- package/dist/links/index.js +1 -0
- package/dist/links/link-action-tool.d.ts +2 -1
- package/dist/links/link-action-tool.js +80 -44
- package/dist/links/status.d.ts +53 -0
- package/dist/links/status.js +175 -0
- package/dist/llm/prompt-cache.d.ts +35 -3
- package/dist/llm/prompt-cache.js +63 -3
- package/dist/llm/providers/openai.d.ts +3 -0
- package/dist/llm/providers/openai.js +57 -14
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/browser.md +4 -2
- package/dist/prompt/sections/context-notes.md +9 -0
- package/dist/protocol/background-result-wakeup.d.ts +8 -1
- package/dist/protocol/background-result-wakeup.js +76 -38
- package/dist/protocol/chat-session-manager.d.ts +7 -1
- package/dist/protocol/chat-session-manager.js +44 -8
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/chat-session.js +4 -1
- package/dist/protocol/server.d.ts +4 -0
- package/dist/protocol/server.js +355 -62
- package/dist/protocol/session-message-result.d.ts +12 -0
- package/dist/protocol/session-message-result.js +42 -0
- package/dist/protocol/session-message-workspace.d.ts +21 -0
- package/dist/protocol/session-message-workspace.js +57 -0
- package/dist/protocol/types.d.ts +4 -0
- package/dist/session/session-manager.js +8 -7
- package/dist/session/session-message.d.ts +17 -2
- package/dist/session/transcript.d.ts +17 -0
- package/dist/session/transcript.js +271 -16
- package/dist/settings/schema.d.ts +9 -0
- package/dist/settings/schema.js +4 -0
- package/dist/themes/paths.js +20 -1
- package/dist/tool-system/browser-bridge.d.ts +21 -1
- package/dist/tool-system/browser-discovery.d.ts +6 -0
- package/dist/tool-system/browser-discovery.js +17 -0
- package/dist/tool-system/builtin/agent.js +23 -9
- package/dist/tool-system/builtin/browser-tools.js +30 -9
- package/dist/tool-system/builtin/context-notes.d.ts +12 -0
- package/dist/tool-system/builtin/context-notes.js +188 -0
- package/dist/tool-system/builtin/index.js +50 -3
- package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
- package/dist/tool-system/builtin/mcp-tools.js +10 -10
- package/dist/tool-system/builtin/send-message-to-session.js +19 -3
- package/dist/tool-system/builtin/tool-search.js +15 -3
- package/dist/tool-system/context.d.ts +9 -0
- package/dist/tool-system/executor.js +5 -3
- package/dist/tool-system/mcp-compat.d.ts +3 -0
- package/dist/tool-system/mcp-compat.js +51 -0
- package/dist/tool-system/mcp-manager.d.ts +27 -26
- package/dist/tool-system/mcp-manager.js +273 -111
- package/dist/tool-system/mcp-workspace.d.ts +18 -0
- package/dist/tool-system/mcp-workspace.js +56 -0
- package/dist/tool-system/permission.d.ts +6 -0
- package/dist/tool-system/permission.js +45 -9
- package/dist/tool-system/plan-mode-allowlist.js +5 -0
- package/dist/tool-system/sandbox/seatbelt.js +71 -2
- package/dist/tool-system/session-tool-host.js +9 -1
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
package/dist/protocol/server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
716
|
-
//
|
|
717
|
-
//
|
|
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 (
|
|
744
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 ??
|
|
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(
|
|
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
|
|
3120
|
-
description: `workspace
|
|
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, {
|
|
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
|
}
|