@superblocksteam/vite-plugin-file-sync 2.0.156 → 2.0.157-next.1
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/ai-service/agent/prompts/build-base-system-prompt.d.ts +9 -2
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +34 -6
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts +2 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +52 -9
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
- package/dist/ai-service/agent/tool-permission-delivery-metrics.d.ts +3 -0
- package/dist/ai-service/agent/tool-permission-delivery-metrics.d.ts.map +1 -0
- package/dist/ai-service/agent/tool-permission-delivery-metrics.js +23 -0
- package/dist/ai-service/agent/tool-permission-delivery-metrics.js.map +1 -0
- package/dist/ai-service/agent/tool-permission-delivery.d.ts +25 -16
- package/dist/ai-service/agent/tool-permission-delivery.d.ts.map +1 -1
- package/dist/ai-service/agent/tool-permission-delivery.js +384 -38
- package/dist/ai-service/agent/tool-permission-delivery.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts +9 -35
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js +71 -98
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.js +3 -2
- package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
- package/dist/ai-service/agent/tools/app-environment/update-app-environment.d.ts +1 -1
- package/dist/ai-service/agent/tools/app-environment/update-app-environment.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/app-environment/update-app-environment.js +13 -1
- package/dist/ai-service/agent/tools/app-environment/update-app-environment.js.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.js +8 -3
- package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
- package/dist/ai-service/agent/tools/build-navigate-preview.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-navigate-preview.js +4 -2
- package/dist/ai-service/agent/tools/build-navigate-preview.js.map +1 -1
- package/dist/ai-service/agent/tools/build-reload-file.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-reload-file.js +4 -2
- package/dist/ai-service/agent/tools/build-reload-file.js.map +1 -1
- package/dist/ai-service/agent/tools/get-logs.d.ts +2 -2
- package/dist/ai-service/agent/tools/integrations/delete-integration.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/delete-integration.js +6 -3
- package/dist/ai-service/agent/tools/integrations/delete-integration.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +13 -13
- package/dist/ai-service/agent/tools/integrations/integration-setup.d.ts +18 -6
- package/dist/ai-service/agent/tools/integrations/integration-setup.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/integration-setup.js +41 -15
- package/dist/ai-service/agent/tools/integrations/integration-setup.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/internal.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/internal.js +4 -2
- package/dist/ai-service/agent/tools/integrations/internal.js.map +1 -1
- package/dist/ai-service/agent/tools/report-security-findings.d.ts +23 -23
- package/dist/ai-service/agent/tools.d.ts +4 -2
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +21 -14
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/git.d.ts +2 -2
- package/dist/ai-service/index.d.ts +26 -25
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +340 -121
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/integrations/store.d.ts +2 -1
- package/dist/ai-service/integrations/store.d.ts.map +1 -1
- package/dist/ai-service/integrations/store.js +3 -2
- package/dist/ai-service/integrations/store.js.map +1 -1
- package/dist/ai-service/judge/tools/playwright-action.d.ts +1 -1
- package/dist/ai-service/judge/tools/submit-feedback.d.ts +1 -1
- package/dist/ai-service/llm/client.d.ts +9 -3
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +10 -3
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.js +3 -0
- package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/llmobs.d.ts +6 -6
- package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/llmobs.js +10 -9
- package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/step-token-usage.d.ts +15 -0
- package/dist/ai-service/llm/stream/observers/step-token-usage.d.ts.map +1 -0
- package/dist/ai-service/llm/stream/observers/step-token-usage.js +88 -0
- package/dist/ai-service/llm/stream/observers/step-token-usage.js.map +1 -0
- package/dist/ai-service/llm/stream/orchestrator.d.ts +4 -0
- package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.js +19 -41
- package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
- package/dist/ai-service/llm/stream/retry-engine.d.ts +1 -1
- package/dist/ai-service/llm/stream/retry-engine.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/retry-engine.js +26 -1
- package/dist/ai-service/llm/stream/retry-engine.js.map +1 -1
- package/dist/ai-service/llm/stream/terminal-reason.d.ts +6 -0
- package/dist/ai-service/llm/stream/terminal-reason.d.ts.map +1 -0
- package/dist/ai-service/llm/stream/terminal-reason.js +73 -0
- package/dist/ai-service/llm/stream/terminal-reason.js.map +1 -0
- package/dist/ai-service/policy-agent-metrics.d.ts +10 -0
- package/dist/ai-service/policy-agent-metrics.d.ts.map +1 -1
- package/dist/ai-service/policy-agent-metrics.js +33 -0
- package/dist/ai-service/policy-agent-metrics.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +29 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +3 -1
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +7 -6
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +18 -5
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js +8 -6
- package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.d.ts +0 -14
- package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.js +0 -36
- package/dist/ai-service/state-machine/helpers/policy-gate-usage-metrics.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/pre-session-metrics.d.ts +2 -0
- package/dist/ai-service/state-machine/helpers/pre-session-metrics.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/pre-session-metrics.js +3 -0
- package/dist/ai-service/state-machine/helpers/pre-session-metrics.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.d.ts +28 -0
- package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.d.ts.map +1 -0
- package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.js +254 -0
- package/dist/ai-service/state-machine/helpers/prompt-degradation-metrics.js.map +1 -0
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts +1 -0
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js +60 -27
- package/dist/ai-service/state-machine/helpers/prompt-request-metrics.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +12 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.js +42 -9
- package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
- package/dist/ai-service/state-machine/mocks.d.ts.map +1 -1
- package/dist/ai-service/state-machine/mocks.js +6 -0
- package/dist/ai-service/state-machine/mocks.js.map +1 -1
- package/dist/ai-service/util/call-peer.d.ts +11 -0
- package/dist/ai-service/util/call-peer.d.ts.map +1 -0
- package/dist/ai-service/util/call-peer.js +47 -0
- package/dist/ai-service/util/call-peer.js.map +1 -0
- package/dist/ai-service/util/peer-liveness.d.ts +23 -0
- package/dist/ai-service/util/peer-liveness.d.ts.map +1 -0
- package/dist/ai-service/util/peer-liveness.js +42 -0
- package/dist/ai-service/util/peer-liveness.js.map +1 -0
- package/dist/ai-service/util/rpc-timeout.d.ts +12 -5
- package/dist/ai-service/util/rpc-timeout.d.ts.map +1 -1
- package/dist/ai-service/util/rpc-timeout.js +12 -5
- package/dist/ai-service/util/rpc-timeout.js.map +1 -1
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +8 -21
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/lock-service/activity-tracker.d.ts +0 -5
- package/dist/lock-service/activity-tracker.d.ts.map +1 -1
- package/dist/lock-service/activity-tracker.js +0 -13
- package/dist/lock-service/activity-tracker.js.map +1 -1
- package/dist/lock-service/index.d.ts +308 -27
- package/dist/lock-service/index.d.ts.map +1 -1
- package/dist/lock-service/index.js +680 -156
- package/dist/lock-service/index.js.map +1 -1
- package/dist/policy-gate-callback-mapper.d.ts +3 -2
- package/dist/policy-gate-callback-mapper.d.ts.map +1 -1
- package/dist/policy-gate-callback-mapper.js.map +1 -1
- package/dist/policy-gate-runner.d.ts +4 -2
- package/dist/policy-gate-runner.d.ts.map +1 -1
- package/dist/policy-gate-runner.js +31 -8
- package/dist/policy-gate-runner.js.map +1 -1
- package/dist/scan-scope-merge.d.ts +16 -0
- package/dist/scan-scope-merge.d.ts.map +1 -0
- package/dist/scan-scope-merge.js +90 -0
- package/dist/scan-scope-merge.js.map +1 -0
- package/dist/socket-manager.d.ts +47 -6
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/socket-manager.js +285 -68
- package/dist/socket-manager.js.map +1 -1
- package/dist/sync-service/index.d.ts.map +1 -1
- package/dist/sync-service/index.js +3 -3
- package/dist/sync-service/index.js.map +1 -1
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +21 -0
- package/dist/test-setup.js.map +1 -0
- package/package.json +8 -8
package/dist/socket-manager.js
CHANGED
|
@@ -2,10 +2,11 @@ import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { WebSocketServer } from "ws";
|
|
5
|
-
import { createISocketClient, DEFAULT_NON_GIT_BRANCH, MAX_SOCKET_PAYLOAD_BYTES, SUPERBLOCKS_LIVE_GIT_BRANCH, TracedEventEmitter, TracedSocket, } from "@superblocksteam/shared";
|
|
5
|
+
import { createISocketClient, DEFAULT_NON_GIT_BRANCH, MAX_SOCKET_PAYLOAD_BYTES, SocketErrorException, SUPERBLOCKS_LIVE_GIT_BRANCH, TracedEventEmitter, TracedSocket, } from "@superblocksteam/shared";
|
|
6
6
|
import { uploadContextToBucketeer } from "./ai-service/context-upload.js";
|
|
7
7
|
import { createSocketRecordingMiddleware } from "./ai-service/recording/recorders/socket-recorder.js";
|
|
8
8
|
import { parseJwt } from "./ai-service/util/parse-jwt.js";
|
|
9
|
+
import { PeerLiveness, } from "./ai-service/util/peer-liveness.js";
|
|
9
10
|
import { clearGitAuthFailed, isGitAuthFailed, markGitAuthFailed, } from "./git-auth-state.js";
|
|
10
11
|
import { GIT_LOG_FORMAT, parseGitLog } from "./git-log.js";
|
|
11
12
|
import { GIT_OPERATION_BUSY_MESSAGE, } from "./git-operation-lock.js";
|
|
@@ -15,6 +16,7 @@ import { doesRemoteLiveBranchExist, ensureLiveBranchCheckedOut, pushLiveBranchWi
|
|
|
15
16
|
import { ensureProviderWorkflowFiles } from "./git-service/provider-workflow-files.js";
|
|
16
17
|
import { getSyncCountsAgainstDefaultBranch } from "./git-service/status-sync.js";
|
|
17
18
|
import { getErrorMeta, getLogger } from "./util/logger.js";
|
|
19
|
+
import { OperationQueue } from "./util/operation-queue.js";
|
|
18
20
|
const SB_WORKTREE_SNAPSHOTS_DIR = join(tmpdir(), "sb-worktree-snapshots");
|
|
19
21
|
/**
|
|
20
22
|
* Pulls the JWT-derived `userId` out of an `Authorization: Bearer <jwt>`
|
|
@@ -127,7 +129,28 @@ export function disposeSocketManager() {
|
|
|
127
129
|
_socketManager = undefined;
|
|
128
130
|
}
|
|
129
131
|
class SocketManager extends TracedEventEmitter {
|
|
132
|
+
peerLiveness = new PeerLiveness();
|
|
133
|
+
socketLiveness = new WeakMap();
|
|
134
|
+
/**
|
|
135
|
+
* Survives `editorSockets.delete` so a just-closed socket can still tell
|
|
136
|
+
* {@link getSurvivingEditorClient} which peerId to prefer among leftovers.
|
|
137
|
+
*/
|
|
138
|
+
editorPeerIdsBySocket = new WeakMap();
|
|
130
139
|
peerIds = new Map();
|
|
140
|
+
/**
|
|
141
|
+
* Editor peers whose presence is registered with the lock service, and the
|
|
142
|
+
* user each was registered for.
|
|
143
|
+
*
|
|
144
|
+
* Presence is per *peer*, not per socket: one tab reconnecting produces a new
|
|
145
|
+
* socket under the same peer id. Remembering "did I register?" on the socket
|
|
146
|
+
* therefore breaks the pair, because the socket that takes presence down need
|
|
147
|
+
* not be the one that put it up — a replacement that never makes an
|
|
148
|
+
* authorized call would close without removing anything, leaving the lock
|
|
149
|
+
* claimed for a peer that is gone.
|
|
150
|
+
*/
|
|
151
|
+
editorPresenceUserIdByPeerId = new Map();
|
|
152
|
+
/** Editor peers that have already triggered post-auth git activation. */
|
|
153
|
+
gitActivatedPeerIds = new Set();
|
|
131
154
|
/** Attached editor sockets, each mapped to the peer that opened it. */
|
|
132
155
|
editorSockets = new Map();
|
|
133
156
|
/**
|
|
@@ -158,6 +181,73 @@ class SocketManager extends TracedEventEmitter {
|
|
|
158
181
|
});
|
|
159
182
|
this.socketServer = wss;
|
|
160
183
|
let activeGitService = gitService;
|
|
184
|
+
const headlessLockOwners = [];
|
|
185
|
+
const headlessLockOperationQueue = new OperationQueue();
|
|
186
|
+
/**
|
|
187
|
+
* Drop every turn this peer owned, and hand the lock back if that was the
|
|
188
|
+
* last of them.
|
|
189
|
+
*
|
|
190
|
+
* Dropping the rows alone leaves the lock stranded. The lock service only
|
|
191
|
+
* hands back when *no* gateway is left, so with a replacement socket
|
|
192
|
+
* already attached that never fires — and once the owner row is gone, a
|
|
193
|
+
* later `editor.releaseTurnLock` finds nothing to release and does
|
|
194
|
+
* nothing. The AppBranchLock would then stay held for the rest of that
|
|
195
|
+
* socket's life, with no turn behind it.
|
|
196
|
+
*
|
|
197
|
+
* Queued with the release path so the two cannot both decide they are
|
|
198
|
+
* last.
|
|
199
|
+
*/
|
|
200
|
+
const dropHeadlessLockOwners = async (peerId) => {
|
|
201
|
+
await headlessLockOperationQueue.enqueue(async () => {
|
|
202
|
+
const ownedAny = headlessLockOwners.some((owner) => owner.peerId === peerId);
|
|
203
|
+
if (!ownedAny) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (let index = headlessLockOwners.length - 1; index >= 0; index--) {
|
|
207
|
+
if (headlessLockOwners[index]?.peerId === peerId) {
|
|
208
|
+
headlessLockOwners.splice(index, 1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (headlessLockOwners.length > 0) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Re-read after the handback's generation wait: a replacement gateway
|
|
215
|
+
// can claim a turn while the old one's write is still finishing.
|
|
216
|
+
await lockService?.relinquishLockForTurn(() => headlessLockOwners.length > 0);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
const dropHeadlessLockOwnersInBackground = (peerId, occasion) => {
|
|
220
|
+
void dropHeadlessLockOwners(peerId).catch((err) => {
|
|
221
|
+
getLogger().warn(`[socket-manager] failed to hand the turn lock back after ${occasion}: ${err instanceof Error ? err.message : String(err)}`);
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
lockService?.setOnGatewayLivenessLapsed?.((peerId) => {
|
|
225
|
+
dropHeadlessLockOwnersInBackground(peerId, "a gateway's liveness lapsed");
|
|
226
|
+
aiService?.endHeadlessTurn(peerId);
|
|
227
|
+
});
|
|
228
|
+
const releaseHeadlessLock = async (peerId, turnId) => {
|
|
229
|
+
return await headlessLockOperationQueue.enqueue(async () => {
|
|
230
|
+
const ownerIndex = headlessLockOwners.findIndex((owner) => owner.peerId === peerId &&
|
|
231
|
+
(turnId ? owner.turnId === turnId : owner.turnId === undefined));
|
|
232
|
+
if (ownerIndex === -1) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const [owner] = headlessLockOwners.splice(ownerIndex, 1);
|
|
236
|
+
if (headlessLockOwners.length > 0) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await lockService?.relinquishLockForTurn(() => headlessLockOwners.length > 0);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (owner) {
|
|
245
|
+
headlessLockOwners.splice(ownerIndex, 0, owner);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
};
|
|
161
251
|
// Provide sync context (branchName, lastCommitId) to the AI service
|
|
162
252
|
// for remote context restore fallback from Bucketeer on fresh pods.
|
|
163
253
|
if (aiService && syncService) {
|
|
@@ -325,10 +415,53 @@ class SocketManager extends TracedEventEmitter {
|
|
|
325
415
|
};
|
|
326
416
|
wss.on("connection", (ws, kind, peerId, userId) => {
|
|
327
417
|
const isEditor = kind === "editor";
|
|
418
|
+
const connectionLiveness = kind === "library" ? undefined : this.peerLiveness.bindConnection();
|
|
419
|
+
if (connectionLiveness) {
|
|
420
|
+
ws.on("message", () => connectionLiveness.markInbound());
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* When an editor authenticates after git was enabled, the lock may
|
|
424
|
+
* still be on 'main'. This calls
|
|
425
|
+
* `ensureRuntimeDbfsBranchConsistency` -> `lockService.switchBranch`
|
|
426
|
+
* to move it to superblocks/live.
|
|
427
|
+
*/
|
|
428
|
+
const activateGitForAuthedEditor = () => {
|
|
429
|
+
if (!activateGitService) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
void activateGitService().catch((err) => {
|
|
433
|
+
getLogger().warn(`[socket-manager] post-auth git activation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
434
|
+
});
|
|
435
|
+
};
|
|
328
436
|
const authMiddleware = async (params, ctx, _peer, next) => {
|
|
329
437
|
const result = await checkAuthorization(ctx.peerAuthorization);
|
|
330
438
|
if (!result.success) {
|
|
331
|
-
throw new
|
|
439
|
+
throw new SocketErrorException(result.status, result.error);
|
|
440
|
+
}
|
|
441
|
+
if (isEditor &&
|
|
442
|
+
userId &&
|
|
443
|
+
!this.editorPresenceUserIdByPeerId.has(peerId) &&
|
|
444
|
+
// The authorization above is awaited, so the socket that asked for
|
|
445
|
+
// it can be gone by now. `peerIds` loses the peer when its last
|
|
446
|
+
// socket closes, so this is what keeps a late answer from
|
|
447
|
+
// registering presence nothing is left to remove.
|
|
448
|
+
this.peerIds.has(peerId)) {
|
|
449
|
+
this.editorPresenceUserIdByPeerId.set(peerId, userId);
|
|
450
|
+
lockService?.addConnectedPeerIdToUser(userId, peerId);
|
|
451
|
+
}
|
|
452
|
+
// After presence, and only for a peer that has proved itself.
|
|
453
|
+
//
|
|
454
|
+
// Git activation moves the DBFS branch, which asks the lock service
|
|
455
|
+
// whether this pod holds the lock. Presence registration is what
|
|
456
|
+
// starts the attach-time acquisition, so running this first — as it
|
|
457
|
+
// did from the connect handler — asks that question before there is
|
|
458
|
+
// an acquisition to observe: `isLockedWhenSettled()` settles what is
|
|
459
|
+
// in flight, and nothing is. The switch then answers "no lock",
|
|
460
|
+
// releases the one that lands moments later, and does not take it
|
|
461
|
+
// back. Ordering them here is what keeps the two consistent.
|
|
462
|
+
if (isEditor && !this.gitActivatedPeerIds.has(peerId)) {
|
|
463
|
+
this.gitActivatedPeerIds.add(peerId);
|
|
464
|
+
activateGitForAuthedEditor();
|
|
332
465
|
}
|
|
333
466
|
// Notify AiService that this peer has just produced a
|
|
334
467
|
// proof-of-auth. AiService deduplicates internally and uses
|
|
@@ -362,7 +495,16 @@ class SocketManager extends TracedEventEmitter {
|
|
|
362
495
|
// it follows the peer rather than reporting into the socket it
|
|
363
496
|
// lost. Naming it is required — otherwise any authorized gateway
|
|
364
497
|
// session could take over another one's turn.
|
|
365
|
-
|
|
498
|
+
const headlessTurnId = readHeadlessTurnId(params);
|
|
499
|
+
if (ctx.method !== "editor.releaseTurnLock") {
|
|
500
|
+
aiService?.claimHeadlessTurn(socket, peerId, headlessTurnId, connectionLiveness);
|
|
501
|
+
if (headlessTurnId) {
|
|
502
|
+
const owner = headlessLockOwners.find((candidate) => candidate.turnId === headlessTurnId);
|
|
503
|
+
if (owner) {
|
|
504
|
+
owner.peerId = peerId;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
366
508
|
}
|
|
367
509
|
return await next();
|
|
368
510
|
};
|
|
@@ -410,12 +552,33 @@ class SocketManager extends TracedEventEmitter {
|
|
|
410
552
|
if (kind !== "gateway") {
|
|
411
553
|
return await aiService.handleAiGenerateWithQueue(aiRequest, queueMode);
|
|
412
554
|
}
|
|
413
|
-
|
|
555
|
+
// Write access is taken per turn, not held for the whole
|
|
556
|
+
// session, so the app is unlocked between prompts and a
|
|
557
|
+
// person can open it. Before the turn is bound, and
|
|
558
|
+
// awaited: a turn that cannot write must fail here carrying
|
|
559
|
+
// the server's own reason — somebody is editing in the
|
|
560
|
+
// browser — rather than bind routing and then unwind it.
|
|
561
|
+
const requestedTurnId = typeof request.requestId === "string"
|
|
414
562
|
? request.requestId
|
|
415
|
-
: undefined
|
|
563
|
+
: undefined;
|
|
564
|
+
const lockOwner = {
|
|
565
|
+
peerId,
|
|
566
|
+
turnId: requestedTurnId,
|
|
567
|
+
};
|
|
568
|
+
const turnId = await headlessLockOperationQueue.enqueue(async () => {
|
|
569
|
+
await lockService?.acquireLockForTurn();
|
|
570
|
+
headlessLockOwners.push(lockOwner);
|
|
571
|
+
return aiService.beginHeadlessTurn(socket, peerId, requestedTurnId, connectionLiveness);
|
|
572
|
+
});
|
|
416
573
|
let result;
|
|
417
574
|
try {
|
|
418
575
|
result = await aiService.handleAiGenerateWithQueue(aiRequest, queueMode);
|
|
576
|
+
const ownerIndex = headlessLockOwners.indexOf(lockOwner);
|
|
577
|
+
for (let index = ownerIndex - 1; index >= 0; index--) {
|
|
578
|
+
if (headlessLockOwners[index]?.peerId === peerId) {
|
|
579
|
+
headlessLockOwners.splice(index, 1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
419
582
|
}
|
|
420
583
|
catch (err) {
|
|
421
584
|
// The turn is bound before the prompt is handed over, so
|
|
@@ -423,6 +586,12 @@ class SocketManager extends TracedEventEmitter {
|
|
|
423
586
|
// it. Left bound, it would capture the next turn a user
|
|
424
587
|
// starts in the browser.
|
|
425
588
|
aiService.abandonHeadlessTurn(peerId, turnId);
|
|
589
|
+
try {
|
|
590
|
+
await releaseHeadlessLock(peerId, turnId);
|
|
591
|
+
}
|
|
592
|
+
catch (releaseErr) {
|
|
593
|
+
getLogger().warn("[socket-manager] failed to release the lock for a rejected headless turn", getErrorMeta(releaseErr));
|
|
594
|
+
}
|
|
426
595
|
throw err;
|
|
427
596
|
}
|
|
428
597
|
// Answering with the turn handle tells the caller two
|
|
@@ -432,6 +601,16 @@ class SocketManager extends TracedEventEmitter {
|
|
|
432
601
|
return { ...result, headlessTurn: { turnId } };
|
|
433
602
|
},
|
|
434
603
|
],
|
|
604
|
+
releaseTurnLock: [
|
|
605
|
+
async (request) => {
|
|
606
|
+
if (kind !== "gateway") {
|
|
607
|
+
throw new Error("Only gateway peers can release a headless turn lock");
|
|
608
|
+
}
|
|
609
|
+
if (await releaseHeadlessLock(peerId, request.headlessTurnId)) {
|
|
610
|
+
aiService?.endHeadlessTurn(peerId, request.headlessTurnId);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
],
|
|
435
614
|
aiTriggerMigrationTurn: [
|
|
436
615
|
async (request) => {
|
|
437
616
|
if (!aiService) {
|
|
@@ -1020,17 +1199,20 @@ class SocketManager extends TracedEventEmitter {
|
|
|
1020
1199
|
activeGitService = undefined;
|
|
1021
1200
|
disconnectInProgress = true;
|
|
1022
1201
|
clearGitAuthFailed();
|
|
1202
|
+
// Keep file-sync / lock wiring; drop only the Git
|
|
1203
|
+
// service so the next Clark turn sees unconnected.
|
|
1204
|
+
aiService?.setRuntimeGitServices(undefined, syncService, gitOperationLock);
|
|
1023
1205
|
// Ensure DBFS sync context is no longer tied to
|
|
1024
1206
|
// `superblocks/live` immediately after native git disconnect.
|
|
1025
1207
|
syncService?.setBranchName(DEFAULT_NON_GIT_BRANCH);
|
|
1026
1208
|
// Native git disconnect should move runtime lock context back
|
|
1027
1209
|
// to default branch before clients reconnect.
|
|
1028
1210
|
if (lockService) {
|
|
1029
|
-
|
|
1211
|
+
// Retain the pre-switch state for the explicit fallback
|
|
1212
|
+
// if an atomic branch transfer fails.
|
|
1213
|
+
const wasLockedBeforeDisconnect = await lockService.isLockedWhenSettled();
|
|
1030
1214
|
try {
|
|
1031
|
-
await lockService.switchBranch(DEFAULT_NON_GIT_BRANCH
|
|
1032
|
-
reacquireLock: wasLockedBeforeDisconnect,
|
|
1033
|
-
});
|
|
1215
|
+
await lockService.switchBranch(DEFAULT_NON_GIT_BRANCH);
|
|
1034
1216
|
}
|
|
1035
1217
|
catch (err) {
|
|
1036
1218
|
getLogger().warn("[git] gitDisconnect failed to switch lock branch back to main", {
|
|
@@ -1282,6 +1464,9 @@ class SocketManager extends TracedEventEmitter {
|
|
|
1282
1464
|
},
|
|
1283
1465
|
},
|
|
1284
1466
|
}));
|
|
1467
|
+
if (connectionLiveness) {
|
|
1468
|
+
this.socketLiveness.set(socket, connectionLiveness);
|
|
1469
|
+
}
|
|
1285
1470
|
// we are storing all of the sockets for a given peerId (editor+library pair of connections)
|
|
1286
1471
|
// which lets us handle synchronization of operations between the two
|
|
1287
1472
|
let existingSockets = this.peerIds.get(peerId);
|
|
@@ -1294,23 +1479,14 @@ class SocketManager extends TracedEventEmitter {
|
|
|
1294
1479
|
// Liveness is granted in the auth middleware, not here: the upgrade
|
|
1295
1480
|
// path only checks `applicationId`, and holding the sandbox open is
|
|
1296
1481
|
// not something an unproven socket gets to do.
|
|
1297
|
-
this.emit("connect:gateway", socket, peerId);
|
|
1482
|
+
this.emit("connect:gateway", socket, peerId, connectionLiveness);
|
|
1298
1483
|
}
|
|
1299
1484
|
else if (isEditor) {
|
|
1300
1485
|
this.editorSockets.set(socket, peerId);
|
|
1301
|
-
this.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
// When a new editor connects after git was enabled, the lock may
|
|
1306
|
-
// still be on 'main'. Trigger git activation which calls
|
|
1307
|
-
// ensureRuntimeDbfsBranchConsistency → lockService.switchBranch
|
|
1308
|
-
// to transfer the lock to superblocks/live.
|
|
1309
|
-
if (activateGitService) {
|
|
1310
|
-
void activateGitService().catch((err) => {
|
|
1311
|
-
getLogger().warn(`[socket-manager] post-connect git activation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1486
|
+
this.editorPeerIdsBySocket.set(socket, peerId);
|
|
1487
|
+
this.emit("connect:editor", socket, peerId, connectionLiveness);
|
|
1488
|
+
// Git activation is deliberately NOT started here — it runs from the
|
|
1489
|
+
// auth middleware, after presence. See `activateGitForAuthedEditor`.
|
|
1314
1490
|
}
|
|
1315
1491
|
else {
|
|
1316
1492
|
this.librarySockets.add(socket);
|
|
@@ -1318,61 +1494,73 @@ class SocketManager extends TracedEventEmitter {
|
|
|
1318
1494
|
}
|
|
1319
1495
|
existingSockets.add(socket);
|
|
1320
1496
|
ws.on("close", (code, reason) => {
|
|
1497
|
+
connectionLiveness?.close();
|
|
1321
1498
|
const peerSockets = this.peerIds.get(peerId);
|
|
1322
1499
|
peerSockets?.delete(socket);
|
|
1323
1500
|
if (peerSockets?.size === 0) {
|
|
1324
1501
|
this.peerIds.delete(peerId);
|
|
1502
|
+
// The peer is gone, so a later reconnect under the same id is a
|
|
1503
|
+
// new arrival and activates git again.
|
|
1504
|
+
this.gitActivatedPeerIds.delete(peerId);
|
|
1325
1505
|
}
|
|
1326
1506
|
getLogger().info(`Socket from ${kind} closed code: ${code} reason: ${reason}`);
|
|
1327
1507
|
if (kind === "gateway") {
|
|
1328
1508
|
this.gatewaySockets.delete(socket);
|
|
1329
1509
|
lockService?.markGatewayDetached(peerId);
|
|
1330
|
-
|
|
1331
|
-
|
|
1510
|
+
// No blanket clear when the last socket goes: every peer's rows are
|
|
1511
|
+
// dropped by its own close, and the one that empties the list is
|
|
1512
|
+
// what hands the lock back.
|
|
1513
|
+
dropHeadlessLockOwnersInBackground(peerId, "a gateway socket closed");
|
|
1514
|
+
aiService?.endHeadlessTurn(peerId, connectionLiveness);
|
|
1515
|
+
this.emit("disconnect:gateway", peerId, connectionLiveness);
|
|
1332
1516
|
}
|
|
1333
1517
|
else if (isEditor) {
|
|
1334
1518
|
this.editorSockets.delete(socket);
|
|
1335
1519
|
const hasOtherEditorSocket = Array.from(peerSockets ?? []).some((peerSocket) => this.editorSockets.has(peerSocket));
|
|
1336
|
-
if (hasOtherEditorSocket) {
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
.
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
.
|
|
1370
|
-
|
|
1371
|
-
|
|
1520
|
+
if (!hasOtherEditorSocket) {
|
|
1521
|
+
// Keyed by peer, so whichever of the peer's sockets is last out
|
|
1522
|
+
// takes the presence down — including one that never authenticated
|
|
1523
|
+
// itself.
|
|
1524
|
+
const presenceUserId = this.editorPresenceUserIdByPeerId.get(peerId);
|
|
1525
|
+
if (presenceUserId) {
|
|
1526
|
+
this.editorPresenceUserIdByPeerId.delete(peerId);
|
|
1527
|
+
lockService?.removeConnectedPeerIdFromUser(presenceUserId, peerId);
|
|
1528
|
+
if (lockService?.connectedUsers.length === 0) {
|
|
1529
|
+
if (aiService?.isBusy()) {
|
|
1530
|
+
getLogger().info("[socket-manager] All users disconnected but AI service is busy, deferring cache cleanup until generation completes");
|
|
1531
|
+
let cleaned = false;
|
|
1532
|
+
const deferredCleanup = () => {
|
|
1533
|
+
if (cleaned)
|
|
1534
|
+
return;
|
|
1535
|
+
cleaned = true;
|
|
1536
|
+
// Only clean up if no users have reconnected in the meantime
|
|
1537
|
+
if (lockService?.connectedUsers.length === 0) {
|
|
1538
|
+
getLogger().info("[socket-manager] Generation ended with no connected users, cleaning up AI service cache");
|
|
1539
|
+
aiService
|
|
1540
|
+
.removeIntegrationCache()
|
|
1541
|
+
.catch((error) => {
|
|
1542
|
+
getLogger().warn(`Error removing integration cache after deferred cleanup: ${error}`);
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
getLogger().info("[socket-manager] Generation ended but users have reconnected, skipping cache cleanup");
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
aiService.once("generationCompleted", deferredCleanup);
|
|
1550
|
+
aiService.once("generationFailed", deferredCleanup);
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
getLogger().info("[socket-manager] All users disconnected, cleaning up AI service cache");
|
|
1554
|
+
aiService
|
|
1555
|
+
?.removeIntegrationCache()
|
|
1556
|
+
.catch((error) => {
|
|
1557
|
+
getLogger().warn(`Error removing integration cache after disconnect: ${error}`);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1372
1560
|
}
|
|
1373
1561
|
}
|
|
1374
1562
|
}
|
|
1375
|
-
this.emit("disconnect:editor", peerId);
|
|
1563
|
+
this.emit("disconnect:editor", peerId, socket, connectionLiveness);
|
|
1376
1564
|
}
|
|
1377
1565
|
else {
|
|
1378
1566
|
this.librarySockets.delete(socket);
|
|
@@ -1404,19 +1592,48 @@ class SocketManager extends TracedEventEmitter {
|
|
|
1404
1592
|
}
|
|
1405
1593
|
}
|
|
1406
1594
|
/**
|
|
1407
|
-
* Any editor socket that does not belong to `
|
|
1595
|
+
* Any editor socket that does not belong to `exclude`.
|
|
1408
1596
|
*
|
|
1409
1597
|
* Used when a peer disconnects: Clark's RPCs are bound to a single editor,
|
|
1410
1598
|
* so a survivor is what keeps API execution alive for a user whose tab is
|
|
1411
1599
|
* still open after a headless build's socket closes.
|
|
1600
|
+
*
|
|
1601
|
+
* Same-id sockets are preferred when `exclude` is the departing socket: a
|
|
1602
|
+
* tab that reconnected keeps the bound Clark context instead of handing it
|
|
1603
|
+
* (and in-flight generation / tool-permission prompts) to whichever other
|
|
1604
|
+
* editor happened to connect first. A string `exclude` still means "this
|
|
1605
|
+
* peer is gone" and skips every socket with that id.
|
|
1412
1606
|
*/
|
|
1413
|
-
getSurvivingEditorClient(
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1607
|
+
getSurvivingEditorClient(exclude) {
|
|
1608
|
+
const excludedPeerId = typeof exclude === "string" ? exclude : undefined;
|
|
1609
|
+
const preferredPeerId = typeof exclude === "string"
|
|
1610
|
+
? undefined
|
|
1611
|
+
: this.editorPeerIdsBySocket.get(exclude);
|
|
1612
|
+
const pick = (onlyPreferred) => {
|
|
1613
|
+
for (const [peer, peerId] of this.editorSockets) {
|
|
1614
|
+
const excluded = excludedPeerId !== undefined
|
|
1615
|
+
? peerId === excludedPeerId
|
|
1616
|
+
: peer === exclude;
|
|
1617
|
+
if (excluded) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (onlyPreferred && peerId !== preferredPeerId) {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
const liveness = this.socketLiveness.get(peer);
|
|
1624
|
+
if (liveness) {
|
|
1625
|
+
return { liveness, peer, peerId };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return undefined;
|
|
1629
|
+
};
|
|
1630
|
+
if (preferredPeerId !== undefined) {
|
|
1631
|
+
const sameId = pick(true);
|
|
1632
|
+
if (sameId) {
|
|
1633
|
+
return sameId;
|
|
1417
1634
|
}
|
|
1418
1635
|
}
|
|
1419
|
-
return
|
|
1636
|
+
return pick(false);
|
|
1420
1637
|
}
|
|
1421
1638
|
callLibraryClients(fn) {
|
|
1422
1639
|
for (const socket of this.librarySockets) {
|