@zq-silk/yui 2.1.0 → 2.3.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.
Files changed (135) hide show
  1. package/README.md +8 -2
  2. package/dist/cli/commandCatalog.js +32 -6
  3. package/dist/cli/commandDiscovery.js +4 -1
  4. package/dist/cli/interactionPolicy.js +4 -0
  5. package/dist/cli.js +23 -3078
  6. package/dist/commands/taskCommands.js +84 -28
  7. package/dist/commands/taskIntegrationCommands.js +8 -17
  8. package/dist/commands/taskUpstreamCommands.js +3 -1
  9. package/dist/context/runContextPack.js +132 -5
  10. package/dist/context/sourceRunContext.js +4 -2
  11. package/dist/context/taskContext.js +6 -1
  12. package/dist/controlPlaneCli.js +3099 -0
  13. package/dist/controller/clientRuntime.js +3 -2
  14. package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
  15. package/dist/controller/jobControl.js +21 -0
  16. package/dist/executor/agentExecutor.js +1 -1
  17. package/dist/executor/effectiveLaunch.js +18 -5
  18. package/dist/executor/fileRoleLaunchPlanner.js +15 -19
  19. package/dist/integration/gitIntegrationService.js +58 -82
  20. package/dist/job/jobCleanupInspection.js +67 -0
  21. package/dist/message/messageContinuation.js +7 -2
  22. package/dist/nativeAgent/agent.js +176 -74
  23. package/dist/nativeAgent/cliDemo.js +37 -0
  24. package/dist/nativeAgent/codingTools.js +11 -0
  25. package/dist/nativeAgent/commandTool.js +215 -0
  26. package/dist/nativeAgent/compactionDemo.js +158 -0
  27. package/dist/nativeAgent/composition.js +71 -0
  28. package/dist/nativeAgent/context/budget.js +44 -0
  29. package/dist/nativeAgent/context/index.js +339 -0
  30. package/dist/nativeAgent/context/providerCompressor.js +103 -0
  31. package/dist/nativeAgent/demo.js +12 -1
  32. package/dist/nativeAgent/evaluation/cases.js +38 -0
  33. package/dist/nativeAgent/evaluation/checks.js +91 -0
  34. package/dist/nativeAgent/evaluation/demo.js +19 -0
  35. package/dist/nativeAgent/evaluation/files.js +54 -0
  36. package/dist/nativeAgent/evaluation/fixture.js +36 -0
  37. package/dist/nativeAgent/evaluation/index.js +239 -0
  38. package/dist/nativeAgent/executionOwner.js +209 -0
  39. package/dist/nativeAgent/filePatterns.js +170 -0
  40. package/dist/nativeAgent/fileToolsSupport.js +202 -0
  41. package/dist/nativeAgent/index.js +13 -0
  42. package/dist/nativeAgent/interaction/cli.js +358 -0
  43. package/dist/nativeAgent/interaction/contracts.js +1 -0
  44. package/dist/nativeAgent/interaction/index.js +3 -0
  45. package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
  46. package/dist/nativeAgent/interaction/renderer.js +34 -0
  47. package/dist/nativeAgent/localSafety.js +258 -0
  48. package/dist/nativeAgent/model/anthropicMessages.js +204 -0
  49. package/dist/nativeAgent/model/chatCompletions.js +210 -0
  50. package/dist/nativeAgent/model/errors.js +47 -0
  51. package/dist/nativeAgent/model/gateway.js +423 -0
  52. package/dist/nativeAgent/model/index.js +7 -0
  53. package/dist/nativeAgent/model/observationAdapter.js +21 -0
  54. package/dist/nativeAgent/model/protocols.js +19 -0
  55. package/dist/nativeAgent/model/responses.js +263 -0
  56. package/dist/nativeAgent/model/types.js +1 -0
  57. package/dist/nativeAgent/model/wire.js +73 -0
  58. package/dist/nativeAgent/observability/index.js +220 -0
  59. package/dist/nativeAgent/product/catalog.js +82 -0
  60. package/dist/nativeAgent/product/config.js +295 -0
  61. package/dist/nativeAgent/product/facts.js +30 -0
  62. package/dist/nativeAgent/product/index.js +62 -0
  63. package/dist/nativeAgent/product/location.js +44 -0
  64. package/dist/nativeAgent/product/runtime.js +276 -0
  65. package/dist/nativeAgent/product/storage.js +49 -0
  66. package/dist/nativeAgent/product/tools.js +47 -0
  67. package/dist/nativeAgent/product/transport.js +54 -0
  68. package/dist/nativeAgent/projectGuidance/index.js +425 -0
  69. package/dist/nativeAgent/searchTools.js +305 -0
  70. package/dist/nativeAgent/session/backends.js +293 -0
  71. package/dist/nativeAgent/session/catalog.js +97 -0
  72. package/dist/nativeAgent/session/catalogDemo.js +87 -0
  73. package/dist/nativeAgent/session/contracts.js +1 -0
  74. package/dist/nativeAgent/session/format.js +269 -0
  75. package/dist/nativeAgent/session/index.js +5 -0
  76. package/dist/nativeAgent/session/location.js +36 -0
  77. package/dist/nativeAgent/session/sqliteFormat.js +134 -0
  78. package/dist/nativeAgent/session/store.js +248 -0
  79. package/dist/nativeAgent/textTools.js +270 -133
  80. package/dist/nativeAgent/toolManager/executor.js +290 -0
  81. package/dist/nativeAgent/toolManager/index.js +4 -0
  82. package/dist/nativeAgent/validation.js +2 -2
  83. package/dist/repository/gitWorkspace.js +41 -12
  84. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  85. package/dist/repository/taskWorkspacePreparer.js +39 -43
  86. package/dist/repository/workspaceCleanupInspection.js +88 -7
  87. package/dist/resources/resourceRegistrar.js +16 -10
  88. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  89. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  90. package/dist/task/archiveDiagnostics.js +26 -8
  91. package/dist/task/archivePreflight.js +26 -9
  92. package/dist/task/leaderArchive.js +9 -15
  93. package/dist/task/taskAuthority.js +56 -0
  94. package/dist/web/assets/client/api.js +8 -0
  95. package/dist/web/assets/client/app.js +74 -9
  96. package/dist/web/assets/client/components.js +95 -36
  97. package/dist/web/assets/client/detail.js +110 -64
  98. package/dist/web/assets/client/dock.js +95 -11
  99. package/dist/web/assets/client/evidence.js +286 -94
  100. package/dist/web/assets/client/i18n.js +182 -62
  101. package/dist/web/assets/client/overview.js +9 -10
  102. package/dist/web/assets/client/records.js +142 -7
  103. package/dist/web/assets/client/sections.js +234 -198
  104. package/dist/web/assets/client/sidebar.js +31 -2
  105. package/dist/web/assets/icons.js +4 -0
  106. package/dist/web/assets/shell.js +2 -1
  107. package/dist/web/assets/styles/components.js +64 -15
  108. package/dist/web/assets/styles/layout.js +11 -4
  109. package/dist/web/assets/styles/responsive.js +16 -6
  110. package/dist/web/assets/styles/tokens.js +1 -1
  111. package/dist/web/assets/styles/views.js +62 -42
  112. package/dist/web/webServer.js +12 -1
  113. package/dist/web/webTaskSurface.js +8 -1
  114. package/docs/agent-result-consumption.md +16 -0
  115. package/docs/agent-result-consumption.zh-CN.md +13 -0
  116. package/docs/examples/agent-offline.mjs +194 -0
  117. package/docs/native-agent.md +283 -0
  118. package/docs/observability/README.md +13 -3
  119. package/docs/observability/README.zh-CN.md +10 -3
  120. package/docs/release-workflow.md +72 -9
  121. package/docs/release-workflow.zh-CN.md +55 -6
  122. package/docs/roles-and-configuration.md +32 -0
  123. package/docs/roles-and-configuration.zh-CN.md +26 -0
  124. package/docs/task-delivery.md +36 -8
  125. package/docs/task-delivery.zh-CN.md +24 -7
  126. package/i18n/README.zh-CN.md +3 -2
  127. package/package.json +1 -1
  128. package/skills/yui-leader/SKILL.md +9 -0
  129. package/skills/yui-leader/references/execution.md +2 -1
  130. package/skills/yui-leader/references/integration.md +20 -0
  131. package/skills/yui-operator/SKILL.md +4 -0
  132. package/skills/yui-reviewer/SKILL.md +8 -0
  133. package/skills/yui-runtime/SKILL.md +11 -0
  134. package/skills/yui-runtime/references/publication.md +20 -4
  135. package/skills/yui-worker/SKILL.md +3 -0
@@ -8,11 +8,15 @@ import {
8
8
  badge, statusBadge, label, tone, card, disclosure, richText, bulletList, emptyState,
9
9
  note, button, chip, mono, inputCard, kv, dot, timeTag
10
10
  } from "/assets/js/components.js";
11
- import { entriesOf, valuesOf, recordCard } from "/assets/js/records.js";
12
- import { renderWork, renderExecution, renderHistory, renderDetails } from "/assets/js/sections.js";
11
+ import { entriesOf, valuesOf, totalOf, recordCard, readExact } from "/assets/js/records.js";
12
+ import { renderRuntime, renderRecords } from "/assets/js/sections.js";
13
+ import { renderDelivery } from "/assets/js/evidence.js";
13
14
 
14
- export const TABS = ["overview", "work", "execution", "history", "details"];
15
- const TAB_ICONS = { overview: "target", work: "layers", execution: "pulse", history: "history", details: "dots" };
15
+ // Overview: where it stands. Delivery: what it produced. Runtime: what is
16
+ // live now. Records: what happened. Retired section names stay addressable.
17
+ export const TABS = ["overview", "delivery", "runtime", "records"];
18
+ export const TAB_ALIASES = { work: "delivery", execution: "runtime", history: "records", details: "records" };
19
+ const TAB_ICONS = { overview: "target", delivery: "package", runtime: "pulse", records: "history" };
16
20
  const SESSION_GROUPS = ["active", "waiting", "background", "quiet", "diagnostic", "stopped", "unknown", "idle"];
17
21
 
18
22
  export function renderTaskDetail(container, data, t, locale, ctx) {
@@ -20,6 +24,7 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
20
24
  const task = data.task;
21
25
  const core = data.core;
22
26
  container.dataset.taskId = task.id;
27
+ data.viewState.coreKey = core.coreCursor;
23
28
  const page = h("div.page.task-page");
24
29
 
25
30
  // --- Header ----------------------------------------------------------------
@@ -48,10 +53,12 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
48
53
  (task.projectBindings || []).length ? h("span.meta-item", null, icon("layers", "icon-sm"),
49
54
  task.projectBindings.map(function (binding) { return binding.projectId; }).join(", ")) : null);
50
55
 
56
+ // Tab counts are current totals from the same bounded read, not samples:
57
+ // open questions, open work items and open execution records.
51
58
  const counts = {
52
- work: entriesOf(core, "work-item").length,
53
- execution: entriesOf(core, "run").length,
54
- history: entriesOf(core, "task-decision").length + entriesOf(core, "task-milestone").length + entriesOf(core, "task-message").length
59
+ overview: core.attention.openInputs.count,
60
+ delivery: totalOf(core, "work-item"),
61
+ runtime: totalOf(core, "run")
55
62
  };
56
63
  const tabs = h("nav.tabs", { role: "tablist", "aria-label": t("tabs.label") }, TABS.map(function (tab, index) {
57
64
  return h("button.tab", {
@@ -63,7 +70,7 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
63
70
  dataset: { tab: tab },
64
71
  onclick: function () { ctx.onTab(tab); }
65
72
  }, icon(TAB_ICONS[tab], "icon-sm"), h("span", null, t("tabs." + tab)),
66
- counts[tab] ? h("span.count", null, String(counts[tab])) : null);
73
+ counts[tab] ? h("span.count" + (tab === "overview" ? ".tone-warn" : ""), null, String(counts[tab])) : null);
67
74
  }));
68
75
  tabs.addEventListener("keydown", function (event) {
69
76
  if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
@@ -87,29 +94,38 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
87
94
  page.append(panels[tab]);
88
95
  });
89
96
  renderOverviewPanel(panels.overview, data, t, locale, ctx);
90
- renderWork(panels.work, data, t, locale, ctx);
91
- renderExecution(panels.execution, data, t, locale, ctx);
92
- renderHistory(panels.history, data, t, locale, ctx);
93
- renderDetails(panels.details, data, t, locale, ctx);
97
+ renderDelivery(panels.delivery, data, t, locale, ctx);
98
+ renderRuntime(panels.runtime, data, t, locale, ctx);
99
+ renderRecords(panels.records, data, t, locale, ctx);
94
100
  container.append(page);
95
101
  updateObservation(container, data, t, locale, ctx);
102
+ const active = panels[ctx.activeTab];
103
+ if (active && active.onShow) active.onShow();
96
104
  }
97
105
 
106
+ // A panel may read on first show (evidence, record pages); it keeps those
107
+ // reads in view state, so showing it again does not repeat them.
98
108
  export function selectTab(container, tab) {
99
109
  container.querySelectorAll(".tab").forEach(function (button) {
100
110
  const active = button.dataset.tab === tab;
101
111
  button.setAttribute("aria-selected", String(active));
102
112
  button.tabIndex = active ? 0 : -1;
103
113
  });
104
- container.querySelectorAll(".tab-panel").forEach(function (panel) { panel.hidden = panel.id !== "panel-" + tab; });
114
+ container.querySelectorAll(".tab-panel").forEach(function (panel) {
115
+ panel.hidden = panel.id !== "panel-" + tab;
116
+ if (!panel.hidden && panel.onShow) panel.onShow();
117
+ });
105
118
  }
106
119
 
120
+ // One reading column, top to bottom: where the Task stands now, what needs the
121
+ // user, what it has reported, its goal and its current decisions.
107
122
  function renderOverviewPanel(panel, data, t, locale, ctx) {
108
123
  const task = data.task;
109
124
  const core = data.core;
110
- // Now: the derived execution status (observation) plus the open questions.
125
+ // Now: the derived execution status (observation), the open questions and
126
+ // who acts next. drawNow() tints the banner with the status tone.
111
127
  const now = card({ className: "now-card" });
112
- now.body.append(h("div", { dataset: { slot: "now" } }));
128
+ now.body.append(h("div", { dataset: { slot: "now" } }), h("p.next-line", { dataset: { slot: "next" } }));
113
129
  panel.append(now);
114
130
 
115
131
  // Needs you: open InputRequests with their answer controls.
@@ -129,15 +145,28 @@ function renderOverviewPanel(panel, data, t, locale, ctx) {
129
145
  panel.append(needs);
130
146
  }
131
147
 
132
- const grid = h("div.card-grid");
133
- grid.append(progressCard(data, t, locale, ctx));
134
- const sessions = card({ title: t("sessions.title"), icon: "terminal", hint: t("sessions.hint") });
135
- sessions.body.append(h("div", { dataset: { slot: "sessions" } }));
136
- grid.append(sessions);
137
- grid.append(goalCard(data, t, ctx));
138
- grid.append(decisionsCard(data, t, locale, ctx));
139
- panel.append(grid);
148
+ const progress = progressCard(data, t, locale, ctx);
149
+ const goal = goalCard(data, t, ctx);
150
+ panel.append(progress);
140
151
  if (task.status === "draft") panel.append(planningCard(data, t, locale));
152
+ panel.append(goal, decisionsCard(data, t, locale, ctx));
153
+ // The Brief is the Overview's main source; when the bounded read withheld
154
+ // its value, read it exactly once per digest and redraw both cards.
155
+ const briefEntry = entriesOf(core, "task-brief")[0];
156
+ if (briefEntry && briefEntry.omitted) {
157
+ const view = data.viewState;
158
+ view.exact = view.exact || {};
159
+ readExact(briefEntry, task.id, ctx, view.exact).then(function (result) {
160
+ if (!progress.isConnected) return;
161
+ const exact = { ...data, briefValue: result.value };
162
+ progress.replaceWith(progressCard(exact, t, locale, ctx));
163
+ goal.replaceWith(goalCard(exact, t, ctx));
164
+ }, function () {});
165
+ }
166
+ }
167
+
168
+ function briefOf(data) {
169
+ return data.briefValue || valuesOf(data.core, "task-brief")[0];
141
170
  }
142
171
 
143
172
  function progressCard(data, t, locale, ctx) {
@@ -148,20 +177,19 @@ function progressCard(data, t, locale, ctx) {
148
177
  if (task.retirementSummary) element.body.append(richText(t("progress.retirement"), task.retirementSummary, t, { className: "callout" }));
149
178
  if (task.archiveSummary || task.archiveReason) element.body.append(richText(t("progress.archive"), [task.archiveSummary, task.archiveReason].filter(Boolean).join("\n\n"), t, { className: "callout" }));
150
179
  const briefEntry = entriesOf(data.core, "task-brief")[0];
151
- const brief = valuesOf(data.core, "task-brief")[0];
180
+ const brief = briefOf(data);
152
181
  if (brief) {
153
182
  element.body.append(richText(t(working ? "progress.focus" : "progress.lastFocus"), brief.currentFocus, t));
154
183
  element.body.append(richText(t(working ? "progress.report" : "progress.lastReport"), brief.leaderSummary, t, { muted: true }));
155
184
  element.body.append(h("p.faint.small", null, t("progress.briefUpdated") + " " + formatDateTime(brief.updatedAt, locale)));
156
185
  } else if (briefEntry) element.body.append(recordCard(briefEntry, task.id, t, ctx, { compact: true }));
157
186
  else element.body.append(emptyState(t("progress.none")));
158
- element.body.append(h("p.next-line", { dataset: { slot: "next" } }));
159
187
  return element;
160
188
  }
161
189
 
162
190
  function goalCard(data, t, ctx) {
163
191
  const task = data.task;
164
- const brief = valuesOf(data.core, "task-brief")[0];
192
+ const brief = briefOf(data);
165
193
  const element = card({ title: t("goal.title"), icon: "target" });
166
194
  if (brief && brief.objective) element.body.append(richText(null, brief.objective, t));
167
195
  if (brief && brief.boundaries && brief.boundaries.length) {
@@ -184,7 +212,8 @@ function goalCard(data, t, ctx) {
184
212
  function decisionsCard(data, t, locale, ctx) {
185
213
  const entries = entriesOf(data.core, "task-decision");
186
214
  const active = entries.filter(function (entry) { return !entry.omitted && entry.value.status === "active"; });
187
- const element = card({ title: t("decisions.title"), icon: "check", count: active.length || null, hint: t("decisions.hint") });
215
+ const total = totalOf(data.core, "task-decision");
216
+ const element = card({ id: "detail-decisions", title: t("decisions.title"), icon: "check", count: total || null, hint: t("decisions.hint") });
188
217
  active.slice(0, 3).forEach(function (entry) {
189
218
  const value = entry.value;
190
219
  element.body.append(h("article.decision", null,
@@ -192,9 +221,10 @@ function decisionsCard(data, t, locale, ctx) {
192
221
  value.rationale ? richText(null, value.rationale, t, { muted: true, threshold: 260 }) : null));
193
222
  });
194
223
  if (!active.length) element.body.append(emptyState(t("decisions.none")));
195
- if (active.length > 3 || data.core.omitted.records || entries.some(function (entry) { return entry.omitted; })) {
196
- element.body.append(button(t("decisions.more"), { variant: "link", icon: "chevron", onClick: function () { ctx.onTab("history"); } }));
197
- }
224
+ element.body.append(button(t("decisions.more"), { variant: "link", icon: "chevron", onClick: function () {
225
+ data.viewState.recordsFilter = "decision";
226
+ ctx.onTab("records");
227
+ } }));
198
228
  return element;
199
229
  }
200
230
 
@@ -240,12 +270,7 @@ export function updateObservation(container, data, t, locale, ctx) {
240
270
  if (execution && !(execution.next.owner === "none" && execution.next.action === "none")) next.append(icon("flag", "icon-sm"), h("span", null, t("now.nextOwner") + " "),
241
271
  h("strong", null, label(t, "exec.owner", execution.next.owner)), h("span", null, " · " + label(t, "exec.action", execution.next.action)));
242
272
  }
243
- const sessions = container.querySelector('[data-slot="sessions"]');
244
- if (sessions && !sessions.contains(document.activeElement)) {
245
- const open = sessions.querySelector("details") && sessions.querySelector("details").open;
246
- drawSessions(sessions, available ? data.runtime.sessions : null, t, locale);
247
- if (open && sessions.querySelector("details")) sessions.querySelector("details").open = true;
248
- }
273
+ drawSessions(container, available ? data.runtime.sessions : null, t, locale);
249
274
  const raw = container.querySelector('[data-slot="runtime-raw"]');
250
275
  if (raw) raw.textContent = available ? JSON.stringify({ roles: data.runtime.roles, runtimeHealth: data.runtime.runtimeHealth }, null, 2) : "";
251
276
  const rawStatus = container.querySelector('[data-slot="runtime-status"]');
@@ -257,9 +282,16 @@ export function updateObservation(container, data, t, locale, ctx) {
257
282
  });
258
283
  }
259
284
 
285
+ const BANNER_TONES = ["tone-info", "tone-ok", "tone-warn", "tone-bad", "tone-idle", "tone-accent"];
286
+
260
287
  function drawNow(slot, data, execution, t, locale) {
261
288
  clear(slot);
262
289
  const inputCount = data.core.attention.openInputs.count;
290
+ const banner = slot.closest(".now-card");
291
+ if (banner) {
292
+ banner.classList.remove.apply(banner.classList, BANNER_TONES);
293
+ banner.classList.add("tone-" + (execution ? tone("exec", execution.status) : data.runtimeStatus === "unavailable" ? "bad" : "idle"));
294
+ }
263
295
  if (!execution) {
264
296
  slot.append(h("div.now-head", null,
265
297
  badge(t("observation." + data.runtimeStatus), data.runtimeStatus === "unavailable" ? "bad" : "idle", { dot: true }),
@@ -300,34 +332,48 @@ function drawNow(slot, data, execution, t, locale) {
300
332
  else slot.append(h("p.now-note", null, inputCount ? t("now.inputs").replace("{count}", String(inputCount)) : t("now.clear")));
301
333
  }
302
334
 
303
- function drawSessions(slot, observation, t, locale) {
304
- clear(slot);
305
- if (!observation) { slot.append(emptyState(t("sessions.unavailable"))); return; }
306
- const chips = SESSION_GROUPS.filter(function (group) { return observation.counts[group]; }).map(function (group) {
307
- return badge(observation.counts[group] + " " + t("session." + group), tone("session", group), { dot: true });
308
- });
309
- if (chips.length) slot.append(h("div.chip-row", null, chips));
310
- if (!observation.sessions.length) { slot.append(note(t("sessions.none"))); return; }
311
- slot.append(h("ul.session-list", null, observation.sessions.map(function (session) {
312
- return h("li.session-row", null,
313
- h("div.session-row-head", null, dot(tone("session", session.group)), h("strong", null, session.roleName),
314
- h("span.faint", null, t("session." + session.group)), h("span.spacer"),
315
- h("span.faint.small", null, session.lastActivityAt ? relativeTime(session.lastActivityAt, locale, t) : t("sessions.unobserved"))),
316
- h("p.small", null, session.reason),
317
- session.waitingReason ? h("p.small", null, t("sessions.waitingFor") + " " + session.waitingReason) : null);
318
- })));
319
- const facts = disclosure(t("sessions.facts"), "sessions");
320
- observation.sessions.forEach(function (session) {
321
- facts.body.append(kv([
322
- [t("sessions.role"), session.roleName],
323
- [t("sessions.lastActivity"), session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : t("sessions.unobserved")],
324
- [t("sessions.inputUpdated"), formatDateTime(session.sourceUpdatedAt, locale)],
325
- [t("sessions.operations"), session.operations.length ? session.operations.join(", ") : null],
326
- [t("sessions.background"), session.background.length ? session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ") : null],
327
- [t("sessions.identity"), mono([session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / "))]
328
- ]));
335
+ // Native Session observation, drawn into the Runtime tab: one line per Role,
336
+ // the group counts beside the Roles heading, and the timestamps under
337
+ // Diagnostics. Activity is never delivery progress.
338
+ function drawSessions(container, observation, t, locale) {
339
+ const sessions = observation ? observation.sessions : [];
340
+ container.querySelectorAll("[data-role-session]").forEach(function (slot) {
341
+ clear(slot);
342
+ const session = sessions.find(function (item) { return item.roleName === slot.dataset.roleSession; });
343
+ if (!session) {
344
+ slot.append(h("span.faint.small", null, observation ? t("session.noneRecorded") : t("sessions.unavailable")));
345
+ return;
346
+ }
347
+ slot.append(h("div.role-session-head", null, dot(tone("session", session.group)),
348
+ h("strong", null, t("session." + session.group)),
349
+ h("span.faint.small", null, session.lastActivityAt ? relativeTime(session.lastActivityAt, locale, t) : t("sessions.unobserved"))));
350
+ if (session.reason) slot.append(h("p.small", null, session.reason));
351
+ if (session.waitingReason) slot.append(h("p.small", null, t("sessions.waitingFor") + " " + session.waitingReason));
329
352
  });
330
- facts.body.append(h("p.faint.small", null, t("sessions.scope") + " " + formatDateTime(observation.readAt, locale)));
331
- slot.append(facts);
353
+ const counts = container.querySelector('[data-slot="session-counts"]');
354
+ if (counts) {
355
+ clear(counts);
356
+ if (observation) SESSION_GROUPS.forEach(function (group) {
357
+ if (observation.counts[group]) counts.append(badge(observation.counts[group] + " " + t("session." + group), tone("session", group), { dot: true }));
358
+ });
359
+ }
360
+ const facts = container.querySelector('[data-slot="session-facts"]');
361
+ if (facts) {
362
+ clear(facts);
363
+ if (!observation) return;
364
+ facts.append(h("h4.sub-head", null, t("sessions.facts")));
365
+ if (!sessions.length) facts.append(note(t("sessions.none")));
366
+ sessions.forEach(function (session) {
367
+ facts.append(kv([
368
+ [t("sessions.role"), session.roleName],
369
+ [t("sessions.lastActivity"), session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : t("sessions.unobserved")],
370
+ [t("sessions.inputUpdated"), formatDateTime(session.sourceUpdatedAt, locale)],
371
+ [t("sessions.operations"), session.operations.length ? session.operations.join(", ") : null],
372
+ [t("sessions.background"), session.background.length ? session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ") : null],
373
+ [t("sessions.identity"), mono([session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / "))]
374
+ ]));
375
+ });
376
+ facts.append(h("p.faint.small", null, t("sessions.scope") + " " + formatDateTime(observation.readAt, locale)));
377
+ }
332
378
  }
333
379
  `;
@@ -8,7 +8,7 @@ import { FitAddon } from "/assets/vendor/addon-fit.mjs";
8
8
  import { h, icon, clear } from "/assets/js/dom.js";
9
9
  import { formatShort } from "/assets/js/format.js";
10
10
  import { label, chip, emptyState, richText, dot } from "/assets/js/components.js";
11
- import { entriesOf, recordCard, recordReader } from "/assets/js/records.js";
11
+ import { entriesOf, totalOf, readExact, recordCard } from "/assets/js/records.js";
12
12
  import { messageComposer } from "/assets/js/forms.js";
13
13
  import { pageToken } from "/assets/js/api.js";
14
14
 
@@ -31,6 +31,7 @@ export function renderDiscussion(host, data, t, locale, actions) {
31
31
  feed = host.querySelector(".feed");
32
32
  }
33
33
  host.dataset.taskId = task.id;
34
+ latest.set(task.id, { data: data, t: t, locale: locale, actions: actions });
34
35
  if (!feed.querySelector('[data-reading="true"]')) drawFeed(feed, data, t, locale, actions);
35
36
  feed.scrollTop = atEnd ? feed.scrollHeight : previousScroll;
36
37
  if (!keepComposer) {
@@ -44,21 +45,92 @@ export function discussionHasUnsent(host) {
44
45
  return !!host.querySelector('[data-unsent="true"]');
45
46
  }
46
47
 
48
+ // The Context read carries the newest messages as summaries; each is read
49
+ // exactly once per digest (kept in the Task's view state) and drawn in full,
50
+ // so a message needs no separate raw-record reader.
51
+ // Older messages are listed on request, newest first, and read the same way.
52
+ const older = new Map();
53
+ const failedReads = new Set();
54
+ const latest = new Map();
55
+
56
+ function readKey(entry) { return entry.ref.refId + "@" + entry.ref.digest; }
57
+
58
+ function exactValue(entry, cache) {
59
+ if (!entry.omitted) return entry.value;
60
+ const hit = cache[entry.ref.store + "/" + entry.ref.refId];
61
+ return hit && hit.digest === entry.ref.digest && hit.result ? hit.result.value : null;
62
+ }
63
+
64
+ function loadOlder(data, actions, state, shownIds) {
65
+ state.pending = true;
66
+ state.error = null;
67
+ let rounds = 0;
68
+ function next() {
69
+ return actions.list(data.task.id, "task-message", { limit: 20, cursor: state.cursor }).then(function (page) {
70
+ const known = new Set(state.items.map(function (item) { return item.ref.refId; }));
71
+ const fresh = page.items.filter(function (item) { return !known.has(item.ref.refId) && !shownIds.has(item.ref.refId); });
72
+ state.items = state.items.concat(fresh);
73
+ state.cursor = page.nextCursor;
74
+ state.done = !page.nextCursor;
75
+ rounds += 1;
76
+ if (!fresh.length && page.nextCursor && rounds < 10) return next();
77
+ });
78
+ }
79
+ // A listing that moved invalidates its continuation; the next request
80
+ // starts again from the newest page and skips what is already shown.
81
+ return next().catch(function (error) { state.error = error.message; state.cursor = null; })
82
+ .then(function () { state.pending = false; });
83
+ }
84
+
47
85
  function drawFeed(feed, data, t, locale, actions) {
48
86
  clear(feed);
49
- const core = data.core;
50
- const entries = entriesOf(core, "task-message").slice().sort(function (a, b) {
51
- return Date.parse((a.value && a.value.createdAt) || 0) - Date.parse((b.value && b.value.createdAt) || 0);
87
+ const task = data.task;
88
+ const cache = data.viewState.exact = data.viewState.exact || {};
89
+ const contextEntries = entriesOf(data.core, "task-message");
90
+ const shownIds = new Set(contextEntries.map(function (entry) { return entry.ref.refId; }));
91
+ if (!older.has(task.id)) older.set(task.id, { items: [], cursor: null, done: false, pending: false, error: null });
92
+ const state = older.get(task.id);
93
+ const olderItems = state.items.filter(function (item) { return !shownIds.has(item.ref.refId); })
94
+ .sort(function (a, b) { return Date.parse(a.createdAt || 0) - Date.parse(b.createdAt || 0); });
95
+ // Context lists messages newest first; the feed reads oldest first.
96
+ const rows = olderItems.map(function (item) { return { entry: { ref: item.ref, summary: item.summary, omitted: true }, at: item.createdAt }; })
97
+ .concat(contextEntries.slice().reverse().map(function (entry) { return { entry: entry, at: null }; }));
98
+ const reads = [];
99
+ rows.forEach(function (row) {
100
+ row.value = exactValue(row.entry, cache);
101
+ if (!row.value && !failedReads.has(readKey(row.entry))) {
102
+ row.reading = true;
103
+ reads.push(readExact(row.entry, task.id, actions, cache).catch(function () { failedReads.add(readKey(row.entry)); }));
104
+ }
105
+ row.at = (row.value && row.value.createdAt) || row.at;
52
106
  });
53
- if (core.omitted.records) feed.append(h("p.feed-note", null, t("discussion.omitted")));
54
- if (!entries.length) {
107
+ if (rows.every(function (row) { return row.at; })) rows.sort(function (a, b) { return Date.parse(a.at) - Date.parse(b.at); });
108
+
109
+ const remaining = totalOf(data.core, "task-message") - shownIds.size - olderItems.length;
110
+ if (actions.list && remaining > 0 && !state.done) {
111
+ const more = h("button.show-more.feed-older", { type: "button", disabled: state.pending },
112
+ state.pending ? t("discussion.olderLoading") : t("discussion.older").replace("{count}", String(remaining)));
113
+ more.addEventListener("click", function () {
114
+ more.disabled = true;
115
+ more.textContent = t("discussion.olderLoading");
116
+ loadOlder(data, actions, state, shownIds).then(function () { redraw(feed, task.id, true); });
117
+ });
118
+ feed.append(more);
119
+ }
120
+ if (state.error) feed.append(h("p.feed-note", null, t("records.unavailable") + " " + state.error));
121
+ if (!rows.length) {
55
122
  feed.append(emptyState(t("discussion.empty"), "chat"));
56
123
  return;
57
124
  }
58
125
  let lastDay = "";
59
- entries.forEach(function (entry) {
60
- if (entry.omitted) { feed.append(recordCard(entry, data.task.id, t, actions, { compact: true })); return; }
61
- const value = entry.value;
126
+ rows.forEach(function (row) {
127
+ if (!row.value) {
128
+ const placeholder = recordCard(row.entry, task.id, t, actions, { compact: true });
129
+ if (row.reading) placeholder.dataset.reading = "true";
130
+ feed.append(placeholder);
131
+ return;
132
+ }
133
+ const value = row.value;
62
134
  const day = value.createdAt ? new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(new Date(value.createdAt)) : "";
63
135
  if (day && day !== lastDay) { feed.append(h("div.feed-day", null, h("span", null, day))); lastDay = day; }
64
136
  const type = (value.author && value.author.type) || "system";
@@ -69,12 +141,24 @@ function drawFeed(feed, data, t, locale, actions) {
69
141
  h("header.msg-head", null, h("strong", null, author),
70
142
  value.intent ? chip(label(t, "intent", value.intent)) : null,
71
143
  value.kind === "role-result" ? chip(label(t, "messageKind", value.kind)) : null,
72
- h("time", { dateTime: value.createdAt || "" }, formatShort(value.createdAt, locale))),
144
+ h("time", { dateTime: value.createdAt || "", title: row.entry.ref.refId }, formatShort(value.createdAt, locale))),
73
145
  h("div.msg-bubble", null, richText(null, value.body || "", t, { threshold: 900 })));
74
- main.append(recordReader(entry, data.task.id, t, actions));
75
146
  message.append(main);
76
147
  feed.append(message);
77
148
  });
149
+ if (reads.length) Promise.all(reads).then(function () { redraw(feed, task.id, false); });
150
+ }
151
+
152
+ // Redraw after a read without losing the reader's place: the bottom stays
153
+ // pinned, and prepended older messages keep the current message in view.
154
+ function redraw(feed, taskId, prepend) {
155
+ const host = feed.closest("[data-task-id]");
156
+ const current = latest.get(taskId);
157
+ if (!feed.isConnected || !host || host.dataset.taskId !== taskId || !current) return;
158
+ const atEnd = feed.scrollHeight - feed.clientHeight - feed.scrollTop < 40;
159
+ const fromBottom = feed.scrollHeight - feed.scrollTop;
160
+ drawFeed(feed, current.data, current.t, current.locale, current.actions);
161
+ feed.scrollTop = atEnd && !prepend ? feed.scrollHeight : feed.scrollHeight - fromBottom;
78
162
  }
79
163
 
80
164
  // --- Live native Session ----------------------------------------------------------