@schoolai/shipyard 3.1.1 → 3.2.0-rc.20260416.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.
@@ -37,7 +37,7 @@ import {
37
37
  VaultKeyPutRequestSchema,
38
38
  VaultKeyPutResponseSchema,
39
39
  classifyClaudeCodeCompatibility
40
- } from "./chunk-ERKQ6CBC.js";
40
+ } from "./chunk-2RV2XXAI.js";
41
41
  import {
42
42
  loadAuthToken
43
43
  } from "./chunk-IHSXN66C.js";
@@ -24345,7 +24345,8 @@ var DiffHunkAnnotationSchema = BaseCommentZodSchema.extend({
24345
24345
  lineNumber: external_exports.number(),
24346
24346
  lineContent: external_exports.string(),
24347
24347
  lineContentHash: external_exports.string(),
24348
- side: external_exports.enum(["original", "modified"])
24348
+ side: external_exports.enum(["original", "modified"]),
24349
+ diffScope: external_exports.enum(["working-tree", "branch", "last-turn", "pr"]).optional()
24349
24350
  });
24350
24351
  var PreviewElementAnnotationSchema = BaseCommentZodSchema.extend({
24351
24352
  annotationType: external_exports.literal("preview-element"),
@@ -24590,6 +24591,7 @@ var CrdtCompactor = class {
24590
24591
  };
24591
24592
  var PLAN_EPOCH = 3;
24592
24593
  var CANVAS_EPOCH = 2;
24594
+ var OBSOLETE_PREFIXES = /* @__PURE__ */ new Set(["typing", "task-view"]);
24593
24595
  var DOCUMENT_PREFIXES = ["plan", "canvas"];
24594
24596
  var PREFIX_SET = new Set(DOCUMENT_PREFIXES);
24595
24597
  function isDocumentPrefix(value) {
@@ -26709,6 +26711,17 @@ var BrowserToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
26709
26711
  external_exports.object({
26710
26712
  type: external_exports.literal("git_ls_files"),
26711
26713
  requestId: external_exports.string()
26714
+ }),
26715
+ external_exports.object({
26716
+ type: external_exports.literal("git_branch_diff_files"),
26717
+ requestId: external_exports.string(),
26718
+ baseRef: external_exports.string().optional()
26719
+ }),
26720
+ external_exports.object({
26721
+ type: external_exports.literal("git_branch_diff_file"),
26722
+ requestId: external_exports.string(),
26723
+ path: external_exports.string(),
26724
+ mergeBase: external_exports.string()
26712
26725
  })
26713
26726
  ]);
26714
26727
  var FileChangeSchema = external_exports.object({
@@ -26770,6 +26783,19 @@ var DaemonToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
26770
26783
  requestId: external_exports.string(),
26771
26784
  files: external_exports.array(external_exports.string())
26772
26785
  }),
26786
+ external_exports.object({
26787
+ type: external_exports.literal("git_branch_diff_files_result"),
26788
+ requestId: external_exports.string(),
26789
+ files: external_exports.array(CheckpointFileEntrySchema),
26790
+ mergeBase: external_exports.string(),
26791
+ baseRef: external_exports.string()
26792
+ }),
26793
+ external_exports.object({
26794
+ type: external_exports.literal("git_branch_diff_file_result"),
26795
+ requestId: external_exports.string(),
26796
+ originalContent: external_exports.string(),
26797
+ modifiedContent: external_exports.string()
26798
+ }),
26773
26799
  external_exports.object({ type: external_exports.literal("error"), requestId: external_exports.string(), error: external_exports.string() }),
26774
26800
  external_exports.object({
26775
26801
  type: external_exports.literal("checkpoint_ready"),
@@ -28754,6 +28780,35 @@ async function rerunChecks(cwd, prNumber) {
28754
28780
  }
28755
28781
  }
28756
28782
 
28783
+ // src/shared/capabilities/git-diff.ts
28784
+ async function getDefaultBranch(cwd) {
28785
+ try {
28786
+ const ref = await runWithTimeout(
28787
+ "git",
28788
+ ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
28789
+ cwd,
28790
+ TIMEOUT_MS
28791
+ );
28792
+ return ref || null;
28793
+ } catch {
28794
+ }
28795
+ for (const candidate of ["origin/main", "origin/master"]) {
28796
+ try {
28797
+ await runWithTimeout("git", ["rev-parse", "--verify", candidate], cwd, TIMEOUT_MS);
28798
+ return candidate;
28799
+ } catch {
28800
+ }
28801
+ }
28802
+ return null;
28803
+ }
28804
+ async function getMergeBase(cwd, baseBranch) {
28805
+ try {
28806
+ return await runWithTimeout("git", ["merge-base", baseBranch, "HEAD"], cwd, TIMEOUT_MS);
28807
+ } catch {
28808
+ return null;
28809
+ }
28810
+ }
28811
+
28757
28812
  // src/shared/capabilities/index.ts
28758
28813
  var AutoModeConfigSchema = external_exports.object({
28759
28814
  cachedGrowthBookFeatures: external_exports.object({ tengu_auto_mode_config: external_exports.object({ enabled: external_exports.string() }) }).passthrough()
@@ -29215,6 +29270,28 @@ function narrow(value) {
29215
29270
  import { unlinkSync } from "fs";
29216
29271
  import { readFile as readFile5, unlink as unlink2, writeFile as writeFile3 } from "fs/promises";
29217
29272
  import { join as join8 } from "path";
29273
+
29274
+ // src/services/bootstrap/classify-uncaught-error.ts
29275
+ function classifyUncaughtError(error2) {
29276
+ if (!(error2 instanceof Error)) return "fatal";
29277
+ const code2 = "code" in error2 && typeof error2.code === "string" ? error2.code : void 0;
29278
+ const msg = error2.message;
29279
+ if (code2 === "EPIPE" || code2 === "ECONNRESET" || code2 === "ECONNABORTED" || code2 === "ECONNREFUSED" || code2 === "ERR_STREAM_DESTROYED") {
29280
+ return "recoverable";
29281
+ }
29282
+ if (msg.includes("can't add channel") && msg.includes("stopped")) {
29283
+ return "recoverable";
29284
+ }
29285
+ if (msg.includes("RTCDataChannel.readyState is not")) {
29286
+ return "recoverable";
29287
+ }
29288
+ if (msg.includes("ProcessTransport is not ready")) {
29289
+ return "recoverable";
29290
+ }
29291
+ return "fatal";
29292
+ }
29293
+
29294
+ // src/services/bootstrap/lifecycle.ts
29218
29295
  function isProcessAlive(pid) {
29219
29296
  try {
29220
29297
  process.kill(pid, 0);
@@ -29244,7 +29321,12 @@ var LifecycleManager = class {
29244
29321
  process.on("SIGTERM", termHandler);
29245
29322
  process.on("SIGINT", intHandler);
29246
29323
  const exceptionHandler = (error2) => {
29247
- this.#log.error({ err: error2 }, "Uncaught exception \u2014 initiating shutdown");
29324
+ const severity = classifyUncaughtError(error2);
29325
+ if (severity === "recoverable") {
29326
+ this.#log.warn({ err: error2 }, "Recoverable uncaught exception \u2014 absorbed");
29327
+ return;
29328
+ }
29329
+ this.#log.error({ err: error2 }, "Fatal uncaught exception \u2014 initiating shutdown");
29248
29330
  void this.#shutdown("uncaughtException");
29249
29331
  };
29250
29332
  const rejectionHandler = (reason) => {
@@ -29588,7 +29670,7 @@ function nanoid(size2 = 21) {
29588
29670
  }
29589
29671
 
29590
29672
  // src/services/bootstrap/signaling.ts
29591
- var DAEMON_NPM_VERSION = true ? "3.1.1" : "unknown";
29673
+ var DAEMON_NPM_VERSION = true ? "3.2.0" : "unknown";
29592
29674
  function createDaemonSignaling(config2) {
29593
29675
  const agentId = config2.agentId ?? nanoid();
29594
29676
  function send(msg) {
@@ -29743,6 +29825,7 @@ function handleBrowserPreviewChannel(port, send, sendBinary, log, options) {
29743
29825
  const requestTimeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
29744
29826
  const activeRequests = /* @__PURE__ */ new Map();
29745
29827
  const cookieJar = /* @__PURE__ */ new Map();
29828
+ const previewPrefix = `/__preview/url/${encodeURIComponent(`http://localhost:${port}`)}/`;
29746
29829
  function respond(msg) {
29747
29830
  send(JSON.stringify(msg));
29748
29831
  }
@@ -29896,7 +29979,7 @@ function handleBrowserPreviewChannel(port, send, sendBinary, log, options) {
29896
29979
  res.on("data", (chunk) => chunks.push(chunk));
29897
29980
  res.on("end", () => {
29898
29981
  const rawJs = Buffer.concat(chunks).toString("utf-8");
29899
- const rewritten = rewriteJsImportPaths(rawJs);
29982
+ const rewritten = rewriteJsImportPaths(rawJs, previewPrefix);
29900
29983
  const body = new TextEncoder().encode(rewritten);
29901
29984
  headers["content-length"] = String(body.byteLength);
29902
29985
  respond({
@@ -30019,8 +30102,9 @@ function injectBaseHref(html, origin) {
30019
30102
  function rewriteRootRelativePaths(html) {
30020
30103
  return html.replace(/((?:src|href|action)\s*=\s*["'])\/(?!\/)/gi, "$1./");
30021
30104
  }
30022
- function rewriteJsImportPaths(js) {
30023
- return js.replace(/((?:from|import)\s*\(\s*["'])\/(?!\/)/g, "$1./").replace(/(from\s+["'])\/(?!\/)/g, "$1./");
30105
+ function rewriteJsImportPaths(js, previewPrefix) {
30106
+ const replacement = previewPrefix ?? "./";
30107
+ return js.replace(/((?:from|import)\s*\(\s*["'])\/(?!\/)/g, `$1${replacement}`).replace(/(from\s+["'])\/(?!\/)/g, `$1${replacement}`);
30024
30108
  }
30025
30109
  function handleBrowserPreviewUrlChannel(send, sendBinary, log, options) {
30026
30110
  const requestTimeoutMs = options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
@@ -30113,7 +30197,10 @@ function handleBrowserPreviewUrlChannel(send, sendBinary, log, options) {
30113
30197
  injectBaseHref(new TextDecoder().decode(rawBody), targetUrl.origin)
30114
30198
  );
30115
30199
  } else if (isJs) {
30116
- body = new TextEncoder().encode(rewriteJsImportPaths(new TextDecoder().decode(rawBody)));
30200
+ const urlPrefix = `/__preview/url/${encodeURIComponent(targetUrl.origin)}/`;
30201
+ body = new TextEncoder().encode(
30202
+ rewriteJsImportPaths(new TextDecoder().decode(rawBody), urlPrefix)
30203
+ );
30117
30204
  } else {
30118
30205
  body = rawBody;
30119
30206
  }
@@ -32301,8 +32388,7 @@ async function sweepStaleTasks(taskStateStore, taskManager, log) {
32301
32388
  const noBroadcast = { broadcast: false };
32302
32389
  for (const action of actions) {
32303
32390
  if (action.kind === "mark_input_required") {
32304
- await taskStateStore.updateTaskStatus(action.taskId, "input_required", noBroadcast);
32305
- await taskStateStore.acknowledge(action.taskId, noBroadcast);
32391
+ await taskStateStore.sweepToInputRequired(action.taskId, noBroadcast);
32306
32392
  log({
32307
32393
  event: "stale_task_swept",
32308
32394
  taskId: action.taskId,
@@ -43227,6 +43313,11 @@ Use \`add_comment\` to annotate your work and respond to reviewer feedback. Use
43227
43313
 
43228
43314
  You can **create** comments on diff and plan surfaces only (\`mode: "diff"\` and \`mode: "plan"\`). You can **reply** to comments on all 5 surfaces. Preview and canvas comments are user-originated feedback on your running app or visualizations \u2014 respond by replying to acknowledge and acting on the feedback.
43229
43315
 
43316
+ **Diff scope** \u2014 \`diff\` surface comments may include a \`diff-scope\` attribute indicating which view the comment was made on:
43317
+ - \`working-tree\` \u2014 uncommitted changes (your latest edits, like \`git diff\`)
43318
+ - \`branch\` \u2014 full branch diff vs base branch (all commits + uncommitted, like \`git diff main...HEAD\`)
43319
+ - \`last-turn\` \u2014 changes from a specific agent turn
43320
+
43230
43321
  **When publishing plans** \u2014 \`mode: "plan"\` for non-obvious design decisions, anchored to specific plan text.
43231
43322
 
43232
43323
  **Periodically during long tasks** \u2014 \`list_comments\` to catch missed feedback, especially after context loss.
@@ -75126,8 +75217,10 @@ function deriveArtifact(annotation, taskId) {
75126
75217
  }
75127
75218
  function surfaceAttrs(annotation) {
75128
75219
  switch (annotation.annotationType) {
75129
- case "diff-hunk":
75130
- return ` file="${escapeXmlAttr(annotation.filePath)}" line="${annotation.lineNumber}"`;
75220
+ case "diff-hunk": {
75221
+ const scopeAttr = annotation.diffScope ? ` diff-scope="${escapeXmlAttr(annotation.diffScope)}"` : "";
75222
+ return ` file="${escapeXmlAttr(annotation.filePath)}" line="${annotation.lineNumber}"${scopeAttr}`;
75223
+ }
75131
75224
  case "plan-text":
75132
75225
  return annotation.anchorText ? ` anchor="${escapeXmlAttr(annotation.anchorText)}"` : "";
75133
75226
  case "preview-element":
@@ -76776,8 +76869,6 @@ ${conversationReplay}` : conversationReplay;
76776
76869
  });
76777
76870
  this.#ownSessionId = null;
76778
76871
  this.#effectiveSpawnMode = { kind: "fresh" };
76779
- this.#config.sessionPersistence.clear(this.#config.channelId).catch(() => {
76780
- });
76781
76872
  }
76782
76873
  if (this.#tryForkFailedRetry(error2)) return;
76783
76874
  if (this.#tryStaleResumeRetry(error2)) return;
@@ -79671,7 +79762,7 @@ var SideThreadRegistry = class {
79671
79762
  };
79672
79763
  },
79673
79764
  onSubprocessEvent: (event) => {
79674
- if (event.type === "turn_complete") {
79765
+ if (event.type === "turn_complete" || event.type === "sdk_error" || event.type === "subprocess_died") {
79675
79766
  this.#deps.onThreadTurnComplete(params.threadId);
79676
79767
  }
79677
79768
  }
@@ -80354,7 +80445,7 @@ var StructuredTaskTracker = class {
80354
80445
  * Attach a file watcher for CC task files. Called after init_received
80355
80446
  * when we know the CC session ID, or immediately for resumed sessions.
80356
80447
  */
80357
- attachFileWatcher(sessionId) {
80448
+ attachFileWatcher(sessionId, options) {
80358
80449
  if (this.#ccTaskWatcherDispose) {
80359
80450
  this.#deps.log({
80360
80451
  event: "file_watcher_already_attached",
@@ -80365,17 +80456,17 @@ var StructuredTaskTracker = class {
80365
80456
  }
80366
80457
  const watcher = createCCTaskFileWatcher(sessionId, this.#deps.log);
80367
80458
  this.#ccTaskFileWriter = createCCTaskFileWriter(watcher.dir, this.#deps.log);
80368
- this.#initFileWatcher(watcher);
80459
+ this.#initFileWatcher(watcher, options);
80369
80460
  if (this.#currentOverlay) {
80370
- this.#flushStructuredTasks();
80461
+ this.#flushStructuredTasks(options);
80371
80462
  }
80372
80463
  }
80373
80464
  /**
80374
80465
  * Apply an overlay on top of CC tasks. Stores the overlay and re-flushes.
80375
80466
  */
80376
- applyOverlay(overlay) {
80467
+ applyOverlay(overlay, options) {
80377
80468
  this.#currentOverlay = overlay;
80378
- this.#flushStructuredTasks();
80469
+ this.#flushStructuredTasks(options);
80379
80470
  }
80380
80471
  processStructuredTaskEvents(content) {
80381
80472
  const events = extractStructuredTaskEvents(content);
@@ -80500,11 +80591,11 @@ var StructuredTaskTracker = class {
80500
80591
  applyDepEdgeAdditions(merged, overlay.depEdges);
80501
80592
  return merged;
80502
80593
  }
80503
- #flushStructuredTasks() {
80594
+ #flushStructuredTasks(options) {
80504
80595
  const overlay = this.#currentOverlay ?? DEFAULT_TASK_OVERLAY;
80505
80596
  const merged = this.#applyOverlayToMap(this.#structuredTasks, overlay);
80506
80597
  const todoProgress = this.#computeTodoProgress(merged);
80507
- this.#deps.updateStructuredTasks(Object.fromEntries(merged), todoProgress);
80598
+ this.#deps.updateStructuredTasks(Object.fromEntries(merged), todoProgress, options);
80508
80599
  if (!this.#suppressWriteThrough) {
80509
80600
  this.#ccTaskFileWriter?.writeMergedTasks(merged);
80510
80601
  }
@@ -80525,9 +80616,12 @@ var StructuredTaskTracker = class {
80525
80616
  currentActivity
80526
80617
  };
80527
80618
  }
80528
- #initFileWatcher(watcher) {
80619
+ #initFileWatcher(watcher, initOptions) {
80620
+ let isFirstRead = !!initOptions?.preserveUpdatedAt;
80529
80621
  this.#ccTaskWatcherDispose = watcher.watch((tasks) => {
80530
- this.#reconcileFromDisk(tasks);
80622
+ const opts = isFirstRead ? initOptions : void 0;
80623
+ isFirstRead = false;
80624
+ this.#reconcileFromDisk(tasks, opts);
80531
80625
  });
80532
80626
  }
80533
80627
  /**
@@ -80537,7 +80631,7 @@ var StructuredTaskTracker = class {
80537
80631
  * - Remove tasks that were previously from disk but are now gone
80538
80632
  * - Re-flush with overlay applied
80539
80633
  */
80540
- #reconcileFromDisk(ccTasks) {
80634
+ #reconcileFromDisk(ccTasks, options) {
80541
80635
  const currentDiskIds = /* @__PURE__ */ new Set();
80542
80636
  for (const file of ccTasks) {
80543
80637
  currentDiskIds.add(file.id);
@@ -80558,7 +80652,7 @@ var StructuredTaskTracker = class {
80558
80652
  }
80559
80653
  this.#suppressWriteThrough = true;
80560
80654
  try {
80561
- this.#flushStructuredTasks();
80655
+ this.#flushStructuredTasks(options);
80562
80656
  } finally {
80563
80657
  this.#suppressWriteThrough = false;
80564
80658
  }
@@ -81296,7 +81390,9 @@ var Task = class _Task {
81296
81390
  log: deps.log
81297
81391
  });
81298
81392
  if (deps.existingSessionId) {
81299
- this.#structuredTaskTracker.attachFileWatcher(deps.existingSessionId);
81393
+ this.#structuredTaskTracker.attachFileWatcher(deps.existingSessionId, {
81394
+ preserveUpdatedAt: true
81395
+ });
81300
81396
  }
81301
81397
  this.#subagentManager = new SubagentManager({
81302
81398
  taskId: deps.taskId,
@@ -82424,6 +82520,8 @@ Use this context to maintain continuity. You have already done this work \u2014
82424
82520
  }
82425
82521
  }
82426
82522
  #handleSdkError(event) {
82523
+ this.#collabQueue.onTurnComplete();
82524
+ this.#pushManager.onTurnEnd();
82427
82525
  this.#deps.metricsCollector.capture("agent_error", {
82428
82526
  taskId: this.#deps.taskId,
82429
82527
  errorType: event.errorSubtype ?? "sdk_error",
@@ -82443,6 +82541,8 @@ Use this context to maintain continuity. You have already done this work \u2014
82443
82541
  }
82444
82542
  }
82445
82543
  #handleSubprocessDied() {
82544
+ this.#collabQueue.onTurnComplete();
82545
+ this.#pushManager.onTurnEnd();
82446
82546
  this.#mcpPoller.stop();
82447
82547
  if (this.#lastTurnResult != null) {
82448
82548
  this.#costBaseline = {
@@ -82524,8 +82624,8 @@ Use this context to maintain continuity. You have already done this work \u2014
82524
82624
  }
82525
82625
  }
82526
82626
  }
82527
- applyOverlay(overlay) {
82528
- this.#structuredTaskTracker.applyOverlay(overlay);
82627
+ applyOverlay(overlay, options) {
82628
+ this.#structuredTaskTracker.applyOverlay(overlay, options);
82529
82629
  }
82530
82630
  /** ---------------------------------------------------------------- */
82531
82631
  /** Resource resolution */
@@ -83202,7 +83302,8 @@ var TaskManager = class {
83202
83302
  this.#tasks.set(taskId, { taskId, channelId, cwd, mode, orchestrator });
83203
83303
  this.#flushPendingStreamSubs(taskId, orchestrator);
83204
83304
  this.#deps.taskStateStore.getTask(taskId).then((record) => {
83205
- if (record?.taskOverlay) orchestrator.applyOverlay(record.taskOverlay);
83305
+ if (record?.taskOverlay)
83306
+ orchestrator.applyOverlay(record.taskOverlay, { preserveUpdatedAt: true });
83206
83307
  }).catch((err) => {
83207
83308
  this.#deps.log({
83208
83309
  event: "overlay_restore_failed",
@@ -83244,10 +83345,27 @@ var TaskManager = class {
83244
83345
  getTaskMode(taskId) {
83245
83346
  return this.#tasks.get(taskId)?.mode ?? "task";
83246
83347
  }
83247
- handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName) {
83248
- const task = this.#tasks.get(taskId);
83348
+ async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName) {
83349
+ let task = this.#tasks.get(taskId);
83249
83350
  if (!task) {
83250
- throw new Error(`No orchestrator for task ${taskId}`);
83351
+ const record = await this.#deps.taskStateStore.getTask(taskId);
83352
+ if (!record) {
83353
+ throw new Error(`No orchestrator for task ${taskId}`);
83354
+ }
83355
+ this.restoreOrchestrator(taskId, record.channelId, {
83356
+ initialState: "cold_idle",
83357
+ cwd: record.cwd,
83358
+ mode: record.mode,
83359
+ costBaseline: {
83360
+ totalCostUsd: record.totalCostUsd,
83361
+ totalOutputTokens: record.totalOutputTokens
83362
+ }
83363
+ });
83364
+ task = this.#tasks.get(taskId);
83365
+ if (!task) {
83366
+ throw new Error(`Failed to restore orchestrator for task ${taskId}`);
83367
+ }
83368
+ this.#deps.log({ event: "task_lazy_restored", taskId, channelId: record.channelId });
83251
83369
  }
83252
83370
  if (cwd && cwd !== task.cwd) {
83253
83371
  task.cwd = cwd;
@@ -83567,8 +83685,13 @@ var TaskManager = class {
83567
83685
  });
83568
83686
  });
83569
83687
  },
83570
- updateStructuredTasks: (tasks, todoProgress) => {
83571
- this.#deps.taskStateStore.updateStructuredTasks(taskId, tasks, todoProgress).catch((err) => {
83688
+ updateStructuredTasks: (tasks, todoProgress, options) => {
83689
+ this.#deps.taskStateStore.updateStructuredTasks(
83690
+ taskId,
83691
+ tasks,
83692
+ todoProgress,
83693
+ options ? { preserveUpdatedAt: options.preserveUpdatedAt } : void 0
83694
+ ).catch((err) => {
83572
83695
  this.#deps.log({
83573
83696
  event: "task_state_store_structured_tasks_failed",
83574
83697
  taskId,
@@ -83715,52 +83838,29 @@ function buildTaskStateStore(dataDir) {
83715
83838
  });
83716
83839
  },
83717
83840
  async updateTaskStatus(taskId, status, options) {
83718
- const task = await store.get(taskId);
83719
- if (!task) return;
83720
- pushBroadcast(options);
83721
- await store.set(taskId, applyStatusTransition(task, status, Date.now()));
83841
+ await safeUpdate(taskId, (task) => applyStatusTransition(task, status, Date.now()), options);
83722
83842
  },
83723
83843
  async updateTitle(taskId, title, options) {
83724
- const task = await store.get(taskId);
83725
- if (!task) return;
83726
- pushBroadcast(options);
83727
- await store.set(taskId, {
83728
- ...task,
83729
- title,
83730
- updatedAt: Date.now()
83731
- });
83844
+ await safeUpdate(taskId, (task) => ({ ...task, title, updatedAt: Date.now() }), options);
83732
83845
  },
83733
83846
  async updateCwd(taskId, cwd, options) {
83734
- const task = await store.get(taskId);
83735
- if (!task) return;
83736
- pushBroadcast(options);
83737
- await store.set(taskId, {
83738
- ...task,
83739
- cwd,
83740
- updatedAt: Date.now()
83741
- });
83847
+ await safeUpdate(taskId, (task) => ({ ...task, cwd, updatedAt: Date.now() }), options);
83742
83848
  },
83743
83849
  async updateMode(taskId, mode, options) {
83744
- const task = await store.get(taskId);
83745
- if (!task) return;
83746
- pushBroadcast(options);
83747
- await store.set(taskId, {
83748
- ...task,
83749
- mode,
83750
- updatedAt: Date.now()
83751
- });
83850
+ await safeUpdate(taskId, (task) => ({ ...task, mode, updatedAt: Date.now() }), options);
83752
83851
  },
83753
83852
  async updateTodoProgress(taskId, progress, options) {
83754
- const task = await store.get(taskId);
83755
- if (!task) return;
83756
- pushBroadcast(options);
83757
- await store.set(taskId, {
83758
- ...task,
83759
- todoCompleted: progress.todoCompleted,
83760
- todoTotal: progress.todoTotal,
83761
- currentActivity: progress.currentActivity,
83762
- updatedAt: Date.now()
83763
- });
83853
+ await safeUpdate(
83854
+ taskId,
83855
+ (task) => ({
83856
+ ...task,
83857
+ todoCompleted: progress.todoCompleted,
83858
+ todoTotal: progress.todoTotal,
83859
+ currentActivity: progress.currentActivity,
83860
+ updatedAt: Date.now()
83861
+ }),
83862
+ options
83863
+ );
83764
83864
  },
83765
83865
  async acknowledge(taskId, options) {
83766
83866
  await safeUpdate(taskId, (task) => ({ ...task, acknowledgedAt: Date.now() }), options);
@@ -83769,39 +83869,42 @@ function buildTaskStateStore(dataDir) {
83769
83869
  await safeUpdate(taskId, (task) => ({ ...task, pinned: !task.pinned }), options);
83770
83870
  },
83771
83871
  async updateStructuredTasks(taskId, tasks, todoProgress, options) {
83772
- const task = await store.get(taskId);
83773
- if (!task) return;
83774
- pushBroadcast(options);
83775
- await store.set(taskId, {
83776
- ...task,
83777
- structuredTasks: tasks,
83778
- ...todoProgress && {
83779
- todoCompleted: todoProgress.todoCompleted,
83780
- todoTotal: todoProgress.todoTotal,
83781
- currentActivity: todoProgress.currentActivity
83782
- },
83783
- updatedAt: Date.now()
83784
- });
83872
+ await safeUpdate(
83873
+ taskId,
83874
+ (task) => ({
83875
+ ...task,
83876
+ structuredTasks: tasks,
83877
+ ...todoProgress && {
83878
+ todoCompleted: todoProgress.todoCompleted,
83879
+ todoTotal: todoProgress.todoTotal,
83880
+ currentActivity: todoProgress.currentActivity
83881
+ },
83882
+ updatedAt: options?.preserveUpdatedAt ? task.updatedAt : Date.now()
83883
+ }),
83884
+ options
83885
+ );
83785
83886
  },
83786
83887
  async updateTaskOverlay(taskId, overlay, options) {
83787
- const task = await store.get(taskId);
83788
- if (!task) return;
83789
- pushBroadcast(options);
83790
- await store.set(taskId, {
83791
- ...task,
83792
- taskOverlay: overlay,
83793
- updatedAt: Date.now()
83794
- });
83888
+ await safeUpdate(
83889
+ taskId,
83890
+ (task) => ({
83891
+ ...task,
83892
+ taskOverlay: overlay,
83893
+ updatedAt: options?.preserveUpdatedAt ? task.updatedAt : Date.now()
83894
+ }),
83895
+ options
83896
+ );
83795
83897
  },
83796
83898
  async updateComposerSettings(taskId, settings, options) {
83797
- const task = await store.get(taskId);
83798
- if (!task) return;
83799
- pushBroadcast(options);
83800
- await store.set(taskId, {
83801
- ...task,
83802
- composerSettings: { ...task.composerSettings, ...settings },
83803
- updatedAt: Date.now()
83804
- });
83899
+ await safeUpdate(
83900
+ taskId,
83901
+ (task) => ({
83902
+ ...task,
83903
+ composerSettings: { ...task.composerSettings, ...settings },
83904
+ updatedAt: Date.now()
83905
+ }),
83906
+ options
83907
+ );
83805
83908
  },
83806
83909
  async updateCostStats(taskId, stats, options) {
83807
83910
  await safeUpdate(
@@ -83827,12 +83930,27 @@ function buildTaskStateStore(dataDir) {
83827
83930
  options
83828
83931
  );
83829
83932
  },
83933
+ async sweepToInputRequired(taskId, options) {
83934
+ await safeUpdate(
83935
+ taskId,
83936
+ (task) => ({
83937
+ ...task,
83938
+ status: "input_required",
83939
+ taskStartedAt: null
83940
+ }),
83941
+ options
83942
+ );
83943
+ },
83830
83944
  async getTask(taskId) {
83831
83945
  return store.get(taskId);
83832
83946
  },
83833
83947
  async listTasks() {
83834
83948
  return store.list();
83835
83949
  },
83950
+ async listTasksWithVersion() {
83951
+ const tasks = await store.list();
83952
+ return { tasks, version: _version };
83953
+ },
83836
83954
  subscribe(listener) {
83837
83955
  taskListeners.add(listener);
83838
83956
  return () => {
@@ -84145,13 +84263,15 @@ ${additionalSystemPrompt}` : basePrompt;
84145
84263
  async function detectInitialCapabilities(tokenStore, settings) {
84146
84264
  return detectCapabilities(tokenStore, settings.preferredAnthropicAuth ?? void 0);
84147
84265
  }
84148
- function applyProxyFiltering(merged, proxyRef, proxyManagedNames, disabledNames) {
84149
- for (const name of proxyManagedNames) {
84266
+ function applyProxyFiltering(merged, proxyRef, disabledNames) {
84267
+ const proxy = proxyRef.current;
84268
+ if (!proxy) return;
84269
+ for (const name of proxy.getAllManagedNames()) {
84150
84270
  if (name in merged) {
84151
84271
  delete merged[name];
84152
84272
  }
84153
84273
  }
84154
- const freshConfigs = proxyRef.current?.getServerConfigs() ?? /* @__PURE__ */ new Map();
84274
+ const freshConfigs = proxy.getServerConfigs();
84155
84275
  for (const [name, config2] of freshConfigs) {
84156
84276
  if (!disabledNames.has(name)) {
84157
84277
  merged[name] = config2;
@@ -84180,7 +84300,7 @@ function collectProxyEndpoints(capabilitiesRef, claudeAiToken) {
84180
84300
  }
84181
84301
  return endpoints;
84182
84302
  }
84183
- async function initializeProxyServer(deps, capabilitiesRef, proxyManagedNames, onReauthComplete) {
84303
+ async function initializeProxyServer(deps, capabilitiesRef, onReauthComplete) {
84184
84304
  const claudeAiToken = await readAnthropicOAuthToken();
84185
84305
  const proxyEndpoints = collectProxyEndpoints(capabilitiesRef, claudeAiToken ?? void 0);
84186
84306
  if (proxyEndpoints.size === 0) return { proxyServer: null };
@@ -84192,11 +84312,14 @@ async function initializeProxyServer(deps, capabilitiesRef, proxyManagedNames, o
84192
84312
  });
84193
84313
  try {
84194
84314
  await proxy.initialize(proxyEndpoints);
84195
- for (const name of proxyEndpoints.keys()) proxyManagedNames.add(name);
84315
+ const connectedNames = proxy.getServerNames();
84316
+ const allNames = [...proxyEndpoints.keys()];
84196
84317
  deps.log({
84197
84318
  event: "mcp_proxy_initialized",
84198
84319
  serverCount: proxyEndpoints.size,
84199
- serverNames: [...proxyEndpoints.keys()]
84320
+ connectedCount: connectedNames.length,
84321
+ connectedNames,
84322
+ failedNames: allNames.filter((n) => !connectedNames.includes(n))
84200
84323
  });
84201
84324
  return { proxyServer: proxy };
84202
84325
  } catch (err) {
@@ -84409,7 +84532,6 @@ async function createDaemon(deps) {
84409
84532
  initialContent
84410
84533
  );
84411
84534
  }
84412
- const proxyManagedNames = /* @__PURE__ */ new Set();
84413
84535
  const resolveMcpServers = () => {
84414
84536
  if (isVanillaAgentMode()) return void 0;
84415
84537
  const userServers = resolveUserMcpServers(capabilitiesRef.current, deps.tokenStore);
@@ -84417,9 +84539,11 @@ async function createDaemon(deps) {
84417
84539
  capabilitiesRef.current?.mcpServers?.filter((s2) => !s2.enabled).map((s2) => s2.name) ?? []
84418
84540
  );
84419
84541
  const pluginServers = resolvePluginMcpServersMap(deps.tokenStore, deps.log, disabledNames);
84420
- if (!userServers && pluginServers.size === 0 && proxyManagedNames.size === 0) return void 0;
84542
+ const proxyManagedNames = proxyRef.current?.getAllManagedNames() ?? [];
84543
+ if (!userServers && pluginServers.size === 0 && proxyManagedNames.length === 0)
84544
+ return void 0;
84421
84545
  const merged = mergePluginAndUserServers(pluginServers, userServers, deps.log);
84422
- applyProxyFiltering(merged, proxyRef, proxyManagedNames, disabledNames);
84546
+ applyProxyFiltering(merged, proxyRef, disabledNames);
84423
84547
  deps.log({
84424
84548
  event: "resolve_mcp_servers",
84425
84549
  capabilityServerCount: capabilitiesRef.current?.mcpServers?.length ?? 0,
@@ -84427,7 +84551,7 @@ async function createDaemon(deps) {
84427
84551
  userServerCount: userServers ? Object.keys(userServers).length : 0,
84428
84552
  userServerNames: userServers ? Object.keys(userServers) : [],
84429
84553
  pluginServerCount: pluginServers.size,
84430
- proxyManaged: [...proxyManagedNames],
84554
+ proxyManaged: proxyManagedNames,
84431
84555
  mergedCount: Object.keys(merged).length,
84432
84556
  mergedNames: Object.keys(merged)
84433
84557
  });
@@ -84493,7 +84617,13 @@ async function createDaemon(deps) {
84493
84617
  taskManager.createTask(params);
84494
84618
  },
84495
84619
  sendMessage: (taskId, content, settings, cwd) => {
84496
- taskManager.handleUserMessage(taskId, content, settings, cwd);
84620
+ taskManager.handleUserMessage(taskId, content, settings, cwd).catch((err) => {
84621
+ deps.log({
84622
+ event: "schedule_send_message_error",
84623
+ taskId,
84624
+ error: err instanceof Error ? err.message : String(err)
84625
+ });
84626
+ });
84497
84627
  },
84498
84628
  getRunningScheduleIds: async () => {
84499
84629
  const running = /* @__PURE__ */ new Set();
@@ -84652,7 +84782,6 @@ async function createDaemon(deps) {
84652
84782
  const { proxyServer } = await initializeProxyServer(
84653
84783
  deps,
84654
84784
  capabilitiesRef,
84655
- proxyManagedNames,
84656
84785
  () => oauthStateStore.invalidate()
84657
84786
  );
84658
84787
  proxyRef.current = proxyServer;
@@ -87109,7 +87238,10 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
87109
87238
  if (resolved) {
87110
87239
  const without = { ...resolved };
87111
87240
  delete without[serverName];
87112
- daemon.preWarmManager.updateMcpServers(without).then(() => daemon.preWarmManager.updateMcpServers(resolved)).then(() => daemon.taskManager.triggerFastMcpPolling()).catch((err) => {
87241
+ daemon.preWarmManager.updateMcpServers(without).then(() => daemon.preWarmManager.updateMcpServers(resolved)).then(() => {
87242
+ daemon.taskManager.reconnectMcpServer(serverName);
87243
+ daemon.taskManager.triggerFastMcpPolling();
87244
+ }).catch((err) => {
87113
87245
  logAdapter({
87114
87246
  event: `${eventName}_reconnect_failed`,
87115
87247
  serverName,
@@ -87328,12 +87460,11 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
87328
87460
  });
87329
87461
  },
87330
87462
  onRequestTaskIndex: () => {
87331
- const snapshotVersion = daemon.taskStateStore.version;
87332
- daemon.taskStateStore.listTasks().then((tasks) => {
87463
+ daemon.taskStateStore.listTasksWithVersion().then(({ tasks, version }) => {
87333
87464
  controlHandler.sendControl({
87334
87465
  type: "task_index_snapshot",
87335
87466
  tasks,
87336
- version: snapshotVersion
87467
+ version
87337
87468
  });
87338
87469
  }).catch((err) => {
87339
87470
  logAdapter({
@@ -87547,19 +87678,6 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
87547
87678
  }
87548
87679
  };
87549
87680
  pushMcpStatus(5);
87550
- const initialSnapshotVersion = daemon.taskStateStore.version;
87551
- daemon.taskStateStore.listTasks().then((tasks) => {
87552
- controlHandler.sendControl({
87553
- type: "task_index_snapshot",
87554
- tasks,
87555
- version: initialSnapshotVersion
87556
- });
87557
- }).catch((err) => {
87558
- logAdapter({
87559
- event: "task_index_initial_push_failed",
87560
- error: err instanceof Error ? err.message : String(err)
87561
- });
87562
- });
87563
87681
  daemon.userSettingsStore.getSettings().then((settings) => {
87564
87682
  controlHandler.sendControl({ type: "user_settings_snapshot", settings });
87565
87683
  }).catch((err) => {
@@ -87661,7 +87779,7 @@ function handleMessageChannel(opts) {
87661
87779
  }
87662
87780
  return result.data;
87663
87781
  }
87664
- function handleSendMessage2(msg) {
87782
+ async function handleSendMessage2(msg) {
87665
87783
  try {
87666
87784
  const settings = {
87667
87785
  model: msg.model,
@@ -87672,7 +87790,7 @@ function handleMessageChannel(opts) {
87672
87790
  if (opts.onUserMessage) {
87673
87791
  opts.onUserMessage(msg.content, settings, msg.cwd, participantId, senderDisplayName);
87674
87792
  } else {
87675
- taskManager.handleUserMessage(
87793
+ await taskManager.handleUserMessage(
87676
87794
  taskId,
87677
87795
  msg.content,
87678
87796
  settings,
@@ -87755,7 +87873,7 @@ function handleMessageChannel(opts) {
87755
87873
  }
87756
87874
  switch (msg.type) {
87757
87875
  case "send_message":
87758
- handleSendMessage2(msg);
87876
+ void handleSendMessage2(msg);
87759
87877
  break;
87760
87878
  case "stop":
87761
87879
  if (opts.onStop) opts.onStop();
@@ -88186,7 +88304,9 @@ function buildCollabPermissions(registry) {
88186
88304
  return {
88187
88305
  visibility: isAllowed,
88188
88306
  mutability: isAllowed,
88189
- creation(_docId, peer) {
88307
+ creation(docId, peer) {
88308
+ const colonIdx = docId.indexOf(":");
88309
+ if (colonIdx > 0 && OBSOLETE_PREFIXES.has(docId.slice(0, colonIdx))) return false;
88190
88310
  if (peer.channelKind === "storage") return true;
88191
88311
  if (peer.peerType === "service") return true;
88192
88312
  const entry = findEntry(registry, peer);
@@ -88259,15 +88379,15 @@ var LEGACY_PREFIXES = [
88259
88379
  "diff-review",
88260
88380
  "preview-annotation"
88261
88381
  ];
88262
- var OBSOLETE_PREFIXES = /* @__PURE__ */ new Set(["typing", "task-view"]);
88263
88382
  function isLegacyDocId(decoded) {
88264
88383
  if (decoded.startsWith("v2:")) return true;
88265
88384
  return LEGACY_PREFIXES.some((prefix) => decoded.startsWith(`${prefix}:`));
88266
88385
  }
88267
88386
  function shouldPrune(decoded, currentEpoch) {
88387
+ const firstColon = decoded.indexOf(":");
88388
+ if (firstColon > 0 && OBSOLETE_PREFIXES.has(decoded.slice(0, firstColon))) return true;
88268
88389
  const parsed = parseDocumentId(decoded);
88269
88390
  if (!parsed) return isLegacyDocId(decoded);
88270
- if (OBSOLETE_PREFIXES.has(parsed.prefix)) return true;
88271
88391
  return parsed.epoch < currentEpoch;
88272
88392
  }
88273
88393
  async function pruneOldEpochData(dataDir, currentEpoch, log) {
@@ -88445,6 +88565,12 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
88445
88565
  case "git_ls_files":
88446
88566
  handleGitLsFiles(msg.requestId);
88447
88567
  break;
88568
+ case "git_branch_diff_files":
88569
+ handleGitBranchDiffFiles(msg.requestId, msg.baseRef);
88570
+ break;
88571
+ case "git_branch_diff_file":
88572
+ handleGitBranchDiffFile(msg.requestId, msg.path, msg.mergeBase);
88573
+ break;
88448
88574
  default:
88449
88575
  assertNever(msg);
88450
88576
  }
@@ -88546,6 +88672,79 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
88546
88672
  respondError(requestId, formatError(err));
88547
88673
  }
88548
88674
  }
88675
+ async function handleGitBranchDiffFiles(requestId, baseRefOverride) {
88676
+ try {
88677
+ const baseRef = baseRefOverride ?? await getDefaultBranch(cwd);
88678
+ if (!baseRef) {
88679
+ respond({
88680
+ type: "git_branch_diff_files_result",
88681
+ requestId,
88682
+ files: [],
88683
+ mergeBase: "",
88684
+ baseRef: ""
88685
+ });
88686
+ return;
88687
+ }
88688
+ const mergeBase = await getMergeBase(cwd, baseRef);
88689
+ if (!mergeBase) {
88690
+ respond({
88691
+ type: "git_branch_diff_files_result",
88692
+ requestId,
88693
+ files: [],
88694
+ mergeBase: "",
88695
+ baseRef
88696
+ });
88697
+ return;
88698
+ }
88699
+ const { stdout } = await execFileAsync3(
88700
+ "git",
88701
+ ["diff", "--name-status", `${mergeBase}..HEAD`],
88702
+ { cwd, maxBuffer: 10 * 1024 * 1024 }
88703
+ );
88704
+ const files = stdout.split("\n").filter(Boolean).map((line) => {
88705
+ const parts = line.split(" ");
88706
+ const statusCode = parts[0] ?? "";
88707
+ const filePath = parts[1] ?? "";
88708
+ let status;
88709
+ if (statusCode === "A") {
88710
+ status = "added";
88711
+ } else if (statusCode === "D") {
88712
+ status = "deleted";
88713
+ } else {
88714
+ status = "modified";
88715
+ }
88716
+ return { path: filePath, status, insertions: 0, deletions: 0 };
88717
+ });
88718
+ respond({
88719
+ type: "git_branch_diff_files_result",
88720
+ requestId,
88721
+ files,
88722
+ mergeBase,
88723
+ baseRef
88724
+ });
88725
+ } catch (err) {
88726
+ respondError(requestId, formatError(err));
88727
+ }
88728
+ }
88729
+ async function handleGitBranchDiffFile(requestId, filePath, mergeBase) {
88730
+ try {
88731
+ const originalContent = await readGitObject(`${mergeBase}:${filePath}`) ?? "";
88732
+ let modifiedContent;
88733
+ try {
88734
+ modifiedContent = await readFile25(resolve(cwd, filePath), "utf-8");
88735
+ } catch {
88736
+ modifiedContent = "";
88737
+ }
88738
+ respond({
88739
+ type: "git_branch_diff_file_result",
88740
+ requestId,
88741
+ originalContent,
88742
+ modifiedContent
88743
+ });
88744
+ } catch (err) {
88745
+ respondError(requestId, formatError(err));
88746
+ }
88747
+ }
88549
88748
  async function handleGitDiffFile(requestId, safeRelPath, absPath, cached) {
88550
88749
  try {
88551
88750
  let originalContent = "";
@@ -89030,6 +89229,9 @@ function handleLSPChannel(cwd, send, log) {
89030
89229
  exited = true;
89031
89230
  child = null;
89032
89231
  });
89232
+ proc.stdin?.on("error", (err) => {
89233
+ log({ event: "lsp_stdin_error", error: err.message });
89234
+ });
89033
89235
  log({ event: "lsp_spawned", pid: proc.pid });
89034
89236
  return proc;
89035
89237
  }
@@ -89066,8 +89268,13 @@ function handleLSPChannel(cwd, send, log) {
89066
89268
  const header = `Content-Length: ${Buffer.byteLength(data)}\r
89067
89269
  \r
89068
89270
  `;
89069
- child.stdin?.write(header);
89070
- child.stdin?.write(data);
89271
+ try {
89272
+ child.stdin?.write(header);
89273
+ child.stdin?.write(data);
89274
+ } catch {
89275
+ log({ event: "lsp_write_failed", exited });
89276
+ child = null;
89277
+ }
89071
89278
  }
89072
89279
  function dispose() {
89073
89280
  disposed = true;
@@ -89248,84 +89455,94 @@ function createPeerManager(config2) {
89248
89455
  return factoryPromise;
89249
89456
  }
89250
89457
  function handleDataChannel(machineId, event) {
89251
- config2.onPeerDataChannel?.(machineId);
89252
- const channel = event.channel;
89253
- const label = channel.label ?? "";
89254
- const route = routeDataChannel(label);
89255
- switch (route.kind) {
89256
- case "thread_messages": {
89257
- config2.log.debug(
89258
- { machineId, taskId: route.taskId, threadId: route.threadId },
89259
- "Thread messages data channel received"
89260
- );
89261
- config2.onThreadMessageChannel?.(machineId, event.channel, route.taskId, route.threadId);
89262
- return;
89263
- }
89264
- case "task_messages": {
89265
- config2.log.debug(
89266
- { machineId, taskId: route.taskId },
89267
- "Task messages data channel received"
89268
- );
89269
- config2.onTaskMessageChannel?.(machineId, event.channel, route.taskId);
89270
- return;
89271
- }
89272
- case "daemon_control": {
89273
- config2.log.info({ machineId }, "Daemon control data channel received");
89274
- config2.onControlChannel?.(machineId, event.channel);
89275
- return;
89276
- }
89277
- case "loro_sync": {
89278
- config2.log.info({ machineId }, "Loro sync data channel received");
89279
- const rawChannel = event.channel;
89280
- guardLoroChannelSend(rawChannel, config2.log);
89281
- config2.webrtcAdapter.attachDataChannel(
89282
- machineIdToPeerId(machineId),
89283
- event.channel
89284
- );
89285
- return;
89286
- }
89287
- case "file_io": {
89288
- config2.log.debug({ machineId, id: route.id }, "File I/O data channel received");
89289
- config2.onFileIOChannel?.(machineId, event.channel, route.id);
89290
- return;
89291
- }
89292
- case "lsp": {
89293
- config2.log.debug({ machineId, id: route.id }, "LSP data channel received");
89294
- config2.onLSPChannel?.(machineId, event.channel, route.id);
89295
- return;
89296
- }
89297
- case "browser_preview": {
89298
- config2.log.debug({ machineId, port: route.port }, "Browser preview data channel received");
89299
- config2.onBrowserPreviewChannel?.(machineId, event.channel, route.port);
89300
- return;
89301
- }
89302
- case "browser_preview_url": {
89303
- config2.log.debug({ machineId }, "Browser preview URL data channel received");
89304
- config2.onBrowserPreviewUrlChannel?.(machineId, event.channel);
89305
- return;
89306
- }
89307
- case "terminal": {
89308
- config2.log.debug(
89309
- { machineId, taskId: route.taskId, terminalId: route.terminalId },
89310
- "Terminal data channel received"
89311
- );
89312
- config2.onTerminalChannel?.(machineId, event.channel, route.taskId, route.terminalId);
89313
- return;
89314
- }
89315
- case "asset_transfer": {
89316
- config2.log.debug({ machineId }, "Asset transfer data channel received");
89317
- config2.onAssetTransferChannel?.(machineId, event.channel);
89318
- return;
89319
- }
89320
- case "unknown": {
89321
- config2.log.warn(
89322
- { machineId, label: route.label },
89323
- "Ignoring unrecognized data channel label"
89324
- );
89325
- return;
89458
+ try {
89459
+ config2.onPeerDataChannel?.(machineId);
89460
+ const channel = event.channel;
89461
+ const label = channel.label ?? "";
89462
+ const route = routeDataChannel(label);
89463
+ switch (route.kind) {
89464
+ case "thread_messages": {
89465
+ config2.log.debug(
89466
+ { machineId, taskId: route.taskId, threadId: route.threadId },
89467
+ "Thread messages data channel received"
89468
+ );
89469
+ config2.onThreadMessageChannel?.(machineId, event.channel, route.taskId, route.threadId);
89470
+ return;
89471
+ }
89472
+ case "task_messages": {
89473
+ config2.log.debug(
89474
+ { machineId, taskId: route.taskId },
89475
+ "Task messages data channel received"
89476
+ );
89477
+ config2.onTaskMessageChannel?.(machineId, event.channel, route.taskId);
89478
+ return;
89479
+ }
89480
+ case "daemon_control": {
89481
+ config2.log.info({ machineId }, "Daemon control data channel received");
89482
+ config2.onControlChannel?.(machineId, event.channel);
89483
+ return;
89484
+ }
89485
+ case "loro_sync": {
89486
+ config2.log.info({ machineId }, "Loro sync data channel received");
89487
+ const rawChannel = event.channel;
89488
+ guardLoroChannelSend(rawChannel, config2.log);
89489
+ config2.webrtcAdapter.attachDataChannel(
89490
+ machineIdToPeerId(machineId),
89491
+ event.channel
89492
+ );
89493
+ return;
89494
+ }
89495
+ case "file_io": {
89496
+ config2.log.debug({ machineId, id: route.id }, "File I/O data channel received");
89497
+ config2.onFileIOChannel?.(machineId, event.channel, route.id);
89498
+ return;
89499
+ }
89500
+ case "lsp": {
89501
+ config2.log.debug({ machineId, id: route.id }, "LSP data channel received");
89502
+ config2.onLSPChannel?.(machineId, event.channel, route.id);
89503
+ return;
89504
+ }
89505
+ case "browser_preview": {
89506
+ config2.log.debug(
89507
+ { machineId, port: route.port },
89508
+ "Browser preview data channel received"
89509
+ );
89510
+ config2.onBrowserPreviewChannel?.(machineId, event.channel, route.port);
89511
+ return;
89512
+ }
89513
+ case "browser_preview_url": {
89514
+ config2.log.debug({ machineId }, "Browser preview URL data channel received");
89515
+ config2.onBrowserPreviewUrlChannel?.(machineId, event.channel);
89516
+ return;
89517
+ }
89518
+ case "terminal": {
89519
+ config2.log.debug(
89520
+ { machineId, taskId: route.taskId, terminalId: route.terminalId },
89521
+ "Terminal data channel received"
89522
+ );
89523
+ config2.onTerminalChannel?.(machineId, event.channel, route.taskId, route.terminalId);
89524
+ return;
89525
+ }
89526
+ case "asset_transfer": {
89527
+ config2.log.debug({ machineId }, "Asset transfer data channel received");
89528
+ config2.onAssetTransferChannel?.(machineId, event.channel);
89529
+ return;
89530
+ }
89531
+ case "unknown": {
89532
+ config2.log.warn(
89533
+ { machineId, label: route.label },
89534
+ "Ignoring unrecognized data channel label"
89535
+ );
89536
+ return;
89537
+ }
89538
+ default:
89539
+ assertNever3(route);
89326
89540
  }
89327
- default:
89328
- assertNever3(route);
89541
+ } catch (err) {
89542
+ config2.log.warn(
89543
+ { machineId, err: err instanceof Error ? err.message : String(err) },
89544
+ "Data channel routing failed (peer connection likely closing)"
89545
+ );
89329
89546
  }
89330
89547
  }
89331
89548
  function setupPeerHandlers(machineId, pc) {
@@ -89347,6 +89564,7 @@ function createPeerManager(config2) {
89347
89564
  if (state === "failed" || state === "closed") {
89348
89565
  config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
89349
89566
  peers.delete(machineId);
89567
+ pc.onconnectionstatechange = null;
89350
89568
  pc.close();
89351
89569
  }
89352
89570
  };
@@ -89402,7 +89620,20 @@ function createPeerManager(config2) {
89402
89620
  if (!pc.createOffer) {
89403
89621
  throw new Error("PeerConnection does not support createOffer");
89404
89622
  }
89405
- const channel = pc.createDataChannel(LORO_SYNC_LABEL, { ordered: true });
89623
+ let channel;
89624
+ try {
89625
+ channel = pc.createDataChannel(LORO_SYNC_LABEL, { ordered: true });
89626
+ } catch (err) {
89627
+ config2.log.warn(
89628
+ { targetMachineId, err: String(err) },
89629
+ "createDataChannel failed (connection likely closing)"
89630
+ );
89631
+ pendingCreates.delete(targetMachineId);
89632
+ peers.delete(targetMachineId);
89633
+ pc.onconnectionstatechange = null;
89634
+ pc.close();
89635
+ throw err;
89636
+ }
89406
89637
  const rawChannel = channel;
89407
89638
  guardLoroChannelSend(rawChannel, config2.log);
89408
89639
  rawChannel.onopen = () => {
@@ -89461,6 +89692,7 @@ function createPeerManager(config2) {
89461
89692
  const pc = peers.get(targetId);
89462
89693
  if (pc) {
89463
89694
  config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(targetId));
89695
+ pc.onconnectionstatechange = null;
89464
89696
  pc.close();
89465
89697
  peers.delete(targetId);
89466
89698
  }
@@ -89468,6 +89700,7 @@ function createPeerManager(config2) {
89468
89700
  destroy() {
89469
89701
  for (const [machineId, pc] of peers) {
89470
89702
  config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
89703
+ pc.onconnectionstatechange = null;
89471
89704
  pc.close();
89472
89705
  }
89473
89706
  peers.clear();
@@ -91478,7 +91711,16 @@ function attachConversationHandler(daemon, dc, channelId, params, log, attempts)
91478
91711
  const handlerOpts = {
91479
91712
  taskId,
91480
91713
  channelId,
91481
- send: (data) => dc.send(data),
91714
+ send: (data) => {
91715
+ try {
91716
+ dc.send(data);
91717
+ } catch (err) {
91718
+ log({
91719
+ event: "message_channel_send_failed",
91720
+ error: err instanceof Error ? err.message : String(err)
91721
+ });
91722
+ }
91723
+ },
91482
91724
  taskManager: daemon.taskManager,
91483
91725
  store: daemon.store,
91484
91726
  log,
@@ -91602,7 +91844,16 @@ function wireThreadErrorFallback(daemon, dc, taskId, threadId, channelId, log) {
91602
91844
  const handler = handleMessageChannel({
91603
91845
  taskId,
91604
91846
  channelId,
91605
- send: (data) => dc.send(data),
91847
+ send: (data) => {
91848
+ try {
91849
+ dc.send(data);
91850
+ } catch (err) {
91851
+ log({
91852
+ event: "message_channel_send_failed",
91853
+ error: err instanceof Error ? err.message : String(err)
91854
+ });
91855
+ }
91856
+ },
91606
91857
  taskManager: daemon.taskManager,
91607
91858
  store: daemon.store,
91608
91859
  log,
@@ -91647,15 +91898,19 @@ function routeSignalingMessage(peerManager, signalingHandle, log) {
91647
91898
  switch (msg.type) {
91648
91899
  case "webrtc-offer":
91649
91900
  if (msg.fromMachineId)
91650
- peerManager?.handleOffer(msg.fromMachineId, toSDPDescription(msg.offer));
91901
+ peerManager?.handleOffer(msg.fromMachineId, toSDPDescription(msg.offer)).catch(
91902
+ (err) => log.error({ event: "webrtc_offer_failed", error: String(err) })
91903
+ );
91651
91904
  break;
91652
91905
  case "webrtc-answer":
91653
91906
  if (msg.fromMachineId)
91654
- peerManager?.handleAnswer(msg.fromMachineId, toSDPDescription(msg.answer));
91907
+ peerManager?.handleAnswer(msg.fromMachineId, toSDPDescription(msg.answer)).catch(
91908
+ (err) => log.error({ event: "webrtc_answer_failed", error: String(err) })
91909
+ );
91655
91910
  break;
91656
91911
  case "webrtc-ice":
91657
91912
  if (msg.fromMachineId)
91658
- peerManager?.handleIce(msg.fromMachineId, toICECandidate(msg.candidate));
91913
+ peerManager?.handleIce(msg.fromMachineId, toICECandidate(msg.candidate)).catch((err) => log.error({ event: "webrtc_ice_failed", error: String(err) }));
91659
91914
  break;
91660
91915
  case "ice-servers":
91661
91916
  peerManager?.updateIceServers(msg.iceServers);
@@ -91696,4 +91951,4 @@ export {
91696
91951
  classifyLogLevel,
91697
91952
  serve
91698
91953
  };
91699
- //# sourceMappingURL=serve-HDRZ2CU6.js.map
91954
+ //# sourceMappingURL=serve-ZD5RISIM.js.map