@zq-silk/yui 0.11.2 → 0.12.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 (91) hide show
  1. package/ARCHITECTURE.md +23 -4
  2. package/README.md +80 -13
  3. package/dist/cli/commandCatalog.js +30 -5
  4. package/dist/cli.js +105 -43
  5. package/dist/commands/configCommands.js +12 -45
  6. package/dist/commands/executionAuditCommands.js +1 -1
  7. package/dist/commands/jobCommands.js +4 -10
  8. package/dist/commands/projectCommands.js +581 -2
  9. package/dist/commands/taskCommands.js +159 -172
  10. package/dist/commands/taskCompletionGate.js +29 -54
  11. package/dist/commands/taskContextCommand.js +55 -11
  12. package/dist/commands/taskInputCommands.js +8 -5
  13. package/dist/commands/taskIntegrationCommands.js +2 -1
  14. package/dist/commands/taskNextActionCommand.js +56 -6
  15. package/dist/commands/taskOverviewCommand.js +17 -31
  16. package/dist/commands/taskRoleRuntimeStatus.js +24 -0
  17. package/dist/commands/taskWorkspaceCommands.js +5 -2
  18. package/dist/config/configCatalog.js +2 -2
  19. package/dist/config/yuiConfig.js +3 -7
  20. package/dist/context/wakeNotification.js +20 -5
  21. package/dist/controller/agentRuntimeObserver.js +247 -51
  22. package/dist/controller/clientRuntime.js +1 -1
  23. package/dist/controller/controller.js +8 -12
  24. package/dist/controller/fileSchedulerStoreAdapter.js +146 -198
  25. package/dist/controller/runtime.js +3 -3
  26. package/dist/controller/runtimeEventInbox.js +17 -7
  27. package/dist/controller/runtimeEventProcessor.js +12 -19
  28. package/dist/controller/runtimeObservationHook.js +45 -0
  29. package/dist/execution/resourceBroker.js +5 -4
  30. package/dist/executor/agentExecutor.js +4 -4
  31. package/dist/executor/effectiveLaunch.js +8 -48
  32. package/dist/executor/fileRoleLaunchPlanner.js +36 -16
  33. package/dist/executor/workspacePreflightClassification.js +23 -2
  34. package/dist/interaction/operatorPresentation.js +33 -89
  35. package/dist/lifecycle/exactRunTerminalization.js +1 -2
  36. package/dist/lifecycle/taskRoleSessionReset.js +5 -13
  37. package/dist/observability/orchestrationMetrics.js +8 -3
  38. package/dist/profile/agentProfile.js +1 -1
  39. package/dist/release/cliHomeReleaseFence.js +123 -0
  40. package/dist/repository/checkoutSwap.js +61 -0
  41. package/dist/repository/gitWorkspace.js +36 -0
  42. package/dist/repository/project.js +53 -3
  43. package/dist/repository/taskWorkspacePreparer.js +176 -60
  44. package/dist/resources/sqliteResourceRegistry.js +1 -1
  45. package/dist/review/deltaRecheck.js +12 -51
  46. package/dist/review/reviewAcceptance.js +26 -0
  47. package/dist/review/reviewConfig.js +0 -31
  48. package/dist/review/reviewDecision.js +113 -0
  49. package/dist/review/reviewOutcomeClassifier.js +1 -1
  50. package/dist/review/reviewRound.js +1 -1
  51. package/dist/review/reviewerAvailability.js +69 -0
  52. package/dist/runtime/agentDriverObservation.js +24 -10
  53. package/dist/runtime/builtinAgentDrivers.js +4 -3
  54. package/dist/runtime/builtinTranscriptObserver.js +301 -64
  55. package/dist/runtime/builtinTranscriptUsage.js +9 -7
  56. package/dist/runtime/index.js +1 -0
  57. package/dist/runtime/runtimeObservation.js +13 -0
  58. package/dist/runtime/runtimeProjection.js +16 -25
  59. package/dist/runtime/sessionTokenMetrics.js +181 -0
  60. package/dist/runtime/structuredProviderHost.js +7 -1
  61. package/dist/scheduler/activeRoleRunDelivery.js +4 -4
  62. package/dist/scheduler/activeTaskProgress.js +7 -6
  63. package/dist/scheduler/leaderWakeupProcessor.js +3 -5
  64. package/dist/scheduler/operatorEvent.js +34 -0
  65. package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
  66. package/dist/scheduler/roleRunStall.js +0 -14
  67. package/dist/scheduler/taskExecutionProjection.js +97 -76
  68. package/dist/scheduler/taskObservabilityProjection.js +10 -11
  69. package/dist/storage/migration/productionRegistry.js +467 -0
  70. package/dist/storage/sqliteSchema.js +18 -8
  71. package/dist/storage/sqliteStore.js +61 -21
  72. package/dist/storage/storeRpc.js +0 -1
  73. package/dist/storage/taskStore.js +47 -31
  74. package/dist/storage/upgrade/recordVersions.js +3 -2
  75. package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
  76. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
  77. package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
  78. package/dist/task/completionReadiness.js +10 -6
  79. package/dist/task/nextAction.js +82 -24
  80. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  81. package/dist/web/assets/client/components.js +13 -3
  82. package/dist/web/assets/client/i18n.js +6 -2
  83. package/dist/web/webSnapshot.js +4 -0
  84. package/i18n/README.zh-CN.md +2 -2
  85. package/package.json +1 -1
  86. package/skills/yui-leader/SKILL.md +53 -16
  87. package/skills/yui-operator/SKILL.md +9 -0
  88. package/skills/yui-reviewer/SKILL.md +28 -7
  89. package/dist/context/sessionContextBudget.js +0 -71
  90. package/dist/lifecycle/contextBudgetRollover.js +0 -81
  91. package/dist/scheduler/operatorNotification.js +0 -59
@@ -12,13 +12,13 @@ export function codexTranscriptUsage(transcript) {
12
12
  });
13
13
  }
14
14
  export function codexTranscriptUsageReport(transcript) {
15
- // Codex token_count events are cumulative session snapshots. Per-request
16
- // input is the delta between consecutive snapshots; the first snapshot is
17
- // treated as one full request.
15
+ // Codex token_count events are cumulative Session snapshots. Per-request
16
+ // input is the non-negative delta between consecutive snapshots; the first
17
+ // snapshot only establishes the generation baseline.
18
18
  let latestInput = 0;
19
19
  let latestCached = 0;
20
20
  let latestOutput = 0;
21
- let previous = 0;
21
+ let previous;
22
22
  let peak = 0;
23
23
  let requests = 0;
24
24
  let seen = false;
@@ -39,9 +39,11 @@ export function codexTranscriptUsageReport(transcript) {
39
39
  requests += 1;
40
40
  seen = true;
41
41
  const total = inputTokens + cachedInputTokens;
42
- const delta = Math.max(0, total - previous);
43
- if (delta > peak)
44
- peak = delta;
42
+ if (previous !== undefined) {
43
+ const delta = Math.max(0, total - previous);
44
+ if (delta > peak)
45
+ peak = delta;
46
+ }
45
47
  previous = total;
46
48
  latestInput = inputTokens;
47
49
  latestCached = cachedInputTokens;
@@ -1,5 +1,6 @@
1
1
  export { createPromptEnvelope } from "./promptEnvelope.js";
2
2
  export { codexTranscriptUsage, codexTranscriptUsageReport, claudeTranscriptUsage, claudeTranscriptUsageReport } from "./builtinTranscriptUsage.js";
3
+ export { projectSessionTokenMetrics, resolveSessionTokenIdentity, unobservedSessionTokenMetrics } from "./sessionTokenMetrics.js";
3
4
  export { createRuntimeBinding } from "./runtimeBinding.js";
4
5
  export { normalizeRuntimeOwner } from "./runtimeOwner.js";
5
6
  export { createSessionLaunchRequest } from "./sessionLaunchRequest.js";
@@ -1,6 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { requireDriverId } from "./agentDriver.js";
3
3
  export const RUNTIME_OBSERVATION_TASK_EVENT = "runtime.observation";
4
+ /**
5
+ * Numeric usage and value-less partial request boundaries are both durable
6
+ * token-projection evidence. The latter deliberately carries no activityId so
7
+ * it cannot be mistaken for lifecycle progress.
8
+ */
9
+ export function isRuntimeTokenEvidence(observation) {
10
+ return observation.kind === "activity.observed"
11
+ && (observation.payload.usage !== undefined
12
+ || (observation.payload.sourceId !== undefined
13
+ && observation.payload.activityId === undefined
14
+ && (observation.payload.observationQuality === "partial"
15
+ || observation.payload.observationQuality === "unavailable")));
16
+ }
4
17
  const KINDS = [
5
18
  "host.observed",
6
19
  "session.started",
@@ -158,16 +158,23 @@ export function projectRuntimeObservation(current, raw) {
158
158
  }
159
159
  case "activity.observed": {
160
160
  const usage = event.payload.usage;
161
- if (usage !== undefined && !usageAdvanced(current.usage, usage)) {
162
- return next(current, { usage: Object.freeze({ ...usage }) });
163
- }
164
- return withActivity(next(current, {
161
+ const lifecycleActivity = event.payload.activityId !== undefined
162
+ && usage === undefined;
163
+ const projected = next(current, {
165
164
  ...(usage === undefined ? {} : { usage: Object.freeze({ ...usage }) }),
166
- session: current.turn === "waiting" ? "active" : current.session,
167
- turn: current.turn === "waiting" ? "accepted" : current.turn,
168
- waitingReason: undefined,
169
- waitId: undefined
170
- }), event.payload.activity, at);
165
+ ...(lifecycleActivity ? {
166
+ session: current.turn === "waiting" ? "active" : current.session,
167
+ turn: current.turn === "waiting" ? "accepted" : current.turn,
168
+ waitingReason: undefined,
169
+ waitId: undefined
170
+ } : {})
171
+ });
172
+ // Token counters are read-only measurements. Only a separate explicit
173
+ // activity observation may advance runtime health or waiting state;
174
+ // usage activityId values identify requests only for token projection.
175
+ if (!lifecycleActivity)
176
+ return projected;
177
+ return withActivity(projected, event.payload.activity, at);
171
178
  }
172
179
  case "observer.health":
173
180
  return next(current, {
@@ -488,22 +495,6 @@ function next(current, patch) {
488
495
  runActivity
489
496
  });
490
497
  }
491
- function usageAdvanced(previous, current) {
492
- if (current.semantics === "remaining-context")
493
- return false;
494
- if (current.semantics === "request-context")
495
- return usageTotal(current) > 0;
496
- // A first cumulative snapshot may contain history from a resumed native
497
- // Session. It establishes a baseline but cannot prove current progress.
498
- if (previous === undefined || previous.semantics !== "cumulative-session")
499
- return false;
500
- return usageTotal(current) > usageTotal(previous);
501
- }
502
- function usageTotal(usage) {
503
- // cachedInputTokens and reasoningTokens are breakdowns of the normalized
504
- // input/output totals, not additional usage.
505
- return usage.inputTokens + usage.outputTokens;
506
- }
507
498
  function dominantOperation(operations) {
508
499
  const kinds = Object.values(operations).map(({ kind }) => kind);
509
500
  if (kinds.includes("subagent"))
@@ -0,0 +1,181 @@
1
+ import { builtinDriverIdForAdapter } from "./builtinAgentDrivers.js";
2
+ import { isRuntimeTokenEvidence, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
3
+ const UNOBSERVED = Object.freeze({ status: "unobserved" });
4
+ export function resolveSessionTokenIdentity(input) {
5
+ if (input?.taskId === undefined
6
+ || input.roleName === undefined
7
+ || input.agentId === undefined
8
+ || input.adapterId === undefined
9
+ || input.launchId === undefined
10
+ || input.nativeSessionId === undefined)
11
+ return null;
12
+ let driverId;
13
+ try {
14
+ driverId = builtinDriverIdForAdapter(input.adapterId);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ return Object.freeze({
20
+ taskId: input.taskId,
21
+ roleName: input.roleName,
22
+ agentId: input.agentId,
23
+ driverId,
24
+ launchId: input.launchId,
25
+ nativeSessionId: input.nativeSessionId,
26
+ sessionGenerationId: input.launchId
27
+ });
28
+ }
29
+ /**
30
+ * Read-only projection over canonical runtime observations. Token values never
31
+ * decide lifecycle, health, scheduling, resource admission, or workflow state.
32
+ */
33
+ export function projectSessionTokenMetrics(events, identity) {
34
+ if (identity === null)
35
+ return unobservedSessionTokenMetrics();
36
+ const observations = events
37
+ .map(runtimeObservationFromTaskEvent)
38
+ .filter((observation) => (observation !== null
39
+ && isRuntimeTokenEvidence(observation)
40
+ && matchesSessionGeneration(observation, identity)))
41
+ .sort(compareObservations);
42
+ const incompleteBoundaries = observations.filter(({ payload }) => (payload.usage === undefined));
43
+ const request = observations.filter(({ payload }) => (payload.usage?.semantics === "request-context"));
44
+ const cumulative = observations.filter(({ payload }) => (payload.usage?.semantics === "cumulative-session"));
45
+ // A generation cannot switch counter semantics without an explicit fact
46
+ // explaining how the two streams overlap. Remaining-context is capacity,
47
+ // not consumption, and is intentionally absent from both metrics.
48
+ if (request.length > 0 && cumulative.length > 0) {
49
+ return unobservedSessionTokenMetrics(identity);
50
+ }
51
+ if (request.length > 0) {
52
+ return projectRequestSnapshots(identity, request, incompleteBoundaries.length > 0);
53
+ }
54
+ if (cumulative.length > 0) {
55
+ return projectCumulativeSnapshots(identity, cumulative, incompleteBoundaries.length > 0);
56
+ }
57
+ return unobservedSessionTokenMetrics(identity);
58
+ }
59
+ export function unobservedSessionTokenMetrics(identity = null) {
60
+ return Object.freeze({
61
+ identity: identity === null ? null : Object.freeze({ ...identity }),
62
+ cumulativeTotal: UNOBSERVED,
63
+ maximumRequestInput: UNOBSERVED
64
+ });
65
+ }
66
+ function projectRequestSnapshots(identity, observations, hasIncompleteBoundary) {
67
+ if (hasIncompleteBoundary || observations.some(({ payload }) => (payload.observationQuality === "partial"
68
+ || payload.observationQuality === "unavailable")))
69
+ return unobservedSessionTokenMetrics(identity);
70
+ const requests = new Map();
71
+ for (const observation of observations) {
72
+ const usage = observation.payload.usage;
73
+ const requestId = observation.payload.activityId ?? observation.semanticKey;
74
+ // Structured providers may publish multiple snapshots for one stable
75
+ // request identity. The latest snapshot supersedes the earlier delivery;
76
+ // replays with the same identity never become a second request.
77
+ requests.set(requestId, usage);
78
+ }
79
+ const values = [...requests.values()];
80
+ const inputTokens = safeSum(values.map((usage) => usage.inputTokens));
81
+ const outputTokens = safeSum(values.map((usage) => usage.outputTokens));
82
+ if (inputTokens === null || outputTokens === null) {
83
+ return unobservedSessionTokenMetrics(identity);
84
+ }
85
+ const totalTokens = safeAdd(inputTokens, outputTokens);
86
+ if (totalTokens === null)
87
+ return unobservedSessionTokenMetrics(identity);
88
+ const cachedInputTokens = values.every((usage) => usage.cachedInputTokens !== undefined)
89
+ ? safeSum(values.map((usage) => usage.cachedInputTokens))
90
+ : null;
91
+ const reasoningTokens = values.every((usage) => usage.reasoningTokens !== undefined)
92
+ ? safeSum(values.map((usage) => usage.reasoningTokens))
93
+ : null;
94
+ return Object.freeze({
95
+ identity: Object.freeze({ ...identity }),
96
+ cumulativeTotal: Object.freeze({
97
+ status: "observed",
98
+ totalTokens,
99
+ inputTokens,
100
+ outputTokens,
101
+ ...(cachedInputTokens === null ? {} : { cachedInputTokens }),
102
+ ...(reasoningTokens === null ? {} : { reasoningTokens })
103
+ }),
104
+ maximumRequestInput: Object.freeze({
105
+ status: "observed",
106
+ inputTokens: Math.max(...values.map((usage) => usage.inputTokens))
107
+ })
108
+ });
109
+ }
110
+ function projectCumulativeSnapshots(identity, observations, hasIncompleteBoundary) {
111
+ const values = observations.map(({ payload }) => payload.usage);
112
+ const requestBoundaryIncomplete = hasIncompleteBoundary || observations.some(({ payload }) => (payload.observationQuality === "partial"
113
+ || payload.observationQuality === "unavailable"));
114
+ let counterRollback = false;
115
+ let maximumRequestInput = null;
116
+ for (let index = 1; index < values.length; index += 1) {
117
+ const previous = values[index - 1];
118
+ const current = values[index];
119
+ if (current.inputTokens < previous.inputTokens
120
+ || current.outputTokens < previous.outputTokens)
121
+ counterRollback = true;
122
+ const requestInput = Math.max(0, current.inputTokens - previous.inputTokens);
123
+ maximumRequestInput = maximumRequestInput === null
124
+ ? requestInput
125
+ : Math.max(maximumRequestInput, requestInput);
126
+ }
127
+ const latest = values.at(-1);
128
+ const totalTokens = safeAdd(latest.inputTokens, latest.outputTokens);
129
+ const cumulativeTotal = counterRollback || totalTokens === null
130
+ ? UNOBSERVED
131
+ : Object.freeze({
132
+ status: "observed",
133
+ totalTokens,
134
+ inputTokens: latest.inputTokens,
135
+ outputTokens: latest.outputTokens,
136
+ ...(latest.cachedInputTokens === undefined
137
+ ? {}
138
+ : { cachedInputTokens: latest.cachedInputTokens }),
139
+ ...(latest.reasoningTokens === undefined
140
+ ? {}
141
+ : { reasoningTokens: latest.reasoningTokens })
142
+ });
143
+ return Object.freeze({
144
+ identity: Object.freeze({ ...identity }),
145
+ cumulativeTotal,
146
+ maximumRequestInput: counterRollback
147
+ || requestBoundaryIncomplete
148
+ || maximumRequestInput === null
149
+ ? UNOBSERVED
150
+ : Object.freeze({ status: "observed", inputTokens: maximumRequestInput })
151
+ });
152
+ }
153
+ function matchesSessionGeneration(observation, identity) {
154
+ const fence = observation.fence;
155
+ return fence.taskId === identity.taskId
156
+ && fence.roleName === identity.roleName
157
+ && fence.agentId === identity.agentId
158
+ && fence.driverId === identity.driverId
159
+ && fence.launchId === identity.launchId
160
+ && fence.nativeSessionId === identity.nativeSessionId
161
+ && fence.sessionGenerationId === identity.sessionGenerationId;
162
+ }
163
+ function compareObservations(left, right) {
164
+ return left.receivedAt.localeCompare(right.receivedAt)
165
+ || (left.sequence ?? -1) - (right.sequence ?? -1)
166
+ || (left.ordinal ?? -1) - (right.ordinal ?? -1)
167
+ || left.eventId.localeCompare(right.eventId);
168
+ }
169
+ function safeSum(values) {
170
+ let total = 0;
171
+ for (const value of values) {
172
+ total = safeAdd(total, value) ?? Number.NaN;
173
+ if (!Number.isSafeInteger(total))
174
+ return null;
175
+ }
176
+ return total;
177
+ }
178
+ function safeAdd(left, right) {
179
+ const total = left + right;
180
+ return Number.isSafeInteger(total) && total >= 0 ? total : null;
181
+ }
@@ -391,7 +391,13 @@ class ClaudeStructuredProviderSession {
391
391
  waiter.reject(new ProviderDeliveryUnknownError("Claude replayed input for a different native Session.", waiter.attemptId));
392
392
  return;
393
393
  }
394
- const nativeTurnId = `claude-input:${waiter.attemptId}`;
394
+ const nativeTurnId = optionalId(message.uuid);
395
+ if (nativeTurnId === undefined) {
396
+ clearTimeout(waiter.timer);
397
+ this.#waiter = undefined;
398
+ waiter.reject(new ProviderDeliveryUnknownError("Claude replayed input without a native message identity.", waiter.attemptId));
399
+ return;
400
+ }
395
401
  this.#activeTurnId = nativeTurnId;
396
402
  clearTimeout(waiter.timer);
397
403
  this.#waiter = undefined;
@@ -4,7 +4,7 @@ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
4
4
  import { prefixYuiTitleInput } from "../run/runIdentity.js";
5
5
  import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
6
6
  import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
7
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
7
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
8
8
  import { RuntimeLaunchError } from "../runtime/ports.js";
9
9
  import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
10
10
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
@@ -881,7 +881,7 @@ function requireResumeSession(role, effective, session) {
881
881
  if (session === null || !hasText(session.nativeSessionId)) {
882
882
  throw new Error(`Role resume has no fixed native session: ${role.taskId}/${role.name}.`);
883
883
  }
884
- if (!effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, effective, role.managedWorkspace)) {
884
+ if (!effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, effective)) {
885
885
  throw new Error(`Role resume effective snapshot drifted: ${role.taskId}/${role.name}.`);
886
886
  }
887
887
  return session.nativeSessionId;
@@ -948,13 +948,13 @@ function validateRoleSession(role, effective, existing, mode, session) {
948
948
  throw new Error(`Ready Role session identity changed: ${role.taskId}/${role.name}.`);
949
949
  }
950
950
  const compatible = mode === "resume"
951
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, effective, role.managedWorkspace)
951
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, effective)
952
952
  : effectiveLaunchSnapshotsCompatible(session.effective, effective);
953
953
  if (!compatible) {
954
954
  throw new Error(`Ready Role session effective snapshot changed: ${role.taskId}/${role.name}.`);
955
955
  }
956
956
  if (existing?.nativeSessionId !== undefined
957
- && effectiveLaunchSnapshotsCompatibleForTaskMain(existing.effective, effective, role.managedWorkspace)
957
+ && effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective)
958
958
  && session.nativeSessionId !== existing.nativeSessionId) {
959
959
  throw new Error(`Ready Role session changed the fixed native session id: ${role.taskId}/${role.name}.`);
960
960
  }
@@ -35,8 +35,7 @@ export function repairOrphanedActiveTasks(store, now, selection) {
35
35
  runs: activeRuns,
36
36
  pendingWakeup: store.getPendingWakeup(task.id),
37
37
  leaderMailbox,
38
- leaderFailure: store.getLeaderFailure(task.id),
39
- operatorNotification: store.getOperatorNotification(task.id)
38
+ leaderFailure: store.getLeaderFailure(task.id)
40
39
  });
41
40
  const queuedAlongsideActiveSibling = hasDispatchableQueuedResourceLane(store, task.id)
42
41
  && !activeRuns.some(({ roleName }) => roleName === "leader")
@@ -45,7 +44,6 @@ export function repairOrphanedActiveTasks(store, now, selection) {
45
44
  if ((queuedAlongsideActiveSibling ? hasLeaderInFlightTurn : hasInFlightTurn)
46
45
  || store.hasOpenInputRequest(task.id)
47
46
  || store.getLeaderFailure(task.id) !== null
48
- || store.getOperatorNotification(task.id) !== null
49
47
  || (projection.status !== "needs-leader-action" && !queuedAlongsideActiveSibling)
50
48
  || hasUnclaimedLeaderWork(store, task.id, leaderMailbox)) {
51
49
  continue;
@@ -121,8 +119,11 @@ function findLastLeaderRun(store, taskId) {
121
119
  };
122
120
  }
123
121
  function hasUnclaimedLeaderWork(store, taskId, leaderMailbox) {
122
+ if (store.getPendingWakeup(taskId) !== null)
123
+ return true;
124
124
  if (leaderMailbox === null)
125
- return store.getPendingWakeup(taskId) !== null;
126
- return leaderMailbox?.processing === null
127
- && leaderMailbox.pending !== null;
125
+ return false;
126
+ return leaderMailbox.processing === null
127
+ && (leaderMailbox.pending.normal !== null
128
+ || leaderMailbox.pending.userCorrection !== null);
128
129
  }
@@ -3,10 +3,9 @@ import { createRunAssignment, serializeRunBootstrapEnvelope } from "../context/r
3
3
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
4
4
  import { prefixYuiTitleInput } from "../run/runIdentity.js";
5
5
  import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
6
- import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
6
+ import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
7
7
  import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
8
  import { recordLeaderFailure } from "./leaderFailure.js";
9
- import { createLeaderRecoveryNotification } from "./operatorNotification.js";
10
9
  import { isSchedulerTaskWorkspaceReady } from "./ports.js";
11
10
  import { RuntimeLaunchError } from "../runtime/ports.js";
12
11
  export async function processLeaderWakeups(store, delivery, now, selection) {
@@ -114,7 +113,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
114
113
  const compatibleSession = existingSession !== null
115
114
  && (reopening
116
115
  ? effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective)
117
- : effectiveLaunchSnapshotsCompatibleForTaskMain(existingSession.effective, role.effective, role.managedWorkspace));
116
+ : effectiveLaunchSnapshotsCompatibleForTaskSession(existingSession.effective, role.effective));
118
117
  const reopenIdentityDrift = reopening
119
118
  && hasNativeSession(existingSession)
120
119
  && !compatibleSession;
@@ -504,7 +503,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
504
503
  session: effectiveSession,
505
504
  claimed: { run, wakeup },
506
505
  failure: recordLeaderFailure(task.id, effectiveSession?.nativeSessionId ?? "(unregistered)", message, now, store.getLeaderFailure(task.id)),
507
- notification: createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)),
508
506
  now
509
507
  });
510
508
  if (failureResult === "state-changed") {
@@ -535,7 +533,7 @@ function validateReadySession(role, effective, existing, mode, session) {
535
533
  throw new Error(`Ready session belongs to another Agent: ${session.agentId}.`);
536
534
  }
537
535
  const compatible = mode === "resume"
538
- ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, effective, role.managedWorkspace)
536
+ ? effectiveLaunchSnapshotsCompatibleForTaskSession(session.effective, effective)
539
537
  : effectiveLaunchSnapshotsCompatible(session.effective, effective);
540
538
  if (!compatible) {
541
539
  throw new Error("Ready Leader session effective snapshot changed.");
@@ -0,0 +1,34 @@
1
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
2
+ import { createTaskEvent } from "../event/taskEvent.js";
3
+ export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
4
+ /** The sole Task-event boundary into the global Operator mailbox. */
5
+ export function enqueueOperatorEvent(store, event, reason, now) {
6
+ enqueueWork(store, { kind: "operator" }, reason, now, [
7
+ { type: "event", taskId: event.taskId, id: event.id }
8
+ ], {
9
+ source: "task-event",
10
+ dedupeKey: `operator-event:${event.taskId}:${event.id}`,
11
+ deliveryMode: "followup",
12
+ lane: "normal"
13
+ });
14
+ }
15
+ /** Routes one Role fact to its supervisor without exposing Provider details to Operator code. */
16
+ export function routeRoleEvent(store, event, roleName, reason, now) {
17
+ if (roleName === "leader") {
18
+ enqueueOperatorEvent(store, event, reason, now);
19
+ return;
20
+ }
21
+ enqueueWork(store, { kind: "role", taskId: event.taskId, roleName: "leader" }, reason, now, [{ type: "event", taskId: event.taskId, id: event.id }], {
22
+ source: "task-event",
23
+ dedupeKey: `leader-event:${event.taskId}:${event.id}`,
24
+ deliveryMode: "followup",
25
+ lane: "normal"
26
+ });
27
+ }
28
+ /** Records the semantic boundary used when a Leader can no longer continue. */
29
+ export function recordLeaderAttentionRequired(store, input) {
30
+ const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, LEADER_ATTENTION_REQUIRED_EVENT, { ...(input.payload ?? {}), reason: input.reason }, input.now);
31
+ store.saveEvent(input.taskId, event);
32
+ enqueueOperatorEvent(store, event, "leader-attention-required", input.now);
33
+ return event;
34
+ }
@@ -1,4 +1,4 @@
1
- import { createInputRequestOperatorPresentation, createLeaderRecoveryOperatorPresentation, createLeaderStallOperatorPresentation, createTaskTerminalOperatorPresentation } from "../interaction/operatorPresentation.js";
1
+ import { createOperatorBatchPresentation } from "../interaction/operatorPresentation.js";
2
2
  import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
3
3
  export async function processOperatorInputNotifications(store, delivery, selection, now = new Date()) {
4
4
  if (selection !== undefined && !selection.full && !selection.operator)
@@ -19,18 +19,18 @@ export async function processOperatorInputNotifications(store, delivery, selecti
19
19
  if (claim.status === "empty")
20
20
  return [];
21
21
  const processing = claim.processing;
22
- const requests = processing.batch.refs
23
- .flatMap((ref) => ref.type === "input"
24
- ? [store.getInputRequest(ref.taskId, ref.id)]
25
- : [])
26
- .filter((request) => request !== null && request.status === "open")
27
- .map((request) => ({ kind: "input", request }));
28
- const notifications = processing.batch.refs
29
- .filter((ref) => ref.type === "task")
30
- .map((ref) => store.getOperatorNotification(ref.id))
31
- .filter((notification) => notification !== null)
32
- .map((notification) => ({ kind: "notification", notification }));
33
- const attentions = deduplicateAttention([...requests, ...notifications]);
22
+ const attentions = deduplicateAttention(processing.batch.refs.flatMap((ref) => {
23
+ if (ref.type === "input") {
24
+ const request = store.getInputRequest(ref.taskId, ref.id);
25
+ return request !== null && request.status === "open"
26
+ ? [{ kind: "input", request }]
27
+ : [];
28
+ }
29
+ if (ref.type !== "event" || store.listEvents === undefined)
30
+ return [];
31
+ const event = store.listEvents(ref.taskId).find(({ id }) => id === ref.id);
32
+ return event === undefined ? [] : [{ kind: "event", event }];
33
+ }));
34
34
  if (attentions.length === 0) {
35
35
  store.completeWorkMailbox(targetMailbox, processing.batchId);
36
36
  return [];
@@ -41,58 +41,35 @@ export async function processOperatorInputNotifications(store, delivery, selecti
41
41
  store.releaseWorkMailbox(targetMailbox, processing.batchId);
42
42
  return attentions.map((attention) => skipped(attention, reason));
43
43
  }
44
- const results = [];
45
- for (const [index, attention] of attentions.entries()) {
46
- try {
47
- const presentation = createAttentionPresentation(attention, store);
48
- const outcome = await delivery.notifyOperatorInputOnce({
49
- ...target,
50
- receiptId: presentation.receiptId,
51
- text: presentation.text
52
- });
53
- if (outcome === "unavailable") {
54
- store.releaseWorkMailbox(targetMailbox, processing.batchId);
55
- results.push(skipped(attention, "operator-unavailable"));
56
- results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-unavailable"))));
57
- break;
58
- }
59
- else if (outcome === "not-ready") {
60
- store.releaseWorkMailbox(targetMailbox, processing.batchId);
61
- results.push(skipped(attention, "operator-not-ready"));
62
- results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
63
- break;
64
- }
65
- else {
66
- results.push({
67
- ...attentionIdentity(attention),
68
- status: outcome
69
- });
70
- if (outcome === "sent") {
71
- if (index === attentions.length - 1) {
72
- store.completeWorkMailbox(targetMailbox, processing.batchId);
73
- }
74
- else {
75
- store.releaseWorkMailbox(targetMailbox, processing.batchId);
76
- }
77
- results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
78
- break;
79
- }
80
- }
81
- }
82
- catch (error) {
44
+ try {
45
+ const presentation = createOperatorBatchPresentation(processing.batchId, attentions.map(toPresentationItem));
46
+ const outcome = await delivery.notifyOperatorInputOnce({
47
+ ...target,
48
+ receiptId: presentation.receiptId,
49
+ text: presentation.text
50
+ });
51
+ if (outcome === "unavailable" || outcome === "not-ready") {
83
52
  store.releaseWorkMailbox(targetMailbox, processing.batchId);
84
- results.push({
85
- ...attentionIdentity(attention),
86
- status: "failed",
87
- error: error instanceof Error ? error.message : String(error)
88
- });
89
- break;
53
+ const reason = outcome === "unavailable" ? "operator-unavailable" : "operator-not-ready";
54
+ return attentions.map((attention) => skipped(attention, reason));
90
55
  }
91
- }
92
- if (results.length > 0 && results.every((result) => result.status === "already-sent")) {
56
+ if (outcome === "sent")
57
+ store.markOperatorTurnStarted(now);
93
58
  store.completeWorkMailbox(targetMailbox, processing.batchId);
59
+ return attentions.map((attention) => ({
60
+ ...attentionIdentity(attention),
61
+ status: outcome
62
+ }));
63
+ }
64
+ catch (error) {
65
+ store.releaseWorkMailbox(targetMailbox, processing.batchId);
66
+ const detail = error instanceof Error ? error.message : String(error);
67
+ return attentions.map((attention) => ({
68
+ ...attentionIdentity(attention),
69
+ status: "failed",
70
+ error: detail
71
+ }));
94
72
  }
95
- return results;
96
73
  }
97
74
  function skipped(attention, reason) {
98
75
  return {
@@ -102,45 +79,28 @@ function skipped(attention, reason) {
102
79
  };
103
80
  }
104
81
  function attentionIdentity(attention) {
105
- if (attention.kind === "input") {
106
- return { inputRequestId: attention.request.id, taskId: attention.request.taskId };
107
- }
108
- if (attention.notification.type === "leader-recovery-failed") {
109
- return {
110
- recoveryTaskId: attention.notification.taskId,
111
- taskId: attention.notification.taskId
112
- };
113
- }
114
- if (attention.notification.type === "leader-stalled") {
115
- return {
116
- stallTaskId: attention.notification.taskId,
117
- taskId: attention.notification.taskId
82
+ return attention.kind === "input"
83
+ ? {
84
+ sourceKind: "input",
85
+ sourceId: attention.request.id,
86
+ taskId: attention.request.taskId
87
+ }
88
+ : {
89
+ sourceKind: "event",
90
+ sourceId: attention.event.id,
91
+ taskId: attention.event.taskId
118
92
  };
119
- }
120
- return {
121
- terminalTaskId: attention.notification.taskId,
122
- taskId: attention.notification.taskId
123
- };
124
93
  }
125
- function createAttentionPresentation(attention, store) {
126
- if (attention.kind === "input") {
127
- return createInputRequestOperatorPresentation(attention.request, store.getPresentationContext());
128
- }
129
- if (attention.notification.type === "leader-recovery-failed") {
130
- return createLeaderRecoveryOperatorPresentation(attention.notification);
131
- }
132
- return attention.notification.type === "leader-stalled"
133
- ? createLeaderStallOperatorPresentation(attention.notification)
134
- : createTaskTerminalOperatorPresentation(attention.notification);
94
+ function toPresentationItem(attention) {
95
+ return attention.kind === "input"
96
+ ? { kind: "input-request", request: attention.request }
97
+ : { kind: "task-event", event: attention.event };
135
98
  }
136
99
  function deduplicateAttention(attentions) {
137
100
  const seen = new Set();
138
101
  return attentions.filter((attention) => {
139
- const key = attention.kind === "input"
140
- ? `input:${attention.request.taskId}:${attention.request.id}`
141
- : attention.notification.type === "leader-stalled"
142
- ? `stall:${attention.notification.taskId}:${attention.notification.runId}:${attention.notification.progressAt}`
143
- : `recovery:${attention.notification.taskId}:${attention.notification.createdAt}`;
102
+ const identity = attentionIdentity(attention);
103
+ const key = `${identity.sourceKind}:${identity.taskId}:${identity.sourceId}`;
144
104
  if (seen.has(key))
145
105
  return false;
146
106
  seen.add(key);