@ordewell/web 0.7.0 → 0.7.2
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/server/main.js +141 -188
- package/dist/server/main.js.map +1 -1
- package/package.json +2 -2
package/dist/server/main.js
CHANGED
|
@@ -103,7 +103,7 @@ var WebConfig = class extends core.BaseConfig {
|
|
|
103
103
|
if (process.env.ORDEWELL_OPENCODE_ENABLED === "true" || process.env.ORDEWELL_OPENCODE_ENABLED === "1") {
|
|
104
104
|
out.push("opencode");
|
|
105
105
|
}
|
|
106
|
-
return out.length > 0 ? out : [
|
|
106
|
+
return out.length > 0 ? out : [...core.DEFAULT_RUNNERS];
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
109
|
function scanWorkspacesImpl(deps) {
|
|
@@ -416,10 +416,6 @@ var AdvisingRunner = class {
|
|
|
416
416
|
var OrchestratorPool = class {
|
|
417
417
|
sessions = /* @__PURE__ */ new Map();
|
|
418
418
|
clients = /* @__PURE__ */ new Map();
|
|
419
|
-
/** The in-flight planning turn's abort controller, one per session — see `cancelPlanning`. */
|
|
420
|
-
planningAborts = /* @__PURE__ */ new Map();
|
|
421
|
-
/** Sessions whose planning slot a compaction holds; a reply must not take it over. */
|
|
422
|
-
compacting = /* @__PURE__ */ new Set();
|
|
423
419
|
registry = (() => {
|
|
424
420
|
const r = new core.RunnerRegistry();
|
|
425
421
|
r.loadUserPlugins();
|
|
@@ -619,7 +615,11 @@ var OrchestratorPool = class {
|
|
|
619
615
|
if (core.PROVIDER_CREDENTIAL_ENV.has(key)) touched = true;
|
|
620
616
|
}
|
|
621
617
|
if (envChanges.ORDEWELL_MAX_PARALLEL !== void 0) {
|
|
622
|
-
for (const session of this.sessions
|
|
618
|
+
for (const [id, session] of this.sessions) {
|
|
619
|
+
session.reschedule().catch((err) => {
|
|
620
|
+
console.error(`[web] reschedule failed for session ${id}: ${err instanceof Error ? err.message : err}`);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
623
|
}
|
|
624
624
|
if (touched) {
|
|
625
625
|
this.modelResolver.invalidate();
|
|
@@ -655,6 +655,7 @@ var OrchestratorPool = class {
|
|
|
655
655
|
const rejectedEnvKeys = admission2?.rejected ?? [];
|
|
656
656
|
const settings = this.getSettings();
|
|
657
657
|
if (rejectedEnvKeys.length > 0) settings.rejectedEnvKeys = rejectedEnvKeys;
|
|
658
|
+
if (switchRecall) settings.switchRecall = switchRecall;
|
|
658
659
|
return settings;
|
|
659
660
|
}
|
|
660
661
|
setOrchestratorModel(model) {
|
|
@@ -683,18 +684,7 @@ var OrchestratorPool = class {
|
|
|
683
684
|
}
|
|
684
685
|
}
|
|
685
686
|
}
|
|
686
|
-
const providers =
|
|
687
|
-
for (const provider of core.PROVIDER_PRIORITY) {
|
|
688
|
-
if (provider === "openai_compatible") {
|
|
689
|
-
if (config.openaiCompatibleBaseUrl) providers.push(provider);
|
|
690
|
-
} else if (provider === "openrouter") {
|
|
691
|
-
if (config.openrouterKey) providers.push(provider);
|
|
692
|
-
} else if (provider === "google") {
|
|
693
|
-
if (config.geminiKey) providers.push(provider);
|
|
694
|
-
} else if (config.getProviderApiKey(provider)) {
|
|
695
|
-
providers.push(provider);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
687
|
+
const providers = core.configuredProviders(config);
|
|
698
688
|
return {
|
|
699
689
|
models: allModels,
|
|
700
690
|
modelsByRunner,
|
|
@@ -729,15 +719,9 @@ var OrchestratorPool = class {
|
|
|
729
719
|
}
|
|
730
720
|
async generatePlan(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
731
721
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
722
|
+
this.destroy(sessionId);
|
|
732
723
|
this.sessions.set(sessionId, session);
|
|
733
|
-
|
|
734
|
-
this.planningAborts.set(sessionId, controller);
|
|
735
|
-
try {
|
|
736
|
-
const plan = await session.generatePlan(goal, runners, { signal: controller.signal });
|
|
737
|
-
return core.migratePlanState(plan);
|
|
738
|
-
} finally {
|
|
739
|
-
this.clearPlanningAbort(sessionId, controller);
|
|
740
|
-
}
|
|
724
|
+
return core.migratePlanState(await session.generatePlan(goal, runners));
|
|
741
725
|
}
|
|
742
726
|
/**
|
|
743
727
|
* Conversational planning (ADR-0002): start the planner dialogue for a
|
|
@@ -747,69 +731,27 @@ var OrchestratorPool = class {
|
|
|
747
731
|
*/
|
|
748
732
|
async startPlanning(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
749
733
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
734
|
+
this.destroy(sessionId);
|
|
750
735
|
this.sessions.set(sessionId, session);
|
|
751
|
-
|
|
752
|
-
this.planningAborts.set(sessionId, controller);
|
|
753
|
-
try {
|
|
754
|
-
return await session.startPlanning(goal, runners, { signal: controller.signal });
|
|
755
|
-
} finally {
|
|
756
|
-
this.clearPlanningAbort(sessionId, controller);
|
|
757
|
-
}
|
|
736
|
+
return session.startPlanning(goal, runners);
|
|
758
737
|
}
|
|
759
|
-
/**
|
|
760
|
-
* Continue the planner dialogue with the user's reply. Refused during a
|
|
761
|
-
* compaction before the abort slot is touched: the session refuses the reply
|
|
762
|
-
* too, but by then the compaction's controller would be gone and a stop could
|
|
763
|
-
* no longer reach its summary turn.
|
|
764
|
-
*/
|
|
738
|
+
/** Continue the planner dialogue with the user's reply; refused while a planner turn is in flight. */
|
|
765
739
|
async continuePlanning(sessionId, message) {
|
|
766
|
-
|
|
767
|
-
const controller = new AbortController();
|
|
768
|
-
this.planningAborts.set(sessionId, controller);
|
|
769
|
-
try {
|
|
770
|
-
return await this.session(sessionId).continueConversation(message, { signal: controller.signal });
|
|
771
|
-
} finally {
|
|
772
|
-
this.clearPlanningAbort(sessionId, controller);
|
|
773
|
-
}
|
|
740
|
+
return this.session(sessionId).continueConversation(message);
|
|
774
741
|
}
|
|
775
|
-
/**
|
|
776
|
-
* Condense a session's conversation. The summary turn is a planning turn as
|
|
777
|
-
* far as a surface is concerned, so it registers the same abort controller
|
|
778
|
-
* and `cancelPlanning` stops it — but only when no turn holds that slot
|
|
779
|
-
* already: taking it over would leave the reply in flight beyond stopping.
|
|
780
|
-
*/
|
|
742
|
+
/** Condense a session's conversation. The summary turn is a planner turn, so `cancelPlanning` stops it. */
|
|
781
743
|
async compactConversation(sessionId) {
|
|
782
744
|
const session = this.session(sessionId);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
this.planningAborts.set(sessionId, controller);
|
|
786
|
-
this.compacting.add(sessionId);
|
|
787
|
-
try {
|
|
788
|
-
const compaction = await session.compactConversation(controller.signal);
|
|
789
|
-
return { ...compaction, plan: session.planState };
|
|
790
|
-
} finally {
|
|
791
|
-
this.compacting.delete(sessionId);
|
|
792
|
-
this.clearPlanningAbort(sessionId, controller);
|
|
793
|
-
}
|
|
745
|
+
const compaction = await session.compactConversation();
|
|
746
|
+
return { ...compaction, plan: session.planState };
|
|
794
747
|
}
|
|
795
748
|
/**
|
|
796
|
-
* Abort the
|
|
749
|
+
* Abort the planner turn in flight for a session, if any. Answers whether
|
|
797
750
|
* there was one to cancel — the route reports that rather than 404ing a
|
|
798
751
|
* session that simply is not planning right now.
|
|
799
752
|
*/
|
|
800
753
|
cancelPlanning(sessionId) {
|
|
801
|
-
|
|
802
|
-
if (!controller) return false;
|
|
803
|
-
controller.abort();
|
|
804
|
-
return true;
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Only clears the map entry if it still holds the controller this turn
|
|
808
|
-
* created — a later turn may already have installed its own by the time
|
|
809
|
-
* this one's `finally` runs (a fresh request racing a slow abort).
|
|
810
|
-
*/
|
|
811
|
-
clearPlanningAbort(sessionId, controller) {
|
|
812
|
-
if (this.planningAborts.get(sessionId) === controller) this.planningAborts.delete(sessionId);
|
|
754
|
+
return this.sessions.get(sessionId)?.abortPlannerTurn() ?? false;
|
|
813
755
|
}
|
|
814
756
|
/**
|
|
815
757
|
* Register a session persisted on disk so its plan becomes executable and
|
|
@@ -824,9 +766,10 @@ var OrchestratorPool = class {
|
|
|
824
766
|
const live = this.sessions.get(sessionId);
|
|
825
767
|
if (live?.planState) return live.planState;
|
|
826
768
|
const saved = core.loadSession(sessionId, workspace);
|
|
827
|
-
if (!saved) throw new
|
|
769
|
+
if (!saved) throw new core.SessionNotFoundError();
|
|
828
770
|
const session = this.createSessionFor(sessionId, workspace);
|
|
829
771
|
session.loadPlan(saved.plan, saved.meta.goal, workspace, { sessionId: saved.meta.id });
|
|
772
|
+
this.destroy(sessionId);
|
|
830
773
|
this.sessions.set(sessionId, session);
|
|
831
774
|
return session.planState ?? saved.plan;
|
|
832
775
|
}
|
|
@@ -853,14 +796,10 @@ var OrchestratorPool = class {
|
|
|
853
796
|
*/
|
|
854
797
|
session(sessionId) {
|
|
855
798
|
const session = this.sessions.get(sessionId);
|
|
856
|
-
if (!session) throw new
|
|
799
|
+
if (!session) throw new core.SessionNotFoundError();
|
|
857
800
|
return session;
|
|
858
801
|
}
|
|
859
|
-
// The Session drops its planner context, but the model call in flight is
|
|
860
|
-
// the pool's to abort: it holds the only signal that reaches it.
|
|
861
802
|
destroy(sessionId) {
|
|
862
|
-
this.planningAborts.get(sessionId)?.abort();
|
|
863
|
-
this.planningAborts.delete(sessionId);
|
|
864
803
|
this.sessions.get(sessionId)?.destroy();
|
|
865
804
|
this.sessions.delete(sessionId);
|
|
866
805
|
}
|
|
@@ -875,6 +814,37 @@ function getSessionList(workspace) {
|
|
|
875
814
|
function removeSession(sessionId, workspace) {
|
|
876
815
|
return core.deleteSession(sessionId, workspace);
|
|
877
816
|
}
|
|
817
|
+
function classify(err) {
|
|
818
|
+
if (err instanceof core.SessionNotFoundError) return { status: 404, code: "session_not_found" };
|
|
819
|
+
if (err instanceof core.NoPlanError) return { status: 400, code: "no_plan" };
|
|
820
|
+
if (err instanceof core.AlreadyExecutingError) return { status: 409, code: "already_executing" };
|
|
821
|
+
if (err instanceof core.WorkspaceNotAProjectError) return { status: 400, code: "workspace_not_a_project" };
|
|
822
|
+
if (err instanceof core.WorkspaceNotFoundError) return { status: 400, code: "workspace_not_found" };
|
|
823
|
+
if (err instanceof core.PlannerTurnStoppedError) return { status: 409, code: "planner_turn_stopped" };
|
|
824
|
+
if (err instanceof core.PlannerTurnDiscardedError) return { status: 409, code: "planner_turn_discarded" };
|
|
825
|
+
if (err instanceof core.ConversationBusyError) return { status: 409, code: "conversation_busy" };
|
|
826
|
+
if (err instanceof core.ConversationEditError || err instanceof core.PlanEditError || err instanceof core.TaskControlError) {
|
|
827
|
+
return { status: 400, code: "refused" };
|
|
828
|
+
}
|
|
829
|
+
return void 0;
|
|
830
|
+
}
|
|
831
|
+
function refuse(c, status, message, code) {
|
|
832
|
+
const body = code ? { error: message, code } : { error: message };
|
|
833
|
+
return c.json(body, status);
|
|
834
|
+
}
|
|
835
|
+
function failure(c, err, label, { fallback = 500, message = "Internal error" } = {}) {
|
|
836
|
+
const classified = classify(err);
|
|
837
|
+
const status = classified?.status ?? fallback;
|
|
838
|
+
if (status >= 500) console.error(`[web] ${label} failed:`, err);
|
|
839
|
+
const body = {
|
|
840
|
+
error: err instanceof Error && err.message || message,
|
|
841
|
+
...classified ? { code: classified.code } : {},
|
|
842
|
+
...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {}
|
|
843
|
+
};
|
|
844
|
+
return c.json(body, status);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// server/routes/sessions.ts
|
|
878
848
|
function sessionsRoute(pool2) {
|
|
879
849
|
const router = new hono.Hono();
|
|
880
850
|
router.get("/", (c) => {
|
|
@@ -886,7 +856,7 @@ function sessionsRoute(pool2) {
|
|
|
886
856
|
const ws = c.req.query("workspace") || process.cwd();
|
|
887
857
|
const id = c.req.param("id");
|
|
888
858
|
const saved = core.loadSessionPlanState(id, ws);
|
|
889
|
-
if (!saved) return c
|
|
859
|
+
if (!saved) return refuse(c, 404, "Session not found", "session_not_found");
|
|
890
860
|
return c.json({ meta: saved.meta, plan: pool2.getPlanState(id) ?? saved.plan });
|
|
891
861
|
});
|
|
892
862
|
router.post("/:id/load", (c) => {
|
|
@@ -896,10 +866,7 @@ function sessionsRoute(pool2) {
|
|
|
896
866
|
const plan = pool2.adoptSavedSession(id, ws);
|
|
897
867
|
return c.json({ ok: true, plan, goal: pool2.getGoal(id) });
|
|
898
868
|
} catch (err) {
|
|
899
|
-
|
|
900
|
-
const message = err instanceof Error ? err.message : void 0;
|
|
901
|
-
const status = message === "Session not found" ? 404 : 500;
|
|
902
|
-
return c.json({ error: message ?? "Failed to load session" }, status);
|
|
869
|
+
return failure(c, err, "load session", { message: "Failed to load session" });
|
|
903
870
|
}
|
|
904
871
|
});
|
|
905
872
|
router.get("/:id/tasks/:taskId/log", (c) => {
|
|
@@ -909,7 +876,7 @@ function sessionsRoute(pool2) {
|
|
|
909
876
|
router.get("/:id/tasks/:taskId/log/:attempt", (c) => {
|
|
910
877
|
const ws = c.req.query("workspace") || process.cwd();
|
|
911
878
|
const attempt = Number(c.req.param("attempt"));
|
|
912
|
-
if (!Number.isInteger(attempt) || attempt < 1) return c
|
|
879
|
+
if (!Number.isInteger(attempt) || attempt < 1) return refuse(c, 400, "attempt must be a positive integer");
|
|
913
880
|
return c.json({ attempt, events: core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt) });
|
|
914
881
|
});
|
|
915
882
|
router.delete("/:id", (c) => {
|
|
@@ -923,21 +890,7 @@ function sessionsRoute(pool2) {
|
|
|
923
890
|
});
|
|
924
891
|
return router;
|
|
925
892
|
}
|
|
926
|
-
|
|
927
|
-
const e = err;
|
|
928
|
-
if (e.message === "Session not found") return c.json({ error: e.message }, 404);
|
|
929
|
-
if (e instanceof core.PlanEditError || e instanceof core.TaskControlError) return c.json({ error: e.message }, 400);
|
|
930
|
-
console.error("[plans] task edit failed:", err);
|
|
931
|
-
return c.json({ error: e.message || "Internal error" }, 500);
|
|
932
|
-
}
|
|
933
|
-
function conversationFailure(c, err) {
|
|
934
|
-
const e = err;
|
|
935
|
-
if (e.message === "Session not found") return c.json({ error: e.message }, 404);
|
|
936
|
-
if (e instanceof core.ConversationBusyError) return c.json({ error: e.message }, 409);
|
|
937
|
-
if (e instanceof core.ConversationEditError) return c.json({ error: e.message }, 400);
|
|
938
|
-
console.error("[plans] conversation edit failed:", err);
|
|
939
|
-
return c.json({ error: e.message || "Internal error" }, 500);
|
|
940
|
-
}
|
|
893
|
+
var NO_CHECKPOINT = "The task is not waiting at a checkpoint \u2014 it was answered already, withdrawn, or has no runner left to hear it.";
|
|
941
894
|
function plansRoute(pool2) {
|
|
942
895
|
const router = new hono.Hono();
|
|
943
896
|
router.post("/:sessionId/generate", async (c) => {
|
|
@@ -951,12 +904,7 @@ function plansRoute(pool2) {
|
|
|
951
904
|
const { models, modelsByRunner } = await pool2.getProviderModels();
|
|
952
905
|
return c.json({ plan, models, modelsByRunner });
|
|
953
906
|
} catch (err) {
|
|
954
|
-
|
|
955
|
-
if (err instanceof core.WorkspaceNotAProjectError) {
|
|
956
|
-
return c.json({ error: err.message, code: "workspace_not_a_project", workspace: err.workspace }, 400);
|
|
957
|
-
}
|
|
958
|
-
console.error("[plans] generate failed:", err);
|
|
959
|
-
return c.json({ error: err instanceof Error ? err.message : "Plan generation failed" }, 500);
|
|
907
|
+
return failure(c, err, "generate", { message: "Plan generation failed" });
|
|
960
908
|
}
|
|
961
909
|
});
|
|
962
910
|
router.post("/:sessionId/execute", async (c) => {
|
|
@@ -964,9 +912,7 @@ function plansRoute(pool2) {
|
|
|
964
912
|
await pool2.session(c.req.param("sessionId")).executePlan();
|
|
965
913
|
return c.json({ status: "running" });
|
|
966
914
|
} catch (err) {
|
|
967
|
-
|
|
968
|
-
const status = e.message === "Session not found" ? 404 : e.message === "No plan to execute" ? 400 : e.message === "Session already executing" ? 409 : 500;
|
|
969
|
-
return c.json({ error: e.message }, status);
|
|
915
|
+
return failure(c, err, "execute");
|
|
970
916
|
}
|
|
971
917
|
});
|
|
972
918
|
router.post("/:sessionId/stop", (c) => {
|
|
@@ -982,7 +928,7 @@ function plansRoute(pool2) {
|
|
|
982
928
|
await pool2.session(c.req.param("sessionId")).markTaskComplete(c.req.param("taskId"));
|
|
983
929
|
return c.json({ ok: true });
|
|
984
930
|
} catch (err) {
|
|
985
|
-
return c
|
|
931
|
+
return failure(c, err, "mark complete", { fallback: 404, message: "Not found" });
|
|
986
932
|
}
|
|
987
933
|
});
|
|
988
934
|
router.post("/:sessionId/tasks/:taskId/uncomplete", async (c) => {
|
|
@@ -990,7 +936,7 @@ function plansRoute(pool2) {
|
|
|
990
936
|
await pool2.session(c.req.param("sessionId")).markTaskIncomplete(c.req.param("taskId"));
|
|
991
937
|
return c.json({ ok: true });
|
|
992
938
|
} catch (err) {
|
|
993
|
-
return c
|
|
939
|
+
return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
|
|
994
940
|
}
|
|
995
941
|
});
|
|
996
942
|
for (const [segment, run2] of [
|
|
@@ -1004,20 +950,29 @@ function plansRoute(pool2) {
|
|
|
1004
950
|
await run2(pool2.session(c.req.param("sessionId")), c.req.param("taskId"));
|
|
1005
951
|
return c.json({ ok: true });
|
|
1006
952
|
} catch (err) {
|
|
1007
|
-
|
|
1008
|
-
return c.json({ error: err.message }, status);
|
|
953
|
+
return failure(c, err, segment);
|
|
1009
954
|
}
|
|
1010
955
|
});
|
|
1011
956
|
}
|
|
1012
|
-
|
|
957
|
+
const messageHandler = (send) => async (c) => {
|
|
1013
958
|
try {
|
|
1014
959
|
const body = await c.req.json().catch(() => ({}));
|
|
1015
960
|
const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
|
|
1016
961
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
|
|
1017
|
-
const id = pool2.session(c.req.param("sessionId"))
|
|
962
|
+
const id = send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
|
|
1018
963
|
return c.json({ id });
|
|
1019
964
|
} catch (err) {
|
|
1020
|
-
return
|
|
965
|
+
return failure(c, err, "task edit");
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
router.post("/:sessionId/tasks/:taskId/messages", messageHandler((session, taskId, text) => session.sendTaskMessage(taskId, text)));
|
|
969
|
+
router.post("/:sessionId/tasks/:taskId/messages/now", messageHandler((session, taskId, text) => session.forceSendTaskMessage(taskId, text)));
|
|
970
|
+
router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
|
|
971
|
+
try {
|
|
972
|
+
const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
973
|
+
return c.json({ sent });
|
|
974
|
+
} catch (err) {
|
|
975
|
+
return failure(c, err, "task edit");
|
|
1021
976
|
}
|
|
1022
977
|
});
|
|
1023
978
|
router.post("/:sessionId/tasks/:taskId/continue", async (c) => {
|
|
@@ -1028,7 +983,7 @@ function plansRoute(pool2) {
|
|
|
1028
983
|
await pool2.session(c.req.param("sessionId")).continueTask(c.req.param("taskId"), text);
|
|
1029
984
|
return c.json({ ok: true });
|
|
1030
985
|
} catch (err) {
|
|
1031
|
-
return
|
|
986
|
+
return failure(c, err, "task edit");
|
|
1032
987
|
}
|
|
1033
988
|
});
|
|
1034
989
|
router.delete("/:sessionId/tasks/:taskId/messages/:messageId", (c) => {
|
|
@@ -1036,7 +991,7 @@ function plansRoute(pool2) {
|
|
|
1036
991
|
const removed = pool2.session(c.req.param("sessionId")).removeQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
1037
992
|
return c.json({ removed });
|
|
1038
993
|
} catch (err) {
|
|
1039
|
-
return
|
|
994
|
+
return failure(c, err, "task edit");
|
|
1040
995
|
}
|
|
1041
996
|
});
|
|
1042
997
|
router.post("/:sessionId/tasks/:taskId/interrupt", async (c) => {
|
|
@@ -1044,7 +999,31 @@ function plansRoute(pool2) {
|
|
|
1044
999
|
await pool2.session(c.req.param("sessionId")).interruptTask(c.req.param("taskId"));
|
|
1045
1000
|
return c.json({ ok: true });
|
|
1046
1001
|
} catch (err) {
|
|
1047
|
-
return
|
|
1002
|
+
return failure(c, err, "task edit");
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
router.post("/:sessionId/tasks/:taskId/checkpoint/approve", (c) => {
|
|
1006
|
+
try {
|
|
1007
|
+
const session = pool2.session(c.req.param("sessionId"));
|
|
1008
|
+
const taskId = c.req.param("taskId");
|
|
1009
|
+
if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
|
|
1010
|
+
session.approveCheckpoint(taskId);
|
|
1011
|
+
return c.json({ ok: true });
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
return failure(c, err, "task edit");
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
router.post("/:sessionId/tasks/:taskId/checkpoint/reject", async (c) => {
|
|
1017
|
+
try {
|
|
1018
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1019
|
+
const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
|
|
1020
|
+
const session = pool2.session(c.req.param("sessionId"));
|
|
1021
|
+
const taskId = c.req.param("taskId");
|
|
1022
|
+
if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
|
|
1023
|
+
session.rejectCheckpoint(taskId, reason || void 0);
|
|
1024
|
+
return c.json({ ok: true });
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
return failure(c, err, "task edit");
|
|
1048
1027
|
}
|
|
1049
1028
|
});
|
|
1050
1029
|
router.get("/:sessionId/tasks/:taskId/merge-gate", (c) => {
|
|
@@ -1057,21 +1036,21 @@ function plansRoute(pool2) {
|
|
|
1057
1036
|
});
|
|
1058
1037
|
return c.json({ mergeGate });
|
|
1059
1038
|
} catch (err) {
|
|
1060
|
-
return
|
|
1039
|
+
return failure(c, err, "task edit");
|
|
1061
1040
|
}
|
|
1062
1041
|
});
|
|
1063
1042
|
router.get("/:sessionId/isolation/diff", async (c) => {
|
|
1064
1043
|
try {
|
|
1065
1044
|
return c.json({ diff: await pool2.session(c.req.param("sessionId")).reviewRunDiff() });
|
|
1066
1045
|
} catch (err) {
|
|
1067
|
-
return
|
|
1046
|
+
return failure(c, err, "task edit");
|
|
1068
1047
|
}
|
|
1069
1048
|
});
|
|
1070
1049
|
router.post("/:sessionId/isolation/merge", async (c) => {
|
|
1071
1050
|
try {
|
|
1072
1051
|
return c.json(await pool2.session(c.req.param("sessionId")).mergeRun());
|
|
1073
1052
|
} catch (err) {
|
|
1074
|
-
return
|
|
1053
|
+
return failure(c, err, "task edit");
|
|
1075
1054
|
}
|
|
1076
1055
|
});
|
|
1077
1056
|
for (const [segment, run2] of [
|
|
@@ -1085,7 +1064,7 @@ function plansRoute(pool2) {
|
|
|
1085
1064
|
await run2(pool2.session(c.req.param("sessionId")));
|
|
1086
1065
|
return c.json({ ok: true });
|
|
1087
1066
|
} catch (err) {
|
|
1088
|
-
return
|
|
1067
|
+
return failure(c, err, "task edit");
|
|
1089
1068
|
}
|
|
1090
1069
|
});
|
|
1091
1070
|
}
|
|
@@ -1094,7 +1073,7 @@ function plansRoute(pool2) {
|
|
|
1094
1073
|
const plan = await pool2.session(c.req.param("sessionId")).resolveConflictAsTask(c.req.param("taskId"));
|
|
1095
1074
|
return c.json({ plan });
|
|
1096
1075
|
} catch (err) {
|
|
1097
|
-
return
|
|
1076
|
+
return failure(c, err, "task edit");
|
|
1098
1077
|
}
|
|
1099
1078
|
});
|
|
1100
1079
|
router.put("/:sessionId/tasks/:taskId", async (c) => {
|
|
@@ -1110,33 +1089,33 @@ function plansRoute(pool2) {
|
|
|
1110
1089
|
try {
|
|
1111
1090
|
result = await session.setTaskDependencies(taskId, dependencies.map(String));
|
|
1112
1091
|
} catch (err) {
|
|
1113
|
-
return c
|
|
1092
|
+
return failure(c, err, "task dependencies", { fallback: 400 });
|
|
1114
1093
|
}
|
|
1115
1094
|
}
|
|
1116
1095
|
if (Object.keys(rest).length > 0 || !hasRunner && !hasDeps) result = await session.updateTask(taskId, rest);
|
|
1117
|
-
if (!result) return c
|
|
1096
|
+
if (!result) return refuse(c, 404, "Task not found", "task_not_found");
|
|
1118
1097
|
return c.json({ ok: true });
|
|
1119
1098
|
} catch (err) {
|
|
1120
|
-
return
|
|
1099
|
+
return failure(c, err, "task edit");
|
|
1121
1100
|
}
|
|
1122
1101
|
});
|
|
1123
1102
|
router.delete("/:sessionId/tasks/:taskId", async (c) => {
|
|
1124
1103
|
try {
|
|
1125
1104
|
const result = await pool2.session(c.req.param("sessionId")).removeTask(c.req.param("taskId"));
|
|
1126
|
-
if (!result) return c
|
|
1105
|
+
if (!result) return refuse(c, 404, "Task not found", "task_not_found");
|
|
1127
1106
|
return c.json({ ok: true });
|
|
1128
1107
|
} catch (err) {
|
|
1129
|
-
return
|
|
1108
|
+
return failure(c, err, "task edit");
|
|
1130
1109
|
}
|
|
1131
1110
|
});
|
|
1132
1111
|
router.post("/:sessionId/tasks", async (c) => {
|
|
1133
1112
|
try {
|
|
1134
1113
|
const body = await c.req.json();
|
|
1135
1114
|
const result = await pool2.session(c.req.param("sessionId")).addTask(body);
|
|
1136
|
-
if (!result) return c
|
|
1115
|
+
if (!result) return refuse(c, 404, "Session not found", "session_not_found");
|
|
1137
1116
|
return c.json({ ok: true });
|
|
1138
1117
|
} catch (err) {
|
|
1139
|
-
return
|
|
1118
|
+
return failure(c, err, "task edit");
|
|
1140
1119
|
}
|
|
1141
1120
|
});
|
|
1142
1121
|
router.post("/:sessionId/review/approve", async (c) => {
|
|
@@ -1144,9 +1123,7 @@ function plansRoute(pool2) {
|
|
|
1144
1123
|
const plan = await pool2.session(c.req.param("sessionId")).approveReview();
|
|
1145
1124
|
return c.json({ plan });
|
|
1146
1125
|
} catch (err) {
|
|
1147
|
-
|
|
1148
|
-
const status = e.message === "Session not found" ? 404 : e.message === "No plan to review" ? 400 : 500;
|
|
1149
|
-
return c.json({ error: e.message }, status);
|
|
1126
|
+
return failure(c, err, "review approve");
|
|
1150
1127
|
}
|
|
1151
1128
|
});
|
|
1152
1129
|
router.post("/:sessionId/tasks/merge", async (c) => {
|
|
@@ -1158,9 +1135,7 @@ function plansRoute(pool2) {
|
|
|
1158
1135
|
const plan = await pool2.session(c.req.param("sessionId")).requestMerge(taskIds);
|
|
1159
1136
|
return c.json({ plan });
|
|
1160
1137
|
} catch (err) {
|
|
1161
|
-
|
|
1162
|
-
const status = e.message === "Session not found" ? 404 : 400;
|
|
1163
|
-
return c.json({ error: e.message }, status);
|
|
1138
|
+
return failure(c, err, "merge", { fallback: 400 });
|
|
1164
1139
|
}
|
|
1165
1140
|
});
|
|
1166
1141
|
router.post("/:sessionId/tasks/:taskId/split", async (c) => {
|
|
@@ -1168,9 +1143,7 @@ function plansRoute(pool2) {
|
|
|
1168
1143
|
const plan = await pool2.session(c.req.param("sessionId")).requestSplit(c.req.param("taskId"));
|
|
1169
1144
|
return c.json({ plan });
|
|
1170
1145
|
} catch (err) {
|
|
1171
|
-
|
|
1172
|
-
const status = e.message === "Session not found" ? 404 : 400;
|
|
1173
|
-
return c.json({ error: e.message }, status);
|
|
1146
|
+
return failure(c, err, "split", { fallback: 400 });
|
|
1174
1147
|
}
|
|
1175
1148
|
});
|
|
1176
1149
|
router.post("/:sessionId/converse/start", async (c) => {
|
|
@@ -1182,11 +1155,7 @@ function plansRoute(pool2) {
|
|
|
1182
1155
|
const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
1183
1156
|
return c.json({ plan });
|
|
1184
1157
|
} catch (err) {
|
|
1185
|
-
|
|
1186
|
-
if (err instanceof core.WorkspaceNotAProjectError) {
|
|
1187
|
-
return c.json({ error: err.message, code: "workspace_not_a_project", workspace: err.workspace }, 400);
|
|
1188
|
-
}
|
|
1189
|
-
return c.json({ error: err instanceof Error ? err.message : "Planning failed" }, 500);
|
|
1158
|
+
return failure(c, err, "converse start", { message: "Planning failed" });
|
|
1190
1159
|
}
|
|
1191
1160
|
});
|
|
1192
1161
|
router.post("/:sessionId/converse/message", async (c) => {
|
|
@@ -1196,24 +1165,21 @@ function plansRoute(pool2) {
|
|
|
1196
1165
|
const plan = await pool2.continuePlanning(c.req.param("sessionId"), message);
|
|
1197
1166
|
return c.json({ plan });
|
|
1198
1167
|
} catch (err) {
|
|
1199
|
-
|
|
1200
|
-
if (e instanceof core.ConversationBusyError) return c.json({ error: e.message }, 409);
|
|
1201
|
-
const status = e.message === "Session not found" ? 404 : 500;
|
|
1202
|
-
return c.json({ error: e.message }, status);
|
|
1168
|
+
return failure(c, err, "conversation message");
|
|
1203
1169
|
}
|
|
1204
1170
|
});
|
|
1205
1171
|
router.post("/:sessionId/conversation/fork", (c) => {
|
|
1206
1172
|
try {
|
|
1207
1173
|
return c.json(pool2.forkConversation(c.req.param("sessionId")));
|
|
1208
1174
|
} catch (err) {
|
|
1209
|
-
return
|
|
1175
|
+
return failure(c, err, "conversation edit");
|
|
1210
1176
|
}
|
|
1211
1177
|
});
|
|
1212
1178
|
router.get("/:sessionId/conversation/rewind-targets", (c) => {
|
|
1213
1179
|
try {
|
|
1214
1180
|
return c.json({ targets: pool2.session(c.req.param("sessionId")).rewindTargets() });
|
|
1215
1181
|
} catch (err) {
|
|
1216
|
-
return
|
|
1182
|
+
return failure(c, err, "conversation edit");
|
|
1217
1183
|
}
|
|
1218
1184
|
});
|
|
1219
1185
|
router.post("/:sessionId/conversation/rewind", async (c) => {
|
|
@@ -1222,29 +1188,19 @@ function plansRoute(pool2) {
|
|
|
1222
1188
|
if (!Number.isInteger(index) || index < 0) return c.json({ error: "index must be a non-negative integer" }, 400);
|
|
1223
1189
|
return c.json(pool2.rewindConversation(c.req.param("sessionId"), index));
|
|
1224
1190
|
} catch (err) {
|
|
1225
|
-
return
|
|
1191
|
+
return failure(c, err, "conversation edit");
|
|
1226
1192
|
}
|
|
1227
1193
|
});
|
|
1228
1194
|
router.post("/:sessionId/conversation/compact", async (c) => {
|
|
1229
1195
|
try {
|
|
1230
1196
|
return c.json(await pool2.compactConversation(c.req.param("sessionId")));
|
|
1231
1197
|
} catch (err) {
|
|
1232
|
-
return
|
|
1233
|
-
}
|
|
1234
|
-
});
|
|
1235
|
-
router.post("/:sessionId/process-queued", async (c) => {
|
|
1236
|
-
try {
|
|
1237
|
-
await pool2.session(c.req.param("sessionId")).processQueuedMessages();
|
|
1238
|
-
return c.json({ ok: true });
|
|
1239
|
-
} catch (err) {
|
|
1240
|
-
const e = err;
|
|
1241
|
-
const status = e.message === "Session not found" ? 404 : 500;
|
|
1242
|
-
return c.json({ error: e.message }, status);
|
|
1198
|
+
return failure(c, err, "conversation edit");
|
|
1243
1199
|
}
|
|
1244
1200
|
});
|
|
1245
1201
|
router.get("/:sessionId/prd", (c) => {
|
|
1246
1202
|
const plan = pool2.getPlan(c.req.param("sessionId"));
|
|
1247
|
-
if (!plan?.prdMarkdown) return c
|
|
1203
|
+
if (!plan?.prdMarkdown) return refuse(c, 404, "No PRD found");
|
|
1248
1204
|
return c.json({ prdMarkdown: plan.prdMarkdown });
|
|
1249
1205
|
});
|
|
1250
1206
|
return router;
|
|
@@ -1295,7 +1251,7 @@ function approvalsRoute(pool2) {
|
|
|
1295
1251
|
const router = new hono.Hono();
|
|
1296
1252
|
router.get("/:sessionId", (c) => {
|
|
1297
1253
|
const sessionId = c.req.param("sessionId");
|
|
1298
|
-
if (!pool2.hasSession(sessionId)) return c
|
|
1254
|
+
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1299
1255
|
return c.json({
|
|
1300
1256
|
pending: pool2.outstandingApprovals(sessionId),
|
|
1301
1257
|
approvedScopes: pool2.approvedScopes(sessionId)
|
|
@@ -1304,10 +1260,10 @@ function approvalsRoute(pool2) {
|
|
|
1304
1260
|
router.post("/:sessionId/:approvalId", async (c) => {
|
|
1305
1261
|
const sessionId = c.req.param("sessionId");
|
|
1306
1262
|
const approvalId = c.req.param("approvalId");
|
|
1307
|
-
if (!pool2.hasSession(sessionId)) return c
|
|
1263
|
+
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1308
1264
|
const answer = answerOf(await c.req.json().catch(() => null));
|
|
1309
1265
|
if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
|
|
1310
|
-
return c
|
|
1266
|
+
return refuse(c, 409, "Approval is no longer outstanding \u2014 it timed out or was already answered.", "approval_not_outstanding");
|
|
1311
1267
|
}
|
|
1312
1268
|
const { decision } = core.toApprovalDecision(answer);
|
|
1313
1269
|
return c.json({ ok: true, granted: core.isGranted({ decision }), decision });
|
|
@@ -1317,12 +1273,13 @@ function approvalsRoute(pool2) {
|
|
|
1317
1273
|
function settingsRoute(pool2) {
|
|
1318
1274
|
const router = new hono.Hono();
|
|
1319
1275
|
router.get("/", (c) => {
|
|
1320
|
-
|
|
1276
|
+
const settings = pool2.getSettings();
|
|
1277
|
+
return c.json(settings);
|
|
1321
1278
|
});
|
|
1322
1279
|
router.patch("/", async (c) => {
|
|
1323
1280
|
const body = await c.req.json();
|
|
1324
1281
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
1325
|
-
return c
|
|
1282
|
+
return refuse(c, 400, "At least one setting field is required");
|
|
1326
1283
|
}
|
|
1327
1284
|
const result = pool2.updateSettings(body);
|
|
1328
1285
|
return c.json(result);
|
|
@@ -1334,6 +1291,7 @@ var COMMANDS = [
|
|
|
1334
1291
|
{ name: "verify", description: "Toggle verification mode \u2014 adds a final evidence-based verification task that runs the full suite (on|off|status)" },
|
|
1335
1292
|
{ name: "transport", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run (terminal|structured|status)" }
|
|
1336
1293
|
];
|
|
1294
|
+
var BOOLEAN_TOGGLES = { tdd: "tdd", verify: "verification" };
|
|
1337
1295
|
function commandsRoute(pool2) {
|
|
1338
1296
|
const router = new hono.Hono();
|
|
1339
1297
|
router.get("/", (c) => {
|
|
@@ -1345,23 +1303,15 @@ function commandsRoute(pool2) {
|
|
|
1345
1303
|
const args2 = body?.args || {};
|
|
1346
1304
|
const command = COMMANDS.find((cmd) => cmd.name === name);
|
|
1347
1305
|
if (!command) {
|
|
1348
|
-
return c
|
|
1306
|
+
return refuse(c, 404, `Unknown command: ${name}`);
|
|
1349
1307
|
}
|
|
1350
|
-
|
|
1308
|
+
const toggle = BOOLEAN_TOGGLES[name];
|
|
1309
|
+
if (toggle) {
|
|
1351
1310
|
const action = args2.action || "status";
|
|
1352
1311
|
if (action === "on") {
|
|
1353
|
-
pool2.updateSettings({
|
|
1312
|
+
pool2.updateSettings({ [toggle]: { enabled: true } });
|
|
1354
1313
|
} else if (action === "off") {
|
|
1355
|
-
pool2.updateSettings({
|
|
1356
|
-
}
|
|
1357
|
-
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1358
|
-
}
|
|
1359
|
-
if (name === "verify") {
|
|
1360
|
-
const action = args2.action || "status";
|
|
1361
|
-
if (action === "on") {
|
|
1362
|
-
pool2.updateSettings({ verification: { enabled: true } });
|
|
1363
|
-
} else if (action === "off") {
|
|
1364
|
-
pool2.updateSettings({ verification: { enabled: false } });
|
|
1314
|
+
pool2.updateSettings({ [toggle]: { enabled: false } });
|
|
1365
1315
|
}
|
|
1366
1316
|
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1367
1317
|
}
|
|
@@ -1370,11 +1320,11 @@ function commandsRoute(pool2) {
|
|
|
1370
1320
|
if (action === "terminal" || action === "structured") {
|
|
1371
1321
|
pool2.updateSettings({ runnerTransport: action });
|
|
1372
1322
|
} else if (action !== "status") {
|
|
1373
|
-
return c
|
|
1323
|
+
return refuse(c, 400, `Unknown transport: ${action} (terminal|structured)`);
|
|
1374
1324
|
}
|
|
1375
1325
|
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1376
1326
|
}
|
|
1377
|
-
return c
|
|
1327
|
+
return refuse(c, 404, `Unknown command: ${name}`);
|
|
1378
1328
|
});
|
|
1379
1329
|
return router;
|
|
1380
1330
|
}
|
|
@@ -1590,6 +1540,9 @@ async function shutdown() {
|
|
|
1590
1540
|
server.close();
|
|
1591
1541
|
process.exit(0);
|
|
1592
1542
|
}
|
|
1543
|
+
process.on("unhandledRejection", (reason) => {
|
|
1544
|
+
console.error(`[web] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
|
|
1545
|
+
});
|
|
1593
1546
|
process.on("SIGINT", () => {
|
|
1594
1547
|
shutdown();
|
|
1595
1548
|
});
|