@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.
- package/README.md +23 -0
- package/dist/cli/commandCatalog.js +34 -7
- package/dist/cli/updateCommand.js +3 -0
- package/dist/commands/configCommands.js +1 -0
- package/dist/commands/globalRoleCommands.js +62 -35
- package/dist/commands/inboxCommands.js +372 -0
- package/dist/commands/roleSkillValidation.js +2 -0
- package/dist/commands/taskCommands.js +39 -7
- package/dist/commands/taskRoleRuntimeStatus.js +2 -0
- package/dist/context/currentSkills.js +48 -0
- package/dist/context/roleSessionContext.js +13 -16
- package/dist/context/runContextPack.js +50 -3
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +27 -4
- package/dist/context/skillCatalog.js +2 -3
- package/dist/context/taskContext.js +38 -12
- package/dist/controlPlaneCli.js +58 -7
- package/dist/controller/controller.js +8 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +26 -6
- package/dist/controller/globalInputDelivery.js +86 -16
- package/dist/controller/globalRuntimeAttention.js +136 -6
- package/dist/controller/operatorNotification.js +6 -1
- package/dist/controller/providerRetryDelivery.js +3 -5
- package/dist/controller/runtime.js +6 -2
- package/dist/controller/structuredProviderObservation.js +51 -15
- package/dist/controller/taskHostTerminalRecovery.js +33 -0
- package/dist/doctor/doctor.js +25 -2
- package/dist/executor/effectiveLaunch.js +2 -2
- package/dist/executor/fileRoleLaunchPlanner.js +8 -15
- package/dist/interaction/notificationPresentation.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/message/globalInputBacklog.js +69 -0
- package/dist/message/globalMessageQuery.js +1 -0
- package/dist/message/inboxReceipt.js +62 -0
- package/dist/message/message.js +6 -1
- package/dist/message/messageContinuation.js +34 -13
- package/dist/message/taskInboxOwnership.js +43 -0
- package/dist/models/modelManagement.js +469 -0
- package/dist/models/modelManagementConfig.js +46 -0
- package/dist/models/modelManagementFiles.js +90 -0
- package/dist/models/modelManagementHttp.js +90 -0
- package/dist/models/modelManagementTypes.js +1 -0
- package/dist/nativeAgent/session/backends.js +1 -1
- package/dist/observability/runtimeIdentity.js +1 -1
- package/dist/output/boundedRead.js +43 -14
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/runtime/agentEndpointIdentity.js +8 -4
- package/dist/runtime/agentEndpointOwnership.js +5 -6
- package/dist/runtime/agentHost.js +165 -24
- package/dist/runtime/agentHostImplementation.js +18 -0
- package/dist/runtime/managedCaller.js +25 -1
- package/dist/runtime/managedIdentity.js +1 -1
- package/dist/runtime/providerRetry.js +19 -1
- package/dist/runtime/sessionCaller.js +53 -0
- package/dist/scheduler/operatorEvent.js +49 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
- package/dist/scheduler/taskWake.js +5 -0
- package/dist/sqlite/driver.js +108 -0
- package/dist/storage/contextRecords.js +28 -0
- package/dist/storage/sqliteSchema.js +21 -1
- package/dist/storage/sqliteStore.js +64 -1
- package/dist/storage/storageSchema.js +1 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +4 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
- package/dist/task/completionReadiness.js +13 -6
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/assetManifest.js +28 -2
- package/dist/web/assets/client/app/manage.js +6 -0
- package/dist/web/assets/client/app/models.js +20 -0
- package/dist/web/assets/client/app/settings.js +41 -9
- package/dist/web/assets/client/i18n/en.js +180 -2
- package/dist/web/assets/client/i18n/zh-CN.js +178 -2
- package/dist/web/assets/client/index.js +8 -0
- package/dist/web/assets/client/layout/manageShell.js +94 -0
- package/dist/web/assets/client/lib/api.js +13 -1
- package/dist/web/assets/client/lib/i18n.js +5 -3
- package/dist/web/assets/client/views/dock/conversation.js +231 -80
- package/dist/web/assets/client/views/models.js +777 -0
- package/dist/web/assets/client/views/skills.js +22 -15
- package/dist/web/assets/shared/icons.js +2 -0
- package/dist/web/assets/shared/manageNav.js +13 -0
- package/dist/web/assets/shell/manage.js +54 -0
- package/dist/web/assets/shell/models.js +7 -0
- package/dist/web/assets/shell/settings.js +1 -35
- package/dist/web/assets/shell/settingsContent.js +30 -0
- package/dist/web/assets/shell/sidebar.js +1 -0
- package/dist/web/assets/styles/index.js +6 -0
- package/dist/web/assets/styles/views/conversation.js +133 -0
- package/dist/web/assets/styles/views/manage.js +71 -0
- package/dist/web/assets/styles/views/models.js +129 -0
- package/dist/web/assets/styles/views/settings.js +28 -14
- package/dist/web/assets/styles/views/sidebar.js +1 -0
- package/dist/web/webServer.js +59 -9
- package/docs/cli-information-contract.md +42 -11
- package/docs/cli-information-contract.zh-CN.md +28 -7
- package/docs/managed-turn-and-session-runtime.md +127 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +83 -3
- package/docs/release-workflow.md +105 -9
- package/docs/release-workflow.zh-CN.md +73 -5
- package/docs/storage-baseline.md +22 -7
- package/docs/storage-baseline.zh-CN.md +14 -5
- package/docs/task-delivery.md +17 -8
- package/docs/task-delivery.zh-CN.md +11 -5
- package/docs/testing/review-evaluation.md +148 -0
- package/docs/testing/review-evaluation.zh-CN.md +117 -0
- package/docs/testing/verification-levels.md +26 -4
- package/docs/testing/verification-levels.zh-CN.md +11 -1
- package/i18n/README.zh-CN.md +19 -0
- package/package.json +5 -2
- package/skills/yui-leader/SKILL.md +28 -11
- package/skills/yui-leader/VERSION +1 -0
- package/skills/yui-leader/references/execution.md +29 -0
- package/skills/yui-leader/references/replicated-execution.md +7 -0
- package/skills/yui-operator/SKILL.md +15 -3
- package/skills/yui-operator/VERSION +1 -0
- package/skills/yui-operator/references/task-delivery.md +2 -0
- package/skills/yui-reviewer/SKILL.md +43 -1
- package/skills/yui-reviewer/VERSION +1 -0
- package/skills/yui-runtime/SKILL.md +144 -15
- package/skills/yui-runtime/VERSION +1 -0
- package/skills/yui-runtime/references/publication.md +16 -5
- package/skills/yui-runtime/references/recovery.md +24 -5
- package/skills/yui-worker/SKILL.md +8 -1
- 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.
|
|
13
|
-
|
|
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.
|
|
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
|
-
|
|
31
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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 +
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
480
|
-
|
|
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
|
+
}
|