@zq-silk/yui 0.16.0 → 0.99.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.

Potentially problematic release.


This version of @zq-silk/yui might be problematic. Click here for more details.

Files changed (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +128 -20
  95. package/docs/release-workflow.zh-CN.md +107 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -1,6 +1,7 @@
1
1
  export const TASK_SURFACE_SCRIPT = `
2
2
  import { node, clear } from "/assets/js/dom.js";
3
3
  import { anchorSection, sectionHead, richText, inputCard, roleCard, runCard, pill } from "/assets/js/components.js";
4
+ import { renderTaskSummary, renderEvidence } from "/assets/js/task-summary.js";
4
5
 
5
6
  // All business facts below retain their Context reference. Expanded values
6
7
  // are current reads, not mutations of a historical Context snapshot.
@@ -16,6 +17,10 @@ export function renderTaskSurface(container, data, t, locale, actions) {
16
17
  const scaffold = node("div", "detail-scaffold task-surface");
17
18
  const summary = node("div", "section-body");
18
19
  summary.append(node("span", "detail-kicker", task.id), node("h2", "detail-title", task.title), pill(t, "status", task.status));
20
+ renderTaskSummary(summary, data, t, locale, actions, recordCard);
21
+ const discuss = node("details", "record-card");
22
+ discuss.dataset.viewKey = "discussion";
23
+ discuss.append(node("summary", "", say("Discuss this Task", "讨论此任务")));
19
24
  const conversation = node("button", "record-open", say("View Leader Session (read-only)", "查看 Leader Session(只读)"));
20
25
  conversation.type = "button";
21
26
  // A Draft's planning Session is a real Leader Session, so it is viewable once it
@@ -26,7 +31,7 @@ export function renderTaskSurface(container, data, t, locale, actions) {
26
31
  const hasLeaderSession = Boolean(leaderRole?.runtimeSession?.nativeSessionId);
27
32
  conversation.disabled = task.status === "archived" || !hasLeaderSession;
28
33
  conversation.addEventListener("click", () => actions.openTerminal({ scope: "task", taskId: task.id, roleName: "leader" }));
29
- summary.append(conversation);
34
+ discuss.append(conversation);
30
35
  const chat = node("form", "record-card");
31
36
  const chatLabel = node("label", "", say("Message to Task Leader", "发送给 Task Leader"));
32
37
  const message = node("textarea", "");
@@ -90,13 +95,15 @@ export function renderTaskSurface(container, data, t, locale, actions) {
90
95
  "提交结果未知。请重新加载并检查已保存消息,不要盲目重发 · ") + requestId;
91
96
  }
92
97
  });
93
- summary.append(chat);
98
+ discuss.append(chat);
99
+ summary.append(discuss);
94
100
  // decision-3 three-action input control (queue / steer / interrupt). This is
95
101
  // the same application-layer path the CLI drives; the Web surface adds no
96
102
  // fourth action and no auto-fallback. Steer/interrupt name an exact current
97
103
  // Turn (expected-target); a mismatch or missing Turn is a visible failure
98
104
  // receipt, never a silent downgrade to a queue.
99
105
  const control = node("details", "record-card");
106
+ control.dataset.viewKey = "control";
100
107
  control.append(node("summary", "", say("Redirect a running Role (queue / steer / interrupt)",
101
108
  "改向运行中的 Role(排队/即时插话/打断)")));
102
109
  const controlForm = node("form", "record-block");
@@ -207,8 +214,8 @@ export function renderTaskSurface(container, data, t, locale, actions) {
207
214
  // durable read and the one that drives re-rendering. The runtime observation
208
215
  // is explicitly optional (it may time out and only ever enriches a live
209
216
  // Session), so it must not decide whether a planning Turn is reported.
210
- const turnEntries = records("turn");
211
- const planningTurns = values("turn").filter((turn) =>
217
+ const turnEntries = records("run");
218
+ const planningTurns = values("run").filter((turn) =>
212
219
  turn.roleName === "leader" && turn.purpose === "planning");
213
220
  // The latest planning Turn, whatever state it reached. A completed planning
214
221
  // Turn is still the real observed fact; reporting only active ones would call
@@ -224,19 +231,19 @@ export function renderTaskSurface(container, data, t, locale, actions) {
224
231
  ? planningTurn.status
225
232
  : withheldTurns ? "withheld" : "not-dispatched";
226
233
  planning.append(node("p", "muted", withheldTurns
227
- ? say("This bounded snapshot withholds Turn detail; read the Turn directly to see planning state.",
228
- "当前受限快照未展开 Turn 详情;请直接读取 Turn 查看规划状态。")
234
+ ? say("This bounded snapshot withholds AgentRun detail; read the original planning Run.",
235
+ "当前受限快照未展开 AgentRun 详情;请读取原始 planning Run。")
229
236
  : planningTurn === null
230
- ? say("No planning Turn has been dispatched yet. Sending a message queues the Leader.",
231
- "尚未派发 planning Turn;发送消息会为 Leader 排队。")
237
+ ? say("No planning AgentRun is included. Direct Session activity is shown separately.",
238
+ "本次读取未包含 planning AgentRun;直接 Session 活动单独展示。")
232
239
  : planningTurn.status === "active"
233
- ? say("Planning Turn is active; messages reach the Task Leader.",
234
- "planning Turn 正在进行;消息会送达 Task Leader。")
235
- : say("Planning Turn reached " + planningTurn.status
240
+ ? say("The planning AgentRun is open; this is not proof of native activity.",
241
+ "planning AgentRun 尚未结清;这不证明原生会话仍有活动。")
242
+ : say("Planning AgentRun reached " + planningTurn.status
236
243
  + "; messages reach the same Task Leader and queue the next Turn.",
237
- "planning Turn 已" + planningTurn.status + ";消息仍送达同一 Task Leader 并为下一个 Turn 排队。")));
244
+ "planning AgentRun 已" + planningTurn.status + ";消息仍送达同一 Task Leader 并为下一个 Turn 排队。")));
238
245
  const facts = [
239
- [say("Planning Turn", "planning Turn"), planningTurn
246
+ [say("Planning AgentRun", "planning AgentRun"), planningTurn
240
247
  ? planningTurn.id + " (" + planningTurn.status + ")"
241
248
  : withheldTurns ? say("withheld by this snapshot", "本快照未展开") : say("none", "无")],
242
249
  [say("Provider terminal", "Provider 终态"),
@@ -251,7 +258,7 @@ export function renderTaskSurface(container, data, t, locale, actions) {
251
258
  ?? say("not currently running", "当前未在运行")],
252
259
  [say("Environment", "环境"), environment?.environmentRef
253
260
  ?? say("empty (legal for planning)", "空环境(规划阶段合法)")],
254
- [say("Turn observed at", "Turn 观测时间"),
261
+ [say("Run record updated", "Run 记录更新时间"),
255
262
  planningTurn?.result?.completedAt ?? planningTurn?.updatedAt ?? say("unknown", "未知")]
256
263
  ];
257
264
  for (const [label, value] of facts) {
@@ -259,8 +266,13 @@ export function renderTaskSurface(container, data, t, locale, actions) {
259
266
  }
260
267
  summary.append(planning);
261
268
  }
262
- if (task.description) summary.append(richText(say("Current requirements", "当前要求"), task.description, t));
263
- if (task.completionSummary) summary.append(richText(t("detail.conclusion"), task.completionSummary, t));
269
+ if (task.description) {
270
+ const requirements = node("details", "record-card");
271
+ requirements.dataset.viewKey = "requirements";
272
+ requirements.append(node("summary", "", say("Original requirements and boundaries", "原始要求与边界")),
273
+ richText(null, task.description, t));
274
+ summary.append(requirements);
275
+ }
264
276
  const edit = node("details", "record-card");
265
277
  edit.append(node("summary", "", say("Edit Task title", "修改任务标题")));
266
278
  const form = node("form", "record-block");
@@ -316,6 +328,9 @@ export function renderTaskSurface(container, data, t, locale, actions) {
316
328
  const value = entry.value;
317
329
  const text = value.content || value.summary || value.body || value.objective || value.title || value.leaderSummary;
318
330
  if (text) card.append(richText(null, text, t));
331
+ if (entry.ref.store === "task-decision" && value.rationale) {
332
+ card.append(richText(say("Reason and boundaries", "理由与边界"), value.rationale, t));
333
+ }
319
334
  if (value.status) card.append(node("p", "muted", value.status));
320
335
  if (value.roleName) card.append(node("p", "muted", value.roleName));
321
336
  if (entry.ref.store === "work-item") {
@@ -357,7 +372,10 @@ export function renderTaskSurface(container, data, t, locale, actions) {
357
372
  if (!entries.length) body.append(node("p", "muted", core.omitted.records
358
373
  ? say("Not included in this compact read", "此紧凑读取未包含该记录")
359
374
  : say("No records", "暂无记录")));
360
- scaffold.append(anchorSection(id, sectionHead(name, { count: entries.length }), body));
375
+ const folded = node("details", "record-card");
376
+ folded.dataset.viewKey = id;
377
+ folded.append(node("summary", "", name + " · " + entries.length), body);
378
+ scaffold.append(anchorSection(id, null, folded));
361
379
  return body;
362
380
  }
363
381
  const focus = section("detail-focus", say("Goal and current focus", "目标与当前关注"), records("task-brief"));
@@ -366,13 +384,10 @@ export function renderTaskSurface(container, data, t, locale, actions) {
366
384
  focus.prepend(richText(t("detail.focus"), brief.currentFocus, t));
367
385
  if (brief.boundaries.length) focus.append(richText(say("Boundaries", "边界"), brief.boundaries.join("\\n"), t));
368
386
  }
369
- const questions = node("div", "section-body");
370
- records("input-request").filter((entry) => entry.omitted || entry.value.status === "open").forEach((entry) => {
371
- questions.append(entry.omitted ? recordCard(entry) : inputCard(entry.value, { single: true }, t, locale, actions));
372
- });
373
- scaffold.append(anchorSection("detail-attention", sectionHead(t("detail.attention")), questions));
374
387
  section("detail-work", say("Responsibilities and acceptance", "工作责任与验收"), records("work-item"));
375
- section("detail-results", say("Saved results and provenance", "已保存结果与来源"), records("artifact").concat(records("candidate")));
388
+ const evidence = node("div", "section-body");
389
+ renderEvidence(evidence, data, t, locale, actions);
390
+ scaffold.append(anchorSection("detail-results", sectionHead(say("Results and evidence", "成果与证据")), evidence));
376
391
  section("detail-messages", t("detail.messages"), records("task-message"));
377
392
  section("detail-history", say("Decisions and milestones", "决策与里程碑"), records("task-decision").concat(records("task-milestone")));
378
393
  section("detail-reviews", t("detail.reviews"), records("review-round"));
@@ -381,10 +396,9 @@ export function renderTaskSurface(container, data, t, locale, actions) {
381
396
  if (entry.omitted) { roles.append(recordCard(entry)); return; }
382
397
  const role = entry.value;
383
398
  const active = values("run").find((run) => run.roleName === role.name && run.status === "active");
384
- const incomplete = core.omitted.records > 0 || records("run").some((entry) => entry.omitted);
385
399
  roles.append(roleCard({
386
400
  ...role, providerRetry: entry.providerRetry,
387
- status: entry.providerRetry?.status === "waiting" ? "waiting" : active || incomplete ? "unknown" : "idle",
401
+ status: entry.providerRetry?.status === "waiting" ? "waiting" : "unknown",
388
402
  effectiveLaunch: active ? active.effective : null,
389
403
  launchDrift: active && active.effective.sourceDesiredRevision !== role.launchRevision
390
404
  }, task, t, locale, actions));
@@ -392,8 +406,12 @@ export function renderTaskSurface(container, data, t, locale, actions) {
392
406
  "No active AgentRun in this read; Session activity is a separate observation.",
393
407
  "此读取中没有活跃 AgentRun;Session 活动属于独立观察。")));
394
408
  });
395
- scaffold.append(anchorSection("detail-roles", sectionHead(t("detail.roles")), roles));
409
+ const roleDetails = node("details", "record-card");
410
+ roleDetails.dataset.viewKey = "roles";
411
+ roleDetails.append(node("summary", "", t("detail.roles")), roles);
412
+ scaffold.append(anchorSection("detail-roles", null, roleDetails));
396
413
  const execution = node("details", "record-card");
414
+ execution.dataset.viewKey = "execution";
397
415
  execution.append(node("summary", "", say("Execution and observations", "展开执行与观察")));
398
416
  execution.append(node("p", "muted", say(
399
417
  "An open execution record is not proof of Agent activity. Native admission and Task progress are separate facts.",
@@ -3,6 +3,7 @@ export const VIEW_SCRIPT = `
3
3
  // All reusable widgets and cards come from components.js.
4
4
  import { clear, node } from "/assets/js/dom.js";
5
5
  import { renderTaskSurface } from "/assets/js/task-surface.js";
6
+ import { renderSessionSummary } from "/assets/js/task-summary.js";
6
7
  import { byNewest, formatDateTime } from "/assets/js/format.js";
7
8
  import {
8
9
  anchorSection,
@@ -162,6 +163,10 @@ export function renderOverview(detail, state, t, locale, onSelect) {
162
163
  const counts = state.counts;
163
164
 
164
165
  const total = counts ? counts.total : 0;
166
+ const say = (en, cn) => locale.startsWith("zh") ? cn : en;
167
+ wrap.append(node("p", "muted", say(
168
+ "Attention counts cover the authorized catalog before search/status filters; task lists below cover this page.",
169
+ "关注计数覆盖搜索/状态过滤前的可见目录;下方任务列表仅覆盖当前页。")));
165
170
  const rail = node("div", "command-rail");
166
171
  rail.append(
167
172
  metricTile(t("metrics.active"), counts ? counts.active : "—", { hot: true }),
@@ -208,6 +213,32 @@ export function renderOverview(detail, state, t, locale, onSelect) {
208
213
  }
209
214
  wrap.append(inbox);
210
215
  if (state.catalogScope) wrap.append(node("p", "", t("catalog.pageScope")));
216
+ const sessions = node("section", "overview-block");
217
+ sessions.append(sectionHead(say("Session activity · current page only", "会话活动 · 仅当前页")));
218
+ sessions.append(node("p", "muted", say(
219
+ "Not loaded by the compact list. Active Task / AgentRun counts cannot establish how many native Sessions are running.",
220
+ "紧凑列表不加载会话观察。Active Task/AgentRun 数不能说明有几个原生会话在运行。")));
221
+ const observe = node("button", "record-open", say("Read this page's Sessions", "读取本页会话"));
222
+ observe.type = "button";
223
+ observe.dataset.observeSessions = "";
224
+ observe.disabled = state.sessionLoading || !state.tasks.length;
225
+ sessions.append(observe);
226
+ if (state.sessionError) sessions.append(node("p", "muted", state.sessionError));
227
+ if (state.sessionOverview) {
228
+ sessions.append(node("p", "muted", say("Read at: ", "读取于:") + formatDateTime(state.sessionOverview.readAt, locale)));
229
+ for (const task of state.sessionOverview.tasks) {
230
+ const card = node("article", "record-card");
231
+ const open = node("button", "record-open", task.taskId + " · " + task.title);
232
+ open.type = "button";
233
+ open.addEventListener("click", () => onSelect(task.taskId));
234
+ card.append(open);
235
+ const body = node("div", "");
236
+ renderSessionSummary(body, task, locale);
237
+ card.append(body);
238
+ sessions.append(card);
239
+ }
240
+ }
241
+ wrap.append(sessions);
211
242
 
212
243
  // Tasks whose Task-first projection says they need attention: blocked,
213
244
  // recovering, or in an attention state. This replaces the raw stalled-AgentRun
@@ -228,6 +259,8 @@ export function renderOverview(detail, state, t, locale, onSelect) {
228
259
  task,
229
260
  t("exec.status." + task.executionStatus),
230
261
  "has-inputs",
262
+ t,
263
+ locale,
231
264
  onSelect
232
265
  ));
233
266
  });
@@ -89,6 +89,7 @@ export const DASHBOARD_HTML = `<!doctype html>
89
89
  <nav id="detail-tabs" class="detail-tabs" aria-label="Task sections" data-i18n-aria-label="tabs.label" hidden>
90
90
  <button class="tab" type="button" data-target="detail-top" data-i18n="tabs.summary">Summary</button>
91
91
  <button class="tab" type="button" data-target="detail-focus" data-i18n="tabs.focus">Focus</button>
92
+ <button class="tab" type="button" data-target="detail-results" data-i18n="tabs.results">Results</button>
92
93
  <button class="tab" type="button" data-target="detail-work" data-i18n="tabs.work">Work items</button>
93
94
  <button class="tab" type="button" data-target="detail-exec" data-i18n="tabs.exec">AgentRuns</button>
94
95
  <button class="tab" type="button" data-target="detail-reviews" data-i18n="tabs.reviews">Reviews</button>
@@ -23,6 +23,27 @@ export const CARD_STYLES = `
23
23
  .task-surface button { min-height:40px; }
24
24
  .task-surface summary { cursor:pointer; padding:8px 0; font-weight:600; }
25
25
  .task-surface [role=status] { overflow-wrap:anywhere; }
26
+ .summary-block h3 { margin:0 0 12px; font-size:16px; }
27
+ .summary-block h4 { margin:12px 0 4px; }
28
+ .summary-block p { overflow-wrap:anywhere; }
29
+ .task-summary-grid { display:grid; gap:12px; min-width:0; align-items:start; }
30
+ .task-summary-grid .summary-block { display:block; }
31
+ .task-summary-grid .summary-block p { margin:8px 0; }
32
+ .task-surface .record-pills { flex-wrap:wrap; max-width:100%; }
33
+ @media (min-width:1100px) {
34
+ .task-summary-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
35
+ }
36
+ .session-counts { display:flex; flex-wrap:wrap; gap:8px; }
37
+ .session-count { padding:6px 10px; background:var(--bg-3); border:1px solid var(--border); border-radius:var(--radius); }
38
+ .session-row { padding:12px 0; border-top:1px solid var(--border); overflow-wrap:anywhere; }
39
+ .artifact-choice { text-align:left; white-space:normal; overflow-wrap:anywhere; }
40
+ .task-surface .artifact-text { max-height:32rem; padding:16px; border:1px solid var(--border); background:var(--bg-1); }
41
+ .task-surface .mono { overflow-wrap:anywhere; }
42
+ .task-surface .muted { overflow-wrap:anywhere; min-width:0; }
43
+ .task-surface .observability-metrics { grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr)); }
44
+ .task-surface .observability-metrics .metric { flex-direction:column; align-items:flex-start; min-width:0; }
45
+ .task-surface .observability-metrics .metric-value { overflow-wrap:anywhere; max-width:100%; }
46
+ .task-surface button { min-height:44px; }
26
47
  /* Overview scaffolding */
27
48
  .overview{display:grid;gap:14px;padding-top:2px}
28
49
  .overview-duo{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start}
@@ -6,7 +6,7 @@ import { parseTaskCatalogOptions } from "../context/taskCatalog.js";
6
6
  import { WebRequestRejected } from "./webMutation.js";
7
7
  import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
8
8
  import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
9
- import { buildWebTaskCatalog, buildWebTaskDetail } from "./webSnapshot.js";
9
+ import { buildWebTaskCatalog, buildWebPageSessions, buildWebTaskDetail } from "./webSnapshot.js";
10
10
  const MAX_JSON_BODY_BYTES = 16 * 1024;
11
11
  const MAX_TERMINAL_MESSAGE_BYTES = 64 * 1024;
12
12
  const MAX_TERMINAL_BUFFERED_BYTES = 1024 * 1024;
@@ -100,6 +100,37 @@ async function handleHttpRequest(request, response, store, dependencies, token,
100
100
  sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
101
101
  return;
102
102
  }
103
+ const artifactTarget = /^\/api\/tasks\/([^/]+)\/(artifacts|evidence)$/.exec(pathname);
104
+ if (artifactTarget && dependencies.surface) {
105
+ if (method !== "GET") {
106
+ sendJson(response, 405, { error: "Artifacts are read-only." }, false);
107
+ return;
108
+ }
109
+ try {
110
+ const taskId = decodeURIComponent(artifactTarget[1]);
111
+ const query = new URL(request.url, "http://localhost").searchParams;
112
+ if (artifactTarget[2] === "evidence") {
113
+ if ([...query.keys()].length)
114
+ throw new WebRequestRejected("Evidence read takes no additional parameters.");
115
+ sendJson(response, 200, dependencies.surface.evidence(taskId), false);
116
+ return;
117
+ }
118
+ if ([...query.keys()].some(key => !["path", "commit"].includes(key))) {
119
+ throw new WebRequestRejected("Artifact read accepts only path and commit.");
120
+ }
121
+ const path = query.get("path");
122
+ const commit = query.get("commit");
123
+ if ((path === null) !== (commit === null))
124
+ throw new WebRequestRejected("Select path and fixed commit together.");
125
+ const result = path === null ? await dependencies.surface.artifacts(taskId)
126
+ : await dependencies.surface.artifact(taskId, path, commit);
127
+ sendJson(response, 200, result, false);
128
+ }
129
+ catch (error) {
130
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Artifact unavailable." }, false);
131
+ }
132
+ return;
133
+ }
103
134
  const globalControl = /^\/api\/roles\/([^/]+)\/control$/.exec(pathname);
104
135
  if (globalControl && dependencies.surface) {
105
136
  try {
@@ -236,18 +267,21 @@ async function handleHttpRequest(request, response, store, dependencies, token,
236
267
  else if (asset !== null) {
237
268
  sendAsset(response, asset, method === "HEAD");
238
269
  }
239
- else if (pathname === "/api/dashboard") {
270
+ else if (pathname === "/api/dashboard" || pathname === "/api/dashboard/sessions") {
240
271
  const query = new URL(request.url, "http://localhost").searchParams;
241
- const snapshot = buildWebTaskCatalog(store, parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
272
+ const options = parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
242
273
  ? (value === "true" ? ["--all"] : ["--invalid-all"])
243
- : [`--${key}`, value])));
274
+ : [`--${key}`, value]));
275
+ const snapshot = pathname.endsWith("/sessions")
276
+ ? buildWebPageSessions(store, options, now())
277
+ : buildWebTaskCatalog(store, options);
244
278
  sendJson(response, 200, snapshot, method === "HEAD");
245
279
  }
246
280
  else if (pathname.startsWith("/api/tasks/")) {
247
281
  const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
248
282
  const detail = taskId.length === 0 || taskId.includes("/")
249
283
  ? null
250
- : buildWebTaskDetail(store, taskId);
284
+ : buildWebTaskDetail(store, taskId, now());
251
285
  sendJson(response, detail === null ? 404 : 200, detail ?? { error: "Task not found." }, method === "HEAD");
252
286
  }
253
287
  else {
@@ -0,0 +1,165 @@
1
+ import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
2
+ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
3
+ import { projectRuntimeTaskEvents, classifyRuntimeHealth } from "../runtime/runtimeProjection.js";
4
+ import { DEFAULT_RUNTIME_HEALTH_POLICY } from "../runtime/runtimeHealthPolicy.js";
5
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
6
+ /** Read-only presentation of the selected native connections, never an AgentRun
7
+ * count. Foreground facts are fenced to the exact input; retained continuations
8
+ * stay attached to their original input and are not counted as extra Sessions.
9
+ * No live probe, timers, persistence or recovery actions are introduced here. */
10
+ export function projectWebSessions(input) {
11
+ const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
12
+ const sessions = [];
13
+ const seen = new Map();
14
+ const observations = input.events.map(event => ({ event, observation: runtimeObservationFromTaskEvent(event) }))
15
+ .filter(entry => entry.observation !== null);
16
+ for (const set of input.sessionSets) {
17
+ const session = set.sessions[set.activeAgentId];
18
+ const binding = set.providerBinding;
19
+ const conversation = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch && c.status === "current");
20
+ const nativeSessionId = session?.nativeSessionId ?? conversation?.conversationId ?? null;
21
+ const identity = JSON.stringify([binding?.providerNamespace ?? session?.adapterId,
22
+ binding?.accountScope ?? set.activeAgentId, nativeSessionId ?? set.owner.roleName]);
23
+ const prior = seen.get(identity);
24
+ if (prior !== undefined) {
25
+ if (sessions[prior].roleName !== set.owner.roleName)
26
+ sessions[prior] = {
27
+ ...sessions[prior], group: "unknown", reason: "Multiple Roles name this native Session; inspect ownership."
28
+ };
29
+ continue;
30
+ }
31
+ seen.set(identity, sessions.length);
32
+ const turn = binding?.run;
33
+ const row = {
34
+ taskId: input.taskId, roleName: set.owner.roleName, agentId: set.activeAgentId,
35
+ nativeSessionId, attemptId: turn?.attemptId ?? null, nativeTurnId: turn?.nativeTurnId ?? null,
36
+ runId: turn?.runId ?? null, group: "unknown", reason: "Current native input is not observable.",
37
+ layer: null, lastActivityAt: null, semanticProgressAt: input.semanticProgressAt ?? null,
38
+ sourceUpdatedAt: turn?.updatedAt ?? session?.updatedAt ?? set.updatedAt,
39
+ waitingReason: null, operations: [], background: []
40
+ };
41
+ if (!session || !binding || !conversation || conversation.conversationId !== session.nativeSessionId) {
42
+ sessions.push(row);
43
+ continue;
44
+ }
45
+ let driverId;
46
+ try {
47
+ driverId = builtinDriverIdForAdapter(session.adapterId);
48
+ }
49
+ catch {
50
+ sessions.push(row);
51
+ continue;
52
+ }
53
+ const fence = { taskId: input.taskId, roleName: set.owner.roleName,
54
+ agentId: session.agentId, driverId, nativeSessionId: session.nativeSessionId };
55
+ const sessionFacts = observations.filter(({ observation }) => Object.entries(fence).every(([key, value]) => observation.fence[key] === value));
56
+ // Each Run's continuation fold has its own authority. Foreground operations
57
+ // from older inputs must not make the current input look active.
58
+ const groups = new Map();
59
+ for (const { event, observation } of sessionFacts) {
60
+ const runId = observation.fence.runId;
61
+ const group = groups.get(runId) ?? [];
62
+ group.push(event);
63
+ groups.set(runId, group);
64
+ }
65
+ const background = [...groups].flatMap(([runId, events]) => {
66
+ const projection = projectRuntimeTaskEvents({ ...fence, ...(runId ? { runId } : {}) }, session.createdAt, events);
67
+ const children = Object.entries(projection.continuations)
68
+ .filter(([, c]) => c.execution !== "quiescent" || c.identityConflict)
69
+ .map(([id, c]) => ({ id: `${runId ?? "native"}/${id}`, execution: c.execution }));
70
+ return [...children, ...Object.entries(projection.operations)
71
+ .filter(([, operation]) => operation.kind === "subagent")
72
+ .map(([id]) => ({ id: `${runId ?? "native"}/operation:${id}`, execution: "unsettled" }))];
73
+ });
74
+ if (!turn) {
75
+ sessions.push({ ...row, background, ...(background.length ? {
76
+ group: "background", reason: "Retained background execution is not settled."
77
+ } : {}) });
78
+ continue;
79
+ }
80
+ const exactEvents = sessionFacts.filter(({ observation }) => {
81
+ const observed = observation;
82
+ const f = observed.fence;
83
+ if (f.continuationId !== undefined || f.runId !== turn.runId)
84
+ return false;
85
+ if (f.receiptId === undefined && f.nativeTurnId === undefined
86
+ && ["host.observed", "session.ended", "session.failed", "observer.health"].includes(observed.kind)) {
87
+ return observed.receivedAt >= turn.submittedAt;
88
+ }
89
+ if (f.receiptId !== undefined && f.receiptId !== turn.attemptId)
90
+ return false;
91
+ if (f.nativeTurnId !== undefined && f.nativeTurnId !== turn.nativeTurnId)
92
+ return false;
93
+ return f.receiptId === turn.attemptId
94
+ || (turn.nativeTurnId !== undefined && f.nativeTurnId === turn.nativeTurnId);
95
+ }).map(entry => entry.event);
96
+ const projection = projectRuntimeTaskEvents({
97
+ ...fence, ...(turn.runId ? { runId: turn.runId } : {}),
98
+ receiptId: turn.attemptId, ...(turn.nativeTurnId ? { nativeTurnId: turn.nativeTurnId } : {})
99
+ }, turn.submittedAt, exactEvents);
100
+ const health = classifyRuntimeHealth({ projection, now: input.now, policy,
101
+ semanticProgressAt: input.semanticProgressAt ?? turn.submittedAt });
102
+ const retry = providerRetryProjection(binding);
103
+ let group;
104
+ let reason = health.reason;
105
+ if (turn.status === "delivery-unknown") {
106
+ group = "unknown";
107
+ reason = "Original input delivery is unknown; do not replay it.";
108
+ }
109
+ else if (session.status === "ended") {
110
+ group = background.length ? "background" : "stopped";
111
+ reason = "The selected Session ended; Task acceptance and retained background execution are separate.";
112
+ }
113
+ else if (retry?.status === "waiting") {
114
+ group = "waiting";
115
+ reason = `Controller-owned Provider retry waiting (${retry.attempts}/${retry.limit}); next eligible ${retry.nextEligibleAt}. Existing work is preserved.`;
116
+ }
117
+ else if (["failed", "cancelled", "rejected"].includes(turn.status)) {
118
+ group = background.length ? "background" : "stopped";
119
+ reason = turn.terminalReason ?? "The selected native execution stopped; Task acceptance is separate.";
120
+ }
121
+ else if (turn.status === "completed") {
122
+ group = background.length ? "background" : "idle";
123
+ reason = background.length ? "Main Turn ended; background execution remains unsettled."
124
+ : "The native Turn ended; this is not Task completion.";
125
+ }
126
+ else if (turn.status !== "accepted") {
127
+ group = "waiting";
128
+ reason = "Waiting for native input acceptance.";
129
+ }
130
+ else if (projection.run === "waiting") {
131
+ group = "waiting";
132
+ }
133
+ else if (["broken", "stopped"].includes(health.layer)) {
134
+ group = "stopped";
135
+ }
136
+ else if (["completed", "failed", "cancelled"].includes(projection.run)) {
137
+ group = "unknown";
138
+ reason = "Native terminal and current input binding disagree; inspect the exact original input.";
139
+ }
140
+ else if (projection.observer.status === "unavailable") {
141
+ group = "unknown";
142
+ }
143
+ else if (health.layer === "diagnostic-needed") {
144
+ group = "diagnostic";
145
+ }
146
+ else if (health.runtimeIdleMs < policy.quietAfterMs && projection.activity.kind !== "provider") {
147
+ group = "active";
148
+ }
149
+ else {
150
+ group = "quiet";
151
+ reason = "Accepted input has no recent structured activity; silence is not a proven deadlock.";
152
+ }
153
+ sessions.push({ ...row, group, reason, layer: health.layer, background,
154
+ lastActivityAt: projection.lastRuntimeActivityAt ?? null,
155
+ waitingReason: projection.waitingReason ?? null, operations: health.activeOperations });
156
+ }
157
+ const counts = {
158
+ active: 0, waiting: 0, quiet: 0, diagnostic: 0, unknown: 0, stopped: 0, idle: 0, background: 0
159
+ };
160
+ for (const session of sessions)
161
+ counts[session.group]++;
162
+ return { scope: "task-selected-native-sessions", taskId: input.taskId,
163
+ readAt: input.now.toISOString(), counts, sessions,
164
+ coverage: "Recorded selected Sessions only; not a live process or historical resource inventory." };
165
+ }
@@ -1,4 +1,5 @@
1
1
  import { providerRetryProjection } from "../runtime/providerRetry.js";
2
+ import { projectWebSessions } from "./webSessions.js";
2
3
  import { readTaskCatalog } from "../context/taskCatalog.js";
3
4
  import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
4
5
  import { retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
@@ -14,6 +15,23 @@ import { projectTaskRemoteDelivery } from "../task/remoteDelivery.js";
14
15
  export function buildWebTaskCatalog(store, options) {
15
16
  return store.transaction(reader => readTaskCatalog(reader, options));
16
17
  }
18
+ /** Explicit optional read of just one visible catalog page. Never part of the
19
+ * compact discovery contract, and never an unfiltered global Session count. */
20
+ export function buildWebPageSessions(store, options, now) {
21
+ return store.transaction(reader => {
22
+ const page = readTaskCatalog(reader, options);
23
+ const tasks = page.tasks.map(task => ({
24
+ title: task.title,
25
+ ...projectWebSessions({ taskId: task.id, sessionSets: reader.listRoleSessionSets(task.id),
26
+ events: reader.listEvents(task.id), now,
27
+ ...(reader.getTaskBrief(task.id)?.updatedAt === undefined ? {} : {
28
+ semanticProgressAt: reader.getTaskBrief(task.id).updatedAt
29
+ }),
30
+ policy: resolveRuntimeHealth(reader.getConfig().runtimeHealth) })
31
+ }));
32
+ return { scope: "current-catalog-page", readAt: now.toISOString(), tasks };
33
+ });
34
+ }
17
35
  export function buildWebTaskDetail(store, taskId, now = new Date()) {
18
36
  return store.transaction((reader) => {
19
37
  const task = reader.getTask(taskId);
@@ -90,6 +108,11 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
90
108
  ...(projectNames.length === 0 ? {} : { projectNames })
91
109
  },
92
110
  execution,
111
+ sessions: projectWebSessions({ taskId, sessionSets: roleSessionSets, events, now,
112
+ ...(reader.getTaskBrief(taskId)?.updatedAt === undefined ? {} : {
113
+ semanticProgressAt: reader.getTaskBrief(taskId).updatedAt
114
+ }),
115
+ policy: resolveRuntimeHealth(reader.getConfig().runtimeHealth) }),
93
116
  remoteDelivery,
94
117
  observability: execution.observability,
95
118
  brief: reader.getTaskBrief(taskId),
@@ -1,3 +1,5 @@
1
+ import { readArtifactCapability } from "../artifacts/artifactCapability.js";
2
+ import { openTaskArtifactRepository } from "../artifacts/taskArtifactRepository.js";
1
3
  import { applyGlobalInputControl } from "../commands/globalRoleCommands.js";
2
4
  import { applyTaskInputControl, sendTaskMessageCommand, updateTaskMetadataCommand } from "../commands/taskCommands.js";
3
5
  import { runTaskInputCommand } from "../commands/taskInputCommands.js";
@@ -37,6 +39,27 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
37
39
  ? { kind: "role", taskId, roleName: "leader" } : { kind: "task", taskId });
38
40
  };
39
41
  return {
42
+ evidence: (taskId) => store.transaction(reader => {
43
+ if (reader.getTask(taskId) === null)
44
+ throw new WebRequestRejected("Task not found.");
45
+ return { taskId, integrations: reader.listIntegrationAttempts(taskId),
46
+ reviews: reader.listReviewRounds(taskId), workspaces: reader.listManagedWorkspaces(taskId) };
47
+ }),
48
+ artifacts: async (taskId) => {
49
+ if (store.getTask(taskId) === null)
50
+ throw new WebRequestRejected("Task not found.");
51
+ const repo = openTaskArtifactRepository(store.rootDirectory(), taskId);
52
+ const commit = await repo.head();
53
+ const entries = commit === null ? [] : await repo.list(commit);
54
+ return { taskId, commit, entries };
55
+ },
56
+ artifact: async (taskId, relativePath, commit) => {
57
+ if (store.getTask(taskId) === null)
58
+ throw new WebRequestRejected("Task not found.");
59
+ // The browser always names a fixed revision. Never substitute HEAD after
60
+ // a missing object, and never execute/render artifact HTML as an app.
61
+ return readArtifactCapability(store.rootDirectory(), taskId, { relativePath, commit });
62
+ },
40
63
  globalState: (roleName) => {
41
64
  const role = store.getGlobalRole(roleName);
42
65
  if (role === null)