@ordewell/web 0.10.5 → 0.10.7

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.
@@ -922,27 +922,29 @@ function sessionsRoute(pool2) {
922
922
  const id = c.req.param("id");
923
923
  const saved = core.loadSessionPlanState(id, ws);
924
924
  if (!saved) return refuse(c, 404, "Session not found", "session_not_found");
925
- 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 } : {} });
926
927
  });
927
928
  router.post("/:id/load", (c) => {
928
929
  const ws = c.req.query("workspace") || process.cwd();
929
930
  try {
930
931
  const id = c.req.param("id");
931
932
  const plan = pool2.adoptSavedSession(id, ws);
932
- 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 } : {} });
933
935
  } catch (err) {
934
936
  return failure(c, err, "load session", { message: "Failed to load session" });
935
937
  }
936
938
  });
937
939
  router.get("/:id/tasks/:taskId/log", (c) => {
938
940
  const ws = c.req.query("workspace") || process.cwd();
939
- 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")) });
940
942
  });
941
943
  router.get("/:id/tasks/:taskId/log/:attempt", (c) => {
942
944
  const ws = c.req.query("workspace") || process.cwd();
943
945
  const attempt = Number(c.req.param("attempt"));
944
946
  if (!Number.isInteger(attempt) || attempt < 1) return refuse(c, 400, "attempt must be a positive integer");
945
- 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")) });
946
948
  });
947
949
  router.delete("/:id", (c) => {
948
950
  const ws = c.req.query("workspace") || process.cwd();
@@ -958,6 +960,9 @@ function sessionsRoute(pool2) {
958
960
  return router;
959
961
  }
960
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
+ }
961
966
  async function runUserCommand(session, command) {
962
967
  const outcome = await session.execute(command, { kind: "user" });
963
968
  if (!outcome.ok) throw new core.TaskControlError(outcome.message);
@@ -984,8 +989,8 @@ function plansRoute(pool2) {
984
989
  try {
985
990
  const session = pool2.session(c.req.param("sessionId"));
986
991
  if (!session.planState || session.planTasks.length === 0) throw new core.NoPlanError("execute");
987
- await runUserCommand(session, { command: "run_plan" });
988
- return c.json({ status: "running" });
992
+ const { message } = await runUserCommand(session, { command: "run_plan" });
993
+ return c.json({ status: "running", message });
989
994
  } catch (err) {
990
995
  return failure(c, err, "execute");
991
996
  }
@@ -996,6 +1001,7 @@ function plansRoute(pool2) {
996
1001
  const session = pool2.session(c.req.param("sessionId"));
997
1002
  const outcome = await session.execute({ command: "stop_run" }, { kind: "user" });
998
1003
  if (!outcome.ok && (session.isExecuting || session.startsPending)) throw new core.TaskControlError(outcome.message);
1004
+ return c.json({ status: "stopped", message: outcome.message });
999
1005
  }
1000
1006
  return c.json({ status: "stopped" });
1001
1007
  } catch (err) {
@@ -1006,47 +1012,28 @@ function plansRoute(pool2) {
1006
1012
  const cancelled = pool2.cancelPlanning(c.req.param("sessionId"));
1007
1013
  return c.json({ cancelled });
1008
1014
  });
1009
- router.post("/:sessionId/tasks/:taskId/complete", async (c) => {
1010
- try {
1011
- await pool2.session(c.req.param("sessionId")).markTaskComplete(c.req.param("taskId"));
1012
- return c.json({ ok: true });
1013
- } catch (err) {
1014
- return failure(c, err, "mark complete", { fallback: 404, message: "Not found" });
1015
- }
1016
- });
1017
- router.post("/:sessionId/tasks/:taskId/uncomplete", async (c) => {
1018
- try {
1019
- await pool2.session(c.req.param("sessionId")).markTaskIncomplete(c.req.param("taskId"));
1020
- return c.json({ ok: true });
1021
- } catch (err) {
1022
- return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
1023
- }
1024
- });
1025
- router.post("/:sessionId/tasks/:taskId/resume/cancel", (c) => {
1015
+ router.post("/:sessionId/tasks/:taskId/resume/cancel", async (c) => {
1026
1016
  try {
1027
- const cancelled = pool2.session(c.req.param("sessionId")).cancelScheduledResume(c.req.param("taskId"));
1028
- 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 });
1029
1019
  } catch (err) {
1030
- return failure(c, err, "cancel resume", { fallback: 404, message: "Not found" });
1031
- }
1032
- });
1033
- router.post("/:sessionId/tasks/:taskId/resume/now", async (c) => {
1034
- try {
1035
- await pool2.session(c.req.param("sessionId")).resumeNow(c.req.param("taskId"));
1036
- return c.json({ ok: true });
1037
- } catch (err) {
1038
- return failure(c, err, "resume now", { fallback: 404, message: "Not found" });
1020
+ return failure(c, err, "cancel resume");
1039
1021
  }
1040
1022
  });
1041
1023
  for (const [segment, command] of [
1042
1024
  ["run", (id) => ({ command: "run_task", task: id })],
1043
1025
  ["retry", (id) => ({ command: "retry_task", task: id, from: "fresh" })],
1044
- ["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 })]
1045
1032
  ]) {
1046
1033
  router.post(`/:sessionId/tasks/:taskId/${segment}`, async (c) => {
1047
1034
  try {
1048
- await runUserCommand(pool2.session(c.req.param("sessionId")), command(c.req.param("taskId")));
1049
- 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 });
1050
1037
  } catch (err) {
1051
1038
  return failure(c, err, segment);
1052
1039
  }
@@ -1056,16 +1043,16 @@ function plansRoute(pool2) {
1056
1043
  try {
1057
1044
  const body = await c.req.json().catch(() => ({}));
1058
1045
  const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason : void 0;
1059
- await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "skip_task", task: c.req.param("taskId"), ...reason ? { reason } : {} });
1060
- 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 });
1061
1048
  } catch (err) {
1062
1049
  return failure(c, err, "skip");
1063
1050
  }
1064
1051
  });
1065
1052
  router.post("/:sessionId/tasks/:taskId/force-start", async (c) => {
1066
1053
  try {
1067
- const queued = await pool2.session(c.req.param("sessionId")).forceStartTask(c.req.param("taskId"));
1068
- return c.json({ ok: true, ...queued ? { queued } : {} });
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 });
1069
1056
  } catch (err) {
1070
1057
  return failure(c, err, "force-start");
1071
1058
  }
@@ -1081,8 +1068,12 @@ function plansRoute(pool2) {
1081
1068
  return failure(c, err, "task edit");
1082
1069
  }
1083
1070
  };
1084
- router.post("/:sessionId/tasks/:taskId/messages", messageHandler(async (session, taskId, text) => (await runUserCommand(session, { command: "message_task", task: taskId, text })).id ?? ""));
1085
- 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));
1086
1077
  router.post("/:sessionId/tasks/:taskId/commands", async (c) => {
1087
1078
  try {
1088
1079
  const body = await c.req.json().catch(() => ({}));
@@ -1095,10 +1086,10 @@ function plansRoute(pool2) {
1095
1086
  return failure(c, err, "task edit");
1096
1087
  }
1097
1088
  });
1098
- router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
1089
+ router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", async (c) => {
1099
1090
  try {
1100
- const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
1101
- 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 });
1102
1093
  } catch (err) {
1103
1094
  return failure(c, err, "task edit");
1104
1095
  }
@@ -1108,57 +1099,40 @@ function plansRoute(pool2) {
1108
1099
  const body = await c.req.json().catch(() => ({}));
1109
1100
  const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
1110
1101
  if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
1111
- await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "retry_task", task: c.req.param("taskId"), from: "session", message: text });
1112
- return c.json({ ok: true });
1113
- } catch (err) {
1114
- return failure(c, err, "task edit");
1115
- }
1116
- });
1117
- router.delete("/:sessionId/tasks/:taskId/messages/:messageId", (c) => {
1118
- try {
1119
- const removed = pool2.session(c.req.param("sessionId")).removeQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
1120
- return c.json({ removed });
1121
- } catch (err) {
1122
- return failure(c, err, "task edit");
1123
- }
1124
- });
1125
- router.post("/:sessionId/tasks/:taskId/interrupt", async (c) => {
1126
- try {
1127
- await pool2.session(c.req.param("sessionId")).interruptTask(c.req.param("taskId"));
1128
- 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 });
1129
1104
  } catch (err) {
1130
1105
  return failure(c, err, "task edit");
1131
1106
  }
1132
1107
  });
1133
- router.post("/:sessionId/tasks/:taskId/checkpoint/approve", (c) => {
1108
+ router.delete("/:sessionId/tasks/:taskId/messages/:messageId", async (c) => {
1134
1109
  try {
1135
- const session = pool2.session(c.req.param("sessionId"));
1136
- const taskId = c.req.param("taskId");
1137
- if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1138
- session.approveCheckpoint(taskId);
1139
- return c.json({ ok: true });
1140
- } catch (err) {
1141
- return failure(c, err, "task edit");
1142
- }
1143
- });
1144
- router.post("/:sessionId/tasks/:taskId/checkpoint/reject", async (c) => {
1145
- try {
1146
- const body = await c.req.json().catch(() => ({}));
1147
- const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
1148
- const session = pool2.session(c.req.param("sessionId"));
1149
- const taskId = c.req.param("taskId");
1150
- if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
1151
- session.rejectCheckpoint(taskId, reason || void 0);
1152
- 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 });
1153
1112
  } catch (err) {
1154
1113
  return failure(c, err, "task edit");
1155
1114
  }
1156
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
+ }
1157
1131
  router.get("/:sessionId/tasks/:taskId/merge-gate", (c) => {
1158
1132
  try {
1159
1133
  const session = pool2.session(c.req.param("sessionId"));
1160
1134
  const tasks = core.flattenTasks(session.planState?.tasks ?? []);
1161
- const mergeGate = session.mergeGate(c.req.param("taskId")).map((id) => {
1135
+ const mergeGate = session.mergeGate(idOfRef(session, c.req.param("taskId"))).map((id) => {
1162
1136
  const dep = tasks.find((t) => t.id === id);
1163
1137
  return { id, order: dep?.order ?? 0, title: dep?.title ?? id };
1164
1138
  });
@@ -1176,7 +1150,10 @@ function plansRoute(pool2) {
1176
1150
  });
1177
1151
  router.post("/:sessionId/isolation/merge", async (c) => {
1178
1152
  try {
1179
- 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.");
1180
1157
  } catch (err) {
1181
1158
  return failure(c, err, "task edit");
1182
1159
  }
@@ -1206,22 +1183,9 @@ function plansRoute(pool2) {
1206
1183
  });
1207
1184
  router.put("/:sessionId/tasks/:taskId", async (c) => {
1208
1185
  try {
1209
- const { assignedRunner, dependencies, ...rest } = await c.req.json();
1210
- if (rest.assignedModel === null) rest.assignedModel = void 0;
1211
- const session = pool2.session(c.req.param("sessionId"));
1212
- const taskId = c.req.param("taskId");
1213
- const hasRunner = typeof assignedRunner === "string";
1214
- const hasDeps = Array.isArray(dependencies);
1215
- let result = null;
1216
- if (hasRunner) result = await session.setTaskRunner(taskId, assignedRunner);
1217
- if (hasDeps) {
1218
- try {
1219
- result = await session.setTaskDependencies(taskId, dependencies.map(String));
1220
- } catch (err) {
1221
- return failure(c, err, "task dependencies", { fallback: 400 });
1222
- }
1223
- }
1224
- 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);
1225
1189
  if (!result) return refuse(c, 404, "Task not found", "task_not_found");
1226
1190
  return c.json({ ok: true });
1227
1191
  } catch (err) {
@@ -1249,8 +1213,11 @@ function plansRoute(pool2) {
1249
1213
  });
1250
1214
  router.post("/:sessionId/review/approve", async (c) => {
1251
1215
  try {
1252
- const plan = await pool2.session(c.req.param("sessionId")).approveReview();
1253
- 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 });
1254
1221
  } catch (err) {
1255
1222
  return failure(c, err, "review approve");
1256
1223
  }
@@ -1415,9 +1382,9 @@ function approvalsRoute(pool2) {
1415
1382
  const answer = answerOf(await c.req.json().catch(() => null));
1416
1383
  const decided = core.toApprovalDecision(answer);
1417
1384
  const request = pool2.outstandingApprovals(sessionId).find((pending) => pending.id === approvalId)?.request;
1418
- if (decided.decision === "deny" && request?.taskId) {
1385
+ if (request?.taskId) {
1419
1386
  const outcome = await pool2.session(sessionId).execute(
1420
- { 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 } : {} },
1421
1388
  { kind: "user" }
1422
1389
  );
1423
1390
  if (!outcome.ok) return refuse(c, 400, outcome.message, "refused");