@lasso-ai/cli 1.3.1 → 1.3.3

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.
@@ -17,6 +17,7 @@ const chalk_1 = __importDefault(require("chalk"));
17
17
  const agent_1 = require("./agent");
18
18
  const project_1 = require("./project");
19
19
  const auth_1 = require("./auth");
20
+ const snapshot_1 = require("./snapshot");
20
21
  exports.DEFAULT_BRIDGE_PORT = 3056;
21
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
22
23
  async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
@@ -147,6 +148,164 @@ function proposalMatchesCurrentSource(cwd, changes) {
147
148
  return false;
148
149
  }
149
150
  }
151
+ async function triggerBuildFix(cwd, taskId, buildErrors, buildCommand, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext) {
152
+ const MAX_RETRIES = 3;
153
+ const context = buildErrorContext.get(taskId);
154
+ if (!context)
155
+ return;
156
+ context.retryCount += 1;
157
+ if (context.retryCount > MAX_RETRIES) {
158
+ // Max retries reached - revert changes and show error
159
+ for (const snapshot of taskSnapshots.get(taskId) || []) {
160
+ if (snapshot.content === null) {
161
+ if (node_fs_1.default.existsSync(snapshot.filePath))
162
+ node_fs_1.default.unlinkSync(snapshot.filePath);
163
+ }
164
+ else {
165
+ node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
166
+ }
167
+ }
168
+ taskSnapshots.delete(taskId);
169
+ buildErrorContext.delete(taskId);
170
+ socket.send(JSON.stringify({
171
+ type: "agent_status",
172
+ taskId,
173
+ status: "error",
174
+ message: `⚠️ Build validation failed after ${MAX_RETRIES} attempts. The change was reverted. Errors:\n${buildErrors.slice(0, 5).join("\n")}${buildErrors.length > 5 ? `\n... and ${buildErrors.length - 5} more errors` : ""}`
175
+ }));
176
+ return;
177
+ }
178
+ // Re-apply the original changes
179
+ try {
180
+ const planned = new Map();
181
+ for (const change of originalChanges) {
182
+ const isNew = !change.oldString;
183
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
184
+ const exists = node_fs_1.default.existsSync(filePath);
185
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
186
+ const prepared = (!exists || isNew)
187
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
188
+ : prepareChange(content, change);
189
+ const fileChanges = planned.get(filePath) || [];
190
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
191
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
192
+ }
193
+ fileChanges.push({ filePath, content, exists, ...prepared });
194
+ planned.set(filePath, fileChanges);
195
+ }
196
+ for (const [filePath, changes] of planned) {
197
+ const exists = changes[0].exists;
198
+ const content = changes[0].content;
199
+ const nextContent = exists
200
+ ? [...changes]
201
+ .sort((a, b) => b.start - a.start)
202
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
203
+ : changes[0].newString;
204
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
205
+ node_fs_1.default.writeFileSync(filePath, nextContent);
206
+ }
207
+ }
208
+ catch (applyError) {
209
+ socket.send(JSON.stringify({
210
+ type: "agent_status",
211
+ taskId,
212
+ status: "error",
213
+ message: `Failed to re-apply original changes: ${applyError instanceof Error ? applyError.message : "Unknown error"}`
214
+ }));
215
+ return;
216
+ }
217
+ // Run agent to fix build errors
218
+ const { runOneShotAgent } = await import("./oneshot.js");
219
+ const fixPrompt = `The previous change caused build errors. Please fix them. Build command: ${buildCommand}\n\nBuild errors:\n${buildErrors.slice(0, 10).join("\n")}${buildErrors.length > 10 ? `\n... and ${buildErrors.length - 10} more errors` : ""}\n\nFix these errors by making minimal changes to the files. Attempt ${context.retryCount} of ${MAX_RETRIES}.`;
220
+ const controller = new AbortController();
221
+ taskControllers.set(taskId, controller);
222
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: `Fixing build errors (attempt ${context.retryCount}/${MAX_RETRIES})...` }));
223
+ try {
224
+ const result = await runOneShotAgent(cwd, fixPrompt, "project", { provider: "google", model: "gemini-2.5-flash", lassoKey: resolvedLassoKey, serverUrl: "https://api.lasso.byorello.space" }, controller.signal, (status, message, detail) => {
225
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
226
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
227
+ }
228
+ }, (prompt) => {
229
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
230
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId, prompt }));
231
+ }
232
+ }, undefined, "https://api.lasso.byorello.space", resolvedLassoKey || agentConfig?.apiKey || collabConfig?.apiKey, taskId, undefined);
233
+ if (result.ok && result.changes && result.changes.length > 0) {
234
+ // Apply the fix
235
+ const snapshots = [];
236
+ const planned = new Map();
237
+ for (const change of result.changes) {
238
+ const isNew = !change.oldString;
239
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
240
+ const exists = node_fs_1.default.existsSync(filePath);
241
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
242
+ const prepared = (!exists || isNew)
243
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
244
+ : prepareChange(content, change);
245
+ const fileChanges = planned.get(filePath) || [];
246
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
247
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
248
+ }
249
+ fileChanges.push({ filePath, content, exists, ...prepared });
250
+ planned.set(filePath, fileChanges);
251
+ }
252
+ taskSnapshots.set(taskId, snapshots);
253
+ for (const [filePath, changes] of planned) {
254
+ const exists = changes[0].exists;
255
+ const content = changes[0].content;
256
+ snapshots.push({ filePath, content: exists ? content : null });
257
+ const nextContent = exists
258
+ ? [...changes]
259
+ .sort((a, b) => b.start - a.start)
260
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
261
+ : changes[0].newString;
262
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
263
+ node_fs_1.default.writeFileSync(filePath, nextContent);
264
+ }
265
+ // Validate the fix
266
+ const validation = await validateBuild(cwd);
267
+ if (validation.passed) {
268
+ // Build fixed!
269
+ buildErrorContext.delete(taskId);
270
+ socket.send(JSON.stringify({
271
+ type: "applied",
272
+ taskId,
273
+ message: `Build errors fixed after ${context.retryCount} attempt${context.retryCount > 1 ? "s" : ""}. ${result.changes.length} file${result.changes.length === 1 ? "" : "s"} updated.`,
274
+ validation
275
+ }));
276
+ }
277
+ else {
278
+ // Still failing - trigger another retry
279
+ socket.send(JSON.stringify({
280
+ type: "agent_status",
281
+ taskId,
282
+ status: "working",
283
+ message: `⚠️ Build still failing. Retrying...`,
284
+ detail: `Build command: ${validation.command}\nErrors:\n${validation.errors.slice(0, 5).join("\n")}${validation.errors.length > 5 ? `\n... and ${validation.errors.length - 5} more errors` : ""}`
285
+ }));
286
+ // Update context with new errors
287
+ buildErrorContext.set(taskId, {
288
+ errors: validation.errors,
289
+ command: validation.command,
290
+ originalChanges: originalChanges,
291
+ retryCount: context.retryCount
292
+ });
293
+ await triggerBuildFix(cwd, taskId, validation.errors, validation.command, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
294
+ }
295
+ }
296
+ else {
297
+ // Agent failed to produce a fix
298
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: result.error || "Failed to fix build errors" }));
299
+ }
300
+ }
301
+ catch (error) {
302
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "Build fix encountered an error" }));
303
+ }
304
+ finally {
305
+ if (taskControllers.get(taskId) === controller)
306
+ taskControllers.delete(taskId);
307
+ }
308
+ }
150
309
  async function validateBuild(cwd) {
151
310
  const pkgPath = node_path_1.default.join(cwd, "package.json");
152
311
  if (!node_fs_1.default.existsSync(pkgPath)) {
@@ -257,6 +416,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
257
416
  const editRequests = new Map();
258
417
  const editConfigs = new Map();
259
418
  const reviewRefreshAttempts = new Map();
419
+ const buildErrorContext = new Map();
260
420
  const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
261
421
  const fileEnv = envRoots.reduce((values, root) => ({
262
422
  ...values,
@@ -276,6 +436,120 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
276
436
  fileEnv.NEXT_LASSO_API_KEY ||
277
437
  "";
278
438
  const lassoKeyConfigured = Boolean(resolvedLassoKey);
439
+ // Task queue for concurrent execution
440
+ const MAX_CONCURRENT_TASKS = 3;
441
+ const taskQueue = [];
442
+ const runningTasks = new Set();
443
+ const taskPriority = new Map(); // Lower number = higher priority
444
+ // Will be set when WebSocket connects
445
+ let currentSocket = null;
446
+ function processTaskQueue() {
447
+ if (runningTasks.size >= MAX_CONCURRENT_TASKS || taskQueue.length === 0 || !currentSocket)
448
+ return;
449
+ const socket = currentSocket; // Capture non-null reference
450
+ taskQueue.sort((a, b) => {
451
+ const priorityA = taskPriority.get(a.taskId) ?? 999;
452
+ const priorityB = taskPriority.get(b.taskId) ?? 999;
453
+ return priorityA - priorityB;
454
+ });
455
+ const task = taskQueue.shift();
456
+ if (!task)
457
+ return;
458
+ runningTasks.add(task.taskId);
459
+ if (task.message.type === "edit") {
460
+ const request = task.message;
461
+ taskControllers.get(request.taskId)?.abort();
462
+ const controller = new AbortController();
463
+ taskControllers.set(request.taskId, controller);
464
+ if (socket.readyState === socket.OPEN) {
465
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: "Working…" }));
466
+ }
467
+ void (0, agent_1.proposeChanges)(cwd, request, task.config, controller.signal, (message, detail, level) => {
468
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
469
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
470
+ }
471
+ }, (prompt) => {
472
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
473
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
474
+ }
475
+ })
476
+ .then((proposal) => {
477
+ if (controller.signal.aborted || socket.readyState !== socket.OPEN)
478
+ return;
479
+ if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
480
+ const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
481
+ const MAX_REFRESH_ATTEMPTS = 3;
482
+ if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
483
+ reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
484
+ setTimeout(() => {
485
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
486
+ enqueueTask(request.taskId, request, task.config, 0);
487
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})` }));
488
+ }
489
+ }, 500 * (refreshAttempts + 1));
490
+ }
491
+ else {
492
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing after multiple refresh attempts. Stop the dev-server edit or try the request again." }));
493
+ }
494
+ return;
495
+ }
496
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
497
+ })
498
+ .catch((error) => {
499
+ if (socket.readyState !== socket.OPEN)
500
+ return;
501
+ if (controller.signal.aborted) {
502
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
503
+ return;
504
+ }
505
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
506
+ })
507
+ .finally(() => {
508
+ if (taskControllers.get(request.taskId) === controller)
509
+ taskControllers.delete(request.taskId);
510
+ runningTasks.delete(request.taskId);
511
+ taskPriority.delete(request.taskId);
512
+ processTaskQueue();
513
+ });
514
+ }
515
+ else if (task.message.type === "ask") {
516
+ const controller = new AbortController();
517
+ taskControllers.set(task.taskId, controller);
518
+ socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: "working", message: "Working…" }));
519
+ void (0, agent_1.answerQuestion)(cwd, {
520
+ taskId: task.taskId,
521
+ question: task.message.question,
522
+ context: task.message.context,
523
+ element: task.message.element,
524
+ messages: task.message.messages,
525
+ }, task.config, controller.signal, (message, detail, level) => {
526
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
527
+ socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: level === "error" ? "error" : "working", message, detail }));
528
+ }
529
+ }, (prompt) => {
530
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
531
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId: task.taskId, prompt }));
532
+ }
533
+ }).then((answer) => {
534
+ if (!controller.signal.aborted)
535
+ socket.send(JSON.stringify({ type: "assistant_message", taskId: task.taskId, message: answer }));
536
+ }).catch((error) => {
537
+ if (!controller.signal.aborted)
538
+ socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
539
+ }).finally(() => {
540
+ if (taskControllers.get(task.taskId) === controller)
541
+ taskControllers.delete(task.taskId);
542
+ runningTasks.delete(task.taskId);
543
+ taskPriority.delete(task.taskId);
544
+ processTaskQueue();
545
+ });
546
+ }
547
+ }
548
+ function enqueueTask(taskId, message, config, priority = 0) {
549
+ taskPriority.set(taskId, priority);
550
+ taskQueue.push({ taskId, message, config });
551
+ processTaskQueue();
552
+ }
279
553
  // Fetch workspace configuration from server (dashboard-configured providers)
280
554
  let workspaceConfig = null;
281
555
  if (lassoKeyConfigured) {
@@ -539,6 +813,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
539
813
  };
540
814
  wss.on("connection", (socket) => {
541
815
  overlaySocket = socket;
816
+ currentSocket = socket;
542
817
  console.log(chalk_1.default.green("✓") + " Overlay connected");
543
818
  const creds = (0, auth_1.loadCredentials)();
544
819
  const userInfo = creds?.userName
@@ -553,57 +828,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
553
828
  if (socket.readyState === socket.OPEN)
554
829
  socket.send(JSON.stringify({ type: "git_state", git }));
555
830
  });
556
- const runEditReview = (request, config, statusMessage) => {
557
- taskControllers.get(request.taskId)?.abort();
558
- const controller = new AbortController();
559
- taskControllers.set(request.taskId, controller);
560
- if (socket.readyState === socket.OPEN) {
561
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage || "Working…" }));
562
- }
563
- void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail, level) => {
564
- if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
565
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
566
- }
567
- }, (prompt) => {
568
- if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
569
- socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
570
- }
571
- })
572
- .then((proposal) => {
573
- if (controller.signal.aborted || socket.readyState !== socket.OPEN)
574
- return;
575
- if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
576
- const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
577
- const MAX_REFRESH_ATTEMPTS = 3;
578
- if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
579
- reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
580
- setTimeout(() => {
581
- if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
582
- runEditReview(request, config, `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})`);
583
- }
584
- }, 500 * (refreshAttempts + 1));
585
- }
586
- else {
587
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing after multiple refresh attempts. Stop the dev-server edit or try the request again." }));
588
- }
589
- return;
590
- }
591
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
592
- })
593
- .catch((error) => {
594
- if (socket.readyState !== socket.OPEN)
595
- return;
596
- if (controller.signal.aborted) {
597
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
598
- return;
599
- }
600
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
601
- })
602
- .finally(() => {
603
- if (taskControllers.get(request.taskId) === controller)
604
- taskControllers.delete(request.taskId);
605
- });
606
- };
607
831
  socket.on("message", async (raw) => {
608
832
  const msg = JSON.parse(raw.toString());
609
833
  if (msg.type === "edit" || msg.type === "ask") {
@@ -712,23 +936,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
712
936
  serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv),
713
937
  };
714
938
  }
715
- socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "Working…" }));
716
- void (0, agent_1.answerQuestion)(cwd, { taskId: msg.taskId, question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail, level) => {
717
- if (!controller.signal.aborted && socket.readyState === socket.OPEN)
718
- socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: level === "error" ? "error" : "working", message, detail }));
719
- }, (prompt) => {
720
- if (!controller.signal.aborted && socket.readyState === socket.OPEN)
721
- socket.send(JSON.stringify({ type: "agent_prompt", taskId: msg.taskId, prompt }));
722
- }).then((answer) => {
723
- if (!controller.signal.aborted)
724
- socket.send(JSON.stringify({ type: "assistant_message", taskId: msg.taskId, message: answer }));
725
- }).catch((error) => {
726
- if (!controller.signal.aborted)
727
- socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
728
- }).finally(() => {
729
- if (taskControllers.get(msg.taskId) === controller)
730
- taskControllers.delete(msg.taskId);
731
- });
939
+ // Enqueue task for concurrent execution
940
+ enqueueTask(msg.taskId, msg, selectedConfig, 1); // Priority 1 for ask tasks
732
941
  }
733
942
  else if (msg.type === "edit") {
734
943
  const localProvider = msg.provider === "cli";
@@ -767,17 +976,29 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
767
976
  editRequests.set(msg.taskId, msg);
768
977
  editConfigs.set(msg.taskId, selectedConfig);
769
978
  reviewRefreshAttempts.set(msg.taskId, 0);
770
- runEditReview(msg, selectedConfig);
979
+ // Enqueue task for concurrent execution with higher priority (0)
980
+ enqueueTask(msg.taskId, msg, selectedConfig, 0);
771
981
  }
772
982
  else if (msg.type === "agent_prompt_response") {
773
983
  (0, agent_1.respondToAgentPrompt)(msg.taskId, msg.response);
774
984
  }
775
985
  else if (msg.type === "stop") {
776
- if (msg.taskId)
986
+ if (msg.taskId) {
777
987
  taskControllers.get(msg.taskId)?.abort();
778
- else
988
+ // Remove from queue and running tasks
989
+ const queueIndex = taskQueue.findIndex(t => t.taskId === msg.taskId);
990
+ if (queueIndex !== -1)
991
+ taskQueue.splice(queueIndex, 1);
992
+ runningTasks.delete(msg.taskId);
993
+ taskPriority.delete(msg.taskId);
994
+ }
995
+ else {
779
996
  for (const controller of taskControllers.values())
780
997
  controller.abort();
998
+ taskQueue.length = 0;
999
+ runningTasks.clear();
1000
+ taskPriority.clear();
1001
+ }
781
1002
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "stopped", message: "Agent stopped." }));
782
1003
  }
783
1004
  else if (msg.type === "runtime_error") {
@@ -962,8 +1183,28 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
962
1183
  }
963
1184
  else if (msg.type === "apply") {
964
1185
  try {
1186
+ // Create snapshot before applying changes
1187
+ const filePaths = [...new Set(msg.changes.map(c => c.filePath))];
1188
+ const parentSnapshot = (0, snapshot_1.getSnapshotByTaskId)(cwd, msg.taskId);
1189
+ // Create pre-apply snapshot
1190
+ const preSnapshot = (0, snapshot_1.createSnapshot)(cwd, msg.taskId, `Pre-apply: ${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"}`, filePaths, parentSnapshot?.id);
965
1191
  const snapshots = [];
966
1192
  const planned = new Map();
1193
+ // Check for conflicts before applying
1194
+ const expectedFiles = msg.changes.map(change => ({
1195
+ filePath: change.filePath,
1196
+ content: change.oldString || null,
1197
+ }));
1198
+ const conflicts = (0, snapshot_1.detectConflicts)(cwd, expectedFiles);
1199
+ if (conflicts.length > 0) {
1200
+ socket.send(JSON.stringify({
1201
+ type: "conflict_detected",
1202
+ taskId: msg.taskId,
1203
+ conflicts,
1204
+ message: `Conflict detected in ${conflicts.length} file${conflicts.length === 1 ? "" : "s"}. The source has changed since the suggestion was generated.`
1205
+ }));
1206
+ return;
1207
+ }
967
1208
  for (const change of msg.changes) {
968
1209
  const isNew = !change.oldString;
969
1210
  const filePath = resolveProposedFile(cwd, change.filePath, true);
@@ -994,8 +1235,31 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
994
1235
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
995
1236
  node_fs_1.default.writeFileSync(filePath, nextContent);
996
1237
  }
1238
+ // Create post-apply snapshot
1239
+ (0, snapshot_1.createSnapshot)(cwd, msg.taskId, `Applied: ${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"}`, filePaths, preSnapshot.id);
997
1240
  // Run build validation after applying changes
998
1241
  const validation = await validateBuild(cwd);
1242
+ if (!validation.passed) {
1243
+ // Build failed - automatically trigger retry with build error context
1244
+ const errorContext = validation.errors.slice(0, 10).join("\n");
1245
+ socket.send(JSON.stringify({
1246
+ type: "agent_status",
1247
+ taskId: msg.taskId,
1248
+ status: "working",
1249
+ message: `⚠️ Build validation failed. Attempting to fix build errors automatically...`,
1250
+ detail: `Build command: ${validation.command}\nErrors:\n${errorContext}${validation.errors.length > 10 ? `\n... and ${validation.errors.length - 10} more errors` : ""}`
1251
+ }));
1252
+ // Store context for retry
1253
+ buildErrorContext.set(msg.taskId, {
1254
+ errors: validation.errors,
1255
+ command: validation.command,
1256
+ originalChanges: msg.changes,
1257
+ retryCount: 0
1258
+ });
1259
+ // Trigger automatic build fix
1260
+ await triggerBuildFix(cwd, msg.taskId, validation.errors, validation.command, msg.changes, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
1261
+ return;
1262
+ }
999
1263
  socket.send(JSON.stringify({
1000
1264
  type: "applied",
1001
1265
  taskId: msg.taskId,
@@ -1023,7 +1287,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
1023
1287
  const refreshAttempts = reviewRefreshAttempts.get(msg.taskId) || 0;
1024
1288
  if (sourceChanged && editRequest && editConfig && refreshAttempts < 1) {
1025
1289
  reviewRefreshAttempts.set(msg.taskId, refreshAttempts + 1);
1026
- runEditReview(editRequest, editConfig, "The source changed. Refreshing the review against the current file…");
1290
+ enqueueTask(msg.taskId, editRequest, editConfig, 0);
1291
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "The source changed. Refreshing the review against the current file…" }));
1027
1292
  }
1028
1293
  else {
1029
1294
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message }));
@@ -1044,9 +1309,117 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
1044
1309
  taskSnapshots.delete(msg.taskId || "");
1045
1310
  socket.send(JSON.stringify({ type: "undone", taskId: msg.taskId, message: snapshots.length ? "The accepted change was reverted." : "There is no accepted change to undo." }));
1046
1311
  }
1312
+ else if (msg.type === "resolve_conflict") {
1313
+ const { taskId, action, conflicts } = msg;
1314
+ if (action === "retry") {
1315
+ // Re-run the edit request with current file content
1316
+ const editRequest = editRequests.get(taskId);
1317
+ const editConfig = editConfigs.get(taskId);
1318
+ if (editRequest && editConfig) {
1319
+ reviewRefreshAttempts.set(taskId, 0);
1320
+ enqueueTask(taskId, editRequest, editConfig, 0);
1321
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "working", message: "Retrying with current file content..." }));
1322
+ }
1323
+ else {
1324
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: "Could not retry: original request not found." }));
1325
+ }
1326
+ }
1327
+ else if (action === "force") {
1328
+ // Force apply despite conflicts (user's choice)
1329
+ try {
1330
+ const snapshots = [];
1331
+ const planned = new Map();
1332
+ // Get the original changes from the edit request
1333
+ const editRequest = editRequests.get(taskId);
1334
+ if (!editRequest) {
1335
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: "Could not force apply: original request not found." }));
1336
+ return;
1337
+ }
1338
+ // Re-prepare changes with current content (may fail, but that's expected)
1339
+ for (const change of editRequest.changesHistory?.[editRequest.changesHistory.length - 1]?.changes || []) {
1340
+ const isNew = !change.oldString;
1341
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
1342
+ const exists = node_fs_1.default.existsSync(filePath);
1343
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
1344
+ const prepared = (!exists || isNew)
1345
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
1346
+ : prepareChange(content, change);
1347
+ const fileChanges = planned.get(filePath) || [];
1348
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
1349
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
1350
+ }
1351
+ fileChanges.push({ filePath, content, exists, ...prepared });
1352
+ planned.set(filePath, fileChanges);
1353
+ }
1354
+ taskSnapshots.set(taskId, snapshots);
1355
+ for (const [filePath, changes] of planned) {
1356
+ const exists = changes[0].exists;
1357
+ const content = changes[0].content;
1358
+ snapshots.push({ filePath, content: exists ? content : null });
1359
+ const nextContent = exists
1360
+ ? [...changes]
1361
+ .sort((a, b) => b.start - a.start)
1362
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
1363
+ : changes[0].newString;
1364
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
1365
+ node_fs_1.default.writeFileSync(filePath, nextContent);
1366
+ }
1367
+ socket.send(JSON.stringify({
1368
+ type: "applied",
1369
+ taskId,
1370
+ message: "Changes force-applied despite conflicts."
1371
+ }));
1372
+ }
1373
+ catch (error) {
1374
+ for (const snapshot of taskSnapshots.get(taskId) || []) {
1375
+ if (snapshot.content === null) {
1376
+ if (node_fs_1.default.existsSync(snapshot.filePath))
1377
+ node_fs_1.default.unlinkSync(snapshot.filePath);
1378
+ }
1379
+ else {
1380
+ node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
1381
+ }
1382
+ }
1383
+ taskSnapshots.delete(taskId);
1384
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "Force apply failed." }));
1385
+ }
1386
+ }
1387
+ else if (action === "rebase") {
1388
+ // User wants to rebase - re-run the edit with updated context
1389
+ const editRequest = editRequests.get(taskId);
1390
+ const editConfig = editConfigs.get(taskId);
1391
+ if (editRequest && editConfig) {
1392
+ reviewRefreshAttempts.set(taskId, 0);
1393
+ enqueueTask(taskId, editRequest, editConfig, 0);
1394
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "working", message: "Rebasing changes against current file content..." }));
1395
+ }
1396
+ else {
1397
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: "Could not rebase: original request not found." }));
1398
+ }
1399
+ }
1400
+ }
1401
+ else if (msg.type === "restore_snapshot") {
1402
+ const success = (0, snapshot_1.restoreSnapshot)(cwd, msg.snapshotId);
1403
+ socket.send(JSON.stringify({
1404
+ type: "agent_status",
1405
+ message: success ? "Snapshot restored successfully." : "Failed to restore snapshot."
1406
+ }));
1407
+ }
1408
+ else if (msg.type === "get_timeline") {
1409
+ const timeline = (0, snapshot_1.getTimeline)(cwd);
1410
+ socket.send(JSON.stringify({ type: "timeline", snapshots: timeline }));
1411
+ }
1412
+ else if (msg.type === "delete_snapshot") {
1413
+ const success = (0, snapshot_1.deleteSnapshot)(cwd, msg.snapshotId);
1414
+ socket.send(JSON.stringify({
1415
+ type: "agent_status",
1416
+ message: success ? "Snapshot deleted." : "Failed to delete snapshot."
1417
+ }));
1418
+ }
1047
1419
  });
1048
1420
  socket.on("close", () => {
1049
1421
  overlaySocket = null;
1422
+ currentSocket = null;
1050
1423
  console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
1051
1424
  });
1052
1425
  });
@@ -1063,8 +1436,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
1063
1436
  });
1064
1437
  bridgeServer.listen(bridgePort, "127.0.0.1");
1065
1438
  function send(msg) {
1066
- if (overlaySocket?.readyState === overlaySocket?.OPEN) {
1067
- overlaySocket.send(JSON.stringify(msg));
1439
+ const socket = currentSocket;
1440
+ if (socket && socket.readyState === socket.OPEN) {
1441
+ socket.send(JSON.stringify(msg));
1068
1442
  }
1069
1443
  }
1070
1444
  return {