@schoolai/shipyard 0.6.0 → 0.8.0-rc.20260221

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/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  PersonalRoomConnection,
13
13
  ROUTES,
14
14
  ValidationErrorResponseSchema
15
- } from "./chunk-CQBP5B4G.js";
15
+ } from "./chunk-OTIQVES4.js";
16
16
  import {
17
17
  __export,
18
18
  getShipyardHome,
@@ -11331,13 +11331,16 @@ var TaskDocumentSchema = Shape.doc({
11331
11331
  updatedAt: Shape.plain.number()
11332
11332
  }),
11333
11333
  conversation: Shape.list(MessageShape),
11334
+ pendingFollowUps: Shape.list(MessageShape),
11334
11335
  sessions: Shape.list(SessionEntryShape),
11335
11336
  diffState: DiffStateShape,
11336
11337
  plans: Shape.list(PlanVersionShape),
11337
11338
  planEditorDocs: Shape.record(Shape.any()),
11338
11339
  diffComments: Shape.record(DiffCommentShape),
11339
- planComments: Shape.record(PlanCommentShape)
11340
+ planComments: Shape.record(PlanCommentShape),
11341
+ deliveredCommentIds: Shape.list(Shape.plain.string())
11340
11342
  });
11343
+ var TERMINAL_TASK_STATES = ["completed", "failed", "canceled"];
11341
11344
  var TOOL_RISK_LEVELS = ["low", "medium", "high"];
11342
11345
  var PERMISSION_DECISIONS = ["approved", "denied"];
11343
11346
  var PermissionRequestEphemeral = Shape.plain.struct({
@@ -11392,7 +11395,10 @@ var WorktreeScriptShape = Shape.plain.struct({
11392
11395
  script: Shape.plain.string()
11393
11396
  });
11394
11397
  var UserSettingsShape = Shape.struct({
11395
- worktreeScripts: Shape.record(WorktreeScriptShape)
11398
+ worktreeScripts: Shape.record(WorktreeScriptShape),
11399
+ composerModel: Shape.plain.string().nullable(),
11400
+ composerReasoning: Shape.plain.string(...REASONING_EFFORTS).nullable(),
11401
+ composerPermission: Shape.plain.string(...PERMISSION_MODES).nullable()
11396
11402
  });
11397
11403
  var TaskIndexDocumentSchema = Shape.doc({
11398
11404
  taskIndex: Shape.record(TaskIndexEntryShape),
@@ -12759,13 +12765,17 @@ function recoverOrphanedTask(taskDoc, log) {
12759
12765
 
12760
12766
  // src/peer-manager.ts
12761
12767
  var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
12768
+ var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
12762
12769
  function machineIdToPeerId(machineId) {
12763
12770
  return machineId;
12764
12771
  }
12765
12772
  async function loadDefaultFactory() {
12766
12773
  const { RTCPeerConnection } = await import("node-datachannel/polyfill");
12767
12774
  return () => {
12768
- const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
12775
+ const pc = new RTCPeerConnection({
12776
+ iceServers: ICE_SERVERS,
12777
+ maxMessageSize: MAX_MESSAGE_SIZE
12778
+ });
12769
12779
  return pc;
12770
12780
  };
12771
12781
  }
@@ -12909,6 +12919,40 @@ function createPeerManager(config2) {
12909
12919
  };
12910
12920
  }
12911
12921
 
12922
+ // src/plan-editor/format-diff-feedback.ts
12923
+ function formatDiffFeedbackForClaudeCode(comments, generalFeedback) {
12924
+ const sections = [];
12925
+ if (generalFeedback) {
12926
+ sections.push("## General Feedback\n");
12927
+ sections.push(generalFeedback);
12928
+ sections.push("");
12929
+ }
12930
+ if (comments.length > 0) {
12931
+ appendCommentSection(sections, comments);
12932
+ }
12933
+ return sections.join("\n").trim();
12934
+ }
12935
+ function appendCommentSection(sections, comments) {
12936
+ sections.push("## Inline Comments on Code Changes\n");
12937
+ sections.push("The user left the following comments on the diff:\n");
12938
+ const byFile = /* @__PURE__ */ new Map();
12939
+ for (const c of comments) {
12940
+ const existing = byFile.get(c.filePath) ?? [];
12941
+ existing.push(c);
12942
+ byFile.set(c.filePath, existing);
12943
+ }
12944
+ for (const [filePath, fileComments] of byFile) {
12945
+ sections.push(`### ${filePath}
12946
+ `);
12947
+ const sorted = [...fileComments].sort((a, b) => a.lineNumber - b.lineNumber);
12948
+ for (const comment2 of sorted) {
12949
+ const sideLabel = comment2.side === "old" ? "before change" : "after change";
12950
+ sections.push(`> Line ${comment2.lineNumber} (${sideLabel}): ${comment2.body}`);
12951
+ }
12952
+ sections.push("");
12953
+ }
12954
+ }
12955
+
12912
12956
  // src/plan-editor/format-plan-feedback.ts
12913
12957
  function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeedback) {
12914
12958
  const sections = [];
@@ -12918,7 +12962,7 @@ function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeed
12918
12962
  sections.push("");
12919
12963
  }
12920
12964
  if (comments.length > 0) {
12921
- appendCommentSection(sections, comments, edited);
12965
+ appendCommentSection2(sections, comments, edited);
12922
12966
  }
12923
12967
  if (original !== edited) {
12924
12968
  sections.push("## Edits Made\n");
@@ -12930,7 +12974,7 @@ function formatPlanFeedbackForClaudeCode(original, edited, comments, generalFeed
12930
12974
  }
12931
12975
  return sections.join("\n").trim();
12932
12976
  }
12933
- function appendCommentSection(sections, comments, edited) {
12977
+ function appendCommentSection2(sections, comments, edited) {
12934
12978
  sections.push("## Inline Comments\n");
12935
12979
  sections.push("The user left the following comments on specific parts of the plan:\n");
12936
12980
  const editedLines = edited.split("\n");
@@ -41672,7 +41716,8 @@ var SessionManager = class {
41672
41716
  * Determine whether to resume an existing session or start fresh.
41673
41717
  *
41674
41718
  * Walks backwards through sessions to find the most recent one with a
41675
- * non-empty agentSessionId that has not failed. If found, returns
41719
+ * non-empty agentSessionId that has not genuinely failed. Interrupted
41720
+ * (user-cancelled) sessions are resumable. If found, returns
41676
41721
  * { resume: true, sessionId } so the caller can pass it to resumeSession().
41677
41722
  */
41678
41723
  shouldResume() {
@@ -41743,7 +41788,7 @@ var SessionManager = class {
41743
41788
  });
41744
41789
  this.#inputController = controller;
41745
41790
  this.#activeQuery = response;
41746
- return this.#processMessages(response, sessionId);
41791
+ return this.#processMessages(response, sessionId, opts.abortController);
41747
41792
  }
41748
41793
  /**
41749
41794
  * Send a follow-up message to the active streaming session.
@@ -41778,6 +41823,9 @@ var SessionManager = class {
41778
41823
  await this.#activeQuery?.setModel(model);
41779
41824
  this.#currentModel = model;
41780
41825
  }
41826
+ async setPermissionMode(mode) {
41827
+ await this.#activeQuery?.setPermissionMode(mode);
41828
+ }
41781
41829
  /**
41782
41830
  * Resume an existing Claude Code session using streaming input mode.
41783
41831
  * Looks up the agentSessionId from the task doc and passes it as `resume`.
@@ -41839,9 +41887,9 @@ var SessionManager = class {
41839
41887
  });
41840
41888
  this.#inputController = controller;
41841
41889
  this.#activeQuery = response;
41842
- return this.#processMessages(response, newSessionId);
41890
+ return this.#processMessages(response, newSessionId, opts?.abortController);
41843
41891
  }
41844
- async #processMessages(response, sessionId) {
41892
+ async #processMessages(response, sessionId, abortController) {
41845
41893
  let agentSessionId = "";
41846
41894
  let lastMessageAt = Date.now();
41847
41895
  let idleTimedOut = false;
@@ -41868,14 +41916,13 @@ var SessionManager = class {
41868
41916
  }
41869
41917
  }
41870
41918
  } catch (error2) {
41871
- const errorMsg2 = idleTimedOut ? "Session idle timeout exceeded" : error2 instanceof Error ? error2.message : String(error2);
41872
- this.#markFailed(sessionId, errorMsg2);
41873
- return {
41919
+ return this.#handleProcessError(
41920
+ error2,
41874
41921
  sessionId,
41875
41922
  agentSessionId,
41876
- status: "failed",
41877
- error: errorMsg2
41878
- };
41923
+ idleTimedOut,
41924
+ abortController
41925
+ );
41879
41926
  } finally {
41880
41927
  clearInterval(idleTimer);
41881
41928
  this.#inputController = null;
@@ -42113,6 +42160,28 @@ var SessionManager = class {
42113
42160
  error: !isSuccess ? errorText ?? message.subtype : void 0
42114
42161
  };
42115
42162
  }
42163
+ #handleProcessError(error2, sessionId, agentSessionId, idleTimedOut, abortController) {
42164
+ if (abortController?.signal.aborted) {
42165
+ this.#markInterrupted(sessionId);
42166
+ return { sessionId, agentSessionId, status: "interrupted" };
42167
+ }
42168
+ const errorMsg = idleTimedOut ? "Session idle timeout exceeded" : error2 instanceof Error ? error2.message : String(error2);
42169
+ this.#markFailed(sessionId, errorMsg);
42170
+ return { sessionId, agentSessionId, status: "failed", error: errorMsg };
42171
+ }
42172
+ #markInterrupted(sessionId) {
42173
+ const idx = this.#findSessionIndex(sessionId);
42174
+ change(this.#taskDoc, (draft) => {
42175
+ const session = idx >= 0 ? draft.sessions.get(idx) : void 0;
42176
+ if (session) {
42177
+ session.status = "interrupted";
42178
+ session.completedAt = Date.now();
42179
+ }
42180
+ draft.meta.status = "canceled";
42181
+ draft.meta.updatedAt = Date.now();
42182
+ });
42183
+ this.#notifyStatusChange("canceled");
42184
+ }
42116
42185
  #markFailed(sessionId, errorMsg) {
42117
42186
  const idx = this.#findSessionIndex(sessionId);
42118
42187
  change(this.#taskDoc, (draft) => {
@@ -42488,6 +42557,34 @@ var TaskEphemeralDeclarations = {
42488
42557
  permReqs: PermissionRequestEphemeral,
42489
42558
  permResps: PermissionResponseEphemeral
42490
42559
  };
42560
+ var TERMINAL_STATUSES = new Set(TERMINAL_TASK_STATES);
42561
+ async function rehydrateTaskDocuments(roomHandle, roomDoc, repo, log) {
42562
+ try {
42563
+ await roomHandle.waitForSync({ kind: "storage", timeout: 5e3 });
42564
+ } catch {
42565
+ log.warn(
42566
+ "Room doc storage sync timed out during rehydration \u2014 task rehydration may be incomplete"
42567
+ );
42568
+ }
42569
+ const roomJson = roomDoc.toJSON();
42570
+ const taskEntries = Object.entries(roomJson.taskIndex ?? {});
42571
+ log.info({ count: taskEntries.length }, "Rehydrating task documents from storage");
42572
+ for (const [taskId, entry] of taskEntries) {
42573
+ if (TERMINAL_STATUSES.has(entry.status)) continue;
42574
+ try {
42575
+ const taskDocId = buildDocumentId("task", taskId, DEFAULT_EPOCH);
42576
+ const taskHandle = repo.get(taskDocId, TaskDocumentSchema, TaskEphemeralDeclarations);
42577
+ await taskHandle.waitForSync({ kind: "storage", timeout: 5e3 });
42578
+ if (recoverOrphanedTask(taskHandle.doc, createChildLogger({ mode: "rehydrate", taskId }))) {
42579
+ updateTaskInIndex(roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
42580
+ }
42581
+ log.debug({ taskId, taskDocId }, "Task document rehydrated");
42582
+ } catch (err) {
42583
+ log.warn({ taskId, err }, "Failed to rehydrate task document");
42584
+ }
42585
+ }
42586
+ log.info("Task document rehydration complete");
42587
+ }
42491
42588
  async function serve(env) {
42492
42589
  if (!env.SHIPYARD_SIGNALING_URL) {
42493
42590
  logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
@@ -42520,6 +42617,7 @@ async function serve(env) {
42520
42617
  const activeTasks = /* @__PURE__ */ new Map();
42521
42618
  const watchedTasks = /* @__PURE__ */ new Map();
42522
42619
  const taskHandles = /* @__PURE__ */ new Map();
42620
+ const lastProcessedConvLen = /* @__PURE__ */ new Map();
42523
42621
  const devSuffix = env.SHIPYARD_DEV ? "-dev" : "";
42524
42622
  const machineId = env.SHIPYARD_MACHINE_ID ?? `${hostname2()}${devSuffix}`;
42525
42623
  const dataDir = resolve(env.SHIPYARD_DATA_DIR.replace("~", homedir2()));
@@ -42733,6 +42831,7 @@ async function serve(env) {
42733
42831
  const typedRoomHandle = roomHandle;
42734
42832
  const typedRoomDoc = roomHandle.doc;
42735
42833
  cleanupStaleSetupEntries(typedRoomDoc, machineId, log);
42834
+ await rehydrateTaskDocuments(roomHandle, typedRoomDoc, repo, log);
42736
42835
  typedRoomHandle.enhancePromptReqs.subscribe(({ key: requestId, value, source }) => {
42737
42836
  if (source !== "remote") return;
42738
42837
  if (!value) return;
@@ -42904,6 +43003,7 @@ async function serve(env) {
42904
43003
  activeTasks,
42905
43004
  watchedTasks,
42906
43005
  taskHandles,
43006
+ lastProcessedConvLen,
42907
43007
  peerManager,
42908
43008
  env,
42909
43009
  machineId,
@@ -42935,6 +43035,7 @@ async function serve(env) {
42935
43035
  }
42936
43036
  watchedTasks.clear();
42937
43037
  taskHandles.clear();
43038
+ lastProcessedConvLen.clear();
42938
43039
  for (const timer of pendingCleanupTimers) clearTimeout(timer);
42939
43040
  pendingCleanupTimers.clear();
42940
43041
  for (const [id, ptyMgr] of terminalPtys) {
@@ -43109,6 +43210,12 @@ function handleMessage(msg, ctx) {
43109
43210
  case "worktree-create-error":
43110
43211
  ctx.log.debug({ type: msg.type }, "Worktree create echo");
43111
43212
  break;
43213
+ case "cancel-task":
43214
+ handleCancelTask(msg, ctx);
43215
+ break;
43216
+ case "control-ack":
43217
+ ctx.log.debug({ type: msg.type }, "Control ack echo");
43218
+ break;
43112
43219
  case "authenticated":
43113
43220
  case "agent-joined":
43114
43221
  case "agent-left":
@@ -43160,6 +43267,32 @@ function handleNotifyTask(msg, ctx) {
43160
43267
  taskLog.error({ err: errMsg }, "Failed to start watching task document");
43161
43268
  });
43162
43269
  }
43270
+ function handleCancelTask(msg, ctx) {
43271
+ const { taskId, requestId } = msg;
43272
+ const taskLog = createChildLogger({ mode: "serve", taskId });
43273
+ const activeTask = ctx.activeTasks.get(taskId);
43274
+ if (!activeTask) {
43275
+ taskLog.warn("Cancel requested but no active task found");
43276
+ ctx.connection.send({
43277
+ type: "control-ack",
43278
+ requestId,
43279
+ taskId,
43280
+ action: "cancel",
43281
+ accepted: false,
43282
+ error: "No active agent running for this task"
43283
+ });
43284
+ return;
43285
+ }
43286
+ taskLog.info("Canceling active task");
43287
+ activeTask.abortController.abort();
43288
+ ctx.connection.send({
43289
+ type: "control-ack",
43290
+ requestId,
43291
+ taskId,
43292
+ action: "cancel",
43293
+ accepted: true
43294
+ });
43295
+ }
43163
43296
  var ENHANCE_PROMPT_TIMEOUT_MS = 3e4;
43164
43297
  var ENHANCE_SYSTEM_PROMPT = `You are a prompt rewriter that transforms rough user requests into clear, specific instructions for an AI coding assistant (Claude Code). The enhanced prompt will be sent AS the user's message to that assistant.
43165
43298
 
@@ -43586,12 +43719,15 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
43586
43719
  try {
43587
43720
  await taskHandle.waitForSync({ kind: "storage", timeout: 5e3 });
43588
43721
  } catch {
43589
- taskLog.debug({ taskDocId }, "No existing task data in storage");
43722
+ taskLog.info({ taskDocId }, "No existing task data in storage");
43590
43723
  }
43591
43724
  try {
43592
43725
  await taskHandle.waitForSync({ kind: "network", timeout: 3e3 });
43593
43726
  } catch {
43594
- taskLog.debug({ taskDocId }, "Network sync timed out (browser may not be connected yet)");
43727
+ taskLog.warn(
43728
+ { taskDocId, timeoutMs: 3e3 },
43729
+ "Network sync timed out (browser may not be connected yet)"
43730
+ );
43595
43731
  }
43596
43732
  if (recoverOrphanedTask(taskHandle.doc, taskLog)) {
43597
43733
  updateTaskInIndex(ctx.roomDoc, taskId, { status: "failed", updatedAt: Date.now() });
@@ -43622,28 +43758,50 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
43622
43758
  onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
43623
43759
  }
43624
43760
  }
43625
- function handleFollowUp(activeTask, conversationLen, taskLog) {
43761
+ function promotePendingFollowUps(taskHandle, activeTask, taskLog) {
43626
43762
  if (!activeTask) return;
43627
- if (conversationLen <= activeTask.lastDispatchedConvLen) {
43628
- taskLog.debug(
43629
- { conversationLen, lastDispatched: activeTask.lastDispatchedConvLen },
43630
- "Conversation unchanged since last dispatch, skipping duplicate"
43631
- );
43763
+ if (activeTask.abortController.signal.aborted) {
43764
+ taskLog.debug("Task is being aborted, skipping pending follow-up promotion");
43632
43765
  return;
43633
43766
  }
43767
+ const json = taskHandle.doc.toJSON();
43768
+ const pending = json.pendingFollowUps ?? [];
43769
+ if (pending.length === 0) return;
43770
+ change(taskHandle.doc, (draft) => {
43771
+ const items = draft.pendingFollowUps.toArray();
43772
+ for (const msg of items) {
43773
+ draft.conversation.push(msg);
43774
+ }
43775
+ if (draft.pendingFollowUps.length > 0) {
43776
+ draft.pendingFollowUps.delete(0, draft.pendingFollowUps.length);
43777
+ }
43778
+ });
43779
+ taskLog.info({ pendingCount: pending.length }, "Promoted pending follow-ups to conversation");
43634
43780
  if (!activeTask.sessionManager.isStreaming) {
43635
- taskLog.debug("Task already running but not streaming, skipping");
43781
+ taskLog.debug("Task not streaming, skipping follow-up dispatch");
43636
43782
  return;
43637
43783
  }
43638
- const contentBlocks = activeTask.sessionManager.getLatestUserContentBlocks();
43639
- if (contentBlocks && contentBlocks.length > 0) {
43784
+ const allContentBlocks = pending.flatMap(
43785
+ (msg) => msg.content.filter((block2) => block2.type === "text" || block2.type === "image")
43786
+ );
43787
+ if (allContentBlocks.length === 0) return;
43788
+ const dispatchFollowUp = () => {
43640
43789
  try {
43641
- taskLog.info("Sending follow-up to active streaming session");
43642
- activeTask.lastDispatchedConvLen = conversationLen;
43643
- activeTask.sessionManager.sendFollowUp(contentBlocks);
43790
+ activeTask.lastDispatchedConvLen = json.conversation.length + pending.length;
43791
+ activeTask.sessionManager.sendFollowUp(allContentBlocks);
43644
43792
  } catch (err) {
43645
- taskLog.warn({ err }, "Failed to send follow-up to streaming session");
43793
+ taskLog.warn({ err }, "Failed to send promoted follow-up");
43646
43794
  }
43795
+ };
43796
+ const lastPending = pending[pending.length - 1];
43797
+ const mappedMode = lastPending?.permissionMode ? mapPermissionMode(lastPending.permissionMode) : void 0;
43798
+ if (mappedMode) {
43799
+ activeTask.sessionManager.setPermissionMode(mappedMode).then(dispatchFollowUp).catch((err) => {
43800
+ taskLog.warn({ err }, "Failed to update permission mode from queued message");
43801
+ dispatchFollowUp();
43802
+ });
43803
+ } else {
43804
+ dispatchFollowUp();
43647
43805
  }
43648
43806
  }
43649
43807
  function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
@@ -43659,22 +43817,35 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
43659
43817
  "onTaskDocChanged evaluation"
43660
43818
  );
43661
43819
  if (ctx.activeTasks.has(taskId)) {
43662
- const conversation2 = json.conversation;
43663
- const lastMessage2 = conversation2[conversation2.length - 1];
43664
- if (lastMessage2?.role === "user") {
43665
- handleFollowUp(ctx.activeTasks.get(taskId), conversation2.length, taskLog);
43820
+ const pendingFollowUps2 = json.pendingFollowUps ?? [];
43821
+ if (pendingFollowUps2.length > 0) {
43822
+ promotePendingFollowUps(taskHandle, ctx.activeTasks.get(taskId), taskLog);
43666
43823
  }
43824
+ const conversation2 = json.conversation;
43667
43825
  const activeLastUserMsg = [...conversation2].reverse().find((m) => m.role === "user");
43668
43826
  const activeCwd = activeLastUserMsg?.cwd ?? process.cwd();
43669
43827
  debouncedDiffCapture(taskId, activeCwd, taskHandle, taskLog);
43670
43828
  debouncedBranchDiffCapture(taskId, activeCwd, taskHandle, taskLog);
43671
43829
  return;
43672
43830
  }
43673
- if (json.meta.status === "working" || json.meta.status === "input-required" || json.meta.status === "starting") {
43674
- taskLog.debug({ status: json.meta.status }, "Status blocks new work, skipping");
43675
- return;
43831
+ const pendingFollowUps = json.pendingFollowUps ?? [];
43832
+ if (pendingFollowUps.length > 0) {
43833
+ taskLog.info(
43834
+ { pendingCount: pendingFollowUps.length },
43835
+ "Promoting orphaned pending follow-ups"
43836
+ );
43837
+ change(taskHandle.doc, (draft) => {
43838
+ const items = draft.pendingFollowUps.toArray();
43839
+ for (const msg of items) {
43840
+ draft.conversation.push(msg);
43841
+ }
43842
+ if (draft.pendingFollowUps.length > 0) {
43843
+ draft.pendingFollowUps.delete(0, draft.pendingFollowUps.length);
43844
+ }
43845
+ });
43676
43846
  }
43677
- const conversation = json.conversation;
43847
+ const freshJson = taskHandle.doc.toJSON();
43848
+ const conversation = freshJson.conversation;
43678
43849
  if (conversation.length === 0) {
43679
43850
  taskLog.debug("No conversation messages, skipping");
43680
43851
  return;
@@ -43683,7 +43854,18 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
43683
43854
  if (!lastMessage || lastMessage.role !== "user") {
43684
43855
  return;
43685
43856
  }
43686
- taskLog.info("New user message detected, starting agent");
43857
+ const prevLen = ctx.lastProcessedConvLen.get(taskId) ?? 0;
43858
+ if (conversation.length <= prevLen) {
43859
+ taskLog.debug(
43860
+ { conversationLen: conversation.length, lastProcessed: prevLen },
43861
+ "No new messages since last run, skipping"
43862
+ );
43863
+ return;
43864
+ }
43865
+ taskLog.info(
43866
+ { prevLen, newLen: conversation.length },
43867
+ "New user message detected, starting agent"
43868
+ );
43687
43869
  const cwd = lastMessage.cwd ?? process.cwd();
43688
43870
  const model = lastMessage.model ?? void 0;
43689
43871
  const permissionMode = mapPermissionMode(lastMessage.permissionMode);
@@ -43700,6 +43882,7 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
43700
43882
  lastDispatchedConvLen: conversation.length
43701
43883
  };
43702
43884
  ctx.activeTasks.set(taskId, activeTask);
43885
+ ctx.lastProcessedConvLen.set(taskId, conversation.length);
43703
43886
  ctx.signaling.updateStatus("running", taskId);
43704
43887
  const turnStartRefPromise = captureTreeSnapshot(cwd);
43705
43888
  turnStartRefPromise.then((ref) => taskLog.debug({ turnStartRef: ref }, "Captured turn start snapshot")).catch((err) => taskLog.warn({ err }, "Failed to capture turn start snapshot"));
@@ -43746,6 +43929,7 @@ async function cleanupTaskRun(opts) {
43746
43929
  const { taskId, cwd, taskHandle, taskLog, turnStartRefPromise, abortController, ctx } = opts;
43747
43930
  const activeTask = ctx.activeTasks.get(taskId);
43748
43931
  activeTask?.sessionManager.closeSession();
43932
+ abortController.abort();
43749
43933
  clearDebouncedTimer(diffDebounceTimers, taskId);
43750
43934
  clearDebouncedTimer(branchDiffTimers, taskId);
43751
43935
  try {
@@ -43764,7 +43948,6 @@ async function cleanupTaskRun(opts) {
43764
43948
  } catch (err) {
43765
43949
  taskLog.warn({ err }, "Failed to capture turn diff");
43766
43950
  }
43767
- abortController.abort();
43768
43951
  for (const [key] of taskHandle.permReqs.getAll()) {
43769
43952
  taskHandle.permReqs.delete(key);
43770
43953
  }
@@ -43773,6 +43956,7 @@ async function cleanupTaskRun(opts) {
43773
43956
  }
43774
43957
  ctx.activeTasks.delete(taskId);
43775
43958
  ctx.signaling.updateStatus("idle");
43959
+ onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
43776
43960
  }
43777
43961
  function clearDebouncedTimer(timers, taskId) {
43778
43962
  const timer = timers.get(taskId);
@@ -43846,6 +44030,19 @@ function resolveExitPlanMode(taskHandle, taskLog, toolUseID, value) {
43846
44030
  "Updated plan reviewStatus in CRDT with rich feedback"
43847
44031
  );
43848
44032
  }
44033
+ function resolveAskUserQuestion(taskLog, toolUseID, input, value) {
44034
+ if (value.decision !== "approved" || !value.message) return input;
44035
+ try {
44036
+ const parsed = JSON.parse(value.message);
44037
+ if ("answers" in parsed && parsed.answers && typeof parsed.answers === "object" && !Array.isArray(parsed.answers)) {
44038
+ taskLog.info({ toolUseID }, "Merged AskUserQuestion answers into input");
44039
+ return { ...input, answers: parsed.answers };
44040
+ }
44041
+ } catch {
44042
+ taskLog.warn({ toolUseID }, "Failed to parse AskUserQuestion answers from message");
44043
+ }
44044
+ return input;
44045
+ }
43849
44046
  function resolvePermissionResponse(ctx) {
43850
44047
  const { taskHandle, roomDoc, taskId, taskLog, toolName, toolUseID, input, suggestions, value } = ctx;
43851
44048
  taskHandle.permReqs.delete(toolUseID);
@@ -43858,6 +44055,10 @@ function resolvePermissionResponse(ctx) {
43858
44055
  if (toolName === "ExitPlanMode") {
43859
44056
  resolveExitPlanMode(taskHandle, taskLog, toolUseID, value);
43860
44057
  }
44058
+ let resolvedInput = input;
44059
+ if (toolName === "AskUserQuestion") {
44060
+ resolvedInput = resolveAskUserQuestion(taskLog, toolUseID, input, value);
44061
+ }
43861
44062
  taskLog.info(
43862
44063
  {
43863
44064
  toolName,
@@ -43869,7 +44070,8 @@ function resolvePermissionResponse(ctx) {
43869
44070
  "Permission response received"
43870
44071
  );
43871
44072
  const decision = value.decision === "approved" ? "approved" : "denied";
43872
- return toPermissionResult(decision, input, suggestions, value.message);
44073
+ const resultMessage = toolName === "AskUserQuestion" && decision === "approved" ? null : value.message;
44074
+ return toPermissionResult(decision, resolvedInput, suggestions, resultMessage);
43873
44075
  }
43874
44076
  function buildCanUseTool(taskHandle, taskLog, roomDoc, taskId) {
43875
44077
  return async (toolName, input, options) => {
@@ -43950,6 +44152,76 @@ function buildCanUseTool(taskHandle, taskLog, roomDoc, taskId) {
43950
44152
  });
43951
44153
  };
43952
44154
  }
44155
+ function collectPlanFeedback(unresolvedPlan, plans, loroDoc) {
44156
+ const parts = [];
44157
+ const byPlanId = /* @__PURE__ */ new Map();
44158
+ for (const c of unresolvedPlan) {
44159
+ const existing = byPlanId.get(c.planId) ?? [];
44160
+ existing.push(c);
44161
+ byPlanId.set(c.planId, existing);
44162
+ }
44163
+ for (const [planId, comments] of byPlanId) {
44164
+ const plan = plans.find((p) => p.planId === planId);
44165
+ const originalMarkdown = plan?.markdown ?? "";
44166
+ const editedMarkdown = serializePlanEditorDoc(loroDoc, planId) || originalMarkdown;
44167
+ const feedback = formatPlanFeedbackForClaudeCode(
44168
+ originalMarkdown,
44169
+ editedMarkdown,
44170
+ comments,
44171
+ null
44172
+ );
44173
+ if (feedback) {
44174
+ parts.push(feedback);
44175
+ }
44176
+ }
44177
+ return parts;
44178
+ }
44179
+ function harvestUndeliveredComments(taskHandle, contentBlocks, log) {
44180
+ const json = taskHandle.doc.toJSON();
44181
+ const deliveredSet = new Set(json.deliveredCommentIds ?? []);
44182
+ const unresolvedDiff = Object.values(json.diffComments).filter(
44183
+ (c) => c.resolvedAt === null && !deliveredSet.has(c.commentId)
44184
+ );
44185
+ const unresolvedPlan = Object.values(json.planComments).filter(
44186
+ (c) => c.resolvedAt === null && !deliveredSet.has(c.commentId)
44187
+ );
44188
+ if (unresolvedDiff.length === 0 && unresolvedPlan.length === 0) {
44189
+ return;
44190
+ }
44191
+ const feedbackParts = [];
44192
+ if (unresolvedDiff.length > 0) {
44193
+ const diffFeedback = formatDiffFeedbackForClaudeCode(unresolvedDiff, null);
44194
+ if (diffFeedback) {
44195
+ feedbackParts.push(diffFeedback);
44196
+ }
44197
+ }
44198
+ if (unresolvedPlan.length > 0) {
44199
+ feedbackParts.push(...collectPlanFeedback(unresolvedPlan, json.plans, taskHandle.loroDoc));
44200
+ }
44201
+ if (feedbackParts.length === 0) {
44202
+ return;
44203
+ }
44204
+ const feedbackText = `
44205
+
44206
+ ---
44207
+ **User feedback on your changes (comments from code review):**
44208
+
44209
+ ${feedbackParts.join("\n\n")}`;
44210
+ contentBlocks.push({ type: "text", text: feedbackText });
44211
+ const harvestedIds = [
44212
+ ...unresolvedDiff.map((c) => c.commentId),
44213
+ ...unresolvedPlan.map((c) => c.commentId)
44214
+ ];
44215
+ change(taskHandle.doc, (draft) => {
44216
+ for (const id of harvestedIds) {
44217
+ draft.deliveredCommentIds.push(id);
44218
+ }
44219
+ });
44220
+ log.info(
44221
+ { diffCommentCount: unresolvedDiff.length, planCommentCount: unresolvedPlan.length },
44222
+ "Harvested undelivered comments as safety net"
44223
+ );
44224
+ }
43953
44225
  async function runTask(opts) {
43954
44226
  const {
43955
44227
  sessionManager: manager,
@@ -43968,13 +44240,14 @@ async function runTask(opts) {
43968
44240
  if (!contentBlocks || contentBlocks.length === 0) {
43969
44241
  throw new Error(`No user message found in task ${taskId}`);
43970
44242
  }
44243
+ harvestUndeliveredComments(taskHandle, contentBlocks, log);
43971
44244
  const textPreview = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join(" ").slice(0, 100);
43972
44245
  const imageCount = contentBlocks.filter((b) => b.type === "image").length;
43973
44246
  log.info(
43974
44247
  { prompt: textPreview || "(images only)", imageCount },
43975
44248
  "Running task with prompt from CRDT"
43976
44249
  );
43977
- const canUseTool = permissionMode === "bypassPermissions" ? void 0 : buildCanUseTool(taskHandle, log, roomDoc, taskId);
44250
+ const canUseTool = buildCanUseTool(taskHandle, log, roomDoc, taskId);
43978
44251
  const stderr = (data) => {
43979
44252
  const trimmed = data.trim();
43980
44253
  if (!trimmed) return;
@@ -43995,7 +44268,7 @@ async function runTask(opts) {
43995
44268
  effort,
43996
44269
  canUseTool,
43997
44270
  stderr,
43998
- allowDangerouslySkipPermissions: permissionMode === "bypassPermissions" ? true : void 0
44271
+ allowDangerouslySkipPermissions: true
43999
44272
  });
44000
44273
  }
44001
44274
  return manager.createSession({
@@ -44008,7 +44281,7 @@ async function runTask(opts) {
44008
44281
  abortController,
44009
44282
  canUseTool,
44010
44283
  stderr,
44011
- allowDangerouslySkipPermissions: permissionMode === "bypassPermissions" ? true : void 0
44284
+ allowDangerouslySkipPermissions: true
44012
44285
  });
44013
44286
  }
44014
44287
 
@@ -44154,7 +44427,7 @@ function handleResult(log, result, startTime) {
44154
44427
  async function handleSubcommand() {
44155
44428
  const subcommand = process.argv[2];
44156
44429
  if (subcommand === "login") {
44157
- const { loginCommand } = await import("./login-625HP2EN.js");
44430
+ const { loginCommand } = await import("./login-6TA7RIBE.js");
44158
44431
  const hasCheck = process.argv.includes("--check");
44159
44432
  await loginCommand({ check: hasCheck });
44160
44433
  return true;