@ryanfw/prompt-orchestration-pipeline 1.2.7 → 1.2.9

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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/config/__tests__/models.test.ts +31 -1
  3. package/src/config/models.ts +81 -35
  4. package/src/config/paths.ts +13 -8
  5. package/src/core/__tests__/config.test.ts +121 -0
  6. package/src/core/__tests__/job-concurrency.test.ts +554 -0
  7. package/src/core/__tests__/orchestrator.test.ts +353 -0
  8. package/src/core/__tests__/pipeline-runner.test.ts +430 -2
  9. package/src/core/__tests__/task-runner.test.ts +1 -2
  10. package/src/core/config.ts +48 -1
  11. package/src/core/job-concurrency.ts +462 -0
  12. package/src/core/orchestrator.ts +370 -57
  13. package/src/core/pipeline-runner.ts +79 -15
  14. package/src/core/status-writer.ts +4 -0
  15. package/src/core/task-runner.ts +1 -1
  16. package/src/providers/__tests__/base.test.ts +1 -1
  17. package/src/ui/client/__tests__/api.test.ts +101 -1
  18. package/src/ui/client/__tests__/job-adapter.test.ts +12 -0
  19. package/src/ui/client/__tests__/useConcurrencyStatus.test.ts +126 -0
  20. package/src/ui/client/adapters/job-adapter.ts +1 -0
  21. package/src/ui/client/api.ts +77 -7
  22. package/src/ui/client/hooks/useConcurrencyStatus.ts +102 -0
  23. package/src/ui/client/types.ts +34 -1
  24. package/src/ui/components/DAGGrid.tsx +11 -1
  25. package/src/ui/components/JobDetail.tsx +2 -1
  26. package/src/ui/components/__tests__/DAGGrid.test.tsx +92 -0
  27. package/src/ui/components/__tests__/JobDetail.test.tsx +62 -0
  28. package/src/ui/components/types.ts +2 -0
  29. package/src/ui/dist/assets/{index-SKy2shWc.js → index-BnAqY4_n.js} +336 -52
  30. package/src/ui/dist/assets/index-BnAqY4_n.js.map +1 -0
  31. package/src/ui/dist/assets/style-BKG0bHu-.css +2 -0
  32. package/src/ui/dist/index.html +2 -2
  33. package/src/ui/embedded-assets.js +6 -6
  34. package/src/ui/pages/PromptPipelineDashboard.tsx +186 -4
  35. package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +272 -1
  36. package/src/ui/server/__tests__/concurrency-endpoint.test.ts +190 -0
  37. package/src/ui/server/__tests__/index.test.ts +92 -3
  38. package/src/ui/server/__tests__/job-control-endpoints.test.ts +660 -3
  39. package/src/ui/server/endpoints/concurrency-endpoint.ts +72 -0
  40. package/src/ui/server/endpoints/job-control-endpoints.ts +248 -37
  41. package/src/ui/server/index.ts +21 -2
  42. package/src/ui/server/router.ts +2 -0
  43. package/src/ui/state/__tests__/watcher.test.ts +31 -0
  44. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +15 -0
  45. package/src/ui/state/transformers/status-transformer.ts +1 -0
  46. package/src/ui/state/types.ts +3 -0
  47. package/src/ui/state/watcher.ts +9 -1
  48. package/src/utils/__tests__/dag.test.ts +35 -0
  49. package/src/utils/dag.ts +1 -0
  50. package/src/ui/dist/assets/index-SKy2shWc.js.map +0 -1
  51. package/src/ui/dist/assets/style-DA1Ma4YS.css +0 -2
@@ -20453,11 +20453,11 @@ const MODEL_CONFIG_RAW = {
20453
20453
  tokenCostOutPerMillion: 4.5
20454
20454
  },
20455
20455
  // Alibaba (Qwen via DashScope, international/Singapore deployment, base tier)
20456
- "alibaba:qwen3-max": {
20456
+ "alibaba:qwen3.6-max-preview": {
20457
20457
  provider: "alibaba",
20458
- model: "qwen3-max",
20459
- tokenCostInPerMillion: 1.2,
20460
- tokenCostOutPerMillion: 6
20458
+ model: "qwen3.6-max-preview",
20459
+ tokenCostInPerMillion: 1.3,
20460
+ tokenCostOutPerMillion: 7.8
20461
20461
  },
20462
20462
  "alibaba:qwen3.6-plus": {
20463
20463
  provider: "alibaba",
@@ -20465,6 +20465,18 @@ const MODEL_CONFIG_RAW = {
20465
20465
  tokenCostInPerMillion: 0.276,
20466
20466
  tokenCostOutPerMillion: 1.651
20467
20467
  },
20468
+ "alibaba:qwen3.6-flash": {
20469
+ provider: "alibaba",
20470
+ model: "qwen3.6-flash",
20471
+ tokenCostInPerMillion: 0.025,
20472
+ tokenCostOutPerMillion: 1.5
20473
+ },
20474
+ "alibaba:qwen3-max": {
20475
+ provider: "alibaba",
20476
+ model: "qwen3-max",
20477
+ tokenCostInPerMillion: 1.2,
20478
+ tokenCostOutPerMillion: 6
20479
+ },
20468
20480
  "alibaba:qwen3.5-plus": {
20469
20481
  provider: "alibaba",
20470
20482
  model: "qwen3.5-plus",
@@ -20514,13 +20526,18 @@ const VALID_MODEL_ALIASES = new Set(
20514
20526
  );
20515
20527
  function aliasToFunctionName(alias) {
20516
20528
  if (typeof alias !== "string") {
20517
- throw new Error(`Invalid model alias: expected string, got ${typeof alias}`);
20529
+ throw new Error(
20530
+ `Invalid model alias: expected string, got ${typeof alias}`
20531
+ );
20518
20532
  }
20519
20533
  if (!alias.includes(":")) {
20520
20534
  throw new Error(`Invalid model alias: "${alias}" does not contain a colon`);
20521
20535
  }
20522
20536
  const model = alias.split(":").slice(1).join(":");
20523
- return model.replace(/[-.]([a-z0-9])/gi, (_, char) => char.toUpperCase());
20537
+ return model.replace(
20538
+ /[-.]([a-z0-9])/gi,
20539
+ (_, char) => char.toUpperCase()
20540
+ );
20524
20541
  }
20525
20542
  Object.freeze(
20526
20543
  Object.fromEntries(
@@ -20542,7 +20559,13 @@ function buildProviderFunctionsIndex() {
20542
20559
  index2[provider] = [];
20543
20560
  }
20544
20561
  index2[provider].push(
20545
- Object.freeze({ alias, provider, model, functionName, fullPath })
20562
+ Object.freeze({
20563
+ alias,
20564
+ provider,
20565
+ model,
20566
+ functionName,
20567
+ fullPath
20568
+ })
20546
20569
  );
20547
20570
  }
20548
20571
  for (const provider of Object.keys(index2)) {
@@ -21634,15 +21657,16 @@ function Code() {
21634
21657
  }
21635
21658
  );
21636
21659
  }
21637
- function isRecord$4(value) {
21660
+ const CONCURRENCY_LIMIT_MESSAGE = "Capacity reached. Wait for a job to finish, then try again.";
21661
+ function isRecord$5(value) {
21638
21662
  return typeof value === "object" && value !== null;
21639
21663
  }
21640
21664
  function normalizeBackendErrorCode(errorData, status) {
21641
- if (!isRecord$4(errorData) || typeof errorData["code"] !== "string") {
21665
+ if (!isRecord$5(errorData) || typeof errorData["code"] !== "string") {
21642
21666
  return getErrorCodeFromStatus(status);
21643
21667
  }
21644
21668
  const code2 = errorData["code"];
21645
- if (code2 === "job_running" || code2 === "job_not_found" || code2 === "conflict" || code2 === "spawn_failed" || code2 === "unknown_error" || code2 === "network_error" || code2 === "dependencies_not_satisfied" || code2 === "unsupported_lifecycle" || code2 === "task_not_found" || code2 === "task_not_pending") {
21669
+ if (code2 === "job_running" || code2 === "job_not_found" || code2 === "conflict" || code2 === "spawn_failed" || code2 === "unknown_error" || code2 === "network_error" || code2 === "malformed_response" || code2 === "dependencies_not_satisfied" || code2 === "unsupported_lifecycle" || code2 === "task_not_found" || code2 === "task_not_pending" || code2 === "concurrency_limit_reached") {
21646
21670
  return code2;
21647
21671
  }
21648
21672
  if (code2 === "JOB_NOT_FOUND" || code2 === "NOT_FOUND") return "job_not_found";
@@ -21656,9 +21680,12 @@ function normalizeBackendErrorCode(errorData, status) {
21656
21680
  return getErrorCodeFromStatus(status);
21657
21681
  }
21658
21682
  function getMessage(value) {
21659
- if (!isRecord$4(value)) return null;
21683
+ if (!isRecord$5(value)) return null;
21660
21684
  return typeof value["message"] === "string" ? value["message"] : null;
21661
21685
  }
21686
+ function hasBackendCode(errorData, code2) {
21687
+ return isRecord$5(errorData) && errorData["code"] === code2;
21688
+ }
21662
21689
  function getErrorCodeFromStatus(status) {
21663
21690
  if (status === 404) return "job_not_found";
21664
21691
  if (status === 409) return "conflict";
@@ -21678,31 +21705,37 @@ function getErrorMessageFromStatus(status) {
21678
21705
  return "Request failed";
21679
21706
  }
21680
21707
  function getRestartErrorMessage(errorData, status) {
21681
- if (isRecord$4(errorData) && errorData["code"] === "job_running") {
21708
+ if (hasBackendCode(errorData, "job_running")) {
21682
21709
  return "Cannot restart a job while it is still running";
21683
21710
  }
21684
- if (isRecord$4(errorData) && errorData["code"] === "spawn_failed") {
21711
+ if (hasBackendCode(errorData, "spawn_failed")) {
21685
21712
  return "Failed to spawn the restarted job";
21686
21713
  }
21714
+ if (hasBackendCode(errorData, "concurrency_limit_reached")) {
21715
+ return CONCURRENCY_LIMIT_MESSAGE;
21716
+ }
21687
21717
  return getMessage(errorData) ?? getErrorMessageFromStatus(status);
21688
21718
  }
21689
21719
  function getStartTaskErrorMessage(errorData, status) {
21690
- if (isRecord$4(errorData) && errorData["code"] === "dependencies_not_satisfied") {
21720
+ if (hasBackendCode(errorData, "dependencies_not_satisfied")) {
21691
21721
  return "Cannot start task before its dependencies are complete";
21692
21722
  }
21693
- if (isRecord$4(errorData) && errorData["code"] === "task_not_found") {
21723
+ if (hasBackendCode(errorData, "task_not_found")) {
21694
21724
  return "Task not found";
21695
21725
  }
21696
- if (isRecord$4(errorData) && errorData["code"] === "task_not_pending") {
21726
+ if (hasBackendCode(errorData, "task_not_pending")) {
21697
21727
  return "Only pending tasks can be started";
21698
21728
  }
21729
+ if (hasBackendCode(errorData, "concurrency_limit_reached")) {
21730
+ return CONCURRENCY_LIMIT_MESSAGE;
21731
+ }
21699
21732
  return getMessage(errorData) ?? getErrorMessageFromStatus(status);
21700
21733
  }
21701
21734
  function getStopErrorMessage(errorData, status) {
21702
- if (isRecord$4(errorData) && errorData["code"] === "job_not_found") {
21735
+ if (isRecord$5(errorData) && errorData["code"] === "job_not_found") {
21703
21736
  return "Job not found";
21704
21737
  }
21705
- if (isRecord$4(errorData) && errorData["code"] === "unsupported_lifecycle") {
21738
+ if (isRecord$5(errorData) && errorData["code"] === "unsupported_lifecycle") {
21706
21739
  return "This job cannot be stopped";
21707
21740
  }
21708
21741
  return getMessage(errorData) ?? getErrorMessageFromStatus(status);
@@ -21710,11 +21743,12 @@ function getStopErrorMessage(errorData, status) {
21710
21743
  async function parseJson(response) {
21711
21744
  try {
21712
21745
  return await response.json();
21713
- } catch {
21746
+ } catch (error) {
21747
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
21714
21748
  return null;
21715
21749
  }
21716
21750
  }
21717
- function toApiError$1(status, message, errorData) {
21751
+ function toApiError$2(status, message, errorData) {
21718
21752
  return {
21719
21753
  code: normalizeBackendErrorCode(errorData, status),
21720
21754
  message,
@@ -21737,7 +21771,7 @@ async function postJson(url, body, getFailureMessage) {
21737
21771
  }
21738
21772
  const payload = await parseJson(response);
21739
21773
  if (response.ok) {
21740
- if (isRecord$4(payload) && payload["ok"] === true) {
21774
+ if (isRecord$5(payload) && payload["ok"] === true) {
21741
21775
  return {
21742
21776
  ok: true,
21743
21777
  message: typeof payload["message"] === "string" ? payload["message"] : void 0
@@ -21745,7 +21779,7 @@ async function postJson(url, body, getFailureMessage) {
21745
21779
  }
21746
21780
  return { ok: true };
21747
21781
  }
21748
- throw toApiError$1(response.status, getFailureMessage(payload, response.status), payload);
21782
+ throw toApiError$2(response.status, getFailureMessage(payload, response.status), payload);
21749
21783
  }
21750
21784
  async function restartJob(jobId, opts = {}) {
21751
21785
  const clearTokenUsage = opts.options?.clearTokenUsage ?? true;
@@ -21766,6 +21800,40 @@ async function startTask(jobId, taskId) {
21766
21800
  async function stopJob(jobId) {
21767
21801
  return postJson(`/api/jobs/${jobId}/stop`, {}, getStopErrorMessage);
21768
21802
  }
21803
+ async function fetchConcurrencyStatus(signal) {
21804
+ let response;
21805
+ let payload;
21806
+ try {
21807
+ response = await fetch("/api/concurrency", { signal });
21808
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
21809
+ payload = await parseJson(response);
21810
+ } catch (error) {
21811
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
21812
+ throw {
21813
+ code: "network_error",
21814
+ message: error instanceof Error ? error.message : "Network request failed"
21815
+ };
21816
+ }
21817
+ if (!response.ok) {
21818
+ throw toApiError$2(
21819
+ response.status,
21820
+ getMessage(payload) ?? getErrorMessageFromStatus(response.status),
21821
+ payload
21822
+ );
21823
+ }
21824
+ if (isRecord$5(payload) && payload["ok"] === true && isJobConcurrencyApiStatus(payload["data"])) {
21825
+ return payload["data"];
21826
+ }
21827
+ throw {
21828
+ code: "malformed_response",
21829
+ message: "Malformed concurrency status response",
21830
+ status: response.status
21831
+ };
21832
+ }
21833
+ function isJobConcurrencyApiStatus(value) {
21834
+ if (!isRecord$5(value)) return false;
21835
+ return typeof value["limit"] === "number" && typeof value["runningCount"] === "number" && typeof value["availableSlots"] === "number" && typeof value["queuedCount"] === "number" && Array.isArray(value["activeJobs"]) && Array.isArray(value["queuedJobs"]) && Array.isArray(value["staleSlots"]);
21836
+ }
21769
21837
  const TaskState = Object.freeze({
21770
21838
  PENDING: "pending",
21771
21839
  RUNNING: "running",
@@ -21849,7 +21917,7 @@ const EMPTY_COSTS = {
21849
21917
  totalInputCost: 0,
21850
21918
  totalOutputCost: 0
21851
21919
  };
21852
- function isRecord$3(value) {
21920
+ function isRecord$4(value) {
21853
21921
  return typeof value === "object" && value !== null;
21854
21922
  }
21855
21923
  function toStringOrNull(value) {
@@ -21859,7 +21927,7 @@ function toNumber(value, fallback = 0) {
21859
21927
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
21860
21928
  }
21861
21929
  function normalizeFiles(files) {
21862
- if (!isRecord$3(files)) return EMPTY_FILES$1;
21930
+ if (!isRecord$4(files)) return EMPTY_FILES$1;
21863
21931
  return {
21864
21932
  artifacts: Array.isArray(files["artifacts"]) ? files["artifacts"].filter((x) => typeof x === "string") : [],
21865
21933
  logs: Array.isArray(files["logs"]) ? files["logs"].filter((x) => typeof x === "string") : [],
@@ -21867,25 +21935,26 @@ function normalizeFiles(files) {
21867
21935
  };
21868
21936
  }
21869
21937
  function normalizeTask(name2, rawTask) {
21870
- const task = isRecord$3(rawTask) ? rawTask : {};
21938
+ const task = isRecord$4(rawTask) ? rawTask : {};
21871
21939
  return {
21872
21940
  name: name2,
21873
21941
  state: normalizeTaskState(task["state"]),
21874
21942
  startedAt: toStringOrNull(task["startedAt"]),
21875
21943
  endedAt: toStringOrNull(task["endedAt"]),
21876
21944
  attempts: typeof task["attempts"] === "number" ? task["attempts"] : void 0,
21945
+ restartCount: typeof task["restartCount"] === "number" ? task["restartCount"] : void 0,
21877
21946
  executionTimeMs: typeof task["executionTimeMs"] === "number" ? task["executionTimeMs"] : void 0,
21878
21947
  currentStage: typeof task["currentStage"] === "string" ? task["currentStage"] : void 0,
21879
21948
  failedStage: typeof task["failedStage"] === "string" ? task["failedStage"] : void 0,
21880
21949
  files: normalizeFiles(task["files"]),
21881
21950
  artifacts: Array.isArray(task["artifacts"]) ? task["artifacts"].filter((x) => typeof x === "string") : void 0,
21882
- tokenUsage: isRecord$3(task["tokenUsage"]) ? task["tokenUsage"] : void 0,
21883
- error: isRecord$3(task["error"]) ? task["error"] : void 0
21951
+ tokenUsage: isRecord$4(task["tokenUsage"]) ? task["tokenUsage"] : void 0,
21952
+ error: isRecord$4(task["error"]) ? task["error"] : void 0
21884
21953
  };
21885
21954
  }
21886
21955
  function normalizeCurrent(current) {
21887
21956
  if (typeof current === "string") return { taskName: current };
21888
- if (!isRecord$3(current) || typeof current["taskName"] !== "string") return null;
21957
+ if (!isRecord$4(current) || typeof current["taskName"] !== "string") return null;
21889
21958
  return {
21890
21959
  taskName: current["taskName"],
21891
21960
  stage: typeof current["stage"] === "string" ? current["stage"] : void 0
@@ -21898,11 +21967,11 @@ function getDisplayCategory(status) {
21898
21967
  }
21899
21968
  function getWarnings(rawTasks) {
21900
21969
  if (rawTasks == null) return [];
21901
- if (Array.isArray(rawTasks) || isRecord$3(rawTasks)) return [];
21970
+ if (Array.isArray(rawTasks) || isRecord$4(rawTasks)) return [];
21902
21971
  return ["Unsupported task collection shape"];
21903
21972
  }
21904
21973
  function normalizeCostsSummary(costs) {
21905
- if (!isRecord$3(costs)) return EMPTY_COSTS;
21974
+ if (!isRecord$4(costs)) return EMPTY_COSTS;
21906
21975
  return {
21907
21976
  totalTokens: toNumber(costs["totalTokens"]),
21908
21977
  totalInputTokens: toNumber(costs["totalInputTokens"]),
@@ -21913,9 +21982,9 @@ function normalizeCostsSummary(costs) {
21913
21982
  };
21914
21983
  }
21915
21984
  function normalizeCostBreakdown(costs) {
21916
- if (!isRecord$3(costs)) return void 0;
21985
+ if (!isRecord$4(costs)) return void 0;
21917
21986
  const entries = Object.entries(costs).flatMap(([name2, value]) => {
21918
- if (!isRecord$3(value)) return [];
21987
+ if (!isRecord$4(value)) return [];
21919
21988
  return [[name2, {
21920
21989
  inputTokens: toNumber(value["inputTokens"]),
21921
21990
  outputTokens: toNumber(value["outputTokens"]),
@@ -21930,12 +21999,12 @@ function normalizeTasks$1(rawTasks) {
21930
21999
  if (rawTasks == null) return {};
21931
22000
  if (Array.isArray(rawTasks)) {
21932
22001
  return Object.fromEntries(rawTasks.map((task, index2) => {
21933
- const record = isRecord$3(task) ? task : {};
22002
+ const record = isRecord$4(task) ? task : {};
21934
22003
  const name2 = typeof record["name"] === "string" ? record["name"] : `task-${index2}`;
21935
22004
  return [name2, normalizeTask(name2, record)];
21936
22005
  }));
21937
22006
  }
21938
- if (isRecord$3(rawTasks)) {
22007
+ if (isRecord$4(rawTasks)) {
21939
22008
  return Object.fromEntries(Object.entries(rawTasks).map(([name2, task]) => {
21940
22009
  return [name2, normalizeTask(name2, task)];
21941
22010
  }));
@@ -21947,7 +22016,7 @@ function adaptBaseJob(apiJob) {
21947
22016
  const tasks = normalizeTasks$1(rawTasks);
21948
22017
  const taskList = Object.values(tasks);
21949
22018
  const doneCount = taskList.filter((task) => task.state === "done").length;
21950
- const rawPipelineConfig = isRecord$3(apiJob["pipelineConfig"]) ? apiJob["pipelineConfig"] : null;
22019
+ const rawPipelineConfig = isRecord$4(apiJob["pipelineConfig"]) ? apiJob["pipelineConfig"] : null;
21951
22020
  const pipelineTaskArray = rawPipelineConfig && Array.isArray(rawPipelineConfig["tasks"]) ? rawPipelineConfig["tasks"] : null;
21952
22021
  const taskCount = pipelineTaskArray ? pipelineTaskArray.length : taskList.length;
21953
22022
  const inferredStatus = deriveJobStatusFromTasks(taskList);
@@ -22002,12 +22071,12 @@ function deriveAllowedActions(adaptedJob, pipelineTasks) {
22002
22071
  }
22003
22072
  const REFRESH_DEBOUNCE_MS = 200;
22004
22073
  const POLL_INTERVAL_MS = 3e3;
22005
- function isRecord$2(value) {
22074
+ function isRecord$3(value) {
22006
22075
  return typeof value === "object" && value !== null;
22007
22076
  }
22008
22077
  function extractJobDetail(payload) {
22009
- if (isRecord$2(payload) && isRecord$2(payload["data"])) return payload["data"];
22010
- if (isRecord$2(payload)) return payload;
22078
+ if (isRecord$3(payload) && isRecord$3(payload["data"])) return payload["data"];
22079
+ if (isRecord$3(payload)) return payload;
22011
22080
  return null;
22012
22081
  }
22013
22082
  function getPipelineTaskCount(detail) {
@@ -22047,7 +22116,7 @@ function applyDetailEvent(detail, event) {
22047
22116
  if (!currentTask) return detail;
22048
22117
  const nextTask = {
22049
22118
  ...currentTask,
22050
- ...isRecord$2(event.data["task"]) ? adaptJobDetail({
22119
+ ...isRecord$3(event.data["task"]) ? adaptJobDetail({
22051
22120
  ...detail,
22052
22121
  tasks: {
22053
22122
  [taskName]: event.data["task"]
@@ -22283,7 +22352,8 @@ function computeDagItems(job, pipeline) {
22283
22352
  subtitle: null,
22284
22353
  body: task?.error?.message ?? null,
22285
22354
  startedAt: task?.startedAt ?? 0,
22286
- endedAt: task?.endedAt ?? null
22355
+ endedAt: task?.endedAt ?? null,
22356
+ restartCount: task?.restartCount ?? 0
22287
22357
  };
22288
22358
  });
22289
22359
  }
@@ -51031,7 +51101,20 @@ function DAGGrid({
51031
51101
  "data-role": "card-header",
51032
51102
  className: `flex items-center justify-between gap-3 rounded-t-lg border-b px-4 py-2 ${reducedMotion ? "" : "transition-opacity duration-300 ease-in-out"} ${getHeaderClasses(getItemStatus(items[index2], index2, activeIndex))}`,
51033
51103
  children: [
51034
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "truncate font-medium", children: items[index2]?.title ?? formatStepName(items[index2]?.id ?? "") }),
51104
+ (items[index2]?.restartCount ?? 0) > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(
51105
+ "span",
51106
+ {
51107
+ "data-role": "restart-badge",
51108
+ className: "inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-sm border border-red-300 bg-red-50 text-red-700 text-[11px] font-medium tabular-nums",
51109
+ title: `Restarted ${items[index2].restartCount} time${items[index2].restartCount === 1 ? "" : "s"}`,
51110
+ "aria-label": `Restarted ${items[index2].restartCount} time${items[index2].restartCount === 1 ? "" : "s"}`,
51111
+ children: [
51112
+ "↻ ",
51113
+ items[index2].restartCount
51114
+ ]
51115
+ }
51116
+ ) : null,
51117
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "min-w-0 flex-1 truncate text-left font-medium", children: items[index2]?.title ?? formatStepName(items[index2]?.id ?? "") }),
51035
51118
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2", children: getItemStatus(items[index2], index2, activeIndex) === "running" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
51036
51119
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-4 w-4", "aria-label": "Active", children: [
51037
51120
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "sr-only", children: "Active" }),
@@ -51185,7 +51268,7 @@ function JobDetail({
51185
51268
  const previous2 = prevDagItemsRef.current;
51186
51269
  const reused = enriched.map((item, index2) => {
51187
51270
  const prior = previous2[index2];
51188
- if (prior && prior.id === item.id && prior.status === item.status && prior.stage === item.stage && prior.title === item.title && prior.subtitle === item.subtitle && prior.body === item.body) {
51271
+ if (prior && prior.id === item.id && prior.status === item.status && prior.stage === item.stage && prior.title === item.title && prior.subtitle === item.subtitle && prior.body === item.body && prior.restartCount === item.restartCount) {
51189
51272
  return prior;
51190
51273
  }
51191
51274
  return item;
@@ -77633,11 +77716,11 @@ function createInitialAnalysisState() {
77633
77716
  error: null
77634
77717
  };
77635
77718
  }
77636
- function isRecord$1(value) {
77719
+ function isRecord$2(value) {
77637
77720
  return typeof value === "object" && value !== null;
77638
77721
  }
77639
77722
  function reduceAnalysisEvent(state, eventType, payload) {
77640
- const data = isRecord$1(payload) ? payload : {};
77723
+ const data = isRecord$2(payload) ? payload : {};
77641
77724
  if (eventType === "started") {
77642
77725
  return {
77643
77726
  ...state,
@@ -77778,6 +77861,86 @@ function PipelineTypeDetail() {
77778
77861
  }
77779
77862
  );
77780
77863
  }
77864
+ const REFETCH_EVENTS$1 = ["state:summary", "state:change"];
77865
+ function isRecord$1(value) {
77866
+ return typeof value === "object" && value !== null;
77867
+ }
77868
+ function toApiError$1(error) {
77869
+ if (isRecord$1(error) && typeof error["code"] === "string" && typeof error["message"] === "string") {
77870
+ return {
77871
+ code: error["code"],
77872
+ message: error["message"],
77873
+ status: typeof error["status"] === "number" ? error["status"] : void 0
77874
+ };
77875
+ }
77876
+ return {
77877
+ code: "unknown_error",
77878
+ message: error instanceof Error ? error.message : "Failed to load concurrency status"
77879
+ };
77880
+ }
77881
+ function useConcurrencyStatus() {
77882
+ const [loading, setLoading] = reactExports.useState(true);
77883
+ const [data, setData] = reactExports.useState(null);
77884
+ const [error, setError] = reactExports.useState(null);
77885
+ const abortRef = reactExports.useRef(null);
77886
+ const dataRef = reactExports.useRef(null);
77887
+ const mountedRef = reactExports.useRef(true);
77888
+ const refetchTimerRef = reactExports.useRef(null);
77889
+ const load = reactExports.useCallback((showLoading = false) => {
77890
+ abortRef.current?.abort();
77891
+ const controller = new AbortController();
77892
+ abortRef.current = controller;
77893
+ if (showLoading || dataRef.current === null) setLoading(true);
77894
+ void fetchConcurrencyStatus(controller.signal).then((status) => {
77895
+ if (!mountedRef.current || abortRef.current !== controller) return;
77896
+ dataRef.current = status;
77897
+ setData(status);
77898
+ setError(null);
77899
+ }).catch((fetchError) => {
77900
+ if (fetchError instanceof DOMException && fetchError.name === "AbortError") return;
77901
+ if (!mountedRef.current || abortRef.current !== controller) return;
77902
+ setError(toApiError$1(fetchError));
77903
+ }).finally(() => {
77904
+ if (!mountedRef.current || abortRef.current !== controller) return;
77905
+ setLoading(false);
77906
+ });
77907
+ }, []);
77908
+ reactExports.useEffect(() => {
77909
+ mountedRef.current = true;
77910
+ load(true);
77911
+ const source2 = new EventSource("/api/events");
77912
+ const onMessage = () => {
77913
+ if (refetchTimerRef.current) return;
77914
+ refetchTimerRef.current = setTimeout(() => {
77915
+ refetchTimerRef.current = null;
77916
+ load(false);
77917
+ }, 0);
77918
+ };
77919
+ for (const eventName of REFETCH_EVENTS$1) {
77920
+ source2.addEventListener(eventName, onMessage);
77921
+ }
77922
+ source2.onerror = () => {
77923
+ if (!mountedRef.current) return;
77924
+ setError({
77925
+ code: "network_error",
77926
+ message: "Live concurrency updates disconnected"
77927
+ });
77928
+ };
77929
+ return () => {
77930
+ mountedRef.current = false;
77931
+ if (refetchTimerRef.current) {
77932
+ clearTimeout(refetchTimerRef.current);
77933
+ refetchTimerRef.current = null;
77934
+ }
77935
+ abortRef.current?.abort();
77936
+ source2.close();
77937
+ };
77938
+ }, [load]);
77939
+ const refetch = reactExports.useCallback(() => {
77940
+ load(true);
77941
+ }, [load]);
77942
+ return { loading, data, error, refetch };
77943
+ }
77781
77944
  function isRecord(value) {
77782
77945
  return typeof value === "object" && value !== null;
77783
77946
  }
@@ -78081,6 +78244,119 @@ function JobTable({
78081
78244
  }) })
78082
78245
  ] }) });
78083
78246
  }
78247
+ const STALE_REASON_LABELS = {
78248
+ missing_current_job: "Missing current job directory",
78249
+ missing_pid: "Lease missing PID past timeout",
78250
+ dead_pid: "Process no longer running",
78251
+ invalid_json: "Lease file is not valid JSON"
78252
+ };
78253
+ function formatTimestamp(value) {
78254
+ if (!value) return "—";
78255
+ const ms = Date.parse(value);
78256
+ if (Number.isNaN(ms)) return value;
78257
+ return new Date(ms).toLocaleString();
78258
+ }
78259
+ function CapacityMetrics({ status }) {
78260
+ const cells = [
78261
+ { label: "Limit", value: status.limit },
78262
+ { label: "Running", value: status.runningCount },
78263
+ { label: "Available", value: status.availableSlots },
78264
+ { label: "Queued", value: status.queuedCount }
78265
+ ];
78266
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("dl", { className: "mb-6 grid grid-cols-2 gap-px overflow-hidden rounded-md border border-gray-200 bg-gray-200 sm:grid-cols-4", children: cells.map((cell2) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white px-4 py-3", children: [
78267
+ /* @__PURE__ */ jsxRuntimeExports.jsx("dt", { className: "text-xs text-gray-500", children: cell2.label }),
78268
+ /* @__PURE__ */ jsxRuntimeExports.jsx("dd", { className: "text-2xl tabular-nums text-gray-900", children: cell2.value })
78269
+ ] }, cell2.label)) });
78270
+ }
78271
+ function ActiveJobsTable({ jobs }) {
78272
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "mb-6", children: [
78273
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "mb-2 text-sm font-medium text-gray-700", children: [
78274
+ "Active jobs (",
78275
+ jobs.length,
78276
+ ")"
78277
+ ] }),
78278
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-hidden rounded-md border border-gray-200 bg-white", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "min-w-full divide-y divide-gray-200", children: [
78279
+ /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 text-left text-sm font-medium text-gray-500", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
78280
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Job ID" }),
78281
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Source" }),
78282
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2 text-right", children: "PID" }),
78283
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Acquired" })
78284
+ ] }) }),
78285
+ /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 text-sm text-gray-700", children: jobs.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("td", { colSpan: 4, className: "px-4 py-3 text-gray-500", children: "No active jobs." }) }) : jobs.map((job) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
78286
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 font-mono text-xs text-gray-900", children: job.jobId }),
78287
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2", children: job.source }),
78288
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 text-right tabular-nums", children: job.pid ?? "—" }),
78289
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 tabular-nums text-gray-600", children: formatTimestamp(job.acquiredAt) })
78290
+ ] }, job.jobId)) })
78291
+ ] }) })
78292
+ ] });
78293
+ }
78294
+ function QueuedJobsTable({ jobs }) {
78295
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "mb-6", children: [
78296
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "mb-2 text-sm font-medium text-gray-700", children: [
78297
+ "Queued jobs (",
78298
+ jobs.length,
78299
+ ")"
78300
+ ] }),
78301
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-hidden rounded-md border border-gray-200 bg-white", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "min-w-full divide-y divide-gray-200", children: [
78302
+ /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-gray-50 text-left text-sm font-medium text-gray-500", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
78303
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Job ID" }),
78304
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Name" }),
78305
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Pipeline" }),
78306
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Queued" })
78307
+ ] }) }),
78308
+ /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-gray-200 text-sm text-gray-700", children: jobs.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("td", { colSpan: 4, className: "px-4 py-3 text-gray-500", children: "No queued jobs." }) }) : jobs.map((job) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
78309
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 font-mono text-xs text-gray-900", children: job.jobId }),
78310
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2", children: job.name ?? "—" }),
78311
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2", children: job.pipeline ?? "—" }),
78312
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 tabular-nums text-gray-600", children: formatTimestamp(job.queuedAt) })
78313
+ ] }, job.jobId)) })
78314
+ ] }) })
78315
+ ] });
78316
+ }
78317
+ function StaleSlotWarnings({ slots }) {
78318
+ if (slots.length === 0) return null;
78319
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "mb-6 rounded-sm border-l-[3px] border-l-yellow-600 bg-yellow-50", children: [
78320
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "px-4 pt-3 text-sm font-medium text-yellow-800", children: [
78321
+ "Stale slots (",
78322
+ slots.length,
78323
+ ")"
78324
+ ] }),
78325
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "min-w-full text-sm text-yellow-900", children: [
78326
+ /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "text-left text-xs font-medium text-yellow-700", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
78327
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Job ID" }),
78328
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { scope: "col", className: "px-4 py-2", children: "Reason" })
78329
+ ] }) }),
78330
+ /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: slots.map((slot) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
78331
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2 font-mono text-xs", children: slot.jobId }),
78332
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-4 py-2", children: STALE_REASON_LABELS[slot.reason] })
78333
+ ] }, slot.jobId)) })
78334
+ ] })
78335
+ ] });
78336
+ }
78337
+ function ConcurrencyPanel() {
78338
+ const { data, error, loading } = useConcurrencyStatus();
78339
+ if (error && !data) {
78340
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-sm border-l-[3px] border-l-yellow-600 bg-yellow-100 p-3 text-sm text-yellow-700", children: [
78341
+ "Unable to load concurrency status: ",
78342
+ error.message
78343
+ ] });
78344
+ }
78345
+ if (loading && !data) {
78346
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-gray-500", children: "Loading concurrency status…" });
78347
+ }
78348
+ if (!data) return null;
78349
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
78350
+ error ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 rounded-sm border-l-[3px] border-l-yellow-600 bg-yellow-50 p-3 text-sm text-yellow-700", children: [
78351
+ "Showing last known concurrency status: ",
78352
+ error.message
78353
+ ] }) : null,
78354
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CapacityMetrics, { status: data }),
78355
+ /* @__PURE__ */ jsxRuntimeExports.jsx(StaleSlotWarnings, { slots: data.staleSlots }),
78356
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ActiveJobsTable, { jobs: data.activeJobs }),
78357
+ /* @__PURE__ */ jsxRuntimeExports.jsx(QueuedJobsTable, { jobs: data.queuedJobs })
78358
+ ] });
78359
+ }
78084
78360
  function PromptPipelineDashboard() {
78085
78361
  const navigate = useNavigate();
78086
78362
  const hookResult = useJobListWithUpdates();
@@ -78101,6 +78377,19 @@ function PromptPipelineDashboard() {
78101
78377
  );
78102
78378
  const runningJobs = grouped.current;
78103
78379
  const aggregateProgress = runningJobs.length === 0 ? 0 : Math.round(runningJobs.reduce((sum, job) => sum + job.progress, 0) / runningJobs.length);
78380
+ const tabLabels = {
78381
+ current: "Current",
78382
+ errors: "Errors",
78383
+ complete: "Complete",
78384
+ concurrency: "Concurrency"
78385
+ };
78386
+ const tabCounts = {
78387
+ current: grouped.current.length,
78388
+ errors: grouped.errors.length,
78389
+ complete: grouped.complete.length,
78390
+ concurrency: null
78391
+ };
78392
+ const tabText = (tab2) => `${tabLabels[tab2]}${tabCounts[tab2] === null ? "" : ` (${tabCounts[tab2]})`}`;
78104
78393
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
78105
78394
  Layout,
78106
78395
  {
@@ -78119,22 +78408,17 @@ function PromptPipelineDashboard() {
78119
78408
  children: [
78120
78409
  error ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-4 rounded-sm border-l-[3px] border-l-yellow-600 bg-yellow-100 p-3 text-sm text-yellow-700", children: "Unable to load jobs from the server" }) : null,
78121
78410
  /* @__PURE__ */ jsxRuntimeExports.jsx(HintBanner, { storageKey: "dashboard-hint", title: "Upload a seed file to get started.", variant: "action", children: "Your first pipeline takes about 2 minutes to complete." }),
78122
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-4 flex flex-wrap gap-2", children: ["current", "errors", "complete"].map((tab2) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
78411
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-4 flex flex-wrap gap-2", children: ["current", "errors", "complete", "concurrency"].map((tab2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
78123
78412
  "button",
78124
78413
  {
78125
78414
  type: "button",
78126
78415
  className: `px-4 py-2 text-sm ${activeTab === tab2 ? "text-[#6d28d9] font-medium border-b-2 border-[#6d28d9]" : "text-gray-500 hover:text-gray-900"}`,
78127
78416
  onClick: () => setActiveTab(tab2),
78128
- children: [
78129
- tab2 === "complete" ? "Complete" : `${tab2.charAt(0).toUpperCase()}${tab2.slice(1)}`,
78130
- " (",
78131
- grouped[tab2].length,
78132
- ")"
78133
- ]
78417
+ children: tabText(tab2)
78134
78418
  },
78135
78419
  tab2
78136
78420
  )) }),
78137
- /* @__PURE__ */ jsxRuntimeExports.jsx(JobTable, { jobs: grouped[activeTab], pipeline: null, onOpenJob: (jobId) => navigate(`/pipeline/${jobId}`) })
78421
+ activeTab === "concurrency" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConcurrencyPanel, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(JobTable, { jobs: grouped[activeTab], pipeline: null, onOpenJob: (jobId) => navigate(`/pipeline/${jobId}`) })
78138
78422
  ]
78139
78423
  }
78140
78424
  );