@superblocksteam/gateway 2.0.167-next.0 → 2.0.169-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -28
- package/dist/agents/resolve-orchestrator-url.js +1 -2
- package/dist/agents/resolve-orchestrator-url.js.map +1 -1
- package/dist/capabilities/lifecycle.d.ts +3 -0
- package/dist/capabilities/lifecycle.js +330 -128
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/live-state-attaches.d.ts +26 -0
- package/dist/capabilities/live-state-attaches.js +41 -0
- package/dist/capabilities/live-state-attaches.js.map +1 -0
- package/dist/capabilities/tool-permission-reply.d.ts +28 -0
- package/dist/capabilities/tool-permission-reply.js +55 -0
- package/dist/capabilities/tool-permission-reply.js.map +1 -0
- package/dist/capabilities/types.d.ts +19 -1
- package/dist/capabilities/types.js.map +1 -1
- package/dist/deadline.d.ts +6 -0
- package/dist/deadline.js +23 -0
- package/dist/deadline.js.map +1 -0
- package/dist/deps.d.ts +7 -0
- package/dist/health/readiness.js +1 -17
- package/dist/health/readiness.js.map +1 -1
- package/dist/integrations/filters.js +2 -2
- package/dist/integrations/filters.js.map +1 -1
- package/dist/orchestrator/client.d.ts +2 -1
- package/dist/orchestrator/client.js +4 -2
- package/dist/orchestrator/client.js.map +1 -1
- package/dist/sabs/app-state.d.ts +1 -0
- package/dist/sabs/app-state.js +9 -0
- package/dist/sabs/app-state.js.map +1 -1
- package/dist/sabs/awaited-decision.d.ts +24 -1
- package/dist/sabs/awaited-decision.js +40 -9
- package/dist/sabs/awaited-decision.js.map +1 -1
- package/dist/sabs/editor-client-methods.d.ts +21 -10
- package/dist/sabs/editor-client-methods.js +48 -24
- package/dist/sabs/editor-client-methods.js.map +1 -1
- package/dist/sabs/session-peer.d.ts +47 -14
- package/dist/sabs/session-peer.js.map +1 -1
- package/dist/sabs/turn-collector.d.ts +27 -1
- package/dist/sabs/turn-collector.js +38 -2
- package/dist/sabs/turn-collector.js.map +1 -1
- package/dist/sabs/websocket-session-peer.d.ts +27 -3
- package/dist/sabs/websocket-session-peer.js +476 -76
- package/dist/sabs/websocket-session-peer.js.map +1 -1
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +9 -3
- package/dist/server/client.js.map +1 -1
- package/dist/start.d.ts +13 -2
- package/dist/start.js +112 -12
- package/dist/start.js.map +1 -1
- package/dist/telemetry/metrics.js +8 -21
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/runtime.d.ts +5 -0
- package/dist/telemetry/runtime.js +22 -8
- package/dist/telemetry/runtime.js.map +1 -1
- package/dist/transports/mcp/admin-tools.d.ts +2 -1
- package/dist/transports/mcp/admin-tools.js +19 -2
- package/dist/transports/mcp/admin-tools.js.map +1 -1
- package/dist/transports/mcp/client-presentation.d.ts +9 -1
- package/dist/transports/mcp/client-presentation.js +10 -0
- package/dist/transports/mcp/client-presentation.js.map +1 -1
- package/dist/transports/mcp/decision-card-html.d.ts +4 -2
- package/dist/transports/mcp/decision-card-html.js +107 -25
- package/dist/transports/mcp/decision-card-html.js.map +1 -1
- package/dist/transports/mcp/decision-elicitation.d.ts +3 -2
- package/dist/transports/mcp/decision-elicitation.js +67 -2
- package/dist/transports/mcp/decision-elicitation.js.map +1 -1
- package/dist/transports/mcp/format-tool-content.d.ts +7 -0
- package/dist/transports/mcp/format-tool-content.js +3 -3
- package/dist/transports/mcp/format-tool-content.js.map +1 -1
- package/dist/transports/mcp/instructions/result.js +1 -0
- package/dist/transports/mcp/instructions/result.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/ask-user.d.ts +2 -1
- package/dist/transports/mcp/instructions/tools/ask-user.js +9 -2
- package/dist/transports/mcp/instructions/tools/ask-user.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/copy.d.ts +5 -2
- package/dist/transports/mcp/instructions/tools/copy.js +20 -5
- package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.d.ts +1 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js +11 -1
- package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
- package/dist/transports/mcp/mount.d.ts +2 -3
- package/dist/transports/mcp/mount.js +395 -112
- package/dist/transports/mcp/mount.js.map +1 -1
- package/dist/transports/mcp/plan-approval.d.ts +23 -13
- package/dist/transports/mcp/plan-approval.js +39 -26
- package/dist/transports/mcp/plan-approval.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
|
+
import { AiMode as SharedAiMode, llmConfigFromFlags, } from "@superblocksteam/library-shared/types";
|
|
3
4
|
import { createISocketClient, ISocket, SocketHeartbeat, } from "@superblocksteam/shared";
|
|
4
5
|
import { canonicalizeOrchestratorBaseUrl, orchestratorUrlDependsOnProfile, pickOrchestratorBaseUrl, } from "../agents/resolve-orchestrator-url.js";
|
|
5
6
|
import { gatewayDebug } from "../debug.js";
|
|
@@ -10,7 +11,7 @@ import { recordCheckpointOutcome, recordIntegrationProfileResolution, } from "..
|
|
|
10
11
|
import { sanitizeAgentFacingText } from "./agent-facing-text.js";
|
|
11
12
|
import { AppState } from "./app-state.js";
|
|
12
13
|
import { buildBrowserContext } from "./browser-context.js";
|
|
13
|
-
import { createEditorClientMethods, loadIntegrationsForPrompt, } from "./editor-client-methods.js";
|
|
14
|
+
import { createEditorClientMethods, loadIntegrationsForPrompt, ToolPermissionListenerUnavailableError, } from "./editor-client-methods.js";
|
|
14
15
|
import { EditorSocket } from "./editor-socket.js";
|
|
15
16
|
import { appendRecentToolRun, buildRecentToolRun } from "./recent-tool-runs.js";
|
|
16
17
|
import { PromptNotSentError, } from "./session-peer.js";
|
|
@@ -185,6 +186,12 @@ const DEFAULT_RECONNECT_GRACE_MS = 20_000;
|
|
|
185
186
|
* on, so reconnect attempts are capped independently.
|
|
186
187
|
*/
|
|
187
188
|
const DEFAULT_RECONNECT_ATTEMPTS = 3;
|
|
189
|
+
/**
|
|
190
|
+
* How many times a takeover rereads a claim snapshot that browser events keep
|
|
191
|
+
* overtaking. Every read it made was answered, so running out keeps the claim
|
|
192
|
+
* instead of failing a working socket as unreachable.
|
|
193
|
+
*/
|
|
194
|
+
const MAX_CLAIM_REFRESHES = 3;
|
|
188
195
|
/**
|
|
189
196
|
* After the job settles, how long to wait for the dev server to finish flushing
|
|
190
197
|
* Clark's files before giving up on checkpointing them.
|
|
@@ -206,10 +213,6 @@ const PENDING_START_POLL_INTERVAL_MS = 250;
|
|
|
206
213
|
* {@link WebSocketSessionPeer.headlessTurnHandle}.
|
|
207
214
|
*/
|
|
208
215
|
const GATEWAY_PROTOCOL = "gateway";
|
|
209
|
-
const DEFAULT_LLM = {
|
|
210
|
-
coreModel: "claude-sonnet-4-5",
|
|
211
|
-
provider: "bedrock",
|
|
212
|
-
};
|
|
213
216
|
/**
|
|
214
217
|
* Reads a {@link LiveEdit} off the caller-facing pair, which names the same two
|
|
215
218
|
* things with the prompt's field names.
|
|
@@ -226,6 +229,106 @@ function isBrowserObserverEvent(event) {
|
|
|
226
229
|
(event.kind === "generation_state" &&
|
|
227
230
|
event.gatewayObserverSource === "browser"));
|
|
228
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Names why a New Chat reset must be ignored, or returns undefined when it may
|
|
234
|
+
* clear chat state. A reset for another generation, or one that cannot be
|
|
235
|
+
* matched to a turn still running, would otherwise end a turn it was never
|
|
236
|
+
* about.
|
|
237
|
+
*/
|
|
238
|
+
function ignoredChatResetReason(held, reset, previous) {
|
|
239
|
+
if (held.pendingTurn) {
|
|
240
|
+
return "pending_turn";
|
|
241
|
+
}
|
|
242
|
+
if (previous &&
|
|
243
|
+
(!reset.generationId || !previous.generationId) &&
|
|
244
|
+
!previous.finished &&
|
|
245
|
+
!previous.collector.isSettling()) {
|
|
246
|
+
return "unkeyed_active_turn";
|
|
247
|
+
}
|
|
248
|
+
if (reset.generationId &&
|
|
249
|
+
previous?.generationId &&
|
|
250
|
+
reset.generationId !== previous.generationId) {
|
|
251
|
+
return "other_generation";
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Outcomes that leave the turn with the editor: one waiting on the user, and
|
|
257
|
+
* one that may still be running where only the editor can see it. An idle
|
|
258
|
+
* editor refutes both.
|
|
259
|
+
*/
|
|
260
|
+
function leavesTurnWithEditor(outcome) {
|
|
261
|
+
return outcome === "detached" || outcome === "needs_decision";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether a snapshot shows nothing newer than the owned turn's outcome: an idle
|
|
265
|
+
* editor, the owned generation completing as the turn already reported, or
|
|
266
|
+
* that generation still waiting on the tool permission the turn asked for. A
|
|
267
|
+
* permission reaches the gateway over its own RPC and never appears as the
|
|
268
|
+
* snapshot's interactive message, so the editor waiting with none is that
|
|
269
|
+
* permission still pending.
|
|
270
|
+
*/
|
|
271
|
+
function agreesWithOwnedOutcome({ generationId, state }, { hasActiveInteractiveMessage, owned, }) {
|
|
272
|
+
const outcome = owned?.finished;
|
|
273
|
+
if (!outcome) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (state === "idle") {
|
|
277
|
+
return !leavesTurnWithEditor(outcome.generationStatus);
|
|
278
|
+
}
|
|
279
|
+
if (generationId !== owned.generationId) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
if (state === "completed") {
|
|
283
|
+
return (outcome.generationStatus === "completed" ||
|
|
284
|
+
outcome.generationStatus === "no_changes");
|
|
285
|
+
}
|
|
286
|
+
return (state === "awaiting_user_input" &&
|
|
287
|
+
!hasActiveInteractiveMessage &&
|
|
288
|
+
outcome.decision?.kind === "tool_permission");
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Whether a snapshot read while taking a turn over is that turn's own state,
|
|
292
|
+
* to replay onto it. One naming the turn's generation is, unless it awaits a
|
|
293
|
+
* decision it does not carry. So is an idle one naming no generation: that is
|
|
294
|
+
* what the editor shows once nothing runs and no terminal state is left to
|
|
295
|
+
* show, and the turn's collector still ignores an idle before the turn was
|
|
296
|
+
* seen generating.
|
|
297
|
+
*/
|
|
298
|
+
function replaysOntoTurn(generationState, known) {
|
|
299
|
+
if (!generationState.generationId) {
|
|
300
|
+
return generationState.state === "idle";
|
|
301
|
+
}
|
|
302
|
+
return (generationState.generationId === known.turn.generationId &&
|
|
303
|
+
(generationState.state !== "awaiting_user_input" ||
|
|
304
|
+
known.hasActiveInteractiveMessage ||
|
|
305
|
+
known.turn.collector.decision() !== undefined));
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* What a chat snapshot read on attach says about the turn this peer knew of.
|
|
309
|
+
*
|
|
310
|
+
* `unchanged` when it shows nothing newer than a finished owned turn (see
|
|
311
|
+
* {@link agreesWithOwnedOutcome}). Clearing then would erase the turn's
|
|
312
|
+
* outcome, checkpoint, retry advice or pending permission while the editor
|
|
313
|
+
* agrees with them. `settled` when nothing is running or waiting on the user.
|
|
314
|
+
* `observable` when there is a generation to follow: one running, one waiting
|
|
315
|
+
* on an answer, or the watched generation completing.
|
|
316
|
+
*/
|
|
317
|
+
function readChatSnapshot(generationState, known) {
|
|
318
|
+
if (agreesWithOwnedOutcome(generationState, known)) {
|
|
319
|
+
return "unchanged";
|
|
320
|
+
}
|
|
321
|
+
const { generationId, state } = generationState;
|
|
322
|
+
const { owned } = known;
|
|
323
|
+
const watchedGenerationId = known.observedGenerationId ?? owned?.generationId;
|
|
324
|
+
if (state === "idle" ||
|
|
325
|
+
(state === "completed" &&
|
|
326
|
+
(!watchedGenerationId || watchedGenerationId !== generationId)) ||
|
|
327
|
+
(state === "awaiting_user_input" && !known.hasActiveInteractiveMessage)) {
|
|
328
|
+
return "settled";
|
|
329
|
+
}
|
|
330
|
+
return "observable";
|
|
331
|
+
}
|
|
229
332
|
export function buildSessionUrl(input) {
|
|
230
333
|
// liveEditUrl is server-sourced (connectLiveEdit), not caller-supplied, so a
|
|
231
334
|
// host allowlist would mostly duplicate the Superblocks API's own trust
|
|
@@ -505,6 +608,11 @@ async function attempt(url, timeoutMs, onSocketError) {
|
|
|
505
608
|
// emitter 'error' rather than reporting it to the caller. Recording it
|
|
506
609
|
// rather than discarding it is what makes a later 1006 explainable.
|
|
507
610
|
socket.on("error", onSocketError);
|
|
611
|
+
// The dev server flushes queued pushes the moment a peer binds, and
|
|
612
|
+
// those frames can share a read with the upgrade response. Without the
|
|
613
|
+
// pause they are emitted before ISocket is listening and lost. The
|
|
614
|
+
// caller resumes once its handlers are attached.
|
|
615
|
+
socket.pause();
|
|
508
616
|
resolve(socket);
|
|
509
617
|
});
|
|
510
618
|
const onError = (error) => settle(() => reject(error));
|
|
@@ -811,9 +919,12 @@ export class WebSocketSessionPeer {
|
|
|
811
919
|
...(failed.recoveryAction
|
|
812
920
|
? { recoveryAction: failed.recoveryAction }
|
|
813
921
|
: {}),
|
|
814
|
-
...(failed.retryable === false
|
|
922
|
+
...(failed.retryable === false || !failed.retryAction
|
|
923
|
+
? { reply: failed.reply }
|
|
924
|
+
: {}),
|
|
815
925
|
...(failed.retryAction ? { retryAction: failed.retryAction } : {}),
|
|
816
926
|
...(failed.retryable === false ? { retryable: false } : {}),
|
|
927
|
+
startFailed: true,
|
|
817
928
|
};
|
|
818
929
|
}
|
|
819
930
|
if (turn && !turn.finished) {
|
|
@@ -837,7 +948,11 @@ export class WebSocketSessionPeer {
|
|
|
837
948
|
? { checkpointWarning: turn.finished.checkpointWarning }
|
|
838
949
|
: {}),
|
|
839
950
|
generationStatus: turn.finished.generationStatus,
|
|
951
|
+
...(turn.mode ? { mode: turn.mode } : {}),
|
|
840
952
|
phase: "finished",
|
|
953
|
+
...(turn.finished.answered
|
|
954
|
+
? { answered: true, reply: turn.finished.reply }
|
|
955
|
+
: {}),
|
|
841
956
|
...(turn.retryAction ? { retryAction: turn.retryAction } : {}),
|
|
842
957
|
...(turn.finished.retryable === false
|
|
843
958
|
? { retryable: false }
|
|
@@ -860,7 +975,7 @@ export class WebSocketSessionPeer {
|
|
|
860
975
|
? { ...(entry.branchName ? { branch: entry.branchName } : {}) }
|
|
861
976
|
: undefined;
|
|
862
977
|
}
|
|
863
|
-
libraryBrowserContext(applicationId, requestedBranch) {
|
|
978
|
+
async libraryBrowserContext(applicationId, requestedBranch) {
|
|
864
979
|
const held = this.held.get(applicationId);
|
|
865
980
|
if (!held || requestedBranch !== held.branchName) {
|
|
866
981
|
return undefined;
|
|
@@ -894,8 +1009,100 @@ export class WebSocketSessionPeer {
|
|
|
894
1009
|
held.organizationId = input.organizationId;
|
|
895
1010
|
}
|
|
896
1011
|
held.sessionToken = input.sessionToken;
|
|
897
|
-
|
|
898
|
-
|
|
1012
|
+
try {
|
|
1013
|
+
await this.synchronizeChatState(held, input.sessionToken);
|
|
1014
|
+
await this.resolveHeldProfileKey(held);
|
|
1015
|
+
// TODO(AR-592): a discovery failure here fails the read even though the
|
|
1016
|
+
// chat state above was just synced.
|
|
1017
|
+
await this.resolveHeldOrchestrator(held);
|
|
1018
|
+
}
|
|
1019
|
+
finally {
|
|
1020
|
+
this.armReadRelease(held);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Bounds a socket held only to read the editor state. Nothing runs on it,
|
|
1025
|
+
* so it gets a finished turn's grace instead of keeping the sandbox awake
|
|
1026
|
+
* and contesting the editor's binding for the life of the process. A turn
|
|
1027
|
+
* seen on it clears this timer and arms its own.
|
|
1028
|
+
*/
|
|
1029
|
+
armReadRelease(held) {
|
|
1030
|
+
const observed = this.observedTurns.get(held.applicationId);
|
|
1031
|
+
if (this.held.get(held.applicationId) !== held ||
|
|
1032
|
+
held.idleTimer ||
|
|
1033
|
+
(observed?.held === held && !observed.finished)) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
held.idleTimer = setTimeout(() => {
|
|
1037
|
+
if (this.held.get(held.applicationId) === held) {
|
|
1038
|
+
this.drop(held, { preserveFinishedObserved: true });
|
|
1039
|
+
}
|
|
1040
|
+
}, this.options.releaseAfterReadMs ?? DEFAULT_RELEASE_AFTER_TURN_MS);
|
|
1041
|
+
held.idleTimer.unref?.();
|
|
1042
|
+
}
|
|
1043
|
+
async synchronizeChatState(held, sessionToken, retryDecision = true) {
|
|
1044
|
+
const revision = held.chatStateRevision;
|
|
1045
|
+
const owned = this.turns.get(held.applicationId);
|
|
1046
|
+
// Read before the snapshot call: ordinary browser text on the new socket
|
|
1047
|
+
// does not bump the revision, yet it replaces the watched turn with one
|
|
1048
|
+
// that has no generation, which would hide a matching completion.
|
|
1049
|
+
const observedGenerationId = this.observedTurns.get(held.applicationId)?.generationId;
|
|
1050
|
+
const isCurrent = () => this.held.get(held.applicationId) === held &&
|
|
1051
|
+
isSocketOpen(held.ws) &&
|
|
1052
|
+
!held.pendingTurn &&
|
|
1053
|
+
this.turns.get(held.applicationId) === owned;
|
|
1054
|
+
const snapshot = await editorSocket(held, sessionToken)
|
|
1055
|
+
.aiChatGetMessages({ force: false })
|
|
1056
|
+
.catch((error) => {
|
|
1057
|
+
const dropped = isCurrent() && held.chatStateRevision === revision;
|
|
1058
|
+
console.warn("gateway chat state sync failed", {
|
|
1059
|
+
applicationId: held.applicationId,
|
|
1060
|
+
dropped,
|
|
1061
|
+
message: describeSocketError(error),
|
|
1062
|
+
});
|
|
1063
|
+
if (dropped) {
|
|
1064
|
+
this.drop(held);
|
|
1065
|
+
}
|
|
1066
|
+
throw error;
|
|
1067
|
+
});
|
|
1068
|
+
if (!snapshot?.generationState || !isCurrent()) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (held.chatStateRevision !== revision) {
|
|
1072
|
+
const observed = this.observedTurns.get(held.applicationId);
|
|
1073
|
+
if (retryDecision &&
|
|
1074
|
+
observed?.finished?.generationStatus === "needs_decision") {
|
|
1075
|
+
await this.synchronizeChatState(held, sessionToken, false);
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const reading = readChatSnapshot(snapshot.generationState, {
|
|
1080
|
+
hasActiveInteractiveMessage: Boolean(snapshot.activeInteractiveMessage),
|
|
1081
|
+
observedGenerationId,
|
|
1082
|
+
owned,
|
|
1083
|
+
});
|
|
1084
|
+
if (reading === "unchanged") {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
this.clearChatSessionState(held.applicationId);
|
|
1088
|
+
if (reading === "settled") {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (snapshot.generationState.state === "awaiting_user_input" &&
|
|
1092
|
+
snapshot.activeInteractiveMessage) {
|
|
1093
|
+
this.observeEditorEvent(held, {
|
|
1094
|
+
kind: "message",
|
|
1095
|
+
message: {
|
|
1096
|
+
...snapshot.activeInteractiveMessage,
|
|
1097
|
+
gatewayObserverSource: "browser",
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
this.observeEditorEvent(held, {
|
|
1102
|
+
...snapshot.generationState,
|
|
1103
|
+
gatewayObserverSource: "browser",
|
|
1104
|
+
kind: "generation_state",
|
|
1105
|
+
});
|
|
899
1106
|
}
|
|
900
1107
|
release(applicationId) {
|
|
901
1108
|
const entry = this.held.get(applicationId);
|
|
@@ -904,14 +1111,18 @@ export class WebSocketSessionPeer {
|
|
|
904
1111
|
}
|
|
905
1112
|
const turn = this.turns.get(applicationId);
|
|
906
1113
|
if (turn?.held === entry && !turn.finished) {
|
|
907
|
-
this.cancelTurn(turn,
|
|
1114
|
+
this.cancelTurn(turn, {
|
|
1115
|
+
reply: "This turn was cancelled because its live-edit session was released.",
|
|
1116
|
+
});
|
|
908
1117
|
}
|
|
909
1118
|
this.drop(entry);
|
|
910
1119
|
}
|
|
911
1120
|
closeAll() {
|
|
912
1121
|
for (const turn of this.turns.values()) {
|
|
913
1122
|
if (!turn.finished) {
|
|
914
|
-
this.cancelTurn(turn,
|
|
1123
|
+
this.cancelTurn(turn, {
|
|
1124
|
+
reply: "This turn was cancelled because the gateway shut down.",
|
|
1125
|
+
});
|
|
915
1126
|
}
|
|
916
1127
|
}
|
|
917
1128
|
for (const applicationId of [...this.observedTurns.keys()]) {
|
|
@@ -921,6 +1132,14 @@ export class WebSocketSessionPeer {
|
|
|
921
1132
|
this.release(applicationId);
|
|
922
1133
|
}
|
|
923
1134
|
}
|
|
1135
|
+
resetForLogin() {
|
|
1136
|
+
this.closeAll();
|
|
1137
|
+
this.promptStarts.clear();
|
|
1138
|
+
this.connecting.clear();
|
|
1139
|
+
this.turns.clear();
|
|
1140
|
+
this.editorStructureByScope.clear();
|
|
1141
|
+
this.inboundMethods.clear();
|
|
1142
|
+
}
|
|
924
1143
|
/**
|
|
925
1144
|
* Sends the prompt and waits for Clark's whole turn.
|
|
926
1145
|
*
|
|
@@ -947,7 +1166,7 @@ export class WebSocketSessionPeer {
|
|
|
947
1166
|
throw new Error("Superblocks turn wait aborted");
|
|
948
1167
|
}
|
|
949
1168
|
if (update.status === "finished") {
|
|
950
|
-
const { activity: _activity, elapsedMs: _elapsedMs, status: _status, ...finished } = update;
|
|
1169
|
+
const { activity: _activity, elapsedMs: _elapsedMs, mode: _mode, status: _status, ...finished } = update;
|
|
951
1170
|
return {
|
|
952
1171
|
...finished,
|
|
953
1172
|
// See startPrompt, which resolved this: the turn may have run on a
|
|
@@ -1081,6 +1300,7 @@ export class WebSocketSessionPeer {
|
|
|
1081
1300
|
}, input.idempotencyKey, input.pendingStartToken))) {
|
|
1082
1301
|
throw new PromptNotSentError("A newer start attempt replaced this one before its prompt was sent");
|
|
1083
1302
|
}
|
|
1303
|
+
const mode = this.turnMode(input);
|
|
1084
1304
|
const turn = {
|
|
1085
1305
|
abort: new AbortController(),
|
|
1086
1306
|
activity: [],
|
|
@@ -1093,6 +1313,7 @@ export class WebSocketSessionPeer {
|
|
|
1093
1313
|
? {}
|
|
1094
1314
|
: { idempotencyKey: input.idempotencyKey }),
|
|
1095
1315
|
listener: () => undefined,
|
|
1316
|
+
...(mode ? { mode } : {}),
|
|
1096
1317
|
...(input.pendingStartToken === undefined
|
|
1097
1318
|
? {}
|
|
1098
1319
|
: { pendingStartToken: input.pendingStartToken }),
|
|
@@ -1193,7 +1414,10 @@ export class WebSocketSessionPeer {
|
|
|
1193
1414
|
async sendPrompt(args) {
|
|
1194
1415
|
const { held, input, integrations, provisional, turn } = args;
|
|
1195
1416
|
const timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1196
|
-
const browserContext = this.browserContextFor(held);
|
|
1417
|
+
const browserContext = await this.browserContextFor(held);
|
|
1418
|
+
if (!held.llmConfig) {
|
|
1419
|
+
console.warn("gateway sending prompt without org model flags; using defaults", { applicationId: held.applicationId });
|
|
1420
|
+
}
|
|
1197
1421
|
let acknowledgment;
|
|
1198
1422
|
try {
|
|
1199
1423
|
// The editor and the smoke client both send the credential twice: once
|
|
@@ -1204,7 +1428,11 @@ export class WebSocketSessionPeer {
|
|
|
1204
1428
|
...(browserContext ? { browserContext } : {}),
|
|
1205
1429
|
jwt: input.sessionToken,
|
|
1206
1430
|
llmConfig: {
|
|
1207
|
-
|
|
1431
|
+
...(held.llmConfig ?? llmConfigFromFlags(undefined)),
|
|
1432
|
+
...(this.options.coreModel
|
|
1433
|
+
? { coreModel: this.options.coreModel }
|
|
1434
|
+
: {}),
|
|
1435
|
+
...(this.options.provider ? { provider: this.options.provider } : {}),
|
|
1208
1436
|
disabledTools: ["build_captureScreenshot", "build_navigatePreview"],
|
|
1209
1437
|
// The ai-service loads org/user/integration knowledge only when
|
|
1210
1438
|
// the caller opts in. Gateway reads the same org flag as the editor
|
|
@@ -1217,13 +1445,12 @@ export class WebSocketSessionPeer {
|
|
|
1217
1445
|
// a browser context, so the two travel together or not at all.
|
|
1218
1446
|
...(browserContext ? { isTestingEnabled: true } : {}),
|
|
1219
1447
|
},
|
|
1220
|
-
interleavedThinking: false,
|
|
1221
|
-
provider: this.options.provider ?? DEFAULT_LLM.provider,
|
|
1222
|
-
thinkingBudgetTokens: "0",
|
|
1223
|
-
thinkingEnabled: false,
|
|
1224
|
-
unifiedProviderEnabled: true,
|
|
1225
1448
|
},
|
|
1226
|
-
|
|
1449
|
+
...(input.responseMetadata?.type === "tool_permission_response"
|
|
1450
|
+
? {}
|
|
1451
|
+
: {
|
|
1452
|
+
mode: input.mode === "BUILD" ? SharedAiMode.BUILD : SharedAiMode.PLAN,
|
|
1453
|
+
}),
|
|
1227
1454
|
prompt: input.prompt,
|
|
1228
1455
|
promptContext: {
|
|
1229
1456
|
attachments: input.attachments ?? [],
|
|
@@ -1303,6 +1530,7 @@ export class WebSocketSessionPeer {
|
|
|
1303
1530
|
: new Error(`Prompt forward failed: ${rejection.message ?? "unknown socket error"}`);
|
|
1304
1531
|
}
|
|
1305
1532
|
turn.revision = this.appState.markTurnStarted();
|
|
1533
|
+
turn.generationId = acknowledgment?.generationId;
|
|
1306
1534
|
args.acceptBufferedEvents(acknowledgment?.generationId);
|
|
1307
1535
|
input.onProgress?.("Superblocks accepted the prompt");
|
|
1308
1536
|
// Only now, with the prompt accepted and a live turn about to go in the
|
|
@@ -1350,9 +1578,32 @@ export class WebSocketSessionPeer {
|
|
|
1350
1578
|
status: "accepted",
|
|
1351
1579
|
};
|
|
1352
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* The mode a new turn runs in. A reply to a tool permission carries no mode
|
|
1583
|
+
* to Superblocks, which carries on in the mode of the turn that asked, so the
|
|
1584
|
+
* reply takes that turn's mode here. When this peer did not run the turn that
|
|
1585
|
+
* asked, the mode is unknown rather than guessed.
|
|
1586
|
+
*/
|
|
1587
|
+
turnMode(input) {
|
|
1588
|
+
const response = input.responseMetadata;
|
|
1589
|
+
if (response?.type !== "tool_permission_response") {
|
|
1590
|
+
return input.mode ?? "PLAN";
|
|
1591
|
+
}
|
|
1592
|
+
const asking = this.turns.get(input.applicationId);
|
|
1593
|
+
const decision = asking?.finished?.decision;
|
|
1594
|
+
return decision?.kind === "tool_permission" &&
|
|
1595
|
+
decision.messageId === response.responseToMessageId
|
|
1596
|
+
? asking?.mode
|
|
1597
|
+
: undefined;
|
|
1598
|
+
}
|
|
1353
1599
|
latestTurn(applicationId) {
|
|
1354
1600
|
return (this.observedTurns.get(applicationId) ?? this.turns.get(applicationId));
|
|
1355
1601
|
}
|
|
1602
|
+
clearChatSessionState(applicationId) {
|
|
1603
|
+
this.clearPendingDecision(applicationId);
|
|
1604
|
+
this.forgetObservedTurn(applicationId);
|
|
1605
|
+
this.supersedeTurn(applicationId);
|
|
1606
|
+
}
|
|
1356
1607
|
observeEditorEvent(held, event) {
|
|
1357
1608
|
if (held.dropping || this.held.get(held.applicationId) !== held) {
|
|
1358
1609
|
return;
|
|
@@ -1376,6 +1627,63 @@ export class WebSocketSessionPeer {
|
|
|
1376
1627
|
this.clearPendingDecision(held.applicationId);
|
|
1377
1628
|
}
|
|
1378
1629
|
const owned = this.turns.get(held.applicationId);
|
|
1630
|
+
const previous = observed ?? owned;
|
|
1631
|
+
if (event.kind === "generation_state" &&
|
|
1632
|
+
event.state === "idle" &&
|
|
1633
|
+
event.chatSessionReset) {
|
|
1634
|
+
const ignored = ignoredChatResetReason(held, event, previous);
|
|
1635
|
+
if (ignored) {
|
|
1636
|
+
console.warn("gateway chat session reset ignored", {
|
|
1637
|
+
applicationId: held.applicationId,
|
|
1638
|
+
reason: ignored,
|
|
1639
|
+
resetHasGenerationId: Boolean(event.generationId),
|
|
1640
|
+
turnHasGenerationId: Boolean(previous?.generationId),
|
|
1641
|
+
});
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
this.clearChatSessionState(held.applicationId);
|
|
1645
|
+
this.armReadRelease(held);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (previous?.finished &&
|
|
1649
|
+
event.kind === "generation_state" &&
|
|
1650
|
+
event.generationId &&
|
|
1651
|
+
event.generationId === previous.generationId &&
|
|
1652
|
+
previous.finished.generationStatus !== "needs_decision") {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (previous?.finished &&
|
|
1656
|
+
previous.held === held &&
|
|
1657
|
+
!userContent &&
|
|
1658
|
+
!activeGeneration) {
|
|
1659
|
+
if (previous.finished.generationStatus === "needs_decision" &&
|
|
1660
|
+
event.kind === "generation_state" &&
|
|
1661
|
+
(event.state === "cancelled" ||
|
|
1662
|
+
event.state === "completed" ||
|
|
1663
|
+
event.state === "idle") &&
|
|
1664
|
+
(!event.generationId ||
|
|
1665
|
+
!previous.generationId ||
|
|
1666
|
+
event.generationId === previous.generationId)) {
|
|
1667
|
+
previous.finished.generationStatus =
|
|
1668
|
+
event.state === "cancelled"
|
|
1669
|
+
? "cancelled"
|
|
1670
|
+
: event.hasSuggestions ||
|
|
1671
|
+
previous.collector.workSignals().length > 0
|
|
1672
|
+
? "completed"
|
|
1673
|
+
: "no_changes";
|
|
1674
|
+
delete previous.finished.decision;
|
|
1675
|
+
this.clearPendingDecision(held.applicationId);
|
|
1676
|
+
this.wake(previous);
|
|
1677
|
+
if (observed) {
|
|
1678
|
+
this.armObservedTurnRelease(held.applicationId, observed);
|
|
1679
|
+
}
|
|
1680
|
+
else if (owned) {
|
|
1681
|
+
this.armPostTurnRelease(held, owned);
|
|
1682
|
+
this.armKeepalive(held);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1379
1687
|
if (owned) {
|
|
1380
1688
|
if (!owned.finished && !userContent && !activeGeneration) {
|
|
1381
1689
|
return;
|
|
@@ -1399,12 +1707,6 @@ export class WebSocketSessionPeer {
|
|
|
1399
1707
|
if (!activeGeneration) {
|
|
1400
1708
|
return;
|
|
1401
1709
|
}
|
|
1402
|
-
if (event.kind === "generation_state" &&
|
|
1403
|
-
event.generationId &&
|
|
1404
|
-
event.generationId === observed.generationId &&
|
|
1405
|
-
observed.finished.generationStatus !== "needs_decision") {
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
1710
|
observed = undefined;
|
|
1409
1711
|
this.clearPendingDecision(held.applicationId);
|
|
1410
1712
|
}
|
|
@@ -1495,7 +1797,7 @@ export class WebSocketSessionPeer {
|
|
|
1495
1797
|
}
|
|
1496
1798
|
}
|
|
1497
1799
|
turn.finished = {
|
|
1498
|
-
|
|
1800
|
+
...turn.collector.replyFields(),
|
|
1499
1801
|
generationStatus,
|
|
1500
1802
|
viewStatus: turn.collector.viewableStatus(),
|
|
1501
1803
|
...decisionFields(decision),
|
|
@@ -1504,13 +1806,15 @@ export class WebSocketSessionPeer {
|
|
|
1504
1806
|
this.wake(turn);
|
|
1505
1807
|
this.armObservedTurnRelease(applicationId, turn);
|
|
1506
1808
|
}
|
|
1507
|
-
settleObservedTurn(turn, generationStatus,
|
|
1809
|
+
settleObservedTurn(turn, generationStatus, gatewayReply) {
|
|
1508
1810
|
if (turn.finished) {
|
|
1509
1811
|
return;
|
|
1510
1812
|
}
|
|
1511
1813
|
tagRetryableLiveEditDeath(turn, { generationStatus });
|
|
1512
1814
|
turn.finished = {
|
|
1513
|
-
|
|
1815
|
+
...(gatewayReply === undefined
|
|
1816
|
+
? turn.collector.replyFields()
|
|
1817
|
+
: { reply: gatewayReply }),
|
|
1514
1818
|
generationStatus,
|
|
1515
1819
|
viewStatus: turn.collector.viewableStatus(),
|
|
1516
1820
|
...decisionFields(turn.collector.decision()),
|
|
@@ -1538,7 +1842,7 @@ export class WebSocketSessionPeer {
|
|
|
1538
1842
|
this.clearTimers(held);
|
|
1539
1843
|
const delayMs = this.options.releaseAfterTurnMs ?? DEFAULT_RELEASE_AFTER_TURN_MS;
|
|
1540
1844
|
held.idleTimer = setTimeout(() => {
|
|
1541
|
-
if (this.observedTurns.get(held.applicationId) === turn &&
|
|
1845
|
+
if ((this.observedTurns.get(held.applicationId) ?? turn) === turn &&
|
|
1542
1846
|
this.held.get(held.applicationId) === held) {
|
|
1543
1847
|
this.drop(held, { preserveFinishedObserved: true });
|
|
1544
1848
|
}
|
|
@@ -1671,6 +1975,7 @@ export class WebSocketSessionPeer {
|
|
|
1671
1975
|
activity,
|
|
1672
1976
|
...(turn.branch === undefined ? {} : { branch: turn.branch }),
|
|
1673
1977
|
elapsedMs,
|
|
1978
|
+
...(turn.mode ? { mode: turn.mode } : {}),
|
|
1674
1979
|
status: "finished",
|
|
1675
1980
|
...turn.finished,
|
|
1676
1981
|
viewStatus: this.currentViewStatus(input.applicationId, turn.branch, turn.finished.viewStatus),
|
|
@@ -1732,7 +2037,7 @@ export class WebSocketSessionPeer {
|
|
|
1732
2037
|
inboundMethods: [...this.inboundMethods].sort(),
|
|
1733
2038
|
});
|
|
1734
2039
|
turn.finished ??= {
|
|
1735
|
-
|
|
2040
|
+
...collector.replyFields(),
|
|
1736
2041
|
generationStatus: "detached",
|
|
1737
2042
|
viewStatus: collector.viewableStatus(),
|
|
1738
2043
|
...decisionFields(collector.decision()),
|
|
@@ -1830,7 +2135,7 @@ export class WebSocketSessionPeer {
|
|
|
1830
2135
|
...(checkpointOutcome?.status === "saved" && checkpointOutcome.warning
|
|
1831
2136
|
? { checkpointWarning: checkpointOutcome.warning }
|
|
1832
2137
|
: {}),
|
|
1833
|
-
|
|
2138
|
+
...collector.replyFields(),
|
|
1834
2139
|
generationStatus,
|
|
1835
2140
|
...(turn.recentRuns.length > 0
|
|
1836
2141
|
? { recentRuns: [...turn.recentRuns] }
|
|
@@ -1849,7 +2154,7 @@ export class WebSocketSessionPeer {
|
|
|
1849
2154
|
// A disconnect / supersede already settled the turn with the reason; the
|
|
1850
2155
|
// abort it caused says less than what is already recorded.
|
|
1851
2156
|
turn.finished ??= {
|
|
1852
|
-
|
|
2157
|
+
...collector.replyFields(),
|
|
1853
2158
|
generationStatus: "timeout",
|
|
1854
2159
|
viewStatus: collector.viewableStatus(),
|
|
1855
2160
|
...decisionFields(collector.decision()),
|
|
@@ -2031,13 +2336,38 @@ export class WebSocketSessionPeer {
|
|
|
2031
2336
|
fresh.listeners.add(turn.listener);
|
|
2032
2337
|
previous.listeners.delete(turn.listener);
|
|
2033
2338
|
fresh.heartbeat.start();
|
|
2034
|
-
|
|
2035
|
-
? "refused"
|
|
2339
|
+
let claim = fresh.sessionToken === undefined
|
|
2340
|
+
? { kind: "refused" }
|
|
2036
2341
|
: await this.proveIdentity({
|
|
2037
2342
|
held: fresh,
|
|
2343
|
+
deadline: input.deadline,
|
|
2038
2344
|
sessionToken: fresh.sessionToken,
|
|
2039
2345
|
turnId: turn.turnId,
|
|
2040
2346
|
});
|
|
2347
|
+
let refreshes = 0;
|
|
2348
|
+
while (claim.kind === "claimed" &&
|
|
2349
|
+
fresh.browserChatStateRevision !== claim.browserRevision &&
|
|
2350
|
+
!turn.finished &&
|
|
2351
|
+
turn.held === previous &&
|
|
2352
|
+
this.turns.get(fresh.applicationId) === turn &&
|
|
2353
|
+
fresh.sessionToken !== undefined) {
|
|
2354
|
+
if (refreshes === MAX_CLAIM_REFRESHES || Date.now() >= input.deadline) {
|
|
2355
|
+
console.warn("gateway clark reattach snapshot kept changing", {
|
|
2356
|
+
applicationId: fresh.applicationId,
|
|
2357
|
+
reason: refreshes === MAX_CLAIM_REFRESHES ? "refresh_cap" : "deadline",
|
|
2358
|
+
refreshes,
|
|
2359
|
+
turnId: turn.turnId,
|
|
2360
|
+
});
|
|
2361
|
+
break;
|
|
2362
|
+
}
|
|
2363
|
+
refreshes++;
|
|
2364
|
+
claim = await this.proveIdentity({
|
|
2365
|
+
held: fresh,
|
|
2366
|
+
deadline: input.deadline,
|
|
2367
|
+
sessionToken: fresh.sessionToken,
|
|
2368
|
+
turnId: turn.turnId,
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2041
2371
|
// Opening and claiming take seconds, and the turn can settle inside them:
|
|
2042
2372
|
// a relayed terminal event, the turn being cancelled, or a newer prompt
|
|
2043
2373
|
// taking the app. runTurn arms the post-turn timers once, against the
|
|
@@ -2046,6 +2376,11 @@ export class WebSocketSessionPeer {
|
|
|
2046
2376
|
// this point but the listener, so there is nothing to undo beyond letting
|
|
2047
2377
|
// the replacement go.
|
|
2048
2378
|
if (turn.finished || turn.held !== previous) {
|
|
2379
|
+
if (fresh.pendingChatSessionReset &&
|
|
2380
|
+
turn.held === previous &&
|
|
2381
|
+
this.turns.get(fresh.applicationId) === turn) {
|
|
2382
|
+
this.clearChatSessionState(fresh.applicationId);
|
|
2383
|
+
}
|
|
2049
2384
|
delete fresh.pendingTurn;
|
|
2050
2385
|
fresh.listeners.delete(turn.listener);
|
|
2051
2386
|
if (fresh.listeners.size === 0 &&
|
|
@@ -2054,7 +2389,7 @@ export class WebSocketSessionPeer {
|
|
|
2054
2389
|
}
|
|
2055
2390
|
return { kind: "stale" };
|
|
2056
2391
|
}
|
|
2057
|
-
if (claim === "unreachable") {
|
|
2392
|
+
if (claim.kind === "unreachable") {
|
|
2058
2393
|
delete fresh.pendingTurn;
|
|
2059
2394
|
fresh.listeners.delete(turn.listener);
|
|
2060
2395
|
this.drop(fresh);
|
|
@@ -2063,7 +2398,7 @@ export class WebSocketSessionPeer {
|
|
|
2063
2398
|
message: "the live-edit session stopped answering on a fresh socket",
|
|
2064
2399
|
};
|
|
2065
2400
|
}
|
|
2066
|
-
if (claim === "refused") {
|
|
2401
|
+
if (claim.kind === "refused") {
|
|
2067
2402
|
delete fresh.pendingTurn;
|
|
2068
2403
|
fresh.listeners.delete(turn.listener);
|
|
2069
2404
|
// Go back to the socket the turn was on, but only while it is still
|
|
@@ -2084,6 +2419,28 @@ export class WebSocketSessionPeer {
|
|
|
2084
2419
|
this.setHeld(fresh);
|
|
2085
2420
|
fresh.heartbeat.start();
|
|
2086
2421
|
this.drop(previous);
|
|
2422
|
+
if (fresh.pendingChatSessionReset) {
|
|
2423
|
+
this.observeEditorEvent(fresh, fresh.pendingChatSessionReset);
|
|
2424
|
+
delete fresh.pendingChatSessionReset;
|
|
2425
|
+
}
|
|
2426
|
+
const snapshot = claim.snapshot;
|
|
2427
|
+
if (!turn.finished &&
|
|
2428
|
+
this.turns.get(fresh.applicationId) === turn &&
|
|
2429
|
+
fresh.chatStateRevision === claim.revision &&
|
|
2430
|
+
snapshot?.generationState &&
|
|
2431
|
+
replaysOntoTurn(snapshot.generationState, {
|
|
2432
|
+
hasActiveInteractiveMessage: Boolean(snapshot.activeInteractiveMessage),
|
|
2433
|
+
turn,
|
|
2434
|
+
})) {
|
|
2435
|
+
if (snapshot.generationState.state === "awaiting_user_input" &&
|
|
2436
|
+
snapshot.activeInteractiveMessage) {
|
|
2437
|
+
turn.listener({
|
|
2438
|
+
kind: "message",
|
|
2439
|
+
message: snapshot.activeInteractiveMessage,
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
turn.listener({ ...snapshot.generationState, kind: "generation_state" });
|
|
2443
|
+
}
|
|
2087
2444
|
return { kind: "took_over" };
|
|
2088
2445
|
}
|
|
2089
2446
|
/**
|
|
@@ -2096,6 +2453,7 @@ export class WebSocketSessionPeer {
|
|
|
2096
2453
|
fresh.apiKey = previous.apiKey;
|
|
2097
2454
|
fresh.branchName = previous.branchName;
|
|
2098
2455
|
fresh.isFactsEnabled = previous.isFactsEnabled;
|
|
2456
|
+
fresh.llmConfig = previous.llmConfig;
|
|
2099
2457
|
fresh.orchestrator = previous.orchestrator;
|
|
2100
2458
|
fresh.orchestratorAgentType = previous.orchestratorAgentType;
|
|
2101
2459
|
fresh.orchestratorStale = previous.orchestratorStale;
|
|
@@ -2124,12 +2482,17 @@ export class WebSocketSessionPeer {
|
|
|
2124
2482
|
* between two sockets on it.
|
|
2125
2483
|
*/
|
|
2126
2484
|
async proveIdentity(input) {
|
|
2485
|
+
if (Date.now() >= input.deadline) {
|
|
2486
|
+
return { kind: "unreachable" };
|
|
2487
|
+
}
|
|
2488
|
+
const revision = input.held.chatStateRevision;
|
|
2489
|
+
const browserRevision = input.held.browserChatStateRevision;
|
|
2127
2490
|
try {
|
|
2128
|
-
await editorSocket(input.held, input.sessionToken).aiChatGetMessages({
|
|
2491
|
+
const snapshot = await beforePromptDeadline(editorSocket(input.held, input.sessionToken).aiChatGetMessages({
|
|
2129
2492
|
force: false,
|
|
2130
2493
|
...this.turnCorrelator(input.turnId),
|
|
2131
|
-
});
|
|
2132
|
-
return "claimed";
|
|
2494
|
+
}), input.deadline);
|
|
2495
|
+
return { kind: "claimed", snapshot, revision, browserRevision };
|
|
2133
2496
|
}
|
|
2134
2497
|
catch (error) {
|
|
2135
2498
|
const message = describeSocketError(error);
|
|
@@ -2140,7 +2503,7 @@ export class WebSocketSessionPeer {
|
|
|
2140
2503
|
// A call that went unanswered says the socket opened onto something that
|
|
2141
2504
|
// is not servicing requests, which is the same fact a lost liveness beat
|
|
2142
2505
|
// reports and nothing like a session declining to hand a turn over.
|
|
2143
|
-
return isTimeout({ message }) ? "unreachable" : "refused";
|
|
2506
|
+
return { kind: isTimeout({ message }) ? "unreachable" : "refused" };
|
|
2144
2507
|
}
|
|
2145
2508
|
}
|
|
2146
2509
|
async reconnect(previous, deadline) {
|
|
@@ -2166,6 +2529,15 @@ export class WebSocketSessionPeer {
|
|
|
2166
2529
|
this.held.set(held.applicationId, held);
|
|
2167
2530
|
this.publishEditorStructure(held);
|
|
2168
2531
|
}
|
|
2532
|
+
hasStoredToolPermission(held, messageId) {
|
|
2533
|
+
if (held.dropping || this.held.get(held.applicationId) !== held) {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
const pending = this.appState.pendingDecision(held.applicationId);
|
|
2537
|
+
return (pending?.branch === held.branchName &&
|
|
2538
|
+
pending?.decision.kind === "tool_permission" &&
|
|
2539
|
+
pending.decision.messageId === messageId);
|
|
2540
|
+
}
|
|
2169
2541
|
recordEditorStructure(held, hasDefaultStructure) {
|
|
2170
2542
|
held.editorHasDefaultStructure = hasDefaultStructure;
|
|
2171
2543
|
this.publishEditorStructure(held);
|
|
@@ -2483,6 +2855,44 @@ export class WebSocketSessionPeer {
|
|
|
2483
2855
|
return undefined;
|
|
2484
2856
|
}
|
|
2485
2857
|
return (event) => {
|
|
2858
|
+
if (isBrowserObserverEvent(event) &&
|
|
2859
|
+
event.kind === "generation_state" &&
|
|
2860
|
+
event.state === "idle" &&
|
|
2861
|
+
event.chatSessionReset &&
|
|
2862
|
+
held.pendingTurn &&
|
|
2863
|
+
this.turns.get(held.applicationId) === held.pendingTurn) {
|
|
2864
|
+
// Whether or not the reset names the pending turn, a claim
|
|
2865
|
+
// snapshot read before it is stale and must be read again.
|
|
2866
|
+
held.browserChatStateRevision++;
|
|
2867
|
+
held.chatStateRevision++;
|
|
2868
|
+
if (event.generationId &&
|
|
2869
|
+
event.generationId === held.pendingTurn.generationId) {
|
|
2870
|
+
held.pendingChatSessionReset = event;
|
|
2871
|
+
}
|
|
2872
|
+
else {
|
|
2873
|
+
console.warn("gateway chat session reset ignored", {
|
|
2874
|
+
applicationId: held.applicationId,
|
|
2875
|
+
reason: "uncorrelated_during_takeover",
|
|
2876
|
+
resetHasGenerationId: Boolean(event.generationId),
|
|
2877
|
+
turnHasGenerationId: Boolean(held.pendingTurn.generationId),
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const browserText = isBrowserObserverEvent(event) &&
|
|
2883
|
+
event.kind === "message" &&
|
|
2884
|
+
event.message.role !== "user" &&
|
|
2885
|
+
(event.message.type === undefined ||
|
|
2886
|
+
event.message.type === "text" ||
|
|
2887
|
+
event.message.type === "text-delta" ||
|
|
2888
|
+
event.message.type === "reasoning");
|
|
2889
|
+
if (isBrowserObserverEvent(event) && !browserText) {
|
|
2890
|
+
held.browserChatStateRevision++;
|
|
2891
|
+
}
|
|
2892
|
+
if (!browserText &&
|
|
2893
|
+
(event.kind === "message" || event.kind === "generation_state")) {
|
|
2894
|
+
held.chatStateRevision++;
|
|
2895
|
+
}
|
|
2486
2896
|
if (event.kind === "sync_status" && event.status) {
|
|
2487
2897
|
held.syncStatus = event.status;
|
|
2488
2898
|
}
|
|
@@ -2499,6 +2909,10 @@ export class WebSocketSessionPeer {
|
|
|
2499
2909
|
}
|
|
2500
2910
|
return;
|
|
2501
2911
|
}
|
|
2912
|
+
if (event.kind === "tool_permission_request" &&
|
|
2913
|
+
!this.hasStoredToolPermission(held, event.messageId)) {
|
|
2914
|
+
throw new ToolPermissionListenerUnavailableError();
|
|
2915
|
+
}
|
|
2502
2916
|
};
|
|
2503
2917
|
}, (method) => {
|
|
2504
2918
|
this.inboundMethods.add(method);
|
|
@@ -2545,32 +2959,6 @@ export class WebSocketSessionPeer {
|
|
|
2545
2959
|
turn.activity.push(line);
|
|
2546
2960
|
this.wake(turn);
|
|
2547
2961
|
},
|
|
2548
|
-
respondToToolPermission: async (payload) => {
|
|
2549
|
-
const current = heldRef.current;
|
|
2550
|
-
if (!current) {
|
|
2551
|
-
return;
|
|
2552
|
-
}
|
|
2553
|
-
const turn = current.pendingTurn ?? this.turns.get(current.applicationId);
|
|
2554
|
-
if (!turn ||
|
|
2555
|
-
turn.finished ||
|
|
2556
|
-
(turn.held !== current && current.pendingTurn !== turn)) {
|
|
2557
|
-
return;
|
|
2558
|
-
}
|
|
2559
|
-
try {
|
|
2560
|
-
await editorSocket(current, current.sessionToken).aiRespondToToolPermission({
|
|
2561
|
-
approved: payload.approved,
|
|
2562
|
-
...this.turnCorrelator(turn.turnId),
|
|
2563
|
-
messageId: payload.messageId,
|
|
2564
|
-
requestId: payload.requestId ?? payload.messageId,
|
|
2565
|
-
});
|
|
2566
|
-
}
|
|
2567
|
-
catch (error) {
|
|
2568
|
-
console.warn("gateway auto-approve tool permission failed", {
|
|
2569
|
-
applicationId: current.applicationId,
|
|
2570
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2571
|
-
});
|
|
2572
|
-
}
|
|
2573
|
-
},
|
|
2574
2962
|
}), [], {
|
|
2575
2963
|
onClose: () => {
|
|
2576
2964
|
const current = this.held.get(input.applicationId);
|
|
@@ -2599,7 +2987,7 @@ export class WebSocketSessionPeer {
|
|
|
2599
2987
|
const applicationEnded = isApplicationClose(close);
|
|
2600
2988
|
this.settleObservedTurn(observed, applicationEnded ? "live_edit_terminated" : "detached", applicationEnded
|
|
2601
2989
|
? LIVE_EDIT_TERMINATED_REPLY + describeClose(close)
|
|
2602
|
-
:
|
|
2990
|
+
: undefined);
|
|
2603
2991
|
return;
|
|
2604
2992
|
}
|
|
2605
2993
|
// An application code is the live edit stating its own end, so there
|
|
@@ -2624,6 +3012,8 @@ export class WebSocketSessionPeer {
|
|
|
2624
3012
|
const held = {
|
|
2625
3013
|
applicationId: input.applicationId,
|
|
2626
3014
|
branchName: input.branchName,
|
|
3015
|
+
chatStateRevision: 0,
|
|
3016
|
+
browserChatStateRevision: 0,
|
|
2627
3017
|
client,
|
|
2628
3018
|
heartbeat,
|
|
2629
3019
|
idleTimer: undefined,
|
|
@@ -2647,6 +3037,9 @@ export class WebSocketSessionPeer {
|
|
|
2647
3037
|
ws,
|
|
2648
3038
|
};
|
|
2649
3039
|
heldRef.current = held;
|
|
3040
|
+
// Paused by `attempt` so no frame arrives before ISocket is listening and
|
|
3041
|
+
// `heldRef` can route it. Frames resume on a later tick, after this block.
|
|
3042
|
+
ws.resume();
|
|
2650
3043
|
if (publish) {
|
|
2651
3044
|
try {
|
|
2652
3045
|
this.assertReadOnlyHoldCurrent(input.applicationId, expectedTurn);
|
|
@@ -3122,7 +3515,7 @@ export class WebSocketSessionPeer {
|
|
|
3122
3515
|
closeCode: input.close.code,
|
|
3123
3516
|
});
|
|
3124
3517
|
this.settleTurn(turn, {
|
|
3125
|
-
|
|
3518
|
+
...turn.collector.replyFields(),
|
|
3126
3519
|
generationStatus: "detached",
|
|
3127
3520
|
viewStatus: turn.collector.viewableStatus(),
|
|
3128
3521
|
...decisionFields(turn.collector.decision()),
|
|
@@ -3140,11 +3533,14 @@ export class WebSocketSessionPeer {
|
|
|
3140
3533
|
if (!prior) {
|
|
3141
3534
|
return;
|
|
3142
3535
|
}
|
|
3143
|
-
this.cancelTurn(prior
|
|
3536
|
+
this.cancelTurn(prior, {
|
|
3537
|
+
replaced: true,
|
|
3538
|
+
reply: "This turn was replaced by a newer prompt on the same app, so its work was cancelled.",
|
|
3539
|
+
});
|
|
3144
3540
|
}
|
|
3145
|
-
cancelTurn(turn,
|
|
3541
|
+
cancelTurn(turn, outcome) {
|
|
3146
3542
|
turn.finished ??= {
|
|
3147
|
-
|
|
3543
|
+
...outcome,
|
|
3148
3544
|
generationStatus: "cancelled",
|
|
3149
3545
|
viewStatus: turn.collector.viewableStatus(),
|
|
3150
3546
|
};
|
|
@@ -3294,7 +3690,7 @@ export class WebSocketSessionPeer {
|
|
|
3294
3690
|
* the app shell cannot boot without — Clark then runs the turn with its
|
|
3295
3691
|
* ordinary tools rather than failing it.
|
|
3296
3692
|
*/
|
|
3297
|
-
browserContextFor(held) {
|
|
3693
|
+
async browserContextFor(held) {
|
|
3298
3694
|
if (!this.options.testingEnabled) {
|
|
3299
3695
|
return undefined;
|
|
3300
3696
|
}
|
|
@@ -3304,7 +3700,7 @@ export class WebSocketSessionPeer {
|
|
|
3304
3700
|
* Library iframe bootstrap from a held session. Used for Gateway's own
|
|
3305
3701
|
* live-canvas screenshot even when Clark's in-sandbox Playwright is off.
|
|
3306
3702
|
*/
|
|
3307
|
-
buildHeldBrowserContext(held) {
|
|
3703
|
+
async buildHeldBrowserContext(held) {
|
|
3308
3704
|
if (!held.orchestratorUrl) {
|
|
3309
3705
|
console.error("gateway live canvas skipped: missing_orchestrator_url", {
|
|
3310
3706
|
applicationId: held.applicationId,
|
|
@@ -3323,8 +3719,11 @@ export class WebSocketSessionPeer {
|
|
|
3323
3719
|
});
|
|
3324
3720
|
return undefined;
|
|
3325
3721
|
}
|
|
3722
|
+
const apiKey = this.options.getAccessToken
|
|
3723
|
+
? await this.options.getAccessToken()
|
|
3724
|
+
: held.apiKey;
|
|
3326
3725
|
return buildBrowserContext({
|
|
3327
|
-
...(
|
|
3726
|
+
...(apiKey ? { apiKey } : {}),
|
|
3328
3727
|
applicationId: held.applicationId,
|
|
3329
3728
|
liveEditUrl: held.liveEdit.url,
|
|
3330
3729
|
orchestratorUrl: held.orchestratorUrl,
|
|
@@ -3482,6 +3881,7 @@ export class WebSocketSessionPeer {
|
|
|
3482
3881
|
organizationId: held.organizationId,
|
|
3483
3882
|
});
|
|
3484
3883
|
held.isFactsEnabled = inventory.isFactsEnabled;
|
|
3884
|
+
held.llmConfig = inventory.llmConfig;
|
|
3485
3885
|
const skipped = held.dropping
|
|
3486
3886
|
? "dropping"
|
|
3487
3887
|
: held.profileKey !== profileKey
|
|
@@ -3503,7 +3903,7 @@ export class WebSocketSessionPeer {
|
|
|
3503
3903
|
profileKey,
|
|
3504
3904
|
});
|
|
3505
3905
|
held.orchestratorUrl = orchestratorUrl;
|
|
3506
|
-
held.orchestrator = new HttpOrchestratorClient(orchestratorUrl);
|
|
3906
|
+
held.orchestrator = new HttpOrchestratorClient(orchestratorUrl, undefined, this.options.getAccessToken);
|
|
3507
3907
|
held.orchestratorAgentType = inventory.agentType;
|
|
3508
3908
|
delete held.orchestratorStale;
|
|
3509
3909
|
console.error("gateway resolved orchestrator url", {
|