@zq-silk/yui 2.18.0 → 2.20.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/dist/cli/commandCatalog.js +2 -0
- package/dist/commands/controllerCommands.js +1 -0
- package/dist/commands/roleConfiguration.js +17 -0
- package/dist/controller/controller.js +10 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +26 -2
- package/dist/controller/messageHealth.js +354 -0
- package/dist/controller/runtime.js +9 -1
- package/dist/controller/runtimeLaunchCoordinator.js +12 -1
- package/dist/controller/taskHostTerminalRecovery.js +11 -0
- package/dist/executor/agentExecutor.js +7 -0
- package/dist/executor/fileRoleLaunchPlanner.js +22 -6
- package/dist/output/rolePresentation.js +1 -0
- package/dist/role/instanceCode.js +7 -0
- package/dist/role/role.js +10 -0
- package/dist/runtime/agentEndpoint.js +1 -0
- package/dist/runtime/agentHost.js +9 -1
- package/dist/runtime/launchBroker.js +3 -0
- package/dist/runtime/runtimeBinding.js +3 -0
- package/dist/runtime/sessionTitle.js +29 -9
- package/dist/runtime/structuredProviderHost.js +11 -1
- package/dist/runtime/tmuxAdapters.js +4 -0
- package/dist/storage/contextRecords.js +16 -0
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/sqliteStore.js +16 -0
- package/dist/storage/storageVersions.js +1 -1
- package/docs/managed-turn-and-session-runtime.md +96 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -0
- package/docs/release-workflow.md +49 -0
- package/docs/release-workflow.zh-CN.md +37 -0
- package/docs/roles-and-configuration.md +49 -0
- package/docs/roles-and-configuration.zh-CN.md +38 -0
- package/docs/storage-baseline.md +4 -1
- package/docs/storage-baseline.zh-CN.md +1 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +1 -1
- package/package.json +1 -1
|
@@ -136,6 +136,7 @@ const agentChildren = [
|
|
|
136
136
|
{ name: "remove", discovery: { surface: "public", audiences: ["public", "operator"] }, summary: "Remove a configured Agent.", usage: "yui config agent remove <id>" }
|
|
137
137
|
];
|
|
138
138
|
const roleProfileOptions = [
|
|
139
|
+
"--instance-code",
|
|
139
140
|
"--description", "--responsibility", "--constraint",
|
|
140
141
|
"--expected-output", "--system-prompt", "--skill"
|
|
141
142
|
];
|
|
@@ -144,6 +145,7 @@ const roleAgentOptions = [
|
|
|
144
145
|
"--allowed-tool", "--disallowed-tool", "--search"
|
|
145
146
|
];
|
|
146
147
|
const roleProfileClearOptions = [
|
|
148
|
+
"--clear-instance-code",
|
|
147
149
|
"--clear-description", "--clear-responsibilities", "--clear-constraints",
|
|
148
150
|
"--clear-expected-output", "--clear-system-prompt", "--clear-skills"
|
|
149
151
|
];
|
|
@@ -187,6 +187,7 @@ export function parseControllerRuntimeSnapshot(result, droppedEvents) {
|
|
|
187
187
|
reads: (runtime.reads ?? null),
|
|
188
188
|
persistence: (runtime.persistence ?? null),
|
|
189
189
|
diagnostics: (runtime.diagnostics ?? null),
|
|
190
|
+
messageHealth: (runtime.messageHealth ?? null),
|
|
190
191
|
scheduler: (runtime.scheduler ?? null),
|
|
191
192
|
preRoutingWait: "unmeasured; event-loop lag is sampled, not per-request queue time",
|
|
192
193
|
clock: "process-monotonic milliseconds; target/request labels are sha256 prefixes"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { validateInstanceCode } from "../role/instanceCode.js";
|
|
2
3
|
import { parseEnabled } from "../role/availability.js";
|
|
3
4
|
import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
|
|
4
5
|
const PROFILE_OPTIONS = [
|
|
@@ -66,6 +67,8 @@ export function roleOptionSpecs(input) {
|
|
|
66
67
|
...(input.includeWorkspace === true ? [["--workspace", "value"]] : []),
|
|
67
68
|
...(input.update ? [["--yes", "flag"]] : []),
|
|
68
69
|
...(input.update ? [["--enabled", "value"]] : []),
|
|
70
|
+
["--instance-code", "value"],
|
|
71
|
+
...(input.update ? [["--clear-instance-code", "flag"]] : []),
|
|
69
72
|
...PROFILE_OPTIONS,
|
|
70
73
|
...agentValueOptions,
|
|
71
74
|
...agentRepeatableOptions,
|
|
@@ -105,6 +108,7 @@ export function parseRoleOptions(args, specs, usage) {
|
|
|
105
108
|
}
|
|
106
109
|
export function roleProfileFrom(parsed) {
|
|
107
110
|
return {
|
|
111
|
+
...roleDisplayPatch(parsed),
|
|
108
112
|
...(parsed.has("--description") ? { description: requiredText(parsed.one("--description")) } : {}),
|
|
109
113
|
responsibilities: parsed.many("--responsibility").map(requiredText),
|
|
110
114
|
constraints: parsed.many("--constraint").map(requiredText),
|
|
@@ -125,6 +129,7 @@ export function roleProfilePatch(parsed) {
|
|
|
125
129
|
["--skill", "--clear-skills"]
|
|
126
130
|
]);
|
|
127
131
|
return {
|
|
132
|
+
...roleDisplayPatch(parsed),
|
|
128
133
|
...(parsed.has("--enabled") ? { enabled: parseEnabled(parsed.one("--enabled")) } : {}),
|
|
129
134
|
...(parsed.has("--description") ? { description: requiredText(parsed.one("--description")) } : {}),
|
|
130
135
|
...(parsed.has("--clear-description") ? { description: undefined } : {}),
|
|
@@ -144,6 +149,18 @@ export function roleProfilePatch(parsed) {
|
|
|
144
149
|
...(parsed.has("--clear-skills") ? { skills: [] } : {})
|
|
145
150
|
};
|
|
146
151
|
}
|
|
152
|
+
function roleDisplayPatch(parsed) {
|
|
153
|
+
assertPairs(parsed, [["--instance-code", "--clear-instance-code"]]);
|
|
154
|
+
try {
|
|
155
|
+
return {
|
|
156
|
+
...(parsed.has("--instance-code") ? { instanceCode: validateInstanceCode(parsed.one("--instance-code")) } : {}),
|
|
157
|
+
...(parsed.has("--clear-instance-code") ? { instanceCode: undefined } : {})
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
147
164
|
export function hasAgentConfigOptions(parsed) {
|
|
148
165
|
return [...parsed.seen].some((option) => AGENT_CONFIG_OPTIONS.has(option));
|
|
149
166
|
}
|
|
@@ -693,6 +693,8 @@ export class FileTaskController {
|
|
|
693
693
|
#jobSupervisor;
|
|
694
694
|
#schedules;
|
|
695
695
|
#globalInputDelivery;
|
|
696
|
+
#messageHealth;
|
|
697
|
+
#lastMessageHealth = null;
|
|
696
698
|
#providerRetry;
|
|
697
699
|
#continuationReconciler;
|
|
698
700
|
#hostTerminalRecovery;
|
|
@@ -753,6 +755,7 @@ export class FileTaskController {
|
|
|
753
755
|
this.#continuationReconciler = options.continuationReconciler;
|
|
754
756
|
this.#hostTerminalRecovery = options.hostTerminalRecovery;
|
|
755
757
|
this.#globalInputDelivery = options.globalInputDelivery;
|
|
758
|
+
this.#messageHealth = options.messageHealth;
|
|
756
759
|
this.#providerRetry = options.providerRetry;
|
|
757
760
|
this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
|
|
758
761
|
windowMs: options.signalWindowMs ?? DEFAULT_SIGNAL_WINDOW_MS,
|
|
@@ -789,6 +792,7 @@ export class FileTaskController {
|
|
|
789
792
|
runtimeMetrics() {
|
|
790
793
|
const metrics = this.#lastRuntimeDrain?.metrics ?? ZERO_DRAIN_METRICS;
|
|
791
794
|
return {
|
|
795
|
+
messageHealth: this.#lastMessageHealth,
|
|
792
796
|
diagnostics: this.#diagnosticMetrics?.() ?? null,
|
|
793
797
|
phases: this.#phases.snapshot(),
|
|
794
798
|
persistence: this.#persistenceMetrics?.() ?? null,
|
|
@@ -1015,6 +1019,12 @@ export class FileTaskController {
|
|
|
1015
1019
|
if (scope.kind === "full" && this.#hostTerminalRecovery !== undefined) {
|
|
1016
1020
|
await this.#observeMaintenance("Host terminal recovery", this.#hostTerminalRecovery);
|
|
1017
1021
|
}
|
|
1022
|
+
if (scope.kind === "full" && this.#messageHealth !== undefined) {
|
|
1023
|
+
// Consume already collected Host evidence, never another live probe.
|
|
1024
|
+
const health = await this.#observeMaintenance("message-health", () => this.#messageHealth(this.#now()));
|
|
1025
|
+
if (health !== undefined)
|
|
1026
|
+
this.#lastMessageHealth = health;
|
|
1027
|
+
}
|
|
1018
1028
|
await this.#phases.observe("provider-retry", "home", () => this.#providerRetry?.reconcile());
|
|
1019
1029
|
await this.#phases.observe("global-input", "home", () => this.#globalInputDelivery?.({ reconcile: scope.kind === "full", now: this.#now() }));
|
|
1020
1030
|
for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
|
|
@@ -35,6 +35,7 @@ import { recordTaskAgentError } from "./taskAgentError.js";
|
|
|
35
35
|
import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure, recordGlobalRuntimeRecovery } from "./globalRuntimeAttention.js";
|
|
36
36
|
import { checkInboxTurn } from "./inboxTurnCheck.js";
|
|
37
37
|
import { queueOperatorNotification } from "./operatorNotification.js";
|
|
38
|
+
import { reconcileMessageHealth } from "./messageHealth.js";
|
|
38
39
|
import { reconcileProjectSync } from "../job/projectSync.js";
|
|
39
40
|
import { serializeError } from "../core/boundedRpc.js";
|
|
40
41
|
import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
|
|
@@ -95,6 +96,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
95
96
|
this.drivers = drivers;
|
|
96
97
|
this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
|
|
97
98
|
}
|
|
99
|
+
reconcileMessageHealth(now, hosts) {
|
|
100
|
+
return reconcileMessageHealth(this.store, now, hosts);
|
|
101
|
+
}
|
|
98
102
|
/** Registry maintenance only; this never executes a Job or starts a process. */
|
|
99
103
|
reconcileProjectSync(now) {
|
|
100
104
|
const errors = [];
|
|
@@ -2110,8 +2114,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2110
2114
|
&& existing.nativeSessionId !== input.session.nativeSessionId
|
|
2111
2115
|
&& existing.status === "active";
|
|
2112
2116
|
if (!defersConversationReplacement && (existing?.nativeSessionId !== input.session.nativeSessionId || existing.status !== "active")) {
|
|
2117
|
+
// Preparation reactivates the fixed native identity, not its name.
|
|
2118
|
+
// Preserve the stored title even for an ended Session; only the
|
|
2119
|
+
// naming receipt may advance it on resume.
|
|
2120
|
+
const { title, ...preparedSession } = input.session;
|
|
2113
2121
|
saveTaskSession(store, role, {
|
|
2114
|
-
...
|
|
2122
|
+
...preparedSession,
|
|
2123
|
+
...(existing?.nativeSessionId !== input.session.nativeSessionId && title !== undefined
|
|
2124
|
+
? { title } : {}),
|
|
2115
2125
|
nativeSessionId: input.session.nativeSessionId
|
|
2116
2126
|
}, "active", input.now);
|
|
2117
2127
|
}
|
|
@@ -2176,7 +2186,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2176
2186
|
recordLaunchedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
|
|
2177
2187
|
return this.store.transaction((store) => {
|
|
2178
2188
|
assertCurrent();
|
|
2179
|
-
|
|
2189
|
+
let session = input.owner.scope === "task"
|
|
2180
2190
|
? recordTaskRuntimeNativeSession(store, {
|
|
2181
2191
|
taskId: input.owner.taskId,
|
|
2182
2192
|
roleName: input.owner.roleName,
|
|
@@ -2192,6 +2202,20 @@ export class FileSchedulerStoreAdapter {
|
|
|
2192
2202
|
nativeSessionId: input.nativeSessionId,
|
|
2193
2203
|
effective: input.effective
|
|
2194
2204
|
}, now);
|
|
2205
|
+
if (input.generatedTitle !== undefined && (session.title === undefined
|
|
2206
|
+
|| session.title === session.generatedTitle || session.title === input.generatedTitle)) {
|
|
2207
|
+
const set = input.owner.scope === "task"
|
|
2208
|
+
? store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName)
|
|
2209
|
+
: store.getGlobalRoleSessionSet(input.owner.roleName);
|
|
2210
|
+
const titled = recordRoleAgentSession(set, {
|
|
2211
|
+
...session, title: input.generatedTitle, generatedTitle: input.generatedTitle
|
|
2212
|
+
}, now);
|
|
2213
|
+
if (titled.owner.scope === "task")
|
|
2214
|
+
store.saveTaskRoleSessionSet(titled);
|
|
2215
|
+
else
|
|
2216
|
+
store.saveGlobalRoleSessionSet(titled);
|
|
2217
|
+
session = titled.sessions[input.agentId];
|
|
2218
|
+
}
|
|
2195
2219
|
if (input.owner.scope === "global") {
|
|
2196
2220
|
let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
|
|
2197
2221
|
let binding = sessions.providerBinding;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
3
|
+
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
5
|
+
import { globalInputBacklog } from "../message/globalInputBacklog.js";
|
|
6
|
+
// These are observation thresholds, not leases or authority to release a writer.
|
|
7
|
+
export const MESSAGE_HEALTH_WARNING_MS = 5 * 60_000;
|
|
8
|
+
export const MESSAGE_HEALTH_CRITICAL_MS = 30 * 60_000;
|
|
9
|
+
const HEADER = "Yui message health v1\n";
|
|
10
|
+
const NOTICE = "operator-notice:message-health:";
|
|
11
|
+
export function classifyMessageHealth(facts, now) {
|
|
12
|
+
const ageMs = facts.oldestAt === null ? 0 : Math.max(0, +now - Date.parse(facts.oldestAt));
|
|
13
|
+
const occupancyMs = facts.processingAt === null ? 0 : Math.max(0, +now - Date.parse(facts.processingAt));
|
|
14
|
+
let state;
|
|
15
|
+
let reason;
|
|
16
|
+
if (facts.stopped) {
|
|
17
|
+
state = "paused";
|
|
18
|
+
reason = "explicit-stop-pause-or-lifecycle";
|
|
19
|
+
}
|
|
20
|
+
else if (facts.count === 0 && facts.handling) {
|
|
21
|
+
state = "busy";
|
|
22
|
+
reason = "claimed-input-handling-is-not-delivery-proof";
|
|
23
|
+
}
|
|
24
|
+
else if (facts.count === 0 && facts.processingAt === null) {
|
|
25
|
+
state = "clear";
|
|
26
|
+
reason = "no-awaiting-delivery";
|
|
27
|
+
}
|
|
28
|
+
else if (facts.turnStatus === "accepted" && !facts.blocked) {
|
|
29
|
+
state = "busy";
|
|
30
|
+
reason = "accepted-turn-duration-is-not-a-fault";
|
|
31
|
+
}
|
|
32
|
+
else if (facts.retryDeadline !== undefined && +now <= Date.parse(facts.retryDeadline)) {
|
|
33
|
+
state = "recovering";
|
|
34
|
+
reason = "bounded-provider-retry-owned";
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
reason = facts.retryDeadline !== undefined ? "retry-budget-expired"
|
|
38
|
+
: facts.blocked ? "retained-delivery-or-processing-barrier"
|
|
39
|
+
: facts.turnStatus === "delivery-unknown" ? "delivery-uncertain"
|
|
40
|
+
: facts.turnStatus === "submitting" ? "submission-unsettled" : "no-delivery-progress";
|
|
41
|
+
state = Math.max(ageMs, occupancyMs) >= MESSAGE_HEALTH_CRITICAL_MS ? "critical"
|
|
42
|
+
: Math.max(ageMs, occupancyMs) >= MESSAGE_HEALTH_WARNING_MS ? "warning" : "waiting";
|
|
43
|
+
}
|
|
44
|
+
return { state, reason, ageMs, occupancyMs };
|
|
45
|
+
}
|
|
46
|
+
// Display-only shell arguments; guidance never executes these commands.
|
|
47
|
+
function arg(value) {
|
|
48
|
+
return /^[a-zA-Z0-9_.:/-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
49
|
+
}
|
|
50
|
+
function recoveryGuidance(o) {
|
|
51
|
+
const { taskId, roleName } = o.target;
|
|
52
|
+
const owner = taskId === undefined ? "Operator (or the user if Operator itself is unavailable)"
|
|
53
|
+
: roleName === "leader" ? "Operator; the affected Leader must not synchronously stop itself"
|
|
54
|
+
: "the authorized Task Leader, or Operator";
|
|
55
|
+
const retryRead = taskId === undefined ? `yui session retry ${arg(roleName)} show`
|
|
56
|
+
: `yui task role session retry ${arg(taskId)} ${arg(roleName)} show`;
|
|
57
|
+
const lines = [
|
|
58
|
+
`Responsible: ${owner}. Guidance is not authorization and executes nothing.`,
|
|
59
|
+
`Snapshot ${o.observedAt}; Current state: ${o.state}; reason: ${o.reason}. Refresh yui controller status --verbose --json before acting; an old notice is not a current instruction.`,
|
|
60
|
+
`Exact target: Session=${o.session ?? "unreported"}, attempt=${o.attemptId ?? "unreported"}, batch=${o.batchId ?? "none"}, wake=${o.wakeId ?? "none"}.`,
|
|
61
|
+
`Read first: ${[...o.reads, retryRead, ...(o.deliveryEvidence ? [o.deliveryEvidence.read] : [])].join("; ")}.`,
|
|
62
|
+
"Check current CLI help and reported Host capabilities as the responsible caller. Missing capability, inaccessible evidence or an identity change blocks the proposed action; do not guess another command or target."
|
|
63
|
+
];
|
|
64
|
+
if (o.state === "recovering") {
|
|
65
|
+
lines.push("Existing bounded retry owns recovery. Observe its exact attempt/deadline; do not duplicate or interrupt it. Escalate its retained failure only if exhausted or needs-attention.");
|
|
66
|
+
}
|
|
67
|
+
else if (o.state === "busy" || o.state === "waiting") {
|
|
68
|
+
lines.push("No intervention is indicated: accepted work or claimed handling can be slow. Inspect actual progress and delivery evidence; silence or a missing ACK is not permission to interrupt.");
|
|
69
|
+
}
|
|
70
|
+
else if (o.state === "paused") {
|
|
71
|
+
lines.push("Respect the deliberate stop/pause, human ownership, matching InputRequest or pending lifecycle cleanup. Inspect that existing obligation; this notice does not authorize resumption or another stop.");
|
|
72
|
+
}
|
|
73
|
+
else if (o.state === "clear") {
|
|
74
|
+
lines.push("Only delivery backlog clearance was observed. No management action is indicated; inspect original inputs and handling before concluding business recovery.");
|
|
75
|
+
}
|
|
76
|
+
else if (taskId === undefined) {
|
|
77
|
+
lines.push("No per-GlobalRole Host-stop command is established by this candidate. Read the original Global Message/control and retry evidence; use current caller capability discovery for a supported targeted lifecycle action. Do not translate Task commands to a GlobalRole or use a stop-all fallback.");
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const target = `${arg(taskId)} ${arg(roleName)}`;
|
|
81
|
+
lines.push("Choose only the path matching refreshed evidence, not a stop/replace/retry sequence. Prefer correction plus guarded retry for a proven rejection when sufficient; stop is for an independently established residual Host barrier, and replacement is not the default.");
|
|
82
|
+
lines.push(`Observed barrier: delivery=${o.deliveryEvidence?.outcome ?? "unreported"}; detail=${o.deliveryEvidence?.detail ?? "unreported"}; Host=${o.hostObservation.state}/${o.hostObservation.reconciliation ?? "unreported"}; retry=${o.retry?.status ?? "unreported"}. These observations do not establish quiescence or prove Goal is the cause; do not clear Goal merely because it says stalled.`);
|
|
83
|
+
if (o.session !== null && o.blocked) {
|
|
84
|
+
lines.push(`For confirmed residual Host ownership, the responsible supervisor may consider the existing targeted management path without waiting for a full repair package or every historical projection to align. Template: yui task role session stop ${target} --reason '<verified target, authority and stop rationale>'. First refresh the exact Session/Host identity, native/background work and event persistence; normal busy work, owned retry, intentional pause, unpersisted Host events or unknown cleanup consequences block a routine stop. The CLI captures and fences the current target; this snapshot cannot pin a later invocation.`);
|
|
85
|
+
lines.push("A stop request being saved is not physical stop. Require the exact Host/resource cleanup result; if cleanup fails or volatile facts remain, retain that process and evidence and escalate the precise boundary, without force-kill or claiming it stopped. After proven stop, let existing authorized delivery/lifecycle reuse the retained conversation when supported; verify the actual native Session identity, never infer reuse from Host ready.");
|
|
86
|
+
lines.push(`Only if reuse is unsuitable and deliberate replacement is justified, preserve durable progress and use the separately authorized template: yui task role session new ${target} --reason '<why replacement is necessary>'. This requests fresh Session selection after fenced cleanup, not same-conversation reuse. A self-replacing Leader must end its turn. Retained inputs/claims still require inspection, not replay.`);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
lines.push("A current controlled Session/residual Host barrier is not established. Inspect the missing runtime or Assignment evidence first; no targeted stop is recommended from this snapshot.");
|
|
90
|
+
}
|
|
91
|
+
if (o.wakeId !== null && roleName === "leader") {
|
|
92
|
+
const wakeTarget = `${arg(taskId)} ${arg(o.wakeId)}`;
|
|
93
|
+
if (o.deliveryEvidence?.outcome === "rejected") {
|
|
94
|
+
lines.push(`After correcting the rejection cause, and only if this exact wake is still unconsumed, its original claim matches and native acceptance/activity are proven absent: yui task wake retry ${wakeTarget} --reason '<verified correction and quiescence>'. Queued is not accepted. Do not retry accepted/unknown input. This candidate does not establish resolve support for rejected wakes; do not assume another recovery package is installed.`);
|
|
95
|
+
}
|
|
96
|
+
else if (o.deliveryEvidence?.outcome === "unknown") {
|
|
97
|
+
lines.push(`Do not replay unknown input. After exact shared-native/background/writer quiescence and persisted evidence, with the original unknown claim still current: yui task wake resolve ${wakeTarget} --reason '<exact quiescence evidence>'. This releases only scheduling without replay, ACK or acceptance. The rightful handler must inspect original effects and settle still-valid obligations separately.`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lines.push("Deferred, accepted or unreported delivery does not meet this candidate's rejected-retry/unknown-resolve preconditions. Keep the original wake and inspect the runtime barrier instead of mechanically invoking either action.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
lines.push("Verify separately: management request saved; exact physical stop if requested; retained conversation reused or explicit replacement recorded; subsequent original wake/message actually accepted by Provider; real handling/progress and remaining obligations. Host ready, pending=0, command success or alert ACK alone proves none of the later stages. If evidence is missing, recovery remains unverified.");
|
|
105
|
+
lines.push("If an action is refused or fails, retain its exact target, receipt/error, original input and workspace; report the smallest missing capability, authority or cleanup evidence to the responsible supervisor. No private retry loop, forced ACK, queue clearing, raw DB edits, unknown-effect replay, model/account/permission changes or whole-Controller/all-Session restart.");
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
function eventType(roleName) { return `message.health.${roleName}`; }
|
|
109
|
+
function prefix(target) { return `${HEADER}${target.roleName}\n`; }
|
|
110
|
+
function noticeId(saved) { return NOTICE + encodeURIComponent(saved.id); }
|
|
111
|
+
function latest(store, target) {
|
|
112
|
+
if (target.taskId !== undefined) {
|
|
113
|
+
const event = store.queryContextRecords(target.taskId, { family: "task-event",
|
|
114
|
+
eventTypes: [eventType(target.roleName)], descending: true, limit: 1 }).records[0]?.value;
|
|
115
|
+
return event ? { id: `${target.taskId}/${event.id}`,
|
|
116
|
+
read: `yui task event show ${arg(target.taskId)} ${arg(event.id)}`,
|
|
117
|
+
evidence: JSON.parse(event.payload.evidence) } : null;
|
|
118
|
+
}
|
|
119
|
+
const message = store.queryGlobalRoleMessages(target.roleName, {
|
|
120
|
+
systemBodyPrefix: prefix(target), descending: true, limit: 1
|
|
121
|
+
}).messages[0];
|
|
122
|
+
return message ? { id: `${target.roleName}/${message.id}`,
|
|
123
|
+
read: `yui role message show ${arg(target.roleName)} ${arg(message.id)}`,
|
|
124
|
+
evidence: JSON.parse(message.body.slice(prefix(target).length)) } : null;
|
|
125
|
+
}
|
|
126
|
+
function record(store, target, evidence, now) {
|
|
127
|
+
if (target.taskId !== undefined) {
|
|
128
|
+
const event = createTaskEvent(store.nextEventId(target.taskId), target.taskId, eventType(target.roleName), { evidence: JSON.stringify(evidence) }, now);
|
|
129
|
+
store.saveEvent(target.taskId, event);
|
|
130
|
+
return { id: `${target.taskId}/${event.id}`, read: `yui task event show ${arg(target.taskId)} ${arg(event.id)}`, evidence };
|
|
131
|
+
}
|
|
132
|
+
const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), target.roleName, prefix(target) + JSON.stringify(evidence), "system", { type: "system" }, now);
|
|
133
|
+
store.saveGlobalRoleMessage(message);
|
|
134
|
+
return { id: `${target.roleName}/${message.id}`, read: `yui role message show ${arg(target.roleName)} ${arg(message.id)}`, evidence };
|
|
135
|
+
}
|
|
136
|
+
function alertStatus(store, saved) {
|
|
137
|
+
if (!saved)
|
|
138
|
+
return { stage: "not-generated", reason: null, messageId: null };
|
|
139
|
+
const message = store.queryGlobalRoleMessages("operator", {
|
|
140
|
+
requestId: noticeId(saved), limit: 1
|
|
141
|
+
}).messages[0];
|
|
142
|
+
if (message?.inbox?.ack)
|
|
143
|
+
return { stage: "handled", reason: null, messageId: message.id };
|
|
144
|
+
if (message?.delivery)
|
|
145
|
+
return { stage: message.delivery.via === "provider" ? "delivered" : "transport-accepted",
|
|
146
|
+
reason: message.delivery.via === "provider" ? null : "provider-acceptance-unconfirmed", messageId: message.id };
|
|
147
|
+
if (message)
|
|
148
|
+
return { stage: "saved", messageId: message.id,
|
|
149
|
+
reason: message.notDelivered?.reason ?? message.control?.outcome
|
|
150
|
+
?? (store.getGlobalRole("operator")?.enabled === false ? "operator-disabled"
|
|
151
|
+
: globalInputBacklog([message], store.getGlobalRoleSessionSet("operator")).reason) };
|
|
152
|
+
return { stage: "generated", messageId: null,
|
|
153
|
+
reason: store.getGlobalRole("operator") === null ? "operator-unconfigured"
|
|
154
|
+
: operatorAvailable(store) ? "operator-notice-pending" : "operator-unavailable" };
|
|
155
|
+
}
|
|
156
|
+
function operatorAvailable(store) {
|
|
157
|
+
const sessions = store.getGlobalRoleSessionSet("operator");
|
|
158
|
+
return sessions?.sessions[sessions.activeAgentId]?.status === "active" && sessions.providerBinding != null;
|
|
159
|
+
}
|
|
160
|
+
/** Same Global Message queue and receipt contract as ordinary Operator notices.
|
|
161
|
+
* No Task mailbox is involved. A busy/unavailable Operator never loses evidence.
|
|
162
|
+
* At most one unsent engineering notice; original failed notices are not replayed. */
|
|
163
|
+
function notify(store, saved, observation, now) {
|
|
164
|
+
if (store.getGlobalRole("operator") === null
|
|
165
|
+
|| !operatorAvailable(store)
|
|
166
|
+
|| store.queryGlobalRoleMessages("operator", { requestId: noticeId(saved), limit: 1 }).count
|
|
167
|
+
|| store.queryGlobalRoleMessages("operator", { pending: true, operatorNotice: true, limit: 1 }).count)
|
|
168
|
+
return;
|
|
169
|
+
const { phase } = saved.evidence;
|
|
170
|
+
const sessions = store.getGlobalRoleSessionSet("operator");
|
|
171
|
+
const session = sessions.sessions[sessions.activeAgentId];
|
|
172
|
+
const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), "operator", [
|
|
173
|
+
`Yui-Summary: Message delivery ${observation.state}: ${observation.target.taskId ?? "global"}/${observation.target.roleName} (retained ${phase} evidence).`,
|
|
174
|
+
`Inspect: ${saved.read}`,
|
|
175
|
+
`Original diagnostic phase=${phase}, first=${saved.evidence.firstAt}, latest evidence=${saved.evidence.at}; guidance below uses the current observation.`,
|
|
176
|
+
"Independent diagnostics: yui controller status --verbose --json",
|
|
177
|
+
`Waiting=${observation.count}, pending=${observation.pendingCount}, processing=${observation.processingCount}, ageMs=${observation.ageMs}, occupancyMs=${observation.occupancyMs}; answered inputs=${observation.answeredInputs.join(", ") || "none observed"}.`,
|
|
178
|
+
observation.recovery,
|
|
179
|
+
"Do not ACK business inputs, release unknown writers, replay input, replace Sessions or change models automatically."
|
|
180
|
+
].join("\n"), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId: noticeId(saved) } });
|
|
181
|
+
store.saveGlobalRoleMessage({ ...message,
|
|
182
|
+
deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } });
|
|
183
|
+
}
|
|
184
|
+
function oldest(times) {
|
|
185
|
+
return times.filter((time) => time != null).sort()[0] ?? null;
|
|
186
|
+
}
|
|
187
|
+
function observe(store, target, now, hosts) {
|
|
188
|
+
const { taskId, roleName } = target;
|
|
189
|
+
const owner = taskId === undefined ? { scope: "global", roleName }
|
|
190
|
+
: { scope: "task", taskId, roleName };
|
|
191
|
+
const task = taskId === undefined ? null : store.getTask(taskId);
|
|
192
|
+
const role = taskId === undefined ? store.getGlobalRole(roleName) : store.getRole(taskId, roleName);
|
|
193
|
+
const sessions = taskId === undefined ? store.getGlobalRoleSessionSet(roleName)
|
|
194
|
+
: store.getTaskRoleSessionSet(taskId, roleName);
|
|
195
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
196
|
+
const binding = sessions?.providerBinding;
|
|
197
|
+
const currentBinding = binding != null && session !== undefined
|
|
198
|
+
&& binding.conversations.some(conversation => conversation.status === "current"
|
|
199
|
+
&& conversation.epoch === binding.currentConversationEpoch && conversation.conversationId === session.nativeSessionId);
|
|
200
|
+
const turn = currentBinding && binding?.run?.authorityEpoch === binding.authority.epoch ? binding.run : null;
|
|
201
|
+
const mailbox = taskId === undefined
|
|
202
|
+
? roleName === "operator" ? store.getWorkMailbox({ kind: "operator" }) : null
|
|
203
|
+
: store.getWorkMailbox({ kind: "role", taskId, roleName });
|
|
204
|
+
const handlingOnly = mailbox?.processing?.owner.startsWith("agent-inbox:") ?? false;
|
|
205
|
+
const batches = [mailbox?.pending, handlingOnly ? null : mailbox?.processing?.batch].filter(batch => batch != null);
|
|
206
|
+
const refs = batches.flatMap(batch => [...batch.refs]);
|
|
207
|
+
const wakeId = mailbox?.processing?.owner.startsWith("leader-notification:")
|
|
208
|
+
? mailbox.processing.owner.slice("leader-notification:".length) : null;
|
|
209
|
+
const wake = wakeId && taskId ? store.getTaskWake(taskId, wakeId) : null;
|
|
210
|
+
const delivery = taskId && mailbox?.processing ? store.queryContextRecords(taskId, {
|
|
211
|
+
family: "task-event", eventTypes: ["notification.delivery"],
|
|
212
|
+
eventAttemptId: mailbox.processing.batchId, descending: true, limit: 1
|
|
213
|
+
}).records[0]?.value : undefined;
|
|
214
|
+
// Delivery failures stay visible even when ordinary pending excludes notDelivered.
|
|
215
|
+
// Handling-only historical receipts never become new queue work.
|
|
216
|
+
const globalPage = taskId === undefined ? store.queryGlobalRoleMessages(roleName, {
|
|
217
|
+
awaitingDelivery: true, unclaimed: true, excludeHealthNotices: true, limit: 8
|
|
218
|
+
}) : null;
|
|
219
|
+
const handling = handlingOnly || taskId === undefined && store.queryGlobalRoleMessages(roleName, {
|
|
220
|
+
awaitingDelivery: true, handling: true, excludeHealthNotices: true, limit: 1
|
|
221
|
+
}).count > 0;
|
|
222
|
+
const messages = globalPage?.messages ?? [];
|
|
223
|
+
const taskPage = taskId === undefined ? null : store.queryContextRecords(taskId, {
|
|
224
|
+
family: "task-message", queuedForRole: roleName, unhandledMessages: true, limit: 8
|
|
225
|
+
});
|
|
226
|
+
const taskMessages = taskPage?.records.map(record => record.value) ?? [];
|
|
227
|
+
const host = hosts.find(item => item.taskId === taskId && item.roleName === roleName
|
|
228
|
+
&& item.attemptId === turn?.attemptId && item.nativeSessionId === session?.nativeSessionId);
|
|
229
|
+
let hostObservation = host
|
|
230
|
+
? { state: host.state, observedAt: host.observedAt, reconciliation: host.reconciliation }
|
|
231
|
+
: { state: "unobserved", observedAt: null, reconciliation: null };
|
|
232
|
+
if (taskId === undefined && turn?.status === "accepted") {
|
|
233
|
+
// Reuse existing Global Host failure evidence, never probe a second time.
|
|
234
|
+
const failure = store.queryGlobalRoleMessages(roleName, {
|
|
235
|
+
systemBodyPrefix: "Yui Global failure evidence ", descending: true, limit: 1
|
|
236
|
+
}).messages[0];
|
|
237
|
+
if (failure) {
|
|
238
|
+
const facts = JSON.parse(failure.body.slice(failure.body.indexOf("\n") + 1));
|
|
239
|
+
if (facts.reason === "host-terminal-unobservable" && facts.attemptId === turn.attemptId
|
|
240
|
+
&& facts.nativeSessionId === session?.nativeSessionId && failure.createdAt >= turn.updatedAt) {
|
|
241
|
+
hostObservation = { state: typeof facts.hostState === "string" ? facts.hostState : "unobservable",
|
|
242
|
+
observedAt: failure.createdAt, reconciliation: facts.cause ?? null };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const count = batches.reduce((n, batch) => n + batch.requestCount, 0)
|
|
247
|
+
+ (globalPage?.count ?? 0) + (taskPage?.count ?? 0);
|
|
248
|
+
const answeredInputs = refs.flatMap(ref => {
|
|
249
|
+
if (ref.type !== "input")
|
|
250
|
+
return [];
|
|
251
|
+
return store.getInputRequest(ref.taskId, ref.id)?.status === "answered" ? [`${ref.taskId}/${ref.id}`] : [];
|
|
252
|
+
});
|
|
253
|
+
// Only explicit matching blocked refs constitute a user wait. An unrelated
|
|
254
|
+
// open InputRequest must not hide an answered input stuck in the same Task.
|
|
255
|
+
const waitingInputs = taskId === undefined || roleName === "leader" ? [] :
|
|
256
|
+
store.listOpenInputRequests([taskId]).filter(request => request.blockedRefs.some(ref => ref.type === "run" && refs.some(queued => queued.type === "run" && queued.id === ref.id)
|
|
257
|
+
|| ref.type === "work-item" && taskMessages.some(message => message.recipient?.workItemId === ref.id)))
|
|
258
|
+
.map(request => request.id);
|
|
259
|
+
const facts = {
|
|
260
|
+
count, handling, oldestAt: oldest([...batches.map(batch => batch.firstQueuedAt), globalPage?.oldestCreatedAt,
|
|
261
|
+
taskMessages[0]?.createdAt]),
|
|
262
|
+
processingAt: handlingOnly && count === 0 ? null : mailbox?.processing?.startedAt ?? null,
|
|
263
|
+
blocked: mailbox?.processing != null && wake?.status !== "consumed"
|
|
264
|
+
&& turn?.status !== "accepted"
|
|
265
|
+
|| messages.some(message => message.notDelivered !== undefined
|
|
266
|
+
|| message.control?.outcome === "delivery-unknown" || message.control?.outcome === "pending")
|
|
267
|
+
|| taskMessages.some(message => message.continuation?.notDeliveredReason !== undefined)
|
|
268
|
+
|| ["idle", "failed", "exited", "unobservable"].includes(hostObservation.state)
|
|
269
|
+
&& hostObservation.reconciliation !== "terminal-republished",
|
|
270
|
+
stopped: role?.enabled === false || session?.endReason === "stopped"
|
|
271
|
+
|| task !== null && (!["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled")
|
|
272
|
+
|| binding?.authority.owner === "human" || binding?.goal?.status === "paused"
|
|
273
|
+
|| waitingInputs.length > 0
|
|
274
|
+
|| hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner))),
|
|
275
|
+
turnStatus: turn?.status,
|
|
276
|
+
...(currentBinding && providerRetryPending(binding)
|
|
277
|
+
&& binding.retry.nativeSessionId === session.nativeSessionId
|
|
278
|
+
&& binding.retry.authorityEpoch === binding.authority.epoch ? { retryDeadline: binding.retry.deadline } : {})
|
|
279
|
+
};
|
|
280
|
+
const classification = classifyMessageHealth(facts, now);
|
|
281
|
+
const readBase = taskId ? `yui task role session inspect ${arg(taskId)} ${arg(roleName)}`
|
|
282
|
+
: `yui config role show ${arg(roleName)}`;
|
|
283
|
+
const observation = {
|
|
284
|
+
...facts, ...classification, target, observedAt: now.toISOString(),
|
|
285
|
+
session: session?.nativeSessionId ?? null, agentId: session?.agentId ?? null, hostObservation,
|
|
286
|
+
attemptId: turn?.attemptId ?? null, nativeTurnId: turn?.nativeTurnId ?? null,
|
|
287
|
+
host: binding?.authority.holderId ?? null, authority: binding?.authority.owner ?? null,
|
|
288
|
+
retry: binding?.retry ? { status: binding.retry.status, deadline: binding.retry.deadline,
|
|
289
|
+
attempts: binding.retry.attempts } : null,
|
|
290
|
+
wakeId, batchId: mailbox?.processing?.batchId ?? null, processingOwner: mailbox?.processing?.owner ?? null,
|
|
291
|
+
deliveryEvidence: delivery ? { outcome: delivery.payload.outcome ?? "unreported",
|
|
292
|
+
detail: delivery.payload.detail ?? null,
|
|
293
|
+
read: `yui task event show ${arg(taskId)} ${arg(delivery.id)}` } : null,
|
|
294
|
+
pendingCount: (mailbox?.pending?.requestCount ?? 0) + (globalPage?.count ?? 0) + (taskPage?.count ?? 0),
|
|
295
|
+
processingCount: mailbox?.processing?.batch.requestCount ?? 0,
|
|
296
|
+
pendingSince: mailbox?.pending?.firstQueuedAt ?? globalPage?.oldestCreatedAt ?? taskMessages[0]?.createdAt ?? null,
|
|
297
|
+
waitingInputs,
|
|
298
|
+
refs: [...refs.slice(0, 8), ...messages.map(message => ({
|
|
299
|
+
messageId: message.id, control: message.control ?? null, notDelivered: message.notDelivered ?? null
|
|
300
|
+
})), ...taskMessages.map(message => ({
|
|
301
|
+
messageId: message.id, recipient: message.recipient, continuation: message.continuation ?? null
|
|
302
|
+
}))], omittedRefs: Math.max(0, refs.length - 8) + Math.max(0, (globalPage?.count ?? 0) - messages.length)
|
|
303
|
+
+ Math.max(0, (taskPage?.count ?? 0) - taskMessages.length),
|
|
304
|
+
answeredInputs: answeredInputs.slice(0, 8),
|
|
305
|
+
reads: [readBase, ...(wakeId && taskId ? [`yui task wake show ${arg(taskId)} ${arg(wakeId)}`] : []),
|
|
306
|
+
...messages.map(message => `yui role message show ${arg(roleName)} ${arg(message.id)}`),
|
|
307
|
+
...taskMessages.map(message => `yui task message show ${arg(`${taskId}/${message.id}`)}`)]
|
|
308
|
+
};
|
|
309
|
+
return { ...observation, recovery: recoveryGuidance(observation) };
|
|
310
|
+
}
|
|
311
|
+
/** Low-rate Controller observation, not an Agent loop. All decisions use current
|
|
312
|
+
* records inside each short transaction; evidence + queue admission are atomic.
|
|
313
|
+
* Open Event/Message content carries audit facts; no new storage envelope/index
|
|
314
|
+
* or independently writable scheduler state is introduced. */
|
|
315
|
+
export function reconcileMessageHealth(store, now = new Date(), hosts = []) {
|
|
316
|
+
const targets = store.listTasks().filter(task => task.status !== "archived").flatMap(task => store.listRoles(task.id).map(role => ({ taskId: task.id, roleName: role.name })));
|
|
317
|
+
targets.push(...store.listGlobalRoles().map(role => ({ roleName: role.name })));
|
|
318
|
+
const items = [];
|
|
319
|
+
for (const target of targets) {
|
|
320
|
+
store.transaction(tx => {
|
|
321
|
+
const observation = observe(tx, target, now, hosts);
|
|
322
|
+
let saved = latest(tx, target);
|
|
323
|
+
const previous = saved?.evidence;
|
|
324
|
+
const active = previous !== undefined && previous.phase !== "recovered";
|
|
325
|
+
const growing = active && observation.state === "warning" && observation.ageMs >= 15 * 60_000
|
|
326
|
+
&& observation.count >= Math.max(8, previous.initialCount * 2);
|
|
327
|
+
const desired = growing ? "critical" : observation.state;
|
|
328
|
+
const phase = desired === "warning" || desired === "critical"
|
|
329
|
+
? (!active || desired === "critical" && previous.phase === "warning" ? desired : null)
|
|
330
|
+
: desired === "clear" && active ? "recovered" : null;
|
|
331
|
+
if (phase !== null) {
|
|
332
|
+
saved = record(tx, target, {
|
|
333
|
+
phase, firstAt: active ? previous.firstAt : now.toISOString(), at: now.toISOString(),
|
|
334
|
+
initialCount: active ? previous.initialCount : observation.count,
|
|
335
|
+
peakCount: Math.max(active ? previous.peakCount : 0, observation.count),
|
|
336
|
+
previous: saved?.id ?? null, observation
|
|
337
|
+
}, now);
|
|
338
|
+
}
|
|
339
|
+
if (saved !== null)
|
|
340
|
+
notify(tx, saved, observation, now);
|
|
341
|
+
if (observation.count > 0 || observation.processingAt !== null || saved !== null) {
|
|
342
|
+
items.push({ ...observation, evidence: saved?.read ?? null, alert: alertStatus(tx, saved),
|
|
343
|
+
incident: saved ? { phase: saved.evidence.phase, firstAt: saved.evidence.firstAt,
|
|
344
|
+
latestEvidenceAt: saved.evidence.at, initialCount: saved.evidence.initialCount } : null });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// Status is an O(1) cached bounded view; exact originals remain independently readable.
|
|
349
|
+
const priority = (state) => state === "critical" ? 0 : state === "warning" ? 1 : state === "clear" ? 3 : 2;
|
|
350
|
+
items.sort((a, b) => priority(a.state) - priority(b.state) || b.ageMs - a.ageMs);
|
|
351
|
+
return { observedAt: now.toISOString(), warningAfterMs: MESSAGE_HEALTH_WARNING_MS,
|
|
352
|
+
criticalAfterMs: MESSAGE_HEALTH_CRITICAL_MS, total: items.length, omitted: Math.max(0, items.length - 64),
|
|
353
|
+
items: items.slice(0, 64) };
|
|
354
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { ControllerDiagnosticJournal } from "./diagnosticJournal.js";
|
|
3
|
+
import { reconcileMessageHealth } from "./messageHealth.js";
|
|
3
4
|
import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
4
5
|
import { prepareNativeHostSteer } from "./nativeHostInput.js";
|
|
5
6
|
import { archiveLeaderTask } from "../task/leaderArchive.js";
|
|
@@ -583,6 +584,7 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
583
584
|
const continuationReconciler = options.continuationMetadata === undefined
|
|
584
585
|
? undefined
|
|
585
586
|
: new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata, options.onError);
|
|
587
|
+
let messageHealthHosts = [];
|
|
586
588
|
const running = await startFileTaskController(home, workerMaintenance
|
|
587
589
|
? workerSchedulerStore(schedulerStore, asyncStoreClient)
|
|
588
590
|
: schedulerStore, delivery, (method, params) => method === "task.role-capabilities"
|
|
@@ -627,7 +629,13 @@ async function createFileTaskControllerRuntime(home, options) {
|
|
|
627
629
|
}
|
|
628
630
|
}),
|
|
629
631
|
jobSupervisor,
|
|
630
|
-
|
|
632
|
+
messageHealth: async (now) => workerMaintenance
|
|
633
|
+
? await asyncStoreClient.invokeObserver("reconcileMessageHealth", [now, messageHealthHosts])
|
|
634
|
+
: reconcileMessageHealth(store, now, messageHealthHosts),
|
|
635
|
+
hostTerminalRecovery: async () => {
|
|
636
|
+
messageHealthHosts = [];
|
|
637
|
+
messageHealthHosts = await recoverTaskHostTerminals(home, store, options.onError ?? (() => undefined));
|
|
638
|
+
},
|
|
631
639
|
globalInputDelivery: (deliveryOptions) => deliverGlobalInputs(home, store, async (roleName) => {
|
|
632
640
|
await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
|
|
633
641
|
}, options.onError ?? (() => undefined), deliveryOptions),
|
|
@@ -69,6 +69,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
let preflightObserved = beforeHostStart === undefined;
|
|
72
|
+
let generatedTitle;
|
|
72
73
|
const observePreflight = (preflight) => {
|
|
73
74
|
if (preflightObserved || !sameOwner(preflight.owner, request.owner)
|
|
74
75
|
|| preflight.runId !== request.runId
|
|
@@ -80,6 +81,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
80
81
|
}
|
|
81
82
|
assertLaunchCurrent();
|
|
82
83
|
preflightObserved = true;
|
|
84
|
+
generatedTitle = preflight.generatedTitle;
|
|
83
85
|
beforeHostStart?.(preflight);
|
|
84
86
|
};
|
|
85
87
|
const shared = {
|
|
@@ -94,7 +96,9 @@ export class RuntimeLaunchCoordinator {
|
|
|
94
96
|
};
|
|
95
97
|
// Even without a startup-facts consumer, recheck after asynchronous Host
|
|
96
98
|
// validation and immediately before creating the process.
|
|
97
|
-
const callback = beforeHostStart === undefined
|
|
99
|
+
const callback = beforeHostStart === undefined
|
|
100
|
+
? (preflight) => { assertLaunchCurrent(); generatedTitle = preflight.generatedTitle; }
|
|
101
|
+
: observePreflight;
|
|
98
102
|
if (request.mode === "resume" && !request.nativeSessionId) {
|
|
99
103
|
throw new Error("Restoring a Session requires its native session id.");
|
|
100
104
|
}
|
|
@@ -109,11 +113,18 @@ export class RuntimeLaunchCoordinator {
|
|
|
109
113
|
throw new Error("Session host returned facts inconsistent with the requested launch.");
|
|
110
114
|
}
|
|
111
115
|
if (binding.nativeSessionId !== undefined) {
|
|
116
|
+
// A live Host may reuse its Provider without applying the new plan.
|
|
117
|
+
// Codex reports the actual naming effect; other adapters can only name
|
|
118
|
+
// at creation and must not overwrite native user names on resume.
|
|
119
|
+
const recordedTitle = request.adapterId === "codex"
|
|
120
|
+
? binding.appliedGeneratedTitle
|
|
121
|
+
: request.mode === "new" ? generatedTitle : undefined;
|
|
112
122
|
this.sessions.recordLaunchedRuntimeNativeSession({
|
|
113
123
|
owner: request.owner,
|
|
114
124
|
agentId: request.agentId,
|
|
115
125
|
adapterId: request.adapterId,
|
|
116
126
|
nativeSessionId: binding.nativeSessionId,
|
|
127
|
+
...(recordedTitle === undefined ? {} : { generatedTitle: recordedTitle }),
|
|
117
128
|
effective
|
|
118
129
|
}, assertLaunchCurrent, this.options.now?.() ?? new Date());
|
|
119
130
|
}
|