@ordewell/web 0.9.0 → 0.10.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.
@@ -2,10 +2,10 @@
2
2
  'use strict';
3
3
 
4
4
  var hono = require('hono');
5
+ var child_process = require('child_process');
5
6
  var core = require('@ordewell/core');
6
7
  var fs = require('fs');
7
8
  var path = require('path');
8
- var child_process = require('child_process');
9
9
  var ws = require('ws');
10
10
  var http = require('http');
11
11
 
@@ -304,17 +304,8 @@ var PoolFileSystem = class extends core.BaseFileSystem {
304
304
  };
305
305
  }
306
306
  async listDirImpl(absPath, depth) {
307
- if (depth > 1) {
308
- const result = await run("tree", ["-L", String(depth), "-I", "node_modules|.git|dist|build", absPath], { timeout: 1e4, env: this.searchEnv() });
309
- if (result.code === 0 && result.stdout.trim()) {
310
- const lines = result.stdout.trim().split("\n");
311
- return { success: true, output: lines.slice(0, 300).join("\n"), truncated: lines.length > 300 };
312
- }
313
- }
314
307
  try {
315
- const entries = await fs.promises.readdir(absPath, { withFileTypes: true });
316
- const lines = entries.slice(0, 200).map((e) => `${e.isDirectory() ? "D" : "F"} ${e.name}`);
317
- return { success: true, output: lines.join("\n") || "(empty directory)", truncated: entries.length > 200 };
308
+ return await core.listTree(absPath, depth, async (abs) => (await fs.promises.readdir(abs, { withFileTypes: true })).map((e) => ({ name: e.name, directory: e.isDirectory() })));
318
309
  } catch (err) {
319
310
  return { success: false, output: `Error listing ${absPath}: ${err.message}`, truncated: false };
320
311
  }
@@ -388,11 +379,22 @@ var OrchestratorPool = class {
388
379
  settingsService = new core.SettingsService();
389
380
  plannerModelMemory = new core.PlannerModelMemory(this.settingsService);
390
381
  runner;
382
+ /** One probe per daemon, asked again whenever the runner list is refreshed (ADR-0033). */
383
+ codexSandbox;
391
384
  constructor(deps = {}) {
392
385
  const pluginNotice = core.removedPluginNotice();
393
386
  if (pluginNotice) console.warn(pluginNotice);
394
387
  this.runner = deps.runner ?? new core.StructuredRunner();
395
388
  this.modelResolver = deps.modelResolver ?? new core.ModelResolver(this.registry, new WebConfig());
389
+ this.codexSandbox = deps.codexSandbox ?? new core.CodexSandboxAvailability(this.registry, { spawn: child_process.spawn, fetch: globalThis.fetch }, { cwd: process.cwd(), env: process.env });
390
+ }
391
+ /**
392
+ * Not awaited: planning must start at once, and the daemon already warms the
393
+ * probe at startup through the catalog. This only covers a daemon that was
394
+ * asked to plan before that finished.
395
+ */
396
+ warmCodexSandbox() {
397
+ this.codexSandbox.ensure().catch((err) => console.error(`[web] codex sandbox probe failed: ${err instanceof Error ? err.message : err}`));
396
398
  }
397
399
  subscribe(sessionId, ws) {
398
400
  if (!this.clients.has(sessionId)) this.clients.set(sessionId, /* @__PURE__ */ new Set());
@@ -438,6 +440,10 @@ var OrchestratorPool = class {
438
440
  getGoal(sessionId) {
439
441
  return this.sessions.get(sessionId)?.currentGoal ?? "";
440
442
  }
443
+ /** Null for a session this process is not holding: there is no run to describe. */
444
+ runSnapshot(sessionId) {
445
+ return this.sessions.get(sessionId)?.runSnapshot() ?? null;
446
+ }
441
447
  runtimeSettings() {
442
448
  return core.sessionRuntimeSettings(this.settingsService.getAll());
443
449
  }
@@ -514,11 +520,23 @@ var OrchestratorPool = class {
514
520
  modelAllowlist: userSettings.modelAllowlist,
515
521
  // The model remembered per planner backend (this task), so a surface can
516
522
  // render what's remembered without a second round-trip.
517
- plannerModels: userSettings.plannerModels
523
+ plannerModels: userSettings.plannerModels,
524
+ run: this.settingsService.getRunSettings()
518
525
  };
519
526
  }
520
527
  /**
521
- * The catalog `PlannerModelMemory.recall` should judge `provider`'s
528
+ * Save the user's run settings, and apply them at once to `sessionId`'s run
529
+ * when that session is open here (ADR-0030). Other sessions' runs keep
530
+ * theirs. Throws a {@link RunSettingsError}, saving nothing, for a value
531
+ * out of range.
532
+ */
533
+ updateRunSettings(changes, sessionId) {
534
+ this.settingsService.updateRunSettings(changes);
535
+ if (sessionId) this.sessions.get(sessionId)?.applyRunSettings(changes);
536
+ return this.getSettings();
537
+ }
538
+ /**
539
+ * The catalog `selectPlanner` should judge `provider`'s
522
540
  * remembered model against — a harness planner's own runner catalog, or the
523
541
  * cross-provider picker list for a vendor (ADR-0009), same split as
524
542
  * `plannerModelItems` in the TUI reducer. Read from whatever the resolver
@@ -532,28 +550,32 @@ var OrchestratorPool = class {
532
550
  }
533
551
  return this.modelResolver.getCachedPickerOptions().map((o) => ({ id: o.id }));
534
552
  }
535
- updateSettings(changes) {
536
- const providerBefore = new WebConfig({
553
+ /**
554
+ * The provider planning right now. A vendor planner is resolved from its
555
+ * model id, so this is read again after a write rather than carried over.
556
+ */
557
+ plannerProviderInEffect() {
558
+ return new WebConfig({
537
559
  enabledRunners: this.enabledRunnerOverride(),
538
560
  providerModelLists: this.cachedProviderLists
539
561
  }).aiProvider;
562
+ }
563
+ updateSettings(changes) {
564
+ const current = {
565
+ provider: this.plannerProviderInEffect(),
566
+ model: process.env.ORCHESTRATOR_MODEL ?? "",
567
+ effort: process.env.ORDEWELL_PLANNER_EFFORT ?? ""
568
+ };
540
569
  const rawEnv = changes.env && typeof changes.env === "object" && !Array.isArray(changes.env) ? changes.env : void 0;
541
- const admission2 = rawEnv ? core.admitSettingsEnv(rawEnv) : void 0;
542
- const envChanges = admission2 ? admission2.accepted : void 0;
543
- const incomingAiProvider = typeof envChanges?.AI_PROVIDER === "string" ? envChanges.AI_PROVIDER : void 0;
544
- const switchedToProvider = incomingAiProvider && incomingAiProvider !== providerBefore ? incomingAiProvider : void 0;
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
- }
551
- if (typeof changes.orchestratorModel === "string") {
552
- process.env.ORCHESTRATOR_MODEL = changes.orchestratorModel;
553
- }
554
- if (typeof changes.plannerThinkingEffort === "string") {
555
- process.env.ORDEWELL_PLANNER_EFFORT = changes.plannerThinkingEffort;
556
- }
570
+ const admission = rawEnv ? core.admitSettingsEnv(rawEnv) : void 0;
571
+ const envChanges = admission ? admission.accepted : void 0;
572
+ const plannerChange = {
573
+ provider: envChanges?.AI_PROVIDER,
574
+ model: envChanges?.ORCHESTRATOR_MODEL ?? stringField(changes.orchestratorModel),
575
+ effort: envChanges?.ORDEWELL_PLANNER_EFFORT ?? stringField(changes.plannerThinkingEffort)
576
+ };
577
+ const touchesPlanner = Object.values(plannerChange).some((v) => v !== void 0);
578
+ const selection = touchesPlanner ? core.selectPlanner(current, plannerChange, this.plannerModelMemory, this.plannerCatalogFor(plannerChange.provider ?? current.provider)) : void 0;
557
579
  if (envChanges) {
558
580
  let touched = false;
559
581
  for (const [key, value] of Object.entries(envChanges)) {
@@ -572,9 +594,9 @@ var OrchestratorPool = class {
572
594
  this.modelResolver.refreshRunnerModels();
573
595
  }
574
596
  }
575
- if (switchRecall) {
576
- process.env.ORCHESTRATOR_MODEL = switchRecall.model;
577
- process.env.ORDEWELL_PLANNER_EFFORT = switchRecall.effort;
597
+ if (selection) {
598
+ process.env.ORCHESTRATOR_MODEL = selection.next.model;
599
+ process.env.ORDEWELL_PLANNER_EFFORT = selection.next.effort;
578
600
  }
579
601
  if (changes.modelAllowlist && typeof changes.modelAllowlist === "object") {
580
602
  const allowlist = changes.modelAllowlist;
@@ -586,22 +608,15 @@ var OrchestratorPool = class {
586
608
  }
587
609
  }
588
610
  }
589
- const envCarriesModelOrEffort = (envChanges?.AI_PROVIDER === void 0 || explicitModel) && (typeof envChanges?.ORCHESTRATOR_MODEL === "string" || typeof envChanges?.ORDEWELL_PLANNER_EFFORT === "string");
590
- if (typeof changes.orchestratorModel === "string" || typeof changes.plannerThinkingEffort === "string" || envCarriesModelOrEffort) {
591
- const rememberProvider = switchedToProvider ?? new WebConfig({
592
- enabledRunners: this.enabledRunnerOverride(),
593
- providerModelLists: this.cachedProviderLists
594
- }).aiProvider;
595
- this.plannerModelMemory.remember(
596
- rememberProvider,
597
- process.env.ORCHESTRATOR_MODEL || "",
598
- process.env.ORDEWELL_PLANNER_EFFORT || ""
599
- );
611
+ if (selection?.remember) {
612
+ const { provider, model, effort } = selection.remember;
613
+ const switched = provider !== current.provider;
614
+ this.plannerModelMemory.remember(switched ? provider : this.plannerProviderInEffect(), model, effort);
600
615
  }
601
- const rejectedEnvKeys = admission2?.rejected ?? [];
616
+ const rejectedEnvKeys = admission?.rejected ?? [];
602
617
  const settings = this.getSettings();
603
618
  if (rejectedEnvKeys.length > 0) settings.rejectedEnvKeys = rejectedEnvKeys;
604
- if (switchRecall) settings.switchRecall = switchRecall;
619
+ if (selection?.recall) settings.switchRecall = selection.recall;
605
620
  return settings;
606
621
  }
607
622
  setOrchestratorModel(model) {
@@ -613,7 +628,8 @@ var OrchestratorPool = class {
613
628
  const runnerIds = this.registry.list().map((p) => p.manifest.name);
614
629
  const [runnerModels, providerLists] = await Promise.all([
615
630
  this.modelResolver.modelsForRunners(runnerIds),
616
- this.modelResolver.refresh()
631
+ this.modelResolver.refresh(),
632
+ this.codexSandbox.ensure()
617
633
  ]);
618
634
  const orchestratorModels = await this.modelResolver.pickerOptions();
619
635
  const providerErrors = this.modelResolver.getDiscoveryErrors();
@@ -653,6 +669,7 @@ var OrchestratorPool = class {
653
669
  async getInstalledRunners() {
654
670
  const config = new WebConfig({ enabledRunners: this.enabledRunnerOverride() });
655
671
  const enabled = new Set(config.enabledRunners);
672
+ await this.codexSandbox.refresh();
656
673
  const runners = this.registry.list();
657
674
  const installedIds = new Set(
658
675
  await this.runnerInstallation.filterInstalled(runners.map((p) => p.manifest.name))
@@ -667,6 +684,7 @@ var OrchestratorPool = class {
667
684
  const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
668
685
  this.destroy(sessionId);
669
686
  this.sessions.set(sessionId, session);
687
+ this.warmCodexSandbox();
670
688
  return core.migratePlanState(await session.generatePlan(goal, runners));
671
689
  }
672
690
  /**
@@ -679,6 +697,7 @@ var OrchestratorPool = class {
679
697
  const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
680
698
  this.destroy(sessionId);
681
699
  this.sessions.set(sessionId, session);
700
+ this.warmCodexSandbox();
682
701
  return session.startPlanning(goal, runners);
683
702
  }
684
703
  /** Continue the planner dialogue with the user's reply; refused while a planner turn is in flight. */
@@ -760,6 +779,9 @@ function getSessionList(workspace) {
760
779
  function removeSession(sessionId, workspace) {
761
780
  return core.deleteSession(sessionId, workspace);
762
781
  }
782
+ function stringField(value) {
783
+ return typeof value === "string" ? value : void 0;
784
+ }
763
785
  function classify(err) {
764
786
  if (err instanceof core.SessionNotFoundError) return { status: 404, code: "session_not_found" };
765
787
  if (err instanceof core.NoPlanError) return { status: 400, code: "no_plan" };
@@ -827,7 +849,9 @@ function sessionsRoute(pool2) {
827
849
  });
828
850
  router.delete("/:id", (c) => {
829
851
  const ws = c.req.query("workspace") || process.cwd();
830
- const ok = removeSession(c.req.param("id"), ws);
852
+ const id = c.req.param("id");
853
+ pool2.destroy(id);
854
+ const ok = removeSession(id, ws);
831
855
  return c.json({ ok });
832
856
  });
833
857
  router.post("/:id/close", (c) => {
@@ -837,6 +861,11 @@ function sessionsRoute(pool2) {
837
861
  return router;
838
862
  }
839
863
  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.";
864
+ async function runUserCommand(session, command) {
865
+ const outcome = await session.execute(command, { kind: "user" });
866
+ if (!outcome.ok) throw new core.TaskControlError(outcome.message);
867
+ return outcome;
868
+ }
840
869
  function plansRoute(pool2) {
841
870
  const router = new hono.Hono();
842
871
  router.post("/:sessionId/generate", async (c) => {
@@ -856,15 +885,24 @@ function plansRoute(pool2) {
856
885
  });
857
886
  router.post("/:sessionId/execute", async (c) => {
858
887
  try {
859
- await pool2.session(c.req.param("sessionId")).executePlan();
888
+ const session = pool2.session(c.req.param("sessionId"));
889
+ if (!session.planState || session.planTasks.length === 0) throw new core.NoPlanError("execute");
890
+ if (session.isExecuting) throw new core.AlreadyExecutingError();
891
+ await runUserCommand(session, { command: "run_plan" });
860
892
  return c.json({ status: "running" });
861
893
  } catch (err) {
862
894
  return failure(c, err, "execute");
863
895
  }
864
896
  });
865
- router.post("/:sessionId/stop", (c) => {
866
- if (pool2.hasSession(c.req.param("sessionId"))) pool2.session(c.req.param("sessionId")).stopExecution();
867
- return c.json({ status: "stopped" });
897
+ router.post("/:sessionId/stop", async (c) => {
898
+ try {
899
+ if (pool2.hasSession(c.req.param("sessionId"))) {
900
+ await pool2.session(c.req.param("sessionId")).execute({ command: "stop_run" }, { kind: "user" });
901
+ }
902
+ return c.json({ status: "stopped" });
903
+ } catch (err) {
904
+ return failure(c, err, "stop");
905
+ }
868
906
  });
869
907
  router.post("/:sessionId/planning/stop", (c) => {
870
908
  const cancelled = pool2.cancelPlanning(c.req.param("sessionId"));
@@ -886,34 +924,79 @@ function plansRoute(pool2) {
886
924
  return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
887
925
  }
888
926
  });
889
- for (const [segment, run2] of [
890
- ["run", (s, id) => s.runTask(id)],
891
- ["force-start", (s, id) => s.forceStartTask(id)],
892
- ["retry", (s, id) => s.retryTask(id)],
893
- ["cancel", (s, id) => s.cancelTask(id)]
927
+ router.post("/:sessionId/tasks/:taskId/resume/cancel", (c) => {
928
+ try {
929
+ const cancelled = pool2.session(c.req.param("sessionId")).cancelScheduledResume(c.req.param("taskId"));
930
+ return c.json({ cancelled });
931
+ } catch (err) {
932
+ return failure(c, err, "cancel resume", { fallback: 404, message: "Not found" });
933
+ }
934
+ });
935
+ router.post("/:sessionId/tasks/:taskId/resume/now", async (c) => {
936
+ try {
937
+ await pool2.session(c.req.param("sessionId")).resumeNow(c.req.param("taskId"));
938
+ return c.json({ ok: true });
939
+ } catch (err) {
940
+ return failure(c, err, "resume now", { fallback: 404, message: "Not found" });
941
+ }
942
+ });
943
+ for (const [segment, command] of [
944
+ ["run", (id) => ({ command: "run_task", task: id })],
945
+ ["retry", (id) => ({ command: "retry_task", task: id, from: "fresh" })],
946
+ ["cancel", (id) => ({ command: "stop_task", task: id })]
894
947
  ]) {
895
948
  router.post(`/:sessionId/tasks/:taskId/${segment}`, async (c) => {
896
949
  try {
897
- await run2(pool2.session(c.req.param("sessionId")), c.req.param("taskId"));
950
+ await runUserCommand(pool2.session(c.req.param("sessionId")), command(c.req.param("taskId")));
898
951
  return c.json({ ok: true });
899
952
  } catch (err) {
900
953
  return failure(c, err, segment);
901
954
  }
902
955
  });
903
956
  }
957
+ router.post("/:sessionId/tasks/:taskId/skip", async (c) => {
958
+ try {
959
+ const body = await c.req.json().catch(() => ({}));
960
+ const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason : void 0;
961
+ await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "skip_task", task: c.req.param("taskId"), ...reason ? { reason } : {} });
962
+ return c.json({ ok: true });
963
+ } catch (err) {
964
+ return failure(c, err, "skip");
965
+ }
966
+ });
967
+ router.post("/:sessionId/tasks/:taskId/force-start", async (c) => {
968
+ try {
969
+ await pool2.session(c.req.param("sessionId")).forceStartTask(c.req.param("taskId"));
970
+ return c.json({ ok: true });
971
+ } catch (err) {
972
+ return failure(c, err, "force-start");
973
+ }
974
+ });
904
975
  const messageHandler = (send) => async (c) => {
905
976
  try {
906
977
  const body = await c.req.json().catch(() => ({}));
907
978
  const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
908
979
  if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
909
- const id = send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
980
+ const id = await send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
910
981
  return c.json({ id });
911
982
  } catch (err) {
912
983
  return failure(c, err, "task edit");
913
984
  }
914
985
  };
915
- router.post("/:sessionId/tasks/:taskId/messages", messageHandler((session, taskId, text) => session.sendTaskMessage(taskId, text)));
986
+ router.post("/:sessionId/tasks/:taskId/messages", messageHandler(async (session, taskId, text) => (await runUserCommand(session, { command: "message_task", task: taskId, text })).id ?? ""));
916
987
  router.post("/:sessionId/tasks/:taskId/messages/now", messageHandler((session, taskId, text) => session.forceSendTaskMessage(taskId, text)));
988
+ router.post("/:sessionId/tasks/:taskId/commands", async (c) => {
989
+ try {
990
+ const body = await c.req.json().catch(() => ({}));
991
+ const fields = typeof body === "object" && body !== null ? body : {};
992
+ if (typeof fields.name !== "string" || !fields.name.trim()) return c.json({ error: "name is required" }, 400);
993
+ const args2 = typeof fields.args === "string" ? fields.args : "";
994
+ const id = pool2.session(c.req.param("sessionId")).invokeTaskCommand(c.req.param("taskId"), fields.name.trim(), args2);
995
+ return c.json({ id });
996
+ } catch (err) {
997
+ return failure(c, err, "task edit");
998
+ }
999
+ });
917
1000
  router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
918
1001
  try {
919
1002
  const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
@@ -927,7 +1010,7 @@ function plansRoute(pool2) {
927
1010
  const body = await c.req.json().catch(() => ({}));
928
1011
  const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
929
1012
  if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
930
- await pool2.session(c.req.param("sessionId")).continueTask(c.req.param("taskId"), text);
1013
+ await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "retry_task", task: c.req.param("taskId"), from: "session", message: text });
931
1014
  return c.json({ ok: true });
932
1015
  } catch (err) {
933
1016
  return failure(c, err, "task edit");
@@ -1115,6 +1198,16 @@ function plansRoute(pool2) {
1115
1198
  return failure(c, err, "conversation message");
1116
1199
  }
1117
1200
  });
1201
+ router.post("/:sessionId/converse/message/now", async (c) => {
1202
+ try {
1203
+ const { message } = await c.req.json();
1204
+ if (!message) return c.json({ error: "message is required" }, 400);
1205
+ const plan = await pool2.session(c.req.param("sessionId")).forceSendPrompt(message);
1206
+ return c.json({ plan: core.surfacePlan(plan) });
1207
+ } catch (err) {
1208
+ return failure(c, err, "conversation message");
1209
+ }
1210
+ });
1118
1211
  router.post("/:sessionId/conversation/fork", (c) => {
1119
1212
  try {
1120
1213
  const fork = pool2.forkConversation(c.req.param("sessionId"));
@@ -1212,11 +1305,18 @@ function approvalsRoute(pool2) {
1212
1305
  const approvalId = c.req.param("approvalId");
1213
1306
  if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
1214
1307
  const answer = answerOf(await c.req.json().catch(() => null));
1215
- if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
1308
+ const decided = core.toApprovalDecision(answer);
1309
+ const request = pool2.outstandingApprovals(sessionId).find((pending) => pending.id === approvalId)?.request;
1310
+ if (decided.decision === "deny" && request?.taskId) {
1311
+ const outcome = await pool2.session(sessionId).execute(
1312
+ { command: "answer_approval", task: request.taskId, approval: approvalId, answer: "deny", ...decided.note ? { note: decided.note } : {} },
1313
+ { kind: "user" }
1314
+ );
1315
+ if (!outcome.ok) return refuse(c, 400, outcome.message, "refused");
1316
+ } else if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
1216
1317
  return refuse(c, 409, "Approval is no longer outstanding \u2014 it timed out or was already answered.", "approval_not_outstanding");
1217
1318
  }
1218
- const { decision } = core.toApprovalDecision(answer);
1219
- return c.json({ ok: true, granted: core.isGranted({ decision }), decision });
1319
+ return c.json({ ok: true, granted: core.isGranted(decided), decision: decided.decision });
1220
1320
  });
1221
1321
  return router;
1222
1322
  }
@@ -1228,10 +1328,21 @@ function settingsRoute(pool2) {
1228
1328
  });
1229
1329
  router.patch("/", async (c) => {
1230
1330
  const body = await c.req.json();
1231
- if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
1331
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length === 0) {
1232
1332
  return refuse(c, 400, "At least one setting field is required");
1233
1333
  }
1234
- const result = pool2.updateSettings(body);
1334
+ const { run: run2, sessionId, ...rest } = body;
1335
+ if (run2 !== void 0) {
1336
+ if (!run2 || typeof run2 !== "object" || Array.isArray(run2)) return refuse(c, 400, "run must be an object of run settings");
1337
+ if (sessionId !== void 0 && typeof sessionId !== "string") return refuse(c, 400, "sessionId must be a string");
1338
+ try {
1339
+ pool2.updateRunSettings(run2, sessionId);
1340
+ } catch (err) {
1341
+ if (err instanceof core.RunSettingsError) return refuse(c, 400, err.message);
1342
+ throw err;
1343
+ }
1344
+ }
1345
+ const result = Object.keys(rest).length > 0 ? pool2.updateSettings(rest) : pool2.getSettings();
1235
1346
  return c.json(result);
1236
1347
  });
1237
1348
  return router;
@@ -1266,6 +1377,8 @@ function createWsHandler(pool2) {
1266
1377
  pool2.unsubscribe(sessionId, ws);
1267
1378
  });
1268
1379
  ws.send(JSON.stringify({ type: "connected", sessionId }));
1380
+ const view = pool2.runSnapshot(sessionId);
1381
+ if (view) ws.send(JSON.stringify({ type: "run_snapshot", sessionId, view }));
1269
1382
  pool2.subscribe(sessionId, ws);
1270
1383
  });
1271
1384
  return (request, socket, head) => {
@@ -1331,9 +1444,9 @@ function withPort(hostname, port2) {
1331
1444
  }
1332
1445
 
1333
1446
  // server/app.ts
1334
- function createApp(pool2, admission2) {
1335
- const app2 = new hono.Hono();
1336
- app2.use("*", async (c, next) => {
1447
+ function createApp(pool2, admission) {
1448
+ const app = new hono.Hono();
1449
+ app.use("*", async (c, next) => {
1337
1450
  const decision = decideAdmission({
1338
1451
  origin: c.req.header("origin") ?? null,
1339
1452
  // The request URL's authority is built from the Host header upstream, so
@@ -1341,21 +1454,21 @@ function createApp(pool2, admission2) {
1341
1454
  // absent entirely, which fails the check anyway.
1342
1455
  host: c.req.header("host") ?? new URL(c.req.url).host,
1343
1456
  authorization: c.req.header("authorization") ?? null
1344
- }, admission2);
1457
+ }, admission);
1345
1458
  if (!decision.admit) return c.json({ error: decision.reason }, decision.status);
1346
1459
  await next();
1347
1460
  });
1348
- app2.route("/api/sessions", sessionsRoute(pool2));
1349
- app2.route("/api/plans", plansRoute(pool2));
1350
- app2.route("/api/runners", runnersRoute(pool2));
1351
- app2.route("/api/models", modelsRoute(pool2));
1352
- app2.route("/api/settings", settingsRoute(pool2));
1353
- app2.route("/api/commands", commandsRoute());
1354
- app2.route("/api/workspaces", workspacesRoute());
1355
- app2.route("/api/approvals", approvalsRoute(pool2));
1356
- return app2;
1461
+ app.route("/api/sessions", sessionsRoute(pool2));
1462
+ app.route("/api/plans", plansRoute(pool2));
1463
+ app.route("/api/runners", runnersRoute(pool2));
1464
+ app.route("/api/models", modelsRoute(pool2));
1465
+ app.route("/api/settings", settingsRoute(pool2));
1466
+ app.route("/api/commands", commandsRoute());
1467
+ app.route("/api/workspaces", workspacesRoute());
1468
+ app.route("/api/approvals", approvalsRoute(pool2));
1469
+ return app;
1357
1470
  }
1358
- function attachWsHandler(server2, pool2, admission2) {
1471
+ function attachWsHandler(server2, pool2, admission) {
1359
1472
  const handler = createWsHandler(pool2);
1360
1473
  server2.on("upgrade", (request, socket, head) => {
1361
1474
  const decision = decideAdmission({
@@ -1363,7 +1476,7 @@ function attachWsHandler(server2, pool2, admission2) {
1363
1476
  host: request.headers.host ?? null,
1364
1477
  authorization: request.headers.authorization ?? null,
1365
1478
  secWebSocketProtocol: request.headers["sec-websocket-protocol"] ?? null
1366
- }, admission2);
1479
+ }, admission);
1367
1480
  if (!decision.admit) {
1368
1481
  refuseUpgrade(socket, decision);
1369
1482
  return;
@@ -1391,7 +1504,7 @@ ${body}`
1391
1504
  }
1392
1505
 
1393
1506
  // server/nodeAdapter.ts
1394
- function createRequestListener(app2) {
1507
+ function createRequestListener(app) {
1395
1508
  return async (req, res) => {
1396
1509
  const bufs = [];
1397
1510
  for await (const chunk of req) bufs.push(chunk);
@@ -1411,7 +1524,7 @@ function createRequestListener(app2) {
1411
1524
  body: req.method !== "GET" && req.method !== "HEAD" ? body : void 0
1412
1525
  });
1413
1526
  try {
1414
- const webRes = await app2.fetch(webReq);
1527
+ const webRes = await app.fetch(webReq);
1415
1528
  res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
1416
1529
  if (webRes.body) {
1417
1530
  const reader = webRes.body.getReader();
@@ -1438,21 +1551,24 @@ if (portIdx !== -1 && args[portIdx + 1]) {
1438
1551
  }
1439
1552
  var watchParentIdx = args.indexOf("--watch-parent");
1440
1553
  var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
1441
- var { token, file: tokenFile } = core.mintDaemonToken(port);
1442
- var admission = { port, token, tokenFile };
1443
1554
  var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
1444
- var app = createApp(pool, admission);
1445
- pool.getProviderModels().catch((err) => {
1446
- console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
1447
- });
1448
- app.get("/", (c) => c.json({ message: "Ordewell API", docs: "/api/workspaces" }));
1449
- var server = http.createServer(createRequestListener(app));
1450
- attachWsHandler(server, pool, admission);
1555
+ var server = http.createServer();
1556
+ var ownsToken = false;
1451
1557
  server.listen(port, "127.0.0.1", () => {
1558
+ const { token, file: tokenFile } = core.mintDaemonToken(port);
1559
+ ownsToken = true;
1560
+ const admission = { port, token, tokenFile };
1561
+ const app = createApp(pool, admission);
1562
+ app.get("/", (c) => c.json({ message: "Ordewell API", docs: "/api/workspaces" }));
1563
+ server.on("request", createRequestListener(app));
1564
+ attachWsHandler(server, pool, admission);
1565
+ pool.getProviderModels().catch((err) => {
1566
+ console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
1567
+ });
1452
1568
  console.error(`[web] Ordewell API server: http://localhost:${port}`);
1453
1569
  });
1454
1570
  function shutdown() {
1455
- core.clearDaemonToken(port);
1571
+ if (ownsToken) core.clearDaemonToken(port);
1456
1572
  pool.destroyAll();
1457
1573
  server.close();
1458
1574
  process.exit(0);