@zq-silk/yui 0.15.11 → 0.15.12

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 (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -20,6 +20,10 @@ const elements = {
20
20
  search: document.querySelector("#search"),
21
21
  filters: document.querySelector("#status-filters"),
22
22
  tasks: document.querySelector("#task-list"),
23
+ catalogNext: document.querySelector("#catalog-next"),
24
+ catalogReset: document.querySelector("#catalog-reset"),
25
+ catalogCount: document.querySelector("#catalog-count"),
26
+ catalogAttentionReset: document.querySelector("#catalog-attention-reset"),
23
27
  detail: document.querySelector("#detail"),
24
28
  mainCol: document.querySelector(".main-col"),
25
29
  topbar: document.querySelector(".topbar"),
@@ -40,6 +44,13 @@ const state = {
40
44
  tasks: [],
41
45
  counts: null,
42
46
  attention: [],
47
+ catalogAttention: null,
48
+ catalogScope: null,
49
+ catalogAll: false,
50
+ catalogTotal: 0,
51
+ catalogCursor: null,
52
+ nextCursor: null,
53
+ attentionFilter: null,
43
54
  generatedAt: null,
44
55
  filter: "all",
45
56
  query: "",
@@ -48,6 +59,7 @@ const state = {
48
59
  detailKey: null
49
60
  };
50
61
  const VALID_FILTERS = ["all", "active", "draft", "completed", "cancelled", "archived"];
62
+ const VALID_ATTENTION = ["openInputs", "pendingOperations", "unknownOperations", "executionSignals"];
51
63
  let terminalSession = null;
52
64
  let terminalStateKey = "terminal.closed";
53
65
  const submittedRequests = new Set();
@@ -120,6 +132,10 @@ function syncUrlFromState(options) {
120
132
  else params.delete("filter");
121
133
  if (state.query) params.set("q", state.query);
122
134
  else params.delete("q");
135
+ if (state.attentionFilter) params.set("attention", state.attentionFilter);
136
+ else params.delete("attention");
137
+ if (state.catalogAll) params.set("all", "true");
138
+ else params.delete("all");
123
139
  applyUrl(params, options);
124
140
  }
125
141
 
@@ -228,9 +244,16 @@ function renderCurrentDetail(force) {
228
244
  function renderDynamicContent() {
229
245
  renderFilters(elements.filters, state, i18n.t, function (filter) {
230
246
  state.filter = filter;
247
+ state.attentionFilter = null;
248
+ state.catalogAll = false;
231
249
  syncUrlFromState({ replace: false });
232
- renderDynamicContent();
250
+ resetCatalog();
233
251
  });
252
+ elements.catalogNext.disabled = !state.nextCursor || refreshing;
253
+ elements.catalogCount.textContent = i18n.t("catalog.count")
254
+ .replace("{shown}", String(state.tasks.length)).replace("{total}", String(state.catalogTotal))
255
+ + (state.attentionFilter ? " · " + i18n.t("catalog." + state.attentionFilter) : "");
256
+ elements.catalogAttentionReset.hidden = !state.attentionFilter;
234
257
  const savedTaskScroll = elements.tasks.scrollTop;
235
258
  renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
236
259
  elements.tasks.scrollTop = savedTaskScroll;
@@ -463,9 +486,17 @@ async function answerInput(input, answer) {
463
486
  }
464
487
 
465
488
  let refreshing = false;
489
+ let catalogRequest = 0;
490
+ function resetCatalog() {
491
+ state.catalogCursor = null;
492
+ state.nextCursor = null;
493
+ refreshDashboard();
494
+ }
466
495
  async function refreshDashboard(options) {
467
- if (refreshing) return;
496
+ if (refreshing && options && options.quiet) return;
497
+ const request = ++catalogRequest;
468
498
  refreshing = true;
499
+ elements.catalogNext.disabled = true;
469
500
  const quiet = options && options.quiet;
470
501
  if (!quiet) {
471
502
  elements.refresh.disabled = true;
@@ -473,17 +504,29 @@ async function refreshDashboard(options) {
473
504
  }
474
505
  const previousInputs = state.counts ? state.counts.openInputs : null;
475
506
  try {
476
- const dashboard = await requestJson("/api/dashboard");
507
+ const query = new URLSearchParams({ view: "compact" });
508
+ if (state.catalogAll) query.set("all", "true");
509
+ if (state.filter !== "all") query.set("status", state.filter);
510
+ if (state.query.trim()) query.set("search", state.query.trim());
511
+ if (state.attentionFilter) query.set("attention", state.attentionFilter);
512
+ if (state.catalogCursor) query.set("cursor", state.catalogCursor);
513
+ const dashboard = await requestJson("/api/dashboard?" + query.toString());
514
+ if (request !== catalogRequest) return;
477
515
  state.tasks = dashboard.tasks;
478
- state.counts = dashboard.counts;
479
- state.attention = dashboard.attention || [];
480
- state.generatedAt = dashboard.generatedAt;
481
- if (state.selected && !state.tasks.some(function (task) { return task.id === state.selected; })) {
482
- clearSelection();
483
- } else {
484
- renderDynamicContent();
485
- }
486
- if (previousInputs !== null && dashboard.counts.openInputs > previousInputs) {
516
+ state.counts = { ...dashboard.counts, openInputs: dashboard.attention.openInputs.count };
517
+ state.catalogAttention = dashboard.attention;
518
+ state.catalogScope = dashboard.scope;
519
+ state.catalogTotal = dashboard.total;
520
+ state.nextCursor = dashboard.nextCursor;
521
+ state.attention = dashboard.attention.openInputs.refs.map(function (ref) {
522
+ const task = state.tasks.find(function (entry) { return entry.id === ref.taskId; });
523
+ return { taskId: ref.taskId, taskTitle: task ? task.title : ref.taskId };
524
+ });
525
+ state.generatedAt = new Date().toISOString();
526
+ // A selected Task may live on another page or outside current filters.
527
+ // Only its detail endpoint can establish that it no longer exists.
528
+ renderDynamicContent();
529
+ if (previousInputs !== null && state.counts.openInputs > previousInputs) {
487
530
  showToast(i18n.t("input.new"));
488
531
  }
489
532
  if (state.selected) {
@@ -492,13 +535,17 @@ async function refreshDashboard(options) {
492
535
  }
493
536
  }
494
537
  } catch {
538
+ if (request !== catalogRequest) return;
495
539
  if (!quiet) {
496
540
  renderError(elements.tasks, i18n.t("errors.dashboard"));
497
541
  showToast(i18n.t("errors.dashboard"));
498
542
  }
499
543
  } finally {
500
- refreshing = false;
501
- if (!quiet) elements.refresh.disabled = false;
544
+ if (request === catalogRequest) {
545
+ refreshing = false;
546
+ elements.refresh.disabled = false;
547
+ elements.catalogNext.disabled = !state.nextCursor;
548
+ }
502
549
  }
503
550
  }
504
551
 
@@ -650,12 +697,37 @@ if (elements.detailTabs) {
650
697
  });
651
698
  }
652
699
 
700
+ let catalogSearchTimer = null;
653
701
  elements.search.addEventListener("input", function () {
654
702
  state.query = elements.search.value;
655
703
  syncUrlFromState({ replace: true });
656
- const savedTaskScroll = elements.tasks.scrollTop;
657
- renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
658
- elements.tasks.scrollTop = savedTaskScroll;
704
+ window.clearTimeout(catalogSearchTimer);
705
+ catalogSearchTimer = window.setTimeout(resetCatalog, 200);
706
+ });
707
+ elements.catalogNext.addEventListener("click", function () {
708
+ if (!state.nextCursor) return;
709
+ state.catalogCursor = state.nextCursor;
710
+ refreshDashboard();
711
+ });
712
+ elements.catalogReset.addEventListener("click", function () {
713
+ resetCatalog();
714
+ });
715
+ elements.catalogAttentionReset.addEventListener("click", function () {
716
+ state.attentionFilter = null;
717
+ state.catalogAll = false;
718
+ syncUrlFromState({ replace: false });
719
+ resetCatalog();
720
+ });
721
+ elements.detail.addEventListener("click", function (event) {
722
+ const button = event.target.closest("[data-catalog-attention]");
723
+ if (!button) return;
724
+ state.attentionFilter = button.dataset.catalogAttention;
725
+ state.catalogAll = state.catalogScope && state.catalogScope.archived === "included";
726
+ state.filter = "all";
727
+ state.query = "";
728
+ elements.search.value = "";
729
+ syncUrlFromState({ replace: false });
730
+ resetCatalog();
659
731
  });
660
732
  elements.refresh.addEventListener("click", function () { refreshDashboard(); });
661
733
  elements.operatorTerminal.addEventListener("click", function () {
@@ -753,6 +825,8 @@ function applyStateFromUrl() {
753
825
  }
754
826
  const query = params.get("q");
755
827
  state.query = query || "";
828
+ state.attentionFilter = VALID_ATTENTION.includes(params.get("attention")) ? params.get("attention") : null;
829
+ state.catalogAll = params.get("all") === "true";
756
830
  if (elements.search) elements.search.value = state.query;
757
831
  const taskId = params.get("task");
758
832
  if (taskId) {
@@ -767,6 +841,7 @@ function applyStateFromUrl() {
767
841
 
768
842
  window.addEventListener("popstate", function () {
769
843
  applyStateFromUrl();
844
+ resetCatalog();
770
845
  });
771
846
 
772
847
  i18n.subscribe(function () {
@@ -228,17 +228,36 @@ export function workItemExecutionCard(projection, t) {
228
228
  return card;
229
229
  }
230
230
 
231
+ export function usageMetricText(metric, t, suffix) {
232
+ if (!metric || metric.value == null) return t("detail.unobserved");
233
+ return metric.value + (suffix || "") + (metric.status === "partial" ? " · " + t("detail.partial") : "");
234
+ }
235
+
236
+ function usageTile(label, metric, t, suffix) {
237
+ const tile = metricTile(label, usageMetricText(metric, t, suffix));
238
+ if (metric?.reasons?.length) {
239
+ tile.append(node("small", "muted", metric.reasons.map(function (reason) {
240
+ return t("usage.reason." + reason);
241
+ }).join("; ")));
242
+ }
243
+ return tile;
244
+ }
245
+
231
246
  export function observabilityMetricCard(observability, t) {
232
247
  if (!observability) return null;
233
248
  const card = node("div", "observability-metrics");
234
249
  const cost = observability.cost || {};
235
250
  const context = observability.context || {};
236
- card.append(metricTile(t("detail.tokens"), cost.tokens + (cost.tokensObservable === false ? "*" : "")));
237
- card.append(metricTile(t("detail.toolCalls"), cost.toolCalls + (cost.toolCallsObservable === false ? "*" : "")));
238
- card.append(metricTile(t("detail.wallClock"), cost.wallClockSeconds + "s"));
251
+ card.append(usageTile(t("detail.tokens"), cost.tokens, t));
252
+ card.append(usageTile(t("detail.toolCalls"), cost.toolCalls, t));
253
+ card.append(usageTile(t("detail.elapsed"), cost.elapsedSeconds, t, "s"));
254
+ card.append(usageTile(t("detail.executionSum"), cost.executionSeconds, t, "s"));
239
255
  card.append(metricTile(t("detail.ready"), (observability.dag?.readyIds || []).length, { hot: true }));
240
256
  card.append(metricTile(t("detail.contextSnapshots"), context.snapshotCount));
241
257
  const contextMeta = node("div", "record-meta observability-context-meta");
258
+ contextMeta.append(node("span", "", t("detail.usageScope")));
259
+ contextMeta.append(node("span", "", t("detail.observedThrough") + " · "
260
+ + (cost.observedThrough ? formatDateTime(cost.observedThrough) : t("detail.unobserved"))));
242
261
  contextMeta.append(node("span", "", t("detail.contextBytes") + " · "
243
262
  + (context.totalBytes === null ? t("detail.partial") : context.totalBytes + " B")));
244
263
  contextMeta.append(node("span", "", t("detail.compression") + " · " + t("detail.unavailable")));
@@ -534,14 +553,9 @@ export function workItemCard(item, titles, t, locale, actions, taskId) {
534
553
  if (item.observability) {
535
554
  const observability = item.observability;
536
555
  const metrics = node("div", "record-meta work-item-observability");
537
- metrics.append(node("span", "", t("detail.cost") + " · "
538
- + (observability.cost.tokensObservable
539
- ? observability.cost.tokens + " tokens"
540
- : t("detail.unobserved"))));
541
- metrics.append(node("span", "", observability.cost.toolCallsObservable
542
- ? observability.cost.toolCalls + " tools"
543
- : t("detail.unobserved")));
544
- metrics.append(node("span", "", observability.cost.wallClockSeconds + "s"));
556
+ metrics.append(usageTile(t("detail.tokens"), observability.cost.tokens, t));
557
+ metrics.append(usageTile(t("detail.toolCalls"), observability.cost.toolCalls, t));
558
+ metrics.append(usageTile(t("detail.executionSum"), observability.cost.executionSeconds, t, "s"));
545
559
  metrics.append(node("span", "", t("detail.contextSnapshots") + " · "
546
560
  + observability.context.snapshotCount));
547
561
  metrics.append(node("span", "", t("detail.results") + " · "
@@ -745,6 +759,30 @@ export function roleCard(role, task, t, locale, actions) {
745
759
  card.append(eff);
746
760
  }
747
761
 
762
+ if (role.providerRetry) {
763
+ const retry = role.providerRetry;
764
+ const retryMeta = node("p", "record-meta");
765
+ const zh = locale.startsWith("zh");
766
+ const labels = zh ? {
767
+ waiting: "等待 Provider 重试", "in-flight": "Provider 重试已提交",
768
+ recovered: "Provider 已恢复", cancelled: "自动重试已取消",
769
+ exhausted: "自动重试预算耗尽", "needs-attention": "需要检查 Provider",
770
+ "delivery-unknown": "投递结果未知,禁止重放"
771
+ } : {
772
+ waiting: "Waiting for Provider retry", "in-flight": "Provider retry submitted",
773
+ recovered: "Provider recovered", cancelled: "Automatic retry cancelled",
774
+ exhausted: "Automatic retry budget exhausted", "needs-attention": "Provider needs attention",
775
+ "delivery-unknown": "Delivery unknown; no replay"
776
+ };
777
+ retryMeta.textContent = (labels[retry.status] || retry.status) + " · "
778
+ + (zh && retry.category === "rate-limit" ? "临时限流" : retry.category) + " · "
779
+ + retry.attempts + "/" + retry.limit
780
+ + (retry.status === "waiting" ? " · " + new Date(retry.nextEligibleAt).toLocaleString(locale) : "")
781
+ + " · " + (zh ? "原成果保留" : "Existing work preserved")
782
+ + (retry.reason ? " · " + retry.reason : "");
783
+ card.append(retryMeta);
784
+ }
785
+
748
786
  if (role.sessionTokens) {
749
787
  const tokenMeta = node("div", "record-meta");
750
788
  const cumulative = role.sessionTokens.cumulativeTotal || {};
@@ -933,10 +971,14 @@ export function taskCard(task, hasAttention, state, t, locale, onSelect) {
933
971
  }
934
972
 
935
973
  // One signal only: an open input (needs the user) outranks a running count.
936
- if (hasAttention) {
937
- const badge = node("span", "task-signal is-input", String(task.openInputCount || 1));
974
+ if (hasAttention || (task.attention && task.attention.openInputs > 0)) {
975
+ const badge = node("span", "task-signal is-input", String(task.attention ? task.attention.openInputs : task.openInputCount || 1));
938
976
  badge.title = t("stats.inputs");
939
977
  button.append(badge);
978
+ } else if (task.attention && task.attention.executionSignals > 0) {
979
+ const badge = node("span", "task-signal", String(task.attention.executionSignals));
980
+ badge.title = t("catalog.executionSignals");
981
+ button.append(badge);
940
982
  } else if (task.workItems && task.workItems.running > 0) {
941
983
  const badge = node("span", "task-signal is-running", String(task.workItems.running));
942
984
  badge.title = t("stats.running");
@@ -1,6 +1,20 @@
1
1
  export const I18N_SCRIPT = `
2
2
  const messages = {
3
3
  "en": {
4
+ "catalog.first": "First page",
5
+ "catalog.all": "All tasks",
6
+ "catalog.clearAttention": "Clear attention filter",
7
+ "catalog.next": "Next page",
8
+ "catalog.count": "{shown} shown / {total} matching",
9
+ "catalog.attention": "Across the catalog",
10
+ "catalog.tasks": "tasks",
11
+ "catalog.openInputs": "Open inputs",
12
+ "catalog.inspectInputs": "Open the Open inputs category to inspect all affected tasks.",
13
+ "catalog.pendingOperations": "Pending operations",
14
+ "catalog.unknownOperations": "Unknown operations",
15
+ "catalog.executionSignals": "Execution facts to inspect",
16
+ "catalog.signalsHelp": "Counts cover the catalog, not just this page. Execution facts include live runs, open work and recovery signals; they do not imply failure. Open a task for its precise execution state.",
17
+ "catalog.pageScope": "Task groups below show this page only. Use Next page or search to discover more.",
4
18
  "a11y.skip": "Skip to the main panel",
5
19
  "actions.answer": "Answer",
6
20
  "actions.back": "Back to task list",
@@ -45,7 +59,9 @@ const messages = {
45
59
  "delivery.pending": "Awaiting delivery",
46
60
  "remoteDelivery.none": "No code delivery",
47
61
  "remoteDelivery.unavailable": "Exact heads unavailable",
48
- "remoteDelivery.pending": "Pending merge",
62
+ "remoteDelivery.pending": "Not delivered",
63
+ "remoteDelivery.uncovered": "Merged PR/MR, acceptance not covered",
64
+ "remoteDelivery.unverified": "Covering merge, not verified",
49
65
  "remoteDelivery.partial": "Partially merged",
50
66
  "remoteDelivery.merged": "Merged",
51
67
  "detail.acceptance": "Acceptance criteria",
@@ -67,7 +83,25 @@ const messages = {
67
83
  "detail.sessionTotalTokens": "Session total tokens",
68
84
  "detail.maximumRequestInputTokens": "Maximum request input",
69
85
  "detail.toolCalls": "Tool calls",
70
- "detail.wallClock": "Wall clock",
86
+ "detail.elapsed": "Task elapsed (including planning/waiting)",
87
+ "detail.executionSum": "Observed native execution sum",
88
+ "detail.usageScope": "Task lifetime · observed sources only · not billing totals",
89
+ "detail.observedThrough": "Evidence through",
90
+ "usage.reason.child-usage-overlap-unproven": "Child usage may overlap parent",
91
+ "usage.reason.source-coverage-partial": "Provider source coverage is partial",
92
+ "usage.reason.native-session-role-overlap": "Multiple Roles share the same native counter",
93
+ "usage.reason.session-usage-unavailable": "Session usage is missing or not safely countable",
94
+ "usage.reason.cumulative-not-allocatable-to-work-item": "Session cumulative usage cannot be allocated to this WorkItem",
95
+ "usage.reason.request-run-attribution-conflict": "Request Run ownership is ambiguous",
96
+ "usage.reason.no-attributable-request-usage": "No exactly attributable request usage",
97
+ "usage.reason.session-identity-missing": "Exact Session identity is missing",
98
+ "usage.reason.run-usage-unobserved": "Some Run usage is unobserved",
99
+ "usage.reason.counter-overflow": "Counter exceeds safe numeric range",
100
+ "usage.reason.no-token-evidence": "No token evidence",
101
+ "usage.reason.tool-history-compacted": "Tool history is compacted; retained calls only",
102
+ "usage.reason.nonzero-session-baseline": "Initial nonzero Session counter excluded",
103
+ "usage.reason.task-lifecycle-time-missing": "Task start/end evidence is missing",
104
+ "usage.reason.native-turn-history-compacted": "Complete retained Turns only; live/incomplete Turns excluded",
71
105
  "detail.ready": "Ready",
72
106
  "detail.contextSnapshots": "Context snapshots",
73
107
  "detail.contextBytes": "Context bytes",
@@ -390,6 +424,20 @@ const messages = {
390
424
  "review.scope.task": "Task"
391
425
  },
392
426
  "zh-CN": {
427
+ "catalog.first": "回到首页",
428
+ "catalog.all": "全部任务",
429
+ "catalog.clearAttention": "清除关注分类筛选",
430
+ "catalog.next": "下一页",
431
+ "catalog.count": "显示 {shown} 项 / 匹配 {total} 项",
432
+ "catalog.attention": "整个目录的关注事实",
433
+ "catalog.tasks": "个任务",
434
+ "catalog.openInputs": "未解决输入",
435
+ "catalog.inspectInputs": "打开“未解决输入”分类,查看全部相关任务。",
436
+ "catalog.pendingOperations": "进行中的操作",
437
+ "catalog.unknownOperations": "结果未知的操作",
438
+ "catalog.executionSignals": "可检查的执行事实",
439
+ "catalog.signalsHelp": "计数覆盖整个目录,不限当前页。执行事实包含活跃运行、未完成工作及恢复信号,不表示执行失败;打开任务查看精确执行状态。",
440
+ "catalog.pageScope": "以下任务分组仅展示当前页;可翻页或搜索发现更多任务。",
393
441
  "a11y.skip": "跳到主面板",
394
442
  "actions.answer": "提交回答",
395
443
  "actions.back": "返回任务列表",
@@ -434,7 +482,9 @@ const messages = {
434
482
  "delivery.pending": "等待送达",
435
483
  "remoteDelivery.none": "无代码交付",
436
484
  "remoteDelivery.unavailable": "精确 head 不可用",
437
- "remoteDelivery.pending": "待合入",
485
+ "remoteDelivery.pending": "尚未交付",
486
+ "remoteDelivery.uncovered": "PR/MR 已合并,未覆盖验收",
487
+ "remoteDelivery.unverified": "已覆盖合并,尚未验证",
438
488
  "remoteDelivery.partial": "部分合入",
439
489
  "remoteDelivery.merged": "已合入",
440
490
  "detail.acceptance": "验收标准",
@@ -456,7 +506,25 @@ const messages = {
456
506
  "detail.sessionTotalTokens": "Session 累计 Token",
457
507
  "detail.maximumRequestInputTokens": "单次请求最大输入",
458
508
  "detail.toolCalls": "工具调用",
459
- "detail.wallClock": "墙钟时间",
509
+ "detail.elapsed": "任务历时(含规划和等待)",
510
+ "detail.executionSum": "已观测原生执行累计",
511
+ "detail.usageScope": "Task 全生命周期 · 仅已观测来源 · 非账单总量",
512
+ "detail.observedThrough": "证据截止",
513
+ "usage.reason.child-usage-overlap-unproven": "子执行用量可能与父统计重叠",
514
+ "usage.reason.source-coverage-partial": "Provider 来源覆盖不完整",
515
+ "usage.reason.native-session-role-overlap": "多个 Role 共享同一原生计数器",
516
+ "usage.reason.session-usage-unavailable": "Session 用量缺失或无法安全计数",
517
+ "usage.reason.cumulative-not-allocatable-to-work-item": "Session 累计用量不能分配给此 WorkItem",
518
+ "usage.reason.request-run-attribution-conflict": "请求的 Run 归属不明确",
519
+ "usage.reason.no-attributable-request-usage": "没有精确归属的请求用量",
520
+ "usage.reason.session-identity-missing": "缺少精确 Session 身份",
521
+ "usage.reason.run-usage-unobserved": "部分 Run 没有用量证据",
522
+ "usage.reason.counter-overflow": "计数超过安全数值范围",
523
+ "usage.reason.no-token-evidence": "没有 token 证据",
524
+ "usage.reason.tool-history-compacted": "工具历史已压缩,仅计保留调用",
525
+ "usage.reason.nonzero-session-baseline": "已排除 Session 首次非零累计基线",
526
+ "usage.reason.task-lifecycle-time-missing": "缺少 Task 起止证据",
527
+ "usage.reason.native-turn-history-compacted": "仅计起止完整的保留 Turn,不含运行中或证据缺失的 Turn",
460
528
  "detail.ready": "可就绪",
461
529
  "detail.contextSnapshots": "上下文快照",
462
530
  "detail.contextBytes": "上下文字节",
@@ -383,7 +383,8 @@ export function renderTaskSurface(container, data, t, locale, actions) {
383
383
  const active = values("run").find((run) => run.roleName === role.name && run.status === "active");
384
384
  const incomplete = core.omitted.records > 0 || records("run").some((entry) => entry.omitted);
385
385
  roles.append(roleCard({
386
- ...role, status: active || incomplete ? "unknown" : "idle",
386
+ ...role, providerRetry: entry.providerRetry,
387
+ status: entry.providerRetry?.status === "waiting" ? "waiting" : active || incomplete ? "unknown" : "idle",
387
388
  effectiveLaunch: active ? active.effective : null,
388
389
  launchDrift: active && active.effective.sourceDesiredRevision !== role.launchRevision
389
390
  }, task, t, locale, actions));
@@ -66,12 +66,17 @@ export function renderFilters(container, state, t, onFilter) {
66
66
  if (!btn) return;
67
67
  btn.classList.toggle("is-active", state.filter === status);
68
68
  const label = btn.querySelector(".filter-label");
69
- if (label) label.textContent = status === "all" ? t("filter.unarchived") : translatedStatus(t, "status", status);
69
+ if (label) label.textContent = status === "all"
70
+ ? t(state.catalogAll ? "catalog.all" : "filter.unarchived") : translatedStatus(t, "status", status);
70
71
  const count = status === "all"
71
- ? (counts.total === undefined ? undefined : counts.total - (counts.archived || 0))
72
+ ? (counts.total === undefined ? undefined : counts.total - (state.catalogAll ? 0 : counts.archived || 0))
72
73
  : counts[status];
73
74
  const badge = btn.querySelector(".filter-count");
74
75
  if (badge) {
76
+ if (status === "archived" && state.catalogScope && state.catalogScope.archived === "excluded") {
77
+ badge.hidden = true;
78
+ return;
79
+ }
75
80
  if (count !== undefined && count !== null) {
76
81
  badge.textContent = String(count);
77
82
  badge.hidden = false;
@@ -83,7 +88,7 @@ export function renderFilters(container, state, t, onFilter) {
83
88
  }
84
89
 
85
90
  function taskGroupOf(task, attentionIds) {
86
- if (attentionIds.has(task.id)) return "attention";
91
+ if (attentionIds.has(task.id) || (task.attention && (task.attention.openInputs > 0 || task.attention.unknownOperations > 0))) return "attention";
87
92
  if (task.status === "active") return "active";
88
93
  if (task.status === "draft") return "draft";
89
94
  if (task.status === "cancelled") return "cancelled";
@@ -107,9 +112,9 @@ export function renderTasks(container, state, t, locale, onSelect) {
107
112
  const attentionIds = new Set((state.attention || []).map(function (item) { return item.taskId; }));
108
113
  const groups = { attention: [], active: [], draft: [], finished: [], cancelled: [], archived: [] };
109
114
  (state.tasks || []).forEach(function (task) {
110
- if (state.filter === "all" && task.status === "archived") return;
115
+ if (state.filter === "all" && !state.catalogAll && task.status === "archived") return;
111
116
  if (state.filter !== "all" && task.status !== state.filter) return;
112
- if (!taskMatchesQuery(task, query, locale)) return;
117
+ if (!state.catalogScope && !taskMatchesQuery(task, query, locale)) return;
113
118
  groups[taskGroupOf(task, attentionIds)].push(task);
114
119
  });
115
120
 
@@ -166,15 +171,33 @@ export function renderOverview(detail, state, t, locale, onSelect) {
166
171
  );
167
172
  wrap.append(rail);
168
173
 
174
+ if (state.catalogAttention) {
175
+ const catalog = node("section", "overview-block");
176
+ catalog.append(sectionHead(t("catalog.attention")));
177
+ catalog.append(node("p", "", t("catalog.signalsHelp")));
178
+ const groups = node("div", "overview-list");
179
+ Object.entries(state.catalogAttention).forEach(function (entry) {
180
+ const kind = entry[0], value = entry[1];
181
+ const button = node("button", "overview-row",
182
+ t("catalog." + kind) + " · " + value.count + " / " + value.taskCount + " " + t("catalog.tasks"));
183
+ button.type = "button";
184
+ button.dataset.catalogAttention = kind;
185
+ button.disabled = value.taskCount === 0;
186
+ groups.append(button);
187
+ });
188
+ catalog.append(groups);
189
+ wrap.append(catalog);
190
+ }
169
191
  const inbox = node("section", "overview-block");
170
192
  const inboxHead = sectionHead(t("overview.inbox"), {
171
- count: (state.attention || []).length
193
+ count: counts ? counts.openInputs : (state.attention || []).length
172
194
  });
173
195
  inbox.append(inboxHead);
174
196
  const items = state.attention || [];
175
197
  if (!items.length) {
176
198
  const empty = node("div", "inbox-empty");
177
- empty.append(node("span", "dot"), node("span", "", t("overview.inboxEmpty")));
199
+ empty.append(node("span", "dot"), node("span", "", counts && counts.openInputs > 0
200
+ ? t("catalog.inspectInputs") : t("overview.inboxEmpty")));
178
201
  inbox.append(empty);
179
202
  } else {
180
203
  const list = node("div", "inbox-list");
@@ -184,6 +207,7 @@ export function renderOverview(detail, state, t, locale, onSelect) {
184
207
  inbox.append(list);
185
208
  }
186
209
  wrap.append(inbox);
210
+ if (state.catalogScope) wrap.append(node("p", "", t("catalog.pageScope")));
187
211
 
188
212
  // Tasks whose Task-first projection says they need attention: blocked,
189
213
  // recovering, or in an attention state. This replaces the raw stalled-AgentRun
@@ -335,9 +359,13 @@ export function renderTaskDetail(detail, data, t, locale, actions) {
335
359
  "muted mono",
336
360
  project.directory + " · " + project.coverage + " · "
337
361
  + (project.expectedLocalCommit ? project.expectedLocalCommit.slice(0, 12) : "unknown")
362
+ + " → " + (project.deliveryLocalCommit ? project.deliveryLocalCommit.slice(0, 12) : "unknown")
338
363
  + " → "
339
364
  + (project.remoteCommit ? project.remoteCommit.slice(0, 12) : "unknown")
365
+ + " · PR/MR: " + (project.state || "none")
366
+ + (project.adoption ? " · " + project.adoption.id : "")
340
367
  ));
368
+ card.append(node("p", "record-copy", project.reason));
341
369
  });
342
370
  summaryBody.append(card);
343
371
  }
@@ -38,6 +38,12 @@ export const DASHBOARD_HTML = `<!doctype html>
38
38
  </label>
39
39
  <div id="status-filters" class="filters" role="group" aria-label="Filter by status" data-i18n-aria-label="filters.label"></div>
40
40
  <div id="task-list" class="task-list" aria-label="Tasks" data-i18n-aria-label="board.title" aria-live="polite"><div class="loading" data-i18n="loading.dashboard">Reading local state…</div></div>
41
+ <div id="catalog-controls" class="catalog-pager" aria-live="polite">
42
+ <button id="catalog-reset" type="button" data-i18n="catalog.first">First page</button>
43
+ <span id="catalog-count"></span>
44
+ <button id="catalog-next" type="button" data-i18n="catalog.next" disabled>Next page</button>
45
+ <button id="catalog-attention-reset" type="button" data-i18n="catalog.clearAttention" hidden>Clear attention filter</button>
46
+ </div>
41
47
  <div class="sidebar-foot">
42
48
  <div class="sidebar-controls">
43
49
  <label class="select-control">
@@ -19,6 +19,13 @@ body.terminal-active .app-shell{grid-template-columns:var(--sidebar-w) minmax(0,
19
19
  .task-group-head{display:flex;align-items:baseline;gap:8px;padding:3px 4px 2px}
20
20
  .sidebar-foot{flex:none;padding-top:10px;margin-top:8px;display:grid;gap:8px}
21
21
  .sidebar-controls{display:grid;grid-template-columns:1fr 1fr;gap:8px;align-items:center}
22
+ .catalog-pager{display:grid;grid-template-columns:1fr 1fr;gap:6px;flex:none;padding-top:10px}
23
+ .catalog-pager span{grid-column:1/-1;grid-row:1;text-align:center;font-size:11px;color:var(--muted)}
24
+ .catalog-pager button{min-height:32px;min-width:0;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font:inherit;font-size:11px;cursor:pointer}
25
+ .catalog-pager button:hover:not(:disabled){border-color:var(--accent)}
26
+ .catalog-pager button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
27
+ .catalog-pager button:disabled{opacity:.45;cursor:default}
28
+ #catalog-attention-reset{grid-column:1/-1}
22
29
  /* Main column = top bar + optional section nav + reading surface */
23
30
  .main-col{display:flex;flex-direction:column;min-width:0;min-height:0;height:100vh;overflow-y:auto;scrollbar-gutter:stable}
24
31
  .topbar{position:sticky;top:0;z-index:30;flex:none;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:11px max(var(--page-space),calc((100% - 1100px) / 2));background:color-mix(in srgb,var(--bg) 92%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1px solid var(--border)}
@@ -1,11 +1,12 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import WebSocket, { WebSocketServer } from "ws";
4
- import { usageError } from "../errors/cliError.js";
4
+ import { CliError, usageError } from "../errors/cliError.js";
5
+ import { parseTaskCatalogOptions } from "../context/taskCatalog.js";
5
6
  import { WebRequestRejected } from "./webMutation.js";
6
7
  import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
7
8
  import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
8
- import { buildWebDashboardSnapshot, buildWebTaskDetail } from "./webSnapshot.js";
9
+ import { buildWebDashboardSnapshot, buildWebTaskCatalog, buildWebTaskDetail } from "./webSnapshot.js";
9
10
  const MAX_JSON_BODY_BYTES = 16 * 1024;
10
11
  const MAX_TERMINAL_MESSAGE_BYTES = 64 * 1024;
11
12
  const MAX_TERMINAL_BUFFERED_BYTES = 1024 * 1024;
@@ -236,7 +237,13 @@ async function handleHttpRequest(request, response, store, dependencies, token,
236
237
  sendAsset(response, asset, method === "HEAD");
237
238
  }
238
239
  else if (pathname === "/api/dashboard") {
239
- sendJson(response, 200, buildWebDashboardSnapshot(store, now()), method === "HEAD");
240
+ const query = new URL(request.url, "http://localhost").searchParams;
241
+ const snapshot = query.has("view")
242
+ ? buildWebTaskCatalog(store, parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
243
+ ? (value === "true" ? ["--all"] : ["--invalid-all"])
244
+ : [`--${key}`, value])))
245
+ : buildWebDashboardSnapshot(store, now());
246
+ sendJson(response, 200, snapshot, method === "HEAD");
240
247
  }
241
248
  else if (pathname.startsWith("/api/tasks/")) {
242
249
  const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
@@ -250,6 +257,10 @@ async function handleHttpRequest(request, response, store, dependencies, token,
250
257
  }
251
258
  }
252
259
  catch (error) {
260
+ if (error instanceof CliError) {
261
+ sendJson(response, 400, { error: error.message }, method === "HEAD");
262
+ return;
263
+ }
253
264
  if (error instanceof URIError) {
254
265
  sendJson(response, 400, { error: "Invalid URL encoding." }, method === "HEAD");
255
266
  return;
@@ -1,3 +1,5 @@
1
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
2
+ import { readTaskCatalog } from "../context/taskCatalog.js";
1
3
  import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
2
4
  import { retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
3
5
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
@@ -9,6 +11,9 @@ import { runExecutionObservation } from "../agentRun/agentRun.js";
9
11
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
10
12
  import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
11
13
  import { projectTaskRemoteDelivery } from "../task/remoteDelivery.js";
14
+ export function buildWebTaskCatalog(store, options) {
15
+ return store.transaction(reader => readTaskCatalog(reader, options));
16
+ }
12
17
  export function buildWebDashboardSnapshot(store, now = new Date()) {
13
18
  return store.transaction((reader) => {
14
19
  const statusCounts = {
@@ -95,12 +100,15 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
95
100
  const activeSession = sessions?.sessions[activeRun?.effective.agentId ?? sessions.activeAgentId];
96
101
  const effectiveLaunch = activeRun?.effective ?? activeSession?.effective ?? null;
97
102
  const delivery = sessions?.providerBinding?.run?.status;
103
+ const providerRetry = providerRetryProjection(sessions?.providerBinding);
98
104
  return {
99
105
  ...role,
106
+ providerRetry,
100
107
  // An open record is not proof that its Agent is running.
101
- status: delivery === "accepted" ? "running"
102
- : delivery === "submitting" || delivery === "deferred" ? "waiting"
103
- : activeSession === undefined && activeRun === undefined ? "idle" : "unknown",
108
+ status: providerRetry?.status === "waiting" ? "waiting"
109
+ : delivery === "accepted" ? "running"
110
+ : delivery === "submitting" || delivery === "deferred" ? "waiting"
111
+ : activeSession === undefined && activeRun === undefined ? "idle" : "unknown",
104
112
  sessionTokens: projectSessionTokenMetrics(events, resolveSessionTokenIdentity(activeSession === undefined
105
113
  ? null
106
114
  : { taskId, roleName: role.name, ...activeSession })),
@@ -162,6 +170,7 @@ function webRemoteDelivery(reader, task) {
162
170
  publications: reader.listPublicationReferences(task.id),
163
171
  managedWorkspaces: reader.listManagedWorkspaces(task.id),
164
172
  runs: reader.listRuns(task.id),
173
+ integrations: reader.listIntegrationAttempts(task.id),
165
174
  currentCandidate: null
166
175
  });
167
176
  }