@zq-silk/yui 2.4.0 → 2.6.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 (58) hide show
  1. package/dist/cli/commandCatalog.js +4 -3
  2. package/dist/commands/taskCommands.js +48 -15
  3. package/dist/commands/taskContextCommand.js +3 -2
  4. package/dist/context/sessionBootstrapManifest.js +8 -2
  5. package/dist/context/taskContext.js +7 -4
  6. package/dist/controller/fileSchedulerStoreAdapter.js +7 -2
  7. package/dist/controller/globalInputDelivery.js +2 -4
  8. package/dist/controller/providerRetryDelivery.js +4 -2
  9. package/dist/executor/agentAdapter.js +3 -5
  10. package/dist/executor/fileRoleLaunchPlanner.js +2 -2
  11. package/dist/interaction/notificationPresentation.js +85 -0
  12. package/dist/interaction/operatorPresentation.js +1 -2
  13. package/dist/kernel/builtinCapabilities.js +2 -1
  14. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  15. package/dist/runtime/providerRetry.js +38 -0
  16. package/dist/runtime/taskSessions.js +240 -0
  17. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  18. package/dist/scheduler/taskExecutionProjection.js +78 -6
  19. package/dist/web/assets/assetManifest.js +2 -0
  20. package/dist/web/assets/client/app/taskView.js +1 -0
  21. package/dist/web/assets/client/domain/records.js +70 -2
  22. package/dist/web/assets/client/domain/runs.js +3 -2
  23. package/dist/web/assets/client/domain/vocab.js +1 -1
  24. package/dist/web/assets/client/i18n/en.js +17 -1
  25. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  26. package/dist/web/assets/client/lib/api.js +4 -1
  27. package/dist/web/assets/client/lib/theme.js +31 -10
  28. package/dist/web/assets/client/views/task/actions.js +38 -21
  29. package/dist/web/assets/client/views/task/observation.js +12 -3
  30. package/dist/web/assets/client/views/task/overview.js +7 -40
  31. package/dist/web/assets/client/views/task/runtime.js +71 -34
  32. package/dist/web/assets/shared/icons.js +2 -0
  33. package/dist/web/assets/shell/index.js +1 -0
  34. package/dist/web/assets/shell/settings.js +1 -0
  35. package/dist/web/assets/styles/views/task.js +14 -0
  36. package/dist/web/webConversation.js +2 -2
  37. package/dist/web/webServer.js +12 -0
  38. package/dist/web/webSessions.js +2 -165
  39. package/dist/web/webSnapshot.js +9 -5
  40. package/dist/web/webTaskActivity.js +54 -0
  41. package/dist/web/webTaskSurface.js +1 -0
  42. package/docs/cli-information-contract.md +1 -1
  43. package/docs/cli-information-contract.zh-CN.md +1 -1
  44. package/docs/managed-turn-and-session-runtime.md +22 -0
  45. package/docs/managed-turn-and-session-runtime.zh-CN.md +15 -0
  46. package/docs/observability/README.md +25 -1
  47. package/docs/observability/README.zh-CN.md +18 -1
  48. package/docs/provider-retry.md +11 -0
  49. package/docs/release-workflow.md +53 -0
  50. package/docs/release-workflow.zh-CN.md +40 -0
  51. package/package.json +1 -1
  52. package/skills/yui-leader/SKILL.md +5 -4
  53. package/skills/yui-leader/references/execution.md +2 -1
  54. package/skills/yui-operator/SKILL.md +3 -0
  55. package/skills/yui-reviewer/SKILL.md +4 -2
  56. package/skills/yui-runtime/SKILL.md +43 -2
  57. package/skills/yui-runtime/references/recovery.md +9 -0
  58. package/skills/yui-worker/SKILL.md +3 -2
@@ -0,0 +1,240 @@
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 projectTaskSessions(input) {
11
+ const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
12
+ const sessions = [];
13
+ const seen = new Map();
14
+ const selectedIdentities = new Set();
15
+ const observationIdentity = (fence) => JSON.stringify([fence.roleName, fence.agentId, fence.driverId, fence.nativeSessionId]);
16
+ const observations = input.events.map(event => ({ event, observation: runtimeObservationFromTaskEvent(event) }))
17
+ .filter(entry => entry.event.taskId === input.taskId && entry.observation?.fence.taskId === input.taskId);
18
+ for (const set of input.sessionSets) {
19
+ if (set.owner.scope !== "task" || set.owner.taskId !== input.taskId)
20
+ continue;
21
+ const session = set.sessions[set.activeAgentId];
22
+ const binding = set.providerBinding;
23
+ const conversation = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch && c.status === "current");
24
+ // Replacement can leave an empty set. That is absence, not uncertain
25
+ // execution; retained child facts are still discovered below.
26
+ if (!session && !conversation && !binding?.run)
27
+ continue;
28
+ const nativeSessionId = session?.nativeSessionId ?? conversation?.conversationId ?? null;
29
+ const identity = JSON.stringify([binding?.providerNamespace ?? session?.adapterId,
30
+ binding?.accountScope ?? set.activeAgentId, nativeSessionId ?? set.owner.roleName]);
31
+ const prior = seen.get(identity);
32
+ if (prior !== undefined) {
33
+ if (sessions[prior].roleName !== set.owner.roleName)
34
+ sessions[prior] = {
35
+ ...sessions[prior], group: "unknown", reason: "Multiple Roles name this native Session; inspect ownership."
36
+ };
37
+ continue;
38
+ }
39
+ seen.set(identity, sessions.length);
40
+ const turn = binding?.run;
41
+ let driverId = null;
42
+ if (session) {
43
+ try {
44
+ driverId = builtinDriverIdForAdapter(session.adapterId);
45
+ }
46
+ catch { /* Unsupported native observation remains explicitly unknown. */ }
47
+ }
48
+ const row = {
49
+ taskId: input.taskId, roleName: set.owner.roleName, agentId: set.activeAgentId,
50
+ driverId,
51
+ nativeSessionId, attemptId: turn?.attemptId ?? null, nativeTurnId: turn?.nativeTurnId ?? null,
52
+ runId: turn?.runId ?? null, group: "unknown", reason: "Current native input is not observable.",
53
+ layer: null, lastActivityAt: null, semanticProgressAt: input.semanticProgressAt ?? null,
54
+ sourceUpdatedAt: turn?.updatedAt ?? session?.updatedAt ?? set.updatedAt,
55
+ waitingReason: null, operations: [], background: []
56
+ };
57
+ if (!session || driverId === null) {
58
+ sessions.push(row);
59
+ continue;
60
+ }
61
+ const fence = { taskId: input.taskId, roleName: set.owner.roleName,
62
+ agentId: session.agentId, driverId, nativeSessionId: session.nativeSessionId };
63
+ selectedIdentities.add(observationIdentity(fence));
64
+ const sessionFacts = observations.filter(({ observation }) => Object.entries(fence).every(([key, value]) => observation.fence[key] === value));
65
+ // Each Run's continuation fold has its own authority. Foreground operations
66
+ // from older inputs must not make the current input look active.
67
+ const groups = new Map();
68
+ for (const { event, observation } of sessionFacts) {
69
+ const runId = observation.fence.runId;
70
+ const group = groups.get(runId) ?? [];
71
+ group.push(event);
72
+ groups.set(runId, group);
73
+ }
74
+ const background = backgroundForRuns(fence, session.createdAt, groups);
75
+ if (!binding || !conversation || conversation.conversationId !== session.nativeSessionId) {
76
+ sessions.push({ ...row, background });
77
+ continue;
78
+ }
79
+ if (!turn) {
80
+ const sessionEvents = sessionFacts.filter(({ observation }) => {
81
+ const f = observation.fence;
82
+ return f.runId === undefined && f.nativeTurnId === undefined
83
+ && f.receiptId === undefined && f.continuationId === undefined;
84
+ }).map(entry => entry.event);
85
+ const projection = projectRuntimeTaskEvents(fence, session.createdAt, sessionEvents);
86
+ const ready = session.status !== "ended" && projection.session === "ready"
87
+ && projection.host !== "exited" && projection.run === "none"
88
+ && projection.observer.status !== "unavailable" && projection.observer.status !== "degraded"
89
+ && projection.goal !== "active" && binding.goal?.status !== "active"
90
+ && Object.keys(projection.operations).length === 0;
91
+ sessions.push({ ...row, background, ...(background.length ? {
92
+ group: "background", reason: "Retained background execution is not settled."
93
+ } : session.status === "ended" || projection.session === "ended" ? {
94
+ group: "stopped", reason: "The selected Session ended with no submitted native input."
95
+ } : ready ? {
96
+ group: "idle", reason: "The selected Session is ready with no submitted native input."
97
+ } : {}) });
98
+ continue;
99
+ }
100
+ const exactEvents = sessionFacts.filter(({ observation }) => {
101
+ const observed = observation;
102
+ const f = observed.fence;
103
+ if (f.continuationId !== undefined || f.runId !== turn.runId)
104
+ return false;
105
+ if (f.receiptId === undefined && f.nativeTurnId === undefined
106
+ && ["host.observed", "session.ended", "session.failed", "observer.health"].includes(observed.kind)) {
107
+ return observed.receivedAt >= turn.submittedAt;
108
+ }
109
+ if (f.receiptId !== undefined && f.receiptId !== turn.attemptId)
110
+ return false;
111
+ if (f.nativeTurnId !== undefined && f.nativeTurnId !== turn.nativeTurnId)
112
+ return false;
113
+ return f.receiptId === turn.attemptId
114
+ || (turn.nativeTurnId !== undefined && f.nativeTurnId === turn.nativeTurnId);
115
+ }).map(entry => entry.event);
116
+ const projection = projectRuntimeTaskEvents({
117
+ ...fence, ...(turn.runId ? { runId: turn.runId } : {}),
118
+ receiptId: turn.attemptId, ...(turn.nativeTurnId ? { nativeTurnId: turn.nativeTurnId } : {})
119
+ }, turn.submittedAt, exactEvents);
120
+ const health = classifyRuntimeHealth({ projection, now: input.now, policy,
121
+ semanticProgressAt: input.semanticProgressAt ?? turn.submittedAt });
122
+ const retry = providerRetryProjection(binding);
123
+ let group;
124
+ let reason = health.reason;
125
+ if (turn.status === "delivery-unknown") {
126
+ group = "unknown";
127
+ reason = "Original input delivery is unknown; do not replay it.";
128
+ }
129
+ else if (session.status === "ended") {
130
+ group = background.length ? "background" : "stopped";
131
+ reason = "The selected Session ended; Task acceptance and retained background execution are separate.";
132
+ }
133
+ else if (retry?.status === "waiting") {
134
+ group = "waiting";
135
+ reason = `Controller-owned Provider retry waiting (${retry.attempts}/${retry.limit}); next eligible ${retry.nextEligibleAt}. Existing work is preserved.`;
136
+ }
137
+ else if (["failed", "cancelled", "rejected"].includes(turn.status)) {
138
+ group = background.length ? "background" : "stopped";
139
+ reason = turn.terminalReason ?? "The selected native execution stopped; Task acceptance is separate.";
140
+ }
141
+ else if (turn.status === "completed") {
142
+ group = background.length ? "background" : "idle";
143
+ reason = background.length ? "Main Turn ended; background execution remains unsettled."
144
+ : "The native Turn ended; this is not Task completion.";
145
+ }
146
+ else if (turn.status !== "accepted") {
147
+ group = "waiting";
148
+ reason = "Waiting for native input acceptance.";
149
+ }
150
+ else if (projection.run === "waiting") {
151
+ group = "waiting";
152
+ }
153
+ else if (["broken", "stopped"].includes(health.layer)) {
154
+ group = "stopped";
155
+ }
156
+ else if (["completed", "failed", "cancelled"].includes(projection.run)) {
157
+ group = "unknown";
158
+ reason = "Native terminal and current input binding disagree; inspect the exact original input.";
159
+ }
160
+ else if (projection.observer.status === "unavailable") {
161
+ group = "unknown";
162
+ }
163
+ else if (health.layer === "diagnostic-needed") {
164
+ group = "diagnostic";
165
+ }
166
+ else if (health.runtimeIdleMs < policy.quietAfterMs && projection.activity.kind !== "provider") {
167
+ group = "active";
168
+ }
169
+ else {
170
+ group = "quiet";
171
+ reason = "Accepted input has no recent structured activity; silence is not a proven deadlock.";
172
+ }
173
+ sessions.push({ ...row, group, reason, layer: health.layer, background,
174
+ lastActivityAt: projection.lastRuntimeActivityAt ?? null,
175
+ waitingReason: projection.waitingReason ?? null, operations: health.activeOperations });
176
+ }
177
+ // Retained, exactly fenced child observations can outlive a selected Session.
178
+ // Do not revive old foreground Turns or enumerate historical Sessions: only
179
+ // identities with still-unsettled child facts enter this current projection.
180
+ const retained = new Map();
181
+ for (const entry of observations) {
182
+ const fence = entry.observation.fence;
183
+ const key = observationIdentity(fence);
184
+ if (!fence.nativeSessionId || selectedIdentities.has(key))
185
+ continue;
186
+ const group = retained.get(key) ?? [];
187
+ group.push(entry);
188
+ retained.set(key, group);
189
+ }
190
+ for (const group of retained.values()) {
191
+ const first = group[0].observation;
192
+ const fence = first.fence;
193
+ const runs = new Map();
194
+ for (const entry of group) {
195
+ const runId = entry.observation.fence.runId;
196
+ const events = runs.get(runId) ?? [];
197
+ events.push(entry.event);
198
+ runs.set(runId, events);
199
+ }
200
+ const background = backgroundForRuns({
201
+ taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
202
+ driverId: fence.driverId, nativeSessionId: fence.nativeSessionId
203
+ }, first.receivedAt, runs);
204
+ if (!background.length)
205
+ continue;
206
+ sessions.push({
207
+ taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
208
+ driverId: fence.driverId,
209
+ nativeSessionId: fence.nativeSessionId, attemptId: null, nativeTurnId: null, runId: null,
210
+ group: "background", reason: "Retained child observations from a non-selected native Session; foreground execution is not inferred.",
211
+ layer: null, lastActivityAt: group.at(-1).observation.receivedAt,
212
+ semanticProgressAt: null, sourceUpdatedAt: group.at(-1).observation.receivedAt,
213
+ waitingReason: null, operations: [], background
214
+ });
215
+ }
216
+ const counts = {
217
+ active: 0, waiting: 0, quiet: 0, diagnostic: 0, unknown: 0, stopped: 0, idle: 0, background: 0
218
+ };
219
+ for (const session of sessions)
220
+ counts[session.group]++;
221
+ return { scope: "task-recorded-native-activity", taskId: input.taskId,
222
+ readAt: input.now.toISOString(), counts, sessions,
223
+ coverage: "Selected Sessions and retained, exactly fenced child observations only; not a live process or complete historical Session inventory." };
224
+ }
225
+ function backgroundForRuns(fence, since, groups) {
226
+ return [...groups].flatMap(([runId, events]) => {
227
+ const projection = projectRuntimeTaskEvents({ ...fence, ...(runId === undefined ? {} : { runId }) }, since, events);
228
+ return [
229
+ ...Object.entries(projection.continuations)
230
+ .filter(([, child]) => child.execution !== "quiescent" || child.identityConflict)
231
+ .map(([id, child]) => ({ id: `${runId ?? "native"}/${id}`, runId: runId ?? null,
232
+ execution: child.identityConflict ? "unknown" : child.execution })),
233
+ // Drivers may emit both forms for one child. This fold is already
234
+ // fenced to Task/Role/Agent/driver/Session/Run; continuation is richer.
235
+ ...Object.entries(projection.operations).filter(([id, operation]) => operation.kind === "subagent" && !Object.hasOwn(projection.continuations, id))
236
+ .map(([id]) => ({ id: `${runId ?? "native"}/operation:${id}`, runId: runId ?? null,
237
+ execution: "unsettled" }))
238
+ ];
239
+ });
240
+ }
@@ -86,15 +86,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
86
86
  attempted = true;
87
87
  const outcome = await delivery.sendOnce({
88
88
  delivery: ready, receiptId: notification.attemptId, notificationId: notification.wakeId,
89
- text: [
90
- `Yui Task notification: task=${task.id} wake=${notification.wakeId}.`,
91
- `Read current context: yui task context ${task.id} --json.`,
92
- `Read the fixed notification window: yui task wake show ${task.id} ${notification.wakeId}.`,
93
- "Read referenced result Messages in full before deciding their disposition.",
94
- "Follow the Leader Skill to advance outstanding Task work from these durable facts, or record the exact decision/authority needed.",
95
- "This is a notification, not an execution assignment. No separate final report is required.",
96
- "Reading context is not an acknowledgement that its requirements have been implemented."
97
- ].join("\n")
89
+ text: notification.presentation
98
90
  });
99
91
  const accepted = outcome.status === "sent" || outcome.status === "already-sent";
100
92
  const disposition = accepted ? "accepted"
@@ -5,6 +5,7 @@ import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
5
5
  import { mailboxBatches } from "../coordination/workMailbox.js";
6
6
  import { actionableExecutionLaneRecoveries, summarizeExecutionGroupHealth } from "../execution/executionHealth.js";
7
7
  import { buildTaskObservabilityProjection } from "./taskObservabilityProjection.js";
8
+ import { projectTaskSessions } from "../runtime/taskSessions.js";
8
9
  import { isRoleRunStalled, latestStallProgressAt } from "./roleRunStall.js";
9
10
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
10
11
  import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
@@ -13,7 +14,7 @@ import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runt
13
14
  * aggregate. This function only reads; it never starts a Controller, queues a
14
15
  * wake, writes a Message, or mutates any record.
15
16
  */
16
- export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
17
+ export function buildTaskExecutionProjection(store, taskId, now = new Date(), nativeSessions) {
17
18
  const task = store.getTask(taskId);
18
19
  if (task === null)
19
20
  return null;
@@ -23,6 +24,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
23
24
  const workItems = store.listWorkItems(taskId);
24
25
  const reviewRounds = store.listReviewRounds(taskId);
25
26
  const runs = operationalTaskRecords(usageRuns, events, "run");
27
+ const brief = store.getTaskBrief(taskId);
26
28
  const leaderMailbox = store.getWorkMailbox({
27
29
  kind: "role",
28
30
  taskId,
@@ -36,6 +38,12 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
36
38
  });
37
39
  return projectTaskExecution({
38
40
  task,
41
+ jobs: store.listDurableJobs(taskId),
42
+ nativeSessions: nativeSessions ?? projectTaskSessions({
43
+ taskId, sessionSets: store.listRoleSessionSets(taskId), events, now,
44
+ ...(brief?.updatedAt === undefined ? {} : { semanticProgressAt: brief.updatedAt }),
45
+ policy: resolveRuntimeHealth(store.getConfig().runtimeHealth)
46
+ }),
39
47
  usageFacts: store.readTaskUsageFacts(taskId),
40
48
  roles,
41
49
  runs,
@@ -56,7 +64,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
56
64
  changeSets: store.listChangeSets(taskId),
57
65
  integrations: store.listIntegrationAttempts(taskId),
58
66
  events,
59
- brief: store.getTaskBrief(taskId),
67
+ brief,
60
68
  pendingWakeup: store.getPendingWakeup(taskId),
61
69
  leaderMailbox,
62
70
  leaderFailure: store.getLeaderFailure(taskId),
@@ -118,8 +126,27 @@ export function projectTaskExecution(facts) {
118
126
  now
119
127
  });
120
128
  const laneRecovery = actionableExecutionLaneRecoveries(groupSummaries)[0];
129
+ const jobs = facts.jobs ?? [];
130
+ const native = facts.nativeSessions;
131
+ const activity = {
132
+ jobs: {
133
+ queued: jobs.filter(job => job.status === "queued").length,
134
+ running: jobs.filter(job => job.status === "running").length,
135
+ unknown: jobs.filter(job => job.status === "unknown-needs-attention").length
136
+ },
137
+ openInputs: inputRequests.filter(input => input.status === "open").length,
138
+ native: {
139
+ active: (native?.counts.active ?? 0) + (native?.counts.quiet ?? 0),
140
+ waiting: native?.counts.waiting ?? 0,
141
+ unknown: (native?.counts.unknown ?? 0) + (native?.counts.diagnostic ?? 0)
142
+ + (native?.sessions.reduce((sum, session) => sum
143
+ + session.background.filter(child => child.execution === "unknown").length, 0) ?? 0),
144
+ background: native?.sessions.reduce((sum, session) => sum + session.background.length, 0) ?? 0
145
+ }
146
+ };
121
147
  const render = (input) => projection({
122
148
  ...input,
149
+ activity,
123
150
  executionGroups: groupSummaries,
124
151
  observability
125
152
  });
@@ -128,10 +155,12 @@ export function projectTaskExecution(facts) {
128
155
  const known = facts.runDelivery?.[run.id];
129
156
  if (known !== undefined && known !== "unobserved")
130
157
  return known;
131
- const observed = events.map(runtimeObservationFromTaskEvent).filter((event) => event?.fence.runId === run.id && event.authority !== "transport"
158
+ const observed = events.map(runtimeObservationFromTaskEvent).filter((event) => event?.fence.runId === run.id && event.fence.taskId === task.id
159
+ && event.fence.roleName === run.roleName && event.fence.agentId === run.effective.agentId
160
+ && event.authority !== "transport"
132
161
  && ["turn.accepted", "turn.completed", "turn.failed", "turn.cancelled"].includes(event.kind)).at(-1);
133
162
  return observed === undefined || observed === null ? "unobserved"
134
- : observed.kind.slice("run.".length);
163
+ : observed.kind.slice("turn.".length);
135
164
  };
136
165
  const runHasSession = (run) => roleSessions.some((session) => (session.roleName === run.roleName
137
166
  && session.agentId === run.effective.agentId
@@ -206,7 +235,7 @@ export function projectTaskExecution(facts) {
206
235
  const activeLeader = activeRuns.find((run) => run.roleName === "leader");
207
236
  const pendingDeliveryRuns = activeRuns.filter((run) => deliveryOf(run) !== "accepted");
208
237
  const hasPendingLeaderWork = pendingWakeup !== null
209
- || leaderMailbox?.pending !== null;
238
+ || leaderMailbox?.pending != null;
210
239
  const recoveryPending = isRecoveryPending(pendingWakeup, leaderMailbox, leaderFailure);
211
240
  const providerRecoveries = (facts.providerRetries ?? []).filter(retry => retry.status === "waiting" || retry.status === "in-flight");
212
241
  const recoveringRunIds = new Set(providerRecoveries.flatMap(retry => [retry.previousRunId, retry.successorRunId].filter((id) => id !== undefined)));
@@ -257,13 +286,30 @@ export function projectTaskExecution(facts) {
257
286
  summary: openInputs[0].question,
258
287
  reason: "open-input-request",
259
288
  monitoring,
260
- failClosed: false,
289
+ failClosed: activity.jobs.unknown > 0 || activity.native.unknown > 0,
261
290
  activeRuns: activeRunViews,
262
291
  attention,
263
292
  blockers,
264
293
  pendingWakeup
265
294
  });
266
295
  }
296
+ if (activity.jobs.unknown > 0 || activity.native.unknown > 0) {
297
+ return render({
298
+ task, status: "attention", owner: "leader", action: "inspect-attention",
299
+ summary: `${activity.jobs.unknown} Job(s) have unknown outcomes; ${activity.native.unknown} native observation(s) need attention. Other parallel activity remains visible; do not infer completion or replay.`,
300
+ reason: "parallel-execution-unknown", monitoring, failClosed: true,
301
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
302
+ });
303
+ }
304
+ const nativeWait = native?.sessions.find(session => session.group === "waiting" && ["user", "permission"].includes(session.waitingReason ?? ""));
305
+ if (nativeWait) {
306
+ return render({
307
+ task, status: "waiting-user", owner: "user", action: "inspect-attention",
308
+ summary: `Native Session ${nativeWait.roleName} is waiting for ${nativeWait.waitingReason}; other activity remains visible.`,
309
+ reason: "native-input-wait", monitoring, failClosed: false,
310
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
311
+ });
312
+ }
267
313
  if (laneRecovery !== undefined && (laneRecovery.runId === undefined || !recoveringRunIds.has(laneRecovery.runId))) {
268
314
  return render({
269
315
  task,
@@ -382,6 +428,15 @@ export function projectTaskExecution(facts) {
382
428
  pendingWakeup
383
429
  });
384
430
  }
431
+ if (native?.sessions.some(session => session.roleName === "leader"
432
+ && ["active", "quiet"].includes(session.group))) {
433
+ return render({
434
+ task, status: "working", owner: "leader", action: "advance-task",
435
+ summary: "Leader native input is accepted; managed AgentRuns and parallel background work remain separate facts.",
436
+ reason: "leader-native-turn", monitoring, failClosed: false,
437
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
438
+ });
439
+ }
385
440
  if (hasPendingLeaderWork) {
386
441
  return render({
387
442
  task,
@@ -449,6 +504,22 @@ export function projectTaskExecution(facts) {
449
504
  pendingWakeup
450
505
  });
451
506
  }
507
+ if (activity.jobs.queued + activity.jobs.running > 0) {
508
+ return render({
509
+ task, status: "waiting-on-jobs", owner: "none", action: "wait-for-jobs",
510
+ summary: `${activity.jobs.running} background Job(s) running; ${activity.jobs.queued} queued. Leader is not currently executing.`,
511
+ reason: "background-jobs", monitoring, failClosed: false,
512
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
513
+ });
514
+ }
515
+ if (activity.native.active + activity.native.waiting + activity.native.background > 0) {
516
+ return render({
517
+ task, status: "waiting-on-agents", owner: "none", action: "wait-for-agents",
518
+ summary: "Recorded native execution or retained background activity remains unsettled; this is separate from managed AgentRuns.",
519
+ reason: "native-execution", monitoring, failClosed: false,
520
+ activeRuns: activeRunViews, attention, blockers, pendingWakeup
521
+ });
522
+ }
452
523
  if (candidateReady || unresolvedIntegration) {
453
524
  return render({
454
525
  task,
@@ -496,6 +567,7 @@ function projection(input) {
496
567
  monitoring: input.monitoring,
497
568
  failClosed: input.failClosed,
498
569
  activeRuns: input.activeRuns,
570
+ activity: input.activity,
499
571
  executionGroups: input.executionGroups ?? [],
500
572
  observability: input.observability,
501
573
  attention: input.attention,
@@ -4,6 +4,7 @@
4
4
  import { readFileSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { APP_SCRIPT, CLIENT_MODULES } from "./client/index.js";
7
+ import { THEME_BOOTSTRAP_SCRIPT } from "./client/lib/theme.js";
7
8
  import { FONT_WOFF2_BASE64 } from "./fontData.js";
8
9
  import { DASHBOARD_HTML } from "./shell/index.js";
9
10
  import { STYLESHEETS } from "./styles/index.js";
@@ -13,6 +14,7 @@ const JS = "text/javascript; charset=utf-8";
13
14
  export const WEB_ASSETS = Object.freeze({
14
15
  ...Object.fromEntries(STYLESHEETS.map(({ name, body }) => [`/assets/css/${name}.css`, { contentType: CSS, body }])),
15
16
  ...Object.fromEntries(Object.entries(CLIENT_MODULES).map(([path, body]) => [`/assets/js/${path}.js`, { contentType: JS, body }])),
17
+ "/assets/theme.js": { contentType: JS, body: THEME_BOOTSTRAP_SCRIPT },
16
18
  "/assets/app.js": { contentType: JS, body: APP_SCRIPT },
17
19
  ...fontAssets(),
18
20
  "/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", JS),
@@ -154,6 +154,7 @@ function detailContext(view) {
154
154
  taskAction: deps.api.taskAction,
155
155
  inspect: api.inspect,
156
156
  list: api.list,
157
+ activity: api.activity,
157
158
  artifacts: api.artifacts,
158
159
  readArtifact: api.artifact,
159
160
  evidence: api.evidence,
@@ -4,9 +4,77 @@ export const RECORDS_SCRIPT = String.raw `
4
4
  // never acknowledges or accepts anything.
5
5
  import { h, icon } from "/assets/js/lib/dom.js";
6
6
  import { button, jsonBlock, mono, note } from "/assets/js/ui/primitives.js";
7
- import { rawDisclosure } from "/assets/js/ui/containers.js";
7
+ import { rawDisclosure, listCard, setCardCount } from "/assets/js/ui/containers.js";
8
8
  import { richText } from "/assets/js/ui/text.js";
9
- import { cachedExact, readExact, summaryFields } from "/assets/js/domain/context.js";
9
+ import { cachedExact, readExact, summaryFields, entriesOf, totalOf, exactCache } from "/assets/js/domain/context.js";
10
+
11
+ // Current-family counts and pages use exactly the Context current filter.
12
+ // A changed Context revalidates already-loaded membership with one bounded
13
+ // page. Only the family's own digest invalidates its pages, not unrelated
14
+ // runtime events. Loading a list does not eagerly inspect its bodies.
15
+ export function currentRecordCard(data, store, t, actions, options) {
16
+ const view = data.viewState;
17
+ exactCache(view);
18
+ view.openRows = view.openRows || {};
19
+ view.currentLists = view.currentLists || {};
20
+ let state = view.currentLists[store];
21
+ if (!state || (!state.started && state.key !== data.core.coreCursor)) {
22
+ state = view.currentLists[store] = {
23
+ key: data.core.coreCursor, items: entriesOf(data.core, store),
24
+ total: totalOf(data.core, store), started: false, cursor: null, error: null, pending: false
25
+ };
26
+ }
27
+ const revalidate = state.key !== data.core.coreCursor;
28
+ state.key = data.core.coreCursor;
29
+ const element = listCard({ id: options.id, title: options.title, icon: options.icon || "pulse", count: state.total });
30
+ const rows = h("div.stack"), footer = h("div.stack", { "aria-live": "polite" });
31
+ element.list.append(rows, footer);
32
+ function draw(updateRows) {
33
+ setCardCount(element, state.total);
34
+ if (updateRows !== false) {
35
+ rows.replaceChildren();
36
+ state.items.forEach(function (item) {
37
+ rows.append(!item.omitted && item.value && options.render ? options.render(item.value, null, item)
38
+ : lazyRow(item, data.task.id, t, actions, {
39
+ cache: view.exact,
40
+ openRows: view.openRows,
41
+ head: h("span", null, item.ref.refId + (item.summary ? " · " + item.summary : "")),
42
+ render: options.render
43
+ }));
44
+ });
45
+ }
46
+ footer.replaceChildren(note(t("activity.loaded") + " " + state.items.length + " / " + state.total));
47
+ if (!state.total) footer.append(note(t("activity.none")));
48
+ if (state.error) footer.append(note(state.error + " · " + t("activity.refreshHint"), "bad"));
49
+ if ((!state.started && state.items.length < state.total) || state.cursor) {
50
+ footer.append(button(t("activity.more") + " (" + Math.max(0, state.total - state.items.length) + ")", {
51
+ disabled: state.pending, onClick: function () { load(state.started); }
52
+ }));
53
+ }
54
+ footer.append(button(t("activity.refresh"), { disabled: state.pending, onClick: function () { load(false); } }));
55
+ element.dataset.reading = String(state.pending);
56
+ }
57
+ async function load(more) {
58
+ if (state.pending) return;
59
+ const busy = function () { return !!rows.querySelector('[data-unsent="true"], [data-reading="true"]'); };
60
+ if (busy()) { state.error = t("activity.busy"); draw(false); return; }
61
+ state.pending = true;
62
+ draw(false);
63
+ try {
64
+ const page = await actions.list(data.task.id, store, { view: "current", limit: 20, cursor: more ? state.cursor : null });
65
+ if (busy()) throw new Error(t("activity.busy"));
66
+ if (more || !state.started || state.digest !== page.digest) {
67
+ state.items = more ? state.items.concat(page.items) : page.items;
68
+ state.cursor = page.nextCursor;
69
+ }
70
+ state.digest = page.digest; state.total = page.total; state.started = true; state.error = null;
71
+ } catch (error) { state.error = error.message; }
72
+ finally { state.pending = false; draw(!state.error); }
73
+ }
74
+ draw();
75
+ if (revalidate) load(false);
76
+ return element;
77
+ }
10
78
 
11
79
  // A row drawn from its exact value: synchronously when cached, otherwise the
12
80
  // summary card until the read returns and replaces it in place.
@@ -16,7 +16,7 @@ export function runCard(run, t, locale) {
16
16
  run.purpose ? chip(label(t, "run.purpose", run.purpose)) : null,
17
17
  run.workItemId ? chip(run.workItemId) : null,
18
18
  h("span.spacer"),
19
- statusBadge(t, "run", "run", run.status)), runBody(run, t, locale));
19
+ statusBadge(t, "run", "run", run.status)), ...runBody(run, t, locale));
20
20
  return element;
21
21
  }
22
22
 
@@ -107,7 +107,8 @@ export function roleCard(role, t, locale, options) {
107
107
  function roleFacts(role, t, locale, opts) {
108
108
  const session = opts.runtimeRole && opts.runtimeRole.runtimeSession;
109
109
  const rows = [];
110
- rows.push([t("role.session"), session && session.nativeSessionId ? mono(session.nativeSessionId) : t("role.noSession")]);
110
+ rows.push([t("role.session"), session && session.nativeSessionId ? mono(session.nativeSessionId)
111
+ : !opts.runtimeRole ? t("activity.notLoaded") : t("role.noSession")]);
111
112
  if (role.launchRevision !== undefined) rows.push([t("role.desired"), "r" + role.launchRevision + (opts.launchDrift ? " · " + t("role.drift") : "")]);
112
113
  if (role.defaultAccess !== undefined) rows.push([t("role.access"), role.defaultAccess]);
113
114
  if (opts.retry) rows.push([t("role.providerRetry"), label(t, "retry", opts.retry.status) + " · "
@@ -22,7 +22,7 @@ export function label(t, prefix, value) {
22
22
  const TONES = {
23
23
  task: { draft: "warn", active: "info", completed: "ok", cancelled: "idle", archived: "idle" },
24
24
  exec: {
25
- "needs-leader-action": "accent", "waiting-on-agents": "info", "waiting-user": "warn",
25
+ "needs-leader-action": "accent", "waiting-on-agents": "info", "waiting-on-jobs": "info", "waiting-user": "warn",
26
26
  recovering: "warn", attention: "bad", "progressing-with-attention": "warn", blocked: "bad",
27
27
  conflicted: "warn", working: "info", completed: "ok", retired: "idle", cancelled: "idle",
28
28
  accepted: "ok", open: "info", archived: "idle"
@@ -58,7 +58,10 @@ export const EN = {
58
58
  "create.hint": "Planning saves a Draft and queues the Leader. It does not start development. Uncheck to save only.",
59
59
  "create.open": "Open saved task",
60
60
  "workbench.next": "Next step",
61
- "workbench.continue": "Continue with the current Leader",
61
+ "workbench.continue": "Talk to Leader",
62
+ "workbench.prompt.running": "Talk to the current Leader to adjust direction or add information.",
63
+ "workbench.prompt.activate": "Start the Task once the draft and plan are confirmed, or keep discussing with the Leader.",
64
+ "workbench.prompt.archive": "This Task has ended. Archive it, or follow up with the Leader.",
62
65
  "workbench.activate": "Start task",
63
66
  "workbench.activateConfirm": "Have you confirmed the Draft and plan? Request activation now?",
64
67
  "workbench.activateHint": "Uses the existing activation process. A saved request is not an active Task.",
@@ -700,6 +703,19 @@ export const EN = {
700
703
  "priority.low": "Low",
701
704
  "exec.status.needs-leader-action": "Needs Leader action",
702
705
  "exec.status.waiting-on-agents": "Waiting on agents",
706
+ "exec.status.waiting-on-jobs": "Waiting on background jobs",
707
+ "exec.action.wait-for-jobs": "Wait for background jobs",
708
+ "activity.loaded": "Loaded",
709
+ "activity.none": "No current items in this source.",
710
+ "activity.notLoaded": "Not loaded. Use the current activity list to read more.",
711
+ "activity.more": "Load more",
712
+ "activity.refresh": "Refresh list",
713
+ "activity.refreshHint": "If the source changed, refresh before continuing.",
714
+ "activity.busy": "Finish or clear the unsent answer, or wait for the current read before replacing this list.",
715
+ "activity.reviews": "Unfinished reviews",
716
+ "activity.native": "Native activity",
717
+ "activity.openSession": "Open current Role Session",
718
+ "activity.scope": "Partial coverage: selected native Sessions and retained, exactly identified child observations. Not a live process or complete historical Session inventory. Native facts may describe an AgentRun above; counts must not be added together.",
703
719
  "exec.status.waiting-user": "Waiting on you",
704
720
  "exec.status.recovering": "Recovering",
705
721
  "exec.status.attention": "Needs attention",
@@ -56,7 +56,10 @@ export const ZH_CN = {
56
56
  "create.hint": "保存 Draft 并通知 Leader 规划,不启动开发。取消勾选则仅保存任务。",
57
57
  "create.open": "打开已保存任务",
58
58
  "workbench.next": "下一步",
59
- "workbench.continue": "与当前 Leader 继续推进",
59
+ "workbench.continue": "与 Leader 对话",
60
+ "workbench.prompt.running": "需要调整方向或补充信息时,直接与当前 Leader 对话。",
61
+ "workbench.prompt.activate": "确认草稿与方案后启动任务,或继续与 Leader 讨论。",
62
+ "workbench.prompt.archive": "任务已结束。可以归档,或继续与 Leader 跟进。",
60
63
  "workbench.activate": "启动任务",
61
64
  "workbench.activateConfirm": "已确认草稿和方案,申请激活此任务?",
62
65
  "workbench.activateHint": "复用现有激活流程。请求已保存不代表任务已激活。",
@@ -698,6 +701,19 @@ export const ZH_CN = {
698
701
  "priority.low": "低",
699
702
  "exec.status.needs-leader-action": "待 Leader 处理",
700
703
  "exec.status.waiting-on-agents": "等待智能体",
704
+ "exec.status.waiting-on-jobs": "等待后台任务",
705
+ "exec.action.wait-for-jobs": "等待后台任务结果",
706
+ "activity.loaded": "已加载",
707
+ "activity.none": "此来源没有当前事项。",
708
+ "activity.notLoaded": "尚未加载;可通过当前活动列表继续读取。",
709
+ "activity.more": "继续加载",
710
+ "activity.refresh": "刷新列表",
711
+ "activity.refreshHint": "若来源已变化,请刷新后继续。",
712
+ "activity.busy": "请先完成或清空未发送的回答,或等待当前读取结束,再替换列表。",
713
+ "activity.reviews": "未结束的审查",
714
+ "activity.native": "原生活动",
715
+ "activity.openSession": "打开该角色的当前会话",
716
+ "activity.scope": "部分覆盖:选中的原生会话,以及身份明确、尚未结束的保留子活动观测;不是实时进程或完整历史会话清单。原生事实可能与上方 AgentRun 描述同一执行,计数不可直接相加。",
701
717
  "exec.status.waiting-user": "等待你回答",
702
718
  "exec.status.recovering": "恢复中",
703
719
  "exec.status.attention": "需要关注",