@zq-silk/yui 2.5.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 (38) hide show
  1. package/dist/cli/commandCatalog.js +3 -2
  2. package/dist/commands/taskCommands.js +8 -3
  3. package/dist/commands/taskContextCommand.js +3 -2
  4. package/dist/context/taskContext.js +7 -4
  5. package/dist/kernel/builtinCapabilities.js +2 -1
  6. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  7. package/dist/runtime/providerRetry.js +38 -0
  8. package/dist/runtime/taskSessions.js +240 -0
  9. package/dist/scheduler/taskExecutionProjection.js +78 -6
  10. package/dist/web/assets/assetManifest.js +2 -0
  11. package/dist/web/assets/client/app/taskView.js +1 -0
  12. package/dist/web/assets/client/domain/records.js +70 -2
  13. package/dist/web/assets/client/domain/runs.js +3 -2
  14. package/dist/web/assets/client/domain/vocab.js +1 -1
  15. package/dist/web/assets/client/i18n/en.js +17 -1
  16. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  17. package/dist/web/assets/client/lib/api.js +4 -1
  18. package/dist/web/assets/client/lib/theme.js +31 -10
  19. package/dist/web/assets/client/views/task/actions.js +38 -21
  20. package/dist/web/assets/client/views/task/observation.js +12 -3
  21. package/dist/web/assets/client/views/task/overview.js +7 -40
  22. package/dist/web/assets/client/views/task/runtime.js +71 -34
  23. package/dist/web/assets/shared/icons.js +2 -0
  24. package/dist/web/assets/shell/index.js +1 -0
  25. package/dist/web/assets/shell/settings.js +1 -0
  26. package/dist/web/assets/styles/views/task.js +14 -0
  27. package/dist/web/webServer.js +12 -0
  28. package/dist/web/webSessions.js +2 -165
  29. package/dist/web/webSnapshot.js +9 -5
  30. package/dist/web/webTaskActivity.js +54 -0
  31. package/dist/web/webTaskSurface.js +1 -0
  32. package/docs/observability/README.md +25 -1
  33. package/docs/observability/README.zh-CN.md +18 -1
  34. package/docs/provider-retry.md +11 -0
  35. package/docs/release-workflow.md +27 -0
  36. package/docs/release-workflow.zh-CN.md +22 -0
  37. package/package.json +1 -1
  38. package/skills/yui-runtime/references/recovery.md +9 -0
@@ -408,8 +408,9 @@ const taskChildren = [
408
408
  {
409
409
  name: "context", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "unbound"] },
410
410
  summary: "Read compact authorized facts, fixed-bound delta, or inspect a Context reference.",
411
- usage: "yui task context [read|list|delta|inspect] <task> [--after <cursor>] [--continuation <cursor>] [--cursor <cursor>] [--limit <n>] [--store <store> --ref <id>] [--digest <digest>]",
412
- options: ["--after", "--continuation", "--cursor", "--limit", "--store", "--ref", "--digest", "--status", "--work-item"]
411
+ usage: "yui task context [read|list|delta|inspect] <task> [--after <cursor>] [--continuation <cursor>] [--cursor <cursor>] [--limit <n>] [--store <store> --ref <id>] [--digest <digest>] [--view <current|all>]",
412
+ options: ["--after", "--continuation", "--cursor", "--limit", "--store", "--ref", "--digest", "--status", "--work-item", "--view"],
413
+ optionValues: { "--view": ["current", "all"] }
413
414
  },
414
415
  {
415
416
  name: "next-action", discovery: { surface: "managed", audiences: ["operator", "leader"] },
@@ -45,7 +45,7 @@ import { copyGlobalRoleToTaskRole, activeRoleAgentBinding, createRole, createRol
45
45
  import { roleLaunchEventPayload, saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
46
46
  import { RUNTIME_SESSION_REPLACE_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
47
47
  import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
48
- import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
48
+ import { controlProviderRetry, providerRunRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
49
49
  import { currentProviderConversation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
50
50
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
51
51
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
@@ -4897,7 +4897,9 @@ function retryRun(args, store, options) {
4897
4897
  return output(`Retry already recorded as ${existing.payload.runId}; retry that exact successor if needed.\n`);
4898
4898
  }
4899
4899
  const sessions = tx.getTaskRoleSessionSet(previous.taskId, previous.roleName);
4900
- const retry = sessions?.providerBinding?.retry;
4900
+ const retry = options.providerRetryChainId !== undefined ? sessions?.providerBinding?.retry
4901
+ : sessions?.providerBinding == null ? undefined
4902
+ : providerRunRetry(sessions.providerBinding, previous.id, clock(options).getTime());
4901
4903
  if (options.providerRetryChainId !== undefined
4902
4904
  && (retry?.chainId !== options.providerRetryChainId || retry.status !== "waiting"
4903
4905
  || retry.previousRunId !== previous.id || clock(options).getTime() < Date.parse(retry.nextEligibleAt)
@@ -4909,7 +4911,10 @@ function retryRun(args, store, options) {
4909
4911
  const successor = tx.getActiveRun(previous.taskId, previous.roleName);
4910
4912
  tx.saveTaskRoleSessionSet({
4911
4913
  ...sessions, providerBinding: {
4912
- ...sessions.providerBinding, retry: {
4914
+ ...sessions.providerBinding,
4915
+ ...(sessions.providerBinding.retry !== undefined && sessions.providerBinding.retry.chainId !== retry.chainId
4916
+ ? { retryHistory: [...(sessions.providerBinding.retryHistory ?? []), sessions.providerBinding.retry] } : {}),
4917
+ retry: {
4913
4918
  ...retry, status: "waiting", successorAutomatic: options.providerRetryChainId !== undefined,
4914
4919
  ...(options.providerRetryChainId === undefined ? {
4915
4920
  intentAt: clock(options).toISOString(),
@@ -17,12 +17,12 @@ export function runTaskContextCommand(args, store, environment = {}) {
17
17
  const action = explicit ? args[0] : "read";
18
18
  const taskId = args[explicit ? 1 : 0];
19
19
  if (!taskId || taskId.startsWith("--"))
20
- throw usageError("Task context usage: yui task context [read|list|delta|inspect] <task> [--after <cursor|timestamp>] [--continuation <cursor>] [--cursor <cursor>] [--limit <1..100>] [--store <store> --ref <id> --digest <digest>] [--status <status>] [--work-item <id>].");
20
+ throw usageError("Task context usage: yui task context [read|list|delta|inspect] <task> [--after <cursor|timestamp>] [--continuation <cursor>] [--cursor <cursor>] [--limit <1..100>] [--store <store> --ref <id> --digest <digest>] [--view <current|all>] [--status <status>] [--work-item <id>].");
21
21
  const options = new Map();
22
22
  const rest = args.slice(explicit ? 2 : 1);
23
23
  const allowed = action === "read" ? [] : action === "delta"
24
24
  ? ["--after", "--continuation", "--limit"] : action === "list"
25
- ? ["--store", "--cursor", "--limit", "--status", "--after", "--work-item"] : ["--store", "--ref", "--digest", "--cursor"];
25
+ ? ["--store", "--cursor", "--limit", "--status", "--after", "--work-item", "--view"] : ["--store", "--ref", "--digest", "--cursor"];
26
26
  for (let i = 0; i < rest.length; i += 2) {
27
27
  const flag = rest[i];
28
28
  const value = rest[i + 1];
@@ -40,6 +40,7 @@ export function runTaskContextCommand(args, store, environment = {}) {
40
40
  throw usageError("Context list requires --store.");
41
41
  data = listTaskContext(store, taskId, family, {
42
42
  cursor: options.get("--cursor"),
43
+ view: options.get("--view"),
43
44
  status: options.get("--status"), after: options.get("--after"), workItemId: options.get("--work-item"),
44
45
  ...(options.has("--limit") ? { limit: Number(options.get("--limit")) } : {})
45
46
  }, environment);
@@ -191,7 +191,10 @@ export function readTaskContextResource(store, taskId, selector, environment = {
191
191
  export function listTaskContext(store, taskId, family, options = {}, environment = {}) {
192
192
  return store.readTransaction(reader => {
193
193
  const scope = authorizeContext(reader, taskId, environment);
194
- const sources = authorizedSources(reader, scope, false, family);
194
+ if (options.view !== undefined && !["current", "all"].includes(options.view)) {
195
+ throw usageError("Context list view must be current or all.");
196
+ }
197
+ const sources = authorizedSources(reader, scope, options.view === "current", family, true);
195
198
  if (!CONTEXT_FAMILIES.includes(family))
196
199
  throw usageError(`Unknown Context store: ${family}.`);
197
200
  if (options.after !== undefined && !Number.isFinite(Date.parse(options.after)))
@@ -216,7 +219,7 @@ export function listTaskContext(store, taskId, family, options = {}, environment
216
219
  }
217
220
  }
218
221
  return recordPage(records, `task:${taskId}/${family}/${JSON.stringify({
219
- status: options.status, after: options.after, workItemId: options.workItemId
222
+ view: options.view ?? "all", status: options.status, after: options.after, workItemId: options.workItemId
220
223
  })}`, options);
221
224
  });
222
225
  }
@@ -447,7 +450,7 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
447
450
  default: return null;
448
451
  }
449
452
  }
450
- function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily) {
453
+ function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily, discovery = false) {
451
454
  const taskId = task.id;
452
455
  const sources = [];
453
456
  const add = (family, id, value) => {
@@ -462,7 +465,7 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
462
465
  const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
463
466
  sources.push({ family, count, *read(offset = 0) {
464
467
  for (const { id, value } of store.queryContextRecords(taskId, {
465
- ...selection, offset, limit: current ? 8 : MAX_RECORDS
468
+ ...selection, offset, limit: current && !discovery ? 8 : MAX_RECORDS
466
469
  }).records)
467
470
  yield materialize(family, id, value);
468
471
  } });
@@ -47,7 +47,7 @@ const definitions = [
47
47
  name: "context.list", summary: "Discover one authorized record family with bounded summaries and exact references.",
48
48
  effect: "query", requiredPermissions: ["task:read"], source: "listTaskContext",
49
49
  inputSchema: object({ taskId: text, store: text, cursor: text, limit: { type: "integer" },
50
- status: text, after: text, workItemId: text }, ["taskId", "store"]),
50
+ view: { type: "string", enum: ["current", "all"] }, status: text, after: text, workItemId: text }, ["taskId", "store"]),
51
51
  outputSchema: { type: "object", required: ["items", "total", "complete", "nextCursor"] }
52
52
  },
53
53
  {
@@ -304,6 +304,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
304
304
  }, callerEnvironment(caller));
305
305
  if (name === "context.list")
306
306
  return listTaskContext(store, taskId, params.store, {
307
+ view: params.view,
307
308
  cursor: params.cursor, limit: params.limit,
308
309
  status: params.status, after: params.after,
309
310
  workItemId: params.workItemId
@@ -26,7 +26,7 @@ function mapClaudeDisplayError(input) {
26
26
  }
27
27
  /** Provider-specific Codex failure recognition. */
28
28
  export function mapCodexAgentError(input) {
29
- return withTransientEvidence(mapCodexDisplayError(input), input);
29
+ return withTransientEvidence(mapCodexDisplayError(input), input, true);
30
30
  }
31
31
  function mapCodexDisplayError(input) {
32
32
  const text = `${input.message}\n${input.raw}`;
@@ -208,7 +208,7 @@ function sessionUnavailable(code) {
208
208
  }
209
209
  /** Replay qualification is stricter than the readable, best-effort category.
210
210
  * Never promote a number occurring in provider prose to submission safety. */
211
- function structuredTransientError(input) {
211
+ function structuredTransientError(input, codex) {
212
212
  if (/insufficient[_ -]quota|quota.{0,25}(exhaust|exceed)|billing[_ -]hard[_ -]limit|usageLimitExceeded|credits?.{0,20}(exhaust|insufficient)/iu.test(`${input.message}\n${input.raw}`))
213
213
  return { category: "access", code: "provider.quota-exhausted", retryable: false };
214
214
  const nodes = [];
@@ -228,7 +228,8 @@ function structuredTransientError(input) {
228
228
  catch { /* Display classification may still use prose. */ }
229
229
  const codes = nodes.flatMap(n => [n.code, n.type, n.error, n.codexErrorInfo]).filter(v => typeof v === "string");
230
230
  if (codes.some(c => ["authentication_error", "permission_error", "invalid_api_key", "invalid_model",
231
- "model_not_found", "usage_limit_exceeded", "insufficient_quota"].includes(c))) {
231
+ "model_not_found", "usage_limit_exceeded", "insufficient_quota"].includes(c))
232
+ || codex && nodes.some(n => ["usageLimitExceeded", "unauthorized", "badRequest"].includes(String(n.codexErrorInfo)))) {
232
233
  return { category: "access", code: "provider.access-denied", retryable: false };
233
234
  }
234
235
  const statuses = nodes.flatMap(n => [n.httpStatusCode, n.statusCode, n.status]).filter(v => typeof v === "number");
@@ -240,6 +241,9 @@ function structuredTransientError(input) {
240
241
  if (statuses.includes(429) || codes.includes("rate_limit_error") || nativeCode === "rate_limit_error") {
241
242
  result = recoverable("rate-limit", "provider.rate-limit");
242
243
  }
244
+ else if (codex && nodes.some(n => n.codexErrorInfo === "serverOverloaded")) {
245
+ result = recoverable("availability", "provider.model-capacity");
246
+ }
243
247
  else if (statuses.some(s => [500, 502, 503, 504].includes(s))
244
248
  || codes.some(c => ["server_error", "overloaded_error"].includes(c))
245
249
  || ["server_error", "overloaded_error"].includes(nativeCode)) {
@@ -277,8 +281,8 @@ function structuredTransientError(input) {
277
281
  }
278
282
  /** Add positive evidence without renaming established diagnostics or letting
279
283
  * a transient nested cause override a known fatal Session/request failure. */
280
- function withTransientEvidence(base, input) {
281
- const evidence = structuredTransientError(input);
284
+ function withTransientEvidence(base, input, codex = false) {
285
+ const evidence = structuredTransientError(input, codex);
282
286
  if (evidence === undefined)
283
287
  return base;
284
288
  const possiblyTransient = ["rate-limit", "availability", "transport", "unknown"].includes(base.category);
@@ -3,6 +3,44 @@ export const PROVIDER_RETRY_BUDGET_MS = 600_000;
3
3
  export function providerRetryPending(binding) {
4
4
  return binding?.retry?.status === "waiting" || binding?.retry?.status === "in-flight";
5
5
  }
6
+ /** An explicit Run retry can carry exact failed-input proof even when the
7
+ * error did not qualify for automation. This does not grant automatic replay:
8
+ * the shared admission and native latest-Turn/quiescence checks still apply. */
9
+ export function providerRunRetry(binding, runId, at) {
10
+ const turn = binding.run;
11
+ const nativeSessionId = binding.conversations.find(c => c.epoch === binding.currentConversationEpoch)?.conversationId;
12
+ if (turn?.runId !== runId || !["failed", "rejected", "deferred"].includes(turn.status)
13
+ || binding.authority.owner !== "controller" || turn.authorityEpoch !== binding.authority.epoch
14
+ || nativeSessionId === undefined)
15
+ return undefined;
16
+ const previous = binding.retry;
17
+ if (previous?.previousRunId === runId && previous.status !== "recovered"
18
+ && previous.failedAttemptId === turn.attemptId
19
+ && previous.authorityEpoch === binding.authority.epoch
20
+ && previous.nativeSessionId === nativeSessionId)
21
+ return previous;
22
+ const failure = turn.failure;
23
+ if (turn.status !== "failed" || turn.nativeTurnId === undefined || turn.input === undefined
24
+ || turn.retrySupported !== true || failure?.error.inputDisposition !== "accepted"
25
+ || failure.error.sessionDisposition !== "recoverable")
26
+ return undefined;
27
+ // A manual attempt can cancel automation when its error changes character.
28
+ // Refresh its failed-Turn proof without granting the same input a new budget.
29
+ const continuing = previous?.status !== "recovered" && previous?.currentAttemptId === turn.attemptId
30
+ && previous.authorityEpoch === binding.authority.epoch && previous.nativeSessionId === nativeSessionId;
31
+ return {
32
+ policyVersion: 1, chainId: continuing ? previous.chainId : turn.attemptId, status: "waiting",
33
+ input: continuing ? previous.input : turn.input, nativeSessionId, authorityEpoch: binding.authority.epoch,
34
+ attempts: continuing ? previous.attempts : 0,
35
+ transportSequence: continuing ? previous.transportSequence : 0,
36
+ startedAt: continuing ? previous.startedAt : iso(at), intentAt: iso(at),
37
+ deadline: continuing ? previous.deadline : iso(at + PROVIDER_RETRY_BUDGET_MS),
38
+ nextEligibleAt: iso(at), updatedAt: iso(at),
39
+ failedAttemptId: turn.attemptId, failureAttemptId: turn.attemptId,
40
+ failedNativeTurnId: turn.nativeTurnId, previousRunId: runId,
41
+ failureRef: failure.ref, error: failure.error, successorAutomatic: false
42
+ };
43
+ }
6
44
  /** Called only in the transaction recording the exact failed/rejected input. */
7
45
  export function recordProviderFailure(binding, input) {
8
46
  let turn = binding.run;
@@ -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
+ }
@@ -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,