@ordewell/web 0.7.1 → 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 +114 -205
- 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,22 +890,7 @@ function sessionsRoute(pool2) {
|
|
|
923
890
|
});
|
|
924
891
|
return router;
|
|
925
892
|
}
|
|
926
|
-
function editFailure(c, err) {
|
|
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
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.";
|
|
934
|
-
function conversationFailure(c, err) {
|
|
935
|
-
const e = err;
|
|
936
|
-
if (e.message === "Session not found") return c.json({ error: e.message }, 404);
|
|
937
|
-
if (e instanceof core.ConversationBusyError) return c.json({ error: e.message }, 409);
|
|
938
|
-
if (e instanceof core.ConversationEditError) return c.json({ error: e.message }, 400);
|
|
939
|
-
console.error("[plans] conversation edit failed:", err);
|
|
940
|
-
return c.json({ error: e.message || "Internal error" }, 500);
|
|
941
|
-
}
|
|
942
894
|
function plansRoute(pool2) {
|
|
943
895
|
const router = new hono.Hono();
|
|
944
896
|
router.post("/:sessionId/generate", async (c) => {
|
|
@@ -952,12 +904,7 @@ function plansRoute(pool2) {
|
|
|
952
904
|
const { models, modelsByRunner } = await pool2.getProviderModels();
|
|
953
905
|
return c.json({ plan, models, modelsByRunner });
|
|
954
906
|
} catch (err) {
|
|
955
|
-
|
|
956
|
-
if (err instanceof core.WorkspaceNotAProjectError) {
|
|
957
|
-
return c.json({ error: err.message, code: "workspace_not_a_project", workspace: err.workspace }, 400);
|
|
958
|
-
}
|
|
959
|
-
console.error("[plans] generate failed:", err);
|
|
960
|
-
return c.json({ error: err instanceof Error ? err.message : "Plan generation failed" }, 500);
|
|
907
|
+
return failure(c, err, "generate", { message: "Plan generation failed" });
|
|
961
908
|
}
|
|
962
909
|
});
|
|
963
910
|
router.post("/:sessionId/execute", async (c) => {
|
|
@@ -965,9 +912,7 @@ function plansRoute(pool2) {
|
|
|
965
912
|
await pool2.session(c.req.param("sessionId")).executePlan();
|
|
966
913
|
return c.json({ status: "running" });
|
|
967
914
|
} catch (err) {
|
|
968
|
-
|
|
969
|
-
const status = e.message === "Session not found" ? 404 : e.message === "No plan to execute" ? 400 : e.message === "Session already executing" ? 409 : 500;
|
|
970
|
-
return c.json({ error: e.message }, status);
|
|
915
|
+
return failure(c, err, "execute");
|
|
971
916
|
}
|
|
972
917
|
});
|
|
973
918
|
router.post("/:sessionId/stop", (c) => {
|
|
@@ -983,7 +928,7 @@ function plansRoute(pool2) {
|
|
|
983
928
|
await pool2.session(c.req.param("sessionId")).markTaskComplete(c.req.param("taskId"));
|
|
984
929
|
return c.json({ ok: true });
|
|
985
930
|
} catch (err) {
|
|
986
|
-
return c
|
|
931
|
+
return failure(c, err, "mark complete", { fallback: 404, message: "Not found" });
|
|
987
932
|
}
|
|
988
933
|
});
|
|
989
934
|
router.post("/:sessionId/tasks/:taskId/uncomplete", async (c) => {
|
|
@@ -991,7 +936,7 @@ function plansRoute(pool2) {
|
|
|
991
936
|
await pool2.session(c.req.param("sessionId")).markTaskIncomplete(c.req.param("taskId"));
|
|
992
937
|
return c.json({ ok: true });
|
|
993
938
|
} catch (err) {
|
|
994
|
-
return c
|
|
939
|
+
return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
|
|
995
940
|
}
|
|
996
941
|
});
|
|
997
942
|
for (const [segment, run2] of [
|
|
@@ -1005,39 +950,29 @@ function plansRoute(pool2) {
|
|
|
1005
950
|
await run2(pool2.session(c.req.param("sessionId")), c.req.param("taskId"));
|
|
1006
951
|
return c.json({ ok: true });
|
|
1007
952
|
} catch (err) {
|
|
1008
|
-
|
|
1009
|
-
return c.json({ error: err.message }, status);
|
|
953
|
+
return failure(c, err, segment);
|
|
1010
954
|
}
|
|
1011
955
|
});
|
|
1012
956
|
}
|
|
1013
|
-
|
|
1014
|
-
try {
|
|
1015
|
-
const body = await c.req.json().catch(() => ({}));
|
|
1016
|
-
const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
|
|
1017
|
-
if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
|
|
1018
|
-
const id = pool2.session(c.req.param("sessionId")).sendTaskMessage(c.req.param("taskId"), text);
|
|
1019
|
-
return c.json({ id });
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
return editFailure(c, err);
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
router.post("/:sessionId/tasks/:taskId/messages/now", async (c) => {
|
|
957
|
+
const messageHandler = (send) => async (c) => {
|
|
1025
958
|
try {
|
|
1026
959
|
const body = await c.req.json().catch(() => ({}));
|
|
1027
960
|
const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
|
|
1028
961
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
|
|
1029
|
-
const id = pool2.session(c.req.param("sessionId"))
|
|
962
|
+
const id = send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
|
|
1030
963
|
return c.json({ id });
|
|
1031
964
|
} catch (err) {
|
|
1032
|
-
return
|
|
965
|
+
return failure(c, err, "task edit");
|
|
1033
966
|
}
|
|
1034
|
-
}
|
|
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)));
|
|
1035
970
|
router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
|
|
1036
971
|
try {
|
|
1037
972
|
const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
1038
973
|
return c.json({ sent });
|
|
1039
974
|
} catch (err) {
|
|
1040
|
-
return
|
|
975
|
+
return failure(c, err, "task edit");
|
|
1041
976
|
}
|
|
1042
977
|
});
|
|
1043
978
|
router.post("/:sessionId/tasks/:taskId/continue", async (c) => {
|
|
@@ -1048,7 +983,7 @@ function plansRoute(pool2) {
|
|
|
1048
983
|
await pool2.session(c.req.param("sessionId")).continueTask(c.req.param("taskId"), text);
|
|
1049
984
|
return c.json({ ok: true });
|
|
1050
985
|
} catch (err) {
|
|
1051
|
-
return
|
|
986
|
+
return failure(c, err, "task edit");
|
|
1052
987
|
}
|
|
1053
988
|
});
|
|
1054
989
|
router.delete("/:sessionId/tasks/:taskId/messages/:messageId", (c) => {
|
|
@@ -1056,7 +991,7 @@ function plansRoute(pool2) {
|
|
|
1056
991
|
const removed = pool2.session(c.req.param("sessionId")).removeQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
1057
992
|
return c.json({ removed });
|
|
1058
993
|
} catch (err) {
|
|
1059
|
-
return
|
|
994
|
+
return failure(c, err, "task edit");
|
|
1060
995
|
}
|
|
1061
996
|
});
|
|
1062
997
|
router.post("/:sessionId/tasks/:taskId/interrupt", async (c) => {
|
|
@@ -1064,18 +999,18 @@ function plansRoute(pool2) {
|
|
|
1064
999
|
await pool2.session(c.req.param("sessionId")).interruptTask(c.req.param("taskId"));
|
|
1065
1000
|
return c.json({ ok: true });
|
|
1066
1001
|
} catch (err) {
|
|
1067
|
-
return
|
|
1002
|
+
return failure(c, err, "task edit");
|
|
1068
1003
|
}
|
|
1069
1004
|
});
|
|
1070
1005
|
router.post("/:sessionId/tasks/:taskId/checkpoint/approve", (c) => {
|
|
1071
1006
|
try {
|
|
1072
1007
|
const session = pool2.session(c.req.param("sessionId"));
|
|
1073
1008
|
const taskId = c.req.param("taskId");
|
|
1074
|
-
if (!session.awaitsCheckpoint(taskId)) return c
|
|
1009
|
+
if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
|
|
1075
1010
|
session.approveCheckpoint(taskId);
|
|
1076
1011
|
return c.json({ ok: true });
|
|
1077
1012
|
} catch (err) {
|
|
1078
|
-
return
|
|
1013
|
+
return failure(c, err, "task edit");
|
|
1079
1014
|
}
|
|
1080
1015
|
});
|
|
1081
1016
|
router.post("/:sessionId/tasks/:taskId/checkpoint/reject", async (c) => {
|
|
@@ -1084,11 +1019,11 @@ function plansRoute(pool2) {
|
|
|
1084
1019
|
const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
|
|
1085
1020
|
const session = pool2.session(c.req.param("sessionId"));
|
|
1086
1021
|
const taskId = c.req.param("taskId");
|
|
1087
|
-
if (!session.awaitsCheckpoint(taskId)) return c
|
|
1022
|
+
if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
|
|
1088
1023
|
session.rejectCheckpoint(taskId, reason || void 0);
|
|
1089
1024
|
return c.json({ ok: true });
|
|
1090
1025
|
} catch (err) {
|
|
1091
|
-
return
|
|
1026
|
+
return failure(c, err, "task edit");
|
|
1092
1027
|
}
|
|
1093
1028
|
});
|
|
1094
1029
|
router.get("/:sessionId/tasks/:taskId/merge-gate", (c) => {
|
|
@@ -1101,21 +1036,21 @@ function plansRoute(pool2) {
|
|
|
1101
1036
|
});
|
|
1102
1037
|
return c.json({ mergeGate });
|
|
1103
1038
|
} catch (err) {
|
|
1104
|
-
return
|
|
1039
|
+
return failure(c, err, "task edit");
|
|
1105
1040
|
}
|
|
1106
1041
|
});
|
|
1107
1042
|
router.get("/:sessionId/isolation/diff", async (c) => {
|
|
1108
1043
|
try {
|
|
1109
1044
|
return c.json({ diff: await pool2.session(c.req.param("sessionId")).reviewRunDiff() });
|
|
1110
1045
|
} catch (err) {
|
|
1111
|
-
return
|
|
1046
|
+
return failure(c, err, "task edit");
|
|
1112
1047
|
}
|
|
1113
1048
|
});
|
|
1114
1049
|
router.post("/:sessionId/isolation/merge", async (c) => {
|
|
1115
1050
|
try {
|
|
1116
1051
|
return c.json(await pool2.session(c.req.param("sessionId")).mergeRun());
|
|
1117
1052
|
} catch (err) {
|
|
1118
|
-
return
|
|
1053
|
+
return failure(c, err, "task edit");
|
|
1119
1054
|
}
|
|
1120
1055
|
});
|
|
1121
1056
|
for (const [segment, run2] of [
|
|
@@ -1129,7 +1064,7 @@ function plansRoute(pool2) {
|
|
|
1129
1064
|
await run2(pool2.session(c.req.param("sessionId")));
|
|
1130
1065
|
return c.json({ ok: true });
|
|
1131
1066
|
} catch (err) {
|
|
1132
|
-
return
|
|
1067
|
+
return failure(c, err, "task edit");
|
|
1133
1068
|
}
|
|
1134
1069
|
});
|
|
1135
1070
|
}
|
|
@@ -1138,7 +1073,7 @@ function plansRoute(pool2) {
|
|
|
1138
1073
|
const plan = await pool2.session(c.req.param("sessionId")).resolveConflictAsTask(c.req.param("taskId"));
|
|
1139
1074
|
return c.json({ plan });
|
|
1140
1075
|
} catch (err) {
|
|
1141
|
-
return
|
|
1076
|
+
return failure(c, err, "task edit");
|
|
1142
1077
|
}
|
|
1143
1078
|
});
|
|
1144
1079
|
router.put("/:sessionId/tasks/:taskId", async (c) => {
|
|
@@ -1154,33 +1089,33 @@ function plansRoute(pool2) {
|
|
|
1154
1089
|
try {
|
|
1155
1090
|
result = await session.setTaskDependencies(taskId, dependencies.map(String));
|
|
1156
1091
|
} catch (err) {
|
|
1157
|
-
return c
|
|
1092
|
+
return failure(c, err, "task dependencies", { fallback: 400 });
|
|
1158
1093
|
}
|
|
1159
1094
|
}
|
|
1160
1095
|
if (Object.keys(rest).length > 0 || !hasRunner && !hasDeps) result = await session.updateTask(taskId, rest);
|
|
1161
|
-
if (!result) return c
|
|
1096
|
+
if (!result) return refuse(c, 404, "Task not found", "task_not_found");
|
|
1162
1097
|
return c.json({ ok: true });
|
|
1163
1098
|
} catch (err) {
|
|
1164
|
-
return
|
|
1099
|
+
return failure(c, err, "task edit");
|
|
1165
1100
|
}
|
|
1166
1101
|
});
|
|
1167
1102
|
router.delete("/:sessionId/tasks/:taskId", async (c) => {
|
|
1168
1103
|
try {
|
|
1169
1104
|
const result = await pool2.session(c.req.param("sessionId")).removeTask(c.req.param("taskId"));
|
|
1170
|
-
if (!result) return c
|
|
1105
|
+
if (!result) return refuse(c, 404, "Task not found", "task_not_found");
|
|
1171
1106
|
return c.json({ ok: true });
|
|
1172
1107
|
} catch (err) {
|
|
1173
|
-
return
|
|
1108
|
+
return failure(c, err, "task edit");
|
|
1174
1109
|
}
|
|
1175
1110
|
});
|
|
1176
1111
|
router.post("/:sessionId/tasks", async (c) => {
|
|
1177
1112
|
try {
|
|
1178
1113
|
const body = await c.req.json();
|
|
1179
1114
|
const result = await pool2.session(c.req.param("sessionId")).addTask(body);
|
|
1180
|
-
if (!result) return c
|
|
1115
|
+
if (!result) return refuse(c, 404, "Session not found", "session_not_found");
|
|
1181
1116
|
return c.json({ ok: true });
|
|
1182
1117
|
} catch (err) {
|
|
1183
|
-
return
|
|
1118
|
+
return failure(c, err, "task edit");
|
|
1184
1119
|
}
|
|
1185
1120
|
});
|
|
1186
1121
|
router.post("/:sessionId/review/approve", async (c) => {
|
|
@@ -1188,9 +1123,7 @@ function plansRoute(pool2) {
|
|
|
1188
1123
|
const plan = await pool2.session(c.req.param("sessionId")).approveReview();
|
|
1189
1124
|
return c.json({ plan });
|
|
1190
1125
|
} catch (err) {
|
|
1191
|
-
|
|
1192
|
-
const status = e.message === "Session not found" ? 404 : e.message === "No plan to review" ? 400 : 500;
|
|
1193
|
-
return c.json({ error: e.message }, status);
|
|
1126
|
+
return failure(c, err, "review approve");
|
|
1194
1127
|
}
|
|
1195
1128
|
});
|
|
1196
1129
|
router.post("/:sessionId/tasks/merge", async (c) => {
|
|
@@ -1202,9 +1135,7 @@ function plansRoute(pool2) {
|
|
|
1202
1135
|
const plan = await pool2.session(c.req.param("sessionId")).requestMerge(taskIds);
|
|
1203
1136
|
return c.json({ plan });
|
|
1204
1137
|
} catch (err) {
|
|
1205
|
-
|
|
1206
|
-
const status = e.message === "Session not found" ? 404 : 400;
|
|
1207
|
-
return c.json({ error: e.message }, status);
|
|
1138
|
+
return failure(c, err, "merge", { fallback: 400 });
|
|
1208
1139
|
}
|
|
1209
1140
|
});
|
|
1210
1141
|
router.post("/:sessionId/tasks/:taskId/split", async (c) => {
|
|
@@ -1212,9 +1143,7 @@ function plansRoute(pool2) {
|
|
|
1212
1143
|
const plan = await pool2.session(c.req.param("sessionId")).requestSplit(c.req.param("taskId"));
|
|
1213
1144
|
return c.json({ plan });
|
|
1214
1145
|
} catch (err) {
|
|
1215
|
-
|
|
1216
|
-
const status = e.message === "Session not found" ? 404 : 400;
|
|
1217
|
-
return c.json({ error: e.message }, status);
|
|
1146
|
+
return failure(c, err, "split", { fallback: 400 });
|
|
1218
1147
|
}
|
|
1219
1148
|
});
|
|
1220
1149
|
router.post("/:sessionId/converse/start", async (c) => {
|
|
@@ -1226,11 +1155,7 @@ function plansRoute(pool2) {
|
|
|
1226
1155
|
const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
1227
1156
|
return c.json({ plan });
|
|
1228
1157
|
} catch (err) {
|
|
1229
|
-
|
|
1230
|
-
if (err instanceof core.WorkspaceNotAProjectError) {
|
|
1231
|
-
return c.json({ error: err.message, code: "workspace_not_a_project", workspace: err.workspace }, 400);
|
|
1232
|
-
}
|
|
1233
|
-
return c.json({ error: err instanceof Error ? err.message : "Planning failed" }, 500);
|
|
1158
|
+
return failure(c, err, "converse start", { message: "Planning failed" });
|
|
1234
1159
|
}
|
|
1235
1160
|
});
|
|
1236
1161
|
router.post("/:sessionId/converse/message", async (c) => {
|
|
@@ -1240,24 +1165,21 @@ function plansRoute(pool2) {
|
|
|
1240
1165
|
const plan = await pool2.continuePlanning(c.req.param("sessionId"), message);
|
|
1241
1166
|
return c.json({ plan });
|
|
1242
1167
|
} catch (err) {
|
|
1243
|
-
|
|
1244
|
-
if (e instanceof core.ConversationBusyError) return c.json({ error: e.message }, 409);
|
|
1245
|
-
const status = e.message === "Session not found" ? 404 : 500;
|
|
1246
|
-
return c.json({ error: e.message }, status);
|
|
1168
|
+
return failure(c, err, "conversation message");
|
|
1247
1169
|
}
|
|
1248
1170
|
});
|
|
1249
1171
|
router.post("/:sessionId/conversation/fork", (c) => {
|
|
1250
1172
|
try {
|
|
1251
1173
|
return c.json(pool2.forkConversation(c.req.param("sessionId")));
|
|
1252
1174
|
} catch (err) {
|
|
1253
|
-
return
|
|
1175
|
+
return failure(c, err, "conversation edit");
|
|
1254
1176
|
}
|
|
1255
1177
|
});
|
|
1256
1178
|
router.get("/:sessionId/conversation/rewind-targets", (c) => {
|
|
1257
1179
|
try {
|
|
1258
1180
|
return c.json({ targets: pool2.session(c.req.param("sessionId")).rewindTargets() });
|
|
1259
1181
|
} catch (err) {
|
|
1260
|
-
return
|
|
1182
|
+
return failure(c, err, "conversation edit");
|
|
1261
1183
|
}
|
|
1262
1184
|
});
|
|
1263
1185
|
router.post("/:sessionId/conversation/rewind", async (c) => {
|
|
@@ -1266,29 +1188,19 @@ function plansRoute(pool2) {
|
|
|
1266
1188
|
if (!Number.isInteger(index) || index < 0) return c.json({ error: "index must be a non-negative integer" }, 400);
|
|
1267
1189
|
return c.json(pool2.rewindConversation(c.req.param("sessionId"), index));
|
|
1268
1190
|
} catch (err) {
|
|
1269
|
-
return
|
|
1191
|
+
return failure(c, err, "conversation edit");
|
|
1270
1192
|
}
|
|
1271
1193
|
});
|
|
1272
1194
|
router.post("/:sessionId/conversation/compact", async (c) => {
|
|
1273
1195
|
try {
|
|
1274
1196
|
return c.json(await pool2.compactConversation(c.req.param("sessionId")));
|
|
1275
1197
|
} catch (err) {
|
|
1276
|
-
return
|
|
1277
|
-
}
|
|
1278
|
-
});
|
|
1279
|
-
router.post("/:sessionId/process-queued", async (c) => {
|
|
1280
|
-
try {
|
|
1281
|
-
await pool2.session(c.req.param("sessionId")).processQueuedMessages();
|
|
1282
|
-
return c.json({ ok: true });
|
|
1283
|
-
} catch (err) {
|
|
1284
|
-
const e = err;
|
|
1285
|
-
const status = e.message === "Session not found" ? 404 : 500;
|
|
1286
|
-
return c.json({ error: e.message }, status);
|
|
1198
|
+
return failure(c, err, "conversation edit");
|
|
1287
1199
|
}
|
|
1288
1200
|
});
|
|
1289
1201
|
router.get("/:sessionId/prd", (c) => {
|
|
1290
1202
|
const plan = pool2.getPlan(c.req.param("sessionId"));
|
|
1291
|
-
if (!plan?.prdMarkdown) return c
|
|
1203
|
+
if (!plan?.prdMarkdown) return refuse(c, 404, "No PRD found");
|
|
1292
1204
|
return c.json({ prdMarkdown: plan.prdMarkdown });
|
|
1293
1205
|
});
|
|
1294
1206
|
return router;
|
|
@@ -1339,7 +1251,7 @@ function approvalsRoute(pool2) {
|
|
|
1339
1251
|
const router = new hono.Hono();
|
|
1340
1252
|
router.get("/:sessionId", (c) => {
|
|
1341
1253
|
const sessionId = c.req.param("sessionId");
|
|
1342
|
-
if (!pool2.hasSession(sessionId)) return c
|
|
1254
|
+
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1343
1255
|
return c.json({
|
|
1344
1256
|
pending: pool2.outstandingApprovals(sessionId),
|
|
1345
1257
|
approvedScopes: pool2.approvedScopes(sessionId)
|
|
@@ -1348,10 +1260,10 @@ function approvalsRoute(pool2) {
|
|
|
1348
1260
|
router.post("/:sessionId/:approvalId", async (c) => {
|
|
1349
1261
|
const sessionId = c.req.param("sessionId");
|
|
1350
1262
|
const approvalId = c.req.param("approvalId");
|
|
1351
|
-
if (!pool2.hasSession(sessionId)) return c
|
|
1263
|
+
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1352
1264
|
const answer = answerOf(await c.req.json().catch(() => null));
|
|
1353
1265
|
if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
|
|
1354
|
-
return c
|
|
1266
|
+
return refuse(c, 409, "Approval is no longer outstanding \u2014 it timed out or was already answered.", "approval_not_outstanding");
|
|
1355
1267
|
}
|
|
1356
1268
|
const { decision } = core.toApprovalDecision(answer);
|
|
1357
1269
|
return c.json({ ok: true, granted: core.isGranted({ decision }), decision });
|
|
@@ -1361,12 +1273,13 @@ function approvalsRoute(pool2) {
|
|
|
1361
1273
|
function settingsRoute(pool2) {
|
|
1362
1274
|
const router = new hono.Hono();
|
|
1363
1275
|
router.get("/", (c) => {
|
|
1364
|
-
|
|
1276
|
+
const settings = pool2.getSettings();
|
|
1277
|
+
return c.json(settings);
|
|
1365
1278
|
});
|
|
1366
1279
|
router.patch("/", async (c) => {
|
|
1367
1280
|
const body = await c.req.json();
|
|
1368
1281
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
1369
|
-
return c
|
|
1282
|
+
return refuse(c, 400, "At least one setting field is required");
|
|
1370
1283
|
}
|
|
1371
1284
|
const result = pool2.updateSettings(body);
|
|
1372
1285
|
return c.json(result);
|
|
@@ -1378,6 +1291,7 @@ var COMMANDS = [
|
|
|
1378
1291
|
{ name: "verify", description: "Toggle verification mode \u2014 adds a final evidence-based verification task that runs the full suite (on|off|status)" },
|
|
1379
1292
|
{ name: "transport", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run (terminal|structured|status)" }
|
|
1380
1293
|
];
|
|
1294
|
+
var BOOLEAN_TOGGLES = { tdd: "tdd", verify: "verification" };
|
|
1381
1295
|
function commandsRoute(pool2) {
|
|
1382
1296
|
const router = new hono.Hono();
|
|
1383
1297
|
router.get("/", (c) => {
|
|
@@ -1389,23 +1303,15 @@ function commandsRoute(pool2) {
|
|
|
1389
1303
|
const args2 = body?.args || {};
|
|
1390
1304
|
const command = COMMANDS.find((cmd) => cmd.name === name);
|
|
1391
1305
|
if (!command) {
|
|
1392
|
-
return c
|
|
1393
|
-
}
|
|
1394
|
-
if (name === "tdd") {
|
|
1395
|
-
const action = args2.action || "status";
|
|
1396
|
-
if (action === "on") {
|
|
1397
|
-
pool2.updateSettings({ tdd: { enabled: true } });
|
|
1398
|
-
} else if (action === "off") {
|
|
1399
|
-
pool2.updateSettings({ tdd: { enabled: false } });
|
|
1400
|
-
}
|
|
1401
|
-
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1306
|
+
return refuse(c, 404, `Unknown command: ${name}`);
|
|
1402
1307
|
}
|
|
1403
|
-
|
|
1308
|
+
const toggle = BOOLEAN_TOGGLES[name];
|
|
1309
|
+
if (toggle) {
|
|
1404
1310
|
const action = args2.action || "status";
|
|
1405
1311
|
if (action === "on") {
|
|
1406
|
-
pool2.updateSettings({
|
|
1312
|
+
pool2.updateSettings({ [toggle]: { enabled: true } });
|
|
1407
1313
|
} else if (action === "off") {
|
|
1408
|
-
pool2.updateSettings({
|
|
1314
|
+
pool2.updateSettings({ [toggle]: { enabled: false } });
|
|
1409
1315
|
}
|
|
1410
1316
|
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1411
1317
|
}
|
|
@@ -1414,11 +1320,11 @@ function commandsRoute(pool2) {
|
|
|
1414
1320
|
if (action === "terminal" || action === "structured") {
|
|
1415
1321
|
pool2.updateSettings({ runnerTransport: action });
|
|
1416
1322
|
} else if (action !== "status") {
|
|
1417
|
-
return c
|
|
1323
|
+
return refuse(c, 400, `Unknown transport: ${action} (terminal|structured)`);
|
|
1418
1324
|
}
|
|
1419
1325
|
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1420
1326
|
}
|
|
1421
|
-
return c
|
|
1327
|
+
return refuse(c, 404, `Unknown command: ${name}`);
|
|
1422
1328
|
});
|
|
1423
1329
|
return router;
|
|
1424
1330
|
}
|
|
@@ -1634,6 +1540,9 @@ async function shutdown() {
|
|
|
1634
1540
|
server.close();
|
|
1635
1541
|
process.exit(0);
|
|
1636
1542
|
}
|
|
1543
|
+
process.on("unhandledRejection", (reason) => {
|
|
1544
|
+
console.error(`[web] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
|
|
1545
|
+
});
|
|
1637
1546
|
process.on("SIGINT", () => {
|
|
1638
1547
|
shutdown();
|
|
1639
1548
|
});
|