@zq-silk/yui 2.6.0 → 2.8.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 (126) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +34 -7
  3. package/dist/cli/updateCommand.js +3 -0
  4. package/dist/commands/configCommands.js +1 -0
  5. package/dist/commands/globalRoleCommands.js +62 -35
  6. package/dist/commands/inboxCommands.js +372 -0
  7. package/dist/commands/roleSkillValidation.js +2 -0
  8. package/dist/commands/taskCommands.js +39 -7
  9. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  10. package/dist/context/currentSkills.js +48 -0
  11. package/dist/context/roleSessionContext.js +13 -16
  12. package/dist/context/runContextPack.js +50 -3
  13. package/dist/context/runInputContract.js +1 -1
  14. package/dist/context/sessionBootstrapManifest.js +27 -4
  15. package/dist/context/skillCatalog.js +2 -3
  16. package/dist/context/taskContext.js +38 -12
  17. package/dist/controlPlaneCli.js +58 -7
  18. package/dist/controller/controller.js +8 -2
  19. package/dist/controller/fileSchedulerStoreAdapter.js +26 -6
  20. package/dist/controller/globalInputDelivery.js +86 -16
  21. package/dist/controller/globalRuntimeAttention.js +136 -6
  22. package/dist/controller/operatorNotification.js +6 -1
  23. package/dist/controller/providerRetryDelivery.js +3 -5
  24. package/dist/controller/runtime.js +6 -2
  25. package/dist/controller/structuredProviderObservation.js +51 -15
  26. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  27. package/dist/doctor/doctor.js +25 -2
  28. package/dist/executor/effectiveLaunch.js +2 -2
  29. package/dist/executor/fileRoleLaunchPlanner.js +8 -15
  30. package/dist/interaction/notificationPresentation.js +2 -2
  31. package/dist/interaction/operatorPresentation.js +1 -1
  32. package/dist/message/globalInputBacklog.js +69 -0
  33. package/dist/message/globalMessageQuery.js +1 -0
  34. package/dist/message/inboxReceipt.js +62 -0
  35. package/dist/message/message.js +6 -1
  36. package/dist/message/messageContinuation.js +34 -13
  37. package/dist/message/taskInboxOwnership.js +43 -0
  38. package/dist/models/modelManagement.js +469 -0
  39. package/dist/models/modelManagementConfig.js +46 -0
  40. package/dist/models/modelManagementFiles.js +90 -0
  41. package/dist/models/modelManagementHttp.js +90 -0
  42. package/dist/models/modelManagementTypes.js +1 -0
  43. package/dist/nativeAgent/session/backends.js +1 -1
  44. package/dist/observability/runtimeIdentity.js +1 -1
  45. package/dist/output/boundedRead.js +43 -14
  46. package/dist/resources/sqliteResourceRegistry.js +1 -1
  47. package/dist/runtime/agentEndpointIdentity.js +8 -4
  48. package/dist/runtime/agentEndpointOwnership.js +5 -6
  49. package/dist/runtime/agentHost.js +165 -24
  50. package/dist/runtime/agentHostImplementation.js +18 -0
  51. package/dist/runtime/managedCaller.js +25 -1
  52. package/dist/runtime/managedIdentity.js +1 -1
  53. package/dist/runtime/providerRetry.js +19 -1
  54. package/dist/runtime/sessionCaller.js +53 -0
  55. package/dist/scheduler/operatorEvent.js +49 -0
  56. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  57. package/dist/scheduler/taskWake.js +5 -0
  58. package/dist/sqlite/driver.js +108 -0
  59. package/dist/storage/contextRecords.js +28 -0
  60. package/dist/storage/sqliteSchema.js +21 -1
  61. package/dist/storage/sqliteStore.js +64 -1
  62. package/dist/storage/storageSchema.js +1 -1
  63. package/dist/storage/storageVersions.js +1 -1
  64. package/dist/storage/storeRpc.js +1 -0
  65. package/dist/storage/taskStore.js +4 -0
  66. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  67. package/dist/task/completionReadiness.js +13 -6
  68. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  69. package/dist/web/assets/assetManifest.js +28 -2
  70. package/dist/web/assets/client/app/manage.js +6 -0
  71. package/dist/web/assets/client/app/models.js +20 -0
  72. package/dist/web/assets/client/app/settings.js +41 -9
  73. package/dist/web/assets/client/i18n/en.js +180 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  75. package/dist/web/assets/client/index.js +8 -0
  76. package/dist/web/assets/client/layout/manageShell.js +94 -0
  77. package/dist/web/assets/client/lib/api.js +13 -1
  78. package/dist/web/assets/client/lib/i18n.js +5 -3
  79. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  80. package/dist/web/assets/client/views/models.js +777 -0
  81. package/dist/web/assets/client/views/skills.js +22 -15
  82. package/dist/web/assets/shared/icons.js +2 -0
  83. package/dist/web/assets/shared/manageNav.js +13 -0
  84. package/dist/web/assets/shell/manage.js +54 -0
  85. package/dist/web/assets/shell/models.js +7 -0
  86. package/dist/web/assets/shell/settings.js +1 -35
  87. package/dist/web/assets/shell/settingsContent.js +30 -0
  88. package/dist/web/assets/shell/sidebar.js +1 -0
  89. package/dist/web/assets/styles/index.js +6 -0
  90. package/dist/web/assets/styles/views/conversation.js +133 -0
  91. package/dist/web/assets/styles/views/manage.js +71 -0
  92. package/dist/web/assets/styles/views/models.js +129 -0
  93. package/dist/web/assets/styles/views/settings.js +28 -14
  94. package/dist/web/assets/styles/views/sidebar.js +1 -0
  95. package/dist/web/webServer.js +59 -9
  96. package/docs/cli-information-contract.md +42 -11
  97. package/docs/cli-information-contract.zh-CN.md +28 -7
  98. package/docs/managed-turn-and-session-runtime.md +127 -5
  99. package/docs/managed-turn-and-session-runtime.zh-CN.md +83 -3
  100. package/docs/release-workflow.md +105 -9
  101. package/docs/release-workflow.zh-CN.md +73 -5
  102. package/docs/storage-baseline.md +22 -7
  103. package/docs/storage-baseline.zh-CN.md +14 -5
  104. package/docs/task-delivery.md +17 -8
  105. package/docs/task-delivery.zh-CN.md +11 -5
  106. package/docs/testing/review-evaluation.md +148 -0
  107. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  108. package/docs/testing/verification-levels.md +26 -4
  109. package/docs/testing/verification-levels.zh-CN.md +11 -1
  110. package/i18n/README.zh-CN.md +19 -0
  111. package/package.json +5 -2
  112. package/skills/yui-leader/SKILL.md +28 -11
  113. package/skills/yui-leader/VERSION +1 -0
  114. package/skills/yui-leader/references/execution.md +29 -0
  115. package/skills/yui-leader/references/replicated-execution.md +7 -0
  116. package/skills/yui-operator/SKILL.md +15 -3
  117. package/skills/yui-operator/VERSION +1 -0
  118. package/skills/yui-operator/references/task-delivery.md +2 -0
  119. package/skills/yui-reviewer/SKILL.md +43 -1
  120. package/skills/yui-reviewer/VERSION +1 -0
  121. package/skills/yui-runtime/SKILL.md +144 -15
  122. package/skills/yui-runtime/VERSION +1 -0
  123. package/skills/yui-runtime/references/publication.md +16 -5
  124. package/skills/yui-runtime/references/recovery.md +24 -5
  125. package/skills/yui-worker/SKILL.md +8 -1
  126. package/skills/yui-worker/VERSION +1 -0
@@ -1,17 +1,17 @@
1
- import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
1
+ import { sendAgentHostRunControl, reconcileAgentHostTurn, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
2
2
  import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
3
3
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
4
  import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
5
  import { providerRetryPending } from "../runtime/providerRetry.js";
6
6
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
7
7
  import { globalMessagePresentation } from "../interaction/notificationPresentation.js";
8
+ import { recordGlobalRuntimeAttention, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
8
9
  /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
9
- export async function deliverGlobalInputs(home, store, ensure, onError) {
10
+ export async function deliverGlobalInputs(home, store, ensure, onError, options = {}) {
10
11
  for (const role of store.listGlobalRoles()) {
11
12
  try {
12
- let pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
13
- && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
14
- if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
13
+ let pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
14
+ if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
15
15
  continue;
16
16
  let sessions = store.getGlobalRoleSessionSet(role.name);
17
17
  // A detached Host keeps an active Session and may reconnect. An explicit
@@ -20,15 +20,82 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
20
20
  continue;
21
21
  if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
22
22
  store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
23
- pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
24
- && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
23
+ pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
25
24
  if (pending.length === 0)
26
25
  continue;
27
26
  }
28
27
  if (providerRetryPending(sessions?.providerBinding))
29
28
  continue;
30
- const current = sessions?.providerBinding?.run;
31
- const obsolete = pending.filter(message => {
29
+ let current = sessions?.providerBinding?.run;
30
+ const bindingBefore = sessions?.providerBinding;
31
+ if (options.reconcile && current != null
32
+ && ["submitting", "accepted", "delivery-unknown"].includes(current.status)
33
+ && bindingBefore?.authority.owner === "controller" && bindingBefore.authority.holderId !== undefined) {
34
+ const ownedTurn = current;
35
+ // Only ask the original Host to republish evidence it already holds.
36
+ // Busy, unknown, unavailable and older Hosts never authorize replay,
37
+ // Session creation, native reads through another client, or timeout settlement.
38
+ let observed;
39
+ try {
40
+ observed = await reconcileAgentHostTurn({
41
+ home, scope: "global", roleName: role.name,
42
+ control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "reconcile-turn",
43
+ nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
44
+ attemptId: current.attemptId, nativeTurnId: current.nativeTurnId,
45
+ authority: { epoch: bindingBefore.authority.epoch, owner: "controller",
46
+ holderId: bindingBefore.authority.holderId } }
47
+ });
48
+ if (observed.reconciliation === "unsupported") {
49
+ throw new Error("Host does not advertise reconcile-turn. Preserve the accepted input and original Host; consume retained Inbox evidence or use an explicitly authorized, quiescence-verified refresh.");
50
+ }
51
+ if (observed.outcome === "rejected" || ["idle", "failed", "exited"].includes(observed.snapshot.state)
52
+ && observed.reconciliation !== "terminal-republished") {
53
+ throw new Error(observed.failure?.detail ?? observed.snapshot.detail ?? "Host cannot reconcile this exact terminal.");
54
+ }
55
+ if (observed.reconciliation === "terminal-republished") {
56
+ store.transaction(tx => recordGlobalRuntimeRecovery(tx, role.name, `terminal-reconcile:${ownedTurn.attemptId}`, {
57
+ nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
58
+ reason: "host-terminal-unobservable", evidence: "exact-terminal-republished"
59
+ }, options.now ?? new Date()));
60
+ }
61
+ }
62
+ catch (error) {
63
+ store.transaction(tx => recordGlobalRuntimeAttention(tx, role.name, `terminal-reconcile:${currentProviderConversation(bindingBefore).conversationId}:${ownedTurn.attemptId}`, { attemptId: ownedTurn.attemptId, nativeTurnId: ownedTurn.nativeTurnId,
64
+ nativeSessionId: currentProviderConversation(bindingBefore).conversationId,
65
+ reason: "host-terminal-unobservable",
66
+ cause: observed === undefined ? "host-control-unavailable"
67
+ : observed.reconciliation === "unsupported" ? "host-control-unsupported"
68
+ : observed.outcome === "rejected" ? "host-control-rejected" : "exact-terminal-not-retained",
69
+ hostState: observed?.snapshot.state ?? null,
70
+ endpointImplementation: observed?.snapshot.endpointImplementation ?? null,
71
+ hostImplementation: observed?.snapshot.hostImplementation ?? null,
72
+ controlCapabilities: observed?.snapshot.controlCapabilities ?? null,
73
+ eventDelivery: observed?.snapshot.eventDelivery ?? null,
74
+ // Endpoint generation is not a loaded Host/package version.
75
+ inputKind: ownedTurn.attemptId.startsWith("direct:") ? "direct" : "managed",
76
+ detail: error instanceof Error ? error.message : String(error),
77
+ recovery: "Inspect the original Host, Session, input and raw Inbox. Controller restart preserves live Hosts; it does not upgrade their loaded observation code. Restore the supported observation path, or explicitly stop/replace with exact native quiescence evidence. Missing eventDelivery means unreported, not healthy. Do not resend unknown input.",
78
+ confirmed: ownedTurn.status, unknown: "Native terminal and remaining writer ownership." }, options.now ?? new Date()));
79
+ continue;
80
+ }
81
+ sessions = store.getGlobalRoleSessionSet(role.name);
82
+ current = sessions?.providerBinding?.run;
83
+ if (sessions?.sessions[sessions.activeAgentId]?.status !== "active"
84
+ || providerRetryPending(sessions?.providerBinding)
85
+ || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
86
+ continue;
87
+ pending = store.queryGlobalRoleMessages(role.name, { pending: true, limit: 64 }).messages;
88
+ if (pending.length === 0)
89
+ continue;
90
+ }
91
+ if (store.queryGlobalRoleMessages(role.name, { handling: true, limit: 0 }).count > 0)
92
+ continue;
93
+ // Interrupt-then reserves the next legal slot even behind a large queue.
94
+ const reservedPage = store.queryGlobalRoleMessages(role.name, {
95
+ pending: true, interruptThen: true, limit: 64
96
+ });
97
+ const reserved = reservedPage.messages;
98
+ const obsolete = reserved.filter(message => {
32
99
  const claim = message.interruptThen;
33
100
  return claim !== undefined && (sessions?.activeAgentId !== claim.targetAgentId
34
101
  || sessions.sessions[claim.targetAgentId]?.nativeSessionId !== claim.targetNativeSessionId
@@ -37,15 +104,18 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
37
104
  });
38
105
  for (const message of obsolete)
39
106
  store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
40
- pending = pending.filter(message => !obsolete.includes(message));
107
+ const obsoleteIds = new Set(obsolete.map(message => message.id));
108
+ pending = pending.filter(message => !obsoleteIds.has(message.id));
41
109
  if (pending.length === 0)
42
110
  continue;
111
+ if (reservedPage.count > reserved.length && obsolete.length === reserved.length)
112
+ continue;
43
113
  if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
44
114
  continue;
45
115
  const matchesTarget = (message) => message.deliveryTarget === undefined
46
116
  || sessions?.activeAgentId === message.deliveryTarget.agentId
47
117
  && sessions.sessions[message.deliveryTarget.agentId]?.nativeSessionId === message.deliveryTarget.nativeSessionId;
48
- const claims = pending.filter(message => message.interruptThen !== undefined);
118
+ const claims = reserved.filter(message => !obsolete.includes(message));
49
119
  let next;
50
120
  if (claims.length > 0) {
51
121
  next = claims.find(message => {
@@ -76,7 +146,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
76
146
  return false;
77
147
  });
78
148
  }
79
- if (next === undefined)
149
+ if (next === undefined || next.inbox !== undefined)
80
150
  continue;
81
151
  const attemptId = `global-input:${role.name}/${next.id}`;
82
152
  if (current?.attemptId === attemptId)
@@ -96,8 +166,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
96
166
  if (binding == null || binding.authority.owner !== "controller")
97
167
  continue;
98
168
  const nativeSessionId = currentProviderConversation(binding).conversationId;
99
- const persisted = store.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
100
- if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
169
+ const persisted = store.queryGlobalRoleMessages(role.name, { id: next.id, limit: 1 }).messages[0];
170
+ if (persisted === undefined || persisted.inbox !== undefined || JSON.stringify(persisted) !== JSON.stringify(next))
101
171
  continue;
102
172
  const claim = next.interruptThen;
103
173
  if (!matchesTarget(next) || claim !== undefined
@@ -125,8 +195,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
125
195
  });
126
196
  if (result.outcome === "rejected") {
127
197
  store.transaction(tx => {
128
- const latest = tx.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
129
- if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
198
+ const latest = tx.queryGlobalRoleMessages(role.name, { id: next.id, limit: 1 }).messages[0];
199
+ if (latest === undefined || latest.inbox !== undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
130
200
  return;
131
201
  const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
132
202
  if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
@@ -1,14 +1,115 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { createGlobalRoleMessage } from "../message/message.js";
3
- import { serializeAgentErrorRaw } from "../runtime/agentError.js";
3
+ import { redactAgentErrorText, serializeAgentErrorRaw } from "../runtime/agentError.js";
4
+ import { summaryMetadata, summaryText } from "../output/boundedRead.js";
5
+ const FAILURE_HEADER = "Yui Global failure evidence ";
6
+ const RECOVERY_HEADER = "Yui Global recovery evidence ";
7
+ const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
8
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
9
+ /** This is a projection of original Message documents, never a persisted
10
+ * aggregate or a new handling/recovery state. Exact observations remain in the
11
+ * existing record-only Message history. Polling the same identity is a no-op. */
12
+ function diagnosticIdentity(store, roleName, identity, facts) {
13
+ const value = object(facts), fence = object(value.fence);
14
+ const failure = object(value.failure), retry = object(value.retry);
15
+ const error = object(failure.error ?? value.error ?? retry.error);
16
+ const sessions = store.getGlobalRoleSessionSet(roleName);
17
+ const binding = sessions?.providerBinding;
18
+ const nativeSessionId = value.nativeSessionId ?? fence.nativeSessionId ?? retry.nativeSessionId
19
+ ?? sessions?.sessions[sessions.activeAgentId]?.nativeSessionId;
20
+ const source = {
21
+ roleName, nativeSessionId,
22
+ host: binding?.authority.holderId,
23
+ endpointImplementation: value.endpointImplementation,
24
+ hostImplementation: value.hostImplementation,
25
+ controlCapabilities: value.controlCapabilities,
26
+ driverId: fence.driverId ?? binding?.providerNamespace,
27
+ source: error.source,
28
+ phase: error.phase,
29
+ reason: value.reason, cause: value.cause,
30
+ agentId: fence.agentId ?? sessions?.activeAgentId, providerNamespace: binding?.providerNamespace,
31
+ accountScope: binding?.accountScope
32
+ };
33
+ const classified = typeof nativeSessionId === "string" && (typeof error.code === "string" && error.code !== "unknown"
34
+ || value.reason === "host-terminal-unobservable" && typeof value.cause === "string");
35
+ const classification = {
36
+ ...source, category: error.category, code: error.code, cause: value.cause,
37
+ detail: value.reason === "host-terminal-unobservable" ? hash(value.detail ?? null) : undefined,
38
+ // Explicit impact, not Turn IDs or free-form provider text.
39
+ inputDisposition: error.inputDisposition ?? failure.inputDisposition,
40
+ sessionDisposition: error.sessionDisposition ?? failure.sessionDisposition,
41
+ confirmed: ["submitting", "accepted", "delivery-unknown", "completed", "failed", "cancelled"].includes(value.confirmed)
42
+ ? value.confirmed : undefined,
43
+ status: value.status,
44
+ // Unclassified failures must never be guessed equivalent.
45
+ unclassified: classified ? undefined : identity
46
+ };
47
+ return { key: hash(classification), source, summary: summaryText(redactAgentErrorText(String(error.message ?? value.detail ?? value.reason ?? "Runtime failure; inspect original evidence"))
48
+ .replace(/[\u0000-\u001f\u007f]/gu, " ")) };
49
+ }
50
+ function evidence(message) {
51
+ if (message.kind !== "system" || ![FAILURE_HEADER, RECOVERY_HEADER].some(header => message.body.startsWith(header)))
52
+ return;
53
+ try {
54
+ return object(JSON.parse(message.body.slice(message.body.indexOf("\n") + 1)));
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ }
60
+ export function runtimeDiagnosticGroups(messages) {
61
+ const groups = [];
62
+ const active = new Map();
63
+ for (const message of messages) {
64
+ const value = evidence(message);
65
+ if (!value)
66
+ continue;
67
+ if (message.body.startsWith(RECOVERY_HEADER)) {
68
+ for (const [key, group] of active) {
69
+ if (group.source.nativeSessionId !== value.nativeSessionId || group.source.host !== value.host
70
+ || value.reason !== undefined && group.source.reason !== value.reason
71
+ || value.reason === undefined && group.source.phase !== "turn-execute"
72
+ && group.source.phase !== "turn-submit")
73
+ continue;
74
+ group.recoveredAt = message.createdAt;
75
+ group.recoveryEvidence = message.id;
76
+ active.delete(key);
77
+ }
78
+ continue;
79
+ }
80
+ const diagnostic = value.diagnostic;
81
+ if (typeof diagnostic?.key !== "string")
82
+ continue; // Old evidence remains in ordinary history.
83
+ const existing = active.get(diagnostic.key);
84
+ if (existing) {
85
+ existing.count++;
86
+ existing.lastAt = message.createdAt;
87
+ existing.latestEvidence = message.id;
88
+ }
89
+ else {
90
+ const group = { ...diagnostic, episode: message.id, count: 1,
91
+ firstAt: message.createdAt, lastAt: message.createdAt,
92
+ firstEvidence: message.id, latestEvidence: message.id };
93
+ groups.push(group);
94
+ active.set(diagnostic.key, group);
95
+ }
96
+ }
97
+ return groups;
98
+ }
99
+ export function runtimeDiagnosticEvidence(messages, key) {
100
+ return messages.filter(message => evidence(message)?.diagnostic?.key === key);
101
+ }
4
102
  /** Global observations have no Task event history. Keep original failed
5
103
  * attempts as ordinary record-only Messages before replacing the live binding. */
6
104
  export function recordGlobalRuntimeFailure(store, roleName, identity, facts, now) {
7
- const header = `Yui Global failure evidence ${createHash("sha256").update(identity).digest("hex")}\n`;
105
+ const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
106
+ const header = `${FAILURE_HEADER}${hash([identity, diagnostic.key])}\n`;
8
107
  const previous = store.listGlobalRoleMessages(roleName).find(message => message.kind === "system" && message.body.startsWith(header));
9
108
  if (previous !== undefined)
10
109
  return `${roleName}/${previous.id}`;
11
- const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + serializeAgentErrorRaw(facts), "system", { type: "system" }, now);
110
+ const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + redactAgentErrorText(JSON.stringify({
111
+ ...object(facts), diagnostic
112
+ })), "system", { type: "system" }, now);
12
113
  store.saveGlobalRoleMessage(message);
13
114
  return `${roleName}/${message.id}`;
14
115
  }
@@ -18,17 +119,46 @@ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, n
18
119
  // A Home may have a custom Global Role without an Operator. Keep its own
19
120
  // diagnostic readable; never create a Role or launch a substitute Agent.
20
121
  const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
21
- const requestId = `runtime-attention:${createHash("sha256").update(`${roleName}\0${identity}`).digest("hex")}`;
22
- if (store.listGlobalRoleMessages(recipient).some(message => message.inputControl?.requestId === requestId))
122
+ const failureRef = recordGlobalRuntimeFailure(store, roleName, identity, facts, now);
123
+ const diagnostic = diagnosticIdentity(store, roleName, identity, facts);
124
+ const group = runtimeDiagnosticGroups(store.listGlobalRoleMessages(roleName))
125
+ .reverse().find(group => group.key === diagnostic.key && group.recoveredAt === undefined);
126
+ const requestId = `runtime-attention:${hash([roleName, diagnostic.key, group?.episode ?? identity])}`;
127
+ if (store.queryGlobalRoleMessages(recipient, { requestId, limit: 1 }).messages.length > 0)
23
128
  return;
24
129
  const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, [
130
+ `Yui-Summary: ${diagnostic.summary}`,
25
131
  `Global Role ${roleName} needs attention. No recovery or new authority is implied.`,
26
132
  "Inspect the exact Session and original input before choosing a next action; unknown effects must not be replayed.",
27
133
  `Original failed attempts remain in Role ${roleName}'s record-only failure Messages; failureRef names the latest evidence.`,
28
- serializeAgentErrorRaw(facts)
134
+ redactAgentErrorText(JSON.stringify({ diagnosticKey: diagnostic.key, source: summaryMetadata(diagnostic.source), failureRef,
135
+ read: `yui role message list ${roleName} --diagnostic ${diagnostic.key}` }))
29
136
  ].join("\n"), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } });
30
137
  store.saveGlobalRoleMessage(recipient === roleName ? {
31
138
  ...message,
32
139
  notDelivered: { reason: "runtime-needs-attention; retained for user or successor inspection", at: now.toISOString() }
33
140
  } : message);
34
141
  }
142
+ /** Called only on an established exact successful observation. A diagnostic
143
+ * recovery says nothing about Message handling, prior effects or Task outcome. */
144
+ export function recordGlobalRuntimeRecovery(store, roleName, identity, facts, now) {
145
+ const messages = store.listGlobalRoleMessages(roleName);
146
+ const host = store.getGlobalRoleSessionSet(roleName)?.providerBinding?.authority.holderId;
147
+ const active = runtimeDiagnosticGroups(messages).filter(group => !group.recoveredAt
148
+ && group.source.nativeSessionId === facts.nativeSessionId && group.source.host === host
149
+ && (facts.reason !== undefined ? group.source.reason === facts.reason
150
+ : ["turn-execute", "turn-submit"].includes(group.source.phase)));
151
+ if (active.length === 0)
152
+ return;
153
+ const header = `${RECOVERY_HEADER}${hash(identity)}\n`;
154
+ if (messages.some(message => message.body.startsWith(header)))
155
+ return;
156
+ const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + serializeAgentErrorRaw({ ...facts, host }), "system", { type: "system" }, now);
157
+ store.saveGlobalRoleMessage(message);
158
+ const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
159
+ const notice = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, `Yui-Summary: Global Role ${roleName} has new runtime recovery evidence; no input handling or Task completion is implied.\n`
160
+ + JSON.stringify({ evidence: `${roleName}/${message.id}`, recoveredGroups: active.length }), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId: `runtime-recovery:${hash([roleName, identity])}` } });
161
+ store.saveGlobalRoleMessage(recipient === roleName ? { ...notice,
162
+ notDelivered: { reason: "runtime-recovery; retained for user or successor inspection", at: now.toISOString() }
163
+ } : notice);
164
+ }
@@ -8,7 +8,7 @@ import { createGlobalRoleMessage } from "../message/message.js";
8
8
  export function queueOperatorNotification(store, input, now = new Date()) {
9
9
  return store.transaction(tx => {
10
10
  const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
11
- const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
11
+ const previous = tx.queryGlobalRoleMessages("operator", { requestId, limit: 1 }).messages[0];
12
12
  const mailbox = tx.getWorkMailbox({ kind: "operator" });
13
13
  const claimed = mailbox?.processing?.batchId === input.batchId;
14
14
  if (previous !== undefined) {
@@ -18,6 +18,11 @@ export function queueOperatorNotification(store, input, now = new Date()) {
18
18
  }
19
19
  if (!claimed)
20
20
  throw new Error(`Operator notification claim changed: ${input.batchId}`);
21
+ // At most one unsent engineering notice. New facts coalesce in the existing
22
+ // mailbox while busy, without rewriting originals or overtaking user input.
23
+ if (tx.queryGlobalRoleMessages("operator", { pending: true, operatorNotice: true, limit: 1 }).count > 0) {
24
+ return { status: "deferred" };
25
+ }
21
26
  const sessions = tx.getGlobalRoleSessionSet("operator");
22
27
  const session = sessions?.sessions[sessions.activeAgentId];
23
28
  if (session?.status !== "active" || sessions?.providerBinding == null)
@@ -1,6 +1,6 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { runTaskCommand } from "../commands/taskCommands.js";
3
- import { enqueueWork } from "../coordination/workMailboxQueue.js";
3
+ import { routeRoleEvent } from "../scheduler/operatorEvent.js";
4
4
  import { createTaskEvent } from "../event/taskEvent.js";
5
5
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
6
  import { redactAgentErrorText } from "../runtime/agentError.js";
@@ -57,8 +57,7 @@ export function createProviderRetryHooks(home, store, ports = {}) {
57
57
  roleName: owner.roleName, chainId: binding.retry.chainId, reason
58
58
  }, new Date(at));
59
59
  store.saveEvent(owner.taskId, event);
60
- enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
61
- : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
60
+ routeRoleEvent(store, event, owner.roleName, "provider-retry-needs-attention", new Date(at));
62
61
  }
63
62
  });
64
63
  };
@@ -142,8 +141,7 @@ export function createProviderRetryHooks(home, store, ports = {}) {
142
141
  roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
143
142
  }, new Date(at));
144
143
  store.saveEvent(owner.taskId, event);
145
- enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
146
- : { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
144
+ routeRoleEvent(store, event, owner.roleName, "provider-retry-needs-attention", new Date(at));
147
145
  }
148
146
  });
149
147
  }
@@ -40,8 +40,10 @@ import { createWebTaskSurface } from "../web/webTaskSurface.js";
40
40
  import { createWebConversationSurface } from "../web/webConversation.js";
41
41
  import { WebRequestRejected } from "../web/webMutation.js";
42
42
  import { inspectAgentHost, sendAgentHostNativeControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
43
+ import { recoverTaskHostTerminals } from "./taskHostTerminalRecovery.js";
43
44
  import { archiveOrdinaryTask } from "../task/ordinaryArchive.js";
44
45
  import { createWebSettings } from "../web/webSettings.js";
46
+ import { createModelManagement } from "../models/modelManagement.js";
45
47
  import { createWebSkills } from "../web/webSkills.js";
46
48
  import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
47
49
  import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
@@ -410,6 +412,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
410
412
  const web = createControllerWeb(store, {
411
413
  previews: () => createTaskPreviews(store, home),
412
414
  skills: createWebSkills(store),
415
+ models: createModelManagement(store),
413
416
  settings: createWebSettings(store, {
414
417
  environment: options.environment ?? process.env, catalogs,
415
418
  refreshConfiguration: () => {
@@ -560,9 +563,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
560
563
  }
561
564
  }),
562
565
  jobSupervisor,
563
- globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
566
+ hostTerminalRecovery: () => recoverTaskHostTerminals(home, store, options.onError ?? (() => undefined)),
567
+ globalInputDelivery: (deliveryOptions) => deliverGlobalInputs(home, store, async (roleName) => {
564
568
  await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
565
- }, options.onError ?? (() => undefined)),
569
+ }, options.onError ?? (() => undefined), deliveryOptions),
566
570
  jobControl,
567
571
  capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
568
572
  ...(continuationReconciler === undefined ? {} : { continuationReconciler }),
@@ -10,10 +10,15 @@ import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
10
10
  import { AGENT_HOST_EVENT_PROTOCOL } from "../runtime/agentHostProtocol.js";
11
11
  import { redactAgentErrorText } from "../runtime/agentError.js";
12
12
  let structuredSequence = 0;
13
- let deliveryFailure;
13
+ const deliveryFailures = new Map();
14
14
  // Only live diagnostics for this process's immutable files, not another queue.
15
15
  // No payload, retry, or authoritative delivery state is kept in this map.
16
16
  const emittedEvents = new Map();
17
+ // Only facts whose immutable Inbox write has not succeeded. The original
18
+ // process must retain these bytes during a filesystem fault: another direct
19
+ // native turn can arrive without Yui admission and replace a last-turn cache.
20
+ // No new durable queue or execution retry; discard only after durable enqueue.
21
+ const unpersistedEvents = new Map();
17
22
  /** Retained in the existing Host Inbox; it never settles a Turn or a resource. */
18
23
  export async function publishStructuredProviderDiagnostic(input) {
19
24
  const diagnostic = input.diagnostic;
@@ -44,6 +49,7 @@ export async function publishStructuredProviderDiagnostic(input) {
44
49
  /** A live transport observation, independent of Provider state and Run truth. */
45
50
  export function structuredProviderEventDelivery(home, environment, nativeSessionId) {
46
51
  try {
52
+ const deliveryFailure = deliveryFailures.get(deliveryKey(home, environment));
47
53
  const inbox = new FileRuntimeEventInbox(home);
48
54
  const pending = [];
49
55
  for (const [id, entry] of emittedEvents) {
@@ -57,16 +63,39 @@ export function structuredProviderEventDelivery(home, environment, nativeSession
57
63
  && (nativeSessionId === undefined || entry.nativeSessionId === nativeSessionId))
58
64
  pending.push(entry);
59
65
  }
66
+ const retained = pendingPersistence(home, environment);
60
67
  return {
61
- pending: pending.length,
62
- pendingTerminals: pending.filter(event => event.terminal).length,
68
+ pending: pending.length + retained.length,
69
+ unpersisted: retained.length,
70
+ pendingTerminals: pending.filter(event => event.terminal).length
71
+ + retained.filter(event => ["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)).length,
63
72
  ...(deliveryFailure === undefined ? {} : { failure: deliveryFailure })
64
73
  };
65
74
  }
66
75
  catch (error) {
67
- return { pending: 0, pendingTerminals: 0, failure: deliveryFault("persistence", error) };
76
+ const retained = pendingPersistence(home, environment);
77
+ return {
78
+ pending: retained.length, unpersisted: retained.length,
79
+ pendingTerminals: retained.filter(event => ["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)).length,
80
+ failure: deliveryFault("persistence", error)
81
+ };
68
82
  }
69
83
  }
84
+ function deliveryKey(home, environment) {
85
+ return JSON.stringify([home, environment.YUI_TASK_ID, environment.YUI_ROLE, environment.YUI_AGENT_ID]);
86
+ }
87
+ function pendingPersistence(home, environment) {
88
+ return [...unpersistedEvents.values()].filter(event => event.home === home
89
+ && event.observation.fence.taskId === environment.YUI_TASK_ID
90
+ && event.observation.fence.roleName === environment.YUI_ROLE
91
+ && event.observation.fence.agentId === environment.YUI_AGENT_ID);
92
+ }
93
+ /** Retry only original native facts, under the original Host's control fence. */
94
+ export async function replayStructuredProviderPersistence(home, environment) {
95
+ const retained = pendingPersistence(home, environment).map(event => event.observation);
96
+ await persistAndApply(home, [], environment);
97
+ return retained;
98
+ }
70
99
  function deliveryFault(stage, error) {
71
100
  return {
72
101
  stage, detail: redactAgentErrorText(error instanceof Error ? error.message : String(error)),
@@ -412,28 +441,35 @@ function requireIdentity(value, label) {
412
441
  }
413
442
  async function persistAndApply(home, observations, environment, connection, startupRunId) {
414
443
  const inbox = new FileRuntimeEventInbox(home);
444
+ const key = deliveryKey(home, environment);
415
445
  const host = {
416
446
  protocol: AGENT_HOST_EVENT_PROTOCOL,
417
447
  adapterId: requireIdentity(environment.YUI_ADAPTER_ID, "Agent adapter id"),
418
448
  workspace: requireIdentity(environment.YUI_WORKSPACE, "Yui workspace"),
419
449
  ...(connection === undefined ? {} : { connection })
420
450
  };
421
- let events;
451
+ const source = { ...host, ...(startupRunId === undefined ? {} : { startupRunId }) };
452
+ for (const observation of observations) {
453
+ unpersistedEvents.set(observation.eventId, { home, observation, host: source });
454
+ }
455
+ const events = [];
422
456
  try {
423
457
  structuredProviderEventDelivery(home, environment); // prune ACKed local diagnostics
424
- events = observations.map(entry => inbox.enqueueObservation(entry, {
425
- ...host,
426
- ...(startupRunId === undefined ? {} : { startupRunId })
427
- }).event);
428
- for (const event of events)
458
+ for (const retained of pendingPersistence(home, environment)) {
459
+ const event = inbox.enqueueObservation(retained.observation, retained.host).event;
460
+ events.push(event);
461
+ unpersistedEvents.delete(retained.observation.eventId);
429
462
  emittedEvents.set(event.id, {
430
463
  home, taskId: event.taskId, roleName: event.observation.fence.roleName,
431
464
  nativeSessionId: event.observation.fence.nativeSessionId,
432
465
  terminal: ["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
433
466
  });
467
+ }
468
+ if (deliveryFailures.get(key)?.stage === "persistence")
469
+ deliveryFailures.delete(key);
434
470
  }
435
471
  catch (error) {
436
- deliveryFailure = deliveryFault("persistence", error);
472
+ deliveryFailures.set(key, deliveryFault("persistence", error));
437
473
  throw error;
438
474
  }
439
475
  // The immutable inbox is authoritative. During a release/update handover the
@@ -463,7 +499,7 @@ async function persistAndApply(home, observations, environment, connection, star
463
499
  return;
464
500
  // The native fact is already durable. An incompatible/refusing
465
501
  // Controller is a reporting fault, not Provider execution failure.
466
- deliveryFailure = deliveryFault("controller", error);
502
+ deliveryFailures.set(key, deliveryFault("controller", error));
467
503
  return;
468
504
  }
469
505
  // A fast Provider can accept the initial AgentRun before the scheduler call
@@ -472,12 +508,12 @@ async function persistAndApply(home, observations, environment, connection, star
472
508
  // `deferred` therefore means "retained for replay", not delivery failure.
473
509
  // The signal below schedules the replay after the transport transaction.
474
510
  if (result.outcome !== "applied" && result.outcome !== "deferred") {
475
- deliveryFailure = deliveryFault("controller", `Structured Provider observation was rejected: ${result.outcome ?? "unknown"}. Event: ${entry.id}`);
511
+ deliveryFailures.set(key, deliveryFault("controller", `Structured Provider observation was rejected: ${result.outcome ?? "unknown"}. Event: ${entry.id}`));
476
512
  return;
477
513
  }
478
514
  }
479
- if (deliveryFailure?.stage === "controller")
480
- deliveryFailure = undefined;
515
+ if (events.length > 0 && deliveryFailures.get(key)?.stage === "controller")
516
+ deliveryFailures.delete(key);
481
517
  await signalController(home, hostTaskId(environment), requireIdentity(environment.YUI_ROLE, "Role"));
482
518
  }
483
519
  /** Capture only identities the Host actually received; the current Controller
@@ -0,0 +1,33 @@
1
+ import { AGENT_HOST_CONTROL_PROTOCOL, reconcileAgentHostTurn } from "../runtime/agentHost.js";
2
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
3
+ import { providerRetryPending } from "../runtime/providerRetry.js";
4
+ /** Existing full reconciliation pass only; no new timer, input or Session. */
5
+ export async function recoverTaskHostTerminals(home, store, onError, reconcile = reconcileAgentHostTurn) {
6
+ for (const candidate of store.listRuntimeSessionCandidates({ scope: "task" })) {
7
+ const owner = candidate.owner;
8
+ if (owner.scope !== "task")
9
+ continue;
10
+ const binding = store.getTaskRoleSessionSet(owner.taskId, owner.roleName)?.providerBinding;
11
+ const turn = binding?.run;
12
+ if (binding === undefined || binding === null || turn == null || providerRetryPending(binding)
13
+ || !["submitting", "accepted", "delivery-unknown"].includes(turn.status)
14
+ || binding.authority.owner !== "controller" || binding.authority.holderId === undefined)
15
+ continue;
16
+ try {
17
+ const result = await reconcile({ home, ...owner, control: {
18
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "reconcile-turn",
19
+ nativeSessionId: currentProviderConversation(binding).conversationId,
20
+ attemptId: turn.attemptId, nativeTurnId: turn.nativeTurnId,
21
+ authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId }
22
+ } });
23
+ if (result.outcome === "rejected" || result.reconciliation === "unsupported") {
24
+ throw new Error(result.failure?.detail
25
+ ?? "Host terminal reconciliation is unsupported; retain the original input and inspect Host/Inbox evidence.");
26
+ }
27
+ }
28
+ catch (error) {
29
+ onError(new Error(`Task Host terminal recovery ${owner.taskId}/${owner.roleName}/${turn.attemptId}: `
30
+ + `${error instanceof Error ? error.message : String(error)} No input was replayed.`, { cause: error }));
31
+ }
32
+ }
33
+ }