@ordewell/web 0.7.1 → 0.8.0
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/README.md +3 -0
- package/dist/server/main.js +154 -328
- 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) {
|
|
@@ -336,20 +336,22 @@ ${err}` : out;
|
|
|
336
336
|
return { success: true, output: combined || "(no output)", truncated: combined.length > 2e4 };
|
|
337
337
|
}
|
|
338
338
|
};
|
|
339
|
+
|
|
340
|
+
// server/adapters/PoolAwareRunner.ts
|
|
339
341
|
var PoolAwareRunner = class {
|
|
340
342
|
constructor(sessionId, broadcast, inner) {
|
|
341
343
|
this.sessionId = sessionId;
|
|
342
344
|
this.broadcast = broadcast;
|
|
343
|
-
this.inner = inner
|
|
345
|
+
this.inner = inner;
|
|
344
346
|
}
|
|
345
347
|
sessionId;
|
|
346
348
|
broadcast;
|
|
347
349
|
inner;
|
|
348
350
|
/**
|
|
349
|
-
* Sessions this plan spawned. The inner runner
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
351
|
+
* Sessions this plan spawned. The inner runner is shared by every plan in
|
|
352
|
+
* the pool, so "stop everything" and "how many are running" must be
|
|
353
|
+
* answered from this set, never delegated — delegating would let one plan's
|
|
354
|
+
* /stop kill another plan's tasks.
|
|
353
355
|
*/
|
|
354
356
|
owned = /* @__PURE__ */ new Set();
|
|
355
357
|
get activeCount() {
|
|
@@ -374,69 +376,22 @@ var PoolAwareRunner = class {
|
|
|
374
376
|
this.owned.clear();
|
|
375
377
|
}
|
|
376
378
|
};
|
|
377
|
-
var TMUX_MISSING_ADVICE = "tmux is not installed, so terminal-transport tasks run without a terminal window you can open or type into. Install tmux, or keep the structured transport (/transport structured).";
|
|
378
|
-
function startTerminalHost(port2, deps = {}) {
|
|
379
|
-
const hasTmuxImpl = deps.hasTmuxImpl ?? core.hasTmux;
|
|
380
|
-
if (!hasTmuxImpl()) return { advice: TMUX_MISSING_ADVICE };
|
|
381
|
-
const runner = (deps.createTmuxRunner ?? ((p) => new core.TmuxRunner({ port: p })))(port2);
|
|
382
|
-
runner.ensureSession().catch((err) => {
|
|
383
|
-
console.error(`[web] tmux session setup failed (will retry on next task spawn): ${err?.message ?? err}`);
|
|
384
|
-
});
|
|
385
|
-
return { runner };
|
|
386
|
-
}
|
|
387
|
-
var AdvisingRunner = class {
|
|
388
|
-
constructor(inner, advice, advise) {
|
|
389
|
-
this.inner = inner;
|
|
390
|
-
this.advice = advice;
|
|
391
|
-
this.advise = advise;
|
|
392
|
-
}
|
|
393
|
-
inner;
|
|
394
|
-
advice;
|
|
395
|
-
advise;
|
|
396
|
-
advised = false;
|
|
397
|
-
get activeCount() {
|
|
398
|
-
return this.inner.activeCount;
|
|
399
|
-
}
|
|
400
|
-
spawn(opts) {
|
|
401
|
-
if (!this.advised) {
|
|
402
|
-
this.advised = true;
|
|
403
|
-
this.advise(this.advice);
|
|
404
|
-
}
|
|
405
|
-
return this.inner.spawn(opts);
|
|
406
|
-
}
|
|
407
|
-
stop(sessionId) {
|
|
408
|
-
this.inner.stop(sessionId);
|
|
409
|
-
}
|
|
410
|
-
stopAll() {
|
|
411
|
-
this.inner.stopAll();
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
379
|
|
|
415
380
|
// server/pool/orchestratorPool.ts
|
|
416
381
|
var OrchestratorPool = class {
|
|
417
382
|
sessions = /* @__PURE__ */ new Map();
|
|
418
383
|
clients = /* @__PURE__ */ new Map();
|
|
419
|
-
|
|
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
|
-
registry = (() => {
|
|
424
|
-
const r = new core.RunnerRegistry();
|
|
425
|
-
r.loadUserPlugins();
|
|
426
|
-
return r;
|
|
427
|
-
})();
|
|
384
|
+
registry = new core.RunnerRegistry();
|
|
428
385
|
modelResolver;
|
|
429
386
|
runnerInstallation = new core.RunnerInstallation(this.registry);
|
|
430
387
|
cachedProviderLists;
|
|
431
388
|
settingsService = new core.SettingsService();
|
|
432
389
|
plannerModelMemory = new core.PlannerModelMemory(this.settingsService);
|
|
433
|
-
|
|
434
|
-
structuredRunner;
|
|
435
|
-
terminalAdvice;
|
|
390
|
+
runner;
|
|
436
391
|
constructor(deps = {}) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.
|
|
392
|
+
const pluginNotice = core.removedPluginNotice();
|
|
393
|
+
if (pluginNotice) console.warn(pluginNotice);
|
|
394
|
+
this.runner = deps.runner ?? new core.StructuredRunner();
|
|
440
395
|
this.modelResolver = deps.modelResolver ?? new core.ModelResolver(this.registry, new WebConfig());
|
|
441
396
|
}
|
|
442
397
|
subscribe(sessionId, ws) {
|
|
@@ -502,12 +457,7 @@ var OrchestratorPool = class {
|
|
|
502
457
|
});
|
|
503
458
|
const fsAdapter = new PoolFileSystem(workspace);
|
|
504
459
|
const broadcast = (msg) => this.broadcast(sessionId, msg);
|
|
505
|
-
const
|
|
506
|
-
const router = new core.TransportRouter({
|
|
507
|
-
terminal: this.terminalAdvice ? new AdvisingRunner(terminal, this.terminalAdvice, (message) => this.broadcast(sessionId, { type: "notice", level: "warn", message })) : terminal,
|
|
508
|
-
structured: this.structuredRunner
|
|
509
|
-
});
|
|
510
|
-
const runner = new PoolAwareRunner(sessionId, broadcast, router);
|
|
460
|
+
const runner = new PoolAwareRunner(sessionId, broadcast, this.runner);
|
|
511
461
|
return core.createSession({
|
|
512
462
|
config,
|
|
513
463
|
notifications: { info() {
|
|
@@ -561,14 +511,10 @@ var OrchestratorPool = class {
|
|
|
561
511
|
// for a planner that is running at "high".
|
|
562
512
|
plannerThinkingEffort: config.plannerThinkingEffort ?? "",
|
|
563
513
|
maxParallel: config.maxParallelSessions,
|
|
564
|
-
tdd: userSettings.tdd,
|
|
565
|
-
verification: userSettings.verification,
|
|
566
514
|
modelAllowlist: userSettings.modelAllowlist,
|
|
567
515
|
// The model remembered per planner backend (this task), so a surface can
|
|
568
516
|
// render what's remembered without a second round-trip.
|
|
569
|
-
plannerModels: userSettings.plannerModels
|
|
570
|
-
// ADR-0018; a run copies it when it starts.
|
|
571
|
-
runnerTransport: userSettings.runnerTransport
|
|
517
|
+
plannerModels: userSettings.plannerModels
|
|
572
518
|
};
|
|
573
519
|
}
|
|
574
520
|
/**
|
|
@@ -596,22 +542,18 @@ var OrchestratorPool = class {
|
|
|
596
542
|
const envChanges = admission2 ? admission2.accepted : void 0;
|
|
597
543
|
const incomingAiProvider = typeof envChanges?.AI_PROVIDER === "string" ? envChanges.AI_PROVIDER : void 0;
|
|
598
544
|
const switchedToProvider = incomingAiProvider && incomingAiProvider !== providerBefore ? incomingAiProvider : void 0;
|
|
599
|
-
const
|
|
545
|
+
const requestedModel = envChanges?.ORCHESTRATOR_MODEL ?? changes.orchestratorModel;
|
|
546
|
+
const explicitModel = typeof requestedModel === "string" && requestedModel.trim().length > 0;
|
|
547
|
+
const switchRecall = switchedToProvider && !explicitModel ? this.plannerModelMemory.recall(switchedToProvider, this.plannerCatalogFor(switchedToProvider)) : void 0;
|
|
548
|
+
if (switchedToProvider && explicitModel && typeof changes.plannerThinkingEffort !== "string" && envChanges?.ORDEWELL_PLANNER_EFFORT === void 0) {
|
|
549
|
+
process.env.ORDEWELL_PLANNER_EFFORT = "";
|
|
550
|
+
}
|
|
600
551
|
if (typeof changes.orchestratorModel === "string") {
|
|
601
552
|
process.env.ORCHESTRATOR_MODEL = changes.orchestratorModel;
|
|
602
553
|
}
|
|
603
554
|
if (typeof changes.plannerThinkingEffort === "string") {
|
|
604
555
|
process.env.ORDEWELL_PLANNER_EFFORT = changes.plannerThinkingEffort;
|
|
605
556
|
}
|
|
606
|
-
if (changes.tdd && typeof changes.tdd.enabled === "boolean") {
|
|
607
|
-
this.settingsService.setTdd(changes.tdd.enabled);
|
|
608
|
-
}
|
|
609
|
-
if (changes.verification && typeof changes.verification.enabled === "boolean") {
|
|
610
|
-
this.settingsService.setVerification(changes.verification.enabled);
|
|
611
|
-
}
|
|
612
|
-
if (core.isRunnerTransport(changes.runnerTransport)) {
|
|
613
|
-
this.settingsService.setRunnerTransport(changes.runnerTransport);
|
|
614
|
-
}
|
|
615
557
|
if (envChanges) {
|
|
616
558
|
let touched = false;
|
|
617
559
|
for (const [key, value] of Object.entries(envChanges)) {
|
|
@@ -619,7 +561,11 @@ var OrchestratorPool = class {
|
|
|
619
561
|
if (core.PROVIDER_CREDENTIAL_ENV.has(key)) touched = true;
|
|
620
562
|
}
|
|
621
563
|
if (envChanges.ORDEWELL_MAX_PARALLEL !== void 0) {
|
|
622
|
-
for (const session of this.sessions
|
|
564
|
+
for (const [id, session] of this.sessions) {
|
|
565
|
+
session.reschedule().catch((err) => {
|
|
566
|
+
console.error(`[web] reschedule failed for session ${id}: ${err instanceof Error ? err.message : err}`);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
623
569
|
}
|
|
624
570
|
if (touched) {
|
|
625
571
|
this.modelResolver.invalidate();
|
|
@@ -640,7 +586,7 @@ var OrchestratorPool = class {
|
|
|
640
586
|
}
|
|
641
587
|
}
|
|
642
588
|
}
|
|
643
|
-
const envCarriesModelOrEffort = envChanges?.AI_PROVIDER === void 0 && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
|
|
589
|
+
const envCarriesModelOrEffort = (envChanges?.AI_PROVIDER === void 0 || explicitModel) && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
|
|
644
590
|
if (typeof changes.orchestratorModel === "string" || typeof changes.plannerThinkingEffort === "string" || envCarriesModelOrEffort) {
|
|
645
591
|
const rememberProvider = switchedToProvider ?? new WebConfig({
|
|
646
592
|
enabledRunners: this.enabledRunnerOverride(),
|
|
@@ -655,6 +601,7 @@ var OrchestratorPool = class {
|
|
|
655
601
|
const rejectedEnvKeys = admission2?.rejected ?? [];
|
|
656
602
|
const settings = this.getSettings();
|
|
657
603
|
if (rejectedEnvKeys.length > 0) settings.rejectedEnvKeys = rejectedEnvKeys;
|
|
604
|
+
if (switchRecall) settings.switchRecall = switchRecall;
|
|
658
605
|
return settings;
|
|
659
606
|
}
|
|
660
607
|
setOrchestratorModel(model) {
|
|
@@ -683,18 +630,7 @@ var OrchestratorPool = class {
|
|
|
683
630
|
}
|
|
684
631
|
}
|
|
685
632
|
}
|
|
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
|
-
}
|
|
633
|
+
const providers = core.configuredProviders(config);
|
|
698
634
|
return {
|
|
699
635
|
models: allModels,
|
|
700
636
|
modelsByRunner,
|
|
@@ -717,11 +653,11 @@ var OrchestratorPool = class {
|
|
|
717
653
|
async getInstalledRunners() {
|
|
718
654
|
const config = new WebConfig({ enabledRunners: this.enabledRunnerOverride() });
|
|
719
655
|
const enabled = new Set(config.enabledRunners);
|
|
720
|
-
const
|
|
656
|
+
const runners = this.registry.list();
|
|
721
657
|
const installedIds = new Set(
|
|
722
|
-
await this.runnerInstallation.filterInstalled(
|
|
658
|
+
await this.runnerInstallation.filterInstalled(runners.map((p) => p.manifest.name))
|
|
723
659
|
);
|
|
724
|
-
return
|
|
660
|
+
return runners.filter((p) => installedIds.has(p.manifest.name)).map((p) => ({
|
|
725
661
|
id: p.manifest.name,
|
|
726
662
|
name: p.manifest.displayName,
|
|
727
663
|
enabled: enabled.has(p.manifest.name)
|
|
@@ -729,15 +665,9 @@ var OrchestratorPool = class {
|
|
|
729
665
|
}
|
|
730
666
|
async generatePlan(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
731
667
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
668
|
+
this.destroy(sessionId);
|
|
732
669
|
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
|
-
}
|
|
670
|
+
return core.migratePlanState(await session.generatePlan(goal, runners));
|
|
741
671
|
}
|
|
742
672
|
/**
|
|
743
673
|
* Conversational planning (ADR-0002): start the planner dialogue for a
|
|
@@ -747,69 +677,27 @@ var OrchestratorPool = class {
|
|
|
747
677
|
*/
|
|
748
678
|
async startPlanning(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
749
679
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
680
|
+
this.destroy(sessionId);
|
|
750
681
|
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
|
-
}
|
|
682
|
+
return session.startPlanning(goal, runners);
|
|
758
683
|
}
|
|
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
|
-
*/
|
|
684
|
+
/** Continue the planner dialogue with the user's reply; refused while a planner turn is in flight. */
|
|
765
685
|
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
|
-
}
|
|
686
|
+
return this.session(sessionId).continueConversation(message);
|
|
774
687
|
}
|
|
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
|
-
*/
|
|
688
|
+
/** Condense a session's conversation. The summary turn is a planner turn, so `cancelPlanning` stops it. */
|
|
781
689
|
async compactConversation(sessionId) {
|
|
782
690
|
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
|
-
}
|
|
691
|
+
const compaction = await session.compactConversation();
|
|
692
|
+
return { ...compaction, plan: session.planState };
|
|
794
693
|
}
|
|
795
694
|
/**
|
|
796
|
-
* Abort the
|
|
695
|
+
* Abort the planner turn in flight for a session, if any. Answers whether
|
|
797
696
|
* there was one to cancel — the route reports that rather than 404ing a
|
|
798
697
|
* session that simply is not planning right now.
|
|
799
698
|
*/
|
|
800
699
|
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);
|
|
700
|
+
return this.sessions.get(sessionId)?.abortPlannerTurn() ?? false;
|
|
813
701
|
}
|
|
814
702
|
/**
|
|
815
703
|
* Register a session persisted on disk so its plan becomes executable and
|
|
@@ -824,9 +712,10 @@ var OrchestratorPool = class {
|
|
|
824
712
|
const live = this.sessions.get(sessionId);
|
|
825
713
|
if (live?.planState) return live.planState;
|
|
826
714
|
const saved = core.loadSession(sessionId, workspace);
|
|
827
|
-
if (!saved) throw new
|
|
715
|
+
if (!saved) throw new core.SessionNotFoundError();
|
|
828
716
|
const session = this.createSessionFor(sessionId, workspace);
|
|
829
717
|
session.loadPlan(saved.plan, saved.meta.goal, workspace, { sessionId: saved.meta.id });
|
|
718
|
+
this.destroy(sessionId);
|
|
830
719
|
this.sessions.set(sessionId, session);
|
|
831
720
|
return session.planState ?? saved.plan;
|
|
832
721
|
}
|
|
@@ -853,14 +742,10 @@ var OrchestratorPool = class {
|
|
|
853
742
|
*/
|
|
854
743
|
session(sessionId) {
|
|
855
744
|
const session = this.sessions.get(sessionId);
|
|
856
|
-
if (!session) throw new
|
|
745
|
+
if (!session) throw new core.SessionNotFoundError();
|
|
857
746
|
return session;
|
|
858
747
|
}
|
|
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
748
|
destroy(sessionId) {
|
|
862
|
-
this.planningAborts.get(sessionId)?.abort();
|
|
863
|
-
this.planningAborts.delete(sessionId);
|
|
864
749
|
this.sessions.get(sessionId)?.destroy();
|
|
865
750
|
this.sessions.delete(sessionId);
|
|
866
751
|
}
|
|
@@ -875,6 +760,37 @@ function getSessionList(workspace) {
|
|
|
875
760
|
function removeSession(sessionId, workspace) {
|
|
876
761
|
return core.deleteSession(sessionId, workspace);
|
|
877
762
|
}
|
|
763
|
+
function classify(err) {
|
|
764
|
+
if (err instanceof core.SessionNotFoundError) return { status: 404, code: "session_not_found" };
|
|
765
|
+
if (err instanceof core.NoPlanError) return { status: 400, code: "no_plan" };
|
|
766
|
+
if (err instanceof core.AlreadyExecutingError) return { status: 409, code: "already_executing" };
|
|
767
|
+
if (err instanceof core.WorkspaceNotAProjectError) return { status: 400, code: "workspace_not_a_project" };
|
|
768
|
+
if (err instanceof core.WorkspaceNotFoundError) return { status: 400, code: "workspace_not_found" };
|
|
769
|
+
if (err instanceof core.PlannerTurnStoppedError) return { status: 409, code: "planner_turn_stopped" };
|
|
770
|
+
if (err instanceof core.PlannerTurnDiscardedError) return { status: 409, code: "planner_turn_discarded" };
|
|
771
|
+
if (err instanceof core.ConversationBusyError) return { status: 409, code: "conversation_busy" };
|
|
772
|
+
if (err instanceof core.ConversationEditError || err instanceof core.PlanEditError || err instanceof core.TaskControlError) {
|
|
773
|
+
return { status: 400, code: "refused" };
|
|
774
|
+
}
|
|
775
|
+
return void 0;
|
|
776
|
+
}
|
|
777
|
+
function refuse(c, status, message, code) {
|
|
778
|
+
const body = code ? { error: message, code } : { error: message };
|
|
779
|
+
return c.json(body, status);
|
|
780
|
+
}
|
|
781
|
+
function failure(c, err, label, { fallback = 500, message = "Internal error" } = {}) {
|
|
782
|
+
const classified = classify(err);
|
|
783
|
+
const status = classified?.status ?? fallback;
|
|
784
|
+
if (status >= 500) console.error(`[web] ${label} failed:`, err);
|
|
785
|
+
const body = {
|
|
786
|
+
error: err instanceof Error && err.message || message,
|
|
787
|
+
...classified ? { code: classified.code } : {},
|
|
788
|
+
...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {}
|
|
789
|
+
};
|
|
790
|
+
return c.json(body, status);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// server/routes/sessions.ts
|
|
878
794
|
function sessionsRoute(pool2) {
|
|
879
795
|
const router = new hono.Hono();
|
|
880
796
|
router.get("/", (c) => {
|
|
@@ -886,20 +802,17 @@ function sessionsRoute(pool2) {
|
|
|
886
802
|
const ws = c.req.query("workspace") || process.cwd();
|
|
887
803
|
const id = c.req.param("id");
|
|
888
804
|
const saved = core.loadSessionPlanState(id, ws);
|
|
889
|
-
if (!saved) return c
|
|
890
|
-
return c.json({ meta: saved.meta, plan: pool2.getPlanState(id) ?? saved.plan });
|
|
805
|
+
if (!saved) return refuse(c, 404, "Session not found", "session_not_found");
|
|
806
|
+
return c.json({ meta: saved.meta, plan: core.surfacePlanState(pool2.getPlanState(id) ?? saved.plan) });
|
|
891
807
|
});
|
|
892
808
|
router.post("/:id/load", (c) => {
|
|
893
809
|
const ws = c.req.query("workspace") || process.cwd();
|
|
894
810
|
try {
|
|
895
811
|
const id = c.req.param("id");
|
|
896
812
|
const plan = pool2.adoptSavedSession(id, ws);
|
|
897
|
-
return c.json({ ok: true, plan, goal: pool2.getGoal(id) });
|
|
813
|
+
return c.json({ ok: true, plan: core.surfacePlan(plan), goal: pool2.getGoal(id) });
|
|
898
814
|
} 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);
|
|
815
|
+
return failure(c, err, "load session", { message: "Failed to load session" });
|
|
903
816
|
}
|
|
904
817
|
});
|
|
905
818
|
router.get("/:id/tasks/:taskId/log", (c) => {
|
|
@@ -909,8 +822,8 @@ function sessionsRoute(pool2) {
|
|
|
909
822
|
router.get("/:id/tasks/:taskId/log/:attempt", (c) => {
|
|
910
823
|
const ws = c.req.query("workspace") || process.cwd();
|
|
911
824
|
const attempt = Number(c.req.param("attempt"));
|
|
912
|
-
if (!Number.isInteger(attempt) || attempt < 1) return c
|
|
913
|
-
return c.json({ attempt, events: core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt) });
|
|
825
|
+
if (!Number.isInteger(attempt) || attempt < 1) return refuse(c, 400, "attempt must be a positive integer");
|
|
826
|
+
return c.json({ attempt, events: core.taskLogForSurface(core.readTaskLog({ baseDir: ws, sessionId: c.req.param("id") }, c.req.param("taskId"), attempt)) });
|
|
914
827
|
});
|
|
915
828
|
router.delete("/:id", (c) => {
|
|
916
829
|
const ws = c.req.query("workspace") || process.cwd();
|
|
@@ -923,22 +836,7 @@ function sessionsRoute(pool2) {
|
|
|
923
836
|
});
|
|
924
837
|
return router;
|
|
925
838
|
}
|
|
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
839
|
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
840
|
function plansRoute(pool2) {
|
|
943
841
|
const router = new hono.Hono();
|
|
944
842
|
router.post("/:sessionId/generate", async (c) => {
|
|
@@ -950,14 +848,10 @@ function plansRoute(pool2) {
|
|
|
950
848
|
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) : pool2.getRunnerState().enabledRunners;
|
|
951
849
|
const plan = await pool2.generatePlan(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
952
850
|
const { models, modelsByRunner } = await pool2.getProviderModels();
|
|
953
|
-
|
|
851
|
+
const notes = (pool2.session(c.req.param("sessionId")).planState?.conversationHistory ?? []).filter((e) => e.kind === "system").map((e) => e.content);
|
|
852
|
+
return c.json({ plan: core.surfacePlanState(plan), models, modelsByRunner, ...notes.length > 0 ? { notes } : {} });
|
|
954
853
|
} 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);
|
|
854
|
+
return failure(c, err, "generate", { message: "Plan generation failed" });
|
|
961
855
|
}
|
|
962
856
|
});
|
|
963
857
|
router.post("/:sessionId/execute", async (c) => {
|
|
@@ -965,9 +859,7 @@ function plansRoute(pool2) {
|
|
|
965
859
|
await pool2.session(c.req.param("sessionId")).executePlan();
|
|
966
860
|
return c.json({ status: "running" });
|
|
967
861
|
} 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);
|
|
862
|
+
return failure(c, err, "execute");
|
|
971
863
|
}
|
|
972
864
|
});
|
|
973
865
|
router.post("/:sessionId/stop", (c) => {
|
|
@@ -983,7 +875,7 @@ function plansRoute(pool2) {
|
|
|
983
875
|
await pool2.session(c.req.param("sessionId")).markTaskComplete(c.req.param("taskId"));
|
|
984
876
|
return c.json({ ok: true });
|
|
985
877
|
} catch (err) {
|
|
986
|
-
return c
|
|
878
|
+
return failure(c, err, "mark complete", { fallback: 404, message: "Not found" });
|
|
987
879
|
}
|
|
988
880
|
});
|
|
989
881
|
router.post("/:sessionId/tasks/:taskId/uncomplete", async (c) => {
|
|
@@ -991,7 +883,7 @@ function plansRoute(pool2) {
|
|
|
991
883
|
await pool2.session(c.req.param("sessionId")).markTaskIncomplete(c.req.param("taskId"));
|
|
992
884
|
return c.json({ ok: true });
|
|
993
885
|
} catch (err) {
|
|
994
|
-
return c
|
|
886
|
+
return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
|
|
995
887
|
}
|
|
996
888
|
});
|
|
997
889
|
for (const [segment, run2] of [
|
|
@@ -1005,39 +897,29 @@ function plansRoute(pool2) {
|
|
|
1005
897
|
await run2(pool2.session(c.req.param("sessionId")), c.req.param("taskId"));
|
|
1006
898
|
return c.json({ ok: true });
|
|
1007
899
|
} catch (err) {
|
|
1008
|
-
|
|
1009
|
-
return c.json({ error: err.message }, status);
|
|
900
|
+
return failure(c, err, segment);
|
|
1010
901
|
}
|
|
1011
902
|
});
|
|
1012
903
|
}
|
|
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) => {
|
|
904
|
+
const messageHandler = (send) => async (c) => {
|
|
1025
905
|
try {
|
|
1026
906
|
const body = await c.req.json().catch(() => ({}));
|
|
1027
907
|
const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
|
|
1028
908
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
|
|
1029
|
-
const id = pool2.session(c.req.param("sessionId"))
|
|
909
|
+
const id = send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
|
|
1030
910
|
return c.json({ id });
|
|
1031
911
|
} catch (err) {
|
|
1032
|
-
return
|
|
912
|
+
return failure(c, err, "task edit");
|
|
1033
913
|
}
|
|
1034
|
-
}
|
|
914
|
+
};
|
|
915
|
+
router.post("/:sessionId/tasks/:taskId/messages", messageHandler((session, taskId, text) => session.sendTaskMessage(taskId, text)));
|
|
916
|
+
router.post("/:sessionId/tasks/:taskId/messages/now", messageHandler((session, taskId, text) => session.forceSendTaskMessage(taskId, text)));
|
|
1035
917
|
router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
|
|
1036
918
|
try {
|
|
1037
919
|
const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
1038
920
|
return c.json({ sent });
|
|
1039
921
|
} catch (err) {
|
|
1040
|
-
return
|
|
922
|
+
return failure(c, err, "task edit");
|
|
1041
923
|
}
|
|
1042
924
|
});
|
|
1043
925
|
router.post("/:sessionId/tasks/:taskId/continue", async (c) => {
|
|
@@ -1048,7 +930,7 @@ function plansRoute(pool2) {
|
|
|
1048
930
|
await pool2.session(c.req.param("sessionId")).continueTask(c.req.param("taskId"), text);
|
|
1049
931
|
return c.json({ ok: true });
|
|
1050
932
|
} catch (err) {
|
|
1051
|
-
return
|
|
933
|
+
return failure(c, err, "task edit");
|
|
1052
934
|
}
|
|
1053
935
|
});
|
|
1054
936
|
router.delete("/:sessionId/tasks/:taskId/messages/:messageId", (c) => {
|
|
@@ -1056,7 +938,7 @@ function plansRoute(pool2) {
|
|
|
1056
938
|
const removed = pool2.session(c.req.param("sessionId")).removeQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
1057
939
|
return c.json({ removed });
|
|
1058
940
|
} catch (err) {
|
|
1059
|
-
return
|
|
941
|
+
return failure(c, err, "task edit");
|
|
1060
942
|
}
|
|
1061
943
|
});
|
|
1062
944
|
router.post("/:sessionId/tasks/:taskId/interrupt", async (c) => {
|
|
@@ -1064,18 +946,18 @@ function plansRoute(pool2) {
|
|
|
1064
946
|
await pool2.session(c.req.param("sessionId")).interruptTask(c.req.param("taskId"));
|
|
1065
947
|
return c.json({ ok: true });
|
|
1066
948
|
} catch (err) {
|
|
1067
|
-
return
|
|
949
|
+
return failure(c, err, "task edit");
|
|
1068
950
|
}
|
|
1069
951
|
});
|
|
1070
952
|
router.post("/:sessionId/tasks/:taskId/checkpoint/approve", (c) => {
|
|
1071
953
|
try {
|
|
1072
954
|
const session = pool2.session(c.req.param("sessionId"));
|
|
1073
955
|
const taskId = c.req.param("taskId");
|
|
1074
|
-
if (!session.awaitsCheckpoint(taskId)) return c
|
|
956
|
+
if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
|
|
1075
957
|
session.approveCheckpoint(taskId);
|
|
1076
958
|
return c.json({ ok: true });
|
|
1077
959
|
} catch (err) {
|
|
1078
|
-
return
|
|
960
|
+
return failure(c, err, "task edit");
|
|
1079
961
|
}
|
|
1080
962
|
});
|
|
1081
963
|
router.post("/:sessionId/tasks/:taskId/checkpoint/reject", async (c) => {
|
|
@@ -1084,11 +966,11 @@ function plansRoute(pool2) {
|
|
|
1084
966
|
const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason.trim() : "";
|
|
1085
967
|
const session = pool2.session(c.req.param("sessionId"));
|
|
1086
968
|
const taskId = c.req.param("taskId");
|
|
1087
|
-
if (!session.awaitsCheckpoint(taskId)) return c
|
|
969
|
+
if (!session.awaitsCheckpoint(taskId)) return refuse(c, 409, NO_CHECKPOINT, "checkpoint_not_waiting");
|
|
1088
970
|
session.rejectCheckpoint(taskId, reason || void 0);
|
|
1089
971
|
return c.json({ ok: true });
|
|
1090
972
|
} catch (err) {
|
|
1091
|
-
return
|
|
973
|
+
return failure(c, err, "task edit");
|
|
1092
974
|
}
|
|
1093
975
|
});
|
|
1094
976
|
router.get("/:sessionId/tasks/:taskId/merge-gate", (c) => {
|
|
@@ -1101,21 +983,21 @@ function plansRoute(pool2) {
|
|
|
1101
983
|
});
|
|
1102
984
|
return c.json({ mergeGate });
|
|
1103
985
|
} catch (err) {
|
|
1104
|
-
return
|
|
986
|
+
return failure(c, err, "task edit");
|
|
1105
987
|
}
|
|
1106
988
|
});
|
|
1107
989
|
router.get("/:sessionId/isolation/diff", async (c) => {
|
|
1108
990
|
try {
|
|
1109
991
|
return c.json({ diff: await pool2.session(c.req.param("sessionId")).reviewRunDiff() });
|
|
1110
992
|
} catch (err) {
|
|
1111
|
-
return
|
|
993
|
+
return failure(c, err, "task edit");
|
|
1112
994
|
}
|
|
1113
995
|
});
|
|
1114
996
|
router.post("/:sessionId/isolation/merge", async (c) => {
|
|
1115
997
|
try {
|
|
1116
998
|
return c.json(await pool2.session(c.req.param("sessionId")).mergeRun());
|
|
1117
999
|
} catch (err) {
|
|
1118
|
-
return
|
|
1000
|
+
return failure(c, err, "task edit");
|
|
1119
1001
|
}
|
|
1120
1002
|
});
|
|
1121
1003
|
for (const [segment, run2] of [
|
|
@@ -1129,16 +1011,16 @@ function plansRoute(pool2) {
|
|
|
1129
1011
|
await run2(pool2.session(c.req.param("sessionId")));
|
|
1130
1012
|
return c.json({ ok: true });
|
|
1131
1013
|
} catch (err) {
|
|
1132
|
-
return
|
|
1014
|
+
return failure(c, err, "task edit");
|
|
1133
1015
|
}
|
|
1134
1016
|
});
|
|
1135
1017
|
}
|
|
1136
1018
|
router.post("/:sessionId/tasks/:taskId/resolve-conflict", async (c) => {
|
|
1137
1019
|
try {
|
|
1138
1020
|
const plan = await pool2.session(c.req.param("sessionId")).resolveConflictAsTask(c.req.param("taskId"));
|
|
1139
|
-
return c.json({ plan });
|
|
1021
|
+
return c.json({ plan: plan && core.surfacePlan(plan) });
|
|
1140
1022
|
} catch (err) {
|
|
1141
|
-
return
|
|
1023
|
+
return failure(c, err, "task edit");
|
|
1142
1024
|
}
|
|
1143
1025
|
});
|
|
1144
1026
|
router.put("/:sessionId/tasks/:taskId", async (c) => {
|
|
@@ -1154,43 +1036,41 @@ function plansRoute(pool2) {
|
|
|
1154
1036
|
try {
|
|
1155
1037
|
result = await session.setTaskDependencies(taskId, dependencies.map(String));
|
|
1156
1038
|
} catch (err) {
|
|
1157
|
-
return c
|
|
1039
|
+
return failure(c, err, "task dependencies", { fallback: 400 });
|
|
1158
1040
|
}
|
|
1159
1041
|
}
|
|
1160
1042
|
if (Object.keys(rest).length > 0 || !hasRunner && !hasDeps) result = await session.updateTask(taskId, rest);
|
|
1161
|
-
if (!result) return c
|
|
1043
|
+
if (!result) return refuse(c, 404, "Task not found", "task_not_found");
|
|
1162
1044
|
return c.json({ ok: true });
|
|
1163
1045
|
} catch (err) {
|
|
1164
|
-
return
|
|
1046
|
+
return failure(c, err, "task edit");
|
|
1165
1047
|
}
|
|
1166
1048
|
});
|
|
1167
1049
|
router.delete("/:sessionId/tasks/:taskId", async (c) => {
|
|
1168
1050
|
try {
|
|
1169
1051
|
const result = await pool2.session(c.req.param("sessionId")).removeTask(c.req.param("taskId"));
|
|
1170
|
-
if (!result) return c
|
|
1052
|
+
if (!result) return refuse(c, 404, "Task not found", "task_not_found");
|
|
1171
1053
|
return c.json({ ok: true });
|
|
1172
1054
|
} catch (err) {
|
|
1173
|
-
return
|
|
1055
|
+
return failure(c, err, "task edit");
|
|
1174
1056
|
}
|
|
1175
1057
|
});
|
|
1176
1058
|
router.post("/:sessionId/tasks", async (c) => {
|
|
1177
1059
|
try {
|
|
1178
1060
|
const body = await c.req.json();
|
|
1179
1061
|
const result = await pool2.session(c.req.param("sessionId")).addTask(body);
|
|
1180
|
-
if (!result) return c
|
|
1062
|
+
if (!result) return refuse(c, 404, "Session not found", "session_not_found");
|
|
1181
1063
|
return c.json({ ok: true });
|
|
1182
1064
|
} catch (err) {
|
|
1183
|
-
return
|
|
1065
|
+
return failure(c, err, "task edit");
|
|
1184
1066
|
}
|
|
1185
1067
|
});
|
|
1186
1068
|
router.post("/:sessionId/review/approve", async (c) => {
|
|
1187
1069
|
try {
|
|
1188
1070
|
const plan = await pool2.session(c.req.param("sessionId")).approveReview();
|
|
1189
|
-
return c.json({ plan });
|
|
1071
|
+
return c.json({ plan: core.surfacePlan(plan) });
|
|
1190
1072
|
} 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);
|
|
1073
|
+
return failure(c, err, "review approve");
|
|
1194
1074
|
}
|
|
1195
1075
|
});
|
|
1196
1076
|
router.post("/:sessionId/tasks/merge", async (c) => {
|
|
@@ -1200,21 +1080,17 @@ function plansRoute(pool2) {
|
|
|
1200
1080
|
return c.json({ error: "taskIds array with at least two ids is required" }, 400);
|
|
1201
1081
|
}
|
|
1202
1082
|
const plan = await pool2.session(c.req.param("sessionId")).requestMerge(taskIds);
|
|
1203
|
-
return c.json({ plan });
|
|
1083
|
+
return c.json({ plan: core.surfacePlan(plan) });
|
|
1204
1084
|
} catch (err) {
|
|
1205
|
-
|
|
1206
|
-
const status = e.message === "Session not found" ? 404 : 400;
|
|
1207
|
-
return c.json({ error: e.message }, status);
|
|
1085
|
+
return failure(c, err, "merge", { fallback: 400 });
|
|
1208
1086
|
}
|
|
1209
1087
|
});
|
|
1210
1088
|
router.post("/:sessionId/tasks/:taskId/split", async (c) => {
|
|
1211
1089
|
try {
|
|
1212
1090
|
const plan = await pool2.session(c.req.param("sessionId")).requestSplit(c.req.param("taskId"));
|
|
1213
|
-
return c.json({ plan });
|
|
1091
|
+
return c.json({ plan: core.surfacePlan(plan) });
|
|
1214
1092
|
} catch (err) {
|
|
1215
|
-
|
|
1216
|
-
const status = e.message === "Session not found" ? 404 : 400;
|
|
1217
|
-
return c.json({ error: e.message }, status);
|
|
1093
|
+
return failure(c, err, "split", { fallback: 400 });
|
|
1218
1094
|
}
|
|
1219
1095
|
});
|
|
1220
1096
|
router.post("/:sessionId/converse/start", async (c) => {
|
|
@@ -1224,13 +1100,9 @@ function plansRoute(pool2) {
|
|
|
1224
1100
|
const ws = workspace || c.req.query("workspace") || process.cwd();
|
|
1225
1101
|
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : pool2.getRunnerState().enabledRunners;
|
|
1226
1102
|
const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
1227
|
-
return c.json({ plan });
|
|
1103
|
+
return c.json({ plan: core.surfacePlan(plan) });
|
|
1228
1104
|
} 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);
|
|
1105
|
+
return failure(c, err, "converse start", { message: "Planning failed" });
|
|
1234
1106
|
}
|
|
1235
1107
|
});
|
|
1236
1108
|
router.post("/:sessionId/converse/message", async (c) => {
|
|
@@ -1238,57 +1110,47 @@ function plansRoute(pool2) {
|
|
|
1238
1110
|
const { message } = await c.req.json();
|
|
1239
1111
|
if (!message) return c.json({ error: "message is required" }, 400);
|
|
1240
1112
|
const plan = await pool2.continuePlanning(c.req.param("sessionId"), message);
|
|
1241
|
-
return c.json({ plan });
|
|
1113
|
+
return c.json({ plan: core.surfacePlan(plan) });
|
|
1242
1114
|
} 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);
|
|
1115
|
+
return failure(c, err, "conversation message");
|
|
1247
1116
|
}
|
|
1248
1117
|
});
|
|
1249
1118
|
router.post("/:sessionId/conversation/fork", (c) => {
|
|
1250
1119
|
try {
|
|
1251
|
-
|
|
1120
|
+
const fork = pool2.forkConversation(c.req.param("sessionId"));
|
|
1121
|
+
return c.json({ ...fork, plan: core.surfacePlan(fork.plan) });
|
|
1252
1122
|
} catch (err) {
|
|
1253
|
-
return
|
|
1123
|
+
return failure(c, err, "conversation edit");
|
|
1254
1124
|
}
|
|
1255
1125
|
});
|
|
1256
1126
|
router.get("/:sessionId/conversation/rewind-targets", (c) => {
|
|
1257
1127
|
try {
|
|
1258
1128
|
return c.json({ targets: pool2.session(c.req.param("sessionId")).rewindTargets() });
|
|
1259
1129
|
} catch (err) {
|
|
1260
|
-
return
|
|
1130
|
+
return failure(c, err, "conversation edit");
|
|
1261
1131
|
}
|
|
1262
1132
|
});
|
|
1263
1133
|
router.post("/:sessionId/conversation/rewind", async (c) => {
|
|
1264
1134
|
try {
|
|
1265
1135
|
const { index } = await c.req.json();
|
|
1266
1136
|
if (!Number.isInteger(index) || index < 0) return c.json({ error: "index must be a non-negative integer" }, 400);
|
|
1267
|
-
|
|
1137
|
+
const rewound = pool2.rewindConversation(c.req.param("sessionId"), index);
|
|
1138
|
+
return c.json({ ...rewound, plan: core.surfacePlan(rewound.plan) });
|
|
1268
1139
|
} catch (err) {
|
|
1269
|
-
return
|
|
1140
|
+
return failure(c, err, "conversation edit");
|
|
1270
1141
|
}
|
|
1271
1142
|
});
|
|
1272
1143
|
router.post("/:sessionId/conversation/compact", async (c) => {
|
|
1273
1144
|
try {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
return conversationFailure(c, err);
|
|
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 });
|
|
1145
|
+
const compacted = await pool2.compactConversation(c.req.param("sessionId"));
|
|
1146
|
+
return c.json({ ...compacted, plan: core.surfacePlan(compacted.plan) });
|
|
1283
1147
|
} catch (err) {
|
|
1284
|
-
|
|
1285
|
-
const status = e.message === "Session not found" ? 404 : 500;
|
|
1286
|
-
return c.json({ error: e.message }, status);
|
|
1148
|
+
return failure(c, err, "conversation edit");
|
|
1287
1149
|
}
|
|
1288
1150
|
});
|
|
1289
1151
|
router.get("/:sessionId/prd", (c) => {
|
|
1290
1152
|
const plan = pool2.getPlan(c.req.param("sessionId"));
|
|
1291
|
-
if (!plan?.prdMarkdown) return c
|
|
1153
|
+
if (!plan?.prdMarkdown) return refuse(c, 404, "No PRD found");
|
|
1292
1154
|
return c.json({ prdMarkdown: plan.prdMarkdown });
|
|
1293
1155
|
});
|
|
1294
1156
|
return router;
|
|
@@ -1339,7 +1201,7 @@ function approvalsRoute(pool2) {
|
|
|
1339
1201
|
const router = new hono.Hono();
|
|
1340
1202
|
router.get("/:sessionId", (c) => {
|
|
1341
1203
|
const sessionId = c.req.param("sessionId");
|
|
1342
|
-
if (!pool2.hasSession(sessionId)) return c
|
|
1204
|
+
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1343
1205
|
return c.json({
|
|
1344
1206
|
pending: pool2.outstandingApprovals(sessionId),
|
|
1345
1207
|
approvedScopes: pool2.approvedScopes(sessionId)
|
|
@@ -1348,10 +1210,10 @@ function approvalsRoute(pool2) {
|
|
|
1348
1210
|
router.post("/:sessionId/:approvalId", async (c) => {
|
|
1349
1211
|
const sessionId = c.req.param("sessionId");
|
|
1350
1212
|
const approvalId = c.req.param("approvalId");
|
|
1351
|
-
if (!pool2.hasSession(sessionId)) return c
|
|
1213
|
+
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1352
1214
|
const answer = answerOf(await c.req.json().catch(() => null));
|
|
1353
1215
|
if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
|
|
1354
|
-
return c
|
|
1216
|
+
return refuse(c, 409, "Approval is no longer outstanding \u2014 it timed out or was already answered.", "approval_not_outstanding");
|
|
1355
1217
|
}
|
|
1356
1218
|
const { decision } = core.toApprovalDecision(answer);
|
|
1357
1219
|
return c.json({ ok: true, granted: core.isGranted({ decision }), decision });
|
|
@@ -1361,64 +1223,28 @@ function approvalsRoute(pool2) {
|
|
|
1361
1223
|
function settingsRoute(pool2) {
|
|
1362
1224
|
const router = new hono.Hono();
|
|
1363
1225
|
router.get("/", (c) => {
|
|
1364
|
-
|
|
1226
|
+
const settings = pool2.getSettings();
|
|
1227
|
+
return c.json(settings);
|
|
1365
1228
|
});
|
|
1366
1229
|
router.patch("/", async (c) => {
|
|
1367
1230
|
const body = await c.req.json();
|
|
1368
1231
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
1369
|
-
return c
|
|
1232
|
+
return refuse(c, 400, "At least one setting field is required");
|
|
1370
1233
|
}
|
|
1371
1234
|
const result = pool2.updateSettings(body);
|
|
1372
1235
|
return c.json(result);
|
|
1373
1236
|
});
|
|
1374
1237
|
return router;
|
|
1375
1238
|
}
|
|
1376
|
-
var COMMANDS = [
|
|
1377
|
-
|
|
1378
|
-
{ name: "verify", description: "Toggle verification mode \u2014 adds a final evidence-based verification task that runs the full suite (on|off|status)" },
|
|
1379
|
-
{ name: "transport", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run (terminal|structured|status)" }
|
|
1380
|
-
];
|
|
1381
|
-
function commandsRoute(pool2) {
|
|
1239
|
+
var COMMANDS = [];
|
|
1240
|
+
function commandsRoute() {
|
|
1382
1241
|
const router = new hono.Hono();
|
|
1383
1242
|
router.get("/", (c) => {
|
|
1384
1243
|
return c.json({ commands: COMMANDS });
|
|
1385
1244
|
});
|
|
1386
|
-
router.post("/:name",
|
|
1245
|
+
router.post("/:name", (c) => {
|
|
1387
1246
|
const name = c.req.param("name");
|
|
1388
|
-
|
|
1389
|
-
const args2 = body?.args || {};
|
|
1390
|
-
const command = COMMANDS.find((cmd) => cmd.name === name);
|
|
1391
|
-
if (!command) {
|
|
1392
|
-
return c.json({ error: `Unknown command: ${name}` }, 404);
|
|
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() });
|
|
1402
|
-
}
|
|
1403
|
-
if (name === "verify") {
|
|
1404
|
-
const action = args2.action || "status";
|
|
1405
|
-
if (action === "on") {
|
|
1406
|
-
pool2.updateSettings({ verification: { enabled: true } });
|
|
1407
|
-
} else if (action === "off") {
|
|
1408
|
-
pool2.updateSettings({ verification: { enabled: false } });
|
|
1409
|
-
}
|
|
1410
|
-
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1411
|
-
}
|
|
1412
|
-
if (name === "transport") {
|
|
1413
|
-
const action = args2.action || "status";
|
|
1414
|
-
if (action === "terminal" || action === "structured") {
|
|
1415
|
-
pool2.updateSettings({ runnerTransport: action });
|
|
1416
|
-
} else if (action !== "status") {
|
|
1417
|
-
return c.json({ error: `Unknown transport: ${action} (terminal|structured)` }, 400);
|
|
1418
|
-
}
|
|
1419
|
-
return c.json({ ok: true, settings: pool2.getSettings() });
|
|
1420
|
-
}
|
|
1421
|
-
return c.json({ error: `Unknown command: ${name}` }, 404);
|
|
1247
|
+
return refuse(c, 404, `Unknown command: ${name}`);
|
|
1422
1248
|
});
|
|
1423
1249
|
return router;
|
|
1424
1250
|
}
|
|
@@ -1524,7 +1350,7 @@ function createApp(pool2, admission2) {
|
|
|
1524
1350
|
app2.route("/api/runners", runnersRoute(pool2));
|
|
1525
1351
|
app2.route("/api/models", modelsRoute(pool2));
|
|
1526
1352
|
app2.route("/api/settings", settingsRoute(pool2));
|
|
1527
|
-
app2.route("/api/commands", commandsRoute(
|
|
1353
|
+
app2.route("/api/commands", commandsRoute());
|
|
1528
1354
|
app2.route("/api/workspaces", workspacesRoute());
|
|
1529
1355
|
app2.route("/api/approvals", approvalsRoute(pool2));
|
|
1530
1356
|
return app2;
|
|
@@ -1612,10 +1438,9 @@ if (portIdx !== -1 && args[portIdx + 1]) {
|
|
|
1612
1438
|
}
|
|
1613
1439
|
var watchParentIdx = args.indexOf("--watch-parent");
|
|
1614
1440
|
var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
|
|
1615
|
-
var { runner: tmuxRunner, advice: terminalAdvice } = startTerminalHost(port);
|
|
1616
1441
|
var { token, file: tokenFile } = core.mintDaemonToken(port);
|
|
1617
1442
|
var admission = { port, token, tokenFile };
|
|
1618
|
-
var pool = new OrchestratorPool({ runner:
|
|
1443
|
+
var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
|
|
1619
1444
|
var app = createApp(pool, admission);
|
|
1620
1445
|
pool.getProviderModels().catch((err) => {
|
|
1621
1446
|
console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
|
|
@@ -1626,14 +1451,15 @@ attachWsHandler(server, pool, admission);
|
|
|
1626
1451
|
server.listen(port, "127.0.0.1", () => {
|
|
1627
1452
|
console.error(`[web] Ordewell API server: http://localhost:${port}`);
|
|
1628
1453
|
});
|
|
1629
|
-
|
|
1454
|
+
function shutdown() {
|
|
1630
1455
|
core.clearDaemonToken(port);
|
|
1631
1456
|
pool.destroyAll();
|
|
1632
|
-
if (tmuxRunner) await tmuxRunner.killSession().catch(() => {
|
|
1633
|
-
});
|
|
1634
1457
|
server.close();
|
|
1635
1458
|
process.exit(0);
|
|
1636
1459
|
}
|
|
1460
|
+
process.on("unhandledRejection", (reason) => {
|
|
1461
|
+
console.error(`[web] unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
|
|
1462
|
+
});
|
|
1637
1463
|
process.on("SIGINT", () => {
|
|
1638
1464
|
shutdown();
|
|
1639
1465
|
});
|