@zq-silk/yui 0.11.3 → 0.12.1

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 (108) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +44 -22
  3. package/dist/cli/commandCatalog.js +13 -10
  4. package/dist/cli/interactionPolicy.js +5 -6
  5. package/dist/cli/updateCommand.js +3 -1
  6. package/dist/cli/updateOrchestrator.js +173 -28
  7. package/dist/cli/updatePorts.js +137 -8
  8. package/dist/cli/upgradeCommand.js +19 -9
  9. package/dist/cli.js +156 -57
  10. package/dist/commands/configCommands.js +13 -46
  11. package/dist/commands/executionAuditCommands.js +3 -2
  12. package/dist/commands/jobCommands.js +4 -10
  13. package/dist/commands/taskCommands.js +234 -200
  14. package/dist/commands/taskCompletionGate.js +29 -54
  15. package/dist/commands/taskContextCommand.js +61 -13
  16. package/dist/commands/taskInputCommands.js +8 -5
  17. package/dist/commands/taskNextActionCommand.js +56 -6
  18. package/dist/commands/taskOverviewCommand.js +17 -31
  19. package/dist/commands/taskRoleRuntimeStatus.js +55 -7
  20. package/dist/commands/taskWorkspaceCommands.js +5 -2
  21. package/dist/config/configCatalog.js +3 -3
  22. package/dist/config/yuiConfig.js +3 -7
  23. package/dist/context/wakeNotification.js +20 -5
  24. package/dist/controller/agentRuntimeObserver.js +247 -51
  25. package/dist/controller/clientRuntime.js +39 -3
  26. package/dist/controller/controller.js +31 -27
  27. package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
  28. package/dist/controller/runtime.js +58 -3
  29. package/dist/controller/runtimeEventInbox.js +17 -7
  30. package/dist/controller/runtimeEventProcessor.js +12 -19
  31. package/dist/controller/runtimeHookRunFence.js +19 -4
  32. package/dist/controller/runtimeObservationHook.js +45 -0
  33. package/dist/controller/structuredProviderObservation.js +20 -3
  34. package/dist/core/controllerClient.js +20 -2
  35. package/dist/core/controllerServer.js +1 -0
  36. package/dist/execution/resourceBroker.js +5 -4
  37. package/dist/executor/agentExecutor.js +52 -50
  38. package/dist/executor/effectiveLaunch.js +8 -48
  39. package/dist/executor/fileRoleLaunchPlanner.js +129 -45
  40. package/dist/executor/workspacePreflightClassification.js +23 -2
  41. package/dist/interaction/operatorPresentation.js +33 -89
  42. package/dist/lifecycle/exactRunTerminalization.js +69 -5
  43. package/dist/observability/executionAudit.js +5 -0
  44. package/dist/observability/orchestrationMetrics.js +8 -3
  45. package/dist/profile/agentProfile.js +1 -1
  46. package/dist/release/cliHomeReleaseFence.js +123 -0
  47. package/dist/release/runtimeRelease.js +20 -0
  48. package/dist/repository/taskWorkspacePreparer.js +176 -60
  49. package/dist/resources/sqliteResourceRegistry.js +1 -1
  50. package/dist/review/deltaRecheck.js +12 -51
  51. package/dist/review/reviewAcceptance.js +26 -0
  52. package/dist/review/reviewConfig.js +0 -31
  53. package/dist/review/reviewDecision.js +113 -0
  54. package/dist/review/reviewOutcomeClassifier.js +1 -1
  55. package/dist/review/reviewRound.js +1 -1
  56. package/dist/review/reviewerAvailability.js +69 -0
  57. package/dist/run/recoveryProjection.js +45 -6
  58. package/dist/runtime/agentDriverObservation.js +24 -10
  59. package/dist/runtime/agentHost.js +159 -85
  60. package/dist/runtime/builtinAgentDrivers.js +4 -3
  61. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  62. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  63. package/dist/runtime/conversationSwitch.js +277 -0
  64. package/dist/runtime/index.js +2 -1
  65. package/dist/runtime/launchBroker.js +12 -0
  66. package/dist/runtime/processExitOutbox.js +88 -0
  67. package/dist/runtime/providerRuntimeIdentity.js +29 -1
  68. package/dist/runtime/runtimeHealthPolicy.js +5 -5
  69. package/dist/runtime/runtimeObservation.js +28 -0
  70. package/dist/runtime/runtimeProjection.js +22 -32
  71. package/dist/runtime/sessionTokenMetrics.js +181 -0
  72. package/dist/runtime/structuredProviderHost.js +7 -1
  73. package/dist/runtime/tmuxAdapters.js +4 -1
  74. package/dist/scheduler/activeRoleRunDelivery.js +34 -199
  75. package/dist/scheduler/activeTaskProgress.js +7 -6
  76. package/dist/scheduler/leaderWakeupProcessor.js +42 -138
  77. package/dist/scheduler/operatorEvent.js +34 -0
  78. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  79. package/dist/scheduler/roleRunStall.js +53 -31
  80. package/dist/scheduler/taskExecutionProjection.js +97 -76
  81. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  82. package/dist/storage/migration/productionRegistry.js +379 -0
  83. package/dist/storage/sqliteSchema.js +66 -23
  84. package/dist/storage/sqliteStore.js +46 -23
  85. package/dist/storage/storeRpc.js +0 -1
  86. package/dist/storage/taskStore.js +15 -30
  87. package/dist/storage/upgrade/homeClassification.js +52 -0
  88. package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
  89. package/dist/storage/upgrade/recordVersions.js +3 -2
  90. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  91. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  92. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
  94. package/dist/task/completionReadiness.js +10 -6
  95. package/dist/task/nextAction.js +82 -58
  96. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  97. package/dist/web/assets/client/components.js +13 -3
  98. package/dist/web/assets/client/i18n.js +6 -2
  99. package/dist/web/webSnapshot.js +7 -1
  100. package/i18n/README.zh-CN.md +2 -2
  101. package/package.json +1 -1
  102. package/skills/yui-leader/SKILL.md +60 -20
  103. package/skills/yui-operator/SKILL.md +13 -4
  104. package/skills/yui-reviewer/SKILL.md +35 -11
  105. package/dist/context/sessionContextBudget.js +0 -71
  106. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  107. package/dist/lifecycle/taskRoleSessionReset.js +0 -126
  108. package/dist/scheduler/operatorNotification.js +0 -59
@@ -8,6 +8,8 @@ import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
8
8
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
9
9
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
10
10
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
11
+ import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
12
+ import { freshConversationLaunchBlockers, projectConversationSwitch } from "../runtime/conversationSwitch.js";
11
13
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
12
14
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
13
15
  .filter((request) => request.status === "open").length;
@@ -65,6 +67,12 @@ export function renderTaskRoleRuntimeStatus(status) {
65
67
  ? ""
66
68
  : ` (${status.runtime.observerDetail})`}`
67
69
  ].filter((value) => value !== undefined).join("; ");
70
+ const sessionTokens = [
71
+ `total=${sessionCumulativeTokenLabel(status.sessionTokens)}`,
72
+ `max-request-input=${status.sessionTokens.maximumRequestInput.status === "observed"
73
+ ? status.sessionTokens.maximumRequestInput.inputTokens
74
+ : "unobserved"}`
75
+ ].join("; ");
68
76
  return [
69
77
  `Task Role status: ${status.taskId}/${status.roleName}`,
70
78
  "",
@@ -86,9 +94,15 @@ export function renderTaskRoleRuntimeStatus(status) {
86
94
  ? `needs-attention (${status.stall.kind ?? "workflow-not-progressing"}; no workflow progress since ${status.stall.progressAt ?? "unknown"})`
87
95
  : "none"}`,
88
96
  ` Native session ${nativeSession}`,
97
+ ` Session switch ${status.conversationSwitch === null
98
+ ? "none"
99
+ : `${status.conversationSwitch.status} (${status.conversationSwitch.requestId}; ${status.conversationSwitch.reason})`}`,
89
100
  ` Agent runtime ${runtime}`,
101
+ ` Session tokens ${sessionTokens}`,
90
102
  ` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
91
- ` Fresh launch ${status.freshLaunchAllowed ? "allowed" : "blocked"}`,
103
+ ` Fresh launch ${status.freshLaunchAllowed
104
+ ? "allowed"
105
+ : `blocked (${status.freshLaunchBlockers.join(", ")})`}`,
92
106
  ` tmux pane ${tmux}`,
93
107
  ` Workspace ${status.workspace.managed ? status.workspace.root : status.workspace.path}`,
94
108
  ...workspaceDetails
@@ -109,19 +123,30 @@ export function taskRoleLastRunLabel(status) {
109
123
  }
110
124
  export function taskRoleNativeSessionLabel(status) {
111
125
  if (status.runtimeCleanupPending && status.nativeSession === null)
112
- return "reset (cleanup-pending)";
126
+ return "unbound (cleanup-pending)";
113
127
  return status.nativeSession?.status ?? "unbound";
114
128
  }
115
129
  export function inspectTaskRoleSessionRecovery(taskId, roleName, store) {
116
130
  const sessions = store.getTaskRoleSessionSet(taskId, roleName);
117
131
  const target = runtimeLifecycleTarget({ scope: "task", taskId, roleName });
118
132
  const runtimeMailbox = store.getWorkMailbox(target);
133
+ const roleMailbox = store.getWorkMailbox({ kind: "role", taskId, roleName });
134
+ const switchBlockers = freshConversationLaunchBlockers({
135
+ sessions,
136
+ events: store.listEvents(taskId),
137
+ mailbox: roleMailbox,
138
+ roleName
139
+ });
140
+ const freshLaunchBlockers = [
141
+ ...switchBlockers,
142
+ ...(hasRuntimeLifecycleWork(runtimeMailbox) ? ["runtime-lifecycle-busy"] : [])
143
+ ];
119
144
  return {
120
145
  taskId,
121
146
  roleName,
122
147
  runtimeCleanupPending: hasRuntimeCleanupObligation(runtimeMailbox),
123
- freshLaunchAllowed: (sessions === null
124
- || sessions.sessions[sessions.activeAgentId] === undefined) && !hasRuntimeLifecycleWork(runtimeMailbox)
148
+ freshLaunchAllowed: freshLaunchBlockers.length === 0,
149
+ freshLaunchBlockers
125
150
  };
126
151
  }
127
152
  export function taskRoleOpenInputLabel(status) {
@@ -178,6 +203,10 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
178
203
  ? { managed: false, path: role.workspace }
179
204
  : { ...managedWorkspace, managed: true };
180
205
  const events = store.listEvents(taskId);
206
+ const conversationSwitch = projectConversationSwitch(events, role.name, sessions);
207
+ const sessionTokens = projectSessionTokenMetrics(events, resolveSessionTokenIdentity(nativeSession === null
208
+ ? null
209
+ : { taskId, roleName: role.name, ...nativeSession }));
181
210
  const runtime = projectTaskRoleRuntime(activeRun, nativeSession, tmux, events, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }), store, taskId, role.name, now);
182
211
  const stalled = activeRun !== null && isRoleRunStalled(events, activeRun.id);
183
212
  const stallProgressAt = activeRun === null
@@ -213,13 +242,29 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
213
242
  nativeSession,
214
243
  tmux,
215
244
  workspace,
245
+ sessionTokens,
246
+ conversationSwitch,
216
247
  runtime,
217
248
  stall
218
249
  };
219
250
  }
251
+ function sessionCumulativeTokenLabel(metrics) {
252
+ const total = metrics.cumulativeTotal;
253
+ if (total.status === "unobserved")
254
+ return "unobserved";
255
+ const breakdown = [
256
+ `input=${total.inputTokens}`,
257
+ `output=${total.outputTokens}`,
258
+ ...(total.cachedInputTokens === undefined ? [] : [`cached-input=${total.cachedInputTokens}`]),
259
+ ...(total.reasoningTokens === undefined ? [] : [`reasoning=${total.reasoningTokens}`])
260
+ ];
261
+ return `${total.totalTokens} (${breakdown.join(", ")})`;
262
+ }
220
263
  function latestStallKind(events, runId) {
221
264
  const event = [...events]
222
- .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
265
+ .filter((candidate) => candidate.type === "run.stalled"
266
+ && candidate.payload.runId === runId
267
+ && candidate.payload.status !== "diagnostic-only")
223
268
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
224
269
  return event?.payload.kind === "delivery-stalled" || event?.payload.kind === "workflow-not-progressing"
225
270
  ? event.payload.kind
@@ -229,7 +274,7 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
229
274
  if (runtimeCleanupPending && nativeSession === null) {
230
275
  return {
231
276
  health: "needs-attention",
232
- healthReason: "the native Session was reset; verified runtime cleanup is pending"
277
+ healthReason: "the native Session is unbound; verified runtime cleanup is pending"
233
278
  };
234
279
  }
235
280
  if (role.status === "failed" || role.status === "exited") {
@@ -291,12 +336,15 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
291
336
  case "broken":
292
337
  return { health: "failed", healthReason: runtime.healthReason };
293
338
  case "stopped":
294
- case "diagnostic-needed":
295
339
  case "ready":
296
340
  case "awaiting-provider-acceptance":
297
341
  case "runtime-unobservable":
298
342
  case "starting":
299
343
  return { health: "needs-attention", healthReason: runtime.healthReason };
344
+ case "diagnostic-needed":
345
+ return runtime.observerStatus === "degraded" || runtime.observerStatus === "unavailable"
346
+ ? { health: "needs-attention", healthReason: runtime.healthReason }
347
+ : { health: "running", healthReason: runtime.healthReason };
300
348
  case "waiting-user":
301
349
  return { health: "blocked-input", healthReason: runtime.healthReason };
302
350
  case "waiting-permission":
@@ -99,8 +99,11 @@ function replaceTaskCommand(args, store, options) {
99
99
  const old = store.getTask(oldId);
100
100
  if (old === null)
101
101
  throw usageError(`Task not found: ${oldId}.`);
102
- if (old.status === "draft" || old.status === "active") {
103
- throw usageError(`Task is open and can be rebuilt instead of replaced: ${old.id}.`);
102
+ if (old.status === "draft") {
103
+ throw usageError(`Draft Task ${old.id} owns no workspace to replace or rebuild; activate it or retire it and create a new Draft.`);
104
+ }
105
+ if (old.status === "active") {
106
+ throw usageError(`Active Task can be rebuilt instead of replaced: ${old.id}.`);
104
107
  }
105
108
  let title = `Replaces ${old.id}: ${old.title}`;
106
109
  for (let index = 1; index < args.length; index += 1) {
@@ -13,7 +13,7 @@ export const CONFIG_DEFINITIONS = Object.freeze([
13
13
  { key: "time-zone", domain: "system", property: "timeZone", label: "Time zone", summary: "IANA timezone for human-facing timestamps (default: Asia/Shanghai).", takesEffect: "The next human-readable timestamp rendering; durable JSON remains UTC." },
14
14
  { key: "reconciliation-interval-seconds", domain: "runtime", property: "reconciliationIntervalSeconds", label: "Reconciliation interval", summary: "Recovery reconciliation interval, 5-300 seconds (default: 120).", takesEffect: "The running Controller is refreshed after the value is saved." },
15
15
  { key: "controller-task-concurrency", domain: "runtime", property: "controllerTaskConcurrency", label: "Controller Task concurrency", summary: "Maximum Tasks reconciled concurrently, 1-32 (default: 4).", takesEffect: "After the Controller restarts." },
16
- { key: "runtime-health", domain: "runtime", property: "runtimeHealth", label: "Runtime health thresholds", summary: "Quiet, diagnostic, and semantic-stall thresholds in seconds; values must be strictly increasing (defaults: 300/600/1800).", takesEffect: "CLI and Web projections immediately; scheduler stall handling after the Controller restarts." },
16
+ { key: "runtime-health", domain: "runtime", property: "runtimeHealth", label: "Runtime health thresholds", summary: "Quiet, checkpoint-overdue, and read-only diagnostic thresholds in seconds; values must be strictly increasing (defaults: 300/900/1800).", takesEffect: "CLI and Web projections immediately; scheduler diagnostics after the Controller restarts." },
17
17
  { key: "agent-launch-inactivity-timeout-seconds", domain: "runtime", property: "agentLaunchInactivityTimeoutSeconds", label: "Agent launch inactivity timeout", summary: "Maximum launch silence before startup fails, 15-3600 seconds (default: 300).", takesEffect: "After the Controller restarts." },
18
18
  { key: "delivery-timeout-seconds", domain: "runtime", property: "deliveryTimeoutSeconds", label: "Delivery timeout", summary: "Total control-plane delivery retry budget, 5-600 seconds (default: 120).", takesEffect: "After the Controller restarts; internal retry cadence remains automatic." },
19
19
  { key: "provider-retry-mode", domain: "runtime", property: "providerRetryMode", label: "Provider retry mode", summary: "Provider retry mode: off, shadow, or enforce (default: enforce).", takesEffect: "The next eligible provider-failure decision." },
@@ -22,8 +22,8 @@ export const CONFIG_DEFINITIONS = Object.freeze([
22
22
  { key: "provider-retry-max-window-seconds", domain: "runtime", property: "providerRetryMaxWindowSeconds", label: "Provider retry max window", summary: "Positive-integer total retry budget per Run lineage in seconds (default: 600).", takesEffect: "The next retry episode; an active episode keeps its existing deadline." },
23
23
  { key: "leader-next-action", domain: "workflow", property: "leaderNextActionMode", label: "Leader next-action mode", summary: "Leader next-action mode: display, warn, or enforce (default: display).", takesEffect: "The next Leader next-action projection or gate." },
24
24
  { key: "leader-semantic-budget-turns", domain: "workflow", property: "leaderSemanticBudgetTurns", label: "Leader semantic budget", summary: "Consecutive yielded Leader turns without durable delivery progress before warning, 1-20 (default: 3).", takesEffect: "The next Leader delivery guard evaluation." },
25
- { key: "context-budget", domain: "workflow", property: "contextBudget", label: "Context budget", summary: `Per-Session context token budget; soft and hard are ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}, with soft below hard (defaults: 100000/120000).`, takesEffect: "The next context-budget evaluation; retained conversation content is unchanged." },
26
- { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger; optional finding-ledger and delta-recheck controls. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
25
+ { key: "context-budget", domain: "workflow", property: "contextBudget", label: "Legacy context budget (inactive)", summary: `Compatibility-only soft and hard values in ${MIN_CONTEXT_BUDGET_TOKENS}-${MAX_CONTEXT_BUDGET_TOKENS}; they are parsed but never control Session or workflow behavior.`, takesEffect: "No runtime effect; Session Token metrics are read-only observations." },
26
+ { key: "review", domain: "workflow", property: "review", label: "Review", summary: "Optional global WorkItem review rule: a configured Role plus always, leader, or final trigger and optional finding-ledger behavior. Setup leaves it disabled so the Leader may review directly or delegate selectively.", takesEffect: "The next Candidate snapshot; in-flight Candidates and ReviewRounds keep their policy." },
27
27
  { key: "resources-gc-mode", domain: "resources", property: "resourcesGcMode", label: "Resources GC mode", summary: "Resource GC mode: report or quarantine (default: report).", takesEffect: "The next resource GC operation." },
28
28
  { key: "resources-gc-auto-quarantine", domain: "resources", property: "resourcesGcAutoQuarantine", label: "Resources GC auto-quarantine", summary: "Automatically quarantine eligible terminal Task resources (default: false).", takesEffect: "The next eligible automatic resource GC operation." },
29
29
  { key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
@@ -227,13 +227,9 @@ export function resolveTmuxHistoryLimit(value) {
227
227
  return resolveBoundedPositiveInteger(value, DEFAULT_TMUX_HISTORY_LIMIT, 1_000, 1_000_000, "tmuxHistoryLimit");
228
228
  }
229
229
  /**
230
- * Issue 04 (context token budget): thresholds for one native Session
231
- * generation's observed per-request input peak, measured in tokens. When the
232
- * peak crosses the soft threshold the Leader wake carries a checkpoint
233
- * advisory; when it crosses the hard threshold the scheduler retires the
234
- * generation and starts a fresh one instead of waiting for provider-side
235
- * auto-compaction. The fields are additive and optional — Homes without them
236
- * keep these defaults, so no config migration is required.
230
+ * Compatibility parser for the retired context-budget setting. Existing
231
+ * Homes may retain these values, but no runtime, scheduler, or lifecycle path
232
+ * consumes them. Session Token metrics are an independent read-only view.
237
233
  */
238
234
  export const DEFAULT_CONTEXT_SOFT_TOKENS = 100_000;
239
235
  export const DEFAULT_CONTEXT_HARD_TOKENS = 120_000;
@@ -4,17 +4,18 @@ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
4
4
  * Issue 04 (context token budget) — long-term design:
5
5
  *
6
6
  * A Leader wake is a NOTIFICATION, not a context dump. The wake envelope
7
- * carries only what the Agent cannot reconstruct from durable records:
8
- * the wake id, the aggregated reason tags, and the delta window. The Agent
9
- * reads the delta content on demand with `yui task wake show <wake-id>` and
10
- * the full projection with `yui task context <task>`.
7
+ * carries only what the Agent needs for immediate orientation: the wake id,
8
+ * aggregated reason tags, delta window, and a bounded list of active frozen
9
+ * Task Reviews. The Agent reads delta content on demand with
10
+ * `yui task wake show <wake-id>` and the full projection with
11
+ * `yui task context <task>`.
11
12
  *
12
13
  * The envelope is mode-agnostic: fresh generations and resumed generations
13
14
  * receive the same minimal text. The native Session is a disposable cache of
14
15
  * working context; Yui's durable Task records (including the wake ledger) are
15
16
  * the checkpoint.
16
17
  */
17
- /** Structural guardrail: the envelope must stay an order of magnitude below any context budget. */
18
+ /** Structural guardrail: the envelope stays far below a normal model context window. */
18
19
  export const WAKE_ENVELOPE_HARD_BYTES = 2_000;
19
20
  /** Maximum reason tags rendered before elision to a count. */
20
21
  const REASON_DISPLAY_LIMIT = 6;
@@ -35,11 +36,25 @@ export function buildTaskWakeEnvelope(reader, request) {
35
36
  runs: operationalTaskRecords(reader.listAgentRuns(request.taskId), events, "agent-run")
36
37
  .filter((record) => Date.parse(record.createdAt) > fromTime).length
37
38
  };
39
+ const activeReviews = reader.listReviewRounds(request.taskId).filter((round) => ((round.scope ?? "work-item") === "task"
40
+ && (round.status === "pending" || round.status === "running")));
41
+ const reviewOrientation = activeReviews.slice(0, 3).map((round) => {
42
+ const projects = round.taskCandidate?.projects ?? [];
43
+ const heads = projects.slice(0, 2).map(({ projectId, commit }) => (`${projectId}@${commit.slice(0, 12)}`)).join("+");
44
+ return `${round.id}/${round.reviewerRoleName}`
45
+ + `/${round.deltaRecheck === undefined ? "full" : "delta"}`
46
+ + `[${round.status}]`
47
+ + `@${heads || round.reviewBaseCommit.slice(0, 12)}`
48
+ + `${projects.length > 2 ? `+${projects.length - 2}` : ""}`;
49
+ }).join(", ");
38
50
  const lines = [
39
51
  `Wake: ${request.wakeId} — delta since ${request.fromCursor}`,
40
52
  ` Reasons: ${renderReasons(request.reasons)}`,
41
53
  ` Changed: ${counts.events} events, ${counts.messages} messages, ${counts.runs} runs`
42
54
  + ` → yui task wake show ${request.taskId} ${request.wakeId}`,
55
+ ` Active Task Reviews: ${activeReviews.length === 0
56
+ ? "none"
57
+ : `${reviewOrientation}${activeReviews.length > 3 ? `, … (+${activeReviews.length - 3})` : ""}`}`,
43
58
  `Full context: yui task context ${request.taskId}`
44
59
  ];
45
60
  const body = lines.join("\n");