@ordewell/web 0.10.4 → 0.10.6

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.
@@ -244,8 +244,8 @@ var PoolFileSystem = class extends core.BaseFileSystem {
244
244
  }
245
245
  async globImpl(pattern, absRoot, headLimit) {
246
246
  if (await this.hasRg()) {
247
- const anchor = await this.searchAnchor(absRoot);
248
- const result2 = await run("rg", core.buildGlobArgs(pattern, absRoot), { timeout: SEARCH_TIMEOUT_MS, cwd: anchor, env: this.searchEnv() });
247
+ const { root, anchor } = await this.searchTarget(absRoot);
248
+ const result2 = await run("rg", core.buildGlobArgs(pattern, root), { timeout: SEARCH_TIMEOUT_MS, cwd: anchor, env: this.searchEnv() });
249
249
  if (result2.code !== 0 && result2.code !== 1 && !result2.stdout) {
250
250
  return { success: false, output: `Glob failed: ${result2.stderr.trim() || "ripgrep error"}`, truncated: false };
251
251
  }
@@ -258,41 +258,50 @@ var PoolFileSystem = class extends core.BaseFileSystem {
258
258
  }
259
259
  const basename = pattern.split("/").pop() || pattern;
260
260
  const excludeArgs = core.SEARCH_EXCLUSIONS.flatMap((dir) => ["-not", "-path", `*/${dir}/*`]);
261
- const result = await run("find", [absRoot, "-type", "f", "-name", basename, ...excludeArgs], { timeout: SEARCH_TIMEOUT_MS, env: this.searchEnv() });
261
+ const { root: findRoot } = await this.searchTarget(absRoot);
262
+ const result = await run("find", [findRoot, "-type", "f", "-name", basename, ...excludeArgs], { timeout: SEARCH_TIMEOUT_MS, env: this.searchEnv() });
262
263
  if (result.code !== 0 && !result.stdout) {
263
264
  return { success: false, output: `Glob failed: ${result.stderr.trim() || "find error"}`, truncated: false };
264
265
  }
265
266
  const capped = core.applyHeadLimit(result.stdout, headLimit);
266
267
  return {
267
268
  success: true,
268
- output: core.formatSearchOutput(capped, absRoot, { emptyMessage: "No files matched." }),
269
+ output: core.formatSearchOutput(capped, findRoot, { emptyMessage: "No files matched." }),
269
270
  truncated: capped.truncated
270
271
  };
271
272
  }
272
273
  /**
273
- * A directory to anchor a search in. `cwd` exists only so anchored
274
- * `--glob`/`--include` patterns resolve against the search root (see
275
- * globImpl), and spawn(2) rejects a `cwd` that is a file with ENOTDIR.
274
+ * The search path and a directory to anchor it in. `cwd` exists only so
275
+ * anchored `--glob`/`--include` patterns resolve against the search root, and
276
+ * spawn(2) rejects a `cwd` that is a file with ENOTDIR.
276
277
  *
277
278
  * `grep`'s `path` is documented as a directory, but the tool description
278
279
  * tells the model to survey with output_mode="files" and then re-run "on a
279
280
  * narrower path" — and what that survey hands back are files. rg and grep
280
281
  * both search a single file happily, so anchor at the parent rather than
281
282
  * failing a legitimately file-scoped search.
283
+ *
284
+ * The root is resolved through symlinks because the OS reports a child's cwd
285
+ * as the resolved path (macOS tmpdir is `/var/...` -> `/private/var/...`),
286
+ * and ripgrep matches an anchored glob against the search path as written: a
287
+ * root that goes through a link never matches and yields a false empty
288
+ * result. Result rows then carry the resolved prefix, which is why callers
289
+ * strip `anchor` rather than the path they were handed.
282
290
  */
283
- async searchAnchor(absRoot) {
291
+ async searchTarget(absRoot) {
284
292
  try {
285
- return (await fs.promises.stat(absRoot)).isDirectory() ? absRoot : path__namespace.dirname(absRoot);
293
+ const root = await fs.promises.realpath(absRoot);
294
+ return { root, anchor: (await fs.promises.stat(root)).isDirectory() ? root : path__namespace.dirname(root) };
286
295
  } catch {
287
- return absRoot;
296
+ return { root: absRoot, anchor: absRoot };
288
297
  }
289
298
  }
290
299
  async grepImpl(pattern, absRoot, opts) {
291
300
  const headLimit = opts.headLimit ?? 100;
292
301
  const useRg = await this.hasRg();
293
- const anchor = await this.searchAnchor(absRoot);
302
+ const { root, anchor } = await this.searchTarget(absRoot);
294
303
  const anchoredInclude = !useRg && opts.include?.includes("/") ? opts.include : void 0;
295
- const result = useRg ? await run("rg", core.buildGrepArgs(pattern, opts, absRoot).args, { timeout: SEARCH_TIMEOUT_MS, cwd: anchor, env: this.searchEnv() }) : await run("grep", core.buildFallbackGrepArgs(pattern, anchoredInclude ? { ...opts, include: void 0 } : opts, absRoot, await this.fallbackGrepRegex()), { timeout: SEARCH_TIMEOUT_MS, cwd: anchor, env: this.searchEnv() });
304
+ const result = useRg ? await run("rg", core.buildGrepArgs(pattern, opts, root).args, { timeout: SEARCH_TIMEOUT_MS, cwd: anchor, env: this.searchEnv() }) : await run("grep", core.buildFallbackGrepArgs(pattern, anchoredInclude ? { ...opts, include: void 0 } : opts, root, await this.fallbackGrepRegex()), { timeout: SEARCH_TIMEOUT_MS, cwd: anchor, env: this.searchEnv() });
296
305
  if (result.code !== 0 && result.code !== 1 && !result.stdout) {
297
306
  return { success: false, output: `Search failed: ${result.stderr.trim() || "search error"}`, truncated: false };
298
307
  }
@@ -561,9 +570,10 @@ var OrchestratorPool = class {
561
570
  * environment's defaults over what the user had chosen.
562
571
  */
563
572
  setRunnerEnabled(id, enabled) {
564
- const current = this.enabledRunnerOverride() ?? new WebConfig().enabledRunners;
565
- const next = enabled ? current.includes(id) ? current : [...current, id] : current.filter((r) => r !== id);
566
- this.settingsService.setEnabledRunners(next);
573
+ this.settingsService.updateEnabledRunners((saved) => {
574
+ const current = saved ?? new WebConfig().enabledRunners;
575
+ return enabled ? current.includes(id) ? current : [...current, id] : current.filter((r) => r !== id);
576
+ });
567
577
  }
568
578
  getSettings() {
569
579
  const config = this.plannerConfig();
@@ -912,27 +922,29 @@ function sessionsRoute(pool2) {
912
922
  const id = c.req.param("id");
913
923
  const saved = core.loadSessionPlanState(id, ws);
914
924
  if (!saved) return refuse(c, 404, "Session not found", "session_not_found");
915
- return c.json({ meta: saved.meta, plan: core.surfacePlanState(pool2.getPlanState(id) ?? saved.plan) });
925
+ const run2 = pool2.runSnapshot(id);
926
+ return c.json({ meta: saved.meta, plan: core.surfacePlanState(pool2.getPlanState(id) ?? saved.plan), ...run2 ? { run: run2 } : {} });
916
927
  });
917
928
  router.post("/:id/load", (c) => {
918
929
  const ws = c.req.query("workspace") || process.cwd();
919
930
  try {
920
931
  const id = c.req.param("id");
921
932
  const plan = pool2.adoptSavedSession(id, ws);
922
- return c.json({ ok: true, plan: core.surfacePlan(plan), goal: pool2.getGoal(id) });
933
+ const run2 = pool2.runSnapshot(id);
934
+ return c.json({ ok: true, plan: core.surfacePlan(plan), goal: pool2.getGoal(id), ...run2 ? { run: run2 } : {} });
923
935
  } catch (err) {
924
936
  return failure(c, err, "load session", { message: "Failed to load session" });
925
937
  }
926
938
  });
927
939
  router.get("/:id/tasks/:taskId/log", (c) => {
928
940
  const ws = c.req.query("workspace") || process.cwd();
929
- return c.json({ attempts: core.listTaskLogAttempts({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId")) });
941
+ return c.json({ attempts: core.listTaskLogAttempts({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId")), position: pool2.streamPosition(c.req.param("id")) });
930
942
  });
931
943
  router.get("/:id/tasks/:taskId/log/:attempt", (c) => {
932
944
  const ws = c.req.query("workspace") || process.cwd();
933
945
  const attempt = Number(c.req.param("attempt"));
934
946
  if (!Number.isInteger(attempt) || attempt < 1) return refuse(c, 400, "attempt must be a positive integer");
935
- return c.json({ attempt, events: core.taskLogForSurface(core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt)) });
947
+ return c.json({ attempt, events: core.taskLogForSurface(core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt)), position: pool2.streamPosition(c.req.param("id")) });
936
948
  });
937
949
  router.delete("/:id", (c) => {
938
950
  const ws = c.req.query("workspace") || process.cwd();
@@ -948,6 +960,9 @@ function sessionsRoute(pool2) {
948
960
  return router;
949
961
  }
950
962
  var NO_CHECKPOINT = "The task is not waiting at a checkpoint: it was answered already, withdrawn, or has no runner left to hear it.";
963
+ function idOfRef(session, ref) {
964
+ return core.resolveUserTaskRef(session.planState?.tasks ?? [], ref)?.id ?? ref;
965
+ }
951
966
  async function runUserCommand(session, command) {
952
967
  const outcome = await session.execute(command, { kind: "user" });
953
968
  if (!outcome.ok) throw new core.TaskControlError(outcome.message);
@@ -974,9 +989,8 @@ function plansRoute(pool2) {
974
989
  try {
975
990
  const session = pool2.session(c.req.param("sessionId"));
976
991
  if (!session.planState || session.planTasks.length === 0) throw new core.NoPlanError("execute");
977
- if (session.isExecuting) throw new core.AlreadyExecutingError();
978
- await runUserCommand(session, { command: "run_plan" });
979
- return c.json({ status: "running" });
992
+ const { message } = await runUserCommand(session, { command: "run_plan" });
993
+ return c.json({ status: "running", message });
980
994
  } catch (err) {
981
995
  return failure(c, err, "execute");
982
996
  }
@@ -987,6 +1001,7 @@ function plansRoute(pool2) {
987
1001
  const session = pool2.session(c.req.param("sessionId"));
988
1002
  const outcome = await session.execute({ command: "stop_run" }, { kind: "user" });
989
1003
  if (!outcome.ok && (session.isExecuting || session.startsPending)) throw new core.TaskControlError(outcome.message);
1004
+ return c.json({ status: "stopped", message: outcome.message });
990
1005
  }
991
1006
  return c.json({ status: "stopped" });
992
1007
  } catch (err) {
@@ -997,47 +1012,28 @@ function plansRoute(pool2) {
997
1012
  const cancelled = pool2.cancelPlanning(c.req.param("sessionId"));
998
1013
  return c.json({ cancelled });
999
1014
  });
1000
- router.post("/:sessionId/tasks/:taskId/complete", async (c) => {
1001
- try {
1002
- await pool2.session(c.req.param("sessionId")).markTaskComplete(c.req.param("taskId"));
1003
- return c.json({ ok: true });
1004
- } catch (err) {
1005
- return failure(c, err, "mark complete", { fallback: 404, message: "Not found" });
1006
- }
1007
- });
1008
- router.post("/:sessionId/tasks/:taskId/uncomplete", async (c) => {
1015
+ router.post("/:sessionId/tasks/:taskId/resume/cancel", async (c) => {
1009
1016
  try {
1010
- await pool2.session(c.req.param("sessionId")).markTaskIncomplete(c.req.param("taskId"));
1011
- return c.json({ ok: true });
1012
- } catch (err) {
1013
- return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
1014
- }
1015
- });
1016
- router.post("/:sessionId/tasks/:taskId/resume/cancel", (c) => {
1017
- try {
1018
- const cancelled = pool2.session(c.req.param("sessionId")).cancelScheduledResume(c.req.param("taskId"));
1019
- return c.json({ cancelled });
1017
+ const { message } = await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "cancel_resume", task: c.req.param("taskId") });
1018
+ return c.json({ cancelled: true, message });
1020
1019
  } catch (err) {
1021
- return failure(c, err, "cancel resume", { fallback: 404, message: "Not found" });
1022
- }
1023
- });
1024
- router.post("/:sessionId/tasks/:taskId/resume/now", async (c) => {
1025
- try {
1026
- await pool2.session(c.req.param("sessionId")).resumeNow(c.req.param("taskId"));
1027
- return c.json({ ok: true });
1028
- } catch (err) {
1029
- return failure(c, err, "resume now", { fallback: 404, message: "Not found" });
1020
+ return failure(c, err, "cancel resume");
1030
1021
  }
1031
1022
  });
1032
1023
  for (const [segment, command] of [
1033
1024
  ["run", (id) => ({ command: "run_task", task: id })],
1034
1025
  ["retry", (id) => ({ command: "retry_task", task: id, from: "fresh" })],
1035
- ["cancel", (id) => ({ command: "stop_task", task: id })]
1026
+ ["cancel", (id) => ({ command: "stop_task", task: id })],
1027
+ ["complete", (id) => ({ command: "mark_complete", task: id })],
1028
+ ["uncomplete", (id) => ({ command: "mark_not_done", task: id })],
1029
+ // The user says the limit has reset early: the task, and every other one waiting on its runner's reset, resumes in its saved session now.
1030
+ ["resume/now", (id) => ({ command: "retry_task", task: id, from: "session" })],
1031
+ ["interrupt", (id) => ({ command: "interrupt_task", task: id })]
1036
1032
  ]) {
1037
1033
  router.post(`/:sessionId/tasks/:taskId/${segment}`, async (c) => {
1038
1034
  try {
1039
- await runUserCommand(pool2.session(c.req.param("sessionId")), command(c.req.param("taskId")));
1040
- return c.json({ ok: true });
1035
+ const { message } = await runUserCommand(pool2.session(c.req.param("sessionId")), command(c.req.param("taskId")));
1036
+ return c.json({ ok: true, message });
1041
1037
  } catch (err) {
1042
1038
  return failure(c, err, segment);
1043
1039
  }
@@ -1047,16 +1043,16 @@ function plansRoute(pool2) {
1047
1043
  try {
1048
1044
  const body = await c.req.json().catch(() => ({}));
1049
1045
  const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason : void 0;
1050
- await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "skip_task", task: c.req.param("taskId"), ...reason ? { reason } : {} });
1051
- return c.json({ ok: true });
1046
+ const { message } = await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "skip_task", task: c.req.param("taskId"), ...reason ? { reason } : {} });
1047
+ return c.json({ ok: true, message });
1052
1048
  } catch (err) {
1053
1049
  return failure(c, err, "skip");
1054
1050
  }
1055
1051
  });
1056
1052
  router.post("/:sessionId/tasks/:taskId/force-start", async (c) => {
1057
1053
  try {
1058
- await pool2.session(c.req.param("sessionId")).forceStartTask(c.req.param("taskId"));
1059
- return c.json({ ok: true });
1054
+ const { message, payload } = await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "force_start", task: c.req.param("taskId") });
1055
+ return c.json({ ok: true, ...payload?.kind === "queued" ? { queued: payload.line } : {}, message });
1060
1056
  } catch (err) {
1061
1057
  return failure(c, err, "force-start");
1062
1058
  }
@@ -1072,8 +1068,12 @@ function plansRoute(pool2) {
1072
1068
  return failure(c, err, "task edit");
1073
1069
  }
1074
1070
  };
1075
- router.post("/:sessionId/tasks/:taskId/messages", messageHandler(async (session, taskId, text) => (await runUserCommand(session, { command: "message_task", task: taskId, text })).id ?? ""));
1076
- router.post("/:sessionId/tasks/:taskId/messages/now", messageHandler((session, taskId, text) => session.forceSendTaskMessage(taskId, text)));
1071
+ const sendMessage = (force) => messageHandler(async (session, taskId, text) => {
1072
+ const { payload } = await runUserCommand(session, { command: "message_task", task: taskId, text, ...force ? { force } : {} });
1073
+ return payload?.kind === "message" ? payload.id : "";
1074
+ });
1075
+ router.post("/:sessionId/tasks/:taskId/messages", sendMessage(false));
1076
+ router.post("/:sessionId/tasks/:taskId/messages/now", sendMessage(true));
1077
1077
  router.post("/:sessionId/tasks/:taskId/commands", async (c) => {
1078
1078
  try {
1079
1079
  const body = await c.req.json().catch(() => ({}));
@@ -1086,10 +1086,10 @@ function plansRoute(pool2) {
1086
1086
  return failure(c, err, "task edit");
1087
1087
  }
1088
1088
  });
1089
- router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
1089
+ router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", async (c) => {
1090
1090
  try {
1091
- const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
1092
- return c.json({ sent });
1091
+ const { message } = await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "force_send_queued", task: c.req.param("taskId"), id: c.req.param("messageId") });
1092
+ return c.json({ sent: true, message });
1093
1093
  } catch (err) {
1094
1094
  return failure(c, err, "task edit");
1095
1095
  }
@@ -1099,57 +1099,40 @@ function plansRoute(pool2) {
1099
1099
  const body = await c.req.json().catch(() => ({}));
1100
1100
  const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
1101
1101
  if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
1102
- await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "retry_task", task: c.req.param("taskId"), from: "session", message: text });
1103
- return c.json({ ok: true });
1104
- } catch (err) {
1105
- return failure(c, err, "task edit");
1106
- }
1107
- });
1108
- router.delete("/:sessionId/tasks/:taskId/messages/:messageId", (c) => {
1109
- try {
1110
- const removed = pool2.session(c.req.param("sessionId")).removeQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
1111
- return c.json({ removed });
1112
- } catch (err) {
1113
- return failure(c, err, "task edit");
1114
- }
1115
- });
1116
- router.post("/:sessionId/tasks/:taskId/interrupt", async (c) => {
1117
- try {
1118
- await pool2.session(c.req.param("sessionId")).interruptTask(c.req.param("taskId"));
1119
- return c.json({ ok: true });
1120
- } catch (err) {
1121
- return failure(c, err, "task edit");
1122
- }
1123
- });
1124
- router.post("/:sessionId/tasks/:taskId/checkpoint/approve", (c) => {
1125
- try {
1126
- const session = pool2.session(c.req.param("sessionId"));
1127
- const taskId = c.req.param("taskId");
1128
- if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1129
- session.approveCheckpoint(taskId);
1130
- return c.json({ ok: true });
1102
+ const { message } = await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "retry_task", task: c.req.param("taskId"), from: "session", message: text });
1103
+ return c.json({ ok: true, message });
1131
1104
  } catch (err) {
1132
1105
  return failure(c, err, "task edit");
1133
1106
  }
1134
1107
  });
1135
- router.post("/:sessionId/tasks/:taskId/checkpoint/reject", async (c) => {
1108
+ router.delete("/:sessionId/tasks/:taskId/messages/:messageId", async (c) => {
1136
1109
  try {
1137
- const body = await c.req.json().catch(() => ({}));
1138
- const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
1139
- const session = pool2.session(c.req.param("sessionId"));
1140
- const taskId = c.req.param("taskId");
1141
- if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1142
- session.rejectCheckpoint(taskId, reason || void 0);
1143
- return c.json({ ok: true });
1110
+ const { message } = await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "remove_queued", task: c.req.param("taskId"), id: c.req.param("messageId") });
1111
+ return c.json({ removed: true, message });
1144
1112
  } catch (err) {
1145
1113
  return failure(c, err, "task edit");
1146
1114
  }
1147
1115
  });
1116
+ for (const answer of ["approve", "reject"]) {
1117
+ router.post(`/:sessionId/tasks/:taskId/checkpoint/${answer}`, async (c) => {
1118
+ try {
1119
+ const body = answer === "reject" ? await c.req.json().catch(() => ({})) : {};
1120
+ const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
1121
+ const session = pool2.session(c.req.param("sessionId"));
1122
+ const taskId = idOfRef(session, c.req.param("taskId"));
1123
+ if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1124
+ const { message } = await runUserCommand(session, { command: "answer_checkpoint", task: taskId, answer, ...reason ? { reason } : {} });
1125
+ return c.json({ ok: true, message });
1126
+ } catch (err) {
1127
+ return failure(c, err, "task edit");
1128
+ }
1129
+ });
1130
+ }
1148
1131
  router.get("/:sessionId/tasks/:taskId/merge-gate", (c) => {
1149
1132
  try {
1150
1133
  const session = pool2.session(c.req.param("sessionId"));
1151
1134
  const tasks = core.flattenTasks(session.planState?.tasks ?? []);
1152
- const mergeGate = session.mergeGate(c.req.param("taskId")).map((id) => {
1135
+ const mergeGate = session.mergeGate(idOfRef(session, c.req.param("taskId"))).map((id) => {
1153
1136
  const dep = tasks.find((t) => t.id === id);
1154
1137
  return { id, order: dep?.order ?? 0, title: dep?.title ?? id };
1155
1138
  });
@@ -1167,7 +1150,10 @@ function plansRoute(pool2) {
1167
1150
  });
1168
1151
  router.post("/:sessionId/isolation/merge", async (c) => {
1169
1152
  try {
1170
- return c.json(await pool2.session(c.req.param("sessionId")).mergeRun());
1153
+ const outcome = await pool2.session(c.req.param("sessionId")).execute({ command: "merge_all" }, { kind: "user" });
1154
+ if (outcome.payload?.kind === "merge") return c.json(outcome.payload.result);
1155
+ if (!outcome.ok) throw new core.TaskControlError(outcome.message);
1156
+ throw new Error("Merge all answered without its result.");
1171
1157
  } catch (err) {
1172
1158
  return failure(c, err, "task edit");
1173
1159
  }
@@ -1197,21 +1183,9 @@ function plansRoute(pool2) {
1197
1183
  });
1198
1184
  router.put("/:sessionId/tasks/:taskId", async (c) => {
1199
1185
  try {
1200
- const { assignedRunner, dependencies, ...rest } = await c.req.json();
1201
- const session = pool2.session(c.req.param("sessionId"));
1202
- const taskId = c.req.param("taskId");
1203
- const hasRunner = typeof assignedRunner === "string";
1204
- const hasDeps = Array.isArray(dependencies);
1205
- let result = null;
1206
- if (hasRunner) result = await session.setTaskRunner(taskId, assignedRunner);
1207
- if (hasDeps) {
1208
- try {
1209
- result = await session.setTaskDependencies(taskId, dependencies.map(String));
1210
- } catch (err) {
1211
- return failure(c, err, "task dependencies", { fallback: 400 });
1212
- }
1213
- }
1214
- if (Object.keys(rest).length > 0 || !hasRunner && !hasDeps) result = await session.updateTask(taskId, rest);
1186
+ const changes = await c.req.json();
1187
+ if (changes.assignedModel === null) changes.assignedModel = void 0;
1188
+ const result = await pool2.session(c.req.param("sessionId")).updateTask(c.req.param("taskId"), changes);
1215
1189
  if (!result) return refuse(c, 404, "Task not found", "task_not_found");
1216
1190
  return c.json({ ok: true });
1217
1191
  } catch (err) {
@@ -1239,8 +1213,11 @@ function plansRoute(pool2) {
1239
1213
  });
1240
1214
  router.post("/:sessionId/review/approve", async (c) => {
1241
1215
  try {
1242
- const plan = await pool2.session(c.req.param("sessionId")).approveReview();
1243
- return c.json({ plan: core.surfacePlan(plan) });
1216
+ const session = pool2.session(c.req.param("sessionId"));
1217
+ const plan = session.planState;
1218
+ if (!plan) throw new core.NoPlanError("review");
1219
+ const { message } = await runUserCommand(session, { command: "approve_review" });
1220
+ return c.json({ plan: core.surfacePlan(plan), message });
1244
1221
  } catch (err) {
1245
1222
  return failure(c, err, "review approve");
1246
1223
  }
@@ -1297,6 +1274,14 @@ function plansRoute(pool2) {
1297
1274
  return failure(c, err, "conversation message");
1298
1275
  }
1299
1276
  });
1277
+ router.post("/:sessionId/converse/resend/cancel", (c) => {
1278
+ try {
1279
+ const cancelled = pool2.session(c.req.param("sessionId")).cancelPlannerResend();
1280
+ return c.json({ cancelled });
1281
+ } catch (err) {
1282
+ return failure(c, err, "cancel resend", { fallback: 404, message: "Not found" });
1283
+ }
1284
+ });
1300
1285
  router.post("/:sessionId/conversation/fork", (c) => {
1301
1286
  try {
1302
1287
  const fork = pool2.forkConversation(c.req.param("sessionId"));
@@ -1397,9 +1382,9 @@ function approvalsRoute(pool2) {
1397
1382
  const answer = answerOf(await c.req.json().catch(() => null));
1398
1383
  const decided = core.toApprovalDecision(answer);
1399
1384
  const request = pool2.outstandingApprovals(sessionId).find((pending) => pending.id === approvalId)?.request;
1400
- if (decided.decision === "deny" && request?.taskId) {
1385
+ if (request?.taskId) {
1401
1386
  const outcome = await pool2.session(sessionId).execute(
1402
- { command: "answer_approval", task: request.taskId, approval: approvalId, answer: "deny", ...decided.note ? { note: decided.note } : {} },
1387
+ { command: "answer_approval", task: request.taskId, approval: approvalId, answer: decided.decision, ...decided.decision === "deny" && decided.note ? { note: decided.note } : {} },
1403
1388
  { kind: "user" }
1404
1389
  );
1405
1390
  if (!outcome.ok) return refuse(c, 400, outcome.message, "refused");